Zookeeper分布式锁
# Zookeeper 分布式锁
代码地址:https://github.com/xuqil/zk-lock
基于 ZooKeeper 的锁是最为常见和可靠的一种方式,ZooKeeper 是一种高性能的分布式协调服务,可以用于实现分布式锁、配置管理、服务发现等功能。在 ZooKeeper 中,可以通过创建临时节点的方式实现分布式锁,即每个进程或线程都创建一个临时节点,当一个节点创建成功时,它就可以获得锁,其他节点则需要等待锁的释放。当节点释放锁时,它所创建的临时节点也会被删除,从而通知其他节点可以重新竞争锁。
# 实现机制
# 加锁机制
- 客户端向 Zookeeper 发起请求,在指定节点(例如
/lock
)下创建一个临时顺序节点(连接断开就会自动删除,解决死锁问题); - 客户端获取 Zookeeper 节点
/lock
下的所有子节点,并且判断刚刚创建的节点是不是最小子节点; - 如果是最小子节点,加锁成功,返回;
- 如果不是最小子节点,则获取它的前一个子节点(正向排序),并且注册监听;
- 当前一个子节点被删除后(锁被其他进程释放了),Zookeeper 会通知客户端,此时客户端需要再次判断自己创建的节点是不是最小节点,如果是,加锁超过,否则继续2~5步骤。
# 释放锁机制
释放锁即删除自己创建的有序临时节点。
# Golang 实现 Zookeeper 分布式锁
# 加锁
// lockWithData attempts to acquire the lock with context.Context, writing data into the lock node.
// It will wait to return until the lock is acquired or an error occurs. If
// this instance already has the lock then ErrDeadlock is returned.
func (l *Lock) lockWithData(ctx context.Context, data []byte) error {
if l.lockPath != "" {
return ErrDeadlock
}
lockPath := ""
var err error
// try to create children node.
for i := 0; i < l.retries; i++ {
// 参加有序的临时节点
lockPath, err = l.c.CreateProtectedEphemeralSequential(l.basePath+"/"+l.key, data, l.acl)
if errors.Is(err, zk.ErrNoNode) {
// 如果父节点不存在,需要先创建父节点
if er := l.createParent(); er != nil {
return er
}
} else if err != nil {
return err
} else {
break
}
}
seq, err := l.parseSeq(lockPath) // 解析节点的序号
if err != nil {
return err
}
for {
lowestSeq := seq
prevSeq := -1
prevPath := ""
children, _, err := l.c.Children(l.basePath) // 拿到所有的子节点
if err != nil {
return err
}
for _, child := range children {
s, err := l.parseSeq(child)
if err != nil {
if errors.Is(err, errDifferentKey) {
continue
}
return err
}
if s < lowestSeq {
lowestSeq = s
}
if s < seq && s > prevSeq { // 获取前一个节点
prevSeq = s
prevPath = child
}
}
// Acquired the lock.
if seq == lowestSeq { // 如果刚刚创建的节点是最小节点,加锁成功
break
}
// 监听前一个节点
// Wait on the node next in line for the lock.
_, _, events, err := l.c.GetW(l.basePath + "/" + prevPath)
if err != nil {
// try again.
if errors.Is(err, zk.ErrNoNode) {
continue
}
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case event := <-events:
if event.Err != nil {
return event.Err
}
}
}
l.lockPath = lockPath
return nil
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 释放锁
// Unlock releases an acquired lock with context.Context. If the lock is not currently acquired by
// this Lock instance than ErrNotLocked is returned.
func (l *Lock) Unlock(ctx context.Context) error {
if l.lockPath == "" {
return ErrNotLocked
}
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// 删除自己创建的有序临时节点
if err := l.c.Delete(l.lockPath, -1); err != nil {
return err
}
l.lockPath = ""
return nil
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 优缺点
优点
- 可靠性高
- 实现较为容易
- 没有惊群效应:没有获取到锁时只监听前一个节点
缺点
- 性能不是最好:每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能,而 Zookeeper 中创建和删除节点只能通过 Leader 服务器来执行,然后 Leader 服务器还需要将数据同步到所有的 Follower 机器上,这样频繁的网络通信,性能的短板是非常突出的。
- ZooKeeper 是一个 CP 系统,即在网络分区的情况下,系统优先保证一致性,而可能牺牲可用性。
在高性能,高并发的场景下,不建议使用 ZooKeepe r的分布式锁。而由于 ZooKeeper 的高可用特性,所以在并发量不是太高的场景,推荐使用 ZooKeeper 的分布式锁。
# 锁优化
Zookeeper是一种分布式协调工具,可以用来实现分布式锁。以下是一些可以优化 Zookeeper 分布式锁的方法:
- 使用 ephemeral_sequential 节点:ephemeral_sequential 节点是 Zookeeper 中一种特殊的节点,它在创建时会自动分配一个唯一的序列号。通过使用这种节点,可以实现一个公平的分布式锁,因为每个客户端创建的节点都有一个唯一的序列号,可以确保每个客户端在获取锁时按照创建节点的顺序来获取锁。同时,这种节点也会在客户端与 Zookeepe r服务器的连接断开时自动删除,从而避免了死锁的问题。
- 设置超时时间:在使用 Zookeeper 分布式锁时,应该设置一个超时时间,以避免出现死锁的情况。当一个客户端获取锁后,在规定的时间内未能释放锁,其他客户端可以将其锁定的节点删除,从而让其他客户端获取锁。
- 减少 Zookeeper 操作:Zookeeper 是一个分布式协调工具,其性能并不如其他单机数据库那么高。因此,在使用 Zookeeper 分布式锁时,应该尽量减少 Zookeeper 的操作次数,以提高性能。例如,可以将锁的持有者信息存储在本地内存中,而不是每次都从 Zookeeper 中获取。
- 使用 Watch 机制:Zookeeper 的Watch机制可以在节点发生变化时通知客户端。在使用分布式锁时,可以使用 Watch 机制来监控锁的变化,以便及时释放锁。例如,当一个客户端持有锁时,其他客户端可以在该节点上设置一个 Watch,一旦该节点被删除,就可以重新获取锁。
- 使用多级节点:当多个客户端需要获取同一个锁时,可以使用多级节点来实现分布式锁。例如,可以在 Zookeeper 中创建一个根节点,在该节点下面创建多个子节点,每个子节点表示一个锁。当客户端需要获取某个锁时,就在该锁节点下创建一个子节点,如果创建成功,就表示该客户端成功获取了锁。如果创建失败,就继续监控该节点下的子节点,等待其他客户端释放锁。当该客户端释放锁时,就可以删除该子节点,从而让其他客户端获取锁。
# 小结
- ZK 可以通过有序临时节点实现排他锁,当创建的节点是最小节点时,获取锁超过。
- ZK 通过临时节点,解决掉了死锁的问题,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉,其他客户端自动获取锁。
- ZK 通过节点排队监听的机制,只监听前一个节点,避免了惊群效应,不会出现锁被释放,所有等待锁的线程/进程都会抢锁。
# 参考
评论
上次更新: 2024/05/29, 14:25:22