Redis实现并发阻塞锁方案
由于用户同时访问线上的下订单接口,导致在扣减库存时出现了异常,这是一个很典型的并发问题,本篇文章为解决并发问题而生,采用的技术为Redis锁机制+多线程的阻塞唤醒方法。
在实现Redis锁机制之前,我们需要了解一下前置知识。
一、前置知识
1、多线程
将wait()、notifyAll()归为到多线程的方法中略有一些不恰当,这两个方法是Object中的方法。
① 当调用了wait()方法后,让当前线程进入等待状态,并且让当前线程释放对象锁,等待既为阻塞状态,等待notifyAll()方法的唤醒。
wait()方法和sleep()方法有一些相似之处,都是使当前线程阻塞,但他们实际是有一些区别的。
- 执行wait() 方法之前需要请求锁,wait()方法执行的时候会释放锁,等待被唤醒的时候竞争锁。
- sleep()只是让当前线程休眠一段时间,无视锁的存在。
- wait() 是Object类的方法 sleep()是Thread的静态方法
② notifyAll()方法为唤醒wait()中的线程。
notifyAll() 和 notify() 方法都是可以唤醒调用了wait()方法,而陷入阻塞的线程。
但是notify()是随机唤醒这个阻塞队列中随机的一个线程,而notifyAll()是唤醒所用的调用了wait()方法而陷入阻塞的线程,让他们自己去抢占对象锁。
notifyAll() 和 notify() 也都是必须在加锁的同步代码块中被调用,它们起的是唤醒的作用,不是释放锁的作用,只用在当前同步代码块中的程序执行完,也就是对象锁自然释放了,notifyAll() 和 notify()方法才会起作用,去唤醒线程。
wait()方法一般是和notify() 或者 notifyAll() 方法一起连用的。
以上为掌握本篇博客必备的多线程知识,如果系统学习多线程的相关知识可查阅博客 程序员田同学
2、Redis
加锁的过程本质上就是往Redis中set值,当别的进程也来set值时候,发现里面已经有值了,就只能放弃获取稍后再试。
Redis提供了一个天然实现锁机制的方法。
在Redis客户端的命令为 setnx(set if not exists)
在集成Springboot中采用的方法为:
redisTemplate.opsForValue().setIfAbsent(key, value);
如果里面set值成功会返回True,如果里面已经存在值就会返回False。
在我们实际使用的时候,setIfAbsent()方法并不是总是返回True和False。
如果我们的业务中加了事务,该方法会返回null,不知道这是一个bug还是什么,这是Redis的一个巨坑,浪费了很长时间才发现了这个问题,如果解决此问题可以跳转到第四章。
二、实现原理
分布式锁本质上要实现的目标就是在 Redis 里面占一个位置,当别的进程也要来占时,发现已经有人占在那里了,就只好放弃或者稍后再试。占位一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占位。先来先占, 事办完了,再调用 del 指令释放茅坑。
其中,发现Redis中已经有值了,当前线程是直接放弃还是稍后再试分别就代表着,非阻塞锁和阻塞锁。
在我们的业务场景中肯定是要稍后再试(阻塞锁),如果是直接放弃(非阻塞锁)在数据库层面就可以直接做,就不需要我们在代码大费周章了。
非阻塞锁只能保存数据的正确性,在高并发的情况下会抛出大量的异常,当一百个并发请求到来时,只有一个请求成功,其他均会抛出异常。
Redis非阻塞锁和 MySQL的乐观锁,最终达到的效果是一样的,乐观锁是采用CAS的思想。
乐观锁方法:表字段 加一个版本号,或者别的字段也可以!加版本号,可以知道控制顺序而已!在update 的时候可以where后面加上version= oldVersion。数据库,在任何并发的情况下,update 成功就是 1 失败就是 0 .可以根据返回的 1 ,0 做相应的处理!
我们更推荐大家使用阻塞锁的方式。
当获取不到锁时候,我们让当前线程使用wait()方法唤醒,当持有锁的线程使用完成后,调用notifyAll()唤醒所有等待的方法。
三、具体实现
以下代码为阻塞锁的实现方式。
业务层:
public String test() throws InterruptedException {
lock("lockKey");
System.out.println("11");
System.out.println("22");
System.out.println(Thread.currentThread().getName()+"***********");
Thread.sleep(2000);
System.out.println("33");
System.out.println("44");
System.out.println("55");
unlock("lockKey");
return "String";
}
锁的工具类:
主要是加锁和解锁的两个方法。
//每一个redis的key对应一个阻塞对象
private static HashMap<String, Object> blockers = new HashMap<>();
//当前获得锁的线程
private static Thread curThread;
public static RedisTemplate redisTemplate = (RedisTemplate) SpringUtils.getBean("redisTemplate") ;
/**
* 加锁
* @param key
* @throws InterruptedException
*/
public static void lock(String key) {
//循环判断是否能够创建key, 不能则直接wait释放CPU执行权
//放不进指说明锁正在被占用
System.out.println(key+"**");
while (!RedisUtil.setLock(key,"1",3)){
synchronized (key) {
blockers.put(key, key);
//wait释放CPU执行权
try {
key.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
blockers.put(key, key);
//能够成功创建,获取锁成功记录当前获取锁线程
curThread = Thread.currentThread();
}
/**
* 解锁
* @param key
*/
public static void unlock(String key) {
//判断是否为加锁的线程执行解锁, 不是则直接忽略
if( curThread == Thread.currentThread()) {
RedisUtil.delete(key);
//删除key之后需要notifyAll所有的应用, 所以这里采用发订阅消息给所有的应用
// RedisUtil.publish("lock", key);
//notifllall其他线程
Object lock = blockers.get(key);
if(lock != null) {
synchronized (lock) {
lock.notifyAll();
}
}
}
}
当我们在不加锁时候,使用接口测试工具测试时,12345并不能都是顺序执行的,会造成输出顺序不一致,如果是在我们的实际场景中,这是输入换成了数据库的select和update,数据出现错乱也是很正常的情况了。
当我们加上锁以后,12345都是顺序输出,并发问题顺利解决了。
四、附录
1、Redis存在的bug
本来lock()方法是直接调用 "Redis.setIfAbsent()" 方法,但是在使用时候一直报空指针异常,最终定位问题为Redis.setIfAbsent()方法存在问题。
在我的实际业务中,下订单的方法使用了@Transflastion增加了事务,导致该方法返回null,我们手写一个实现setIfAbsent()的作用。
/**
* 只有key不存在时,才设置值, 返回true, 否则返回false
*
* @param key key 不能为null
* @param value value 不能为null
* @param timeout 过期时长, 单位为妙
* @return
*/
public static Boolean setLock(String key,String value, long timeout) {
SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
redisTemplate.opsForValue().setIfAbsent(key, value);
redisTemplate.expire(key,timeout, TimeUnit.SECONDS);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
};
return (Boolean) redisTemplate.execute(sessionCallback);
}
方便对比,以下贴上原本的setIfAbsent()方法。
/**
* 只有key不存在时,才设置值, 返回true, 否则返回false [警告:事务或者管道情况下会报错-可使用 setLock方法]
*
* @param key key 不能为null
* @param value value 不能为null
* @param timeout 过期时长, 单位为妙
* @return
*/
@Deprecated
public static <T> Boolean setIfAbsent(String key, T value, long timeout) {
// redisTemplate.multi();
ValueOperations<String, T> valueOperations = redisTemplate.opsForValue();
Boolean aBoolean = valueOperations.setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
// redisTemplate.exec();
return aBoolean;
}
2、MySQL的锁机制
在并发场景下MySQL会报错,报错信息如下:
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
; SQL []; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
问题出现的原因是,某一种表频繁被锁表,导致另外一个事务超时,出现问题的原因是MySQL的机制。
MySQL更新时如果where字段存在索引会使用行锁,否则会使用表锁。
我们使用navichat在where字段上加上索引,问题顺利的迎刃而解。
Redis实现并发阻塞锁方案的更多相关文章
- Redis高并发分布式锁详解
为什么需要分布式锁 1.为了解决Java共享内存模型带来的线程安全问题,我们可以通过加锁来保证资源访问的单一,如JVM内置锁synchronized,类级别的锁ReentrantLock. 2.但是随 ...
- Redis的并发竞争问题,你用哪些方案来解决?
Redis的并发竞争问题,主要是发生在并发写竞争. 考虑到redis没有像db中的sql语句,update val = val + 10 where ...,无法使用这种方式进行对数据的更新. 假如有 ...
- 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存
原文:http://blog.csdn.net/heyewu4107/article/details/71009712 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存 问 ...
- 12.redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗?
作者:中华石杉 面试题 redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗? 面试官心理分析 这个也是线上非常常见的一个问题,就是多客户端同时并发写一个 ke ...
- 利用redis实现分布式事务锁,解决高并发环境下库存扣减
利用redis实现分布式事务锁,解决高并发环境下库存扣减 问题描述: 某电商平台,首发一款新品手机,每人限购2台,预计会有10W的并发,在该情况下,如果扣减库存,保证不会超卖 解决方案一 利用数据 ...
- Redis实现高并发分布式锁
分布式锁场景在分布式环境下多个操作需要以原子的方式执行首先启一个springboot项目,再引入redis依赖包: <!-- https://mvnrepository.com/artifa . ...
- Redis的并发竞争问题的解决方案总结
什么是Redis的并发竞争问题 Redis的并发竞争问题,主要是发生在并发写竞争. 考虑到redis没有像db中的sql语句,update val = val + 10 where ...,无法使用这 ...
- Redis高并发和快速的原因
一.Redis的高并发和快速原因 1.redis是基于内存的,内存的读写速度非常快: 2.redis是单线程的,省去了很多上下文切换线程的时间: 3.redis使用多路复用技术,可以处理并发的连接 ...
- redis客户端、分布式锁及数据一致性
Redis Java客户端有很多的开源产品比如Redission.Jedis.lettuce等. Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持:Redis ...
随机推荐
- mybatis学习一:基于xml与注解配置入门实例与问题
注:本case参考自:http://www.cnblogs.com/ysocean/p/7277545.html 一:Mybatis的介绍: MyBatis 本是apache的一个开源项目iBatis ...
- 详细描述一下 Elasticsearch 搜索的过程?
想了解 ES 搜索的底层原理,不再只关注业务层面了. 解答: 搜索拆解为"query then fetch" 两个阶段. query 阶段的目的:定位到位置,但不取. 步骤拆解如下 ...
- Spring基于xml注入bean的几种方式?
(1)Set方法注入: (2)构造器注入:①通过index设置参数的位置:②通过type设置参数类型: (3)静态工厂注入: (4)实例工厂:
- Java 中会存在内存泄漏吗,请简单描述?
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被 广泛使用于服务器端编程的一个重要原因):然而在实际开发中,可能会存在无 用但可达的对象,这些对象不能被 GC 回 ...
- Homebrew安装环境
p.p1 { margin: 0; font: 11px Menlo; color: rgba(0, 0, 0, 1) } span.s1 { font-variant-ligatures: no-c ...
- SVN在idea中操作解析图
进入的位置
- DB2表数据导出、导入及常用sql使用总结
一.DB2数据的导出: export to [path(例:D:"TABLE1.ixf)]of ixf select [字段(例: * or col1,col2,col3)] from ...
- Windows CMD常用命令集合
CMD命令: 开始->运行->键入cmd或command(在命令行里可以看到系统版本.文件系统版本) chcp 修改默认字符集chcp 936默认中文chcp 65001 1. appwi ...
- 记一次dotnet拆分包,并希望得大佬指点
记一次dotnet拆分包,并希望得大佬指点 之前做了一个用于excel导入导出的包, 定义了一些接口, 然后基于 NPOI EPPlus MiniExcel 做了三种实现 接口大概长下面这样(现在可以 ...
- caioj 1001: [视频]实数运算1[水题]
题意:输入两个实数a和b,输出它们的和 题解:简单题不写题解了-- 代码: #include <cstdio> double a, b; int main() { while (~scan ...