文章目录
- 写在前面
- 分布式锁的三个属性
- 实现容错性
-
- [方法一:基于多个 Redis 节点实现分布式锁](#方法一:基于多个 Redis 节点实现分布式锁 "#_Redis__20")
-
- [问题一:进程可能会被挂起,直到锁的 TTL 过期](#问题一:进程可能会被挂起,直到锁的 TTL 过期 "#_TTL__33")
- 问题二:墙上时钟在分布式系统中不可靠
- ⽅法⼆:复制(Replication)
- [Chubby 设计与实现](#Chubby 设计与实现 "#Chubby__91")
- [ZooKeeper 实现分布式锁](#ZooKeeper 实现分布式锁 "#ZooKeeper__104")
- [etcd 实现分布式锁](#etcd 实现分布式锁 "#etcd__115")
- [分布式互斥问题(Distributed mutual exclusion)](#分布式互斥问题(Distributed mutual exclusion) "#Distributed_mutual_exclusion_120")
- 总结
- 复盘
写在前面
分布式锁的应用场景就是高并发,高并发下如果锁出了任何问题,就可能会导致脏数据的产生,那么,用redis做分布式锁真的安全吗?
分布式锁的三个属性
- 互斥(Mutual Exclusion):同⼀时刻只有⼀个客户端持有锁
- 避免死锁(Dead lock free):设置锁的存活时间(Time To Live,TTL)
- 容错(Fault tolerance):避免单点故障,锁服务要有⼀定容错性
分布式锁就⼀定要实现这三个属性吗?
未必!
如果你的业务⽆关紧要,如果你的业务是可以挂掉的内部系统,如果你的业务可以接受出错的时候,直接返回错误给⽤户,那⼀个单节点 Redis 或关系型数据库的分布式锁就能满⾜你的需求。
如果你的业务不允许随意宕机,那我们就要来好好讨论容错性了。
实现容错性
讨论"分布式",意味着可能会发⽣各种各样的错误,Google 公布的数据显示:
参考引⽤:research.google.com/people/jeff...
方法一:基于多个 Redis 节点实现分布式锁
加锁:
1.依次对多个 Redis 实例进⾏加锁,使⽤单实例 Redis 的加锁命令;
2.每次获取锁的超时时间远⼩于 TTL,超时则认为失败,继续向下⼀个节点获取锁;
3.计算总消耗时间,只有在超过半数节点都成功获取锁,并且总消耗时间⼩于 TTL,才认为成功持有锁;
4.成功获取锁后,要重新计算 TTL = TTL - 总消耗时间;
5.如果获取锁失败,要向所有 Redis 实例发送解锁命令。
解锁:
1.删除所有实例中的 key
也就是我们说的-红锁(RedLock),实现起来其实挺麻烦的,但是,实现了RedLock又引发了其他的问题。
问题一:进程可能会被挂起,直到锁的 TTL 过期
当客户端1获取到第一个锁之后,程序恰好进行GC了stop-the-world(或者其他原因导致的挂起),而这个时间恰好又超过了过期时间,就会造成客户端2获取不到锁。
该问题可以延长TTL,但是治标不治本,仍有系统挂起超时的隐患。
问题二:墙上时钟在分布式系统中不可靠
计算机科学家类Leslie Lamport的逻辑时钟论文中,提出了物理时钟在分布式系统中并不可靠,不能用物理时钟来判断事件的先后顺序。
- 分布式集群靠 NTP 时钟同步,但仍未能保证每台机器⾛时相同
- 时间戳不可靠:aphyr.com/posts/299-t...
- Google Spanner 设计了⼀套复杂的时间机制(TrueTime)来实现强⼀致性
时钟漂移问题是真实存在的
也就是说,RedLock虽然避免了单点故障,但是有着一定程度上的时间不一致问题,仍会导致高并发下锁的不一致问题。
⽅法⼆:复制(Replication)
- 不依赖多个 Redis 节点,数据存储服务⾃身保证容错性
- 复制有很多种⽅法,但要保证数据强⼀致性,即 CAP 定理中的 CP
- 提供⼀个强⼀致、能够容错⼀定数量节点的分布式锁服务
主从异步复制
1.主节点收到写请求,执⾏完毕
2.主节点响应客户端
3.主节点复制到从节点
如果复制之前主节点宕机、损坏------会造成数据丢失。
主从同步复制
1.主节点收到写请求,执⾏完毕
2.主节点复制到从节点
3.主节点收到所有从节点的确认信息,响应客户端
如果任意⼀个节点宕机、损坏、I/O 阻塞------会造成系统可⽤性降低(但数据没丢)
主从半同步复制
1.主节点收到写请求,执⾏完毕
2.主节点复制到从节点
3.主节点收到⼀个从节点的确认信息,响应客户端
4.其余从节点继续复制数据
如果从节点 2 因为某种原因写失败------会造成从节点数据不⼀致
基于 Quorum 的数据冗余复制
●W + R > N 且 W > N/2
●需要解决冲突
●需要数据修复
●案例:Dynamo、Cassandra
只能实现最终⼀致性
分布式共识算法 Paxos 或 Raft
- 实现强⼀致性(线性⼀致性)
- 容忍不超过半数节点故障
- 案例:Spanner、etcd、CockroachDB......
缺点也不是没有,就是⼯程上⽐较难实现
Chubby 设计与实现
系统架构
- 如 Chubby 由⼀个 Master 和多个副本组成,只有 Master 能够处理请求和读写⽂件,副本通过 Paxos 算法复制 Master 来实现容错
- 提供类 UNIX ⽂件名称:/is/foo/wombat/pouch,可以降低培训难度
- 每个节点包含⼀些 metadata,存储单调递增的编号、访问控制 ACL 策略等
其它系统特性,可以解决乱序到达等问题
- sequencer:引⼊序列号,可以通过检查序列号是否合法来避免乱序到达问题
- 事件:⽀持事件监听,例如:⽂件内容改变、⼦节点添加、锁的获取
- 缓存:Chubby 通过客户端内存缓存来减少读流量
- session 和 keepalive
- fail-overs:能够处理 Master 宕机或重新选举
ZooKeeper 实现分布式锁
1.调⽤ create(),并设置 sequence 和 ephemeral 标志;
2.调⽤ getChildren() 获取⼦节点列表,不要设置 watch 标志(可以避免惊群效应);
3.检查⼦节点列表,如果步骤 1 创建的节点的序列号最⼩,则客户端持有该分布式锁,结束;
4.如果创建的序列号不是最⼩的,则客户端调⽤ exist(p, watch=true) 监听⽐当前序列号⼩⼀位的节点(记为 p),当 p 被删除时收到事件通知;
5.如果 exist() 返回 null,即前⼀个分布式锁被释放了,转到步骤 2;否则需要⼀直等待步骤 4 中 watch 的通知。
(惊群效应------持有锁的线程释放锁,剩下的线程争先抢后的竞争锁,导致cpu异常偏高)
参考引⽤:zookeeper.apache.org/doc/r3.7.0/...
etcd 实现分布式锁
共识算法 + TTL + 序列号 + watch 机制
参考引⽤:github.com/etcd-io/etc...
分布式互斥问题(Distributed mutual exclusion)
其他分布式算法简单介绍
● Lamport
● Ring-based
● Ricart-Agrawala
● Maekawa
总结
复盘
1.系统设计本质上是在做取舍(trade-off),没有完美的架构,更多的情况是要在⼏个互相竞争的问题之间进⾏权衡。有时候,基于单节点 Redis 实现的分布式锁就能满⾜需求;
2.微信实现了⾃⼰的 Chubby,Facebook 实现了 Delos,但实现⼀个共识协议(⽆论是 Paxos 还是 Raft)是颇具难度的,⽆⾮必要,不要轻易造轮⼦。