網站首頁 編程語言 正文
一、摘要
在上一篇文章中,我們詳細的介紹了隨著下單流量逐漸上升,為了降低數據庫的訪問壓力,通過請求唯一ID+redis分布式鎖來防止接口重復提交,流程圖如下!
每次提交的時候,需要先調用后端服務獲取請求唯一ID,然后才能提交。
對于這樣的流程,不少的同學可能會感覺到非常雞肋,尤其是單元測試,需要每次先獲取submitToken
值,然后才能提交!
能不能不用這么麻煩,直接服務端通過一些規則組合,生成本次請求唯一ID呢?
答案是可以的!
今天我們就一起來看看,如何通過服務端來完成請求唯一 ID 的生成?
二、方案實踐
我們先來看一張圖,這張圖就是本次方案的核心流程圖。
實現的邏輯,流程如下:
- 1.用戶點擊提交按鈕,服務端接受到請求后,通過規則計算出本次請求唯一ID值
- 2.使用
redis
的分布式鎖服務,對請求 ID 在限定的時間內嘗試進行加鎖,如果加鎖成功,繼續后續流程;如果加鎖失敗,說明服務正在處理,請勿重復提交 - 3.最后一步,如果加鎖成功后,需要將鎖手動釋放掉,以免再次請求時,提示同樣的信息
引入緩存服務后,防止重復提交的大體思路如上,實踐代碼如下!
2.1、引入 redis 組件
本次 demo 項目是基于SpringBoot
版本進行構建,添加相關的redis
依賴環境如下:
<!--?引入springboot?--> <parent> ????<groupId>org.springframework.boot</groupId> ????<artifactId>spring-boot-starter-parent</artifactId> ????<version>2.1.0.RELEASE</version> </parent> ...... <!--?Redis相關依賴包,采用jedis作為客戶端?--> <dependency> ????<groupId>org.springframework.boot</groupId> ????<artifactId>spring-boot-starter-data-redis</artifactId> ????<exclusions> ????????<exclusion> ????????????<groupId>redis.clients</groupId> ????????????<artifactId>jedis</artifactId> ????????</exclusion> ????????<exclusion> ????????????<artifactId>lettuce-core</artifactId> ????????????<groupId>io.lettuce</groupId> ????????</exclusion> ????</exclusions> </dependency> <dependency> ????<groupId>redis.clients</groupId> ????<artifactId>jedis</artifactId> </dependency> <dependency> ????<groupId>org.apache.commons</groupId> ????<artifactId>commons-pool2</artifactId> </dependency>
2.2、添加 redis 環境配置
在全局配置application.properties
文件中,添加redis
相關服務配置如下
#?項目名 spring.application.name=springboot-example-submit #?Redis數據庫索引(默認為0) spring.redis.database=1 #?Redis服務器地址 spring.redis.host=127.0.0.1 #?Redis服務器連接端口 spring.redis.port=6379 #?Redis服務器連接密碼(默認為空) spring.redis.password= #?Redis服務器連接超時配置 spring.redis.timeout=1000 #?連接池配置 spring.redis.jedis.pool.max-active=8 spring.redis.jedis.pool.max-wait=1000 spring.redis.jedis.pool.max-idle=8 spring.redis.jedis.pool.min-idle=0 spring.redis.jedis.pool.time-between-eviction-runs=100
2.3、編寫服務驗證邏輯,通過 aop 代理方式實現
首先創建一個@SubmitLimit
注解,通過這個注解來進行方法代理攔截!
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public?@interface?SubmitLimit?{ ????/** ?????*?指定時間內不可重復提交(僅相對上一次發起請求時間差),單位毫秒 ?????*?@return ?????*/ ????int?waitTime()?default?1000; ????/** ?????*?指定請求頭部key,可以組合生成簽名 ?????*?@return ?????*/ ????String[]?customerHeaders()?default?{}; ????/** ?????*?自定義重復提交提示語 ?????*?@return ?????*/ ????String?customerTipMsg()?default?""; }
編寫方法代理服務,增加防止重復提交的驗證,實現了邏輯如下!
@Order(1) @Aspect @Component public?class?SubmitLimitAspect?{ ????private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(SubmitLimitAspect.class); ????/** ?????*?redis分割符 ?????*/ ????private?static?final?String?REDIS_SEPARATOR?=?":"; ????/** ?????*?默認鎖對應的值 ?????*/ ????private?static?final?String?DEFAULT_LOCK_VALUE?=?"DEFAULT_SUBMIT_LOCK_VALUE"; ????/** ?????*?默認重復提交提示語 ?????*/ ????private?static?final?String?DEFAULT_TIP_MSG?=?"服務正在處理,請勿重復提交!"; ????@Value("${spring.application.name}") ????private?String?applicationName; ????@Autowired ????private?RedisLockService?redisLockService; ????/** ?????*?方法調用環繞攔截 ?????*/ ????@Around(value?=?"@annotation(com.example.submittoken.config.annotation.SubmitLimit)") ????public?Object?doAround(ProceedingJoinPoint?joinPoint){ ????????HttpServletRequest?request?=?getHttpServletRequest(); ????????if(Objects.isNull(request)){ ????????????return?ResResult.getSysError("請求參數不能為空!"); ????????} ????????//獲取注解配置的參數 ????????SubmitLimit?submitLimit?=?getSubmitLimit(joinPoint); ????????//組合生成key,通過key實現加鎖和解鎖 ????????String?lockKey?=?buildSubmitLimitKey(joinPoint,?request,?submitLimit.customerHeaders()); ????????//嘗試在指定的時間內加鎖 ????????boolean?lock?=?redisLockService.tryLock(lockKey,?DEFAULT_LOCK_VALUE,?Duration.ofMillis(submitLimit.waitTime())); ????????if(!lock){ ????????????String?tipMsg?=?StringUtils.isEmpty(submitLimit.customerTipMsg())???DEFAULT_TIP_MSG?:?submitLimit.customerTipMsg(); ????????????return?ResResult.getSysError(tipMsg); ????????} ????????try?{ ????????????//繼續執行后續流程 ????????????return?execute(joinPoint); ????????}?finally?{ ????????????//執行完畢之后,手動將鎖釋放 ????????????redisLockService.releaseLock(lockKey,?DEFAULT_LOCK_VALUE); ????????} ????} ????/** ?????*?執行任務 ?????*?@param?joinPoint ?????*?@return ?????*/ ????private?Object?execute(ProceedingJoinPoint?joinPoint){ ????????try?{ ????????????return?joinPoint.proceed(); ????????}?catch?(CommonException?e)?{ ????????????return?ResResult.getSysError(e.getMessage()); ????????}?catch?(Throwable?e)?{ ????????????LOGGER.error("業務處理發生異常,錯誤信息:",e); ????????????return?ResResult.getSysError(ResResultEnum.DEFAULT_ERROR_MESSAGE); ????????} ????} ????/** ?????*?獲取請求對象 ?????*?@return ?????*/ ????private?HttpServletRequest?getHttpServletRequest(){ ????????RequestAttributes?ra?=?RequestContextHolder.getRequestAttributes(); ????????ServletRequestAttributes?sra?=?(ServletRequestAttributes)ra; ????????HttpServletRequest?request?=?sra.getRequest(); ????????return?request; ????} ????/** ?????*?獲取注解值 ?????*?@param?joinPoint ?????*?@return ?????*/ ????private?SubmitLimit?getSubmitLimit(JoinPoint?joinPoint){ ????????MethodSignature?methodSignature?=?(MethodSignature)?joinPoint.getSignature(); ????????Method?method?=?methodSignature.getMethod(); ????????SubmitLimit?submitLimit?=?method.getAnnotation(SubmitLimit.class); ????????return?submitLimit; ????} ????/** ?????*?組合生成lockKey ?????*?生成規則:項目名+接口名+方法名+請求參數簽名(對請求頭部參數+請求body參數,取SHA1值) ?????*?@param?joinPoint ?????*?@param?request ?????*?@param?customerHeaders ?????*?@return ?????*/ ????private?String?buildSubmitLimitKey(JoinPoint?joinPoint,?HttpServletRequest?request,?String[]?customerHeaders){ ????????//請求參數=請求頭部+請求body ????????String?requestHeader?=?getRequestHeader(request,?customerHeaders); ????????String?requestBody?=?getRequestBody(joinPoint.getArgs()); ????????String?requestParamSign?=?DigestUtils.sha1Hex(requestHeader?+?requestBody); ????????String?submitLimitKey?=?new?StringBuilder() ????????????????.append(applicationName) ????????????????.append(REDIS_SEPARATOR) ????????????????.append(joinPoint.getSignature().getDeclaringType().getSimpleName()) ????????????????.append(REDIS_SEPARATOR) ????????????????.append(joinPoint.getSignature().getName()) ????????????????.append(REDIS_SEPARATOR) ????????????????.append(requestParamSign) ????????????????.toString(); ????????return?submitLimitKey; ????} ????/** ?????*?獲取指定請求頭部參數 ?????*?@param?request ?????*?@param?customerHeaders ?????*?@return ?????*/ ????private?String?getRequestHeader(HttpServletRequest?request,?String[]?customerHeaders){ ????????if?(Objects.isNull(customerHeaders))?{ ????????????return?""; ????????} ????????StringBuilder?sb?=?new?StringBuilder(); ????????for?(String?headerKey?:?customerHeaders)?{ ????????????sb.append(request.getHeader(headerKey)); ????????} ????????return?sb.toString(); ????} ????/** ?????*?獲取請求body參數 ?????*?@param?args ?????*?@return ?????*/ ????private?String?getRequestBody(Object[]?args){ ????????if?(Objects.isNull(args))?{ ????????????return?""; ????????} ????????StringBuilder?sb?=?new?StringBuilder(); ????????for?(Object?arg?:?args)?{ ????????????if?(arg?instanceof?HttpServletRequest ????????????????????||?arg?instanceof?HttpServletResponse ????????????????????||?arg?instanceof?MultipartFile ????????????????????||?arg?instanceof?BindResult ????????????????????||?arg?instanceof?MultipartFile[] ????????????????????||?arg?instanceof?ModelMap ????????????????????||?arg?instanceof?Model ????????????????????||?arg?instanceof?ExtendedServletRequestDataBinder ????????????????????||?arg?instanceof?byte[])?{ ????????????????continue; ????????????} ????????????sb.append(JacksonUtils.toJson(arg)); ????????} ????????return?sb.toString(); ????} }
部分校驗邏輯用到了redis
分布式鎖,具體實現邏輯如下:
/** ?*?redis分布式鎖服務類 ?*?采用LUA腳本實現,保證加鎖、解鎖操作原子性 ?* ?*/ @Component public?class?RedisLockService?{ ????/** ?????*?分布式鎖過期時間,單位秒 ?????*/ ????private?static?final?Long?DEFAULT_LOCK_EXPIRE_TIME?=?60L; ????@Autowired ????private?StringRedisTemplate?stringRedisTemplate; ????/** ?????*?嘗試在指定時間內加鎖 ?????*?@param?key ?????*?@param?value ?????*?@param?timeout?鎖等待時間 ?????*?@return ?????*/ ????public?boolean?tryLock(String?key,String?value,?Duration?timeout){ ????????long?waitMills?=?timeout.toMillis(); ????????long?currentTimeMillis?=?System.currentTimeMillis(); ????????do?{ ????????????boolean?lock?=?lock(key,?value,?DEFAULT_LOCK_EXPIRE_TIME); ????????????if?(lock)?{ ????????????????return?true; ????????????} ????????????try?{ ????????????????Thread.sleep(1L); ????????????}?catch?(InterruptedException?e)?{ ????????????????Thread.interrupted(); ????????????} ????????}?while?(System.currentTimeMillis()?<?currentTimeMillis?+?waitMills); ????????return?false; ????} ????/** ?????*?直接加鎖 ?????*?@param?key ?????*?@param?value ?????*?@param?expire ?????*?@return ?????*/ ????public?boolean?lock(String?key,String?value,?Long?expire){ ????????String?luaScript?=?"if?redis.call('setnx',?KEYS[1],?ARGV[1])?==?1?then?return?redis.call('expire',?KEYS[1],?ARGV[2])?else?return?0?end"; ????????RedisScript<Long>?redisScript?=?new?DefaultRedisScript<>(luaScript,?Long.class); ????????Long?result?=?stringRedisTemplate.execute(redisScript,?Collections.singletonList(key),?value,?String.valueOf(expire)); ????????return?result.equals(Long.valueOf(1)); ????} ????/** ?????*?釋放鎖 ?????*?@param?key ?????*?@param?value ?????*?@return ?????*/ ????public?boolean?releaseLock(String?key,String?value){ ????????String?luaScript?=?"if?redis.call('get',?KEYS[1])?==?ARGV[1]?then?return?redis.call('del',?KEYS[1])?else?return?0?end"; ????????RedisScript<Long>?redisScript?=?new?DefaultRedisScript<>(luaScript,?Long.class); ????????Long?result?=?stringRedisTemplate.execute(redisScript,?Collections.singletonList(key),value); ????????return?result.equals(Long.valueOf(1)); ????} }
部分代碼使用到了序列化相關類JacksonUtils
,源碼如下:
public?class?JacksonUtils?{ ????private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(JacksonUtils.class); ????private?static?final?ObjectMapper?objectMapper?=?new?ObjectMapper(); ????static?{ ????????//?對象的所有字段全部列入 ????????objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); ????????//?忽略未知的字段 ????????objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,?false); ????????//?讀取不認識的枚舉時,當null值處理 ????????objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL,?true); //????????序列化忽略未知屬性 ????????objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,?false); ????????//忽略字段大小寫 ????????objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES,?true); ????????objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE,?true); ????????SimpleModule?module?=?new?SimpleModule(); ????????module.addSerializer(Long.class,?ToStringSerializer.instance); ????????module.addSerializer(Long.TYPE,?ToStringSerializer.instance); ????????objectMapper.registerModule(module); ????} ????public?static?String?toJson(Object?object)?{ ????????if?(object?==?null)?{ ????????????return?null; ????????} ????????try?{ ????????????return?objectMapper.writeValueAsString(object); ????????}?catch?(Exception?e)?{ ????????????LOGGER.error("序列化失敗",e); ????????} ????????return?null; ????} ????public?static?<T>?T?fromJson(String?json,?Class<T>?classOfT)?{ ????????if?(json?==?null)?{ ????????????return?null; ????????} ????????try?{ ????????????return?objectMapper.readValue(json,?classOfT); ????????}?catch?(Exception?e)?{ ????????????LOGGER.error("反序列化失敗",e); ????????} ????????return?null; ????} ????public?static?<T>?T?fromJson(String?json,?Type?typeOfT)?{ ????????if?(json?==?null)?{ ????????????return?null; ????????} ????????try?{ ????????????return?objectMapper.readValue(json,?objectMapper.constructType(typeOfT)); ????????}?catch?(Exception?e)?{ ????????????LOGGER.error("反序列化失敗",e); ????????} ????????return?null; ????} }
2.4、在相關的業務接口上,增加SubmitLimit注解即可
@RestController @RequestMapping("order") public?class?OrderController?{ ????@Autowired ????private?OrderService?orderService; ????/** ?????*?下單,指定請求頭部參與請求唯一值計算 ?????*?@param?request ?????*?@return ?????*/ ????@SubmitLimit(customerHeaders?=?{"appId",?"token"},?customerTipMsg?=?"正在加緊為您處理,請勿重復下單!") ????@PostMapping(value?=?"confirm") ????public?ResResult?confirmOrder(@RequestBody?OrderConfirmRequest?request){ ????????//調用訂單下單相關邏輯 ????????orderService.confirm(request); ????????return?ResResult.getSuccess(); ????} }
其中最關鍵的一個步就是將唯一請求 ID ?的生成,放在服務端通過組合來實現,在保證防止接口重復提交的效果同時,也可以顯著的降低接口測試復雜度!
三、小結
本次方案相比于上一個方案,最大的改進點在于:將接口請求唯一 ID 的生成邏輯,放在服務端通過規則組合來實現,不需要前端提交接口的時候強制帶上這個參數,在滿足防止接口重復提交的要求同時,又能減少前端和測試提交接口的復雜度!
需要特別注意的是:使用redis
的分布式鎖,推薦單機環境,如果redis
是集群環境,可能會導致鎖短暫無效!
原文鏈接:https://mp.weixin.qq.com/s/Wedb1MyybIXdQEp5xvnxLw
相關推薦
- 2022-07-06 C++數據結構深入探究棧與隊列_C 語言
- 2022-08-13 VMware vCenter 無法創建自定義規范
- 2022-07-10 網絡I/o編程模型23 netty的出站與入站中handler加載與執行順序
- 2023-01-14 ubuntu端向日葵鍵盤輸入卡頓問題及解決_Linux
- 2022-05-25 Flutter實現倒計時功能_Android
- 2022-09-15 python接口測試對修改密碼接口進行壓測_python
- 2022-11-07 pandas中字典和dataFrame的相互轉換_python
- 2024-01-30 深入理解Scrapy中XPath的`following-sibling`選擇器
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支