SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交

文章介绍了接口幂等性的概念及其重要性,特别是在支付场景中的应用。通过使用Redis的Set机制,结合自定义注解和AOP切面处理,确保同一用户在1秒内对POST接口的多次请求只会执行一次,从而避免重复操作。文中详细阐述了实现步骤,包括导入相关依赖、配置Redis、编写自定义注解、创建切面以及测试流程。
摘要由CSDN通过智能技术生成

目录

一、什么是幂等性

二、REST风格与幂等性

三、解决思路

四、实战

4.1、导入依赖

4.2、编写application.yml文件

4.3、redis序列化

4.4、自定义注解

4.5、编写切片

4.6、统一返回值

4.7、简单的异常处理

4.8、controller测试

4.9、Service

五、测试

5.1、postman进行测试

5.2、查看redisKey

 六、总结


一、什么是幂等性

        接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致,不会因 为多次点击而产生了副作用; 比如说经典的支付场景:用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了条,这就没有保证接口的幂等性。

二、REST风格与幂等性

 所以我们要解决的就是POST请求!

三、解决思路

大概主流的解决方案:

  • token机制(前端带着在请求头上带着标识,后端验证)
  • 加锁机制
    • 数据库悲观锁(锁表)
    • 数据库乐观锁(version号进行控制)
    • 业务层分布式锁(加分布式锁redisson)
  • 全局唯一索引机制
  • redis的set机制
  • 前端按钮加限制

咱们的解决方案就是redis的set机制!

        同一个用户,任何POST保存相关的接口,1s内只能提交一次。

        完全使用后端来进行控制,前端可以加限制,不过体验不好!

        后端通过自定义注解,在需要防幂等接口上添加注解,利用AOP切片,减少和业务的耦合! 在切片中获取用户的token、user_id、url构成redis的唯一key! 第一次请求会先判断key是否存在,如果不存在,则往redis添加一个主键key,设置过期时间;

        如果有异常会主动删除key,万一没有删除失败,等待1s,redis也会自动删除,时间误差是可以接受的! 第二个请求过来,先判断key是否存在,如果存在,则是重复提交,返回保存信息!!

四、实战

4.1、导入依赖

<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>

4.2、编写application.yml文件

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:

4.3、redis序列化

/**
 * @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;
    }
}

4.4、自定义注解

/**
 * 自定义注解防止表单重复提交
 * @author yunyan
 * @date 2023/6/11 15:25
 */
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface RepeatSubmit {

    /**
     * 防重复操作过期时间,默认1s
     */
    long expireTime() default 1;
}

4.5、编写切片

/**
 * @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("请勿重复提交");
        }
    }
}

4.6、统一返回值

@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);
    }
}

4.7、简单的异常处理

/**
 * @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());
    }
}

4.8、controller测试

/**
 * @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));
    }
}

4.9、Service

/**
 * @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);
    }
}

五、测试

5.1、postman进行测试

输入请求: http://localhost:8081/test/saveSysLog 请求参数:

{
    "title":"你好",
    "method":"post",
    "operName":"我是测试幂等性的"
}

发送请求两次:

  查看数据库中发现只有一条保存成功。

5.2、查看redisKey

 六、总结

        这样就解决了幂等性问题,再也不会有错误数据了,减少了一个bug提交!这是一个都要重视的问题,必须要解决,不然可能会出现问题。

Java自定义注解可以用于实现接口的幂等操作。所谓接口的幂等,就是同一个请求多次执行也不会产生重复的副作用。 要实现接口的幂等,可以通过以下步骤: 1. 定义一个自定义注解,例如@Idempotent,用于标识接口方法的幂等性。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { } ``` 2. 在接口方法上使用@Idempotent注解,表示该接口方法是幂等的。 ```java public interface MyService { @Idempotent void myMethod(); } ``` 3. 在实现类中,通过AOP的方式实现接口方法的幂等逻辑。可以使用AspectJ或Spring AOP等框架来实现。 ```java @Component @Aspect public class IdempotentAspect { @Autowired private IdempotentService idempotentService; @Around("@annotation(com.example.Idempotent)") public void checkIdempotent(ProceedingJoinPoint joinPoint) throws Throwable { // 获取方法参数,生成幂等key String idempotentKey = generateIdempotentKey(joinPoint.getArgs()); // 判断该key是否已经执行过,如果执行过则直接返回 if (idempotentService.isExecuted(idempotentKey)) { return; } // 执行方法体 joinPoint.proceed(); // 标记该key已经执行过 idempotentService.markExecuted(idempotentKey); } // 生成幂等key的逻辑 private String generateIdempotentKey(Object[] args) { // 根据接口方法的参数生成唯一标识,例如将参数拼接成字符串 return StringUtils.join(args); } } ``` 通过以上步骤,我们就可以实现接口方法的幂等性。在执行接口方法之前,会先判断该方法是否已经执行过,如果已执行过,则直接返回,避免重复执行产生副作用。同时,通过自定义注解@Idempotent的标识,可以简化幂等逻辑的实现和管理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值