分布式锁 🔒
MySQL 分布式锁
表记录
CREATE TABLE `database_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource` int NOT NULL COMMENT '锁定的资源',
`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
PRIMARY KEY (`id`),
UNIQUE KEY `uiq_idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
获取锁:
INSERT INTO database_lock(resource, description) VALUES (1, 'lock');
释放锁的时,可以删除这条数据:
DELETE FROM database_lock WHERE resource = 1;
缺点:
- 这种锁没有失效时间,一旦释放锁的操作失败就会导致锁记录一直在数据库中,其它线程无法获得锁。这个缺陷也很好解决,比如可以做一个定时任务去定时清理。
- 这种锁的可靠性依赖于数据库。建议设置备库,避免单点,进一步提高可靠性。
- 这种锁是非阻塞的,因为插入数据失败之后会直接报错,想要获得锁就需要再次操作。如果需要阻塞式的,可以弄个
for
循环、while
循环之类的,直至INSERT
成功再返回。 - 这种锁也是非可重入的,因为同一个线程在没有释放锁之前无法再次获得锁,因为数据库中已经存在同一份记录了。想要实现可重入锁,可以在数据库中添加一些字段,比如获得锁的主机信息、线程信息等,那么在再次获得锁的时候可以先查询数据,如果当前的主机信息和线程信息等能被查到的话,可以直接把锁分配给它。
悲观锁
我们必须关闭 MySQL 数据库的自动提交属性,因为 MySQL 默认使用autocommit
模式,也就是说,当你执行一个更新操作后,MySQL 会立刻将结果进行提交。
mysql> SET AUTOCOMMIT = 0;
获取锁:
SELECT * FROM database_lock WHERE id = 1 FOR UPDATE;
释放锁:
COMMIT;
MySQL InnoDB 引起在加锁的时候,只有明确地指定主键(或索引)的才会执行行锁 (只锁住被选取的数据),否则 MySQL 将会执行表锁(将整个数据表单给锁住)。在悲观锁中,每一次行数据的访问都是独占的,只有当正在访问该行数据的请求事务提交以后,其他请求才能依次访问该数据,否则将阻塞等待锁的获取。
Redis 分布式锁
Redis 实现的分布式锁,请阅读 Redis 分布式锁
ZooKeeper 分布式锁
最简单锁实现
- lock: 创建一个带有 EPHEMERAL 标志的 designated znode,创建成功,锁成功;否则,客户端可以设置上 watch 标志,当当前的 leader 挂掉的时候,即时地获得通知。
- unlock: 客户端显示地删除 znode 的时候,或者客户端挂掉的时候,锁自动释放。znode 被删除的时候,其他想要获取锁的客户端可以获取到通知。
缺点:
- herd effect (羊群效应):如果有许多客户端等待获取锁,那么当锁被释放时,他们都会争夺锁,即使只有一个客户端可以获取锁。
- 只实现了独占锁。
没有羊群效应的锁实现
顺序临时节点:Zookeeper 提供一个多层级的节点命名空间(节点称为 Znode
),每个节点都用一个以斜杠(/)分隔的路径来表示,而且每个节点都有父节点(根节点除外),非常类似于文件系统。节点类型可以分为持久节点(PERSISTENT
)、临时节点(EPHEMERAL
),每个节点还能被标记为有序性(SEQUENTIAL
),一旦节点被标记为有序性,那么整个节点就具有顺序自增的特点。一般我们可以组合这几类节点来创建我们所需要的节点,例如,创建一个持久节点作为父节点,在父节点下面创建临时节点,并标记该临时节点为有序性。
Lock:
n = create(l + “/lock-”, EPHEMERAL|SEQUENTIAL)
C = getChildren(l, false)
if n is lowest znode in C, exit
p = znode in C ordered just before n
if exists(p, true) wait for watch event
goto 2
Unlock:
delete(n)
首先,我们需要建立一个父节点,节点类型为持久节点(PERSISTENT
) ,每当需要访问共享资源时,就会在父节点下建立相应的顺序子节点,节点类型为临时节点(EPHEMERAL
),且标记为有序性(SEQUENTIAL
),并且以临时节点名称 + 父节点名称 + 顺序号组成特定的名字。在建立子节点后,对父节点下面的所有以临时节点名称 name 开头的子节点进行排序,判断刚刚建立的子节点顺序号是否是最小的节点,如果是最小节点,则获得锁。如果不是最小节点,则阻塞等待锁,并且获得该节点的上一顺序节点,为其注册监听事件,等待节点对应的操作获得锁。当调用完共享资源后,删除该节点,关闭 zk,进而可以触发监听事件,释放该锁。
一般我们还可以直接引用 Curator
框架来实现 Zookeeper 分布式锁,代码如下:
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
try {
// do some work inside of the critical section here
} finally {
lock.release();
}
}
读/写锁
Write Lock:
n = create(l + “/write-”, EPHEMERAL|SEQUENTIAL)
C = getChildren(l, false)
if n is lowest znode in C, exit
p = znode in C ordered just before n
if exists(p, true) wait for event
Read Lock:
n = create(l + “/read-”, EPHEMERAL|SEQUENTIAL)
C = getChildren(l, false)
if no write znodes lower than n in C, exit
p = write znode in C ordered just before n
if exists(p, true) wait for event
goto 3
对比 Redis 锁
- Redis AP,ZooKeeper CP
- Redis 非公平,ZooKeeper 可以公平