单体架构时,数据一致性根本不是问题。一个本地事务,@Transactional注解一加,要么全成功,要么全回滚,简单粗暴。可一旦拆成微服务,每个服务独立数据库,一个业务操作跨多个服务,本地事务直接失效。订单创建了,库存没扣;支付成功了,订单状态没更新——数据不一致的坑,几乎每个团队都踩过。
这道坎的本质,是分布式事务问题。CAP理论告诉我们,分区容忍性必须保证的前提下,一致性和可用性只能二选一。微服务架构下,强一致性代价极高,所以BASE理论成了主流:基本可用、软状态、最终一致。换句话说,我们追求的不是“时刻一致”,而是“最终一致”。
那具体怎么落地?常见方案有几种,各有适用场景。
2PC/3PC,两阶段提交,数据库层面支持,但性能差、阻塞严重,微服务场景基本不用。TCC,Try-Confirm-Cancel,业务侵入大,每个操作都要写三个方法,适合金融等高一致性场景,但开发成本高。Saga,把长事务拆成多个本地事务,每个事务有补偿操作,适合业务流程长、一致性要求相对宽松的场景。本地消息表,是目前中小团队最常用的方案:业务操作和消息写入放在同一个本地事务,然后异步投递消息,下游消费成功后更新状态,失败则重试。事务消息,如RocketMQ,把消息投递和本地事务绑定,实现最终一致,省去本地消息表的维护,但依赖特定MQ。
实际项目中,我推荐优先考虑本地消息表+定时补偿。它不依赖特殊中间件,实现简单,可控性强。具体做法:在订单库中建一张消息表,创建订单和插入消息在同一个事务里。然后有个定时任务扫描消息表,投递到MQ,下游消费后回调确认。如果消费失败,定时任务会重试,直到成功。配合幂等设计,基本能解决80%的跨服务一致性问题。
但比技术方案更重要的是业务设计。很多分布式事务问题,其实是领域边界没划清。比如订单和库存,如果强行拆成两个服务,每次下单都要跨服务扣库存,一致性自然难保。更好的做法是:把强关联的业务放在同一个服务里,或者通过聚合根设计,让一个服务拥有完整的数据主权。能避免分布式事务,就尽量避免。
另外,幂等和重试是最终一致性的基石。下游服务必须保证同一个消息多次消费结果一致,通常用唯一业务ID+去重表实现。重试策略要设置上限和退避,避免雪崩。
最后,监控和补偿不能少。最终一致性意味着中间状态可能持续一段时间,必须有对账机制,定期核对核心数据,发现不一致自动修复或告警。
从单体到微服务,数据一致性这道坎,迈过去的关键不是追求强一致,而是接受最终一致,用合适的方案+清晰的领域边界+完善的补偿机制,把不一致控制在可接受范围内。没有银弹,但有一套组合拳。记住:能不分库就不分库,能本地事务就别分布式事务。架构是演进而非一步到位,数据一致性也是如此。