前言
对于系统查多改少的数据,可以通过缓存来提升系统的访问性能。一般情况下我们会采用 Redis ,但是如果仅仅依赖 Redis 很容易出现缓存雪崩的情况。为了防止缓存雪崩可以通过 Redis 高可用,主从+哨兵解决方案、本地 ehcache 缓存 + hystrix 限流&降级、Redis 持久化 等手段有效的防止缓存雪崩。其中同时使用本地缓存和Redis 缓存就是两级缓存。本文主要介绍的内容就是:在SpringBoot 项目下实现两级缓存,具体的介绍内容如下:
- 什么是缓存雪崩?
- 什么是两级缓存?
- SpringBoot 实现两级缓存解决方案。
- SpringBoot 集成 layering-cache 实现两级缓存
什么是缓存雪崩?
对于不了解什么是缓存雪崩小伙伴,这里在简单的介绍一下。如果您了解什么是缓存雪崩可以绕过该小节。
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
这就是缓存雪崩。

为了防止缓存雪崩,一般会采用方式如下:
事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
事中:本地 Ehcache 缓存 + Hystrix 限流&降级,避免 MySQL 被打死。
事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
缓存雪崩介绍引用 https://github.com/shishan100/Java-Interview-Advanced 中华石杉--互联网Java进阶面试训练营
什么是两级缓存?
一级缓存就是:本地缓存,二级缓存就是:Redis。当访问请求过来的时候,先从一级缓存获取数据,如果一级缓存不存在则从二级缓存中获取,从二级缓存获取到数据后在将其设置到一级缓存。
为什么两级缓存可以防止缓存雪崩?
当Redis 宕机后请求会获取本地缓存而不是直接走MySql 来防止缓存雪崩。还有一点是:Redis缓存操作会有一大部分的网络开销,使用本地缓存可以避免网络开销,提升系统的性能。
SpringBoot 实现两级缓存解决方案
Spring Cache
Spring Cache 是 Spring 默认提供的缓存解决方案,如果是单独使用 一级或二级缓存,比较容易实现。但是如果结合使用,需要自己处理缓存同步、二级缓存等功能。
Alibaba JetCache 框架
JetCache是一个基于Java的缓存系统封装,提供统一的API和注解来简化缓存的使用。JetCache提供了比SpringCache更加强大的注解,可以原生的支持TTL、两级缓存、分布式自动刷新,还提供了Cache接口用于手工缓存操作。当前有四个实现,RedisCache、TairCache(此部分未在github开源)、CaffeineCache(in memory)和一个简易的LinkedHashMapCache(in memory),要添加新的实现也是非常简单的。
缺点是:JetCache 没有提供默认 一级缓存和二级缓存同步,需要自己手动实现。
文档:https://github.com/alibaba/jetcache/wiki/Home_CN
Layering Cache 框架
layering-cache是一个支持分布式环境的多级缓存框架,使用方式和spring-cache类似,主要目的是在使用注解的时候支持配置过期时间。layering-cache其实是一个两级缓存,一级缓存使用Caffeine作为本地缓存,二级缓存使用redis作为集中式缓存。并且基于redis的Pub/Sub做缓存的删除,所以它是一个适用于分布式环境下的一个缓存系统。
支持 支持缓存监控统计 支持缓存过期时间在注解上直接配置 支持二级缓存的自动刷新(当缓存命中并发现缓存将要过期时会开启一个异步线程刷新缓存) 刷新缓存分为强刷新和软刷新,强刷新直接调用缓存方法,软刷新直接改缓存的时间 缓存Key支持SpEL表达式 新增FastJsonRedisSerializer,KryoRedisSerializer序列化,重写String序列化。支持同一个缓存名称设置不同的过期时间 输出INFO级别的监控统计日志 二级缓存是否允许缓存NULL值支持配置 二级缓存空值允许配置时间倍率
综合 Spring Cache、Alibaba JetCache、Layering Cache 个人推荐使用功能更全的 Layering Cache,接下来正式开启 SpringBoot 集成 layering-cache 实战 !
SpringBoot 集成 layering-cache 实战
application.properties 配置
spring.layering-cache.stats=true
spring.layering-cache.namespace=layering-cache
spring.layering-cache.layeringCacheServletEnabled=true
spring.layering-cache.urlPattern=/layering-cache/*
spring.layering-cache.loginUsername=admin
spring.layering-cache.loginPassword=123456
spring.layering-cache.enableUpdate=true
spring.redis.host=localhost
spring.redis.port=6379
- spring.layering-cache.stats = 是否开启缓存统计 true 是 false 否
- spring.layering-cache.namespace = 命名空间,必须唯一般使用服务名
- spring.layering-cache.layeringCacheServletEnabled = 是否开启内置的监控页面
- spring.layering-cache.urlPattern = 内置的监控页面访问路径
- spring.layering-cache.loginUsername = 内置的监控登录用户账号
- spring.layering-cache.loginPassword = 内置的监控登录用户密码
- spring.layering-cache.enableUpdate = 内置的监控是否启用更新权限
引入依赖:
com.github.xiaolyuhlayering-cache-starter2.0.8
org.springframework.bootspring-boot-starter-data-redis
org.springframework.bootspring-boot-starter-aop
com.alibabafastjson1.2.22
配置RedisTemplate:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
return createRedisTemplate(redisConnectionFactory);
}
public RedisTemplate createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//Map
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();return redisTemplate;
}
}
使用 layering 包中的 @Cacheable @CachePut @CatchEvict 声明一级和一级缓存配置
具体缓存操作 UserService
@Service
public class UserService {
private Logger log = LoggerFactory.getLogger(UserService.class);
/**
* 根据用户Id 获取用户信息
* key:[user:info+userId值] value: [User]
* @param userId
* @return
*/
@Cacheable(value = "user:info", depict = "用户信息缓存", key = "#userId",
firstCache = @FirstCache(expireTime = 2, timeUnit = TimeUnit.SECONDS),
secondaryCache = @SecondaryCache(expireTime = 30, preloadTime = 3, forceRefresh = true, timeUnit = TimeUnit.SECONDS))
public User getUserById(String userId) {
log.info("userId:{}未走缓存",userId);
return new User(userId,"zhuoqianmingyue"+userId, 1);
}
/**
* key:[user:info+userId值] value: [User]
* 根据用户Id 设置缓存
* @param userId
* @return
*/
@CachePut(value = "user:info", key = "#userId", depict = "用户信息缓存",
firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS),
secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, timeUnit = TimeUnit.SECONDS))
public User saveUser(String userId) {
log.info("userId:{}设置缓存",userId);
return new User(userId,"zhuoqianmingyue"+userId, 1);
}
/**
* key:[user:info+userId值] value: [User]
* 根据用户Id 删除缓存
* @param userId
*/
@CacheEvict(value = "user:info", key = "#userId")
public void evictUser(String userId) {
log.info("删除userId:{}缓存",userId);
}
/**
* 删除所有用户
*/
@CacheEvict(value = "user:info", allEntries = true)
public void evictAllUser() {
}
}
前台调用Service 的Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUserById(@PathVariable String id){
return userService.getUserById(id);
}
@GetMapping("/save/{id}")
public User saveUser(@PathVariable String id){
return userService.saveUser(id);
}
@GetMapping("/delete/{id}")
public void deleteUser(@PathVariable String id){
userService.evictUser(id);
}
@GetMapping("/deleteAll")
public void deleteAllUser(@PathVariable String id){
userService.evictAllUser();
}
}
访问内置监控

具体参数说明请查看:文档:https://github.com/xiaolyuh/layering-cache/wiki/文档
小结
本文介绍了什么是缓存雪崩、2级缓存概念与操作流程、 SpringBoot 实现两级缓存解决方案以及 SpringBoot 集成 layering-cache 实战操作。关于 layering-cache 使用注意事项、原理等其他操作请参看:https://github.com/xiaolyuh/layering-cache。
参考文献
- https://github.com/xiaolyuh/layering-cache
- https://github.com/alibaba/jetcachehttps://github.com/zhuoqianmingyue/Java-Interview-Advanced/blob/master/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md
- https://github.com/alibaba/jetcache/wiki/Home_CN
更多推荐