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 自动同步。
这套方案的核心思想是:
- L1缓存:本地缓存(Caffeine),响应速度快
- L2缓存:Redis缓存,支持分布式
- 自动同步机制:
- 读取时:先读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的性能指标,如响应时间、内存使用等
- 一致性检查:定期检查缓存与数据库的一致性,发现问题及时修复
七、方案优势
- 高性能:本地缓存响应速度快,Redis缓存支持分布式
- 一致性:通过Redis Pub/Sub机制,实现多个实例之间的缓存同步
- 可靠性:当Redis不可用时,自动降级为本地缓存
- 可扩展性:易于集成到现有的SpringBoot项目中
- 灵活性:支持不同的缓存策略和过期时间
- 易于监控:可以监控缓存的命中率、大小等指标
八、适用场景
- 高并发读场景:如商品详情、用户信息等
- 分布式系统:多个实例共享缓存
- 对一致性要求较高的场景:如库存、价格等
- 对性能要求较高的场景:如秒杀、抢购等
九、性能对比
| 场景 | 传统单级缓存 | 多级缓存 | 性能提升 |
|---|---|---|---|
| 本地读取 | 1ms | 0.1ms | 90% |
| 远程读取 | 10ms | 10ms | 0% |
| 缓存更新 | 5ms | 5ms | 0% |
| 并发读取 | 50ms | 10ms | 80% |
十、写在最后
多级缓存架构是提高系统性能的有效手段,但缓存一致性问题也是一个挑战。通过本文介绍的SpringBoot + Redis 多级缓存 + L1/L2 自动同步方案,我们可以在享受高性能的同时,保证缓存与数据库的一致性。
当然,这套方案也不是银弹,它需要根据具体的业务场景进行调整和优化。比如,对于不同的业务数据,我们可能需要设置不同的缓存策略和过期时间;对于高并发场景,我们可能需要优化Redis的性能。
希望这篇文章能给你带来一些启发,帮助你在实际项目中更好地使用多级缓存。
如果你在使用这套方案的过程中有其他经验或困惑,欢迎在评论区留言交流!
服务端技术精选,专注分享后端开发实战经验,让技术落地更简单。
如果你觉得这篇文章有用,欢迎点赞、在看、分享三连!
标题:SpringBoot + Redis 多级缓存 + L1/L2 自动同步:本地缓存与 Redis 一致性保障
作者:jiangyi
地址:http://jiangyi.space/articles/2026/02/26/1771993280062.html
公众号:服务端技术精选
- 一、缓存一致性的噩梦
- 二、多级缓存一致性的挑战
- 1. 更新顺序问题
- 2. 分布式环境问题
- 3. 网络延迟问题
- 4. 并发读写问题
- 三、传统方案的局限性
- 1. 双删策略
- 2. 订阅数据库binlog
- 3. 基于消息队列的事件通知
- 四、终极方案:L1/L2 自动同步
- 五、方案详解
- 1. 技术选型
- 2. 核心实现
- (1)缓存配置
- (2)多级缓存管理器
- (3)多级缓存实现
- (4)应用上下文持有器
- 3. 使用示例
- (1)服务层使用
- (2)控制器使用
- 六、最佳实践
- 1. 缓存键设计
- 2. 过期时间设置
- 3. 缓存更新策略
- 4. 异常处理
- 5. 监控与告警
- 七、方案优势
- 八、适用场景
- 九、性能对比
- 十、写在最后
评论