自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

SpringBoot自定義注解+AOP+redis實(shí)現(xiàn)防接口冪等性重復(fù)提交,從概念到實(shí)戰(zhàn)

開發(fā) 架構(gòu)
本次解決是對于高并發(fā)不高的情況,適用于一般的管理系統(tǒng),給出的解決方案!高并發(fā)的還是建議加分布式鎖!

一、前言

在面試中,經(jīng)常會有一道經(jīng)典面試題,那就是:怎么防止接口重復(fù)提交?小編也是背過的,好幾種方式,但是一直沒有實(shí)戰(zhàn)過,做多了管理系統(tǒng),發(fā)現(xiàn)這個事情真的沒有過多的重視。最近在測試過程中,發(fā)現(xiàn)了多次提交會保存兩條數(shù)據(jù),進(jìn)而導(dǎo)致程序出現(xiàn)問題!

問題已經(jīng)出現(xiàn)我們就解決一下吧??!

本次解決是對于高并發(fā)不高的情況,適用于一般的管理系統(tǒng),給出的解決方案?。「卟l(fā)的還是建議加分布式鎖?。?/p>

下面我們來聊聊冪等性是什么?

二、什么是冪等性

接口冪等性就是用戶對于同一操作發(fā)起的一次請求或者多次請求的結(jié)果是一致的,不會因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生了副作用;比如說經(jīng)典的支付場景:用戶購買了商品支付扣款成功,但是返回結(jié)果的時候網(wǎng)絡(luò)異常,此時錢已經(jīng)扣了,用戶再次點(diǎn)擊按鈕,此時會進(jìn)行第二次扣款,返回結(jié)果成功,用戶查詢余額返發(fā)現(xiàn)多扣錢了,流水記錄也變成了條,這就沒有保證接口的冪等性;可謂:商家美滋滋,買家罵咧咧!

防接口重復(fù)提交,這是必須要做的一件事情!

三、REST風(fēng)格與冪等性

以常用的四種來分析哈!

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機(jī)制(前端帶著在請求頭上帶著標(biāo)識,后端驗(yàn)證)
  • 加鎖機(jī)制
  • 數(shù)據(jù)庫悲觀鎖(鎖表)
  • 數(shù)據(jù)庫樂觀鎖(version號進(jìn)行控制)
  • 業(yè)務(wù)層分布式鎖(加分布式鎖redisson)
  • 全局唯一索引機(jī)制
  • redis的set機(jī)制
  • 前端按鈕加限制

小編的解決方案就是redis的set機(jī)制!

同一個用戶,任何POST保存相關(guān)的接口,1s內(nèi)只能提交一次。

完全使用后端來進(jìn)行控制,前端可以加限制,不過體驗(yàn)不好!

后端通過自定義注解,在需要防冪等接口上添加注解,利用AOP切片,減少和業(yè)務(wù)的耦合!在切片中獲取用戶的token、user_id、url構(gòu)成redis的唯一key!
第一次請求會先判斷key是否存在,如果不存在,則往redis添加一個主鍵key,設(shè)置過期時間。

如果有異常會主動刪除key,萬一沒有刪除失敗,等待1s,redis也會自動刪除,時間誤差是可以接受的!
第二個請求過來,先判斷key是否存在,如果存在,則是重復(fù)提交,返回保存信息??!

五、實(shí)戰(zhàn)

SpringBoot版本為2.7.4

1. 導(dǎ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>

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、自定義注解

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

/**
* 防重復(fù)操作過期時間,默認(rèn)1s
*/
long expireTime() default 1;
}

5、編寫切片

異常信息大家換成自己想拋的異常,小編這里就沒有詳細(xì)劃分異常,就是為了寫博客而記錄的不完美項(xiàng)目哈??!

/**
* @author wangzhenjun
* @date 2022/11/16 8:54
*/
@Slf4j
@Component
@Aspect
public class RepeatSubmitAspect {

@Autowired
private RedisTemplate redisTemplate;
/**
* 定義切點(diǎn)
*/
@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();
// 獲取防重復(fù)提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 獲取token當(dāng)做key,小編這里是新后端項(xiàng)目獲取不到哈,先寫死
// 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,小編這里沒辦法獲取,大家可以在項(xiàng)目中加上
*/
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 {
//正常執(zhí)行方法并返回
return joinPoint.proceed();
} catch (Throwable throwable) {
redisTemplate.delete(redisKey);
throw new Throwable(throwable);
}
} else {
// 拋出異常
throw new Throwable("請勿重復(fù)提交");
}
}
}

6、統(tǒng)一返回值

@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 = "系統(tǒng)異常,請聯(lián)系管理員";
//沒有權(quán)限的響應(yīng)碼
public static final Integer NO_AUTH_COOD = 999;

//執(zhí)行成功
public static <T> Result<T> success(T data){
return new Result<>(SUCCESS_CODE,SUCCESS_MSG,data);
}
//執(zhí)行失敗
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,"");
}
//傳入錯誤碼的數(shù)據(jù)
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;

// 默認(rèn)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進(jìn)行測試

輸入請求:
http://localhost:8087/test/saveSysLog請求參數(shù):

{
"title":"你好",
"method":"post",
"operName":"我是測試冪等性的"
}

發(fā)送請求兩次:

圖片

2、查看數(shù)據(jù)庫

只會有一條保存成功!

圖片

3、查看redisKey

在10s會自動刪除,就可以在次提交!

圖片

4、控制臺

圖片

七、總結(jié)

這樣就解決了冪等性問題,再也不會有錯誤數(shù)據(jù)了,減少了一個bug提交!這是一個都要重視的問題,必須要解決,不然可能會出現(xiàn)問題。

責(zé)任編輯:姜華 來源: 小王博客基地
相關(guān)推薦

2022-12-13 09:19:06

高并發(fā)SpringBoot

2024-07-02 11:42:53

SpringRedis自定義

2024-10-09 10:46:41

springboot緩存redis

2024-04-03 09:18:03

Redis數(shù)據(jù)結(jié)構(gòu)接口防刷

2025-02-23 08:00:00

冪等性Java開發(fā)

2024-05-28 09:26:46

2023-03-07 08:19:16

接口冪等性SpringBoot

2023-10-09 07:37:01

2024-06-14 09:30:58

2023-10-11 07:57:23

springboot微服務(wù)

2020-09-04 13:30:43

Java自定義代碼

2024-03-13 15:18:00

接口冪等性高并發(fā)

2023-10-24 13:48:50

自定義注解舉值驗(yàn)證

2023-08-01 08:54:02

接口冪等網(wǎng)絡(luò)

2020-11-12 07:43:06

Redis冪等性接口

2021-01-18 14:34:59

冪等性接口客戶端

2024-04-01 08:11:20

2022-11-01 11:15:56

接口策略模式

2021-02-20 11:40:35

SpringBoot占位符開發(fā)技術(shù)

2020-11-25 11:20:44

Spring注解Java
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號