SpringBoot系列教程之防重放與操作冪等
前言
日常開發(fā)中,我們可能會碰到需要進行防重放與操作冪等的業(yè)務(wù),本文記錄SpringBoot實現(xiàn)簡單防重與冪等
防重放,防止數(shù)據(jù)重復提交
操作冪等性,多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同
解決什么問題?
表單重復提交,用戶多次點擊表單提交按鈕
接口重復調(diào)用,接口短時間內(nèi)被多次調(diào)用
思路如下:
1、前端頁面表提交鈕置灰不可點擊+js節(jié)流防抖
2、Redis防重Token令牌
3、數(shù)據(jù)庫唯一主鍵 + 樂觀鎖
具體方案
pom引入依賴
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--添加MyBatis-Plus依賴 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!--添加MySQL驅(qū)動依賴 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
一個測試表
CREATE TABLE `idem` ( `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '唯一主鍵', `msg` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '業(yè)務(wù)數(shù)據(jù)', `version` int(8) NOT NULL COMMENT '樂觀鎖版本號', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '防重放與操作冪等測試表' ROW_FORMAT = Compact;
前端頁面
先寫一個test頁面,引入jq
<!DOCTYPE html>
<!--解決idea thymeleaf 表達式模板報紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>防重放與操作冪等</title>
<!-- 引入靜態(tài)資源 -->
<script th:src="@{/js/jquery-1.9.1.min.js}" type="application/javascript"></script>
</head>
<body>
<form>
<!-- 隱藏域 -->
<input type="hidden" id="token" th:value="${token}"/>
<!-- 業(yè)務(wù)數(shù)據(jù) -->
id:<input id="id" th:value="${id}"/> <br/>
msg:<input id="msg" th:value="${msg}"/> <br/>
version:<input id="version" th:value="${version}"/> <br/>
<!-- 操作按鈕 -->
<br/>
<input type="submit" value="提交" onclick="formSubmit(this)"/>
<input type="reset" value="重置"/>
</form>
<br/>
<button id="btn">節(jié)流測試,點我</button>
<br/>
<button id="btn2">防抖測試,點我</button>
</body>
<script>
/*
//插入
for (let i = 0; i < 5; i++) {
$.get("http://localhost:10010/idem/insert?id=1&msg=張三"+i+"&version=1",null,function (data){
console.log(data);
});
}
//修改
for (let i = 0; i < 5; i++) {
$.get("http://localhost:10010/idem/update?id=1&msg=李四"+i+"&version=1",null,function (data){
console.log(data);
});
}
//刪除
for (let i = 0; i < 5; i++) {
$.get("http://localhost:10010/idem/delete?id=1",null,function (data){
console.log(data);
});
}
//查詢
for (let i = 0; i < 5; i++) {
$.get("http://localhost:10010/idem/select?id=1",null,function (data){
console.log(data);
});
}
//test表單測試
for (let i = 0; i < 5; i++) {
$.get("http://localhost:10010/test/test?token=abcd&id=1&msg=張三"+i+"&version=1",null,function (data){
console.log(data);
});
}
//節(jié)流測試
for (let i = 0; i < 5; i++) {
document.getElementById('btn').onclick();
}
//防抖測試
for (let i = 0; i < 5; i++) {
document.getElementById('btn2').onclick();
}
*/
function formSubmit(but){
//按鈕置灰
but.setAttribute("disabled","disabled");
let token = $("#token").val();
let id = $("#id").val();
let msg = $("#msg").val();
let version = $("#version").val();
$.ajax({
type: 'post',
url: "/test/test",
contentType:"application/x-www-form-urlencoded",
data: {
token:token,
id:id,
msg:msg,
version:version,
},
success: function (data) {
console.log(data);
//按鈕恢復
but.removeAttribute("disabled");
},
error: function (xhr, status, error) {
console.error("ajax錯誤!");
//按鈕恢復
but.removeAttribute("disabled");
}
});
return false;
}
document.getElementById('btn').onclick = throttle(function () {
console.log('節(jié)流測試 helloworld');
}, 1000)
// 節(jié)流:給定一個時間,不管這個時間你怎么點擊,點上天,這個時間內(nèi)也只會執(zhí)行一次
// 節(jié)流函數(shù)
function throttle(fn, delay) {
var lastTime = new Date().getTime()
delay = delay || 200
return function () {
var args = arguments
var nowTime = new Date().getTime()
if (nowTime - lastTime >= delay) {
lastTime = nowTime
fn.apply(this, args)
}
}
}
document.getElementById('btn2').onclick = debounce(function () {
console.log('防抖測試 helloworld');
}, 1000)
// 防抖:給定一個時間,不管怎么點擊按鈕,每點一次,都會在最后一次點擊等待這個時間過后執(zhí)行
// 防抖函數(shù)
function debounce(fn, delay) {
var timer = null
delay = delay || 200
return function () {
var args = arguments
var that = this
clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
</script>
</html>按鈕置灰不可點擊
點擊提交按鈕后,將提交按鈕置灰不可點擊,ajax響應(yīng)后再恢復按鈕狀態(tài)
function formSubmit(but){
//按鈕置灰
but.setAttribute("disabled","disabled");
let token = $("#token").val();
let id = $("#id").val();
let msg = $("#msg").val();
let version = $("#version").val();
$.ajax({
type: 'post',
url: "/test/test",
contentType:"application/x-www-form-urlencoded",
data: {
token:token,
id:id,
msg:msg,
version:version,
},
success: function (data) {
console.log(data);
//按鈕恢復
but.removeAttribute("disabled");
},
error: function (xhr, status, error) {
console.error("ajax錯誤!");
//按鈕恢復
but.removeAttribute("disabled");
}
});
return false;
}
js節(jié)流、防抖
節(jié)流:給定一個時間,不管這個時間你怎么點擊,點上天,這個時間內(nèi)也只會執(zhí)行一次
document.getElementById('btn').onclick = throttle(function () {
console.log('節(jié)流測試 helloworld');
}, 1000)
// 節(jié)流:給定一個時間,不管這個時間你怎么點擊,點上天,這個時間內(nèi)也只會執(zhí)行一次
// 節(jié)流函數(shù)
function throttle(fn, delay) {
var lastTime = new Date().getTime()
delay = delay || 200
return function () {
var args = arguments
var nowTime = new Date().getTime()
if (nowTime - lastTime >= delay) {
lastTime = nowTime
fn.apply(this, args)
}
}
}
防抖:給定一個時間,不管怎么點擊按鈕,每點一次,都會在最后一次點擊等待這個時間過后執(zhí)行
document.getElementById('btn2').onclick = debounce(function () {
console.log('防抖測試 helloworld');
}, 1000)
// 防抖:給定一個時間,不管怎么點擊按鈕,每點一次,都會在最后一次點擊等待這個時間過后執(zhí)行
// 防抖函數(shù)
function debounce(fn, delay) {
var timer = null
delay = delay || 200
return function () {
var args = arguments
var that = this
clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
Redis
防重Token令牌
跳轉(zhuǎn)前端表單頁面時,設(shè)置一個UUID作為token,并設(shè)置在表單隱藏域
/**
* 跳轉(zhuǎn)頁面
*/
@RequestMapping("index")
private ModelAndView index(String id){
ModelAndView mv = new ModelAndView();
mv.addObject("token",UUIDUtil.getUUID());
if(id != null){
Idem idem = new Idem();
idem.setId(id);
List select = (List)idemService.select(idem);
idem = (Idem)select.get(0);
mv.addObject("id", idem.getId());
mv.addObject("msg", idem.getMsg());
mv.addObject("version", idem.getVersion());
}
mv.setViewName("test.html");
return mv;
}<form>
<!-- 隱藏域 -->
<input type="hidden" id="token" th:value="${token}"/>
<!-- 業(yè)務(wù)數(shù)據(jù) -->
id:<input id="id" th:value="${id}"/> <br/>
msg:<input id="msg" th:value="${msg}"/> <br/>
version:<input id="version" th:value="${version}"/> <br/>
<!-- 操作按鈕 -->
<br/>
<input type="submit" value="提交" onclick="formSubmit(this)"/>
<input type="reset" value="重置"/>
</form>后臺查詢redis緩存,如果token不存在立即設(shè)置token緩存,允許表單業(yè)務(wù)正常進行;如果token緩存已經(jīng)存在,拒絕表單業(yè)務(wù)
PS:token緩存要設(shè)置一個合理的過期時間
/**
* 表單提交測試
*/
@RequestMapping("test")
private String test(String token,String id,String msg,int version){
//如果token緩存不存在,立即設(shè)置緩存且設(shè)置有效時長(秒)
Boolean setIfAbsent = template.opsForValue().setIfAbsent(token, "1", 60 * 5, TimeUnit.SECONDS);
//緩存設(shè)置成功返回true,失敗返回false
if(Boolean.TRUE.equals(setIfAbsent)){
//模擬耗時
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印測試數(shù)據(jù)
System.out.println(token+","+id+","+msg+","+version);
return "操作成功!";
}else{
return "操作失敗,表單已被提交...";
}
}
for循環(huán)測試中,5個操作只有一個執(zhí)行成功!



數(shù)據(jù)庫
唯一主鍵 + 樂觀鎖
查詢操作自帶冪等性
/**
* 查詢操作,天生冪等性
*/
@Override
public Object select(Idem idem) {
QueryWrapper<Idem> queryWrapper = new QueryWrapper<>();
queryWrapper.setEntity(idem);
return idemMapper.selectList(queryWrapper);
}查詢沒什么好說的,只要數(shù)據(jù)不變,查詢條件不變的情況下查詢結(jié)果必然冪等

唯一主鍵可解決插入操作、刪除操作
/**
* 插入操作,使用唯一主鍵實現(xiàn)冪等性
*/
@Override
public Object insert(Idem idem) {
String msg = "操作成功!";
try{
idemMapper.insert(idem);
}catch (DuplicateKeyException e){
msg = "操作失敗,id:"+idem.getId()+",已經(jīng)存在...";
}
return msg;
}
/**
* 刪除操作,使用唯一主鍵實現(xiàn)冪等性
* PS:使用非主鍵條件除外
*/
@Override
public Object delete(Idem idem) {
String msg = "操作成功!";
int deleteById = idemMapper.deleteById(idem.getId());
if(deleteById == 0){
msg = "操作失敗,id:"+idem.getId()+",已經(jīng)被刪除...";
}
return msg;
}利用主鍵唯一的特性,捕獲處理重復操作




樂觀鎖可解決更新操作
/**
* 更新操作,使用樂觀鎖實現(xiàn)冪等性
*/
@Override
public Object update(Idem idem) {
String msg = "操作成功!";
// UPDATE table SET [... 業(yè)務(wù)字段=? ...], version = version+1 WHERE (id = ? AND version = ?)
UpdateWrapper<Idem> updateWrapper = new UpdateWrapper<>();
//where條件
updateWrapper.eq("id",idem.getId());
updateWrapper.eq("version",idem.getVersion());
//version版本號要單獨設(shè)置
updateWrapper.setSql("version = version+1");
idem.setVersion(null);
int update = idemMapper.update(idem, updateWrapper);
if(update == 0){
msg = "操作失敗,id:"+idem.getId()+",已經(jīng)被更新...";
}
return msg;
}執(zhí)行更新sql語句時,where條件帶上version版本號,如果執(zhí)行成功,除了更新業(yè)務(wù)數(shù)據(jù),同時更新version版本號標記當前數(shù)據(jù)已被更新
UPDATE table SET [... 業(yè)務(wù)字段=? ...], version = version+1 WHERE (id = ? AND version = ?)
執(zhí)行更新操作前,需要重新執(zhí)行插入數(shù)據(jù)


以上for循環(huán)測試中,5個操作同樣只有一個執(zhí)行成功!
后記
redis、樂觀鎖不要在代碼先查詢后if判斷,這樣會存在并發(fā)問題,導致數(shù)據(jù)不準確,應(yīng)該把這種判斷放在redis、數(shù)據(jù)庫
錯誤示例:
//獲取最新緩存
String redisToken = template.opsForValue().get(token);
//為空則放行業(yè)務(wù)
if(redisToken == null){
//設(shè)置緩存
template.opsForValue().set(token, "1", 60 * 5, TimeUnit.SECONDS);
//業(yè)務(wù)處理
}else{
//拒絕業(yè)務(wù)
}錯誤示例:
//獲取最新版本號
Integer version = idemMapper.selectById(idem.getId()).getVersion();
//版本號相同,說明數(shù)據(jù)未被其他人修改
if(version == idem.getVersion()){
//正常更新
}else{
//拒絕更新
}防重與冪等暫時先記錄到這,后續(xù)再進行補充
代碼開源
代碼已經(jīng)開源、托管到我的GitHub、碼云:
GitHub:https://github.com/huanzi-qch/springBoot
碼云:https://gitee.com/huanzi-qch/springBoot
總結(jié)
到此這篇關(guān)于SpringBoot系列教程之防重放與操作冪等的文章就介紹到這了,更多相關(guān)SpringBoot防重放與操作冪等內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Mybatis-plus和pagehelper依賴沖突的方法示例
這篇文章主要介紹了解決Mybatis-plus和pagehelper依賴沖突的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04
基于MyBatis的數(shù)據(jù)持久化框架的使用詳解
Mybatis是一個優(yōu)秀的開源、輕量級持久層框架,它對JDBC操作數(shù)據(jù)庫的過程進行封裝。本文將為大家講解一下基于MyBatis的數(shù)據(jù)持久化框架的使用,感興趣的可以了解一下2022-08-08
關(guān)于使用Mybatisplus自帶的selectById和insert方法時的一些問題
這篇文章主要介紹了關(guān)于使用Mybatisplus自帶的selectById和insert方法時的一些問題,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
Java基礎(chǔ)之switch分支結(jié)構(gòu)詳解
這篇文章主要介紹了Java基礎(chǔ)之switch分支結(jié)構(gòu)詳解,文中有非常詳細的代碼示例,對正在學習java的小伙伴們有很大的幫助,需要的朋友可以參考下2021-05-05
SpringBoot在RequestBody中使用枚舉參數(shù)案例詳解
這篇文章主要介紹了SpringBoot在RequestBody中使用枚舉參數(shù)案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-09-09

