分布式锁Redis、ZooKeeper 和数据库实现分布式锁的优缺点、实现方式以及适用场景
核心概念:一个合格的分布式锁需要什么?
在比较具体实现之前,我们必须先了解一个健壮的分布式锁应具备的特性:
互斥性:在任意时刻,只有一个客户端能持有锁。
安全性:不会发生死锁。即使一个客户端在持有锁期间崩溃,没有主动解锁,也能保证后续其他客户端能够加锁。
容错性:只要大部分(超过一半)的锁服务节点存活,客户端就能正常获取和释放锁。
避免惊群效应:当锁被释放时,多个等待的客户端中只有一个能成功获得锁。
可重入性:同一个客户端在已经持有锁的情况下,可以再次成功获取锁。
三种实现方式的详细比较
| 特性 | 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 的过期时间(毫秒)。这保证了安全性,避免了死锁。
基础实现流程:
加锁:
SET lock_resource_name my_random_value NX PX 30000my_random_value必须是全局唯一的值(如UUID),用于标识加锁的客户端。这至关重要,它用于在释放锁时验证这是自己加的锁,防止误删其他客户端的锁。30000是锁的自动过期时间,单位毫秒。
业务操作:执行需要受锁保护的业务逻辑。
释放锁:使用 Lua 脚本保证原子性。
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1]) elsereturn 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. 数据库实现
通常有两种方式:基于数据库表的唯一索引或乐观锁。
基于唯一索引(悲观锁):
创建锁表:
CREATE 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`) );加锁:向表中插入一条记录。
INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES ('resource_1', 'my_uuid', NOW() + INTERVAL 30 SECOND);利用数据库的唯一约束,只有一个客户端能插入成功,成功即代表获取锁。
lock_value的作用同 Redis,用于安全释放锁。expire_time用于防止死锁,需要一个定时任务来清理过期的锁。
业务操作:执行需要受锁保护的业务逻辑。
释放锁:
DELETE FROM distributed_lock WHERE lock_key = 'resource_1' AND lock_value = 'my_uuid';
优缺点:
优点:实现简单,直接利用现有数据库,理解成本低。
缺点:
性能最差,数据库 IO 开销大,容易成为系统瓶颈。
数据库单点问题(虽然可以用主从,但主从延迟又会带来锁一致性问题)。
需要处理超时和清理过期锁,实现不优雅。
不具备可重入性和自动唤醒功能。
微服务架构中的分布式锁需求
在微服务架构中,随着服务被拆分成多个独立的进程,传统的单机锁机制无法满足跨服务的同步需求,分布式锁成为必需的基础组件。
微服务中常见的分布式锁场景
1. 资源争用场景
库存扣减与超卖防止
// 伪代码示例
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. 分布式定时任务调度
确保集群中只有一个实例执行任务
@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. 防止重复操作
用户重复提交订单
@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. 分布式环境下的初始化操作
配置加载或缓存预热
@Service
public class ConfigService {private volatile boolean initialized = false;@PostConstructpublic 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 客户端(推荐)
<!-- Maven 依赖 -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.27.0</version>
</dependency>配置类:
@Configuration
public class RedissonConfig {@Value("${redis.host:localhost}")private String redisHost;@Value("${redis.port:6379}")private String redisPort;@Beanpublic 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);}
}服务类:
@Service
public class RedisDistributedLockService {@Autowiredprivate 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();}}
}使用示例:
@Service
public class ProductService {@Autowiredprivate 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 框架(推荐)
<!-- Maven 依赖 -->
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>5.5.0</version>
</dependency>配置类:
@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();}@Beanpublic InterProcessMutex interProcessMutex(CuratorFramework curatorFramework) {// 这是一个示例bean,实际使用时根据不同的锁路径创建return new InterProcessMutex(curatorFramework, "/locks");}
}服务类:
@Service
public class ZkDistributedLockService {@Autowiredprivate 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;}}
}使用示例 - 配置中心数据同步:
@Service
public class ConfigSyncService {@Autowiredprivate 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. 数据库分布式锁在微服务中的实现
锁表结构:
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='分布式锁表';服务类:
@Service
@Transactional
public class DatabaseDistributedLockService {@Autowiredprivate 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. 锁的粒度控制
// 好的实践 - 细粒度锁
String lockKey = "order:" + orderId;// 不好的实践 - 粗粒度锁
String lockKey = "order_lock"; // 所有订单操作都串行化2. 超时时间设置
// 根据业务操作预估合理超时时间
long timeout = calculateBusinessTimeout(); // 动态计算
distributedLockService.tryLock(lockKey, timeout);3. 故障恢复机制
@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. 监控与告警
@Component
public class LockMonitor {@EventListenerpublic void handleLockAcquisitionFailure(LockAcquisitionFailureEvent event) {// 记录锁获取失败指标metrics.increment("lock.acquisition.failure");// 触发告警if (event.getFailureCount() > threshold) {alertService.sendAlert("分布式锁获取异常频繁");}}
}总结
在微服务架构中选择分布式锁方案时:
Redis:适用于绝大多数业务场景,性能优秀,生态成熟
ZooKeeper:适用于对一致性要求极高的核心业务场景
数据库:适用于简单场景或作为过渡方案
推荐策略:在微服务架构中,可以基于 Redis 构建主要的分布式锁能力,对于特别关键的业务(如资金交易)使用 ZooKeeper 作为补充,形成多层次的锁策略。同时,无论选择哪种方案,都要做好监控、熔断和降级准备,确保锁服务不会成为系统的单点故障。
