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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
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数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
9天前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
43 16
|
1月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
59 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
1月前
|
NoSQL Redis 数据库
计数器 分布式锁 redis实现
【10月更文挑战第5天】
48 1
|
1月前
|
NoSQL 算法 关系型数据库
Redis分布式锁
【10月更文挑战第1天】分布式锁用于在多进程环境中保护共享资源,防止并发冲突。通常借助外部系统如Redis或Zookeeper实现。通过`SETNX`命令加锁,并设置过期时间防止死锁。为避免误删他人锁,加锁时附带唯一标识,解锁前验证。面对锁提前过期的问题,可使用守护线程自动续期。在Redis集群中,需考虑主从同步延迟导致的锁丢失问题,Redlock算法可提高锁的可靠性。
74 4
|
1月前
|
缓存 NoSQL Ubuntu
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
55 3
|
1月前
|
缓存 NoSQL 算法
面试题:Redis如何实现分布式锁!
面试题:Redis如何实现分布式锁!
|
3月前
|
NoSQL Redis
基于Redis的高可用分布式锁——RedLock
这篇文章介绍了基于Redis的高可用分布式锁RedLock的概念、工作流程、获取和释放锁的方法,以及RedLock相比单机锁在高可用性上的优势,同时指出了其在某些特殊场景下的不足,并提到了ZooKeeper作为另一种实现分布式锁的方案。
112 2
基于Redis的高可用分布式锁——RedLock
|
3月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
1月前
|
存储 缓存 NoSQL
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
62 4

热门文章

最新文章