当前位置:   article > 正文

SpringBoot系列——防重放与操作幂等

springboot 会话重放

 前言

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

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

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

  解决什么问题?

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

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

  思路如下:

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

  2、Redis防重Token令牌

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

  具体方案

  pom引入依赖

  1. <!-- Redis -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. </dependency>
  6. <!-- thymeleaf模板 -->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  10. </dependency>
  11. <!--添加MyBatis-Plus依赖 -->
  12. <dependency>
  13. <groupId>com.baomidou</groupId>
  14. <artifactId>mybatis-plus-boot-starter</artifactId>
  15. <version>3.4.0</version>
  16. </dependency>
  17. <!--添加MySQL驱动依赖 -->
  18. <dependency>
  19. <groupId>mysql</groupId>
  20. <artifactId>mysql-connector-java</artifactId>
  21. </dependency>


927c6539d0d09b3ea5b9914f812a1dcf.png


  一个测试表

  1. CREATE TABLE `idem` (
  2. `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '唯一主键',
  3. `msg` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '业务数据',
  4. `version` int(8) NOT NULL COMMENT '乐观锁版本号',
  5. PRIMARY KEY (`id`) USING BTREE
  6. ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '防重放与操作幂等测试表' ROW_FORMAT = Compact;

  前端页面

  先写一个test页面,引入jq


82f6205ed9657777f688d018690292ab.png


View Code

  按钮置灰不可点击

  点击提交按钮后,将提交按钮置灰不可点击,ajax响应后再恢复按钮状态

  1. function formSubmit(but){
  2. //按钮置灰
  3. but.setAttribute("disabled","disabled");
  4. let token = $("#token").val();
  5. let id = $("#id").val();
  6. let msg = $("#msg").val();
  7. let version = $("#version").val();
  8. $.ajax({
  9. type: 'post',
  10. url: "/test/test",
  11. contentType:"application/x-www-form-urlencoded",
  12. data: {
  13. token:token,
  14. id:id,
  15. msg:msg,
  16. version:version,
  17. },
  18. success: function (data) {
  19. console.log(data);
  20. //按钮恢复
  21. but.removeAttribute("disabled");
  22. },
  23. error: function (xhr, status, error) {
  24. console.error("ajax错误!");
  25. //按钮恢复
  26. but.removeAttribute("disabled");
  27. }
  28. });
  29. return false;
  30. }


a4a2738e834560764b80560e486c0514.png


  js节流、防抖

  节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次

  1. document.getElementById('btn').onclick = throttle(function () {
  2. console.log('节流测试 helloworld');
  3. }, 1000)
  4. // 节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次
  5. // 节流函数
  6. function throttle(fn, delay) {
  7. var lastTime = new Date().getTime()
  8. delay = delay || 200
  9. return function () {
  10. var args = arguments
  11. var nowTime = new Date().getTime()
  12. if (nowTime - lastTime >= delay) {
  13. lastTime = nowTime
  14. fn.apply(this, args)
  15. }
  16. }
  17. }


2e77cd75a649fb3de8d64f34c2a6d26d.png


  防抖:给定一个时间,不管怎么点击按钮,每点一次,都会在最后一次点击等待这个时间过后执行

  1. document.getElementById('btn2').onclick = debounce(function () {
  2. console.log('防抖测试 helloworld');
  3. }, 1000)
  4. // 防抖:给定一个时间,不管怎么点击按钮,每点一次,都会在最后一次点击等待这个时间过后执行
  5. // 防抖函数
  6. function debounce(fn, delay) {
  7. var timer = null
  8. delay = delay || 200
  9. return function () {
  10. var args = arguments
  11. var that = this
  12. clearTimeout(timer)
  13. timer = setTimeout(function () {
  14. fn.apply(that, args)
  15. }, delay)
  16. }
  17. }


7841c9198b3f72c54d95db9a1ee235b0.png


  Redis

  防重Token令牌

  跳转前端表单页面时,设置一个UUID作为token,并设置在表单隐藏域

  1. /**
  2. * 跳转页面
  3. */
  4. @RequestMapping("index")
  5. private ModelAndView index(String id){
  6. ModelAndView mv = new ModelAndView();
  7. mv.addObject("token",UUIDUtil.getUUID());
  8. if(id != null){
  9. Idem idem = new Idem();
  10. idem.setId(id);
  11. List select = (List)idemService.select(idem);
  12. idem = (Idem)select.get(0);
  13. mv.addObject("id", idem.getId());
  14. mv.addObject("msg", idem.getMsg());
  15. mv.addObject("version", idem.getVersion());
  16. }
  17. mv.setViewName("test.html");
  18. return mv;
  19. }
  20. <form>
  21. <!-- 隐藏域 -->
  22. <input type="hidden" id="token" th:value="${token}"/>
  23. <!-- 业务数据 -->
  24. id:<input id="id" th:value="${id}"/> <br/>
  25. msg:<input id="msg" th:value="${msg}"/> <br/>
  26. version:<input id="version" th:value="${version}"/> <br/>
  27. <!-- 操作按钮 -->
  28. <br/>
  29. <input type="submit" value="提交" onclick="formSubmit(this)"/>
  30. <input type="reset" value="重置"/>
  31. </form>

  后台查询redis缓存,如果token不存在立即设置token缓存,允许表单业务正常进行;如果token缓存已经存在,拒绝表单业务

  PS:token缓存要设置一个合理的过期时间

  1. /**
  2. * 表单提交测试
  3. */
  4. @RequestMapping("test")
  5. private String test(String token,String id,String msg,int version){
  6. //如果token缓存不存在,立即设置缓存且设置有效时长(秒)
  7. Boolean setIfAbsent = template.opsForValue().setIfAbsent(token, "1", 60 * 5, TimeUnit.SECONDS);
  8. //缓存设置成功返回true,失败返回false
  9. if(Boolean.TRUE.equals(setIfAbsent)){
  10. //模拟耗时
  11. try {
  12. Thread.sleep(2000);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. //打印测试数据
  17. System.out.println(token+","+id+","+msg+","+version);
  18. return "操作成功!";
  19. }else{
  20. return "操作失败,表单已被提交...";
  21. }
  22. }


eba813423c4d71b53e217575c2c4b789.png


  for循环测试中,5个操作只有一个执行成功!


7f6bd94db009e9a0d5579b89a58e2c76.png



feeda0f381c4740ab39be54e3472aaaa.png



4f453bfb12ec0057ff454ae94416142c.png


  数据库

  唯一主键 + 乐观锁

  查询操作自带幂等性

  1. /**
  2. * 查询操作,天生幂等性
  3. */
  4. @Override
  5. public Object select(Idem idem) {
  6. QueryWrapper<Idem> queryWrapper = new QueryWrapper<>();
  7. queryWrapper.setEntity(idem);
  8. return idemMapper.selectList(queryWrapper);
  9. }

  查询没什么好说的,只要数据不变,查询条件不变的情况下查询结果必然幂等


4879e153212d4565249ec08959538ef1.png


  唯一主键可解决插入操作、删除操作

  1. /**
  2. * 插入操作,使用唯一主键实现幂等性
  3. */
  4. @Override
  5. public Object insert(Idem idem) {
  6. String msg = "操作成功!";
  7. try{
  8. idemMapper.insert(idem);
  9. }catch (DuplicateKeyException e){
  10. msg = "操作失败,id:"+idem.getId()+",已经存在...";
  11. }
  12. return msg;
  13. }
  14. /**
  15. * 删除操作,使用唯一主键实现幂等性
  16. * PS:使用非主键条件除外
  17. */
  18. @Override
  19. public Object delete(Idem idem) {
  20. String msg = "操作成功!";
  21. int deleteById = idemMapper.deleteById(idem.getId());
  22. if(deleteById == 0){
  23. msg = "操作失败,id:"+idem.getId()+",已经被删除...";
  24. }
  25. return msg;
  26. }

  利用主键唯一的特性,捕获处理重复操作


07a49c49ad2418f7c516c3e29b2ba342.png



729c8482791c55a0cf5df6e60a1967b9.png



0c9fce75a022decc66803c653d314eec.png



acdedfa3031c3427116211da8a15de52.png


  乐观锁可解决更新操作

  1. /**
  2. * 更新操作,使用乐观锁实现幂等性
  3. */
  4. @Override
  5. public Object update(Idem idem) {
  6. String msg = "操作成功!";
  7. // UPDATE table SET [... 业务字段=? ...], version = version+1 WHERE (id = ? AND version = ?)
  8. UpdateWrapper<Idem> updateWrapper = new UpdateWrapper<>();
  9. //where条件
  10. updateWrapper.eq("id",idem.getId());
  11. updateWrapper.eq("version",idem.getVersion());
  12. //version版本号要单独设置
  13. updateWrapper.setSql("version = version+1");
  14. idem.setVersion(null);
  15. int update = idemMapper.update(idem, updateWrapper);
  16. if(update == 0){
  17. msg = "操作失败,id:"+idem.getId()+",已经被更新...";
  18. }
  19. return msg;
  20. }

  执行更新sql语句时,where条件带上version版本号,如果执行成功,除了更新业务数据,同时更新version版本号标记当前数据已被更新

UPDATE table SET [... 业务字段=? ...], version = version+1 WHERE (id = ? AND version = ?)

  执行更新操作前,需要重新执行插入数据


1141c1233f0d0666b83c8dda03a55511.png



23cf5dfe5f5bc05a435f4c232df614ef.png


  以上for循环测试中,5个操作同样只有一个执行成功!

  后记

  redis、乐观锁不要在代码先查询后if判断,这样会存在并发问题,导致数据不准确,应该把这种判断放在redis、数据库

  错误示例:

  1. //获取最新缓存
  2. String redisToken = template.opsForValue().get(token);
  3. //为空则放行业务
  4. if(redisToken == null){
  5. //设置缓存
  6. template.opsForValue().set(token, "1", 60 * 5, TimeUnit.SECONDS);
  7. //业务处理
  8. }else{
  9. //拒绝业务
  10. }

  错误示例:

  1. //获取最新版本号
  2. Integer version = idemMapper.selectById(idem.getId()).getVersion();
  3. //版本号相同,说明数据未被其他人修改
  4. if(version == idem.getVersion()){
  5. //正常更新
  6. }else{
  7. //拒绝更新
  8. }

  防重与幂等暂时先记录到这,后续再进行补充

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/234757
推荐阅读
相关标签
  

闽ICP备14008679号