redisLock redis分布式锁

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介:
  • redis setnx cmmand

  • java object condition queue 条件队列

  • retrycount 带有重试次数限制

  • object wait time 带有超时时间的wait

  • delete lock 删除远程锁

  • acquire lock 申请lock

  • release lock 释放lock

  • demo 演示

  • 锁的粒度问题,锁分解、锁分段

  • github https://github.com/Plen-wang/redis-lock


redis setnx 命令特性

当指定key不存在时才设置。也就是说,如果返回1说明你的命令被执行成功了,redis服务器中的key是你之前设置的值。如果返回0,说明你设置的key在redis服务器里已经存在。

1
2
3
4
5
6
7
8
9
10
Long status, expire = 0L;
status = jedis.setnx(lockKey, redisIdentityKey); /**设置 lock key.*/
if  (status > 0) {
     expire = jedis.expire(lockKey, lockKeyExpireSecond); /**set  redis key expire time.*/
}
if  (status > 0 && expire > 0) {
     logger.info(String.
             format( "t:%s,当前节点:%s,获取到锁:%s" , Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
     return  true ; /**获取到lock*/
}

如果设置成功了,才进行过期时间设置,防止你的retry lock重复设置这个过期时间,导致永远不过期。

 java object condition queue 条件队列

这里有一个小窍门,可以尽可能的最大化cpu利用率又可以解决公平性问题。


当你频繁retry的时候,要么while(true)死循环,然后加个Thread.sleep,或者CAS。前者存在一定线程上下文切换开销(Thread.sleep是不会释放出当前内置锁),而CAS在不清楚远程锁被占用多久的情况会浪费很多CPU计算周期,有可能一个任务计算个十几分钟,CPU不可能空转这么久。


这里我尝试使用condition queue条件队列特性来实现(当然肯定还有其他更优的方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if  (isWait && retryCounts < RetryCount) {
     retryCounts++;
     synchronized ( this ) { //借助object condition queue 来提高CPU利用率
         logger.info(String.
                 format( "t:%s,当前节点:%s,尝试等待获取锁:%s" , Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
         this .wait(WaitLockTimeSecond);  //未能获取到lock,进行指定时间的wait再重试.
     }
else  if  (retryCounts == RetryCount) {
     logger.info(String.
             format( "t:%s,当前节点:%s,指定时间内获取锁失败:%s" , Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
     return  false ;
else  {
     return  false ; //不需要等待,直接退出。
}


使用条件队列的好处就是,它虽然释放出了CPU但是也不会持有当前synchronized,这样就可以让其他并发进来的线程也可以获取到当前内置锁,然后形成队列。当wait时间到了被调度唤醒之后才会重新来申请synchronized锁。

简单讲就是不会再锁上等待而是在队列里等待。java object每一个对象都持有一个条件队列,与当前内置锁配合使用。

retrycount 带有重试次数限制

等待远程redis lock肯定是需要一定重试机制,但是这种重试是需要一定的限制。

1
2
3
4
5
     /**
      * 重试获取锁的次数,可以根据当前任务的执行时间来设置。
      * 需要时间=RetryCount*(WaitLockTimeSecond/1000)
      */
     private  static  final  int  RetryCount = 10;

这种等待是需要用户指定的, if (isWait && retryCounts < RetryCount) ,当isWait为true才会进行重试。


object wait time 带有超时时间的wait

object.wait(timeout),条件队列中的方法wait是需要一个waittime。

1
2
3
4
5
/**
      * 等待获取锁的时间,可以根据当前任务的执行时间来设置。
      * 设置的太短,浪费CPU,设置的太长锁就不太公平。
      */
     private  static  final  long  WaitLockTimeSecond = 2000;

默认2000毫秒。

1
this .wait(WaitLockTimeSecond);  //未能获取到lock,进行指定时间的wait再重试.

注意:this.wait虽然会blocking住,但是这里的内置锁是会立即释放出来的。所以,有时候我们可以借助这种特性来优化特殊场景。


delete lock 删除远程锁

释放redis lock比较简单,直接del key就好了

1
2
3
4
5
6
long  status = jedis.del(lockKey);
         if  (status > 0) {
             logger.info(String.
                     format( "t:%s,当前节点:%s,释放锁:%s 成功。" , Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
             return  true ;
         }

一旦delete 之后,首先wait唤醒的线程将会获得锁。


acquire lock 申请lock 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
      * 带超时时间的redis lock.
      *
      * @param lockKeyExpireSecond 锁key在redis中的过去时间
      * @param lockKey             lock key
      * @param isWait              当获取不到锁时是否需要等待
      * @throws Exception lockKey is empty throw exception.
      */
     public  Boolean acquireLockWithTimeout( int  lockKeyExpireSecond, String lockKey, Boolean isWait) throws Exception {
         if  (StringUtils.isEmpty(lockKey))  throw  new  Exception( "lockKey is empty." );
 
         int  retryCounts = 0;
         while  ( true ) {
             Long status, expire = 0L;
             status = jedis.setnx(lockKey, redisIdentityKey); /**设置 lock key.*/
             if  (status > 0) {
                 expire = jedis.expire(lockKey, lockKeyExpireSecond); /**set  redis key expire time.*/
             }
             if  (status > 0 && expire > 0) {
                 logger.info(String.
                         format( "t:%s,当前节点:%s,获取到锁:%s" , Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                 return  true ; /**获取到lock*/
             }
 
             try  {
                 if  (isWait && retryCounts < RetryCount) {
                     retryCounts++;
                     synchronized ( this ) { //借助object condition queue 来提高CPU利用率
                         logger.info(String.
                                 format( "t:%s,当前节点:%s,尝试等待获取锁:%s" , Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                         this .wait(WaitLockTimeSecond);  //未能获取到lock,进行指定时间的wait再重试.
                     }
                 else  if  (retryCounts == RetryCount) {
                     logger.info(String.
                             format( "t:%s,当前节点:%s,指定时间内获取锁失败:%s" , Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                     return  false ;
                 else  {
                     return  false ; //不需要等待,直接退出。
                 }
             catch  (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }

release lock 释放lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
      * 释放redis lock。
      *
      * @param lockKey lock key
      * @throws Exception lockKey is empty throw exception.
      */
     public  Boolean releaseLockWithTimeout(String lockKey) throws Exception {
         if  (StringUtils.isEmpty(lockKey))  throw  new  Exception( "lockKey is empty." );
 
         long  status = jedis.del(lockKey);
         if  (status > 0) {
             logger.info(String.format( "当前节点:%s,释放锁:%s 成功。" , getRedisIdentityKey(), lockKey));
             return  true ;
         }
         logger.info(String.format( "当前节点:%s,释放锁:%s 失败。" , getRedisIdentityKey(), lockKey));
         return  false ;
     }

demo 演示

2017-06-18 13:57:43.867  INFO 1444 --- [nio-8080-exec-1] c.plen.opensource.implement.RedisLocker  : t:23,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,获取到锁:product:10100101:shopping

2017-06-18 13:57:47.062  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:57:49.063  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:57:51.064  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:57:53.066  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:57:55.068  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:57:57.069  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:57:59.070  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:01.071  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:03.072  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:05.073  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:07.074  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,指定时间内获取锁失败:product:10100101:shopping

2017-06-18 13:58:23.768  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:25.769  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:27.770  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:29.772  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:31.773  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:33.774  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:35.774  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,获取到锁:product:10100101:shopping


thread 23 优先获取到对商品ID 10100101 进行修改,所以先锁住当前商品。

t:23,当前节点:843d3ec0-9c22-4d8a-bcaa-745dba35b8a4,获取到锁:product:10100101:shopping


紧接着,thread 25也来对当前商品 10100101进行修改,所以在尝试获取锁。


2017-06-18 13:50:11.021  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:13.023  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:15.026  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:17.028  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:19.030  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:21.031  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:23.035  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:25.037  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:27.041  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:29.042  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:50:35.289  INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:946b7250-29f3-459b-8320-62d31e6f1fc4,指定时间内获取锁失败:product:10100101:shopping


在进行了retry10次(2000毫秒,2秒)之后,获取失败,直接返回,等待下次任务调度开始。

2017-06-18 13:58:07.074  INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker  : t:25,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,指定时间内获取锁失败:product:10100101:shopping

2017-06-18 13:58:23.768  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:25.769  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:27.770  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:29.772  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:31.773  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:33.774  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,尝试等待获取锁:product:10100101:shopping

2017-06-18 13:58:35.774  INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker  : t:28,当前节点:5f81f482-295a-4394-b8cb-d7282e51dd6e,获取到锁:product:10100101:shopping


thread 28 发起对商品 10100101 进行修改,retry6次之后获取到lock。


锁的粒度问题,锁分解、锁分段

这里的例子比较简单。如果在并发比较大的情况下是需要结合锁分解、锁分段来进行优化的。

修改商品,没有必要锁住整个商品库,只需要锁住你需要修改的指定ID的商品。也可以借鉴锁分段思路,将数据按照一定维度进行划分,然后加上不同维度的锁,可以提升CPU性能。可以根据商品catagory来设计段锁或者batch来设计段锁。


 github 

源码已提交gihub,代码如有不对请多指教。

github地址:https://github.com/Plen-wang/redis-lock



 本文转自 王清培 51CTO博客,原文链接:http://blog.51cto.com/wangqingpei557/1939490,如需转载请自行联系原作者



相关实践学习
基于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 算法 安全
Redlock 算法-主从redis分布式锁主节点宕机锁丢失的问题
Redlock 算法-主从redis分布式锁主节点宕机锁丢失的问题
152 0
|
1月前
|
NoSQL 关系型数据库 MySQL
分布式锁(redis/mysql)
分布式锁(redis/mysql)
55 1
|
24天前
|
NoSQL Java Redis
如何通俗易懂的理解Redis分布式锁
在多线程并发的情况下,我们如何保证一个代码块在同一时间只能由一个线程访问呢?
34 2
|
1月前
|
人工智能 监控 NoSQL
【万字长文 一文搞定】Redis:从新手村到大师殿堂的奥德赛之旅 9种实现分布式锁的全技术指南
【万字长文 一文搞定】Redis:从新手村到大师殿堂的奥德赛之旅 9种实现分布式锁的全技术指南
81 4
|
1月前
|
消息中间件 存储 NoSQL
【Redis项目实战】使用Springcloud整合Redis分布式锁+RabbitMQ技术实现高并发预约管理处理系统
【Redis项目实战】使用Springcloud整合Redis分布式锁+RabbitMQ技术实现高并发预约管理处理系统
|
1月前
|
NoSQL API Redis
Redis分布式锁实现的三个核心
Redis分布式锁实现的三个核心
|
1月前
|
NoSQL Java Redis
Redis分布式锁和Java锁的区别
Redis分布式锁和Java锁的主要区别在于它们的适用范围和实现机制。
39 2
|
3月前
|
NoSQL Java 测试技术
字节二面:Spring Boot Redis 可重入分布式锁实现原理?
字节二面:Spring Boot Redis 可重入分布式锁实现原理?
158 1
|
3月前
|
存储 缓存 NoSQL
【分布式】Redis与Memcache的对比分析
【1月更文挑战第25天】【分布式】Redis与Memcache的对比分析
|
3月前
|
监控 NoSQL Linux
【分布式】Redis的持久化方案解析
【1月更文挑战第25天】【分布式】Redis的持久化方案解析

热门文章

最新文章