分布式鎖詳解:從數(shù)據(jù)庫(kù)實(shí)現(xiàn)到中間件選型
引言
在分布式系統(tǒng)中,我們經(jīng)常需要對(duì)共享資源進(jìn)行互斥訪問(wèn)。比如:
- 防止商品超賣(mài)
- 避免重復(fù)下單
- 確保任務(wù)只被處理一次
- 保護(hù)共享資源不被并發(fā)修改
這就需要一個(gè)分布式鎖機(jī)制。與單機(jī)環(huán)境下的線程鎖不同,分布式鎖需要在多個(gè)服務(wù)實(shí)例間生效,這帶來(lái)了新的挑戰(zhàn)。
圖片
分布式鎖的核心要求
一個(gè)可靠的分布式鎖必須滿足以下要求:
- 互斥性
在任意時(shí)刻,只能有一個(gè)客戶端持有鎖
不能出現(xiàn)多個(gè)客戶端同時(shí)持有鎖的情況
- 可重入性
同一個(gè)客戶端可以多次獲取同一把鎖
需要維護(hù)鎖的重入計(jì)數(shù)
- 防死鎖
客戶端崩潰時(shí),鎖必須能自動(dòng)釋放
鎖必須有過(guò)期機(jī)制
- 高可用
鎖服務(wù)不能成為系統(tǒng)瓶頸
鎖服務(wù)必須保證高可用
基于數(shù)據(jù)庫(kù)的實(shí)現(xiàn)
圖片
1. 悲觀鎖實(shí)現(xiàn)
最簡(jiǎn)單的方式是利用數(shù)據(jù)庫(kù)的行鎖:
-- 創(chuàng)建鎖表
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 實(shí)現(xiàn)示例:
@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()) {
// 鎖不存在,創(chuàng)建鎖
jdbcTemplate.update(
"INSERT INTO distributed_lock " +
"(lock_key, lock_value, version, expire_time) " +
"VALUES (?, ?, 1, ?)",
lockKey,
lockValue,
LocalDateTime.now().plusSeconds(expireSeconds)
);
return true;
}
// 檢查鎖是否過(guò)期
Map<String, Object> lock = result.get(0);
LocalDateTime expireTime = ((Timestamp) lock.get("expire_time"))
.toLocalDateTime();
if (expireTime.isBefore(LocalDateTime.now())) {
// 鎖已過(guò)期,更新鎖
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. 樂(lè)觀鎖實(shí)現(xiàn)
使用版本號(hào)實(shí)現(xiàn)樂(lè)觀鎖:
@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;
}
}
數(shù)據(jù)庫(kù)實(shí)現(xiàn)的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):
實(shí)現(xiàn)簡(jiǎn)單
容易理解
不需要額外組件
- 缺點(diǎn):
性能較差
數(shù)據(jù)庫(kù)壓力大
無(wú)法優(yōu)雅處理鎖超時(shí)
基于 Redis 的實(shí)現(xiàn)
1. 單節(jié)點(diǎn)實(shí)現(xiàn)
使用 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 集群環(huán)境下,使用 RedLock 算法:
public class RedLock {
private final List<StringRedisTemplate> redisList;
private final int quorum; // 大多數(shù)節(jié)點(diǎn)數(shù)
public boolean acquireLock(String lockKey, String lockValue, long expireMillis) {
int acquiredLocks = 0;
long startTime = System.currentTimeMillis();
// 嘗試在每個(gè)節(jié)點(diǎn)上獲取鎖
for (StringRedisTemplate redis : redisList) {
if (tryAcquireLock(redis, lockKey, lockValue, expireMillis)) {
acquiredLocks++;
}
}
// 計(jì)算獲取鎖消耗的時(shí)間
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 實(shí)現(xiàn)的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):
性能高
實(shí)現(xiàn)相對(duì)簡(jiǎn)單
支持自動(dòng)過(guò)期
- 缺點(diǎn):
需要額外維護(hù) Redis 集群
RedLock 算法實(shí)現(xiàn)復(fù)雜
時(shí)鐘依賴問(wèn)題
基于 ZooKeeper 的實(shí)現(xiàn)
圖片
利用 ZooKeeper 的臨時(shí)節(jié)點(diǎn)機(jī)制:
public class ZookeeperDistributedLock {
private final CuratorFramework client;
private final String lockPath;
public boolean acquireLock(String lockKey) throws Exception {
// 創(chuàng)建臨時(shí)節(jié)點(diǎn)
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);
}
// 實(shí)現(xiàn)可重入鎖
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 實(shí)現(xiàn)的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):
可靠性高
自動(dòng)釋放鎖
支持監(jiān)聽(tīng)機(jī)制
- 缺點(diǎn):
性能一般
實(shí)現(xiàn)復(fù)雜
需要維護(hù) ZooKeeper 集群
業(yè)務(wù)場(chǎng)景分析
1. 秒殺場(chǎng)景
場(chǎng)景特點(diǎn):
- 并發(fā)量極高
- 時(shí)間窗口集中
- 對(duì)性能要求極高
- 數(shù)據(jù)一致性要求高
推薦方案: 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 的高性能滿足并發(fā)要求
- Lua 腳本保證原子性
- 內(nèi)存操作速度快
- 集群方案保證可用性
2. 定時(shí)任務(wù)場(chǎng)景
場(chǎng)景特點(diǎn):
- 多實(shí)例部署
- 任務(wù)不能重復(fù)執(zhí)行
- 故障轉(zhuǎn)移需求
- 實(shí)時(shí)性要求不高
推薦方案: ZooKeeper
public class ScheduledTaskLock {
private final CuratorFramework client;
public void executeTask() {
String taskPath = "/scheduled-tasks/daily-report";
try {
// 創(chuàng)建臨時(shí)節(jié)點(diǎn)
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(taskPath);
try {
// 執(zhí)行任務(wù)
generateDailyReport();
} finally {
// 刪除節(jié)點(diǎn)
client.delete().forPath(taskPath);
}
} catch (NodeExistsException e) {
// 其他實(shí)例正在執(zhí)行
log.info("Task is running on other instance");
}
}
}
原因分析:
- ZooKeeper 的臨時(shí)節(jié)點(diǎn)特性保證故障時(shí)自動(dòng)釋放鎖
- 強(qiáng)一致性保證任務(wù)不會(huì)重復(fù)執(zhí)行
- Watch 機(jī)制便于監(jiān)控任務(wù)執(zhí)行狀態(tài)
3. 訂單支付場(chǎng)景
場(chǎng)景特點(diǎn):
- 并發(fā)量適中
- 數(shù)據(jù)一致性要求高
- 需要事務(wù)支持
- 有業(yè)務(wù)回滾需求
推薦方案: 數(shù)據(jù)庫(kù)行鎖 + 事務(wù)
@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);
// 檢查訂單狀態(tài)
if (!"PENDING".equals(order.get("status"))) {
return false;
}
// 執(zhí)行支付邏輯
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;
}
}
原因分析:
- 數(shù)據(jù)庫(kù)事務(wù)保證數(shù)據(jù)一致性
- 行鎖防止并發(fā)支付
- 便于與其他業(yè)務(wù)集成
- 支持事務(wù)回滾
4. 庫(kù)存扣減場(chǎng)景
場(chǎng)景特點(diǎn):
- 并發(fā)量較高
- 需要預(yù)占庫(kù)存
- 需要處理超時(shí)釋放
- 對(duì)性能要求較高
推薦方案: Redis + 延時(shí)隊(duì)列
@Service
public class InventoryLockService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean lockInventory(String productId, int quantity, String orderId) {
// 加鎖并預(yù)占庫(kù)存
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) {
// 添加延時(shí)釋放任務(wù)
redisTemplate.opsForZSet().add(
"inventory:timeout",
orderId,
System.currentTimeMillis() + 1800000
);
}
return locked;
}
}
原因分析:
- Redis 的高性能滿足并發(fā)要求
- 延時(shí)隊(duì)列處理超時(shí)釋放
- 原子操作保證數(shù)據(jù)一致性
- 便于擴(kuò)展和監(jiān)控
實(shí)現(xiàn)方案對(duì)比
特性 | 數(shù)據(jù)庫(kù) | Redis | ZooKeeper |
性能 | 低 | 高 | 中 |
可靠性 | 高 | 中 | 高 |
實(shí)現(xiàn)復(fù)雜度 | 低 | 中 | 高 |
維護(hù)成本 | 低 | 中 | 高 |
自動(dòng)釋放 | 需要額外實(shí)現(xiàn) | 支持 | 支持 |
可重入性 | 需要額外實(shí)現(xiàn) | 需要額外實(shí)現(xiàn) | 需要額外實(shí)現(xiàn) |
最佳實(shí)踐
- 選擇建議
簡(jiǎn)單場(chǎng)景:使用數(shù)據(jù)庫(kù)實(shí)現(xiàn)
高性能要求:使用 Redis 實(shí)現(xiàn)
高可靠要求:使用 ZooKeeper 實(shí)現(xiàn)
- 實(shí)現(xiàn)建議
設(shè)置合理的超時(shí)時(shí)間
實(shí)現(xiàn)可重入機(jī)制
添加監(jiān)控和告警
做好日志記錄
- 使用建議
縮小鎖的粒度
減少鎖的持有時(shí)間
避免死鎖
做好異常處理
結(jié)論
分布式鎖是分布式系統(tǒng)中的一個(gè)基礎(chǔ)組件,選擇合適的實(shí)現(xiàn)方案需要考慮:
- 性能要求
- 可靠性要求
- 開(kāi)發(fā)維護(hù)成本
- 團(tuán)隊(duì)技術(shù)棧
沒(méi)有最好的方案,只有最合適的方案。在實(shí)際應(yīng)用中,要根據(jù)具體場(chǎng)景選擇合適的實(shí)現(xiàn)方式。
正文內(nèi)容從這里開(kāi)始(可直接省略,亦可配圖說(shuō)明)。