幂等性校验

1.什么是幂等性
    幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同
    在计算机中编程中,一个幂等操作的特点是  其任意多次执行所产生的影响均与一次执行的影响相同
    幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变

2.什么是接口幂等性
  在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。
这里的副作用是不会对结果产生破坏或者产生不可预料的结果。


3.为什么需要实现幂等性
在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,如:
    前端重复提交表单: 在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
    用户恶意进行刷单: 例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
    接口超时重复提交: 很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
    消息进行重复消费: 当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。
使用幂等性最大的优势在于使接口保证任何幂等性操作,免去因重试等造成系统产生的未知的问题。


4.常见场景
    订单接口,不能多次创建订单
    支付接口,重复支付同一笔订单只能扣一次钱
    支付宝回调接口,可能会多次回调,必须处理重复回调
    普通表单提交接口,因为网络超时等原因多次点击提交,只能成功一次
    .........


5.常见方案
5.1 数据库唯一主键
  数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。

5.2 数据库乐观锁

  数据库乐观锁方案一般只能适用于执行“更新操作”的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。

5.3 防重 Token 令牌
  简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 进行校验,如果 Key 存在就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 就返回重复执行的错误信息,这样来保证幂等操作。
    ① 服务端提供获取 Token 的接口,该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串。
    ② 客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。
    ③ 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
    ④ 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
    ⑤ 客户端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers。
    ⑥ 服务端接收到请求后从 Headers 中拿到 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。
    ⑦ 服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。


5.4 分布式锁
  redis(jedis、redisson)或zookeeper实现分布式锁

  如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段无法确定。那么这时候就可以引入分布式锁,通过第三方的系统(Redis 或 Zookeeper),在业务系统插入数据或更新数据,获取分布式锁,然后做操作,之后再释放锁。这样其实是把多线程并发锁的思路引入了多个系统,也就是分布式系统中的解决思路。

  要注意的是,某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户 ID + 后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁需要第三方系统提供))。


6、实战
思路分析
我们这里以第三种方案为例进行实战编码
代码编写总体思路:
    编写自定义注解,哪个接口需要做幂等校验,就打在哪个接口上,灵活方便,对业务代码无侵入性
    编写拦截器,作用:做接口幂等性校验,思路如下:
    不是请求方法的请求,全部放行
    请求方法的这些请求中,需要做过滤,没有打自定义注解的方法,全部放行,不需要做幂等校验
    打了自定义注解的方法,全部需要做幂等校验,获取请求头中的token值,然后做非空判断,然后看redis是否存在,不存在直接返回错误提示
    存在,则根据token删除Redis的值:如果删除成功,放行,如果删除失败,直接返回错误提示

 

7 如何判断订单请求是重复的

  • 插入订单前,先查一下订单表,有无重复订单? 难以用SQL条件定义到底什么是“重复订单”
  • 订单的用户、商品、价格一样就是重复订单? 万一这用户就是连续下了俩一模一样订单呢?

所以保证幂等性要做到:

(1)每个请求须有唯一标识

  比如订单支付请求,得包含订单 id,一个订单 id 最多只能成功支付一次。

(2)每次处理完请求后,须有记录标识该请求已被处理

  在 MySQL 中记录一个状态字段。如支付之前记录一条这个订单的支付流水。

(3)每次接收请求时,判断之前是否处理过

  若有一个订单已支付,就肯定已有一条支付流水。若重复发送这个请求,则此时先插入/支付流水,发现 orderId 已存在,唯一约束生效,报错重复 Key。就不会再重复扣款。

  在往 DB 插记录时,一般不提供主键,而由 DB 在插入时自动生成。这样重复的请求就会导致插入重复的数据。MySQL 的主键自带唯一性约束,若在一条 INSERT 语句提供主键,且该主键值在表中已存在,则该条 INSERT 会执行失败。因此可利用 DB 的“主键唯一约束”,在插数据时带上主键,以此实现创建订单接口的幂等性。

  给 Order 服务添加一个“orderId 生成”的接口,无参,返回值就是一个【全局唯一】订单号。在用户进入创建订单页面时,前端页面先调用该 orderId 生成接口得到一个订单号,在用户提交订单时,在创建订单的请求中携带该订单号。该订单号其实就是订单表的主键,于是,重复请求中带的都是同一订单号。订单服务在订单表中插入数据的时候,执行的这些重复 INSERT 语句中的主键,也都是同一个订单号。而 DB 唯一约束保证,只有一次 INSERT 执行成功。实际要结合业务,如使用 Redis,用 orderId 作为唯一K。只有成功插入这个支付流水,才可执行扣款。

posted @ 2023-12-07 13:24  壹索007  阅读(221)  评论(0编辑  收藏  举报