当你负责的电商平台迎来流量峰值,商品详情页突然卡顿到无法加载,数据库连接池告警短信疯狂轰炸 —— 这不是恐怖片,而是很多开发者都经历过的 “缓存危机”。今天要分享的这套 “本地缓存 + Redis” 二级缓存方案,就像给系统装上了双重防护网,既能让响应速度飙到微秒级,又能稳稳扛住高并发冲击。
为什么单一缓存总在 “掉链子”?
做过高并发系统的开发者都懂:缓存是性能优化的 “核武器”,但用不好就会变成 “定时炸弹”。
纯本地缓存(比如 HashMap、Caffeine)虽然快如闪电,却像个 “孤岛”—— 多服务部署时数据无法共享,A 服务更新了数据,B 服务的缓存还是老版本,一致性问题让人头大。
纯 Redis 缓存解决了分布式共享问题,却躲不过 “远程调用延迟” 的坑。高并发下,哪怕单次 Redis 请求只花 10ms,数万用户同时访问时,延迟也会累积成秒级卡顿,甚至拖垮数据库。
更要命的是 “缓存老三样” 陷阱:
缓存穿透:黑客用无效 ID 疯狂请求,缓存和数据库都查不到,请求直接穿透到数据库,瞬间打满连接池;
缓存击穿:某个热点 Key(比如爆款商品 ID)突然过期,几万请求同时冲向数据库,瞬间压垮服务;
缓存雪崩:大量缓存在同一时间过期,请求像雪崩一样淹没数据库,系统直接瘫痪。
在电商商品详情、新闻资讯列表这些高频访问场景中,这些问题可能导致用户瞬间流失,损失难以估量。
二级缓存:让 “快” 与 “稳” 完美结合
所谓 “二级缓存”,就是在系统里搭起两层防护:
第一层:应用本地的缓存(本文用 Guava Cache 实现),像 “家门口的便利店”,随取随用,响应时间压到微秒级;
第二层:分布式 Redis 缓存,像 “中央仓库”,保证多服务数据一致;
查询链路:先查本地缓存→再查 Redis→最后查数据库,层层递进减少无效请求。
更新数据时,两层缓存会同步更新,既保证了速度,又解决了分布式一致性问题。
哪些场景最需要这套方案?
如果你遇到过这些痛点,那这套工具类简直是 “救星”:
高频读场景:商品详情、用户信息、配置参数等被频繁访问的数据,本地缓存能把响应时间从毫秒级压到微秒级;
分布式协同场景:库存扣减、订单状态同步等需要多服务共享状态的场景,Redis 能保证数据 “全局一致”;
缓存问题高发区:只要系统出现过热点 Key、数据库压力过大,或者被穿透 / 击穿 / 雪崩坑过,这套方案都能派上用场。
核心架构:3 个组件撑起 “双保险”
这套工具类采用 “注解驱动 + 自动初始化” 设计,不用写复杂配置,加个注解就能用。核心组件只有 3 个,却能撑起整套高可用逻辑:
1. @LocalCacheConfig:一行注解搞定缓存配置
这是缓存的 “控制面板”,在类上添一行注解,就能定义缓存名称、最大容量和过期时间,不用手动写初始化代码。
@LocalCacheConfig(value = "productCache", maxSize = 10000, expireSeconds = 300)
public class ProductService { ... }
value:缓存名称(必填,比如 "productCache" 对应商品缓存);
maxSize:最多存多少条数据(默认 5000,防止内存溢出);
expireSeconds:过期时间(默认 60 秒,自动清理旧数据)。
2. LocalCacheConfigProcessor:缓存的 “自动启动器”
作为 Spring 的 “幕后助手”,它会在系统启动时自动扫描带 @LocalCacheConfig 注解的类,帮你初始化好本地缓存实例。开发者啥都不用做,注解一加,缓存就 ready 了。
// 自动扫描注解并初始化缓存
@Component
public class LocalCacheConfigProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
LocalCacheConfig annotation = bean.getClass().getAnnotation(LocalCacheConfig.class);
if (annotation != null) {
// 自动初始化缓存
cacheManager.initLocalCache(annotation.value(), annotation.maxSize(), annotation.expireSeconds());
}
return bean;
}
}
3. TwoLevelCacheManager:缓存逻辑的 “大脑”
这是整套方案的核心,封装了本地缓存和 Redis 的所有操作,还内置了防穿透、击穿、雪崩的 “防御机制”。
防缓存击穿:给热点 Key"上锁"
当热点 Key 过期时,用 ReentrantLock 保证只有一个请求去查数据库,其他请求等待缓存更新,避免数据库 “单点过载”。
// 伪代码逻辑:加锁+双重检查
public T get(...) {
if (本地和Redis都没命中) {
ReentrantLock lock = 获取锁;
lock.lock();
try {
// 双重检查:防止锁等待期间缓存已更新
if (Redis仍未命中) {
查数据库→更新缓存;
}
} finally {
lock.unlock();
}
}
}
防缓存雪崩:给过期时间 “加随机”
如果大量缓存同时过期,就会引发 “雪崩”。解决方案是给每个缓存的过期时间加个随机偏移,让过期时间 “错落有致”。
// 过期时间=基础时间+随机偏移(比如300秒+5~30秒随机值)
long randomOffset = redisExpireSeconds > 10 ?
ThreadLocalRandom.current().nextLong(1, redisExpireSeconds / 10 + 1) : 1;
redisTemplate.opsForValue().set(key, value, redisExpireSeconds + randomOffset, TimeUnit.SECONDS);
防缓存穿透:对空值 “说不”
当请求无效数据(比如 ID=-1)时,数据库查不到结果,此时不缓存空值,直接返回,避免无效请求反复穿透到数据库。
5 分钟上手:实战使用示例
用起来超简单,只需两步:加注解、调 API。
第一步:在服务类上添加缓存配置
@Service
@LocalCacheConfig(value = "userCache", maxSize = 5000, expireSeconds = 180)
public class UserService {
@Autowired
private TwoLevelCacheManager cacheManager;
// ...
}
第二步:用缓存管理器查询数据
支持字符串、数字、日期、自定义对象甚至 List 等复杂类型,自动序列化 / 反序列化。
// 查询用户信息:先查本地缓存→Redis→数据库
public User getUserById(Long userId) {
return cacheManager.get(
"userCache", // 缓存名称
"user_" + userId, // 缓存Key
User.class, // 返回类型
300, // Redis过期时间(秒)
() -> userMapper.selectById(userId) // 数据库查询逻辑
);
}
// 查询用户列表(复杂类型示例)
public List<User> getVipUsers() {
return cacheManager.getList(
"userCache",
"vip_users",
User.class, // List元素类型
600,
() -> userMapper.selectVipUsers()
);
}
代码实现
MaskValue (注解)
package cn.ideamake.business.component.mask;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Barcke
* @version 1.0
* @projectName ideamake-framework
* @className MaskPhone
* @date 2025/8/13 15:50
* @slogan: 源于生活 高于生活
* @description: 是否需要加密返回值
**/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MaskValue {
Class<? extends MaskStrategy> value() default DefaultMaskPhoneStrategy.class;
}
MaskStrategy (脱敏策略)
package cn.ideamake.business.component.mask;
/**
* @author Barcke
* @version 1.0
* @projectName ideamake-framework
* @className MaskStrategy
* @date 2025/8/13 15:55
* @slogan: 源于生活 高于生活
* @description:
**/
public interface MaskStrategy {
/**
* 数据脱敏
* @return
*/
Object mask(Object value);
/**
* @return true 表示需要脱敏
*/
boolean shouldMask(Object value);
}
MaskResponseAdvice (响应体脱敏)
package cn.ideamake.business.component.mask;
import cn.hutool.extra.spring.SpringUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Deque;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Barcke
* @version 优化版
* @projectName ideamake-framework
* @className MaskResponseAdvice
* @date 2025/8/13 15:51
* @slogan: 源于生活 高于生活
* @description: 响应体脱敏处理,优化实现
**/
@RestControllerAdvice
@Slf4j
public class MaskResponseAdvice implements ResponseBodyAdvice<Object> {
// 缓存方法是否需要脱敏,提升性能
private static final Map<MethodParameter, Boolean> METHOD_MASK_NEEDED_CACHE = new ConcurrentHashMap<>();
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 全部 Controller 返回都拦截
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body == null) {
return null;
}
// 优先从缓存读取是否需要脱敏
Boolean needMask = METHOD_MASK_NEEDED_CACHE.get(returnType);
if (Boolean.FALSE.equals(needMask)) {
return body;
}
// 检测并执行脱敏
MaskDetectResult detectResult = new MaskDetectResult();
maskObject(body, new IdentityHashMap<>(), detectResult);
// 首次检测后缓存结果
if (needMask == null) {
METHOD_MASK_NEEDED_CACHE.put(returnType, detectResult.needMask);
}
return body;
}
/**
* 脱敏处理,防止循环引用,提升性能和可读性
*/
private void maskObject(Object rootObj, Map<Object, Boolean> visited, MaskDetectResult detectResult) {
if (rootObj == null) {
return;
}
Deque<Object> stack = new ArrayDeque<>();
stack.push(rootObj);
while (!stack.isEmpty()) {
Object obj = stack.pop();
Class<?> objClass = obj.getClass();
if (isJavaBasicType(objClass) || visited.containsKey(obj)) {
continue;
}
visited.put(obj, Boolean.TRUE);
// 处理集合
if (obj instanceof Collection<?>) {
for (Object item : (Collection<?>) obj) {
if (item != null) {
stack.push(item);
}
}
continue;
}
// 处理Map
if (obj instanceof Map<?, ?>) {
for (Object value : ((Map<?, ?>) obj).values()) {
if (value != null) {
stack.push(value);
}
}
continue;
}
// 只处理有脱敏注解的字段
List<FieldCache.FieldMaskMeta> maskFields = FieldCache.getMaskFields(objClass);
if (!maskFields.isEmpty()) {
detectResult.needMask = true;
}
for (FieldCache.FieldMaskMeta meta : maskFields) {
Field field = meta.getField();
try {
Object value = field.get(obj);
MaskStrategy strategy = meta.getStrategy();
if (strategy.shouldMask(value)) {
Object maskedValue = strategy.mask(value);
field.set(obj, maskedValue);
}
} catch (Exception ignored) {
}
}
// 递归处理所有非基础类型字段
for (Field field : getAllFields(objClass)) {
Class<?> fieldType = field.getType();
if (isJavaBasicType(fieldType) || fieldType.isEnum()) {
continue;
}
try {
Object value = field.get(obj);
if (value != null) {
stack.push(value);
}
} catch (Exception ignored) {
}
}
}
}
/**
* 获取类及其所有父类的字段,避免同名字段混淆
*/
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Set<String> fieldNames = new HashSet<>();
for (Class<?> c = clazz; c != null && c != Object.class; c = c.getSuperclass()) {
for (Field field : c.getDeclaredFields()) {
if (fieldNames.add(field.getName())) {
field.setAccessible(true);
fields.add(field);
}
}
}
return fields;
}
/**
* 判断是否为Java基础类型、包装类型、String、时间类型、枚举等
*/
private static boolean isJavaBasicType(Class<?> clazz) {
if (clazz == null) {
return false;
}
if (clazz.isPrimitive() || clazz.isEnum()) {
return true;
}
if (clazz == String.class || clazz == Boolean.class || clazz == Character.class) {
return true;
}
if (Number.class.isAssignableFrom(clazz)) {
return true;
}
if (Date.class.isAssignableFrom(clazz)) {
return true;
}
return clazz.getPackage() != null && clazz.getPackage().getName().startsWith("java.time");
}
/**
* 标记本次是否发现需要脱敏的字段
*/
private static class MaskDetectResult {
boolean needMask = false;
}
}
/**
* 字段脱敏元数据缓存
*/
@Slf4j
class FieldCache {
private static final Map<Class<?>, List<FieldMaskMeta>> CACHE = new ConcurrentHashMap<>();
public static List<FieldMaskMeta> getMaskFields(Class<?> clazz) {
return CACHE.computeIfAbsent(clazz, FieldCache::scanMaskFields);
}
private static List<FieldMaskMeta> scanMaskFields(Class<?> clazz) {
List<FieldMaskMeta> list = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(MaskValue.class)) {
field.setAccessible(true);
MaskValue maskValue = field.getAnnotation(MaskValue.class);
MaskStrategy strategy = null;
try {
strategy = SpringUtil.getBean(maskValue.value());
} catch (Exception e) {
log.error("获取bean失败:{}", maskValue, e);
}
if (strategy != null) {
list.add(new FieldMaskMeta(field, strategy));
}
}
// 可扩展更多注解与策略映射
}
return list;
}
@AllArgsConstructor
@Getter
public static class FieldMaskMeta {
private final Field field;
private final MaskStrategy strategy;
}
}
DefaultMaskPhoneStrategy 默认的加密策略(手机号加密)
package cn.ideamake.business.component.mask;
import cn.hutool.core.convert.Convert;
import cn.ideamake.auth.context.IdeamakeSubjectContext;
import cn.ideamake.auth.context.IdeamakeUserInfo;
import cn.ideamake.common.util.PhoneEncyUtil;
import java.util.Objects;
/**
* @author Barcke
* @version 1.0
* @projectName ideamake-framework
* @className DefaultMaskPhoneStrategy
* @date 2025/8/13 16:07
* @slogan: 源于生活 高于生活
* @description: 默认的手机号加密策略
**/
public class DefaultMaskPhoneStrategy implements MaskStrategy {
@Override
public Object mask(Object value) {
if (value == null) {
return null;
}
return PhoneEncyUtil.encryptPhoneAccordAreaCode(Convert.toStr(value));
}
@Override
public boolean shouldMask(Object value) {
IdeamakeUserInfo ideamakeUserInfo = IdeamakeSubjectContext.getInfo();
boolean ifMaskPhone = Convert.toStr(value) != null
&& Convert.toStr(value).matches(".*[a-zA-Z].*");
if (ideamakeUserInfo == null && !ifMaskPhone) {
return false;
}
return Objects.equals(IdeamakeSubjectContext.get().getUserBO().getIfPhoneEncrypt(), 0)
|| ifMaskPhone;
}
}
MaskConfiguration (配置)
package cn.ideamake.business.config;
import cn.ideamake.business.component.mask.DefaultMaskPhoneStrategy;
import cn.ideamake.business.component.mask.MaskResponseAdvice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Barcke
* @version 1.0
* @projectName ideamake-framework
* @className MaskConfiguration
* @date 2025/8/13 19:14
* @slogan: 源于生活 高于生活
* @description:
**/
@Slf4j
@Configuration
public class MaskConfiguration {
@Bean
@ConditionalOnMissingBean
public DefaultMaskPhoneStrategy defaultMaskPhoneStrategy(){
return new DefaultMaskPhoneStrategy();
}
@Bean
@ConditionalOnMissingBean
public MaskResponseAdvice maskResponseAdvice(){
return new MaskResponseAdvice();
}
}
为什么这套方案值得用?
够快:本地缓存把响应时间压到微秒级,高并发下用户体验丝滑;
够稳:Redis 保证分布式一致性,三大缓存问题全解决;
够简单:注解驱动零配置,API 直观,新手也能快速上手;
够安全:完善的线程池管理和资源清理,无内存泄漏风险。
未来规划:更强大的缓存能力
目前这套工具类已经能应对大部分高并发场景,接下来还会加入缓存预热(系统启动时提前加载热点数据)、动态配置刷新(不重启服务更新缓存参数)等功能,让缓存管理更智能。
如果你也在为高并发缓存问题头疼,不妨试试这套 “双保险” 方案 —— 让本地缓存负责 “快如闪电”,Redis 负责 “稳如泰山”,系统再也不怕流量峰值的冲击!
1654

被折叠的 条评论
为什么被折叠?



