首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 實現基於 Redis 的分布式鎖:從原理到生產級代碼完整指南

     編輯:Java 相關知識

為什麼需要分布式鎖?

單體應用中 synchronized/ReentrantLock 僅限於單 JVM。微服務、集群部署下,跨進程同步需借助外部共享存儲(Redis、ZooKeeper、etcd、資料庫)。Redis 因高效能、原子操作豐富、部署簡單成為首選。

核心原理:SETNX + 過期時間 + Lua 腳本

演進歷程

  1. setnx(key, value):僅當 key 不存在時設值,返回 1 成功、0 失敗。缺陷:無原子設定過期,客戶端崩潰導致死鎖。
  2. setnx + expire:兩條指令非原子,中間崩潰同樣死鎖。
  3. set(key, value, NX, EX, seconds):Redis 2.6.12+ 原子指令,**生產環境標準寫法**。
  4. Lua 腳本釋放鎖:if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end 保證只能刪除自己的鎖,防誤刪。

生產級鎖介面設計


public interface DistributedLock extends AutoCloseable {
    /**
     * 獲取鎖(阻塞)
     * @param timeout 最大等待時間
     * @param unit 時間單位
     * @return true 獲取成功,false 超時失敗
     * @throws InterruptedException 線程被中斷
     */
    boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;

    /**
     * 非阻塞嘗試獲取鎖
     * @return true 成功,false 立即失敗
     */
    boolean tryLock();

    /**
     * 釋放鎖
     * @throws IllegalMonitorStateException 當前線程不持有鎖
     */
    void unlock();

    /** 是否由當前線程持有 */
    boolean isHeldByCurrentThread();

    /** 關閉釋放資源(實現 AutoCloseable 支援 try-with-resources) */
    @Override void close();
}

完整實現:RedisDistributedLock


public class RedisDistributedLock implements DistributedLock {
    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 final DefaultRedisScript<Long> UNLOCK_SCRIPT_OBJ = 
        new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);

    private final RedisTemplate<String, String> redisTemplate;
    private final String lockKey;
    private final long expireMillis;
    private final String lockValue; // UUID + ThreadId,唯一標識鎖擁有者
    private volatile boolean locked = false;

    public RedisDistributedLock(RedisTemplate<String, String> redisTemplate, 
                                String lockKey, long expireMillis) {
        this.redisTemplate = redisTemplate;
        this.lockKey = "lock:" + lockKey;
        this.expireMillis = expireMillis;
        this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
    }

    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        long timeoutNanos = unit.toNanos(timeout);
        long start = System.nanoTime();
        
        while (true) {
            // 1. 原子獲取鎖
            Boolean acquired = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
            
            if (Boolean.TRUE.equals(acquired)) {
                locked = true;
                return true;
            }

            // 2. 計算剩餘等待時間
            long elapsed = System.nanoTime() - start;
            long remaining = timeoutNanos - elapsed;
            if (remaining <= 0) return false;

            // 3. 短暫休眠避免佔用 CPU(可優化為 Redis Pub/Sub 通知)
            Thread.sleep(Math.min(50, TimeUnit.NANOSECONDS.toMillis(remaining)));
            
            // 4. 檢查中斷
            if (Thread.interrupted()) throw new InterruptedException();
        }
    }

    @Override
    public boolean tryLock() {
        Boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
        locked = Boolean.TRUE.equals(acquired);
        return locked;
    }

    @Override
    public void unlock() {
        if (!locked) throw new IllegalMonitorStateException("當前線程不持有鎖");
        
        // 執行 Lua 腳本原子釋放
        Long result = redisTemplate.execute(UNLOCK_SCRIPT_OBJ, 
            Collections.singletonList(lockKey), lockValue);
        
        locked = false;
        if (result == null || result == 0) {
            // 鎖已過期被他人持有,記錄警告
            log.warn("鎖釋放失敗,可能已過期轉移: key={}, value={}", lockKey, lockValue);
        }
    }

    @Override
    public boolean isHeldByCurrentThread() {
        if (!locked) return false;
        String current = redisTemplate.opsForValue().get(lockKey);
        return lockValue.equals(current);
    }

    @Override
    public void close() {
        if (locked) unlock();
    }
}

使用範例:分布式 ID 生成器


@Service
public class DistributedIdGenerator {
    private final RedisTemplate<String, String> redisTemplate;
    private final AtomicLong localCounter = new AtomicLong(0);

    public DistributedIdGenerator(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String nextId(String bizTag) {
        String lockKey = "id_gen_lock:" + bizTag;
        try (DistributedLock lock = new RedisDistributedLock(redisTemplate, lockKey, 5000)) {
            if (!lock.tryLock(3, TimeUnit.SECONDS)) {
                throw new IllegalStateException("獲取分布式鎖超時: " + bizTag);
            }
            
            // 關鍵區段:從 Redis 獲取段號、本地緩存發號
            long segment = redisTemplate.opsForValue().increment("id_segment:" + bizTag, 1000);
            long start = (segment - 1) * 1000;
            return bizTag + "-" + (start + localCounter.incrementAndGet());
        }
    }
}

進階優化:Redisson 框架

手寫鎖教學價值大,生產建議用 Redisson:

  • 自動續期(Watchdog):防止業務執行超過鎖過期時間
  • 可重入、公平鎖、紅綠燈、信號量、ReadWriteLock 完整實現
  • 支援集群、Sentinel、主從、單機多種部署模式
  • Pub/Sub 鎖釋放通知,無輪詢開銷

// Redisson 使用範例
RLock lock = redisson.getLock("my_lock");
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS); // 等待 3s,自動續期 10s
if (acquired) {
    try { /* 關鍵業務 */ } 
    finally { lock.unlock(); }
}

常見坑與避坑指南

坑點後果解決方案
鎖過期時間 < 業務執行時間鎖自動釋放,並發衝突Redisson Watchdog / 預估最大執行時間 × 2
直接 del 釋放鎖誤刪他人鎖Lua 腳本驗證 value 後刪除
未設定獲取超時死等導致線程耗盡必須使用 tryLock(timeout, unit)
鎖 Key 衝突業務互斥錯誤命名規範:lock:biz:resource:id
單點 Redis 故障鎖服務不可用Redis Sentinel/Cluster + Redisson 容錯

替代方案對比

方案一致性效能複雜度適用場景
Redis (SET NX EX)AP(極少數情況下可能失鎖)極高低高併發、允許極低概率失鎖(如緩存重建)
Redisson RedLockCP(多實例多數派)中中強一致性要求(如分布式事務協調)
ZooKeeper / etcdCP(強一致)中高配置管理、領導者選舉、元數據存儲
資料庫 FOR UPDATECP低低低併發、已有資料庫、無額外基建
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved