李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 走心评论
  • 系列文章
  • Now
  • 每日心情
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

深度解析多级缓存架构:从设计到落地,彻底解决数据一致性难题

2025年11月4日 约 6,402 字22 分钟 27点热度 1人点赞 0条评论
本文最后更新于 2025年11月4日,距今已 277 天,其中的信息可能已经发生变化,请注意甄别。

在高并发系统中,缓存是提升性能的核心手段,但单一缓存往往难以平衡“速度、容量、一致性”三者的需求。基于 Spring Boot 生态的 Caffeine 本地缓存 + Redis 分布式缓存 + MySQL 数据库 三级缓存架构,已成为行业标配——它能将查询延迟从 MySQL 的百毫秒级,降至 Redis 的毫秒级、Caffeine 的微秒级,吞吐量提升 10-100 倍。

但缓存层级越多,数据一致性问题越突出:本地缓存与分布式缓存不同步、缓存与数据库数据偏差、并发读写冲突……这些问题可能导致用户看到旧数据、业务逻辑异常。本文将从架构设计、一致性根源、解决方案、实战落地四个维度,详细拆解多级缓存的设计要点与一致性保障方案,结合 RocketMQ 事务消息、Spring Cache 抽象等技术,提供可直接落地的生产级方案。

一、先搞懂:为什么需要多级缓存?单级缓存的痛点

在设计多级缓存前,先明确“为什么不能只用 Redis 或本地缓存”——单级缓存的局限性决定了多级架构的必要性:

缓存类型 优点 缺点 适用场景
本地缓存(Caffeine) 访问速度最快(微秒级)、无网络开销 集群环境下节点缓存独立、容量受限(依赖应用内存) 热点数据(如首页商品、高频查询配置)
分布式缓存(Redis) 集群共享、容量大、支持持久化 网络开销(毫秒级)、集群运维复杂 全局共享数据(如用户信息、订单状态)
数据库(MySQL) 数据可靠(ACID)、支持复杂查询 访问最慢(百毫秒级)、高并发下易瓶颈 最终数据存储、复杂业务查询

多级缓存的核心价值:通过“层层递进”的缓存策略,兼顾速度、容量与一致性——热点数据存在本地缓存(最快),全局数据存在 Redis(共享),原始数据存在 MySQL(可靠),形成“快查优先、慢查兜底”的架构。

典型三级缓存查询链路(性能最优)

  1. 优先查询 Caffeine 本地缓存,命中则直接返回(微秒级);
  2. 本地缓存未命中,查询 Redis 分布式缓存,命中则更新本地缓存后返回(毫秒级);
  3. Redis 未命中,查询 MySQL 数据库,命中则更新 Redis 和本地缓存后返回(百毫秒级);
  4. 数据库未命中,返回空值(或默认值),并缓存空值避免缓存穿透。

性能对比(基于 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. 线程 1 查询数据,未命中缓存和数据库,开始查询 MySQL;
    2. 线程 2 同时更新数据,更新 MySQL → 删除 Redis 缓存;
    3. 线程 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);
    }
}

七、总结:多级缓存一致性的核心原则

多级缓存架构的设计与一致性保障,本质是“在性能与一致性之间找平衡”,核心原则可总结为:

  1. 更新策略:坚持 Cache Aside Pattern,“先更 DB,再删缓存,发通知”,避免并发冲突;
  2. 分布式一致性:用事务消息确保缓存失效通知可靠投递,集群节点同步清理本地缓存;
  3. 特殊场景防护:缓存穿透用“布隆过滤器 + 空值缓存”,缓存击穿用“互斥锁 + 热点 key 永不过期”,缓存雪崩用“过期时间随机化 + 熔断降级”;
  4. 兜底方案:定期全量同步缓存,确保最终一致性;
  5. 监控运维:重点监控缓存命中率、延迟、失效次数,快速定位问题。

没有放之四海而皆准的方案,实际落地时需根据业务场景调整——比如高频更新的数据可缩短缓存过期时间,低频更新的热点数据可设置永不过期+手动更新。通过本文的方案,可构建一个“高性能、高可用、最终一致”的多级缓存架构,支撑百万级甚至千万级 QPS 系统稳定运行。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/hou-duan/4557

推荐阅读

  • 缓存架构实战指南:6大核心缓存技术深度解析与落地方案
  • 从3秒到30毫秒!SpringBoot树形结构深度优化指南:不止于O(n)算法的全链路提速方案
  • Spring Boot 指定外部配置文件的方式
  • Spring Boot 配置加载优先级总结
  • 关于服务的探活端口和业务端口不一致有什么问题
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: Caffeine MySQL Redis SpringBoot 缓存
最后更新:2025年11月4日

岁月同一天 8 月 8 日

回望过去的今天,你在写什么

  • 4 年前 2022年8月8日
    减肥四个月~

    今年四月初开始每天跳绳减肥,到了今天刚好四个月了。从一开始的每天跳绳十分钟到半个小时,再从半个小时到一个小时,最后又稳定…

相关文章
  • CompletableFuture使用详解2025年5月28日
  • 使用RocketMQ时,服务启动过程中,Consumer在服务未启动时消费消息问题处理2022年6月23日
  • 从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志2020年3月19日
  • 本地部署 DeepSeek 模型并进行 Spring Boot 整合2025年2月16日
  • SpringBoot常用注解2025年5月28日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

九年没联系的女友突然问我借钱,我开口道:四十万行么?不够了再说话。”前女友很是高兴地回复说:“够用,还是我男朋友有办法啊,你放心,我日后一定还给你,九年多没见了你现在干啥呢,这么有钱?”我呵呵一笑:“没事,不够用您尽管说话,俺现在改做抵押贷款了。”

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
Kratos+ v1.1.14版本更新说明 Spring Boot 指定外部配置文件的方式 Spring Boot 配置加载优先级总结 Claude Fable 5(claude-fable-5)深度详解 如何通过命令查看Java应用内存中对象数量 关于服务的探活端口和业务端口不一致有什么问题
给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能AI时代,个人技术博客的出路在哪里?增加了两套复古皮肤-牛皮纸、千禧网页这个域名注册整整十年了,十年时间,真快啊WordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了
MybatisCodeHelperPro激活 解决kubectl exec -it xxxx-service-bfbd45bb9-ktvzj bash -n bit error: exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead See 'kubectl exec -h' for help and examples Apollo配置中心中的protalDB的作用是什么 k8s + docker + Jenkins使用Pipeline部署SpringBoot项目时Jenkins错误集锦 彻底搞懂mysql日志系统binlog,redolog,undolog Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
最近评论
Huo 发布于 8 小时前(08月07日) 挺有特色的主题,还是感觉 WP 的确是强大
李锋镝 发布于 21 小时前(08月07日) 没理解你想说啥
aboss 发布于 21 小时前(08月07日) 你的后台web-login?
李锋镝 发布于 22 小时前(08月07日) 这个专门的插件实现的功能更好更全,还能对接支付之类的
李锋镝 发布于 22 小时前(08月07日) 自定义登录地址是为了防止大部分机器人通过WP固定登录页面暴力破解用户账号密码
标签聚合
K8s 日常 Claude docker JVM WordPress Redis AI编程 多线程 SQL 架构 MySQL IDEA JAVA SpringBoot Spring AI 数据库 ElasticSearch 分布式
友情链接
  • Blogs·CN
  • 懋和道人
  • 旧时繁华
  • Honesty
  • 拾趣博客导航
  • 搬砖日记
  • 知向前端
  • 临窗旋墨
  • 皮皮社
  • 瓦匠个人小站
  • 志文工作室
  • 韩小韩博客
  • 风渡言
  • 彬红茶日记
  • 老张博客
  • Mr.Sun的博客
  • 韩情脉脉
  • 哥斯拉
  • 林羽凡

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

正在博友圈履约中

域名年龄

Theme Kratos+ By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号