Java雪崩案例

wen java案例 3

本文目录导读:

Java雪崩案例

  1. 案例一:缓存雪崩(Cache Avalanche)
  2. 案例二:服务雪崩(Service Avalanche,依赖故障蔓延)

我们通常所说的“雪崩”,在Java分布式系统中,一般指“缓存雪崩”,但也可能是“服务雪崩”,这两者是不同的概念,下面分别用两个经典的代码级案例来还原场景、分析原因并给出解决方案。


缓存雪崩(Cache Avalanche)

场景还原
某电商平台在凌晨12点进行大促,Redis中存储了所有商品的库存和详情,为了节省内存,所有缓存设置了统一的过期时间(如30分钟),当12:00:00准时,大量缓存同时过期,大量用户请求同时涌入数据库(MySQL),数据库连接池瞬间被击穿,CPU飙升,导致数据库宕机,整个系统瘫痪。

代码模拟(JVM层模拟,原理与Redis相同):

public class CacheAvalancheDemo {
    // 模拟缓存存储(用JVM Map代替Redis)
    private static Map<String, String> cache = new ConcurrentHashMap<>();
    private static Map<String, String> db = new ConcurrentHashMap<>(); // 模拟数据库
    // 模拟数据库查询(很慢)
    public static String queryDb(String key) {
        try {
            Thread.sleep(300); // 模拟慢SQL
        } catch (InterruptedException e) {}
        return "value-" + key;
    }
    // 初始化缓存,设置统一过期时间
    public static void initCache() {
        for (int i = 0; i < 1000; i++) {
            String key = "product_" + i;
            cache.put(key, queryDb(key));
        }
        System.out.println("缓存初始化完成,等待统一过期...");
    }
    // 获取数据(不带缓冲的原始方法)
    public static String getDataBad(String key) {
        String value = cache.get(key);
        if (value == null) {
            // 缓存没数据,直接去打DB —— 雪崩点!
            value = queryDb(key);
            cache.put(key, value);
        }
        return value;
    }
    // 获取数据(带互斥锁:防止大量请求同时打DB,只允许一个线程去查DB)
    public static String getDataWithLock(String key) {
        String value = cache.get(key);
        if (value == null) {
            // 只有第一个线程能拿到锁
            synchronized (CacheAvalancheDemo.class) {
                value = cache.get(key); // 二次检查
                if (value == null) {
                    value = queryDb(key);
                    cache.put(key, value);
                }
            }
        }
        return value;
    }
    public static void main(String[] args) throws InterruptedException {
        initCache();
        System.out.println("模拟统一过期时刻...");
        Thread.sleep(1100); // 假设缓存清空
        // 模拟100个请求同时访问同一个不存在的key
        int threadCount = 100;
        CountDownLatch latch = new CountDownLatch(threadCount);
        // 不用锁的情况:
        long start = System.currentTimeMillis();
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);
        for (int i = 0; i < threadCount; i++) {
            pool.execute(() -> {
                getDataBad("product_1"); // 由于缓存失效,会打DB 100次
                latch.countDown();
            });
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("不加锁总耗时:" + (end - start) + "ms (数据库被打了100次)");
        // 重置
        Thread.sleep(1000); // 等待完成
        // 用锁的情况:
        start = System.currentTimeMillis();
        CountDownLatch latch2 = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            pool.execute(() -> {
                getDataWithLock("product_2");
                latch2.countDown();
            });
        }
        latch2.await();
        end = System.currentTimeMillis();
        System.out.println("加锁总耗时:" + (end - start) + "ms (数据库只被打了1次)");
        pool.shutdown();
    }
}

解决方案(针对缓存雪崩):

  1. 过期时间打散:不要统一过期,加一个随机数(如 base + random(0~300s))。
  2. 互斥锁/单飞:如上述代码,只允许一个线程去查DB并回填缓存(使用 SETNX 或语言级锁)。
  3. 多级缓存:本地缓存(Caffeine) + Redis,本地缓存不会全部过期。
  4. 缓存永久不过期:后台线程异步更新缓存。
  5. 熔断降级:如果DB压力过大,直接返回默认值或限流。

服务雪崩(Service Avalanche,依赖故障蔓延)

场景还原
微服务架构中,OrderService 依赖 UserServiceProductServiceUserService 因为某个接口慢(比如SQL死锁),导致响应时间变长(从50ms变成5秒)。OrderService 的Tomcat线程池有100个线程,因为 UserService 慢,100个线程全部卡在等待响应上,新的请求 OrderService 无法处理,全部排队超时。ProductService 也被拖垮,因为 OrderService 占着连接不释放,最终整个系统雪崩。

代码模拟(线程池耗尽):

public class ServiceAvalancheDemo {
    // 模拟下游服务:一开始正常,后来变得极慢
    static class DownstreamService {
        static boolean isSlow = false;
        static String call() {
            if (isSlow) {
                try { Thread.sleep(5000); } catch (InterruptedException e) {}
                return "slow-response";
            } else {
                try { Thread.sleep(50); } catch (InterruptedException e) {}
                return "fast-response";
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        // 模拟上游服务(OrderService)的线程池,大小只有10个
        ExecutorService orderServicePool = Executors.newFixedThreadPool(10);
        int totalRequests = 50; // 50个请求进来
        // 阶段1:下游正常,快速处理
        System.out.println("=== 阶段1:下游正常 ===");
        CountDownLatch latch1 = new CountDownLatch(totalRequests);
        for (int i = 0; i < totalRequests; i++) {
            orderServicePool.execute(() -> {
                DownstreamService.call(); // 50ms
                latch1.countDown();
            });
        }
        latch1.await();
        System.out.println("10个线程处理50个请求,无压力");
        // 阶段2:下游变慢,导致线程池耗尽
        System.out.println("=== 阶段2:下游变慢(雪崩开始) ===");
        DownstreamService.isSlow = true;
        // 只发5个请求,但每个都要5秒,占用5个线程5秒
        for (int i = 0; i < 5; i++) {
            orderServicePool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 开始阻塞5秒");
                DownstreamService.call(); // 5s
            });
        }
        Thread.sleep(100); // 确保线程全部被占用
        // 这时候再来新请求,线程池已满,会走拒绝策略(抛异常/等待)
        System.out.println("尝试提交第6个任务...");
        try {
            orderServicePool.execute(() -> {
                System.out.println("新请求进来了");
            });
        } catch (RejectedExecutionException e) {
            System.err.println("雪崩发生:新请求被拒绝(线程池已满)");
        }
        // 如果使用线程池+无界队列,则是排队等待,最终超时。
        Thread.sleep(1000);
        orderServicePool.shutdownNow();
    }
}

解决方案(针对服务雪崩):

  1. 超时控制:在HTTP/RPC调用时,设置 connectTimeoutreadTimeout(如Dubbo的 timeout=2000)。
  2. 线程池隔离(舱壁模式):为高风险的依赖分配独立的线程池,如Hystrix的 ThreadPoolKey
  3. 熔断器:如果错误率超过阈值(如50%),直接熔断,不再调用下游,快速失败(如Sentinel、Hystrix)。
  4. 限流:对入口进行QPS限制(如Sentinel的 FlowRule)。
  5. 重试降级:失败后走兜底逻辑(返回默认值/缓存旧值)。

对应代码(超时+熔断的关键逻辑伪码):

// 模拟带熔断的调用
public class CircuitBreaker {
    volatile boolean open = false; // true则熔断
    long lastOpenTime;
    int failCount = 0;
    public String call() {
        if (open) {
            throw new RuntimeException("熔断中");
        }
        try {
            String result = doRpcWithTimeout(2000); // 超时2秒
            failCount = 0; // 成功则重置
            return result;
        } catch (Exception e) {
            if (++failCount >= 5) {
                open = true; // 5次失败则熔断
                lastOpenTime = System.currentTimeMillis();
            }
            throw e;
        }
    }
}

  • 缓存雪崩:是数据层面的击穿(大量缓存同时失效导致DB打爆),应对 “错峰过期”互斥锁多级缓存
  • 服务雪崩:是资源层面的耗尽(依赖慢导致线程池/连接池占满),应对 “超时”隔离熔断限流

实际生产环境中,两者常常叠加:缓存雪崩 → 数据库压力大 → 服务响应慢 → 服务雪崩,建议监控告警 + 兜底策略 + 压测演练(混沌工程/故障演练)三者结合。

抱歉,评论功能暂时关闭!