分布式缓存
- Redis
- memcached
- Etcd
单机缓存
- ehcache
- java 内存集合
- caffeine(Java 缓存性能之王,高性能)
Redis 缓存实现
Redis 数据结构
基本
- String 字符串
- List 列表
- Set 集合
- Hash 哈希
- Zset 集合
高级
- bollmfilter(布隆过滤器,只要从大量的数据中快速过滤值,比如邮件黑名单拦截)
- geo(计算地理位置)
- hyperloglog(pv/uv)
- pub/sub(发布订阅,类似于消息队列)
- BitMap
自定义序列化
为了防止写入 Redis 的数据乱码、浪费空间等,可以自定义序列化器,示例代码如下
package com.wfh.xunai.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
return redisTemplate;
}
}
Java 操作 Redis
Spring Data Redis
1、使用方式:引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
2、配置 redis 地址
spring:
# redis 配置
redis:
port: 6379
host: localhost
database: 0
设计缓存 key
关键点:不同用户看到的数据不同
比如:xunai:user:recommend:userid
ps:注意:redis 内存不能无限增加,一定要设计过期时间
缓存预热
问题:即使使用了缓存,第一个用户访问的还是数据库
缓存预热的优点
-
- 解决了上面的问题。可以让用户始终访问速度很快
- 也能一定程度上保护了数据库
缺点
- 增加了开发成本
- 预热的时间和时间如果错了,有可能让缓存的数据不对或者太老
- 需要额外的占用空间
怎么缓存预热
1、定时任务
2、手动触发
实现
用定时任务,每天刷新所有用户的推荐列表
定时任务实现
1、使用 Spring Scheduler(推荐,默认整合了)
控制定时任务的执行
要控制定时任务在同一个时间只能由一台服务器执行
方案:
1、分裂定时任务程序和主程序,只在一个服务器运行定时任务,成本太大
2、写死配置,每个服务器都执行定时任务,但是只有 ip 符合配置的服务器踩真是执行业务逻辑,其他的直接返回
3、动态配置
4、分布式锁
锁
在有限资源的情况下,控制同一时间只有某些线程能访问到资源
Java 实现锁:synchronized 关键字,juc 的类
问题:只对单个 JVM 有效
分布式锁
强锁机制
怎么保证同一个时间只能有一台服务器能强到锁
核心思想: 先来的人先把数据改成自己的唯一标识,后来的人发现标识已经存在,就抢锁失败,继续等待
等先来的人把方法执行结束,把标识清空,其他的人继续抢锁
Redis 实现:内存数据库,读写速度快
setnx:
注意
1、用完要释放
2、锁要加上过期时间
3、如果方法执行时间过长,锁就提前过期了?
导致问题:
1、连锁效应:释放掉别人的锁
解决方案:续期
Redis 实现分布式锁
示例代码
// list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));
list.remove(0);
// 数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list");
rList.add("yupi");
System.out.println("rlist:" + rList.get(0));
rList.remove(0);
分布式锁保证定时任务不重复执行
void testWatchDog() {
RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
try {
// 只有一个线程能获取到锁
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
// todo 实际要执行的方法
doSomeThings();
System.out.println("getLock: " + Thread.currentThread().getId());
}
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
}
注意:
1、等待时间(waittime)设置为 0,表示只抢一次锁,抢不到就放弃
2、一定要释放锁,卸载 finnally 中
Redisson 看门狗机制
开启一个监听线程,如果方法还没有执行完,就自动重置 redis 锁的过期时间
原理:
1、监听当前的线程,默认过期时间是 30s,没 10s 续期一次
2、如果线程挂掉,就不会续期
Redis + Caffeine多级缓存实现
首先引入maven依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version> <!– 请根据需要选择最新版 –>
</dependency>
什么是Caffeine:
Caffeine 是一个高性能、基于 Java 8 的本地缓存库,由 Ben Manes 开发,是 Google Guava Cache 的继任者。它在设计上借鉴了 Guava Cache 的优点,并引入了更先进的缓存淘汰算法(如 Window TinyLFU),显著提升了性能和内存效率。
一、Caffeine 核心特性
- 高性能:采用 Window TinyLFU 淘汰策略,在高并发场景下表现优异。
- 自动加载:支持通过 LoadingCache 自动加载缺失的缓存项。
- 异步支持:提供 AsyncLoadingCache,支持异步加载和返回 CompletableFuture。
- 丰富的过期策略:
- 基于写入时间(expireAfterWrite)
- 基于访问时间(expireAfterAccess)
- 自定义过期(expireAfter)
- 大小限制:可设置最大缓存条目数(maximumSize)或权重(maximumWeight)。
- 监听器:支持缓存项被移除时的回调(RemovalListener)。
- 统计信息:可开启命中率、加载次数等指标统计(recordStats)。
二、基本使用示例
1. 手动缓存(Manual Cache)
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
// 手动放入
cache.put("key1", "value1");
// 获取(可能为 null)
String value = cache.getIfPresent("key1");
// 获取或计算(若不存在)
String value2 = cache.get("key2", k -> createValue(k));
2. 自动加载缓存(Loading Cache)
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.build(key -> getValueFromDB(key)); // 自动加载函数
String value = loadingCache.get("someKey"); // 自动调用加载函数
3. 异步加载缓存(Async Loading Cache)
AsyncLoadingCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.buildAsync(key -> CompletableFuture.supplyAsync(() -> getValueFromDB(key)));
CompletableFuture<String> future = asyncCache.get("someKey");
三、常用配置详解
| maximumSize(long) | 设置缓存最大条目数(基于条目数量) |
| maximumWeight(long) + weigher(Weigher) | 按权重限制总大小(如按对象字节数) |
| expireAfterWrite(Duration) | 写入后多久过期 |
| expireAfterAccess(Duration) | 最后一次访问后多久过期 |
| refreshAfterWrite(Duration) | 写入后多久可刷新(异步刷新,不影响当前读) |
| removalListener(RemovalListener) | 缓存项被移除时触发回调 |
| recordStats() | 启用统计信息(可通过 cache.stats() 查看) |
⚠️ 注意:expireAfterWrite 和 expireAfterAccess 不会主动清理过期数据,而是通过缓存操作(如 get/put)触发“惰性清理”。
四、淘汰策略:Window TinyLFU
Caffeine 使用 Window TinyLFU 算法,结合了:
- TinyLFU:近似记录访问频率,节省内存;
- Window(窗口)机制:保留新进入的条目一段时间,避免“突发访问”被误淘汰。
相比 LRU 或 FIFO,该策略在各种访问模式下(如热点数据、突发流量)都表现出色。
五、Redis+Caffeine实战
/**
* 分页获取所有公共空间的图表(带多级缓存)
* 缓存策略: Caffeine(L1) -> Redis(L2) -> DB
*/
@Override
public Page<DiagramVO> getPublicDiagramsByPage(DiagramQueryRequest pageRequest) {
int current = pageRequest.getCurrent();
int pageSize = pageRequest.getPageSize();
// 构造key
String redisKey = String.format(RedisPrefixConstant.ALL_DIAGRAM + "%s:%s:", current, pageSize);
String cacheKey = String.format(CachePrefixConstant.ALL_DIAGRAM + "%s:%s", current, pageSize);
// 先查询Caffeine中是否存在
Page<DiagramVO> cachePage = diagramsPageCache.getIfPresent(cacheKey);
if (cachePage == null) {
// 本地缓存为空,去查询redis
// 先查询redis是否存在
String pageStr = stringRedisTemplate.opsForValue().get(redisKey);
if (StringUtils.isEmpty(pageStr)) {
// 如果Redis中是空的话,就查询数据库并构造缓存
Page<Diagram> page = new Page<>(pageRequest.getCurrent(), pageRequest.getPageSize());
LambdaQueryWrapper<Diagram> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.isNull(Diagram::getSpaceId);
Page<Diagram> resultPage = this.page(page, queryWrapper);
List<DiagramVO> diagramVOList = resultPage.getRecords().stream()
.map(DiagramVO::objToVo)
.toList();
Page<DiagramVO> resPage = new Page<>(pageRequest.getCurrent(), pageRequest.getPageSize());
resPage.setRecords(diagramVOList);
resPage.setTotal(resultPage.getTotal());
String jsonStr = JSONUtil.toJsonStr(resPage);
// 设置Redis缓存
stringRedisTemplate.opsForValue().set(redisKey, jsonStr, RandomUtil.randomInt(1, 5), TimeUnit.MINUTES);
// 设置Caffeine缓存
diagramsPageCache.put(cacheKey, resPage);
return resPage;
} else {
// redis中不为空的话,直接反序列化之后返回给前端
Page<DiagramVO> page = JSONUtil.toBean(pageStr, Page.class);
// 同时设置本地缓存
diagramsPageCache.put(cacheKey, page);
return page;
}
}
// 本地缓存不为空,直接返回
return cachePage;
}
这样,当我们在查询图表分页数据的时候就会先查询本地缓存,如果本地缓存不存在就去查询Redis缓存并写入的本地缓存,如果Redis中也不存在,那么直接去查询数据库,并先写入到Redis然后写入到本地缓存中。

