中国建设银行曲江支行网站,网站建设与管理方案的总结,旅游管理网站业务模块,wordpress图集主题幂等性介绍
现如今很多系统都会基于分布式或微服务思想完成对系统的架构设计。那么在这一个系统中#xff0c;就会存在若干个微服务#xff0c;而且服务间也会产生相互通信调用。那么既然产生了服务调用#xff0c;就必然会存在服务调用延迟或失败的问题。当出现这种问题就会存在若干个微服务而且服务间也会产生相互通信调用。那么既然产生了服务调用就必然会存在服务调用延迟或失败的问题。当出现这种问题服务端会进行重试等操作或客户端有可能会进行多次点击提交。如果这样请求多次的话那最终处理的数据结果就一定要保证统一如支付场景。此时就需要通过保证业务幂等性方案来完成。
什么是幂等性 幂等是一个数学与计算机学概念即 f(n) 1^n无论 n 为多少f (n) 的值永远为 1在数学中某一元运算为幂等时其作用在任一元素两次后会和其作用一次的结果相同。 在编程开发中对于幂等的定义为无论对某一个资源操作了多少次其影响都应是相同的。 换句话说就是在接口重复调用的情况下对系统产生的影响是一样的但是返回值允许不同如查询。
幂等函数或幂等方法是指可以使用相同参数重复执行并能获得相同结果的函数。这些函数不会影响系统状态也不用担心重复执行会对系统造成改变。
幂等性不仅仅只是一次或多次操作对资源没有产生影响还包括第一次操作产生影响后以后多次操作不会再产生影响。并且幂等关注的是是否对资源产生影响而不关注结果。
幂等性维度 幂等性设计主要从两个维度进行考虑空间、时间。 空间定义了幂等的范围如生成订单的话不允许出现重复下单。时间定义幂等的有效期。有些业务需要永久性保证幂等如下单、支付等。而部分业务只要保证一段时间幂等即可。
同时对于幂等的使用一般都会伴随着出现锁的概念用于解决并发安全问题。
以 SQL 为例
select * from table where id1。此 SQL 无论执行多少次虽然结果有可能出现不同都不会对数据产生改变具备幂等性。insert into table(id,name) values(1,heima)。此 SQL 如果 id 或 name 有唯一性约束多次操作只允许插入一条记录则具备幂等性。如果不是则不具备幂等性多次操作会产生多条数据。update table set score100 where id 1。此 SQL 无论执行多少次对数据产生的影响都是相同的。具备幂等性。update table set score50score 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 的机制实现防止重复提交。 简单的说就是调用方在调用接口的时候先向后端请求一个全局 IDToken请求的时候携带这个全局 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 还存在第二次请求也会验证通过执行具体业务操作。 对于这个问题的解决方案的思想就是并行变串行。会造成一定性能损耗与吞吐量降低。 第一种方案对于业务代码执行和删除 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 {Autowiredprivate OrderFeign orderFeign;/*** 服务端生成token* return*/GetMapping(/genToken)public String genToken(){String token orderFeign.genToken();return token;}}拦截器 修改 token_common新增 feign 拦截器 Component
public class FeignInterceptor implements RequestInterceptor {Overridepublic void apply(RequestTemplate requestTemplate) {//传递令牌RequestAttributes requestAttributes RequestContextHolder.getRequestAttributes();if (requestAttributes ! null){HttpServletRequest request ((ServletRequestAttributes) requestAttributes).getRequest();if (request ! null){EnumerationString headerNames request.getHeaderNames();while (headerNames.hasMoreElements()){String headerName headerNames.nextElement();if (token.equals(headerName)){String headerValue request.getHeader(headerName);//传递tokenrequestTemplate.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 {Overridepublic 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;}Autowiredprivate 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(重复请求);}}Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}Overridepublic 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 测试执行可以发现只有一个请求是成功的其他全部被判定为重复请求。 本文由传智教育博学谷狂野架构师教研团队发布。 如果本文对您有帮助欢迎关注和点赞如果您有任何建议也可留言评论或私信您的支持是我坚持创作的动力。 转载请注明出处