分布式锁的实现

在分布式环境中,经常遇到多台机器上的多个进程对同一数据的操作,如果这些进程不做互斥处理的话,往往会出现不符合预期的错误,比如商品超卖、红包超发、账号多扣款等。

多台机器上的进程的互斥,可以通过锁来达成。接下来我们介绍几种分布式锁的实现方式。

方式一、Redis setnx

这是比较常见的一种简单实现方式

我们先来看一下Redis setnx的文档介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SETNX key value

将 key 的值设为 value ,当且仅当 key 不存在。

若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

可用版本:
>= 1.0.0
时间复杂度:
O(1)
返回值:
设置成功,返回 1 。
设置失败,返回 0 。

带过期时间的分布式锁实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
class DistributeLock {
private $redis;

public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}

/**
* @param $key string 锁的key
* @param $expire int 锁定的时长,单位秒
* @return bool true:锁定成功;false:锁定失败
*/
function lock($key, $expire) {
try {
if ($this->redis->setnx($key, 1) {
$this->redis->expire($key, $expire);
return true;
}
} catch (Exception $e) {
$this->redis->del($key);
}
return false;
}

function unlock($key) {
$this->redis->del($key);
}
}

看似没问题,想一下这种情况,setnx成功之后,expire操作执行失败了,进程crash,则该key设置的锁永远得不到释放。

这种实现方式的问题在于,setnx与expire操作不是原子操作,存在单点故障时锁无法释放的问题。

方式二、RedLock

这是redis之父antirez提出的分布式锁实现方式。

Redlock获取锁的过程。

1
2
3
4
5
1. 获取开始时间
2. 去各个节点获取锁
3. 再次获取时间。
4. 计算获取锁的时间,检查获取锁的时间是否小于获取锁的时间。
5. 持有锁。

antirez版redlock-rb

PHP版redlock-php

Redlock获取锁的过程。

向各个节点发送del命令,删除锁

存在的问题

依赖于分布式机器时钟的同步。

这个问题曾引起分布式专家martin与Redis之父Antirez之间的论战。参见参考文档。

Martin 对 RedLock的指控:

1
2
3
1. 分布式的锁具有一个自动释放的功能。锁的互斥性,只在过期时间之内有效,锁过期释放以后就会造成多个Client 持有锁。

2. RedLock 整个系统是建立在,一个在实际系统无法保证的系统模型上的。在这个例子中就是系统假设时间是同步且可信的。

Martin对锁的改进,增加了token,实际是利用了CAS的乐观锁实现:

最后,martin推荐Zookeeper实现分布式锁。

Antirez的回应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
首先在实际的系统中,从两个方面来看:

1. 系统暂停,网络延迟。
2. 系统的时间发生阶跃。
对于第一个问题。上文已经提到了,RedLock做了一些微小的工作,但是没办法完全避免。其他带有自动释放的分布式锁也没有办法。

第二个问题,Martin认为系统时间的阶跃主要来自两个方面:

1. 人为修改。
2. 从NTP服务收到了一个跳跃时时钟更新。

对于人为修改,能说啥呢?人要搞破坏没办法避免。

NTP受到一个阶跃时钟更新,对于这个问题,需要通过运维来保证。需要将阶跃的时间更新到服务器的时候,应当采取小步快跑的方式。多次修改,每次更新时间尽量小。

方式三、基于ZooKeeper实现

原理分析:

1
2
3
4
5
1. 锁即Zookeeper上的一个节点
2. 客户端A发起一个加锁请求,先会在你要加锁的node下搞一个临时顺序节点。
3. 如果创建的几点序号是1,则获得锁
4. 序号不是1,则监听上一个序号释放锁。
5. 释放锁即删除临时节点。

初始状态:

获取锁的流程

参考文献

  1. SETNX
  2. SETNX key value
  3. PHP版redlock-php
  4. Redis RedLock 完美的分布式锁么?
  5. Is Redlock Safe?
  6. Martin 和 antirez关于RedLock的论战
  7. 七张图彻底讲清楚ZooKeeper分布式锁的实现原理【石杉的架构笔记】