服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - Redis+Caffeine实现分布式二级缓存组件实战教程

Redis+Caffeine实现分布式二级缓存组件实战教程

2022-08-07 10:19上帝爱吃苹果- Redis

这篇文章主要介绍了Redis+Caffeine实现分布式二级缓存组件实战教程,介绍了分布式二级缓存的优势,使用组件的方法,通过示例代码给大家介绍的非常详细,需要的朋友可以参考下

前言

在生产中已有实践,本组件仅做个人学习交流分享使用。github:https://github.com/axinSoochow/redis-caffeine-cache-starter
个人水平有限,欢迎大家在评论区轻喷。

所谓二级缓存

缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。

平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。
但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了进程内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存。

  • 系统是否需要缓存CPU占用:如果你有某些应用需要消耗大量的cpu去计算获得结果。
  • 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。

分布式二级缓存的优势

Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。
已经有Redis了,干嘛还需要了解Guava,Caffeine这些进程缓存呢:

  • Redis如果不可用,这个时候我们只能访问数据库,很容易造成雪崩,但一般不会出现这种情况。
  • 访问Redis会有一定的网络I/O以及序列化反序列化开销,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。

所以如果仅仅是使用Redis,能满足我们大部分需求,但是当需要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。

二级缓存操作过程数据读流程描述

Redis+Caffeine实现分布式二级缓存组件实战教程

redis 与本地缓存都查询不到值的时候,会触发更新过程,整个过程是加锁的缓存失效流程描述

Redis+Caffeine实现分布式二级缓存组件实战教程

redis更新与删除缓存key都会触发,清除redis缓存后

如何使用组件?

组件是基于Spring Cache框架上改造的,在项目中使用分布式缓存,仅仅需要在缓存注解上增加:cacheManager ="L2_CacheManager",或者 cacheManager = CacheRedisCaffeineAutoConfiguration.分布式二级缓存

?
1
2
3
4
//这个方法会使用分布式二级缓存来提供查询
@Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager")
public Config getAllValidateConfig() {
}

如果你想既使用分布式缓存,又想用分布式二级缓存组件,那你需要向Spring注入一个 @Primary 的 CacheManager bean

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Primary
@Bean("deaultCacheManager")
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
    // 设置缓存的默认过期时间,也是使用Duration设置
    config = config.entryTtl(Duration.ofMinutes(2)).disableCachingNullValues();
 
    // 设置一个初始化的缓存空间set集合
    Set<String> cacheNames =  new HashSet<>();
    cacheNames.add(CacheNames.CACHE_15MINS);
    cacheNames.add(CacheNames.CACHE_30MINS);
 
    // 对每个缓存空间应用不同的配置
    Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
    configMap.put(CacheNames.CACHE_15MINS, config.entryTtl(Duration.ofMinutes(15)));
    configMap.put(CacheNames.CACHE_30MINS, config.entryTtl(Duration.ofMinutes(30)));
  
    // 使用自定义的缓存配置初始化一个cacheManager
    RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
        .initialCacheNames(cacheNames)  // 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
        .withInitialCacheConfigurations(configMap)
        .build();
    return cacheManager;
}

然后:

?
1
2
3
4
5
6
7
8
9
//这个方法会使用分布式二级缓存
@Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager")
public Config getAllValidateConfig() {
}
 
//这个方法会使用分布式缓存
@Cacheable(cacheNames = CacheNames.CACHE_12HOUR)
public Config getAllValidateConfig2() {
}

核心实现方法

核心其实就是实现 org.springframework.cache.CacheManager接口与继承org.springframework.cache.support.AbstractValueAdaptingCache,在Spring缓存框架下实现缓存的读与写。

RedisCaffeineCacheManager实现CacheManager 接口

RedisCaffeineCacheManager.class 主要来管理缓存实例,根据不同的 CacheNames 生成对应的缓存管理bean,然后放入一个map中。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package com.axin.idea.rediscaffeinecachestarter.support;
 
import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
 
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
 
@Slf4j
public class RedisCaffeineCacheManager implements CacheManager {
 
    private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);
 
    private static ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
 
    private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
 
    private RedisTemplate<Object, Object> stringKeyRedisTemplate;
 
    private boolean dynamic = true;
 
    private Set<String> cacheNames;
    {
        cacheNames = new HashSet<>();
        cacheNames.add(CacheNames.CACHE_15MINS);
        cacheNames.add(CacheNames.CACHE_30MINS);
        cacheNames.add(CacheNames.CACHE_60MINS);
        cacheNames.add(CacheNames.CACHE_180MINS);
        cacheNames.add(CacheNames.CACHE_12HOUR);
    }
    public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
                                     RedisTemplate<Object, Object> stringKeyRedisTemplate) {
        super();
        this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
        this.stringKeyRedisTemplate = stringKeyRedisTemplate;
        this.dynamic = cacheRedisCaffeineProperties.isDynamic();
    }
 
    //——————————————————————— 进行缓存工具 ——————————————————————
    /**
    * 清除所有进程缓存
    */
    public void clearAllCache() {
        stringKeyRedisTemplate.convertAndSend(cacheRedisCaffeineProperties.getRedis().getTopic(), new CacheMessage(null, null));
    }
 
    /**
    * 返回所有进程缓存(二级缓存)的统计信息
    * result:{"缓存名称":统计信息}
    * @return
    */
    public static Map<String, CacheStats> getCacheStats() {
        if (CollectionUtils.isEmpty(cacheMap)) {
            return null;
        }
 
        Map<String, CacheStats> result = new LinkedHashMap<>();
        for (Cache cache : cacheMap.values()) {
            RedisCaffeineCache caffeineCache = (RedisCaffeineCache) cache;
            result.put(caffeineCache.getName(), caffeineCache.getCaffeineCache().stats());
        }
        return result;
    }
 
    //—————————————————————————— core —————————————————————————
    @Override
    public Cache getCache(String name) {
        Cache cache = cacheMap.get(name);
        if(cache != null) {
            return cache;
        }
        if(!dynamic && !cacheNames.contains(name)) {
            return null;
        }
 
        cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(name), cacheRedisCaffeineProperties);
        Cache oldCache = cacheMap.putIfAbsent(name, cache);
        logger.debug("create cache instance, the cache name is : {}", name);
        return oldCache == null ? cache : oldCache;
    }
 
    @Override
    public Collection<String> getCacheNames() {
        return this.cacheNames;
    }
 
    public void clearLocal(String cacheName, Object key) {
        //cacheName为null 清除所有进程缓存
        if (cacheName == null) {
            log.info("清除所有本地缓存");
            cacheMap = new ConcurrentHashMap<>();
            return;
        }
 
        Cache cache = cacheMap.get(cacheName);
        if(cache == null) {
            return;
        }
 
        RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
        redisCaffeineCache.clearLocal(key);
    }
 
    /**
    * 实例化本地一级缓存
    * @param name
    * @return
    */
    private com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(String name) {
        Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
        CacheRedisCaffeineProperties.CacheDefault cacheConfig;
        switch (name) {
            case CacheNames.CACHE_15MINS:
                cacheConfig = cacheRedisCaffeineProperties.getCache15m();
                break;
            case CacheNames.CACHE_30MINS:
                cacheConfig = cacheRedisCaffeineProperties.getCache30m();
                break;
            case CacheNames.CACHE_60MINS:
                cacheConfig = cacheRedisCaffeineProperties.getCache60m();
                break;
            case CacheNames.CACHE_180MINS:
                cacheConfig = cacheRedisCaffeineProperties.getCache180m();
                break;
            case CacheNames.CACHE_12HOUR:
                cacheConfig = cacheRedisCaffeineProperties.getCache12h();
                break;
            default:
                cacheConfig = cacheRedisCaffeineProperties.getCacheDefault();
        }
        long expireAfterAccess = cacheConfig.getExpireAfterAccess();
        long expireAfterWrite = cacheConfig.getExpireAfterWrite();
        int initialCapacity = cacheConfig.getInitialCapacity();
        long maximumSize = cacheConfig.getMaximumSize();
        long refreshAfterWrite = cacheConfig.getRefreshAfterWrite();
 
        log.debug("本地缓存初始化:");
        if (expireAfterAccess > 0) {
            log.debug("设置本地缓存访问后过期时间,{}秒", expireAfterAccess);
            cacheBuilder.expireAfterAccess(expireAfterAccess, TimeUnit.SECONDS);
        }
        if (expireAfterWrite > 0) {
            log.debug("设置本地缓存写入后过期时间,{}秒", expireAfterWrite);
            cacheBuilder.expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS);
        }
        if (initialCapacity > 0) {
            log.debug("设置缓存初始化大小{}", initialCapacity);
            cacheBuilder.initialCapacity(initialCapacity);
        }
        if (maximumSize > 0) {
            log.debug("设置本地缓存最大值{}", maximumSize);
            cacheBuilder.maximumSize(maximumSize);
        }
        if (refreshAfterWrite > 0) {
            cacheBuilder.refreshAfterWrite(refreshAfterWrite, TimeUnit.SECONDS);
        }
        cacheBuilder.recordStats();
        return cacheBuilder.build();
    }
}

RedisCaffeineCache 继承 AbstractValueAdaptingCache

核心是get方法与put方法。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package com.axin.idea.rediscaffeinecachestarter.support;
 
import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
 
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
 
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
 
    private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);
 
    private String name;
 
    private RedisTemplate<Object, Object> redisTemplate;
 
    @Getter
    private Cache<Object, Object> caffeineCache;
 
    private String cachePrefix;
 
    /**
     * 默认key超时时间 3600s
     */
    private long defaultExpiration = 3600;
 
    private Map<String, Long> defaultExpires = new HashMap<>();
    {
        defaultExpires.put(CacheNames.CACHE_15MINS, TimeUnit.MINUTES.toSeconds(15));
        defaultExpires.put(CacheNames.CACHE_30MINS, TimeUnit.MINUTES.toSeconds(30));
        defaultExpires.put(CacheNames.CACHE_60MINS, TimeUnit.MINUTES.toSeconds(60));
        defaultExpires.put(CacheNames.CACHE_180MINS, TimeUnit.MINUTES.toSeconds(180));
        defaultExpires.put(CacheNames.CACHE_12HOUR, TimeUnit.HOURS.toSeconds(12));
    }
 
    private String topic;
    private Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap();
 
    protected RedisCaffeineCache(boolean allowNullValues) {
        super(allowNullValues);
    }
 
    public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate,
                              Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
        super(cacheRedisCaffeineProperties.isCacheNullValues());
        this.name = name;
        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
        this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();
        this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration();
        this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();
        defaultExpires.putAll(cacheRedisCaffeineProperties.getRedis().getExpires());
    }
 
    @Override
    public String getName() {
        return this.name;
    }
 
    @Override
    public Object getNativeCache() {
        return this;
    }
 
    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        Object value = lookup(key);
        if (value != null) {
            return (T) value;
        }
        //key在redis和缓存中均不存在
        ReentrantLock lock = keyLockMap.get(key.toString());
 
        if (lock == null) {
            logger.debug("create lock for key : {}", key);
            keyLockMap.putIfAbsent(key.toString(), new ReentrantLock());
            lock = keyLockMap.get(key.toString());
        }
        try {
            lock.lock();
            value = lookup(key);
            if (value != null) {
                return (T) value;
            }
            //执行原方法获得value
            value = valueLoader.call();
            Object storeValue = toStoreValue(value);
            put(key, storeValue);
            return (T) value;
        } catch (Exception e) {
            throw new ValueRetrievalException(key, valueLoader, e.getCause());
        } finally {
            lock.unlock();
        }
    }
 
    @Override
    public void put(Object key, Object value) {
        if (!super.isAllowNullValues() && value == null) {
            this.evict(key);
            return;
        }
        long expire = getExpire();
        logger.debug("put:{},expire:{}", getKey(key), expire);
        redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.SECONDS);
 
        //缓存变更时通知其他节点清理本地缓存
        push(new CacheMessage(this.name, key));
        //此处put没有意义,会收到自己发送的缓存key失效消息
//        caffeineCache.put(key, value);
    }
 
    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        Object cacheKey = getKey(key);
        // 使用setIfAbsent原子性操作
        long expire = getExpire();
        boolean setSuccess;
        setSuccess = redisTemplate.opsForValue().setIfAbsent(getKey(key), toStoreValue(value), Duration.ofSeconds(expire));
 
        Object hasValue;
        //setNx结果
        if (setSuccess) {
            push(new CacheMessage(this.name, key));
            hasValue = value;
        }else {
            hasValue = redisTemplate.opsForValue().get(cacheKey);
        }
 
        caffeineCache.put(key, toStoreValue(value));
        return toValueWrapper(hasValue);
    }
 
    @Override
    public void evict(Object key) {
        // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
        redisTemplate.delete(getKey(key));
 
        push(new CacheMessage(this.name, key));
 
        caffeineCache.invalidate(key);
    }
 
    @Override
    public void clear() {
        // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
        Set<Object> keys = redisTemplate.keys(this.name.concat(":*"));
        for (Object key : keys) {
            redisTemplate.delete(key);
        }
 
        push(new CacheMessage(this.name, null));
        caffeineCache.invalidateAll();
    }
 
    /**
     * 取值逻辑
     * @param key
     * @return
     */
    @Override
    protected Object lookup(Object key) {
        Object cacheKey = getKey(key);
        Object value = caffeineCache.getIfPresent(key);
        if (value != null) {
            logger.debug("从本地缓存中获得key, the key is : {}", cacheKey);
            return value;
        }
 
        value = redisTemplate.opsForValue().get(cacheKey);
 
        if (value != null) {
            logger.debug("从redis中获得值,将值放到本地缓存中, the key is : {}", cacheKey);
            caffeineCache.put(key, value);
        }
        return value;
    }
 
    /**
     * @description 清理本地缓存
     */
    public void clearLocal(Object key) {
        logger.debug("clear local cache, the key is : {}", key);
        if (key == null) {
            caffeineCache.invalidateAll();
        } else {
            caffeineCache.invalidate(key);
        }
    }
 
    //————————————————————————————私有方法——————————————————————————
 
    private Object getKey(Object key) {
        String keyStr = this.name.concat(":").concat(key.toString());
        return StringUtils.isEmpty(this.cachePrefix) ? keyStr : this.cachePrefix.concat(":").concat(keyStr);
    }
 
    private long getExpire() {
        long expire = defaultExpiration;
        Long cacheNameExpire = defaultExpires.get(this.name);
        return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
    }
 
    /**
     * @description 缓存变更时通知其他节点清理本地缓存
     */
    private void push(CacheMessage message) {
        redisTemplate.convertAndSend(topic, message);
    }
}

关于分布式本地缓存失效

现在的线上生产的都是多个节点,如果本节点的缓存失效了,是需要通过中间件来通知其他节点失效消息的。本组件考虑到学习分享让大家引入的依赖少点,就直接通过 redis 来发送消息了,实际生产过程中换成成熟的消息中间件(kafka、RocketMQ)来做通知更为稳妥。

到此这篇关于Redis+Caffeine实现分布式二级缓存组件实战教程的文章就介绍到这了,更多相关Redis Caffeine分布式二级缓存内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://www.cnblogs.com/keeya/p/16556172.html

延伸 · 阅读

精彩推荐
  • RedisRedis sort 排序命令详解

    Redis sort 排序命令详解

    这篇文章主要介绍了Redis sort 排序命令详解,本文讲解了默认排序命令、排序方式命令、BY语法、GET用法示例等内容,需要的朋友可以参考下 ...

    Redis教程网2692019-10-26
  • Redis你真的了解redis为什么要提供pipeline功能

    你真的了解redis为什么要提供pipeline功能

    Redis本身是一个cs模式的tcp server, client可以通过一个socket连续发起多个请求命令。这篇文章带领大家学习redis为什么要提供pipeline功能,需要的朋友可以参考下...

    mushishi7542021-08-11
  • Redis浅谈Redis主从复制以及主从复制原理

    浅谈Redis主从复制以及主从复制原理

    在现有企业中80%公司大部分使用的是redis单机服务,在实际的场景当中单一节点的redis容易面临风险。本文将介绍Redis主从复制以及主从复制原理。...

    八重樱6622021-08-08
  • RedisRedis数据过期策略的实现详解

    Redis数据过期策略的实现详解

    最近项目当中遇到一个需求场景,需要清空一些存放在Redis的数据,本文对Redis的过期机制简单的讲解一下,具有一定的参考价值,感兴趣的小伙伴们可以参...

    法号阿兴9292021-11-17
  • Redis详解centos7 yum安装redis及常用命令

    详解centos7 yum安装redis及常用命令

    这篇文章主要介绍了centos7 yum安装redis及常用命令,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...

    钢铁侠的知识库1512020-12-20
  • RedisRedis实现Session共享与单点登录

    Redis实现Session共享与单点登录

    本文主要介绍了Redis实现Session共享与单点登录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随...

    如来神掌十八式9112022-07-13
  • RedisRedis击穿穿透雪崩产生原因分析及解决思路面试

    Redis击穿穿透雪崩产生原因分析及解决思路面试

    这篇文章主要为大家介绍了Redis击穿穿透雪崩产生原因及解决思路的面试问题答案参考,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步...

    Q.E.D8292022-03-09
  • Redis引入客户端缓存,Redis6算是把缓存玩明白了…

    引入客户端缓存,Redis6算是把缓存玩明白了…

    今天Hydra要和大家分享的技术,在思想上和上面两级缓存有些类似,不过不需要借助其他本地缓存中间件,只使用Redis自身服务端和客户端就可以实现。这就...

    码农参上9292022-04-28