日常开发中,我们可能会碰到需要进行防重放与操作幂等的业务,本文记录SpringBoot实现简单防重与幂等
防重放,防止数据重复提交
操作幂等性,多次执行所产生的影响均与一次执行的影响相同
解决什么问题?
表单重复提交,用户多次点击表单提交按钮
接口重复调用,接口短时间内被多次调用
思路如下:
1、前端页面表提交钮置灰不可点击+js节流防抖
2、Redis防重Token令牌
3、数据库唯一主键 + 乐观锁
pom引入依赖
org.springframework.boot spring-boot-starter-data-redis 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
防重放与操作幂等
按钮置灰不可点击
点击提交按钮后,将提交按钮置灰不可点击,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) } }
防重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; }
后台查询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) { QueryWrapperqueryWrapper = 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 = ?) UpdateWrapperupdateWrapper = 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{ //拒绝更新 }
防重与幂等暂时先记录到这,后续再进行补充