Anotações personalizadas do SpringBoot + AOP + redis para obter envio repetido idempotente anti-interface

Índice

1. O que é idempotência

2. Estilo REST e idempotência

3. Soluções

Quatro, combate real

4.1. Dependências de importação

4.2, escreva o arquivo application.yml

4.3, serialização redis

4.4. Anotações personalizadas

4.5. Escrevendo fatias

4.6. Valor de retorno unificado

4.7. Tratamento de exceção simples

4.8, teste do controlador

4.9、Serviço

5. Teste

5.1, carteiro para testes

5.2, veja redisKey

 6. Resumo


1. O que é idempotência

        A idempotência da interface significa que o resultado 同一操作iniciado pelo usuário não causará efeitos colaterais devido a múltiplos cliques; por exemplo, no cenário de pagamento clássico: o usuário compra o produto e o pagamento é deduzido com sucesso, mas a rede está anormal quando o resultado é retornado. O dinheiro foi deduzido e o usuário clica no botão novamente, e a segunda dedução será realizada neste momento, e o resultado retornado é bem-sucedido. O usuário verifica o saldo e descobre que mais dinheiro foi deduzido sexo.一次请求或者多次请求一致

2. Estilo REST e idempotência

 Então é só pedir POST!

3. Soluções

Provavelmente a solução principal:

  • mecanismo de token (o front-end carrega o logotipo no cabeçalho da solicitação e o back-end verifica)
  • Mecanismo de tranca
    • Bloqueio pessimista do banco de dados (tabela de bloqueio)
    • Bloqueio otimista do banco de dados (número da versão para controlar)
    • Bloqueio distribuído da camada de negócios (mais redison de bloqueio distribuído)
  • Mecanismo de índice exclusivo global
  • O mecanismo de configuração do redis
  • Botão frontal mais limite

Nossa solução é o mecanismo definido de redis!

        Para o mesmo usuário, qualquer interface relacionada ao armazenamento POST pode ser enviada apenas uma vez em 1 segundo.

        Use totalmente o back-end para controle e o front-end pode ser restrito, mas a experiência não é boa!

        O back-end usa anotações personalizadas para adicionar anotações a interfaces que exigem anti-idempotência e usa divisão AOP para reduzir o acoplamento com o negócio! token、user_id、urlObtenha a chave exclusiva do usuário que constitui o redis na fatia ! A primeira solicitação determinará primeiro se a chave existe; caso contrário, adicionará uma chave primária ao redis e definirá o tempo de expiração;

        Se houver uma exceção, a chave será excluída ativamente. Se não houver falha na exclusão, o redis a excluirá automaticamente após aguardar 1 segundo. O erro de tempo é aceitável! Quando vier a segunda solicitação, primeiro julgue se a chave existe, se existir, ela será enviada repetidamente e as informações salvas serão retornadas! !

Quatro, combate real

4.1. Dependências de importação

<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, escreva o arquivo 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, serialização 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. Anotações personalizadas

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

/**
 * @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. Valor de retorno unificado

@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. Tratamento de exceção simples

/**
 * @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, teste do controlador

/**
 * @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、Serviço

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

5.1, carteiro para testes

Solicitação de entrada:  http://localhost:8081/test/saveSysLog Parâmetros da solicitação:

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

Envie a solicitação duas vezes:

  Verifique o banco de dados e descubra que apenas um foi salvo com sucesso.

5.2, veja redisKey

 6. Resumo

        Isso resolve o problema de idempotência, não haverá mais dados errados e menos um envio de bug! Este é um problema ao qual todos devem estar atentos e deve ser resolvido, caso contrário, podem surgir problemas.

Acho que você gosta

Origin blog.csdn.net/qq_54247497/article/details/131474001
Recomendado
Clasificación