分布式的事务该怎么做?

分布式八大坑

分布式就是魔鬼啊! 

张大胖最近十分感慨,他所在的公司原来有个电商系统,后来随着用户量越来越大,对系统的可用性要求越来越高。 CTO要求把系统进行拆分, 从一个单体的应用,拆分成微服务组成的应用。 

微服务听起来很美好,但是其中的苦只有做过的人才知道。  

在原来的单体应用中,订单模块想要调用库存和支付,只要调用相关的类或者接口就可以了,只有一个数据库,轻轻松松就可以把所有操作放到一个事务当中,保证不会出现扣了库存但是支付失败的情况。 

(单体应用) 

 现在好了,系统成了分布式,原来的进程间调用变成了跨越网络的HTTP调用,这数据库也从单个数据库变成了多个独立的数据库,原来的事务肯定是不起作用了! 

大神Bill告诉张大胖分布式有八个大坑, 千万别跳到坑里去:  

当你在构建一个分布式系统的时候,可能会不由自主地做一些假设,这些假设从长期来看,都是错误的,都会导致大麻烦: 

1. 网络是可靠的 

2. (调用)没有延迟 

3.  无限的带宽 

4. 网络是安全的 

5. (系统)拓扑结构不会改变 

6. 有个管理员在管理这个系统 

7. (数据)传输代价为零 

8. 网络是同质的(同类的)  

这第一条就很要命,网络是不可靠的, 网络调用失败的可能性是非常高的,很有可能出现扣减了库存,但是没有支付的情况。 

分布式的事务

怎么样让扣减库存和支付服务能在一个类似数据库事务中完成,要么都做,要么都不做呢? 张大胖觉得十分头疼。 

张大胖首先想到了两阶段的提交(2PC),但是2PC是针对底层的数据资源层实现的,现在要做的是业务层的事情, 况且这2PC也很不好用啊。 

(码农翻身注: 2PC的故事参见《Java帝国之宫廷内斗》) 

他觉得必须要有一个协调人居中协调各个微服务,让他们处于一个“事务”中, 可是这协调者该如何实现? 

Bill 递给他一篇文章:“你要实现的就是分布式事务了!看看这篇文章吧!”  

张大胖接过打印的文章,标题是:《Distributed Atomic Transactions over RESTful Services》 ,他的头嗡地一声就变大了,哀叹道:“英文的啊,你还是给我讲讲吧!” 

“英文很重要啊,大胖同学!” Bill说道,“其实这个分布式事务的原理很简单,它的精华就是冻结资源幂等性。”

 (此处应该插入一个英语广告,哈哈。) 

张大胖说:“这幂等性我知道,就是一个操作不管是执行一千次,一万次,效果和执行一次是一样的。 这冻结资源是怎么回事?” 

“拿咱们的系统举个例子吧,订单服务要调用库存服务扣减库存(假设数量为2),还要调用支付服务从用户余额扣钱(假设为100), 那订单服务第一步就告诉库存服务,给我冻结2个库存; 告诉支付服务,给我冻结100块钱。在这一步,两个服务要做业务检查,看看库存余额够不够,如果足够,就冻结他们,防止其他调用也进行了扣减操作,导致本次调用余额不足。 这一步,我们称之为尝试(Try)。 ” 

 (注: 这里也可以对库存数量和用户余额做扣减)  

库存服务和支付服务操作的都是自己的表,冻结操作可以放到一个本地事务中,保证原子性。 

 “明白, 接下来呢?” 张大胖问道。 

 “这一步如果成功完成,订单服务就可以进入第二步,告诉这两个服务真正地执行扣减操作,这一步叫做Confirm。” 

 (注:如果在第一步已经做了扣减,这里只需要修改相关状态即可,大家可自行脑补。 ) 

“慢着,如果调用支付服务进行Confirm时出错怎么办? ” 张大胖问道。

 “很简单,那就告诉库存服务和订单服务,都进行Cancel操作, 把冻结的数量进行恢复。” 

Bill说道。 “我们把这套机制叫做 Try - Confirm -Cancel,简称TCC。对于每个每个微服务来讲,都要提供try , confirm , cancel这三个接口。” Bill接着说,“另外每个微服务也得有一个专门用来管理TCC的组件。” 

异常场景

张大胖心想,你说得简单,这都是所谓Happy Path , 在分布式环境中出错是必然的,他很快找到了第一个场景: 

场景1 : 库存服务的Try操作完成, 支付服务的Try操作没有完成, 怎么办?  

Bill说:“这很好处理,订单服务可以尝试去调用库存的Cancel操作(这应该是个幂等操作,可以多次调用),把冻结的库存释放。” 

张大胖说:“那如果出现网络问题,订单服务无法联系库存服务了呢?” 

“不用担心,库存服务的TCC组件能够发现冻结的时间已经超时,会自动把冻结的库存给释放。”  

场景2 : 两个微服务的Try操作都完成, 然后发生网络故障,导致两个Confirm都无法进行 

Bill说: “和第一种情况一下,TCC组件会发现超时,释放冻结的资源, 当然,冻结的这部分资源在释放前的一段时间内不可以被使用。” 

“可是,如果库存服务所在的机器已经挂掉了呢?怎么计算超时?” 张大胖问道。 

“这是个好问题,所以TCC系统必须得记录日志,把那些没有完成的事情记录下来,持久化到硬盘上。这样下次重启就可以接着执行了。” 

场景3 : Try操作都已完成,资源已经冻结,在第三步中库存服务Confirm成功,库存做了扣减, 但是支付服务挂掉了,余额还处于冻结状态, 怎么办? 

Bill 说道:“那可以多尝试几次, 让支付服务做Confirm操作(很明显,这个Confirm操作必须得是一个幂等操作才行)。如果实在是无法成功,那就可以让库存服务做Cancel操作。 如果还是不行,只有让人工介入了。” 

怪不得Bill一直在强调幂等性,原来真正的作用是这样啊。

转向BASE?

  张大胖想了想,似乎各种情况都能覆盖了, 但是还有实现层面的大问题: 

(1) 就是对于try (冻结资源), confrim , cancel(恢复资源)这样的操作都需要程序员去写代码实现。 

比如对于支付服务, 至少的实现三个方法: 

tryPayment(......) 

confirmPayment(......) 

cancelPayment(......)  

这样TCC框架才可以去调用。  

(2) 还得自己搞个TCC框架。 

Bill 笑道: “那没办法,分布式就是这么烦人。TCC框架倒是有一些现成的,比如Atomikos,tcc-transaction,Hmily等, 但是那些try,confrim, cancel是业务方法,程序员必须得写, 跑不掉的。”  

“就没有别的办法了吗?”

“有啊,也可以尝试下另外一个最终一致性的模型,叫做BASE。”  Bill随手又递过来一篇论文,名字是《BASE: An Acid Alternative》  

“有没有搞错 ! 又是英文的!”  

“你要是不想看英文的,就去看看老刘写的《Java帝国之宫廷内斗》吧!”

猜你喜欢

转载自blog.csdn.net/zl1zl2zl3/article/details/84940402