赞
踩
目录
接口幂等性就是用户对于同一操作
发起的一次请求或者多次请求
的结果是一致的
,不会因 为多次点击而产生了副作用; 比如说经典的支付场景:用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了条,这就没有保证接口的幂等性。
所以我们要解决的就是POST
请求!
大概主流的解决方案:
咱们的解决方案就是redis的set机制!
同一个用户,任何POST保存相关的接口,1s内只能提交一次。
完全使用后端来进行控制,前端可以加限制,不过体验不好!
后端通过自定义注解,在需要防幂等接口上添加注解,利用AOP切片,减少和业务的耦合! 在切片中获取用户的token、user_id、url
构成redis的唯一key! 第一次请求会先判断key是否存在,如果不存在,则往redis添加一个主键key,设置过期时间;
如果有异常会主动删除key,万一没有删除失败,等待1s,redis也会自动删除,时间误差是可以接受的! 第二个请求过来,先判断key是否存在,如果存在,则是重复提交,返回保存信息!!
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>1.18.2</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-aop</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!-- Druid -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>1.1.16</version>
- </dependency>
- <!--jdbc-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-jdbc</artifactId>
- </dependency>
-
- <!-- mysql -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!-- mybatis-plus -->
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>3.5.1</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- server:
- port: 8081
-
- spring:
- redis:
- host: localhost
- port: 6379
- password: 123456
- datasource:
- #使用阿里的Druid
- type: com.alibaba.druid.pool.DruidDataSource
- driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC
- username: root
- password:
- /**
- * @author yunyan
- * @date 2023/6/11 15:20
- */
- @Configuration
- public class RedisConfig {
-
- @Bean
- @SuppressWarnings(value = { "unchecked", "rawtypes" })
- public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
- {
- RedisTemplate<Object, Object> template = new RedisTemplate<>();
- template.setConnectionFactory(connectionFactory);
- Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
-
- // 使用StringRedisSerializer来序列化和反序列化redis的key值
- template.setKeySerializer(new StringRedisSerializer());
- template.setValueSerializer(serializer);
-
- // Hash的key也采用StringRedisSerializer的序列化方式
- template.setHashKeySerializer(new StringRedisSerializer());
- template.setHashValueSerializer(serializer);
-
- template.afterPropertiesSet();
- return template;
- }
- }
- /**
- * 自定义注解防止表单重复提交
- * @author yunyan
- * @date 2023/6/11 15:25
- */
- @Target(ElementType.METHOD) // 注解只能用于方法
- @Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
- @Documented
- public @interface RepeatSubmit {
-
- /**
- * 防重复操作过期时间,默认1s
- */
- long expireTime() default 1;
- }
- /**
- * @author 云烟
- * @date 2023/6/11 16:00
- */
- @Slf4j
- @Component
- @Aspect
- public class RepeatSubmitAspect {
-
- @Autowired
- private RedisTemplate redisTemplate;
- /**
- * 定义切点
- */
- @Pointcut("@annotation(com.example.demo.annotation.RepeatSubmit)")
- public void repeatSubmit() {}
-
- @Around("repeatSubmit()")
- public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
-
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
- .getRequestAttributes();
- HttpServletRequest request = attributes.getRequest();
- Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
- // 获取防重复提交注解
- RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
- // 获取token当做key,这里是新后端项目获取不到,先写死
- // String token = request.getHeader("Authorization");
- String tokenKey = "hhhhhhh,nihao";
- if (StringUtils.isBlank(token)) {
- throw new RuntimeException("token不存在,请登录!");
- }
- String url = request.getRequestURI();
- /**
- * 通过前缀 + url + token 来生成redis上的 key
- *
- */
- String redisKey = "repeat_submit_key:"
- .concat(url)
- .concat(tokenKey);
- log.info("==========redisKey ====== {}",redisKey);
-
- if (!redisTemplate.hasKey(redisKey)) {
- redisTemplate.opsForValue().set(redisKey, redisKey, annotation.expireTime(), TimeUnit.SECONDS);
- try {
- //正常执行方法并返回
- return joinPoint.proceed();
- } catch (Throwable throwable) {
- redisTemplate.delete(redisKey);
- throw new Throwable(throwable);
- }
- } else {
- // 抛出异常
- throw new Throwable("请勿重复提交");
- }
- }
- }
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public class Result<T> {
- private Integer code;
-
- private String msg;
-
- private T data;
-
- //成功码
- public static final Integer SUCCESS_CODE = 200;
- //成功消息
- public static final String SUCCESS_MSG = "SUCCESS";
-
- //失败
- public static final Integer ERROR_CODE = 201;
- public static final String ERROR_MSG = "系统异常,请联系管理员";
- //没有权限的响应码
- public static final Integer NO_AUTH_COOD = 999;
-
- //执行成功
- public static <T> Result<T> success(T data){
- return new Result<>(SUCCESS_CODE,SUCCESS_MSG,data);
- }
- //执行失败
- public static <T> Result failed(String msg){
- msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
- return new Result(ERROR_CODE,msg,"");
- }
- //传入错误码的方法
- public static <T> Result failed(int code,String msg){
- msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
- return new Result(code,msg,"");
- }
- //传入错误码的数据
- public static <T> Result failed(int code,String msg,T data){
- msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
- return new Result(code,msg,data);
- }
- }
- /**
- * @author yunyan
- * @date 2023/6/11 16:05
- */
- @Slf4j
- @RestControllerAdvice
- public class GlobalExceptionHandler {
-
- @ExceptionHandler(value = Throwable.class)
- public Result handleException(Throwable throwable){
- log.error("错误",throwable);
- return Result.failed(500, throwable.getCause().getMessage());
- }
- }
- /**
- * @author yunyan
- * @date 2023/6/11 16:20
- */
- @RestController
- @RequestMapping("/test")
- public class TestController {
-
- @Autowired
- private SysLogService sysLogService;
-
- // 默认1s,方便测试查看,写10s
- @RepeatSubmit(expireTime = 10)
- @PostMapping("/saveSysLog")
- public Result saveSysLog(@RequestBody SysLog sysLog){
- return Result.success(sysLogService.saveSyslog(sysLog));
- }
- }
- /**
- * @author yunyan
- * @date 2023/6/11 16:40
- */
- @Service
- public class SysLogServiceImpl implements SysLogService {
- @Autowired
- private SysLogMapper sysLogMapper;
- @Override
- public int saveSyslog(SysLog sysLog) {
- return sysLogMapper.insert(sysLog);
- }
- }
输入请求: http://localhost:8081/test/saveSysLog
请求参数:
- {
- "title":"你好",
- "method":"post",
- "operName":"我是测试幂等性的"
- }
发送请求两次:
查看数据库中发现只有一条保存成功。
这样就解决了幂等性问题,再也不会有错误数据了,减少了一个bug提交!这是一个都要重视的问题,必须要解决,不然可能会出现问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。