Java 从零实现属于你的 Redis 分布式锁

Java 从零实现属于你的 Redis 分布式锁

作者:老马啸西风 2020-10-19 07:30:57

开发

前端

新闻

分布式

Redis 我们想要解决分布式系统中的并发问题,就需要引入分布式锁的概念。

[[347022]]

 

 

redis分布式锁

为什么需要分布式锁

在 jdk 中为我们提供了加锁的方式:

(1)synchronized 关键字

(2)volatile + CAS 实现的乐观锁

(3)ReadWriteLock 读写锁

(4)ReenTrantLock 可重入锁

等等,这些锁为我们变成提供极大的便利性,保证在多线程的情况下,保证线程安全。

但是在分布式系统中,上面的锁就统统没用了。

我们想要解决分布式系统中的并发问题,就需要引入分布式锁的概念。

java 代码实现

创作动机

首先是对锁实现原理的一个实现,理论指导实践,实践完善理论。

晚上关于 redis 分布式锁的文章一大堆,但是也都稂莠不齐。

redis 分布式锁工具有时候中间件团队不见得会提供,提供了也不见得经常维护,不如自己实现一个,知道原理,也方便修改。

接口定义

为了便于和 JDK 复用,我们让接口继承自 jdk 的 Lock 接口。

  1. package com.github.houbb.lock.api.core; 
  2.  
  3. import java.util.concurrent.TimeUnit; 
  4. import java.util.concurrent.locks.Lock; 
  5.  
  6. /** 
  7.  * 锁定义 
  8.  * @author binbin.hou 
  9.  * @since 0.0.1 
  10.  */ 
  11. public interface ILock extends Lock { 
  12.  
  13.     /** 
  14.      * 尝试加锁 
  15.      * @param time 时间 
  16.      * @param unit 当为 
  17.      * @param key key 
  18.      * @return 返回 
  19.      * @throws InterruptedException 异常 
  20.      * @since 0.0.1 
  21.      */ 
  22.     boolean tryLock(long time, TimeUnit unit, 
  23.                     String key) throws InterruptedException; 
  24.  
  25.     /** 
  26.      * 尝试加锁 
  27.      * @param key key 
  28.      * @return 返回 
  29.      * @since 0.0.1 
  30.      */ 
  31.     boolean tryLock(String key); 
  32.  
  33.     /** 
  34.      * 解锁 
  35.      * @param key key 
  36.      * @since 0.0.1 
  37.      */ 
  38.     void unlock(String key); 
  39.  

方法我们只添加了三个比较常用的核心方法,作为第一个版本,简单点。

后续陆续添加即可。

抽象实现

为了便于后期添加更多的所实现,这里首先实现了一个公用的抽象父类。

  1. package com.github.houbb.lock.redis.core; 
  2.  
  3. import com.github.houbb.lock.api.core.ILock; 
  4. import com.github.houbb.lock.redis.constant.LockRedisConst; 
  5. import com.github.houbb.wait.api.IWait; 
  6.  
  7. import java.util.concurrent.TimeUnit; 
  8. import java.util.concurrent.locks.Condition; 
  9.  
  10. /** 
  11.  * 抽象实现 
  12.  * @author binbin.hou 
  13.  * @since 0.0.1 
  14.  */ 
  15. public abstract class AbstractLockRedis implements ILock { 
  16.  
  17.     /** 
  18.      * 锁等待 
  19.      * @since 0.0.1 
  20.      */ 
  21.     private final IWait wait; 
  22.  
  23.     protected AbstractLockRedis(IWait wait) { 
  24.         this.wait = wait; 
  25.     } 
  26.  
  27.     @Override 
  28.     public void lock() { 
  29.         throw new UnsupportedOperationException(); 
  30.     } 
  31.  
  32.     @Override 
  33.     public void lockInterruptibly() throws InterruptedException { 
  34.         throw new UnsupportedOperationException(); 
  35.     } 
  36.  
  37.     @Override 
  38.     public boolean tryLock() { 
  39.         return tryLock(LockRedisConst.DEFAULT_KEY); 
  40.     } 
  41.  
  42.     @Override 
  43.     public void unlock() { 
  44.         unlock(LockRedisConst.DEFAULT_KEY); 
  45.     } 
  46.  
  47.     @Override 
  48.     public boolean tryLock(long time, TimeUnit unit, String key) throws InterruptedException { 
  49.         long startTimeMills = System.currentTimeMillis(); 
  50.  
  51.         // 一次获取,直接成功 
  52.         boolean result = this.tryLock(key); 
  53.         if(result) { 
  54.             return true
  55.         } 
  56.  
  57.         // 时间判断 
  58.         if(time <= 0) { 
  59.             return false
  60.         } 
  61.         long durationMills = unit.toMillis(time); 
  62.         long endMills = startTimeMills + durationMills; 
  63.  
  64.         // 循环等待 
  65.         while (System.currentTimeMillis() < endMills) { 
  66.             result = tryLock(key); 
  67.             if(result) { 
  68.                 return true
  69.             } 
  70.  
  71.             // 等待 10ms 
  72.             wait.wait(TimeUnit.MILLISECONDS, 10); 
  73.         } 
  74.         return false
  75.     } 
  76.  
  77.     @Override 
  78.     public synchronized boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 
  79.         return tryLock(time, unit, LockRedisConst.DEFAULT_KEY); 
  80.     } 
  81.  
  82.     @Override 
  83.     public Condition newCondition() { 
  84.         throw new UnsupportedOperationException(); 
  85.     } 
  86.  

最核心的实际上是 public boolean tryLock(long time, TimeUnit unit, String key) throws InterruptedException 方法。

这个方法会调用 this.tryLock(key) 获取锁,如果成功,直接返回;如果不成功,则循环等待。

这里设置了超时时间,如果超时,则直接返回 true。

redis 锁实现

我们实现的 redis 分布锁,继承自上面的抽象类。

  1. package com.github.houbb.lock.redis.core; 
  2.  
  3. import com.github.houbb.heaven.util.lang.StringUtil; 
  4. import com.github.houbb.id.api.Id; 
  5. import com.github.houbb.id.core.util.IdThreadLocalHelper; 
  6. import com.github.houbb.lock.redis.constant.LockRedisConst; 
  7. import com.github.houbb.lock.redis.exception.LockRedisException; 
  8. import com.github.houbb.lock.redis.support.operator.IOperator; 
  9. import com.github.houbb.wait.api.IWait; 
  10.  
  11. /** 
  12.  * 这里是基于 redis 实现 
  13.  * 
  14.  * 实际上也可以基于 zk/数据库等实现。 
  15.  * 
  16.  * @author binbin.hou 
  17.  * @since 0.0.1 
  18.  */ 
  19. public class LockRedis extends AbstractLockRedis { 
  20.  
  21.     /** 
  22.      * redis 操作实现 
  23.      * @since 0.0.1 
  24.      */ 
  25.     private final IOperator redisOperator; 
  26.  
  27.     /** 
  28.      * 主键标识 
  29.      * @since 0.0.1 
  30.      */ 
  31.     private final Id id; 
  32.  
  33.     public LockRedis(IWait wait, IOperator redisOperator, Id id) { 
  34.         super(wait); 
  35.         this.redisOperator = redisOperator; 
  36.         this.id = id; 
  37.     } 
  38.  
  39.     @Override 
  40.     public boolean tryLock(String key) { 
  41.         final String requestId = id.id(); 
  42.         IdThreadLocalHelper.put(requestId); 
  43.  
  44.         return redisOperator.lock(key, requestId, LockRedisConst.DEFAULT_EXPIRE_MILLS); 
  45.     } 
  46.  
  47.     @Override 
  48.     public void unlock(String key) { 
  49.         final String requestId = IdThreadLocalHelper.get(); 
  50.         if(StringUtil.isEmpty(requestId)) { 
  51.             String threadName = Thread.currentThread().getName(); 
  52.             throw new LockRedisException("Thread " + threadName +" not contains requestId"); 
  53.         } 
  54.  
  55.         boolean unlock = redisOperator.unlock(key, requestId); 
  56.         if(!unlock) { 
  57.             throw new LockRedisException("Unlock key " + key + " result is failed!"); 
  58.         } 
  59.     } 

这里就是 redis 锁的核心实现了,如果不太理解,建议回顾一下原理篇:

redis 分布式锁原理详解

加锁

加锁部分,这里会生成一个 id 标识,用于区分当前操作者。

为了安全也设置了默认的超时时间。

当然这里是为了简化调用者的使用成本,开发在使用的时候只需要关心自己要加锁的 key 即可。

当然,甚至连加锁的 key 都可以进一步抽象掉,比如封装 @DistributedLock 放在方法上,即可实现分布式锁。这个后续有时间可以拓展,原理也不难。

解锁

解锁的时候,就会获取当前进程的持有标识。

凭借当前线程持有的 id 标识,去解锁。

IOperator

我们对 redis 的操作进行了抽象,为什么抽象呢?

因为 redis 服务种类实际很多,可以是 redis 单点,集群,主从,哨兵。

连接的客户端也可以很多,jedis,spring redisTemplate, codis, redisson 等等。

这里为了后期拓展方便,就对操作进行了抽象。

接口

定义接口如下:

  1. package com.github.houbb.lock.redis.support.operator; 
  2.  
  3. /** 
  4.  * Redis 客户端 
  5.  * @author binbin.hou 
  6.  * @since 0.0.1 
  7.  */ 
  8. public interface IOperator { 
  9.  
  10.     /** 
  11.      * 尝试获取分布式锁 
  12.      * 
  13.      * @param lockKey    锁 
  14.      * @param requestId  请求标识 
  15.      * @param expireTimeMills 超期时间 
  16.      * @return 是否获取成功 
  17.      * @since 0.0.1 
  18.      */ 
  19.     boolean lock(String lockKey, String requestId, int expireTimeMills); 
  20.  
  21.     /** 
  22.      * 解锁 
  23.      * @param lockKey 锁 key 
  24.      * @param requestId 请求标识 
  25.      * @return 结果 
  26.      * @since 0.0.1 
  27.      */ 
  28.     boolean unlock(String lockKey, String requestId); 
  29.  

jedis 实现

我们实现一个 jedis 单点版本的:

  1. package com.github.houbb.lock.redis.support.operator.impl; 
  2.  
  3. import com.github.houbb.lock.redis.constant.LockRedisConst; 
  4. import com.github.houbb.lock.redis.support.operator.IOperator; 
  5. import redis.clients.jedis.Jedis; 
  6.  
  7. import java.util.Collections; 
  8.  
  9. /** 
  10.  * Redis 客户端 
  11.  * @author binbin.hou 
  12.  * @since 0.0.1 
  13.  */ 
  14. public class JedisOperator implements IOperator { 
  15.  
  16.     /** 
  17.      * jedis 客户端 
  18.      * @since 0.0.1 
  19.      */ 
  20.     private final Jedis jedis; 
  21.  
  22.     public JedisOperator(Jedis jedis) { 
  23.         this.jedis = jedis; 
  24.     } 
  25.  
  26.     /** 
  27.      * 尝试获取分布式锁 
  28.      * 
  29.      * expireTimeMills 保证当前进程挂掉,也能释放锁 
  30.      * 
  31.      * requestId 保证解锁的是当前进程(锁的持有者) 
  32.      * 
  33.      * @param lockKey         锁 
  34.      * @param requestId       请求标识 
  35.      * @param expireTimeMills 超期时间 
  36.      * @return 是否获取成功 
  37.      * @since 0.0.1 
  38.      */ 
  39.     @Override 
  40.     public boolean lock(String lockKey, String requestId, int expireTimeMills) { 
  41.         String result = jedis.set(lockKey, requestId, LockRedisConst.SET_IF_NOT_EXIST, LockRedisConst.SET_WITH_EXPIRE_TIME, expireTimeMills); 
  42.         return LockRedisConst.LOCK_SUCCESS.equals(result); 
  43.     } 
  44.  
  45.     /** 
  46.      * 解锁 
  47.      * 
  48.      * (1)使用 requestId,保证为当前锁的持有者 
  49.      * (2)使用 lua 脚本,保证执行的原子性。 
  50.      * 
  51.      * @param lockKey   锁 key 
  52.      * @param requestId 请求标识 
  53.      * @return 结果 
  54.      * @since 0.0.1 
  55.      */ 
  56.     @Override 
  57.     public boolean unlock(String lockKey, String requestId) { 
  58.         String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
  59.         Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); 
  60.         return LockRedisConst.RELEASE_SUCCESS.equals(result); 
  61.     } 
  62.  

这里时最核心的部分。

别看简单几行代码,需要注意的点还是很多的。

加锁

加锁时附带 requestId,用来标识自己为锁的持有者。

SETNX 当 key 不存在时才进行加锁。

设置加锁的过期时间,避免因异常等原因未释放锁,导致锁的长时间占用。

解锁

使用 lua 脚本,保证操作的原子性。

为了证明为锁的持有者,传入 requestId。

测试验证

maven 引入

  1. <dependency> 
  2.     <groupId>com.github.houbb</groupId> 
  3.     <artifactId>lock-core</artifactId> 
  4.     <version>0.0.1</version> 
  5. </dependency> 

测试代码

  1. Jedis jedis = new Jedis("127.0.0.1"6379); 
  2. IOperator operator = new JedisOperator(jedis); 
  3.  
  4. // 获取锁 
  5. ILock lock = LockRedisBs.newInstance().operator(operator).lock(); 
  6.  
  7. try { 
  8.     boolean lockResult = lock.tryLock(); 
  9.     System.out.println(lockResult); 
  10.     // 业务处理 
  11. catch (Exception e) { 
  12.     e.printStackTrace(); 
  13. finally { 
  14.     lock.unlock(); 

小结

到这里,一个简单版本的 redis 分布式锁就实现完成了。

当然还有很多可以改进的地方:

(1)比如引入递增的 sequence,避免分布式锁中的 GC 导致的问题

(2)对于更多 redis 服务端+客户端的支持

(3)对于注解式 redis 分布式锁的支持

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/256000.html<

(0)
运维的头像运维
上一篇2025-05-01 02:45
下一篇 2025-05-01 02:47

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注