FeelingLife FeelingLife
首页
  • Go

    • Go基础知识
  • Python

    • Python进阶
  • 操作系统
  • 计算机网络
  • MySQL
  • 学习笔记
  • 常用到的算法
  • Docker
  • Kubernetes
  • Observability
  • 容器底层
其他技术
  • 友情链接
  • 收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

xuqil

一介帆夫
首页
  • Go

    • Go基础知识
  • Python

    • Python进阶
  • 操作系统
  • 计算机网络
  • MySQL
  • 学习笔记
  • 常用到的算法
  • Docker
  • Kubernetes
  • Observability
  • 容器底层
其他技术
  • 友情链接
  • 收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Redis

  • Nginx

  • Linux

  • SSO和OAuth2

  • 分布式

    • 分布式锁

      • 分布式锁
      • Zookeeper分布式锁
        • 实现机制
          • 加锁机制
          • 释放锁机制
        • Golang 实现 Zookeeper 分布式锁
          • 加锁
          • 释放锁
        • 优缺点
        • 锁优化
        • 小结
        • 参考
      • Redis分布式锁
    • 服务注册与发现

  • Other

  • F5

  • 其他技术
  • 分布式
  • 分布式锁
xuqil
2023-05-21
目录

Zookeeper分布式锁

# Zookeeper 分布式锁

代码地址:https://github.com/xuqil/zk-lock

基于 ZooKeeper 的锁是最为常见和可靠的一种方式,ZooKeeper 是一种高性能的分布式协调服务,可以用于实现分布式锁、配置管理、服务发现等功能。在 ZooKeeper 中,可以通过创建临时节点的方式实现分布式锁,即每个进程或线程都创建一个临时节点,当一个节点创建成功时,它就可以获得锁,其他节点则需要等待锁的释放。当节点释放锁时,它所创建的临时节点也会被删除,从而通知其他节点可以重新竞争锁。

# 实现机制

# 加锁机制

  1. 客户端向 Zookeeper 发起请求,在指定节点(例如/lock)下创建一个临时顺序节点(连接断开就会自动删除,解决死锁问题);
  2. 客户端获取 Zookeeper 节点/lock下的所有子节点,并且判断刚刚创建的节点是不是最小子节点;
  3. 如果是最小子节点,加锁成功,返回;
  4. 如果不是最小子节点,则获取它的前一个子节点(正向排序),并且注册监听;
  5. 当前一个子节点被删除后(锁被其他进程释放了),Zookeeper 会通知客户端,此时客户端需要再次判断自己创建的节点是不是最小节点,如果是,加锁超过,否则继续2~5步骤。

image-20230521170010985

# 释放锁机制

释放锁即删除自己创建的有序临时节点。

# 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

# 释放锁

// 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

# 优缺点

优点

  • 可靠性高
  • 实现较为容易
  • 没有惊群效应:没有获取到锁时只监听前一个节点

缺点

  • 性能不是最好:每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能,而 Zookeeper 中创建和删除节点只能通过 Leader 服务器来执行,然后 Leader 服务器还需要将数据同步到所有的 Follower 机器上,这样频繁的网络通信,性能的短板是非常突出的。
  • ZooKeeper 是一个 CP 系统,即在网络分区的情况下,系统优先保证一致性,而可能牺牲可用性。

在高性能,高并发的场景下,不建议使用 ZooKeepe r的分布式锁。而由于 ZooKeeper 的高可用特性,所以在并发量不是太高的场景,推荐使用 ZooKeeper 的分布式锁。

# 锁优化

Zookeeper是一种分布式协调工具,可以用来实现分布式锁。以下是一些可以优化 Zookeeper 分布式锁的方法:

  1. 使用 ephemeral_sequential 节点:ephemeral_sequential 节点是 Zookeeper 中一种特殊的节点,它在创建时会自动分配一个唯一的序列号。通过使用这种节点,可以实现一个公平的分布式锁,因为每个客户端创建的节点都有一个唯一的序列号,可以确保每个客户端在获取锁时按照创建节点的顺序来获取锁。同时,这种节点也会在客户端与 Zookeepe r服务器的连接断开时自动删除,从而避免了死锁的问题。
  2. 设置超时时间:在使用 Zookeeper 分布式锁时,应该设置一个超时时间,以避免出现死锁的情况。当一个客户端获取锁后,在规定的时间内未能释放锁,其他客户端可以将其锁定的节点删除,从而让其他客户端获取锁。
  3. 减少 Zookeeper 操作:Zookeeper 是一个分布式协调工具,其性能并不如其他单机数据库那么高。因此,在使用 Zookeeper 分布式锁时,应该尽量减少 Zookeeper 的操作次数,以提高性能。例如,可以将锁的持有者信息存储在本地内存中,而不是每次都从 Zookeeper 中获取。
  4. 使用 Watch 机制:Zookeeper 的Watch机制可以在节点发生变化时通知客户端。在使用分布式锁时,可以使用 Watch 机制来监控锁的变化,以便及时释放锁。例如,当一个客户端持有锁时,其他客户端可以在该节点上设置一个 Watch,一旦该节点被删除,就可以重新获取锁。
  5. 使用多级节点:当多个客户端需要获取同一个锁时,可以使用多级节点来实现分布式锁。例如,可以在 Zookeeper 中创建一个根节点,在该节点下面创建多个子节点,每个子节点表示一个锁。当客户端需要获取某个锁时,就在该锁节点下创建一个子节点,如果创建成功,就表示该客户端成功获取了锁。如果创建失败,就继续监控该节点下的子节点,等待其他客户端释放锁。当该客户端释放锁时,就可以删除该子节点,从而让其他客户端获取锁。

# 小结

  • ZK 可以通过有序临时节点实现排他锁,当创建的节点是最小节点时,获取锁超过。
  • ZK 通过临时节点,解决掉了死锁的问题,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉,其他客户端自动获取锁。
  • ZK 通过节点排队监听的机制,只监听前一个节点,避免了惊群效应,不会出现锁被释放,所有等待锁的线程/进程都会抢锁。

# 参考

  • ZooKeeper Recipes and Solutions (opens new window)
上次更新: 2024/05/29, 06:25:22
分布式锁
Redis分布式锁

← 分布式锁 Redis分布式锁→

最近更新
01
VXLAN互通实验
05-13
02
VXLAN
05-13
03
VLAN
05-13
更多文章>
Theme by Vdoing | Copyright © 2018-2025 FeelingLife | 粤ICP备2022093535号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式