詳解如何利用Redis實現(xiàn)生成唯一ID
一、摘要
在上一篇文章中,我們詳細(xì)的介紹了隨著下單流量逐漸上升,為了降低數(shù)據(jù)庫的訪問壓力,通過請求唯一ID+redis分布式鎖來防止接口重復(fù)提交,流程圖如下!
每次提交的時候,需要先調(diào)用后端服務(wù)獲取請求唯一ID,然后才能提交。
對于這樣的流程,不少的同學(xué)可能會感覺到非常雞肋,尤其是單元測試,需要每次先獲取submitToken
值,然后才能提交!
能不能不用這么麻煩,直接服務(wù)端通過一些規(guī)則組合,生成本次請求唯一ID呢?
答案是可以的!
今天我們就一起來看看,如何通過服務(wù)端來完成請求唯一 ID 的生成?
二、方案實踐
我們先來看一張圖,這張圖就是本次方案的核心流程圖。
實現(xiàn)的邏輯,流程如下:
- 1.用戶點擊提交按鈕,服務(wù)端接受到請求后,通過規(guī)則計算出本次請求唯一ID值
- 2.使用
redis
的分布式鎖服務(wù),對請求 ID 在限定的時間內(nèi)嘗試進(jìn)行加鎖,如果加鎖成功,繼續(xù)后續(xù)流程;如果加鎖失敗,說明服務(wù)正在處理,請勿重復(fù)提交 - 3.最后一步,如果加鎖成功后,需要將鎖手動釋放掉,以免再次請求時,提示同樣的信息
引入緩存服務(wù)后,防止重復(fù)提交的大體思路如上,實踐代碼如下!
2.1、引入 redis 組件
本次 demo 項目是基于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ù)配置如下
#?項目名 spring.application.name=springboot-example-submit #?Redis數(shù)據(jù)庫索引(默認(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ù)器連接超時配置 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ù)驗證邏輯,通過 aop 代理方式實現(xiàn)
首先創(chuàng)建一個@SubmitLimit
注解,通過這個注解來進(jìn)行方法代理攔截!
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public?@interface?SubmitLimit?{ ????/** ?????*?指定時間內(nèi)不可重復(fù)提交(僅相對上一次發(fā)起請求時間差),單位毫秒 ?????*?@return ?????*/ ????int?waitTime()?default?1000; ????/** ?????*?指定請求頭部key,可以組合生成簽名 ?????*?@return ?????*/ ????String[]?customerHeaders()?default?{}; ????/** ?????*?自定義重復(fù)提交提示語 ?????*?@return ?????*/ ????String?customerTipMsg()?default?""; }
編寫方法代理服務(wù),增加防止重復(fù)提交的驗證,實現(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)鎖對應(yīng)的值 ?????*/ ????private?static?final?String?DEFAULT_LOCK_VALUE?=?"DEFAULT_SUBMIT_LOCK_VALUE"; ????/** ?????*?默認(rèn)重復(fù)提交提示語 ?????*/ ????private?static?final?String?DEFAULT_TIP_MSG?=?"服務(wù)正在處理,請勿重復(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("請求參數(shù)不能為空!"); ????????} ????????//獲取注解配置的參數(shù) ????????SubmitLimit?submitLimit?=?getSubmitLimit(joinPoint); ????????//組合生成key,通過key實現(xiàn)加鎖和解鎖 ????????String?lockKey?=?buildSubmitLimitKey(joinPoint,?request,?submitLimit.customerHeaders()); ????????//嘗試在指定的時間內(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í)行完畢之后,手動將鎖釋放 ????????????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ā)生異常,錯誤信息:",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 ?????*?生成規(guī)則:項目名+接口名+方法名+請求參數(shù)簽名(對請求頭部參數(shù)+請求body參數(shù),取SHA1值) ?????*?@param?joinPoint ?????*?@param?request ?????*?@param?customerHeaders ?????*?@return ?????*/ ????private?String?buildSubmitLimitKey(JoinPoint?joinPoint,?HttpServletRequest?request,?String[]?customerHeaders){ ????????//請求參數(shù)=請求頭部+請求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; ????} ????/** ?????*?獲取指定請求頭部參數(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(); ????} ????/** ?????*?獲取請求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(); ????} }
部分校驗邏輯用到了redis
分布式鎖,具體實現(xiàn)邏輯如下:
/** ?*?redis分布式鎖服務(wù)類 ?*?采用LUA腳本實現(xiàn),保證加鎖、解鎖操作原子性 ?* ?*/ @Component public?class?RedisLockService?{ ????/** ?????*?分布式鎖過期時間,單位秒 ?????*/ ????private?static?final?Long?DEFAULT_LOCK_EXPIRE_TIME?=?60L; ????@Autowired ????private?StringRedisTemplate?stringRedisTemplate; ????/** ?????*?嘗試在指定時間內(nèi)加鎖 ?????*?@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)); ????} }
部分代碼使用到了序列化相關(guān)類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); ????????//?讀取不認(rèn)識的枚舉時,當(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; ????/** ?????*?下單,指定請求頭部參與請求唯一值計算 ?????*?@param?request ?????*?@return ?????*/ ????@SubmitLimit(customerHeaders?=?{"appId",?"token"},?customerTipMsg?=?"正在加緊為您處理,請勿重復(fù)下單!") ????@PostMapping(value?=?"confirm") ????public?ResResult?confirmOrder(@RequestBody?OrderConfirmRequest?request){ ????????//調(diào)用訂單下單相關(guān)邏輯 ????????orderService.confirm(request); ????????return?ResResult.getSuccess(); ????} }
其中最關(guān)鍵的一個步就是將唯一請求 ID 的生成,放在服務(wù)端通過組合來實現(xiàn),在保證防止接口重復(fù)提交的效果同時,也可以顯著的降低接口測試復(fù)雜度!
三、小結(jié)
本次方案相比于上一個方案,最大的改進(jìn)點在于:將接口請求唯一 ID 的生成邏輯,放在服務(wù)端通過規(guī)則組合來實現(xiàn),不需要前端提交接口的時候強(qiáng)制帶上這個參數(shù),在滿足防止接口重復(fù)提交的要求同時,又能減少前端和測試提交接口的復(fù)雜度!
需要特別注意的是:使用redis
的分布式鎖,推薦單機(jī)環(huán)境,如果redis
是集群環(huán)境,可能會導(dǎo)致鎖短暫無效!
到此這篇關(guān)于詳解如何利用Redis實現(xiàn)生成唯一ID的文章就介紹到這了,更多相關(guān)Redis生成唯一ID內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis實現(xiàn)排行榜及相同積分按時間排序功能的實現(xiàn)
這篇文章主要介紹了Redis實現(xiàn)排行榜及相同積分按時間排序,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08阿里云官方Redis開發(fā)規(guī)范總結(jié)
本文主要介紹了阿里云官方Redis開發(fā)規(guī)范總結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08