基于Redission的分布式锁实战【代码案例详解】
【辰兮要努力】:hello你好我是辰兮,很高兴你能来阅读,昵称是希望自己能不断精进,向着优秀程序员前行!
博客来源于项目以及编程中遇到的问题总结,偶尔会有读书分享,我会陆续更新Java前端、后台、数据库、项目案例等相关知识点总结,感谢你的阅读和关注,希望我的博客能帮助到更多的人,分享获取新知,大家一起进步!
吾等采石之人,应怀大教堂之心,愿我们奔赴在各自的热爱里…
一、为什么需要分布式锁
在系统中,当存在多个进程和线程可以改变某个共享数据时,就容易出现并发问题导致共享数据的不一致性。
单体系统:如果多个线程要访问共享资源的时候,我们通常线程间加锁的机制,在某一个时刻,只有一个线程可以对这个资源进行操作,其他线程需要等待锁的释放,Java中也有一些处理锁的机制,比如synchronized。
分布式系统:当某个资源可以被多个系统访问使用到的时候,为了保证大家访问这个数据是一致性的,那么就要求再同一个时刻,只能被一个系统使用,这时候线程之间的锁机制就无法起到作用了,因为分布式环境中,系统是会部署到不同的机器上面的,那么就需要【分布式锁】了。
解决共享资源操作可能引发的数据问题
二、Redission入门
2.1 Redission执行流程
Redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
Redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?
2.2 Watch Dog 机制
Redisson中有一个watchdog看门狗的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s(默认配置)
这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
Redisson的"看门狗"逻辑保证了没有死锁发生。
备注:如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁
2.3 对比setnx
1、加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁
2、解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁
(1)存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,
那么竞争该锁的线程都会执行不了,产生死锁的情况。
(2)解决方案:设置锁超时时间
- 1
- 2
- 3
3、设置锁超时时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间
(1)存在问题:setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁,
但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。
(2)解决方案:redis的set命令支持在获取锁的同时设置key的过期时
- 1
- 2
- 3
4、使用set命令加锁并设置锁过期时间:
(1)存在问题:假如线程A成功得到了锁,并且设置的超时时间是 30 秒。
如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
(2)解决方案:可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。
但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性
- 1
- 2
- 3
- 4
- 5
- 6
如上总结下来,如果使用传统的Redission的底层封装相关的代码帮助我们解决了一系列此问题
原子性 原子性 原子性
三、代码案例
分享一下Redission的代码使用案例:超简单
引入pom.xml依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
- 1
- 2
- 3
- 4
- 5
模拟代码
@RestController
public class IndexController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "product_101";
RLock redissonLock = redisson.getLock(lockKey);
try {
//执行锁
redissonLock.lock(); //setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//释放锁
redissonLock.unlock();
}
return "end";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
Redis在命令队列层面还是单线程的, Redis在IO层面是做了多线程的优化
从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。
本期的分享到这里了,最近会陆续分享项目中遇到的问题以及解决方案,我们下期见!
📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤️ 分享👥 留言💬thanks!!!
📚愿我们奔赴在各自的热爱里!
文章来源: blessing.blog.csdn.net,作者:辰兮要努力,版权归原作者所有,如需转载,请联系作者。
原文链接:blessing.blog.csdn.net/article/details/126236861
- 点赞
- 收藏
- 关注作者
评论(0)