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

前言在程序开发的过程中是否遇到如下的问题: 同一件商品手速很快多点击了几次,在后盾生成了两笔订单。同一笔订单点了因为网络卡顿,点了两次领取,后果发现反复领取了。微服务架构下利用间通过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

前端面试每日-31-第184天

今天的知识点 (2019.10.17) —— 第184天[html] 如何给一个下拉选项进行分组?[css] 请描述下你对translate()方法的理解[js] 说下你对面向对象的理解[软技能] 你上家公司的接口是怎么管理的?《论语》,曾子曰:“吾日三省吾身”(我每天多次反省自己)。 前端面试每日3+1题,以面试题来驱动学习,每天进步一点! 让努力成为一种习惯,让奋斗成为一种享受!相信 坚持 的力量!!!欢迎在 Issues 和朋友们一同讨论学习! 项目地址:前端面试每日3+1 【推荐】欢迎跟 jsliang 一起折腾前端,系统整理前端知识,目前正在折腾 LeetCode,打算打通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个Star, 同时欢迎微信扫码关注 前端剑解 公众号,并加入 “前端学习每日3+1” 微信群相互交流(点击公众号的菜单:进群交流)。 学习不打烊,充电加油只为遇到更好的自己,365天无节假日,每天早上5点纯手工发布面试题(死磕自己,愉悦大家)。希望大家在这浮夸的前端圈里,保持冷静,坚持每天花20分钟来学习与思考。在这千变万化,类库层出不穷的前端,建议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢迎大家到Issues交流,鼓励PR,感谢Star,大家有啥好的建议可以加我微信一起交流讨论!希望大家每日去学习与思考,这才达到来这里的目的!!!(不要为了谁而来,要为自己而来!)交流讨论欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个[Star] https://github.com/haizlin/fe...

October 17, 2019 · 1 min · jiezi

程序员过关斩将你的业务是可变的吗

请不要跟我说用ES或者其他,其实很多中小公司的业务就是如此,就是基于mysql或者sqlserver 来搞这样的业务业务场景不知道通过D妹子的阐述,大家了解情况了没。这里菜菜再详细说一下。D妹子的程序记录了订单的log来供其他业务(比如统计)使用,这里就以统计业务来说,OrderLog表设计如下: 列名数据类型描述OrderIdnvarchar(100)订单号,主键UserIdint下单用户idAmountint订单的金额其他字段省略... 除此之外还有一个用户信息表UserInfo,设计如下: 列名数据类型描述UserIdint用户id,主键ProvinceIdint用户省的idCityIdint用户市的idCountyIdint用户区县的id涉及到拆单等复杂的订单操作,表的设计可能并非如此,但是不影响菜菜要说的事变数的业务现在假如要统计某个省的订单总数,sql如下: select count(0) from OrderLog o inner join UserInfo u on o.UserId=u.UserId where ProvinceId=@ProvinceId有问题吗,sql没问题,这时候用户A的省市区县信息突然变了(也许是在其他地区买房,户口迁移了),也就是说UserInfo表里的信息变了,那用以上的sql统计用户A以前省市区县的订单信息是不是就会出错了呢?(产品狗说在哪下的订单就属于哪的订单) 业务的定位以上的问题你觉得是不是很简单呢?只要稍微修改一下表也许就够了。但是,菜菜要说的不是针对这一个业务场景,而是所有的业务场景的设计。那你有没有想过为什么D妹子的设计会出现这样的问题呢? 深刻理解业务才能避免以上类似的错误发生,一定要深刻理解不变和可变的业务点。 拿D妹子的统计来说,你的业务是统计区域的订单数,这个业务在产品设计上定义的是不变性,也就是说在行为产生的那个时间点就确定了业务性质,这个业务的性质不会随着其他变而变。具体到当前业务就是:用户在X省下的订单不会随着用户区域信息的变化而变化,说白了就是说用户在X省生成的订单永远属于X省。 谈到业务性质的不变性,对应的就有业务的可变性。假如你开发过类似于QQ空间这样的业务,那肯定也做过类似访客的功能。当要显示访客记录的时候,访客的名称在多数情况的设计中属于可变性的业务。什么意思呢?也就是说一个用户修改了姓名,那所有显示这个用户访问记录的的地方姓名都会同时改变。 说到这里,各位再回头看一下D妹子的业务,这里又牵扯到一个系统设计的问题,众所周知,一个好的系统设计需要把业务的变化点抽象提取出来,D妹子订单统计的业务变化点在于用户的省市区县会变化,订单的金额、订单号等信息不会变化。所以你们觉得是不是D妹子的数据表可以修改一下呢? 数据表的改进改进用户信息按照以上的阐述,D妹子业务的变化点在于用户的省市区域信息,所以可以把用户信息的表抽象提取出来,主键不再是用户id 列名数据类型描述Idint主键Id,主键UserIdint用户idProvinceIdint用户省的idCityIdint用户市的idCountyIdint用户区县的id这样的话用户订单log表中就变为 列名数据类型描述OrderIdnvarchar(100)订单号,主键UserBIdint对应用户表中的主键idAmountint订单的金额其他字段省略... 这样设计的话,如果用户的省市区县信息有变动,相应的用户信息表中会存在多条用户省市区县数据 这里的用户信息表并非是用户对象的主表,而是根据订单业务衍生出来的表改进业务数据表根据业务的变性和不变性,既然把订单区域统计的业务定义为不变的业务性质,那订单的log表完全可以这样设计 列名数据类型描述OrderIdnvarchar(100)订单号,主键UserIdint下单用户idProvinceIdint用户省的idCityIdint用户市的idCountyIdint用户区县的idAmountint订单的金额其他字段省略... 写在最后各位读到这里,可能会感觉菜菜这次写的其实很鸡肋,但是,D妹子的场景却是真实环境中遇到的问题。问题的本质还是变性业务和非变性业务的定义和划分,和架构设计一样,数据库的设计其实也需要把变动的业务存储点进行抽象,其实应该说是抽离出来。 希望大家有所收获 --菜菜 添加关注,查看更精美版本,收获更多精彩

June 14, 2019 · 1 min · jiezi

QQ音乐API-koa2实现-全接口实现

QQMusicAPIQQ音乐API koa2 版本, 通过Web网页版请求QQ音乐接口数据, 有问题请提 issue, 或者你有其他想法欢迎PR.Github 知乎 掘金 环境要求因为本项目采用的是koa2, 所以请确保你的node版本是7.6.0+node -v安装git@github.com:Rain120/qq-music-api.gitnpm install项目启动// npm i -g nodemonnpm run start// or don't install nodemonnode app.js项目监听端口是3200 使用文档使用apis详见文档 关于本人Rain120: 前端菜鸟, 入职前端1年, 公司的技术栈是React, 因为公司官网由我重构过, 我使用的Vue.js重构的。目前正在脱坑, 求大佬内推呀API结构图 API接口koa接口说明(参数, 地址, 效果图)获取QQ音乐产品的下载地址接口说明: 调用此接口, 可获取QQ音乐标准产品下载链接 接口地址: /downloadQQMusic 调用例子: /downloadQQMusic 示例截图: 获取歌单分类接口说明: 调用此接口, 可获取歌单分类, 包含category信息 接口地址: /getSongListCategories 调用例子: /getSongListCategories <details> <summary>SortID</summary> sortId: 1, sortName: 默认sortId: 2, sortName: 最新sortId: 3, sortName: 最热sortId: 4, sortName: 评分sortId: 5, sortName: none</details> ...

June 4, 2019 · 4 min · jiezi

对接口规范的一些思考

起因团队中如果不同的项目,不同的人员可能在接口设计上有许多不统一的地方。导致了开发效率低下的问题。由于我在工作中遇到了,所以整理下来,说一说自己的一些看法。 怎样进行接口规范化因为每个人对自己使用语言有不同的理解、HTTP协议熟悉程度不同、思维逻辑、开发经验不一样。对接口规范有想法的人应该提出自己的观点,给出自己的理由。让别人去评价,讨论出一套统一的规则,最终统一成一个内部的标准。形成统一标准后由相关人员写出示例。例如前端要对GET请求针对jQuery.ajax、fetch、axios等请求库给出示例代码。以后直接参照示例代码进行开发。 由于每个项目在定义接口时有许多不同的方式,我根据以往的经验,从请求方法、请求头、请求体、响应状态码、响应体等几个方面对接口的规范说说自己的看法。 我对标准的理解我们不同的项目使用的请求方式大概有两种: GET、POSTGET、POST、PUT、DELETE如果使用前者,PSOT的URL中应该指明要执行的动作,而后者不需要指定。 POST /api/user/add HTTP/1.1POST /api/user/set HTTP/1.1POST /api/user/delete HTTP/1.1# 这里例子中约定PUT是新增,POST是修改POST /api/user HTTP/1.1PUT /api/user HTTP/1.1DELETE /api/user HTTP/1.1至于使用前者还是后者,我更倾向于前者。如果使用后者时,什么情况下使用PSOT、什么情况下使用PUT,搜索到了到了一些资料,但看不懂。 接口URL接口URL应该见名知意,有一定的规律。这里我不太懂,就不做过多赘述。 数据类型的约定前端请求中不应该有undefined,因为后端不支持(json也不支持)该数据类型。如果Content-Type为multipart/form-data,前端不应该传null,因为会被转化成字符串,后端不能判断出这是用户输入还是null类型。 每个项目应该约定请求时下面这些数据代表什么意思 null数据类型表示什么空字符串类型表示什么GET请求作用GET请求应该读取数据,不应该产生任何的“副作用”操作。这里要注意一点浏览器对URL长度是有限制的,如果查询的URL长度过长会引起不可预期的后果。可以采用POST/PUT进行查询。 方式GET请求的参数应该放在请求的URL中而不应该放在请求体中。例如下面是一个标准的这个GET请求(不相关HTTP头字段已剔除) GET /api/user?userId=12345 HTTP/1.1Host: http://www.example.comPOST/PUT/DELETE请求这三种请求方法传参数格式都相同,下面以POST为例。POST类型使用的方式非常多样,见识过各种各样奇葩的方式,也是耽误时间最长的,严重影响开发进度。这里只讨论我认为标准的方式。 作用POST请求用于新增、修改或删除数据,少数情况下用于查询数据。 方式POST请求的参数必须放在请求体中。而POST的请求方式有四种方式: application/x-www-form-urlencodedmultipart/form-dataapplication/jsontext/xml这几种方式通过HTTP头中的Content-Type头字段进行控制。 multipart/form-data我们现在使用的最多的是multipart/form-data。 POST /api/user/set HTTP/1.1Host: http://www.example.comContent-Type: multipart/form-data; boundary=----WebKitFormBoundary2KbanAZwv0mKceX0------WebKitFormBoundary2KbanAZwv0mKceX0Content-Disposition: form-data; name="userName"张三------WebKitFormBoundary2KbanAZwv0mKceX0Content-Disposition: form-data; name="userId"123456------WebKitFormBoundary2KbanAZwv0mKceX0--这种方式不适合复杂数据类型的传递,例如有个接口需要同时修改多个用户: const userList = [ { userID: 123, userName: '张三', isAdmin: true, }, { userID: 456, userName: '李四', isAdmin: false, },];那么在POST请求时只能这么做 POST /api/userlist/set HTTP/1.1Host: http://www.example.comContent-Type: multipart/form-data; boundary=----WebKitFormBoundary2KbanAZwv0mKceX0------WebKitFormBoundary2KbanAZwv0mKceX0Content-Disposition: form-data; name="userID"123,456------WebKitFormBoundary2KbanAZwv0mKceX0Content-Disposition: form-data; name="userName"张三,李四------WebKitFormBoundary2KbanAZwv0mKceX0Content-Disposition: form-data; name="isAdmin"1,0------WebKitFormBoundary2KbanAZwv0mKceX0--更重要的是这种方式不支持数据类型,传入的所有格式的数据都会转成字符串类型。后端经常要使用1表示true,需要将数组或对象拆分开。 ...

May 29, 2019 · 1 min · jiezi

系统的讲解-PHP-接口签名验证

概览工作中,我们时刻都会和接口打交道,有的是调取他人的接口,有的是为他人提供接口,在这过程中肯定都离不开签名验证。 在设计签名验证的时候,一定要满足以下几点: 可变性:每次的签名必须是不一样的。时效性:每次请求的时效性,过期作废。唯一性:每次的签名是唯一的。完整性:能够对传入数据进行验证,防止篡改。下面主要分享一些工作中常用的加解密的方法。 常用验证举例:/api/login?username=xxx&password=xxx&sign=xxx 发送方和接收方约定一个加密的盐值,进行生成签名。 示例代码: //创建签名private function _createSign(){ $strSalt = '1scv6zfzSR1wLaWN'; $strVal = ''; if ($this->params) { $params = $this->params; ksort($params); $strVal = http_build_query($params, '', '&', PHP_QUERY_RFC3986); } return md5(md5($strSalt).md5($strVal));}//验证签名if ($_GET['sign'] != $this->_createSign()) { echo 'Invalid Sign.';}上面使用到了 MD5 方法,MD5 属于单向散列加密。 单向散列加密定义把任意长的输入串变化成固定长的输出串,并且由输出串难以得到输入串,这种方法称为单项散列加密。 常用算法MD5SHAMACCRC优点以 MD5 为例。 方便存储:加密后都是固定大小(32位)的字符串,能够分配固定大小的空间存储。损耗低:加密/加密对于性能的损耗微乎其微。文件加密:只需要32位字符串就能对一个巨大的文件验证其完整性。不可逆:大多数的情况下不可逆,具有良好的安全性。缺点存在暴力破解的可能性,最好通过加盐值的方式提高安全性。应用场景用于敏感数据,比如用户密码,请求参数,文件加密等。推荐密码的存储方式password_hash() 使用足够强度的单向散列算法创建密码的哈希(hash)。 示例代码: //密码加密$password = '123456';$strPwdHash = password_hash($password, PASSWORD_DEFAULT);//密码验证if (password_verify($password, $strPwdHash)) { //Success} else { //Fail}PHP 手册地址: http://php.net/manual/zh/func... 对称加密定义同一个密钥可以同时用作数据的加密和解密,这种方法称为对称加密。 常用算法DESAESAES 是 DES 的升级版,密钥长度更长,选择更多,也更灵活,安全性更高,速度更快。 ...

May 10, 2019 · 4 min · jiezi