数据一致性说白了,就是要求关联数据在不同的时间、不同的地方,看到的都得是同一个状态,同一个结果。尤其是在分布式系统里,数据东一块西一块,一个业务操作可能要动好几个库,更新好几个服务的内存状态。比如上面说的下单扣库存,订单库、库存库、用户积分库,这三个地方的数据必须步调一致,要么一起成功,要么一起失败,不能搞"分裂"。
那为啥会不一致呢?根源太多了。最常见的就是网络问题。服务A调服务B更新数据,结果请求超时了,A不知道B到底成了没,是重试还是回滚?这一犹豫,数据可能就花了。还有机器宕机,一个事务正干到一半,突然断电,数据库重启后可能就留了个"半吊子"现场。代码逻辑有bug,或者并发控制没做好,多个线程同时改一条数据,也容易把数据改得亲妈都不认识。
为了解决这些幺蛾子,前辈们想了不少招。
- 强一致性:硬刚,但代价大
最干脆的就是玩强一致性,核心武器是分布式事务。这就像找个总指挥(事务协调器),保证所有参与方(比如订单服务、库存服务)要么全部提交,要么全部回滚。经典的二阶段提交(2PC)就是干这个的。第一阶段,协调者问大家:"我准备这么干,你们能不能行?"大家都回复"行",第二阶段再发正式命令:"好,那就这么干了!"要是有一个人说"不行",那就全体回滚。
这招确实能保证ACID,但缺点也明显,同步阻塞太厉害,性能瓶颈突出,而且协调者自己要是挂了,整个系统都可能被卡住。所以一般用在金融、交易这些对钱要求绝对精确的核心场景,为了正确性可以牺牲点性能。
- 最终一致性:退一步海阔天空
大部分业务场景其实用不着那么"刚",可以接受数据"暂时不一致",但保证经过一小段时间后,数据最终会变成一致的。这就是最终一致性,算是分布式系统里的一个"折中"智慧。
实现最终一致性,有几个常用的模式:
异步确保型:这是最普遍的玩法。核心是靠消息队列。比如订单服务处理完本地事务后,发个消息到MQ,积分服务监听这个消息,然后去给用户加积分。哪怕积分服务暂时挂了,消息也会在MQ里存着,等它活了再消费。这里的关键点是,要保证订单服务本地事务和发消息这两个操作,本身必须是一个原子操作。不然数据库事务提交了,消息没发出去,那就彻底丢了。可以用类似"事务消息表"或者RocketMQ的事务消息机制来解决。
补偿型(TCC):这就是个"后悔药"机制。把一个业务操作分成三步:Try(尝试)、Confirm(确认)、Cancel(取消)。比如扣库存,Try阶段先冻结一部分库存,Confirm阶段才真正把冻结的库存扣掉,如果中间出了问题,就执行Cancel,把冻结的库存释放回去。TCC对业务侵入性强,代码写起来麻烦,但控制粒度细,非常灵活。
最大努力通知型:这个更简单粗暴一点。操作完成后,不断地、反复地(有次数限制)调用对方接口,直到对方返回成功为止。比如支付成功后,支付平台会不停地回调我们系统的接口通知支付结果。这招实现简单,但数据一致性保障的强度不如前面几种,算是尽了"最大努力"。
- 幂等性:防重复的"护身符"
不管用哪种方案,幂等性都是必须考虑的。因为网络超时可能导致上游重试,一个请求可能被重复发送。如果接口不幂等,重复扣款、重复发货就来了。实现幂等常见的方法有:利用数据库唯一索引防止重复插入;在业务层面使用Token机制或者状态机;或者记录一个全局唯一的业务ID,处理前先查一下这个ID干过没有。
- 用对工具也很关键
现在很多成熟的中间件帮我们封装了复杂性。比如用Redis事务或者Lua脚本来保证对多个Key操作的原子性。选用支持强一致性的分布式数据库,比如TiDB。或者通过CDC工具(如Canal、Debezium)去解析数据库的binlog,把数据变更实时同步到其他系统,这在做数据异构、数据同步时很有用。
总结一下
搞后端,数据一致性是个绕不开的坎。没啥银弹,全靠权衡。选强一致性还是最终一致性?得看你的业务场景。是要求绝对的精确,还是可以接受短暂的延迟?技术方案是为业务服务的。平时设计的时候,多画画流程图,想想异常分支,关键操作把日志打详细点,真出问题了也能快速定位。这块内容水很深,慢慢踩坑,慢慢积累吧。