1. 什么是幂等

例如:
- 创建业务订单时,一次业务请求只能创建一个订单。
- 用户发起一笔付款请求,应该只扣除用户账号一次钱,即使遇到网络重发或系统bug重发时,也只扣除一次钱。
- 支付宝回调接口, 可能会多次回调, 必须处理重复回调。
- 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
- 前端重复提交选中的数据,后台应该只产生对应本次提交的一个响应结果。
等等
2. 产生原因
由于重复点击或者网络重发 eg:
(1) 点击提交按钮两次;
(2) 点击刷新按钮;
(3) 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
(4) 使用浏览器历史记录重复提交表单;
(5) 浏览器重复的HTTP请求;
(6) nginx重发等情况;
(7) 分布式RPC的try重发等;
3.“天然”幂等以及“人工”幂等
(1)“天然”幂等
编程中常见幂等
- select查询天然幂等
- delete删除也是幂等,删除同一个多次效果一样
- update直接更新某个值的,幂等
- update更新累加操作的,非幂等
- insert非幂等操作,每次新增一条
CRUD分析
操作 | 幂等性 |
---|---|
新增类请求(C) | 数据库自增主键,不具备幂等性 |
查询类动作(R) | 重复查询不会产生或变更新的数据,因此查询是天然具备幂等性 |
基于主键的计算式更新(U) | 不具备幂等性,即:UPDATE goods SET number=number-1 WHERE id=1 |
基于主键的非计算式更新(U) | 具备幂等性,即:UPDATE goods SET number=3 WHERE id=1 |
基于主建的删除(D) | 具备幂等性 |
业务层面都是逻辑删除(即Update操作)(U) | 具备幂等性 |
RESTful
方法 | 幂等性 | 对应CRUD操作 |
---|---|---|
POST | 不幂等 | C |
GET | 幂等 | R |
PUT | 幂等 | U |
DELETE | 幂等 | D |
PATCH | 不幂等 | U |
GET、PUT、DELETE都是幂等操作,而POST不是,以下进行分析:
GET请求很好理解,是对资源进行多次查询。
PUT请求可以认为将A修改成B,接下来一直修改,都是为B,与之前结果一致,属于基于主键的非计算机更新。
DELETE请求则是将资源删除后,后面多次删除这条数据,结构一致。
PATCH请求是基于主键的计算式更新。
(2)“人工”幂等
POST请求不是幂等操作,一次请求就添加一次数据,如果需要在POST请求时加上人为的幂等,下面就来说说幂等实现的方法。
4. 常见解决方案
- token令牌 -- 防止页面重复提交
- 悲观锁 -- 获取数据的时候加锁(锁表或锁行)
使用select ... for update ,这种和 synchronized 锁住先查再insert or update一样,但要避免死锁,效率也较差 针对单体 请求并发不大 可以推荐使用 - 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
如果只是更新已有的数据,没有必要对业务进行加锁,设计表结构时使用乐观锁,一般通过version来做乐观锁,这样既能保证执行效率,又能保证幂等。例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version# 。但是,乐观锁存在失效的情况,就是常说的ABA问题。如果version版本一直是自增的就不会出现ABA的情况。 - 去重表
这种方法适用于业务中有唯一标识的场景。假定以上场景,一个订单对应一个唯一的订单号,一个订单支付一次,这时候订单号可作为唯一标识,并且把订单号作为唯一索引(查出来只取符合条件的一条数据),将订单号写入去重表的操作,放入一个事务当中,如果订单重复创建,数据库会抛出唯一约束,事务进行回滚。这其实也用到的唯一ID,但与全局唯一ID相比,只是单单针对一个业务流程而已。
使用订单号orderNo做为去重表的唯一索引,每次请求都根据订单号向去重表中插入一条数据。第一次请求查询订单支付状态,当然订单没有支付,进行支付操作,无论成功与否,执行完后更新订单状态为成功或失败,删除去重表中的数据。后续的订单因为表中唯一索引而插入失败,则返回操作失败,直到第一次的请求完成(成功或失败)。可以看出防重表作用是加锁的功能。 - 分布式锁 -- redis(jedis、redisson)或zookeeper实现
防重表可以使用分布式锁代替,比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单支付请求完成,下次请求才能进来。相比去重表,将并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。 - 状态机 -- 状态变更, 更新数据时判断状态
状态机是一种数学模型,通常体现为一个状态转换图,可以理解为自动门有两个状态,open和closed,closed状态下,如果读取开门信号,那么状态就会切换为open。open状态下如果读取关门信号,状态就会切换为closed。 我们可以设置状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为1,付款失败为-1。在做状态机更新时,我们就这可以这样控制: update goods_order set status = #{status} where id = #{id} and status = #{status} - 支付缓冲区
把订单的支付请求都快速地接下来,一个快速接单的缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的待支付订单。优点是同步转异步,高吞吐量。缺点是不能及时地返回支付结果,需要后续监听支付结果的异步返回。
5. 本文实现
本文采用通过redis + token机制实现接口幂等性校验
实现思路
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示
如果不存在, 说明参数不合法或者是重复请求, 返回提示即可。
