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

接口幂等问题探究

  前言
  最近遇到一些问题,表单重复提交,导致插入重复数据到数据库,这里查询一些通用的方案,自己都实践一下,以后好回顾。
  实践代码项目 Github: https://github.com/h-dj/Spring-Learning/tree/master/repeat-submit 一、什么是接口幂等性?
  幂等含义
  幂等 (idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
  在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。--- 百科
  简单理解:就是针对一个操作,不管做多少次,产生的效果都是一样的。
  举例: 前端对同一表单数据的重复提交,后台应该 只会产生一条记录 我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统 bug 重发,也 应该只扣一次钱 发送消息,也 应该只发一次 ,同样的短信如果多次发给用户,用户会崩溃 创建业务订单,一次业务请求 只能创建一个 ,不能出现创建多个订单 二、那些情况下会需要接口幂等性?
  对于业务中需要考虑幂等性的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有以下几种场景: 前端重复提交:提交订单,用户快速重复点击多次,造成后端生成多个内容重复的订单。 接口超时重试:对于给第三方调用的接口,为了防止网络抖动或其他原因造成请求丢失,这样的接口一般都会设计成超时重试多次。 消息重复消费:MQ 消息中间件,消息重复消费。 三、接口幂等性解决方案3.1、一般解决方案了解
  前端方面: 控制操作次数,例如:提交按钮仅可操作一次(提交动作后按钮置灰) 及时重定向,例如:下单/支付成功后跳转到成功提示页面,这样消除了浏览器前进或后退造成的重复提交问题。
  后台方面: 数据库去重表(唯一索引):利用数据库的唯一索引特性,保证唯一的逻辑使用唯一索引,可以是在原来的业务表添加唯一索引,也可以新建一个特定的去重表 使用分布式锁:借助中间件:Redis 、Zookeeper注意要保证中间件的高可用 Token 机制:进入页面时先申请 Token,提交表单时再使用 Token 请求,后台根据 Token 判断是否重复请求前端申请 Token 和 携带 Token 的行为需要进行封装,不然每个页面都需要手动,比较繁琐,后台的话可以使用 注解+ AOP 统一拦截 分布式项目,依然需要借助中间件:Redis
  注意: 一般推荐前端后台一起组合接口幂等方案,这样比较安全高效。
  以下准备使用加入购物车为例,实现各个方案 3.2、数据库去重表(唯一索引)操作步骤1、通过请求的业务参数,组成唯一 ID2、通过 ID 查询去重表中是否存在记录,存在则抛出 重复请求异常(是否抛出异常,根据具体业务决定)3、否则,向去重表插入记录,如果插入异常,说明有多个请求同时执行,抛出重复请求异常4、去重表插入记录成功后,执行加入购物车操作5、执行加入购物车操作成功后,删除去重表记录注意: 去重表操作 和 业务处理要在同一个事物中,方式业务处理失败后,没有回滚去重表记录,导致商品加入购物车后,不能在增加数量 创建表 DROP TABLE IF EXISTS `cart`; CREATE TABLE `cart` (   `id` bigint(20) NOT NULL AUTO_INCREMENT,   `product_id` bigint(20) DEFAULT NULL,   `product_name` varchar(500) DEFAULT NULL COMMENT "商品名称",   `product_sku_id` bigint(20) DEFAULT NULL,   `member_id` bigint(20) DEFAULT NULL,   `quantity` int(11) DEFAULT NULL COMMENT "购买数量",   `price` decimal(10,2) DEFAULT NULL COMMENT "添加到购物车的价格",   PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT="购物车表";  DROP TABLE IF EXISTS `deduplicate`; CREATE TABLE `deduplicate` ( 	id BIGINT(20) auto_increment NOT NULL, 	unique_id varchar(100) NULL COMMENT "唯一ID", 	CONSTRAINT deduplicate_PK PRIMARY KEY (id), 	CONSTRAINT deduplicate_UN UNIQUE KEY (unique_id)  -- 唯一索引 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT="去重表"; 添加到购物车逻辑 @RestController @RequestMapping("/cart") public class CartController {     @Autowired     private ICartService iCartService;      @ApiRepeatUniqueIdSubmit(keyExpression = "@cartController.getUserId()+"_"+#cartPO.getProductId() +"_"+#cartPO.getProductSkuId()")     @PostMapping(value = "/add")     public String add(@RequestBody CartPO cartPO) throws InterruptedException {         cartPO.setMemberId(getUserId());         iCartService.addCart(cartPO);         return "ok";     }     /**      * 获取当前登录用户ID      *      * @return      */     public Long getUserId() {         return 1001L;     } } @Transactional(rollbackFor = Exception.class) @Override public void addCart(CartPO cartPO) throws InterruptedException {      LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery()         .eq(CartPO::getMemberId, cartPO.getMemberId())         .eq(CartPO::getProductId, cartPO.getProductId())         .eq(CartPO::getProductSkuId, cartPO.getProductSkuId());     //查询商品,已添加到购物车的,就增加数量即可(业务逻辑幂等)     //因为 select 和 save 操作不是串行执行的,可能有两个线程同时查询到商品没有添加到购物车     //然后同一个商品被两个线程分别入库了,导致购物车出现相同商品的两条记录     List list = this.list(queryWrapper);     //模拟耗时     TimeUnit.SECONDS.sleep(1);     if (list == null || list.isEmpty()) {         //添加到购物车         this.save(cartPO);     } else {         CartPO updateCartPO = list.get(0);         //数量加一         LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate()             .eq(CartPO::getId, updateCartPO.getId())             .setSql("quantity = quantity + 1");          this.update(updateWrapper);     } } 编写 AOP 拦截处理 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface ApiRepeatUniqueIdSubmit {      /**      * 唯一key,支持Spring EL 表达式      *      * @return      * @ 符号引用 Spring 注册的bean      * # 符合引用方法上的参数      * param?.id  其中? 是避免param为空时,发生空指针异常      * @see https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html      */     String keyExpression(); } @Component @Aspect public class ApiRepeatSubmitUniqueIdAspect {      @Autowired     private ApplicationContext applicationContext;      @Autowired     private IDeduplicateService iDeduplicateService;      @Pointcut("@annotation(cn.hdj.repeatsubmit.aspect.ApiRepeatUniqueIdSubmit)")     public void pointCut() {     }     @Transactional(rollbackFor = Exception.class)     @Around("pointCut()")     public Object around(ProceedingJoinPoint joinPoint) throws Throwable {          Signature signature = joinPoint.getSignature();         MethodSignature msig = (MethodSignature) signature;         Method targetMethod = msig.getMethod();         ApiRepeatUniqueIdSubmit apiRepeatSubmit = targetMethod.getAnnotation(ApiRepeatUniqueIdSubmit.class);         String keyExpression = apiRepeatSubmit.keyExpression();          Map argMap = SpringElUtil.getArgMap(joinPoint);         //获取业务参数,组成唯一ID         String uniqueId = SpringElUtil.createElBuilder()                 .setArgMap(argMap)                 .setBeanFactory(applicationContext)                 .setTarget(String.class)                 .setKeyExpression(keyExpression)                 .build();          LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery()                 .eq(DeduplicatePO::getUniqueId, uniqueId);          long count = this.iDeduplicateService.count(queryWrapper);         if (count > 0) {             throw new DuplicateKeyException("不要重复提交!");         }          //插入去重表         DeduplicatePO deduplicatePO = new DeduplicatePO();         deduplicatePO.setUniqueId(uniqueId);         try {             this.iDeduplicateService.save(deduplicatePO);         } catch (Exception e) {             throw new DuplicateKeyException("不要重复提交!");         }          Object proceed = joinPoint.proceed();          //执行完删除         this.iDeduplicateService.removeById(deduplicatePO);          return proceed;     } } 3.3、分布式锁
  分布式锁可以使用 Redis 和 Zookeeper ,更多关于 Redis 和 Zookeeper 的使用 请自行查阅资料。以下使用 Redis 来实现分布式锁 引入依赖      org.redisson     redisson-spring-boot-starter     3.16.6  配置 spring:   redis:     # https://github.com/redisson/redisson/wiki/2.-Configuration     database: "0"     host: "127.0.0.1"     port: "6379"     #password: "123456" #    ssl: #    timeout: #    cluster: #      nodes: #    sentinel: #      master: #      nodes: 在 AOP 获取锁的逻辑 //创建锁 RLock lock = this.redissonClient.getLock(LOCK_PREFIX + uniqueId);  //判断是否被抢占了锁 if (lock.isLocked()) {     throw new DuplicateKeyException("不要重复提交!"); }  //尝试获取锁, 默认30秒会超时过期, 并启动线程监听,自动续签 //当客户端异常,终止了续签线程,超时后会删除锁,避免发生死锁 //如果自己手动设置了超时过期时间,则不会启动线程监听,自动续签 if (lock.tryLock()) {     try {         return joinPoint.proceed();     } finally {         //释放锁         lock.unlock();     } } throw new DuplicateKeyException("不要重复提交!"); 3.4、使用 Token 机制操作步骤1、在访问页面时,先获取 Token ,保持到指定的地方2、在点击加入购物车时,把 Token 放到 Header 或请求参数中,带给后台3、后台验证 Token 并删除,表示该 Token 已使用4、执行加入购物车逻辑5、成功响应前端(看业务需求,是否需要重新获取 Token) 创建 Token 生成和验证服务 @Service public class TokenServiceImpl implements TokenService {      public static final String TOKEN_PREFIX = "token_idempotent";     public static final String TOKEN_HEADER_NAME = "x-token";     public static final Long TOKEN_EXPIRE_TIME = 5 * 60L;      @Autowired     private RedissonClient redissonClient;      @Override     public String createToken() {         String ID = UUID.randomUUID().toString();         RBucket bucket = this.redissonClient.getBucket(String.format("%s:%s", TOKEN_PREFIX, ID), StringCodec.INSTANCE);         //默认超时5分钟         bucket.trySet(ID, TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);         return ID;     }     @Override     public boolean checkToken(HttpServletRequest request) {         // 从请求头中获取token信息         String token = request.getHeader(TOKEN_HEADER_NAME);         if (!StringUtils.hasLength(token)) {             //从参数中获取token值             token = request.getParameter(TOKEN_HEADER_NAME);         }         if (!StringUtils.hasLength(token)) {             throw new DuplicateKeyException("重复提交,提交失败");         }          RBucket bucket = this.redissonClient.getBucket(String.format("%s:%s", TOKEN_PREFIX, token), StringCodec.INSTANCE);          //获取,并删除         String ID = bucket.getAndDelete();         //不存在,则重复提交         if (ID == null) {             throw new DuplicateKeyException("重复提交,提交失败");         }         return true;     } } 在 AOP 中检查 Component @Aspect public class ApiRepeatSubmitTokenAspect {     @Autowired     private TokenService tokenService;      @Autowired     private HttpServletRequest request;      @Pointcut("@annotation(cn.hdj.repeatsubmit.aspect.ApiRepeatTokenSubmit)")     public void pointCut() {     }     @Before("pointCut()")     public void Before(JoinPoint joinPoint) {         tokenService.checkToken(request);     } } 前端进入页面时是先获取 Token $.ajax({     type: "GET",     url: "/token/create",     contentType:"application/json",     success: function(token){         sessionStorage.setItem("x-token",token);     } }); 请求时带上 Token $("#addCart").click(function(){     //按钮置灰     $("#addCart").attr("disabled","disabled");     $.ajax({         type: "POST",         url: "/cart/add",         headers:{             //携带token             "x-token":sessionStorage.getItem("x-token")         },         data: JSON.stringify({             productId: $("input[name=product_id]").val(),             productSkuId: $("input[name=product_sku_id]").val(),             productName: $("input[name=product_name]").val(),             price: $("input[name=price]").val(),             quantity: $("input[name=quantity]").val()         }),         contentType:"application/json",         success: function(msg){             console.log(msg)         },         complete: function(msg){             $("#addCart").removeAttr("disabled");         }     }); }); 总结
  以上是较为常见通用的幂等方案,但实际业务可能比较个性化,需要跟业务结合进行考虑,采用合适的方法或结合使用,例如: 如果该业务是存在状态流转,可以采用状态机策略进行业务幂等判断 如果该业务是更新数据,可以采用多版本策略,在需要更新的业务表上加上版本号 参考https://myprojectt.readthedocs.io/zh_CN/latest/项目实战04请求幂等性.html https://xie.infoq.cn/article/107fd263605e9d184b78bf093 https://segmentfault.com/a/1190000023555975
  本文作者:JiaJianHuang
  本文链接:https://www.cnblogs.com/JianJianHuang/p/15702912.html

乔布斯27年前的采访被曝光我不看中钱微软没品位最近B站上有一段特别火的遗失的乔布斯的采访,采访发生在其被排挤出苹果10年后(1985年,乔布斯被自己引荐的CEOJohnSculley排挤出苹果),重掌苹果的2年前(采访过后的1分子动理论的基本内容一物体是由大量分子组成的我们在初中已经学过,物体是由大量分子组成的。需要指出的是在研究物质的化学性质时,我们认为组成物质的微粒是分子原子或者离子。但是,在研究物体的热运动性质和规律从一升一降看广东科技新走势原标题从一升一降看广东科技新走势4月15日上午,全省科技创新大会在广州召开,会上颁发了2021年度广东省科学技术奖180个获奖项目(人),与上一年度持平,其中自然科学奖22项技术发vivoXFold折叠屏旗舰综合体验后发者能先至否?进入2022年,Top手机品牌纷纷扎入折叠屏手机市场,以多旗舰策略继续冲击高端,而市场份额一度登顶的vivo却迟迟不见动静。vivo官方表示,折叠屏方案在不断推倒重来中潜心打磨,旨天玑9000自研芯vivoX80系列跑分样张曝光韩伯啸透露迭代规则没想到vivo这次新机发布可能会这么密集,就在vivoXNote首款折叠屏新机vivoXFold以及首款平板电脑vivoPad等三款旗舰级新品之后,近日关于vivoX80系列的爆料2799起,骁龙8独显80W快充助力,iQOONeo6无敌了对于当下的智能手机市场,只能说友商们的内卷真的是越来越过了,尤其是在中端市场的表现很是激进。自红米K50系列发布以后,中端市场的惊喜就从未间断过,比高端市场还要更有料。在红米K50超大屏幕新机vivoXNote发售,vivoX60提前来砸场,大米价再现发布一款全面表现出色vivoXNote大屏旗舰机不仅是目前市面上唯一一款采用骁龙8Gen1的7英寸大屏商务旗舰,发布一款全面表现出色vivoXNote大屏旗舰机还拥有蔡司专业影像轻OPPOK10K10Pro入网首发天玑8000,将于下周发布4月15日消息,近日OPPOK系列新机K10K10Pro出现在了工信部的页面中。OPPOK10将首发联发科天玑8000处理器,搭载一块6。59英寸1080P分辨率LCD屏幕,120一分钟资讯华为苹果小米魅族一加中兴,最新消息哈喽,我是毛小毛。关注我,每日带你了解最新数码资讯!一分钟资讯第318篇,感谢你的阅读。华为Mate50Pro外观曝光近日,设计师Hoiindi基于一份尚待进一步证实的爆料,绘制了华为将为旧手机推新功能3步解决卡顿日前,华为将为用户推出最新的功能,以解决容量不足以及卡顿的问题。这项新功能为系统云翻新,用户可以通过云备份等操作,上传所有数据后恢复出厂设置,再恢复数据,最终清理掉手机中的垃圾,解小米12ultra要搞大动作近日有关小米12ultra的爆料越来越多,雷总也在社交平台疯狂推销存货11ultra,可以预见距离这一新影像旗舰发布也不远了。ultra系列作为小米高端系列,主打高性能影像,在上一
Day。jsv1。11。1发布,轻量级Javascript时间日期库Day。js是一个仅2kb大小的轻量级JavaScript时间日期处理库,目前发布了1。11。1版本,带来如下改动Bug修复重构以替换不推荐使用的String。prototype。iPhoneXSMax再上线,虽有512G256G大内存,但价格妥协了提起iPhone,大家总是有说不完的热情,主要都是向往于iPhone的高流畅度,使用体验很好,是手机界的天花板,大家自然想拥有这样的产品。不过考虑到iPhone确实很贵,所以一般用国产存储芯片发威!内存条价格杀疯了!果链惊现国产3DNAND电子发烧友网报道(文黄晶晶)根据ICInsights统计,2020年全球存储芯片市场规模达1,267亿美元,其中DRAM和NANDFlash市场规模较大,占比分别为53和44,NOAI有望解决城市地震监测的噪音难题最近,科学家开发了一套基于深度学习的计算机算法,能够从城市地震监测的大量噪声信号中辨别出地震事件。这将帮助人们未来在城市中更好地开展地震活动检测。他们的结果发表在SCIENCEAD高通胀压力下劳资大战将至?苹果纽约门店工会组织者寻求30美元最低时薪高通胀环境下,苹果在纽约市地标中央车站的纽约旗舰零售门店还未正式成立工会,就提出了涨工资的诉求。美东时间18日周一,苹果纽约中央车站工会组织者在自称FruitStandWorkerC类型转换适用的时机与场合WhentousewhichcastincUsedynamiccastforconvertingpointersreferenceswithinaninheritancehiera网约车冷下来了?4月订单比1月下滑超2亿单,司机称收入下降不少华夏时报(www。chinatimes。net。cn)记者闫晓寒卢晓北京报道5月12日早上不到八点,家住朝阳区的小李打开打车软件准备叫车,发现app弹出提示应疫情防控工作要求已暂停扫地机市场再出新旗舰,追觅S10Pro加入战团,新一代王炸出山前言2021年底,科沃斯发布X1,它支持扫地拖地自动清洗拖布自动集尘,这四项基本功能,使它成为了2021年的王炸机型。前不久,石头也发布了全功能顶级机型G10S,它除了具备科沃斯X北大发声明抗议外国厂商垄断仪器,国产激光仪器处境尴尬北京大学北京核磁共振中心发布终止与某外国仪器厂商中国区业务往来的声明!可以看出这是一次愤怒忍无可忍的反抗该中心批评该供应商在中国核磁共振谱仪市场一家独大,并且大幅提高售后服务费用和家里宽带从100M升级到200M,需要更换路由器吗?家庭宽带从100M升级到200M,需要用到千兆路由器才可以支持,如果之前不是千兆路由器的话,就需要更换,除路由器外其它如网线以及网卡都需要支持千兆传输才可以,否则宽带升级前后没什么最便宜的天玑8100来了,红米note11T系列,1500就搞定最便宜的天玑8100要多少钱?你可能会说起码要2000,而现在又一款天玑8100的神机要出世了,并且售价很大可能性是1500元。天玑8100性能相当于骁龙888,功耗可以参考骁龙8