首页 > 数据库 >SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战

SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战

时间:2022-11-21 11:33:57浏览次数:81  
标签:SpringBoot 自定义 boot redis Result msg new public


一、前言

在面试中,经常会有一道经典面试题,那就是:​​怎么防止接口重复提交?​​​ 小编也是背过的,好几种方式,但是一直没有实战过,做多了管理系统,发现这个事情真的没有过多的重视。
最近在测试过程中,发现了多次提交会保存两条数据,进而导致程序出现问题!

问题已经出现我们就解决一下吧!!

本次解决是对于高并发不高的情况,适用于一般的管理系统,给出的解决方案!!高并发的还是建议加分布式锁!!

下面我们来聊聊幂等性是什么?

二、什么是幂等性

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

防接口重复提交,这是必须要做的一件事情!!

三、REST风格与幂等性

以常用的四种来分析哈!

REST

是否支持幂等

SQL例子

GET


SELECT * FROM table WHER id = 1

PUT


UPDATE table SET age=18 WHERE id = 1

DELETE


DELETE FROM table WHERE id = 1

POST


INSERT INTO table (id,age) VALUES(1,21)

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

四、解决思路

大概主流的解决方案:

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

小编的解决方案就是redis的set机制!

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

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

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

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

五、实战

SpringBoot版本为​​2.7.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>

2. 编写yml

server:
port: 8087

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:

3. redis序列化

/**
* @author wangzhenjun
* @date 2022/11/17 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. 自定义注解

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

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

5. 编写切片

异常信息大家换成自己想抛的异常,小编这里就没有详细划分异常,就是为了写博客而记录的不完美项目哈!!

/**
* @author wangzhenjun
* @date 2022/11/16 8:54
*/
@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
* 可以在加上用户id,小编这里没办法获取,大家可以在项目中加上
*/
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("请勿重复提交");
}
}
}

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

7. 简单的全局异常处理

这是残缺版,大家不要模仿!!

/**
* @author wangzhenjun
* @date 2022/11/17 15:33
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(value = Throwable.class)
public Result handleException(Throwable throwable){
log.error("错误",throwable);
return Result.failed(500, throwable.getCause().getMessage());
}
}

8. controller测试

/**
* @author wangzhenjun
* @date 2022/10/26 16:51
*/
@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));
}
}

9. service

/**
* @author wangzhenjun
* @date 2022/11/10 16:45
*/
@Service
public class SysLogServiceImpl implements SysLogService {
@Autowired
private SysLogMapper sysLogMapper;
@Override
public int saveSyslog(SysLog sysLog) {
return sysLogMapper.insert(sysLog);
}
}

六、测试

1. postman进行测试

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

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

发送请求两次:

SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战_spring

2. 查看数据库

只会有一条保存成功!

SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战_redis_02

3. 查看redisKey

在10s会自动删除,就可以在次提交!

SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战_redis_03

4. 控制台

SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战_幂等性_04

七、总结

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

完结撒花,如果对你有帮助,还请点个关注哈!!你的支持是我写作的动力!!!


可以看下一小编的微信公众号,和网站文章首发看,欢迎关注,一起交流哈!!

​点击访问!小编自己的网站,里面也是有很多好的文章哦!​


标签:SpringBoot,自定义,boot,redis,Result,msg,new,public
From: https://blog.51cto.com/wangzhenjun/5873184

相关文章

  • Redis系列11:内存淘汰策略
    Redis系列1:深刻理解高性能Redis的本质Redis系列2:数据持久化提高可用性Redis系列3:高可用之主从架构Redis系列4:高可用之Sentinel(哨兵模式)Redis系列5:深入分析Cluster集......
  • 第五十三章 开发自定义标签 - Using csr %cspQuote Methods
    第五十三章开发自定义标签-Usingcsr%cspQuoteMethodsUsingcsr%cspQuoteMethods%cspQuote例程定义包含两个不同引用方法的定义。QuoteQuoteCSPQuoteMe......
  • Springboot整合Swagger(二)
    1、创建Springboot项目2、引入swagger依赖<!--SpringBoot-starter--><dependency><groupId>org.springframework.boot</groupId>......
  • Redis
    为什么用缓存:一句话:因为速度快,好用缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力实际开......
  • SpringBoot使用@Async的总结!
    一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。之前有写过一篇文章叫做:异步编程利器:CompletableFuture在实际工作中也更加推荐使用CompletableFuture......
  • SpringBoot14(监听机制)
    一、java监听机制(麻烦、繁琐,不推荐)二、SpringBoot监听机制三、代码实现1-包的定义2-listener代码块1-MyApplicationContextInitializerMyApplicationCont......
  • Redis-HyperLoglog
    Redis-HyperLoglog概览HyperLogLog作为一个实现基数统计的高效数据结构,被用在计算日活数据等应用领域,每个键占用的内容都是12k,但因为不存储实际的数据,因此可以统计2^64个......
  • 网页背景图自定义比例缩小或扩大覆盖整个页面
    body{background-size:cover;text-align:center;/*此部分支持chrome,应该也支持firefox*/background:rgb(246,248,249);background:url(../img/......
  • Redission的几种写法
    失败无限重试-多个线程竞争,第一个拿到锁第二个会无限重试RLocklock=redisson.getLock("码哥字节");try{//1.最常用的第一种写法lock.lock();......
  • Jenkins容器<二>---发布springboot项目 20221004
    一、Jenkins容器<->---通过docker安装 20221004 二、Jenkins容器<二>---发布springboot项目 20221004   1、系统配置      安装插件 ......