SpringBoot?Cache?二級(jí)緩存的使用
二級(jí)緩存介紹
二級(jí)緩存分為本地緩存和遠(yuǎn)程緩存,也可稱為內(nèi)存緩存和網(wǎng)絡(luò)緩存
常見(jiàn)的流行緩存框架
- 本地緩存:Caffeine,Guava Cache
- 遠(yuǎn)程緩存:Redis,MemCache
二級(jí)緩存的訪問(wèn)流程

二級(jí)緩存的優(yōu)勢(shì)與問(wèn)題
- 優(yōu)勢(shì):二級(jí)緩存優(yōu)先使用本地緩存,訪問(wèn)數(shù)據(jù)非??欤行p少和遠(yuǎn)程緩存之間的數(shù)據(jù)交換,節(jié)約網(wǎng)絡(luò)開(kāi)銷(xiāo)
- 問(wèn)題:分布式環(huán)境下本地緩存存在一致性問(wèn)題,本地緩存變更后需要通知其他節(jié)點(diǎn)刷新本地緩存,這對(duì)一致性要求高的場(chǎng)景可能不能很好的適應(yīng)
SpringBoot Cache 組件
SpringBoot Cache 組件提供了一套緩存管理的接口以及聲明式使用的緩存的注解
引入 SpringBoot Cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>如何集成第三方緩存框架到 Cache 組件
實(shí)現(xiàn) Cache 接口,適配第三方緩存框架的操作,實(shí)現(xiàn) CacheManager 接口,提供緩存管理器的 Bean
SpringBoot Cache 默認(rèn)提供了 Caffeine、ehcache 等常見(jiàn)緩存框架的管理器,引入相關(guān)依賴后即可使用
引入 Caffeine
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>SpringBoot Redis 提供了 Redis 緩存的實(shí)現(xiàn)及管理器
引入 Redis 緩存、RedisTemplate
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>SpringBoot Cache 聲明式緩存注解
@Cacheable:執(zhí)行方法前,先從緩存中獲取,沒(méi)有獲取到才執(zhí)行方法,并將其結(jié)果更新到緩存
@CachePut:執(zhí)行方法后,將其結(jié)果更新到緩存
@CacheEvict:執(zhí)行方法后,清除緩存
@Caching:組合前三個(gè)注解
@Cacheable 注解的常用屬性:
- cacheNames/value:緩存名稱
- key:緩存數(shù)據(jù)的 key,默認(rèn)使用方法參數(shù)值,支持 SpEL
- keyGenerator:指定 key 的生成器,和 key 屬性二選一
- cacheManager:指定使用的緩存管理器。
- condition:在方法執(zhí)行開(kāi)始前檢查,在符合 condition 時(shí),進(jìn)行緩存操作
- unless:在方法執(zhí)行完成后檢查,在符合 unless 時(shí),不進(jìn)行緩存操作
- sync:是否使用同步模式,同步模式下,多個(gè)線程同時(shí)未命中一個(gè) key 的數(shù)據(jù),將阻塞競(jìng)爭(zhēng)執(zhí)行方法
SpEL 支持的表達(dá)式

本地緩存 Caffeine
Caffeine 介紹
Caffeine 是繼 Guava Cache 之后,在 SpringBoot 2.x 中默認(rèn)集成的緩存框架
Caffeine 使用了 Window TinyLFU 淘汰策略,緩存命中率極佳,被稱為現(xiàn)代高性能緩存庫(kù)之王
創(chuàng)建一個(gè) Caffeine Cache
Cache<String, Object> cache = Caffeine.newBuilder().build();
Caffeine 內(nèi)存淘汰策略
- FIFO:先進(jìn)先出,命中率低
- LRU:最近最久未使用,不能應(yīng)對(duì)冷門(mén)突發(fā)流量,會(huì)導(dǎo)致熱點(diǎn)數(shù)據(jù)被淘汰
- LFU:最近最少使用,需要維護(hù)使用頻率,占用內(nèi)存空間,
- W-TinyLFU:LFU 的變種,綜合了 LRU LFU 的長(zhǎng)處,高命中率,低內(nèi)存占用
Caffeine 緩存失效策略
1.基于容量大小
根據(jù)最大容量
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10000)
.build();根據(jù)權(quán)重
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumWeight(10000)
.weigher((Weigher<String, Object>) (s, o) -> {
// 根據(jù)不同對(duì)象計(jì)算權(quán)重
return 0;
})
.build();2.基于引用類(lèi)型
基于弱引用,當(dāng)不存在強(qiáng)引用時(shí)淘汰
Cache<String, Object> cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build();基于軟引用,當(dāng)不存在強(qiáng)引用且內(nèi)存不足時(shí)淘汰
Cache<String, Object> cache = Caffeine.newBuilder()
.softValues()
.build();3.基于過(guò)期時(shí)間
expireAfterWrite,寫(xiě)入后一定時(shí)間后過(guò)期
Cache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();expireAfterAccess(long, TimeUnit),訪問(wèn)后一定時(shí)間后過(guò)期,一直訪問(wèn)則一直不過(guò)期
expireAfter(Expiry),自定義時(shí)間的計(jì)算方式
Caffeine 線程池
- Caffeine 默認(rèn)使用 ForkJoinPool.commonPool()
- Caffeine 線程池可通過(guò) executor 方法設(shè)置
Caffeine 指標(biāo)統(tǒng)計(jì)
- Caffeine 通過(guò)配置 recordStats 方法開(kāi)啟指標(biāo)統(tǒng)計(jì),通過(guò)緩存的 stats 方法獲取信息
- Caffeine 指標(biāo)統(tǒng)計(jì)的內(nèi)容有:命中率,加載數(shù)據(jù)耗時(shí),緩存數(shù)量相關(guān)等Caffeine Cache 的種類(lèi)
1.普通 Cache
Cache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
// 存入
cache.put("key1", "123");
// 取出
Object key1Obj = cache.getIfPresent("key1");
// 清除
cache.invalidate("key1");
// 清除全部
cache.invalidateAll();2.異步 Cache
響應(yīng)結(jié)果通過(guò) CompletableFuture 包裝,利用線程池異步執(zhí)行
AsyncCache<String, Object> asyncCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.buildAsync();
// 存入
asyncCache.put("key1", CompletableFuture.supplyAsync(() -> "123"));
// 取出
CompletableFuture<Object> key1Future = asyncCache.getIfPresent("key1");
try {
Object key1Obj = key1Future.get();
} catch (InterruptedException | ExecutionException e) {
//
}
// 清除
asyncCache.synchronous().invalidate("key1");
// 清除全部
asyncCache.synchronous().invalidateAll();3.Loading Cache
和普通緩存使用方式一致
在緩存未命中時(shí),自動(dòng)加載數(shù)據(jù)到緩存,需要設(shè)置加載數(shù)據(jù)的回調(diào),比如從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build(key -> {
// 獲取業(yè)務(wù)數(shù)據(jù)
return "Data From DB";
});4.異步 Loading Cache
和異步緩存使用方式一致
在緩存未命中時(shí),自動(dòng)加載數(shù)據(jù)到緩存,與 Loading Cache 不同的是,加載數(shù)據(jù)是異步的
// 使用 AsyncCache 的線程池異步加載
AsyncLoadingCache<String, Object> asyncCache0 = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.buildAsync(key -> {
// 獲取業(yè)務(wù)數(shù)據(jù)
return "Data From DB";
});
// 指定加載使用的線程池
AsyncLoadingCache<String, Object> asyncCache1 = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> {
// 異步獲取業(yè)務(wù)數(shù)據(jù)
return "Data From DB";
}, otherExecutor));注意:AsyncLoadingCache 不支持弱引用和軟引用相關(guān)淘汰策略
Caffeine 自動(dòng)刷新機(jī)制
Caffeine 可通過(guò) refreshAfterWrite 設(shè)置定時(shí)刷新
必須是指定了 CacheLoader 的緩存,即 LoadingCache 和 AsyncLoadingCache
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.refreshAfterWrite(3, TimeUnit.SECONDS)
.build(key -> {
// 獲取業(yè)務(wù)數(shù)據(jù)
return "Data From DB";
});refreshAfterWrite 是一種定時(shí)刷新,key 過(guò)期時(shí)并不一定會(huì)立即刷新
實(shí)現(xiàn)二級(jí)緩存
配置類(lèi) DLCacheProperties
@Data
@ConfigurationProperties(prefix = "uni-boot.cache.dl")
public class DLCacheProperties {
/**
* 是否存儲(chǔ) null 值
*/
private boolean allowNullValues = true;
/**
* 過(guò)期時(shí)間,為 0 表示不過(guò)期,默認(rèn) 30 分鐘
* 單位:毫秒
*/
private long defaultExpiration = 30 * 60 * 1000;
/**
* 針對(duì) cacheName 設(shè)置過(guò)期時(shí)間,為 0 表示不過(guò)期
* 單位:毫秒
*/
private Map<String, Long> cacheExpirationMap;
/**
* 本地緩存 caffeine 配置
*/
private LocalConfig local = new LocalConfig();
/**
* 遠(yuǎn)程緩存 redis 配置
*/
private RemoteConfig remote = new RemoteConfig();
@Data
public static class LocalConfig {
/**
* 初始化大小,為 0 表示默認(rèn)
*/
private int initialCapacity;
/**
* 最大緩存?zhèn)€數(shù),為 0 表示默認(rèn)
* 默認(rèn)最多 5 萬(wàn)條
*/
private long maximumSize = 10000L;
}
@Data
public static class RemoteConfig {
/**
* Redis pub/sub 緩存刷新通知主題
*/
private String syncTopic = "cache:dl:refresh:topic";
}
}緩存實(shí)現(xiàn) DLCache
本地緩存基于 Caffeine,遠(yuǎn)程緩存使用 Redis
實(shí)現(xiàn) SpringBoot Cache 的抽象類(lèi),AbstractValueAdaptingCache
@Slf4j
@Getter
public class DLCache extends AbstractValueAdaptingCache {
private final String name;
private final long expiration;
private final DLCacheProperties cacheProperties;
private final Cache<String, Object> caffeineCache;
private final RedisTemplate<String, Object> redisTemplate;
public DLCache(String name, long expiration, DLCacheProperties cacheProperties,
Cache<String, Object> caffeineCache, RedisTemplate<String, Object> redisTemplate) {
super(cacheProperties.isAllowNullValues());
this.name = name;
this.expiration = expiration;
this.cacheProperties = cacheProperties;
this.caffeineCache = caffeineCache;
this.redisTemplate = redisTemplate;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
protected Object lookup(Object key) {
String redisKey = getRedisKey(key);
Object val;
val = caffeineCache.getIfPresent(key);
// val 是 toStoreValue 包裝過(guò)的值,為 null 則 key 不存在
// 因?yàn)榇鎯?chǔ)的 null 值被包裝成了 DLCacheNullVal.INSTANCE
if (ObjectUtil.isNotNull(val)) {
log.debug("DLCache local get cache, key:{}, value:{}", key, val);
return val;
}
val = redisTemplate.opsForValue().get(redisKey);
if (ObjectUtil.isNotNull(val)) {
log.debug("DLCache remote get cache, key:{}, value:{}", key, val);
caffeineCache.put(key.toString(), val);
return val;
}
return val;
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
T val;
val = (T) lookup(key);
if (ObjectUtil.isNotNull(val)) {
return val;
}
// 雙檢鎖
synchronized (key.toString().intern()) {
val = (T) lookup(key);
if (ObjectUtil.isNotNull(val)) {
return val;
}
try {
// 攔截的業(yè)務(wù)方法
val = valueLoader.call();
// 加入緩存
put(key, val);
} catch (Exception e) {
throw new DLCacheException("DLCache valueLoader fail", e);
}
return val;
}
}
@Override
public void put(Object key, Object value) {
putRemote(key, value);
sendSyncMsg(key);
putLocal(key, value);
}
@Override
public void evict(Object key) {
// 先清理 redis 再清理 caffeine
clearRemote(key);
sendSyncMsg(key);
clearLocal(key);
}
@Override
public void clear() {
// 先清理 redis 再清理 caffeine
clearRemote(null);
sendSyncMsg(null);
clearLocal(null);
}
private void sendSyncMsg(Object key) {
String syncTopic = cacheProperties.getRemote().getSyncTopic();
DLCacheRefreshMsg refreshMsg = new DLCacheRefreshMsg(name, key);
// 加入 SELF_MSG_MAP 防止自身節(jié)點(diǎn)重復(fù)處理
DLCacheRefreshListener.SELF_MSG_MAP.add(refreshMsg);
redisTemplate.convertAndSend(syncTopic, refreshMsg);
}
private void putLocal(Object key, Object value) {
// toStoreValue 包裝 null 值
caffeineCache.put(key.toString(), toStoreValue(value));
}
private void putRemote(Object key, Object value) {
if (expiration > 0) {
// toStoreValue 包裝 null 值
redisTemplate.opsForValue().set(getRedisKey(key), toStoreValue(value), expiration, TimeUnit.MILLISECONDS);
return;
}
redisTemplate.opsForValue().set(getRedisKey(key), toStoreValue(value));
}
public void clearRemote(Object key) {
if (ObjectUtil.isNull(key)) {
Set<String> keys = redisTemplate.keys(getRedisKey("*"));
if (ObjectUtil.isNotEmpty(keys)) {
keys.forEach(redisTemplate::delete);
}
return;
}
redisTemplate.delete(getRedisKey(key));
}
public void clearLocal(Object key) {
if (ObjectUtil.isNull(key)) {
caffeineCache.invalidateAll();
return;
}
caffeineCache.invalidate(key);
}
/**
* 檢查是否允許緩存 null
*
* @param value 緩存值
* @return 不為空則 true,為空但允許則 false,否則異常
*/
private boolean checkValNotNull(Object value) {
if (ObjectUtil.isNotNull(value)) {
return true;
}
if (isAllowNullValues() && ObjectUtil.isNull(value)) {
return false;
}
// val 不能為空,但傳了空
throw new DLCacheException("Check null val is not allowed");
}
@Override
protected Object fromStoreValue(Object storeValue) {
if (isAllowNullValues() && DLCacheNullVal.INSTANCE.equals(storeValue)) {
return null;
}
return storeValue;
}
@Override
protected Object toStoreValue(Object userValue) {
if (!checkValNotNull(userValue)) {
return DLCacheNullVal.INSTANCE;
}
return userValue;
}
/**
* 獲取 redis 完整 key
*/
private String getRedisKey(Object key) {
// 雙冒號(hào),與 spring cache 默認(rèn)一致
return this.name.concat("::").concat(key.toString());
}
/**
* 在緩存時(shí)代替 null 值,以區(qū)分是 key 不存在還是 val 為 null
*/
@Data
public static class DLCacheNullVal {
public static final DLCacheNullVal INSTANCE = new DLCacheNullVal();
private String desc = "nullVal";
}
}注意:需要區(qū)分緩存 get 到 null 值和 key 不存在,因此使用了 DLCacheNullVal 來(lái)代替 null 值
緩存管理器 DLCacheManager
緩存管理器
實(shí)現(xiàn) SpringBoot Cache 的 CacheManager 接口
@Slf4j
@RequiredArgsConstructor
public class DLCacheManager implements CacheManager {
private final ConcurrentHashMap<String, DLCache> cacheMap = new ConcurrentHashMap<>();
private final DLCacheProperties cacheProperties;
private final RedisTemplate<String, Object> redisTemplate;
@Override
public DLCache getCache(String name) {
return cacheMap.computeIfAbsent(name, (o) -> {
DLCache dlCache = buildCache(o);
log.debug("Create DLCache instance, name:{}", o);
return dlCache;
});
}
private DLCache buildCache(String name) {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
// 設(shè)置過(guò)期時(shí)間 expireAfterWrite
long expiration = 0;
// 獲取針對(duì) cache name 設(shè)置的過(guò)期時(shí)間
Map<String, Long> cacheExpirationMap = cacheProperties.getCacheExpirationMap();
if (ObjectUtil.isNotEmpty(cacheExpirationMap) && cacheExpirationMap.get(name) > 0) {
expiration = cacheExpirationMap.get(name);
} else if (cacheProperties.getDefaultExpiration() > 0) {
expiration = cacheProperties.getDefaultExpiration();
}
if (expiration > 0) {
caffeine.expireAfterWrite(expiration, TimeUnit.MILLISECONDS);
}
// 設(shè)置參數(shù)
LocalConfig localConfig = cacheProperties.getLocal();
if (ObjectUtil.isNotNull(localConfig.getInitialCapacity()) && localConfig.getInitialCapacity() > 0) {
caffeine.initialCapacity(localConfig.getInitialCapacity());
}
if (ObjectUtil.isNotNull(localConfig.getMaximumSize()) && localConfig.getMaximumSize() > 0) {
caffeine.maximumSize(localConfig.getMaximumSize());
}
return new DLCache(name, expiration, cacheProperties, caffeine.build(), redisTemplate);
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(cacheMap.keySet());
}
}緩存刷新監(jiān)聽(tīng)器
緩存消息
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DLCacheRefreshMsg {
private String cacheName;
private Object key;
}緩存刷新消息監(jiān)聽(tīng)
@Slf4j
@RequiredArgsConstructor
@Component
public class DLCacheRefreshListener implements MessageListener, InitializingBean {
public static final ConcurrentHashSet<DLCacheRefreshMsg> SELF_MSG_MAP = new ConcurrentHashSet<>();
private final DLCacheManager dlCacheManager;
private final DLCacheProperties cacheProperties;
private final RedisMessageListenerContainer listenerContainer;
@Override
public void onMessage(Message message, byte[] pattern) {
// 序列化出刷新消息
DLCacheRefreshMsg refreshMsg = (DLCacheRefreshMsg) RedisUtil.getTemplate().getValueSerializer().deserialize(message.getBody());
if (ObjectUtil.isNull(refreshMsg)) {
return;
}
// 判斷是不是自身節(jié)點(diǎn)發(fā)出
if (SELF_MSG_MAP.contains(refreshMsg)) {
SELF_MSG_MAP.remove(refreshMsg);
return;
}
log.debug("DLCache refresh local, cache name:{}, key:{}", refreshMsg.getCacheName(), refreshMsg.getKey());
// 清理本地緩存
dlCacheManager.getCache(refreshMsg.getCacheName()).clearLocal(refreshMsg.getKey());
}
@Override
public void afterPropertiesSet() {
// 注冊(cè)到 RedisMessageListenerContainer
listenerContainer.addMessageListener(this, new ChannelTopic(cacheProperties.getRemote().getSyncTopic()));
}
}使用二級(jí)緩存
注入 DLCacheManager
@Bean(name = "dlCacheManager")
public DLCacheManager dlCacheManager(DLCacheProperties cacheProperties, RedisTemplate<String, Object> redisTemplate) {
return new DLCacheManager(cacheProperties, redisTemplate);
}使用 @Cacheable 配合 DLCacheManager
@ApiOperation("測(cè)試 @Cacheable")
@Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_cacheable")
public String testCacheable() {
log.info("testCacheable 執(zhí)行");
return "Cacheable";
}
@ApiOperation("測(cè)試 @Cacheable null 值")
@Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_cacheable_null")
public String testCacheableNull() {
log.info("testCacheableNull 執(zhí)行");
return null;
}
@ApiOperation("測(cè)試 @CachePut")
@CachePut(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_put")
public String testPut() {
return "Put";
}
@ApiOperation("測(cè)試 @CacheEvict")
@CacheEvict(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_evict")
public String testEvict() {
return "Evict";
}到此這篇關(guān)于SpringBoot Cache 二級(jí)緩存的使用的文章就介紹到這了,更多相關(guān)SpringBoot Cache 二級(jí)緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot整合單機(jī)緩存ehcache的實(shí)現(xiàn)
- SpringBoot中整合Ehcache實(shí)現(xiàn)熱點(diǎn)數(shù)據(jù)緩存的詳細(xì)過(guò)程
- Spring中的@Cacheable緩存注解詳解
- 詳解如何使用SpringBoot的緩存@Cacheable
- Spring中緩存注解@Cache的使用詳解
- SpringCache緩存處理詳解
- 詳解Springboot @Cacheable 注解(指定緩存位置)
- Spring Cache @Cacheable 緩存在部分Service中不生效的解決辦法
- Springboot使用@Cacheable注解實(shí)現(xiàn)數(shù)據(jù)緩存
- SpringBoot使用Spring?Cache高效處理緩存數(shù)據(jù)
相關(guān)文章
Springboot如何使用mybatis實(shí)現(xiàn)攔截SQL分頁(yè)
這篇文章主要介紹了Springboot使用mybatis實(shí)現(xiàn)攔截SQL分頁(yè),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
SpringBoot+Thymeleaf靜態(tài)資源的映射規(guī)則說(shuō)明
這篇文章主要介紹了SpringBoot+Thymeleaf靜態(tài)資源的映射規(guī)則說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
java用靜態(tài)工廠代替構(gòu)造函數(shù)使用方法和優(yōu)缺點(diǎn)
這篇文章主要介紹了java用靜態(tài)工廠代替構(gòu)造函數(shù)使用方法和優(yōu)缺點(diǎn),需要的朋友可以參考下2014-02-02
mybatis-plus分頁(yè)查詢的實(shí)現(xiàn)實(shí)例
頁(yè)查詢是一項(xiàng)常用的數(shù)據(jù)庫(kù)查詢方法,本文主要介紹了mybatis-plus分頁(yè)查詢的實(shí)現(xiàn)實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-06-06
SpringBoot Application注解原理及代碼詳解
這篇文章主要介紹了SpringBoot Application注解原理及代碼詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
redis scan命令導(dǎo)致redis連接耗盡,線程上鎖的解決
這篇文章主要介紹了redis scan命令導(dǎo)致redis連接耗盡,線程上鎖的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11

