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

學無先后,達者為師

網站首頁 編程語言 正文

詳解如何利用Redis實現生成唯一ID_Redis

作者:鴨血粉絲Tang ? 更新時間: 2022-12-14 編程語言

一、摘要

在上一篇文章中,我們詳細的介紹了隨著下單流量逐漸上升,為了降低數據庫的訪問壓力,通過請求唯一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

欄目分類
最近更新