分布式锁的几种实现方式

分布式锁的几种实现方式

为什么需要分布式锁?

其实跟单体应用在多线程情况下访问同一共享资源会加锁一样,使用分布式锁是为了在分布式环境下,系统部署在多个机器下,实现多个进程访问同一临界资源,保证在同一时刻只有一个进程可以访问。

分布式锁

分布式锁的三个 主要核心要素:

    安全性、互斥性。在同一时间内,不允许多个 client 同时获得锁。 活性。无论 client 出现 crash 还是遭遇网络分区,你都需要确保任意故障场景下,都不 会出现死锁,常用的解决方案是超时和自动过期机制。 高可用、高性能。加锁、释放锁的过程性能开销要尽量低,同时要保证高可用,避免单 点故障。

分布式锁的实现

基于数据库实现

这个方案一般不会使用,虽然基于数据库实现的分布式锁,是最容易理解的。但是因为数据库需要落到硬盘上,频繁读 取数据库会导致 IO 开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。

要实现分布式锁,最简单的方式就是创建一张锁表,然后通过操作该表中的数据来实现。当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。通过select。。for update访问同一条数据,for update会锁定数据,其他线程只能等待。

Redis

setnx

没设置超时,如果执行业务操作的时候服务挂了死锁了怎么办
setnx expire

释放了别人的锁怎么办

可以通过给key设置唯一的uuid,然后释放的时候通过uuid来判断这把锁是否是自己的,防止释放了别人的锁。

秒杀时扣减库存不是原子性操作怎么办

lua+redis

加锁时间到了业务操作没执行完锁释放了怎么办

Redisson

分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

当前开源框架Redisson就解决了这个分布式锁问题。

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。

Redis主备切换或者脑裂导致锁被两个线程获取了怎么办

为什么锁不安全

道 Redis 是基于主备异步复制协议实现的 Master-Slave 数据同步,如下图所 示,若 client A 执行 SET key value EX 10 NX 命令,redis-server 返回给 client A 成功 后,Redis Master 节点突然出现 crash 等异常,这时候 Redis Slave 节点还未收到此命令 的同步。若你部署了 Redis Sentinel 等主备切换服务,那么它就会以 Slave 节点提升为主,此时 Slave 节点因并未执行 SET key value EX 10 NX 命令,因此它收到 client B 发起的加锁的 此命令后,它也会返回成功给 client。

那么在同一时刻,集群就出现了两个 client 同时获得锁,分布式锁的互斥性、安全性就被 破坏了。 除了主备切换可能会导致基于 Redis 实现的分布式锁出现安全性问题,在发生网络分区等 场景下也可能会导致出现脑裂,Redis 集群出现多个 Master,进而也会导致多个 client 同 时获得锁。

RedLock算法

Zookeeper

ZooKeeper 是一个分布式的、提供分布式应 用协调服务的组件,它的分布式锁实现基于 ZooKeeper 的数据结构中的临时顺序节点来实现的。

zk的节点

ZooKeeper 的树形数据存储结构主要由 4 种节点构成:

持久节点

这是默认的节点类型,一直存在于 ZooKeeper 中

持久顺序节点

在创建节点时,ZooKeeper 根据节点创建的时间顺序对节点进行编号

临时节点

与持久节点不同,当客户端与 ZooKeeper 断开连接后,该进程创建的临时节点就会被删除

临时顺序节点

按时间顺序编号的临时节点。

watch

zk的watch特性可以监听节点变化

决分布式锁的羊群效应问题

在分布式锁问题中,会经常遇到羊群效应。所谓羊群效应,就是在整个分布式锁的竞争过程中,大量的“Watcher 通知”和“子节点列表的获取”操作重复运行,并且大多数节点的 运行结果都是判断出自己当前并不是编号最小的节点,继续等待下一次通知,而不是执行业 务逻辑。 这就会对 ZooKeeper 服务器造成巨大的性能影响和网络冲击。更甚的是,如果同一时间多个节点对应的客户端完成事务或事务中断引起节点消失,ZooKeeper 服务器就会在短时 间内向其他客户端发送大量的事件通知。

解决方案

监听自己前一个节点,来判断是否轮到自己获取锁:

    在与该方法对应的持久节点的目录下,为每个进程创建一个临时顺序节点。 每个进程获取所有临时节点列表,对比自己的编号是否最小,若最小,则获得锁。 若本进程对应的临时节点编号不是最小的,则继续判断 :
  1. 若本进程为读请求,则向比自己序号小的最后一个写请求节点注册 watch 监听,当 监听到该节点释放锁后,则获取锁;
  2. 若本进程为写请求,则向比自己序号小的最后一个读请求节点注册 watch 监听,当 监听到该节点释放锁后,获取锁

总结

使用 ZooKeeper的临时顺序节点+watch机制,可以完美解决设计分布式锁时遇到的各种问题,比如单点故障、不可重入、死锁等问题。虽然 ZooKeeper 实现的分布式锁,几乎能涵盖所有分布式锁的特性,且易于实现,但需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。

etcd分布式锁

etcd介绍

etcd 是一个高可用的分布式K-V系统,可以用来实现各种分布式协同服务,采用raft一致性算法,基于go语言实现,使用bbolt存储引擎,可以处理几个GB的数据。

使用场景
    k8s使用etcd来做服务发现与配置信息管理 openstack使用etcd来做配置管理和分布式锁 ROOK使用etcd研发编排引擎
etcd与Redis区别
    数据复制上Redis是主备异步复制、etcd使用的是Raft,前者可能会丢数据,为了保证读写一致性,etcd读写性能相比Redis 差距比较大数据分片上Redis有各种集群版解决方案,可以承载上T数据,存储的一般是用户数 据,而etcd定位是个低容量的关键元数据存储,db大小一般不超过8g 存储引擎和API上Redis 内存实现了各种丰富数据结构,而etcd仅是kv API, 使用的是持久化存储boltdb。

etcd实现分布式锁

加锁的过程需要确保安全性、互斥性。比如,当 key 不存 在时才能创建,否则查询相关 key 信息,而 etcd 提供的事务能力正好可以满足我们的诉 求。

事务的特性

etcd的事务由 IF 语句、Then 语句、Else 语句组成。其中在 IF 语句中,支持比较 key 的是修改版本号 mod_revision 和创建版本号 create_revision。 在分布式锁场景,你就可以通过 key 的创建版本号 create_revision 来检查 key 是否已存在,因为一个 key 不存在的话,它的 create_revision 版本号就是 0。

若 create_revision 是 0,你就可发起 put 操作创建相关 key

实现分布式锁的方案有多种,比如:

    可以通过 client 是否成功创建一个固 定的 key,来判断此 client 是否获得锁 可以通过多个 client 创建 prefix 相同,名称 不一样的 key,哪个 key 的 revision 最小,最终就是它获得锁
锁的安全性问题

相比 Redis 基于主备异步复制导致锁的安全性问题,etcd 是基于 Raft 共识算法实现的, 一个写请求需要经过集群多数节点确认。因此一旦分布式锁申请返回给 client 成功后,它一定是持久化到了集群多数节点上,不会出现 Redis 主备异步复制可能导致丢数据的问 题,具备更高的安全性。

锁的活性

etcd的Lease 就是一种活性检测机制,它提供了检测各个客 户端存活的能力。你的业务 client 需定期向 etcd 服务发送"特殊心跳"汇报健康状态,若你 未正常发送心跳,并超过和 etcd 服务约定的最大存活时间后,就会被 etcd 服务移除此 Lease 和其关联的数据。

通过 Lease 机制就优雅地解决了 client 出现 crash 故障、client 与 etcd 集群网络出现隔 离等各类故障场景下的死锁问题。一旦超过 Lease TTL,它就能自动被释放,确保了其他 client 在 TTL 过期后能正常申请锁,保障了业务的可用性。

锁的可用性

当一个持有锁的 client crash 故障后,其他 client 如何快速感知到此锁失效了,快速获得 锁呢,最大程度降低锁的不可用时间呢?

etcd的Watch特性提供了高效的数据监听能力。当其他 client 收到 Watch Delete 事件后,就可快速判断自己是否有资格获得锁,极大减少了锁的不可用时间。

实现

etcd 社区提供了一个名为 concurrency 包帮助你更简单、正确地使用分布式锁、分布式选举。

核心流程:

    首先通过 concurrency.NewSession 方法创建 Session,本质是创建了一个 TTL 为 10 的 Lease 其次得到 session 对象后,通过 concurrency.NewMutex 创建了一个 mutex 对象,包 含 Lease、key prefix 等信息 然后通过 mutex 对象的 Lock 方法尝试获取锁 最后使用结束,可通过 mutex 对象的 Unlock 方法释放锁

总结对比

四种实现方式:

数据库实现分布式锁最为简单,但是对数据库压力最大;

Redis易于理解,但如果方方面面都考虑到实现较为复杂,比如要考虑到锁到超时时间但是业务没执行完,又或者因为主备切换的过程中出现故障导致锁的不安全性,要考虑使用RedLock算法;

ZK可靠性最高,有封装好的框架,很容易实现分布式锁的 功能,并且几乎解决了数据库锁和缓存式锁的不足,但是因为频繁地添加和删除节点,性能不如Redis;

etcd可以通过它的事务特性MVCC去使用分布式锁,并且因为它基于Raft协议天然保证了安全性,Lease机制会去保证锁的活性,watch机制保证可用性,也是一种很好的实现方案;

经验分享 程序员 微信小程序 职场和发展