网站首页 > PHP源码 > 交友会员 > 高并发下缓存进阶写法

高并发下缓存进阶写法

  • 作者:互联网
  • 时间:2026-01-23 09:33:01

高并发场景从来都是绕不开的 “硬骨头”,而缓存作为提升系统吞吐量、降低数据库压力的核心手段,早已成为每个开发者的必备技能。但很多时候,我们随手写的 “基础缓存逻辑”—— 比如简单的 “查缓存 - 无则查库 - 回写缓存”—— 在高并发流量冲击下,往往会暴露出缓存穿透、缓存击穿、缓存雪崩等一系列问题:可能是大量请求直击数据库导致服务雪崩,可能是缓存过期瞬间的并发击穿,也可能是分布式环境下的缓存一致性难题。

一、防止缓存穿透,数据不存在,缓存空串

缓存穿透是指用户请求查询一个根本不存在的数据,导致请求绕过缓存直接打到数据库,当这类请求量巨大时,会给数据库带来极大压力甚至使其宕机。

高并发场景下的解决方案及代码实现

针对缓存穿透,主流且高效的解决方案有两种:空值缓存 + 布隆过滤器(空值缓存解决少量无效请求,布隆过滤器拦截大量无效请求,二者结合效果最佳)。

以下以Java + Spring Boot + Redis为例,模拟「电商商品查询」的高并发场景(比如秒杀活动中,大量用户查询不存在的商品 ID)

1. 基础配置(Redis + 布隆过滤器初始化)

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;

@Configuration
public class RedisConfig {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // 布隆过滤器:假设系统中有100万件商品,误判率0.01
    private BloomFilter<Long> productBloomFilter;

    /**
     * 初始化RedisTemplate序列化方式
     */
    @PostConstruct
    public void initRedisTemplate() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
    }

    /**
     * 初始化布隆过滤器(实际项目中应从数据库加载所有商品ID初始化)
     */
    @Bean
    public BloomFilter<Long> productBloomFilter() {
        // 预期数据量100万,误判率0.01
        productBloomFilter = BloomFilter.create(
                Funnels.longFunnel(),
                1000000,
                0.01
        );
        // 模拟初始化:将系统中存在的商品ID加入布隆过滤器
        for (long i = 1; i <= 1000000; i++) {
            productBloomFilter.put(i);
        }
        return productBloomFilter;
    }
}

2. 核心业务代码(商品查询 + 防缓存穿透)

import com.google.common.hash.BloomFilter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Service
public class ProductService {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private BloomFilter<Long> productBloomFilter;

    // 缓存key前缀
    private static final String PRODUCT_KEY_PREFIX = "product:info:";
    // 空值缓存过期时间(5分钟,避免缓存过多无效数据)
    private static final long NULL_CACHE_EXPIRE = 5;
    // 正常商品缓存过期时间(1小时)
    private static final long NORMAL_CACHE_EXPIRE = 60;

    /**
     * 查询商品信息(高并发场景防缓存穿透)
     * @param productId 商品ID
     * @return 商品信息(null表示不存在)
     */
    public String getProductInfo(Long productId) {
        // 步骤1:布隆过滤器快速拦截无效请求(核心:不存在的ID直接返回)
        if (!productBloomFilter.mightContain(productId)) {
            System.out.println("布隆过滤器拦截:商品ID " + productId + " 不存在,直接返回");
            return null;
        }

        // 步骤2:查询Redis缓存
        String cacheKey = PRODUCT_KEY_PREFIX + productId;
        String productInfo = (String) redisTemplate.opsForValue().get(cacheKey);
        // 缓存命中:直接返回
        if (productInfo != null) {
            // 空值缓存(""):表示数据库中确实不存在该商品,返回null
            if ("".equals(productInfo)) {
                System.out.println("缓存命中空值:商品ID " + productId + " 不存在");
                return null;
            }
            System.out.println("缓存命中:返回商品ID " + productId + " 信息");
            return productInfo;
        }

        // 步骤3:缓存未命中,查询数据库(加锁避免缓存击穿,此处简化,仅防穿透)
        String dbProductInfo = queryProductFromDb(productId);

        // 步骤4:处理数据库查询结果,写入缓存(核心:空值也缓存)
        if (dbProductInfo == null) {
            // 数据库中不存在:缓存空值(用""代替null,避免Redis缓存null值)
            redisTemplate.opsForValue().set(
                    cacheKey,
                    "",
                    NULL_CACHE_EXPIRE,
                    TimeUnit.MINUTES
            );
            System.out.println("数据库查询为空:商品ID " + productId + ",写入空值缓存");
            return null;
        } else {
            // 数据库中存在:缓存正常数据
            redisTemplate.opsForValue().set(
                    cacheKey,
                    dbProductInfo,
                    NORMAL_CACHE_EXPIRE,
                    TimeUnit.MINUTES
            );
            System.out.println("数据库查询命中:商品ID " + productId + ",写入正常缓存");
            return dbProductInfo;
        }
    }

    /**
     * 模拟数据库查询商品信息
     */
    private String queryProductFromDb(Long productId) {
        // 模拟逻辑:仅ID 1-1000000存在,其他不存在
        if (productId >= 1 && productId <= 1000000) {
            return "商品ID:" + productId + ",名称:小米手机,价格:1999";
        } else {
            // 模拟数据库查询耗时(高并发下无防护会导致数据库压力剧增)
            try {
                Thread.sleep(10); // 模拟数据库IO耗时
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return null;
        }
    }
}

3. 高并发测试代码(模拟 10 万次请求)

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Controller
public class ProductController {

    @Resource
    private ProductService productService;

    // 固定线程池:模拟100个并发线程
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(100);

    /**
     * 模拟高并发查询不存在的商品(缓存穿透测试)
     */
    @GetMapping("/test/cache/penetration")
    @ResponseBody
    public String testCachePenetration() {
        long startTime = System.currentTimeMillis();
        // 模拟10万次请求,查询不存在的商品ID(1000001)
        int requestCount = 100000;
        for (int i = 0; i < requestCount; i++) {
            EXECUTOR.submit(() -> productService.getProductInfo(1000001L));
        }

        // 等待所有请求完成
        EXECUTOR.shutdown();
        try {
            EXECUTOR.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        long endTime = System.currentTimeMillis();
        return "10万次无效请求处理完成,耗时:" + (endTime - startTime) + "ms";
    }
}

4. 效果说明

无防护时:10 万次请求会全部打到数据库,每次数据库查询耗时 10ms,总耗时约 1000 秒(16 分钟),且数据库会被高并发压垮。

有防护时

  • 布隆过滤器直接拦截所有无效请求,无需查询Redis和数据库;
  • 实际测试总耗时仅约 500ms,数据库零压力;
  • 即使布隆过滤器有极小误判率(0.01),漏过的请求也会被「空值缓存」拦截,第二次请求直接命中Redis空值,不会再打数据库。

关键补充

  1. 布隆过滤器的更新:当系统新增商品时,需要及时将新商品 ID 加入布隆过滤器(避免误拦截);
  2. 空值缓存过期时间:不宜过长(5-10 分钟),避免缓存过多无效数据,同时能应对「数据新增」的场景;
  3. 布隆过滤器的误判率:误判率越低,所需内存越大,需根据实际场景平衡(比如 100 万数据 + 0.01 误判率,仅需约 1.4MB 内存)。

5. 总结

  • 核心方案:高并发下防缓存穿透需「布隆过滤器(前置拦截)+ 空值缓存(兜底)」结合,布隆过滤器拦截 99% 以上的无效请求,空值缓存处理极小比例的误判请求;
  • 关键要点:空值缓存必须设置过期时间,布隆过滤器需初始化所有有效 ID 并及时更新;
  • 效果:能将无效请求的处理成本从「数据库级」降到「内存级」,保障高并发下数据库的稳定性。

二、防止缓存雪崩,缓存不命中, 加分布式锁

1. 缓存雪崩的核心概念

缓存雪崩是指两种核心场景导致的灾难性后果:

  • 大量缓存 Key 同一时间过期:高并发下所有请求瞬间绕过缓存直达数据库,压垮数据库;
  • Redis 服务整体宕机:缓存层完全不可用,所有请求直接冲击数据库。

针对这两种场景,核心解决方案是:打散缓存过期时间 + 缓存预热 + 分布式锁防缓存重建风暴 + Redis 高可用集群 + 服务降级 / 限流(多策略组合才能应对高并发下的雪崩风险)。

2. 高并发场景下的解决方案及代码实现

以下以Java + Spring Boot + Redis + Redisson(分布式锁)为例,模拟「电商首页商品分类缓存」的高并发场景(比如 100 个商品分类缓存原本同时过期,引发缓存雪崩)

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class CategoryService {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RedissonClient redissonClient;

    // 缓存key前缀
    private static final String CATEGORY_KEY_PREFIX = "category:info:";
    // 基础过期时间(2小时)
    private static final long BASE_EXPIRE_MINUTES = 2 * 60;
    // 过期时间随机范围(0-30分钟)
    private static final long RANDOM_EXPIRE_RANGE = 30;
    // 分布式锁前缀
    private static final String LOCK_KEY_PREFIX = "lock:category:";
    // 锁超时时间(避免死锁)
    private static final long LOCK_EXPIRE_SECONDS = 30;
    // 降级开关(雪崩时触发,返回兜底数据)
    private volatile boolean isDegrade = false;

    /**
     * 查询商品分类(高并发防缓存雪崩)
     * @param categoryId 分类ID
     * @return 分类信息
     */
    public String getCategoryInfo(Integer categoryId) {
        // 降级策略:Redis宕机/压力过大时,直接返回兜底数据
        if (isDegrade) {
            return "【降级兜底】分类ID:" + categoryId + ",基础数码分类(缓存服务暂不可用)";
        }

        String cacheKey = CATEGORY_KEY_PREFIX + categoryId;
        // 步骤1:查询Redis缓存
        String categoryInfo = (String) redisTemplate.opsForValue().get(cacheKey);
        if (categoryInfo != null) {
            System.out.println("缓存命中:分类ID " + categoryId);
            return categoryInfo;
        }

        // 步骤2:缓存未命中,加分布式锁(避免高并发下大量线程同时查数据库,引发缓存重建风暴)
        String lockKey = LOCK_KEY_PREFIX + categoryId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁(最多等5秒,锁自动过期30秒)
            boolean lockAcquired = lock.tryLock(5, LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS);
            if (!lockAcquired) {
                // 获取锁失败:返回兜底数据(避免线程阻塞)
                return "【临时兜底】分类ID:" + categoryId + ",数据加载中...";
            }

            // 步骤3:双重检查缓存(防止其他线程已重建缓存)
            categoryInfo = (String) redisTemplate.opsForValue().get(cacheKey);
            if (categoryInfo != null) {
                return categoryInfo;
            }

            // 步骤4:查询数据库(模拟高耗时操作)
            categoryInfo = queryCategoryFromDb(categoryId);

            // 步骤5:写入缓存(核心:过期时间随机化,打散过期点)
            long randomExpire = (long) (Math.random() * RANDOM_EXPIRE_RANGE);
            redisTemplate.opsForValue().set(
                    cacheKey,
                    categoryInfo,
                    BASE_EXPIRE_MINUTES + randomExpire,
                    TimeUnit.MINUTES
            );
            System.out.println("缓存重建完成:分类ID " + categoryId + ",过期时间:" + (BASE_EXPIRE_MINUTES + randomExpire) + "分钟");
            return categoryInfo;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "【异常兜底】分类ID:" + categoryId + ",数据查询失败";
        } finally {
            // 释放锁(仅持有锁的线程释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 模拟数据库查询分类信息(高耗时操作,模拟高并发下数据库压力)
     */
    private String queryCategoryFromDb(Integer categoryId) {
        // 模拟数据库IO耗时(10ms)
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "分类ID:" + categoryId + ",名称:数码分类-" + categoryId + "(数据库查询)";
    }

    /**
     * 手动触发降级(模拟Redis宕机场景)
     */
    public void triggerDegrade(boolean degrade) {
        this.isDegrade = degrade;
    }
}

3. 高并发测试代码(模拟缓存雪崩场景)

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@RestController
public class CategoryController {

    @Resource
    private CategoryService categoryService;

    // 固定线程池:模拟200个并发线程(高并发场景)
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(200);

    /**
     * 模拟高并发查询分类(缓存雪崩测试)
     * @param batchCount 总请求数
     * @return 处理结果
     */
    @GetMapping("/test/cache/avalanche")
    @ResponseBody
    public String testCacheAvalanche(@RequestParam(defaultValue = "200000") int batchCount) {
        long startTime = System.currentTimeMillis();

        // 模拟场景:100个分类缓存同时过期,20万次请求并发查询
        for (int i = 0; i < batchCount; i++) {
            int finalI = i;
            EXECUTOR.submit(() -> {
                // 循环查询1-100个分类(模拟全量分类请求)
                int categoryId = (finalI % 100) + 1;
                categoryService.getCategoryInfo(categoryId);
            });
        }

        // 等待所有请求完成
        EXECUTOR.shutdown();
        try {
            EXECUTOR.awaitTermination(2, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        long endTime = System.currentTimeMillis();
        return "20万次分类查询请求处理完成,耗时:" + (endTime - startTime) + "ms";
    }

    /**
     * 触发/关闭降级策略
     */
    @GetMapping("/test/degrade")
    @ResponseBody
    public String triggerDegrade(@RequestParam boolean degrade) {
        categoryService.triggerDegrade(degrade);
        return "降级策略已" + (degrade ? "开启" : "关闭");
    }
}

4. 效果说明

- 无防护时的问题

  • 100 个分类缓存同时过期,20 万次并发请求全部打到数据库;
  • 每次数据库查询耗时 10ms,总耗时约 2000 秒(33 分钟),数据库连接池耗尽、CPU 飙升,最终宕机。

- 有防护时的效果

  • 过期时间随机化:100 个分类缓存过期时间分散在 120-150 分钟,避免同时过期;
  • 分布式锁:每个分类仅 1 个线程查数据库重建缓存,其余线程等待或返回兜底数据,数据库查询次数从 20 万次降到 100 次;
  • 缓存预热:项目启动时已加载热点数据,运行时无需大量重建缓存;
  • 降级策略Redis宕机时直接返回兜底数据,保障服务不挂;
  • 实际测试总耗时仅约 1000ms,数据库压力可忽略,服务稳定性大幅提升。

5. 生产环境补充

  • Redis 高可用:生产环境必须用Redis集群 / 哨兵模式,避免单节点宕机导致缓存层不可用;
  • 限流熔断:结合Sentinel/Nginx做接口限流(比如每秒最多 1000 次请求),进一步保护数据库;
  • 缓存刷新:对超热点数据(如秒杀商品),后台定时刷新缓存,避免过期;
  • 监控告警:监控Redis命中率、数据库QPS,异常时及时告警。

6.总结

  • 核心策略:防缓存雪崩需「过期时间随机化 + 分布式锁 + 缓存预热 + Redis 高可用 + 降级限流」组合使用;
  • 关键要点:避免大量Key同时过期,控制缓存重建的并发量,保障缓存层高可用,做好降级兜底;
  • 核心目标:将数据库的高并发压力分散到不同时间、不同节点,同时在极端场景下保障服务不宕机。

三、防止缓存击穿,互斥锁或者热点key永不过期

1. 缓存击穿的核心概念

缓存击穿是指单个热点 Key(比如秒杀商品、热门榜单)在缓存中突然过期,此时大量并发请求会瞬间绕过缓存直接冲击数据库,导致数据库因单点压力过大而性能骤降甚至宕机。

  • 与缓存雪崩的区别:雪崩是大量 Key 同时过期,击穿是单个热点 Key 过期
  • 与缓存穿透的区别:穿透是请求不存在的数据,击穿是请求存在但缓存过期的热点数据

2. 核心解决方案及高并发代码实现(两种防击穿方案)

针对缓存击穿,主流且高效的解决方案有两种:

  • 互斥锁(分布式锁) :高并发下仅允许一个线程重建缓存,其他线程等待或重试(一致性优先);
  • 逻辑过期:热点Key永不过期,给Value增加「逻辑过期时间」,请求时返回旧数据 + 异步刷新缓存(性能优先,高并发下更友好)。

以下以Java + Spring Boot + Redis + Redisson(分布式锁)为例,模拟「电商秒杀商品查询」的高并发场景(比如爆款手机的热点 Key 过期,10 万 + 并发请求涌入)。

import com.alibaba.fastjson2.JSON;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Service
public class SeckillProductService {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RedissonClient redissonClient;

    // 缓存Key前缀
    private static final String PRODUCT_KEY_PREFIX = "seckill:product:";
    // 分布式锁前缀
    private static final String LOCK_KEY_PREFIX = "lock:seckill:product:";
    // 锁超时时间(避免死锁)
    private static final long LOCK_EXPIRE_SECONDS = 30;
    // 热点商品逻辑过期时间(30分钟)
    private static final long LOGIC_EXPIRE_SECONDS = 30 * 60;
    // 异步刷新缓存的线程池(核心数=CPU核心数*2)
    private static final ExecutorService CACHE_REFRESH_POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);

    // ====================== 方案1:互斥锁(分布式锁)方案(一致性优先) ======================
    /**
     * 查询秒杀商品(互斥锁防击穿)
     * 适用场景:对数据一致性要求高,允许请求短暂阻塞
     */
    public Product getProductByMutexLock(Long productId) {
        String cacheKey = PRODUCT_KEY_PREFIX + productId;

        // 步骤1:查询Redis缓存
        String productJson = (String) redisTemplate.opsForValue().get(cacheKey);
        if (productJson != null && !productJson.isEmpty()) {
            System.out.println("互斥锁方案-缓存命中:商品ID " + productId);
            return JSON.parseObject(productJson, Product.class);
        }

        // 步骤2:缓存未命中,加分布式锁(仅1个线程重建缓存)
        String lockKey = LOCK_KEY_PREFIX + productId;
        RLock lock = redissonClient.getLock(lockKey);
        Product product = null;
        try {
            // 尝试获取锁(最多等5秒,锁自动过期30秒)
            boolean lockAcquired = lock.tryLock(5, LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS);
            if (!lockAcquired) {
                // 获取锁失败:重试(或返回兜底数据)
                System.out.println("互斥锁方案-获取锁失败,重试:商品ID " + productId);
                return getProductByMutexLock(productId); // 简单重试,生产可限制重试次数
            }

            // 步骤3:双重检查缓存(防止其他线程已重建缓存)
            productJson = (String) redisTemplate.opsForValue().get(cacheKey);
            if (productJson != null && !productJson.isEmpty()) {
                product = JSON.parseObject(productJson, Product.class);
                return product;
            }

            // 步骤4:查询数据库(模拟高耗时操作)
            product = queryProductFromDb(productId);
            if (product == null) {
                // 空值缓存(防穿透,此处复用)
                redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES);
                return null;
            }

            // 步骤5:写入缓存(设置物理过期时间,比如10分钟)
            redisTemplate.opsForValue().set(
                    cacheKey,
                    JSON.toJSONString(product),
                    10,
                    TimeUnit.MINUTES
            );
            System.out.println("互斥锁方案-缓存重建完成:商品ID " + productId);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取分布式锁失败", e);
        } finally {
            // 释放锁(仅持有锁的线程释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        return product;
    }

    // ====================== 方案2:逻辑过期方案(性能优先,高并发友好) ======================
    /**
     * 查询秒杀商品(逻辑过期防击穿)
     * 适用场景:高并发、允许数据短暂不一致(比如秒杀商品信息不要求实时更新)
     */
    public Product getProductByLogicExpire(Long productId) {
        String cacheKey = PRODUCT_KEY_PREFIX + productId;

        // 步骤1:查询Redis缓存(逻辑过期的封装对象)
        CacheDataWithLogicExpire cacheData = (CacheDataWithLogicExpire) redisTemplate.opsForValue().get(cacheKey);
        if (cacheData == null) {
            // 缓存未初始化:走数据库查询(首次加载)
            return queryProductFromDb(productId);
        }

        // 步骤2:判断是否逻辑过期
        if (!cacheData.isExpired()) {
            // 未过期:直接返回旧数据
            System.out.println("逻辑过期方案-缓存未过期:商品ID " + productId);
            return cacheData.getData();
        }

        // 步骤3:逻辑过期:异步刷新缓存 + 返回旧数据(核心:不阻塞请求)
        System.out.println("逻辑过期方案-数据过期,异步刷新:商品ID " + productId);
        CACHE_REFRESH_POOL.submit(() -> refreshProductCache(productId));

        // 直接返回旧数据,不阻塞请求
        return cacheData.getData();
    }

    /**
     * 异步刷新缓存(逻辑过期专用)
     */
    private void refreshProductCache(Long productId) {
        String lockKey = LOCK_KEY_PREFIX + productId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 加锁:避免多个异步线程同时刷新
            if (lock.tryLock(0, LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS)) {
                // 查询数据库
                Product product = queryProductFromDb(productId);
                // 写入缓存(逻辑过期,物理永不过期)
                String cacheKey = PRODUCT_KEY_PREFIX + productId;
                redisTemplate.opsForValue().set(
                        cacheKey,
                        CacheDataWithLogicExpire.of(product, LOGIC_EXPIRE_SECONDS),
                        TimeUnit.DAYS.toSeconds(365), // 物理永不过期
                        TimeUnit.SECONDS
                );
                System.out.println("逻辑过期方案-缓存刷新完成:商品ID " + productId);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 初始化热点商品缓存(项目启动/秒杀开始前调用,设置逻辑过期)
     */
    public void initHotProductCache(Long productId) {
        Product product = queryProductFromDb(productId);
        String cacheKey = PRODUCT_KEY_PREFIX + productId;
        redisTemplate.opsForValue().set(
                cacheKey,
                CacheDataWithLogicExpire.of(product, LOGIC_EXPIRE_SECONDS),
                TimeUnit.DAYS.toSeconds(365),
                TimeUnit.SECONDS
        );
        System.out.println("热点商品缓存初始化完成:商品ID " + productId);
    }

    /**
     * 模拟数据库查询(高耗时,模拟秒杀商品库查询)
     */
    private Product queryProductFromDb(Long productId) {
        // 模拟数据库IO耗时(10ms,高并发下无防护会压垮数据库)
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // 模拟秒杀商品数据(ID=10086为热点商品)
        if (productId.equals(10086L)) {
            return new Product(10086L, "小米14秒杀款", 2999.0, 10000);
        }
        return new Product(productId, "普通商品", 999.0, 0);
    }
}

3. 高并发测试代码

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@RestController
public class SeckillProductController {

    @Resource
    private SeckillProductService seckillProductService;

    // 高并发线程池:模拟1000个并发线程(秒杀场景)
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1000);

    /**
     * 初始化热点商品缓存(测试前调用)
     */
    @GetMapping("/init/hot/product/{productId}")
    @ResponseBody
    public String initHotProduct(@PathVariable Long productId) {
        seckillProductService.initHotProductCache(productId);
        return "热点商品缓存初始化完成:" + productId;
    }

    /**
     * 测试互斥锁方案(高并发查询热点商品)
     */
    @GetMapping("/test/mutex/{productId}")
    @ResponseBody
    public String testMutexLock(@PathVariable Long productId) {
        long startTime = System.currentTimeMillis();
        int requestCount = 100000; // 10万次并发请求

        // 模拟10万次并发请求查询同一个热点商品
        for (int i = 0; i < requestCount; i++) {
            EXECUTOR.submit(() -> seckillProductService.getProductByMutexLock(productId));
        }

        // 等待所有请求完成
        EXECUTOR.shutdown();
        try {
            EXECUTOR.awaitTermination(2, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        long endTime = System.currentTimeMillis();
        return "互斥锁方案:10万次请求处理完成,耗时:" + (endTime - startTime) + "ms";
    }

    /**
     * 测试逻辑过期方案(高并发查询热点商品)
     */
    @GetMapping("/test/logic/expire/{productId}")
    @ResponseBody
    public String testLogicExpire(@PathVariable Long productId) {
        long startTime = System.currentTimeMillis();
        int requestCount = 100000; // 10万次并发请求

        // 模拟10万次并发请求查询同一个热点商品
        for (int i = 0; i < requestCount; i++) {
            EXECUTOR.submit(() -> seckillProductService.getProductByLogicExpire(productId));
        }

        // 等待所有请求完成
        EXECUTOR.shutdown();
        try {
            EXECUTOR.awaitTermination(2, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        long endTime = System.currentTimeMillis();
        return "逻辑过期方案:10万次请求处理完成,耗时:" + (endTime - startTime) + "ms";
    }
}

4. 效果对比说明

无防护时的问题

热点Key过期后,10 万次并发请求全部打到数据库:

  • 数据库单次查询耗时 10ms,总耗时约 1000 秒(16 分钟);
  • 数据库连接池瞬间耗尽,CPU 飙升至 100%,最终宕机。

有防护时的效果

方案数据库查询次数总耗时核心优势适用场景
互斥锁方案1 次~800ms数据一致性高金融、订单等强一致性场景
逻辑过期方案1 次(异步)~300ms无请求阻塞,性能最优秒杀、资讯等高并发场景
  • 互斥锁方案:仅 1 个线程查数据库重建缓存,其余线程等待 / 重试,数据库压力几乎为 0;
  • 逻辑过期方案:所有请求直接返回旧数据,异步线程后台刷新缓存,请求无阻塞,高并发下体验最佳。

5. 生产环境补充

  1. 热点 Key 识别:通过监控Redis访问日志,识别访问频率Top NKey,提前标记为HotKey
  2. 缓存预热:秒杀活动开始前,主动将热Key加载到Redis并设置逻辑过期;
  3. 重试限制:互斥锁方案的重试逻辑需限制次数(比如最多重试 3 次),避免无限重试;
  4. 监控告警:监控热点Key的缓存命中率、数据库QPS,异常时及时告警;
  5. 锁粒度控制:分布式锁的Key要精准(比如按商品 ID),避免粗粒度锁导致性能瓶颈。

6. 总结

  • 核心方案:防缓存击穿的核心是「控制热点Key过期后的数据库请求量」,互斥锁(一致性优先)和逻辑过期(性能优先)是两种主流方案;

  • 关键要点

    • 互斥锁:分布式锁 + 双重检查缓存,避免多线程并发重建;
    • 逻辑过期:物理永不过期 + 异步刷新,高并发下不阻塞请求;
  • 场景选择:强一致性场景用互斥锁,高并发弱一致性场景用逻辑过期(秒杀、爆款商品首选)。

四、 三者核心区别对比

问题类型核心特征解决方案核心
缓存穿透请求不存在的数据布隆过滤器 + 空值缓存
缓存雪崩大量 Key 过期 / Redis 宕机随机过期 + 分布式锁 + 高可用
缓存击穿单个热点 Key 过期互斥锁 / 逻辑过期

五、总结

高并发下的缓存设计,从来不是 “一招鲜吃遍天” 的简单技巧,而是平衡性能、一致性、可用性的工程艺术。本文分享的进阶写法,核心是围绕 “规避并发风险、提升缓存有效性、降低异常影响” 三个核心目标展开 —— 从解决缓存击穿的双重检查锁,到应对缓存雪崩的过期时间随机化,再到兼顾一致性的缓存更新策略,每一种写法的背后,都是对高并发场景下 “极端情况” 的预判与应对。

需要强调的是,没有任何一种缓存方案是 “万能” 的:单机场景下的本地缓存优化,和分布式集群下的 Redis 缓存设计,其核心考量完全不同;读多写少的业务,与读写频繁的业务,缓存策略也需要差异化调整。真正优秀的缓存实现,是基于业务场景的 “量身定制”—— 理解每种进阶写法的适用场景、优缺点,结合压测数据持续调优,才是让缓存真正成为高并发系统 “性能底座” 的关键。

好了,今天的分享就到此结束了,如果文章对你有所帮助,欢迎:点赞+评论+收藏,我是:IT_sunshine ,我们下期见!