Redis实现
使用spring-data-redis提供的接口实现redis分布式锁
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
一、利用SETNX设置KEY、VALUE并设置超时时间实现分布式锁
redis2.x版本中提供了不同参数的setIfAbsent方法,看下spring-data-redis源码
public Boolean setIfAbsent(K key, V value) {
byte[] rawKey = rawKey(key);
byte[] rawValue = rawValue(value);
return execute(connection -> connection.setNX(rawKey, rawValue));
}
public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {
byte[] rawKey = rawKey(key);
byte[] rawValue = rawValue(value);
Expiration expiration = Expiration.from(timeout, unit);
return execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.ifAbsent()));
}
手写lock
public boolean redisLock(String key,String lock,long timeout){
boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, lock);
if(flag){
/**
* 拿到锁设置锁key的超时时间
*/
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
return flag;
}
public static Boolean lock(String key,String lock,long timeout){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, lock, timeout,TimeUnit.SECONDS);
if (flag) {
log.info("获取锁成功");
}
return flag;
}
第一种方式:redisLock()方法是非原子性的,当执行完setIfAbsent方法后,服务器宕机或者重启,这时候key并没有设置过期时间,会导致死锁。
第二种方式:lock()方法是原子性的,调用setIfAbsent时同步设置key的过期时间
注意:这两种方式不可以用于多线程或者事务中,调用setIfAbsent返回的是null
二、SETNX设置kv(v为系统当前时间+超时时间)和GETSET(设置一个值并返回旧值)实现分布式锁
public static Boolean redisDistributedLock(String key, long timeout) {
//锁的值为当前系统时间+过期时间
long lockValue = System.currentTimeMillis() + timeout;
log.info("锁的初始值:{}",lockValue);
if (stringRedisTemplate.opsForValue().setIfAbsent(key, String.valueOf(lockValue))) {
log.info("获取锁成功");
return true;
}else {
log.info("我没有获取到锁");
}
//其他人没有获取到锁
//获取当前锁的值
String currentLockValue = stringRedisTemplate.opsForValue().get(key);
log.info("获取当前锁的值:{}",currentLockValue);
//判断锁的值小于当前系统时间说明锁已经过期
if(StringUtils.isNotEmpty(currentLockValue) && Long.parseLong(currentLockValue) < System.currentTimeMillis()){
//getAndSet方法,返回旧的值并设置新值,线程安全所以只会有一个线程重新设置锁的新值
String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, String.valueOf(lockValue));
log.info("获取oldValue的值:{}",oldValue);
//比较getAndSet方法获取最近的值和开始的值,如果不相等就证明已经被其他线程获取了
if(StringUtils.isNotEmpty(oldValue) && oldValue.equals(currentLockValue)){
return true;
}
}
return false;
}
这种方式没有设置key的过期时间,而是将value值设置为当前系统时间+过期时间。此方式要求服务器时间一致。
Redisson锁
1、redisson依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
2、配置类
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson() {
// 单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
config.useSingleServer().setPassword("123456");
return (Redisson) Redisson.create(config);
}
}
3、redisson自动续期(看门狗)
public void task3() {
//获取锁,没有获取到锁的会阻塞
//redisson设置一个key的默认过期时间为30s
//redisson会自动续期
//设置lockey
String lockKey = "taskLock";
RLock lock = redisson.getLock(lockKey);
//上锁
/**
* 处理业务执行时间大于锁的时间,自动续期
* 不设置过期时间,默认锁的时间为30s,每1/3的时间就自动续期,业务处理完需要手动释放锁
*/
lock.lock();
//lock.lock(10,TimeUnit.SECONDS); 这种是10秒后锁自动过期,不会有自动续期的机制
//boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
try {
//模拟业务执行
Thread.sleep(40000);
log.info("模拟业务执行了40s");
} catch (Exception e){
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
}
lock.lock(); 是阻塞式等待的,默认加锁时间是30s;如果业务超长,运行期间会自动续期到30s。不用担心业务时间长,锁自动过期被删掉;加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题;
也可以自己指定解锁时间lock.lock(10,TimeUnit.SECONDS),10秒钟自动解锁,自己指定解锁时间redis不会自动续期;
看门狗原理
源码分析
private void renewExpiration() {
RedissonBaseLock.ExpirationEntry ee = (RedissonBaseLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonBaseLock.ExpirationEntry ent = (RedissonBaseLock.ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
} else {
if (res) {
RedissonBaseLock.this.renewExpiration();
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
1、如果lock锁指定了过期时间,到期后锁自动释放,不会自动续期
2、当我们没有设置过期时间,默认的过期时间是30s,即 lockWatchdogTimeout = 30000ms。
自动续期时间: internalLockLeaseTime / 3 ,如果设置lockWatchdogTimeout = 60000ms,那么自动续期时间为60/3,即20秒自动续期。
解释一下:源码中
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
如果没有设置看门狗超时时间,默认30s,那么每隔10秒自动续期。
如果设置了看门狗超时时间,就是lockWatchdogTimeout/3秒自动续期