Java開發 - 緩存

?一、RedisUtil封裝

package com.qj.redis.util;import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;/*** Redis工具類* 提供Redis常見的操作封裝,簡化Redis使用* 使用Spring的RedisTemplate進行底層操作*/
@Component
@Slf4j
public class RedisUtil {// 緩存key的分隔符private static final String CACHE_KEY_SEPARATOR = ".";// Redis操作模板,由Spring注入@Resourceprivate RedisTemplate redisTemplate;/*** 構建緩存key* 將多個字符串參數用分隔符連接起來形成統一的key格式** @param strObjs key的組成部分,可變參數* @return 拼接后的完整key*/public String buildKey(String... strObjs) {return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR));}/*** 判斷key是否存在** @param key 要檢查的key* @return true-存在,false-不存在*/public boolean exist(String key) {return redisTemplate.hasKey(key);}/*** 刪除指定的key** @param key 要刪除的key* @return true-刪除成功,false-刪除失敗*/public boolean del(String key) {return redisTemplate.delete(key);}/*** 設置鍵值對** @param key 鍵* @param value 值*/public void set(String key, String value) {redisTemplate.opsForValue().set(key, value);}/*** 設置鍵值對(僅當key不存在時)** @param key 鍵* @param value 值* @param time 過期時間* @param timeUnit 時間單位* @return true-設置成功,false-設置失敗(key已存在)*/public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);}/*** 獲取指定key的值** @param key 鍵* @return key對應的值,如果key不存在則返回null*/public String get(String key) {return (String) redisTemplate.opsForValue().get(key);}/*** 向有序集合中添加元素** @param key 集合key* @param value 元素值* @param score 分數(用于排序)* @return true-添加成功,false-添加失敗*/public Boolean zAdd(String key, String value, Long score) {return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));}/*** 獲取有序集合的元素數量** @param key 集合key* @return 集合中的元素數量*/public Long countZset(String key) {return redisTemplate.opsForZSet().size(key);}/*** 獲取有序集合中指定范圍的元素** @param key 集合key* @param start 開始索引(從0開始)* @param end 結束索引(-1表示到最后)* @return 元素集合*/public Set<String> rangeZset(String key, long start, long end) {return redisTemplate.opsForZSet().range(key, start, end);}/*** 從有序集合中移除指定元素** @param key 集合key* @param value 要移除的元素值* @return 移除的元素數量*/public Long removeZset(String key, Object value) {return redisTemplate.opsForZSet().remove(key, value);}/*** 從有序集合中移除多個元素** @param key 集合key* @param value 要移除的元素集合*/public void removeZsetList(String key, Set<String> value) {value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));}/*** 獲取有序集合中指定元素的分數** @param key 集合key* @param value 元素值* @return 元素的分數*/public Double score(String key, Object value) {return redisTemplate.opsForZSet().score(key, value);}/*** 獲取有序集合中指定分數范圍的元素** @param key 集合key* @param start 開始分數* @param end 結束分數* @return 元素集合*/public Set<String> rangeByScore(String key, long start, long end) {return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));}/*** 增加有序集合中指定元素的分數** @param key 集合key* @param obj 元素值* @param score 要增加的分數* @return 增加后的新分數*/public Object addScore(String key, Object obj, double score) {return redisTemplate.opsForZSet().incrementScore(key, obj, score);}/*** 獲取有序集合中指定元素的排名(從小到大排序,排名從0開始)** @param key 集合key* @param obj 元素值* @return 元素的排名,如果元素不存在返回null*/public Object rank(String key, Object obj) {return redisTemplate.opsForZSet().rank(key, obj);}
}

二、Redis實現自動預熱緩存

說明:當項目啟動的時候,預熱一部分的緩存的場景,要創建在項目啟動時就加載緩存的模塊!

1. 定義緩存的抽象類AbstractCache

package com.qj.redis.init;import org.springframework.stereotype.Component;/*** 緩存抽象基類* 定義緩存操作的基本骨架,提供緩存初始化、獲取、清理和重載的通用接口* 使用Spring的@Component注解,便于被子類繼承并納入Spring容器管理*/
@Component
public abstract class AbstractCache {/*** 初始化緩存* 抽象方法,需要子類具體實現緩存初始化邏輯* 例如:從數據庫加載數據到緩存中*/public abstract void initCache();/*** 獲取緩存數據* 抽象方法,需要子類具體實現緩存獲取邏輯* @param <T> 緩存數據類型泛型* @return 返回指定類型的緩存數據*/public abstract <T> T getCache();/*** 清理緩存* 抽象方法,需要子類具體實現緩存清理邏輯* 例如:刪除Redis中的相關緩存鍵*/public abstract void clearCache();/*** 重新加載緩存* 默認實現:先清理現有緩存,然后重新初始化緩存* 提供了緩存刷新的標準流程,子類可按需重寫*/public void reloadCache() {clearCache();initCache();}
}

2. 實現需要進行緩存的類

(1)SysUserCache

package com.qj.sys.cache;import com.qj.redis.init.AbstractCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;/*** 系統用戶緩存實現類* 繼承AbstractCache抽象類,提供系統用戶相關的緩存操作具體實現* 使用Redis作為緩存存儲介質,通過RedisTemplate進行操作*/
@Component // 聲明為Spring組件,由Spring容器管理
public class SysUserCache extends AbstractCache {// 定義系統用戶緩存在Redis中的鍵名private static final String SYS_USER_CACHE_KEY = "SYS_USER";// 注入Redis操作模板@Autowiredprivate RedisTemplate redisTemplate;/*** 初始化緩存* 實現抽象方法,將系統用戶數據加載到Redis緩存中* 此處為示例,實際應用中可能需要從數據庫查詢數據后存入緩存*/@Overridepublic void initCache() {// 實際項目中,這里應該與數據庫或其他數據源聯動// 示例:將用戶數據存入Redis,使用字符串類型存儲redisTemplate.opsForValue().set(SYS_USER_CACHE_KEY, "qj1");}/*** 獲取緩存數據* 實現抽象方法,從Redis中獲取系統用戶緩存數據* 如果緩存不存在,會自動重新加載緩存* @param <T> 返回數據類型泛型* @return 返回系統用戶緩存數據*/@Overridepublic <T> T getCache() {// 檢查緩存鍵是否存在,如果不存在則重新加載緩存if (!redisTemplate.hasKey(SYS_USER_CACHE_KEY).booleanValue()) {reloadCache(); // 調用父類的重新加載緩存方法}// 從Redis中獲取緩存數據并返回return (T) redisTemplate.opsForValue().get(SYS_USER_CACHE_KEY);}/*** 清理緩存* 實現抽象方法,刪除Redis中的系統用戶緩存*/@Overridepublic void clearCache() {// 從Redis中刪除系統用戶緩存鍵redisTemplate.delete(SYS_USER_CACHE_KEY);}
}

(2)UserCache

package com.qj.sys.cache;import com.qj.redis.init.AbstractCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;/*** 用戶緩存實現類* 繼承AbstractCache抽象類,提供用戶相關的緩存操作具體實現* 使用Redis作為緩存存儲介質,通過RedisTemplate進行操作* 與SysUserCache類似但獨立,可根據業務需求區分不同用戶緩存*/
@Component // 聲明為Spring組件,由Spring容器管理
public class UserCache extends AbstractCache {// 定義用戶緩存在Redis中的鍵名// 注意:此鍵名與SysUserCache中的鍵名不同,避免緩存鍵沖突private static final String USER_CACHE_KEY = "USER";// 注入Redis操作模板,用于執行Redis命令@Autowiredprivate RedisTemplate redisTemplate;/*** 初始化緩存* 實現抽象方法,將用戶數據加載到Redis緩存中* 此處為示例,實際應用中需要從數據庫或其他數據源獲取真實數據*/@Overridepublic void initCache() {// 實際項目中,這里應該與數據庫或其他數據源聯動// 示例:將用戶數據存入Redis,使用字符串類型存儲// 注意:此處僅為演示,實際應存儲真實用戶數據redisTemplate.opsForValue().set(USER_CACHE_KEY, "qj2");}/*** 獲取緩存數據* 實現抽象方法,從Redis中獲取用戶緩存數據* 采用懶加載模式:如果緩存不存在,會自動重新加載緩存* @param <T> 返回數據類型泛型* @return 返回用戶緩存數據,實際類型根據調用上下文確定*/@Overridepublic <T> T getCache() {// 檢查緩存鍵是否存在,如果不存在則重新加載緩存// 這種設計確保調用getCache時總能獲取到數據(即使緩存意外失效)if (!redisTemplate.hasKey(USER_CACHE_KEY).booleanValue()) {reloadCache(); // 調用父類的重新加載緩存方法}// 從Redis中獲取緩存數據并返回,進行類型轉換return (T) redisTemplate.opsForValue().get(USER_CACHE_KEY);}/*** 清理緩存* 實現抽象方法,刪除Redis中的用戶緩存* 通常在數據更新后調用,確保緩存與數據源的一致性*/@Overridepublic void clearCache() {// 從Redis中刪除用戶緩存鍵redisTemplate.delete(USER_CACHE_KEY);}
}

3. 定義類來獲取ApplicationContext

說明:當一個類實現了ApplicationContextAware接口之后,這個類就可以方便的獲得ApplicationContext對象(Spring上下文),Spring發現某個Bean實現了ApplicationContextAware接口,Spring容器會在創建該Bean之后,自動調用該Bean的setApplicationContext(參數)方法,調用該方法時,會將容器本身ApplicationContext對象作為參數傳遞給該方法。

package com.qj.redis.util;import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;/*** Spring上下文工具類* 實現ApplicationContextAware接口,用于獲取Spring應用上下文* 提供靜態方法方便在非Spring管理的類中獲取Spring容器中的Bean*/
@Component // 聲明為Spring組件,由Spring容器管理
public class SpringContextUtil implements ApplicationContextAware {// 靜態變量保存Spring應用上下文private static ApplicationContext applicationCtxt;/*** 獲取Spring應用上下文* @return 返回當前Spring應用上下文實例*/public static ApplicationContext getApplicationContext() {return applicationCtxt;}/*** 設置Spring應用上下文* 實現ApplicationContextAware接口的方法,Spring容器啟動時自動調用* @param applicationContext Spring應用上下文* @throws BeansException 如果設置過程中出現異常*/@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {// 將傳入的應用上下文賦值給靜態變量,使其可在靜態方法中使用applicationCtxt = applicationContext;}/*** 根據類型獲取Spring容器中的Bean實例* @param type Bean的Class類型* @param <T> Bean類型泛型* @return 返回指定類型的Bean實例*/public static <T> T getBean(Class<T> type) {return applicationCtxt.getBean(type);}
}

4. 啟動并初始化緩存InitCache

說明:在使用SpringBoot構建項目時,我們通常有一些預先數據的加載。那么SpringBoot提供了CommandLineRunner方式來實現,CommandLineRunner是一個接口,我們需要時,只需實現該接口就行(如果存在多個加載的數據,我們也可以使用@Order注解來排序)?

package com.qj.redis.init;import com.qj.redis.util.SpringContextUtil;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import java.util.Map;
import java.util.Map.Entry;/*** 緩存初始化啟動器* 實現CommandLineRunner接口,在Spring Boot應用啟動后自動執行緩存預熱* 通過@ConditionalOnProperty條件注解控制是否啟用緩存預熱功能*/
@Component // 聲明為Spring組件,由Spring容器管理
@ConditionalOnProperty(name = "init.cache.enable", havingValue = "true") // 條件注解:只有當配置文件中init.cache.enable=true時才啟用該類
public class InitCache implements CommandLineRunner {/*** 項目啟動時自動執行的方法* 實現CommandLineRunner接口的run方法,Spring Boot啟動完成后會自動調用* @param args 命令行參數* @throws Exception 可能拋出的異常*/@Overridepublic void run(String... args) throws Exception {// 獲取所有需要預熱的緩存實例ApplicationContext applicationContext = SpringContextUtil.getApplicationContext();// 從Spring容器中獲取所有AbstractCache類型的Bean// 這些Bean都是具體的緩存實現類(如SysUserCache、UserCache等)Map<String, AbstractCache> beanMap = applicationContext.getBeansOfType(AbstractCache.class);// 如果存在緩存Bean,則遍歷并調用它們的初始化方法if (!beanMap.isEmpty()) {for (Entry<String, AbstractCache> entry : beanMap.entrySet()) {// 獲取AbstractCache的具體實現類實例// 這里通過SpringContextUtil再次獲取Bean是為了確保獲取的是Spring管理的代理對象// 這樣可以保證AOP等Spring特性正常工作AbstractCache abstractCache = (AbstractCache) SpringContextUtil.getBean(entry.getValue().getClass());// 調用緩存初始化方法,將數據加載到緩存中abstractCache.initCache();}}// 輸出緩存預熱完成日志System.out.println("緩存預熱成功...");}
}

5. 配置文件開啟

init:cahce:enable: true

6. 啟動并測試

說明:可以看到在項目啟動時,控制臺順利輸出“緩存成功...”,說明項目成功運行!

注意:查看Redis集群也正常看到已被緩存的兩個Key的數據!

三、分布式鎖的實現

說明:此處不使用Redission來直接實現,完全手動實現一個搶占式的分布式鎖

使用場景:

(1)任務調度(集群環境下,一個服務的多個實例的任務不想同一時間都進行執行)

(2)并發修改相關(操作同一個數據)

1. 依賴文件pom.xml

說明:添加commons-lang包,用來做字符串的校驗!

Apache Commons Lang 是一個提供了許多Java語言核心類擴展功能的工具庫,主要包括:

  1. 字符串處理:增強的字符串操作方法

  2. 數值處理:數字和數值類型的工具類

  3. 對象操作:對象比較、哈希碼生成、toString方法等

  4. 異常處理:異常鏈和嵌套異常處理

  5. 系統屬性:Java系統屬性訪問工具

  6. 隨機數生成:更強大的隨機數生成器

  7. 日期時間處理:日期和時間操作工具

<dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version>
</dependency>

2. 運行時異常類ShareLockException

/*** 共享鎖異常類* 自定義運行時異常,用于處理共享鎖操作中的異常情況* 繼承自RuntimeException,屬于非受檢異常(unchecked exception)* * 使用場景:* - 當共享鎖獲取失敗時拋出* - 當共享鎖操作超時時拋出* - 當共享鎖狀態異常時拋出*/
public class ShareLockException extends RuntimeException {/*** 構造函數* @param message 異常詳細信息,用于說明異常原因和上下文*/public ShareLockException(String message) {// 調用父類RuntimeException的構造函數,傳入異常消息super(message);}}

3. RedisUtil中添加相關方法

package com.qj.redis.util;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** Redis操作工具類* 封裝常用的Redis操作方法,提供簡潔的API供業務代碼調用* 使用Spring的RedisTemplate進行底層操作*/
@Component // 聲明為Spring組件,由Spring容器管理
public class RedisUtil {// 注入Redis操作模板@Autowiredprivate RedisTemplate redisTemplate;// 這里可能有其他方法,用...表示/*** 設置鍵值對(僅當鍵不存在時)* 原子操作,常用于分布式鎖的實現* * @param key 緩存的鍵* @param value 緩存的值* @param time 過期時間* @param timeUnit 時間單位* @return 設置成功返回true,鍵已存在返回false*/public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);}/*** 根據鍵獲取緩存值* * @param key 緩存的鍵* @return 對應的值,如果鍵不存在返回null*/public String get(String key) {String value = (String) redisTemplate.opsForValue().get(key);return value;}/*** 刪除指定的緩存鍵* * @param key 要刪除的緩存鍵* @return 刪除成功返回true,鍵不存在返回false*/public boolean del(String key) {return redisTemplate.delete(key);}
}

4. 實現:Redis分布式搶占鎖RedisShareLockUtil

package com.qj.redis.util;import com.qj.redis.exception.ShareLockException;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;/*** Redis分布式鎖工具類* 基于Redis實現的分布式鎖,支持加鎖、解鎖和嘗試加鎖操作* 使用請求標識(requestId)確保只有加鎖者才能解鎖,防止誤刪其他客戶端的鎖*/
@Component // 聲明為Spring組件,由Spring容器管理
public class RedisShareLockUtil {// 注入Redis工具類@Resourceprivate RedisUtil redisUtil;// 加鎖超時時間(毫秒),防止無限等待private Long TIME_OUT = 1000L;/*** 加鎖方法(阻塞式)* 在指定時間內嘗試獲取分布式鎖,如果獲取不到會進行重試* * @param lockKey 鎖的鍵名* @param requestId 請求標識(通常使用UUID),用于確保只有加鎖者才能解鎖* @param time 鎖的持有時間(毫秒)* @return 加鎖成功返回true,否則返回false* @throws ShareLockException 當參數異常時拋出*/public boolean lock(String lockKey, String requestId, Long time) {// 參數校驗if (StringUtils.isBlank(lockKey) || StringUtils.isBlank(requestId) || time <= 0) {throw new ShareLockException("分布式鎖-加鎖參數異常");}long currentTime = System.currentTimeMillis();long outTime = currentTime + TIME_OUT; // 計算超時時間點Boolean result = false;// 在超時時間內循環嘗試獲取鎖while (currentTime < outTime) {// 嘗試獲取鎖result = redisUtil.setNx(lockKey, requestId, time, TimeUnit.MILLISECONDS);if (result) {// 獲取鎖成功,返回truereturn result;}// 獲取鎖失敗,休眠100毫秒后重試try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 更新當前時間currentTime = System.currentTimeMillis();}// 超時后仍未獲取到鎖,返回falsereturn result;}/*** 解鎖方法* 根據鎖鍵和請求標識釋放分布式鎖,確保只有加鎖者才能解鎖* * @param key 鎖的鍵名* @param requestId 請求標識,必須與加鎖時使用的標識一致* @return 解鎖成功返回true,否則返回false* @throws ShareLockException 當參數異常時拋出*/public boolean unLock(String key, String requestId) {// 參數校驗if (StringUtils.isBlank(key) || StringUtils.isBlank(requestId)) {throw new ShareLockException("分布式鎖-解鎖參數異常");}try {// 獲取鎖當前的值String value = redisUtil.get(key);// 檢查請求標識是否匹配,防止誤刪其他客戶端的鎖if (requestId.equals(value)) {// 只有加鎖者才能解鎖redisUtil.del(key);return true;}} catch (Exception e) {// 記錄日志,這里應該使用日志框架記錄異常信息// 補日志}// 解鎖失敗return false;}/*** 嘗試加鎖方法(非阻塞式)* 嘗試獲取分布式鎖一次,無論成功與否都立即返回* * @param lockKey 鎖的鍵名* @param requestId 請求標識* @param time 鎖的持有時間(毫秒)* @return 加鎖成功返回true,否則返回false* @throws ShareLockException 當參數異常時拋出*/public boolean tryLock(String lockKey, String requestId, Long time) {// 參數校驗if (StringUtils.isBlank(lockKey) || StringUtils.isBlank(requestId) || time <= 0) {throw new ShareLockException("分布式鎖-嘗試加鎖參數異常");}// 嘗試獲取鎖一次return redisUtil.setNx(lockKey, requestId, time, TimeUnit.MILLISECONDS);}
}

5. 測試:調用分布式鎖

package com.qj.sys.controller;import com.qj.redis.util.RedisShareLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
@RequestMapping("test")
public class TestController {@Autowiredprivate RedisShareLockUtil redisShareLockUtil;@GetMapping("/testRedisShareLock")public String testRedisShareLock() {boolean result = redisShareLockUtil.lock("qj", "123456", 10000L);log.info("分布式鎖獲取:{}", result);return String.valueOf(result);}}

控制臺返回:(獨占鎖)

四、Redis實現延遲隊列

說明:我們有一個任務的情況下,我們會期望這個任務在某個時間點去執行,那么就要使用延遲隊列。一般延遲隊列可以使用RabbitMQ或者RocketMQ來進行實現,另外一種常用的方式就是使用Redis來進行實現了!

實現方案:基于redis來進行實現,我們主要使用的是zset這個數據類型天生的具有score的特性。zset可以根據score放入,而且可以通過range進行排序獲取,以及刪除指定的值。從業務上,我們可以再新增任務的時候放入,再通過定時任務進行拉取,要注意的一點就是拉取的時候要有分布式鎖,不要進行重復拉取,或者交由分布式任務調度來處理拉取,都是可以的。

使用場景:我們更加偏向于 定時群發,定時取消 等。就舉一個發博客的例子吧,博客我們可以選擇定時發布,那么就可以應用redis的延遲隊列來進行實現。要注意的一個點就是小心大key的產生,要做好延遲隊列的key的隔離。

1. 延遲任務的實體類

package com.qj.sys.delayQueue;import lombok.Data;import java.util.Date;@Data
public class MassMailTask {// 相關任務IDprivate Long taskId;// 延遲任務的開始時間private Date startTime;
}

2. 任務對延遲隊列的推送方法和拉取的方法

實現思路:

入隊:入隊消息體一定要有時間的概念,把時間轉換為毫秒,來作為我們zset的score;底層就是zset的add方法,由key,value以及score來組成。

出隊:出隊要基于rangeByScore來進行實現,指定我們的score的區間,也就是我們要拉取哪些的任務,拉取成功之后,我們先去執行業務邏輯,執行成功之后,我們再將其從消息隊列進行刪除。

package com.qj.sys.delayQueue;import com.alibaba.fastjson.JSON;
import com.qj.redis.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;import javax.annotation.Resource;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;/*** 群發郵件任務延時隊列服務* 使用Redis的ZSet(有序集合)實現延時隊列功能* 用于處理定時群發郵件任務的調度*/
@Service // 聲明為Spring服務組件
@Slf4j // 使用Lombok的日志注解
public class MassMailTaskService {// Redis中ZSet結構的key,用于存儲群發郵件任務private static final String MASS_MAIL_TASK_KEY = "massMailTask";// 注入Redis工具類@Resourceprivate RedisUtil redisUtil;/*** 將群發郵件任務推送到延時隊列* 使用Redis的ZSet結構,以任務開始時間作為分數(score)* * @param massMailTask 群發郵件任務對象*/public void pushMassMailTaskQueue(MassMailTask massMailTask) {// 獲取任務開始時間Date startTime = massMailTask.getStartTime();// 校驗開始時間是否有效if (startTime == null) {return;}// 如果開始時間早于或等于當前時間,則不加入隊列if (startTime.compareTo(new Date()) <= 0) {return;}// 記錄日志log.info("定時群發任務加入延時隊列,massMailTask:{}", JSON.toJSON(massMailTask));// 使用Redis的ZSet數據結構存儲任務// key: MASS_MAIL_TASK_KEY// value: 任務ID的字符串形式// score: 任務開始時間的時間戳redisUtil.zAdd(MASS_MAIL_TASK_KEY, massMailTask.getTaskId().toString(), startTime.getTime());}/*** 從延時隊列中拉取到期的群發郵件任務* 獲取分數(時間戳)在0到當前時間之間的所有任務* 獲取后會從隊列中移除這些任務* * @return 返回到期任務的ID集合*/public Set<Long> poolMassMailTaskQueue() {// 獲取當前時間之前的所有任務// 參數說明:key, minScore(0), maxScore(當前時間戳)Set<String> taskIdSet = redisUtil.rangeByScore(MASS_MAIL_TASK_KEY, 0, System.currentTimeMillis());// 記錄日志log.info("獲取延遲群發任務,taskIdSet:{}", JSON.toJSON(taskIdSet));// 如果任務集合為空,返回空集合if (CollectionUtils.isEmpty(taskIdSet)) {return Collections.emptySet();}// 從Redis中移除已獲取的任務redisUtil.removeZsetList(MASS_MAIL_TASK_KEY, taskIdSet);// 將任務ID字符串集合轉換為Long類型集合return taskIdSet.stream().map(n -> Long.parseLong(n)).collect(Collectors.toSet());}
}

3. 編寫測試類

注意:由于可能是分布式服務,所以可能是定時循環拉取,在拉取的時候不能所有服務的拉取都去拉,而是只允許一個任務去拉取,要么使用xxljob來實現,要么就選擇分布式鎖,只允許一個服務能夠拉取并執行!

package com.qj.sys;import com.alibaba.fastjson.JSON;
import com.qj.redis.util.RedisShareLockUtil;
import com.qj.sys.delayQueue.MassMailTask;
import com.qj.sys.delayQueue.MassMailTaskService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.CollectionUtils;import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Set;
import java.util.UUID;@SpringBootTest(classes = {SysApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Slf4j
public class MassMailTaskTest {// 注入MassMailTaskService,用于操作延時任務隊列@Resourceprivate MassMailTaskService massMailTaskService;// 注入RedisShareLockUtil,用于分布式鎖的操作@Resourceprivate RedisShareLockUtil redisShareLockUtil;/*** 測試方法:推送郵件任務到延時隊列*/@Testpublic void push() throws Exception {// 創建一個SimpleDateFormat對象,用于將時間字符串轉換為Date對象SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 創建一個MassMailTask對象,表示一個郵件任務MassMailTask massMailTask = new MassMailTask();massMailTask.setTaskId(1L); // 設置任務IDmassMailTask.setStartTime(simpleDateFormat.parse("2023-01-08 23:59:00")); // 設置任務的開始時間// 將任務插入到延時隊列中massMailTaskService.pushMassMailTaskQueue(massMailTask);// 輸出日志,確認任務已插入log.info("定時任務已插入!");}/*** 測試方法:處理延時任務隊列中的任務*/@Testpublic void deal() {// 定義鎖的key值,用于Redis分布式鎖String lockKey = "test.delay.task";// 創建一個唯一的請求ID,用于標識當前請求String requestId = UUID.randomUUID().toString();try {// 嘗試獲取分布式鎖,鎖定5秒鐘boolean locked = redisShareLockUtil.lock(lockKey, requestId, 5L);// 如果獲取鎖失敗,則直接返回if (!locked) {return;}// 從延時隊列中獲取一批任務IDSet<Long> taskIdSet = massMailTaskService.poolMassMailTaskQueue();// 打印獲取到的任務ID集合log.info("DelayTaskTest.deal.taskIdSet:{}", JSON.toJSON(taskIdSet));// 如果任務集合為空,則返回if (CollectionUtils.isEmpty(taskIdSet)) {return;}// 處理獲取到的任務,可以在此處添加具體的業務邏輯} catch (Exception e) {// 處理異常并輸出日志log.error("延時任務拉取執行失敗:{}", e.getMessage(), e);} finally {// 無論如何,釋放分布式鎖redisShareLockUtil.unLock(lockKey, requestId);}}
}

4. 運行測試

(1)第一步:任務推送延遲隊列

(2)第二步:定時任務未到時間,直接進行拉取

(3)第三步:定時任務到時間了,然后進行拉取

說明:可以看到,到了時間之后,就可以正常拉取到定時任務!

(4)第四步:在延遲隊列中插入兩個延遲任務

說明:可以看到,到了時間之后,就可以正常拉取到所有定時任務,也是為什么需要使用ZSet來存儲的原因!

五、使用Redis的Lua腳本實現CAS

說明:使用Lua腳本實現 CAS(比較并交換)的過程!

1. RedisLua工具類

package com.qj.sys.redislua;import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;/*** Redis Lua腳本執行組件* 封裝了比較并設置(CAS)操作的Lua腳本執行邏輯* 提供原子性的比較并設置功能,確保在分布式環境下的數據一致性*/
@Component // 聲明為Spring組件,由Spring容器管理
@Slf4j // Lombok日志注解
public class CompareAndSetLua {// 注入Redis操作模板@Resourceprivate RedisTemplate redisTemplate;// Redis腳本對象,用于執行Lua腳本private DefaultRedisScript<Boolean> casScript;/*** 初始化方法* 在Bean創建后自動執行,加載Lua腳本并配置腳本執行器*/@PostConstruct // 在依賴注入完成后自動執行public void init() {// 創建Redis腳本執行器casScript = new DefaultRedisScript<>();// 設置腳本返回類型為BooleancasScript.setResultType(Boolean.class);// 從類路徑加載Lua腳本文件// 假設腳本文件位于resources目錄下的compareAndSet.luacasScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));}/*** 執行比較并設置操作* 調用Lua腳本實現原子性的比較并設置功能* * @param key Redis鍵名* @param oldValue 期望的舊值* @param newValue 要設置的新值* @return 操作成功返回true,失敗返回false*/public boolean compareAndSet(String key, Long oldValue, Long newValue) {// 創建鍵列表,Lua腳本中通過KEYS[1]訪問ArrayList<String> keys = new ArrayList<>();keys.add(key);// 執行Lua腳本// 參數說明:腳本對象、鍵列表、參數列表(舊值和新值)Boolean result = (Boolean) redisTemplate.execute(casScript, keys, oldValue, newValue);// 返回操作結果return result;}
}

2. 相關Lua腳本(compareAndSetLua.lua )

注意:在此key下,如果傳入的oldValue和存在的值相同,則更新為newValue,否則不變!

-- Redis Lua腳本:原子性比較并設置(Compare and Set)操作
-- 實現類似Java中Atomic類的CAS(Compare and Swap)功能-- 從鍵參數列表中獲取第一個鍵名
local key = KEYS[1]-- 從參數列表中獲取期望的舊值和要設置的新值
local oldValue = ARGV[1]
local newValue = ARGV[2]-- 從Redis中獲取指定鍵的當前值
local redisValue = redis.call('get', key)-- 判斷條件:
-- 1. 如果鍵不存在(redisValue為false)
-- 2. 或者當前值等于期望的舊值(轉換為數字比較)
if (redisValue == false or tonumber(redisValue) == tonumber(oldValue))
then-- 條件滿足,設置新值redis.call('set', key, newValue)-- 返回操作成功return true
else-- 條件不滿足,當前值與期望的舊值不匹配-- 返回操作失敗return false
end

3. 運行測試

package com.qj.sys;import com.qj.sys.redislua.CompareAndSetLua;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;import javax.annotation.Resource;/*** Redis Lua腳本測試類* 用于測試基于Lua腳本實現的原子性比較并設置(CAS)操作* 演示如何使用Lua腳本保證Redis操作的原子性*/
@SpringBootTest(classes = {SysApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Spring Boot測試注解
@RunWith(SpringRunner.class) // 使用SpringRunner運行測試
@Slf4j // Lombok日志注解
public class RedisLuaTest {// 注入Redis操作模板@Resourceprivate RedisTemplate redisTemplate;// 注入自定義的Lua腳本執行組件@Resourceprivate CompareAndSetLua compareAndSetLua;/*** Redis Lua腳本測試方法* 演示如何使用Lua腳本實現原子性的比較并設置操作* 先設置一個初始值,然后使用Lua腳本進行原子性的比較和更新*/@Testpublic void redisLuaTest() {// 獲取字符串值操作接口ValueOperations<String, Long> opsForValue = redisTemplate.opsForValue();// 在Redis中設置一個鍵值對,鍵為"qj",值為18opsForValue.set("qj", 18L);// 記錄當前的值log.info("qj的值為:{}", opsForValue.get("qj"));// 使用Lua腳本執行比較并設置操作// 參數說明:key, 期望的舊值, 要設置的新值// 只有當當前值等于期望的舊值時,才會設置新值boolean result = compareAndSetLua.compareAndSet("qj", 18L, 19L);// 根據操作結果輸出相應信息if (result) {log.info("修改成功!qj的值為:{}", opsForValue.get("qj"));} else {log.info("修改失敗,當前值已不是期望的舊值");}}
}

運行效果:

六、Spring注解緩存實現

說明1:因為查詢的時候,每次都走數據庫會導致查詢非常緩慢,所以Spring提供了一套緩存機制,在查詢相同接口的時候會先查詢緩存,再查詢數據庫,大大提高了接口響應速度!

說明2:Spring Boot會自動配置合適的CacheManager作為相關緩存的提供程序(此處配置了Redis的CacheManager)當你在配置類(@Configuration)上使用@EnableCaching注解時,會觸發一個后處理器(post processor ),它檢查每個Spring bean,查看是否已經存在注解對應的緩存;如果找到了,就會自動創建一個代理攔截方法調用,使用緩存的bean執行處理。

注意:在實際工作中基本不使用Spring注解緩存,因為無法為每個緩存單獨設置過期時間(除非為每個緩存進行單獨的配置),很可能導致整個業務產生緩存雪崩現象的出現!

1. 開啟緩存

說明:需要在啟動類上加上@EnableCaching注解!

2. 加上@Cacheable和@CacheEvict注解

說明:在業務接口上加上@Cacheable注解,并且為了保證數據一致性,需要配合@CacheEvict注解一起使用,用于在增刪改的時候進行對緩存數據一致性的保障!

/*** 通過主鍵查詢單條數據* 使用@Cacheable注解實現緩存功能,提高查詢性能* 當多次查詢相同id的數據時,只有第一次會真正調用方法,后續直接從緩存中返回結果* * @param id 用戶主鍵ID* @return 包含用戶數據的Result對象* * @Cacheable 注解說明:* - cacheNames: 指定緩存名稱,用于區分不同的緩存區域* - key: 使用SpEL表達式生成緩存鍵,格式為'querySysUserById'+用戶ID*   例如:當id=123時,緩存鍵為"querySysUserById123"*/
@GetMapping("/get/{id}")
@Cacheable(cacheNames = "SysUser", key = "'querySysUserById'+#id")
public Result<SysUserPo> queryById(@PathVariable("id") Long id) {// 調用服務層方法查詢用戶數據// 只有在緩存不存在時,才會執行此方法return Result.ok(this.sysUserService.queryById(id));
}/*** 編輯用戶數據* 使用@CacheEvict注解清除緩存,確保數據更新后緩存的一致性* 當用戶數據更新后,清除對應的緩存,迫使下次查詢時重新從數據庫加載最新數據* * @param sysUserReq 用戶請求數據* @return 包含更新后用戶數據的Result對象* * @CacheEvict 注解說明:* - cacheNames: 指定要清除的緩存名稱,與@Cacheable中的cacheNames對應* - key: 使用SpEL表達式生成要清除的緩存鍵,格式為'querySysUserById'+用戶ID*   注意:這里假設sysUserReq中包含id字段,否則需要調整SpEL表達式*/
@PutMapping("/edit")
@CacheEvict(cacheNames = "SysUser", key = "'querySysUserById'+#id")
public Result<SysUserPo> edit(@RequestBody SysUserReq sysUserReq) {// 將請求對象轉換為數據傳輸對象SysUserDto sysUserDto = SysUserDtoConvert.INSTANCE.convertReqToDto(sysUserReq);// 調用服務層方法更新用戶數據// 方法執行成功后,會自動清除指定的緩存return Result.ok(this.sysUserService.update(sysUserDto));
}

3. 錯誤測試1:Redis亂碼和不過期問題

說明:可以看到存入的緩存數據是亂碼,并且TTL時間為-1永不過期!

4. 解決方案:修改RedisCacheManager

說明:在Redis自動配置中,修改注入Bean容器的RedisCacheManager,修改其創建方式即可!

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;/*** Redis配置類* 配置RedisTemplate和RedisCacheManager,自定義序列化方式和緩存策略*/
@Configuration // 聲明為配置類,Spring啟動時會自動加載
public class RedisConfig {/*** 配置RedisTemplate* 設置鍵和值的序列化方式,以及連接工廠* * @param redisConnectionFactory Redis連接工廠,由Spring自動注入* @return 配置好的RedisTemplate實例*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 創建RedisTemplate實例RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();// 創建字符串序列化器,用于鍵的序列化RedisSerializer<String> redisSerializer = new StringRedisSerializer();// 設置連接工廠redisTemplate.setConnectionFactory(redisConnectionFactory);// 設置鍵的序列化方式為字符串序列化redisTemplate.setKeySerializer(redisSerializer);// 設置哈希鍵的序列化方式為字符串序列化redisTemplate.setHashKeySerializer(redisSerializer);// 設置值的序列化方式為Jackson JSON序列化redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());// 設置哈希值的序列化方式為Jackson JSON序列化redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());return redisTemplate;}/*** 配置Redis緩存管理器* 設置緩存值的序列化方式和默認過期時間* * @param redisConnectionFactory Redis連接工廠,由Spring自動注入* @return 配置好的RedisCacheManager實例*/@Beanpublic RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {// 創建非阻塞的Redis緩存寫入器RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);// 創建序列化對,使用Jackson JSON序列化器SerializationPair<Object> pair = SerializationPair.fromSerializer(jackson2JsonRedisSerializer());// 配置Redis緩存默認設置RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() // 獲取默認配置.serializeValuesWith(pair) // 設置值的序列化方式.entryTtl(Duration.ofSeconds(10)); // 設置緩存條目默認過期時間為10秒// 創建并返回Redis緩存管理器return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);}/*** 創建Jackson JSON序列化器* 用于Redis值的序列化和反序列化* * @return 配置好的Jackson2JsonRedisSerializer實例*/private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {// 創建Jackson JSON序列化器,支持任何Object類型Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);// 創建ObjectMapper實例,配置JSON序列化和反序列化規則ObjectMapper objectMapper = new ObjectMapper();// 設置所有屬性(包括私有屬性)都可見objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 配置反序列化時忽略未知屬性,避免因JSON中包含未知屬性而報錯objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);// 將配置好的ObjectMapper設置到序列化器中jsonRedisSerializer.setObjectMapper(objectMapper);return jsonRedisSerializer;}
}

5. 錯誤測試2:獲取緩存失敗

說明:可以看到數據亂碼問題解決,并且也實現了10s過期!

問題:但是第一次請求成功的情況下,第二次請求就會發生如下錯誤,意味著從Redis中獲取的數據無法正確的進行類型轉換!

6. 解決方案:修改Jackson序列化配置

注意:可以看到Redis緩存的數據中帶上了類型!

7. 測試成功

說明:多次操作都能成功從緩存中獲取數據,接口響應速度大幅度提高!

七、guava實現本地緩存工具

封裝CacheUtil(Guava實現)

說明:利用guava本地緩存和函數式編程來實現一個本地緩存。

注意:因為之前緩存使用Redis來做,如果當所有的緩存都存儲在Redis中的時候,一旦網絡不穩定導致未及時相應,所有請求都可能被阻塞,導致內存和CPU被打滿,從而引起重大問題!

1. 配置相關依賴

<guava.version>19.0</guava.version>
<fastjson.version>1.2.75</fastjson.version><!-- guava相關依賴 -->
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>${guava.version}</version>
</dependency>
<!-- fastjson相關依賴 -->
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>${fastjson.version}</version>
</dependency>

2. CacheUtil

說明:此處只有查詢時添加緩存的操作,沒有實現編輯時刪除緩存的操作!

package com.qj.redis.util;import com.alibaba.fastjson.JSON;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;@Component
@Slf4j
public class CacheUtil<K, V> {// 意圖從nacos的配置文件中獲取,流量大的時候開啟本地緩存,流量小的時候關閉本地緩存@Value("${guava.cache.switch}")public Boolean switchCache;// 初始化一個guava的Cacheprivate Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(5000).expireAfterWrite(3, TimeUnit.SECONDS).build();public Map<K, V> getResult(List<K> skuIdList, String cachePrefix,Class<V> clazz, Function<List<K>, Map<K, V>> function) {if (CollectionUtils.isEmpty(skuIdList)) {return Collections.emptyMap();}Map<K, V> resultMap = new HashMap<>(16);// 1)本地緩存未開if (!switchCache) {// 從rpc接口查所有數據,返回結果集resultMap = function.apply(skuIdList);return resultMap;}// 2)默認開啟本地緩存List<K> noCacheList = new ArrayList<>();// (2.1)查guava緩存for (K skuId : skuIdList) {String cacheKey = cachePrefix + "_" + skuId;String content = localCache.getIfPresent(cacheKey);if (StringUtils.isNotBlank(content)) {// 能查到的直接放進結果集中V v = JSON.parseObject(content, clazz);resultMap.put(skuId, v);} else {// 查不到的先放進noCacheList中,后面統一使用rpc查詢noCacheList.add(skuId);}}// (2.2)如果沒有查不到的,直接返回結果集if (CollectionUtils.isEmpty(noCacheList)) {return resultMap;}// (2.3)如果有查不到的,從rpc接口查guava中沒有緩存的數據Map<K, V> noCacheResultMap = function.apply(noCacheList);// (2.4)如果rpc接口也沒查到任何數據,直接返回結果集if (CollectionUtils.isEmpty(noCacheResultMap)) {return resultMap;}// (2.5)將從rpc中查出的結果,添加guava的本地緩存和結果集中for (Entry<K, V> entry : noCacheResultMap.entrySet()) {K skuId = entry.getKey();V content = entry.getValue();// 查詢內容放進結果集resultMap.put(skuId, content);// 查詢內容放進guava本地緩存String cacheKey = cachePrefix + "_" + skuId;localCache.put(cacheKey, JSON.toJSONString(content));}// (2.6)返回結果集return resultMap;}
}

3. 調用CacheUtil

說明:此處使用了偽代碼來實現,此處重點是 —— 將無法在緩存中查詢的數據,轉而需要通過使用RPC框架或者調用本地Service層查詢數據庫的代碼,使用函數式編程方式,將其調用作為一個參數傳入CacheUtil的方法中!

package com.qj.sys.controller;import com.qj.redis.util.CacheUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Collections;
import java.util.List;
import java.util.Map;/*** 測試控制器* 用于演示本地緩存的使用方式和效果*/
@RestController // 聲明為REST控制器,所有方法返回值直接作為HTTP響應體
@Slf4j // Lombok日志注解
@RequestMapping("test") // 設置基礎請求路徑為/test
public class TestController {// 注入緩存工具類@Autowiredprivate CacheUtil cacheUtil;/*** 測試本地緩存接口* 演示如何使用CacheUtil獲取多種類型的數據并利用緩存提高性能* * @param skuIdList SKU ID列表,通過請求參數傳入*/@GetMapping("/testLocalCache")public void testLocalCache(List<Long> skuIdList) {// 獲取SKU名稱信息,使用緩存優化// 參數說明:// 1. skuIdList: 需要查詢的SKU ID列表// 2. "skuInfo.skuName": 緩存鍵前綴,用于區分不同類型的緩存// 3. SkuInfo.class: 返回值類型// 4. Lambda表達式: 緩存未命中時的數據加載邏輯cacheUtil.getResult(skuIdList, "skuInfo.skuName", SkuInfo.class, (list) -> {// 當緩存中不存在數據時,執行此方法獲取數據Map<Long, SkuInfo> skuInfo = getSkuInfo(skuIdList);return skuInfo;});// 獲取SKU價格信息,同樣使用緩存優化cacheUtil.getResult(skuIdList, "skuInfo.skuPrice", SkuPrice.class, (list) -> {// 當緩存中不存在數據時,執行此方法獲取數據Map<Long, SkuPrice> skuPrice = getSkuPrice(skuIdList);return skuPrice;});}/*** 模擬RPC接口 - 獲取SKU信息* 實際項目中可以使用OpenFeign等工具實現遠程調用* * @param skuIdList SKU ID列表* @return SKU信息映射表*/public Map<Long, SkuInfo> getSkuInfo(List<Long> skuIdList) {// 模擬遠程調用,返回空映射// 實際項目中這里會調用商品服務的接口獲取SKU信息return Collections.emptyMap();}/*** 模擬RPC接口 - 獲取SKU價格* 實際項目中可以使用OpenFeign等工具實現遠程調用* * @param skuIdList SKU ID列表* @return SKU價格映射表*/public Map<Long, SkuPrice> getSkuPrice(List<Long> skuIdList) {// 模擬遠程調用,返回空映射// 實際項目中這里會調用價格服務的接口獲取SKU價格return Collections.emptyMap();}/*** SKU信息內部類* 用于表示商品基本信息*/class SkuInfo {private Long id; // SKU IDprivate String name; // SKU名稱}/*** SKU價格內部類* 用于表示商品價格信息*/class SkuPrice {private Long id; // SKU IDprivate Double price; // SKU價格}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/94957.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/94957.shtml
英文地址,請注明出處:http://en.pswp.cn/web/94957.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

光伏發多少電才夠用?匹配家庭用電需求

在“雙碳”目標推動下&#xff0c;新能源產業迎來爆發式增長&#xff0c;家庭屋頂光伏憑借清潔環保、能降低電費的優勢&#xff0c;成為越來越多家庭的選擇。但很多家庭在安裝前都會陷入一個核心困惑&#xff1a;到底裝多大容量的光伏系統&#xff0c;發多少電才能剛好滿足自家…

如何管理跨境電商多語種素材?數字資產本地化指南

核心要點&#xff1a; 問題&#xff1a; 多語言內容管理真的那么難嗎&#xff1f;多語種內容素材雜亂、反復翻譯浪費預算、上線延遲影響市場窗口期&#xff0c;跨境電商如何高效管理全球素材&#xff1f; 答案&#xff1a; 借助 AI 驅動的數字資產管理系統&#xff0c;跨境品…

Git 8 ,git 分支開發( 切換分支開發,并設置遠程倉庫默認分支 )

目錄 前言 一、&#x1f4cd;環境背景 二、&#x1f4bb; 完整流程 三、&#x1f4dd; 順序總覽 四、&#x1f539;關系圖例 五、?暫存警告 六、?? 默認分支 七、&#x1f7e3;更多操作 前言 在團隊開發或多人協作的項目中&#xff0c;Git 是最常用的版本管理工具。一個常見…

如何在mysql中執行創建數據庫的腳本文件?

1、先準備好腳本文件&#xff0c;.sql擴展名的把腳本文件放在某個盤的根目錄&#xff08;也可以不是根目錄&#xff0c;根目錄的話路徑會簡單一些&#xff09;,這里我放在C盤的根目錄下。腳本文件內容如下&#xff1a;/* SQLyog Community v13.1.1 (32 bit) MySQL - 5.7.26 : D…

《AI智脈速遞》2025 年 8 月22 日 - 29 日

歐盟 AI 法案正式生效&#xff1a;禁止社會評分&#xff0c;規范生成式 AI 內容標注 8 月 21 日&#xff0c;歐盟《人工智能法案》全面實施&#xff0c;明確禁止社會評分、實時面部識別等高風險 AI 應用&#xff0c;要求生成式 AI 內容必須標注來源。該法案被視為全球最嚴格的 …

iOS 審核 4.3a【二進制加固】

我們應該知道,面對iOS 上架 遇到4.3a的問題或者制作馬甲包.最基礎的操作就是混淆代碼尤其是我們專業做上架的,需要對各種語言的編譯模式,產物,以及ipa構成都需要非常了解, 每種語言開發的App的編譯產物不同,針對不同的編譯產物做不同的處理方式有一些經驗的開發者, 應該知道 目…

使用Python腳本執行Git命令

說明&#xff1a;本文介紹如何使用Python腳本在某個目錄下執行Git命令 編碼 直接上代碼 import os import subprocessdef open_git_bash_and_run_command(folder_path, git_command):# 檢查文件夾路徑是否存在if not os.path.exists(folder_path):print(f"錯誤&#xff1a…

2025docker快速部署Nginx UI可視化管理平臺

1、nginx-ui簡介 Nginx UI 是一個開源項目&#xff0c;旨在為著名的 Web 服務器和反向代理軟件 Nginx 提供一個基于網頁的圖形化用戶界面&#xff08;GUI&#xff09;。它的核心目標是讓 Nginx 的配置和管理變得可視化、簡單化和自動化&#xff0c;從而降低其使用門檻&#xf…

數據防泄與最小可見:ABP 統一封裝行級安全(RLS)+ 列級脫敏

數據防泄與最小可見&#xff1a;ABP 統一封裝行級安全&#xff08;RLS&#xff09; 列級脫敏 TL;DR&#xff1a;把“誰能看到哪些行、字段可見到哪一位”下沉到數據庫強制層&#xff08;PostgreSQL&#xff1a;RLS 安全視圖&#xff1b;SQL Server&#xff1a;RLS DDM&#x…

網絡編程 04:TCP連接,客戶端與服務器的區別,實現 TCP 聊天及文件上傳,Tomcat 的簡單使用

一、概述 記錄時間 [2025-08-29] 前置文章&#xff1a; 網絡編程 01&#xff1a;計算機網絡概述&#xff0c;網絡的作用&#xff0c;網絡通信的要素&#xff0c;以及網絡通信協議與分層模型 網絡編程 02&#xff1a;IP 地址&#xff0c;IP 地址的作用、分類&#xff0c;通過 …

最小生成樹——Kruskal

標題什么是生成樹&#xff1f; 對于一張無向圖&#xff0c;由nnn個頂點和n?1n-1n?1條邊構成地聯通子圖&#xff0c;叫做這個無向圖 生成樹 最小生成樹就是指邊權之和最小的生成樹 如何求最小生成樹&#xff1f; Kruskal 介紹: 存圖時只存每條邊地起點、終點&#xff0c;…

ADFS 和 OAuth 的區別

ADFS 和 OAuth 的區別 ADFS(Active Directory Federation Services)和 OAuth 都是身份認證與授權領域的技術,但它們的設計目標、應用場景和實現方式有顯著區別。以下從核心定義、技術特性、應用場景等方面詳細對比: 核心定義與設計目標 技術 核心定義 設計目標 ADFS 微軟…

神經網絡參數量計算詳解

1. 神經網絡參數量計算基本原理 1.1 什么是神經網絡參數 神經網絡的參數主要包括&#xff1a; 權重&#xff08;Weights&#xff09;&#xff1a;連接不同神經元之間的權重矩陣偏置&#xff08;Bias&#xff09;&#xff1a;每個神經元的偏置項批歸一化參數&#xff1a;BatchNo…

手寫鏈路追蹤

1. 什么是鏈路追蹤 鏈路追蹤是指在分布式系統中&#xff0c;將一次請求的處理過程進行記錄并聚合展示的一種方法。目的是將一次分布式請求的調用情況集中在一處展示&#xff0c;如各個服務節點上的耗時、請求具體到達哪臺機器上、每個服務節點的請求狀態等。這樣就可以輕松了解…

從零開始的python學習——常量與變量

? ? ? ? ? づ?ど &#x1f389; 歡迎點贊支持&#x1f389; 個人主頁&#xff1a;勵志不掉頭發的內向程序員&#xff1b; 專欄主頁&#xff1a;python學習專欄&#xff1b; 文章目錄 前言 一、常量和表達式 二、變量類型 2.1、什么是變量 2.2、變量語法 &#xff08;1&a…

基于51單片機環境監測設計 光照 PM2.5粉塵 溫濕度 2.4G無線通信

1 系統功能介紹 本設計是一套 基于51單片機的環境監測系統&#xff0c;能夠實時采集環境光照、PM2.5、溫濕度等參數&#xff0c;并通過 2.4G無線模塊 NRF24L01 實現數據傳輸。系統具備本地顯示與報警功能&#xff0c;可通過按鍵設置各類閾值和時間&#xff0c;方便用戶進行環境…

【Flask】測試平臺開發,產品管理實現添加功能-第五篇

概述在前面的幾篇開發文章中&#xff0c;我們只是讓數據在界面上進行了展示&#xff0c;但是沒有添加按鈕的功能&#xff0c;接下來我們需要開發一個添加的按鈕&#xff0c;用戶產品功能的創建和添加抽公共數據鏈接方法添加接口掌握post實現和請求數據處理前端掌握Button\Dilog…

循環高級(2)

6.練習3 打印九九乘法表7.練習3 制表符詳解對齊不了原因&#xff1a;name補到8zhangsan本身就是8&#xff0c;補完就變成16解決辦法&#xff1a;1.去掉zhangsan\t,這樣前后都是82.name后面加2個\t加一個\t&#xff0c;name\t就是占8個&#xff0c;再加一個\t&#xff0c;就變成…

盒馬生鮮 小程序 逆向分析

聲明 本文章中所有內容僅供學習交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包內容、敏感網址、數據接口等均已做脫敏處理&#xff0c;嚴禁用于商業用途和非法用途&#xff0c;否則由此產生的一切后果均與作者無關&#xff01; 逆向分析 部分python代碼 params {&…

【Linux系統】線程控制

1. POSIX線程庫 (pthreads)POSIX線程&#xff08;通常稱為pthreads&#xff09;是IEEE制定的操作系統線程API標準。Linux系統通過glibc庫實現了這個標準&#xff0c;提供了創建和管理線程的一系列函數。核心特性命名約定&#xff1a;絕大多數函數都以 pthread_ 開頭&#xff0c…