范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

SpringBoot自定义注解AOPredis实现防接口重复提交,概念到实战

  一、前言
  在面试中,经常会有一道经典面试题,那就是: 怎么防止接口重复提交?
  小编也是背过的,好几种方式,但是一直没有实战过,做多了管理系统,发现这个事情真的没有过多的重视。
  最近在测试过程中,发现了多次提交会保存两条数据,进而导致程序出现问题!
  问题已经出现我们就解决一下吧!!
  本次解决是对于高并发不高的情况,适用于一般的管理系统,给出的解决方案!!高并发的还是建议加分布式锁!!
  下面我们来聊聊幂等性是什么? 二、什么是幂等性
  接口幂等性就是用户对于 同一操作  发起的一次请求或者多次请求  的结果是一致的  ,不会因
  为多次点击而产生了副作用;
  比如说经典的支付场景:用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了条,这就没有保证接口的幂等性;
  可谓:商家美滋滋,买家骂咧咧!!
  防接口重复提交,这是必须要做的一件事情!! 三、REST风格与幂等性
  以常用的四种来分析哈!
  REST
  是否支持幂等
  SQL例子
  GET
  是
  SELECT * FROM table WHER id = 1
  PUT
  是
  UPDATE table SET age=18 WHERE id = 1
  DELETE
  是
  DELETE FROM table WHERE id = 1
  POST
  否
  INSERT INTO table (id,age) VALUES(1,21)
  所以我们要解决的就是 POST  请求!四、解决思路
  大概主流的解决方案: token机制(前端带着在请求头上带着标识,后端验证) 加锁机制 数据库悲观锁(锁表) 数据库乐观锁(version号进行控制) 业务层分布式锁(加分布式锁redisson) 全局唯一索引机制 redis的set机制 前端按钮加限制
  小编的解决方案就是redis的set机制!
  同一个用户,任何POST保存相关的接口,1s内只能提交一次。
  完全使用后端来进行控制,前端可以加限制,不过体验不好!
  后端通过自定义注解,在需要防幂等接口上添加注解,利用AOP切片,减少和业务的耦合!
  在切片中获取用户的 token、user_id、url  构成redis的唯一key!
  第一次请求会先判断key是否存在,如果不存在,则往redis添加一个主键key,设置过期时间;
  如果有异常会主动删除key,万一没有删除失败,等待1s,redis也会自动删除,时间误差是可以接受的!
  第二个请求过来,先判断key是否存在,如果存在,则是重复提交,返回保存信息!! 五、实战
  SpringBoot版本为 2.7.4  1. 导入依赖     org.springframework.boot     spring-boot-starter-data-redis       org.projectlombok     lombok     1.18.2       org.springframework.boot     spring-boot-starter-aop       org.springframework.boot     spring-boot-starter-web        com.alibaba     druid-spring-boot-starter     1.1.16        org.springframework.boot     spring-boot-starter-jdbc         mysql     mysql-connector-java        com.baomidou     mybatis-plus-boot-starter     3.5.1       org.springframework.boot     spring-boot-starter-test     test  2. 编写ymlserver:   port: 8087  spring:   redis:     host: localhost     port: 6379     password: 123456   datasource:     #使用阿里的Druid     type: com.alibaba.druid.pool.DruidDataSource     driver-class-name: com.mysql.cj.jdbc.Driver     url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC     username: root     password: 3. redis序列化/**  * @author wangzhenjun  * @date 2022/11/17 15:20  */ @Configuration public class RedisConfig {      @Bean     @SuppressWarnings(value = { "unchecked", "rawtypes" })     public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)     {         RedisTemplate template = new RedisTemplate<>();         template.setConnectionFactory(connectionFactory);         Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);          // 使用StringRedisSerializer来序列化和反序列化redis的key值         template.setKeySerializer(new StringRedisSerializer());         template.setValueSerializer(serializer);          // Hash的key也采用StringRedisSerializer的序列化方式         template.setHashKeySerializer(new StringRedisSerializer());         template.setHashValueSerializer(serializer);          template.afterPropertiesSet();         return template;     } } 4. 自定义注解/**  * 自定义注解防止表单重复提交  * @author wangzhenjun  * @date 2022/11/17 15:18  */ @Target(ElementType.METHOD) // 注解只能用于方法 @Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期 @Documented public @interface RepeatSubmit {      /**      * 防重复操作过期时间,默认1s      */     long expireTime() default 1; } 5. 编写切片
  异常信息大家换成自己想抛的异常,小编这里就没有详细划分异常,就是为了写博客而记录的不完美项目哈!! /**  * @author wangzhenjun  * @date 2022/11/16 8:54  */ @Slf4j @Component @Aspect public class RepeatSubmitAspect {      @Autowired     private RedisTemplate redisTemplate;     /**      * 定义切点      */     @Pointcut("@annotation(com.example.demo.annotation.RepeatSubmit)")     public void repeatSubmit() {}      @Around("repeatSubmit()")     public Object around(ProceedingJoinPoint joinPoint) throws Throwable {          ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder                 .getRequestAttributes();         HttpServletRequest request = attributes.getRequest();         Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();         // 获取防重复提交注解         RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);         // 获取token当做key,小编这里是新后端项目获取不到哈,先写死         // String token = request.getHeader("Authorization");         String tokenKey = "hhhhhhh,nihao";         if (StringUtils.isBlank(token)) {             throw new RuntimeException("token不存在,请登录!");         }         String url = request.getRequestURI();         /**          *  通过前缀 + url + token 来生成redis上的 key          *  可以在加上用户id,小编这里没办法获取,大家可以在项目中加上          */         String redisKey = "repeat_submit_key:"                 .concat(url)                 .concat(tokenKey);         log.info("==========redisKey ====== {}",redisKey);          if (!redisTemplate.hasKey(redisKey)) {             redisTemplate.opsForValue().set(redisKey, redisKey, annotation.expireTime(), TimeUnit.SECONDS);             try {                 //正常执行方法并返回                 return joinPoint.proceed();             } catch (Throwable throwable) {                 redisTemplate.delete(redisKey);                 throw new Throwable(throwable);             }         } else {             // 抛出异常             throw new Throwable("请勿重复提交");         }     } } 6. 统一返回值@Data @NoArgsConstructor @AllArgsConstructor public class Result {     private Integer code;      private String msg;      private T data;      //成功码     public static final Integer SUCCESS_CODE = 200;     //成功消息     public static final String SUCCESS_MSG = "SUCCESS";      //失败     public static final Integer ERROR_CODE = 201;     public static final String ERROR_MSG = "系统异常,请联系管理员";     //没有权限的响应码     public static final Integer NO_AUTH_COOD = 999;      //执行成功     public static  Result success(T data){         return new Result<>(SUCCESS_CODE,SUCCESS_MSG,data);     }     //执行失败     public static  Result failed(String msg){         msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;         return new Result(ERROR_CODE,msg,"");     }     //传入错误码的方法     public static  Result failed(int code,String msg){         msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;         return new Result(code,msg,"");     }     //传入错误码的数据     public static  Result failed(int code,String msg,T data){         msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;         return new Result(code,msg,data);     } } 7. 简单的全局异常处理
  这是残缺版,大家不要模仿!! /**  * @author wangzhenjun  * @date 2022/11/17 15:33  */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler {      @ExceptionHandler(value = Throwable.class)     public Result handleException(Throwable throwable){         log.error("错误",throwable);         return Result.failed(500, throwable.getCause().getMessage());     } } 8. controller测试/**  * @author wangzhenjun  * @date 2022/10/26 16:51  */ @RestController @RequestMapping("/test") public class TestController {      @Autowired     private SysLogService sysLogService;      // 默认1s,方便测试查看,写10s     @RepeatSubmit(expireTime = 10)     @PostMapping("/saveSysLog")     public Result saveSysLog(@RequestBody SysLog sysLog){         return Result.success(sysLogService.saveSyslog(sysLog));     } } 9. service/**  * @author wangzhenjun  * @date 2022/11/10 16:45  */ @Service public class SysLogServiceImpl implements SysLogService {     @Autowired     private SysLogMapper sysLogMapper;     @Override     public int saveSyslog(SysLog sysLog) {         return sysLogMapper.insert(sysLog);     } } 六、测试1. postman进行测试
  输入请求:
  http://localhost:8087/test/saveSysLog
  请求参数:{     "title":"你好",     "method":"post",     "operName":"我是测试幂等性的" }
  发送请求两次:
  在这里插入图片描述 2. 查看数据库
  只会有一条保存成功!
  在这里插入图片描述 3. 查看redisKey
  在10s会自动删除,就可以在次提交!
  在这里插入图片描述
  4. 控制台
  在这里插入图片描述 七、总结
  这样就解决了幂等性问题,再也不会有错误数据了,减少了一个bug提交!这是一个都要重视的问题,必须要解决,不然可能会出现问题。
  完结撒花,如果对你有帮助,还请点个关注哈!!你的支持是我写作的动力!!!
  可以看下一小编的微信公众号,和网站文章首发看,欢迎关注,一起交流哈!!

对话哪吒汽车江峰2023年推出双门跑车,哪吒S要做燃油车颠覆者文懂车帝原创常思玥懂车帝原创2022广州车展懂车帝原创行业2022年的年终大戏第20届广州车展在12月30日拉开大幕。回望即将走过的2022年,中国汽车产业遭受了前所未有的巨大挑战飙上热搜!特斯拉突然宣布最狠猛降4。8万!光伏新能源大爆发,狂掀涨停潮!一股闪崩跌近80,北京继续严格执行全域禁放中国基金报晨曦大家好,又到了美好的周五!来一起关注今天上午的市场行情和最新资讯经历了昨日大涨后,1月6日,A股市场开盘表现平缓上证指数平开,深证成指微跌0。04,沪深300微涨0。糯叽叽的毛晓彤太美了!喜欢摇粒绒羊羔毛皮草的姐妹,快来学在不了解女明星之前,我都觉得她们是高高在上不食人间烟火甚至穿衣打扮都不接地气的,但真正了解了她们之后,我发现她们也很有烟火气。这其中的典型代表就是毛晓彤,近期私服多以毛绒单品为主的喜欢穿大衣的胖女孩,选品避免这四个雷区,一样能穿出时尚感大衣可以说是女性朋友都很喜欢的服饰,但有不少人并无法展现出它应有的魅力。尤其是身材偏胖的姐妹,稍有不慎就会暴露自身缺陷,让形象大打折扣。而造成穿衣不时尚的最大原因,就是在选品上面出韦世豪辟谣不会回归山东泰山,上港才是喜欢的球队日前,广州队队长韦世豪回归山东泰山的消息频出,据报道韦世豪将以3年180万的费用,回归山东泰山队,新赛季将身披阔别已久的橙色战袍征战中超,为培养自己的母队效力。本赛季,广州队在年轻翻墙有风险,小心被处罚随着互联网技术的发展,一些人受限于客观条件,因为种种原因,无法走出国门,去开眼看世界,但身在国内,又想了解外面的世界。近年来,自媒体业务的蓬勃发展,一些从事自媒体业务的人员,有的到冬季0紫外线,也要小心被晒黑!进入冬季,很多朋友会问,冬天的阳光没有夏天那么强烈,还需要防晒吗?在回答这个问题之前,首先要明确的一个问题是防晒防的是什么?01,紫外线防不胜防防晒防的是紫外线,而紫外线可分为UV有人吃了退烧药后脑中风?专家高危人群小心!最近有报道称,有市民吃了退烧药后不久出现脑中风,送医后才知,原是跟大量出汗没及时补水基础疾病没控制好等密不可分。阳了后如何呵护脑健康?为何有的中老年人,在抗原转阴后,还常有眩晕,甚当你把问题当成问题时,这个世界没有问题如果我们不断地选择去寻找有利于事情发展的痕迹,哪怕是只有一点蛛丝马迹的可能性也不放过的话,那事情就一定会被一点一点地引向好的方向,反过来亦然,即使有99是有利的,我们如果盯住1不利阳康以后,不小心劳累,突发不适,已经呼叫救护车,还能干点啥?阳过阳康以后,不小心劳累以后有后悔药吗?现实生活里面,有人刚刚新冠以后,没有了明显的自觉症状,就自以为全好了。于是开始娱乐赴宴饮酒卡拉OK做家务洗澡桑拿等等,绝大多数人可能没事,但伊春民情纯朴古风存小兴安岭伊春市是个民风纯朴之地,这里汇聚着二十多个的少数民族,绿水青山以它的灵秀,滋养着生活在大森林里的人们。由此而派生出有着大森林文化气息的,独具伊春特点的民俗风情。本土文化更是
寒衣节是隔开生者与逝者的一扇门昨夜,一夜未眠望着窗外天上的星星,我情不自禁的流泪了。这星星好像我的母亲,无时无刻不在想她。又是一年寒衣节,你在天上冷不冷,捎去的衣服都收到了吗?这是逝者的十月一,与国庆节相比,它方便面巨头倒下从20亿巅峰到贱卖,只因为老板太自信方便面巨头真栽了!3年把公司从年销20亿干倒闭凭野心搞垮自二十一世纪初,康师傅统一两家企业垄断了方便面80的市场,剩余的20也被华龙白象南街村等小品牌占据。很明显,在这个充满竞争和甲钴胺不只修复神经,还对这5种疾病有效,早知早好相信很多人都听说过神经炎,甚至有一些老人受到神经炎的影响,比如有的患者刚开始会感觉到耳后下颌角有疼痛,之后出现面部疼痛和歪斜,这就是面部神经炎症。治疗面部神经炎的药物包含了甲钴胺,金彭推出新电动三轮车,加大车厢设计,载重量高,提速爬坡更快对于很多用户来说,近两年在选择电动三轮车时,首要考虑的性能就是载重及爬坡能力。但对于大多数车型而言,很多电动三轮车都存在载重低且提速爬坡差等情况。对此,今天来带大家看一款新电动三轮米芾变成灵魂画手,这个长得像名画呐喊的字是啥?扬子晚报紫牛新闻记者沈昭对于书法爱好者们来说,临帖是必做功课,也有不少人喜欢在网上分享自己临帖的过程,交流学书经验。这两日,一个奇特的字破圈了,在视频中网友所临写的这个字看起来真是退休后最惬意的活法放下身段,过闲云野鹤的生活,余生就会安澜有人说人生如茶,第一道苦若生命,第二道甜似爱情,第三道淡如清风。走进迟暮的年纪,见识过大风大浪,经历过雨雪风霜,看淡尔虞我诈,看开悲欢离合,看透世事变动,最后发现淡若清风,才是人生慢慢来,等一下你的灵魂我们生活在一个日新月异竞争激烈的时代,忙碌的工作学习家务一直缠绕着我们的灵魂,活得像一个被上了发条的机器人,每天为完成各种任务指标而努力,渐渐地在生活中迷失了自我,找不到生活的真正这宁红夜COS太绝了!什么灵魂精华都不重要,和本人长得一模一样提起以武侠吃鸡风靡全球的国产多人竞技游戏永劫无间,相信不少人对它的大名如雷贯耳。自打去年暑假期间上线后,就深得海内外玩家和顶流主播们的喜爱,仅短短半年时间,就成功售卖了超1000万大龄剩女真的注定要孤独一生吗?大龄剩女真的注定要孤独一生吗?大龄剩女,顾名思义就是年龄大的剩女。说到大龄剩女我们很多人都会想到一个词剩女,就会想到孤独一生。而大龄剩女也不能够否认自己一生需要面对无数次婚姻和爱情阿圭罗希望阿根廷在世界杯夺冠,关键战将会是14决赛直播吧10月25日讯尽管如今已经退役,但阿圭罗对足球依然重返热忱,尤其对阿根廷国家队。在一次接受采访时,阿圭罗谈论了对阿根廷国家队在卡塔尔世界杯上表现的期望。阿圭罗说道我希望阿根廷世界上最脏的人去世曾60年不洗澡据英国镜报10月25日报道,伊朗男子AmouHaji因60年不洗澡,被称为世界上最脏的人。当地时间10月23日,Haji在伊朗南部的法尔斯省一个名叫Dejgah的村庄中去世,享年9