核心概念:一個合格的分佈式鎖需要什麼?

在比較具體實現之前,我們必須先了解一個健壯的分佈式鎖應具備的特性:

  1. 互斥性:在任意時刻,只有一個客户端能持有鎖。
  2. 安全性:不會發生死鎖。即使一個客户端在持有鎖期間崩潰,沒有主動解鎖,也能保證後續其他客户端能夠加鎖。
  3. 容錯性:只要大部分(超過一半)的鎖服務節點存活,客户端就能正常獲取和釋放鎖。
  4. 避免驚羣效應:當鎖被釋放時,多個等待的客户端中只有一個能成功獲得鎖。
  5. 可重入性:同一個客户端在已經持有鎖的情況下,可以再次成功獲取鎖。

三種實現方式的詳細比較

特性

Redis

ZooKeeper

數據庫(如MySQL)

實現複雜度

中等

較低

(但需處理重試、超時)

性能

最高(內存操作)

中等(ZK需要共識協議)

最低(磁盤IO)

一致性保證

(AP,異步複製)

(CP,基於ZAB協議)

(依賴數據庫事務)

避免死鎖機制

Key過期時間

臨時節點(客户端斷開自動刪除)

超時時間(需額外定時任務清理)

鎖喚醒機制

Pub/Sub 或 客户端自旋

Watch機制(天然事件通知)

主動輪詢(性能差)

可重入性

需客户端邏輯實現

原生支持(同一Session)

需客户端邏輯實現

主要缺點

主從切換可能導致鎖失效

性能不如Redis,有廣播風暴風險

性能最差,數據庫壓力大,易死鎖


他們是如何實現的?

1. Redis 實現

Redis 實現分佈式鎖的核心命令是 SET key value NX PX milliseconds

  • NX:僅當 Key 不存在時才設置。這保證了互斥性。
  • PX:設置 Key 的過期時間(毫秒)。這保證了安全性,避免了死鎖。

基礎實現流程:

  1. 加鎖
SET lock_resource_name my_random_value NX PX 30000
  • my_random_value 必須是全局唯一的值(如UUID),用於標識加鎖的客户端。這至關重要,它用於在釋放鎖時驗證這是自己加的鎖,防止誤刪其他客户端的鎖。
  • 30000 是鎖的自動過期時間,單位毫秒。
  1. 業務操作:執行需要受鎖保護的業務邏輯。
  2. 釋放鎖:使用 Lua 腳本保證原子性。
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
  • 腳本先比較當前鎖的值是否與自己設置的值相等,相等才刪除。GET 和 DEL 操作在 Lua 腳本中是原子的。

高級方案 - Redlock算法:
為了克服 Redis 主從架構下(主節點宕機,鎖信息未同步到從節點導致鎖失效)的問題,Redis 作者提出了 Redlock 算法。它需要多個(通常為5個)獨立的 Redis 主節點(非集羣)。

  1. 客户端獲取當前毫秒級時間戳 T1。
  2. 依次向 N 個 Redis 實例發送加鎖命令(使用相同的 Key 和隨機值)。
  3. 只有當客户端從超過半數(N/2+1) 的節點上成功獲取鎖,且總耗時小於鎖的過期時間,才認為加鎖成功。
  4. 鎖的實際有效時間 = 初始有效時間 - 獲取鎖的總耗時。
  5. 如果加鎖失敗,客户端會向所有 Redis 實例發起釋放鎖的請求。

優缺點:

  • 優點:性能極高,實現相對簡單,社區支持好(如 Redisson 客户端)。
  • 缺點:基礎模式在主從故障切換時不安全;Redlock 算法複雜,性能有損耗,且存在爭議(如 Martin Kleppmann 的批評)。
2. ZooKeeper 實現

ZooKeeper 的數據模型類似於文件系統,它的臨時順序節點是實現分佈式鎖的核心。

實現流程(排他鎖):

  1. 加鎖
  • 客户端在指定的鎖節點(如 /locks/my_lock)下創建一個臨時順序節點,假設為 /locks/my_lock/seq-00000001
  • ZooKeeper 會保證這個節點在客户端會話(Session)結束時(如連接斷開)被自動刪除。這天然地避免了死鎖
  • 客户端獲取 /locks/my_lock 下的所有子節點,並判斷自己創建的子節點是否為序號最小的一個。
  • 如果是,則成功獲取鎖。
  • 如果不是,則對序號排在自己前面的那個節點設置 Watch 監聽。
  1. 業務操作:執行需要受鎖保護的業務邏輯。
  2. 鎖釋放/監聽
  • 當持有鎖的客户端完成操作或會話結束時,臨時節點會被刪除。
  • 監聽該節點的下一個客户端會收到 ZooKeeper 的通知,然後再次檢查自己是否是最小節點,如果是,則成功獲取鎖。

優缺點:

  • 優點:鎖強一致,安全可靠(臨時節點防死鎖),有天然的等待隊列機制(Watch),避免了驚羣效應。
  • 缺點:性能比 Redis 差,因為每次寫操作都需要在集羣內達成共識。添加和刪除節點會觸發大量 Watch 事件,存在廣播風暴風險。
3. 數據庫實現

通常有兩種方式:基於數據庫表的唯一索引樂觀鎖

基於唯一索引(悲觀鎖):

  1. 創建鎖表
CREATE TABLE `distributed_lock` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `lock_key` varchar(64) NOT NULL,
  `lock_value` varchar(255) NOT NULL,
  `expire_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_lock_key` (`lock_key`)
);
  1. 加鎖:向表中插入一條記錄。
INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES ('resource_1', 'my_uuid', NOW() + INTERVAL 30 SECOND);
  • 利用數據庫的唯一約束,只有一個客户端能插入成功,成功即代表獲取鎖。
  • lock_value 的作用同 Redis,用於安全釋放鎖。
  • expire_time 用於防止死鎖,需要一個定時任務來清理過期的鎖。
  1. 業務操作:執行需要受鎖保護的業務邏輯。
  2. 釋放鎖
DELETE FROM distributed_lock WHERE lock_key = 'resource_1' AND lock_value = 'my_uuid';

優缺點:

  • 優點:實現簡單,直接利用現有數據庫,理解成本低。
  • 缺點
  • 性能最差,數據庫 IO 開銷大,容易成為系統瓶頸。
  • 數據庫單點問題(雖然可以用主從,但主從延遲又會帶來鎖一致性問題)。
  • 需要處理超時和清理過期鎖,實現不優雅。
  • 不具備可重入性和自動喚醒功能。

微服務架構中的分佈式鎖需求

在微服務架構中,隨着服務被拆分成多個獨立的進程,傳統的單機鎖機制無法滿足跨服務的同步需求,分佈式鎖成為必需的基礎組件。

微服務中常見的分佈式鎖場景

1. 資源爭用場景

庫存扣減與超賣防止

// 偽代碼示例
public boolean deductStock(Long productId, Integer quantity) {
    String lockKey = "stock_lock:" + productId;
    DistributedLock lock = distributedLockService.tryLock(lockKey, 3000L);
    try {
        if (lock != null) {
            // 查詢庫存
            Integer currentStock = stockService.getStock(productId);
            if (currentStock >= quantity) {
                // 扣減庫存
                stockService.updateStock(productId, currentStock - quantity);
                return true;
            }
        }
        return false;
    } finally {
        if (lock != null) {
            lock.unlock();
        }
    }
}
2. 分佈式定時任務調度

確保集羣中只有一個實例執行任務

@Component
public class ScheduledReportTask {
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2點執行
    public void generateDailyReport() {
        String lockKey = "task:generate_daily_report";
        if (distributedLockService.tryLock(lockKey, 3600L)) { // 鎖1小時
            try {
                // 生成日報邏輯
                reportService.generateDailyReport();
            } finally {
                distributedLockService.unlock(lockKey);
            }
        }
    }
}
3. 防止重複操作

用户重複提交訂單

@Service
public class OrderService {
    public CreateOrderResult createOrder(CreateOrderRequest request) {
        String lockKey = "order_create:" + request.getUserId();
        // 5秒內防止同一用户重複提交
        if (!distributedLockService.tryLock(lockKey, 5000L)) {
            throw new BusinessException("請求過於頻繁,請稍後再試");
        }
        try {
            // 創建訂單邏輯
            return doCreateOrder(request);
        } finally {
            distributedLockService.unlock(lockKey);
        }
    }
}
4. 分佈式環境下的初始化操作

配置加載或緩存預熱

@Service
public class ConfigService {
    private volatile boolean initialized = false;
    @PostConstruct
    public void initConfig() {
        String lockKey = "config_initialization";
        if (distributedLockService.tryLock(lockKey, 60000L)) {
            try {
                // 雙重檢查,防止重複初始化
                if (!initialized) {
                    loadGlobalConfig();
                    warmUpCache();
                    initialized = true;
                }
            } finally {
                distributedLockService.unlock(lockKey);
            }
        }
    }
}

三種方案在微服務中的詳細實現

1. Redis 分佈式鎖在微服務中的最佳實踐

使用 Redisson 客户端(推薦)
org.redissonredisson-spring-boot-starter3.27.0

配置類:

@Configuration
public class RedissonConfig {
    @Value("${redis.host:localhost}")
    private String redisHost;
    @Value("${redis.port:6379}")
    private String redisPort;
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://" + redisHost + ":" + redisPort)
              .setDatabase(0)
              .setConnectionPoolSize(64)
              .setConnectionMinimumIdleSize(24)
              .setIdleConnectionTimeout(10000)
              .setConnectTimeout(10000)
              .setTimeout(3000);
        return Redisson.create(config);
    }
}

服務類:

@Service
public class RedisDistributedLockService {
    @Autowired
    private RedissonClient redissonClient;
    /**
     * 可重入鎖
     */
    public boolean tryReentrantLock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    /**
     * 公平鎖 - 按照請求順序獲得鎖
     */
    public boolean tryFairLock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getFairLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    /**
     * 讀寫鎖 - 讀讀不互斥,讀寫、寫寫互斥
     */
    public boolean tryWriteLock(String lockKey, long waitTime, long leaseTime) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);
        RLock writeLock = rwLock.writeLock();
        try {
            return writeLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

使用示例:

@Service
public class ProductService {
    @Autowired
    private RedisDistributedLockService lockService;
    public void updateProductInventory(Long productId, Integer delta) {
        String lockKey = "product_inventory:" + productId;
        // 嘗試獲取鎖,最多等待2秒,鎖持有時間10秒
        if (lockService.tryReentrantLock(lockKey, 2000, 10000)) {
            try {
                // 業務邏輯
                productRepository.updateInventory(productId, delta);
                // 模擬耗時操作
                Thread.sleep(500);
            } catch (Exception e) {
                log.error("更新庫存失敗", e);
            } finally {
                lockService.unlock(lockKey);
            }
        } else {
            throw new BusinessException("系統繁忙,請稍後重試");
        }
    }
}

2. ZooKeeper 分佈式鎖在微服務中的實現

使用 Curator 框架(推薦)
org.apache.curatorcurator-recipes5.5.0

配置類:

@Configuration
public class CuratorConfig {
    @Value("${zookeeper.connect-string:localhost:2181}")
    private String connectString;
    @Bean(initMethod = "start", destroyMethod = "close")
    public CuratorFramework curatorFramework() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        return CuratorFrameworkFactory.builder()
                .connectString(connectString)
                .sessionTimeoutMs(60000)
                .connectionTimeoutMs(15000)
                .retryPolicy(retryPolicy)
                .namespace("microservice-locks")
                .build();
    }
    @Bean
    public InterProcessMutex interProcessMutex(CuratorFramework curatorFramework) {
        // 這是一個示例bean,實際使用時根據不同的鎖路徑創建
        return new InterProcessMutex(curatorFramework, "/locks");
    }
}

服務類:

@Service
public class ZkDistributedLockService {
    @Autowired
    private CuratorFramework curatorFramework;
    private final Map lockMap = new ConcurrentHashMap<>();
    /**
     * 獲取互斥鎖
     */
    public boolean tryAcquireMutex(String lockPath, long timeout, TimeUnit unit) {
        try {
            InterProcessMutex lock = lockMap.computeIfAbsent(lockPath,
                path -> new InterProcessMutex(curatorFramework, path));
            return lock.acquire(timeout, unit);
        } catch (Exception e) {
            log.error("獲取ZooKeeper鎖失敗", e);
            return false;
        }
    }
    /**
     * 釋放鎖
     */
    public void releaseMutex(String lockPath) {
        try {
            InterProcessMutex lock = lockMap.get(lockPath);
            if (lock != null && lock.isAcquiredInThisProcess()) {
                lock.release();
            }
        } catch (Exception e) {
            log.error("釋放ZooKeeper鎖失敗", e);
        }
    }
    /**
     * 獲取讀寫鎖
     */
    public boolean tryAcquireWriteLock(String lockPath, long timeout, TimeUnit unit) {
        try {
            InterProcessReadWriteLock rwLock = new InterProcessReadWriteLock(curatorFramework, lockPath);
            return rwLock.writeLock().acquire(timeout, unit);
        } catch (Exception e) {
            log.error("獲取ZooKeeper寫鎖失敗", e);
            return false;
        }
    }
},>

使用示例 - 配置中心數據同步:

@Service
public class ConfigSyncService {
    @Autowired
    private ZkDistributedLockService lockService;
    public void syncGlobalConfig() {
        String lockPath = "/config/sync/global";
        if (lockService.tryAcquireMutex(lockPath, 5, TimeUnit.SECONDS)) {
            try {
                // 只有獲得鎖的服務實例執行配置同步
                log.info("開始同步全局配置...");
                configService.syncFromCentral();
                log.info("全局配置同步完成");
            } finally {
                lockService.releaseMutex(lockPath);
            }
        } else {
            log.info("其他服務實例正在執行配置同步,跳過本次執行");
        }
    }
}

3. 數據庫分佈式鎖在微服務中的實現

鎖表結構:

CREATE TABLE `distributed_lock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `lock_key` varchar(255) NOT NULL COMMENT '鎖定的資源key',
  `lock_value` varchar(255) NOT NULL COMMENT '鎖的值(UUID)',
  `expire_time` datetime NOT NULL COMMENT '鎖過期時間',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_lock_key` (`lock_key`),
  KEY `idx_expire_time` (`expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分佈式鎖表';

服務類:

@Service
@Transactional
public class DatabaseDistributedLockService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    private static final String INSERT_SQL =
        "INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES (?, ?, ?)";
    private static final String DELETE_SQL =
        "DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?";
    private static final String CLEAN_EXPIRED_SQL =
        "DELETE FROM distributed_lock WHERE expire_time < NOW()";
    /**
     * 嘗試獲取鎖
     */
    public boolean tryLock(String lockKey, long expireMillis) {
        String lockValue = UUID.randomUUID().toString();
        LocalDateTime expireTime = LocalDateTime.now().plus(expireMillis, ChronoUnit.MILLIS);
        try {
            // 清理過期鎖
            jdbcTemplate.update(CLEAN_EXPIRED_SQL);
            // 嘗試插入獲取鎖
            int affected = jdbcTemplate.update(INSERT_SQL, lockKey, lockValue, expireTime);
            return affected > 0;
        } catch (DuplicateKeyException e) {
            // 鎖已被其他線程持有
            return false;
        }
    }
    /**
     * 釋放鎖
     */
    public boolean unlock(String lockKey, String lockValue) {
        int affected = jdbcTemplate.update(DELETE_SQL, lockKey, lockValue);
        return affected > 0;
    }
    /**
     * 帶重試的鎖獲取
     */
    public boolean tryLockWithRetry(String lockKey, long expireMillis, int maxRetries, long retryInterval) {
        for (int i = 0; i < maxRetries; i++) {
            if (tryLock(lockKey, expireMillis)) {
                return true;
            }
            try {
                Thread.sleep(retryInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        return false;
    }
}

微服務場景下的選型矩陣

場景分類

具體場景

推薦方案

理由

高併發業務

秒殺、搶購、庫存扣減

Redis

性能要求極高,允許極低概率的鎖失效

金融交易

賬户餘額變更、交易處理

ZooKeeper 或 Redis + 事務補償

強一致性要求,不能出現重複操作

定時任務

報表生成、數據歸檔

Redis 或 ZooKeeper

Redis性能好,ZooKeeper更可靠

配置管理

配置熱更新、服務發現

ZooKeeper

天然的一致性協調能力

工作流控制

審批流程、狀態機

Redis

性能好,支持複雜的鎖類型

簡單業務

低頻操作、內部管理

數據庫

無需引入新組件,維護簡單

微服務架構中的最佳實踐

1. 鎖的粒度控制

// 好的實踐 - 細粒度鎖
String lockKey = "order:" + orderId;
// 不好的實踐 - 粗粒度鎖
String lockKey = "order_lock"; // 所有訂單操作都串行化

2. 超時時間設置

// 根據業務操作預估合理超時時間
long timeout = calculateBusinessTimeout(); // 動態計算
distributedLockService.tryLock(lockKey, timeout);

3. 故障恢復機制

@Service
public class ResilientLockService {
    @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public void doWithLock(String lockKey, Runnable businessLogic) {
        // 獲取鎖並執行業務邏輯
        if (tryLock(lockKey)) {
            try {
                businessLogic.run();
            } finally {
                unlock(lockKey);
            }
        }
    }
}

4. 監控與告警

@Component
public class LockMonitor {
    @EventListener
    public void handleLockAcquisitionFailure(LockAcquisitionFailureEvent event) {
        // 記錄鎖獲取失敗指標
        metrics.increment("lock.acquisition.failure");
        // 觸發告警
        if (event.getFailureCount() > threshold) {
            alertService.sendAlert("分佈式鎖獲取異常頻繁");
        }
    }
}

總結

在微服務架構中選擇分佈式鎖方案時:

  • Redis:適用於絕大多數業務場景,性能優秀,生態成熟
  • ZooKeeper:適用於對一致性要求極高的核心業務場景
  • 數據庫:適用於簡單場景或作為過渡方案

推薦策略:在微服務架構中,可以基於 Redis 構建主要的分佈式鎖能力,對於特別關鍵的業務(如資金交易)使用 ZooKeeper 作為補充,形成多層次的鎖策略。同時,無論選擇哪種方案,都要做好監控、熔斷和降級準備,確保鎖服務不會成為系統的單點故障。