- 作者: 博学谷狂野架构师
- GitHub: GitHub地址(有我精心准备的130本电子书PDF)
只共享干货、不吹水,让我们一同加油!
幂等性介绍
现如今许多体系都会依据分布式或微服务思维完结对体系的架构规划。那么在这一个体系中,就会存在若干个微服务,而且服务间也会发生相互通信调用。那么已然发生了服务调用,就必然会存在服务调用延迟或失利的问题。当呈现这种问题,服务端会进行重试等操作或客户端有或许会进行屡次点击提交。假如这样恳求屡次的话,那最终处理的数据成果就必定要确保统一,如付出场景。此刻就需求经过确保业务幂等性计划来完结。
什么是幂等性
幂等是一个数学与核算机学概念,即
f(n) = 1^n
,不论n为多少,f(n)的值永远为1,在数学中某一元运算为幂等时,其效果在任一元素两次后会和其效果一次的成果相同。
在编程开发中,关于幂等的界说为:不论对某一个资源操作了多少次,其影响都应是相同的。 换句话说便是:在接口重复调用的情况下,对体系发生的影响是相同的,可是回来值答应不同,如查询。
幂等函数或幂等办法是指能够运用相同参数重复履行,并能取得相同成果的函数。这些函数不会影响体系状况,也不必忧虑重复履行会对体系形成改变。
幂等性不仅仅仅仅一次或屡次操作对资源没有发生影响,还包括第一次操作发生影响后,今后屡次操作不会再发生影响。而且幂等重视的是是否对资源发生影响,而不重视成果。
幂等性维度
幂等性规划首要从两个维度进行考虑:空间、时间。
- 空间:界说了幂等的范围,如生成订单的话,不答应呈现重复下单。
- 时间:界说幂等的有效期。有些业务需求永久性确保幂等,如下单、付出等。而部分业务只要确保一段时间幂等即可。
一同关于幂等的运用一般都会伴随着呈现锁的概念,用于处理并发安全问题。
以SQL为例
-
select * from table where id=1
。此SQL不论履行多少次,尽管成果有或许呈现不同,都不会对数据发生改变,具有幂等性。 -
insert into table(id,name) values(1,'heima')
。此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屡次操作,发生的成果相同,具有幂等性。
什么是接口幂等性
在
HTTP/1.1
中,对幂等性进行了界说。
它描绘了一次和屡次恳求某一个资源关于资源本身应该具有相同的成果(网络超时等问题除外),即第一次恳求的时分对资源发生了副效果,可是今后的屡次恳求都不会再对资源发生副效果。
这里的副效果是不会对成果发生破坏或许发生不行预料的成果。也便是说,其任意屡次履行对资源本身所发生的影响均与一次履行的影响相同。
为什么需求完结幂等性
运用幂等性最大的优势在于使接口确保任何幂等性操作,免除因重试等形成体系发生的未知的问题。
在接口调用时一般情况下都能正常回来信息不会重复提交,不过在遇见以下情况时能够就会呈现问题:
前端重复提交表单
在填写一些表格时分,用户填写完结提交,许多时分会因网络动摇没有及时对用户做出提交成功响应,致运用户认为没有成功提交,然后一向点提交按钮,这时就会发生重复提交表单恳求。
用户歹意进行刷单
例如在完结用户投票这种功用时,假如用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票成果与事实严重不符。
接口超时重复提交
许多时分 HTTP 客户端东西都默许敞开超时重试的机制,特别是第三方调用接口时分,为了避免网络动摇超时等形成的恳求失利,都会增加重试机制,导致一个恳求提交屡次。
音讯进行重复消费
当运用 MQ 音讯中间件时分,假如发生音讯中间件呈现过错未及时提交消费信息,导致发生重复消费。
引入幂等性后对体系有什么影响
幂等性是为了简化客户端逻辑处理,能放置重复提交等操作,但却增加了服务端的逻辑杂乱性和成本,其首要是:
- 把并行履行的功用改为串行履行,下降了履行功率。
- 增加了额定操控幂等的业务逻辑,杂乱化了业务功用;
所以在运用时分需求考虑是否引入幂等性的必要性,依据实践业务场景详细分析,除了业务上的特殊要求外,一般情况下不需求引入的接口幂等性。
Restful API 接口幂等
现在流行的 Restful 推荐的几种 HTTP 接口办法中,分别存在幂等行与不能确保幂等的办法,如下:
HTTP协议语义幂等性
HTTP协议有两种办法:RESTFUL、SOA。现在关于WEB API,更多的会运用RESTFUL风格界说。为了更好的完结接口语义界说,HTTP关于常用的四种恳求办法也界说了幂等性的语义。
- GET:用于获取资源,屡次操作不会对数据发生影响,具有幂等性。留意不是成果。
- POST:用于新增资源,对同一个URI进行两次POST操作会在服务端创立两个资源,不具有幂等性。
- PUT:用于修正资源,对同一个URI进行屡次PUT操作,发生的影响和第一次相同,具有幂等性。
- DELETE:用于删去资源,对同一个URI进行屡次DELETE操作,发生的影响和第一次相同,具有幂等性
综上所述,这些仅仅仅仅HTTP协议建议在依据RESTFUL风格界说WEB API时的语义,并非强制性。一同关于幂等性的完结,肯定是经过前端或服务端完结。
业务问题抛出
在业务开发与分布式体系规划中,幂等性是一个十分重要的概念,有十分多的场景需求考虑幂等性的问题,特别关于现在的分布式体系,经常性的考虑重试、重发等操作,一旦发生这些操作,则必需求考虑幂等性问题。以交易体系、付出体系等特别显着,如:
- 当用户购物进行下单操作,用户操作屡次,但订单体系关于本次操作只能发生一个订单。
- 当用户对订单进行付款,付出体系不论呈现什么问题,应该只对用户扣一次款。
- 当付出成功对库存扣减时,库存体系对订单中产品的库存数量也只能扣减一次。
- 当对产品进行发货时,也需确保物流体系有且只能发一次货。
在电商体系中还有十分多的场景需求确保幂等性。可是一旦考虑幂等后,服务逻辑务必会变的更加杂乱。因而是否要考虑幂等,需求依据详细业务场景详细分析。而且在完结幂等时,还会把并行履行的功用改为串行化,下降了履行功率。
此处以下单减库存为例,当用户生成订单成功后,会对订单中产品进行扣减库存。 订单服务会调用库存服务进行库存扣减。库存服务会完结详细扣减完结。
现在关于功用调用的规划,有或许呈现调用超时,由于呈现如网络颤动,尽管库存服务履行成功了,但成果并没有在超时时间内回来,则订单服务也会进行重试。那就会呈现问题,stock关于之前的履行现已成功了,仅仅成果没有准时回来。而订单服务又从头建议恳求对产品进行库存扣减。 此刻呈现库存扣减两次的问题。 关于这种问题,就需求经过幂等性进行成果。
处理计划
关于幂等的考虑,首要处理两点前后端交互与服务间交互。这两点有时都要考虑幂等性的完结。从前端的思路处理的话,首要有三种:前端防重、PRG形式、Token机制。
前端防重
经过前端防重确保幂等是最简略的完结办法,前端相关特色和JS代码即可完结设置。可靠性并不好,有经历的人员能够经过东西越过页面仍能重复提交。首要适用于表单重复提交或按钮重复点击。
PRG形式
PRG形式即POST-REDIRECT-GET。当用户进行表单提交时,会重定向到别的一个提交成功页面,而不是停留在原先的表单页面。这样就避免了用户刷新导致重复提交。一同避免了经过浏览器按钮前进/撤退导致表单重复提交。是一种比较常见的前端防重战略。
Token形式
经过token机制来确保幂等是一种十分常见的处理计划,一同也合适绝大部分场景。该计划需求前后端进行必定程度的交互来完结。
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
表达式来注销查询与删去操作。
完结流程
经过token机制来确保幂等是一种十分常见的处理计划,一同也合适绝大部分场景。该计划需求前后端进行必定程度的交互来完结。
- 服务端提供获取token接口,供客户端进行运用。服务端生成token后,假如当前为分布式架构,将token存放于redis中,假如是单体架构,能够保存在jvm缓存中。
- 当客户端获取到token后,会带着着token建议恳求。
- 服务端接收到客户端恳求后,首先会判断该token在redis中是否存在。假如存在,则完结进行业务处理,业务处理完结后,再删去token。假如不存在,代表当前恳求是重复恳求,直接向客户端回来对应标识。
业务履行机遇
先履行业务再删去token
可是现在有一个问题,当前是先履行业务再删去token。
在高并发下,很有或许呈现第一次拜访时token存在,完结详细业务操作。但在还没有删去token时,客户端又带着token建议恳求,此刻,由于token还存在,第2次恳求也会验证经过,履行详细业务操作。
关于这个问题的处理计划的思维便是并行变串行。会形成必定性能损耗与吞吐量下降。
- 第一种计划:关于业务代码履行和删去token全体加线程锁。当后续线程再来拜访时,则阻塞排队。
- 第二种计划:凭借redis单线程和incr是原子性的特色。当第一次获取token时,以token作为key,对其进行自增。然后将token进行回来,当客户端带着token拜访履行业务代码时,关于判断token是否存在不必删去,而是对其继续incr。假如incr后的回来值为2。则是一个合法恳求答应履行,假如是其他值,则代表是非法恳求,直接回来。
先删去token再履行业务
那假如先删去token再履行业务呢?其实也会存在问题,假设详细业务代码履行超时或失利,没有向客户端回来清晰成果,那客户端就很有或许会进行重试,但此刻之前的token现已被删去了,则会被认为是重复恳求,不再进行业务处理。
这种计划无需进行额定处理,一个token只能代表一次恳求。一旦业务履行呈现反常,则让客户端从头获取令牌,从头建议一次拜访即可。推荐运用先删去token计划
可是不论先删token还是后删token,都会有一个相同的问题。每次业务恳求都回发生一个额定的恳求去获取token。可是,业务失利或超时,在出产环境下,一万个里最多也就十个左右会失利,那为了这十来个恳求,让其他九千九百多个恳求都发生额定恳求,就有一些因小失大了。尽管redis性能好,可是这也是一种资源的浪费。
依据业务完结
生成Token
修正token_service_order工程中OrderController,新增生成令牌办法genToken
@Autowired
private IdWorker idWorker;
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/genToken")
public String genToken(){
String token = String.valueOf(idWorker.nextId());
redisTemplate.opsForValue().set(token,0,30, TimeUnit.MINUTES);
return token;
}
新增接口
修正token_service_api工程,新增OrderFeign接口。
@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderFeign {
@GetMapping("/genToken")
public String genToken();
}
获取token
修正token_web_order工程中WebOrderController,新增获取token办法
@RestController
@RequestMapping("worder")
public class WebOrderController {
@Autowired
private OrderFeign orderFeign;
/**
* 服务端生成token
* @return
*/
@GetMapping("/genToken")
public String genToken(){
String token = orderFeign.genToken();
return token;
}
}
拦截器
修正token_common,新增feign拦截器
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
//传递令牌
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null){
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
if (request != null){
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
if ("token".equals(headerName)){
String headerValue = request.getHeader(headerName);
//传递token
requestTemplate.header(headerName,headerValue);
}
}
}
}
}
}
发动类
修正token_web_order发动类
@Bean
public FeignInterceptor feignInterceptor(){
return new FeignInterceptor();
}
新增订单
修正token_service_order中OrderController,新增增加订单办法
/**
* 生成订单
* @param order
* @return
*/
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order order, HttpServletRequest request){
//获取令牌
String token = request.getHeader("token");
//校验令牌
try {
if (redisTemplate.delete(token)){
//令牌删去成功,代表不是重复恳求,履行详细业务
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";
}
}else {
//删去令牌失利,重复恳求
System.out.println("repeat request");
return "repeat request";
}
}catch (Exception e){
throw new RuntimeException("体系反常,请重试");
}
}
修正token_service_order_api中OrderFeign。
@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderFeign {
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order order);
@GetMapping("/genToken")
public String genToken();
}
修正token_web_order中WebOrderController,新增增加订单办法
/**
* 新增订单
*/
@PostMapping("/addOrder")
public String addOrder(@RequestBody Order order){
String result = orderFeign.genOrder(order);
return result;
}
测验
经过postman获取令牌,将令牌放入恳求头中。敞开两个postman tab页面。一同增加订单,能够发现一个履行成功,另一个重复恳求。
{"id":"123321","totalNum":1,"payMoney":1,"payType":"1","payTime":"2020-05-20","receiverContact":"heima","receiverMobile":"15666666666","receiverAddress":"beijing"}
依据自界说注解完结
直接把token完结嵌入到办法中会形成很多重复代码的呈现。因而能够经过自界说注解将上述代码进行改造。在需求确保幂等的办法上,增加自界说注解即可。
自界说注解
在token_common中新建自界说注解Idemptent
/**
* 幂等性注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idemptent {
}
创立拦截器
在token_common中新建拦截器
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";
}
}
测验
获取令牌后,在jemeter中模仿高并发拜访,设置50个并发拜访
新增一个http request,并设置相关信息
增加HTTP Header Manager
测验履行,能够发现,只要一个恳求是成功的,其他悉数被判定为重复恳求。
往期干货:
- 为什么大家都说 SELECT * 功率低?
- 从阿里规约看Spring业务
- 【代码级】全链路压测的全体架构规划,以及5种完结计划(流量染色、数据隔离、接口隔离、零侵入、服务监控)
- 最近沉浸Redis网络模型,无法自拔!总算知道Redis为啥这么快了
- 拆开Netty,我发现了这个8个从来没见过的东西?
- 学习 Shell准没错
- 最近迷上了源码,Tomcat源码,看我这篇就够了
- 为什么这11道JVM面试题这么重要(附答案)
- 芯片战争50年,Intel为什么干不掉AMD?
- 翻了ConcurrentHashMap1.7 和1.8的源码,我总结了它们的首要差异。
- 爱上源码,重学Spring AOP深化
- 9000字,唠唠架构中的规划形式
- 号外号外!Ant Design Mobile 5.6.0最新实用指南!
- 15755字,解锁MySQL性能优化新姿势
- 怎么搞定MySQL锁(大局锁、表级锁、行级锁)?这篇文章告知你答案!太TMD详细了!!!
本文由
传智教育博学谷狂野架构师
教研团队发布。假如本文对您有协助,欢迎
重视
和点赞
;假如您有任何建议也可留言谈论
或私信
,您的支撑是我坚持创造的动力。转载请注明出处!