关于分布式系统的数据一致性问题

转载自:https://blog.****.net/liutao363071094/article/details/53169444
    https://blog.****.net/liutao363071094/article/details/53169464

  拿经常使用的功能来举例,最近网购比较热门,就以京东为例的,来看看京东的一个简单的购物流程:
关于分布式系统的数据一致性问题
  用户在京东上下了一个订单,发现自己在京东的账户里面有余额,然后使用余额支付,支付成功之后,订单状态修改为支付成功,然后通知仓库发货。假设订单系统,支付系统,仓库系统是三个独立的应用,是独立部署的,系统之间通过远程服务调用。
  订单的有三个状态:I:初始 P:已支付 W:已出库,订单金额100, 会员帐户余额200。

  如果整个流程比较顺利,正常情况下,订单的状态会变为I->P->W,会员帐户余额100,订单出库。但是如果流程不顺利了?考虑以下几种情况:
  1. 订单系统调用支付系统支付订单,支付成功,但是返回给订单系统数据超时,订单还是I(初始状态),但是此时会员帐户余额100,会员肯定会马上找京东骂京东,为啥不给发货,我都付钱了
  2. 订单系统调用支付系统成功,状态也已经更新成功,但是通知仓库发货失败,这个时候订单是P(已支付)状态,此时会员帐户余额是100,但是仓库不会发货。会员也要骂京东。
  3. 订单系统调用支付系统成功,状态也已经更新成功,然后通知仓库发货,仓库告诉订单系统,没有货了。这个时候数据状态和第二种情况一样。


对于问题一,来分析一下解决方案,能想到的解决方案如下:
1 调用支付系统支付订单的时候先不扣钱,订单状态更新完成之后,在通知支付系统扣钱。
  如果采用这种设计方案,那么在同一时刻,这个用户,又支付了另外一笔订单,订单价格200,顺利完成了整个订单支付流程,由于当前订单的状态已经变成了支付成功,但是实际用户已经没有钱支付了,这笔订单的状态就不一致了。即使用户在同一个时刻没有进行另外的订单支付行为,通知支付系统扣钱这个动作也有可能完不成,因为也有可能失败,反而增加了系统的复杂性。

2 订单系统自动发起重试,多重试几次,例如三次,直到扣款成功为止。
  这个看起来也是不错的考虑,但是和解决方案1一样,解决不了问题,还会带来新的问题,假设订单系统第一次调用支付系统成功,但是没有收到应答,订单系统又发起调用,完了,重复支付,一次订单支付了200。又或者支付系统正在发布,你重试多少次都一样,都会失败。这个时候用户在等待,你怎么处理?

3 在第2种方案的基础上,先解决订单的重复支付行为,需要在支付系统上对订单号进行控制,一笔订单如果已经支付成功,不能在进行支付。返回重复支付标识。那么订单系统根据返回的标识,更新订单状态。
  接下来解决重试问题,假设重试三次,如果三次都失败,先返回给用户提示支付结果未知。假设这个时候用户重新发起支付,订单系统调用支付系统,发现订单已经支付,那么继续下面的流程。如果会员没有发起支付,系统定时(一分钟一次)去核对订单状态,如果发现已经被支付,则继续后续的流程。

  这种方案,用户体验非常差,告诉用户支付结果未知,用户一定会骂你,你丫咋回事情,明明支付了,你告诉我未知。假设告诉用户支付失败,万一实际是成功的咋办。你告诉用户支付成功,万一支付失败咋办。

4 第3种方案能够解决订单和支付数据的一致性问题,但是用户体验非常差。当然这种情况比较可能是少数,可以牺牲这一部分的用户体验,还有没有更好的解决方案,既能照顾用户体验,又能够保证资金的安全性。
  再回来看看第一种方案,我们先不扣钱,但是有木有办法让这一部分钱不让用户使用,对了,我们先把这一部分钱冻结起来,订单系统先调用支付系统成功的时候,支付系统先不扣钱,而是先把钱冻结起来,不让用户给其他订单支付,然后等订单系统把订单状态更新为支付成功的时候,再通知支付系统,你扣钱吧,这个时候支付系统扣钱,完成后续的操作。

  看起来这个方案不错,再仔细分析一下流程,这个方案还存在什么问题,假设订单系统在调用支付系统冻结的时候,支付系统冻结成功,但是订单系统超时,这个时候返回给用户,告知用户支付失败,如果用户再次支付这笔订单,那么由于支付系统进行控制,告诉订单系统冻结成功,订单系统更新状态,然后通知支付系统,扣钱吧。如果这个时候通知失败,木有问题,反正钱都已经是冻结的了,用户不能用,只要定时扫描订单和支付状态,进行扣钱而已。
  那么如果用户重新拍下来一笔订单,100块钱,对新的订单进行支付,这个时候由于先前那一笔订单的钱被冻结了,这个时候用户余额剩余100,冻结100,发现可用的余额足够,那就直接在对用户扣钱。这个时候余额剩余0,冻结100。先前那一笔怎么办,一个办法就是定时扫描,发现订单状态是初始的话,就对用户的支付余额进行解冻处理。这个时候用户的余额变成100,订单数据和支付数据又一致了。假设原先用户余额只有100,被冻结了,用户重新下单,支付的时候就失败了啊,的确会发生这一种情况,所以要尽可能的保证在第一次订单结果不明确的情况,尽早解冻用户余额,比如10秒之内。但是不管如何快速,总有数据不一致的时刻,这个是没有办法避免的。

  下面在分析一下第二个问题:订单系统调用支付系统成功,状态也已经更新成功,但是通知仓库发货失败,这个时候订单是P(已支付)状态,此时会员帐户余额是100,但是仓库不会发货。
  这个相对来说是比较简单的,可以采取重试机制,如果发现通知仓库发货失败,就一致重试,这里面有两种方式:
  1. 异步方式:通过类似MQ(消息通知)的机制,这个是异步的通知
  2. 同步调用:类似于远程过程调用

  对于同步的调用的方式,比较简单,能够及时获取结果,对于异步的通知,就必须采用请求、应答的方式进行。

  再来看看第三个问题:订单系统调用支付系统成功,状态也已经更新成功,然后通知仓库发货,仓库告诉订单系统,没有货了。
  还是考虑几种解决的方案
  1.在会员下单的时刻,就告诉仓库,我要你把货物留下来,
  2.在会员支付订单时候,在支付之前检查仓库有没有货,如果没有货,就告知会员木有货物了
  3.如果会员支付成功,这个时候没有货了,就会退款给用户或者等待有货的时候在发货

  正常情况,京东的仓库一般都是有货的,所以影响到的会员很少,但是在秒杀和营销的时候,这个时候就不一定了,我们考虑假设仓库有10台iphone。
  如果采用第一种方案,
  1.在会员下单的时候,相当于库存就-1,那么用户恶意拍下来,没有去支付,就影响到了其他用户的购买。京东可以设置一个订单超时时间,如果这段时间内没有支付,就自动取消订单
  2.在会员支付之前,检查仓库有货,这种方案了,对于用户体验不好,但是对于京东比较好,至少我东西都卖出去了。那些没有及时付款的用户,只能投诉了京东无故取消订单
  3.第三种方案,这个方案体验更不好,而且用户感觉受到京东欺诈,但是对于京东来说,比第二种方案更有益,毕竟我还可以多卖出一点东西。