日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

詳解如何利用Redis實(shí)現(xiàn)生成唯一ID_Redis

作者:鴨血粉絲Tang ? 更新時(shí)間: 2022-12-14 編程語(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

欄目分類
最近更新