2、如何实现服务之间的数据一致性
说到数据一致性这个话题,我们可以想到的最常用最熟悉的解决问题的方式就是事务处理了。它存在的意义是为了保证系统中所有的数据都是符合预期的,并且存在关联关系的数据之间不会产生矛盾,即数据状态一致性。事务的概念,起源于数据库,发展到今天,几乎在每一个业务系统中都会涉及到。尤其在一些大型复杂的分布式系统中,事务的概念已经不仅局限于数据库,还可以延伸为一切需要保证数据一致性的应用场景,包括但不限于数据库、事务内存、缓存、消息队列、分布式存储等等,这些都有可能会用到事务处理。
今天我们探讨的主题是服务之间数据一致性问题。当然,实际生产落地中,在不同业务背景下,具备可行性的方案也是非常多的,各有优劣和适用场景。我们从中选择一两个具体实现,聊一些相关的设计和实践。
3、几个核心名词
为了方便正在阅读本篇文章的同学对后续内容的阅读和理解,我们先对文章中使用到的几个名词的语义做一些解释和约定:
业务侧系统:指的是发起执行业务操作的一方系统服务,可以简单理解为消费方。
平台侧系统:指的是为发起执行业务操作而提供基础能力的一方系统服务,可以简单理解为服务方。
执行业务操作:指的是对数据的读写操作需要依赖多个系统服务的协同完成,业务侧系统发起,平台侧系统执行最终的数据读写操作。这样场景中就普遍存在着服务之间的数据一致性问题(备注 :业务侧系统和平台侧系统是一个逻辑概念,并非一定要存在具体的应用服务与之对应)。
业务操作标识:指的是业务操作应该归属的业务类型或者业务场景的标识,它是平台侧系统创建并且统一管理的,然后发放给业务侧系统,在业务侧系统的服务调用平台侧系统提供服务能力的身份信息。业务标识可以设计为单层结构,也可以设计为多层结构,符合当前系统和业务的需求即可。
业务操作唯一ID:指的是某个具体业务操作在某一次或者多次重复执行的唯一性标识。生产实践中,它一般是由业务侧系统的服务自定义实现和管理的,也可以是基于平台侧系统提供有约束性质和方便管理的规则限制下,再由业务侧系统的服务自定义实现,后者是比较推荐的方式。
业务操作记录表:指的是记录业务操作的日志流水表。生产实践中,一般是由业务侧系统的服务创建并且管理。
4、方案一:业务侧系统保证最终一致性
4.1 核心思想
通过业务侧系统的服务保证数据的最终一致性,其核心思想就是业务侧系统记录下来每一次具体业务操作的执行流水日志信息,并且对没有全部成功的变更结果,触发执行数据一致性的校验核对工作。
4.2 设计原则
- 平台侧系统服务,提供支持执行业务操作的基础服务能力的接口。特别强调一点,这里是需要根据业务操作标识和业务操作唯一ID来实现接口的幂等设计。为什么我们有了唯一ID,同时还是需要有业务操作标识?因为在实际的生产实践中,在各种内因和外因的背景下,需要兼顾系统的稳定性和业务迭代的灵活性,很难做到绝对的全局性唯一ID的生成。更多时候,只需要在某个业务侧系统的内部,保证全局唯一性即可,这也是符合实际情况的系统设计。类似的解决问题的思路,在其他的系统设计场景,也是有非常高的借鉴价值的。
- 平台侧系统服务,提供执行业务操作后的结果查询接口,支持根据业务操作标识和业务操作的唯一性ID查询能力。
- 业务操作记录表,支持记录和识别业务操作的标识和每次执行的唯一ID。
- 业务侧系统服务,触发对业务操作记录表的数据一致性的检查核对工作,执行核对的方式,比如实时的同步检查核对、准实时的异步检查核对、定时任务的异步检查核对等等,为了保证自己和平台侧系统的数据最终一致性。
4.3 流程图
4.3.1 数据一致性的校验核对同步执行流程
4.3.2 数据一致性的校验核对异步核对链路
5、方案二:平台侧系统保证最终一致性
5.1 核心思想
- 通过平台侧系统的服务保证数据的最终一致性,核心思想是平台侧系统的每一次的数据变更,都主动地寻找业务侧系统,来确认本次数据变更结果是否符合预期。
5.2 设计的基本原则:
平台侧系统,提供支持业务操作执行的基础服务能力的接口,需要根据业务操作标识和唯一ID做幂等设计。它和方案一的一致性原则类似,省略不再赘述。
平台侧系统,提供业务操作的执行结果确认的回调SPI,可以方便业务侧系统来实现,根据业务操作标识和业务操作的唯一ID。
业务侧系统,提供根据业务操作标识和业务操作的唯一ID,来判断两边的数据是否具备一致性的回调实现。
5.3 流程图
5.3.1 数据一致性的校验核对同步核对链路
5.3.2 数据一致性的校验核对异步核对链路
6、实践过程中一些经验分享
这一部分,我将会对平台侧系统和业务侧系统的接口设计的部分细节,做一些简单的扩展阐述。希望为大家后续的研发工作提供一些思路。后续的文章中,将会针对其中一些具体的解决方案,做更详细的阐述。
首先,接口幂等性设计,将从如下角度进行阐述: 数据结构,状态存储,异常处理,返回结果唯一等等角度做一些总结分享。
6.1 数据结构设计
接口幂等性设计,是基于业务操作的标识(这里是称之为Tag)和业务操作的唯一ID来实现的。业务操作标识的设计,可以是单层的设计,也可以是多层的设计。其中,多层的设计是为了满足业务侧系统的存在复杂并且多业务场景的诉求。业务操作的唯一ID的生成方式,可以是没有任何业务含义的自增趋势的不可重复的ID,比如MySQL的自增主键ID,分布式ID生成器等等方式,也可以是业务侧系统的某些特定的业务字段 ,比如用户的userId,订单的orderId,商品的spuId,skuId等等。在实际实践中,后者是我们比较推荐的常用方式,可以实现在不增加系统复杂度和额外依赖资源的同时,又可以和业务侧系统达到高度的契合。
6.2 状态存储设计
在一般情况下,建议把MySQL存储当做我们首选的存储,MySQL提供非常完善的数据一致性保证能力,最简单的方式是基于数据库的联合唯一索引设计,多次层Tag + 唯一ID的业务唯一键。但是也是有缺陷的,比如MySQL自身的性能瓶颈和昂贵的存储成本。性能上的瓶颈,可以通过访问MySQL的幂等校验之前,增加访问Redis的幂等校验,校验不通过抛出异常,在MySQL幂等校验通过以后,异步刷数据到Redis中,这样保证Redis校验通过的同时MySQL校验一定是通过的。我们可以接受Redis的幂等校验的不准确性,仅仅是期望它成为流量漏斗的上层,为MySQL承担起流量过滤作用,当然你可以有其他的更多的方案来做这件事,甚至组合起来使用。也可以增加分库分表的策略,来解决MySQL的性能瓶颈。在MySQL的存储成本是相对比较高的,我们可以对历史的数据做归档处理,只保留一部分的热数据,原则上保持单表的数据行数在500w~1000w之间,同时也可以有能支持一定量的历史数量查询。同时这个过程也需要考虑无锁处理问题和MySQL空间碎片的问题等等。
6.3 异常处理设计
第一步,明确导致发生异常的原因有哪些?一般可以归为几个分类,网路异常,数据格式错误,业务逻辑异常。第二步,针对特定类型的问题,我们做出相应处理方案。比如我们重试机制,控制重试频次,重试周期的衰减时间执行控制,处理数据处理的终态的异常数据的兜底处理机制等等方式。
- 我需要保证接口的返回的数据,再多次重复调用执行,依旧保证完全相同。我们可以基于状态机的流转控制,返回相同的状态码,也可以对一些核心业务参数做核对校验,如果不通过返回特定的异常码等等。
- 此外,平台侧系统的提供给基础能力接口的设计要求我们研发同学思考和考虑的更多,比如一致性延迟问题,状态机的设计,并发问题处理,接口不可用解决等等。
- 能否在业务侧系统服务期望的时间点,完成数据一致性的校验核对工作?若有延迟,延迟是多少?尤其是极端场景下的延迟是多少?
- 案例:如果使用定时任务,做数据一致性校验核对工作。比如一个周期(假设1min),还有很多数据未完成核对工作,剩余多少,以及对业务侧系统的影响。解决思路:1. 评估和设计一个合理的周期大小;2. 选择全量核对和增量核对的选择;3. 增加核对的扫描的数据范围的策略;4. 增量核对确保不丢失未核对过的数据,等等。
- 案例:如果使用MQ消息,我们可能面临的问题是消息堆积,消息丢失等等场景MQ问题带来的数据不一致问题。
- 案例:如果使用同步等待方式,是可以将数据一致性的延迟降低为0,但是系统吞吐能力和可用性等等,都是无法保证,这也是选择权衡的结果。
- 基于状态机的设计中,一定是有初始态和终态的,代表数据的核对工作,有始有终。至于中间态,可以有多个中间态,也可以是仅有一个中间态,这个和实际的需求和背景相关联的,可以灵活地控制。其中的终态,一般情况下都不会只有一种,而是有两大类,一种是成功的终态表示数据实现最终一致性,一种是失败的终态表示不因为不可抗拒的因素导致的数据不一致产生。失败的终态,也是可以设计出多种状态,根据实际需要来设计。比如多次重试从初始态到终态的耗时和处于失败态的数据核对检测工作的占比,一定程度上代表着业务侧系统对数据一致性延迟的容忍度。这应该是我们必须关注的核心指标信息。
6.4 返回结果唯一
6.5 延迟问题的容忍度
6.6 基于状态机的设计
6.7 并发问题
- 我们在创建一个初始化态的流水日志记录的时候,是一个MySQL的insert操作(假设你选择了MySQL作为存储),需要避免创建多条的业务操作唯一ID的记录。最简单粗暴的方式,依赖DB的联合唯一索引是可以实现的。但是需要考虑在并发比较多的时候,带来的性能和吞吐问题,甚至导致创建初始化态就失败的问题。
- 对于相同数据并发写的问题,我们成功执行一条insert语句,大多数情况可以满足我们业务侧系统的预期。我们可以采用加锁,排队等待,分组等待排队等等手段,限制类似场景的并发数来解决。这种方式,随着业务的发展扩张,可能会面临系统的吞吐量不足以支撑业务的问题。
- 解决上述的吞吐量下降的问题,我们可能又会想到采用MQ的方式来削峰填谷,因为实际生产实践中,并发写问题的往往都是一个特点 瞬时性发生的系统尖刺。采用MQ的方式,可以保证平台侧系统创建初始化态的流水日志的系统吞吐量。
- 在以上的基础之上,我们还是可以采用隔离拆分的方式,比如服务接口拆分层面的隔离,MQ的topic拆分的隔离等等,配合不同的限流熔断等等系统保护策略的方式以及不同的系统资源倾斜等等,解决平台侧系统的性能问题。
6.8 需要解决不可用
- 熔断限流,资源隔离,多元化的降级策略等等,这些是大家都非常熟悉的系统可用性保障的手段,这部分相关的内容,就不再展开叙述了。
6.9 需要提供可视化和可观测
完善告警机制,比如异常状态告警,超出阈值告警等等,让相关的业务侧系统和平台侧系统同学可以快速感知到问题并且介入解决问题。
建设监控大盘,比如 MySQL,Redis,MQ,以及数据核对工作的状态的监控等等,都是需要我们去一步一步建设起来的。
定位和排查问题的工具,拆分后的系统,其系统的复杂度是指数增长的,这个方面也是非常重要的。
7、总结
在本篇文章中,阐述了两种处理数据一致性问题的解决方案,从核心思想,设计原则,系统交互流程等等做了详细的阐述,比对两种方案,各有优劣和各自的适用场景。方案一,业务侧系统来保证数据的一致性,更适用于对数据的一致性有相对比较强的耦合依赖关系的业务场景,需要依赖业务操作的执行结果做出判断,执行不同后续业务逻辑分支的执行。 案例: 同一个商品在不同修改商品信息(变更不同的字段,变更不同表的字段)的入口触发异步更新C端缓存的单品维度的商品全量缓存数据构建,变更的事务是在成功完成提交以后,方可执行本次变更对应的后续缓存构建。方案二,平台侧系统来保证数据的一致性,更适用于业务侧系统,关注点是数据的最终执行结果的业务场景,案例: 不同业务场景入口的库存扣减和库存回滚执行结果。最后,提到在生产实践过程中一些经验和解决方案的总结分享,每个点都是值得继续深入探讨。