mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-29 23:23:43 +00:00
Update 分布式.md
补充用 MySQL 行锁实现分布式锁,以及锁过期时间的取舍,附录 A7。
This commit is contained in:
+32
-1
@@ -49,6 +49,7 @@
|
||||
* [A4 重复提交与唯一索引](#a4-重复提交与唯一索引)
|
||||
* [A5 定时任务的重复执行与加锁](#a5-定时任务的重复执行与加锁)
|
||||
* [A6 分片与按分片重试](#a6-分片与按分片重试)
|
||||
* [A7 MySQL 行锁实现的分布式锁](#a7-mysql-行锁实现的分布式锁)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -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/)
|
||||
|
||||
Reference in New Issue
Block a user