Redission 分布式锁
辰兮要努力 人气:5一、为什么需要分布式锁
在系统中,当存在多个进程和线程可以改变某个共享数据时,就容易出现并发问题导致共享数据的不一致性。
单体系统:如果多个线程要访问共享资源的时候,我们通常线程间加锁的机制,在某一个时刻,只有一个线程可以对这个资源进行操作,其他线程需要等待锁的释放,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 等等的并发及线程安全问题。
加载全部内容