接口幂等性的几种解决方案

接口幂等性的几种解决方案

幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的

很多重要的情况都需要幂等的特性来支持。比如如下的几种业务场景:

  • 前端重复提交数据,应该后台只产生对应这个数据的一个响应;
  • 我们发起一笔付款请求,应该只扣用户账户一次钱;
  • 发送短信给用户,也应该也只能只发一次;
  • 创建业务订单,一次业务请求只能创建一个,创建多个就会出大问题等等。

幂等性方案

在设计幂等接口时,重点关注新增接口和更新接口。因为查询和删除操作,天生是幂等的(有些删除比较特殊,也不满足幂等性,关于这一点该文章不展开说明),不需要我们提供额外的技术手段来保证幂等性。

对于新增和更新接口,大致有以下几种方案可以保证接口幂等性。

1、唯一索引:防止新增脏数据。

比如:每个用户只能有一个资金账户,怎么防止给用户创建了多个账户呢?给资金账户表中的用户 ID 加唯一索引。

方案要点:唯一索引或唯一组合索引,用来防止新增数据的时候存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在,直接返回查询结果);

2、token机制:防止页面重复提交。

原理上一般通过 redis 来实现。当客户端请求页面时,服务器会生成一个随机数 Token,将该 Token 放置到 redis 缓存中,然后将Token发给客户端(一般通过构造hidden表单)。 下次客户端提交请求时,Token会随着表单一起提交到服务器端。

服务器端第一次验证相同之后,会将 redis 中的 Token 值删除,若用户重复提交,第二次验证会失败,因为用户提交的表单中的 Token redis 中 Token 已经删除了。

3、悲观锁

获取数据的时候加锁获取。

select * from table_xxx where id='xxx' for update;
注意:id 字段一定是主键或者唯一索引。悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会比较长,影响服务器的性能;

4、乐观锁

乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。

乐观锁可以通过添加 version 来实现。

update table_xxx set name=#name#,version=version+1 where version=#version#;

5、分布式锁

如果是分布式系统,无法构建全局唯一索引,这时候可以引入分布式锁,通过中间件(redis或zookeeper)构建分布式锁。

一般的操作都是,先去获取锁,做操作,之后释放锁,这其实是把多线程并发的思路,引入多个系统。

方案要点:锁的标示怎么区分业务场景;一般是通过(用户ID+业务场景等)获取分布式锁

6、select + insert

并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。

注意:核心高并发流程不要用这种方法;

7、状态机幂等

比如在设计订单状态的时候,肯定会涉及到状态机(订单状态变更),状态在不同的情况下可能需要的处理是不一样的。 如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。

总结

幂等与是不是分布式高并发还有JavaEE都没有关系。关键是你的业务场景是不是幂等的。

在设计系统时,我们始终要考虑的问题是怎么高效的实现系统功能,并且数据也要准确,比如不能出现多扣款,多打款等等的问题。这时候就需要仔细考虑如何实现幂等性操作了。

以上内容转自:

https://piterjia.github.io/2020/05/04/api-idempotent/#幂等性方案

实践

最近项目中遇到类似于订单服务的功能,需要保证人为或网络异常造成的多次操作的幂等性。一开始考虑用乐观锁,在表中添加一个version来控制,但是因为功能是类似于订单,故在新增时还是会操作脏数据问题,故最后选用唯一索引,新建了一个交易流水表,交易流水表中绑定操作人和被操作订单号,交易成功就生成一个流水,每次执行交易前检查是否以及被执行。目前,在我看来,乐观锁更适合于修改或者删除操作,不适用于新增操作。而唯一索引就更适合新增场景,用唯一索引或者唯一组合索引保证数据的唯一性。

posted @ 2022-06-06 20:41  浪漫主义程序员  阅读(512)  评论(0编辑  收藏  举报