本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~
在技术面试的时候,面试官特别喜欢问高并发场景,而一旦问了高并发场景,必然会问到在该场景下的接口幂等性问题,基本上是秤不离砣的。这感觉就像在下雨天,巧克力和音乐更配一样。
其实,幂不幂等跟高并发是没有关系的。那么,到底什么是接口幂等性呢?
在业务系统中,接口幂等性是指对某接口执行一次或多次相同操作的结果是相同的,不会产生额外影响。
一般来讲,对一个接口执行多次相同操作的原因,主要包括以下几种:
(1)用户点击按钮重复提交表单、刷新浏览器页面、点击浏览器回退按钮;
(2)黑客截取URL重复执行请求;
(3)系统内部超时重试或代码逻辑错误;
接下来我们来看下,有哪些比较常用的接口幂等性解决方案:
MySQL唯一索引
假设这样一个业务场景,用户在某电商平台上提交了购买商品的订单,业务流转到了支付环节,此时应该如何保证支付接口的幂等性,不会对用户的一笔订单出现重复支付的情况呢?
在“支付单”表中,为订单号创建唯一索引是一个最简单直接的方案。
具体流程如下图所示:
这种技术方案的优点非常明显,落地实现简单,只需要增加一个唯一索引,并处理好索引冲突返回的错误信息就可以。
当然,缺点也是有的,由于唯一索引不能使用Change Buffer(写缓冲),这会导致MySQL数据库磁盘的随机IO增加,从而影响数据库的性能。
MySQL去重表
MySQL去重表的技术方案是,在当前数据库中建一张表,表中只有主键和被创建成唯一索引的那个字段。
有人可能会觉得,这种方案跟上文中的“MySQL唯一索引”方案,从作用上来讲区别不大,反而更加复杂了,需要再额外创建一张表,代码改动范围也变大了,这不是多此一举吗?
其实不是的,存在即合理,我们还是以上文中的业务场景为例进行说明。
用户在某电商平台上提交了购买商品的订单,业务流转到了支付环节,此时由于用户银行卡中的余额不足导致支付失败,那这个订单和对应的支付的状态,也全部变更为“支付失败”。
几天之后用户发工资了,他银行卡中的余额已经充裕了,再次对该笔订单发起支付操作时,通常会有两种处理方案。
方案一:重新生成一个新的订单,发起支付操作。
方案二:对原来的订单发起支付操作。
如果选择了方案二,那就有可能会出现一个订单对应多个支付单的情况,多个支付单中包括一个成功的支付单和N个(N >= 0)失败的支付单。
支付单表记录如下图所示:
这种情况下,我们就不能在该表中为订单号创建唯一索引了,可以选择另建一个去重表,在该表中只存储状态为“待支付”或“支付成功”的支付单订单号,这样也可以起到保证接口幂等性的效果。
具体流程如下图所示:
这种技术方案与“MySQL去重表”方案的实现思路大同小异,是对其的一种补充,实现方式要复杂一些。
Redis Sequence Number
对Kafka消息中间件比较熟悉的同学应该知道,Kafka是支持生产者幂等的,不过只能保证单个生产者中单分区的幂等。
具体流程如下图所示:
我们从中可以看到,Kafka是通过引入PID和Sequence来实现生产者幂等的。
生产者每发送一次消息就会将Sequence进行递增,而Broker会将接收到新消息的Sequence与内存中的Sequence进行比对,校验前者是否为后者的值+1,若成立则接收该消息,若前者的值小于等于后者的值,则将该消息进行丢弃。
如果我们在业务系统中实现接口幂等性的时候,找不到一个字段可用来进行唯一校验,则可以参照该技术方案进行实现。
当然,在我们的业务系统中,可以通过Redis集中式缓存来代替Kafka中的内存进行实现,并通过Redis key过期失效的特性淘汰比较久远的Sequence。
这种技术方案用来解决特定场景的接口幂等性问题,实现起来并不复杂,但需要在主业务逻辑外的分支业务逻辑上,进行较为完善的考虑和处理。
Redis Token
Redis Token方案也是一种实现接口幂等性的常见技术方案,比较有意思的地方在于,它多了一个Token发放的步骤。
具体流程如下图所示:
在Redis Token技术方案中,很多同学都在纠结,当Token被判定为存在的时候,到底应该删除Token后执行业务操作,还是应该执行业务操作后删除Token。
我个人更倾向于前者,因为后者在执行业务操作成功,删除Token失败的情况下,此时若执行重试操作,会出现再次执行业务操作的情况,也就是没有彻底解决接口幂等性问题。
状态机
状态机方案的逻辑是,只允许处于特定状态的情况下,进行接口的业务逻辑操作。
比如:允许处于未兑奖状态的彩票进行兑奖,兑奖成功后将彩票状态从“未兑奖”变更为“已兑奖”。
当然,该流程我们需要考虑事务操作,来保证“将彩票状态变更为已兑奖”和“进行兑奖操作”两个业务操作的一致性。
关于锁方案
我在网上也看过一些解决接口幂等性问题的文章,里面会提到通过锁机制来解决接口幂等性问题,如:分布式锁、数据库悲观锁、数据库乐观锁等。
但是从根本上讲,锁机制解决的是“隔离性”问题。
我们就拿数据库锁来讲,当数据库中的多个事务同时存取同一数据时,若对该并发操作不加控制,就可能会出现读取和存储不正确数据的情况。
此时,就需要通过锁机制对公共数据资源进行并发控制,来保证数据的一致性和完整性。
但是,只有当一个事务对某条或某些数据进行读写操作时,才会对其进行加锁,操作完成后会对这些数据进行解锁,为其他处于等待中的事务让行,并不是一直处于加锁状态的。
而接口幂等性,则是指对该接口执行一次或多次相同操作的结果是相同的,不会产生额外影响。
也就是说,锁机制只能保证对某接口执行多次相同操作不是同时执行的,但这些操作还是可以顺序执行的,并不一定能保证多次相同操作的结果是相同的。