分布式锁是在分布式系统中实现跨节点资源互斥访问的一种机制,用于解决多进程、多机器环境下的并发控制问题。与单机锁(如synchronized)不同,分布式锁需要依赖外部共享存储(如Redis、ZooKeeper)来协调不同节点间的锁状态。
核心原理
全局唯一性
通过共享存储(如Redis的SETNX命令)确保同一时间只有一个客户端能获取锁。原子性操作
锁的获取和释放必须是原子操作,避免竞态条件。例如:# Redis的SET命令同时设置值和过期时间(原子操作) SET lock_key "value" NX PX 30000 # 30秒过期锁超时机制
防止锁持有者崩溃后锁无法释放,通过设置过期时间自动失效。
分布式锁的实现方式
1. 基于Redis的实现
单点Redis:
import redis import time class RedisLock: def __init__(self, redis_client, lock_key, expire_time=30): self.redis = redis_client self.lock_key = lock_key self.expire_time = expire_time self.uuid = str(uuid.uuid4()) # 唯一标识锁持有者 def acquire(self): return self.redis.set(self.lock_key, self.uuid, nx=True, ex=self.expire_time) def release(self): script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ return self.redis.eval(script, 1, self.lock_key, self.uuid)RedLock算法(多节点Redis):
向多个独立的Redis节点同时获取锁,超过半数成功则认为获取成功,提高可靠性。
2. 基于ZooKeeper的实现
创建临时顺序节点,最小节点持有者获得锁,其他节点监听前一个节点的删除事件。
public class ZkLock implements AutoCloseable { private final CuratorFramework client; private final String lockPath; private String currentPath; private String previousPath; public ZkLock(CuratorFramework client, String lockPath) { this.client = client; this.lockPath = lockPath; } public void acquire() throws Exception { if (currentPath == null) { currentPath = client.create() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(lockPath + "/lock-"); } List<String> children = client.getChildren().forPath(lockPath); Collections.sort(children); if (currentPath.endsWith(children.get(0))) { return; // 当前节点是最小节点,获取锁成功 } // 监听前一个节点 previousPath = lockPath + "/" + children.get(children.indexOf(currentPath.substring(lockPath.length() + 1)) - 1); CountDownLatch latch = new CountDownLatch(1); NodeCache cache = new NodeCache(client, previousPath); cache.getListenable().addListener(() -> { if (cache.getCurrentData() == null) { latch.countDown(); } }); cache.start(); // 检查前一个节点是否已删除 if (client.checkExists().forPath(previousPath) == null) { return; } latch.await(); // 等待前一个节点释放锁 } @Override public void close() throws Exception { client.delete().forPath(currentPath); } }
3. 基于数据库的实现
通过唯一索引或
INSERT ... ON DUPLICATE KEY UPDATE实现:-- 创建锁表 CREATE TABLE distributed_lock ( lock_key VARCHAR(64) PRIMARY KEY, expire_time DATETIME NOT NULL ); -- 获取锁 INSERT INTO distributed_lock (lock_key, expire_time) VALUES ('resource_key', NOW() + INTERVAL 30 SECOND) ON DUPLICATE KEY UPDATE expire_time = NOW() + INTERVAL 30 SECOND;
分布式锁的典型应用场景
分布式任务调度
防止多个节点同时执行同一任务。缓存失效重建
避免缓存击穿时大量请求同时重建缓存。库存扣减
保证分布式系统中库存操作的原子性。
分布式锁的设计要点
可重入性
同一客户端可多次获取同一把锁,需记录获取次数。锁的续租
通过定时任务延长锁的过期时间,防止任务执行时间过长导致锁提前释放。异常处理
- 锁持有者崩溃时,通过过期时间自动释放锁。
- 释放锁时验证锁的所有者,防止误释放。
与单机锁的对比
| 特性 | 单机锁 | 分布式锁 |
|---|---|---|
| 作用范围 | 同一进程内 | 跨进程、跨机器 |
| 实现方式 | JVM内置(如synchronized) | 依赖外部存储(Redis、ZooKeeper) |
| 可靠性 | 高 | 取决于外部存储的可靠性 |
| 性能 | 高(无网络开销) | 低(网络调用开销) |
| 复杂度 | 低 | 高(需处理网络分区等) |
注意事项
网络分区问题
当发生网络分区时,可能导致多个节点同时认为自己持有锁(如Redis的脑裂问题)。锁的粒度
避免使用全局锁,尽量细化锁的粒度以提高并发度。性能权衡
分布式锁引入网络开销,需根据业务场景选择合适的实现方案。
总结
分布式锁是分布式系统中实现资源互斥的关键机制,常用Redis、ZooKeeper或数据库实现。设计时需考虑原子性、可重入性、锁超时和异常处理等问题。在选择实现方案时,需根据业务场景权衡性能、可靠性和复杂度。合理使用分布式锁能有效避免多节点并发带来的数据一致性问题。