一、SETNX
在Redis中一般会使用setnx命令实现分布式锁。当使用setnx
命令设置一个kv时
如:
setnx lockKey lockValue
当lockKey存在时,会返回1时表示设置成功。
当lockKey不存在时,会返回0时表示设置值失败。
根据以上理论就可以实现redis的分布式锁:
加锁
第一版:只使用setnx,这种方式的缺点是容易产生死锁,因为有可能忘记解锁,或者解锁失败。
setnx key value
第二版:给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
setnx key value
expire key seconds
第三版:通过
“set...nx...”
命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。
set key value nx ex seconds
解锁
解锁就是删除代表锁的那份数据。
del key
问题:虽然看起来已经很完美了,但实际上还有隐患,如下图。进程A在任务没有执行完毕时,锁已经到期被释放了。等进程A的任务执行结束后,它依然会尝试释放锁,因为它的代码逻辑就是任务结束后释放锁。但是,它的锁早已自动释放过了,所以此时它释放的可能是其他线程的锁。
想要解决这个问题,我们需要解决两件事情:
(1)
在加锁时就要给锁设置一个标识,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。可以为key
赋一个随机值,来充当进程的标识。
(2)解锁时要先判断、再释放,这两步需要保证原子性,否则第二步失败的话,就会出现死锁。而获取和删除命令不是原子的,这就需要采用Lua
脚本,通过
Lua
脚本将两个命令编排在一起,而整个Lua脚本的执行过程是原子的。
按照以上思路最终方案如下:
加锁
set key random-value nx ex seconds
解锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0 end
二、使用RedisTemplate实现
有了以上理论基础,再通过Spring框架提供的RedisTemplate我们就可以轻松的封装出自己的RedisLock类
/**
* Redis 分布式锁
*
**/
@Component
public class RedisLockUtils {
@Autowired
private RedisTemplate redisTemplate;
//分布式锁过期时间 s 可以根据业务自己调节
private static final Long LOCK_REDIS_TIMEOUT = 10L;
//分布式锁休眠 至 再次尝试获取 的等待时间 ms 可以根据业务自己调节
public static final Long LOCK_REDIS_WAIT = 500L;
/**
* 加锁
**/
public Boolean getLock(String key,String value){
Boolean lockStatus = this.redisTemplate.opsForValue().setIfAbsent(key,value, Duration.ofSeconds(LOCK_REDIS_TIMEOUT));
return lockStatus;
}
/**
* 释放锁
**/
public Long releaseLock(String key,String value){
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript,Long.class);
Long releaseStatus = (Long)this.redisTemplate.execute(redisScript, Collections.singletonList(key),value);
return releaseStatus;
}
}
调用例子:
@Service
public class SysUserServiceImpl implements ISysUserService
{
private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);
@Autowired
private RedisLockUtils redisLockUtils;
@Override
public Boolean demo1(Double money) throws InterruptedException {
String key = "test:key";
String value = IdUtils.randomUUID();
//redis尝试获取锁,加锁
Boolean getLock = this.redisLockUtils.getLock(key,value);
if(getLock){
log.info("{}:成功获取[{}]锁",Thread.currentThread().getName(),key);
//业务开始
SysUser addMoenyPO = new SysUser();
Long userId = SecurityUtils.getUserId();
addMoenyPO.setUserId(userId);
addMoenyPO.setMoney(money);
this.userMapper.toUpMoney(addMoenyPO);
//业务结束
//释放分布式锁
this.redisLockUtils.releaseLock(key,value);
log.info("{}:释放[{}]锁",Thread.currentThread().getName(),key);
}else{
//线程休眠 然后尝试递归获取锁
log.info("{}:尝试获取[{}]锁",Thread.currentThread().getName(),key);
Thread.sleep(RedisLockUtils.LOCK_REDIS_WAIT);
this.topUp(money);
}
return true;
}
}