分布式锁方案对比

不可靠的分布式锁

NPC 三个问题都是客观存在且无法消除的,可惜大部分人都没有正视或考虑过这些问题,所以才有了让分布式锁钻空子的机会。下面借用一个 Martin 大神的例子来说明一下为什么分布式锁不能解决正确性问题,先上时序图:

分布式锁时序图

在上面的时序图中,假设锁服务本身是完美的,它总是能保证任一时刻最多只有一个客户端获得锁。上图中出现的 lease 这个词可以暂且认为就等同于一个带有自动过期功能的锁。客户端 1 在获得锁之后发生了很长时间的 GC pause,在此期间,它获得的锁过期了,而客户端 2 获得了锁。当客户端 1 从 GC pause 中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源(上图中是一个存储服务)发起了写数据请求,而这时锁实际上被客户端 2 持有,因此两个客户端的写请求就有可能冲突(锁的互斥作用失效了)。

既然分布式锁无法解决正确性(correctness)问题,那么它有什么用呢?它可以用在优化程序效率(efficiency),协调各个客户端避免做重复的工作。即使锁偶尔失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果,比如重复发送了一封同样的 email。

Fencing Token

那应该如何解决正确性问题呢?Martin 给出了一种方法,称为 fencing token。fencing token 是一个单调递增的数字,当客户端成功获取锁的时候它随同锁一起返回给客户端。而客户端访问共享资源的时候带着这个 fencing token,这样提供共享资源的服务就能根据它进行检查,拒绝掉延迟到来的访问请求(避免了冲突)。时序图如下:

基于 Fencing 的时序图

在上图中,客户端 1 先获取到的锁,它获取到的 fencing token 值较小,等于 33,而客户端 2 后获取到的锁,它获取到的 fencing token 值较大,等于 34。客户端 1 从 GC pause 中恢复过来之后,依然是向存储服务发送访问请求,但是带了 fencing token = 33。存储服务发现它之前已经处理过 34 的请求,所以会拒绝掉这次 33 的请求,这样就避免了冲突。当使用 ZooKeeper 作为锁服务时,可以用事务标识 zxid 或节点版本 cversion 来充当 fencing token,这两个都可以满足单调递增的要求。

请注意,只靠客户端自己检查锁状态是不够的,这种机制要求资源本身参与检查所持 fencing token 信息,如果发现已经处理过更高版本的 fencing token,要拒绝持有低版本 fencing token 的写请求。