Java接口幂等性设计场景解决方案v1.0
分布式服务接口的幂等性如何设计(比如不能重复扣款)?
一个分布式系统中的某个接口,要保证幂等性,如何保证?这个事,其实是你做分布式系统的时候必须要考虑的一个生产环境的技术问题,为什么呢?
实际案例1:
假如你有个服务提供一个付款业务的接口,而这个服务分别部署在5台服务器上,然后用户在前端操作时,不知道为啥,一个订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的服务器上,这下好了,一个订单扣款扣了两次。
实际案例2:
订单系统调用支付系统进行支付,结果不消息网络,然后订单系统走了前面我们看到的重试retry机制,那就给你重试一次吧,那么支付系统收到了一个支付请求两次,而且因为负载均衡算法落在了不同的机器上。
小结:
所以你必须得知道这事,否则你做出来的分布式系统恐怕很容易埋坑!
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个简单的例子:那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常了,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要对数据操作加入事务即可,发生错误的时候立即回滚,但是再响应客户端的时候也有可能网络中断或者异常等等情况。
- 网络问题/用户误操作/恶意操作,用户点击了多次
- 网络问题,微服务重试retry
- 网络问题很常见,100次请求,都ok;1万次请求可能1次超时会重试;10万次可能10次超时会重试,100万次可能100次超时会重试;如果100个请求重复了,你没处理,导致订单扣款2次,100个订单都扣错了,每天被100个用户投诉,一个月被3000个用户投诉。
- 前端重复提交:前端瞬时点击多次造成表单重复提交
- 接口超时重试:接口可能会因为某些原因而调用失败,处于容错性考虑会加上失败重试的机制。如果接口调用一半,再次调用就会因为脏数据的存在而产生异常
- 消息重复消费:在使用消息中间件来处理消息队列,且手动ack确认消息被正常消费时。如果消费者突然断开链接,那么已经执行了一半的消息会重新放回队列。被其他消费者重新消费时就会导致结果异常,如数据库重复数据, 数据库数据冲突,资源重复等
- 请求重发:网络抖动引发的nginx重发请求,造成重复调用。
秒杀场景下,一个用户只能购买同一商品一次的解决方法:采用用户ID+商品ID,存储到redis中,使用redis中的setNX操作,等待自然过期。
用户注册时,用户点击注册按钮多次,是不是会注册多个用户?我们可以在用户进入注册页面后由后台生成一个token,传给前端页面,用户在点击提交时,将token带给后台,后台使用该token作为分布式锁,setNX操作,执行成功后不释放锁,等待自然过期。
用户注册时,用户点击注册按钮多次,是不是会注册多个用户? 我们可以使用手机号作为mysql用户表唯一key。也就是一个手机号只能注册一次。
update操作可能存在幂等性的问题:
1.用户更改个人信息,疯狂点击按钮,不会发生幂等性问题,因为数据始终为修改后的数据。
2.用户购买商品,用户在点击后,网络出现问题,可能再次点击,这样就会出现幂等性问题,导致购买了多次,可以使用乐观锁
根据唯一id删除不会出现幂等性问题,因为第二次删除的时候mysql中已经不存在该数据
查询操作不会改变数据,所以是天然的幂等性操作。
使用Token机制,或使用Token + 分布式锁的方案来解决幂等性问题。
通过Token 机制实现接口的幂等性,这是一种比较通用性的实现方法。
具体流程步骤:
- 客户端会先发送一个请求去获取Token,服务端会生成一个全局唯一的ID作为Token保存在Redis中,同时把这个ID返回给客户端;
- 客户端第二次调用业务请求的时候必须携带这个Token;
- 服务端会校验这个 Token,如果校验成功,则执行业务,并删除Redis中的 Token;
- 如果校验失败,说明Redis中已经没有对应的 Token,则表示重复操作,直接返回指定的结果给客户端。
通过MySQL唯一索引的特性实现接口的幂等性。
具体流程步骤:
- 建立一张去重表,其中某个字段需要建立唯一索引;
- 客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中;
- 因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑;
- 如果插入失败,则代表已经执行过当前请求,直接返回。
通过Redis的SETNX命令实现接口的幂等性。
SETNX key value:当且仅当key不存在时将key的值设为value;若给定的key已经存在,则SETNX不做任何动作。设置成功时返回1,否则返回0。
具体流程步骤:
- 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段;
- 将该字段以SETNX的方式存入Redis中,并根据业务设置相应的超时时间;
- 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑;
- 如果设置失败,则代表已经执行过当前请求,直接返回。
为需要保证幂等性的每一次请求创建一个唯一的标识token,先获取token,并将此token存入到redis,请求接口时,将此token放在header或者作为请求参数请求接口,后端接口判断redis中是否存在此token;
- 如果存在,则正常处理业务逻辑,并从redis中删除此token,那么,如果是重复请求,由于token已经被删除,则不能能够通过校验,返回重复提交
- 如果不存在,说明参数不合法或者是重复请求,返回提示即可
- 当页面加载的时候通过接口获取token
- 当访问接口时,会经过,如果发现该接口中有自定义的幂等性注解,说明该接口需要验证幂等性(查看请求头里是否有key=token的值,如果有,并且删除成功,那么接口就访问成功,否则为重复提交;
- 如果发现该接口没有自定义的幂等性注解,则放行。
- springBoot
- redis
- 自定义幂等性注解+请求拦截
- Jmeter压测工具
该注解的目的是为了实现幂等性的校验,即添加了该注解的接口要实现幂等性验证
- 获取token
浏览器访问:http://localhost:9090/getToken,获取token的值
- 执行幂等性业务接口
- 第一次,在postman中调用当前接口,并在请求头中设置token
- 第二次,再次postman中访问该业务接口,显示重复调用的提示
- 查看num的值得接口
浏览器访问:http://localhost:9090/getNum
使用方法参考Jmeter压力测试工具使用说明v1.0
通过以上代码演示了解到,本案例对submit接口方法使用了基于token的幂等性解决方案,也就是当前submit接口方法只能调用一次,如果由于网络抖动或者网络异常出现多点或者点击多次的情况,就会出现报错提示,不允许调用当前接口,那么也就解决了当前业务接口幂等性的问题。
Java教程:一文详解函数式接口
函数式编程是一种编程规范或一种编程思想,简单可以理解问将运算或实现过程看做是函数的计算。 Java8为了实现函数式编程,提出了3个重要的概念:Lambda表达式、方法引用、函数式接口。现在很多公司都在使用lambda表达式进行代码编写,甚至知名的Java的插件也都在Lambda,比如数据库插件MybatisPlus。Lambda表达式的使用是需要函数式接口的支持,即lambda表达式的核心就是使用大量的函数式接口。本文带领大家全面了解函数式接口的定义和使用。
- 函数式接口概述
- 自定义函数式接口
- 常用函数式接口
- 函数式接口的练习
如果接口里只有一个抽象方法,那么就是函数式接口,可以使用注解(@FunctionalInterface)检测该接口是否是函数式接口,即只能有一个抽象方法。
注意事项
由于接口当中抽象方法的public abstract是可以省略的,所以定义一个函数式接口很简单:
对于刚刚定义好的MyFunctionalInterface函数式接口,典型使用场景就是作为方法的参数:
java.util.function.Supplier<T>接口,它意味着\”供给\” , 对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
使用Supplier接口作为方法参数类型,通过Lambda表达式求出List集合(存储int数据)中的最大值。提示:接口的泛型请使用java.lang.Integer类。
代码示例:
java.util.function.Consumer<T>接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
Consumer接口中包含抽象方法void accept(T t): 消费一个指定泛型的数据。
代码示例:
如果一个方法的参数和返回值全都是Consumer类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口中的default方法andThen。下面是JDK的源代码:
备注:java.util.Objects的requireNonNull静态方法将会在参数为null时主动抛出NullPointerException异常。这省去了重复编写if语句和抛出空指针异常的麻烦。 andThen是默认方法,由Consumer的对象调用,而且参数和返回值都是Consumer对象
要想实现组合,需要两个或多个Lambda表达式即可,而andThen的语义正是“一步接一步”操作。例如两个步骤组合的情况:
代码示例:
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。但是我们却没有使用andThen方法,其实我上面的写法,就是andThen底层的代码实现。
为了方便大家理解,下面我们使用andThen方法进行演示。
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。
andThen原理分析图解:
java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有进有出,所以称为“函数Function”。
Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。
代码示例:
将String类型转换为Integer类型。
Function接口中有一个默认的andThen方法,用来进行组合操作。JDK源代码如:
该方法同样用于“先做什么,再做什么”的场景,和Consumer中的andThen差不多:
代码示例:
将String的数字,转成int数字,再把int数字扩大10倍
第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过andThen按照前后顺序组合到了一起。运行结果将会打印1230。但是我们却没有使用andThen方法,其实我上面的写法,就是andThen底层的代码实现。
请注意,Function的前置条件泛型和后置条件泛型可以相同。
为了方便大家理解,下面我们使用andThen方法进行演示
运行结果仍然是1230。
andThen原理分析图解:
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T>接口。
Predicate接口中包含一个抽象方法:boolean test(T t)。用于条件判断的场景:
条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法and。其JDK源码为:
代码示例:
判断一个字符串既要包含大写“H”,又要包含大写“W”
与and的“与”类似,默认方法or实现逻辑关系中的“或”。JDK源码为:
代码示例:
字符串包含大写H或者包含大写W”
关于and和or方法的原理,可以参考andThen方法原理
“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法negate的JDK源代码为:
从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在test方法调用之前调用negate方法,正如and和or方法一样:
本文通过具体的例子,演示了函数式接口的定义和使用。以及常用的函数式接口。并给出了相关的练习题目。对于部分函数式接口中的默认方法,进行了图解分析,让你更加深刻的理解函数式接口的思想和目的。在以后实际的编程过程中,对于集合的操作,可以通过Stream流完成,而Stream流中的很多方法的参数都是函数式接口,通过本文的学习,你已经掌握了函数式接口的使用,相信后面学习Stream流是非常容易的。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。