系统幂等性设计与实践
幂等性
什么是幂等性
HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源**对于资源本身**应该具有同样的结果(网络超时等问题除外)。也就是说,**其任意多次执行对资源本身所产生的影响均与一次执行的影响相同**。
简单来说,是指无论调用多少次都不会有不同结果的 HTTP 方法。
什么情况下需要幂等
业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:
1. 用户在APP上连续点击了多次提交订单,后台应该只产生一个订单;
2. 向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。 **很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。**
解决方案
1. 乐观锁:基于版本号version实现, 在更新数据那一刻校验数据(会出现ABA问题)
2. 布式锁:redis 或 zookeeper 实现
3. version令牌: 防止页面重复提交
4. 防重表:防止新增脏数据
5. 消息队列:把请求快速缓冲起来,然后异步任务处理,优点:提高吞吐量,不足:不能及时响应返回对应结果,需要后续接口监听异步接口
实现幂等性
本次采用version令牌的方式实现幂等性,即采用 redis + version机制拦截器实现接口幂等性校验;
实现思路:
- 首先网关是全部请求的入口点,为了保证幂等性,即需要全局统一的version机制,先获取version,并且把version放入到redis中,然后请求业务接口时候,将上一步获取的version,放到header中(或者参数中)进行请求
- 服务端接收到对应的请求,首先采用拦截器的方式拦截对应参数,去redis中查找是否有存在该version
- 如果存在,执行业务逻辑之前在删除version,那么如果重复提交,由于version被删除,则返回给客户端提示 参数异常
- 如果本身就不存在,直接说明参数不合法
打开项目: common-spring-boot-starter
1.定义需要扫描的注解
com.open.capacity.common.annotation.ApiIdempotent
package com.open.capacity.common.annotation; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /\*\* \* 定义接口 幂等的注解 \*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
2.定义需要启动幂等拦截器的注解,采用Import的方式
com.open.capacity.common.annotation.EnableApiIdempotent
package com.open.capacity.common.annotation; import com.open.capacity.common.selector.ApiIdempotentImportSelector;
import org.springframework.context.annotation.Import; import java.lang.annotation.\*; /\*\* \* 启动幂等拦截器 \* @author gitgeek \* @create 2019年9月5日 \* 自动装配starter \* 选择器 \*/ @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented @Import(ApiIdempotentImportSelector.class)
public @interface EnableApiIdempotent {
}
3.导入的选择器(这里填写好要导入的全类名就行),导入ApiIdempotentConfig
com.open.capacity.common.selector.ApiIdempotentImportSelector
package com.open.capacity.common.selector; import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata; /\*\* \* \*/
public class ApiIdempotentImportSelector implements ImportSelector { /\*\* \* Select and return the names of which class(es) should be imported based on \* the {@link AnnotationMetadata} of the importing @{@link Configuration} class. \* \* @param importingClassMetadata \*/ @Override public String\[\] selectImports(AnnotationMetadata importingClassMetadata) { return new String\[\]{ "com.open.capacity.common.config.ApiIdempotentConfig" }; }
}
4.ApiIdempotentConfig自动配置类,定义好ApiIdempotentInterceptor拦截器
com.open.capacity.common.config.ApiIdempotentConfig
package com.open.capacity.common.config; import com.open.capacity.common.interceptor.ApiIdempotentInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration
@ConditionalOnClass(WebMvcConfigurer.class)
public class ApiIdempotentConfig implements WebMvcConfigurer { @Resource private RedisTemplate redisTemplate ; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ApiIdempotentInterceptor(redisTemplate)).addPathPatterns("/\*\*") ; }
}
5.ApiIdempotentInterceptor拦截器,对ApiIdempotent注解的方法 或者类进行拦截幂等接口
com.open.capacity.common.interceptor.ApiIdempotentInterceptor
package com.open.capacity.common.interceptor; import com.open.capacity.common.annotation.ApiIdempotent;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method; @AllArgsConstructor
public class ApiIdempotentInterceptor implements HandlerInterceptor { private static final String VERSION\_NAME = "version"; private RedisTemplate redisTemplate ; 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(); // TODO: 2019-08-27 获取目标方法上的幂等注解 ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class); if (methodAnnotation != null) { checkApiIdempotent(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示 } return true; } private void checkApiIdempotent(HttpServletRequest request) { String version = request.getHeader(VERSION\_NAME); if (StringUtils.isBlank(version)) {// header中不存在version version = request.getParameter(VERSION\_NAME); if (StringUtils.isBlank(version)) {// parameter中也不存在version throw new IllegalArgumentException("无效的参数"); } } if (!redisTemplate.hasKey(version)) { throw new IllegalArgumentException("不存在对应的参数"); } Boolean bool = redisTemplate.delete(version); if (!bool) { throw new IllegalArgumentException("没有删除对应的version"); } } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { }
}
## 如何使用
1.UserCenterApp 用户中心,在启动类上加@EnableApiIdempotent启动幂等拦截器,然后通过@ApiIdempotent注解
**com.open.capacity.UserCenterApp**
package com.open.capacity; import com.open.capacity.common.annotation.EnableApiIdempotent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Configuration; import com.open.capacity.common.port.PortApplicationEnvironmentPreparedEventListener;
import com.open.capacity.log.annotation.EnableLogging; /\*\*
\* @author 作者 owen E-mail: 624191343@qq.com
\* @version 创建时间:2018年4月5日 下午19:52:21
\* 类说明
\*/ @Configuration
@EnableLogging
@EnableDiscoveryClient
@SpringBootApplication
@EnableApiIdempotent
public class UserCenterApp { public static void main(String\[\] args) {
// 固定端口启动
// SpringApplication.run(UserCenterApp.class, args); //随机端口启动 SpringApplication app = new SpringApplication(UserCenterApp.class); app.addListeners(new PortApplicationEnvironmentPreparedEventListener()); app.run(args); } }
2.SysUserController 控制层
@ApiIdempotent 标记了该方法需要接口幂等
**com.open.capacity.user.controller.SysUserController**
/\*\* \* @author 作者 owen E-mail: 624191343@qq.com
\* @version 创建时间:2017年11月12日 上午22:57:51 \*用户 \*/
@Slf4j
@RestController
@Api(tags = "USER API")
public class SysUserController { @Autowired private SysUserService sysUserService; /\*\* \* 测试幂等接口 \* @param sysUser \* @return \*/ @PostMapping("/users/save") @ApiIdempotent public Result save(@RequestBody SysUser sysUser) { return sysUserService.saveOrUpdate(sysUser); } }
## 整体流程
1.首先进去网关,**api-gateway**项目,先通过 getVersion 获取对应的版本号,这个版本号可以根据自己业务修改对应的格式
**com.open.capacity.client.controller.UserController**
/\*\* \* @author 作者 owen E-mail: 624191343@qq.com \* @version 创建时间:2018年4月5日 下午19:52:21 \*/
@RestController
public class UserController { @GetMapping("/getVersion") public Result token() { String str = RandomUtil.randomString(24); StrBuilder token = new StrBuilder(); token.append(str); redisTemplate.opsForValue().set(token.toString(), token.toString(),300); return Result.succeed(token.toString(),""); } }
curl -i -X GET \\ 'http://127.0.0.1:9200/getVersion' { "datas": "8329lw34ii7ctsgibdfdkm2z", "resp\_code": 0, "resp\_msg": ""
}
2.请求幂等接口,这里单独写一个接口;@ApiIdempotent被该注解标记的接口,需要在在头部或者在参数加入version参数,否则无法过接口;
com.open.capacity.user.controller.SysUserController
/\*\* \* @author 作者 owen E-mail: 624191343@qq.com
\* @version 创建时间:2017年11月12日 上午22:57:51 \*用户 \*/
@Slf4j
@RestController
@Api(tags = "USER API")
public class SysUserController { @Autowired private SysUserService sysUserService; /\*\* \* 测试幂等接口 \* @param sysUser \* @return \*/ @PostMapping("/users/save") @ApiIdempotent public Result save(@RequestBody SysUser sysUser) { return sysUserService.saveOrUpdate(sysUser); } }
curl -i -X POST \\ -H "Content-Type:application/json" \\ -H "version:qcrro9jkymsx2t5b6ij3lc0p" \\ -d \\
'{ "id": "", "username": "admin", "nickname": "admin", "phone": "15914395926", "sex": "0", "roleId": "1"
}' \\ 'http://127.0.0.1:9200/api-user/users/save'
系统幂等性设计与实践相关推荐
- vivo全球商城:库存系统架构设计与实践
作者:vivo官网商城开发团队 - Xu Yi.Yan Chao 本文是vivo商城系列文章,主要介绍vivo商城库存系统发展历程.架构设计思路以及应对业务场景的实践. 一.业务背景 库存系统是电商商 ...
- 计算机毕设之餐厅选座订餐系统的设计与实践
「代码+论文+PPT」免费下载链接: http://106.55.47.97/host-pictures/java.png 概述 餐厅网络预约座位自助选餐系统的设计目的是为了方便广大消费者和提高餐厅管 ...
- 亿级商城促销系统架构设计与实践
作者:vivo互联网官方商城开发团队-Liu JianZhu 一.前言 随着商城业务渠道不断扩展,促销玩法不断增多,原商城v2.0架构已经无法满足不断增加的活动玩法,需要进行促销系统的独立建设,与商城 ...
- vivo商城促销系统架构设计与实践-概览篇
一.前言 随着商城业务渠道不断扩展,促销玩法不断增多,原商城v2.0架构已经无法满足不断增加的活动玩法,需要进行促销系统的独立建设,与商城解耦,提供纯粹的商城营销活动玩法支撑能力. 我们将分系列来介绍 ...
- vivo 亿级优惠券系统架构设计与实践
作者:vivo互联网开发团队-Yan Chao 一.业务背景 优惠券是电商常见的营销手段,具有灵活的特点,既可以作为促销活动的载体,也是重要的引流入口.优惠券系统是vivo商城营销模块中一个重要组成部 ...
- 很全的敏感词匹配系统的设计与实践
作者:vivo互联网服务器团队-Liang Kangwu 一.前言 谛听系统是vivo的内容审核平台,保障了vivo各互联网产品持续健康的发展.谛听支持审核多种内容类型,但日常主要审核的内容是文本,下 ...
- vivo全球商城优惠券系统架构设计与实践
业务背景 优惠券是电商常见的营销手段,具有灵活的特点,既可以作为促销活动的载体,也是重要的引流入口.优惠券系统是vivo商城营销模块中一个重要组成部分,早在15年vivo商城还是单体应用时,优惠券就是 ...
- 爱奇艺海外运营系统的设计和实践
海外技术团队针对海外运营需求,整合爱奇艺人群运营.移动端动态化.A/B 测试等资源,从零到一设计落地了海外运营系统:IQ Explorer . 从原型设计到上线至今,历经多个版本的迭代,覆盖了海外本地 ...
- vivo 全球商城:商品系统架构设计与实践
作者:vivo官网商城开发团队-Ju Changjiang 一.前言 随着用户量级的快速增长,vivo官方商城v1.0的单体架构逐渐暴露出弊端:模块愈发臃肿.开发效率低下.性能出现瓶颈.系统维护困难. ...
最新文章
- linux查看nginx并发连接情况
- JAVA编译显示存在不安全_java – 编译时出现“未经检查或不安全的操作”错误...
- iOS之深入解析高阶容器的原理和应用
- 7款最佳jQuery和JavaScript的PDF查看器插件
- Map、List、Set在Java中的各种遍历方法
- MyBatis(一)------目录
- python中unicode编码表_python unicode 编码整理
- 上机7 java异常处理,JavaSE学习笔记(七)——java异常处理机制
- mysql中sql添加表字段_SQL如何添加字段
- 极客日报第 35 期:国外运营商拒为小米 10T Pro 启用双卡;苹果明年 9 月或发布电动汽车;谷歌被指与 Facebook 密谋垄断
- 酷炫的可视化数据地图都是咋做的?10分钟学会
- 英语中比较重要的动词
- opencv task3------答题卡识别
- 【新手村专属】亚太杯数模参赛经验
- C#项目获取当前时间的农历时间
- 计算机硬盘大小转换(B,KB,MB,GB,TB,PB之间的大小转换)
- 第三方打码平台超级鹰图文识别,md5算法加密
- python中元组的特点_Python中的元组介绍
- 题解:《你的飞碟在这儿》、《哥德巴赫猜想》
- uvm_barrier
热门文章
- 显示一个屏幕,隐藏其他屏幕
- 经营升级渐成影院运营主课题,怎样才能交出技术改造好答卷?
- InMobi收购美国Sprint旗下数据和广告公司Pinsight Media
- python set union_python – set.union()抱怨它在传入生成器时没有参数
- matlab 数据降维和重构_核主成分分析(Kernel PCA, KPCA)的MATLAB 实现
- js根据月数计算有效日期_你真的看懂产品生产日期了么
- 油井在哪_身边榜样 | 张景岗:“油井越‘生气,自己越开心”
- 数据库SQL语句 使用DML语言 插入数据
- matlab交流电源并联,交流电路串联谐振和并联谐振的仿真分析
- delphi 解析json java_Delphi处理JSON格式数据