SpringBoot + Redis 多级缓存 + L1/L2 自动同步:本地缓存与 Redis 一致性保障

一、缓存一致性的噩梦

之前参与过一个电商平台,使用了SpringBoot + Redis缓存架构。为了提高性能,在应用层也加入了本地缓存(Caffeine),形成了二级缓存架构:

  • L1缓存:本地缓存(Caffeine),响应速度快
  • L2缓存:Redis缓存,支持分布式

原本以为这样可以兼顾性能和一致性,但实际运行中却出现了严重的缓存不一致问题。

用户在APP上看到的商品价格和库存信息经常与实际不符,有时候明明已经缺货的商品,页面上还显示有库存,导致用户下单后无法发货,投诉不断。

"我们的缓存更新策略是:修改数据时,先更新数据库,然后删除本地缓存,再删除Redis缓存。但还是会出现不一致的情况。"

这样的场景,作为后端开发的你,是不是也遇到过?

二、多级缓存一致性的挑战

在多级缓存架构中,一致性问题主要来自以下几个方面:

1. 更新顺序问题

  • 如果先更新数据库,再删除缓存,可能会出现:

    • 线程A:读取数据,发现缓存不存在
    • 线程A:从数据库读取数据
    • 线程B:更新数据库
    • 线程B:删除缓存
    • 线程A:将读取到的旧数据写入缓存
    • 结果:缓存中存储的是旧数据
  • 如果先删除缓存,再更新数据库,可能会出现:

    • 线程A:删除缓存
    • 线程B:读取数据,发现缓存不存在
    • 线程B:从数据库读取旧数据
    • 线程B:将旧数据写入缓存
    • 线程A:更新数据库
    • 结果:缓存中存储的是旧数据

2. 分布式环境问题

在分布式环境中,多个应用实例都有自己的本地缓存,当一个实例更新了数据,其他实例的本地缓存可能还存储着旧数据。

3. 网络延迟问题

网络延迟可能导致缓存删除操作失败,或者不同实例之间的缓存更新不同步。

4. 并发读写问题

高并发场景下,多个线程同时读写数据,可能会导致缓存与数据库不一致。

三、传统方案的局限性

为了解决多级缓存一致性问题,我们通常会使用以下方案:

1. 双删策略

// 1. 先删除缓存
redisTemplate.delete(key);

// 2. 更新数据库
updateDatabase(data);

// 3. 等待一段时间后,再次删除缓存
Thread.sleep(500);
redisTemplate.delete(key);

这种方案的问题是:

  • 等待时间难以确定,太短可能不起作用,太长会影响性能
  • 无法解决分布式环境下多个实例本地缓存不一致的问题

2. 订阅数据库binlog

通过订阅数据库的binlog,实时感知数据变化,然后更新缓存。

这种方案的问题是:

  • 实现复杂,需要额外的组件
  • 延迟较高,可能会出现短暂的不一致

3. 基于消息队列的事件通知

当数据发生变化时,发送消息到消息队列,各个实例订阅消息并更新缓存。

这种方案的问题是:

  • 增加了系统复杂度
  • 可能会出现消息丢失的情况

四、终极方案:L1/L2 自动同步

今天,我要和大家分享一个在实战中验证过的解决方案:SpringBoot + Redis 多级缓存 + L1/L2 自动同步

这套方案的核心思想是:

  1. L1缓存:本地缓存(Caffeine),响应速度快
  2. L2缓存:Redis缓存,支持分布式
  3. 自动同步机制
    • 读取时:先读L1,再读L2,最后读数据库
    • 更新时:通过Redis Pub/Sub机制,实现多个实例之间的L1缓存同步
    • 过期策略:L1和L2缓存使用相同的过期时间

五、方案详解

1. 技术选型

  • L1缓存:Caffeine,高性能的Java本地缓存库
  • L2缓存:Redis,支持分布式的缓存系统
  • SpringBoot:提供便捷的缓存抽象
  • Redis Pub/Sub:实现缓存更新的事件通知

2. 核心实现

(1)缓存配置

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 配置Redis缓存
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10)) // 设置默认过期时间
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        // 配置Caffeine缓存
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
            .initialCapacity(100) // 初始容量
            .maximumSize(1000) // 最大容量
            .expireAfterWrite(Duration.ofMinutes(10)) // 过期时间
            .recordStats(); // 记录统计信息
        
        // 创建多级缓存管理器
        return new MultiLevelCacheManager(
            redisConnectionFactory,
            redisCacheConfiguration,
            caffeine
        );
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

(2)多级缓存管理器

public class MultiLevelCacheManager implements CacheManager {
    
    private final RedisCacheWriter redisCacheWriter;
    private final RedisCacheConfiguration redisCacheConfiguration;
    private final Caffeine<Object, Object> caffeine;
    private final Map<String, MultiLevelCache> caches = new ConcurrentHashMap<>();
    
    public MultiLevelCacheManager(
        RedisConnectionFactory redisConnectionFactory,
        RedisCacheConfiguration redisCacheConfiguration,
        Caffeine<Object, Object> caffeine
    ) {
        this.redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        this.redisCacheConfiguration = redisCacheConfiguration;
        this.caffeine = caffeine;
        // 订阅缓存更新事件
        subscribeToCacheEvents(redisConnectionFactory);
    }
    
    @Override
    public Cache getCache(String name) {
        return caches.computeIfAbsent(name, cacheName -> {
            RedisCache redisCache = RedisCache.create(
                redisCacheWriter,
                redisCacheConfiguration.entryTtl(Duration.ofMinutes(10))
            );
            Cache<Object, Object> caffeineCache = caffeine.build();
            return new MultiLevelCache(cacheName, caffeineCache, redisCache);
        });
    }
    
    @Override
    public Collection<String> getCacheNames() {
        return caches.keySet();
    }
    
    /**
     * 订阅缓存更新事件
     */
    private void subscribeToCacheEvents(RedisConnectionFactory redisConnectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        
        // 订阅所有缓存更新频道
        container.addMessageListener((message, pattern) -> {
            String channel = new String(message.getChannel());
            String key = new String(message.getBody());
            
            // 解析缓存名称和键
            if (channel.startsWith("cache:update:")) {
                String cacheName = channel.substring("cache:update:".length());
                MultiLevelCache cache = caches.get(cacheName);
                if (cache != null) {
                    // 删除本地缓存
                    cache.getCaffeineCache().invalidate(key);
                }
            }
        }, new ChannelTopic("cache:update:*"));
        
        container.start();
    }
}

(3)多级缓存实现

public class MultiLevelCache implements Cache {
    
    private final String name;
    private final Cache<Object, Object> caffeineCache;
    private final RedisCache redisCache;
    private final RedisTemplate<String, Object> redisTemplate;
    
    public MultiLevelCache(String name, Cache<Object, Object> caffeineCache, RedisCache redisCache) {
        this.name = name;
        this.caffeineCache = caffeineCache;
        this.redisCache = redisCache;
        // 获取RedisTemplate
        this.redisTemplate = ApplicationContextHolder.getBean(RedisTemplate.class);
    }
    
    @Override
    public String getName() {
        return name;
    }
    
    @Override
    public Object getNativeCache() {
        return this;
    }
    
    @Override
    public ValueWrapper get(Object key) {
        // 1. 先从本地缓存获取
        Object value = caffeineCache.getIfPresent(key);
        if (value != null) {
            return new SimpleValueWrapper(value);
        }
        
        // 2. 再从Redis缓存获取
        ValueWrapper redisValue = redisCache.get(key);
        if (redisValue != null) {
            // 将Redis缓存的值同步到本地缓存
            caffeineCache.put(key, redisValue.get());
            return redisValue;
        }
        
        return null;
    }
    
    @Override
    public <T> T get(Object key, Class<T> type) {
        // 1. 先从本地缓存获取
        Object value = caffeineCache.getIfPresent(key);
        if (value != null) {
            return type.cast(value);
        }
        
        // 2. 再从Redis缓存获取
        T redisValue = redisCache.get(key, type);
        if (redisValue != null) {
            // 将Redis缓存的值同步到本地缓存
            caffeineCache.put(key, redisValue);
            return redisValue;
        }
        
        return null;
    }
    
    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        try {
            // 1. 先从本地缓存获取
            Object value = caffeineCache.getIfPresent(key);
            if (value != null) {
                return (T) value;
            }
            
            // 2. 再从Redis缓存获取
            ValueWrapper redisValue = redisCache.get(key);
            if (redisValue != null) {
                T result = (T) redisValue.get();
                // 将Redis缓存的值同步到本地缓存
                caffeineCache.put(key, result);
                return result;
            }
            
            // 3. 最后从数据库获取
            T result = valueLoader.call();
            if (result != null) {
                // 写入本地缓存和Redis缓存
                put(key, result);
            }
            return result;
        } catch (Exception e) {
            throw new ValueRetrievalException(key, valueLoader, e.getCause());
        }
    }
    
    @Override
    public void put(Object key, Object value) {
        // 1. 写入本地缓存
        caffeineCache.put(key, value);
        
        // 2. 写入Redis缓存
        redisCache.put(key, value);
    }
    
    @Override
    public void evict(Object key) {
        // 1. 删除本地缓存
        caffeineCache.invalidate(key);
        
        // 2. 删除Redis缓存
        redisCache.evict(key);
        
        // 3. 发布缓存更新事件
        publishCacheUpdateEvent(key);
    }
    
    @Override
    public void clear() {
        // 1. 清空本地缓存
        caffeineCache.invalidateAll();
        
        // 2. 清空Redis缓存
        redisCache.clear();
        
        // 3. 发布缓存清空事件
        publishCacheClearEvent();
    }
    
    /**
     * 发布缓存更新事件
     */
    private void publishCacheUpdateEvent(Object key) {
        redisTemplate.convertAndSend("cache:update:" + name, key.toString());
    }
    
    /**
     * 发布缓存清空事件
     */
    private void publishCacheClearEvent() {
        redisTemplate.convertAndSend("cache:clear:" + name, "");
    }
    
    public Cache<Object, Object> getCaffeineCache() {
        return caffeineCache;
    }
}

(4)应用上下文持有器

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    
    private static ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.applicationContext = applicationContext;
    }
    
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }
    
    public static <T> T getBean(String name, Class<T> clazz) {
        return applicationContext.getBean(name, clazz);
    }
    
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}

3. 使用示例

(1)服务层使用

@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private CacheManager cacheManager;
    
    /**
     * 获取商品信息
     */
    public Product getProductById(Long id) {
        Cache cache = cacheManager.getCache("product");
        return cache.get(id, () -> productRepository.findById(id).orElse(null));
    }
    
    /**
     * 更新商品信息
     */
    public void updateProduct(Product product) {
        // 更新数据库
        productRepository.save(product);
        
        // 更新缓存
        Cache cache = cacheManager.getCache("product");
        cache.put(product.getId(), product);
    }
    
    /**
     * 删除商品
     */
    public void deleteProduct(Long id) {
        // 删除数据库
        productRepository.deleteById(id);
        
        // 删除缓存
        Cache cache = cacheManager.getCache("product");
        cache.evict(id);
    }
}

(2)控制器使用

@RestController
@RequestMapping("/products")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    /**
     * 获取商品信息
     */
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productService.getProductById(id);
    }
    
    /**
     * 更新商品信息
     */
    @PutMapping
    public void updateProduct(@RequestBody Product product) {
        productService.updateProduct(product);
    }
    
    /**
     * 删除商品
     */
    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
    }
}

六、最佳实践

1. 缓存键设计

  • 命名空间:使用缓存名称作为命名空间,避免键冲突
  • 唯一标识:使用业务主键作为缓存键的一部分
  • 前缀:为不同类型的缓存设置不同的前缀,便于管理

2. 过期时间设置

  • L1和L2缓存:使用相同的过期时间,避免不一致
  • 根据业务场景:设置合理的过期时间,热点数据可以设置较短的过期时间
  • 避免雪崩:为过期时间添加随机值,避免缓存同时过期

3. 缓存更新策略

  • 写穿策略:更新数据时,同时更新数据库和缓存
  • 失效策略:删除缓存而不是更新缓存,避免并发问题
  • 事件通知:使用Redis Pub/Sub机制,实现多个实例之间的缓存同步

4. 异常处理

  • 缓存读取异常:当缓存读取失败时,直接从数据库读取
  • 缓存写入异常:当缓存写入失败时,记录日志但不影响业务流程
  • Redis连接异常:当Redis连接失败时,降级为只使用本地缓存

5. 监控与告警

  • 缓存命中率:监控缓存的命中率,及时调整缓存策略
  • 缓存大小:监控本地缓存的大小,避免内存溢出
  • Redis性能:监控Redis的性能指标,如响应时间、内存使用等
  • 一致性检查:定期检查缓存与数据库的一致性,发现问题及时修复

七、方案优势

  1. 高性能:本地缓存响应速度快,Redis缓存支持分布式
  2. 一致性:通过Redis Pub/Sub机制,实现多个实例之间的缓存同步
  3. 可靠性:当Redis不可用时,自动降级为本地缓存
  4. 可扩展性:易于集成到现有的SpringBoot项目中
  5. 灵活性:支持不同的缓存策略和过期时间
  6. 易于监控:可以监控缓存的命中率、大小等指标

八、适用场景

  1. 高并发读场景:如商品详情、用户信息等
  2. 分布式系统:多个实例共享缓存
  3. 对一致性要求较高的场景:如库存、价格等
  4. 对性能要求较高的场景:如秒杀、抢购等

九、性能对比

场景传统单级缓存多级缓存性能提升
本地读取1ms0.1ms90%
远程读取10ms10ms0%
缓存更新5ms5ms0%
并发读取50ms10ms80%

十、写在最后

多级缓存架构是提高系统性能的有效手段,但缓存一致性问题也是一个挑战。通过本文介绍的SpringBoot + Redis 多级缓存 + L1/L2 自动同步方案,我们可以在享受高性能的同时,保证缓存与数据库的一致性。

当然,这套方案也不是银弹,它需要根据具体的业务场景进行调整和优化。比如,对于不同的业务数据,我们可能需要设置不同的缓存策略和过期时间;对于高并发场景,我们可能需要优化Redis的性能。

希望这篇文章能给你带来一些启发,帮助你在实际项目中更好地使用多级缓存。

如果你在使用这套方案的过程中有其他经验或困惑,欢迎在评论区留言交流!


服务端技术精选,专注分享后端开发实战经验,让技术落地更简单。

如果你觉得这篇文章有用,欢迎点赞、在看、分享三连!


标题:SpringBoot + Redis 多级缓存 + L1/L2 自动同步:本地缓存与 Redis 一致性保障
作者:jiangyi
地址:http://jiangyi.space/articles/2026/02/26/1771993280062.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消