分布式锁实现:Redis、Zookeeper、数据库三种方案对比

分布式锁实现:Redis、Zookeeper、数据库三种方案对比

引言

在单体架构时代,我们可以轻松地使用Java语言内置的机制——如synchronized关键字或ReentrantLock类——来解决并发问题。然而,随着业务量的激增和微服务架构的普及,应用部署由单节点变为多节点集群,原本的线程锁只能控制单个JVM进程内的并发,无法跨越进程屏障。

这就引出了分布式锁的概念。试想一个典型的电商“秒杀”场景:库存只有100件,瞬间有成千上万的请求从不同的服务器实例涌入。如果缺乏有效的跨进程并发控制,极易出现“超卖”现象,导致巨大的资损。

本文将深入剖析三种主流的分布式锁实现方案:数据库、Redis和Zookeeper,从原理到实战代码,帮助你做出最适合业务场景的技术选型。

核心概念:分布式锁的特性

一个合格的分布式锁实现,必须具备以下核心特性:

  1. 互斥性:在任意时刻,只有一个客户端能持有锁,这是锁的最基本功能。
  2. 防死锁:锁必须有超时机制,防止客户端在持有锁后崩溃导致锁永远无法释放。
  3. 高可用:锁服务自身必须具备高可用性,不能成为系统的单点故障。
  4. 可重入性:同一个线程(或同一个客户端连接)可以多次获取同一把锁,而不会自己阻塞自己。
  5. 安全性:锁只能被持有它的客户端释放,不能被其他客户端误删。

方案一:基于数据库的实现

技术原理

数据库实现分布式锁主要依赖其“唯一索引”约束。

  1. 加锁:在一张专门设计的锁表中插入一条记录。如果插入成功,则视为获取锁成功;若发生唯一索引冲突异常,则说明锁已被占用。
  2. 解锁:删除该条记录。
  3. 防死锁:通常需要一个定时任务或通过记录的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

posted @ 2026-02-28 18:01  寒人病酒  阅读(0)  评论(0)    收藏  举报