核心概念:一个合格的分布式锁需要什么?
在比较具体实现之前,我们必须先了解一个健壮的分布式锁应具备的特性:
-
互斥性:在任意时刻,只有一个客户端能持有锁。
-
安全性:不会发生死锁。即使一个客户端在持有锁期间崩溃,没有主动解锁,也能保证后续其他客户端能够加锁。
-
容错性:只要大部分(超过一半)的锁服务节点存活,客户端就能正常获取和释放锁。
-
避免惊群效应:当锁被释放时,多个等待的客户端中只有一个能成功获得锁。
-
可重入性:同一个客户端在已经持有锁的情况下,可以再次成功获取锁。
三种实现方式的详细比较
| 特性 | Redis | ZooKeeper | 数据库(如MySQL) |
|---|---|---|---|
| 实现复杂度 | 中等 | 较低 | 低(但需处理重试、超时) |
| 性能 | 最高(内存操作) | 中等(ZK需要共识协议) | 最低(磁盘IO) |
| 一致性保证 | 弱(AP,异步复制) | 强(CP,基于ZAB协议) | 强(依赖数据库事务) |
| 避免死锁机制 | Key过期时间 | 临时节点(客户端断开自动删除) | 超时时间(需额外定时任务清理) |
| 锁唤醒机制 | Pub/Sub 或 客户端自旋 | Watch机制(天然事件通知) | 主动轮询(性能差) |
| 可重入性 | 需客户端逻辑实现 | 原生支持(同一Session) | 需客户端逻辑实现 |
| 主要缺点 | 主从切换可能导致锁失效 | 性能不如Redis,有广播风暴风险 | 性能最差,数据库压力大,易死锁 |
他们是如何实现的?
1. Redis 实现
Redis 实现分布式锁的核心命令是 SET key value NX PX milliseconds。
-
NX:仅当 Key 不存在时才设置。这保证了互斥性。
-
PX:设置 Key 的过期时间(毫秒)。这保证了安全性,避免了死锁。
基础实现流程:
-
加锁:
bashSET lock_resource_name my_random_value NX PX 30000-
my_random_value必须是全局唯一的值(如UUID),用于标识加锁的客户端。这至关重要,它用于在释放锁时验证这是自己加的锁,防止误删其他客户端的锁。 -
30000是锁的自动过期时间,单位毫秒。
-
-
业务操作:执行需要受锁保护的业务逻辑。
-
释放锁:使用 Lua 脚本保证原子性。
Luaif redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end- 脚本先比较当前锁的值是否与自己设置的值相等,相等才删除。
GET和DEL操作在 Lua 脚本中是原子的。
- 脚本先比较当前锁的值是否与自己设置的值相等,相等才删除。
高级方案 - Redlock算法:
为了克服 Redis 主从架构下(主节点宕机,锁信息未同步到从节点导致锁失效)的问题,Redis 作者提出了 Redlock 算法。它需要多个(通常为5个)独立的 Redis 主节点(非集群)。
-
客户端获取当前毫秒级时间戳 T1。
-
依次向 N 个 Redis 实例发送加锁命令(使用相同的 Key 和随机值)。
-
只有当客户端从超过半数(N/2+1) 的节点上成功获取锁,且总耗时小于锁的过期时间,才认为加锁成功。
-
锁的实际有效时间 = 初始有效时间 - 获取锁的总耗时。
-
如果加锁失败,客户端会向所有 Redis 实例发起释放锁的请求。
优缺点:
-
优点:性能极高,实现相对简单,社区支持好(如 Redisson 客户端)。
-
缺点:基础模式在主从故障切换时不安全;Redlock 算法复杂,性能有损耗,且存在争议(如 Martin Kleppmann 的批评)。
2. ZooKeeper 实现
ZooKeeper 的数据模型类似于文件系统,它的临时顺序节点是实现分布式锁的核心。
实现流程(排他锁):
-
加锁:
-
客户端在指定的锁节点(如
/locks/my_lock)下创建一个临时顺序节点 ,假设为/locks/my_lock/seq-00000001。 -
ZooKeeper 会保证这个节点在客户端会话(Session)结束时(如连接断开)被自动删除。这天然地避免了死锁。
-
客户端获取
/locks/my_lock下的所有子节点,并判断自己创建的子节点是否为序号最小的一个。 -
如果是,则成功获取锁。
-
如果不是,则对序号排在自己前面的那个节点 设置 Watch 监听。
-
-
业务操作:执行需要受锁保护的业务逻辑。
-
锁释放/监听:
-
当持有锁的客户端完成操作或会话结束时,临时节点会被删除。
-
监听该节点的下一个客户端会收到 ZooKeeper 的通知,然后再次检查自己是否是最小节点,如果是,则成功获取锁。
-
优缺点:
-
优点:锁强一致,安全可靠(临时节点防死锁),有天然的等待队列机制(Watch),避免了惊群效应。
-
缺点:性能比 Redis 差,因为每次写操作都需要在集群内达成共识。添加和删除节点会触发大量 Watch 事件,存在广播风暴风险。
3. 数据库实现
通常有两种方式:基于数据库表的唯一索引 或乐观锁。
基于唯一索引(悲观锁):
-
创建锁表:
sqlCREATE TABLE `distributed_lock` ( `id` int(11) NOT NULL AUTO_INCREMENT, `lock_key` varchar(64) NOT NULL, `lock_value` varchar(255) NOT NULL, `expire_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_lock_key` (`lock_key`) ); -
加锁:向表中插入一条记录。
sqlINSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES ('resource_1', 'my_uuid', NOW() + INTERVAL 30 SECOND);-
利用数据库的唯一约束,只有一个客户端能插入成功,成功即代表获取锁。
-
lock_value的作用同 Redis,用于安全释放锁。 -
expire_time用于防止死锁,需要一个定时任务来清理过期的锁。
-
-
业务操作:执行需要受锁保护的业务逻辑。
-
释放锁:
sqlDELETE FROM distributed_lock WHERE lock_key = 'resource_1' AND lock_value = 'my_uuid';
优缺点:
-
优点:实现简单,直接利用现有数据库,理解成本低。
-
缺点:
-
性能最差,数据库 IO 开销大,容易成为系统瓶颈。
-
数据库单点问题(虽然可以用主从,但主从延迟又会带来锁一致性问题)。
-
需要处理超时和清理过期锁,实现不优雅。
-
不具备可重入性和自动唤醒功能。
-
微服务架构中的分布式锁需求
在微服务架构中,随着服务被拆分成多个独立的进程,传统的单机锁机制无法满足跨服务的同步需求,分布式锁成为必需的基础组件。
微服务中常见的分布式锁场景
1. 资源争用场景
库存扣减与超卖防止
java
// 伪代码示例
public boolean deductStock(Long productId, Integer quantity) {
String lockKey = "stock_lock:" + productId;
DistributedLock lock = distributedLockService.tryLock(lockKey, 3000L);
try {
if (lock != null) {
// 查询库存
Integer currentStock = stockService.getStock(productId);
if (currentStock >= quantity) {
// 扣减库存
stockService.updateStock(productId, currentStock - quantity);
return true;
}
}
return false;
} finally {
if (lock != null) {
lock.unlock();
}
}
}
2. 分布式定时任务调度
确保集群中只有一个实例执行任务
java
@Component
public class ScheduledReportTask {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void generateDailyReport() {
String lockKey = "task:generate_daily_report";
if (distributedLockService.tryLock(lockKey, 3600L)) { // 锁1小时
try {
// 生成日报逻辑
reportService.generateDailyReport();
} finally {
distributedLockService.unlock(lockKey);
}
}
}
}
3. 防止重复操作
用户重复提交订单
java
@Service
public class OrderService {
public CreateOrderResult createOrder(CreateOrderRequest request) {
String lockKey = "order_create:" + request.getUserId();
// 5秒内防止同一用户重复提交
if (!distributedLockService.tryLock(lockKey, 5000L)) {
throw new BusinessException("请求过于频繁,请稍后再试");
}
try {
// 创建订单逻辑
return doCreateOrder(request);
} finally {
distributedLockService.unlock(lockKey);
}
}
}
4. 分布式环境下的初始化操作
配置加载或缓存预热
java
@Service
public class ConfigService {
private volatile boolean initialized = false;
@PostConstruct
public void initConfig() {
String lockKey = "config_initialization";
if (distributedLockService.tryLock(lockKey, 60000L)) {
try {
// 双重检查,防止重复初始化
if (!initialized) {
loadGlobalConfig();
warmUpCache();
initialized = true;
}
} finally {
distributedLockService.unlock(lockKey);
}
}
}
}
三种方案在微服务中的详细实现
1. Redis 分布式锁在微服务中的最佳实践
使用 Redisson 客户端(推荐)
java
<!-- Maven 依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>
配置类:
java
@Configuration
public class RedissonConfig {
@Value("${redis.host:localhost}")
private String redisHost;
@Value("${redis.port:6379}")
private String redisPort;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setDatabase(0)
.setConnectionPoolSize(64)
.setConnectionMinimumIdleSize(24)
.setIdleConnectionTimeout(10000)
.setConnectTimeout(10000)
.setTimeout(3000);
return Redisson.create(config);
}
}
服务类:
java
@Service
public class RedisDistributedLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 可重入锁
*/
public boolean tryReentrantLock(String lockKey, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* 公平锁 - 按照请求顺序获得锁
*/
public boolean tryFairLock(String lockKey, long waitTime, long leaseTime) {
RLock lock = redissonClient.getFairLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* 读写锁 - 读读不互斥,读写、写写互斥
*/
public boolean tryWriteLock(String lockKey, long waitTime, long leaseTime) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);
RLock writeLock = rwLock.writeLock();
try {
return writeLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
使用示例:
java
@Service
public class ProductService {
@Autowired
private RedisDistributedLockService lockService;
public void updateProductInventory(Long productId, Integer delta) {
String lockKey = "product_inventory:" + productId;
// 尝试获取锁,最多等待2秒,锁持有时间10秒
if (lockService.tryReentrantLock(lockKey, 2000, 10000)) {
try {
// 业务逻辑
productRepository.updateInventory(productId, delta);
// 模拟耗时操作
Thread.sleep(500);
} catch (Exception e) {
log.error("更新库存失败", e);
} finally {
lockService.unlock(lockKey);
}
} else {
throw new BusinessException("系统繁忙,请稍后重试");
}
}
}
2. ZooKeeper 分布式锁在微服务中的实现
使用 Curator 框架(推荐)
XML
<!-- Maven 依赖 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
配置类:
java
@Configuration
public class CuratorConfig {
@Value("${zookeeper.connect-string:localhost:2181}")
private String connectString;
@Bean(initMethod = "start", destroyMethod = "close")
public CuratorFramework curatorFramework() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
return CuratorFrameworkFactory.builder()
.connectString(connectString)
.sessionTimeoutMs(60000)
.connectionTimeoutMs(15000)
.retryPolicy(retryPolicy)
.namespace("microservice-locks")
.build();
}
@Bean
public InterProcessMutex interProcessMutex(CuratorFramework curatorFramework) {
// 这是一个示例bean,实际使用时根据不同的锁路径创建
return new InterProcessMutex(curatorFramework, "/locks");
}
}
服务类:
javascript
@Service
public class ZkDistributedLockService {
@Autowired
private CuratorFramework curatorFramework;
private final Map<String, InterProcessMutex> lockMap = new ConcurrentHashMap<>();
/**
* 获取互斥锁
*/
public boolean tryAcquireMutex(String lockPath, long timeout, TimeUnit unit) {
try {
InterProcessMutex lock = lockMap.computeIfAbsent(lockPath,
path -> new InterProcessMutex(curatorFramework, path));
return lock.acquire(timeout, unit);
} catch (Exception e) {
log.error("获取ZooKeeper锁失败", e);
return false;
}
}
/**
* 释放锁
*/
public void releaseMutex(String lockPath) {
try {
InterProcessMutex lock = lockMap.get(lockPath);
if (lock != null && lock.isAcquiredInThisProcess()) {
lock.release();
}
} catch (Exception e) {
log.error("释放ZooKeeper锁失败", e);
}
}
/**
* 获取读写锁
*/
public boolean tryAcquireWriteLock(String lockPath, long timeout, TimeUnit unit) {
try {
InterProcessReadWriteLock rwLock = new InterProcessReadWriteLock(curatorFramework, lockPath);
return rwLock.writeLock().acquire(timeout, unit);
} catch (Exception e) {
log.error("获取ZooKeeper写锁失败", e);
return false;
}
}
}
使用示例 - 配置中心数据同步:
java
@Service
public class ConfigSyncService {
@Autowired
private ZkDistributedLockService lockService;
public void syncGlobalConfig() {
String lockPath = "/config/sync/global";
if (lockService.tryAcquireMutex(lockPath, 5, TimeUnit.SECONDS)) {
try {
// 只有获得锁的服务实例执行配置同步
log.info("开始同步全局配置...");
configService.syncFromCentral();
log.info("全局配置同步完成");
} finally {
lockService.releaseMutex(lockPath);
}
} else {
log.info("其他服务实例正在执行配置同步,跳过本次执行");
}
}
}
3. 数据库分布式锁在微服务中的实现
锁表结构:
sql
CREATE TABLE `distributed_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`lock_key` varchar(255) NOT NULL COMMENT '锁定的资源key',
`lock_value` varchar(255) NOT NULL COMMENT '锁的值(UUID)',
`expire_time` datetime NOT NULL COMMENT '锁过期时间',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_lock_key` (`lock_key`),
KEY `idx_expire_time` (`expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
服务类:
java
@Service
@Transactional
public class DatabaseDistributedLockService {
@Autowired
private JdbcTemplate jdbcTemplate;
private static final String INSERT_SQL =
"INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES (?, ?, ?)";
private static final String DELETE_SQL =
"DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?";
private static final String CLEAN_EXPIRED_SQL =
"DELETE FROM distributed_lock WHERE expire_time < NOW()";
/**
* 尝试获取锁
*/
public boolean tryLock(String lockKey, long expireMillis) {
String lockValue = UUID.randomUUID().toString();
LocalDateTime expireTime = LocalDateTime.now().plus(expireMillis, ChronoUnit.MILLIS);
try {
// 清理过期锁
jdbcTemplate.update(CLEAN_EXPIRED_SQL);
// 尝试插入获取锁
int affected = jdbcTemplate.update(INSERT_SQL, lockKey, lockValue, expireTime);
return affected > 0;
} catch (DuplicateKeyException e) {
// 锁已被其他线程持有
return false;
}
}
/**
* 释放锁
*/
public boolean unlock(String lockKey, String lockValue) {
int affected = jdbcTemplate.update(DELETE_SQL, lockKey, lockValue);
return affected > 0;
}
/**
* 带重试的锁获取
*/
public boolean tryLockWithRetry(String lockKey, long expireMillis, int maxRetries, long retryInterval) {
for (int i = 0; i < maxRetries; i++) {
if (tryLock(lockKey, expireMillis)) {
return true;
}
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return false;
}
}
微服务场景下的选型矩阵
| 场景分类 | 具体场景 | 推荐方案 | 理由 |
|---|---|---|---|
| 高并发业务 | 秒杀、抢购、库存扣减 | Redis | 性能要求极高,允许极低概率的锁失效 |
| 金融交易 | 账户余额变更、交易处理 | ZooKeeper 或 Redis + 事务补偿 | 强一致性要求,不能出现重复操作 |
| 定时任务 | 报表生成、数据归档 | Redis 或 ZooKeeper | Redis性能好,ZooKeeper更可靠 |
| 配置管理 | 配置热更新、服务发现 | ZooKeeper | 天然的一致性协调能力 |
| 工作流控制 | 审批流程、状态机 | Redis | 性能好,支持复杂的锁类型 |
| 简单业务 | 低频操作、内部管理 | 数据库 | 无需引入新组件,维护简单 |
微服务架构中的最佳实践
1. 锁的粒度控制
java
// 好的实践 - 细粒度锁
String lockKey = "order:" + orderId;
// 不好的实践 - 粗粒度锁
String lockKey = "order_lock"; // 所有订单操作都串行化
2. 超时时间设置
java
// 根据业务操作预估合理超时时间
long timeout = calculateBusinessTimeout(); // 动态计算
distributedLockService.tryLock(lockKey, timeout);
3. 故障恢复机制
java
@Service
public class ResilientLockService {
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void doWithLock(String lockKey, Runnable businessLogic) {
// 获取锁并执行业务逻辑
if (tryLock(lockKey)) {
try {
businessLogic.run();
} finally {
unlock(lockKey);
}
}
}
}
4. 监控与告警
java
@Component
public class LockMonitor {
@EventListener
public void handleLockAcquisitionFailure(LockAcquisitionFailureEvent event) {
// 记录锁获取失败指标
metrics.increment("lock.acquisition.failure");
// 触发告警
if (event.getFailureCount() > threshold) {
alertService.sendAlert("分布式锁获取异常频繁");
}
}
}
总结
在微服务架构中选择分布式锁方案时:
-
Redis:适用于绝大多数业务场景,性能优秀,生态成熟
-
ZooKeeper:适用于对一致性要求极高的核心业务场景
-
数据库:适用于简单场景或作为过渡方案
推荐策略 :在微服务架构中,可以基于 Redis 构建主要的分布式锁能力,对于特别关键的业务(如资金交易)使用 ZooKeeper 作为补充,形成多层次的锁策略。同时,无论选择哪种方案,都要做好监控、熔断和降级准备,确保锁服务不会成为系统的单点故障。