Redis分布式锁
毕业后一直做.Net工作,我喜欢C#更优美简洁的语法(虽然有些关键字或者类的命名有点隐晦)。当然Java也不能丢掉,Java的很多开源技术更能让我拓展视野,在分布式方面也更容易上手。空余时间正在将自己的一个个人项目用java重写,设计为一个分布式的项目,其中有减库存的操作。要做到全局同步,分布式锁正好用于解决此问题,在分布式环境下,多线程共享临界资源的场景下,分布式锁是一种非常重要的组件。Redis的单线程,setnx命令也是得天独厚。
我定义了一个接口,希望在将来做Redisson RedLock算法实现和ZK的分布式锁实现。下面lock是阻塞锁,实现应包含重试机制,trylock是非阻塞锁。
1 public interface DistributedLocker { 2 3 boolean lock(String key) throws InterruptedException; 4 5 boolean lock(String key,int expireSecond,int waitSecond) throws InterruptedException; 6 7 boolean tryLock(String key); 8 9 boolean tryLock(String key,int expireSecond); 10 11 boolean releaseLock(String key); 12 }
1 public class RedisLocker implements DistributedLocker { 2 3 private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); 4 private JedisClient jedisClient; 5 6 public void setClient(JedisClient client) { 7 this.jedisClient = client; 8 } 9 10 private final int defaultExpireSeconds = 5; 11 12 private final int defaultWaitSeconds = 100; 13 14 @Override 15 public boolean lock(String key) throws InterruptedException { 16 17 return lock(key, defaultExpireSeconds, defaultWaitSeconds); 18 } 19 20 @Override 21 public boolean lock(String key, int expireSecond, int waitSecond) throws InterruptedException { 22 int maxDelayMillis = waitSecond * 1000; 23 boolean isLockSucceed=false; 24 while (maxDelayMillis>0){ 25 int delayTime = (int) (Math.random() * 20); 26 maxDelayMillis-=delayTime; 27 Thread.sleep(delayTime); 28 System.out.println("wait "+delayTime); 29 long startTime = System.currentTimeMillis(); 30 isLockSucceed= tryLock(key,expireSecond); 31 long endTime = System.currentTimeMillis(); 32 System.out.println("network"+(endTime-startTime)); 33 maxDelayMillis=maxDelayMillis-delayTime-(int)(endTime-startTime); 34 System.out.println("剩余"+maxDelayMillis); 35 if (isLockSucceed){ 36 System.out.println("lock ok 剩余"+maxDelayMillis); 37 break; 38 } 39 } 40 41 return isLockSucceed; 42 } 43 44 @Override 45 public boolean tryLock(String key) { 46 return tryLock(key, defaultExpireSeconds); 47 } 48 49 @Override 50 public boolean tryLock(String key, int expireSeconds) { 51 52 String lockToken = threadLocal.get(); 53 if (StringUtils.isBlank(lockToken)) { 54 System.out.println("token为空" + lockToken); 55 lockToken = UUID.randomUUID().toString(); 56 threadLocal.set(lockToken); 57 } 58 59 boolean isLockSucceed = jedisClient.setNX(key, lockToken); 60 if (isLockSucceed) { //如果加锁成功 61 jedisClient.expire(key, expireSeconds); 62 } else {//如果加锁失败 判断是否应该重入 63 String tokenFromRedis = jedisClient.get(key); 64 System.out.println("tokenFromRedis:" + tokenFromRedis); 65 if (lockToken.equals(tokenFromRedis)) { 66 isLockSucceed = true;//可重入 67 System.out.println("重入成功"); 68 } 69 } 70 System.out.println("获取锁结果" + isLockSucceed + " token为" + lockToken); 71 return isLockSucceed; 72 } 73 74 @Override 75 public boolean releaseLock(String key) { 76 return jedisClient.del(key); 77 } 78 }
可靠性分析:
1. key是一定要设置过期时间的,setnx原生命令不包含expire选项,需要使用key的命令。非原子操作遇到expire命令不成功也许是个灾难。原生的命令好像支持直接带expire 在jedis中不确定是不是版本问题 需要再确认。
2. 释放锁,也就是del key的时候,命令可能会执行失败,导致其他线程长期拿不到锁。
3. 锁丢失,为了解决单点问题,可能引入主从加哨兵(master&&slave&&sentinel),或者集群(redis cluster)。拿主从来说,如果master setnx命令执行成功,在数据未同步给slave的瞬间, master挂掉,从升主。这时 一个setnx的key,可能会被两个线程set成功,也就是两个线程都拿到了锁。
4. 临界时间,如果一个线程使用锁后,准备del 锁,这时key过期了,其他线程立即创建key 持有锁,现在del命令到达redis并删除了刚创建的key,就很惨了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?