RedisLockUtil

This commit is contained in:
wells
2026-08-05 17:55:52 +08:00
parent 6ec17b45f0
commit ecb372119c
4 changed files with 306 additions and 84 deletions
+5
View File
@@ -90,6 +90,11 @@
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.44.0</version>
</dependency>
<!-- Spring Boot Test --> <!-- Spring Boot Test -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -0,0 +1,38 @@
package com.tailbet.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedissonConfig {
@Autowired
private RedisProperties redisProps;
@Bean(destroyMethod = "shutdown")
RedissonClient redissonSingle() {
Config config = new Config();
config.setCodec(new JsonJacksonCodec());
config.useSingleServer()
.setAddress("redis://" + redisProps.getHost() + ":" + redisProps.getPort())
.setDatabase(redisProps.getDatabase())
.setPassword(redisProps.getPassword())
.setTimeout(3000)
.setRetryAttempts(3)// # 如果尝试在此限制之内发送成功,则开始启用 timeout 计时。
.setRetryInterval(1000)//命令重试发送时间间隔,单位:毫秒
// .setPingConnectionInterval(5000)//**此项务必设置为redisson解决之前bug的timeout问题关键*****
.setIdleConnectionTimeout(10000)//连接空闲超时,单位:毫秒
.setConnectionPoolSize(2000)//# 连接池大小
.setConnectionMinimumIdleSize(20)//最小空闲连接数
;
return Redisson.create(config);
}
}
@@ -1,32 +0,0 @@
package com.tailbet.util;
import java.lang.annotation.*;
/**
* Redis分布式锁注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/**
* 锁的key,支持SpEL表达式
*/
String key();
/**
* 锁过期时间(秒)
*/
int expire() default 30;
/**
* 是否等待锁
*/
boolean waitLock() default false;
/**
* 等待锁的最大时间(毫秒)
*/
long waitTime() default 3000;
}
+262 -51
View File
@@ -1,83 +1,294 @@
package com.tailbet.util; package com.tailbet.util;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate; import org.redisson.api.RLock;
import org.springframework.data.redis.core.script.DefaultRedisScript; import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/** /**
* Redis分布式锁工具 * Redis 分布式锁工具类(静态工具方法)
* <p>
* 使用 Redisson 实现,支持:
* <ul>
* <li>tryLock 模式(等待时间 + 租赁时间)</li>
* <li>lock 模式(阻塞获取)</li>
* <li>事务提交后释放锁(防止脏读)</li>
* </ul>
* <p>
* 使用前需要调用 {@link #setRedissonClient(RedissonClient)} 初始化,
* 或通过 Spring 注入 {@link RedisLockUtil} bean 并调用初始化方法。
*/ */
@Slf4j @Slf4j
@Component
@RequiredArgsConstructor
public class RedisLockUtil { public class RedisLockUtil {
private final StringRedisTemplate redisTemplate; private static final String LOCK_PREFIX = "redis:lock:";
private static final String LOCK_PREFIX = "lock:"; private static RedissonClient redissonClient;
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
/** /**
* 加锁 * 设置 RedissonClient 实例(静态初始化)
*/ */
public boolean lock(String key, String value, long expireSeconds) { public static void setRedissonClient(RedissonClient client) {
String lockKey = LOCK_PREFIX + key; redissonClient = client;
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
} }
public static RedissonClient getRedissonClient() {
return redissonClient;
}
private RedisLockUtil() {
// 工具类禁止实例化
}
// ==================== tryLock 模式 ====================
/** /**
* * 尝试获取锁,执行逻辑后释放
*
* @param lockName 锁名称(会自动添加前缀)
* @param waitTime 等待时间
* @param leaseTime 租赁时间
* @param timeUnit 时间单位
* @param supplier 获取锁后执行的逻辑
* @return 执行结果,锁获取失败返回 null
*/ */
public boolean unlock(String key, String value) { public static <T> T tryLock(String lockName, int waitTime, int leaseTime,
String lockKey = LOCK_PREFIX + key; TimeUnit timeUnit, Supplier<T> supplier) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class); String fullLockName = LOCK_PREFIX + lockName;
Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), value); RLock lock = redissonClient.getLock(fullLockName);
return result != null && result == 1; boolean locked = false;
}
/**
* 尝试获取锁
*/
public String tryLock(String key, long expireSeconds, long waitTimeMillis) {
String lockKey = LOCK_PREFIX + key;
String value = String.valueOf(System.currentTimeMillis());
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < waitTimeMillis) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
return value;
}
try { try {
TimeUnit.MILLISECONDS.sleep(50); locked = lock.tryLock(waitTime, leaseTime, timeUnit);
if (locked) {
return supplier.get();
}
log.warn("[RedisLock] 获取锁失败, lockName={}", lockName);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return null; log.error("[RedisLock] 获取锁被中断, lockName={}", lockName, e);
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
} }
} }
return null; return null;
} }
/** /**
* 释放锁 * 尝试获取锁,执行逻辑后释放锁(使用默认时间单位秒)
*/ */
public void delete(String key) { public static <T> T tryLock(String lockName, int waitTime, int leaseTime,
String lockKey = LOCK_PREFIX + key; Supplier<T> supplier) {
redisTemplate.delete(lockKey); return tryLock(lockName, waitTime, leaseTime, TimeUnit.SECONDS, supplier);
}
/**
* 尝试获取锁,执行逻辑后释放锁(使用默认租赁时间10秒)
*/
public static <T> T tryLock(String lockName, int waitTime, Supplier<T> supplier) {
return tryLock(lockName, waitTime, 10, TimeUnit.SECONDS, supplier);
}
/**
* 尝试获取锁,执行逻辑后释放锁(使用默认等待时间3秒,租赁时间10秒)
*/
public static <T> T tryLock(String lockName, Supplier<T> supplier) {
return tryLock(lockName, 3, 10, TimeUnit.SECONDS, supplier);
}
// ==================== lock 模式(阻塞获取) ====================
/**
* 阻塞获取锁,执行逻辑后释放锁
*
* @param lockName 锁名称
* @param supplier 获取锁后执行的逻辑
*/
public static <T> T lock(String lockName, Supplier<T> supplier) {
return lock(lockName, 0, -1, TimeUnit.SECONDS, supplier);
}
/**
* 阻塞获取锁(带租赁时间),执行逻辑后释放锁
*/
public static <T> T lock(String lockName, int leaseTime, TimeUnit timeUnit,
Supplier<T> supplier) {
return lock(lockName, 0, leaseTime, timeUnit, supplier);
}
/**
* 阻塞获取锁(带等待时间和租赁时间),执行逻辑后释放锁
*/
public static <T> T lock(String lockName, int waitTime, int leaseTime,
TimeUnit timeUnit, Supplier<T> supplier) {
String fullLockName = LOCK_PREFIX + lockName;
RLock lock = redissonClient.getLock(fullLockName);
boolean locked = false;
try {
// lock() 方法会一直阻塞直到获取锁
// 如果 leaseTime = -1,则使用 Redisson 默认的 30 秒
if (leaseTime <= 0) {
lock.lock();
} else {
lock.lock(leaseTime, timeUnit);
}
locked = true;
return supplier.get();
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// ==================== 事务提交后释放锁 ====================
/**
* 尝试获取锁,在事务提交后再释放锁(防止并发读取未提交数据)
* <p>
* 适用于:分布式锁 + 数据库事务 配合使用的场景
*
* @param lockName 锁名称
* @param waitTime 等待时间
* @param leaseTime 租赁时间
* @param timeUnit 时间单位
* @param supplier 获取锁后执行的逻辑
* @return 执行结果,锁获取失败返回 null
*/
public static <T> T tryLockAfterCommit(String lockName, int waitTime, int leaseTime,
TimeUnit timeUnit, Supplier<T> supplier) {
return tryLockAfterCommit(lockName, waitTime, leaseTime, timeUnit, false, supplier);
}
/**
* 尝试获取锁,在事务提交后再释放锁(使用默认等待时间3秒,租赁时间10秒)
*/
public static <T> T tryLockAfterCommit(String lockName, Supplier<T> supplier) {
return tryLockAfterCommit(lockName, 3, 10, TimeUnit.SECONDS, false, supplier);
}
/**
* 尝试获取锁,在事务提交后再释放锁
*
* @param lockName 锁名称
* @param waitTime 等待时间
* @param leaseTime 租赁时间
* @param timeUnit 时间单位
* @param releaseOnRollback 是否在事务回滚后释放锁(false 则始终在 afterCommit 释放)
* @param supplier 获取锁后执行的逻辑
* @return 执行结果,锁获取失败返回 null
*/
public static <T> T tryLockAfterCommit(String lockName, int waitTime, int leaseTime,
TimeUnit timeUnit, boolean releaseOnRollback,
Supplier<T> supplier) {
String fullLockName = LOCK_PREFIX + lockName;
RLock lock = redissonClient.getLock(fullLockName);
boolean locked = false;
try {
locked = lock.tryLock(waitTime, leaseTime, timeUnit);
if (!locked) {
log.warn("[RedisLock] 获取锁失败, lockName={}", lockName);
return null;
}
// 有活跃事务,注册 afterCommit 回调释放锁
if (org.springframework.transaction.support.TransactionSynchronizationManager.isSynchronizationActive()) {
org.springframework.transaction.support.TransactionSynchronizationManager
.registerSynchronization(new org.springframework.transaction.support.TransactionSynchronization() {
@Override
public void afterCommit() {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("[RedisLock] 事务提交后释放锁, lockName={}", lockName);
}
}
@Override
public void afterCompletion(int status) {
// 如果设置了回滚时也释放,或者不是回滚
if (releaseOnRollback || status == STATUS_COMMITTED) {
return; // 已在 afterCommit 处理
}
// 回滚情况下释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("[RedisLock] 事务回滚后释放锁, lockName={}", lockName);
}
}
});
return supplier.get();
} else {
// 无事务,直接释放锁
return supplier.get();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("[RedisLock] 获取锁被中断, lockName={}", lockName, e);
return null;
} finally {
// 如果没有活跃事务,需要在这里释放锁
if (locked && lock.isHeldByCurrentThread() &&
!org.springframework.transaction.support.TransactionSynchronizationManager.isSynchronizationActive()) {
lock.unlock();
}
}
}
// ==================== 锁状态检查 ====================
/**
* 检查锁是否被持有
*/
public static boolean isLocked(String lockName) {
String fullLockName = LOCK_PREFIX + lockName;
RLock lock = redissonClient.getLock(fullLockName);
return lock.isLocked();
}
/**
* 检查锁是否被当前线程持有
*/
public static boolean isHeldByCurrentThread(String lockName) {
String fullLockName = LOCK_PREFIX + lockName;
RLock lock = redissonClient.getLock(fullLockName);
return lock.isHeldByCurrentThread();
}
// ==================== 强制释放 ====================
/**
* 强制释放锁(谨慎使用)
*/
public static void forceUnlock(String lockName) {
String fullLockName = LOCK_PREFIX + lockName;
RLock lock = redissonClient.getLock(fullLockName);
if (lock.isLocked()) {
lock.forceUnlock();
log.warn("[RedisLock] 强制释放锁, lockName={}", lockName);
}
}
// ==================== Spring 初始化组件 ====================
/**
* Spring 环境下使用的初始化组件
* <p>
* 在 Spring 环境中,可以将此类配置为 Bean 并在 PostConstruct 中调用 {@link #setRedissonClient(RedissonClient)}
*/
@Component
public static class RedisLockUtilInitializer {
@Autowired
private RedissonClient redissonClient;
@jakarta.annotation.PostConstruct
public void init() {
setRedissonClient(redissonClient);
log.info("[RedisLock] RedissonClient 初始化完成");
}
} }
} }