国外网站 网站 推荐,河南建设厅二建公示网站首页,汕头澄海房价,男士手表网站目录 一、简介1.1 什么是幂等#xff1f;1.2 为什么需要幂等性#xff1f;1.3 接口超时#xff0c;应该如何处理#xff1f;1.4 幂等性对系统的影响 二、Restful API 接口的幂等性三、实现方式3.1 数据库层面#xff0c;主键/唯一索引冲突3.2 数据库层面#xff0c;乐观锁… 目录 一、简介1.1 什么是幂等1.2 为什么需要幂等性1.3 接口超时应该如何处理1.4 幂等性对系统的影响 二、Restful API 接口的幂等性三、实现方式3.1 数据库层面主键/唯一索引冲突3.2 数据库层面乐观锁3.3 数据库层面悲观锁select for update【不推荐】3.4 数据库层面状态机3.5 应用层面token令牌【不推荐】3.6 应用层面分布式锁【推荐】 四、Java 代码实现4.1 NotRepeat 注解4.2 AOP 切面4.3 RedisUtils 工具类4.4 测试类4.5 测试结果 一、简介
1.1 什么是幂等
幂等 是一个数学与计算机科学概念英文 idempotent [aɪˈdempətənt]。
在数学中幂等用函数表达式就是f(x) f(f(x))。比如 求绝对值 的函数就是幂等的abs(x) abs(abs(x))。计算机科学中幂等表示一次和多次请求某一个资源应该具有同样的作用。
满足幂等条件的性能叫做 幂等性。
1.2 为什么需要幂等性 我们开发一个转账功能假设我们调用下游接口 超时 了。一般情况下超时可能是网络传输丢包的问题也可能是请求时没送到还有可能是请求到了返回结果却丢了。这时候我们是否可以 重试 呢如果重试的话是否会多赚了一笔钱呢 在我们日常开发中会存在各种不同系统之间的相互远程调用。调用远程服务会有三个状态成功、失败、超时。
前两者都是明确的状态但超时则是 未知状态。我们转账 超时 的时候如果下游转账系统做好 幂等性校验我们判断超时后直接发起重试既可以保证转账正常进行又可以保证不会多转一笔。
日常开发中需要考虑幂等性的场景
前端重复提交比如提交 form 表单时如果快速点击提交按钮就可能产生两条一样的数据。用户恶意刷单例如在用户投票这种功能时如果用户针对一个用户进行重复提交投票这样会导致接口接收到用户重复提交的投票信息会使投票结果与事实严重不符。接口超时重复提交很多时候 HTTP 客户端工具都默认开启超时重试的机制尤其是第三方调用接口的时候为了防止网络波动等造成的请求失败都会添加重试机制导致一个请求提交多次。MQ重复消费消费者读取消息时有可能会读取到重复消息。
1.3 接口超时应该如何处理
如果我们调用下游接口超时了我们应该如何处理其实从生产者和消费者两个角度来看有两种方案处理
方案一消费者角度。在接口超时后调用下游接口检查数据状态 如果查询到是成功就走成功流程如果是失败就按失败处理重新请求。 方案二生产者角度。下游接口支持幂等上有系统如果调用超时发起重试即可。 两种方案都是可以的但如果是 MQ重复消费的场景方案一处理并不是很妥当所以我们还是要求下游系统 对外接口支持幂等。
1.4 幂等性对系统的影响
幂等性是为了简化客户端逻辑处理能防止重复提交等操作但却增加了服务端的逻辑复杂性和成本其主要是
把并行执行的功能改为串行执行降低了执行效率。增加了额外控制幂等的业务逻辑复杂化了业务功能。
在使用前需要根据实际业务场景具体分析除了业务上的特殊要求外一般情况下不需要引入接口的幂等性。
二、Restful API 接口的幂等性
Restful 推荐的几种 HTTP 接口方法中不同的请求对幂等性的要求不同
请求类型是否幂等描述GET是GET 方法用于获取资源。一般不会也不应当对系统资源进行改变所以是幂等的。POST否POST 方法用于创建新的资源。每次执行都会新增数据所以不是幂等的。PUT不一定PUT 方法一般用于修改资源。该操作分情况判断是否满足幂等更新中直接根据某个值进行更新也能保持幂等。不过执行累加操作的更新是非幂等的。DELETE不一定DELETE 方法一般用于删除资源。该操作分情况判断是否满足幂等当根据唯一值进行删除时满足幂等但是带查询条件的删除则不一定满足。例如根据条件删除一批数据后又有新增数据满足该条件再执行就会将新增数据删除需要根据业务判断是否校验幂等。 三、实现方式
3.1 数据库层面主键/唯一索引冲突
日常开发中为了实现接口幂等性校验可以这样实现
提前在数据库中为唯一存在的字段如唯一流水号 bizSeq 字段添加唯一索引或者直接设置为主键。请求过来直接将数据插入、更新到数据库中并进行 try-catch 捕获。如果抛出异常说明为重复请求可以直接返回成功或提示请求重复。 补充 也可以新建一张 防止重复点击表将唯一标识放到表中存为主键或唯一索引然后配合 tra-catch 对重复点击的请求进行处理。 伪代码如下
/*** 幂等处理*/
Rsp idempotentRequest req{try {insert(req);} catch (DuplicateKeyException e) {//拦截是重复请求直接返回成功log.info(主键冲突是重复请求直接返回成功流水号{},bizSeq);return rsp;}//正常处理请求dealRequest(req);return rsp;
}3.2 数据库层面乐观锁
乐观锁乐观锁在操作数据时非常乐观认为别人不会同时在修改数据。因此乐观锁不会上锁只是在执行更新的时候判断一下在此期间是否有人修改了数据。
乐观锁的实现
就是给表多加一列 version 版本号每次更新数据前先查出来确认下是不是刚刚的版本号没有改动再去执行更新并升级 versionversionversion1。
比如我们更新前先查一下数据查出来的版本号是 version1。
select order_idversion from order where order_id666然后使用 version1 和 订单ID 一起作为条件再去更新
update order set version version 1statusP where order_id666 and version 1最后更新成功才可以处理业务逻辑如果更新失败默认为重复请求直接返回。
流程图如下 为什么版本号建议自增呢 因为乐观锁存在 ABA 的问题如果 version 版本一直是自增的就不会出现 ABA 的情况。 3.3 数据库层面悲观锁select for update【不推荐】
悲观锁通俗点讲就是很悲观每次去操作数据时都觉得别人中途会修改所以每次在拿数据的时候都会上锁。官方点讲就是共享资源每次只给一个线程使用其他线程阻塞用完后再把资源转让给其它资源。
悲观锁的实现 在订单业务场景中假设先查询出订单如果查到的是处理中状态就处理完业务然后再更新订单状态为完成。如果查到订单并且不是处理中的状态则直接返回。 可以使用数据库悲观锁select … for update解决这个问题
begin; # 1.开始事务
select * from order where order_id666 for update # 查询订单判断状态,锁住这条记录
ifstatus !处理中{//非处理中状态直接返回return ;
}
## 处理业务逻辑
update order set status完成 where order_id666 # 更新完成
commit; # 5.提交事务注意
这里的 order_id 需要是主键或索引只用行级锁锁住这条数据即可如果不是主键或索引会锁住整张表。悲观锁在同一事务操作过程中锁住了一行数据。这样 别的请求过来只能等待如果当前事务耗时比较长就很影响接口性能。所以一般 不建议用悲观锁的实现方式。
3.4 数据库层面状态机
很多业务表都是由状态的比如转账流水表就会有 0-待处理1-处理中2-成功3-失败的状态。转账流水更新的时候都会涉及流水状态更新即涉及 状态机即状态变更图。我们可以利用状态机来实现幂等性校验。
状态机的实现
比如转账成功后把 处理中 的转账流水更新为成功的状态SQL 如下
update transfor_flow set status 2 where biz_seq666 and status 1;流程图如下 第1次请求来时bizSeq 流水号是 666该流水的状态是处理中值是 1要更新为 2-成功的状态所以该 update 语句可以正常更新数据sql 执行结果的影响行数是 1流水状态最后变成了 2。第2次请求也过来了如果它的流水号还是 666因为该流水状态已经变为 2-成功的状态所以更新结果是0不会再处理业务逻辑接口直接返回。
伪代码实现如下
Rsp idempotentTransferRequest req{String bizSeq req.getBizSeq();int rows update transfr_flow set status2 where biz_seq#{bizSeq} and status1;if(rows1){log.info(“更新成功,可以处理该请求”);//其他业务逻辑处理return rsp;} else if(rows 0) {log.info(“更新不成功不处理该请求”);//不处理直接返回return rsp;}log.warn(数据异常)return rsp
}3.5 应用层面token令牌【不推荐】
token 唯一令牌方案一般包括两个请求阶段
客户端请求申请获取请求接口用的token服务端生成token返回客户端带着token请求服务端校验token。
流程图如下 客户端发送请求申请获取 token。服务端生成全局唯一的 token保存到 redis 中一般会设置一个过期时间然后返回给客户端。客户端带着 token发起请求。服务端去 redis 确认 token 是否存在一般用 redis.del(token) 的方式如果存在会删除成功即处理业务逻辑如果删除失败则直接返回结果。 补充 这种方式个人不推荐说两方面原因 需要前后端联调才能实现存在沟通成本最终效果可能与设想不一致。如果前端多次获取多个 token还是可以重复请求的如果再在获取 token 处加分布式锁控制就不如直接用分布式锁来控制幂等性了即下面这种解决方式。 3.6 应用层面分布式锁【推荐】
分布式锁 实现幂等性的逻辑就是请求过来时先去尝试获取分布式锁如果获取成功就执行业务逻辑反之获取失败的话就舍弃请求直接返回成功。
流程图如下 分布式锁可以使用 Redis也可以使用 Zookeeper不过 Redis 相对好点比较轻量级。Redis 分布式锁可以使用 setIfAbsent() 来实现注意分布式锁的 key 必须为业务的唯一标识。Redis 执行设置 key 的动作时要设置过期时间防止释放锁失败。这个过期时间不能太短太短拦截不了重复请求也不能设置太长请求量多的话会占用存储空间。 四、Java 代码实现
4.1 NotRepeat 注解
NotRepeat 注解用于修饰需要进行幂等性校验的类。
NotRepeat.java
import java.lang.annotation.*;/*** 幂等性校验注解*/
Target(ElementType.METHOD)
Retention(RetentionPolicy.RUNTIME)
Documented
public interface NotRepeat {}4.2 AOP 切面
AOP切面监控被 Idempotent 注解修饰的方法调用实现幂等性校验逻辑。
IdempotentAOP.java
import com.demo.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;/*** 重复点击校验*/
Slf4j
Aspect
Component
public class IdempotentAOP {/** Redis前缀 */private String API_IDEMPOTENT_CHECK API_IDEMPOTENT_CHECK:;Resourceprivate HttpServletRequest request;Resourceprivate RedisUtils redisUtils;/*** 定义切面*/Pointcut(annotation(com.demo.annotation.NotRepeat))public void notRepeat() {}/*** 在接口原有的方法执行前将会首先执行此处的代码*/Before(notRepeat())public void doBefore(JoinPoint joinPoint) {String uri request.getRequestURI();// 登录后才做校验UserInfo loginUser AuthUtil.getLoginUser();if (loginUser ! null) {assert uri ! null;String key loginUser.getAccount() _ uri;log.info( 【IDEMPOTENT】开始幂等性校验加锁account: {}uri: {}, loginUser.getAccount(), uri);// 加分布式锁boolean lockSuccess redisUtils.setIfAbsent(API_IDEMPOTENT_CHECK key, 1, 30, TimeUnit.MINUTES);log.info( 【IDEMPOTENT】分布式锁是否加锁成功:{}, lockSuccess);if (!lockSuccess) {if (uri.contains(contract/saveDraftContract)) {log.error( 【IDEMPOTENT】文件保存中请稍后);throw new IllegalArgumentException(文件保存中请稍后);} else if (uri.contains(contract/saveContract)) {log.error( 【IDEMPOTENT】文件发起中请稍后);throw new IllegalArgumentException(文件发起中请稍后);}}}}/*** 在接口原有的方法执行后都会执行此处的代码final*/After(notRepeat())public void doAfter(JoinPoint joinPoint) {// 释放锁String uri request.getRequestURI();assert uri ! null;UserInfo loginUser SysUserUtil.getloginUser();if (loginUser ! null) {String key loginUser.getAccount() _ uri;log.info( 【IDEMPOTENT】幂等性校验结束释放锁account: {}uri: {}, loginUser.getAccount(), uri);redisUtils.del(API_IDEMPOTENT_CHECK key);}}
}4.3 RedisUtils 工具类
RedisUtils.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.concurrent.TimeUnit;/*** redis工具类*/
Slf4j
Component
public class RedisUtils {/*** 默认RedisObjectSerializer序列化*/Autowiredprivate RedisTemplateString, Object redisTemplate;/*** 加分布式锁*/public boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);}/*** 释放锁*/public void del(String... keys) {if (keys ! null keys.length 0) {//将参数key转为集合redisTemplate.delete(Arrays.asList(keys));}}
}4.4 测试类
OrderController.java
import com.demo.annotation.NotRepeat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Arrays;
import java.util.List;/*** 幂等性校验测试类*/
RequestMapping(/order)
RestController
public class OrderController {NotRepeatGetMapping(/orderList)public ListString orderList() {// 查询列表return Arrays.asList(Order_A, Order_B, Order_C);// throw new RuntimeException(参数错误);}
}4.5 测试结果
请求地址http://localhost:8080/order/orderList
日志信息如下 经测试加锁后正常处理业务、抛出异常都可以正常释放锁。
整理完毕完结撒花~ 参考地址
1.实战实现幂等的8种方案https://blog.csdn.net/sufu1065/article/details/122335349
2.Java中的幂等性https://blog.csdn.net/JewaveOxford/article/details/103578372
3.Spring Boot 实现接口幂等性的 4 种方案还有谁不会https://blog.csdn.net/youanyyou/article/details/114464708