怎么做网站更新和维护,建设工程教育网app,网站建设中 下载,网站建设经费方案常见分布式锁的原理
4.1 Redisson
Redis 2.6之后才可以执行lua脚本#xff0c;比起管道而言#xff0c;这是原子性的#xff0c;模拟一个商品减库存的原子操作#xff1a;
//lua脚本命令执行方式#xff1a;redis-cli --eval /tmp/test.lua , 10
jedis.set(produ…常见分布式锁的原理
4.1 Redisson
Redis 2.6之后才可以执行lua脚本比起管道而言这是原子性的模拟一个商品减库存的原子操作
//lua脚本命令执行方式redis-cli --eval /tmp/test.lua , 10
jedis.set(product_stock_10016, 15);
//初始化商品10016的库存
String script local count redis.call(get, KEYS[1]) local a tonumber(count) local b tonumber(ARGV[1]) if a b then redis.call(set, KEYS[1], a-b) return 1 end return 0 ;
Object obj jedis.eval(script, Arrays.asList(product_stock_10016), Arrays.asList(10));
System.out.println(obj);4.1.1 尝试加锁的逻辑 上面的org.redisson.RedissonLock#lock()通过调用自己方法内部的lock方法的org.redisson.RedissonLock#tryAcquire方法。之后调用 org.redisson.RedissonLock#tryAcquireAsync 首先调用内部的org.redisson.RedissonLock#tryLockInnerAsync设置对应的分布式锁 到这里获取锁的逻辑就结束了如果这里没有获取到在Future的回调里面就会直接return会在外层有一个while true的循环订阅释放锁的消息准备被唤醒。如果说加锁成功就开始执行锁续命逻辑。 4.1.2 锁续命逻辑 lua脚本最后是以毫秒为单位返回key的剩余过期时间。成功加锁之后org.redisson.RedissonLock#scheduleExpirationRenewal中将会调用org.redisson.RedissonLock#renewExpiration这个方法内部就有锁续命的逻辑是一个定时任务等10s执行。 执行的时候尝试执行的续命逻辑使用的是Lua脚本当前的锁有值就续命没有就直接返回0 返回0之后外层会判断延时成功就会再次调用自己否则延时调用结束不再为当前的锁续命。所以这里的续命不是一个真正的定时而是循环调用自己的延时任务。 4.1.3 循环间隔抢锁机制 如果一开始就加锁成功就直接返回。 如果一开始加锁失败没抢到锁的线程就会在while循环中尝试加锁加锁成功就结束循环否则等待当前锁的超时时间之后再次尝试加锁。所以实现逻辑默认是非公平锁 里面有一个subscribe的逻辑会监听对应加锁的key当锁释放之后publish对应的消息此时如果没有到达对应的锁的超时时间也会尝试获取锁避免时间浪费。 4.1.4 释放锁和唤醒其他线程的逻辑 前面没有抢到锁的线程会监听对应的queue后面抢到锁的线程释放锁的时候会发送一个消息。 订阅的时候指定收到消息时候的逻辑会唤醒阻塞之后执行while循环
4.1.5 重入锁的逻辑 存在对应的锁就对对应的hash结构的value直接1和Java重入锁的逻辑是一致的。
4.2 RedLock解决非单体项目的Redis主从架构的锁失效
https://redis.io/docs/manual/patterns/distributed-locks/ 查看Redis官方文档对于单节点的Redis 使用setnx和lua del删除分布式锁是足够的但是主从架构的场景下锁先加在一个master节点上默认是异步同步到从节点此时master挂了会选择slave为master此时又可以加锁就会导致超卖。但是如果使用zookeeper来实现的话由于zk是CP的所以CP不存在这样的问题。 Redis文档中给出了RedLock的解决办法使用redLock真的可以解决吗 4.2.1 RedLock 原理 基于客户端的实现是基于多个独立的Redis Master节点的一种实现一般为5。client依次向各个节点申请锁若能从多数个节点中申请锁成功并满足一些条件限制那么client就能获取锁成功。它通过独立的N个Master节点避免了使用主备异步复制协议的缺陷只要多数Redis节点正常就能正常工作显著提升了分布式锁的安全性、可用性。 注意图中所有的节点都是master节点。加锁超过半数成功就认为是成功。具体流程 获取锁
获取当前时间T1作为后续的计时依据
按顺序地依次向5个独立的节点来尝试获取锁 SET resource_name my_random_value NX PX 30000
计算获取锁总共花了多少时间判断获取锁成功与否
时间T2-T1
多数节点的锁N/21
当获取锁成功后的有效时间要从初始的时间减去第三步算出来的消耗时间
如果没能获取锁成功尽快释放掉锁。
释放锁
向所有节点发起释放锁的操作不管这些节点有没有成功设置过。
public String redlock() {String lockKey product_001;//这里需要自己实例化不同redis实例的redisson客户端连接这里只是伪代码用一个redisson客户端简化了RLock lock1 redisson.getLock(lockKey);RLock lock2 redisson.getLock(lockKey);RLock lock3 redisson.getLock(lockKey);/*** 根据多个 RLock 对象构建 RedissonRedLock 最核心的差别就在这里*/RedissonRedLock redLock new RedissonRedLock(lock1, lock2, lock3);try {/*** waitTimeout 尝试获取锁的最大等待时间超过这个值则认为获取锁失败* leaseTime 锁的持有时间,超过这个时间锁会自动失效值应设置为大于业务处理的时间确保在锁有效期内业务能处理完*/boolean res redLock.tryLock(10, 30, TimeUnit.SECONDS);if (res) {//成功获得锁在这里处理业务}} catch (Exception e) {throw new RuntimeException(lock fail);} finally {//无论如何, 最后都要解锁redLock.unlock();}return end;
}但是它的实现建立在一个不安全的系统模型上的它依赖系统时间当时钟发生跳跃时也可能会出现安全性问题。分布式存储专家Martin对RedLock的分析文章Redis作者的也专门写了一篇文章进行了反驳。
Martin KleppmannHow to do distributed locking
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html AntirezIs Redlock safe?
http://antirez.com/news/101 4.2.2 RedLock 问题一持久化机制导致重复加锁 如果是上面的架构图一般生产都不会配置AOF的每一条命令都落磁盘一般会设置一些间隔时间比如1s如果ABC节点加锁成功有一个节点C恰好是在1s内加锁还没有落盘此时挂了就会导致其他客户端通过CDE又会加锁成功。 4.2.3 RedLock 问题二主从下重复加锁 除非多部署一些节点但是这样会导致加锁时间变长这样比较下来效果就不如zk了。 4.2.4 RedLock 问题三时钟跳跃导致重复加锁 C节点发生了时钟跳跃导致加上的锁没有到达实际的超时时间就被误以为超时而释放此时其他客户端就可以重复加锁了。 4.3 Curator
InterProcessMutex 可重入锁的分析 五、业务中使用分布式锁的注意点
获取的锁要设置有效期假设我们未设置key自动过期时间在Set key value NX 后如果程序crash或者发生网络分区后无法与Redis节点通信毫无疑问其他 client 将永远无法获得锁这将导致死锁服务出现中断。 SETNX和EXPIRE命令去设置key和过期时间这也是不正确的因为你无法保证SETNX和EXPIRE命令的原子性。 自己使用 setnx 实现Redis锁的时候注意并发情况下不要释放掉别人的锁业务逻辑执行时间超过锁的过期时间导致恶性循环。一般 1加锁的时候需要指定value的内容是当前进程中的当前线程的唯一标记不要使用线程ID作为当前线程的锁的标记因为不同实例上的线程ID可能是一样的。 2释放锁的逻辑会写在finally 释放锁时候要判断锁对应的value而且要使用lua脚本实现原子 del 操作。因为if逻辑判断完之后也可能失效导致删除别人的锁。 3针对扣减库存这个逻辑lua脚本里面实现Redis比较库存、扣减库存操作的原子性。通过判断Redis Decr命令的返回值即可。此命令会返回扣减后的最新库存若小于0则表示超卖。
5.1 自己实现分布式锁的坑
setnx不关心锁的顺序导致删除别人的锁 锁失效之后别人加锁成功自己把别人的锁删了。 我们无法预估程序执行需要的锁的时间。
public String deductStock() {String lockKey lock:product_101;Boolean result stringRedisTemplate.opsForValue().setIfAbsent(lockKey, deltaqin);stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);try {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 {stringRedisTemplate.delete(lockKey);}return end;
}setnx关心锁的顺序还是删除了别人的锁 并发会卡在各种地方卡住的时候过期了就会删掉别人加的锁 错误的原因还是因为解锁的逻辑不是原子性的这里可以参考Redisson的解锁逻辑使用lua脚本实现。
public String deductStock() {String lockKey lock:product_101;String clientId UUID.randomUUID().toString();Boolean result stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)if (!result) {return error_code;}try {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 {if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {// 卡在这里锁过期了其他线程又可以加锁此时又把其他线程新加的锁删掉了stringRedisTemplate.delete(lockKey);}}return end;
}解决办法 这种问题解决的办法就是使用锁续命比如使用一个定时任务间隔小于锁的超时时间每隔一段时间就给锁续命除非线程自己主动删除。这也是Redisson的实现思路。 5.2 锁优化分段加锁逻辑
针对一个商品要开启秒杀的时候会将商品的库存预先加载到Redis缓存中比如有100个库存此时可以分为5个key每一个key有20个库存。可以把分布式锁的性能提升5倍。 例如
product_10111_stock 100product_10111_stock1 20product_10111_stock2 20product_10111_stock3 20product_10111_stock4 20product_10111_stock5 20请求来了可以随机可以轮询扣减完之后就标记不要下次再分配到这个库存。
六、分布式锁的真相与选择
6.1 分布式锁的真相
需要满足的几个特性 互斥不同线程、进程互斥。
超时机制临界区代码耗时导致网络原因导致。可以使用额外的线程续命保证。
完备的锁接口阻塞的和非阻塞的接口都要有lock和tryLock。
可重入性当前请求的节点 线程唯一标识。
公平性锁唤醒时候按照顺序唤醒。
正确性进程内的锁不会因为报错死锁因为崩溃的时候整个进程都会结束。但是多实例部署时死锁就很容易发生如果粗暴使用超时机制解决死锁问题就默认了下面这个假设
锁的超时时间 获取锁的时延 执行临界区代码的时间 各种进程的暂停比如 GC
但上述假设其实无法保证的。 将分布式锁定位为可以容忍非常小概率互斥语义失效场景下的锁服务。一般来说一个分布式锁服务它的正确性要求越高性能可能就会越低。 6.2 分布式锁的选择
数据库db操作性能较差并且有锁表的风险一般不考虑。
优点实现简单、易于理解
缺点对数据库压力大
Redis适用于并发量很大、性能要求很高而可靠性问题可以通过其他方案去弥补的场景。
优点易于理解
缺点自己实现、不支持阻塞
Redisson相对于Jedis其实更多用在分布式的场景。
优点提供锁的方法可阻塞
Zookeeper适用于高可靠高可用而并发量不是太高的场景。
优点支持阻塞
缺点需理解Zookeeper、程序复杂
Curator
优点提供锁的方法
缺点Zookeeper强一致慢
Etcd安全和可靠性上有保证但是比较重。
不推荐自己编写的分布式锁推荐使用Redisson和Curator实现的分布式锁。