场景
- 互联网秒杀
- 抢优惠券
为什么需要分布式锁?
实际项目中,我们通常会采用分布式,也就说会做负载均衡,多台机器来同时响应请求。此时如果不采用分布式锁是锁不住的,因为不同之间机器之间的锁不是同一个。
通过setnx简单的加分布式锁
setnx的作用就是,如果key已经存在,不做任何操作。伪代码如下:
boolean result = setnx("lock", "111");
if (!result) {
return;
}
这里做真正的set操作之类的,要被锁上的代码
del("lock");
以上代码,
- 当线程A进入时,尝试去设置lock,设置成功,不会被return,执行set操作
- 线程B进入,也尝试去设置lock, 设置失败,被return了
- 最后A操作设置结束后,删除这个锁,保证其他的线程能进入。
这就是一把最简单的分布式锁。
注意事项:
在执行过程中可能抛异常,导致最后删除锁的代码没有被执行,解决办法是用:
try {
...
} finally {
del("lock");
}
宕机了,导致finally都没法执行,那么解决办法是设置有效期, redis就会在到达有效期的时候清除掉锁,注意设置有效期时要有原子性,即设置锁和有效期用一行命令执行。
2仍然有问题,在设置有效期的时候,有可能会因为线程执行时间长而导致锁被删掉的情况出现。
例如:
假设设置10秒过期:
- A线程执行完毕假设需要15秒,那么在第10秒的时候,redis会自动清除这个锁;
- 因为锁被清除,线程B可以重新加锁并进入,那么就有可能和线程A一起同时改变共享变量;
- 此外,线程A执行到15秒完毕以后,手动删除锁,但是删除的是B加的锁;
- B还没执行完,C又能进来了。
- 导致这把锁根本没用了。
- 如何解决3中提到的两个问题:
问题1:有效期到了,但是线程没有执行完,导致锁被自动释放了?
问题2:如何防止线程A删掉了线程B的锁?
对于问题2,可以这么解决:
在通过setnx加锁的时候,将value设置成一个唯一的id,比如通过UUID生成一个。
在删除锁的时候,进行判断,如果当前的ID等于redis中设置的ID,才可以删除。
这样就保证了不会删除其他线程的锁,只能删除自己的锁。
对于问题1,有个思路就是:
起一个分线程开启定时任务,每隔一定时间检测线程是否还存活,如果还存活就给加的锁续命一段时间,保证它的锁不会在线程结束前被释放掉。
关于具体实现,已经不需要我们手动去写了,很多框架已经封装好了,比如Redisson,可以很容易的实现:
注意:
检查时间通常为设置的失效时间的1/3,比如失效时间设置为30秒,那么每10秒就会去看是否还持有锁,如果持有的话就延长10秒的时间。 这样子就保证了该锁不会被自动释放掉。如果持续那特殊情况,该线程宕机了,那么它就不再持有锁了,也就不会再给它延长时间了,就会被redis在有效期内释放掉。
Rlock redissonLock = redisson.getLock(LockKey);
try {
redissonLock.lock();
xxxx业务逻辑
} finally {
}
主从结构Redis锁失效问题
在上面,我们的解决方案仅适用于单机redis。如果是Master-slave的redis的话,有可能出现以下情况:
- 加锁给Master;
- Master要去同步给slave, 但是还没同步完,Master挂了;
- Slave中没有了那把锁;
- 其他线程发现没有那把锁了,进入,导致线程安全问题。
那么如何解决呢?
方式1:使用Zookeeper。
Redis在CAP原则中,实现的是AP原则,即可用性和分区容错性。
这体现在Redis在写锁成功后,会立马通知客户端写锁成功,保证客户端快速向下执行。但是也就导致了主从结构Redis锁失效的问题。
Zookeeper则实现的是CP,即一致性和分区容错性。这就意味着写锁成功后,Zookeeper不会立马通知客户端锁写成功了,而是要先把锁同步到Slave中,(在Zookeeper中叫follower),然后再通知客户端,让客户端向下执行。
redis可能会出现主从锁失效的问题,但是该情况其实出现的很少,Zookeeper虽然可以保证一致性,解决该问题,但是Zookeeper的性能远远不如Redis。
方式2:使用RedLock (红锁)不推荐使用
Redlock仍然是基于Redis的,它的工作原理就是,超过半数的Redis节点加锁成功,才算真正的加锁成功。当然这样会导致性能下降,以及锁回滚的问题。