基于Redission的分布式锁实战
作者:辰兮要努力
一、为什么需要分布式锁
在系统中,当存在多个进程和线程可以改变某个共享数据时,就容易出现并发问题导致共享数据的不一致性。
单体系统:如果多个线程要访问共享资源的时候,我们通常线程间加锁的机制,在某一个时刻,只有一个线程可以对这个资源进行操作,其他线程需要等待锁的释放,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)解决方案:设置锁超时时间
3、设置锁超时时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间
(1)存在问题:setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁, 但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。
(2)解决方案:redis的set命令支持在获取锁的同时设置key的过期时
4、使用set命令加锁并设置锁过期时间:
(1)存在问题:假如线程A成功得到了锁,并且设置的超时时间是 30 秒。 如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
(2)解决方案:可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。 在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。 但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性
………………
如上总结下来,如果使用传统的Redission的底层封装相关的代码帮助我们解决了一系列此问题
原子性 原子性 原子性
三、代码案例
分享一下Redission的代码使用案例:超简单
引入pom.xml依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.5</version> </dependency>
模拟代码
@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"; } }
Redis在命令队列层面还是单线程的, Redis在IO层面是做了多线程的优化
从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。
到此这篇关于基于Redission的分布式锁实战的文章就介绍到这了,更多相关Redission 分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!