一、背景
- 在分布式系统中,一个应用部署在多台机器中,在某些场景下,为了保证数据的一致性,要求在同一时刻,同一任务只在一个节点上运行,即保证某个行为在同一时刻只能被一个线程执行。
- 在单机单进程多线程环境下,通过锁很容易做到,比如
mutex
、spinlock
、信号量
等。 - 在多机多进程环境下,就需要使用
分布式锁
来解决了。- 分布式场景:我们的应用由多个节点构成,这些节点可能分布在不同的机器中,也有可能分布在不同的网络环境中,通常这些进程之间通过
socket
进行通信。
- 分布式场景:我们的应用由多个节点构成,这些节点可能分布在不同的机器中,也有可能分布在不同的网络环境中,通常这些进程之间通过
二、分布式锁
- 是什么类型的锁 ?
- 在分布式场景中实现互斥类型的锁。
- 互斥类型:同一时刻只允许一个执行体进入临界资源。
- 在分布式场景中实现互斥类型的锁。
- 解决了什么问题 ?
- 在分布式场景中,同时只允许一个节点执行某类任务。
- 锁 = 资源 + 行为:
- 资源:
- 要记录进程的全局唯一 ID。
- 放在
数据库
、zookeeper
或etcd
中,可以让所有的执行体访问。
- 行为:加锁、解锁。(以网络通信的方式)
- 加锁:把当前进程的唯一标识打到当前数据库的某一个字段中,作为一个标记,说明当前进程持有锁。
- 加锁的方式:原来没有标记,现在打上标记了。
- 解锁:谁加的锁,谁释放锁,也就是加锁对象和解锁对象必须是同一个对象,除了因为网络异常而造成的锁超时情况。
- 解锁的方式:持锁方去清除标记,比如置为 0,这样其他的进程才能去加锁。
- 加锁:把当前进程的唯一标识打到当前数据库的某一个字段中,作为一个标记,说明当前进程持有锁。
- 资源:
三、分布式锁特性
- 互斥性。
- 锁打上标记:加锁。
- 锁取消标记:解锁。
- 标记:执行体的唯一标识,可以通过雪花算法生成。
- 锁超时。
- 在分布式场景中,允许一个进程退出,并希望其他的进程能够继续工作。
- 当持有锁的进程想要解锁的时候,由于某些原因,比如进程宕机、网络异常,导致无法清除数据库中的标记,也就是持锁的进程没有能力去释放锁了,那么我们应该提供一种机制,让数据库自动地去释放锁 → 时间一到,数据库自动地释放锁。
- 可用性。
- 合理时间内得到合理的回复。
- 实现:
- 计算型:开多个备份点。
- 存储型:
- 多个备份点。
- 主从切换。
- 容错性。
- 一致性来解决(半数以上)。
- raft 一致性算法。
- redlock。
高可用 = 可用性 + 容错性
。
- 一致性来解决(半数以上)。
四、分布式锁类型
- 重入锁和非重入锁。
- 重入锁:
- 允许同一个线程多次获取同一把锁,而不会导致死锁。当一个线程持有锁时,它可以再次获取相同的锁而不被阻塞。
std::recursive_mutex
。
- 非重入锁:
- 不允许同一个线程在持有锁的情况下再次获取相同的锁,会导致死锁。
std::mutex
。
- 重入锁:
- 公平锁和非公平锁。
- 公平锁:排队,对应互斥锁。
- 非公平锁:轮询,对应自旋锁。
五、实现分布式锁
- 基于中间件来实现 → 所有的节点都能访问到。
- 资源存储在中间件中。
- 加锁、解锁行为基于中间件的特性来实现。
MySQL 实现分布式锁
- 锁的存储:表。
- 获取锁和释放锁:字段。
- 互斥语义:
- 唯一性约束:
unique key
、primary key
。 - 利用 innodb 中的 S 锁和 X 锁互斥。
select * from lock where ... lock in share mode; update lock set ... where ...
- 唯一性约束:
- 锁超时:
- 表增加一个字段:加锁时间戳。
- 另起进程(超进程),定时检测是否超时。
- 其他进程怎么获取锁:主动探寻,实现定时器 → MySQL 只能实现非公平锁。
- 递归锁:表中增加一个字段 count。
- 创建表。
DROP TABLE IF EXISTS `dislock`; CREATE TABLE