分布式锁一般都怎样实现?

Sherwin.Wei Lv7

分布式锁一般都怎样实现?

分布式锁需要实现多个应用实例之间的临界资源竞争,因此它需要依赖三方组件才能实现这样的功能。

常见依赖 Redis、ZooKeeper 来实现分布式锁。

Redis 实现

基于缓存实现分布式锁性能上会有优势,可以使用 Redis SETNX (SET if Not eXists)实现分布式锁。

注意锁需要设置过期时间,防止应用程序崩溃导致锁没有释放而阻塞后面的所有操作。

获取锁:使用 jedis.set(lockKey, lockValue, "NX", "PX", lockTimeout) 尝试获取锁,NX 确保键不存在时才设置,PX 设置键的过期时间(毫秒)。

释放锁:使用 Lua 脚本确保只有持有锁的客户端才能删除锁。Lua 脚本会检查键的值是否等于 lockValue,如果是则删除该键。

简单示例如下,acquireLock 为获取锁,releaseLock 为释放锁:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

public class RedisDistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;

private Jedis jedis;
private String lockKey;
private String lockValue;
private int lockTimeout;

public RedisDistributedLock(Jedis jedis, String lockKey, int lockTimeout) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockTimeout = lockTimeout;
this.lockValue = UUID.randomUUID().toString();
}

public boolean acquireLock() {
String result = jedis.set(lockKey, lockValue, "NX", "PX", lockTimeout);
return LOCK_SUCCESS.equals(result);
}

public boolean releaseLock() {
String releaseScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Object result = jedis.eval(releaseScript, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
return RELEASE_SUCCESS.equals(result);
}

public static void main(String[] args) {
// 创建一个 Jedis 连接实例
Jedis jedis = new Jedis("localhost", 6379);

// 创建分布式锁实例
RedisDistributedLock lock = new RedisDistributedLock(jedis, "my_lock", 10000);

// 尝试获取锁
if (lock.acquireLock()) {
try {
// 执行你的业务逻辑
System.out.println("Lock acquired, executing business logic...");
} finally {
// 释放锁
lock.releaseLock();
System.out.println("Lock released.");
}
} else {
System.out.println("Unable to acquire lock, exiting...");
}

// 关闭 Jedis 连接
jedis.close();
}
}

注意 lockValue 需要保证唯一,防止被别的客户端释放了锁

这里有个问题,如果业务还没执行完,则 Redis 的锁已经到期了怎么办?因此引入“看门狗”机制,即起一个后台定时任务,不断地给锁续期,如果锁释放了或客户端实例被关闭则停止续期,Redisson 提供了此功能。

注意:未指定超时时间的分布式锁才会续期,如果指定了超时时间则不会续期,默认 30s 超时,每 10s 续期一次,续期时长为 30s。

除了锁续期问题,还有单点故障问题,如果这台 Redis 挂了怎么办?分布锁就加不上了,业务就被阻塞了。

因此需要引入 Redis 主从,利用哨兵进行故障转移,但是这又会产生新的问题,如果 master 挂了,锁的信息还未传给 salve 节点,此时 slave 上是没加锁的,因此可能导致多个实例都成功上锁。

所以 Redis 作者又提出了 RedLock 即红锁,通过引入多个主节点共同加锁来解决单点故障问题(没有哨兵和 slave 了)。

比如现在有 5 个 Redis 节点(官方推荐至少 5 个),客户端获取当前时间 T1,然后依次利用 SETNX 对 5 个 Redis 节点加锁,如果成功 3 个及以上(大多数),再次获取当前时间 T2,如果 T2-T1 小于锁的超时时间,则加锁成功,反之则失败。

如果加锁失败则向全部节点调用释放锁的操作。

但是这个 redlock 还是有缺点的,首先它比较重,需要 5 个实例,成本不低。

其次如果发生时钟偏移,比如 5 个节点中有几个节点时间偏移了,导致锁提前超时了,那么有可能出现新客户端争抢到锁的情况,但是这个属于运维层面的问题。

还有一个就是 GC 问题,如果客户端抢到锁之后,发生了长时间的 GC 导致 redis 中锁都过期了,这样一来别的客户端就能得到锁了,且老客户端 GC 后正常执行后续的操作,导致并发修改,数据可能就不对了。不过这个问题无法避免,任何锁都可能会这样。

关于 RedLock 可以使用 Redisson ,它提供了 RedLock。

一般业务上,如果我们要使用 Redis 分布式锁,基本上使用 Redisson 客户端。

ZooKeeper

除了 Redis 还可以使用 ZooKeeper 的临时有序节点实现分布式锁。

临时 能保证超时释放,有序 能选出谁抢到了锁。

大致流程:多进程争抢创建 ZooKeeper 指定目录下的临时有序节点,创建序号最小的节点即抢到锁的进程,释放锁可以删除此节点,如果服务端挂了也会释放这个节点。

优点:如果本身已经引入了 ZooKeeper 则成本不大,实现比较简单。

缺点:相比于 Redis 性能没那么好,ZooKeeper 的写入只能写到主节点,然后同步到从节点。并且临时节点如果产生网络抖动,节点也会被删除,导致多个客户端抢到锁(当然有重试机制,产生的概率比较低)。

可使用 curator 客户端实现的分布式锁接口。

Comments
On this page
分布式锁一般都怎样实现?