成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

SpringBoot自定義注解+AOP+redis實現防接口冪等性重復提交,從概念到實戰

開發 前端
本次解決是對于高并發不高的情況,適用于一般的管理系統,給出的解決方案?。「卟l的還是建議加分布式鎖!

一、前言

在面試中,經常會有一道經典面試題,那就是:怎么防止接口重復提交?小編也是背過的,好幾種方式,但是一直沒有實戰過,做多了管理系統,發現這個事情真的沒有過多的重視。最近在測試過程中,發現了多次提交會保存兩條數據,進而導致程序出現問題!

問題已經出現我們就解決一下吧?。?/p>

本次解決是對于高并發不高的情況,適用于一般的管理系統,給出的解決方案!!高并發的還是建議加分布式鎖??!

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

二、什么是冪等性

接口冪等性就是用戶對于同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點擊而產生了副作用;比如說經典的支付場景:用戶購買了商品支付扣款成功,但是返回結果的時候網絡異常,此時錢已經扣了,用戶再次點擊按鈕,此時會進行第二次扣款,返回結果成功,用戶查詢余額返發現多扣錢了,流水記錄也變成了條,這就沒有保證接口的冪等性;可謂:商家美滋滋,買家罵咧咧!

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

三、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;
}

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{

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":"我是測試冪等性的"
}

發送請求兩次:

圖片

2、查看數據庫

只會有一條保存成功!

圖片

3、查看redisKey

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

圖片

4、控制臺

圖片

七、總結

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

責任編輯:姜華 來源: 小王博客基地
相關推薦

2023-03-03 09:11:12

高并發SpringBoot

2024-07-02 11:42:53

SpringRedis自定義

2024-10-09 10:46:41

springboot緩存redis

2024-04-03 09:18:03

Redis數據結構接口防刷

2025-02-23 08:00:00

冪等性Java開發

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

2020-09-04 13:30:43

Java自定義代碼

2023-10-11 07:57:23

springboot微服務

2024-03-13 15:18:00

接口冪等性高并發

2021-01-18 14:34:59

冪等性接口客戶端

2023-08-01 08:54:02

接口冪等網絡

2020-11-12 07:43:06

Redis冪等性接口

2023-10-24 13:48:50

自定義注解舉值驗證

2024-04-01 08:11:20

2020-11-25 11:20:44

Spring注解Java

2022-11-01 11:15:56

接口策略模式

2021-02-20 11:40:35

SpringBoot占位符開發技術
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 少妇精品久久久久久久久久 | 亚洲一区精品在线 | 9999视频 | 亚洲人在线播放 | 91精品国产综合久久久久久丝袜 | 中文字幕专区 | 精品久久久久久久 | 日韩高清国产一区在线 | 久久高清免费视频 | 国产一区二区三区免费观看视频 | 天堂一区二区三区 | 性欧美精品一区二区三区在线播放 | 欧美日韩三区 | 欧美成人h版在线观看 | 日本高清视频在线播放 | 天堂久久一区 | 午夜爽爽男女免费观看hd | 免费观看a级毛片在线播放 黄网站免费入口 | 久久久999免费视频 999久久久久久久久6666 | 91视频在线观看 | 夜夜久久 | 久久网站免费视频 | 国产japanhdxxxx麻豆 | 全免费a级毛片免费看视频免 | 九九热精品视频 | 欧美黑人国产人伦爽爽爽 | 国产免费观看视频 | 九九热视频这里只有精品 | 久久久精品国产 | 欧美激情精品久久久久 | 日本不卡一区二区三区在线观看 | 久久精品久久久久久 | 在线观看国产精品视频 | 中文字幕在线播放不卡 | 亚洲人成人一区二区在线观看 | 欧美日日| 欧美黑人狂野猛交老妇 | 欧美久久精品一级c片 | 国产一二三视频在线观看 | 欧美一区二区在线播放 | 欧美激情一区 |