From 0e1f5721252c9bb600d71de98a727557d5409747 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Thu, 29 Oct 2026 04:55:48 +0800 Subject: [PATCH] =?UTF-8?q?Update=20=E5=88=86=E5=B8=83=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充用 MySQL 行锁实现分布式锁,以及锁过期时间的取舍,附录 A7。 --- notes/分布式.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/notes/分布式.md b/notes/分布式.md index f55f0c2d..a71925ea 100644 --- a/notes/分布式.md +++ b/notes/分布式.md @@ -49,6 +49,7 @@ * [A4 重复提交与唯一索引](#a4-重复提交与唯一索引) * [A5 定时任务的重复执行与加锁](#a5-定时任务的重复执行与加锁) * [A6 分片与按分片重试](#a6-分片与按分片重试) + * [A7 MySQL 行锁实现的分布式锁](#a7-mysql-行锁实现的分布式锁) * [参考资料](#参考资料) @@ -74,6 +75,17 @@ - 只能是非阻塞锁,插入失败直接就报错了,需要等待锁时只能由调用方循环重试; - 不可重入,已经获得锁的进程也必须重新获取锁。 +也可以用 InnoDB 的行锁:为每个要保护的资源预先插入一行,加锁时在事务中执行 SELECT ... FOR UPDATE,业务执行完提交事务,锁随之释放: + +```sql +BEGIN; +SELECT * FROM distributed_lock WHERE name = 'order:1024' FOR UPDATE; +-- 执行业务 +COMMIT; +``` + +其它客户端执行同一条语句时阻塞,超过 innodb_lock_wait_timeout(默认 50 秒)返回错误;加上 NOWAIT 时拿不到锁立即返回错误,相当于非阻塞锁。持有锁的进程崩溃后连接断开,事务回滚,锁立即释放:kill -9 持有锁的进程后,等待中的客户端马上拿到了锁(MySQL 8.0 实测,见[附录 A7](#a7-mysql-行锁实现的分布式锁))。代价是持有锁期间一直占用一个数据库连接和一个未提交的事务,并发高时会耗尽连接池。 + ### Redis 的 SET NX 指令 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么插入失败并返回 0,否则插入成功并返回 1。它和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否处于锁定状态。 @@ -87,7 +99,7 @@ SET lock_key unique_value NX PX 30000 还需要注意以下几点: - 释放锁:value 设置为客户端的唯一标识,释放锁时先判断 value 是否为自己设置的再删除,避免误删其它客户端的锁,判断和删除需要使用 Lua 脚本保证原子性; -- 锁续期:如果业务执行时间超过了过期时间,锁会被提前释放,可以使用后台线程定时延长过期时间,例如下文 Redisson 的看门狗; +- 锁续期:如果业务执行时间超过了过期时间,锁会被提前释放;过期时间太长,持有者崩溃后其它客户端又要等很久。过期时间通常取业务正常耗时的几倍,耗时难以估计时使用后台线程定时延长过期时间,例如下文 Redisson 的看门狗; - 主从切换:Redis 的主从复制是异步的,如果主节点加锁后还未同步到从节点就宕机,从节点晋升为主节点之后锁就丢失了。 ```lua @@ -888,6 +900,24 @@ for uid in select_ids(total, index): # SELECT id FROM h3_users WHERE id % tota 重试后 短信 10000 条,覆盖用户 10000 个,收到两条及以上的用户 0 个 ``` +### A7 MySQL 行锁实现的分布式锁 + +表 distributed_lock(name VARCHAR(64) PRIMARY KEY, owner VARCHAR(64)) 中预先插入 order:1024 这一行(实验用的表名加了前缀,下面已去掉)。一个进程在事务中执行 SELECT ... FOR UPDATE 后一直持有,另一个客户端依次尝试 NOWAIT、等待 2 秒超时、正常等待(1 秒后 kill -9 持有锁的进程): + +```python +bc.execute("SELECT * FROM distributed_lock WHERE name = 'order:1024' FOR UPDATE NOWAIT") +bc.execute('SET innodb_lock_wait_timeout = 2') +bc.execute("SELECT * FROM distributed_lock WHERE name = 'order:1024' FOR UPDATE") +``` + +```text +innodb_lock_wait_timeout = 50 +NOWAIT:0.1 ms 返回 (3572, 'Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.') +阻塞等待:2.00 s 后返回 (1205, 'Lock wait timeout exceeded; try restarting transaction') +1 s 后 kill -9 持有锁的进程 +第二个客户端等待 1.01 s 后拿到锁: (('order:1024', None),) +``` + ## 参考资料 - [Apache Dubbo: Architecture](https://dubbo.apache.org/docs/v2.7/user/preface/architecture/) @@ -897,6 +927,7 @@ for uid in select_ids(total, index): # SELECT id FROM h3_users WHERE id % tota - 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015. - [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia) - [Distributed Locks with Redis](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/) +- [MySQL 8.0: Locking Reads](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html) - [How to do distributed locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) - [ZooKeeper Programmer's Guide](https://zookeeper.apache.org/doc/current/zookeeperProgrammers.html) - [etcd: KV API guarantees](https://etcd.io/docs/v3.5/learning/api_guarantees/)