核心概念:一個合格的分佈式鎖需要什麼?
在比較具體實現之前,我們必須先了解一個健壯的分佈式鎖應具備的特性:
- 互斥性:在任意時刻,只有一個客户端能持有鎖。
- 安全性:不會發生死鎖。即使一個客户端在持有鎖期間崩潰,沒有主動解鎖,也能保證後續其他客户端能夠加鎖。
- 容錯性:只要大部分(超過一半)的鎖服務節點存活,客户端就能正常獲取和釋放鎖。
- 避免驚羣效應:當鎖被釋放時,多個等待的客户端中只有一個能成功獲得鎖。
- 可重入性:同一個客户端在已經持有鎖的情況下,可以再次成功獲取鎖。
三種實現方式的詳細比較
|
特性
|
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 的過期時間(毫秒)。這保證了安全性,避免了死鎖。
基礎實現流程:
- 加鎖:
SET lock_resource_name my_random_value NX PX 30000
my_random_value必須是全局唯一的值(如UUID),用於標識加鎖的客户端。這至關重要,它用於在釋放鎖時驗證這是自己加的鎖,防止誤刪其他客户端的鎖。30000是鎖的自動過期時間,單位毫秒。
- 業務操作:執行需要受鎖保護的業務邏輯。
- 釋放鎖:使用 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 主節點(非集羣)。
- 客户端獲取當前毫秒級時間戳 T1。
- 依次向 N 個 Redis 實例發送加鎖命令(使用相同的 Key 和隨機值)。
- 只有當客户端從超過半數(N/2+1) 的節點上成功獲取鎖,且總耗時小於鎖的過期時間,才認為加鎖成功。
- 鎖的實際有效時間 = 初始有效時間 - 獲取鎖的總耗時。
- 如果加鎖失敗,客户端會向所有 Redis 實例發起釋放鎖的請求。
優缺點:
- 優點:性能極高,實現相對簡單,社區支持好(如 Redisson 客户端)。
- 缺點:基礎模式在主從故障切換時不安全;Redlock 算法複雜,性能有損耗,且存在爭議(如 Martin Kleppmann 的批評)。
2. ZooKeeper 實現
ZooKeeper 的數據模型類似於文件系統,它的臨時順序節點是實現分佈式鎖的核心。
實現流程(排他鎖):
- 加鎖:
- 客户端在指定的鎖節點(如
/locks/my_lock)下創建一個臨時順序節點,假設為/locks/my_lock/seq-00000001。 - ZooKeeper 會保證這個節點在客户端會話(Session)結束時(如連接斷開)被自動刪除。這天然地避免了死鎖。
- 客户端獲取
/locks/my_lock下的所有子節點,並判斷自己創建的子節點是否為序號最小的一個。 - 如果是,則成功獲取鎖。
- 如果不是,則對序號排在自己前面的那個節點設置 Watch 監聽。
- 業務操作:執行需要受鎖保護的業務邏輯。
- 鎖釋放/監聽:
- 當持有鎖的客户端完成操作或會話結束時,臨時節點會被刪除。
- 監聽該節點的下一個客户端會收到 ZooKeeper 的通知,然後再次檢查自己是否是最小節點,如果是,則成功獲取鎖。
優缺點:
- 優點:鎖強一致,安全可靠(臨時節點防死鎖),有天然的等待隊列機制(Watch),避免了驚羣效應。
- 缺點:性能比 Redis 差,因為每次寫操作都需要在集羣內達成共識。添加和刪除節點會觸發大量 Watch 事件,存在廣播風暴風險。
3. 數據庫實現
通常有兩種方式:基於數據庫表的唯一索引或樂觀鎖。
基於唯一索引(悲觀鎖):
- 創建鎖表:
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`)
);
- 加鎖:向表中插入一條記錄。
INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES ('resource_1', 'my_uuid', NOW() + INTERVAL 30 SECOND);
- 利用數據庫的唯一約束,只有一個客户端能插入成功,成功即代表獲取鎖。
lock_value的作用同 Redis,用於安全釋放鎖。expire_time用於防止死鎖,需要一個定時任務來清理過期的鎖。
- 業務操作:執行需要受鎖保護的業務邏輯。
- 釋放鎖:
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 作為補充,形成多層次的鎖策略。同時,無論選擇哪種方案,都要做好監控、熔斷和降級準備,確保鎖服務不會成為系統的單點故障。