SpringBoot系列教程之防重放与操作幂等

网友投稿 286 2022-08-04


SpringBoot系列教程之防重放与操作幂等

目录前言具体方案前端页面Redis数据库后记代码开源总结

前言

日常开发中,我们可能会碰到需要进行防重放与操作幂等的业务,本文记录SpringBoot实现简单防重与幂等

防重放,防止数据重复提交

操作幂等性,多次执行所产生的影响均与一次执行的影响相同

解决什么问题?

表单重复提交,用户多次点击表单提交按钮

接口重复调用,接口短时间内被多次调用

思路如下:

1、前端页面表提交钮置灰不可点击+js节流防抖

2、Redis防重Token令牌

3、数据库唯一主键 + 乐观锁

具体方案

pom引入依赖

org.springframework.boot

spring-boot-starter-data-redis

http://

org.springframework.boot

spring-boot-starter-thymeleaf

com.baomidou

mybatis-plus-boot-starter

3.4.0

mysql

mysql-connector-java

一个测试表

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

id:

msg:

version:




按钮置灰不可点击

点击提交按钮后,将提交按钮置灰不可点击,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;

}

id:

msg:

version:


后台查询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 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 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

总结


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:java基础之接口组成更新的实现(java接口与实现总结)
下一篇:idea设置JVM运行参数的几种方式(idea设置jvm启动参数)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~