天天看点

redis cluster 分布式锁_漫谈分布式锁之Redis实现

redis cluster 分布式锁_漫谈分布式锁之Redis实现

笔耕墨耘,深研术道。

redis cluster 分布式锁_漫谈分布式锁之Redis实现

01写在前面 Redis是一个高性能的内存数据库,常用于数据库、缓存和消息中间件。它提供了丰富的数据结构,更适合各种业务场景;基于AP模型,Redis保证了其高可用和高性能。

本文主要内容:

  • Redis实现分布式锁的依据
  • 基于setnx + expire的不断探索
  • 分布式利器—Redisson

  • Redlock算法及其争议

02Redis实现分布式锁的依据

Redis分布式锁的实现的核心依据:redis的单线程模型,保证最终只有一个setnx命令可以执行成功;

如何理解Redis的单线程模型(选读)?

Redis基于Reactor模式开发了自己网络事件处理器:这个处理器被称为文件事件处理器(file event handler):

  • 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器;
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
注:文件事件处理器是统称,是网络事件处理的模块(或者说方案),其包括四个组成部分:套接字、I/O多路复用程序、文件事件分派器(dispatcher),以及事件处理器。

文件事件处理器四个组成部分如下:

redis cluster 分布式锁_漫谈分布式锁之Redis实现

文件事件处理器的四个组成部分

  • I/O多路复用程序负责监听多个套接字,对于产生了事件的套接字,将它们放入一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字(单线程模型);
  • 当上一个套接字产生的事件被处理完毕之后,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字;
  • 文件事件分派器接受I/O多路复用程序传来的套接字,并根据套接字产生的事件类型,调用相应的事件处理器;
  • 这些事件处理器是一个个函数,它们会执行相应的操作。

总结:虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性。03基于setnx + expire的不断探索 为了代码的复用和可维护性,这里提供一个分布式锁的接口,各个版本的方案均实现了此接口。

基本实现代码:这里给出了阻塞和非阻塞两种方式。

public interface DistributedLock {    /**     * 阻塞     *     * @param lockKey     * @param timeout     */    void lock(String lockKey, long timeout);    /**     * 非阻塞     *     * @param lockKey     * @param timeout     * @return     */    boolean tryLock(String lockKey, long timeout);    /**     * 释放锁     *     * @param lockKey     */    void unlock(String lockKey);}
           

利用stringRedisTemplate实现,以下代码并不严谨,仅仅是演示某些问题。

@Override    public void lock(String lockKey, long timeout) {        BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(lockKey);        while (true) {            Boolean b = boundValueOps.setIfAbsent("1");            if (b) {                Boolean expire = boundValueOps.expire(timeout, TimeUnit.SECONDS);                if (expire) {                    return;                }            }            // 休眠1s后再重试            LockSupport.parkNanos(1*1000*1000);        }    }    @Override    public boolean tryLock(String lockKey, long timeout) {        BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(lockKey);        Boolean b = boundValueOps.setIfAbsent("1");        if (b) {            Boolean expire = boundValueOps.expire(timeout, TimeUnit.SECONDS);            if (expire) {                return true;            }        }        return false;    }    @Override    public void unlock(String lockKey) {        stringRedisTemplate.delete(lockKey);    }
           

上述代码,是一种简单的分布式锁的实现,但是问题也较明显:

  • 非原子性:setnx + expire两条命令非原子操作,有可能导致死锁;
  • 锁误解除:B线程加的锁,可能被A线程释放掉,导致锁失效;
  • 超时并发:业务执行时间超过锁超时时间,导致锁失效,产生并发安全问题;
  • 不可重入:不支持可重入;
  • 集群问题:Redis哨兵模式和集群模式带来的问题,主从发生failover时候带来的锁失效。

针对以上问题,提供以下解决方案:

Q1 非原子性

  • 方案一:在高版本redis中(Redis 2.6.12以后),官方完善了setnx:SET key value [EX seconds] [PX milliseconds] [NX|XX];
  • 方案二:使用LUA脚本,如下:
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)then return 0;end;redis.call('expire', KEYS[1], tonumber(ARGV[2]));return 1;
           

Q2 & Q3  锁误解除和锁超时并发

如下图:

redis cluster 分布式锁_漫谈分布式锁之Redis实现

当锁超时释放后,会有其他线程争抢锁。此时,会发生线程A释放了线程B占用锁的情况,并且会导致锁失效产生并发问题(图中棕色所示)。 针对锁误解除的问题,可以:

  • 加锁时候,设置value值,这个value值来标记当前线程,严格来说,该值在所有客户端和所有锁定请求中必须唯一。
This value must be unique across all clients and all lock requests.
  • 在解锁的时候判断解锁线程是否是占有锁的线程,出于安全性(原子性)考虑,这段解锁逻辑使用LUA脚本编写。如下:
Basically the random value is used in order to release the lock in a safe way, with a script that tells Redis: remove the key only if it exists and the value stored at the key is exactly the one I expect to be.
if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end
           

针对锁超时带来的并发问题,可以:

为获取锁的线程(图中线程A)设置一个守护线程,守护线程周期性地给当前锁续期,当线程A执行完成任务,会显示关闭守护线程;即使线程A挂掉,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。如下图:

redis cluster 分布式锁_漫谈分布式锁之Redis实现

补充:Redisson的锁续期实现,基于netty的时间轮算法。

Q4 不可重入

参考Redisson利用hash数据结构实现对线程的重入计数。这个问题将在Redisson分布式锁源码分析里面讲述。

Q5 集群问题

我们知道Redis的主从复制是异步的,主从发生failover时将带来锁失效问题。

What happens if the Redis master goes down? Well, let’s add a slave! And use it if the master is unavailable. This is unfortunately not viable. By doing so we can’t implement our safety property of mutual exclusion, because Redis replication is asynchronous.
redis cluster 分布式锁_漫谈分布式锁之Redis实现

如上图所示:

  • 左边client在Master获取到锁;
  • 在将锁信息同步到slave之前,master挂掉,此时发生failover;
  • slave节点升级为新的master(New Master),此时,其他线程来获取锁,发现并没有其他线程占用,也加锁成功。这导致了锁失效。

解决方案:

可以使用Redlock算法。

We propose an algorithm, called Redlock, which implements a DLM which we believe to be safer than the vanilla single instance approach.

04分布式利器—Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。这里我们关注其对分布式锁的支持,它解决了我们上面论述的几种问题。

概览Redisson锁的实现

redis cluster 分布式锁_漫谈分布式锁之Redis实现

如上图Redisson的类图,总结常用对象:

  • 可重入锁(Reentrant Lock)

  • 公平锁(Fair Lock)

  • 联锁(MultiLock)

  • 红锁(RedLock)

  • 读写锁(ReadWriteLock)

  • 信号量(Semaphore)

这里我们以RedissonLock(最常用的)为例,了解下其常用的一些api:

public void lock();// 不建议使用,锁续期仅仅在leaseTime = -1时生效public void lock(long leaseTime, TimeUnit unit);public boolean tryLock();// 不建议使用,锁续期仅仅在leaseTime = -1时生效public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit);public boolean tryLock(long waitTime, TimeUnit unit);
           

上述api的实际使用,这里不再演示,相对比较简单,可以通过其官网学习:

https://github.com/redisson/redisson/wiki

注意事项

  • 锁续期生效的场景:锁续期仅仅在leaseTime = -1时生效;
  • 锁【lock.lock()】的使用必须紧跟try代码块,且unlock要放到finally块第一行。这点是阿里规范,说明如下:
redis cluster 分布式锁_漫谈分布式锁之Redis实现

05Redlock算法及其争议 为了解决集群环境下,分布式锁的缺陷,Antirez发明了Redlock算法,它的实现流程比较复杂,不过业界已经有很多开源的类库封装实现了。比如Redisson提供的RedissonRedLock对象,其实现了Redlock描述的加锁算法。

Redlock算法

为了使用Redlock算法,需要提供多个Redis实例,并且这些实例之间相互独立,没有主从关系(不会有数据的同步等)。同很多分布式算法一样,Redlock也使用大多数机制。加锁时候,它会向过半节点发送set(key,value,nx=True,ex=xxx)指令,只要过半节点set成功,就认为加锁成功。释放锁时候,需要向所有节点发送del指令。不过Redlock算法还需要考虑超时问题、出错重试、时钟漂移等很多细节问题,同时因为Redlock需要向多个节点进行读写,意味着其相比单实例的Redis的性能会下降一些。

Redlock算法的争议

可以参阅:

[1] https://redis.io/topics/distlock

[2] http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

[3] http://antirez.com/news/101

06总结 至此,简单介绍了分布式锁基于Redis的实现。实际项目中建议使用Redisson提供的锁来保证临界资源的安全性。对于追求业务强一致的业务场景,可以利用分布式协调器来实现,如基于zookeeper的实现,这将在后面提到。07引用 [1] https://redis.io/topics/distlock

[2] https://github.com/redisson/redisson/wiki

[3] https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA

[4] https://learnku.com/articles/47769

[5] 黄健宏,《Redis设计与实现》,机械工业出版社

[6] Josiah L. Carlson,黄健宏,《Redis实战》,中国工信出版社

[7] 钱文品,《Redis深度历险》,中国工信出版社

发现“在看”和“赞”了吗,戳我试试吧

redis cluster 分布式锁_漫谈分布式锁之Redis实现

继续阅读