场景

  • 互联网秒杀
  • 抢优惠券

为什么需要分布式锁?

实际项目中,我们通常会采用分布式,也就说会做负载均衡,多台机器来同时响应请求。此时如果不采用分布式锁是锁不住的,因为不同之间机器之间的锁不是同一个。

通过setnx简单的加分布式锁

setnx的作用就是,如果key已经存在,不做任何操作。伪代码如下:

  1. boolean result = setnx("lock", "111");
  2. if (!result) {
  3. return;
  4. }
  5. 这里做真正的set操作之类的,要被锁上的代码
  6. del("lock");

以上代码,

  • 当线程A进入时,尝试去设置lock,设置成功,不会被return,执行set操作
  • 线程B进入,也尝试去设置lock, 设置失败,被return了
  • 最后A操作设置结束后,删除这个锁,保证其他的线程能进入。

这就是一把最简单的分布式锁。

注意事项:

  1. 在执行过程中可能抛异常,导致最后删除锁的代码没有被执行,解决办法是用:

    1. try {
    2. ...
    3. } finally {
    4. del("lock");
    5. }
  2. 宕机了,导致finally都没法执行,那么解决办法是设置有效期, redis就会在到达有效期的时候清除掉锁,注意设置有效期时要有原子性,即设置锁和有效期用一行命令执行。

  3. 2仍然有问题,在设置有效期的时候,有可能会因为线程执行时间长而导致锁被删掉的情况出现。

例如:
假设设置10秒过期:

  • A线程执行完毕假设需要15秒,那么在第10秒的时候,redis会自动清除这个锁
  • 因为锁被清除,线程B可以重新加锁并进入,那么就有可能和线程A一起同时改变共享变量;
  • 此外,线程A执行到15秒完毕以后,手动删除锁,但是删除的是B加的锁
  • B还没执行完,C又能进来了。
  • 导致这把锁根本没用了。
  1. 如何解决3中提到的两个问题:

问题1:有效期到了,但是线程没有执行完,导致锁被自动释放了?
问题2:如何防止线程A删掉了线程B的锁?

对于问题2,可以这么解决:
在通过setnx加锁的时候,将value设置成一个唯一的id,比如通过UUID生成一个。
在删除锁的时候,进行判断,如果当前的ID等于redis中设置的ID,才可以删除。
这样就保证了不会删除其他线程的锁,只能删除自己的锁。

对于问题1,有个思路就是:
起一个分线程开启定时任务,每隔一定时间检测线程是否还存活,如果还存活就给加的锁续命一段时间,保证它的锁不会在线程结束前被释放掉。

关于具体实现,已经不需要我们手动去写了,很多框架已经封装好了,比如Redisson,可以很容易的实现:
image.png

注意:
检查时间通常为设置的失效时间的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节点加锁成功,才算真正的加锁成功。当然这样会导致性能下降,以及锁回滚的问题。