本地缓存三大雷区:Java高并发下的致命隐患

本地缓存三大雷区:Java高并发下的致命隐患

编码文章call10242025-08-25 20:58:533A+A-

导语

某电商大促因缓存误用导致数据库瘫痪!本文通过压测重现+源码剖析,揭示缓存穿透、缓存雪崩、数据不一致三大致命陷阱,提供百万QPS验证的解决方案。文末附缓存健康检查脚本。


一、缓存穿透:数据库的隐形杀手

灾难现场
接口被刷/user?id=-1,导致数据库CPU 100%

问题代码

public User getUser(long id) {
    User user = cache.get(id); // 1. 查缓存
    if (user == null) {
        user = db.query("SELECT * FROM users WHERE id = " + id); // 2. 查数据库
        cache.put(id, user); // 3. 写缓存(缓存null值!)
    }
    return user;
}
// 攻击者传入不存在的ID,缓存大量null值

压测数据

攻击QPS

缓存命中率

数据库QPS

500

0%

500

5000

0%

5000(数据库宕机)

解决方案

// 1. 布隆过滤器拦截(Guava)
private static final BloomFilter<Long> userFilter = BloomFilter.create(
    Funnels.longFunnel(), 1000000, 0.01); // 100万用户,1%误判

public User safeGetUser(long id) {
    if (!userFilter.mightContain(id)) return null; // 拦截非法ID
    
    User user = cache.get(id);
    if (user != null) return user;
    
    user = db.getById(id);
    if (user != null) {
        cache.put(id, user);
    } else {
        cache.put(id, User.EMPTY, 30); // 缓存空对象30秒
    }
    return user;
}

// 2. 缓存预热机制
@PostConstruct
public void preheatCache() {
    List<Long> allIds = db.getAllUserIds();
    allIds.forEach(id -> cache.get(id)); // 触发加载
}

二、缓存雪崩:系统崩溃的导火索

血泪案例
万级缓存同时过期,数据库瞬时压力激增500%

错误配置

// 所有缓存设置相同TTL
public Product getProduct(long id) {
    Product product = cache.get(id);
    if (product == null) {
        product = db.getProduct(id);
        cache.put(id, product, 60 * 60); // 全部1小时过期
    }
    return product;
}

雪崩效应

缓存数量

同时过期比例

数据库峰值压力

1万

100%

10倍

10万

100%

数据库宕机

工业级方案

// 1. 过期时间随机化
int baseTtl = 60 * 60; // 1小时
int randomTtl = baseTtl + ThreadLocalRandom.current().nextInt(300); // +随机5分钟
cache.put(id, product, randomTtl);

// 2. 热点数据永不过期+异步更新
public Product getHotProduct(long id) {
    Product product = cache.get(id);
    if (product == null) {
        product = db.getProduct(id);
        cache.put(id, product); // 永不过期
        scheduleRefresh(id); // 后台定时更新
    }
    return product;
}

// 3. 熔断降级机制(Hystrix)
@HystrixCommand(fallbackMethod = "getProductFallback")
public Product getProductWithCircuitBreaker(long id) {
    // 业务逻辑
}

三、数据不一致:脏缓存的业务灾难

诡异故障
订单已支付,页面仍显示"待付款"

问题根源

public void updateOrder(Order order) {
    db.update(order); // 1. 更新数据库
    cache.delete(order.getId()); // 2. 删除缓存
    // 极端情况下:读请求在1和2之间执行,加载旧数据到缓存
}

解决方案

// 1. 延迟双删策略(最终一致性)
public void safeUpdate(Order order) {
    db.update(order);
    cache.delete(order.getId());
    
    // 延迟二次删除
    executor.schedule(() -> cache.delete(order.getId()), 1, TimeUnit.SECONDS);
}

// 2. 数据库Binlog同步(强一致)
@Transactional
public void updateOrder(Order order) {
    db.update(order); 
    // 通过Canal监听Binlog更新缓存
}

// 3. 版本号控制
public class VersionedCache {
    private final Map<Long, Pair<Integer, Object>> cache = new ConcurrentHashMap<>();
    
    public void put(long id, int version, Object value) {
        cache.compute(id, (k, v) -> 
            (v == null || version > v.getLeft()) ? Pair.of(version, value) : v
        );
    }
}
点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4