从崩溃到丝滑:高并发下的缓存 “双保险“ 实战指南

当你负责的电商平台迎来流量峰值,商品详情页突然卡顿到无法加载,数据库连接池告警短信疯狂轰炸 —— 这不是恐怖片,而是很多开发者都经历过的 “缓存危机”。今天要分享的这套 “本地缓存 + 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 负责 “稳如泰山”,系统再也不怕流量峰值的冲击!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值