分布式服务一致性实现
一个常见的场景:用户从购物车中选择多个商品购买。下单和支付是两码事,这里分开来说。 下单:
- 将选定商品从购物车删除
- 对所有商品进行占库存操作
- 获取商品信息
- 生成订单
首先,占库存、更新购物车、生成订单都属于写操作,获取商品信息属于读操作。 假设商品中心管理商品,用户中心管理购物车,订单中心管理订单,前台应用协调所有的服务。
整个下单操作交由前台进行协调,那么整个流程到达前台后应该具体为:
- 前台调用用户中心接口更新购物车
- 前台调用商品中心接口占库存
- 前台调用商品中心接口获取商品信息
- 前台调用订单中心接口生成订单
存在什么问题?当库存不足导致下单失败时,需要对购物车的更新进行回滚。
这个时候就需要用到分布式事务保证强一致性,但是分布式事务下会对性能产生较大影响。
- 分布式事务需要参与者多次交互
- 分布式事务异步化实现难度大
- 关于 2PC,3PC,TCC 的问题上一节介绍过
- 为了分布式事务而去部署另外的高可用中间件,成本太高
那么怎么设计,才可以即可以保证事务的一致性,又可以不使用分布式事务呢? 现在对上述步骤进行稍微的调整,就可以实现:
- 前台调用商品中心接口占库存
- 前台调用用户中心接口更新购物车
- 前台调用商品中心接口获取商品信息
- 前台调用订单中心接口生成订单
仅仅将 1、2 步进行了调换,就可以实现一致性了吗?为什么呢? ok,我们知道事务发生异常进行回滚,那么都有什么异常呢?
- 服务不可用异常
- 业务抛出异常
服务不可用异常基本上不会发生在高可用系统上,通过重试或节点切换就可以解决。
而业务抛出异常只存在于第一步中,在占库存成功之后,就意味着整个下单的必要条件已经达成了。 这个时候后面的操作就算失败,通过重试也一定可以成功(除非服务不可用或程序出现问题),而不需要执行撤销操作。 如果分布式服务不存在撤销操作,那么就不需要分布式事务的加入。
保证两个原则,就可以避免 undo 的发生:
- 将 undo 转变成 redo,redo 失败日志记录后人工兜底。
- 存在业务抛出异常的事务放在第一步完成。就算失败,也不需要对其他独立事务进行回滚。
上面的情况还存在 undo 吗?
存在。极端情况下,下单多次,那么就会进行多次的占库存,而更新购物车只有一方成功。 这里通过分布式锁可以很容易解决,因为购物车是用户私有的,在极端情况下才存在竞争,使用分布式锁更多的是异常规避。
突然宕机的情况
- 占库存时,商品中心发生宕机,那么商品中心的独立事务就不会提交。
- 其他微服务下发生宕机,前台进行 redo,除非所有节点都 down。
- 分布式锁需要设置过期时间避免购物车被一直占用,看情况使用 redisson 进行续期,一般不需要。
什么操作存在 undo ?
上述讲到的业务抛出异常(资源不存在,库存不足等)才需要进行 undo,因为这意味着重试无效。
如果存在多个操作,都存在上述的业务抛出异常怎么办?
这种情况比较复杂,首先肯定是进行业务拆分,所以下单和支付才是分开的两个功能。 不管是先减库存还是先扣款,当后面的操作失败时,前面的操作只能回滚。 但如果无法对业务进行拆分,那么就可以尝试仅对这些操作使用分布式事务。
异步化
从结果论展开,用户并不关心功能的过程,即过程对用户是透明的,用户只关心最终的结果。 所以在不影响一致性的条件下,快速响应用户的请求而不是展示冗长的进度条已经成为多数重服务的解决办法之一。 而快速响应的实现方式就是异步化。异步化作为提高性能、优化体验的杀器,在分布式场景下也能大放异彩。 消息队列就是强大的异步化工具之一。
依旧是这个例子:
- 前台调用商品中心接口占库存
- 前台调用用户中心接口更新购物车
- 前台调用商品中心接口获取商品信息
- 前台调用订单中心接口生成订单
其中 2、3、4 是可以进行异步化的,则上述操作就变为:
- 前台调用商品中心接口占库存
- 前台发送消息到消息队列
在第二步执行结束就可以直接返回响应给用户了,例如反馈给用户:下单成功,生成订单中等通知。
消息到达消息队列后首先被用户中心消费,消费后再把消息发回消息队列,然后订单中心消费,订单中心同步查询商品信息并生成订单。多次 redo 失败,会将消息迁移到死信队列,可以进行补偿性回滚或者人工重试。
生成订单成功则将结果写到 redis 中,然后让客户端轮询 redis 获取执行结果,得知事务成功后就可以执行查询订单操作了。