Redis:SETNX解决分布式锁误删问题
- 一.概述
- 二. 分布式锁(初级)
- (1)锁接口
- (2)锁实现类+上锁
- (3)释放锁
- (4)存在的问题
- 三. 改进释放锁
- (1)准备unlock.lua脚本
- (2)提前读取脚本
- (3)实现释放锁
一.概述
前提:解决“一人一单”问题时,使用Redis互斥锁 锁住联合查询、扣库存、创建订单的步骤,防止同一个userID的多个线程去创建订单,最后在finally{ } 释放锁;
问题:出现锁的误删现象;
分析:
当线程1获取锁,而此时线程1执行的业务因为某些情况被**阻塞**,阻塞的时间太长,TTL
到期导致锁被释放;
这时线程2就能获取锁;线程2执行自己的业务;
假设此时线程1 阻塞完了,业务完成,执行DEL 释放锁的逻辑!则线程2的锁被释放了!(误删)
由于锁被删除,则线程3也能获取锁;此时就有两个线程并行执行!
原因:
1.业务阻塞导致线程1的锁提前释放;
2.线程1释放了线程2的锁;
解决:
上锁时 添加 线程标识;
解锁时 判断 线程标识;
二. 分布式锁(初级)
需求:
1.获取锁时的value使用 UUID + 线程ID
作为线程标识;
2.释放锁时先获取锁中的线程标识,判断 于当前线程标识是否一致!
一致则释放锁;
不一致则不释放;
(1)锁接口
(2)锁实现类+上锁
使用 SETNX
/ setIfAbsent
作为互斥锁;
key
是传入的name;
value
=UUID+线程ID
(保证不易冲突),UUID使用hutool
中的UUID.randomUUID.toString,会去掉默认的横线 “-”;
(3)释放锁
先通过key获取当前线程的线程标识(value);
然后判断当前锁中的线程标识是否等于锁的线程标识即value;
注意:这里的UUID
是静态static修饰的,类加载的时候就会产生,同一个项目(JVM)内UUID相同,不同的项目(JVM)UUID则不同,这样就能防止线程ID作为标识存在不同JVM之间重复冲突的问题;
总结:通过上锁时添加线程标识、解锁时判断线程标识,解决了锁误删,提高健壮性!
(4)存在的问题
依然可能产生误删!
线程1判断成功准备释放锁,此时线程1 阻塞,如GC中的stw;(判断锁和释放锁是两个动作)
当阻塞时间够长超过TTL,则锁超时释放;
此时线程2可以获取锁,并执行业务;
此时线程1阻塞结束,而之前已经判断过锁了,线程1认为锁是自己的,所以直接释放锁,再次造成误删!
原因:判断锁和释放锁是两个动作!应该有原子性!
三. 改进释放锁
由于判断锁和释放锁是两个动作,【当判断锁和释放锁之间】产生了阻塞,则会导致误删;
解决:
使判断锁和释放锁具有 原子性
!
需求:使用基于Lua脚本实现分布式锁的释放锁逻辑;
使用RedisTemplate中的Excute()
方法来执行Lua脚本,参数分别是:脚本、KEYS参数集合、ARGS参数集合;(不需要指定key的个数)
(1)准备unlock.lua脚本
KEY参数传入锁的key;
AVG参数传入当前线程标识;
并将unlock.lua 脚本放到项目的 resources
中下;
(2)提前读取脚本
为了避免在使用脚本的时候才去读脚本,造成IO流影响性能,使用DefaultRedisScript
读取脚本,使用静态代码块提前读取;
(3)实现释放锁
执行脚本实现释放锁,此时只有一行代码(在Lua脚本中),可以保证判断线程标识和释放锁的 原子性
!
使用RedisTemplate的 execute()
方法执行Lua脚本;
KEYS参数必须是集合,使用Collections.singletonList快速生成;
总结:
利用SETNX
实现互斥性;
利用EXPIRE
保证超时释放;
使用Redis集群保证高可用,高并发;
运行Lua
脚本保证原子性;
key
:将用户ID作为锁的key,保证同一个用户ID获取同一把锁,而不同用户之间不阻塞!
value
:为了防止锁误删,使用线程标识作为value 以区别不同的线程,释放锁的时候先判断再释放;