《见「微」知「著」系列——微服务接口幂等性设计篇》(一)微服务为什么要实现接口的幂等性?常见的微服务接口幂等性解决方案有哪些? 当别人五一去淄博吃烧烤的时候,你却窝在家里,默默地打开某电商平台疯狂下单各种烧烤料,打算在家复制一下淄博烧烤,这样就不用舟车劳顿,还能一饱口福。 你正在憧憬着烤肉吃在嘴里的满足感,手一哆嗦连续了好几次支付按钮,手机立马收到一条短信,银行账户被扣了1000大洋。 “我草,我只买了200的烧烤料,怎么扣我1000大洋呢?”你一边骂骂咧咧,一边拨通了客服的电话。
杜撰这个小故事,就是为了引出今天的话题——一个好的系统应该如何防止接口的重复无效请求,也就是接口的幂等性问题。 作为用户,我们能够看到的现象就是短时间内连续点了几次支付按钮,银行卡被多扣了好几倍的大洋,其实这背后就是系统没有做好接口幂等性设计。 接口没有幂等性,某人可能就会受到损失,这次是用户,下次可能就是商家,反正总有人会受伤。 由此可见,接口幂等性的重要性。 也因为这个原因,接口幂等性是面试中出场率非常高的一个面试题,出场率高的原因就在于,在实际生产中,几乎任何一个系统都必可避免的会遇到这个问题。 本文就为大家介绍常见的微服务幂等性解决方案。 什么接口幂等性 幂等性本身是个数学概念,后来被用于计算机领域,由于本文和数学没有半脑钱的关系,因此就不再赘述数学概念。 我们主要聊聊微服务幂等性及其解决方案。 接口幂等性指的是一个接口操作一次和操作多次,结果都是相同的,即多次执行所产生的影响均与一次执行的影响相同。 举个例子。 商家查询某个商品的库存,在这个库存没有被更新的前提下,查询一次是100个,查询100万次也还是100个,只有这样才能保证数据的一致性。 再举个例子。 删除ID=6的数据,无论你执行删除操作一次还是一百次,结果都是相同的——ID=6的这条数据被删除(无论逻辑删除或者物理删除)了。 微服务为什么要实现接口的幂等性 至于微服务为什么要实现服务的幂等性这个问题,本文开头的故事其实已经给出了答案。 除了上文说到的接口重复提交,还有接口超时重试,消息重复消费等场景,都需要保证接口的幂等性。 简而言之,接口的幂等性可以保证数据的一致性。 下面介绍几种常见的微服务接口的幂等性解决方案 如果以前端、后端划分的话,接口幂等性的实现方案分为前端拦截、PRG模式和后端防护。 前端拦截 前端拦截是指在WEB页面通过JS进行请求的拦截。 类似JS如下: 举个例子。 当我们支付按钮之后,按钮立马置为不可用(置灰)状态,以此来避免用户的重复。 前端拦截虽然实现起来比较简单,但是却又致命伤。 假如用户是一个程序员甚至黑客,那他就可以直接绕过页面的JS执行,直接模拟请求后端接口(通过postman或者apipost等其他工具),这样前端拦截就没啥效果了。 这个大概就叫做降维打击了吧。 因此,前端拦截主要作为预防误操作,后端的接口校验才是重头戏,也是最后一道防线。 下面介绍几种常见的后端接口的幂等性解决方案。
PRG模式(POST-REDIRECT-GET) 当用户进行表单提交时,会重定向到另外一个提交成功页面,而不是停留在原先的表单页面,这样就避免了用户刷新导致重复提交。 同时防止了通过浏览器的前进/后退按钮导致的表单重复提交。 PRG模式是一种比较常见的前端防重策略。 关系型数据库实现接口的幂等性 通过关系型数据库实现接口的幂等性有多种方案,下面一一介绍。 通过悲观锁来实现接口的幂等性 通过悲观锁来实现接口的幂等性,就是在查询要处理的数据时,通过for update 等方式把这条数据加上锁,这样我们在操作时,就不用担心其他人修改修改数据了,因为其他人抢不到锁根本没法操作。 通过悲观锁来实现接口的幂等性,适用更新操作。 总体来说,通过悲观锁来实现接口的幂等性,需要锁住一些数据,如这个事务逻辑耗时比较久,就会导致后面其他需要处理这条数据的请求的堆积,无法及时响应,甚至超时报错。 总之,不推荐使用这种方式来实现接口的幂等性。 通过唯一索引/主键来实现接口的幂等性 本方案就是利用数据库中唯一索引/主键唯一约束的特性。 一般来说,唯一索引/主键比较适用于插入时的幂等性,可以保证一张表中只能存在一条带该唯一索引/主键的数据记录。 数据库唯一索引/主键可以避免脏数据的产生,当插入重复数据时数据库会抛出异常,以此来保证数据的唯一性。 这种方案相当于被动防御,即使接口重复提交,最终也只有一个请求会成功执行(不一定是第一个请求)。 通过唯一索引/主键来实现接口的幂等性时需要注意的是,该主键不能使用数据库的自增主键,而应该使用分布式全局ID来做主键,这样才能保证在分布式环境下ID的全局唯一性。 通过唯一索引/主键来实现接口的幂等性适用插入和删除操作。
举个例子。 我们在前端页面新增(用户)按钮时,会跳转到用户新增页面。 此时会先向后端请求分布式全局ID服务接口,一个全局唯一的ID,待用户信息填写完毕后,用户保存按钮时,全局唯一的ID和用户信息一起提交到后端。 后端进行各种业务处理,然后将用户信息入库,第一个请求是可以正常入库的,因为用户表的主键不存在这个全局唯一的ID。 但是,后续的不论是因为接口超时的重试,还是前端误操作多几次等各种原因造成的请求重复提交,都会因为表里已经存在这个全局唯一的ID了,会报主键冲突的错误。 通过乐观锁来实现幂接口的等性 常见的乐观锁可以通过版本号机制来实现,当然这种方案针对更新操作有效,对于新增操作没有效果。 大致流程是这样的: 建表时,给每个表加上一个version字段,用于表示当前数据的版本,每执行一次更新或者删除操作,version 都加上1。 注意,version只能往上加不能往下减。 SQL语句大致如下: 如上SQL所示,更新用户的昵称。 在更新之前把这个条记录查询出来,此时,版本号(version = 11)也要跟着查询出来。 当我们将昵称改为’小王呀’之后,保存按钮。 后端接口进行数据更新时,除了将ID作为条件,还会将version = 11作为条件。 一旦有其他操作先我一步更新了ID = 1111的数据,那么version必然大于11。 此时,我再根据version = 11更新,肯定是更新不成功的,然后就给可以给前端返回一个提示信息,告诉用户数据已经被修改了,请刷新数据后再更新……
Java的锁实现接口的幂等性 本方案是指通过JDK提供的锁,如 Lock 或者是 synchronized等来实现接口的幂等性。 举个例子。 我们要添加一条用户信息,结果手一抖连续点了十来下,前端没有防重复请求的措施,后端通过Jdk提供的ReentrantReadWriteLock的写锁来实现接口的幂等性,代码如下。Controller for循环用于模拟前端多次提交。Service 如上所示,防重复请求的逻辑大致如下: 1、加锁。 2、查询当前用户信息是否已经入库了,如果已经入库说明当前请求时重复请求,不做任何处理,跳转到第4步。如果没有入库,则执行第3步。 3、执行入库操作。 4、释放锁。结果
结果如上图所示,我们执行10次请求,结果控制台打印了9次重复请求,说明只有一个请求成功处理,数据库如下所示。
通过Java的锁实现接口的幂等性只支持单机环境,因为JDK内置的索都是单机索,无法处理分布式环境的接口的幂等性。 而且要执行业务查询操作,性能上也有一定的损耗,使用场景有限(新增)。 分布式锁实现接口的幂等性 分布式锁实现接口幂等性的逻辑大致如下: 在每次执行业务方法之前先尝试分布式锁。 成功到锁,说明是第一次请求,执行具体的业务逻辑即可;锁失败,说明不是第一次请求,直接丢弃当前请求即可。
一般使用 Redis 或者 ZooKeeper 来实现分布式锁。 Redis实现分布式锁 本方案是基于 SETNX 命令实现的,该命令在处理成功时返回 1,设置失败时返回 0。 该命令的含义是:当key不存在时,可以成功将 key 的值设为 value ,命令返回1;当给定的 key 已经存在,则 SETNX 不做任何动作,返回0。
基于Redis实现分布式锁有两点要注意: 1、要给key设置一个合理的过期时间,不然占用的内存越来越多。 2、key必须是业务唯一标识(一个字段或者多个字段拼接都行)。 ZooKeeper实现分布式锁 本方案利用ZooKeeper的znode节点的特性实现分布式锁。节点类型持久节点。一旦创建,则永久存在于ZooKeeper中,除非手动删除。持久有序节点。一旦创建,则永久存在于ZooKeeper中,除非手动删除。同时每个节点都会默认存在节点序号,每个节点的序号都是有序递增的,如xxx00001、xxx00002、xxx00003….xxxNNNNN。临时节点。当节点创建后,一旦服务器重启或宕机,会被自动删除。临时有序节点。当节点创建后,一旦服务器重启或宕机,会被自动删除。同时每个节点都会默认存在节点序号,每个节点的序号都是有序递增的,如xxx00001、xxx00002、xxx00003….xxxNNNNN。 主要流程如下: 1、在Zookeeper上创建一个临时节点(包含业务唯一标识)。创建失败说明是重复请求,直接返回。创建成功则执行第2步。 2、查询当前信息是否已经处理过了,如果已经处理,则直接跳转到第4步。如果没有处理,则执行第3步。 3、执行后续业务逻辑。 4、删除该临时节点。 Tonken机制实现接口的幂等性 Tonken机制实现接口的幂等性也是一种比较常见的解决方案,本方案适合大部分场景。但是该方案有一定的复杂性,而且需要前后端进行一定程度的交互来完成。 本方案需要两步: 前端Token
如上图所示。 前端在处理请求之前,需要先一个Token,Token存入Redis(如果是单体应用,也可以存在JVM内存中),用于实现接口的幂等性。 执行业务请求
如上图所示。 前端请求后端时要带着上一步生成的Token。 后端判断当前Token是否在Redis中存在,如果存在则先删除Token,然后执行业务处理逻辑。 如果不存在,说明当前请求不是第一个请求,直接返回即可。 注意 在高并发场景下,很有可能出现这种情况: 第一次请求时Token存在,然后处理具体业务逻辑,此时还没有删除Token。 恰好客户端又携带该Token发起了一个请求,由于该Token还存在,第二次请求也会正常通过,进而执行具体业务处理。 因此,推荐先删除Token,然后再执行业务逻辑。 另外,从Redis中删除Token的逻辑建议用 Lua 脚本实现,保证原子性。 注意事项 1、接口幂等性的实现与判断都需要消耗一定的资源,因此,不应该给每个接口都提供幂等性判断,要根据实际的业务情况和操作类型来进行区分。 例如,查询和删除操作就没必要提供接口幂等性判断。
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/74427.html