李锋镝的博客

  • 首页
  • 时间轴
  • 插件
  • 评论区显眼包🔥
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
  • 留言
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

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

2025年11月4日 62点热度 1人点赞 0条评论

在高并发系统中,缓存是提升性能的核心手段,但单一缓存往往难以平衡“速度、容量、一致性”三者的需求。基于 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

相关文章

  • 从3秒到30毫秒!SpringBoot树形结构深度优化指南:不止于O(n)算法的全链路提速方案
  • SpringBoot日志链路追踪深度实战:基于 TraceId 打通全链路日志
  • Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
  • 重构 Controller 终极指南:从臃肿到优雅的 7 大黄金法则 + 实战技巧
  • 从万级到千万级:排行榜系统的6种实现方案深度解析(含原理、优化与实战)
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: Caffeine MySQL Redis SpringBoot 缓存
最后更新:2025年11月4日

李锋镝

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

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

位卑未敢忘忧国,事定犹须待阖棺。

那年今日(12月17日)

  • 1981年:德国足球运动员蒂姆·维泽出生
  • 1971年:印度和东巴基斯坦达成停火协议
  • 1909年:比利时国王利奥波德二世逝世
  • 1905年:狙击之王西蒙·海耶出生
  • 1902年:京师大学堂正式开学
  • 更多历史事件
最新 热点 随机
最新 热点 随机
AI原生数据库新标杆:seekdb深度解析,轻量架构与混合搜索的双重革命 做了一个WordPress文章热力图插件 Spring WebFlux底层原理深度剖析-从响应式流到事件循环的全链路拆解 Spring WebFlux深度解析:异步非阻塞架构与实战落地指南 规范驱动AI编程:用OpenSpec实现100%可控开发,从需求到代码的全流程闭环 WordPress网站换了个字体,差点儿把样式换崩了
玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?使用WireGuard在Ubuntu 24.04系统搭建VPNWordPress实现用户评论等级排行榜插件Gemini 3 Pro 深度测评:多模态AI编程的跨代际突破,从一句话到完整应用的全链路革命WordPress网站换了个字体,差点儿把样式换崩了
使用itext和freemarker来根据Html模板生成PDF文件,加水印、印章 项目中不用 redis 分布式锁,怎么防止用户重复提交? SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports JAVA线程池简析(JDK1.6) IDEA版本2020.*全局MAVEN配置 Gemini 3 深度解析:从像素级复刻到 AGI 雏形,多模态 AI 如何重构开发与创作?
标签聚合
JVM WordPress SQL 日常 K8s 架构 SpringBoot AI编程 MySQL ElasticSearch 多线程 分布式 数据库 AI JAVA docker 设计模式 Spring IDEA Redis
友情链接
  • Blogs·CN
  • Honesty
  • 临窗旋墨
  • 哥斯拉
  • 彬红茶日记
  • 志文工作室
  • 搬砖日记
  • 旧时繁华
  • 林羽凡
  • 瓦匠个人小站
  • 皮皮社
  • 知向前端
  • 蜗牛工作室
  • 韩小韩博客
  • 风渡言

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3