如何保证接口的幂等性

51

为什么会产生接口幂等性问题

在计算机网络中,可能会遇到网络抖动、临时故障,或者服务调用失败,尤其是分布式系统中,接口调用失败更为常见。为了保证服务完整性,我们一般会发起接口重试调用,除了天然幂等的查询类接口不需要幂等处理以外,增删改类接口如果不处理幂等可能会对系统造成很大影响,因此接口的幂等设计尤为重要。

对于业务中需要考虑幂等的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这种情况的发生一般有以下几种常见的场景:

  • 前端重复提交:用于在表单提交时由于网络波动没有及时作出提交成功响应,导致用户主观任务没有提交成功,然后重复点击提交。

  • 接口超时重试:第三方调用接口时,由于超时等异常情况造成请求失败,都会添加重试机制,导致一个请求提交多次。

  • 消息重复消费:当使用MQ消息中间件时,如果发生消息中间件出现错误未及时提交消息,导致发生重复消费。

幂等性解决方案

方案一:前端控制

针对第一种场景下既然是用户重复提交导致的,那我们可以想办法让用户没法重复提交。在前端做拦截,比如当按钮点击一次后置灰或者隐藏。但是前端往往并不可靠,还是得后端处理才更放心。

方案二:Token机制

逻辑就是:用户进入表单页面首先调用后端获取Token,后端返回Token并存入Redis,当用户提交表单时将Token也作为入参,后端先删除Redis中的Token,删除成功则保存表单数据,失败则提示用户重复。

这里为什么不限判断Redis中是否有这个Token再删除?是因为要保证操作的原子性,极端情况下,第一个请求查询到Redis中存在这个Token还没来得及删除,第二个请求过来也查询到这个Token,那么还是会造成重复提交的问题。

Token机制需要先请求后期Token的接口,在有些情况下明显并不合适。我们大部分请求都是要落到数据库,所以我们可以从数据库着手。

方案三:唯一索引

这种方案很好理解,使用唯一索引避免脏数据的添加,当重复数据落到数据库时抛出异常,保证数据的唯一性,唯一索引可以支持插入、更新、删除业务操作。

具体流程步骤:

  • 建立一张去重表,其中某个字段建议唯一索引

  • 客户端去请求服务端,服务器会将这次请求的一些信息插入去重表

  • 由于表中某个字段是唯一索引,如果插入成功则表明表中没有这次请求信息执行后续逻辑,如果失败则代表重复请求

方案四:悲观锁

这里说的悲观锁是基于数据库层面,在获取数据时枷锁,党统十多个重复请求时,其他请求都无法进行操作,悲观锁只适用于更新操作。

例如:

select name from t_goods where id =1 for update;

值得注意的是:上面的id字段一定要是主键或者唯一索引,不然行锁就会变成表锁。悲观锁使用时一般配合事务一起使用,数据锁定时间可能会很长,根据实际情况选用。

当然,除了数据库层面,悲观锁的实现还可用Redis等缓存锁、Zookeeper锁

在请求量比较大的情况下,使用悲观锁明显不合适,这时候就到乐观锁上场了。

方案五:乐观锁

可以通过版本号实现,为表增加一个version字段,当数据需要更新时,先去数据库获取此时的version版本号:

select version from t_goods where id =1;

更新数据时首先要对比版本号,不想当说明已经有其他数据区更新数据了,提示更新失败。

update t_goods set count = count+1,version = version+1 wehre version = #{version};

还有一种是通过状态机制实现,其实也是乐观锁。这种方法适合在有状态流转的情况下,比如订单的创建和付款,创建肯定先于付款,这是可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来实现幂等性。

update t_goods set status = #{status} wehre id=1 and status < #{status};

同样,乐观锁也只适用于更新操作。

方案六:分布式锁

除了数据库层面的业务操作,有时候还有发短信、消息推送等操作,那数据库层面的锁就不合适了。这时候就要考虑代码层面的锁,而Java自带的锁只适用于单机环境,风不是集群部署情况下并不适用,这时候就可以采用分布式锁来实现(Redis或者Zookeeper)。

拿Redis分布式锁举例,比如一个订单发起支付请求,支付系统会去Redis换成中查询该订单的key,如果不存在则以key为订单号写入Redis。查询订单是否已支付,如果没有则进行支付,支付完成后删除该订单号对应的key。通过Redis做到了分布式锁,只有这次订单支付请求完成,下次请求才能进来。当然,一般key要设置过期时间,发生异常时要删除key。

下一篇写一下如何实现分布式锁。