在高并发系统中,缓存是提升性能的核心手段,但单一缓存往往难以平衡“速度、容量、一致性”三者的需求。基于 Spring Boot 生态的 Caffeine 本地缓存 + Redis 分布式缓存 + MySQL 数据库 三级缓存架构,已成为行业标配——它能将查询延迟从 MySQL 的百毫秒级,降至 Redis 的毫秒级、Caffeine 的微秒级,吞吐量提升 10-100 倍。
但缓存层级越多,数据一致性问题越突出:本地缓存与分布式缓存不同步、缓存与数据库数据偏差、并发读写冲突……这些问题可能导致用户看到旧数据、业务逻辑异常。本文将从架构设计、一致性根源、解决方案、实战落地四个维度,详细拆解多级缓存的设计要点与一致性保障方案,结合 RocketMQ 事务消息、Spring Cache 抽象等技术,提供可直接落地的生产级方案。
一、先搞懂:为什么需要多级缓存?单级缓存的痛点
在设计多级缓存前,先明确“为什么不能只用 Redis 或本地缓存”——单级缓存的局限性决定了多级架构的必要性:
| 缓存类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地缓存(Caffeine) | 访问速度最快(微秒级)、无网络开销 | 集群环境下节点缓存独立、容量受限(依赖应用内存) | 热点数据(如首页商品、高频查询配置) |
| 分布式缓存(Redis) | 集群共享、容量大、支持持久化 | 网络开销(毫秒级)、集群运维复杂 | 全局共享数据(如用户信息、订单状态) |
| 数据库(MySQL) | 数据可靠(ACID)、支持复杂查询 | 访问最慢(百毫秒级)、高并发下易瓶颈 | 最终数据存储、复杂业务查询 |
多级缓存的核心价值:通过“层层递进”的缓存策略,兼顾速度、容量与一致性——热点数据存在本地缓存(最快),全局数据存在 Redis(共享),原始数据存在 MySQL(可靠),形成“快查优先、慢查兜底”的架构。
典型三级缓存查询链路(性能最优)
- 优先查询 Caffeine 本地缓存,命中则直接返回(微秒级);
- 本地缓存未命中,查询 Redis 分布式缓存,命中则更新本地缓存后返回(毫秒级);
- Redis 未命中,查询 MySQL 数据库,命中则更新 Redis 和本地缓存后返回(百毫秒级);
- 数据库未命中,返回空值(或默认值),并缓存空值避免缓存穿透。
性能对比(基于 10 万 QPS 压测):
| 查询链路 | 平均延迟 | 吞吐量(QPS) | 数据库压力 |
|---|---|---|---|
| 直接查询 MySQL | 150ms | 1000 | 100% |
| Redis + MySQL | 15ms | 10000 | 10% |
| Caffeine + Redis + MySQL | 2ms | 50000+ | 1% |
二、架构详解:每一层缓存的设计要点与配置实战
多级缓存的落地,关键在于每一层缓存的合理配置——参数设置不当会导致性能瓶颈或一致性问题。以下是生产级配置方案:
1. 第一层:Caffeine 本地缓存(热点数据提速)
Caffeine 是 Java 本地缓存的最优选择,性能远超 Guava Cache,支持多种过期策略,核心配置需关注“缓存大小”“过期时间”“淘汰策略”。
(1)核心配置与原理
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.Cache;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CaffeineConfig {
/**
* 本地缓存配置(热点商品缓存)
* 策略:最大 1 万条数据,写入后 5 分钟过期,基于 LRU 淘汰
*/
@Bean
public Cache productLocalCache() {
return new CaffeineCache("productCache",
Caffeine.newBuilder()
.maximumSize(10_000) // 最大缓存数量(避免内存溢出)
.expireAfterWrite(5, TimeUnit.MINUTES) // 写入后过期(适合更新不频繁的热点数据)
.expireAfterAccess(2, TimeUnit.MINUTES) // 访问后过期(适合长尾数据,避免占用内存)
.recordStats() // 记录缓存统计(命中率、失效次数等)
.build()
);
}
/**
* 本地缓存配置(用户信息缓存)
* 策略:最大 5 万条数据,写入后 30 分钟过期,基于 LFU 淘汰
*/
@Bean
public Cache userLocalCache() {
return new CaffeineCache("userCache",
Caffeine.newBuilder()
.maximumSize(50_000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.evictionListener((key, value, cause) ->
log.info("用户缓存失效:key={}, 原因={}", key, cause)
) // 失效监听器(便于排查)
.build()
);
}
}
(2)关键参数说明
- 淘汰策略:
LRU(Least Recently Used):淘汰最近最少使用的数据(适合热点数据稳定场景);LFU(Least Frequently Used):淘汰使用频率最低的数据(适合长尾数据场景);FIFO(First In First Out):按写入顺序淘汰(不推荐,易淘汰热点数据)。
- 过期策略:
expireAfterWrite:写入后固定时间过期(适合更新不频繁的数据,如商品详情);expireAfterAccess:最后一次访问后过期(适合访问频率不均的数据,如用户信息);expireAfter:自定义过期逻辑(如根据数据类型动态设置过期时间)。
2. 第二层:Redis 分布式缓存(全局共享)
Redis 作为分布式缓存,需解决“集群高可用”“持久化”“序列化”三大问题,避免缓存失效或数据丢失。
(1)生产级 Redis 配置
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.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RedisCacheConfig {
/**
* Redis 缓存管理器(支持不同缓存的差异化配置)
*/
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
// 默认缓存配置(全局通用)
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 默认过期时间 30 分钟
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
) // key 序列化(String)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
) // value 序列化(JSON,支持对象)
.disableCachingNullValues() // 不缓存 null 值(避免缓存穿透)
.prefixCacheNameWith("cache:") // 缓存 key 前缀(避免冲突);
// 差异化缓存配置(针对不同业务)
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
// 商品缓存:过期时间 1 小时,不序列化多余字段
cacheConfigs.put("productCache", defaultConfig.entryTtl(Duration.ofHours(1)));
// 用户缓存:过期时间 2 小时,支持快速失效
cacheConfigs.put("userCache", defaultConfig.entryTtl(Duration.ofHours(2)));
// 订单缓存:过期时间 15 分钟(更新频繁)
cacheConfigs.put("orderCache", defaultConfig.entryTtl(Duration.ofMinutes(15)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
(2)Redis 集群与持久化建议
- 集群模式:生产环境使用 Redis Cluster(3 主 3 从),确保高可用,避免单点故障;
- 持久化:开启 RDB + AOF 混合持久化(RDB 全量备份,AOF 增量备份),防止缓存数据丢失;
- 内存策略:配置
maxmemory-policy allkeys-lru(内存满时淘汰最近最少使用的 key),避免 OOM。
3. 第三层:MySQL 数据库(最终数据存储)
MySQL 作为缓存的“兜底”,需优化索引和读写分离,避免成为性能瓶颈:
- 索引优化:为缓存查询的关键字段(如
user.id、product.id)建立主键索引或唯一索引; - 读写分离:主库负责写操作,从库负责读操作(缓存未命中时的查询路由到从库);
- 分库分表:数据量超大时(千万级以上),采用分库分表分散压力(如 Sharding-JDBC)。
三、一致性问题根源:4 大场景+时序图解析
多级缓存的一致性问题,本质是“数据更新与缓存操作的时序不匹配”或“分布式环境下的信息不同步”。以下是 4 种核心场景及根源分析:
1. 场景 1:更新数据库后,缓存未更新(最常见)
- 操作时序:更新 MySQL → 未删除/更新 Redis → 未通知其他节点清理本地缓存;
- 后果:后续查询命中旧缓存,返回脏数据;
- 示例:用户修改昵称后,Redis 仍存储旧昵称,其他服务节点的本地缓存也未更新。
2. 场景 2:缓存更新失败(部分更新成功)
- 操作时序:更新 MySQL → 删除 Redis 成功 → 发送缓存失效消息失败;
- 后果:当前节点本地缓存已清理,但其他节点本地缓存仍为旧数据;
- 示例:服务 A 更新数据后,Redis 缓存已删除,但向 RocketMQ 发送消息时失败,服务 B、C 的本地缓存未清理。
3. 场景 3:并发读写冲突(高并发下必现)
- 操作时序:
- 线程 1 查询数据,未命中缓存和数据库,开始查询 MySQL;
- 线程 2 同时更新数据,更新 MySQL → 删除 Redis 缓存;
- 线程 1 查询 MySQL 成功,将旧数据写入 Redis 和本地缓存;
- 后果:线程 2 的更新操作被覆盖,缓存中存入旧数据,一致性被破坏。
4. 场景 4:缓存过期策略不合理
- 过期时间过短:缓存频繁失效,大量请求穿透到数据库,引发缓存雪崩;
- 过期时间过长:数据更新后,旧缓存长期存在,导致数据不一致。
四、解决方案:从策略到落地,彻底解决一致性问题
解决多级缓存一致性,核心原则是“最终一致性”(分布式系统中强一致性成本过高),通过“Cache Aside Pattern + 消息通知 + 特殊场景防护”三层方案实现。
1. 核心策略:Cache Aside Pattern(缓存旁路模式)
这是最成熟的缓存更新策略,核心思想是“数据库为主,缓存为辅”——缓存仅作为查询优化,不参与业务逻辑,更新时先保证数据库一致性,再同步缓存。
(1)查询操作流程(三级缓存联动)
import org.springframework.cache.Cache;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource(name = "userLocalCache")
private Cache userLocalCache;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
/**
* 查询用户信息(三级缓存联动)
*/
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
User user;
// 1. 查询本地缓存(Caffeine)
user = userLocalCache.get(cacheKey, User.class);
if (user != null) {
log.info("命中本地缓存,userId:{}", userId);
return user;
}
// 2. 查询分布式缓存(Redis)
String userJson = redisTemplate.opsForValue().get(cacheKey);
if (userJson != null) {
user = JSON.parseObject(userJson, User.class);
// 回写本地缓存(更新缓存,下次查询更快)
userLocalCache.put(cacheKey, user);
log.info("命中Redis缓存,userId:{}", userId);
return user;
}
// 3. 查询数据库(MySQL)
user = userMapper.selectById(userId);
if (user != null) {
// 写入Redis缓存(设置过期时间)
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
// 写入本地缓存
userLocalCache.put(cacheKey, user);
log.info("查询数据库并缓存,userId:{}", userId);
} else {
// 缓存空值(避免缓存穿透,设置较短过期时间)
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
userLocalCache.put(cacheKey, null);
log.info("用户不存在,缓存空值,userId:{}", userId);
}
return user;
}
}
(2)更新操作流程(保证一致性)
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Resource(name = "userLocalCache")
private Cache userLocalCache;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 更新用户信息(Cache Aside Pattern)
*/
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(User user) {
Long userId = user.getId();
String cacheKey = "user:" + userId;
try {
// 1. 先更新数据库(保证数据可靠性)
int rows = userMapper.updateById(user);
if (rows == 0) {
log.error("更新用户失败,用户不存在,userId:{}", userId);
return false;
}
// 2. 再删除Redis缓存(而非更新,避免并发冲突)
redisTemplate.delete(cacheKey);
log.info("删除Redis缓存,userId:{}", userId);
// 3. 清理当前节点本地缓存
userLocalCache.evict(cacheKey);
log.info("清理本地缓存,userId:{}", userId);
// 4. 发送事务消息,通知其他节点清理本地缓存
sendCacheInvalidateTxMsg(cacheKey);
log.info("发送缓存失效消息,userId:{}", userId);
return true;
} catch (Exception e) {
log.error("更新用户失败,userId:{}", userId, e);
// 事务回滚,数据库更新撤销,缓存无需额外处理
return false;
}
}
/**
* 发送事务消息(确保消息可靠发送)
*/
private void sendCacheInvalidateTxMsg(String cacheKey) {
// 事务消息:确保数据库更新成功后,消息才被投递
rocketMQTemplate.sendMessageInTransaction(
"cache-invalidate-tx-group", // 事务生产者组
"cache-invalidate-topic", // 消息主题
MessageBuilder.withPayload(cacheKey)
.setHeader("msgType", "USER_CACHE_INVALIDATE")
.build(),
null // 附加参数(无)
);
}
}
(3)关键设计思路
- 删除缓存而非更新:更新缓存可能导致并发冲突(如线程 A 更新缓存,线程 B 同时删除缓存),删除缓存让后续查询重新从数据库加载最新数据,更安全;
- 事务保障:数据库更新和事务消息发送原子性,避免“数据库更新成功,消息发送失败”;
- 空值缓存:防止缓存穿透,但需设置较短过期时间(如 5 分钟)。
2. 分布式一致性:RocketMQ 事务消息通知
在集群环境中,一个服务节点更新数据后,其他节点的本地缓存仍为旧数据——需通过消息队列发送“缓存失效通知”,让所有节点清理本地缓存。
(1)事务消息生产者(确保消息可靠)
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
/**
* 缓存失效事务消息监听器(确保数据库更新成功后,消息才提交)
*/
@RocketMQTransactionListener(txProducerGroup = "cache-invalidate-tx-group")
@Component
public class CacheInvalidateTxListener implements RocketMQLocalTransactionListener {
/**
* 执行本地事务(数据库更新已在业务方法中完成,此处仅校验)
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
String cacheKey = (String) msg.getPayload();
log.info("执行本地事务,校验缓存失效消息,cacheKey:{}", cacheKey);
// 此处可校验数据库数据是否更新成功(如查询用户最新信息)
return RocketMQLocalTransactionState.COMMIT; // 提交消息
} catch (Exception e) {
log.error("本地事务执行失败", e);
return RocketMQLocalTransactionState.ROLLBACK; // 回滚消息
}
}
/**
* 事务回查(解决消息状态未知问题)
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String cacheKey = (String) msg.getPayload();
log.info("事务回查,cacheKey:{}", cacheKey);
// 回查数据库,确认数据是否更新成功
String userId = cacheKey.split(":")[1];
User user = userMapper.selectById(Long.parseLong(userId));
if (user != null) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
(2)消息消费者(清理本地缓存)
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 消费缓存失效消息,清理本地缓存
*/
@RocketMQMessageListener(
topic = "cache-invalidate-topic",
consumerGroup = "cache-invalidate-consumer-group",
messageModel = MessageModel.CLUSTERING // 集群消费(每个节点都消费)
)
@Component
public class CacheInvalidateConsumer implements RocketMQListener<String> {
@Resource(name = "userLocalCache")
private Cache userLocalCache;
@Resource(name = "productLocalCache")
private Cache productLocalCache;
@Override
public void onMessage(String cacheKey) {
try {
log.info("收到缓存失效消息,cacheKey:{}", cacheKey);
// 根据缓存key前缀,清理对应本地缓存
if (cacheKey.startsWith("user:")) {
userLocalCache.evict(cacheKey);
log.info("清理用户本地缓存,cacheKey:{}", cacheKey);
} else if (cacheKey.startsWith("product:")) {
productLocalCache.evict(cacheKey);
log.info("清理商品本地缓存,cacheKey:{}", cacheKey);
}
} catch (Exception e) {
log.error("消费缓存失效消息失败,cacheKey:{}", cacheKey, e);
// 消息重试(RocketMQ默认重试16次,可配置)
throw new RuntimeException("消费失败,触发重试", e);
}
}
}
(3)消息重试与死信队列配置
- 重试策略:消费失败后,RocketMQ 会自动重试,重试间隔从 100ms 递增到 10s,避免瞬间重试压垮服务;
- 死信队列:重试 16 次仍失败的消息,进入死信队列(
%DLQ%cache-invalidate-consumer-group),后续人工处理; - 幂等消费:本地缓存清理是幂等操作(多次清理无副作用),无需额外处理幂等性。
3. 特殊场景防护:穿透、击穿、雪崩
即使实现了核心策略,仍需防护三种极端场景,避免缓存架构失效。
(1)缓存穿透(查询不存在的数据)
- 问题:恶意请求查询不存在的用户 ID(如
userId=999999),缓存未命中,所有请求穿透到数据库,导致数据库崩溃; - 解决方案:缓存空值 + 布隆过滤器;
布隆过滤器实现(Guava):
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
@Component
public class UserBloomFilter {
// 布隆过滤器:预计数据量 100 万,误判率 0.01
private BloomFilter<Long> bloomFilter;
@Autowired
private UserMapper userMapper;
@PostConstruct
public void initBloomFilter() {
// 1. 查询所有用户 ID(生产环境建议分批查询或从数据仓库同步)
List<Long> userIds = userMapper.selectAllUserIds();
// 2. 初始化布隆过滤器
bloomFilter = BloomFilter.create(
Funnels.longFunnel(),
userIds.size(),
0.01 // 误判率(越小,占用内存越大)
);
// 3. 将用户 ID 加入布隆过滤器
userIds.forEach(bloomFilter::put);
log.info("布隆过滤器初始化完成,用户总数:{}", userIds.size());
}
/**
* 判断用户是否可能存在(存在则返回 true,不存在则返回 false)
*/
public boolean mightContain(Long userId) {
return bloomFilter.mightContain(userId);
}
}
// 在查询方法中添加布隆过滤器校验
public User getUserById(Long userId) {
// 先通过布隆过滤器校验,不存在则直接返回 null
if (!userBloomFilter.mightContain(userId)) {
log.info("布隆过滤器校验不存在,userId:{}", userId);
return null;
}
// 后续查询逻辑...
}
(2)缓存击穿(热点 key 过期)
- 问题:热点数据(如首页爆款商品)缓存过期瞬间,大量请求直达数据库,导致数据库压力骤增;
- 解决方案:互斥锁 + 热点 key 永不过期;
Redis 互斥锁实现:
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
// 释放锁的 Lua 脚本(原子操作)
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private DefaultRedisScript<Long> releaseLockScript;
@PostConstruct
public void init() {
releaseLockScript = new DefaultRedisScript<>(RELEASE_LOCK_SCRIPT, Long.class);
}
/**
* 获取互斥锁
* @param lockKey 锁 key
* @param lockValue 锁值(避免误释放)
* @param expireTime 过期时间(避免死锁)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String lockValue, long expireTime) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS)
);
}
/**
* 释放互斥锁(Lua 脚本保证原子性)
* @param lockKey 锁 key
* @param lockValue 锁值
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String lockValue) {
Long result = redisTemplate.execute(
releaseLockScript,
Collections.singletonList(lockKey),
lockValue
);
return result != null && result > 0;
}
}
// 在查询方法中添加互斥锁
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
User user;
// 1. 查询本地缓存
user = userLocalCache.get(cacheKey, User.class);
if (user != null) {
return user;
}
// 2. 获取互斥锁(防止缓存击穿)
String lockKey = "lock:" + cacheKey;
String lockValue = UUID.randomUUID().toString();
boolean locked = false;
try {
// 尝试获取锁,过期时间 3 秒(避免死锁)
locked = redisLock.tryLock(lockKey, lockValue, 3);
if (locked) {
// 3. 再次查询 Redis(防止其他线程已更新缓存)
String userJson = redisTemplate.opsForValue().get(cacheKey);
if (userJson != null) {
user = JSON.parseObject(userJson, User.class);
userLocalCache.put(cacheKey, user);
return user;
}
// 4. 查询数据库并更新缓存
user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
userLocalCache.put(cacheKey, user);
} else {
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
userLocalCache.put(cacheKey, null);
}
} else {
// 未获取到锁,返回默认值或重试
log.warn("获取锁失败,userId:{}", userId);
return null;
}
} finally {
// 释放锁
if (locked) {
redisLock.releaseLock(lockKey, lockValue);
}
}
return user;
}
(3)缓存雪崩(大量缓存同时过期)
- 问题:同一时间大量缓存过期(如凌晨 2 点批量更新缓存),导致大量请求穿透到数据库,引发数据库雪崩;
- 解决方案:过期时间加随机值 + 缓存集群高可用 + 熔断降级;
过期时间随机化实现:
// 写入 Redis 时,添加随机过期时间(±5 分钟)
int baseExpire = 30; // 基础过期时间 30 分钟
int random = new Random().nextInt(10) - 5; // -5 到 +5 分钟
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), baseExpire + random, TimeUnit.MINUTES);
熔断降级(Sentinel 配置):
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class SentinelConfig {
@PostConstruct
public void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
// 数据库查询限流规则:QPS 不超过 1000
FlowRule dbRule = new FlowRule();
dbRule.setResource("userMapper.selectById"); // 资源名(MyBatis 方法名)
dbRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 按 QPS 限流
dbRule.setCount(1000); // 阈值 1000 QPS
rules.add(dbRule);
FlowRuleManager.loadRules(rules);
log.info("Sentinel 限流规则初始化完成");
}
}
五、实战优化:Spring Cache 抽象整合多级缓存
手动管理三级缓存代码繁琐,可通过 Spring Cache 抽象整合 Caffeine 和 Redis,实现“注解式缓存”,简化开发。
1. 自定义多级缓存管理器
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 自定义多级缓存管理器(Caffeine + Redis)
*/
@Component
public class MultiLevelCacheManager implements CacheManager {
private final CacheManager caffeineCacheManager;
private final CacheManager redisCacheManager;
public MultiLevelCacheManager(SimpleCacheManager caffeineCacheManager, RedisCacheManager redisCacheManager) {
this.caffeineCacheManager = caffeineCacheManager;
this.redisCacheManager = redisCacheManager;
}
@Override
public Cache getCache(String name) {
Cache caffeineCache = caffeineCacheManager.getCache(name);
Cache redisCache = redisCacheManager.getCache(name);
// 自定义 Cache 实现,优先查询 Caffeine,再查询 Redis
return new MultiLevelCache(name, caffeineCache, redisCache);
}
@Override
public Collection<String> getCacheNames() {
List<String> names = new ArrayList<>(caffeineCacheManager.getCacheNames());
names.addAll(redisCacheManager.getCacheNames());
return names;
}
/**
* 多级缓存实现
*/
private static class MultiLevelCache implements Cache {
private final String name;
private final Cache caffeineCache;
private final Cache redisCache;
public MultiLevelCache(String name, Cache caffeineCache, Cache redisCache) {
this.name = name;
this.caffeineCache = caffeineCache;
this.redisCache = redisCache;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
public ValueWrapper get(Object key) {
// 1. 查询 Caffeine
ValueWrapper caffeineValue = caffeineCache.get(key);
if (caffeineValue != null) {
return caffeineValue;
}
// 2. 查询 Redis
ValueWrapper redisValue = redisCache.get(key);
if (redisValue != null) {
// 回写 Caffeine
caffeineCache.put(key, redisValue.get());
return redisValue;
}
return null;
}
@Override
public <T> T get(Object key, Class<T> type) {
ValueWrapper wrapper = get(key);
return wrapper != null ? (T) wrapper.get() : null;
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper wrapper = get(key);
if (wrapper != null) {
return (T) wrapper.get();
}
try {
T value = valueLoader.call();
put(key, value);
return value;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void put(Object key, Object value) {
// 同时写入 Caffeine 和 Redis
caffeineCache.put(key, value);
redisCache.put(key, value);
}
@Override
public void evict(Object key) {
// 同时删除 Caffeine 和 Redis
caffeineCache.evict(key);
redisCache.evict(key);
}
@Override
public void clear() {
caffeineCache.clear();
redisCache.clear();
}
}
}
2. 注解式缓存使用
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 查询用户(注解式缓存,自动走多级缓存)
*/
@Cacheable(value = "userCache", key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
/**
* 更新用户(注解式缓存失效)
*/
@CacheEvict(value = "userCache", key = "#user.id")
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(User user) {
return userMapper.updateById(user) > 0;
}
}
六、监控与运维:确保缓存架构稳定运行
多级缓存的运维核心是“监控指标 + 日志排查”,需关注以下关键指标:
1. 核心监控指标(Prometheus + Grafana)
- 缓存命中率:Caffeine 命中率(目标 ≥ 95%)、Redis 命中率(目标 ≥ 90%);
- 缓存失效次数:异常增长可能是过期策略不合理或更新操作失败;
- 查询延迟:各层级缓存的查询延迟(Caffeine < 1ms,Redis < 10ms,MySQL < 100ms);
- 数据库压力:缓存未命中时的数据库 QPS、延迟;
2. 日志打印要点
- 缓存命中/未命中日志(便于分析命中率);
- 缓存更新/删除日志(便于排查一致性问题);
- 消息发送/消费日志(便于追踪缓存失效通知);
- 异常日志(如缓存更新失败、消息消费失败)。
3. 定期同步机制(最终一致性兜底)
即使前面的方案都生效,仍可能因极端情况(如消息丢失、服务宕机)导致数据不一致——需定期全量同步缓存:
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class CacheSyncTask {
@Autowired
private UserMapper userMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Resource(name = "userLocalCache")
private Cache userLocalCache;
/**
* 每日凌晨 3 点全量同步用户缓存(兜底方案)
*/
@Scheduled(cron = "0 0 3 * * ?")
public void syncUserCache() {
log.info("开始全量同步用户缓存");
long start = System.currentTimeMillis();
// 分批查询用户(避免一次性加载过多数据)
int pageSize = 1000;
int pageNum = 1;
while (true) {
List<User> users = userMapper.selectPage(pageNum, pageSize);
if (users.isEmpty()) {
break;
}
// 批量更新缓存
for (User user : users) {
String cacheKey = "user:" + user.getId();
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
userLocalCache.put(cacheKey, user);
}
pageNum++;
}
log.info("全量同步用户缓存完成,耗时{}ms", System.currentTimeMillis() - start);
}
}
七、总结:多级缓存一致性的核心原则
多级缓存架构的设计与一致性保障,本质是“在性能与一致性之间找平衡”,核心原则可总结为:
- 更新策略:坚持 Cache Aside Pattern,“先更 DB,再删缓存,发通知”,避免并发冲突;
- 分布式一致性:用事务消息确保缓存失效通知可靠投递,集群节点同步清理本地缓存;
- 特殊场景防护:缓存穿透用“布隆过滤器 + 空值缓存”,缓存击穿用“互斥锁 + 热点 key 永不过期”,缓存雪崩用“过期时间随机化 + 熔断降级”;
- 兜底方案:定期全量同步缓存,确保最终一致性;
- 监控运维:重点监控缓存命中率、延迟、失效次数,快速定位问题。
没有放之四海而皆准的方案,实际落地时需根据业务场景调整——比如高频更新的数据可缩短缓存过期时间,低频更新的热点数据可设置永不过期+手动更新。通过本文的方案,可构建一个“高性能、高可用、最终一致”的多级缓存架构,支撑百万级甚至千万级 QPS 系统稳定运行。
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接
文章评论