这是一个重构系统新老系统同时服役切量迁移的业务场景,老系统正在线上服役为各业务方提供接口查询功能,新系统重构完成后需要对接接入,调用流量要陆续从老系统切换到新系统,最终老系统迭代下线。
需要解决的分布式事务问题就是,双系统的数据是异构、分散的,线上不可停量,需要陆续切换完成,因此需要事先将老库存量数据洗入新库,此过程中增量数据写入是存在的,但是最终新老库是相对一致和统一的,该场景需要解决的是数据双库的双写问题。
系统 | 写操作 | 读操作 | 调用方 |
---|---|---|---|
老系统 | 异步写(MQ) | 读(冗余查) | 历史存量渠道 |
新系统 | 同步写 | 读 | 新业务渠道/历史存量切换渠道 |
A: 同步写辛库,MQ异步写老库
- 本地事务 + 消息事务表
业务数据持久化开启数据库本地事务,该事务中记录业务数据
和同步状态
信息- 确保本地事务一定成功,不保证异步MQ事务
数据库事务成功后再发送写老库的MQ,保证本地事务一定完成才会触发MQ发送,这样确保本地事务一定成功,MQ可能成功也可能失败- 重试MQ事务状态,最终一致
如果MQ事务失败,通过定时任务轮询进行重发驱动,最终一致
A: 增加冗余查询
增加冗余对另一库的冗余查询进行Double Check。由于调用方几乎不可能同时使用新老系统做业务,因此延迟时间取决于MQ异步消费写的速度,如果场景比较复杂要确保绝对一致可以增加该处理方式
A: 业务时间 + 业务ID
- (1)同一个
业务ID
代表一个同一笔业务,可以依此进行业务防重处理
消息业务ID 业务时间 消费时间 处理逻辑 ID T T 执行 ID T T+1 防重不处理
- (2)同一个
业务ID
的基础上增加业务时间
,可以依此保证业务数据的实时刷新业务时间、消费时间同序
消息业务ID 业务时间 消费时间 处理逻辑 ID T T 执行 ID T+1 T+1 执行(覆盖) 业务时间、消费时间乱序
消息业务ID 业务时间 消费时间 处理逻辑 ID T T+1 抛弃(业务时间较早) ID T+1 T 执行
- (3)不同
业务ID
的基础上增加业务时间
,可以依此保证不同业务数据的按照业务时间刷新
消息业务ID 业务时间 消费时间 处理逻辑 ID T+1 T+1 执行(业务时间较新) ID+X T T 抛弃(业务时间较早)
这是一个认证系统以来外部核验系统进行用户身份鉴权的场景,即认证系统记录认证发起记录,并请求到外部的核验系统发起一笔核验请求,用户在核验系统确认后核验结果返回到认证系统确认用户的真实数据状态。
该流程中认证系统是一个本地系统,存放用户发起的认证流水信息和核验状态,依赖外部核验系统返回该笔认证记录的核验状态,由于核验过程是异步的,用户可以选择任意时间完成或者永远不完成,需要保证每次认证流程只有一笔业务发起,而且需要根据业务时间进行核验流程的超时进行强制取消或者补偿查询对齐核验状态等,需要解决的分布式事务是认证流水、核验结果的一致性。
数据 | 系统 | 事务 | 正向事务 | 逆向事务 |
---|---|---|---|---|
认证流水数据 | 本地系统 | 本地事务 | 本地事务2PC保证 | 定时任务,超时关闭流程 |
核验状态数据 | 外部系统 | 分布式事务 | 联同本地事务提交 / 定时任务驱动补偿提交 | 外部系统控制 |
A:通过定时任务补偿触发二次提交,只要外部事物提交一直处于未成功,便一直会被重试提交,直到成功
A:以本地事务认证流水的结果为准。本地事务是通过定时任务进行补充提交+外部事务状态核验查询的,即时在临界点外部事务完成了,但是超过了业务处理时间已经关闭,不会再补充修改,这也是根据业务场景做的取舍,用户可以再次发起新流程进行核验
该业务是在分库分表场景下,需要一个切分键字段进行数据分片路由,一般我们ToC业务的话会使用userId这样的字段进行切分。然而水平切分数据带来了数据库读写性能的同时也存在一个问题,那就是查询必须携带切分键才可以完成,因为要依赖它进行数据路由查询,比如在以userId进行数据路由切分时,如果想按照用户的身份证、姓名等进行匹配查询是无法实现的,因为我们事先是不知道userId、身份证、姓名的等值匹配关系。一般而言,我们可以通过HBase + ES的方案进行解决,即监听不同库的binlog日志,将其按照时间进行排序切分汇入HBase表中,加入要检索的索引到ES中解决分库分表下数据切片产生的聚合问题。
基于以上场景,除了通过HBase+ES实现之外,还可以通过切分键与非切分键进行数据绑定解决,但是由于切分键与非切分键的路由一般不一致,数据会分散到不同库,因此无法做本地事务,这是我们需要解决的分布式事务问题。
字段 | 处理方式 | 路由键 | 存储格式 |
---|---|---|---|
切分键 | DB同步写,按照切分键路由 | 切分键 | <切分键,非切分键A,非切分键B,非切分键C> |
非切分键 | MQ异步写,按照非切分键路由冗余存储,保存与切分键数据关系 | 非切分键 | <非切分键A,切分键> <非切分键B,切分键> <非切分键C,切分键> |
A:事务处理开始阶段通过Redis记录事务开启的分布式锁标识,当其他存在冲突的同业务在该事务的处理过程有查询时,通过判断分布式锁标识是否存在来判断事务的开启,若存在则异步等待并发起间隔查询直到事务超时,若事务超时周期内反复查询到则返回,否则根据事务处理后结果返回
A:相当于两个2PC事务协作。
- 1.一阶段DB、MQ同时prepare(并行)
- 2.二阶段DB先commit,成功后MQ再commit(串行)
流程 阶段 操作 Ack反馈 持久化 是否结束 分布式事务成功 正向流程 Prepare DB prepare
MQ prepareYes No No No 正向流程 Commit DB commit
MQ commitYes Yes Yes Yes - - - - - - 异常流程 Prepare DB 或 MQ 异常 No No Yes No 异常流程 Commit DB提交异常 No No Yes No 异常流程 Commit MQ提交超时
回调本地事务状态,本地事务成功则提交MQ事务Yes Yes Yes Yes 异常流程 Commit MQ提交超时
回调本地事务状态,本地事务失败则取消MQ事务Yes No Yes No
A:不会,而且要避免这种情况发生。两个2PC事务的prepare准备阶段可以同时发起,但在commit提交阶段要先保证本地数据库事务成功之后再进行MQ事务消息的commit,也就是在commit阶段是存在依赖关系、串行化之行的
A:prepare阶段失败、本地事务commit阶段则均不会持久化;当prepare阶段成功、本地事务commit成功,此时MQ的commit阶段异常,则依赖MQ事务消息的超时commit机制触发回调本地事务状态接口方法来决定MQ的二阶段是commit还是cancel
TCC事务其实主要包含两个阶段:Try阶段、Confirm/Cancel阶段。
从TCC的逻辑模型上我们可以看到,TCC的核心思想是,Try阶段检查并预留资源,确保在Confirm阶段有资源可用,这样可以最大程度的确保Confirm阶段能够执行成功。这里的资源可以是DB
,或者MQ
,RPC
,只要是分布式环境中的事务载体就可以,而且需要这些分布式事务的参与者具备正逆向条件,比如DB、MQ的事务可以支持2PC,可以根据协调者对其进行事务提交或者取消操作,RPC比如账户服务可以支持正向增加也可以支持逆向减少,除此之外,DB、MQ要自身支持事务的ACID,这是参与分布式事务的原子性保证,RPC底层也是DB,这里可以等同理解。以上参与者具备参与分布式事务的基本条件后便可以进行整合和使用,业务流程的驱动和事务的完整性由中间协调者来操作和推进。
这是一个电商系统比较经典的下单、扣款、发货流程,在下单之前会通过一系列商品在架状态、库存数量、购买限制等有效性过滤,然后在业务系统中进行一个商品兑换的场景。
由于是商品兑换,对于用户和系统本身来说是这个过程一个原子性的、一键完成的操作,因此下单+动账+发货是一个现实存在的分布式事务。这里假设订单数据和动账数据在一个本地数据库事务中,持久化开启数据库本地事务,该事务中记录订单生成数据
和动账数据
信息,以及发货状态
的信息记录。这里需要解决的分布式事务是订单数据、动账数据、发货状态三种的最终一致。
数据 | 系统 | 事务 | 正向流程 | 逆向流程 |
---|---|---|---|---|
订单数据 | 本地系统 | 本地事务 | 本地事务2PC保证 | 定时任务,异常关单 |
动账数据 | 本地系统 | 本地事务 | 本地事务2PC保证 | 定时任务,逆向补账 |
发货状态 | 外部系统 | 分布式事务 | 联同本地事务提交 / 定时任务补偿提交 | 定时任务,驱动发货或取消 |
A: 定时任务根据发货状态进行发货流程驱动
- 定时任务补偿再次发货
发货成功则分布式事务最终一致,下游发货系统进行发货防重处理- 发货失败进入逆向流程
定时任务驱动发货最终一致理论上可以一直进行,但是发货可能有一个流程时效和库存售罄的问题,可以根据业务场景进行配置比如2天内重复调用失败或调用返回无库存则进入逆向关单退款流程使得分布式事务恢复成最开始的一致性状态
A:账户流水表做唯一索引
正逆向类型 + 业务ID
,和账户额度表进行本地事务操作,确保只能成功一笔正向/逆向业务操作
当一个App有了足够多的用户体量,便开始有商家进行广告或商品的投放,平台通过包装运营活动、广告位等,将用户曝光与商家付费结合达到流量变现的目的。
当用户进行浏览、关注、商品购买、视频观看、App下载等多种任务,我们会根据商家配置等付费规则进行商家广告费用的扣减,同时还要为用户完成任务进行奖励结算,此时的分布式事务便是商家账户扣减与用户账户增加的数据一致性问题
角色 | 数据 | 系统 | 操作 |
---|---|---|---|
平台 | 业务流水 | 结算系统 | 扣减商户费用、增加用户余额 |
商户 | 商户余额 | 商户系统 | 扣减费用 |
用户 | 用户余额 | 用户系统 | 增加余额 |
A: 定时任务根据结算状态进行结算流程驱动,会一直轮询补偿尝试结算用户,直到成功。
A: 用户做任务时一定是做了前置校验进行平台任务发放的,也就是说用户任务只要完成必须要进行结算,这是一个不能逆向的流程。即使极端情况下商户余额空了暂时无法结算到用户也要一直重试,一旦商户余额充足则继续整个结算流程。
抽奖是运营活动中比较常见的方式,对于用户来说需要做的是参加设定人物获取积分,攒够积分就可以开始抽奖,抽中后即等待奖品入账,一般券会立刻入账使用,而实物商品则需要耐心等待物流送到用户手上。
关于抽奖,涉及账户动账、库存参与次数扣减、抽奖等多个环节,该场景要解决的分布式事务是账户动账、活动库存变更、抽奖下单数据一致性。
数据 | 系统 | 操作 |
---|---|---|
账户余额 | 交易系统 | 扣减账户余额 |
活动库存 | 运营活动系统 | 扣减库存、用户日参&总参 |
业务流水 | 运营活动系统 | 业务ID[动账凭证],抽奖ID[抽奖凭证],活动ID |
抽奖流水 | 抽奖系统 | 抽奖下单,获取中奖结果 |
A: 抽奖业务对于用户来说实时性要求很高,正向流程没有完成的话,没有通过补偿流程驱动的必要性了,用户体验不好容易产生问题,补偿流程只针对账户扣减成功扣没有顺利完成抽奖的情况进行账户补款即可。而且这部分补款是有延迟的,在C端展示可以优化或者忽略合并掉,保证的是账户额度无损。
系统数据发生变更时,会对外部系统产生一定影响,外部系统需要知道这种数据变化,这便是数据状态同步的场景。一般来说数据交互可以有推(Push)、拉(Pull) 两种形式,这里先说推模式,即数据变更方负责将变化通知到数据关注方。
这里要保证的是数据变更在多个应用中的状态一致。
角色 | 驱动方式 | 通信方式 |
---|---|---|
数据变更方 | MQ + RPC 最终一致 | 调用接口通知其他系统 |
数据关注方 | RPC调用成功更新数据 | 提供接口接收数据变更 |
这里是弱一致性的实现,没有做本地事务表和定时任务轮询对比各事务状态进行补偿操作。完全依赖于MQ的失败重试驱动,若RPC调用失败即数据同步业务方失败,MQ会一直进行重试操作,随着重试次数增加,重试间隔也会增加,这里也可以业务自行进行最大努力尝试次数的控制,超过多少次尝试仍失败则放弃,因此不能保证最终一致。
这里是数据同步的说拉模式,即数据关注方对数据变更方进行数据状态变更的监听,这种处理方式处理的主动权在于数据关注方,数据变更方只负责和保证一定通知到数据变更情况,是否能够同步成功则由监听方处理和兼容。
这里要保证的是数据变更在多个应用中的状态一致。
角色 | 驱动方式 | 通信方式 |
---|---|---|
数据变更方 | 生产MQ | 数据变更存入MQ队列 |
数据关注方 | 监听MQ | 消费MQ处理数据变更情况 |
这里也是弱一致性的实现,没有做本地事务表和定时任务轮询对比各事务状态进行补偿操作。完全依赖于MQ消费方的处理,若消费方处理失败或在消息队列规定时间内没有消费完毕,则数据无法保证最终一致。
在秒杀场景中,最复杂的除了解决高并发问题外,最核心的就是活动商品的库存控制、变更问题,一般商品库存会初始化到Redis缓存中进行管理,秒杀方法会对Redis缓存库存数量进行校验、扣减操作,通过MQ异步扣减DB库存,既利用Reids原子操作进行库存数量操作,又利用缓存抗住高并发请求,起到异步削峰的作用,这是秒杀的正向流程。而逆向流程是用户秒杀到商品预占了库存,但是没有及时进行订单支付或者进行了订单取消,此时要发起对库存的恢复操作。
这里的分布式事务是Redis缓存库存与DB库存数量一致性问题。
存储介质 | 库存操作方式 |
---|---|
Redis | incr 、decr累计或扣减 |
DB | MQ异步扣减 |
Q:秒杀场景会出现哪些分布式问题?
A: 根据如上流程图,扣减缓存库存、创建订单、异步MQ发送是在一个同步的函数方法中的三个非原子的子步骤,而秒杀场景下流量洪峰会一瞬间打满线程,以上三个子步骤任何异步都会出现问题,因为都是先扣缓存库存数量,根据实践经验看,极端情况下会出现扣减缓存库存成功了,后面创建订单失败了或者异步MQ没有发出来无法削减DB库存数量,因此数据结果是缓存库存扣减的多,DB扣减的少,实际抢购卖出的少,换句话说就是出现了少卖的现象。
Q:会不会出现超卖现象?
A: 不会。依赖于Redis单线程命令执行的保证。这里需要注意的是读、写命令不是一致,可以结合分布式锁实现,也可以通过Lua脚本实现命令的原子性执行。
这里也是一个弱一致性的实现,业务场景我们保证不超卖即可,对于极端情况出现的库存数量无效多扣减做战略性放弃,一般情况下不会影响大多业务使用,如果非要吹毛求疵达到账户金额那种强一致性,思路也很简单,可以借助定时任务轮询对比缓存与DB库存数量进行校验,这里还要考虑到其他在行流程如超市关单库存恢复,仍然在行的秒杀活动等,保证数据处理不多加不多减。
DB、RPC、MQ
这些能够提供事务能力的中间件或接口服务WAL + Redo Log + 刷盘策略
保证Ack机制+刷盘策略
保证bizId
代表全局唯一的业务标识。在MQ重发、重复消费、乱序,RPC重复调用等场景进行业务防重兼容处理。正逆向类型 + 业务ID
进行拦截《软件架构设计:大型网站技术架构与业务架构融合之道》
用户增长运营活动系统
电商大促秒杀活动系统
用户中心认证系统