基于redis实现分布式锁(下)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 基于redis实现分布式锁(下)

加锁脚本

Redis 提供了 Hash (哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的 锁的重入次数,然后利用 lua 脚本判断逻辑。

1. if (redis.call('exists', KEYS[1]) == 0 or 
2.     redis.call('hexists', KEYS[1], ARGV[1]) == 1) 
3. then
4.     redis.call('hincrby', KEYS[1], ARGV[1], 1);
5.     redis.call('expire', KEYS[1], ARGV[2]);
6.    return 1;
7. else
8. return 0;
9. end

假设值为:KEYS:[lock], ARGV[uuid, expire]如果锁不存在或者这是自己的锁,就通过hincrby(不存在就新增并加1,存在就加1)获取锁或者锁次 数加1。

解锁脚本

1. -- 判断 hash set 可重入 key 的值是否等于 0
2. -- 如果为 nil 代表 自己的锁已不存在,在尝试解其他线程的锁,解锁失败
3. -- 如果为 0 代表 可重入次数被减 1
4. -- 如果为 1 代表 该可重入 key 解锁成功
5. if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then 
6.    return nil; 
7. elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then 
8.    return 0; 
9. else
10.     redis.call('del', KEYS[1]); 
11.    return 1; 
12. end;

这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中,三个返回值含义如下:

1 代表解锁成功,锁被释放

0 代表可重入次数被减 1

null 代表其他线程尝试解锁,解锁失败

代码实现

由于加解锁代码量相对较多,这里可以封装成一个工具类:

具体实现:

1. public class RedisDistributeLock{
2. 
3. 
4. private StringRedisTemplate redisTemplate;
5. 
6. //线程局部变量,可以在线程内共享参数
7. private String lockName;
8. private String static uuid;
9. private Integer expire = 30;
10. private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
11. 
12. public DistributedRedisLock(StringRedisTemplate redisTemplate, String lockName) {
13. this.redisTemplate = redisTemplate;
14. this.lockName = lockName;
15. this.uuid = THREAD_LOCAL.get();
16. if (StringUtils.isBlank(uuid)) {
17. this.uuid = UUID.randomUUID().toString();
18.             THREAD_LOCAL.set(uuid);
19.         }
20.     }
21. 
22. public void lock() {
23. this.lock(expire);
24.     }
25. 
26. public void lock(Integer expire) {
27. this.expire = expire;
28. String script = "if (redis.call('exists', KEYS[1]) == 0 or" +
29. "redis.call('hexists', KEYS[1], ARGV[1]) == 1)" +
30. "        then" +
31. "  redis.call('hincrby', KEYS[1], ARGV[1], 1);" +
32. "  redis.call('expire', KEYS[1], ARGV[2]);" +
33. "  return 1;" +
34. "else" +
35. "        return 0;" +
36. " end";
37. if (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
38.                 Arrays.asList(lockName), uuid, expire.toString())) {
39. try {
40.                 Thread.sleep(60);
41.             } catch (InterruptedException e) {
42.                 e.printStackTrace();
43.             }
44. //没有获取到锁重试
45.             lock(expire);
46.         }
47.     }
48. 
49. public void unlock() {
50. String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then" +
51. "  return nil; " +
52. "elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then" +
53. "  return 0; " +
54. "else" +
55. "  redis.call('del', KEYS[1]);" +
56. "  return 1;" +
57. "end;";
58. //如果返回值没有使用Boolean,Spring-data-redis 进行类型转换时将会把 null
59. //转为 false,这就会影响我们逻辑判断
60. //所以返回类型只好使用 Long:null-解锁失败;0-重入次数减1;1-解锁成功。
61. Long result = this.redisTemplate.execute(new DefaultRedisScript<>
62.                 (script, Long.class), Arrays.asList(lockName), uuid);
63. // 如果未返回值,代表尝试解其他线程的锁
64. if (result == null) {
65. throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName: " + lockName + " with request: " + uuid);
66.         } else if (result == 1) {
67.             THREAD_LOCAL.remove();
68.         }
69.     }
70. 
71. }

使用及测试

在业务代码中使用:

1. public void checkAndLock() {
2.    // 加锁,获取锁失败重试
3.    RedisDistributeLock lock = new RedisDistributeLock(this.redisTemplate, 
4. "lock");
5.     lock.lock();
6.    // 先查询库存是否充足
7.    Stock stock = this.stockMapper.selectById(1L);
8.    // 再减库存
9.    if (stock != null && stock.getCount() > 0){
10.         stock.setCount(stock.getCount() - 1);
11.        this.stockMapper.updateById(stock);
12.    }
13.    // this.testSubLock();
14.    // 释放锁
15.     lock.unlock();
16. }

测试:

测试可重入性:

自动续期

lua脚本:

1. if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then 
2.     redis.call('expire', KEYS[1], ARGV[2]); 
3.    return 1; 
4. else
5.    return 0; 
6. end

在RedisDistributeLock中添加renewExpire方法:

1. private static final Timer TIMER = new Timer();
2. 
3. /**
4.      * 开启定时器,自动续期
5.      */
6. private void renewExpire() {
7. String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +
8. "redis.call('expire', KEYS[1], ARGV[2]); " +
9. "return 1; " +
10. "else " +
11. "return 0; end";
12.         TIMER.schedule(new TimerTask() {
13. @Override
14. public void run() {
15. //如果uuid为空,则终止定时任务
16. if (StringUtils.isNotBlank(uuid)) {
17.                     redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
18.                             Arrays.asList(lockName), RedisDistributeLock.this.uuid,
19.                             expire.toString());
20.                     renewExpire();
21.                 }
22.             }
23.         },expire * 1000 / 3);
24.     }

在lock方法中使用:

在unlock方法中添加红框中的代码:

总结

特征:

   1.独占排他:setnx

   2.防死锁:

       redis客户端程序获取到锁之后,立马宕机。给锁添加过期时间

       不可重入:可重入

   3.防误删:

       先判断是否自己的锁才能删除

   4.原子性:

       加锁和过期时间之间

       判断和释放锁之间

   5.可重入性:hash + lua脚本

   6.自动续期:Timer定时器 + lua脚本

锁操作:

   1.加锁:

       1.setnx:独占排他   死锁、不可重入、原子性

       2.set k v ex 30 nx:独占排他、死锁         不可重入

       3.hash + lua脚本:可重入锁

           1.判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby)并设置过期时间(expire)

           2.如果锁被占用,则判断是否当前线程占用的,如果是则重入(hincrby)并重置过期时间(expire)

           3.否则获取锁失败,将来代码中重试

       4.Timer定时器 + lua脚本:实现锁的自动续期

   

   2.解锁

       1.del:导致误删

       2.先判断再删除同时保证原子性:lua脚本

       3.hash + lua脚本:可重入

           1.判断当前线程的锁是否存在,不存在则返回nil,将来抛出异常

           2.存在则直接减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1

           3.不为0,则返回0

      3.重试:递归 循环  


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
相关文章
|
27天前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
136 2
|
2月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
2月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
117 8
|
4月前
|
数据采集 存储 NoSQL
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
291 67
|
3月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
925 7
|
4月前
|
缓存 监控 NoSQL
Redis设计与实现——分布式Redis
Redis Sentinel 和 Cluster 是 Redis 高可用与分布式架构的核心组件。Sentinel 提供主从故障检测与自动切换,通过主观/客观下线判断及 Raft 算法选举领导者完成故障转移,但存在数据一致性和复杂度问题。Cluster 支持数据分片和水平扩展,基于哈希槽分配数据,具备自动故障转移和节点发现机制,适合大规模高并发场景。复制机制包括全量同步和部分同步,通过复制积压缓冲区优化同步效率,但仍面临延迟和资源消耗挑战。两者各有优劣,需根据业务需求选择合适方案。
|
4月前
|
NoSQL 算法 安全
redis分布式锁在高并发场景下的方案设计与性能提升
本文探讨了Redis分布式锁在主从架构下失效的问题及其解决方案。首先通过CAP理论分析,Redis遵循AP原则,导致锁可能失效。针对此问题,提出两种解决方案:Zookeeper分布式锁(追求CP一致性)和Redlock算法(基于多个Redis实例提升可靠性)。文章还讨论了可能遇到的“坑”,如加从节点引发超卖问题、建议Redis节点数为奇数以及持久化策略对锁的影响。最后,从性能优化角度出发,介绍了减少锁粒度和分段锁的策略,并结合实际场景(如下单重复提交、支付与取消订单冲突)展示了分布式锁的应用方法。
318 3
|
4月前
|
存储 NoSQL Java
从扣减库存场景来讲讲redis分布式锁中的那些“坑”
本文从一个简单的库存扣减场景出发,深入分析了高并发下的超卖问题,并逐步优化解决方案。首先通过本地锁解决单机并发问题,但集群环境下失效;接着引入Redis分布式锁,利用SETNX命令实现加锁,但仍存在死锁、锁过期等隐患。文章详细探讨了通过设置唯一标识、续命机制等方法完善锁的可靠性,并最终引出Redisson工具,其内置的锁续命和原子性操作极大简化了分布式锁的实现。最后,作者剖析了Redisson源码,揭示其实现原理,并预告后续关于主从架构下分布式锁的应用与性能优化内容。
224 0
|
6月前
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
575 0
分布式爬虫框架Scrapy-Redis实战指南
|
7月前
|
NoSQL Java 中间件
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
1011 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁