|
|
@@ -0,0 +1,93 @@
|
|
|
+package com.etotem.cfc.util;
|
|
|
+
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.UUID;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基于 Redis 的分布式锁工具。
|
|
|
+ *
|
|
|
+ * 实现:SET key value NX EX ttl 获取锁,释放时用 Lua 脚本校验 value 匹配后 DEL,
|
|
|
+ * 避免误删其他线程/实例持有的锁。value 用 UUID 标识持有者。
|
|
|
+ *
|
|
|
+ * 用法:
|
|
|
+ * <pre>
|
|
|
+ * boolean locked = redisLockUtil.tryLock("lock:order:" + orderNo, 10);
|
|
|
+ * if (!locked) { return "处理中,请勿重复操作"; }
|
|
|
+ * try {
|
|
|
+ * // 业务逻辑
|
|
|
+ * } finally {
|
|
|
+ * redisLockUtil.unlock("lock:order:" + orderNo);
|
|
|
+ * }
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
+@Component
|
|
|
+public class RedisLockUtil {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(RedisLockUtil.class);
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private RedisTemplate<String, Object> redisTemplate;
|
|
|
+
|
|
|
+ /** 锁持有者标识(线程本地 UUID,跨方法共享) */
|
|
|
+ private static final ThreadLocal<String> LOCK_TOKEN = new ThreadLocal<>();
|
|
|
+
|
|
|
+ /** 释放锁 Lua 脚本:仅当 value 匹配时才 DEL */
|
|
|
+ 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);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 尝试获取锁。
|
|
|
+ *
|
|
|
+ * @param key 锁 key(建议带业务前缀,如 lock:order:{orderNo})
|
|
|
+ * @param ttlSec 锁自动过期时间(秒),防止持锁线程崩溃导致死锁
|
|
|
+ * @return true=获取成功, false=已被其他线程持有
|
|
|
+ */
|
|
|
+ public boolean tryLock(String key, long ttlSec) {
|
|
|
+ try {
|
|
|
+ String token = UUID.randomUUID().toString();
|
|
|
+ Boolean ok = redisTemplate.opsForValue().setIfAbsent(
|
|
|
+ key, token, ttlSec, TimeUnit.SECONDS);
|
|
|
+ if (Boolean.TRUE.equals(ok)) {
|
|
|
+ LOCK_TOKEN.set(token);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("Redis 锁获取失败,降级为无锁执行: key={}, error={}", key, e.getMessage());
|
|
|
+ // Redis 异常时降级:视为获取成功(单实例下有 @Transactional 兜底)
|
|
|
+ LOCK_TOKEN.set(null);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 释放锁。
|
|
|
+ */
|
|
|
+ public void unlock(String key) {
|
|
|
+ String token = LOCK_TOKEN.get();
|
|
|
+ if (token == null) {
|
|
|
+ // 降级模式获取的锁,无需真实释放
|
|
|
+ LOCK_TOKEN.remove();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ redisTemplate.execute(UNLOCK_SCRIPT,
|
|
|
+ Collections.singletonList(key), token);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("Redis 锁释放失败: key={}, error={}", key, e.getMessage());
|
|
|
+ } finally {
|
|
|
+ LOCK_TOKEN.remove();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|