redistemplate分布式锁实现_基于 Redis SETNX 实现分布式锁

bc1279ac32e5f25c314f92a5511af64f.png

环境与配置

  • Redis 任意版本即可

  • SpringBoot 任意版本即可,但是需要依赖 spring-boot-starter-data-redis

1<dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-data-redis</artifactId> 4</dependency> 5

看一看 Redis 社区对 SETNX 的解释

Redis SETNX

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists".

Return value: Integer reply, specifically:

  • 1 if the key was set
  • 0 if the key was not set

由于当某个 key 不存在的时候,SETNX 才会设置该 key。且由于 Redis 采用单进程单线程模型,所以,不需要担心并发的问题。那么,就可以利用 SETNX 的特性维护一个 key,存在的时候,即锁被某个线程持有;不存在的时候,没有线程持有锁。

关于实现的解释

由于只涉及到 Redis 的操作,所以,代码实现比较简单。只对外提供两个接口:获取锁、释放锁。 IDistributedLock: 操作接口定义 RedisLock: IDistributedLock 的实现类 DistributedLockUtil: 分布式锁工具类 SpringContextUtil: 获取当前 classpath 中的 Bean

SETNX 命令对应到 StringRedisTemplate 的 api 是 setIfAbsent,如下所示

1/** 2 * Set {@code key} to hold the string {@code value} if {@code key} is absent. 3 * 4 * @param key must not be {@literal null}. 5 * @param value 6 * @see <a href="http://redis.io/commands/setnx">Redis Documentation: SETNX</a> 7 */ 8Boolean setIfAbsent(K key, V value); 9

源码和注释信息

1/** 2 * <h1>分布式锁接口</h1> 3 * 只需要两个接口: 获取锁与释放锁 4 */ 5public interface IDistributedLock { 6 /** 7 * <h2>获取锁</h2> 8 * */ 9 boolean acquire(); 10 /** 11 * <h2>释放锁</h2> 12 * */ 13 void release(); 14} 15import SpringContextUtil; 16import lombok.extern.slf4j.Slf4j; 17import org.springframework.data.redis.core.StringRedisTemplate; 18 19/** 20 * <h1>基于 Redis 实现的分布式锁</h1> 21 */ 22@Slf4j 23public class RedisLock implements IDistributedLock { 24 25 /** redis client */ 26 private static StringRedisTemplate redisTemplate; 27 28 private String lockKey; // 锁的键值 29 private int expireMsecs = 15 * 1000; // 锁超时, 防止线程得到锁之后, 不去释放锁 30 private int timeoutMsecs = 15 * 1000; // 锁等待, 防止线程饥饿 31 private boolean locked = false; // 是否已经获取锁 32 33 RedisLock(String lockKey) { 34 this.lockKey = lockKey; 35 } 36 37 RedisLock(String lockKey, int timeoutMsecs) { 38 this.lockKey = lockKey; 39 this.timeoutMsecs = timeoutMsecs; 40 } 41 42 RedisLock(String lockKey, int expireMsecs, int timeoutMsecs) { 43 this.lockKey = lockKey; 44 this.expireMsecs = expireMsecs; 45 this.timeoutMsecs = timeoutMsecs; 46 } 47 48 public String getLockKey() { 49 return this.lockKey; 50 } 51 52 @Override 53 public synchronized boolean acquire() { 54 55 int timeout = timeoutMsecs; 56 57 if (redisTemplate == null) { 58 redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class); 59 } 60 61 try { 62 63 while (timeout >= 0) { 64 65 long expires = System.currentTimeMillis() + expireMsecs + 1; 66 String expiresStr = String.valueOf(expires); // 锁到期时间 67 68 if (redisTemplate.opsForValue().setIfAbsent(lockKey, expiresStr)) { 69 locked = true; 70 log.info("[1] 成功获取分布式锁!"); 71 return true; 72 } 73 String currentValueStr = redisTemplate.opsForValue().get(lockKey); // redis里的时间 74 75 // 判断是否为空, 不为空的情况下, 如果被其他线程设置了值, 则第二个条件判断是过不去的 76 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { 77 78 String oldValueStr = redisTemplate.opsForValue().getAndSet(lockKey, expiresStr); 79 80 // 获取上一个锁到期时间, 并设置现在的锁到期时间 81 // 只有一个线程才能获取上一个线程的设置时间 82 // 如果这个时候, 多个线程恰好都到了这里, 但是只有一个线程的设置值和当前值相同, 它才有权利获取锁 83 if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { 84 locked = true; 85 log.info("[2] 成功获取分布式锁!"); 86 return true; 87 } 88 } 89 90 timeout -= 100; 91 Thread.sleep(100); 92 } 93 } catch (Exception e) { 94 log.error("获取锁出现异常, 必须释放: {}", e.getMessage()); 95 } 96 97 return false; 98 } 99 100 @Override 101 public synchronized void release() { 102 103 if (redisTemplate == null) { 104 redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class); 105 } 106 107 try { 108 if (locked) { 109 110 String currentValueStr = redisTemplate.opsForValue().get(lockKey); // redis里的时间 111 112 // 校验是否超过有效期, 如果不在有效期内, 那说明当前锁已经失效, 不能进行删除锁操作 113 if (currentValueStr != null && Long.parseLong(currentValueStr) > System.currentTimeMillis()) { 114 redisTemplate.delete(lockKey); 115 locked = false; 116 log.info("[3] 成功释放分布式锁!"); 117 } 118 } 119 } catch (Exception e) { 120 log.error("释放锁出现异常, 必须释放: {}", e.getMessage()); 121 } 122 } 123} 124/** 125 * <h1>分布式锁工具类</h1> 126 */ 127public class DistributedLockUtil { 128 129 /** 130 * 获取分布式锁 131 * 默认获取锁15s超时, 锁过期时间15s 132 */ 133 public static IDistributedLock getDistributedLock(String lockKey) { 134 lockKey = assembleKey(lockKey); 135 return new RedisLock(lockKey); 136 } 137 138 /** 139 * 获取分布式锁 140 */ 141 public static IDistributedLock getDistributedLock(String lockKey, int timeoutMsecs) { 142 lockKey = assembleKey(lockKey); 143 return new RedisLock(lockKey, timeoutMsecs); 144 } 145 146 /** 147 * 获取分布式锁 148 */ 149 public static IDistributedLock getDistributedLock(String lockKey, int timeoutMsecs, int expireMsecs) { 150 lockKey = assembleKey(lockKey); 151 return new RedisLock(lockKey, expireMsecs, timeoutMsecs); 152 } 153 154 /** 155 * 对 key 进行拼接 156 */ 157 private static String assembleKey(String lockKey) { 158 return String.format("imooc_analyze_%s", lockKey); 159 } 160} 161import org.springframework.beans.BeansException; 162import org.springframework.context.ApplicationContext; 163import org.springframework.context.ApplicationContextAware; 164import org.springframework.stereotype.Component; 165 166/** 167 * <h1>获取当前 classpath 中的 Bean</h1> 168 */ 169@Component 170public class SpringContextUtil implements ApplicationContextAware { 171 172 private static ApplicationContext applicationContext; 173 174 @Override 175 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 176 SpringContextUtil.applicationContext = applicationContext; 177 } 178 179 public static ApplicationContext getApplicationContext() { 180 return applicationContext; 181 } 182 183 @SuppressWarnings("unchecked") 184 public static <T> T getBean(Class c) throws BeansException { 185 return (T) applicationContext.getBean(c); 186 } 187} 188

代码交流 2021