分布式锁实现:Redis、Zookeeper、数据库三种方案对比
分布式锁实现:Redis、Zookeeper、数据库三种方案对比
引言
在单体架构时代,我们可以轻松地使用Java语言内置的机制——如synchronized关键字或ReentrantLock类——来解决并发问题。然而,随着业务量的激增和微服务架构的普及,应用部署由单节点变为多节点集群,原本的线程锁只能控制单个JVM进程内的并发,无法跨越进程屏障。
这就引出了分布式锁的概念。试想一个典型的电商“秒杀”场景:库存只有100件,瞬间有成千上万的请求从不同的服务器实例涌入。如果缺乏有效的跨进程并发控制,极易出现“超卖”现象,导致巨大的资损。
本文将深入剖析三种主流的分布式锁实现方案:数据库、Redis和Zookeeper,从原理到实战代码,帮助你做出最适合业务场景的技术选型。
核心概念:分布式锁的特性
一个合格的分布式锁实现,必须具备以下核心特性:
- 互斥性:在任意时刻,只有一个客户端能持有锁,这是锁的最基本功能。
- 防死锁:锁必须有超时机制,防止客户端在持有锁后崩溃导致锁永远无法释放。
- 高可用:锁服务自身必须具备高可用性,不能成为系统的单点故障。
- 可重入性:同一个线程(或同一个客户端连接)可以多次获取同一把锁,而不会自己阻塞自己。
- 安全性:锁只能被持有它的客户端释放,不能被其他客户端误删。
方案一:基于数据库的实现
技术原理
数据库实现分布式锁主要依赖其“唯一索引”约束。
- 加锁:在一张专门设计的锁表中插入一条记录。如果插入成功,则视为获取锁成功;若发生唯一索引冲突异常,则说明锁已被占用。
- 解锁:删除该条记录。
- 防死锁:通常需要一个定时任务或通过记录的
create_time字段配合“过期时间”逻辑来清理超时的记录。
实战代码
首先,我们需要一张锁表。
CREATE TABLE `distributed_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`lock_key` varchar(64) NOT NULL COMMENT '锁的唯一标识',
`lock_value` varchar(64) NOT NULL COMMENT '锁持有者标识,如UUID',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_lock_key` (`lock_key`) -- 核心保证互斥性
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Java实现示例:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.UUID;
@Component
public class DatabaseLock {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 尝试获取锁
* @param lockKey 锁的键
* @param expireSeconds 过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, int expireSeconds) {
String lockValue = UUID.randomUUID().toString();
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireSeconds);
String sql = "INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES (?, ?, ?)";
try {
// 利用唯一索引冲突特性,插入成功即获取锁
int affectedRows = jdbcTemplate.update(sql, lockKey, lockValue, expireTime);
return affectedRows > 0;
} catch (Exception e) {
// 捕获主键/唯一键冲突异常,说明锁已被占用
return false;
}
}
/**
* 释放锁
* @param lockKey 锁的键
* @param lockValue 锁的值(必须匹配才能删除,防止误删)
*/
public void unlock(String lockKey, String lockValue) {
String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?";
jdbcTemplate.update(sql, lockKey, lockValue);
}
}
优缺点分析
- 优点:实现简单,理解成本低,完全依赖现有的数据库设施,无需引入新组件。
- 缺点:
- 性能瓶颈:数据库连接资源宝贵,并发写入压力大时,数据库IO容易成为瓶颈。
- 锁失效风险:数据库主从同步延迟可能导致锁数据不一致;没有原生的过期机制,需要额外逻辑处理死锁。
- 不可重入:上述实现不具备可重入性,需要复杂的逻辑改造。
方案二:基于Redis的实现
技术原理
Redis基于内存操作,性能极高。实现分布式锁通常使用SETNX(Set if Not eXists)命令。
早期做法是先SETNX,再EXPIRE设置过期时间,但这不是原子操作。现代Redis规范推荐使用扩展命令:
SET key value NX PX timeout
* NX:只有键不存在时才设置。
* PX:设置毫秒级过期时间。
关键难点:如何保证“解锁”操作的安全性(不能删别人的锁)?这就需要配合Lua脚本,保证“判断锁归属”和“删除锁”的原子性。
实战代码
这里展示基于原生Jedis的原子化实现,这也是Redisson等框架的核心逻辑简化版。
```java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java

浙公网安备 33010602011771号