阿拉丁和灯

Thoughts, stories and ideas.



基于Redis的分布式锁原理及Tair RDB实现


背景 —— 分布式锁

应用开发的时候,经常有同步执行某段代码的要求。在单个JVM里面,我们有synchronized关键字,代码加上synchronized,即可以保证同一时间只有一个线程会执行这段代码。但是如果是要保证多台机器上的服务,只有一个会执行某段代码,利用JVM提供的锁就不够了,我们需要一个分布式锁。

最自然的方式是,使用分布式缓存作为分布式锁的实现,加锁的时候,到缓存服务器上设置一个key,其他进程的代码看到这个key已经存在了,就表明锁已经锁上,就会阻塞自己的代码,等待锁释放。基本原理非常简单,但是如果需要实现一个严谨的分布式锁,需要有更多的考虑。

实现

在做进一步的讨论之前,先说下我们要使用的分布式缓存产品:Redis和Tair RDB。Redis是使用非常广泛的分布式缓存产品,本质上是一个key-value 存储,同时支持集合等更复杂一些的数据结构。Tair RDB是阿里内部一个兼容Redis命令的产品。基本的Redis命令都可以跑在Tair RDB上,而且Tair RDB还支持更多的扩展命令。我们今天以Tair RDB作为实现分布式锁的平台。

回到分布式锁的讨论。具体来说,分布式锁需要满足:

分布式锁的功能要求

  1. 互斥性:在任何时候,一个锁只能被一个客户端所持有。
  2. 存活性A:不会死锁。即便持有某个锁的客户端挂掉或者掉线,其他客户端仍然可以获取该锁。
  3. 存活性B:容错。只要大部分分布式缓存的节点在线运行着,客户端就可以获取和释放锁。

要满足1互斥性,需要保证加锁操作是原子化的,否则的话,在多个客户端并发争夺锁的时候,有可能会发生多个客户端都认为自己抢到了锁的情况。在加锁的时候,需要

  1. 判断锁是否存在
  2. 不存在则加锁 实际上要做两件事情,但是,原子性要求这两件事要么同时完成,要么都不做。Redis提供了setnx来实现先检查是否存在,不存在则创建的操作,可以用于实现这里的加锁操作。具体代码是
SETNX lockkey lockvalue

要满足存活性A,需要考虑持有锁的客户端掉线的情况,这个时候,客户端永远不会主动释放锁,这就需要服务端有一个锁超时机制,Redis的set命令可以设置数据的超时时间,可以实现我们的要求。具体代码是

// 当lockkey不存在的时候,创建lockkey,设置其值为lockvalue,超时时间设为100秒
SET lockkey lockvalue expiration EX 100 NX 

注意这里的set命令支持NX选项,实际上包含了setnx命令的功能。

上面只考虑了加锁的情形。那么解锁呢?解锁的时候,需要判断锁是否是我加的,如果是,则解锁(删除掉Redis key)。其中判断是否是自己加的锁的方法,是看锁对应的key的值。在加锁的时候,每个客户端会对key设置一个代表自己标识的值。解锁的时候,只有key的值是我们的标识,才会进行解锁。也就是说,解锁就是要执行:1.先检查锁对应的key的值是否自己的值(锁是否自己加的),如果是,2.删除这个key(释放锁)。因为锁有超时机制,如果持有锁的客户端在快要超时的时候执行释放锁的动作,就有可能出现这样的情况:在1之后,2之前,锁正好超时,而这个时候另外一个客户端获取到了这个锁,然后2又执行了,就会错误地释放别人持有的锁。这个过程可以描述为下图:

为了解决这个问题,释放锁的操作也要做到原子性。在Redis里面,通常的做法是运行一段Lua脚本,脚本里面做判断和删除,而Redis将这段脚本作为一个原子操作执行。代码如下:

 if redis.call('get', KEYS[1]) == ARGV[1] then
     return redis.call('del', KEYS[1])
 else
     return 0
 end

Lua脚本在服务端作为一个原子操作被执行,解决了判断和释放中间可能会发生超时的问题。可以用这个方法来释放锁。但是,Tair RDB提供了更加方便的工具CAD命令。CAD命令的全称是Compare And Delete,跟大名鼎鼎的CAS(Compare and Set)命令是一个系列的,都是把两件事情合在一个操作里执行,从而保证操作原子性的指令。使用CAD命令实现释放锁非常简单,代码如下:

CAD(key,  value)

以上命令的效果相当于

if(get(key)==value) del(key)

在加锁和解锁两个步骤中都满足了以上的两个要求(互斥性,存活性A),一个基本的分布式锁就有了,只要缓存服务器能够正常工作,客户端就可以可靠地利用分布式锁来同步多个进程的代码,保证在同一时间,永远只有一个客户端能够持有某个锁。对于满足以上两个要求的锁,我们可以称为跨进程锁。它还不是完整意义上的分布式锁,因为它完全依赖一台单一的缓存服务器,一旦缓存服务器挂掉,锁就不能用了。在进阶一节我们会讨论完全意义上的分布式锁。

代码

满足以上两点要求(互斥性,存活性A)的分布式锁的完整代码如下(代码为Kotlin语言):

实现代码

代码分为初始化(1、2、3),加锁(4)和解锁(5)三部分。注意,在加锁的时候,使用了带 nx 和 ex 参数的set命令。使用nx是为了保证原子性,这样就不用再分两步,先判断锁是否已存在(已锁上),不存在再加锁。使用ex是为了能够做到超时自动释放,防止持有锁的进程意外挂掉,没有人再来释放这个锁,导致其他人永远等待锁释放。key的值是一个随机数,代表本客户端的标识,在解锁的时候用于识别这个锁是否是自己加的。

另外,这里在解锁的时候使用了CAD命令。

使用方代码

使用方代码分为四步:获取LockManager(图中6,需要指定Tair RDB的instance和password),获取某一名字的锁的对象(图中7),加锁(8),解锁(9)。

进阶

前面我们列了分布式锁的三个要求,但是只实现了前两个。如果想要进一步保证当(一部分)Redis/Tair RDB服务器挂掉或掉线的时候,分布式锁仍然可用,那么就要实现第三个要求。要做到这一点,需要有

  1. 多个缓存主服务器
  2. 一个容错的分布式锁算法

在Redis的官网上,Redis的官方提出了一个算法,RedLock,实现了容错的分布式锁,只要有超过一半的缓存服务器能够正常工作,系统就可以保证分布式锁的可用性。具体请参考:Distributed locks with Redis。网站上并且给出了RedLock的多种语言实现,可以参考。这里就不展示代码了。

总结

分布式锁使用广泛,利用分布式缓存来实现分布式锁非常自然,因为分布式缓存解决了分布式锁实现上面的一个基本要求:保证并发情况下设置某一个key的操作的原子性。但是分布式锁的实现要求不止于此,要保证分布式锁的严格正确性,需要考虑到在客户端掉线,甚至服务端不可用的情况。这些情况要求在实现分布式锁的时候支持超时自动释放,以及在释放的时候检查锁的拥有者是否是自身,并保证其原子性。基于Redis缓存的分布式锁,有相对标准的实现,将以上问题都考虑在内。Tair RDB作为兼容Redis的产品,基于Redis的分布式锁实现可以方便地迁移到Tair RDB。Tair RDB的CAD命令使得实现更加简洁。

参考资料

REDIS IN ACTION - 6.2.3 Building a lock in Redis
https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-2-distributed-locking/6-2-3-building-a-lock-in-redis/

Distributed locks with Redis
https://redis.io/topics/distlock

SETNX key value
https://redis.io/commands/setnx

谈谈Redis的SETNX
https://huoding.com/2015/09/14/463

Redis SET command syntax
https://redis.io/commands/set

Tair RDB - CAS命令用于删除分布式锁
https://yuque.antfin-inc.com/tair-v587/rdb-manual/cas-cad#08asye

分布式锁的多种实现方式
https://www.atatech.org/articles/65713

分布式锁简要介绍及Tair实现分布式锁源码(支持可重入与自旋等待)
https://www.atatech.org/articles/77540

女娲分布式锁服务的Best Practices:基于Lease的灵活多样的锁管理机制设计
https://www.atatech.org/articles/38933

分布式锁的实现
https://www.atatech.org/articles/66974

一种通用接口幂等组件的探索与实现
https://www.atatech.org/articles/107870

玩转redis
https://www.atatech.org/articles/120998

使用 Redis 实现分布式系统轻量级协调技术
https://www.ibm.com/developerworks/cn/opensource/os-cn-redis-coordinate/index.html

Tair分布式锁 实践经验
https://www.atatech.org/articles/30653



View or Post Comments