SpringBoot系列——防重放与操作幂等
mhr18 2024-12-04 13:59 18 浏览 0 评论
前言
日常开发中,我们可能会碰到需要进行防重放与操作幂等的业务,本文记录SpringBoot实现简单防重与幂等
防重放,防止数据重复提交
操作幂等性,多次执行所产生的影响均与一次执行的影响相同
解决什么问题?
表单重复提交,用户多次点击表单提交按钮
接口重复调用,接口短时间内被多次调用
思路如下:
1、前端页面表提交钮置灰不可点击+js节流防抖
2、Redis防重Token令牌
3、数据库唯一主键 + 乐观锁
具体方案
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驱动依赖 -->
<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 '业务数据',
`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>
<!-- 引入静态资源 -->
<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}"/>
<!-- 业务数据 -->
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">节流测试,点我</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);
});
}
//节流测试
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('节流测试 helloworld');
}, 1000)
// 节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次
// 节流函数
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)
// 防抖:给定一个时间,不管怎么点击按钮,每点一次,都会在最后一次点击等待这个时间过后执行
// 防抖函数
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响应后再恢复按钮状态
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节流、防抖
节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次
document.getElementById('btn').onclick = throttle(function () {
console.log('节流测试 helloworld');
}, 1000)
// 节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次
// 节流函数
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)
// 防抖:给定一个时间,不管怎么点击按钮,每点一次,都会在最后一次点击等待这个时间过后执行
// 防抖函数
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令牌
跳转前端表单页面时,设置一个UUID作为token,并设置在表单隐藏域
/**
* 跳转页面
*/
@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}"/>
<!-- 业务数据 -->
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不存在立即设置token缓存,允许表单业务正常进行;如果token缓存已经存在,拒绝表单业务
PS:token缓存要设置一个合理的过期时间
/**
* 表单提交测试
*/
@RequestMapping("test")
private String test(String token,String id,String msg,int version){
//如果token缓存不存在,立即设置缓存且设置有效时长(秒)
Boolean setIfAbsent = template.opsForValue().setIfAbsent(token, "1", 60 * 5, TimeUnit.SECONDS);
//缓存设置成功返回true,失败返回false
if(Boolean.TRUE.equals(setIfAbsent)){
//模拟耗时
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印测试数据
System.out.println(token+","+id+","+msg+","+version);
return "操作成功!";
}else{
return "操作失败,表单已被提交...";
}
}
for循环测试中,5个操作只有一个执行成功!
数据库
唯一主键 + 乐观锁
查询操作自带幂等性
/**
* 查询操作,天生幂等性
*/
@Override
public Object select(Idem idem) {
QueryWrapper<Idem> queryWrapper = new QueryWrapper<>();
queryWrapper.setEntity(idem);
return idemMapper.selectList(queryWrapper);
}
查询没什么好说的,只要数据不变,查询条件不变的情况下查询结果必然幂等
唯一主键可解决插入操作、删除操作
/**
* 插入操作,使用唯一主键实现幂等性
*/
@Override
public Object insert(Idem idem) {
String msg = "操作成功!";
try{
idemMapper.insert(idem);
}catch (DuplicateKeyException e){
msg = "操作失败,id:"+idem.getId()+",已经存在...";
}
return msg;
}
/**
* 删除操作,使用唯一主键实现幂等性
* PS:使用非主键条件除外
*/
@Override
public Object delete(Idem idem) {
String msg = "操作成功!";
int deleteById = idemMapper.deleteById(idem.getId());
if(deleteById == 0){
msg = "操作失败,id:"+idem.getId()+",已经被删除...";
}
return msg;
}
利用主键唯一的特性,捕获处理重复操作
乐观锁可解决更新操作
/**
* 更新操作,使用乐观锁实现幂等性
*/
@Override
public Object update(Idem idem) {
String msg = "操作成功!";
// UPDATE table SET [... 业务字段=? ...], version = version+1 WHERE (id = ? AND version = ?)
UpdateWrapper<Idem> updateWrapper = new UpdateWrapper<>();
//where条件
updateWrapper.eq("id",idem.getId());
updateWrapper.eq("version",idem.getVersion());
//version版本号要单独设置
updateWrapper.setSql("version = version+1");
idem.setVersion(null);
int update = idemMapper.update(idem, updateWrapper);
if(update == 0){
msg = "操作失败,id:"+idem.getId()+",已经被更新...";
}
return msg;
}
执行更新sql语句时,where条件带上version版本号,如果执行成功,除了更新业务数据,同时更新version版本号标记当前数据已被更新
UPDATE table SET [... 业务字段=? ...], version = version+1 WHERE (id = ? AND version = ?)
执行更新操作前,需要重新执行插入数据
以上for循环测试中,5个操作同样只有一个执行成功!
后记
redis、乐观锁不要在代码先查询后if判断,这样会存在并发问题,导致数据不准确,应该把这种判断放在redis、数据库
错误示例:
//获取最新缓存
String redisToken = template.opsForValue().get(token);
//为空则放行业务
if(redisToken == null){
//设置缓存
template.opsForValue().set(token, "1", 60 * 5, TimeUnit.SECONDS);
//业务处理
}else{
//拒绝业务
}
错误示例:
//获取最新版本号
Integer version = idemMapper.selectById(idem.getId()).getVersion();
//版本号相同,说明数据未被其他人修改
if(version == idem.getVersion()){
//正常更新
}else{
//拒绝更新
}
防重与幂等暂时先记录到这,后续再进行补充
代码开源
代码已经开源、托管到我的GitHub、码云:
GitHub:https://github.com/huanzi-qch/springBoot
码云:https://gitee.com/huanzi-qch/springBoot
版权声明
作者:huanzi-qch
出处:https://www.cnblogs.com/huanzi-qch
若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.
相关推荐
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
- 备份软件调用rman接口备份报错RMAN-06820 ORA-17629 ORA-17627
-
一、报错描述:备份归档报错无法连接主库进行归档,监听问题12541RMAN-06820:WARNING:failedtoarchivecurrentlogatprimarydatab...
- 增量备份修复物理备库gap(增量备份恢复数据库步骤)
-
适用场景:主备不同步,主库归档日志已删除且无备份.解决方案:主库增量备份修复dg备库中的gap.具体步骤:1、停止同步>alterdatabaserecovermanagedstand...
- 一分钟看懂,如何白嫖sql工具(白嫖数据库)
-
如何白嫖sql工具?1分钟看懂。今天分享一个免费的sql工具,毕竟现在比较火的NavicatDbeaverDatagrip都需要付费才能使用完整功能。幸亏今天有了这款SQLynx,它不仅支持国内外...
- 「开源资讯」数据管理与可视化分析平台,DataGear 1.6.1 发布
-
前言数据齿轮(DataGear)是一款数据库管理系统,使用Java语言开发,采用浏览器/服务器架构,以数据管理为核心功能,支持多种数据库。它的数据模型并不是原始的数据库表,而是融合了数据库表及表间关系...
- 您还在手工打造增删改查代码么,该神器带你脱离苦海
-
作为Java开发程序,日常开发中,都会使用Spring框架,完成日常的功能开发;在相关业务系统中,难免存在各种增删改查的接口需求开发。通常来说,实现增删改查有如下几个方式:纯手工打造,编写各种Cont...
- Linux基础知识(linux基础知识点及答案)
-
系统目录结构/bin:命令和应用程序。/boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。/dev:dev是Device(设备)的缩写,该目录...
- PL/SQL 杂谈(二)(pl/sql developer使用)
-
承接(一)部分。我们从结构和功能这两个方面展示PL/SQL的关键要素。可以看看PL/SQL的优雅的代码。写出一个好的代码,就和文科生写出一篇优秀的作文一样,那么赏心悦目。1、与SQL的集成PL/S...
- 电商ERP系统哪个好用?(电商erp哪个好一点)
-
电商ERP系统哪个好用?做电商的,谁还没被ERP折腾过?有老板说:“我们早就上了ERP,订单、库存、财务全搞定,系统用得飞起。”也有运营吐槽:“系统是上了,可库存老不准,订单漏单错单天天有,财务对账还...
- 汽车检测线系统实例,看集中控制与PLC分布控制
-
PLC可编程控制器,上个世纪70年代初,为取代早期继电器控制线路,开始采取存储指令方式,完成顺序控制而设计的。开始仅有逻辑运算、计时、计数等简单功能。随着微处理的发展,PLC可编程能力日益提高,已经能...
- 苹果五件套成公司年会奖品主角,几大小技巧教你玩转苹果新品
-
钱江晚报·小时新闻记者张云山随着春节的临近,各家大公司的年会又将陆续上演。上周,各大游戏公司的年会大奖,苹果五件套又成了标配。在上海的游戏公司中,莉莉丝奖品列表拉得相当长,从特等奖到九等奖还包含了特...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)