分布式锁
# 分布式锁
# 分布式锁是什么
分布式锁是一种用于协调多个进程或线程之间访问共享资源的机制,它可以避免多个进程或线程同时对共享资源进行修改而导致的数据不一致问题。在分布式系统中,由于数据的分散存储在不同的节点上,因此需要一种可靠的分布式锁机制。
分布式锁通常需要满足以下几个条件:
- 互斥性:在任何时刻,只能有一个进程或线程获得锁。
- 安全性:一旦一个进程或线程获得锁,其他进程或线程无法修改该锁的状态,只有锁的持有者可以释放锁。
- 高可用性:分布式锁应该具有高可用性,即当某个节点或进程故障时,其他节点或进程可以接管该锁。
- 性能:分布式锁应该具有高性能,即在高并发的情况下,锁的获取和释放应该尽量快速。
# 为什么要用分布式锁
当多个协程/线程/进程同时读写一个共享资源时,如果没有锁的情况下,会造成数据损坏。
例如一个秒杀活动中,库存总共有1000件商品,同时有1000人参与秒杀;假设有1000个进程/线程/协程同一时刻对秒杀库存进行读写,各自将库存数目减1,那么库存的中商品的数目最终会是多少呢?
func TestNoLockSecKill(t *testing.T) {
inventory := 10
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(inventory *int) {
defer wg.Done()
time.Sleep(2 * time.Millisecond)
*inventory -= 1
}(&inventory)
}
wg.Wait()
fmt.Println("inventory:", inventory)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 单机环境
在单机环境下,分两种情况:
单进程:同一个进程中多个线程/协程同时读取共享资源时,可以通过互斥线程锁来防止数据损坏;
func TestMutexLock(t *testing.T) { inventory := 1000 var wg sync.WaitGroup var lock sync.Mutex for i := 0; i < 1000; i++ { wg.Add(1) go func(inventory *int) { defer wg.Done() time.Sleep(2 * time.Millisecond) lock.Lock() *inventory -= 1 lock.Unlock() }(&inventory) } wg.Wait() fmt.Println("inventory:", inventory) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19多进程:在单机环境下,多个进程也可以利用内核提供的排他锁来防止数据损坏;
func TestProcessLock(t *testing.T) { lockFile := "./lock.pid" lock, err := os.Create(lockFile) if err != nil { t.Fatal(err) } defer os.Remove(lockFile) defer lock.Close() err = syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) if err != nil { t.Fatal("running, ./lock.pid exists", err) } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 分布式环境
单机环境演变成分布式环境后,我们需要一个分布式服务,提供相同的功能,不同的机器通过它获取锁。获得锁的机器可以独占访问共享资源。
从独立锁到分布式锁:
# CAP
CAP 即:
- Consistency(一致性)
- 所有节点同时看到相同的数据
- Availability(可用性)
- 可用性意味着任何发出数据请求的客户端都会得到响应,即使一个或多个节点已关闭。另一种表述方式——分布式系统中的所有工作节点都无一例外地为任何请求返回有效响应。
- Partition tolerance(分区容忍性)
- 尽管任意消息丢失或系统的一部分发生故障,系统仍继续运行
这三个性质对应了分布式系统的三个指标,而 CAP 理论说的就是:一个分布式系统,不可能同时做到这三点。
# 分布式锁的实现
常见的实现分布式锁的方法有:
- 基于数据库(例如 MySQL)的锁
- 基于缓存(例如 Redis)的锁
- 基于 ZooKeeper 的锁
分布式锁根据锁资源的安全性分为以下几种:
- 基于异步复制的分布式系统,如 MySQL、Tair、Redis
- 基于 Paxos 的分布式共识系统,如 ZooKeeper、etcd、Consul
基于异步复制的分布式系统面临数据丢失(锁丢失)的风险,不够安全。这样的系统往往通过 TTL 机制提供细粒度的锁服务。适用于时间敏感的业务,需要设置较短的有效期,任务较短,发生锁丢失对业务的影响相对较小。
基于 Paxos 协议的分布式系统通过共识协议保证数据的多副本,数据安全性高。它经常使用租约(session)机制来提供粗粒度的锁服务。该系统有一定的使用需求,适用于对安全性敏感,需要长期持有锁,不能承受锁丢失的业务。
# 基于数据库的锁
优点:原理简单,实现容易;
缺点:
- 由于数据库本身制约,性能较差,不支持高并发。
- 锁超时不方便
- 锁释放通知不方便
- 高可用的方案比较少
# 基于 Redis 的锁
优点:Redis 本身是内存数据库,基于 Redis 实现的锁天然支持高并发,性能最好
缺点:
- Redis 集群是个 AP 系统,不是 CP 系统,可能丢数据数据不是强一致,数据可能丢失
- 以广播的形式进行锁释放同时,引起惊群
# 基于 ZooKeeper 的锁
优点:由于 Zookeeper 是基于分布式集群,它的可靠性最好
缺点:
- 基于 Zookeeper 的加锁操作都需要在 Leader 上实现,不适合高并发
- ZooKeeper 是一个 CP 系统,即在网络分区的情况下,系统优先保证一致性,而可能牺牲可用性。