分布式鎖詳解:從數據庫實現到中間件選型
引言
在分布式系統中,我們經常需要對共享資源進行互斥訪問。比如:
- 防止商品超賣
- 避免重復下單
- 確保任務只被處理一次
- 保護共享資源不被并發修改
這就需要一個分布式鎖機制。與單機環境下的線程鎖不同,分布式鎖需要在多個服務實例間生效,這帶來了新的挑戰。
圖片
分布式鎖的核心要求
一個可靠的分布式鎖必須滿足以下要求:
- 互斥性
在任意時刻,只能有一個客戶端持有鎖
不能出現多個客戶端同時持有鎖的情況
- 可重入性
同一個客戶端可以多次獲取同一把鎖
需要維護鎖的重入計數
- 防死鎖
客戶端崩潰時,鎖必須能自動釋放
鎖必須有過期機制
- 高可用
鎖服務不能成為系統瓶頸
鎖服務必須保證高可用
基于數據庫的實現
圖片
1. 悲觀鎖實現
最簡單的方式是利用數據庫的行鎖:
-- 創建鎖表
CREATE TABLE distributed_lock (
lock_key VARCHAR(50) PRIMARY KEY,
lock_value VARCHAR(50),
version INT,
expire_time TIMESTAMP
);
-- 獲取鎖
SELECT * FROM distributed_lock
WHERE lock_key = 'order_lock'
FOR UPDATE;
Java 實現示例:
@Service
public class DatabaseDistributedLock {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public boolean acquireLock(String lockKey, String lockValue, long expireSeconds) {
try {
// 使用 FOR UPDATE 加鎖查詢
String sql = "SELECT * FROM distributed_lock " +
"WHERE lock_key = ? FOR UPDATE";
List<Map<String, Object>> result = jdbcTemplate.queryForList(
sql, lockKey
);
if (result.isEmpty()) {
// 鎖不存在,創建鎖
jdbcTemplate.update(
"INSERT INTO distributed_lock " +
"(lock_key, lock_value, version, expire_time) " +
"VALUES (?, ?, 1, ?)",
lockKey,
lockValue,
LocalDateTime.now().plusSeconds(expireSeconds)
);
return true;
}
// 檢查鎖是否過期
Map<String, Object> lock = result.get(0);
LocalDateTime expireTime = ((Timestamp) lock.get("expire_time"))
.toLocalDateTime();
if (expireTime.isBefore(LocalDateTime.now())) {
// 鎖已過期,更新鎖
jdbcTemplate.update(
"UPDATE distributed_lock " +
"SET lock_value = ?, version = version + 1, expire_time = ? " +
"WHERE lock_key = ?",
lockValue,
LocalDateTime.now().plusSeconds(expireSeconds),
lockKey
);
return true;
}
return false;
} catch (Exception e) {
return false;
}
}
}
2. 樂觀鎖實現
使用版本號實現樂觀鎖:
@Service
public class OptimisticLock {
@Autowired
private JdbcTemplate jdbcTemplate;
public boolean acquireLock(String lockKey, String lockValue, int version) {
int updated = jdbcTemplate.update(
"UPDATE distributed_lock " +
"SET lock_value = ?, version = version + 1 " +
"WHERE lock_key = ? AND version = ?",
lockValue,
lockKey,
version
);
return updated > 0;
}
}
數據庫實現的優缺點:
- 優點:
實現簡單
容易理解
不需要額外組件
- 缺點:
性能較差
數據庫壓力大
無法優雅處理鎖超時
基于 Redis 的實現
1. 單節點實現
使用 Redis 的 SETNX 命令:
@Service
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean acquireLock(String lockKey, String lockValue, long expireSeconds) {
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
}
public boolean releaseLock(String lockKey, String lockValue) {
// 使用 Lua 腳本確保原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
return redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(lockKey),
lockValue
);
}
}
2. RedLock 算法
在 Redis 集群環境下,使用 RedLock 算法:
public class RedLock {
private final List<StringRedisTemplate> redisList;
private final int quorum; // 大多數節點數
public boolean acquireLock(String lockKey, String lockValue, long expireMillis) {
int acquiredLocks = 0;
long startTime = System.currentTimeMillis();
// 嘗試在每個節點上獲取鎖
for (StringRedisTemplate redis : redisList) {
if (tryAcquireLock(redis, lockKey, lockValue, expireMillis)) {
acquiredLocks++;
}
}
// 計算獲取鎖消耗的時間
long elapsedTime = System.currentTimeMillis() - startTime;
long remainingTime = expireMillis - elapsedTime;
// 判斷是否獲取到足夠的鎖
if (acquiredLocks >= quorum && remainingTime > 0) {
return true;
} else {
// 釋放所有獲取的鎖
releaseLocks(lockKey, lockValue);
return false;
}
}
private boolean tryAcquireLock(
StringRedisTemplate redis,
String lockKey,
String lockValue,
long expireMillis
) {
try {
return redis.opsForValue()
.setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
} catch (Exception e) {
return false;
}
}
}
Redis 實現的優缺點:
- 優點:
性能高
實現相對簡單
支持自動過期
- 缺點:
需要額外維護 Redis 集群
RedLock 算法實現復雜
時鐘依賴問題
基于 ZooKeeper 的實現
圖片
利用 ZooKeeper 的臨時節點機制:
public class ZookeeperDistributedLock {
private final CuratorFramework client;
private final String lockPath;
public boolean acquireLock(String lockKey) throws Exception {
// 創建臨時節點
String path = lockPath + "/" + lockKey;
try {
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path);
return true;
} catch (NodeExistsException e) {
return false;
}
}
public void releaseLock(String lockKey) throws Exception {
String path = lockPath + "/" + lockKey;
client.delete().forPath(path);
}
// 實現可重入鎖
public class ReentrantZookeeperLock {
private final ThreadLocal<Integer> lockCount = new ThreadLocal<>();
public boolean acquire() throws Exception {
Integer count = lockCount.get();
if (count != null && count > 0) {
// 入
lockCount.set(count + 1);
return true;
}
if (acquireLock("lock")) {
lockCount.set(1);
return true;
}
return false;
}
public void release() throws Exception {
Integer count = lockCount.get();
if (count == null) {
return;
}
count--;
if (count > 0) {
lockCount.set(count);
} else {
lockCount.remove();
releaseLock("lock");
}
}
}
}
ZooKeeper 實現的優缺點:
- 優點:
可靠性高
自動釋放鎖
支持監聽機制
- 缺點:
性能一般
實現復雜
需要維護 ZooKeeper 集群
業務場景分析
1. 秒殺場景
場景特點:
- 并發量極高
- 時間窗口集中
- 對性能要求極高
- 數據一致性要求高
推薦方案: Redis + Lua腳本
@Service
public class SeckillLockService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean trySecKill(String productId, String userId) {
// Lua腳本保證原子性
String script =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('set', KEYS[1], ARGV[1]) " +
" redis.call('decrby', KEYS[2], 1) " +
" return 1 " +
"end " +
"return 0";
List<String> keys = Arrays.asList(
"seckill:lock:" + productId + ":" + userId,
"seckill:stock:" + productId
);
return redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
keys,
"1"
);
}
}
原因分析:
- Redis 的高性能滿足并發要求
- Lua 腳本保證原子性
- 內存操作速度快
- 集群方案保證可用性
2. 定時任務場景
場景特點:
- 多實例部署
- 任務不能重復執行
- 故障轉移需求
- 實時性要求不高
推薦方案: ZooKeeper
public class ScheduledTaskLock {
private final CuratorFramework client;
public void executeTask() {
String taskPath = "/scheduled-tasks/daily-report";
try {
// 創建臨時節點
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(taskPath);
try {
// 執行任務
generateDailyReport();
} finally {
// 刪除節點
client.delete().forPath(taskPath);
}
} catch (NodeExistsException e) {
// 其他實例正在執行
log.info("Task is running on other instance");
}
}
}
原因分析:
- ZooKeeper 的臨時節點特性保證故障時自動釋放鎖
- 強一致性保證任務不會重復執行
- Watch 機制便于監控任務執行狀態
3. 訂單支付場景
場景特點:
- 并發量適中
- 數據一致性要求高
- 需要事務支持
- 有業務回滾需求
推薦方案: 數據庫行鎖 + 事務
@Service
public class PaymentLockService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public boolean processPayment(String orderId, BigDecimal amount) {
// 使用 FOR UPDATE 鎖定訂單記錄
String sql = "SELECT * FROM orders WHERE order_id = ? FOR UPDATE";
Map<String, Object> order = jdbcTemplate.queryForMap(sql, orderId);
// 檢查訂單狀態
if (!"PENDING".equals(order.get("status"))) {
return false;
}
// 執行支付邏輯
jdbcTemplate.update(
"UPDATE orders SET status = 'PAID' WHERE order_id = ?",
orderId
);
// 記錄支付流水
jdbcTemplate.update(
"INSERT INTO payment_log (order_id, amount) VALUES (?, ?)",
orderId, amount
);
return true;
}
}
原因分析:
- 數據庫事務保證數據一致性
- 行鎖防止并發支付
- 便于與其他業務集成
- 支持事務回滾
4. 庫存扣減場景
場景特點:
- 并發量較高
- 需要預占庫存
- 需要處理超時釋放
- 對性能要求較高
推薦方案: Redis + 延時隊列
@Service
public class InventoryLockService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean lockInventory(String productId, int quantity, String orderId) {
// 加鎖并預占庫存
String script =
"local stock = redis.call('get', KEYS[1]) " +
"if stock and tonumber(stock) >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]) " +
" redis.call('setex', KEYS[2], 1800, ARGV[1]) " +
" return 1 " +
"end " +
"return 0";
List<String> keys = Arrays.asList(
"inventory:" + productId,
"inventory:lock:" + orderId
);
boolean locked = redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
keys,
String.valueOf(quantity)
);
if (locked) {
// 添加延時釋放任務
redisTemplate.opsForZSet().add(
"inventory:timeout",
orderId,
System.currentTimeMillis() + 1800000
);
}
return locked;
}
}
原因分析:
- Redis 的高性能滿足并發要求
- 延時隊列處理超時釋放
- 原子操作保證數據一致性
- 便于擴展和監控
實現方案對比
特性 | 數據庫 | Redis | ZooKeeper |
性能 | 低 | 高 | 中 |
可靠性 | 高 | 中 | 高 |
實現復雜度 | 低 | 中 | 高 |
維護成本 | 低 | 中 | 高 |
自動釋放 | 需要額外實現 | 支持 | 支持 |
可重入性 | 需要額外實現 | 需要額外實現 | 需要額外實現 |
最佳實踐
- 選擇建議
簡單場景:使用數據庫實現
高性能要求:使用 Redis 實現
高可靠要求:使用 ZooKeeper 實現
- 實現建議
設置合理的超時時間
實現可重入機制
添加監控和告警
做好日志記錄
- 使用建議
縮小鎖的粒度
減少鎖的持有時間
避免死鎖
做好異常處理
結論
分布式鎖是分布式系統中的一個基礎組件,選擇合適的實現方案需要考慮:
- 性能要求
- 可靠性要求
- 開發維護成本
- 團隊技術棧
沒有最好的方案,只有最合適的方案。在實際應用中,要根據具體場景選擇合適的實現方式。
正文內容從這里開始(可直接省略,亦可配圖說明)。