- 原子性
- 过期时间
- 锁续期
- 正确释放锁
下面我会介绍它们的意义以及作用
原子性言简意赅,用转账服务举例,扣款和增加余额两个操作,要么都成功,要么都失败
redis使用setnx(key,value)来进行加锁,其中,key为资源ID,value可以存放操作的用户id
可能会引起的问题:
- 如果锁没来得及释放就宕机了,会出现死锁
解决方案:给key设置个过期时间:expire(key,time,unit);
也就是两条命令
redis.set(key,value); redis.expire(key,time,unit);
那么,如果加锁成功后还没设置过期时间就宕机了,岂不是还是死锁了?
所以,原子性是redis分布式锁的关键点之一
过期时间不带过期时间可能发生死锁,上文已经提到
锁续期(著名的看门狗机制)假设锁设置过期时间为3s,但是业务执行需要4s,那么业务还没执行完锁就释放了,肯定不行,于是引入锁续期的概念
解决方案:开辟另外一个线程,专门用于锁续期,上锁的时候起个线程进行死循环续期,核心流程是判断锁的时间过了三分之一就给他重新续期为上锁时间,比如设置的锁是3s,过了1s还没执行完则重新设置为3s
正确释放锁也是锁续期就能解决该问题,具体伪代码
private volatile boolean isRunning;
// 抢锁成功
if (RESULT_OK.equals(client.setNxPx(key, value, ttl))) {
// 续期
renewalTask = new RenewTask(new IRenewalHandler() {
@Override
public void callBack() throws LockException {
// 刷新值
client.expire(key, ttl <= 0 ? 10 : ttl);
}
}, ttl);
renewalTask.start();
}
// 续期线程的逻辑
@Override
public void run() {
while (isRunning) {
try {
// 1、续租,刷新值
call.callBack();
LOGGER.info("续租成功!");
// 2、三分之一过期时间续租
TimeUnit.SECONDS.sleep(this.ttl * 1000 / 3);
} catch (InterruptedException e) {
close();
} catch (LockException e) {
close();
}
}
}
public void close() {
isRunning = false;
}
问题 2:客户端 A 上了锁,客户端 B 搞破坏,上来就 unlock 解锁,这时候直接执行redis.del(key)将客户端 A 的锁给释放了。这就相当于释放了别人的锁,这种情况怎么解决呢?
首先我们应该想一个问题:客户端 B 释放锁的时候为什么会释放掉客户端 A 的锁呢?因为锁的 key 肯定是一样的,毕竟锁的是同资源嘛。那么就需要从 value 入手了,解锁前先判断下这个 key 的 value 是不是自己加的,value 不能是线程 ID,因为分布式环境线程 ID 会重复,所以可以换成类似 userID 等业务主键;或者也可以换成随机数,因为加锁、解锁都在一个方法里,解锁的时候是可以得到这个随机数的。其伪代码如下:
// 判断订单 ID 的锁是自己上的方可释放
if((userId).equals(stringRedisTemplate.opsForValue().get(orderId))) {
stringRedisTemplate.delete(orderId);
}
这样行吗?肯定不行的,因为判断里的 Redis 获取操作和 del 操作是非原子的,如果你设置了超时时间,而你的业务在超时时间内没有执行完,那么这个锁就会被释放,其他线程拿到锁,以上恰好发生在 get 之后、del 之前,会删除其他的锁,那么是不是就脏读了呢?但是如果有续期的话就不存在此问题。但还是尽量用 lua 脚本,lua 脚本如下:
// 如果get的值等于传进来的值,就给它del
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
所以,什么叫“正确释放锁”?一言以蔽之:利用锁续期机制防止出现业务没执行完成锁就到期了、提前被释放的情况,且释放锁的时候还要判断是不是自己加的锁,避免误释放了别人的锁。
二、Redis实现分布式的几种部署方式 1、单机直接部署一台Redis,然后单机加锁
- 优点:简单,部署、使用都很方便
- 缺点:宕机直接GG,单点故障
使用哨兵机制,故障时自动主从切换
存在问题:锁写到Master时还没同步到Slave时Master挂了,随后选举了新的Master,但是新master无锁,不安全
3、集群集群只是做了slot分片,实际还是单机
4、红锁(Redlock)是Redis实现分布式锁方式中相对安全的手段
核心思路:搞几个独立的Master,比如5个,然后挨个加锁,只要超过一半以上(5/2+1)就代表加锁成功,释放时也逐台释放,好处在于一台master挂了还有其他的,不耽误,但并不是100%安全
三、Redis 实现分布式锁的优缺点凡事都有两面性,Redis 实现分布式锁亦然。
其优点包括:
- 大多数互联网企业都在用 Redis,所以无需额外引入其他中间件,节省了成本开销;
- Redis 的性能还是很高的,所以对业务系统的性能影响相对较小;
- Redisson 客户端类库将锁的所有用法都封装了(可重入锁、读写锁、公平锁等),用户调用就完事了,所以支持得比较友好。
另外,其缺点有如下:
- Redis 虽然提供了很多种部署架构,单机、哨兵、集群以及著名的红锁(RedLock),但是没有哪一种是 100% 安全的。
- 红锁(RedLock)虽然是相对最安全的一种方式,但是它极其麻烦,而且对性能的开销也是其他锁的 N 倍,因为需要逐个加锁,需要多次与 Redis 通信。



