分布式就是魔鬼啊!
张大胖最近十分感慨,他所在的公司原来有个电商系统,后来随着用户量越来越大,对系统的可用性要求越来越高。 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一直在强调幂等性,原来真正的作用是这样啊。
张大胖想了想,似乎各种情况都能覆盖了, 但是还有实现层面的大问题:
(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帝国之宫廷内斗》吧!”
帮神策数据做个调查“男程序员之理想中的另一半”, 填写问券就可能获得价值299的双肩包(抽奖,一共10个),程序员必备之良品。
切记, 最后一个问题,一定要填写自己的联系方式,否则抽奖就没你的份儿了。
ps: 这个调查问卷是用问卷星做的,填写完以后,问卷星会显示讨厌的抽奖/广告,直接关闭即可。双肩包抽奖是神策数据工作人员手动做的,和问卷星显示的抽奖没有关系。
长按二维码或者点击阅读原文,参与抽奖。