关于接口设计:接口幂等该如何设计和实现

前言在程序开发的过程中是否遇到如下的问题: 同一件商品手速很快多点击了几次,在后盾生成了两笔订单。同一笔订单点了因为网络卡顿,点了两次领取,后果发现反复领取了。微服务架构下利用间通过RPC调用失败,进入重试机制,导致一个申请提交屡次。黑客利用充值抓包到的数据,进行屡次调用充值、评论、拜访,造成数据的异样。这些问题均能够通过接口幂等性设计来解决。幂等性意味着同一个申请无论被反复执行多少次,都能产生雷同的后果,不会导致反复的操作或不统一的数据状态。 在古代分布式系统中,接口的幂等性设计和实现至关重要。本文将深入探讨接口幂等的重要性、实现办法以及可能面临的挑战,并提供测试接口幂等性的无效策略。 什么是接口幂等性接口幂等性指的是一个接口或操作在雷同的申请参数下,无论被执行多少次,其后果都是统一的且不会产生副作用。换句话说,如果一个申请曾经胜利执行,再次执行雷同的申请应该不会对系统状态产生任何额定的影响。例如,一个获取用户信息的接口就是幂等的,因为屡次获取同一个用户的信息不会扭转零碎的状态。 相同,非幂等接口可能会导致反复的操作和潜在的问题。以领取操作为例,如果没有实现幂等性,反复领取可能会给用户和商家带来不必要的麻烦和损失。 为什么须要接口幂等性避免反复操作:幂等性能够确保零碎不会因为反复的申请而产生反复的操作,从而防止数据谬误和不统一。进步系统可靠性:在网络不稳固或其余异常情况下,反复的申请是很常见的。幂等性能够帮忙零碎解决这些反复申请,而不会导致系统出错或不稳固。加强用户体验:用户不须要放心因为不小心反复操作而导致的问题,从而进步了用户的应用体验和满意度。简化错误处理:因为幂等接口能够平安地解决反复申请,因而在处理错误和复原时更加容易,缩小了简单的谬误复原逻辑。如何设计接口幂等性应用惟一标识:为每个申请调配一个惟一的标识,例如申请 ID 或流水号。通过在申请中传递这个惟一标识,零碎能够判断是否曾经解决过该申请。设计幂等的操作:确保操作自身是幂等的。例如,更新数据时能够采纳"更新或插入"的策略,而不是间接批改已有记录。应用事务:在波及多个数据库操作的状况下,应用事务来确保整个操作的原子性和幂等性。利用缓存:将申请的后果缓存起来,当接管到雷同的申请时,间接返回缓存中的后果,防止反复执行操作。如何实现接口幂等性以下实现形式是基于demo实现,用于阐明幂等性的设计和实现。 惟一标识:能够通过生成全局惟一的 ID(如 UUID)来标识每个申请。在申请的参数中蕴含这个 ID,服务器在解决申请时能够依据 ID 来判断是否曾经解决过该申请。服务端生成 requestId 之后将 requestId 放到redis中,当然须要给 ID 设置一个生效工夫,超时的 ID 也会被删除。 public class RequestIdGenerator { public static String generateRequestId() { Stirng uuid = UUID.randomUUID().toString(); putCacheIfAbsent(uuid); return uuid; }}在接口中,将生成的申请 ID 与申请参数一起传递给服务器。 // 生成申请 IDString requestId = RequestIdGenerator.generateRequestId();// 构建申请参数Map<String, String> requestParams = new HashMap<>();requestParams.put("requestId", requestId);requestParams.put("otherParam", "value");// 发送申请httpClient.sendRequest(requestParams);服务器在接管到申请后,能够依据申请 requestId 来判断是否曾经解决过该申请,并进行相应的解决。 当后端接管到订单提交的申请的时候,会先判断requestId在缓存中是否存在,第一次申请的时候,requestId肯定存在,也会失常返回后果,然而第二次携带同一个requestId的时候被回绝了。 幂等的操作:以订单状态更新为例,如果订单曾经处于最终状态(如已领取或已发货),再次更新订单状态不会扭转其理论状态,因而是幂等的。public class OrderService { public void updateOrderStatus(String orderId, OrderStatus status) { // 依据 orderId 获取订单 Order order = orderIdToOrderMapper orderIdToOrder(orderId); // 判断订单是否处于最终状态 if (order.isFinalStatus()) { // 订单已处于最终状态,不须要进行理论的更新操作 return; } // 更新订单状态 order.setStatus(status); orderRepository.save(order); }}事务:在数据库操作中,能够应用事务来保障操作的原子性和幂等性。如果某个操作失败,事务能够回滚到之前的状态,防止不统一的数据。@Transactionalpublic void performTransactionalOperation() { // 开启事务 Transaction transaction = transactionManager.beginTransaction(); transaction.setIsolationLevel(IsolationLevel.READ_COMMITTED); transaction.setPropagationBehavior(Propagation.REQUIRED); // 数据库操作 1 //... // 数据库操作 2 //... // 提交事务 transactionManager.commit();}开启事务是一种乐观锁实现的形式,一开始更新数据就把数据加锁了,具备强烈的独占和排他个性。 ...

February 28, 2024 · 1 min · jiezi

关于接口设计:一种接口依赖关系分层方案-京东云技术团队

1、背景到店商详迭代过程中,须要提供的对外能力越来越多,如预约日历、左近门店、为你举荐等。这其中不可避免会呈现多个下层能力依赖同一个底层接口的场景。最后采纳的计划是对外API入口进来后获取对应的能力,并发调用多项能力,由能力层调用对应的数据链路,进行业务解决。然而,随着接入性能的增多,这种状况导致了底层数据服务的反复调用,如商品配置信息,在一次API调用过程中反复调了3次,当流量增大或能力项愈多时,对底层服务的压力会成倍增加。 正值618大促,各方接口的调用都会大幅度减少。通过梳理接口依赖关系来缩小反复调用,对本零碎而言,升高了调用数据接口时的线程占用次数,能够无效降级CPU。对调用方来说,缩小了调用次数,可缩小调用方的资源耗费,保障底层服务的稳定性。 原始调用形式: 2、优化基于上述问题,采纳底层接口依赖分层调用的计划。梳理接口依赖关系,逐层向上调用,注入数据,如此将同一接口的调用抽取到某层,仅调用一次,即可在整条链路应用。 改良调用形式: 只有分层后即可在每层采纳多线程并发的形式调用,因为同一层级中的接口无先后依赖关系。 3、如何分层?接下来,如何梳理接口层级关系就至关重要。 接口梳理分层流程如下: 第一步:构建层级构造 首先获取到能力层依赖项并遍历,而后调用生成数据节点办法。办法流程如下:构建以后节点,检测循环依赖(存在循环依赖会导致栈溢出),获取并遍历节点依赖项,递归生成子节点,寄存子节点。 第二步:节点平铺 定义Map保护平铺构造,调用平铺办法。办法流程如下:遍历层级构造,判断以后节点是否已存在map中,存在时与原节点比拟将层级大的节点放入(去除反复项),不存在时间接放入即可。而后解决子节点,递归调用平铺办法,解决所有节点。 第三步:分层(分组排序) 流解决平铺构造,解决层级分组,存储在TreeMap中保护天然排序。对应key中的数据节点Set<DataNode>需用多线程并发调用,以保障链路调用工夫 1 首先,定义数据结构用于保护调用链路Q1:为什么须要定义先人节点? A1:为了判断接口是否存在循环依赖。如果接口存在循环依赖而不检测将导致调用栈溢出,故而在调用过程中要防止并检测循环依赖。在遍历子节点过程中,如果发现以后节点的先人曾经蕴含以后子节点,阐明依赖关系呈现了环路,即循环依赖,此时抛异样终止后续流程防止栈溢出。 public class DataNode { /** * 节点名称 */ private String name; /** * 节点层级 */ private int level; /** * 先人节点 */ private List<String> ancestors; /** * 子节点 */ private List<DataNode> children;}2 获取能力层的接口依赖,并生成对应的数据节点Q1:生成节点时如何保护层级? A1:从能力层依赖开始,层级从1递减。每获取一次底层依赖,底层依赖所生成的节点层级即父节点层级+1。 /** * 构建层级构造 * * @param handlers 接口依赖 * @return 数据节点集 */private List<DataNode> buildLevel(Set<String> handlers) { List<DataNode> result = Lists.newArrayList(); for (String next : handlers) { DataNode dataNode = generateNode(next, 1, null, null); result.add(dataNode); } return result;}/** * 生成数据节点 * * @param name 节点名称 * @param level 节点层级 * @param ancestors 先人节点(除父辈) * @param parent 父节点 * @return DataNode 数据节点 */private DataNode generateNode(String name, int level, List<String> ancestors, String parent) { AbstractInfraHandler abstractInfraHandler = abstractInfraHandlerMap.get(name); Set<String> infraDependencyHandlerNames = abstractInfraHandler.getInfraDependencyHandlerNames(); // 根节点 DataNode dataNode = new DataNode(name); dataNode.setLevel(level); dataNode.putAncestor(ancestors, parent); if (CollectionUtils.isNotEmpty(dataNode.getAncestors()) && dataNode.getAncestors().contains(name)) { throw new IllegalStateException("依赖关系中存在循环依赖,请查看以下handler:" + JsonUtil.toJsonString(dataNode.getAncestors())); } if (CollectionUtils.isNotEmpty(infraDependencyHandlerNames)) { // 存在子节点,子节点层级+1 for (String next : infraDependencyHandlerNames) { DataNode child = generateNode(next, level + 1, dataNode.getAncestors(), name); dataNode.putChild(child); } } return dataNode;}层级构造如下: ...

June 27, 2023 · 2 min · jiezi

关于接口设计:关于接口可维护性的一些建议-京东云技术团队

作者:D瓜哥 在做新需要开发或者相干零碎的保护更新时,尤其是波及到不同零碎的接口调用时,在可维护性方面,总感觉有很多中央差强人意。一些零星思考,抛砖引玉,心愿引发更多的思考和探讨。总结了大略有如下几条倡议: 在接口正文中退出接口文档链接将调用接口处写上被调用接口文档链接将接口源代码公布到私服仓库对于状态值常量,优先在接口参数类或者返回值类中定义如果应用 Map 对象作为传输载体,要提供 Key 值定义常量针对 Map 返回值,能够思考应用将 Map 转化成对象尽可能简化接口依赖只传递必要字段,尽量避免大而全的接口将接口的参数和返回值原始数据打印到日志中将 RPC 接口的类名及办法打印到日志中核心思想:以人为本,就近准则,触手可及上面,D瓜哥对每一条倡议做一个具体阐明。 1. 在接口正文中退出接口文档链接在做接口开发时,无论是对自有接口的降级革新,还是针对内部接口的从头接入,都波及到接口文档。不同之处是,前者的工作重点是书写或者更新接口文档;而后者是依据接口文档开发适合的接入代码。然而,常常遇到的一个麻烦是,找不到接口文档。在组内须要找老同事询问;如果是跨部门,还须要两层甚至三层的进行转接,十分麻烦。 D瓜哥认为,在这种状况下,为了不便大家保护,最好的方法就是将接口文档链接间接放在代码正文中,这样后续保护的人员,间接就能够点击链接中转接口文档,简略不便高效。如果是新建的接口,就能够先创立一个空文档,把链接放在正文中,后续再书写文档内容。如果是保护已有接口,能够在保护时,将缺失的链接退出到正文中,本人不便,也不便其他人进行后续的保护更新。这样,在循序渐进的过程中,逐渐就能够把文档链接补充到代码中,不便保护代码,也同步更新文档。 2. 将调用接口处写上被调用接口文档链接在调用其余零碎的接口时,没有接口文档,简直举步维艰。在第一次接入接口时,绝大多数状况下,都是参考着接口文档做接入工作。然而,目前的状况时,接入时参考文档,参考完就顺手把文档给“扔了”。后续如果还须要做进一步降级保护,还须要到处找接口文档;另外,交互的零碎不免有一些 Bug,在和其余系统维护人员对接解决 Bug 时,只有接口没有文档,对方可能也须要去找文档链接。无形中,很多工夫都节约在了找文档的过程中。 D瓜哥最近尝试了一个实际,就是在接口调用的中央,把接口文档链接当做正文退出到代码中。这样,无论是后续保护降级,还是沟通协调解决问题,都十分不便。他人问接口是什么,连贯口+文档都能够一把复制就搞定。 通过最近一段时间的实际状况来看,这个解决十分不便,是一个十分值得推广的实际。再插一句,也能够像一条倡议一样,能够在保护代码时,一直把已接入的接口文档退出到调用接口的中央,循序渐进,不便后续人保护降级。 3. 将接口源代码公布到私服仓库接口文档链接在正文中,在构建后果中就不复存在了。所以,为了不便接口应用方能够在接口中查问到对应的接口文档,就须要把源码也公布到私服仓库中。 这里只阐明一下 Java 的相干解决方法。如果应用 Maven 作为构建工具的话,默认是不会将源代码公布到私服仓库中的。对于如何将源代码公布到,在 降级 Maven 插件:将源码公布到私服仓库 中曾经做过相干介绍,这里就不再赘述。 除了将源码公布到私服仓库,另外,还倡议编译构建时,放弃办法的原始参数命名。这个也能够通过配置 Maven 插件来实现,具体配置见: 降级 Maven 插件:字节码文件蕴含原始参数名称。 4. 对于状态值常量,优先在接口参数类或者返回值类中定义在做接口开发时,很多数据都有一个状态值,比方订单状态,再比方接口状态等等。目前的一个状况时,这些状态值大部分书写在文档中,在接入接口时,须要接入方自定义这些状态值。这就有些繁琐了,而且状态定义也不明确,甚至有可能脱漏一些重要的状态值。有些懒省事,间接在代码中硬编码一个魔法值,后续保护的跟还须要依据上下文反推这个值的含意,十分不利于保护。 D瓜哥集体感觉,有两个解决方法: 如果状态值不是很多,优先在接口参数类或者返回值类中定义。如果状态值很多,能够思考独自抽取成一个常量类或者枚举类。这样应用的时候,触手可及。不须要到处去找。 5. 如果应用 Map 对象作为传输载体,要提供 Key 值定义常量有些零碎可能思考不便减少字段,抉择应用 Map 作为数据载体。本人开发的时候很爽,然而给接口接入却十分不敌对。接入方从 Map 中获取数据时,要么本人定义 Key 值;要么间接应用魔法值硬编码在代码中。应用前者计划,就须要在各个接入方都须要自定义一套;应用后者,初期是省事了,起初保护的人员就懵逼了。这都无形中减少了很多保护老本。 D瓜哥感觉一个计划更优,那就是间接由接口提供方来定义这些能够取值的 Key 值常量。这样,任何接入方都能够间接应用这些常量。 6. 针对 Map 返回值,能够思考应用将 Map 转化成对象针对 Map 的解决,即便依照 如果应用 Map 对象作为传输载体,要提供 Key 值定义常量 举荐的做法,定义了相干的 Key,在取值时,也略有麻烦,须要一直的 map.get(KEY)。一个更简略的办法是自定义一个类型,应用工具将 Map 对象转化成自定义类型的对象。这样就能够间接应用办法调用来取值。 ...

May 17, 2023 · 2 min · jiezi

关于接口设计:如何设计一个安全的对外接口

安全措施安全措施大体来看次要在两个方面,一方面就是如何保证数据在传输过程中的安全性,另一个方面是数据曾经达到服务器端,服务器端如何辨认数据,如何不被攻打;上面具体看看都有哪些安全措施。 1、数据加密数据在传输过程中是很容易被抓包的,如果间接传输比方通过http协定,那么用户传输的数据能够被任何人获取;所以必须对数据加密,常见的做法对关键字段加密比方用户明码间接通过md5加密;当初支流的做法是应用https协定,在http和tcp之间增加一层加密层(SSL层),这一层负责数据的加密和解密; 2、数据加签数据加签就是由发送者产生一段无奈伪造的一段数字串,来保证数据在传输过程中不被篡改;你可能会问数据如果曾经通过https加密了,还有必要进行加签吗?数据在传输过程中通过加密,实践上就算被抓包,也无奈对数据进行篡改;然而咱们要晓得加密的局部其实只是在外网,当初很多服务在内网中都须要通过很多服务跳转,所以这里的加签能够避免内网中数据被篡改; 3、工夫戳机制数据是很容易被抓包的,然而通过如上的加密,加签解决,就算拿到数据也不能看到实在的数据;然而有不法者不关怀实在的数据,而是间接拿到抓取的数据包进行歹意申请;这时候能够应用工夫戳机制,在每次申请中退出以后的工夫,服务器端会拿到以后工夫和音讯中的工夫相减,看看是否在一个固定的工夫范畴内比方5分钟内;这样歹意申请的数据包是无奈更改外面工夫的,所以5分钟后就视为非法申请了; 4、AppId机制大部分网站根本都须要用户名和明码能力登录,并不是谁来能应用我的网站,这其实也是一种平安机制;对应的对外提供的接口其实也须要这么一种机制,并不是谁都能够调用,须要应用接口的用户须要在后盾开明appid,提供给用户相干的密钥secret;在调用的接口中须要通过appid+secret获取到access_token,而后每个申请都须要附带access_token,服务器端会进行相干的验证; 5、限流机制原本就是实在的用户,并且开明了appid,然而呈现频繁调用接口的状况;这种状况须要给相干appid限流解决,罕用的限流算法有令牌桶和漏桶算法; 6、黑名单机制如果此appid进行过很多非法操作,或者说专门有一个中黑零碎,通过剖析之后间接将此appid列入黑名单,所有申请间接返回错误码; 7、数据合法性校验这个能够说是每个零碎都会有的解决机制,只有在数据是非法的状况下才会进行数据处理;每个零碎都有本人的验证规定,当然也可能有一些常规性的规定,比方身份证长度和组成,电话号码长度和组成等等; 如何实现以上大体介绍了一下罕用的一些接口安全措施,当然可能还有其余我不晓得的形式,心愿大家补充,上面看看以上这些办法措施,具体如何实现; 1、数据加密当初支流的加密形式有对称加密和非对称加密;对称加密:对称密钥在加密和解密的过程中应用的密钥是雷同的,常见的对称加密算法有DES,AES;长处是计算速度快,毛病是在数据传送前,发送方和接管方必须约定好秘钥,而后使单方都能保留好秘钥,如果一方的秘钥被泄露,那么加密信息也就不平安了;非对称加密:服务端会生成一对密钥,私钥寄存在服务器端,公钥能够公布给任何人应用;长处就是比起对称加密更加平安,然而加解密的速度比对称加密慢太多了;宽泛应用的是RSA算法;RSA的非对称加密实现参考这里 两种形式各有优缺点,而https的实现形式正好是联合了两种加密形式,整合了单方的长处,在平安和性能方面都比拟好; 对称加密和非对称加密代码实现,jdk提供了相干的工具类能够间接应用,此处不过多介绍;对于https如何配置应用相对来说简单一些,能够参这篇文章HTTPS剖析与实战 2、数据加签数据签名应用比拟多的是md5算法,将须要提交的数据通过某种形式组合(如:参数名ASCII码从小到大排序)为一个字符串,而后通过md5生成一段加密字符串,这段加密字符串就是数据包的签名,服务端接管到数据后,以同样的形式进行操作,而后比拟签名,就能够辨认数据是否被篡改过,数据签名一个简略的例子: str:参数1={参数1}&参数2={参数2}&……&参数n={参数n}&key={用户密钥};MD5.encrypt(str);3、工夫戳机制解密后的数据,通过签名认证后,咱们拿到数据包中的客户端工夫戳字段,而后用服务器以后工夫去减客户端工夫,看后果是否在一个区间内,设计思维如下: long interval=5*60*1000;//超时工夫long clientTime=request.getparameter("clientTime");long serverTime=System.currentTimeMillis();if(serverTime-clientTime>interval){ return new Response("超过解决时长")}4、AppId机制生成一个惟一的AppId即可,密钥Secret应用字母、数字等特殊字符随机生成即可;生成惟一AppId依据理论状况看是否须要全局惟一;然而不论是否全局惟一最好让生成的Id有如下属性:趋势递增:这样在保留数据库的时候,应用索引性能更好;信息安全:尽量不要间断的,容易发现法则;对于全局惟一Id生成的形式常见的有类snowflake形式等;对于access_token的生成能够应用JWT生成,存入redis中并设置过期工夫。 String access_token = JWT.create().withAudience(employee.getId().toString(), employee.getName()).sign(Algorithm.HMAC256(secret));5、限流机制罕用的限流算法包含:令牌桶限流,漏桶限流,计数器限流;1.令牌桶限流令牌桶算法的原理是零碎以肯定速率向桶中放入令牌,填满了就抛弃令牌;申请来时会先从桶中取出令牌,如果能取到令牌,则能够持续实现申请,否则期待或者拒绝服务;令牌桶容许肯定水平突发流量,只有有令牌就能够解决,反对一次拿多个令牌;2.漏桶限流漏桶算法的原理是依照固定常量速率流出申请,流入申请速率任意,当申请数超过桶的容量时,新的申请期待或者拒绝服务;能够看出漏桶算法能够强制限度数据的传输速度;3.计数器限流计数器是一种比较简单粗犷的算法,次要用来限度总并发数,比方数据库连接池、线程池、秒杀的并发数;计数器限流只有肯定工夫内的总申请数超过设定的阀值则进行限流; 具体基于以上算法如何实现,Guava提供了RateLimiter工具类基于基于令牌桶算法: //示意1秒钟容许解决的申请数量RateLimiter limiter = RateLimiter.create(200);if (!limiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) { result.put("code", "10003"); result.put("msg", "接口的访问量超过限度!"); return result;}以上形式只能用在单利用的申请限流,单利用的申请限流还能够通过AtomicInteger,Semaphore来实现,然而上述计划都不反对集群限流,不能进行全局限流;这个时候就须要分布式限流,能够基于redis+lua来实现;能够参考这篇文章 6、黑名单机制咱们能够给每个用户设置一个状态比方包含:初始化状态,失常状态,中黑状态,敞开状态等等;或者咱们间接通过分布式配置核心,间接保留黑名单IP列表,每次查看是否在列表中即可;理论我的项目中往往因为网关或者反向代理服务器的存在,服务端通过request.getRemoteAddr()不能间接拿到客户端的真是ip,这里须要依据理论状况依照具体的参数进行获取; public static String getRemoteAddr(HttpServletRequest req) { if (req == null) throw new NullPointerException("HttpServletRequest is null"); // X-Forwarded-For:Squid 服务代理 String ip = req.getHeader("X-Forwarded-For"); // Proxy-Client-IP:apache 服务代理 if ((ip == null) || (ip.length() == 0) || "unknown".equalsIgnoreCase(ip)) ip = req.getHeader("Proxy-Client-IP"); // WL-Proxy-Client-IP:weblogic 服务代理 if ((ip == null) || (ip.length() == 0) || "unknown".equalsIgnoreCase(ip)) ip = req.getHeader("WL-Proxy-Client-IP"); // X-Real-IP:nginx服务代理 if ((ip == null) || (ip.length() == 0) || "unknown".equalsIgnoreCase(ip)) ip = req.getHeader("X-Real-IP"); // HTTP_CLIENT_IP:有些代理服务器 if ((ip == null) || (ip.length() == 0) || "unknown".equalsIgnoreCase(ip)) ip = req.getHeader("HTTP_CLIENT_IP"); // 还是不能获取到,最初再通过request.getRemoteAddr();获取 if ((ip == null) || (ip.length() == 0) || "unknown".equalsIgnoreCase(ip)) ip = req.getRemoteAddr(); // 有些网络通过多层代理,那么获取到的ip就会有多个,个别都是通过逗号(,)宰割开来,并且第一个ip为客户端的实在IP return ((ip == null) || (ip.trim().length() == 0)) ? null : ip.split(",")[0].trim();}7、数据合法性校验合法性校验包含:常规性校验以及业务校验;常规性校验:包含签名校验,必填校验,长度校验,类型校验,格局校验等;业务校验:依据理论业务而定,比方订单金额不能小于0等; ...

April 6, 2022 · 1 min · jiezi

关于接口设计:接口服务中的幂等性设计和防重保证详细分析幂等性设计几种实现方法

什么是幂等性幂等性定义: 一次和屡次申请某一个资源对于资源自身应该具备同样的后果任意屡次执行对资源自身所产生的影响均与一次执行的影响雷同幂等性定义的几个重点: 幂等不仅仅只是一次或者屡次申请对资源没有副作用 比方,查询数据库操作,没有增删改,无论多少次操作对数据库都没有任何影响幂等还包含第一次申请的时候对资源产生了副作用,然而当前的屡次申请都不会再对资源产生副作用幂等关注的是当前屡次申请是否对资源产生副作用,并不关注后果网络超时等问题,不是幂等的探讨范畴幂等性是零碎服务对外一种承诺,而不是实现承诺只有调用接口胜利,内部屡次调用对系统的影响是统一的申明为幂等的服务会认为内部调用失败是常态,并且失败后必然会有重试 幂等性的应用场景业务开发中,常常遇到反复提交的状况: 因为网络问题无奈收到申请后果而从新发动申请前端的操作抖动而造成的反复提交的状况在交易系统中,领取零碎这种反复提交造成的问题尤为显著: 用户在APP上间断点击屡次提交订单,后盾应该只产生一个订单向领取零碎发动申请,因为网络问题或者零碎Bug问题导致重发,领取零碎应该只做一次扣除操作申明幂等的服务认为,内部调用者会存在屡次调用的状况,为了避免内部屡次调用对系统的数据状态产生屡次扭转,须要将服务设计为幂等 幂等和防重反复提交的状况和服务幂等的初衷是不同的 反复提交是在第一次申请曾经胜利的状况下 ,人为地进行屡次操作, 导致不满足幂等要求的服务屡次扭转状态幂等更多应用的状况是第一次申请因为某些状况,不如超时,而导致不晓得后果或者申请失败的异常情况下,发动屡次申请幂等的目标是申请屡次确认第一次申请胜利,不会因为屡次申请而呈现屡次的状态变动 保障幂等性的状况在SQL中,有以下三种场景,只有第三种场景须要保障幂等性: SELECT col1 FROM tab1 WHERE col2=2 : 无论执行多少次都不会扭转状态,是人造的幂等UPDATE tab1 SET col1=1 WHERE col2=2 : 无论执行胜利多少次状态都是统一的,也是幂等操作UPDATE tab1 SET col1=col1+1 WHERE col2=2: 每次执行的后果都会发生变化,这种不是幂等的,要采取策略保障幂等性 设计幂等性服务幂等使得客户端逻辑解决很简略,然而服务端逻辑会很简单满足幂等性服务须要蕴含两点逻辑: 首先去查问上一次的执行状态,如果没有则认为是第一次申请在服务扭转状态的业务逻辑前保障防反复提交的逻辑 保障幂等策略幂等须要通过惟一的业务单号来保障: 雷同的业务单号,认为是同一业务应用惟一的业务单号确保:前面屡次雷同业务单号的解决逻辑和执行成果是统一的幂等实现示例-领取: 先查问订单是否领取过如果曾经领取过,返回领取胜利如果没有领取,则进行领取流程,批改订单的状态为已领取 防反复提交策略在保障幂等的策略中,执行是分两步执行的,前面一步依赖下面一步的查问后果,这样就无奈保障原子性无奈保障原子性在高并发的状况下会存在问题: 第二次申请在第一次申请的下一步订单状态没有批改为"已领取状态"时进行为了解决这个问题 :将查问和变更状态操作加锁,并将并行操作改为串行执行 乐观锁如果只是更新已有的数据,没有必要对业务进行加锁设计表构造时应用乐观锁,个别通过version来实现乐观锁: 保障执行效率保障幂等 UPDATE tab1SET col1=1,version=version+1WHERE version=#version# 因为ABA问题会导致乐观锁存在生效的状况,只有保障version值自增就不会呈现ABA的问题 防重表应用orderNo作为去重表中的惟一索引,每次申请都依据订单号orderNo向去重表中插入一条数据: 第一次申请查问订单领取状态: 订单没有领取进行领取操作无论胜利与否,执行实现之后更新订单的状态为胜利或失败,删除去重表中的数据后续订单因为表中的惟一索引插入失败,返回操作失败,直到第一次申请实现(胜利或者失败)防重表的作用是实现加锁的性能 分布式锁能够应用Redis分布式锁代替防重表的性能示例: 订单发动领取申请领取零碎会去Redis缓存中查问是否存在该订单Key如果不存在,向Redis中减少Key为订单号查问订单领取是否曾经领取如果没有则进行领取,领取实现后删除该订单的Key通过Redis实现分布式锁,只有这次订单申请实现,下次申请才会进来比照去重表,Redis分布式锁将放并发做在缓存中,效率更高同一时间只能实现一次领取申请 token令牌token令牌分为两个阶段: 申请token阶段: 在进入到提交订单页面之前,须要订单零碎依据用户信息向领取零碎发动一次申请token的申请领取零碎将token保留到Redis缓存中,给领取阶段应用领取阶段: 订单零碎获取到申请的token, 发动领取申请,领取零碎查看Redis是否存在该token 如果存在,示意第一次发动领取申请,删除缓存中的token开始领取逻辑解决如果缓存中不存在,示意非法申请 领取缓冲区领取缓冲区: 将订单的领取申请都疾速地接管下来,是一个疾速接管申请的缓冲管道应用异步工作解决管道中的数据,过滤调掉反复的待领取的数据长处: 同步转异步,高吞吐毛病: 无奈及时返回领取后果,须要后续监听领取后果的异步返回 幂等是为了简化客户端逻辑,然而减少了服务提供者的逻辑和老本幂等的应用须要依据具体场景具体分析减少了额定管制幂等的业务逻辑,简单了业务性能将并行的性能转化为串行,升高了执行效率

June 19, 2021 · 1 min · jiezi

关于接口设计:接口如何优雅的串行

在平时开发中,尽管咱们都会尽可能的去防止接口串行调用,然而在某种特定场景下无奈防止要这样做。上面将介绍一些常见的办法以及存在的问题 1、回调2、promise3、async/await

December 14, 2020 · 1 min · jiezi