1. 分布式锁的核心价值与设计原则
在分布式系统中,多个服务实例对共享资源的并发访问是一个经典难题。想象一下电商平台的库存扣减场景:当两个用户同时下单购买最后一件商品时,如果没有分布式锁的保护,系统可能会错误地认为库存充足,导致超卖问题。分布式锁正是为了解决这类跨进程、跨机器的资源竞争问题而诞生的。
1.1 从本地锁到分布式锁的演进
在单体应用时代,我们使用Java中的synchronized或ReentrantLock等本地锁就能解决多线程并发问题。这些锁机制通过JVM内部的内存操作实现互斥,性能极高且易于使用。但随着系统架构演进为分布式微服务,本地锁的局限性立即显现:
- 作用范围仅限于单个JVM进程
- 无法跨服务实例实现全局互斥
- 在容器化部署环境下完全失效
java复制// 本地锁在分布式环境中的失效示例
public class InventoryService {
private final Object lock = new Object();
private int stock = 100;
public void deduct() {
synchronized(lock) { // 只能锁住当前实例的调用
if (stock > 0) {
stock--;
}
}
}
}
1.2 分布式锁的七大核心特性
一个生产级的分布式锁必须满足以下设计要求:
| 特性 | 描述 | 重要程度 |
|---|---|---|
| 互斥性 | 在任意时刻,只有一个客户端能持有锁 | 必须 |
| 防死锁 | 即使持有锁的客户端崩溃,锁也最终能够被释放 | 必须 |
| 安全释放 | 锁只能被持有它的客户端释放,不能被其他客户端误删 | 必须 |
| 容错性 | 锁服务本身具有高可用性,部分节点故障不影响锁的正常使用 | 高 |
| 可重入性 | 同一个客户端(线程)可以多次获取同一个锁 | 推荐 |
| 公平性 | 按照请求顺序获取锁,避免饥饿 | 可选 |
| 阻塞/非阻塞 | 支持获取不到锁时阻塞等待或立即返回 | 推荐 |
提示:在实际项目中,我们通常需要根据业务场景在这些特性之间做出权衡。例如秒杀系统更关注互斥性和性能,而金融交易系统则更强调安全释放和公平性。
2. 基于数据库的分布式锁实现
2.1 唯一索引方案
这是最直观的实现方式,利用数据库的唯一约束保证互斥性:
sql复制CREATE TABLE distributed_lock (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
lock_name VARCHAR(64) NOT NULL,
lock_holder VARCHAR(128) NOT NULL, -- 持有者标识
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expire_time TIMESTAMP, -- 过期时间
UNIQUE KEY uk_lock_name (lock_name)
) ENGINE=InnoDB;
加锁操作通过插入语句实现:
sql复制INSERT INTO distributed_lock
(lock_name, lock_holder, expire_time)
VALUES
('order_lock_001', 'instance-1-thread-1', DATE_ADD(NOW(), INTERVAL 30 SECOND));
解锁时需要验证持有者身份:
sql复制DELETE FROM distributed_lock
WHERE lock_name = 'order_lock_001'
AND lock_holder = 'instance-1-thread-1';
2.2 排他锁方案
利用SELECT...FOR UPDATE语句实现行级锁:
java复制// 基于JPA的实现示例
@Transactional
public boolean tryLock(String lockName, String holderId, long expireSeconds) {
// 先尝试插入
try {
lockRepository.insertLock(lockName, holderId, expireSeconds);
return true;
} catch (DuplicateKeyException e) {
// 如果已存在,检查是否过期
DistributedLock existing = lockRepository.findByName(lockName);
if (existing != null && existing.getExpireTime().before(new Date())) {
// 过期锁可以抢占
lockRepository.deleteById(existing.getId());
lockRepository.insertLock(lockName, holderId, expireSeconds);
return true;
}
return false;
}
}
2.3 数据库方案的优缺点分析
优势:
- 实现简单,无需引入额外中间件
- 利用现有数据库基础设施
- ACID特性保证操作原子性
劣势:
- 性能瓶颈(单机TPS通常在千级)
- 不具备自动续期能力
- 锁表可能成为单点故障
- 需要处理连接池耗尽问题
生产建议:仅适用于并发量低(<500TPS)、对性能不敏感的场景,且建议配合定时任务清理过期锁。
3. Redis分布式锁深度解析
3.1 基础实现方案
Redis的SET命令配合NX/EX选项可以实现原子性加锁:
bash复制SET lock_key "unique_client_id" NX PX 30000
安全释放锁的Lua脚本:
lua复制if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Java实现示例:
java复制public class RedisLock {
private final StringRedisTemplate redisTemplate;
public boolean tryLock(String key, String value, long expireMs) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(key, value, expireMs, TimeUnit.MILLISECONDS)
);
}
public boolean unlock(String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
return Long.valueOf(1).equals(
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value
)
);
}
}
3.2 Redisson高级特性
看门狗机制
Redisson通过后台线程定期(默认每10秒)检查锁状态并自动续期:
java复制// 看门狗工作流程示例
public void run() {
while (true) {
if (lock.isHeldByCurrentThread()) {
// 续期操作
redisTemplate.expire(lockKey, 30, TimeUnit.SECONDS);
}
Thread.sleep(10000); // 10秒检查一次
}
}
可重入锁实现
Redisson使用Redis Hash结构存储锁信息:
bash复制HSET lock_key field1 "client1:thread1:1" # 最后数字表示重入次数
3.3 Redlock算法详解
Redlock算法的核心步骤:
- 获取当前毫秒级时间戳T1
- 依次向N个Redis实例发送加锁命令
- 计算获取锁总耗时 = 当前时间T2 - T1
- 当且仅当满足:
- 在多数节点上加锁成功
- 总耗时小于锁过期时间
- 如果加锁失败,向所有节点发送解锁命令
java复制// Redisson RedLock使用示例
RLock lock1 = redissonClient1.getLock("lock");
RLock lock2 = redissonClient2.getLock("lock");
RLock lock3 = redissonClient3.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
if (redLock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
redLock.unlock();
}
4. Zookeeper分布式锁实现
4.1 核心原理
Zookeeper通过两种特殊节点实现分布式锁:
- 临时节点:客户端会话结束自动删除
- 顺序节点:自动追加单调递增序号
锁节点结构示例:
code复制/locks/order_lock
├── _c_1234567890000000001
├── _c_1234567890000000002
└── _c_1234567890000000003
4.2 Curator框架实现
java复制// 创建锁实例
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order_lock");
// 加锁逻辑
public void processWithLock(String orderId) throws Exception {
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 业务逻辑
processOrder(orderId);
} finally {
lock.release();
}
}
}
4.3 Zookeeper锁的优缺点
优势:
- 天然支持公平锁
- 无过期时间问题
- 强一致性保证
- 自动清理机制
劣势:
- 性能相对较低(通常比Redis慢一个数量级)
- 需要维护Zookeeper集群
- 对网络波动敏感
5. 生产环境最佳实践
5.1 锁粒度设计原则
- 粗粒度:
lock("global_inventory_lock") - 细粒度:
lock("inventory_lock:product_123")
建议采用分段锁提升并发性能:
java复制// 将商品ID哈希到16个锁桶中
int bucket = productId.hashCode() & 0xF;
RLock lock = redisson.getLock("inventory_lock:" + bucket);
5.2 异常处理规范
java复制try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
// 降级处理
fallback();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Lock interrupted", e);
throw new BusinessException("Operation interrupted");
}
5.3 监控与告警
建议监控以下指标:
- 锁等待时间
- 锁持有时间
- 锁获取失败率
- 锁续期次数
prometheus复制# Prometheus监控指标示例
redisson_lock_wait_seconds_sum{name="order_lock"}
redisson_lock_held_seconds_max{name="order_lock"}
6. 技术选型决策树
根据业务场景选择最合适的方案:
-
是否已有Redis/ZK基础设施?
- 是 → 优先使用现有中间件
- 否 → 考虑数据库方案或引入Redis
-
性能要求是否极高(>5000TPS)?
- 是 → Redis方案
- 否 → 进入下一步
-
是否需要强一致性保证?
- 是 → Zookeeper方案
- 否 → Redis方案
-
是否需要公平锁?
- 是 → Zookeeper方案
- 否 → Redis方案
个人经验:在电商系统中,我们90%的场景使用Redis分布式锁,只有对一致性要求极高的资金操作才会使用Zookeeper。数据库方案仅用于一些后台批处理任务。