共计 1966 个字符,预计需要花费 5 分钟才能阅读完成。
导读 | 什么是幂等性? 所谓幂等,就是任意多次执行所产生的影响均与一次执行的影响相同。 |
在计算机应用中,可能遇到网络抖动,临时故障,或者服务调用失败,尤其是分布式系统中,接口调用失败更为常见。为了保证服务的完整性,我们可能会发起接口的重试调用,如果接口不处理幂等,可能对系统造成很大的影响,因此接口的幂等设计尤其更为重要。
对于业务中需要考虑幂等性的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这种情况的发生有以下几种常见的场景:
前端重复提交:用户在提交表单的时候,可能会因网络波动没有及时做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
接口超时重试:第三方调用接口时候,为了超时等异常情况造成的请求失败,都会添加重试机制,导致一个请求提交多次。
消息重复消费:当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。
那我们应该能怎样保证接口的幂等性呢?
可以思考一下,第一种场景下,既然是用户重复提交导致的,那我们可以想办法让用户没办法重复提交。
方案一:前端控制
在前端做拦截,比如按钮点击一次之后就置灰或者隐藏。但是往往前端并不可靠,还是得后端处理才更放心。
方案二:Token 机制
用户进入表单页面首先调用后台接口获取 token 并存入 redis,当用户提交表单时将 token 也作为入参,后端先删除 redis 中的 token,删除成功则保存表单数据,失败则提示用户重复提交。
这里为什么不先判断 redis 是否存在这个 token 再删除,是因为要保证操作的原子性,极端情况下,第一个请求查询到 redis 中存在这个 token,还没来得及删除,第二个请求进来,也查询到 redis 中存在这个 token,那么还是会造成重复提交的问题。
token 机制需要先请求获取 token 的接口,在有些情况下很明显并不合适。我们大部分请求都是要落到数据库的,所以我们可以从数据库着手。
方案三、唯一索引
这种方案就比较好理解了,使用唯一索引可以避免脏数据的添加,当插入重复数据时数据库会抛异常,保证了数据的唯一性。唯一索引可以支持插入、更新、删除业务操作。
方案四、悲观锁
这里所说的悲观锁是基于数据库层面的,在获取数据时进行加锁,当同时有多个重复请求时,其他请求都无法进行操作。悲观锁只适用于更新操作。
// 例如
select name from t_goods where id=1 for update;
注意:id 字段一定要是主键或者唯一索引,不然会锁住整张表,这是会死人的。悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用。
在请求量比较大的情况下,使用悲观锁明显不合适,这时候就到乐观锁上场了。
方案五、乐观锁
可以通过版本号实现,为表增加一个 version 字段,当数据需要更新时,先去数据库里获取此时的 version 版本号。
select version from t_goods where id=1
更新数据时首先要对比版本号,如果不相等说明已经有其他的请求去更新数据了,提示更新失败。
update t_goods set count=count+1,version=version+1 where version=#{version}
还有一种是通过状态机实现的,其实也是乐观锁的原理。这种方法适合在有状态流转的情况下,比如订单的创建和付款,订单的创建肯定是在付款之前,这时我们可以通过在设计状态字段时,使用 int 类型,并且通过值类型的大小来实现幂等性。
update t_goods set status=#{status} where id=1 and status
同样,乐观锁也只适用于更新操作。
方案六、分布式锁
有时候我们的业务不仅仅是操作数据库,也可能是发送短信、消息等等,那数据库层面的锁就不适合了。这种情况下就要考虑代码层面的锁了,而 java 的自带的锁在分布式集群部署的场景下并不适用,那么就可以采用分布式锁来实现(Redis 或 Zookeeper)。
拿 Redis 分布式锁举例,比如一个订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key,如果不存在,则以 Key 为订单号向 Redis 插入。查询订单是否已经支付,如果没有则进行支付,支付完成后删除该订单号的 Key。通过 Redis 做到了分布式锁,只有这次订单支付请求完成,下次请求才能进来。当然这里需要设置一个 Key 的过期时间,在发生异常的时候还要注意删除 Redis 的 Key。
总结
接口的幂等性是一个很常见的问题,需要根据具体业务场景的不同,选择合适的解决方案。