在上篇 SpringBoot_redis使用实战(一)_docker环境 记录了
本文主要学习springboot中使用redis作为缓存的相关实战内容, 及各种缓存问题产生原因及解决方式,代码部分主要涉及
springboot和redis基础整合请参考: SpringBoot_redis使用实战(一)_docker环境
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
添加spring-boot-starter-cache 并开启@EnableCaching 会自动注入CacheManager的实现
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
}
@Cacheable(value = "demo-app:business-cache", key = "'account-id' + #id")
@Override
public Account findAccountByPrimaryKey(long id) {
return this.accountMapper.selectById(id);
}
效果:查询结果自动缓存到redis,重复查询同一个key,不会发起数据查询请求
@CachePut(value = "demo-app:business-cache", key = "'account-id:' + #account.id")
@Override
public Account updateAccount(Account account) {
this.accountMapper.updateById(account);
return account;
}
@CacheEvict(value = "demo-app:business-cache", key = "'account-id:' + #account.id")
@Override
public void deleteAccount(Account account) {
this.accountMapper.deleteById(account.getId());
}
@CacheConfig
作用在标注在类上,抽取缓存相关注解的公共配置,可抽取的公共配置有缓存名字、主键生成器等
@CacheConfig(cacheNames = RedisKeyConstant.CACHE_BUSINESS)
@Service("userService")
@Transactional(rollbackFor=Exception.class)
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
@Autowired
private AccountMapper accountMapper;
@CachePut(key = "'account-id:' + #account.id")
@Override
public Account updateAccount(Account account) {
this.accountMapper.updateById(account);
return account;
}
@CacheEvict(key = "'account-id:' + #account.id")
@Override
public void deleteAccount(Account account) {
this.accountMapper.deleteById(account.getId());
}
@Cacheable(key = "'account-id:' + #id")
@Override
public Account findAccountByPrimaryKey(long id) {
return this.accountMapper.selectById(id);
}
}
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
// 键序列化方式 redis字符串序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
// 值序列化方式 简单json序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));
return defaultCacheConfiguration;
}
不同类型缓存时长错开过期, 同类缓存加随机数
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 不同类型缓存---使用不同过期策略
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap();
// 系统缓存过期时间(s):24 * 60 * 60
cacheConfigMap.put(RedisKeyConstant.SYSTEM_CACHE, this.useJsonCacheConfiguration(Duration.ofSeconds(24 * 60 * 60)));
// 业务缓存过期时间(s):20 * 60
cacheConfigMap.put(RedisKeyConstant.BUSINESS_CACHE, this.useJsonCacheConfiguration(Duration.ofSeconds(20 * 60)));
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(this.useJsonCacheConfiguration())
.withInitialCacheConfigurations(cacheConfigMap)
.build();
return redisCacheManager;
}
private RedisCacheConfiguration useJsonCacheConfiguration(Duration ttl) {
RedisCacheConfiguration defaultCacheConfiguration = this.useJsonCacheConfiguration();
defaultCacheConfiguration.entryTtl(ttl);
return defaultCacheConfiguration;
}
/**使用json序列化的缓存配置*/
private RedisCacheConfiguration useJsonCacheConfiguration() {
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
// 键序列化方式 redis字符串序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
// 值序列化方式 简单json序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));
return defaultCacheConfiguration;
}
}
对于即将来临的大量请求,我们可以提前走一遍系统,将数据提前缓存在Redis中,并设置不同的过期时间。
TODO 代码
Redis的哨兵模式和集群模式,为防止Redis集群单节点故障,可以通过这两种模式实现高可用。
TODO 补充docker部署哨兵和集群模式
对于某个需要频繁获取的信息,缓存在Redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景是不适合的。
比如这个热点数据的过期时间是1h,那么每到59minutes时,通过定时任务去更新这个热点key,并重新设置其过期时间。
这是解决缓存击穿比较常用的方法。
互斥锁简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试
请求参数校验,明显错误直接拦截返回(比如ID的长度, 自增ID有效范围,明显不符合直接返回错误
对于某个查询为空的数据,可以将这个空结果进行Redis缓存,但是设置很短的过期时间,比如30s,可以根据实际业务设定。注意一定不要影响正常业务
CustomRedisCacheWriter必须实现RedisCacheWriter, 所有方法实现和DefaultRedisCacheWriter保持一致. put方法增加ObjectUtils.nullSafeEquals(value, RedisSerializer.java().serialize(NullValue.INSTANCE)空值判断. 为空设置ttl=30
/**
* 重写DefaultRedisCacheWriter.put方法-- 修改null数据缓存时间为20秒
* @see org.springframework.data.redis.cache.DefaultRedisCacheWriter
*/
public class CustomRedisCacheWriter implements RedisCacheWriter {
private final RedisConnectionFactory connectionFactory;
private final Duration sleepTime;
public CustomRedisCacheWriter(RedisConnectionFactory connectionFactory) {
this(connectionFactory, Duration.ZERO);
}
CustomRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
Assert.notNull(sleepTime, "SleepTime must not be null!");
this.connectionFactory = connectionFactory;
this.sleepTime = sleepTime;
}
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
// 缓存穿透--如果缓存为空值,使用快速过期时间
if (ObjectUtils.nullSafeEquals(value, RedisSerializer.java().serialize(NullValue.INSTANCE))) {
ttl = Duration.ofSeconds(30);
}
final Duration durationTtl = ttl;
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
Assert.notNull(value, "Value must not be null!");
this.execute(name, (connection) -> {
if (shouldExpireWithin(durationTtl)) {
connection.set(key, value, Expiration.from(durationTtl.toMillis(), TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.upsert());
} else {
connection.set(key, value);
}
return "OK";
});
}
// 其他方法省略,和DefaultRedisCacheWriter保持一致
}
生产环境建议:CustomRedisCacheWriter 其实会注册成单例bean, 可以定义nullValueTtl属性,由application.yaml传入, 或者使用nacos,可以达到动态设置效果.有兴趣可以自己试试. 另外这里ttl可以加上随机数,解决缓存雪崩的问题.
使用RedisCacheManager.builder(cacheWriter) 传入自定义CustomRedisCacheWriter的实现
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
@Bean
public RedisCacheManager cacheManager(CustomRedisCacheWriter cacheWriter) {
// 不同类型缓存---使用不同过期策略
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap();
// 系统缓存过期时间(s):24 * 60 * 60
cacheConfigMap.put(RedisKeyConstant.SYSTEM_CACHE, this.useJsonCacheConfiguration(Duration.ofSeconds(24 * 60 * 60)));
// 业务缓存过期时间(s):20 * 60
cacheConfigMap.put(RedisKeyConstant.BUSINESS_CACHE, this.useJsonCacheConfiguration(Duration.ofSeconds(20 * 60)));
RedisCacheManager redisCacheManager = RedisCacheManager.builder(cacheWriter)
.cacheDefaults(this.useJsonCacheConfiguration())
.withInitialCacheConfigurations(cacheConfigMap)
.build();
return redisCacheManager;
}
@Bean
public CustomRedisCacheWriter cacheWriter(RedisConnectionFactory redisConnectionFactory) {
return new CustomRedisCacheWriter(redisConnectionFactory);
}
private RedisCacheConfiguration useJsonCacheConfiguration(Duration ttl) {
RedisCacheConfiguration defaultCacheConfiguration = this.useJsonCacheConfiguration();
defaultCacheConfiguration.entryTtl(ttl);
return defaultCacheConfiguration;
}
/**使用json序列化的缓存配置*/
private RedisCacheConfiguration useJsonCacheConfiguration() {
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
// 键序列化方式 redis字符串序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
// 值序列化方式 简单json序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));
return defaultCacheConfiguration;
}
}
布隆过滤器是一种数据结构,利用极小的内存,可以判断大量的数据“一定不存在或者可能存在”。
对于缓存穿透,我们可以将查询的数据条件都哈希到一个足够大的布隆过滤器中,用户发送的请求会先被布隆过滤器拦截,一定不存在的数据就直接拦截返回了,从而避免下一步对数据库的压力
TODO
缓存降级是指缓存失效或者缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或者访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对业务的影响程度。
可选方案:
方案 | 说明 | 评价 |
---|---|---|
方案一:异常捕获 | 获取缓存的地方增加异常捕获逻辑,查询不到缓存中的配置, 查询数据库 | 需要业务中到处硬编码很不友好。 |
方案二: 多级缓存管理器 | 检测主缓存管理器不可用(redis连接超时),自动切换成另一款管理器(读内存/读默认/甚至无缓存) | 对业务入侵较小, 可以实现自动切换 |
/**
* 可降级的缓存管理器
*/
public class CacheDegradeManager implements CacheManager {
private static final Logger LOGGER = LoggerFactory.getLogger(CacheDegradeManager.class);
/**主缓存管理器*/
private final LinkedHashMap<String, CacheManager> mainCacheManagers;
/**主缓存不可用后管理器*/
private final LinkedHashMap<String, CacheManager> failCacheManagers;
private final AtomicBoolean activeStatus = new AtomicBoolean(false);
protected LinkedHashMap<String, CacheManager> cacheManagers;
public CacheDegradeManager(LinkedHashMap<String, CacheManager> mainCacheManagers, LinkedHashMap<String, CacheManager> failCacheManagers) {
this.mainCacheManagers = mainCacheManagers;
this.failCacheManagers = failCacheManagers;
// 启动使用主缓存管理器
this.cacheManagers = mainCacheManagers;
}
@Override
public Cache getCache(String name) {
for (CacheManager cacheManager : this.cacheManagers.values()) {
Cache cache = cacheManager.getCache(name);
if (cache != null) {
return cache;
}
}
return null;
}
@Override
public Collection<String> getCacheNames() {
Set<String> names = new LinkedHashSet<>();
for (CacheManager manager : this.cacheManagers.values()) {
names.addAll(manager.getCacheNames());
}
return Collections.unmodifiableSet(names);
}
public void up() {
if (!this.activeStatus.get()) {
LOGGER.info("【主缓存】连接正常,启用缓存[{}]", StringUtils.join(this.mainCacheManagers.keySet(), ","));
this.cacheManagers = this.mainCacheManagers;
this.activeStatus.set(true);
}
}
public void down() {
if (this.activeStatus.get()) {
LOGGER.warn("【主缓存】连接断开,缓存降级[{}]", StringUtils.join(this.failCacheManagers.keySet(), ","));
this.cacheManagers = this.failCacheManagers;
this.activeStatus.set(false);
}
}
public boolean isActive() {
return this.activeStatus.get();
}
public LinkedHashMap<String, CacheManager> getCacheManagers() {
return cacheManagers;
}
public void setCacheManagers(LinkedHashMap<String, CacheManager> cacheManagers) {
this.cacheManagers = cacheManagers;
}
}
/**
* 降级缓存监听器
*/
public class CacheDegradeListener implements InitializingBean {
private final LettuceConnectionFactory lettuceConnectionFactory;
private final CacheManager cacheManager;
public CacheDegradeListener(LettuceConnectionFactory lettuceConnectionFactory, CacheManager cacheManager) {
this.lettuceConnectionFactory = lettuceConnectionFactory;
this.cacheManager = cacheManager;
}
@Override
public void afterPropertiesSet() throws Exception {
if (!(this.cacheManager instanceof CacheDegradeManager)) {
return;
}
CacheDegradeManager onelinkCacheManager = (CacheDegradeManager) this.cacheManager;
this.lettuceConnectionFactory.getClientResources().eventBus().get().subscribe(event -> {
if (event instanceof ConnectionDeactivatedEvent || event instanceof ConnectionActivatedEvent) {
synchronized (CacheDegradeListener.class) {
if (event instanceof ConnectionDeactivatedEvent) {
onelinkCacheManager.down();
} else {
onelinkCacheManager.up();
}
}
}
// 开启监听后立马测试连接test connection。
this.lettuceConnectionFactory.getConnection().keyCommands().keys("test".getBytes(StandardCharsets.UTF_8));
});
}
}
/**
* 测试可降级缓存管理
*/
@Configuration
@EnableCaching
public class CacheDegradeConfig {
/**
* 可降级缓存监听
*/
@Bean
public CacheDegradeListener degradeCacheListener(LettuceConnectionFactory lettuceConnectionFactory, CacheManager cacheManager) {
return new CacheDegradeListener(lettuceConnectionFactory, cacheManager);
}
@Primary
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//主缓存管理器
LinkedHashMap<String, CacheManager> mainCacheManagers = new LinkedHashMap<>();
mainCacheManagers.put("RedisCache", this.createRedisCacheManager(redisConnectionFactory));
//失败缓存管理器
LinkedHashMap<String, CacheManager> failCacheManager = new LinkedHashMap<>();
failCacheManager.put("NoOpCache", new NoOpCacheManager());
return new CacheDegradeManager(mainCacheManagers, failCacheManager);
}
/**
* 缓存管理器
*/
@Bean
public RedisCacheManager createRedisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration defaultCacheConfiguration = this.useJsonCacheConfiguration();
RedisCacheConfiguration businessCacheConfiguration = this.useJsonCacheConfiguration(Duration.ofSeconds(20 * 60));
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap();
cacheConfigMap.put("demo-app:business-cache", businessCacheConfiguration);
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultCacheConfiguration)
.withInitialCacheConfigurations(cacheConfigMap)
.build();
return redisCacheManager;
}
@Bean
public CustomRedisCacheWriter cacheWriter(RedisConnectionFactory redisConnectionFactory) {
return new CustomRedisCacheWriter(redisConnectionFactory);
}
private RedisCacheConfiguration useJsonCacheConfiguration(Duration ttl) {
return this.useJsonCacheConfiguration().entryTtl(ttl);
}
/**使用json序列化的缓存配置*/
private RedisCacheConfiguration useJsonCacheConfiguration() {
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
// 键序列化方式 redis字符串序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
// 值序列化方式 简单json序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));
return defaultCacheConfiguration;
}
}