org.springframework.boot
diff --git a/src/main/java/com/tailbet/config/RedissonConfig.java b/src/main/java/com/tailbet/config/RedissonConfig.java
new file mode 100644
index 0000000..b5b1785
--- /dev/null
+++ b/src/main/java/com/tailbet/config/RedissonConfig.java
@@ -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);
+ }
+}
diff --git a/src/main/java/com/tailbet/util/RedisLock.java b/src/main/java/com/tailbet/util/RedisLock.java
deleted file mode 100644
index bab983e..0000000
--- a/src/main/java/com/tailbet/util/RedisLock.java
+++ /dev/null
@@ -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;
-}
diff --git a/src/main/java/com/tailbet/util/RedisLockUtil.java b/src/main/java/com/tailbet/util/RedisLockUtil.java
index 58ca953..857b06e 100644
--- a/src/main/java/com/tailbet/util/RedisLockUtil.java
+++ b/src/main/java/com/tailbet/util/RedisLockUtil.java
@@ -1,83 +1,294 @@
package com.tailbet.util;
-import cn.hutool.core.util.StrUtil;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
-import java.util.Collections;
import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
/**
- * Redis分布式锁工具
+ * Redis 分布式锁工具类(静态工具方法)
+ *
+ * 使用 Redisson 实现,支持:
+ *
+ * - tryLock 模式(等待时间 + 租赁时间)
+ * - lock 模式(阻塞获取)
+ * - 事务提交后释放锁(防止脏读)
+ *
+ *
+ * 使用前需要调用 {@link #setRedissonClient(RedissonClient)} 初始化,
+ * 或通过 Spring 注入 {@link RedisLockUtil} bean 并调用初始化方法。
*/
@Slf4j
-@Component
-@RequiredArgsConstructor
public class RedisLockUtil {
- private final StringRedisTemplate redisTemplate;
+ private static final String LOCK_PREFIX = "redis:lock:";
- private static final String LOCK_PREFIX = "lock:";
-
- 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";
+ private static RedissonClient redissonClient;
/**
- * 加锁
+ * 设置 RedissonClient 实例(静态初始化)
*/
- public boolean lock(String key, String value, long expireSeconds) {
- String lockKey = LOCK_PREFIX + key;
- Boolean result = redisTemplate.opsForValue()
- .setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
- return Boolean.TRUE.equals(result);
+ public static void setRedissonClient(RedissonClient client) {
+ redissonClient = client;
}
- /**
- * 解锁
- */
- public boolean unlock(String key, String value) {
- String lockKey = LOCK_PREFIX + key;
- DefaultRedisScript script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
- Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), value);
- return result != null && result == 1;
+ public static RedissonClient getRedissonClient() {
+ return redissonClient;
}
- /**
- * 尝试获取锁
- */
- public String tryLock(String key, long expireSeconds, long waitTimeMillis) {
- String lockKey = LOCK_PREFIX + key;
- String value = String.valueOf(System.currentTimeMillis());
- long startTime = System.currentTimeMillis();
+ private RedisLockUtil() {
+ // 工具类禁止实例化
+ }
- while (System.currentTimeMillis() - startTime < waitTimeMillis) {
- Boolean result = redisTemplate.opsForValue()
- .setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
- if (Boolean.TRUE.equals(result)) {
- return value;
+ // ==================== tryLock 模式 ====================
+
+ /**
+ * 尝试获取锁,执行逻辑后释放锁
+ *
+ * @param lockName 锁名称(会自动添加前缀)
+ * @param waitTime 等待时间
+ * @param leaseTime 租赁时间
+ * @param timeUnit 时间单位
+ * @param supplier 获取锁后执行的逻辑
+ * @return 执行结果,锁获取失败返回 null
+ */
+ public static T tryLock(String lockName, int waitTime, int leaseTime,
+ TimeUnit timeUnit, Supplier supplier) {
+ String fullLockName = LOCK_PREFIX + lockName;
+ RLock lock = redissonClient.getLock(fullLockName);
+ boolean locked = false;
+ try {
+ locked = lock.tryLock(waitTime, leaseTime, timeUnit);
+ if (locked) {
+ return supplier.get();
}
- try {
- TimeUnit.MILLISECONDS.sleep(50);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- return null;
+ log.warn("[RedisLock] 获取锁失败, lockName={}", lockName);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ log.error("[RedisLock] 获取锁被中断, lockName={}", lockName, e);
+ } finally {
+ if (locked && lock.isHeldByCurrentThread()) {
+ lock.unlock();
}
}
return null;
}
/**
- * 释放锁
+ * 尝试获取锁,执行逻辑后释放锁(使用默认时间单位秒)
*/
- public void delete(String key) {
- String lockKey = LOCK_PREFIX + key;
- redisTemplate.delete(lockKey);
+ public static T tryLock(String lockName, int waitTime, int leaseTime,
+ Supplier supplier) {
+ return tryLock(lockName, waitTime, leaseTime, TimeUnit.SECONDS, supplier);
+ }
+
+ /**
+ * 尝试获取锁,执行逻辑后释放锁(使用默认租赁时间10秒)
+ */
+ public static T tryLock(String lockName, int waitTime, Supplier supplier) {
+ return tryLock(lockName, waitTime, 10, TimeUnit.SECONDS, supplier);
+ }
+
+ /**
+ * 尝试获取锁,执行逻辑后释放锁(使用默认等待时间3秒,租赁时间10秒)
+ */
+ public static T tryLock(String lockName, Supplier supplier) {
+ return tryLock(lockName, 3, 10, TimeUnit.SECONDS, supplier);
+ }
+
+ // ==================== lock 模式(阻塞获取) ====================
+
+ /**
+ * 阻塞获取锁,执行逻辑后释放锁
+ *
+ * @param lockName 锁名称
+ * @param supplier 获取锁后执行的逻辑
+ */
+ public static T lock(String lockName, Supplier supplier) {
+ return lock(lockName, 0, -1, TimeUnit.SECONDS, supplier);
+ }
+
+ /**
+ * 阻塞获取锁(带租赁时间),执行逻辑后释放锁
+ */
+ public static T lock(String lockName, int leaseTime, TimeUnit timeUnit,
+ Supplier supplier) {
+ return lock(lockName, 0, leaseTime, timeUnit, supplier);
+ }
+
+ /**
+ * 阻塞获取锁(带等待时间和租赁时间),执行逻辑后释放锁
+ */
+ public static T lock(String lockName, int waitTime, int leaseTime,
+ TimeUnit timeUnit, Supplier 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();
+ }
+ }
+ }
+
+ // ==================== 事务提交后释放锁 ====================
+
+ /**
+ * 尝试获取锁,在事务提交后再释放锁(防止并发读取未提交数据)
+ *
+ * 适用于:分布式锁 + 数据库事务 配合使用的场景
+ *
+ * @param lockName 锁名称
+ * @param waitTime 等待时间
+ * @param leaseTime 租赁时间
+ * @param timeUnit 时间单位
+ * @param supplier 获取锁后执行的逻辑
+ * @return 执行结果,锁获取失败返回 null
+ */
+ public static T tryLockAfterCommit(String lockName, int waitTime, int leaseTime,
+ TimeUnit timeUnit, Supplier supplier) {
+ return tryLockAfterCommit(lockName, waitTime, leaseTime, timeUnit, false, supplier);
+ }
+
+ /**
+ * 尝试获取锁,在事务提交后再释放锁(使用默认等待时间3秒,租赁时间10秒)
+ */
+ public static T tryLockAfterCommit(String lockName, Supplier 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 tryLockAfterCommit(String lockName, int waitTime, int leaseTime,
+ TimeUnit timeUnit, boolean releaseOnRollback,
+ Supplier 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 环境下使用的初始化组件
+ *
+ * 在 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 初始化完成");
+ }
}
}