網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
一、摘要
在上一篇文章中,我們?cè)敿?xì)的介紹了隨著下單流量逐漸上升,為了降低數(shù)據(jù)庫(kù)的訪問壓力,通過請(qǐng)求唯一ID+redis分布式鎖來防止接口重復(fù)提交,流程圖如下!
每次提交的時(shí)候,需要先調(diào)用后端服務(wù)獲取請(qǐng)求唯一ID,然后才能提交。
對(duì)于這樣的流程,不少的同學(xué)可能會(huì)感覺到非常雞肋,尤其是單元測(cè)試,需要每次先獲取submitToken
值,然后才能提交!
能不能不用這么麻煩,直接服務(wù)端通過一些規(guī)則組合,生成本次請(qǐng)求唯一ID呢?
答案是可以的!
今天我們就一起來看看,如何通過服務(wù)端來完成請(qǐng)求唯一 ID 的生成?
二、方案實(shí)踐
我們先來看一張圖,這張圖就是本次方案的核心流程圖。
實(shí)現(xiàn)的邏輯,流程如下:
- 1.用戶點(diǎn)擊提交按鈕,服務(wù)端接受到請(qǐng)求后,通過規(guī)則計(jì)算出本次請(qǐng)求唯一ID值
- 2.使用
redis
的分布式鎖服務(wù),對(duì)請(qǐng)求 ID 在限定的時(shí)間內(nèi)嘗試進(jìn)行加鎖,如果加鎖成功,繼續(xù)后續(xù)流程;如果加鎖失敗,說明服務(wù)正在處理,請(qǐng)勿重復(fù)提交 - 3.最后一步,如果加鎖成功后,需要將鎖手動(dòng)釋放掉,以免再次請(qǐng)求時(shí),提示同樣的信息
引入緩存服務(wù)后,防止重復(fù)提交的大體思路如上,實(shí)踐代碼如下!
2.1、引入 redis 組件
本次 demo 項(xiàng)目是基于SpringBoot
版本進(jìn)行構(gòu)建,添加相關(guān)的redis
依賴環(huán)境如下:
<!--?引入springboot?--> <parent> ????<groupId>org.springframework.boot</groupId> ????<artifactId>spring-boot-starter-parent</artifactId> ????<version>2.1.0.RELEASE</version> </parent> ...... <!--?Redis相關(guān)依賴包,采用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 環(huán)境配置
在全局配置application.properties
文件中,添加redis
相關(guān)服務(wù)配置如下
#?項(xiàng)目名 spring.application.name=springboot-example-submit #?Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0) spring.redis.database=1 #?Redis服務(wù)器地址 spring.redis.host=127.0.0.1 #?Redis服務(wù)器連接端口 spring.redis.port=6379 #?Redis服務(wù)器連接密碼(默認(rèn)為空) spring.redis.password= #?Redis服務(wù)器連接超時(shí)配置 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、編寫服務(wù)驗(yàn)證邏輯,通過 aop 代理方式實(shí)現(xiàn)
首先創(chuàng)建一個(gè)@SubmitLimit
注解,通過這個(gè)注解來進(jìn)行方法代理攔截!
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public?@interface?SubmitLimit?{ ????/** ?????*?指定時(shí)間內(nèi)不可重復(fù)提交(僅相對(duì)上一次發(fā)起請(qǐng)求時(shí)間差),單位毫秒 ?????*?@return ?????*/ ????int?waitTime()?default?1000; ????/** ?????*?指定請(qǐng)求頭部key,可以組合生成簽名 ?????*?@return ?????*/ ????String[]?customerHeaders()?default?{}; ????/** ?????*?自定義重復(fù)提交提示語(yǔ) ?????*?@return ?????*/ ????String?customerTipMsg()?default?""; }
編寫方法代理服務(wù),增加防止重復(fù)提交的驗(yàn)證,實(shí)現(xiàn)了邏輯如下!
@Order(1) @Aspect @Component public?class?SubmitLimitAspect?{ ????private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(SubmitLimitAspect.class); ????/** ?????*?redis分割符 ?????*/ ????private?static?final?String?REDIS_SEPARATOR?=?":"; ????/** ?????*?默認(rèn)鎖對(duì)應(yīng)的值 ?????*/ ????private?static?final?String?DEFAULT_LOCK_VALUE?=?"DEFAULT_SUBMIT_LOCK_VALUE"; ????/** ?????*?默認(rèn)重復(fù)提交提示語(yǔ) ?????*/ ????private?static?final?String?DEFAULT_TIP_MSG?=?"服務(wù)正在處理,請(qǐng)勿重復(fù)提交!"; ????@Value("${spring.application.name}") ????private?String?applicationName; ????@Autowired ????private?RedisLockService?redisLockService; ????/** ?????*?方法調(diào)用環(huán)繞攔截 ?????*/ ????@Around(value?=?"@annotation(com.example.submittoken.config.annotation.SubmitLimit)") ????public?Object?doAround(ProceedingJoinPoint?joinPoint){ ????????HttpServletRequest?request?=?getHttpServletRequest(); ????????if(Objects.isNull(request)){ ????????????return?ResResult.getSysError("請(qǐng)求參數(shù)不能為空!"); ????????} ????????//獲取注解配置的參數(shù) ????????SubmitLimit?submitLimit?=?getSubmitLimit(joinPoint); ????????//組合生成key,通過key實(shí)現(xiàn)加鎖和解鎖 ????????String?lockKey?=?buildSubmitLimitKey(joinPoint,?request,?submitLimit.customerHeaders()); ????????//嘗試在指定的時(shí)間內(nèi)加鎖 ????????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?{ ????????????//繼續(xù)執(zhí)行后續(xù)流程 ????????????return?execute(joinPoint); ????????}?finally?{ ????????????//執(zhí)行完畢之后,手動(dòng)將鎖釋放 ????????????redisLockService.releaseLock(lockKey,?DEFAULT_LOCK_VALUE); ????????} ????} ????/** ?????*?執(zhí)行任務(wù) ?????*?@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("業(yè)務(wù)處理發(fā)生異常,錯(cuò)誤信息:",e); ????????????return?ResResult.getSysError(ResResultEnum.DEFAULT_ERROR_MESSAGE); ????????} ????} ????/** ?????*?獲取請(qǐng)求對(duì)象 ?????*?@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 ?????*?生成規(guī)則:項(xiàng)目名+接口名+方法名+請(qǐng)求參數(shù)簽名(對(duì)請(qǐng)求頭部參數(shù)+請(qǐng)求body參數(shù),取SHA1值) ?????*?@param?joinPoint ?????*?@param?request ?????*?@param?customerHeaders ?????*?@return ?????*/ ????private?String?buildSubmitLimitKey(JoinPoint?joinPoint,?HttpServletRequest?request,?String[]?customerHeaders){ ????????//請(qǐng)求參數(shù)=請(qǐng)求頭部+請(qǐng)求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; ????} ????/** ?????*?獲取指定請(qǐng)求頭部參數(shù) ?????*?@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(); ????} ????/** ?????*?獲取請(qǐng)求body參數(shù) ?????*?@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(); ????} }
部分校驗(yàn)邏輯用到了redis
分布式鎖,具體實(shí)現(xiàn)邏輯如下:
/** ?*?redis分布式鎖服務(wù)類 ?*?采用LUA腳本實(shí)現(xiàn),保證加鎖、解鎖操作原子性 ?* ?*/ @Component public?class?RedisLockService?{ ????/** ?????*?分布式鎖過期時(shí)間,單位秒 ?????*/ ????private?static?final?Long?DEFAULT_LOCK_EXPIRE_TIME?=?60L; ????@Autowired ????private?StringRedisTemplate?stringRedisTemplate; ????/** ?????*?嘗試在指定時(shí)間內(nèi)加鎖 ?????*?@param?key ?????*?@param?value ?????*?@param?timeout?鎖等待時(shí)間 ?????*?@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)); ????} }
部分代碼使用到了序列化相關(guān)類JacksonUtils
,源碼如下:
public?class?JacksonUtils?{ ????private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(JacksonUtils.class); ????private?static?final?ObjectMapper?objectMapper?=?new?ObjectMapper(); ????static?{ ????????//?對(duì)象的所有字段全部列入 ????????objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); ????????//?忽略未知的字段 ????????objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,?false); ????????//?讀取不認(rèn)識(shí)的枚舉時(shí),當(dāng)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、在相關(guān)的業(yè)務(wù)接口上,增加SubmitLimit注解即可
@RestController @RequestMapping("order") public?class?OrderController?{ ????@Autowired ????private?OrderService?orderService; ????/** ?????*?下單,指定請(qǐng)求頭部參與請(qǐng)求唯一值計(jì)算 ?????*?@param?request ?????*?@return ?????*/ ????@SubmitLimit(customerHeaders?=?{"appId",?"token"},?customerTipMsg?=?"正在加緊為您處理,請(qǐng)勿重復(fù)下單!") ????@PostMapping(value?=?"confirm") ????public?ResResult?confirmOrder(@RequestBody?OrderConfirmRequest?request){ ????????//調(diào)用訂單下單相關(guān)邏輯 ????????orderService.confirm(request); ????????return?ResResult.getSuccess(); ????} }
其中最關(guān)鍵的一個(gè)步就是將唯一請(qǐng)求 ID ?的生成,放在服務(wù)端通過組合來實(shí)現(xiàn),在保證防止接口重復(fù)提交的效果同時(shí),也可以顯著的降低接口測(cè)試復(fù)雜度!
三、小結(jié)
本次方案相比于上一個(gè)方案,最大的改進(jìn)點(diǎn)在于:將接口請(qǐng)求唯一 ID 的生成邏輯,放在服務(wù)端通過規(guī)則組合來實(shí)現(xiàn),不需要前端提交接口的時(shí)候強(qiáng)制帶上這個(gè)參數(shù),在滿足防止接口重復(fù)提交的要求同時(shí),又能減少前端和測(cè)試提交接口的復(fù)雜度!
需要特別注意的是:使用redis
的分布式鎖,推薦單機(jī)環(huán)境,如果redis
是集群環(huán)境,可能會(huì)導(dǎo)致鎖短暫無效!
原文鏈接:https://mp.weixin.qq.com/s/Wedb1MyybIXdQEp5xvnxLw
相關(guān)推薦
- 2022-02-13 使用filter過濾器計(jì)算數(shù)組中符合條件的長(zhǎng)度
- 2022-08-29 C語(yǔ)言關(guān)鍵字auto與register及static專項(xiàng)詳解_C 語(yǔ)言
- 2022-09-13 flutter狀態(tài)管理Provider的使用學(xué)習(xí)_IOS
- 2022-06-18 python使用Random隨機(jī)生成列表的方法實(shí)例_python
- 2022-06-30 Oracle對(duì)PL/SQL中的異常處理_oracle
- 2023-03-28 python?list與numpy數(shù)組效率對(duì)比_python
- 2022-09-24 win2019?ftp服務(wù)器搭建圖文教程_FTP服務(wù)器
- 2022-06-08 PostgreSQL并行計(jì)算算法及參數(shù)強(qiáng)制并行度設(shè)置方法_PostgreSQL
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支