目录
一、情景再现
二、分布式锁主流实现方案
三、实现Redis分布式锁
0、启动redis
1、指令:setnx key value
2、指令:del key
3、指令 :expire key seconds
4、指令:ttl key
5、指令:set key value nx ex seconds
四、代码实现分布式锁
五、优化分布式锁
1、死锁
解决办法:设置过期时间
2、误删锁
解决办法:设置UUID
3、删除缺乏原子性
解决办法:LUA脚本保证删除原子性
思考:
核心区别:原子性
一、情景再现
- 单体单机部署的系统被演化成分布式集群系统后
- 由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效
- 单纯的Java API并不能提供分布式锁的能力
- 为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
- 示意图(说明:我们探讨的分布式锁是针对分布式项目/架构而言[...],和redis集群无关)
单体项目锁机制示意图:
分布式项目锁机制示意图:
二、分布式锁主流实现方案
1、基于数据库实现分布式锁
2、基于缓存Redis实现分布式锁
3、基于Zookeeper实现分布式锁
性能对比:
性能:redis最高
可靠性:Zookeeper最高
三、实现Redis分布式锁
0、启动redis
1、指令:setnx key value
setnx:上锁
key:锁的键
value:锁的值
在这个key删除之前,不能执行相同key的上锁指令
2、指令:del key
删除key,也就是释放锁
3、指令 :expire key seconds
给锁key设置过期时间,目的防止死锁,一直不释放锁就会造成死锁
4、指令:ttl key
查看某个锁key的过期时间
5、指令:set key value nx ex seconds
设置锁的同时,指定该锁的过期时间,防止死锁。
这个指令的原子性的,防止setnx key value/setnx key seconds两条指令中间执行被打断。
到期自动删除
四、代码实现分布式锁
情景再现:
在单机redis下,用springboot+redis实现分布式锁的使用
示意图:
当index1、index2、index3都想去执行业务逻辑的时候,先要到redis中获得锁(执行setnx指令成功),然后执行业务逻辑,必须释放锁,不然后续的请求进不来造成死锁
1、先初始化数据
2、执行以下代码:
@GetMapping("/lock")public void lock() {//第一步:获取锁Boolean lock=redisTemplate.opsForValue().setIfAbsent("lock", "ok");if (lock){//这个key为num的要先设置默认值,否则会出现问题Object value = redisTemplate.opsForValue().get("num");//1.判断返回的value是否有值if(value == null || !StringUtils.hasText(value.toString())){return;}//2.有值就转成intint num = Integer.parseInt(value.toString());//3.将num+1再转回去redisTemplate.opsForValue().set("num", num + 1);//4.释放锁redisTemplate.delete("lock");}else {//获取锁失败,休眠100ms再此获取try {Thread.sleep(100);lock();//递归重新执行}catch (InterruptedException e){e.printStackTrace();}}}
3、保证Linux可以访问到springboot项目
4、用ab工具测试,指令:ab -n 1000 -c 100 http://ip:端口/api接口路径
请求1000次,每次按100请求
num就变成了1000
五、优化分布式锁
1、死锁
在前面的代码中,我们没有设置分布式锁的过期时间,如果因为业务异常没有释放锁,可能会导致死锁
解决办法:设置过期时间
2、误删锁
用户A业务操作因为卡顿,超过了设置的锁过期时间,于是自动释放了,此时用户B就获取到锁了,但是可能用户A的卡顿结束就会去释放锁,本次释放的锁就是用户B的锁了。
也就是步骤3释放了1的锁,步骤5释放了用户B在步骤4设置的锁
属于是释放锁时未验证当前锁是否仍属于自己
解决办法:设置UUID
1、给锁设置唯一的UUID
2、释放锁的时候查看是不是设置的同一把锁
3、造成这个问题的本质是缺乏原子性
3、删除缺乏原子性
用户A和用户B操作步骤如下:
1)比较uuid发现一样
2)准备删掉,但是还没有删除时,在1步获取的锁到了过期期间,自动释放
3)同时因为高并发情况下,B用户执行了第4步,瞬间冲进来了,获取/设置了锁lock
4)这时del lock就是B用户的锁lock,又出现了误删,也就是说A释放了B的锁
解决办法:LUA脚本保证删除原子性
解答:
用指令得到lock,这个值是当前请求的锁的值,ARGV是uuid值,如果相等就删除
执行
这个Array.asList("lock")会传递给script的KEYS[1],uuid会传给ARGV[1]
思考:
都是判断uuid和key的值是不是一样的,为什么直接比较uuid不行
核心区别:原子性
方案 | 执行方式 | 是否原子 | 是否可能误删 |
---|---|---|---|
Lua 脚本 | Redis 单线程执行(GET + DEL 一步完成) | ✅ 是 | ❌ 不会误删 |
普通 Java 代码 | 先 GET (Java端判断)→ 再 DEL (两步操作) | ❌ 非原子 | ✅ 可能误删 |