老码农讲述后端风云
作者:网友投稿 时间:2018-10-25 09:06
经过一个月的折腾,终于分家了。
原来的订单模块,库存模块,积分模块,支付模块......摇身一变,成为了一个个独立系统。

主人给这些独立的系统起了一个时髦的名字: 微服务!
有些微服务是主人的心头肉,他们“霸占”了一台或者多台机器,像我这个积分模块,哦不,是积分系统,不受人待见,只能委屈一下,和另外几个家伙共享一台机器了。
主人说我们现在是分布式的系统了,大家要齐心协力,共同完成原来的任务。
原来大伙都居住在一个JVM中,模块之间都是直接的函数调用,如今每个人对外提供的都是基于HTTP的API: 想要访问别人,需要准备好JSON数据,然后通过HTTP发送给过去,人家处理以后,再发送一个JSON的响应。


真是麻烦,哪怕一次最简单的沟通都要跨越网络了。
重复执行
提起这网络我心里就来气, 想想原来大家都在一个进程中,那调用速度才叫爽。 现在可好,一是慢如蜗牛,二是不可靠,时不时就会出错。
30毫秒以前,订单这家伙调用我的接口,要给一个叫做U0002的用户增加200积分,我很乐意地执行了。
POST /xxx/BonusPoint/U0002
{"value:200"}
可是,当我想把积分的调用结果告诉订单系统的时候,发现网络已经断开,发送失败。 怎么办? 我想反正已经执行过了,Forget it !
可是订单那小子对我这边的情况一无所知,心里琢磨着也许是我这边出错了, 死心眼的他又发起了同样的调用。
对我而言,这个新的调用和之前的那个没有一毛钱关系。(不要忘了,HTTP是没有状态的)我就老老实实地再执行一遍。
结果可想而知,用户"U0002"的积分被无端地增加了两次!
订单小伙说:“这样不行啊,你得记住我曾经发起过调用,这样第二次就不用执行了!”
“开玩笑!HTTP是无状态的, 我怎么可能记录你曾经的调用?”
“我们可以增加一点儿状态, 每次调用,我给你发一个Transaction ID, 简称TxID,你处理完以后, 需要把这个TxID,UserID, 积分等信息给保存到数据库中。”
POST /xxx/BonusPoint/U0002
{"txid":"T0001","value":"200"}
我说:“这有什么用?”
“每次执行的时候,你都可以从数据库中查一下啊,如果看到同样的TxID已经存在了,那就说明之前执行过,就不用重复执行了。如果不存在,才真正去执行。”


这倒是一个好主意,虽然我增加了一点工作量,需要一点额外的存储空间(正好借此机会要一个好点儿的服务器!),但是却有一个很好的特性: 对于同一个TxID,无论调用多少次,那执行效果就如同执行了一次,肯定不会出错。
后来我们才知道,人类把这个特性叫做幂等性。
一般来说,在后端数据不变的情况下,读操作都是幂等的,不管读取多少次,得到的结果都是一样的。 但是写操作就不同了,每次操作都会导致数据发生变化。要想让一个操作可以执行多次,而没有副作用,一定得想办法记录下这个操作执行过没有。
遗漏执行
我把新API告诉大家: 一定要给我传递过来一个TxID啊, 否则别怪我不处理!
这一天,我接收到了两个HTTP的调用,第一次是这样的:
POST /xxx/BonusPoint/U0002
{"txid":"T0010","value":"200"}
于是我很高兴地执行了,并且把T0010这个txid给保存了下来。
然后第二个调用又来了, 和第一个一模一样:
POST /xxx/BonusPoint/U0002
{"txid":"T0010","value":"200"}
我用T0010一查,数据库已经存在,我就知道,不用再处理了, 直接告诉对方:处理完成。
没想到的是, 用户很快就抱怨了:为什么我增加了两次积分(每次200),但实际上只增加了一次呢?
这肯定不是我的锅, 我这边没有任何问题,一切按照设计执行。 我说:“刚才是谁发起的调用,检查下调用的日志!”
调查了调用方的日志才发现,那两次调用是两个系统发出的!
碰巧,这两个系统生成了相同的TxID : T0010 , 这就导致我认为是同一个调用的两次尝试, 实际上这是这是完全不同的两次调用。
真相大白,TxID是罪魁祸首,可见这个TxID在整个分布式的系统中不能重复,一定得是唯一的才行。
分布式ID
怎么在一个分布式的系统中生成为一个唯一的ID呢?




