缓存概述
解决不同设备间速度不匹配问题。
互联网分层架构:降低数据库压力,提升系统整体性能,缩短访问时间。
高并发问题
- 缓存并发(击穿):缓存过期后将尝试从后端数据库获取数据
- 缓存穿透:不存在的 key,请求直接落库查询
- 缓存雪崩:缓存大面积失效,请求直接落库查询
需求说明
- 通过在方法上增加缓存注解,调用方法时根据指定 key 缓存返回数据,再次调用从缓存中获取
- 可通过注解指定不同的缓存时长
- 避免缓存击穿:缓存失效后使用互斥锁限制查库数量
- 避免缓存穿透:对于 null 支持短时间存储
- 避免缓存雪崩:可支持每个 key 增加随机时长
一、Spring Cache 整合 Redis 实现
利用 Spring Cache 处理 Redis 缓存数据
Spring Cache 注解@Cacheable
携带的sync()
属性可支持互斥锁限制单个线程处理,可避免缓存击穿
注意开启 Spring Cache 需要在配置类(或启动类)上增加@EnableCaching
1 :缓存管理器注入配置时,处理 缓存空间 cacheNames() / value()
与时长对应
可根据 配置或代码写死 指定不同 缓存空间 缓存时长
此方式以 缓存空间名 为标识区分时长,未配置的缓存空间走全局设定
点击查看代码 ExpandRedisConfig.java
# yml 配置expand-cache-config: ttl-map: '{"yml-ttl":1000,"hello":2000}'// 引入配置@Value("#{${expand-cache-config.ttl-map:null}}")private Map ttlMap;/** * 注入缓存管理器及处理配置中的缓存时长 */@Bean(BEAN_REDIS_CACHE_MANAGER)public RedisCacheManager expandRedisCacheManager(RedisConnectionFactory factory) { /* 使用 Jackson 作为值序列化处理器 FastJson 存在部分转换问题如:Set 存储后因为没有对应的类型保存无法转换为 JSONArray(实现 List ) 导致失败 */ ObjectMapper om = JsonUtil.createJacksonObjectMapper(); GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(om); // 配置key、value 序列化(解决乱码的问题) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() // key 使用 string 序列化方式 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8)) // value 使用 jackson 序列化方式 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer)) // 配置缓存空间名称前缀 .prefixCacheNameWith("spring:cache:") // 配置全局缓存过期时间 .entryTtl(Duration.ofMinutes(30L)); // 专门指定某些缓存空间的配置,如果过期时间,这里的 key 为缓存空间名称 Map configMap = new HashMap(); // 代码写死示例 configMap.put("world", config.entryTtl(Duration.ofSeconds(60))); Set<Map.Entry> entrySet = Optional.ofNullable(ttlMap).map(Map::entrySet).orElse(Collections.emptySet()); for (Map.Entry entry : entrySet) { // 指定特定缓存空间对应的过期时间 configMap.put(entry.getKey(), config.entryTtl(Duration.ofSeconds(entry.getValue()))); } RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(factory); // 使用自定义缓存管理器附带自定义参数随机时间,注意此处为全局设定,5-最小随机秒,30-最大随机秒 return new ExpandRedisCacheManager(redisCacheWriter, config, configMap, 5, 30);}
2 :在缓存空间名 cacheNames() / value()
中附带时间字符串
自定义缓存管理器继承
RedisCacheManager
,重写创建缓存处理器方法,拿到缓存空间名与缓存配置进行更新缓存时长处理
此方式以 缓存空间名中非指定时间部分 为标识区分时长,缓存空间名不指定时间走全局设定
使用示例:@Cacheable(cacheNames = "prefix#5m", cacheManager = "expandRedisCacheManager")
点击查看代码 ExpandRedisCacheManager.java
@Overrideprotected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { String theName = name; if (name.contains(NAME_SPLIT_SYMBOL)) { // 名称中存在#标记,修改实际名称,替换默认配置的缓存时长为指定缓存时长 String[] nameArr = name.split(NAME_SPLIT_SYMBOL); theName = nameArr[0]; Duration duration = TimeUtil.parseDuration(nameArr[1]); if (duration != null) { cacheConfig = cacheConfig.entryTtl(duration); } } // 使用自定义缓存处理器附带自定义参数随机时间,将注入的随机时间传递 return new ExpandRedisCache(theName, cacheWriter, cacheConfig, this.minRandomSecond, this.maxRandomSecond);}
3 :自定义缓存注解支持 Spring Cache
- 自定义注解 使用
@Cacheable
标识,可支持 Spring Cache 处理 - 自定义注解增加设置缓存时长的属性:
timeout() + unit()
,需要与注入注解的初始化配置方生效 - 自定义缓存注解过期时间初始化配置,利用 Spring Component Bean 获取到使用自定义注解的方法,利用反射获取注解属性并设置缓存空间过期时间;Map 处理,同一名称缓存空间将会出现替换情景
- 此处理实质仍是以缓存空间名
cacheNames() / value()
中非时间部分为标识区分时长
点击查看代码 ExpandCacheExpireConfig.java
/** * Spring Bean 加载后处理 * 获取所有 @Component 注解的 Bean 判断类中方法是否存在 @SpringCacheable 注解,存在进行过期时间设置 */@PostConstructpublic void init() { Map beanMap = beanFactory.getBeansWithAnnotation(Component.class); if (MapUtil.isEmpty(beanMap)) { return; } beanMap.values().forEach(item -> ReflectionUtils.doWithMethods(item.getClass(), method -> { ReflectionUtils.makeAccessible(method); putConfigTtl(method); }) ); expandRedisCacheManager.initializeCaches();}/** * 利用反射设置方法注解上配置的过期时间 * @param method 注解了自定义缓存的方法 */private void putConfigTtl(Method method) { ExpandCacheable annotation = method.getAnnotation(ExpandCacheable.class); if (annotation == null) { return; } String[] cacheNames = annotation.cacheNames(); if (ArrayUtil.isEmpty(cacheNames)) { cacheNames = annotation.value(); } // 反射获取缓存管理器初始化配置并设值 Map initialCacheConfiguration = (Map) ReflectUtil.getFieldValue(expandRedisCacheManager, "initialCacheConfiguration"); RedisCacheConfiguration defaultCacheConfig = (RedisCacheConfiguration) ReflectUtil.getFieldValue(expandRedisCacheManager, "defaultCacheConfig"); Duration ttl = Duration.ofSeconds(annotation.unit().toSeconds(annotation.timeout())); for (String cacheName : cacheNames) { initialCacheConfiguration.put(cacheName, defaultCacheConfig.entryTtl(ttl)); }}
4 :自定义缓存处理器,在设置缓存时处理时长
继承 Spring 缓存处理器
RedisCache
,重写设置缓存方法
可针对 null 进行短时间存储避免缓存穿透、增加随机时长避免缓存雪崩
点击查看代码 ExpandRedisCache.java
@Overridepublic void put(Object key, @Nullable Object value) { Object cacheValue = preProcessCacheValue(value); // 替换父类设置缓存时长处理 Duration duration = getDynamicDuration(cacheValue); cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), duration);}/** * 获取动态时长 */private Duration getDynamicDuration(Object cacheValue) { // 如果缓存值为 null,固定返回时长为 30s 避免缓存穿透 if (NullValue.INSTANCE.equals(cacheValue)) { return Duration.ofSeconds(30); } int randomInt = RandomUtil.randomInt(this.minRandomSecond, this.maxRandomSecond); return this.cacheConfig.getTtl().plus(Duration.ofSeconds(randomInt));}
小结
- 优点:使用 Spring 自带功能,通用性强
- 缺点:针对缓存空间处理缓存时长,缓存时间一致可能导致缓存雪崩,自定义处理需要理解相应源码实现
参考:
- Spring cache整合Redis,并给它一个过期时间!
- 让 @Cacheable 可配置 Redis 过期时间
- @Cacheable注解配合Redis设置缓存随机失效时间
- 聊聊如何基于spring @Cacheable扩展实现缓存自动过期时间以及自动刷新
- SpringBoot实现Redis缓存(SpringCache+Redis的整合)
二、自定义 AOP 实现
使用 Spring AOP 切面对注解拦截以支持方法调用结果进行缓存。
1. 注解属性定义
- 支持不同类型缓存 key:
key() + keyType()
- 支持依据条件( SpEL 表达式)设定排除不走缓存:
unless()
- 支持缓存 key 自定义过期时长( Redis 缓存):
timeout() + unit()
- 支持缓存 key 自定义过期时长增加随机时长( Redis 缓存):
addRandomDuration()
,注意固定了随机范围,可避免缓存雪崩 - 支持本地缓存设置:
useLocal() + localTimeout()
,注意本地缓存存在全局最大时长限制
2. AOP 切面处理
- 缓存存储数据时加锁(synchronized)执行,避免缓存击穿
- 对 null 值进行固定格式字符串缓存,避免缓存穿透
点击查看代码 MethodCacheAspect.java
/** * 利用 AOP 环绕通知对注解方法返回进行缓存处理 */@Around("@annotation(cn.eastx.practice.demo.cache.config.custom.MethodCacheable)")public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodCacheableOperation operation = MethodCacheableOperation.convert(joinPoint); if (Objects.isNull(operation)) { return joinPoint.proceed(); } Object result = getCacheData(operation); if (Objects.nonNull(result)) { return convertCacheData(result); } // 加锁处理同步执行 synchronized (operation.getKey().intern()) { result = getCacheData(operation); if (Objects.nonNull(result)) { return convertCacheData(result); } result = joinPoint.proceed(); setDataCache(operation, result); } return result;}/** * 设置数据缓存 * 特殊值缓存需要转换,特殊值包括 null * @param operation 操作对象 * @param data 数据 */private void setDataCache(MethodCacheableOperation operation, Object data) { // null缓存处理,固定存储时长,防止缓存穿透 if (Objects.isNull(data)) { redisUtil.setEx(operation.getKey(), NULL_VALUE, SPECIAL_VALUE_DURATION); return; } // 存在实际数据缓存处理 redisUtil.setEx(operation.getKey(), data, operation.getDuration()); if (Boolean.TRUE.equals(operation.getUseLocal())) { LocalCacheUtil.put(operation.getKey(), data, operation.getLocalDuration()); }}
小结
- 优点:自定义 Spring AOP 实现,可定制化处理程度较高,当前以支持两级缓存(Redis 缓存 + 本地缓存)
- 缺点:相对于 Spring 自带 Cache ,部分功能存在缺失不够完善
其他
本文 demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-cache
推荐阅读:
- 缓存那些事 – 美团技术团队 明辉
- 高并发之缓存 – 开拖拉机的蜡笔小新
作者:EastX
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/cnx01/p/16818865.html