Redis 作为缓存击穿了怎么办先互斥锁

FreeGuideOnline 最新 2026-07-04

请求 → 查缓存 → (有) → 返回 | (无) → 尝试加锁 → (成功) → 查DB → 写缓存 → 解锁 → 返回 | (失败) → 休眠 → 重试


## 用 Redis 实现互斥锁防击穿

下面以 Java 和 Jedis 客户端为例,展示一个简单可靠的互斥锁实现。

### 环境准备
- Redis 服务已启动
- Java 项目引入 Jedis 依赖

```java
// 引入 Jedis
import redis.clients.jedis.Jedis;
import java.util.Collections;

核心代码

public class CacheBreakdownSolution {
    private Jedis jedis = new Jedis("localhost", 6379);
    private static final String LOCK_KEY = "lock:product:1001"; // 锁的key
    private static final String LOCK_VALUE = "1";               // 锁的值
    private static final int LOCK_EXPIRE = 10;                  // 锁过期时间(秒)
    private static final int RETRY_INTERVAL = 100;              // 重试间隔(毫秒)
    private static final int MAX_RETRY = 10;                    // 最大重试次数

    public String getProductInfo(String productId) {
        String cacheKey = "product:" + productId;
        // 1. 先查缓存
        String cacheData = jedis.get(cacheKey);
        if (cacheData != null) {
            return cacheData;  // 缓存命中直接返回
        }

        // 2. 缓存未命中,尝试获取锁
        int retryCount = 0;
        while (retryCount < MAX_RETRY) {
            // 尝试加锁,使用 SETNX 并设置过期时间
            String result = jedis.set(LOCK_KEY, LOCK_VALUE, "NX", "EX", LOCK_EXPIRE);
            if ("OK".equals(result)) {
                // 3. 获取锁成功,查询数据库并重建缓存
                try {
                    // 再次检查缓存,防止双重查询时已有其他线程重建
                    cacheData = jedis.get(cacheKey);
                    if (cacheData != null) {
                        return cacheData;
                    }
                    // 模拟查询数据库
                    String dbData = queryFromDB(productId);
                    if (dbData != null) {
                        // 写入缓存,设置合理的过期时间
                        jedis.setex(cacheKey, 3600, dbData);
                    } else {
                        // 缓存空值,防止缓存穿透(此处不展开),设置较短过期时间
                        jedis.setex(cacheKey, 60, "null");
                    }
                    return dbData;
                } finally {
                    // 4. 释放锁(使用 Lua 脚本保证原子性)
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                                    "return redis.call('del', KEYS[1]) " +
                                    "else return 0 end";
                    jedis.eval(script, Collections.singletonList(LOCK_KEY),
                               Collections.singletonList(LOCK_VALUE));
                }
            } else {
                // 5. 获取锁失败,等待后重试
                try {
                    Thread.sleep(RETRY_INTERVAL);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                retryCount++;
                // 重试前再查一次缓存,可能其他线程已建立缓存
                cacheData = jedis.get(cacheKey);
                if (cacheData != null) {
                    return cacheData;
                }
            }
        }
        // 超过最大重试次数,可降级返回默认值或抛出异常
        return "系统繁忙,请稍后重试";
    }

    private String queryFromDB(String productId) {
        // 模拟数据库查询
        return "ProductInfo{" + productId + "}";
    }
}

关键要点说明

  1. 原子加锁:使用 set key value NX EX seconds 一条命令完成加锁 + 设置过期时间,避免死锁。
  2. 锁过期:必须设置过期时间,防止因程序崩溃或网络问题导致锁永远无法释放。
  3. 二次缓存检查:获得锁后应再次查询缓存,因为在排队期间可能其他线程已经重建了缓存。
  4. 安全释放锁:通过 Lua 脚本判断锁的值是否为自己所设置,再执行删除,避免误删其他线程的锁。
  5. 重试机制:加锁失败的请求不能无限等待,应设置最大重试次数或超时时间,同时每次重试前可查询一次缓存,减少无谓等待。
  6. 缓存空值:数据库无数据时缓存一个特殊值(如 "null"),可以防止缓存穿透再次打到数据库。

使用 Redisson 简化实现

手动管理锁的细节容易出错,在实际项目中更推荐使用成熟的分布式锁框架,比如 Redisson。Redisson 提供了 RLock 对象,内部已经实现了自动续期、可重入、公平锁等高级特性。

引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.17.5</version>
</dependency>

Redisson 配置

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

业务代码

public String getProductInfoWithRedisson(String productId) {
    String cacheKey = "product:" + productId;
    RLock lock = redisson.getLock("lock:" + cacheKey);
    String cacheData = jedis.get(cacheKey);
    if (cacheData != null) {
        return cacheData;
    }

    try {
        // 尝试加锁,最多等待5秒,锁自动过期时间20秒
        if (lock.tryLock(5, 20, TimeUnit.SECONDS)) {
            // 双重检查
            cacheData = jedis.get(cacheKey);
            if (cacheData != null) {
                return cacheData;
            }
            // 查询数据库
            String dbData = queryFromDB(productId);
            if (dbData != null) {
                jedis.setex(cacheKey, 3600, dbData);
            } else {
                jedis.setex(cacheKey, 60, "null");
            }
            return dbData;
        } else {
            // 获取锁超时,直接降级
            return "服务暂时不可用";
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return "系统异常";
    } finally {
        // 判断锁是否由当前线程持有,再释放
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}