首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

数据一致性只和数据库有关系?

题图:Free-Photos in pixabay CC0

数据库的 ACID,应该所有后端程序员都听说过,也是我们必须了解的知识。ACID 里面的 C 就是 Consistency(一致性)。

但是,一致性仅仅是 C 吗?从一个普通用户角度来考虑,当然不是。用户角度的一致性,应该是数据库实现了 ACID 后的效果。用现实的例子来说明就是:

我发起银行转账,不能是我帐号的钱减少了,但是接收方却没收到;不能说银行职员能看到双方的钱是对的,但是用户自己看到的不对;不能说我刚刚看到的帐是对的,另一个时候,或者去另一台机器,或者换另一个方式查就不对了。

那我们开发人员,是不是只要利用数据库提供的 ACID 特性,就能达到用户想要的效果呢?要注意什么呢?

数据库

关系型,单机

在单机使用 RDBMS 数据库如 Oracle、MySQL、PostgreSQL 的情况下,数据库本身提供的 ACID 机制,已经能基本保证数据操作后的完整和一致性了。开发人员要做的,只是确保要维持数据一致性的变更操作代码,同在一个 transaction 里面。我刚工作的时候,当时还是用原始的 JDBC 连接 Oracle,还要手动打开关闭数据库 connection 的连接,统一 commit,或者出错后 rollback。现在,Spring 等框架已经能够用 AOP 和 Annotation 的方式来标注 transaction 的范围。

非关系型,单机

最近几年流行的 NoSQL,像 MongoDB, Redis,它们的 ACID 就不一样了。它们并不是 ACID compliant 的。MongoDB 的 ACID 是 Document 级别的。也就是说,一个数据操作,只能保证一个 Collection 里面的一个 Document 上的所有数据改动是同时成功和失败。假设一个数据操作涉及多 Document 的变动,比如用了 参数,或者更改不同 Collection 的 Document,这些改动都不能保证所有 Document 的更改同时成功,或者同时失败。而 Redis 的 Transaction 就更不一样了。

主从复制

在 Monolithic system 里面,数据库多数是单机。即便为了灾备需求,或者支持读写分离,甚至声称异地多活的系统,也只是启用了数据库的主从复制功能( Master-Slave 模式的 replication )。一般的主从复制,主库的数据和从库的数据肯定会有延时。即便是使用 Master-Master 和实时同步机制,也有可能有延时,或者数据冲突。如果强制使用更严格的一致性写入确认,如 MongoDB 的 Write Concern 设置为 majority 或者 jornal 的话,数据库的性能又会有很大的影响。

我这方面的经验不多,而且现在还有像新出的 Google Spanner 这样的全球分布式,同步复制的 NewSQL 数据库,需要更多了解一下。欢迎大家给意见和指正。

外部组件或系统

可以看到,仅仅利用数据库提供的 ACID 支持,也不是一定能达到用户想要的一致性效果的。而且,很多时候,一些用户感知的一致性,背后还涉及到数据库以外的系统。

缓存

缓存,可能会是为了解决性能问题,最早引入的组件了。但是,一旦引入缓存,数据的一致性就有可能更容易有偏差,即便是在使用单机服务器的情况下。在文章「业务与缓存」里面,提到的缓存失效和更新的策略,是影响数据一致性的重要因素。

外部系统

有些时候,当数据发生改动时,我们还需要通知外部系统,比如,用户注册成功后发送邮件,或短信通知;给用户打款后,发送微信,或短信通知;SOA 架构下,上游系统的数据改变后,需要通知下游系统等。这时候,用户角度的数据一致性,其实还包含了这些外部系统的相应操作,也应该被触发,被体现。内部系统的数据变动,和外部系统的反应,如何能保持一致?能保持一致吗?

在上一家公司的时候,我们利用 Oracle 的 XA Transaction 支持,来尽量确保数据库的改动,能和 JMS 的消息发送 保持同时成功或者失败。但是,如果数据改动后要发邮件,短信,或微信通知,这些现在没类似的支持,是极有可能无法保持一致的。

很巧的是,公众号「程序人生」最新的文章「不要等客户来通知问题」里面的摩拜单车解锁问题,刚好为我提供了一个很好的例子。作者扫码后,单车锁一直没开,但是又认为作者已经成功开锁使用了。所以,它一直不让作者自己操作结束,又不让他重新扫新的单车。最后,要等问题被反应到摩拜开发人员内部,才得以解决。这个状况发生的原因可能是,手机端上传单车开锁指令后,后台的数据状态已经标记为使用状态了,甚至开锁指令都已经下达到自行车上了,但是自行车锁就是没有成功打开。你说,这里数据一致了吗?对系统来说可能勉强算是,但对用户来说就不是了。

解决方案

既然那么多情况可能导致数据的不一致,怎么解决呢?

恰当的建模

使用 NoSQL 和 RDBMS 建模的时候,要考虑的因素很不一样。MongoDB 更多是考虑嵌套,冗余,而不是追求更高的范式要求。这在「Node.js 微信后台搭建系列 - 数据建模」,「一个简单的支付业务与模型演变」一文里面也稍微提过。

两阶段提交(2PC)

两阶段提交(Two-Phase Commit)是一种协议和分布式算法,来协调多操作的原子性。前面说的 Oracle XA Transaction 就是利用 2PC 实现的。MongoDB 里面没有提供多 Document 更改的原子性支持,所以一些场景可以通过在 MongoDB 里面用 2PC 来实现多 Document 的 Transaction 确保数据的一致性。但是,在业务复杂的情况下,自己模拟 2PC 还是很麻烦的。

任务重试

出错重试,应该是很常见的操作了。但是,重试的处理,有几个地方是要注意的:

幂等原则

同步还是异步?

重试次数

出错能否重试,要看这个重试的逻辑是否幂等(Idempotency),或者多次执行都生效的影响到底严不严重。

先说影响。比如说你的系统支持用户提现,成功后需要发通知。用户设置的通知有微信,短信,和邮件通知(这是有多担心钱被偷)。假设你实现的重试任务代码,负责所有通知(包括微信,短信,邮件等),而不是微信,短信,邮件等有各自的独立任务。那么在发通知的时候,假如第一次发微信的时候失败了,但是短信邮件成功了,这个重试任务如果还是被标记为失败。下次重试的时候,它就会重复发送了一些通知。这个任务多次执行的影响,对用户来说可能很烦,但是不大。

还有一个要考虑的是,选择同步还是异步重试。这取决于业务场景,和出错部分的严重程度。必须一致的关键数据部分出错,要么中止回滚,同时警告用户,要么只能同步重试处理。但是,如果是在 Node.js 这样的单线程服务,可能就不应该重试,或者要严格控制重试次数。要不然,除了当前用户受影响,说不定共用服务的其它用户也遭殃。

操作顺序

如果说数据不一致无法完全避免,那如何最大化避免数据不一致,并在出错后有迹可循呢?

先处理出错可能性低的部分

先内部系统,再外部系统

先记录操作唯一性,再标记不同状态

从用户表 user 中减少提取的额度

把这个额度记录到 transaction 中的一条包含唯一性的流水记录里,标记处理中

通过微信 API 通知把提取额度转到用户零钱

成功后把 transaction 中的流水记录标记成功,否则标记失败

这里涉及 4 步操作。假如每一步都有可能出错,安排 1 和 2 两步在前面是因为同是内部系统,出错可能性低一些。即便第 1 步成功,但是第二步失败,用户的余额还是可以通过 replay transaction 里面所有的收支记录来刷新,或者这里做特定异常处理。

这里面的第 3 步,是外部系统,涉及网络操作,所以是最有可能出错的。所以第 3 步前必须先有操作记录,而且有唯一性(比如订单号)标识。出错后可以通过此标识,像微信查询该转账操作是否成功。

最后才更新流水记录的状态,也是为了能保证最终的完整性,和提供异常数据监控的可能。

异常数据主动监控,补偿

一般来说,如果所有的操作,都是系统内部触发,那么出错的时候,都应该有记录,并且可以重试。但是,像前面提到的摩拜单车的例子,解锁部分的硬件操作,锁有没有打开这个状态并没有反馈回内部系统,导致不一致的状态已经脱离了内部系统范畴。这就不是重试能解决的了。异常数据的主动监控和补偿就派上用场了。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20171213G104L200?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券