0%

分布式锁

分布式锁详解:实现方案与对比分析

在分布式系统中,多个节点竞争同一资源(如库存扣减、订单创建)时,需通过分布式锁保证操作的原子性,避免数据不一致。常见实现方式包括 Redis、ZooKeeper 和数据库,每种方案各有优劣,需根据场景选择。以下详细解析各方案的原理、代码实现及适用场景。

Redis 分布式锁:高性能的选择

Redis 凭借其高性能和原子操作特性,成为分布式锁的主流实现方案。核心思路是通过 set 命令的原子性实现加锁,结合过期时间和唯一标识避免死锁。

基础实现(setnx + expire 的问题与优化)

  • 原始方案:使用 setnx 加锁(仅当 key 不存在时成功),expire 设置过期时间(避免死锁)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 加锁:若 key 不存在则设置,返回 1 表示成功
    Long setnx = jedis.setnx("lock:order", "1");
    if (setnx == 1) {
    // 设置过期时间(30 秒)
    jedis.expire("lock:order", 30);
    // 执行业务逻辑
    // ...
    // 释放锁
    jedis.del("lock:order");
    }

    问题setnxexpire 是非原子操作,若加锁后未设置过期时间就宕机,会导致锁永久有效。

  • 优化方案:使用 Redis 2.6.12+ 支持的 set 命令扩展参数,实现 “加锁 + 过期时间” 原子操作:

    1
    2
    3
    4
    5
    6
    // NX:仅 key 不存在时设置;EX:过期时间单位为秒
    String result = jedis.set("lock:order", "requestId123", "NX", "EX", 30);
    if ("OK".equals(result)) {
    // 加锁成功,执行业务
    // ...
    }

释放锁的原子性(避免误删)

若直接使用 del 释放锁,可能删除其他节点的锁(如当前节点业务超时,锁已过期并被其他节点获取)。解决方案:通过唯一标识(如 UUID)验证锁归属,并使用 Lua 脚本保证释放操作的原子性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 释放锁的 Lua 脚本:仅当 value 匹配时删除 key
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";

// 执行脚本:KEYS 为锁 key,ARGV 为唯一标识
Long result = (Long) jedis.eval(
luaScript,
Collections.singletonList("lock:order"),
Collections.singletonList("requestId123")
);
if (result == 1) {
System.out.println("释放锁成功");
}

Redisson 框架:生产级实现

手动实现存在锁续约(避免业务未完成锁过期)、集群一致性等问题,推荐使用 Redisson 框架,其封装了完整的分布式锁逻辑:

  • 依赖引入

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.20.0</version>
    </dependency>
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 初始化客户端(支持单机、集群等模式)
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    RedissonClient redisson = Redisson.create(config);

    // 获取锁对象
    RLock lock = redisson.getLock("lock:order");
    try {
    // 加锁(默认 30 秒过期,自动续约)
    lock.lock();
    // 执行业务逻辑(如扣减库存)
    // ...
    } finally {
    // 释放锁
    lock.unlock();
    }
  • 核心特性

    • 自动续约(Watch Dog 机制):若业务未完成,自动延长锁过期时间;
    • 支持集群模式(Redis Cluster),解决单点故障问题;
    • 提供公平锁、读写锁等高级功能。

ZooKeeper 分布式锁:强一致性的选择

ZooKeeper 基于临时有序节点和监听机制实现分布式锁,天然支持公平锁,适合对一致性要求高的场景。

实现原理

  1. 加锁:所有节点在指定路径(如 /locks)下创建临时有序节点(如 /locks/lock-0000000001)。
  2. 竞争锁
    • 节点创建后,获取 /locks 下的所有子节点,若自身是序号最小的节点,则获得锁;
    • 若不是,则监听前一个节点(如 /locks/lock-0000000000)的删除事件。
  3. 释放锁:完成业务后删除自身节点,或会话失效时临时节点自动删除,触发下一个节点的监听事件。

Curator 框架:简化实现

Curator 是 ZooKeeper 官方推荐的客户端,其 recipes 模块封装了分布式锁实现:

  • 依赖引入

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.4.0</version>
    </dependency>
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 初始化客户端
    RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
    CuratorFramework client = CuratorFrameworkFactory
    .newClient("localhost:2181", retryPolicy);
    client.start();

    // 创建分布式锁
    InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
    try {
    // 尝试加锁(最多等待 10 秒)
    if (lock.acquire(10, TimeUnit.SECONDS)) {
    // 执行业务逻辑
    // ...
    }
    } finally {
    // 释放锁
    if (lock.isAcquiredInThisProcess()) {
    lock.release();
    }
    }
  • 核心特性

    • 公平锁:通过有序节点保证 “先到先得”,避免饥饿问题;
    • 自动释放:临时节点随会话失效而删除,避免死锁;
    • 强一致性:基于 ZooKeeper 的 CP 特性,适合金融等对一致性敏感的场景。

数据库分布式锁:简单但性能有限

基于数据库的行锁或表锁实现分布式锁,适合并发量低、架构简单的场景。

基于行锁(select for update

利用 InnoDB 的行锁机制,通过 select ... for update 锁定特定记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 加锁:锁定 id=1 的记录(行锁)
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
// for update 会阻塞其他事务的更新操作
PreparedStatement ps = conn.prepareStatement(
"select * from lock_table where id = 1 for update"
);
ps.executeQuery();
// 执行业务逻辑
// ...
conn.commit(); // 释放锁
} catch (SQLException e) {
conn.rollback();
}

基于锁表

创建专门的锁表,通过插入 / 删除记录实现锁控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 锁表结构
CREATE TABLE distributed_lock (
id INT PRIMARY KEY,
lock_name VARCHAR(50) UNIQUE, -- 锁名称
holder VARCHAR(50), -- 持有节点标识
expire_time TIMESTAMP -- 过期时间
);

-- 加锁:插入记录(唯一索引保证原子性)
INSERT INTO distributed_lock (lock_name, holder, expire_time)
VALUES ('order_lock', 'node1', NOW() + INTERVAL 30 SECOND);

-- 释放锁:删除记录
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND holder = 'node1';

缺点

  • 性能差:数据库 IO 开销大,不适合高并发场景;
  • 可能死锁:若事务未提交,行锁 / 表锁会长期持有;
  • 无自动续约:需手动处理锁过期问题。

三种方案对比与选型

方案 优点 缺点 适用场景
Redis 锁 高性能,支持高并发;部署简单 弱一致性(主从同步可能延迟);需处理续约 互联网高并发场景(如秒杀、电商)
ZooKeeper 锁 强一致性;天然公平锁;自动释放 性能较低(创建节点开销大);部署复杂 金融、支付等强一致性场景
数据库锁 实现简单,无需额外组件 性能差,不支持高并发;易死锁 低并发、架构简单的内部系统

欢迎关注我的其它发布渠道