SpringBoot custom annotations + AOP + redis to achieve anti-interface idempotent repeated submission

Table of contents

1. What is idempotency

2. REST style and idempotence

3. Solutions

Four, actual combat

4.1. Import dependencies

4.2, write application.yml file

4.3, redis serialization

4.4. Custom annotations

4.5. Writing slices

4.6. Unified return value

4.7. Simple exception handling

4.8, controller test

4.9、Service

5. Test

5.1, postman for testing

5.2, view redisKey

 6. Summary


1. What is idempotency

        The idempotence of the interface means that the result 同一操作initiated by the user will not cause side effects due to multiple clicks; for example, in the classic payment scenario: the user purchases the product and the payment is deducted successfully, but the network is abnormal when the result is returned. The money has been deducted, and the user clicks the button again, and the second deduction will be performed at this time, and the returned result is successful. The user checks the balance and finds that more money has been deducted. sex.一次请求或者多次请求一致

2. REST style and idempotence

 So all we have to do is POSTrequest!

3. Solutions

Probably the mainstream solution:

  • token mechanism (the front-end carries the logo on the request header, and the back-end verifies)
  • locking mechanism
    • Database pessimistic lock (lock table)
    • Database optimistic lock (version number to control)
    • Business layer distributed lock (plus distributed lock redisson)
  • Global unique index mechanism
  • The set mechanism of redis
  • Front button plus limit

Our solution is the set mechanism of redis!

        For the same user, any interface related to POST storage can only be submitted once within 1 second.

        Completely use the backend for control, and the frontend can be restricted, but the experience is not good!

        The backend uses custom annotations to add annotations to interfaces that require anti-idempotence, and uses AOP slicing to reduce coupling with the business! token、user_id、urlGet the user's unique key that constitutes redis in the slice ! The first request will first determine whether the key exists, if not, add a primary key key to redis, and set the expiration time;

        If there is an exception, the key will be deleted actively. If there is no deletion failure, redis will automatically delete it after waiting for 1 second. The time error is acceptable! When the second request comes, first judge whether the key exists, if it exists, it will be submitted repeatedly, and the saved information will be returned! !

Four, actual combat

4.1. Import dependencies

<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, write application.yml file

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 serialization

/**
 * @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. Custom annotations

/**
 * 自定义注解防止表单重复提交
 * @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. Writing slices

/**
 * @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. Unified return value

@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. Simple exception handling

/**
 * @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 test

/**
 * @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. Test

5.1, postman for testing

Input Request:  http://localhost:8081/test/saveSysLog Request Parameters:

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

Send the request twice:

  Check the database and find that only one is saved successfully.

5.2, view redisKey

 6. Summary

        This solves the idempotence problem, there will be no more wrong data, and one less bug submission! This is a problem that everyone must pay attention to, and it must be resolved, otherwise problems may arise.

Guess you like

Origin blog.csdn.net/qq_54247497/article/details/131474001