
水手辛巴德
2022/10/02阅读:47主题:橙心
程序员八股文之分布式篇
聊聊分布式事务
在微服务系统架构中,服务之间通过RPC远程调用,因而系统之间的通信可靠性显得很重要。
分布式事务就是为了保证不同服务下的数据库数据的一致性,简单说就是不可靠通信下实现事务的特性(ACID)
常见的问题场景
机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠TCP、存储数据丢失、其他异常等。
2PC模式
介绍
2PC,指的是准备阶段(Prepare Phase)和提交阶段(Commit Phase);当事务协调者获取到多个服务都已经准备好后,可以开始提交阶段的处理。如下图:

总结
-
使用分布式事务成本低,适用于数据库层面 -
性能不太理想,无法满足高并发场景(需要等待所有服务都准备好才可提交事务) -
MySQL的XA实现没有记Prepare阶段日志,在主备切换后会导致主库与备库数据不一致 -
XA应用场景有限,许多NOSQL不支持XA -
同步阻塞问题。2PC两个阶段中,参与者和协调者通信都是同步的,导致整个事务的长时间阻塞,造成系统性能下降
3PC(2PC的优化版本)
介绍
在2PC模式存在的问题上做了改进,在协调节点和事务参与者都引入了超时机制(若某个服务触发超时,该服务会自动提交)
第一阶段的Prepare阶段分成了两步,canCommit和preCommit。如下图:

总结
-
2PC、3PC都属于强一致性事务 -
超时后的强制提交会导致数据不一致问题,解决方案如下: -
增加异步补偿任务 -
数据监控 -
人工补录
-
Seata分布式事务管理
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata术语
TC-事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚
TM-事务管理器
定义全局事务的范围,开始全局事务、提交或回滚全局事务
RM-资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务状态,并驱动分支事务提交或回滚。
AT模式
整体机制
是对两阶段提交协议的演变而来,如下:
-
一阶段,业务数据和回滚日志记录在同一个本地事务中提交,释放本地资源和连接资源 -
二阶段,提交异步化,非常快速的完成;回滚方式通过一阶段回滚日志(UNDO_LOG表)进行反向补偿。

实现原理
-
通过反向SQL实现事务数据回滚 -
需要在数据库额外附加UNDO_LOG表
处理过程
-
一阶段prepare行为:在本地事务中,一并提交业务数据更新和响应回滚日志记录 -
二阶段commit行为:马上成功结束,自动异步批量清理回滚日志 -
二阶段rollback行为:通过回滚日志,自动生成补偿操作,完成数据回滚
特点
-
性能高 -
柔性事务,存在数据不一致的中间状态 -
难易程度:简单,靠Seata自己解析反向SQL并回滚 -
使用要求:创建UNDO_LOG表、所有服务与数据库必须要自己拥有管理权 -
最好都是统一使用MySQL -
应用场景:高并发的互联网应用,允许数据出现短期内的不一致(可通过补录保证最终一致性)
TCC模式
TCC模式即try(prepare行为)、confirm(commit行为),cancel(rollback行为),需要自定进行事务代码编写!
处理流程
所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。该模式不依赖底层数据资源的事务支持。
-
一阶段prepare行为:调用自定义的prepare逻辑 -
二阶段commit行为:调用自定义的commit逻辑 -
二阶段rollback行为:调用自定义的rollback逻辑
特点
-
性能好 -
柔性事务,存在数据不一致的情况 -
实现复杂,需要自己手动编写事务回滚逻辑,不太推荐! -
使用要求:所有服务与数据库必须要自己拥有管理权;支持异构数据库 -
应用场景:高并发互联网应用,允许数据短期内不一致
SAGA
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

适用场景
-
业务流程长,流程多 -
参与者包含其他公司或遗留系统服务 -
调用支付宝接口
总结
-
优点:一阶段提交本地事务无锁,高性能;事件驱动架构,参与者可异步执行,高吞吐;补偿服务易于实现 -
缺点:不保证隔离性;实现复杂,提交与回滚需要程序员自己编排;存在数据不一致的中间状态
XA模式

特点
-
性能低 -
强一致性(刚性事务) -
难易程度:简单,基于数据库自带特性实现 -
使用要求:使用支持XA方案的关系型数据库,如MySQL等 -
应用场景:金融行业、并发量不大但数据很重要的项目。
聊聊分布式锁
微服务架构中为了保证数据的最终一致性,就需要用到分布式事务、分布式锁等技术方案来解决;
使用分布式锁目的是为了保证一个方法在同一时间内只能被一个线程执行
分布式锁特征
-
互斥性,任意时刻,只有一个线程持有锁 -
锁超时释放,锁超时了,可释放,防止资源浪费及死锁 -
可重入性,同一个线程可重复获取当前持有的锁 -
高可用、高性能,加锁、解锁开销尽可能低,且保证高可用(避免分布式锁失效) -
安全性,锁只能被持有的线程删除,不能被其他线程删除
解决方案
基于redis方案
使用redis原子性命令:SET EX/PX NX + 唯一值()
-
NX:表示key不存在时,才能set成功 -
EX(seconds):key过期时间,单位秒 -
PX(millisseconds):key过期时间,单位为毫秒
使用Redisson方案
redisson的所有方法底层都是lua脚本实现,保证了原子性操作;加锁默认的过期时间为30s,避免了死锁的情况
加锁
-
阻塞式等待,默认加锁时间为30s(看门狗默认时间) -
自动续期,若业务超长,运行期间自动给锁续期30s,不必担心业务时间过长,锁自动过期被删除的情况 -
解锁时间>业务执行时间
读写锁
-
写锁(排它锁) -
读锁(共享锁)
信号量
-
阻塞方法,可做限流场景使用 -
acquire() -
release() -
tryAcquire()
使用zookeeper方案
zookeeper有四种节点类型,包括持久节点、持久顺序节点、临时节点和临时顺序节点。可以利用zookeeper的临时顺序节点特性实现分布式锁
思路
当客户端对某个方法加锁时,在 ZooKeeper 中该方法对应的指定节点目录下,生成一个唯一的临时有序节点。
判断是否获取锁,只需要判断持有的节点是否是有序节点中序号最小的一个,当释放锁的时候,将这个临时节点删除即可,这种方式可以避免服务宕机导致的锁无法释放而产生的死锁问题。
使用场景
-
库存超卖、扣减库存 -
缓存击穿、缓存雪崩 -
秒杀
分布式锁方案总结
排序从低到高
-
难易程度:缓存>zookeeper -
性能角度:zookeeper<缓存 -
可靠性:缓存 < zookeeper
聊聊Redis和MySQL缓存一致性问题
常见方案
双写模式
-
写DB、写cache
失效模式
-
写DB、删cache
总结
-
删除缓存或者更新数据库失败而导致的数据不一致,可以使用重试机制(MQ)确保删除/更新操作成功 -
大多数场景,Redis只用来作为只读缓存。针对只读缓存,我们既可以先删除缓存在更新数据库或者先更新数据库后删除缓存都行 -
建议优先使用先更新DB后删除缓存 -
先删除缓存值,在更新数据库,有可能导致请求命中key失败,直接到数据库,从而给数据库带来压力 -
若业务层要求数据强一致性时,我们需要在更新数据库时,先在Redis缓存客户端暂存并发读请求,等数据库更新完,缓存值删除后,再读取数据,从而保证数据一致性。
-
补充知识
-
刚性事务:遵循ACID特性,例如:2PC、3PC -
柔性事务:遵循BASE理论,最终一致性
作者介绍
