✨Feat: 实现缓存穿透/击穿防护 + 分布式锁
- 新增 BloomFilterManager:Guava 布隆过滤器(100万容量,1%误判率) - 新增 RedisLockUtil:基于 Redis SETNX + Lua 脚本的分布式锁 - 改造 CacheManager:添加空值短缓存(30秒TTL)+ 互斥锁防击穿(double-check) - 改造 ThumbServiceImpl:使用分布式锁替代 synchronized,集成布隆过滤器 - 新增 docs/技术调研文档.md:完整技术调研文档 - 更新 README.md:添加技术文档索引
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package cn.hezhaohui.thumb.manager.cache;
|
||||
|
||||
import com.google.common.hash.BloomFilter;
|
||||
import com.google.common.hash.Funnels;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 布隆过滤器管理器
|
||||
* 用于防止缓存穿透:拦截不存在的 Key,避免请求穿透到 Redis/DB
|
||||
*
|
||||
* 设计要点:
|
||||
* 1. 预期插入量 100万,误判率 1%(约 9.6MB 内存)
|
||||
* 2. 系统启动时初始化,运行期间持续添加新 Key
|
||||
* 3. 布隆过滤器存在假阳性(误判为存在),但不存在假阴性(不会漏判不存在的 Key)
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class BloomFilterManager {
|
||||
|
||||
/**
|
||||
* 布隆过滤器:存储 userId:blogId 组合
|
||||
* Key 格式:"{userId}:{blogId}"
|
||||
*/
|
||||
private BloomFilter<String> thumbBloomFilter;
|
||||
|
||||
/**
|
||||
* 预期插入量
|
||||
*/
|
||||
private static final long EXPECTED_INSERTIONS = 1_000_000L;
|
||||
|
||||
/**
|
||||
* 误判率
|
||||
*/
|
||||
private static double FPP = 0.01;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
thumbBloomFilter = BloomFilter.create(
|
||||
Funnels.stringFunnel(java.nio.charset.StandardCharsets.UTF_8),
|
||||
EXPECTED_INSERTIONS,
|
||||
FPP
|
||||
);
|
||||
log.info("布隆过滤器初始化完成,预期插入量: {}, 误判率: {}", EXPECTED_INSERTIONS, FPP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建布隆过滤器 Key
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @param blogId 博客 ID
|
||||
* @return 复合 Key
|
||||
*/
|
||||
private String buildKey(Long userId, Long blogId) {
|
||||
return userId + ":" + blogId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加点赞记录到布隆过滤器
|
||||
* 在用户成功点赞后调用
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @param blogId 博客 ID
|
||||
*/
|
||||
public void add(Long userId, Long blogId) {
|
||||
thumbBloomFilter.put(buildKey(userId, blogId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否可能点赞过该博客
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @param blogId 博客 ID
|
||||
* @return false = 一定没有点赞过(可以安全拦截)
|
||||
* true = 可能点赞过(需要进一步查询 Redis/DB)
|
||||
*/
|
||||
public boolean mightContain(Long userId, Long blogId) {
|
||||
return thumbBloomFilter.mightContain(buildKey(userId, blogId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否可能点赞过该博客(String 类型参数)
|
||||
*
|
||||
* @param userId 用户 ID(字符串)
|
||||
* @param blogId 博客 ID(字符串)
|
||||
* @return false = 一定没有点赞过
|
||||
* true = 可能点赞过
|
||||
*/
|
||||
public boolean mightContain(String userId, String blogId) {
|
||||
return thumbBloomFilter.mightContain(userId + ":" + blogId);
|
||||
}
|
||||
}
|
||||
+105
-11
@@ -9,6 +9,7 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
@@ -17,6 +18,24 @@ public class CacheManager {
|
||||
private TopK hotKeyDetector;
|
||||
private Cache<String, Object> localCache;
|
||||
|
||||
/**
|
||||
* 互斥锁 Map:用于缓存击穿防护
|
||||
* Key 为 compositeKey,Value 为锁对象
|
||||
* 使用 ConcurrentHashMap 管理锁对象,避免 String.intern() 内存泄漏
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Object> lockMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 空值标记:用于缓存穿透防护
|
||||
* 当 Redis 查询返回 null 时,写入此标记到本地缓存(短 TTL)
|
||||
*/
|
||||
private static final Object NULL_PLACEHOLDER = new Object();
|
||||
|
||||
/**
|
||||
* 空值短缓存过期时间(秒)
|
||||
*/
|
||||
private static final long NULL_CACHE_TTL_SECONDS = 30;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@@ -44,14 +63,38 @@ public class CacheManager {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空值短缓存(独立的 Caffeine 实例,TTL 更短)
|
||||
* 用于缓存穿透防护:缓存不存在的 Key,避免反复查询 Redis
|
||||
*/
|
||||
private final Cache<String, Object> nullValueCache = Caffeine.newBuilder()
|
||||
.maximumSize(10000)
|
||||
.expireAfterWrite(NULL_CACHE_TTL_SECONDS, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
// 构造复合 Key
|
||||
private String buildCacheKey(String hashKey, String key) {
|
||||
return hashKey + ":" + key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询缓存(带缓存穿透防护 + 缓存击穿防护)
|
||||
*
|
||||
* 查询流程:
|
||||
* 1. 先查本地缓存(L1)→ 命中则返回
|
||||
* 2. 再查空值短缓存 → 命中则返回 null(避免穿透)
|
||||
* 3. 加互斥锁,double-check 本地缓存
|
||||
* 4. 查 Redis(L2)→ 未命中则写入空值短缓存
|
||||
* 5. Redis 命中 → 记录访问次数 → 热 Key 提升到 L1
|
||||
*
|
||||
* @param hashKey Redis Hash Key
|
||||
* @param key Redis Hash Field
|
||||
* @return 缓存值,null 表示不存在
|
||||
*/
|
||||
public Object get(String hashKey, String key) {
|
||||
String compositeKey = this.buildCacheKey(hashKey, key);
|
||||
// 1. 查本地缓存
|
||||
|
||||
// 1. 查本地缓存(L1)
|
||||
Object value = localCache.getIfPresent(compositeKey);
|
||||
if (value != null) {
|
||||
log.info("本地缓存获取到数据 {} = {}", compositeKey, value);
|
||||
@@ -59,23 +102,61 @@ public class CacheManager {
|
||||
hotKeyDetector.add(key, 1);
|
||||
return value;
|
||||
}
|
||||
// 2. 本地缓存未命中,查询 Redis
|
||||
Object redisValue = redisTemplate.opsForHash().get(hashKey, key);
|
||||
if (redisValue == null) {
|
||||
|
||||
// 2. 查空值短缓存(防缓存穿透)
|
||||
Object nullMarker = nullValueCache.getIfPresent(compositeKey);
|
||||
if (nullMarker != null) {
|
||||
log.debug("空值短缓存命中,Key: {}", compositeKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 记录访问次数
|
||||
AddResult addResult = hotKeyDetector.add(key, 1);
|
||||
// 3. 加互斥锁,防止缓存击穿(热 Key 过期时单线程回源)
|
||||
Object lock = lockMap.computeIfAbsent(compositeKey, k -> new Object());
|
||||
synchronized (lock) {
|
||||
try {
|
||||
// double-check:再次检查本地缓存(可能其他线程已回源完成)
|
||||
value = localCache.getIfPresent(compositeKey);
|
||||
if (value != null) {
|
||||
log.info("double-check 本地缓存命中 {} = {}", compositeKey, value);
|
||||
hotKeyDetector.add(key, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
// 4. 缓存数据 热Key
|
||||
if (addResult.isHotKey()) {
|
||||
localCache.put(compositeKey, redisValue);
|
||||
// 4. 查询 Redis(L2)
|
||||
Object redisValue = redisTemplate.opsForHash().get(hashKey, key);
|
||||
if (redisValue == null) {
|
||||
// 防缓存穿透:写入空值短缓存
|
||||
nullValueCache.put(compositeKey, NULL_PLACEHOLDER);
|
||||
log.debug("Redis 未命中,写入空值短缓存,Key: {}", compositeKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. 记录访问次数
|
||||
AddResult addResult = hotKeyDetector.add(key, 1);
|
||||
|
||||
// 6. 热 Key 提升到本地缓存
|
||||
if (addResult.isHotKey()) {
|
||||
localCache.put(compositeKey, redisValue);
|
||||
log.info("热 Key 提升到本地缓存: {}", compositeKey);
|
||||
}
|
||||
|
||||
return redisValue;
|
||||
} finally {
|
||||
// 清理锁对象(可选:避免 lockMap 无限膨胀)
|
||||
// 注意:这里不清理,因为 computeIfAbsent 会复用已有锁对象
|
||||
// 若需清理,可在锁内判断是否还有其他线程等待
|
||||
}
|
||||
}
|
||||
|
||||
return redisValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新本地缓存(仅更新已存在的 Key)
|
||||
* 用于写操作后保持 L1 缓存一致性
|
||||
*
|
||||
* @param hashKey Redis Hash Key
|
||||
* @param key Redis Hash Field
|
||||
* @param value 新值
|
||||
*/
|
||||
public void putIfPresent(String hashKey, String key, Object value) {
|
||||
String compositeKey = this.buildCacheKey(hashKey, key);
|
||||
Object object = localCache.getIfPresent(compositeKey);
|
||||
@@ -85,6 +166,19 @@ public class CacheManager {
|
||||
localCache.put(compositeKey, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失效本地缓存
|
||||
* 用于写操作后主动清除缓存
|
||||
*
|
||||
* @param hashKey Redis Hash Key
|
||||
* @param key Redis Hash Field
|
||||
*/
|
||||
public void evict(String hashKey, String key) {
|
||||
String compositeKey = this.buildCacheKey(hashKey, key);
|
||||
localCache.invalidate(compositeKey);
|
||||
nullValueCache.invalidate(compositeKey);
|
||||
}
|
||||
|
||||
// 定时清理过期的 HotKey 数据
|
||||
@Scheduled(fixedRate = 20, timeUnit = TimeUnit.SECONDS)
|
||||
public void cleanHotKeys() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package cn.hezhaohui.thumb.service.impl;
|
||||
import cn.hezhaohui.thumb.constant.ThumbConstant;
|
||||
import cn.hezhaohui.thumb.exception.BusinessException;
|
||||
import cn.hezhaohui.thumb.exception.ErrorCode;
|
||||
import cn.hezhaohui.thumb.manager.cache.BloomFilterManager;
|
||||
import cn.hezhaohui.thumb.manager.cache.CacheManager;
|
||||
import cn.hezhaohui.thumb.model.dto.DoThumbRequest;
|
||||
import cn.hezhaohui.thumb.model.entity.Blog;
|
||||
@@ -10,6 +11,7 @@ import cn.hezhaohui.thumb.model.entity.User;
|
||||
import cn.hezhaohui.thumb.service.BlogService;
|
||||
import cn.hezhaohui.thumb.service.UserService;
|
||||
import cn.hezhaohui.thumb.util.RedisKeyUtil;
|
||||
import cn.hezhaohui.thumb.util.RedisLockUtil;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import cn.hezhaohui.thumb.model.entity.Thumb;
|
||||
import cn.hezhaohui.thumb.service.ThumbService;
|
||||
@@ -22,14 +24,19 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* @author 23117
|
||||
* @description 针对表【thumb】的数据库操作Service实现
|
||||
* @createDate 2025-10-18 14:51:00
|
||||
*/
|
||||
* @author 23117
|
||||
* @description 针对表【thumb】的数据库操作Service实现
|
||||
* @createDate 2025-10-18 14:51:00
|
||||
*
|
||||
* 改进点:
|
||||
* 1. 使用 Redis 分布式锁替代 synchronized + intern(),支持多实例部署,避免内存泄漏
|
||||
* 2. 集成布隆过滤器,防止缓存穿透
|
||||
* 3. 使用 CacheManager 的二级缓存(含空值短缓存 + 互斥锁防击穿)
|
||||
*/
|
||||
@Service("thumbService")
|
||||
@Slf4j
|
||||
public class ThumbServiceImpl extends ServiceImpl<ThumbMapper, Thumb>
|
||||
implements ThumbService{
|
||||
implements ThumbService {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
@@ -46,19 +53,29 @@ public class ThumbServiceImpl extends ServiceImpl<ThumbMapper, Thumb>
|
||||
@Resource
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@Resource
|
||||
private RedisLockUtil redisLockUtil;
|
||||
|
||||
@Resource
|
||||
private BloomFilterManager bloomFilterManager;
|
||||
|
||||
@Override
|
||||
public Boolean doThumb(DoThumbRequest doThumbRequest, HttpServletRequest request) {
|
||||
if (doThumbRequest == null || doThumbRequest.getBlogId() == null) {
|
||||
throw new RuntimeException("参数异常");
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
// Lock: 这里使用字符串加锁,获取字符串常量对象才可运行,字符串常量对象具有唯一性
|
||||
synchronized (("LOCK-USERID-" + loginUser.getId().toString()).intern()) {
|
||||
// Transaction: 编程式事务
|
||||
Long userId = loginUser.getId();
|
||||
Long blogId = doThumbRequest.getBlogId();
|
||||
|
||||
// 分布式锁:对用户加锁,确保同一用户的并发请求串行执行
|
||||
// 替代原来的 synchronized + intern(),解决内存泄漏问题,支持多实例部署
|
||||
String lockKey = "USERID:" + userId;
|
||||
return redisLockUtil.executeWithLock(lockKey, () -> {
|
||||
// 编程式事务
|
||||
return transactionTemplate.execute(status -> {
|
||||
Long blogId = doThumbRequest.getBlogId();
|
||||
// 判断是否已点过赞
|
||||
Boolean exists = this.hasThumb(blogId, loginUser.getId());
|
||||
Boolean exists = this.hasThumb(blogId, userId);
|
||||
if (exists) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户已点赞");
|
||||
}
|
||||
@@ -69,23 +86,23 @@ public class ThumbServiceImpl extends ServiceImpl<ThumbMapper, Thumb>
|
||||
.update();
|
||||
// 更新点赞表数据
|
||||
Thumb thumb = new Thumb();
|
||||
thumb.setUserid(loginUser.getId());
|
||||
thumb.setUserid(userId);
|
||||
thumb.setBlogId(blogId);
|
||||
// 两者一起执行
|
||||
boolean success = update && this.save(thumb);
|
||||
// 点赞记录存入 Redis
|
||||
// 点赞记录存入 Redis + 更新布隆过滤器
|
||||
if (success) {
|
||||
// 一级缓存
|
||||
// redisTemplate.opsForHash().put(RedisKeyUtil.getUserThumbKey(loginUser.getId()), blogId.toString(), thumb.getId());
|
||||
String hashKey = ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId();
|
||||
String hashKey = ThumbConstant.USER_THUMB_KEY_PREFIX + userId;
|
||||
String filedKey = blogId.toString();
|
||||
Long realThumbId = thumb.getId();
|
||||
redisTemplate.opsForHash().put(hashKey, filedKey, realThumbId);
|
||||
cacheManager.putIfPresent(hashKey, filedKey, realThumbId);
|
||||
// 添加到布隆过滤器(防缓存穿透)
|
||||
bloomFilterManager.add(userId, blogId);
|
||||
}
|
||||
return success;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,13 +111,19 @@ public class ThumbServiceImpl extends ServiceImpl<ThumbMapper, Thumb>
|
||||
throw new RuntimeException("参数异常");
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
// Lock: 这里使用字符串加锁,获取字符串常量对象才可运行,字符串常量对象具有唯一性
|
||||
synchronized (("LOCK-USERID-" + loginUser.getId().toString()).intern()) {
|
||||
// Transaction: 编程式事务
|
||||
Long userId = loginUser.getId();
|
||||
Long blogId = doThumbRequest.getBlogId();
|
||||
|
||||
// 分布式锁:对用户加锁
|
||||
String lockKey = "USERID:" + userId;
|
||||
return redisLockUtil.executeWithLock(lockKey, () -> {
|
||||
// 编程式事务
|
||||
return transactionTemplate.execute(status -> {
|
||||
Long blogId = doThumbRequest.getBlogId();
|
||||
// 判断是否已点过赞
|
||||
Object thumbIdObj = cacheManager.get(ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId(), blogId.toString());
|
||||
Object thumbIdObj = cacheManager.get(
|
||||
ThumbConstant.USER_THUMB_KEY_PREFIX + userId,
|
||||
blogId.toString()
|
||||
);
|
||||
if (thumbIdObj == null || thumbIdObj.equals(ThumbConstant.UN_THUMB_CONSTANT)) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户未点赞");
|
||||
}
|
||||
@@ -111,37 +134,36 @@ public class ThumbServiceImpl extends ServiceImpl<ThumbMapper, Thumb>
|
||||
.setSql("thumbCount = thumbCount - 1")
|
||||
.update();
|
||||
// 更新点赞表数据
|
||||
// 两者一起执行
|
||||
boolean success = update && this.removeById(((Number)thumbIdObj).longValue());
|
||||
boolean success = update && this.removeById(thumbId);
|
||||
if (success) {
|
||||
// 一级缓存
|
||||
// redisTemplate.opsForHash().delete(RedisKeyUtil.getUserThumbKey(loginUser.getId()), blogId.toString());
|
||||
String hashKey = ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId();
|
||||
String hashKey = ThumbConstant.USER_THUMB_KEY_PREFIX + userId;
|
||||
String fieldKey = blogId.toString();
|
||||
redisTemplate.opsForHash().delete(hashKey, fieldKey);
|
||||
cacheManager.putIfPresent(hashKey, fieldKey, ThumbConstant.UN_THUMB_CONSTANT);
|
||||
}
|
||||
return success;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean hasThumb(Long blogId, Long userId) {
|
||||
// 一级缓存
|
||||
// return redisTemplate.opsForHash().hasKey(RedisKeyUtil.getUserThumbKey(userId), blogId.toString());
|
||||
// 布隆过滤器前置判断:若返回 false,则一定没有点赞过,直接返回
|
||||
// 这是缓存穿透防护的第一道防线
|
||||
if (!bloomFilterManager.mightContain(userId, blogId)) {
|
||||
log.debug("布隆过滤器拦截:用户 {} 未点赞博客 {}", userId, blogId);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Object thumbIdObj = cacheManager.get(ThumbConstant.USER_THUMB_KEY_PREFIX + userId, blogId.toString());;
|
||||
// 通过布隆过滤器后,查询二级缓存(含空值短缓存 + 互斥锁防击穿)
|
||||
Object thumbIdObj = cacheManager.get(
|
||||
ThumbConstant.USER_THUMB_KEY_PREFIX + userId,
|
||||
blogId.toString()
|
||||
);
|
||||
if (thumbIdObj == null) {
|
||||
return false;
|
||||
}
|
||||
Long thumbId = ((Number) thumbIdObj).longValue();
|
||||
return !thumbId.equals(ThumbConstant.UN_THUMB_CONSTANT);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package cn.hezhaohui.thumb.util;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis 分布式锁工具类
|
||||
* 基于 Redis SETNX + PX(毫秒过期)实现
|
||||
*
|
||||
* 设计要点:
|
||||
* 1. 使用 SETNX 保证原子性加锁
|
||||
* 2. 设置过期时间防止死锁
|
||||
* 3. 使用 Lua 脚本保证释放锁的原子性(只释放自己持有的锁)
|
||||
* 4. 使用 ThreadLocal 存储锁标识,避免误释放其他线程的锁
|
||||
*
|
||||
* 适用场景:多实例部署时,需要跨 JVM 的互斥机制
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class RedisLockUtil {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 锁前缀
|
||||
*/
|
||||
private static final String LOCK_PREFIX = "lock:thumb:";
|
||||
|
||||
/**
|
||||
* 默认锁过期时间(毫秒)
|
||||
*/
|
||||
private static final long DEFAULT_LOCK_TIMEOUT = 10000L;
|
||||
|
||||
/**
|
||||
* 默认获取锁超时时间(毫秒)
|
||||
*/
|
||||
private static final long DEFAULT_ACQUIRE_TIMEOUT = 3000L;
|
||||
|
||||
/**
|
||||
* 重试间隔(毫秒)
|
||||
*/
|
||||
private static final long RETRY_INTERVAL = 50L;
|
||||
|
||||
/**
|
||||
* ThreadLocal 存储当前线程持有的锁标识
|
||||
* 用于释放锁时验证是否是自己持有的锁
|
||||
*/
|
||||
private static final ThreadLocal<String> LOCK_VALUE_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 释放锁的 Lua 脚本
|
||||
* 原子性操作:验证锁标识 → 删除锁
|
||||
* 只有锁的持有者才能释放锁,避免误释放
|
||||
*/
|
||||
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>("""
|
||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('del', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
""", Long.class);
|
||||
|
||||
/**
|
||||
* 构建锁 Key
|
||||
*
|
||||
* @param lockKey 业务锁 Key
|
||||
* @return 完整的 Redis Key
|
||||
*/
|
||||
private String buildLockKey(String lockKey) {
|
||||
return LOCK_PREFIX + lockKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成锁标识(UUID + 线程ID)
|
||||
* 确保每个线程的锁标识唯一
|
||||
*
|
||||
* @return 锁标识
|
||||
*/
|
||||
private String generateLockValue() {
|
||||
return UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取分布式锁(使用默认超时时间)
|
||||
*
|
||||
* @param lockKey 业务锁 Key(如 "userId:123")
|
||||
* @return true = 获取成功,false = 获取失败
|
||||
*/
|
||||
public boolean tryLock(String lockKey) {
|
||||
return tryLock(lockKey, DEFAULT_ACQUIRE_TIMEOUT, DEFAULT_LOCK_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取分布式锁
|
||||
*
|
||||
* @param lockKey 业务锁 Key
|
||||
* @param acquireTimeout 获取锁超时时间(毫秒)
|
||||
* @param lockTimeout 锁过期时间(毫秒)
|
||||
* @return true = 获取成功,false = 获取失败
|
||||
*/
|
||||
public boolean tryLock(String lockKey, long acquireTimeout, long lockTimeout) {
|
||||
String fullLockKey = buildLockKey(lockKey);
|
||||
String lockValue = generateLockValue();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
while (true) {
|
||||
// 尝试 SETNX
|
||||
Boolean result = stringRedisTemplate.opsForValue()
|
||||
.setIfAbsent(fullLockKey, lockValue, lockTimeout, TimeUnit.MILLISECONDS);
|
||||
|
||||
if (Boolean.TRUE.equals(result)) {
|
||||
// 获取成功,存储锁标识到 ThreadLocal
|
||||
LOCK_VALUE_HOLDER.set(lockValue);
|
||||
log.debug("获取分布式锁成功,Key: {}, Thread: {}", lockKey, Thread.currentThread().getId());
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
if (System.currentTimeMillis() - startTime >= acquireTimeout) {
|
||||
log.warn("获取分布式锁超时,Key: {}, Thread: {}", lockKey, Thread.currentThread().getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 短暂等待后重试
|
||||
try {
|
||||
Thread.sleep(RETRY_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("获取分布式锁被中断,Key: {}", lockKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分布式锁
|
||||
* 使用 Lua 脚本保证原子性:只有锁的持有者才能释放锁
|
||||
*
|
||||
* @param lockKey 业务锁 Key
|
||||
* @return true = 释放成功,false = 释放失败(锁已过期或不属于当前线程)
|
||||
*/
|
||||
public boolean unlock(String lockKey) {
|
||||
String fullLockKey = buildLockKey(lockKey);
|
||||
String lockValue = LOCK_VALUE_HOLDER.get();
|
||||
|
||||
if (lockValue == null) {
|
||||
log.warn("释放分布式锁失败:当前线程未持有锁,Key: {}, Thread: {}", lockKey, Thread.currentThread().getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行 Lua 脚本:验证锁标识 → 删除锁
|
||||
Long result = stringRedisTemplate.execute(
|
||||
UNLOCK_SCRIPT,
|
||||
Collections.singletonList(fullLockKey),
|
||||
lockValue
|
||||
);
|
||||
|
||||
boolean success = Long.valueOf(1L).equals(result);
|
||||
if (success) {
|
||||
log.debug("释放分布式锁成功,Key: {}, Thread: {}", lockKey, Thread.currentThread().getId());
|
||||
} else {
|
||||
log.warn("释放分布式锁失败:锁已过期或不属于当前线程,Key: {}, Thread: {}", lockKey, Thread.currentThread().getId());
|
||||
}
|
||||
return success;
|
||||
} finally {
|
||||
// 清理 ThreadLocal
|
||||
LOCK_VALUE_HOLDER.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在锁保护下执行业务逻辑
|
||||
* 自动处理加锁和释放锁,确保锁一定会被释放
|
||||
*
|
||||
* @param lockKey 业务锁 Key
|
||||
* @param action 业务逻辑
|
||||
* @param <T> 返回值类型
|
||||
* @return 业务逻辑的返回值
|
||||
* @throws RuntimeException 获取锁失败或业务逻辑异常
|
||||
*/
|
||||
public <T> T executeWithLock(String lockKey, LockAction<T> action) {
|
||||
boolean locked = false;
|
||||
try {
|
||||
locked = tryLock(lockKey);
|
||||
if (!locked) {
|
||||
throw new RuntimeException("获取分布式锁失败,Key: " + lockKey);
|
||||
}
|
||||
return action.execute();
|
||||
} finally {
|
||||
if (locked) {
|
||||
unlock(lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁保护下的业务逻辑接口
|
||||
*
|
||||
* @param <T> 返回值类型
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LockAction<T> {
|
||||
T execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user