基于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 Redis
基于Redis的高可用分布式锁——RedLock
这篇文章介绍了基于Redis的高可用分布式锁RedLock的概念、工作流程、获取和释放锁的方法,以及RedLock相比单机锁在高可用性上的优势,同时指出了其在某些特殊场景下的不足,并提到了ZooKeeper作为另一种实现分布式锁的方案。
68 2
基于Redis的高可用分布式锁——RedLock
|
1月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
15天前
|
存储 NoSQL Redis
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
Redis持久化、RDB和AOF方案、Redis主从集群、哨兵、分片集群、散列插槽、自动手动故障转移
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
|
1月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
这篇文章介绍了如何在SpringBoot项目中整合Redis,并探讨了缓存穿透、缓存雪崩和缓存击穿的问题以及解决方法。文章还提供了解决缓存击穿问题的加锁示例代码,包括存在问题和问题解决后的版本,并指出了本地锁在分布式情况下的局限性,引出了分布式锁的概念。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
|
1月前
|
NoSQL 安全 Java
nicelock--一个注解即可使用Redis分布式锁!
Nicelock的引入为分布式系统中的资源同步访问提供了一个简单高效和可靠的解决方案。通过注解的方式,简化了锁的实现和使用,使开发人员可以将更多精力专注于业务逻辑的实现,而不是锁的管理。此外,Nicelock在保持简单易用的同时,也提供了足够的灵活性和可靠性,满足了不同应用场景下对分布式锁的需求。
32 1
|
1月前
|
NoSQL Go Redis
用 Go + Redis 实现分布式锁
用 Go + Redis 实现分布式锁
|
1月前
|
NoSQL Java Redis
Redis字符串数据类型之INCR命令,通常用于统计网站访问量,文章访问量,实现分布式锁
这篇文章详细解释了Redis的INCR命令,它用于将键的值增加1,通常用于统计网站访问量、文章访问量,以及实现分布式锁,同时提供了Java代码示例和分布式锁的实现思路。
44 0
|
NoSQL Java 关系型数据库
浅谈Redis实现分布式锁
浅谈Redis实现分布式锁
|
存储 canal 缓存
|
NoSQL PHP Redis
redis实现分布式锁
redis实现分布式锁
166 0
redis实现分布式锁