
- 外文名
- idempotent
- 常见于
- 抽象代数
- 中文名
- 幂等
- 属 性
- 一个数学与计算机学概念
-
2022-03-22 10:31:46
微服务与幂等性
随着应用架构由单体架构到微服务架构进行演变,现如今市面上超过50%的应用都会基于分布式或微服务完成系统架构设计。在微服务架构体系内,就会存在若干个微服务,这些服务可能基于RPC或者HTTPS等协议进行通讯。那么既然服务之间存在相互调用,那么必然存在服务调用延迟或者失败的情况,当出现这种问题,服务端会进行重试等操作或客户端有可能会进行多次点击提交。如果这样请求多次的话,那最终处理的数据结果就一定要保证统一,如支付场景。此时就需要通过保证业务幂等性方案来完成。
幂等性简介
幂等本身是一个数学概念。即 f(n) = 1^n ,无论n为多少,f(n)的值永远为1。在编程开发中,对于幂等的定义为:无论对某一个资源操作了多少次,其影响都应是相同的。 换句话说就是:在接口重复调用的情况下,对系统产生的影响是一样的,但是返回值允许不同,如查询。
幂等性包括数据幂等、接口幂等、服务幂等、消息幂等。以SQL为例:
select * from table where id=1
。此SQL无论执行多少次,虽然结果有可能出现不同,都不会对数据产生 改变,具备幂等性。insert into table(id,name) values(1,'小莫')
。此SQL如果id或name有唯一性约束,多次操作只允许插 入一条记录,则具备幂等性。如果不是,则不具备幂等性,多次操作会产生多条数据。update table set score=100 where id = 1
。此SQL无论执行多少次,对数据产生的影响都是相同的。具备幂等性。update table set score=50+score where id = 1
。此SQL涉及到了计算,每次操作对数据都会产生影响。 不具备幂等性。delete from table where id = 1
。此SQL多次操作,产生的结果相同,具备幂等性。
幂等性设计主要从两个维度进行考虑:空间、时间。- 空间:定义了幂等的范围,如生成订单的话,不允许出现重复下单。
- 时间:定义幂等的有效期。有些业务需要永久性保证幂等,如下单、支付等。而部分业务只要保证一段时间幂等即可。
同时对于幂等的使用一般都会伴随着出现锁的概念,用于解决并发安全问题。
业务与幂等性
在业务开发与分布式系统设计中,幂等性是一个非常重要的概念,有非常多的场景需要考虑幂等性的问题,尤其对于现在的分布式系统,经常性的考虑重试、重发等操作,一旦产生这些操作,则必须要考虑幂等性问题。以交易系统、支付系统等尤其明显,如:
- 当用户购物进行下单操作,用户操作多次,但订单系统对于本次操作只能产生一个订单。
- 当用户对订单进行付款,支付系统不管出现什么问题,应该只对用户扣一次款。
- 当支付成功对库存扣减时,库存系统对订单中商品的库存数量也只能扣减一次。
- 当对商品进行发货时,也需保证物流系统有且只能发一次货。
在电商系统中还有非常多的场景需要保证幂等性。但是一旦考虑幂等后,服务逻辑务必会变的更加复杂。因此是否要考虑幂等,需要根据具体业务场景具体分析。而且在实现幂等时,还会把并行执行的功能改为串行化,降低了执行效率。
此处以下单减库存为例,当用户生成订单成功后,会对订单中商品进行扣减库存。 订单服务会调用库存服务 进行库存扣减。库存服务会完成具体扣减实现。
现在对于功能调用的设计,有可能出现调用超时,因为出现如网络抖动,虽然库存服务执行成功了,但结果并没有在超时时间内返回,则订单服务也会进行重试。那就会出现问题,stock对于之前的执行已经成功了, 只是结果没有按时返回。而订单服务又重新发起请求对商品进行库存扣减。 此时出现库存扣减两次的问题。 对于这种问题,就需要通过幂等性进行结果。
接口幂等
对于幂等的考虑,主要解决两点前后端交互与服务间交互。这两点有时都要考虑幂等性的实现。从前端的思路解决 的话,主要有三种:前端防重、PRG模式、Token机制。
前端防重
通过前端防重保证幂等是最简单的实现方式,前端相关属性和JS代码即可完成设置。可靠性并不好,有经验的人员 可以通过工具跳过页面仍能重复提交。主要适用于表单重复提交或按钮重复点击。
PRG模式
PRG模式即POST-REDIRECT-GET。当用户进行表单提交时,会重定向到另外一个提交成功页面,而不是停留在原先的表单页面。这样就避免了用户刷新导致重复提交。同时防止了通过浏览器按钮前进/后退导致表单重复提交。 是一种比较常见的前端防重策略。
Token机制
方案介绍
通过token机制来保证幂等是一种非常常见的解决方案,同时也适合绝大部分场景。该方案需要前后端进行一定程 度的交互来完成。
1)服务端提供获取token接口,供客户端进行使用。服务端生成token后,如果当前为分布式架构,将token存放 于redis中,如果是单体架构,可以保存在jvm缓存中。
2)当客户端获取到token后,会携带着token发起请求。
3)服务端接收到客户端请求后,首先会判断该token在redis中是否存在。如果存在,则完成进行业务处理,业务 处理完成后,再删除token。如果不存在,代表当前请求是重复请求,直接向客户端返回对应标识。
但是现在有一个问题,当前是先执行业务再删除token。在高并发下,很有可能出现第一次访问时token存在,完 成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求 也会验证通过,执行具体业务操作。
对于这个问题的解决方案的思想就是并行变串行。会造成一定性能损耗与吞吐量降低。
第一种方案:对于业务代码执行和删除token整体加线程锁。当后续线程再来访问时,则阻塞排队。
第二种方案:借助redis单线程和incr是原子性的特点。当第一次获取token时,以token作为key,对其进行自增。 然后将token进行返回,当客户端携带token访问执行业务代码时,对于判断token是否存在不用删除,而是对其继续incr。如果incr后的返回值为2。则是一个合法请求允许执行,如果是其他值,则代表是非法请求,直接返回。
那如果先删除token再执行业务呢?其实也会存在问题,假设具体业务代码执行超时或失败,没有向客户端返回明确结果,那客户端就很有可能会进行重试,但此时之前的token已经被删除了,则会被认为是重复请求,不再进行业务处理。
这种方案无需进行额外处理,一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌, 重新发起一次访问即可。推荐使用先删除token方案
但是无论先删token还是后删token,都会有一个相同的问题。每次业务请求都回产生一个额外的请求去获取 token。但是,业务失败或超时,在生产环境下,一万个里最多也就十个左右会失败,那为了这十来个请求,让其他九千九百多个请求都产生额外请求,就有一些得不偿失了。虽然redis性能好,但是这也是一种资源的浪费。实现
基于自定义业务流程实现
这种实现方式省略,与传统实现无异。基于自定义注解实现
直接把token实现嵌入到方法中会造成大量重复代码的出现。因此可以通过自定义注解将上述代码进行改造。在需 要保证幂等的方法上,添加自定义注解即可。
- 在token_common中新建自定义注解Idemptent
public class IdemptentInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); Idemptent annotation = method.getAnnotation(Idemptent.class); if (annotation != null){ //进行幂等性校验 checkToken(request); } return true; } @Autowired private RedisTemplate redisTemplate; //幂等性校验 private void checkToken(HttpServletRequest request) { String token = request.getHeader("token"); if (StringUtils.isEmpty(token)){ throw new RuntimeException("非法参数"); } boolean delResult = redisTemplate.delete(token); if (!delResult){ //删除失败 throw new RuntimeException("重复请求"); } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
- 修改token_service_order启动类,让其继承WebMvcConfigurerAdapter
@Bean public IdemptentInterceptor idemptentInterceptor() { return new IdemptentInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { //幂等拦截器 registry.addInterceptor(idemptentInterceptor()); super.addInterceptors(registry); }
- 更新token_service_order与token_service_order_api,新增添加订单方法,并且方法添加自定义幂等注解
@Idemptent @PostMapping("/genOrder2") public String genOrder2(@RequestBody Order order){ order.setId(String.valueOf(idWorker.nextId())); order.setCreateTime(new Date()); order.setUpdateTime(new Date()); int result = orderService.addOrder(order); if (result == 1){ System.out.println("success"); return "success"; }else { System.out.println("fail"); return "fail"; } }
更多相关内容 -
高并发系统数据幂等的解决方案
2020-12-15 15:02:45在系统开发过程中,经常遇到数据重复插入、重复更新、消息重发发送等等问题,因为应用系统的复杂逻辑以及网络交互存在的不确定性,会导致这一重复现象,但是有些逻辑是需要有幂等特性的,否则造成的后果会比较严重,... -
Java接口幂等性设计原理解析
2020-08-19 03:12:52主要介绍了Java接口幂等性设计原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 -
Redis分布式锁解决接口幂等的两种方案
2020-12-21 00:56:55Redis分布式锁解决接口幂等的两种方案一、背景二、基础知识三、解决方案四、实验五、说在最后的话 一、背景 还在为不了解分布式锁而烦恼吗?还在为众多微服务接口不幂等而发愁吗?如果是,并且有兴趣同我一起学习,... -
RocketMQDedupListener:RocketMQ消息幂等去重消费者,支持使用MySQL或者Redis做幂等表,开箱即用
2021-05-30 10:26:12通用的RocketMQ消息幂等去重消费者工具类,开箱即用 支持利用Redis或者MySQL做幂等表。 支持业务主键去重或消息ID去重(默认) 支持消息重复并发控制(重复的消息消费成功/失败前,不会同时消费第二条) 接近于... -
Springboot+redis+Interceptor+自定义annotation实现接口自动幂等
2020-08-25 21:03:23本篇文章给大家介绍了使用springboot和拦截器、redis来优雅的实现接口幂等,对于幂等在实际的开发过程中是十分重要的,因为一个接口可能会被无数的客户端调用,如何保证其不影响后台的业务处理,如何保证其只影响... -
幂等性和支付订单防重复
2019-03-20 09:12:57业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复提交造成的问题有尤其明显。 -
并发和幂等性
2018-01-04 22:28:37高并发和幂等性、高并发和幂等性问题解决场景和方案,觉得好用! -
互联网API接口幂等设计
2018-10-07 12:03:01互联网API接口幂等设计:保证唯一的意思 如何防止接口不能重复提交===保证接口幂等性 -
商品扣库存幂等流程.pdf
2021-09-29 18:13:54完善的商品扣库存幂等流程 -
分布式系统(微服务架构)的一致性和幂等性问题相关概念解析
2021-02-23 21:38:19要想更好的理解分布式系统,并正确使用甚至构建分布式系统,需要理解其中的两个关键概念——分布式系统的数据一致性和分布式系统的幂等性。如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis... -
可交换幂等矩阵的性质及推广 (2013年)
2021-05-21 18:29:06在满足AB=BA的条件下分别给出当A为幂等矩阵,B为任意方阵时,线性组合k1A+k2B为幂等矩阵的充分必要条件,并且利用该结果直接得出当A、B均为幂等矩阵时,A与B的和、差、积仍为幂等矩阵的条件;A与B的和、差、积的值域、核,... -
14-幂等生产者和事务生产者是一回事吗.pdf
2020-05-19 17:33:21在Kafka中,Producer默认不是幂等性的,但我们可以创建幂等性Producer。它其实是0.11.0.0版本引入的 新功能。在此之前,Kafka向分区发送数据时,可能会出现同一条消息被发送了多次,导致消息重复的情 况。在0.11之后... -
38_分布式系统中接口的幂等性该如何保证?比如不能重复扣款?.zip
2020-11-13 23:15:55https://mp.csdn.net/console/uploadResources?spm=1011.2124.3001.4171 -
数量幂等矩阵的秩等式的进一步研究 (2012年)
2021-06-15 07:31:12当存在非零数λ与μ使P2=λP,Q2=μQ时,称P,Q都是数量幂等矩阵.数量λ,μ对数量幂等矩阵P,Q起到基本的确定作用.从寻找与数量λ,μ无关的数量幂等矩阵P,Q的运算的秩等式出发,得到了与λ,μ的“大小”无关的... -
分布式系统互斥性与幂等性问题的分析与解决
2021-02-25 22:39:33如何用较低成本实现系统的高可用、易伸缩、可扩展等目标就显得越发重要。为了解决这一系列问题,系统架构也在不断演进。传统的集中式系统已经逐渐无法满足要求,分布式系统被使用在更多的场景中。分布式系统由独立的... -
自定义注解解决API接口幂等设计防止表单重复提交(生成token存放到redis中)
2018-07-28 22:43:06自定义封装注解类,(生成token存放到redis中)通过注解的方式解决API接口幂等设计防止表单重复提交 -
循环环的构造及其幂等元和幂零根 (1988年)
2021-05-17 11:49:16本文首先研究了循环环的构造,完全解决了这种环类的构造问题;继而进一步研完了这种环的幂等元及其计算方法以及其有单位元的子环;最后又讨论了这种环的幕零根. -
什么叫幂等?如何实现幂等性?
2020-12-18 12:55:16转载自:什么叫幂等?如何实现幂等性? 非商业转载,如有侵权,可联系本人删除 什么叫幂等?如何实现幂等性? 什么是幂等性? 幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次...转载自:什么叫幂等?如何实现幂等性?
非商业转载,如有侵权,可联系本人删除
什么叫幂等?如何实现幂等性?
什么是幂等性?
幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。
在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
什么是接口幂等性?
在
HTTP/1.1
中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
为什么需要实现幂等性?
在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,如:
-
前端重复提交表单:在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
-
用户恶意进行刷单:例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
-
接口超时重复提交:很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
-
消息进行重复消费:当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。
使用幂等性最大的优势在于使接口保证任何幂等性操作,免去因重试等造成系统产生的未知的问题。
引入幂等性后对系统有什么影响?
幂等性是为了简化客户端逻辑处理,能放置重复提交等操作,但却增加了服务端的逻辑复杂性和成本,其主要是:
-
把并行执行的功能改为串行执行,降低了执行效率。
-
增加了额外控制幂等的业务逻辑,复杂化了业务功能;
所以在使用时候需要考虑是否引入幂等性的必要性,根据实际业务场景具体分析,除了业务上的特殊要求外,一般情况下不需要引入的接口幂等性。
Restful API 接口幂等性如何?
现在流行的 Restful 推荐的几种 HTTP 接口方法中,分别存在幂等行与不能保证幂等的方法,如下:
-
√
满足幂等 -
x
不满足幂等 -
-
可能满足也可能不满足幂等,根据实际业务逻辑有关
方案一:数据库唯一主键实现幂等性
数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。
适用操作
-
插入操作
-
删除操作
使用限制
-
需要生成全局唯一主键 ID;
主要流程
主要流程如下:
-
客户端执行创建请求,调用服务端接口。
-
服务端执行业务逻辑,生成一个分布式
ID
,将该 ID 充当待插入数据的主键,然 后执数据插入操作,运行对应的SQL
语句。 -
服务端将该条数据插入数据库中,如果插入成功则表示没有重复调用接口。如果抛出主键重复异常,则表示数据库中已经存在该条记录,返回错误信息到客户端。
方案二:数据库乐观锁实现幂等性
数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
适用操作
-
更新操作
使用限制
-
需要数据库对应业务表中添加额外字段
描述示例
例如,存在如下的数据表中:
为了每次执行更新时防止重复更新,确定更新的一定是要更新的内容,我们通常都会添加一个
version
字段记录当前的记录版本,这样在更新时候将该值带上,那么只要执行更新操作就能确定一定更新的是某个对应版本下的信息。这样每次执行更新时候,都要指定要更新的版本号,如下操作就能准确更新
version=5
的信息:UPDATE my_table SET price=price+50,version=version+1 WHERE id=1 AND version=5
上面
WHERE
后面跟着条件id=1 AND version=5
被执行后,id=1
的version
被更新为6
,所以如果重复执行该条 SQL 语句将不生效,因为id=1 AND version=5
的数据已经不存在,这样就能保住更新的幂等,多次更新对结果不会产生影响。方案三:防重 Token 令牌实现幂等性
针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用
Token
的机制实现防止重复提交。简单的说就是调用方在调用接口的时候先向后端请求一个全局
ID(Token)
,请求的时候携带这个全局ID
一起请求(Token
最好将其放到Headers
中),后端需要对这个Token
作为Key
,用户信息作为Value
到Redis
中进行键值内容校验,如果Key
存在且Value
匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的Key
或Value
不匹配就返回重复执行的错误信息,这样来保证幂等操作。适用操作
-
插入操作
-
更新操作
-
删除操作
使用限制
-
需要生成全局唯一
Token
串 -
需要使用第三方组件
Redis
进行数据效验
主要流程:
-
服务端提供获取 Token 的接口,该 Token 可以是一个序列号,也可以是一个分布式
ID
或者UUID
串。 -
客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。
-
然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
-
将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
-
客户端在执行提交表单时,把 Token 存入到
Headers
中,执行业务请求带上该Headers
。 -
服务端接收到请求后从
Headers
中拿到 Token,然后根据 Token 到 Redis 中查找该key
是否存在。 -
服务端根据 Redis 中是否存该
key
进行判断,如果存在就将该key
删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。
注意,在并发情况下,执行 Redis 查找数据与删除需要保证原子性,否则很可能在并发下无法保证幂等性。其实现方法可以使用分布式锁或者使用
Lua
表达式来注销查询与删除操作。方案四: 下游传递唯一序列号实现幂等性
所谓请求序列号,其实就是每次向服务端请求时候附带一个短时间内唯一不重复的序列号,该序列号可以是一个有序
ID
,也可以是一个订单号,一般由下游生成,在调用上游服务端接口时附加该序列号和用于认证的ID
。当上游服务器收到请求信息后拿取该 序列号 和下游 认证ID 进行组合,形成用于操作 Redis 的
Key
,然后到 Redis 中查询是否存在对应的Key
的键值对,根据其结果:-
如果存在,就说明已经对该下游的该序列号的请求进行了业务处理,这时可以直接响应重复请求的错误信息。
-
如果不存在,就以该
Key
作为 Redis 的键,以下游关键信息作为存储的值(例如下游商传递的一些业务逻辑信息),将该键值对存储到 Redis 中 ,然后再正常执行对应的业务逻辑即可。
适用操作
-
插入操作
-
更新操作
-
删除操作
使用限制
-
要求第三方传递唯一序列号;
-
需要使用第三方组件 Redis 进行数据效验;
主要流程
-
下游服务生成分布式
ID
作为序列号,然后执行请求调用上游接口,并附带唯一序列号与请求的认证凭据ID。 -
上游服务进行安全效验,检测下游传递的参数中是否存在序列号和凭据ID。
-
上游服务到 Redis 中检测是否存在对应的序列号与认证ID组成的
Key
,如果存在就抛出重复执行的异常信息,然后响应下游对应的错误信息。如果不存在就以该序列号和认证ID组合作为Key
,以下游关键信息作为Value
,进而存储到 Redis 中,然后正常执行接来来的业务逻辑。
“
上面步骤中插入数据到 Redis 一定要设置过期时间。这样能保证在这个时间范围内,如果重复调用接口,则能够进行判断识别。如果不设置过期时间,很可能导致数据无限量的存入 Redis,致使 Redis 不能正常工作。
实现接口幂等示例
这里使用防重 Token 令牌方案,该方案能保证在不同请求动作下的幂等性,实现逻辑可以看上面写的”防重 Token 令牌”方案,接下来写下实现这个逻辑的代码。
1. Maven 引入相关依赖
这里使用
Maven
工具管理依赖,这里在pom.xml
中引入SpringBoot
、Redis
、lombok
相关依赖。<dependencies> <!--springboot web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--springboot data redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
2. 配置连接 Redis 的参数
在
application
配置文件中配置连接Redis
的参数,如下:spring: redis: ssl: false host: 127.0.0.1 port: 6379 database: 0 timeout: 1000 password: lettuce: pool: max-active: 100 max-wait: -1 min-idle: 0 max-idle: 20
3. 创建与验证 Token 工具类
创建用于操作 Token 相关的 Service 类,里面存在 Token 创建与验证方法,其中:
-
Token
创建方法:使用UUID
工具创建Token
串,设置以“idempotent_token:“+“Token串”
作为Key
,以用户信息当成Value
,将信息存入 Redis 中。 -
Token
验证方法:接收 Token 串参数,加上 Key 前缀形成Key
,再传入value
值,执行Lua
表达式(Lua
表达式能保证命令执行的原子性)进行查找对应Key
与删除操作。执行完成后验证命令的返回结果,如果结果不为空且非0,则验证成功,否则失败。
@Slf4j @Service public class TokenUtilService { @Autowired private StringRedisTemplate redisTemplate; /** * 存入 Redis 的 Token 键的前缀 */ private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:"; /** * 创建 Token 存入 Redis,并返回该 Token * * @param value 用于辅助验证的 value 值 * @return 生成的 Token 串 */ public String generateToken(String value) { // 实例化生成 ID 工具对象 String token = UUID.randomUUID().toString(); // 设置存入 Redis 的 Key String key = IDEMPOTENT_TOKEN_PREFIX + token; // 存储 Token 到 Redis,且设置过期时间为5分钟 redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES); // 返回 Token return token; } /** * 验证 Token 正确性 * * @param token token 字符串 * @param value value 存储在Redis中的辅助验证信息 * @return 验证结果 */ public boolean validToken(String token, String value) { // 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); // 根据 Key 前缀拼接 Key String key = IDEMPOTENT_TOKEN_PREFIX + token; // 执行 Lua 脚本 Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value)); // 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,若果结果不为空和0,则验证通过 if (result != null && result != 0L) { log.info("验证 token={},key={},value={} 成功", token, key, value); return true; } log.info("验证 token={},key={},value={} 失败", token, key, value); return false; } }
4、创建测试的 Controller 类
创建用于测试的
Controller
类,里面有获取Token
与测试接口幂等性的接口,内容如下:@Slf4j @RestController public class TokenController { @Autowired private TokenUtilService tokenService; /** * 获取 Token 接口 * * @return Token 串 */ @GetMapping("/token") public String getToken() { // 获取用户信息(这里使用模拟数据) // 注:这里存储该内容只是举例,其作用为辅助验证,使其验证逻辑更安全,如这里存储用户信息,其目的为: // - 1)、使用"token"验证 Redis 中是否存在对应的 Key // - 2)、使用"用户信息"验证 Redis 的 Value 是否匹配。 String userInfo = "mydlq"; // 获取 Token 字符串,并返回 return tokenService.generateToken(userInfo); } /** * 接口幂等性测试接口 * * @param token 幂等 Token 串 * @return 执行结果 */ @PostMapping("/test") public String test(@RequestHeader(value = "token") String token) { // 获取用户信息(这里使用模拟数据) String userInfo = "mydlq"; // 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息 boolean result = tokenService.validToken(token, userInfo); // 根据验证结果响应不同信息 return result ? "正常调用" : "重复调用"; } }
最后总结
幂等性是开发当中很常见也很重要的一个需求,尤其是支付、订单等与金钱挂钩的服务,保证接口幂等性尤其重要。在实际开发中,我们需要针对不同的业务场景我们需要灵活的选择幂等性的实现方式:
-
对于下单等存在唯一主键的,可以使用“唯一主键方案”的方式实现。
-
对于更新订单状态等相关的更新场景操作,使用“乐观锁方案”实现更为简单。
-
对于上下游这种,下游请求上游,上游服务可以使用“下游传递唯一序列号方案”更为合理。
-
类似于前端重复提交、重复下单、没有唯一ID号的场景,可以通过
Token
与Redis
配合的“防重 Token 方案”实现更为快捷。
上面只是给与一些建议,再次强调一下,实现幂等性需要先理解自身业务需求,根据业务逻辑来实现这样才合理,处理好其中的每一个结点细节,完善整体的业务流程设计,才能更好的保证系统的正常运行。最后做一个简单总结,然后本博文到此结束,如下:
-
-
线性模型的正交幂等超场
2020-03-31 02:29:17我们从sgoldstino,inflatino和sinflaton的质量趋于无穷大的形式极限中的线性模型中,导出具有受约束的正交幂等超场的超对称/超重力模型。 sinflaton质量保持有限的情况会导致模型具有“松弛”约束,其中sinflaton... -
幂等矩阵的性质毕业论文.doc
2021-09-18 11:59:35幂等矩阵的性质毕业论文.doc -
P21_创建Company资源POST_P20_HTTP方法的安全性与幂等性_Routine.Api2020_2_7.rar
2020-02-09 11:09:18P21_创建Company资源POST_P20_HTTP方法的安全性与幂等性_Routine.Api2020_2_7.rar 使用 ASP.NET Core RESTful API ReSharper ApiController ...● 幂等性是指方法无论执行多少次都会得到同样的结果。 -
n阶k次幂等矩阵的性质 (2011年)
2021-05-07 19:09:38在幂等矩阵的基础上,通过类比幂等矩阵的性质,得出了n阶k次幂等矩阵的性质,并对其性质给予了必要的证明. -
分布式架构中的幂等性
2018-07-24 09:12:47电商的很多业务,考虑更多的是 BASE(即Basically Available、Soft state、和Eventually consistent),而不是 ACID(Atomicity、Consistency、Isolation和 Durability)。即为了满足高负载的用户访问,我们可以容忍... -
RabbitMQ消费端幂等性概念及解决方案.docx
2021-10-26 07:48:33RabbitMQ消费端幂等性概念及解决方案.docx -
系统接口幂等性规范
2019-08-11 01:09:46NULL 博文链接:https://mingyang2013.iteye.com/blog/2227015 -
关于幂等矩阵秩的讨论与Cochran定理的注记 (2014年)
2021-06-15 07:19:25注意到最近的幂等矩阵秩的讨论,指出了相关文献之间的联系和不足,简化了其证明过程,并说明这些讨论与在概率统计和矩阵理论中都有重要价值的Cochran定理有着密切的关系. -
什么是接口的幂等性以及如何实现接口幂等性
2021-04-22 09:26:462、什么是接口的幂等性 3、不做接口的幂等性会产生什么影响 4、什么情况下需要保证接口的幂等性 4.1 select:查询操作 4.2 insert:新增操作 4.3 delete:删除操作 4.3.1 绝对删除 具有幂等性 4.3.2 相对删除...目录
1、接口调用存在的问题
在大多数情况下,一个大系统都会拆分为多个微服务组成。也就是说,一个大系统的完整功能往往是由多个子系统的小功能构建而成的,而一个子系统服务往往会调用另外一个子系统提供出来的服务,而服务调用无非就是使用RPC接口通信,既然是通信,那么就有可能在服务器处理数据完毕后返回结果的时候挂掉,这个时候客户端发现已经过了很久 但还是没能从服务器端拿到正确的响应,那么,客户端就有可能会多次点击按钮以触发多次接口请求,那么,处理数据的结果是否要统一呢?答案是肯定的,尤其是在支付场景。
2、什么是接口的幂等性
接口幂的等性,就是指 用户对于同一操作发起的一次请求或者多次请求,其操作的结果都是一致的,不会因为进行多次请求而产生副作用。这里的副作用,可以认为在多次请求操作时,每一次请求对数据的状态都会产生影响。注意,这里并没有要求接口返回的结果是一致的,而是要求被数据的状态是一致的。例如:update order set moeny = 100 where orderId = 2029282312; 该操作无论执行多少次,被操作数据的状态都是一致的。
3、不做接口的幂等性会产生什么影响
支付场景:用户购买商品后,发起支付操作,支付系统处理支付成功后,由于网络原因没有及时返回操作成功的信息给用户,其实这个时候订单已经扣过款,相应的支付流水也都已经生成。这个时候,用户又点击支付操作,此时会进行第二次扣款,扣款成功后吧操作成功的信息返回给了用户。用户去查看支付订单和流水时 会发现自己支付了两次,完蛋了,该系统要被用户投诉了。这就是没有保证接口的幂等性而造成的不良后果。
4、什么情况下需要保证接口的幂等性
在【增删改查】4个SQL操作中,尤为需要注意的就是增加和修改操作。
4.1 select:查询操作
查询操作不会对数据产生副作用。查询一次或者查询多次,在数据不变的情况下,查询结果都是一样的,所以,select 操作是天然的幂等操作。
4.2 insert:新增操作
新增操作在重复提交的场景下会出现幂等性问题,比如以上的支付场景。
insert into product_info (id, price);
上述 insert SQL,ID是自增主键,执行多次就会新增多条记录,对结果集产生了副作用,所以,insert 操作天然不具有幂等性。
4.3 delete:删除操作
删除操作可以分为两种:绝对删除和相对删除。其中,绝对删除不会对数据产生副作用,具有幂等性;相对删除会对数据产生副作用,不具有幂等性。
4.3.1 绝对删除 具有幂等性
delete from order where id = 3;
无论该SQL执行多少次,对结果集产生的效果都是一样的,只删除了一条数据,不会对数据产生副作用,所以,它具有幂等性。
4.3.2 相对删除 不具有幂等性
delete from order where id > 23;
该SQL每执行一次,对结果集产生的结果可能都不一样,同一操作执行多次对数据产生了副作用,所以,它不具有幂等性。
4.4 update:更新操作
更新操作可以分为两种:绝对更新和相对更新。其中,绝对更新不会对数据产生副作用,具有幂等性;相对更新会对数据产生副作用,不具有幂等性。
4.4.1 绝对更新 具有幂等性
update Goods set stock = 586 where goodId = 10;
无论该SQL执行多少次,对结果集产生的效果都是一样的,只更新了一条数据,不会对数据产生副作用,所以,它具有幂等性。
4.4.2 相对更新 不具有幂等性
update Goods set stock = stock + 1 where goodid = 10;
该SQL每执行一次,对结果集产生的结果都不一样,库存数量都会增加10,同一操作执行多次对数据产生了副作用,所以,它不具有幂等性。
5、使用幂等的业务场景
5.1 前端重复提交
用户注册、用户创建商品订单等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端就收到了好几次提交,这时,就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug。
5.2 接口超时重试
对于给第三方调用的接口,有可能会因为网络原因而调用超时失败,这时,一般在设计的时候会对接口调用加上超时/失败重试机制。如果第一次调用已经执行了一半业务逻辑时,发生了网络异常,这时,再次调用时就会因为脏数据的存在而出现调用异常。
5.3 MQ消息重复消费
在使用消息中间件来处理消息队列,且手动 ACK 确认消息被正常消费时,如果消费者突然断开连接,那么已经执行了一半的消息就会被重新放回队列。当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据、数据库数据冲突、资源重复等。
6、幂等性的解决方案
6.1 唯一索引
使用唯一索引可以避免脏数据的 insert,当插入重复数据时数据库会抛出异常,保证了数据的唯一性。
6.2 乐观锁
这里的乐观锁指的是用乐观锁的原理去实现:为表增加一个 version 字段,当数据需要更新 update 时,先去表中获取此时的 version 版本号。
select version from tableName where Id = 1;
更新数据时,首先和最新的版本号作比较,如果不相等,则说明已经有其他的请求去更新数据了,则本次提示更新会失败,让用户重试即可。
update tableName set count = count + 1, version = version + 1 where version = #{version};
6.3 悲观锁
乐观锁可以实现的,往往使用悲观锁也能实现:即在获取被操作数据的时候进行加锁。当同时有多个重复请求过来时,其他请求都会因无法获得被操作数据的锁而阻塞住,因此,其他请求都无法对被操作数据进行操作。
6.4 CAS思想保证接口幂等性
状态机制来实现接口幂等性(一个事务的状态是不可逆的)。
针对更新操作,例如 电商订单的支付状态:0=待支付,1=支付中,2=支付成功,3=支付失败。
update Orders set status = 1 where status = 0 and orderId = “201251487987”; update Orders set status = 2 where status = 1 and orderId = “201251487987”; update Orders set status = 3 where status = 1 and orderId = “201251487987”;
该SQL语句利用【订单状态的CAS】来保证该操作的幂等性。比如,要进行订单支付,先用CAS思想做更新订单状态的操作,然后再去做实际支付的操作:
(1)返回影响行数=1,则代表订单状态修改成功,可以继续执行后面的支付业务代码。
(2)返回影响行数=0,则代表订单状态修改失败,该订单已经不是待支付订单了,不可以继续执行后面的支付业务代码。(其实这里的解释有待商榷)。
注释:实际这里是利用CAS原理。
6.5 分布式锁
幂等的本质是分布式锁的问题,分布式锁正常可以通过 redis 或 zookeeper 来实现。
在分布式环境下,锁定全局唯一资源,使多个请求串行化,实际表现为互斥锁,可以防止重复,以此来解决幂等性问题。
6.6 基于token+redis机制实现(通用性强)
token 机制的核心思想:是为每一次操作都生成一个唯一性的凭证,也就是 token。一个token 在操作的每一个阶段只有一次执行权,一旦执行成功,则保存执行结果并且删除该 token。对重复的请求,因为没有了先前的那个 token 而返回指定的同一个结果给客户端。
通过【 token+redis 机制】实现接口的幂等性,这是一种比较通用性的实现方法。示意图如下:
具体流程步骤:
(1)客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token,并且保存在 redis 中,同时会把这个 ID 返回给客户端。
(2)客户端第二次调用业务请求的时候,必须携带这个 token。
(3)服务端会校验这个 token:如果校验成功,则执行业务,并删除 redis 中的 token。如果校验失败,说明 redis 中已经没有了对应的 token,则表示是重复操作,直接返回指定的结果给客户端。
注意:对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本实现,以此来保证多个操作的原子性。全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成。
6.7 基于redis命令setnx实现
(1)这种实现方式是基于Redis的一个命令 setnx 实现的。
(2)setnx key value:当且仅当 key 不存在时,将 key 的值设为 value,并返回 1。若给定的 key 已经存在,则 setnx 不做任何动作,并返回 0。注意:该命令在设置成功时返回 1,设置失败时返回 0。
(3)通过【 redis的命令 setnx 】实现接口的幂等性,示意图如下:
具体流程步骤:
(1)客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段 。
(2)将该字段以 setnx 的方式存入 redis 中,并根据业务设置相应的超时时间 timeout。
(3)如果设置成功,则证明这是第一次请求,则执行后续的业务逻辑。
(4)如果设置失败,则证明这不是第一次请求,已经执行过当前请求,直接返回即可。
6.8 通过业务代码逻辑判断实现
通过【业务代码逻辑判断】实现接口幂等性,只能针对一些满足判断的业务逻辑实现,具有一定局限性。比如:用户购买商品的订单系统与支付系统。
订单系统负责记录用户的购买记录以及订单的流转状态(orderStatus)。支付系统用于付款,提供如下接口。订单系统与支付系统通过分布式交互。
boolean pay ( int accountid, BigDecimal amount ); // 用于付款,扣除用户余额
这种情况下,支付系统已经扣款,但是,因为网络原因,订单系统没有获取到支付系统返回的确切结果,因此,订单系统需要重试。
由上图可见,支付系统并没有做到接口的幂等性,订单系统第一次调用和第二次调用,用户分别被扣了两次钱,不符合幂等性原则(同一个订单,无论是调用了多少次,用户都只会扣款一次)。
如果需要支持幂等性,付款接口需要修改为以下接口:
boolean pay ( int orderId, int accountId, BigDecimal amount);
通过 orderId 来标定订单的唯一性,支付系统只要检测到该订单已经支付过,则第二次调用就不会扣款,而是会直接返回结果:
在不同的业务中,不同接口需要有不同的幂等性,特别是在分布式系统中,因为网络原因而未能得到确定的结果,往往需要支持接口做幂等性校验。
随着分布式系统及微服务的普及,因为网络原因而导致调用系统未能获取到确切结果而导致重试,这就需要被调用系统具有幂等性。
例如上文所阐述的支付系统,针对同一个订单保证支付的幂等性,一旦订单的支付状态确定之后,以后的操作都会返回相同的结果,对用户的扣款也只会有一次。
这种接口的幂等性,简化到数据层面的操作就是:
update userAmount set amount = amount - 'value', paystatus = 'paid' where orderId= 'orderid' and paystatus = 'unpay';
其中:value 是用户要减少的金额,paystatus 代表支付状态( paid 代表已经支付,unpay 代表未支付 ),orderid 是唯一的订单号。
在上文中提到的订单系统,订单具有自己的状态(orderStatus),订单状态存在一定的单向流转。
订单首先有:订单提交(0),订单付款中(1),订单付款成功(2),订单付款失败(3),简化之后:
(1)当 orderStatus = 1 时,其前置状态只能是 0,也就是说:将 orderStatus 由【 0 -> 1】是需要幂等性的,SQL如下:
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0;
(2)当 orderStatus = 0、1 两种状态时,对订单执行【 0 -> 1】的状态流转操作应该是具有幂等性的。这时候,需要在执行 update 操作之前检测 orderStatus 是否已经 = 1,如果已经 = 1,则直接返回 true 即可。
(3)当 orderStatus = 2 时,再进行订单状态【 0 -> 1】操作就无法成功。但是,幂等性是针对同一个请求的,也就是针对同一个 requestId 保持幂等,这时候再执行:
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0;
接口就会返回失败,系统没有产生修改,如果再发一次,requestId 还是相同的,对系统同样没有产生修改。
这几种实现幂等的方式其实都是大同小异的,类似的还有使用【状态机、悲观锁、乐观锁】的方式来实现,都是比较简单的。总之,当你去设计一个接口的时候,接口的幂等性都是首要考虑的问题,特别是当你负责设计转账、支付这种涉及到 money 的接口,要格外注意。
99、参考
(1)https://javayz.blog.csdn.net/article/details/109684180
(2)https://blog.csdn.net/qq_29978863/article/details/107739744
(3)https://www.cnblogs.com/huaixiaonian/p/9577567.html