创建订单实现幂等的一点思考
幂等的概念
大部分文章都会说,同一个操作,进行多次操作后,结果是一样的,就可以说这个操作是支持幂等的。感觉不太准确,比如一个http get操作,可能每次的结果都不一样,但是其实是幂等的。看了很多文章,感觉下面的定义比较准确:
一个操作如果多次任意执行所产生的影响(或者叫副作用),都是相同的。
创建订单的幂等
如果一个用户分两次下单,购买的商品都是一样的。
第一次请求:user1:购买一个商品product1;
第二次请求:user1:还是购买一个商品product1;
这种场景也很常见,是需要生成两个订单的。这样子看起来貌似创建订单的接口做不了幂等,因为业务数据一样的情况下,还是需要生成多个订单。但是这样子设计还是有个坑,万一创建订单的接口超时了呢?并且调用方进行了重试的话,那就可能变成用户其实想下一个单,但是订单系统其实生成了多个订单。比如说:
调用方发起创建订单的请求,订单系统收到了,并成功创建订单了。但是由于系统原因或者网络原因等,没有及时告知调用方订单已经创建成功,调用方一直等待回复,直到超时了。调用方再次发起了创建订单的请求,这个时候就可能会生成多个订单。
如果订单接口不支持幂等的情况下,如何应付这种情况呢?有两种方法
第一种:
当调用方调用订单接口超时了,是会收到异常的,这个时候调用方捕获到这个异常后,
不要进行重试操作了,调用订单的一个回滚接口,将订单取消掉。
虽然看起来很low,但是还是有人这么做的。
第二种:
让订单系统提供一个订单是否创建成功的查询接口,根据一些关键业务字段去查询,如果查询到已经创建成功了,则调用方不要重试了。
上面两种方案都有人用过,但是都没实现幂等。其实针对上面的场景,用幂等来设计也不是很难。可以使用一个唯一的流水号ID,用来标识是不是同一个请求或者交易。这种ID通常都需要具备 全局唯一性
。假设让客户端来生成这个ID,每个创建订单的请求生成一个唯一的ID。那么订单系统如何根据来实现幂等呢?通常有两种。
第一种:
先将这个ID保存到一个流水表里面,并且流水表中将这个ID设置为
UNIQUE KEY
,如果插入出现冲突了,则说明这个创建订单的请求已经处理过了,直接返回之前的操作结果。
第二种:
根据ID读取流水表,如果没有读取到,则创建订单和插入流水表。如果读取到了,则返回之前的操作结果。
不建议使用第二种方式,因为大部分情况下的请求都不是重试来的,让100%的请求都要去读取流水表,实在是不应该。另外,读取流水表的操作也是有潜在风险的,因为用数据库的读检查来确保数据存在性可能因为竞争而不生效,存在竞态条件。
建议用第一种方案,因为本来流水表就是要插入,顺便利用 UNIQUE KEY
的冲突特性来判断。
现在我们用第一种方案完整描述一下整个处理过程。
当调用方携带流水号ID调用创建订单的接口,如果出现超时了,调用方不知道订单到底创建成功还是失败,这个时候,用
同一个
流水号进行重试,订单系统虽然收到了两个请求,但是由于流水号ID是同一个,可以根据流水表来做幂等操作。并告知对方订单创建成功与否。
这里又有一个坑,万一调用方进行重试的时候,重新生成一个流水号,那就没得救了,会生成多个订单了。这个只能让客户端来保证了。
关于多重幂等
假设创建订单的接口在创建订单的时候,还需要依赖一些外部系统,如果订单创建接口实现了幂等,但是外部接口没有实现幂等的话,还是可能出现 幂等漏洞
。属于整个链路幂等的问题了。好复杂。目前还没想好如何处理这种情况呢。
思考题
调用方创建唯一ID,服务端用流水表这种方式实现幂等,非常依赖这个唯一ID。万一这个ID丢失了呢?咋破?目前我也在思考这个问题。
创建订单实现幂等的一点思考,首发于 文章 - 伯乐在线。