系统幂等性设计与实践
冪等性
什么是冪等性
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'總結
以上是生活随笔為你收集整理的系统幂等性设计与实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 互联网系统设计原则
- 下一篇: 新闻系统粗略说明文档