一、基本概念
分布式锁,是单机锁的一种扩展,主要是为了锁住分布式系统中不同机器代码的物理块或逻辑块。以此保证不同机器之间的逻辑一致性。
二、一个简单的案例
对DB写操作的双检锁案例
-
伪代码如下
if (可以插入一条数据) { lock { if (可以插入一条数据) { // 插入一条数据 } } }
- 上面代码中的lock如果是单机情况下的锁的话,在一台服务器多线程情况下是没有问题的。但是如果是分布式的情况下,单机的锁只能锁住一台服务器的物理代码块,是无法防住其他机器产生的脏数据的。
- 分布式情况下这个lock就需要换成分布式锁以保证数据一致性。
三、分布式锁实现原理
所需的依赖
- 一个拥有强一致性的服务发现存储仓库。(保证数据一致性)
- 一个具有高可用性的服务发现存储仓库。(保证服务的稳定性)
- 在这里我们使用etcd作为服务发现存储仓库。
具体实现原理
我们本次所使用的etcd的能力
- etcd节点的保持独占能力。(同一时间只有一台服务器可以拥有节点)
- 节点事件监听能力。(可以在节点被释放/占有时通知观察者-服务器)
- 节点主动释放能力。(服务器可以主动释放拥有节点)
- 节点超时自动释放能力。(超时自动释放节点)
在代码中,需要准备以下几个内容
- etcd长监听。(监听etcd节点,在节点变更时作出响应)
- 事件队列。(存放争夺etcd节点的方法)
- 争夺锁事件。
- 等待锁超时事件。(没画在流程图中,在争夺锁事件入队列时注册,持有锁时注销)
- 持有锁超时事件。(没画在流程图中,在持有锁时注册,在释放锁时注销)
- 释放锁事件。
基本代码流程图
-
正常代码流程图(markdown画的,有点丑)
st=>start: 项目启动 watcher=>operation: 监听etcd ed=>end: 等待请求 st->watcher->ed
st=>start: 开始执行lock ed=>end: 结束 watcher=>operation: 监听etcd hasEvent=>condition: 事件队列为空 pushEvent=>operation: 争夺锁事件 入等待队列 watcherEvent=>operation: etcd节点超时事件和释放事件 popEvent=>operation: 争夺锁事件 出事件队列 execute=>operation: 执行被锁住的代码块 fight=>condition: 争夺锁失败 unlock=>operation: 释放锁 st->fight fight(no)->execute->unlock->ed fight(yes)->pushEvent->watcherEvent->popEvent->fight
-
事件细节伪代码
-
准备一个清空所有相关事件的万能方法
假设eventQueue是这种业务对应的事件队列 假设fightEvent是该次执行的争夺锁事件 假设clear为清除超时事件的方法 假设holdEvent为持有锁超时事件 假设waitEvent为等待锁超时事件 假设unLockEvent为释放锁事件 // 清空所有事件 - clearAll eventQueue.remove(fightEvent) clear(waitEvent) clear(holdEvent)
-
监听etcd的两个事件
// 当etcd锁超时事件或etcd主动释放事件发生时 // 争夺锁事件 出事件队列 fightEvent = eventQueue.pop // 执行争夺锁事件 执行 fightEvent
-
争夺锁事件
// 争夺锁事件 执行 etcd争夺锁方法 if (抢到锁了) { // 清空所有事件 执行 clearAll // 设置持有锁超时事件 设置 holdEvent } else { // 争夺锁事件如果是二次入队列,建议到队列头,而不是到队列尾 eventQueue.push(fightEvent) }
-
等待锁超时事件(用时间轮延时执行)
// 清空所有事件 执行 clearAll // 抛出异常 throw new Exception
-
持有锁超时事件(用时间轮延时执行)
// 执行释放锁事件 执行 unLockEvent
-
释放锁事件
// 释放锁事件 执行 etcd释放锁方法 // 清空所有事件 执行 clearAll
-
-
备注
以上代码可以用有限状态机设计模式来设计业务
-
图解分布式服务器与etcd集群交互
- 项目启动时初始化,服务器A、B、C是一个分布式系统(不考虑哪台为master)
- 同时发来10个请求,此时经过算法分配假设变成了这样
- 开始争夺锁时,10个线程同时向etcd争夺锁
- 假设请求2争夺成功,那么第一次竞争完毕后除了请求2,其他请求都进入了各自的事件队列等待etcd释放锁的通知
- 当请求2执行完代码块后,向etcd发送释放锁请求
- etcd收到释放锁请求,将节点删除,触发compareAndDelete事件(释放锁事件)
- 三台服务器接收到释放锁的信息,各自让事件队列头的请求向etcd发送争夺锁请求
- 锁抢完后,重复4->6的行为
- 项目启动时初始化,服务器A、B、C是一个分布式系统(不考虑哪台为master)
-
补充说明
- etcd还有一个功能是控制时序,这样的话就可以将每个请求抢锁的行为控制到只剩一次,业务执行的顺序由etcd控制,不过我没有试过,有兴趣的同学可以试一试。
- 之前基于分布式锁的原理还写了一个分布式缓存锁,是通过锁缓存来防止缓存击穿的,有空的时候补上。感谢各位同学看我的文章看到此处。