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