diff --git a/pom.xml b/pom.xml index 1894bc0..148f33e 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,11 @@ runtime + + org.redisson + redisson-spring-boot-starter + 3.44.0 + 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 实现,支持: + *

+ *

+ * 使用前需要调用 {@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 初始化完成"); + } } }