分布式缓存详解
本文深入讲解分布式缓存核心原理与实战,涵盖 Redis 数据结构、持久化、集群、缓存策略、三大问题、分布式锁、本地缓存、多级缓存架构。
目录
一、分布式缓存概述
1.1 为什么需要分布式缓存
在高并发系统中,数据库是最常见的性能瓶颈。分布式缓存通过将热点数据存储在内存中,大幅降低数据库压力:
| 响应时间 | 5~50ms | 0.1~1ms |
| QPS(单节点) | 1,000~5,000 | 100,000+ |
| 数据容量 | TB级(磁盘) | GB级(内存) |
| 并发模型 | 连接池/线程 | 单线程IO多路复用 |
1.2 缓存的核心价值
- 降低延迟:内存访问速度是磁盘的 10万倍
- 提升吞吐:Redis 单节点可支撑 10W+ QPS
- 保护数据库:削峰填谷,避免DB被打垮
- 降低成本:减少数据库扩容需求
1.3 缓存的代价
- 数据一致性:缓存与DB数据可能不一致
- 系统复杂度:需处理穿透/击穿/雪崩
- 运维成本:Redis集群的部署与监控
- 内存成本:热点数据占用大量内存
二、Redis核心数据结构
2.1 String(字符串)
最基础的数据类型,可存储字符串、整数、浮点数。
@Service
public class StringCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 缓存用户Token
*/
public void cacheToken(Long userId, String token) {
String key = "user:token:" + userId;
redisTemplate.opsForValue().set(key, token, 30, TimeUnit.MINUTES);
}
/**
* 分布式计数器(文章浏览量)
*/
public Long incrementViewCount(Long articleId) {
String key = "article:views:" + articleId;
return redisTemplate.opsForValue().increment(key);
}
/**
* 分布式锁基础操作
*/
public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 批量获取(Pipeline减少网络往返)
*/
public List<String> batchGet(List<String> keys) {
return redisTemplate.opsForValue().multiGet(keys);
}
}
2.2 Hash(哈希)
适合存储对象,支持字段级别的独立操作。
@Service
public class HashCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 缓存用户对象(字段级更新)
*/
public void cacheUser(User user) {
String key = "user:info:" + user.getId();
Map<String, String> userMap = new HashMap<>();
userMap.put("name", user.getName());
userMap.put("email", user.getEmail());
userMap.put("age", String.valueOf(user.getAge()));
redisTemplate.opsForHash().putAll(key, userMap);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
/**
* 更新单个字段(无需读取整个对象)
*/
public void updateUserName(Long userId, String newName) {
String key = "user:info:" + userId;
redisTemplate.opsForHash().put(key, "name", newName);
}
/**
* 获取单个字段
*/
public String getUserName(Long userId) {
String key = "user:info:" + userId;
Object name = redisTemplate.opsForHash().get(key, "name");
return name != null ? name.toString() : null;
}
/**
* 购物车(商品ID -> 数量)
*/
public void addToCart(Long userId, Long productId, int quantity) {
String key = "cart:" + userId;
redisTemplate.opsForHash().increment(key, productId.toString(), quantity);
}
}
2.3 List(列表)
双端链表结构,适合实现消息队列和最新列表。
@Service
public class ListCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 最新消息列表(保留最新100条)
*/
public void pushNews(String news) {
String key = "news:latest";
redisTemplate.opsForList().leftPush(key, news);
redisTemplate.opsForList().trim(key, 0, 99); // 只保留100条
}
/**
* 获取最新N条消息
*/
public List<String> getLatestNews(int count) {
String key = "news:latest";
return redisTemplate.opsForList().range(key, 0, count – 1);
}
/**
* 简单消息队列(阻塞式消费)
*/
public String consumeMessage(String queueKey, long timeoutSeconds) {
return redisTemplate.opsForList().rightPop(queueKey, timeoutSeconds, TimeUnit.SECONDS);
}
/**
* 生产者
*/
public void produceMessage(String queueKey, String message) {
redisTemplate.opsForList().leftPush(queueKey, message);
}
}
2.4 Set(集合)
无序不重复集合,支持交并差集运算。
@Service
public class SetCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 用户点赞(去重)
*/
public boolean likeArticle(Long articleId, Long userId) {
String key = "article:likes:" + articleId;
return Boolean.TRUE.equals(redisTemplate.opsForSet().add(key, userId.toString()));
}
/**
* 共同关注(交集)
*/
public Set<String> getCommonFollowers(Long userA, Long userB) {
String keyA = "user:following:" + userA;
String keyB = "user:following:" + userB;
return redisTemplate.opsForSet().intersect(keyA, keyB);
}
/**
* 抽奖(随机弹出)
*/
public String drawLottery(String lotteryKey) {
return redisTemplate.opsForSet().pop(lotteryKey);
}
/**
* 判断用户是否已参与
*/
public boolean hasParticipated(String lotteryKey, Long userId) {
return Boolean.TRUE.equals(
redisTemplate.opsForSet().isMember(lotteryKey, userId.toString()));
}
}
2.5 ZSet(有序集合)
每个元素关联一个分数,按分数排序。
@Service
public class ZSetCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 排行榜(分数越高排名越前)
*/
public void updateScore(String rankKey, String member, double score) {
redisTemplate.opsForZSet().add(rankKey, member, score);
}
/**
* 获取Top N
*/
public Set<ZSetOperations.TypedTuple<String>> getTopN(String rankKey, int n) {
return redisTemplate.opsForZSet().reverseRangeWithScores(rankKey, 0, n – 1);
}
/**
* 获取用户排名
*/
public Long getUserRank(String rankKey, String member) {
Long rank = redisTemplate.opsForZSet().reverseRank(rankKey, member);
return rank != null ? rank + 1 : null; // 排名从1开始
}
/**
* 延迟队列(score为执行时间戳)
*/
public void addDelayTask(String task, long executeTimeMillis) {
redisTemplate.opsForZSet().add("delay:queue", task, executeTimeMillis);
}
/**
* 消费到期任务
*/
public Set<String> pollDueTasks() {
long now = System.currentTimeMillis();
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore("delay:queue", 0, now);
if (tasks != null && !tasks.isEmpty()) {
redisTemplate.opsForZSet().remove("delay:queue", tasks.toArray());
}
return tasks;
}
}
2.6 Bitmap(位图)
基于String的位操作,适合布尔状态存储。
@Service
public class BitmapCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 用户签到(offset为一年中的第几天)
*/
public void signIn(Long userId, int dayOfYear) {
String key = "signin:" + userId + ":" + LocalDate.now().getYear();
redisTemplate.opsForValue().setBit(key, dayOfYear, true);
}
/**
* 判断某天是否签到
*/
public boolean hasSignedIn(Long userId, int dayOfYear) {
String key = "signin:" + userId + ":" + LocalDate.now().getYear();
Boolean bit = redisTemplate.opsForValue().getBit(key, dayOfYear);
return Boolean.TRUE.equals(bit);
}
/**
* 统计连续在线天数(BITCOUNT)
*/
public Long countSignDays(Long userId) {
String key = "signin:" + userId + ":" + LocalDate.now().getYear();
// 使用Redis命令 BITCOUNT
return redisTemplate.execute((RedisCallback<Long>)
connection -> connection.bitCount(key.getBytes()));
}
}
2.7 HyperLogLog(基数统计)
概率型数据结构,用极小内存估算集合基数(误差约0.81%)。
@Service
public class HyperLogLogService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 记录页面UV(独立访客)
*/
public void recordVisit(String pageId, String userId) {
String key = "uv:" + pageId + ":" + LocalDate.now();
redisTemplate.opsForHyperLogLog().add(key, userId);
}
/**
* 获取UV数量
*/
public Long getUVCount(String pageId) {
String key = "uv:" + pageId + ":" + LocalDate.now();
return redisTemplate.opsForHyperLogLog().size(key);
}
/**
* 合并多天UV(去重)
*/
public Long mergeWeeklyUV(String pageId, List<String> dailyKeys) {
String destKey = "uv:weekly:" + pageId;
byte[][] keys = dailyKeys.stream()
.map(k -> k.getBytes())
.toArray(byte[][]::new);
redisTemplate.execute((RedisCallback<Long>) connection -> {
connection.pfMerge(destKey.getBytes(), keys);
return connection.pfCount(destKey.getBytes());
});
return redisTemplate.opsForHyperLogLog().size(destKey);
}
}
三、Redis持久化
3.1 RDB(Redis Database)
RDB 通过 fork 子进程生成某一时刻的全量快照。
触发方式:
- SAVE:阻塞主进程(生产禁用)
- BGSAVE:fork子进程,不阻塞
- 配置自动触发:save 900 1(900秒内至少1次修改)
配置示例(redis.conf):
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis
rdbcompression yes
3.2 AOF(Append Only File)
AOF 记录每条写命令,重启时重放命令恢复数据。
同步策略:
| always | 每条命令都fsync | 最高 | 最低 |
| everysec(推荐) | 每秒fsync一次 | 最多丢1秒 | 均衡 |
| no | 由OS决定flush | 可能丢较多 | 最高 |
配置示例:
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
3.3 混合持久化(Redis 4.0+)
AOF 重写时,前半段用 RDB 格式,后半段追加 AOF 命令,兼顾恢复速度和数据完整性。
aof-use-rdb-preamble yes
3.4 Java中验证持久化状态
@Service
public class RedisPersistenceMonitor {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取持久化状态信息
*/
public Map<String, String> getPersistenceInfo() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("persistence");
Map<String, String> result = new HashMap<>();
if (info != null) {
result.put("rdb_last_bgsave_status",
info.getProperty("rdb_last_bgsave_status"));
result.put("rdb_last_save_time",
info.getProperty("rdb_last_save_time"));
result.put("aof_enabled",
info.getProperty("aof_enabled"));
result.put("aof_last_write_status",
info.getProperty("aof_last_write_status"));
}
return result;
}
/**
* 手动触发BGSAVE
*/
public void triggerBgSave() {
redisTemplate.execute((RedisCallback<String>) connection -> {
connection.bgSave();
return "BGSAVE triggered";
});
}
}
四、Redis集群
4.1 主从复制
主节点处理写请求,从节点异步复制数据并处理读请求。
核心流程:
4.2 哨兵(Sentinel)
哨兵集群监控主节点健康状态,自动完成故障转移。
故障转移流程:
Spring Boot 哨兵配置:
spring:
data:
redis:
sentinel:
master: mymaster
nodes: 192.168.1.10:26379,192.168.1.11:26379,192.168.1.12:26379
password: ${REDIS_PASSWORD}
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 4
4.3 Redis Cluster
去中心化分片集群,数据按 CRC16(key) % 16384 分配到不同槽。
核心特性:
- 16384个哈希槽分布在多个主节点
- 客户端收到 MOVED 重定向到正确节点
- 每个主节点有从节点做故障备份
- 支持在线扩缩容(槽迁移)
Spring Boot Cluster配置:
spring:
data:
redis:
cluster:
nodes: 192.168.1.10:7000,192.168.1.11:7001,192.168.1.12:7002
max-redirects: 3
password: ${REDIS_PASSWORD}
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 8
4.4 集群方案Java配置类
@Configuration
public class RedisClusterConfig {
@Bean
@ConditionalOnProperty(name = "redis.mode", havingValue = "cluster")
public RedisConnectionFactory clusterConnectionFactory(
@Value("${spring.data.redis.cluster.nodes}") String nodes,
@Value("${spring.data.redis.password}") String password) {
String[] nodeArray = nodes.split(",");
List<RedisNode> redisNodes = Arrays.stream(nodeArray)
.map(node -> {
String[] parts = node.split(":");
return new RedisNode(parts[0], Integer.parseInt(parts[1]));
})
.collect(Collectors.toList());
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
clusterConfig.setClusterNodes(redisNodes);
clusterConfig.setPassword(RedisPassword.of(password));
clusterConfig.setMaxRedirects(3);
GenericObjectPoolConfig<?> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(32);
poolConfig.setMaxIdle(16);
poolConfig.setMinIdle(8);
LettucePoolingClientConfiguration clientConfig =
LettucePoolingClientConfiguration.builder()
.poolConfig(poolConfig)
.commandTimeout(Duration.ofMillis(3000))
.build();
return new LettuceConnectionFactory(clusterConfig, clientConfig);
}
}
五、缓存策略
5.1 Cache Aside(旁路缓存)
最常用的缓存模式,应用程序直接管理缓存。
读流程:先读缓存 → 命中则返回 → 未命中则读DB → 写入缓存 → 返回
写流程:先更新DB → 再删除缓存
@Service
public class CacheAsideService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private static final long CACHE_TTL_MINUTES = 30;
/**
* 读:Cache Aside模式
*/
public User getUserById(Long userId) {
String key = "user:" + userId;
// 1. 先读缓存
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 2. 缓存未命中,读数据库
User user = userMapper.selectById(userId);
if (user == null) {
return null;
}
// 3. 写入缓存(加随机偏移防雪崩)
long ttl = CACHE_TTL_MINUTES + ThreadLocalRandom.current().nextInt(5);
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), ttl, TimeUnit.MINUTES);
return user;
}
/**
* 写:先更新DB,再删除缓存
*/
@Transactional
public void updateUser(User user) {
// 1. 更新数据库
userMapper.updateById(user);
// 2. 删除缓存(下次读时重建)
String key = "user:" + user.getId();
redisTemplate.delete(key);
}
}
5.2 Read Through(读穿透)
缓存层作为数据访问的统一入口,miss时自动从DB加载。
@Service
public class ReadThroughService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
/**
* Read Through:缓存层自动加载
* 对调用方透明,只需访问缓存层
*/
public User getUser(Long userId) {
String key = "user:" + userId;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 缓存层自动从DB加载(对调用方透明)
User user = loadFromDBAndCache(key, userId);
return user;
}
private synchronized User loadFromDBAndCache(String key, Long userId) {
// 双重检查
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
User user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
30, TimeUnit.MINUTES);
}
return user;
}
}
5.3 Write Through(写穿透)
写操作由缓存层代理,同步写入DB。
@Service
public class WriteThroughService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
/**
* Write Through:缓存层代理写入
* 保证缓存和DB的同步更新
*/
@Transactional
public void saveUser(User user) {
String key = "user:" + user.getId();
// 1. 更新数据库
userMapper.updateById(user);
// 2. 同步更新缓存(而非删除)
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
30, TimeUnit.MINUTES);
}
}
5.4 Write Behind(异步写回)
只写缓存,异步批量刷回数据库,适合写密集场景。
@Service
public class WriteBehindService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private final BlockingQueue<User> writeQueue = new LinkedBlockingQueue<>(10000);
/**
* 写操作只写缓存,异步入队
*/
public void updateUser(User user) {
String key = "user:" + user.getId();
// 1. 立即更新缓存(保证读一致性)
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
30, TimeUnit.MINUTES);
// 2. 放入异步写队列
writeQueue.offer(user);
}
/**
* 异步批量刷DB(定时任务)
*/
@Scheduled(fixedDelay = 1000)
public void flushToDatabase() {
List<User> batch = new ArrayList<>();
writeQueue.drainTo(batch, 100); // 每次最多100条
if (!batch.isEmpty()) {
// 批量更新数据库
userMapper.batchUpdate(batch);
}
}
}
六、缓存穿透
6.1 问题描述
查询一个不存在的数据,缓存中没有,DB中也没有,导致每次请求都穿透到数据库。恶意攻击时可导致DB崩溃。
6.2 方案一:布隆过滤器
在缓存前加一层布隆过滤器,快速判断数据是否可能存在。
@Service
public class BloomFilterService {
private final BloomFilter<Long> bloomFilter;
@Autowired
private UserMapper userMapper;
@Autowired
private StringRedisTemplate redisTemplate;
public BloomFilterService() {
// 预期100万元素,误判率1%
this.bloomFilter = BloomFilter.create(
Funnels.longFunnel(), 1_000_000, 0.01);
}
/**
* 系统启动时预热布隆过滤器
*/
@PostConstruct
public void warmUp() {
List<Long> allUserIds = userMapper.selectAllUserIds();
allUserIds.forEach(bloomFilter::put);
}
/**
* 带布隆过滤器的查询
*/
public User getUserById(Long userId) {
// 1. 布隆过滤器判断(不存在则一定不存在)
if (!bloomFilter.mightContain(userId)) {
return null; // 直接拦截,不穿透到DB
}
// 2. 正常缓存查询逻辑
String key = "user:" + userId;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
User user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
30, TimeUnit.MINUTES);
}
return user;
}
/**
* 新增用户时同步更新布隆过滤器
*/
public void addUser(User user) {
userMapper.insert(user);
bloomFilter.put(user.getId());
}
}
6.3 方案二:空值缓存
对查询结果为空的Key也进行缓存,设置较短TTL。
@Service
public class NullValueCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String NULL_PLACEHOLDER = "NULL";
private static final long NULL_TTL_SECONDS = 60; // 空值缓存60秒
public User getUserById(Long userId) {
String key = "user:" + userId;
String cached = redisTemplate.opsForValue().get(key);
// 命中空值标记,直接返回null
if (NULL_PLACEHOLDER.equals(cached)) {
return null;
}
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 查询数据库
User user = userMapper.selectById(userId);
if (user == null) {
// 缓存空值,防止反复穿透
redisTemplate.opsForValue().set(key, NULL_PLACEHOLDER,
NULL_TTL_SECONDS, TimeUnit.SECONDS);
return null;
}
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
30, TimeUnit.MINUTES);
return user;
}
}
七、缓存击穿
7.1 问题描述
某个热点Key在过期瞬间,大量并发请求同时穿透到数据库,造成DB瞬时压力暴增。
7.2 方案一:互斥锁重建缓存
只允许一个线程去重建缓存,其他线程等待或返回旧数据。
@Service
public class MutexLockCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private static final long LOCK_TTL_SECONDS = 10;
private static final long CACHE_TTL_MINUTES = 30;
public User getUserById(Long userId) {
String key = "user:" + userId;
String lockKey = "lock:user:" + userId;
// 1. 尝试读缓存
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 2. 缓存未命中,尝试获取互斥锁
String requestId = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, LOCK_TTL_SECONDS, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 3. 双重检查(可能其他线程已重建)
cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 4. 查询DB并重建缓存
User user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
CACHE_TTL_MINUTES, TimeUnit.MINUTES);
}
return user;
} finally {
// 5. 释放锁(Lua保证原子性)
releaseLock(lockKey, requestId);
}
} else {
// 6. 未获取到锁,短暂等待后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getUserById(userId); // 递归重试
}
}
private void releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), requestId);
}
}
7.3 方案二:逻辑过期(永不真正过期)
缓存永不设置TTL,在Value中存储逻辑过期时间,过期后异步重建。
@Data
public class CacheData<T> {
private T data;
private LocalDateTime expireTime; // 逻辑过期时间
}
@Service
public class LogicalExpireCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private final ExecutorService cacheRebuildExecutor =
Executors.newFixedThreadPool(4);
/**
* 预热缓存(设置逻辑过期时间,不设TTL)
*/
public void warmUpUser(Long userId) {
User user = userMapper.selectById(userId);
if (user != null) {
CacheData<User> cacheData = new CacheData<>();
cacheData.setData(user);
cacheData.setExpireTime(LocalDateTime.now().plusMinutes(30));
String key = "user:" + userId;
redisTemplate.opsForValue().set(key, JSON.toJSONString(cacheData));
// 注意:不设置TTL,永不真正过期
}
}
public User getUserById(Long userId) {
String key = "user:" + userId;
String cached = redisTemplate.opsForValue().get(key);
if (cached == null) {
return null;
}
CacheData<User> cacheData = JSON.parseObject(cached,
new TypeReference<CacheData<User>>() {});
// 判断逻辑过期
if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return cacheData.getData();
}
// 已过期,尝试异步重建(当前请求仍返回旧数据)
String lockKey = "lock:user:" + userId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
cacheRebuildExecutor.submit(() -> {
try {
warmUpUser(userId); // 重建缓存
} finally {
redisTemplate.delete(lockKey);
}
});
}
// 返回旧数据(保证可用性)
return cacheData.getData();
}
}
八、缓存雪崩
8.1 问题描述
大量缓存Key在同一时间过期,或Redis节点宕机,导致大量请求直接打到数据库。
8.2 方案一:随机过期时间
在基础TTL上增加随机偏移,打散过期时间。
@Service
public class RandomTtlCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final int BASE_TTL_MINUTES = 30;
private static final int RANDOM_RANGE_MINUTES = 10;
/**
* 设置带随机偏移的过期时间
*/
public void setWithRandomTtl(String key, String value) {
int randomOffset = ThreadLocalRandom.current().nextInt(RANDOM_RANGE_MINUTES);
long ttl = BASE_TTL_MINUTES + randomOffset;
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.MINUTES);
}
/**
* 批量预热缓存(打散过期时间)
*/
public void batchWarmUp(Map<String, String> dataMap) {
dataMap.forEach((key, value) -> {
int randomOffset = ThreadLocalRandom.current().nextInt(RANDOM_RANGE_MINUTES);
long ttl = BASE_TTL_MINUTES + randomOffset;
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.MINUTES);
});
}
}
8.3 方案二:多级缓存
本地缓存作为第一道防线,即使Redis不可用也能扛住部分流量。
@Service
public class MultiLevelFallbackService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
// 本地缓存作为降级方案
private final Cache<Long, User> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public User getUserById(Long userId) {
// L1: 本地缓存
User user = localCache.getIfPresent(userId);
if (user != null) {
return user;
}
// L2: Redis(带降级)
try {
String key = "user:" + userId;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
user = JSON.parseObject(cached, User.class);
localCache.put(userId, user); // 回填L1
return user;
}
} catch (Exception e) {
// Redis不可用时降级到DB
}
// L3: 数据库
user = userMapper.selectById(userId);
if (user != null) {
localCache.put(userId, user);
try {
redisTemplate.opsForValue().set("user:" + userId,
JSON.toJSONString(user), 30, TimeUnit.MINUTES);
} catch (Exception ignored) {
// Redis不可用时忽略
}
}
return user;
}
}
8.4 方案三:限流降级
当DB压力过大时,通过限流保护数据库。
@Service
public class RateLimitCacheService {
@Autowired
private UserMapper userMapper;
// 使用Guava RateLimiter限制DB访问频率
private final RateLimiter dbRateLimiter = RateLimiter.create(1000); // 每秒1000次
public User getUserFromDB(Long userId) {
// 尝试获取令牌(非阻塞)
if (!dbRateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
// 限流:返回降级数据或抛出异常
throw new ServiceException("系统繁忙,请稍后重试");
}
return userMapper.selectById(userId);
}
}
九、缓存一致性
9.1 延迟双删
先删缓存 → 更新DB → 延迟一段时间 → 再删缓存,解决并发读写不一致。
@Service
public class DelayDoubleDeleteService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
private static final long DELAY_MILLIS = 500; // 延迟500ms
@Transactional
public void updateUser(User user) {
String key = "user:" + user.getId();
// 1. 第一次删除缓存
redisTemplate.delete(key);
// 2. 更新数据库
userMapper.updateById(user);
// 3. 延迟第二次删除(解决并发读导致的脏数据)
scheduler.schedule(() -> {
redisTemplate.delete(key);
}, DELAY_MILLIS, TimeUnit.MILLISECONDS);
}
}
9.2 Canal监听Binlog
通过Canal伪装为MySQL从节点,监听Binlog变更,异步更新/删除缓存。
@Component
public class CanalCacheSyncListener {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 监听Canal消息(通过MQ消费)
* Canal将Binlog变更发送到RocketMQ/Kafka
*/
@RocketMQMessageListener(
topic = "canal-user-topic",
consumerGroup = "cache-sync-group"
)
public void onMessage(CanalMessage message) {
if ("UPDATE".equals(message.getType()) || "DELETE".equals(message.getType())) {
String tableName = message.getTable();
if ("t_user".equals(tableName)) {
String userId = message.getData().get("id");
String key = "user:" + userId;
redisTemplate.delete(key);
// 也可以主动重建缓存
}
}
}
}
@Data
public class CanalMessage {
private String database;
private String table;
private String type; // INSERT / UPDATE / DELETE
private Map<String, String> data;
private Map<String, String> old; // 更新前的值
}
十、分布式锁
10.1 SETNX基础实现
@Service
public class SimpleDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁(SET key value NX EX)
*/
public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁(Lua脚本保证原子性:判断+删除)
*/
public boolean releaseLock(String lockKey, String requestId) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId);
return Long.valueOf(1L).equals(result);
}
/**
* 使用示例:秒杀扣库存
*/
public boolean seckill(Long productId) {
String lockKey = "lock:seckill:" + productId;
String requestId = UUID.randomUUID().toString();
if (!tryLock(lockKey, requestId, 5)) {
return false; // 获取锁失败
}
try {
// 扣减库存(临界区)
String stockKey = "product:stock:" + productId;
Long stock = redisTemplate.opsForValue().decrement(stockKey);
return stock != null && stock >= 0;
} finally {
releaseLock(lockKey, requestId);
}
}
}
10.2 Redisson分布式锁(生产推荐)
Redisson 提供可重入、自动续期(看门狗)的分布式锁。
@Service
public class RedissonLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 可重入锁(自动续期)
*/
public void transferMoney(Long fromAccount, Long toAccount, BigDecimal amount) {
// 按ID排序避免死锁
String lockKey1 = "lock:account:" + Math.min(fromAccount, toAccount);
String lockKey2 = "lock:account:" + Math.max(fromAccount, toAccount);
RLock lock1 = redissonClient.getLock(lockKey1);
RLock lock2 = redissonClient.getLock(lockKey2);
try {
// 尝试获取锁(等待3秒,持有10秒后自动释放)
boolean acquired = lock1.tryLock(3, 10, TimeUnit.SECONDS);
if (!acquired) {
throw new BusinessException("操作频繁,请稍后重试");
}
boolean acquired2 = lock2.tryLock(3, 10, TimeUnit.SECONDS);
if (!acquired2) {
throw new BusinessException("操作频繁,请稍后重试");
}
try {
// 执行转账业务逻辑
doTransfer(fromAccount, toAccount, amount);
} finally {
lock2.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("操作被中断");
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
}
}
/**
* 看门狗自动续期(不指定leaseTime)
* 默认30秒过期,每10秒自动续期
*/
public void longRunningTask(String taskId) {
RLock lock = redissonClient.getLock("lock:task:" + taskId);
try {
lock.lock(); // 看门狗自动续期,直到unlock
// 执行耗时任务(无需担心锁过期)
processTask(taskId);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 公平锁(按请求顺序获取)
*/
public void fairLockExample(String resource) {
RLock fairLock = redissonClient.getFairLock("fair:lock:" + resource);
try {
fairLock.lock();
// 按顺序执行
} finally {
fairLock.unlock();
}
}
/**
* 读写锁
*/
public void readWriteLockExample(String configKey) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw:lock:" + configKey);
// 读操作
try {
rwLock.readLock().lock();
// 多个读线程可同时持有
} finally {
rwLock.readLock().unlock();
}
// 写操作
try {
rwLock.writeLock().lock();
// 独占
} finally {
rwLock.writeLock().unlock();
}
}
private void doTransfer(Long from, Long to, BigDecimal amount) {
// 转账逻辑
}
private void processTask(String taskId) {
// 任务处理逻辑
}
}
10.3 RedLock(多节点容错)
当Redis主节点宕机且锁未同步到从节点时,单节点锁可能失效。RedLock通过多数节点加锁解决。
@Service
public class RedLockService {
@Autowired
private RedissonClient redissonClient;
/**
* RedLock:向多个独立Redis节点加锁
* 需要在多个Redis实例上分别创建RedissonClient
*/
public void criticalOperation(String resourceId) {
// 实际生产中需要配置多个独立的RedissonClient
RLock lock1 = redissonClient.getLock("lock:" + resourceId);
// RLock lock2 = redissonClient2.getLock("lock:" + resourceId);
// RLock lock3 = redissonClient3.getLock("lock:" + resourceId);
// Redisson 3.x 已废弃RedLock,推荐使用MultiLock替代
RLock multiLock = new RedissonMultiLock(lock1);
// RLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
try {
boolean acquired = multiLock.tryLock(5, 30, TimeUnit.SECONDS);
if (acquired) {
// 执行关键操作
doCriticalWork(resourceId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
multiLock.unlock();
}
}
private void doCriticalWork(String resourceId) {
// 关键业务逻辑
}
}
十一、本地缓存
11.1 Caffeine(高性能本地缓存)
Caffeine 使用 W-TinyLFU 算法,命中率优于 LRU,是 Spring Boot 默认本地缓存实现。
@Configuration
public class CaffeineCacheConfig {
/**
* 手动创建Caffeine缓存
*/
@Bean
public Cache<String, Object> manualCache() {
return Caffeine.newBuilder()
.maximumSize(10_000) // 最大容量
.expireAfterWrite(10, TimeUnit.MINUTES) // 写后过期
.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后过期
.recordStats() // 开启统计
.removalListener((key, value, cause) -> {
// 移除监听
System.out.println("Cache removed: " + key + ", cause: " + cause);
})
.build();
}
/**
* 自动加载缓存(LoadingCache)
*/
@Bean
public LoadingCache<Long, User> userLoadingCache(UserMapper userMapper) {
return Caffeine.newBuilder()
.maximumSize(5_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(5, TimeUnit.MINUTES) // 异步刷新
.recordStats()
.build(userId -> userMapper.selectById(userId)); // 加载函数
}
/**
* 异步加载缓存
*/
@Bean
public AsyncLoadingCache<Long, User> asyncUserCache(UserMapper userMapper) {
return Caffeine.newBuilder()
.maximumSize(5_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.buildAsync(userId -> userMapper.selectById(userId));
}
}
@Service
public class CaffeineCacheService {
@Autowired
private LoadingCache<Long, User> userLoadingCache;
/**
* 使用LoadingCache(自动加载)
*/
public User getUser(Long userId) {
return userLoadingCache.get(userId); // miss时自动调用加载函数
}
/**
* 手动操作
*/
public void invalidateUser(Long userId) {
userLoadingCache.invalidate(userId);
}
/**
* 获取缓存统计
*/
public CacheStats getStats() {
return userLoadingCache.stats();
// hitRate(), hitCount(), missCount(), evictionCount()
}
}
11.2 Guava Cache
@Service
public class GuavaCacheService {
private final com.google.common.cache.LoadingCache<String, String> configCache;
@Autowired
private ConfigMapper configMapper;
public GuavaCacheService(ConfigMapper configMapper) {
this.configMapper = configMapper;
this.configCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return configMapper.getValueByKey(key);
}
});
}
public String getConfig(String key) {
try {
return configCache.get(key);
} catch (ExecutionException e) {
throw new RuntimeException("加载配置失败: " + key, e);
}
}
public void refreshConfig(String key) {
configCache.refresh(key); // 异步刷新
}
public void invalidateAll() {
configCache.invalidateAll();
}
}
十二、多级缓存架构
12.1 架构设计
请求 → L1(Caffeine) → L2(Redis) → L3(MySQL)
↑ 回填 ↑ 回填
更新 → 更新DB → 删Redis → 发MQ → 各节点失效L1
12.2 完整实现
@Service
public class MultiLevelCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
@Autowired
private RedisMessageTemplate messageTemplate;
// L1: 本地缓存
private final Cache<Long, User> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
private static final long REDIS_TTL_MINUTES = 30;
/**
* 多级缓存读取
*/
public User getUserById(Long userId) {
// L1: 本地缓存
User user = localCache.getIfPresent(userId);
if (user != null) {
return user;
}
// L2: Redis
String key = "user:" + userId;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
user = JSON.parseObject(cached, User.class);
localCache.put(userId, user); // 回填L1
return user;
}
// L3: 数据库
user = userMapper.selectById(userId);
if (user != null) {
// 回填L2
long ttl = REDIS_TTL_MINUTES + ThreadLocalRandom.current().nextInt(5);
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
ttl, TimeUnit.MINUTES);
// 回填L1
localCache.put(userId, user);
}
return user;
}
/**
* 更新数据 + 多级缓存失效
*/
@Transactional
public void updateUser(User user) {
// 1. 更新数据库
userMapper.updateById(user);
// 2. 删除Redis缓存
String key = "user:" + user.getId();
redisTemplate.delete(key);
// 3. 发布消息,通知所有节点失效本地缓存
messageTemplate.convertAndSend("cache:invalidate",
"user:" + user.getId());
}
/**
* 监听缓存失效消息(每个节点都监听)
*/
@RedisMessageListener(channel = "cache:invalidate")
public void onCacheInvalidate(String message) {
// 解析key,失效本地缓存
if (message.startsWith("user:")) {
Long userId = Long.parseLong(message.substring(5));
localCache.invalidate(userId);
}
}
}
十三、Spring Cache抽象
13.1 基本配置
@Configuration
@EnableCaching
public class SpringCacheConfig {
/**
* Redis缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
// 序列化配置
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jsonSerializer))
.disableCachingNullValues();
// 不同缓存空间不同TTL
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("user", defaultConfig.entryTtl(Duration.ofMinutes(30)));
cacheConfigs.put("config", defaultConfig.entryTtl(Duration.ofHours(2)));
cacheConfigs.put("hotData", defaultConfig.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
}
13.2 注解使用
@Service
@CacheConfig(cacheNames = "user")
public class UserCacheService {
@Autowired
private UserMapper userMapper;
/**
* 查询缓存(key使用SpEL表达式)
*/
@Cacheable(key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
/**
* 条件缓存(只缓存活跃用户)
*/
@Cacheable(key = "#userId", condition = "#userId > 0")
public User getActiveUser(Long userId) {
return userMapper.selectActiveById(userId);
}
/**
* 更新后删除缓存
*/
@CacheEvict(key = "#user.id")
@Transactional
public void updateUser(User user) {
userMapper.updateById(user);
}
/**
* 更新并同步缓存
*/
@CachePut(key = "#user.id")
@Transactional
public User updateUserAndCache(User user) {
userMapper.updateById(user);
return user; // 返回值将更新到缓存
}
/**
* 删除所有缓存
*/
@CacheEvict(allEntries = true)
public void clearAllUserCache() {
// 清空user缓存空间
}
/**
* 多缓存空间操作
*/
@Caching(
evict = {
@CacheEvict(cacheNames = "user", key = "#userId"),
@CacheEvict(cacheNames = "userList", allEntries = true)
}
)
public void deleteUser(Long userId) {
userMapper.deleteById(userId);
}
}
13.3 自定义缓存Key生成器
@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 类名:方法名:参数
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName());
sb.append(":").append(method.getName());
for (Object param : params) {
sb.append(":").append(param != null ? param.toString() : "null");
}
return sb.toString();
}
}
十四、Redis在Java中的使用
14.1 Jedis(同步客户端)
@Configuration
public class JedisConfig {
@Bean
public JedisPool jedisPool(
@Value("${redis.host}") String host,
@Value("${redis.port}") int port,
@Value("${redis.password}") String password) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(32);
poolConfig.setMaxIdle(16);
poolConfig.setMinIdle(8);
poolConfig.setMaxWaitMillis(3000);
poolConfig.setTestOnBorrow(true);
return new JedisPool(poolConfig, host, port, 3000, password);
}
}
@Service
public class JedisService {
@Autowired
private JedisPool jedisPool;
public String get(String key) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.get(key);
}
}
public void set(String key, String value, int expireSeconds) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(key, expireSeconds, value);
}
}
/**
* Pipeline批量操作
*/
public List<Object> pipelineGet(List<String> keys) {
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipeline = jedis.pipelined();
keys.forEach(pipeline::get);
return pipeline.syncAndReturnAll();
}
}
/**
* Lua脚本(原子操作)
*/
public Long decrStock(String stockKey) {
String script =
"local stock = tonumber(redis.call('get', KEYS[1])) " +
"if stock > 0 then " +
" redis.call('decr', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
try (Jedis jedis = jedisPool.getResource()) {
return (Long) jedis.eval(script,
Collections.singletonList(stockKey),
Collections.emptyList());
}
}
}
14.2 Lettuce(异步/响应式客户端)
@Service
public class LettuceService {
@Autowired
private RedisConnectionFactory connectionFactory;
/**
* 异步操作
*/
public CompletableFuture<String> asyncGet(String key) {
RedisConnection connection = connectionFactory.getConnection();
// Lettuce底层使用Netty,天然支持异步
return CompletableFuture.supplyAsync(() -> {
byte[] value = connection.get(key.getBytes());
return value != null ? new String(value) : null;
});
}
/**
* 响应式操作(Reactive)
*/
public Mono<String> reactiveGet(String key) {
ReactiveRedisTemplate<String, String> reactiveTemplate =
new ReactiveRedisTemplate<>(connectionFactory,
RedisSerializationContext.string());
return reactiveTemplate.opsForValue().get(key);
}
}
14.3 RedisTemplate(Spring封装)
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value序列化(JSON)
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
@Service
public class RedisTemplateService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 通用缓存操作
*/
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
public <T> T get(String key, Class<T> clazz) {
Object value = redisTemplate.opsForValue().get(key);
return clazz.cast(value);
}
/**
* 批量删除(使用SCAN避免阻塞)
*/
public void deleteByPattern(String pattern) {
Set<String> keys = new HashSet<>();
ScanOptions options = ScanOptions.scanOptions()
.match(pattern)
.count(1000)
.build();
try (Cursor<String> cursor = redisTemplate.scan(options)) {
while (cursor.hasNext()) {
keys.add(cursor.next());
if (keys.size() >= 1000) {
redisTemplate.delete(keys);
keys.clear();
}
}
}
if (!keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
/**
* 执行Redis命令(通用)
*/
public Object executeCommand(String command, String... args) {
return redisTemplate.execute((RedisCallback<Object>) connection -> {
byte[][] rawArgs = Arrays.stream(args)
.map(String::getBytes)
.toArray(byte[][]::new);
return connection.execute(command, rawArgs);
});
}
}
十五、最佳实践
15.1 Key设计规范
/**
* 缓存Key常量类(统一管理)
*/
public final class CacheKeys {
private CacheKeys() {}
private static final String SEPARATOR = ":";
/** 用户信息: user:info:{userId} */
public static String userInfo(Long userId) {
return "user:info" + SEPARATOR + userId;
}
/** 商品库存: product:stock:{productId} */
public static String productStock(Long productId) {
return "product:stock" + SEPARATOR + productId;
}
/** 分布式锁: lock:{business}:{resourceId} */
public static String lock(String business, String resourceId) {
return "lock" + SEPARATOR + business + SEPARATOR + resourceId;
}
/** 排行榜: rank:{type}:{date} */
public static String rank(String type, String date) {
return "rank" + SEPARATOR + type + SEPARATOR + date;
}
}
15.2 大Key处理
@Service
public class BigKeyHandler {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* Hash大Key分片存储
* 原始: user:detail:{userId} (可能包含上百个字段)
* 分片: user:detail:{userId}:0, user:detail:{userId}:1 …
*/
public void saveLargeHash(Long userId, Map<String, String> fields) {
int shardSize = 50; // 每个分片50个字段
List<Map.Entry<String, String>> entries = new ArrayList<>(fields.entrySet());
for (int i = 0; i < entries.size(); i += shardSize) {
int end = Math.min(i + shardSize, entries.size());
String shardKey = "user:detail:" + userId + ":" + (i / shardSize);
Map<String, String> shard = new HashMap<>();
for (int j = i; j < end; j++) {
shard.put(entries.get(j).getKey(), entries.get(j).getValue());
}
redisTemplate.opsForHash().putAll(shardKey, shard);
}
}
/**
* 异步删除大Key(UNLINK代替DEL)
*/
public void asyncDeleteBigKey(String key) {
redisTemplate.unlink(key); // Redis 4.0+ 非阻塞删除
}
}
15.3 热Key处理
@Service
public class HotKeyHandler {
@Autowired
private StringRedisTemplate redisTemplate;
// 本地缓存分散热Key压力
private final Cache<String, String> localHotCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.SECONDS) // 极短过期
.build();
/**
* 热Key多副本打散
* 将 hot:key 拆分为 hot:key:0, hot:key:1, …, hot:key:N
*/
public String getHotKey(String baseKey) {
// 先查本地缓存
String localValue = localHotCache.getIfPresent(baseKey);
if (localValue != null) {
return localValue;
}
// 随机选择一个副本
int replicaCount = 4;
int replicaIndex = ThreadLocalRandom.current().nextInt(replicaCount);
String replicaKey = baseKey + ":" + replicaIndex;
String value = redisTemplate.opsForValue().get(replicaKey);
if (value != null) {
localHotCache.put(baseKey, value);
}
return value;
}
/**
* 写入所有副本
*/
public void setHotKey(String baseKey, String value, long ttlSeconds) {
int replicaCount = 4;
for (int i = 0; i < replicaCount; i++) {
redisTemplate.opsForValue().set(
baseKey + ":" + i, value, ttlSeconds, TimeUnit.SECONDS);
}
}
}
15.4 缓存预热
@Component
public class CacheWarmUpRunner implements ApplicationRunner {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ProductMapper productMapper;
private final ExecutorService warmUpExecutor = Executors.newFixedThreadPool(4);
@Override
public void run(ApplicationArguments args) {
// 系统启动时异步预热热点数据
warmUpExecutor.submit(this::warmUpHotProducts);
}
private void warmUpHotProducts() {
List<Product> hotProducts = productMapper.selectHotProducts(1000);
for (Product product : hotProducts) {
String key = "product:" + product.getId();
long ttl = 30 + ThreadLocalRandom.current().nextInt(10);
redisTemplate.opsForValue().set(key,
JSON.toJSONString(product), ttl, TimeUnit.MINUTES);
}
}
}
15.5 监控与告警
@Service
public class RedisMonitorService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取Redis运行状态
*/
public Map<String, String> getRedisHealth() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info();
Map<String, String> health = new HashMap<>();
if (info != null) {
health.put("used_memory_human", info.getProperty("used_memory_human"));
health.put("connected_clients", info.getProperty("connected_clients"));
health.put("keyspace_hits", info.getProperty("keyspace_hits"));
health.put("keyspace_misses", info.getProperty("keyspace_misses"));
health.put("instantaneous_ops_per_sec",
info.getProperty("instantaneous_ops_per_sec"));
// 计算命中率
long hits = Long.parseLong(info.getProperty("keyspace_hits", "0"));
long misses = Long.parseLong(info.getProperty("keyspace_misses", "0"));
double hitRate = (hits + misses) > 0
? (double) hits / (hits + misses) * 100 : 0;
health.put("hit_rate", String.format("%.2f%%", hitRate));
}
return health;
}
/**
* 慢查询日志
*/
public List<Object> getSlowLog(int count) {
return redisTemplate.execute((RedisCallback<List<Object>>)
connection -> connection.slowLogGet(count));
}
}
15.6 生产环境CheckList
| 所有Key必须设置TTL | 防止内存无限增长 |
| 禁止使用 KEYS * | 使用 SCAN 替代 |
| 避免大Key(>10KB) | 拆分或使用Hash分片 |
| 连接池配置 | Lettuce: 共享连接; Jedis: maxTotal=32 |
| 序列化方式 | JSON(可读)或 Protobuf(性能) |
| 淘汰策略 | maxmemory-policy allkeys-lru |
| 持久化 | 主节点AOF(everysec) + 从节点RDB |
| 监控 | 内存/命中率/连接数/慢查询 |
| 安全 | 设置密码 + 禁用危险命令 + 绑定IP |
| 热Key | 本地缓存 + 多副本 |
| 雪崩防护 | 随机TTL + 多级缓存 + 限流 |
| 一致性 | 先更新DB再删缓存 + 延迟双删/Canal |







