深入剖析 Java 分布式锁:Redis/ZooKeeper/Etcd 实现差异与底层逻辑

Java分布式锁技术深度解析

在 Java 分布式系统中,分布式锁是解决跨进程、跨服务器共享资源并发竞争的核心方案(例如秒杀库存扣减、分布式任务调度、避免重复提交等场景)。与单机锁(synchronized、ReentrantLock)不同,分布式锁需要协调多个节点的状态,确保同一时刻只有一个节点能获取锁。

一、分布式锁的核心要求

一个可靠的分布式锁必须满足以下特性:

  1. 互斥性:同一时刻只有一个客户端能持有锁;
  2. 安全性:锁只能被持有它的客户端释放,防止误释放他人的锁;
  3. 可用性:分布式环境下,锁服务不能单点故障(需高可用);
  4. 原子性:锁的获取 / 释放操作必须是原子的,避免网络延迟导致的状态不一致;
  5. 可重入性(可选):同一客户端可重复获取同一把锁(避免死锁);
  6. 超时释放(可选):防止客户端崩溃后锁一直占用(避免死锁)。

二、Java 中分布式锁的实现方案

常用的实现方案有 4 种,各有优劣,需根据场景选择:

方案 1:基于 Redis 的分布式锁(最常用)

Redis 因高性能、高可用(主从 + 哨兵 / 集群),是分布式锁的首选方案。核心利用 Redis 的 原子命令(SETNX、EX、Lua 脚本)保证锁的原子性。

1. 核心原理
  • 获取锁:用 SET key value NX EX timeout 命令(原子操作):
    • NX:仅当 key 不存在时才设置(保证互斥);
    • EX timeout:自动过期(避免死锁);
    • value:用 UUID + 线程 ID 标识客户端(保证安全性,仅能释放自己的锁)。
  • 释放锁:用 Lua 脚本原子性校验 value 并删除 key(避免误删他人锁)。
  • 可重入性:需额外存储锁的持有次数(例如 Redis Hash 结构)。
  • 高可用:Redis 集群(主从 + 哨兵),避免单点故障。
2. 手动实现(简化版)
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class RedisDistributedLock {
    private final StringRedisTemplate redisTemplate;
    private final String lockKey; // 锁的唯一标识(例如 "stock_lock_1001")
    private final long expireTime; // 锁超时时间(秒)
    private String lockValue; // 当前客户端的锁值(UUID+线程ID)

    // 释放锁的Lua脚本(原子校验+删除)
    private static final String UNLOCK_SCRIPT = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
            """;

    public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockKey, long expireTime) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey;
        this.expireTime = expireTime;
        this.lockValue = UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
    }

    // 获取锁
    public boolean tryLock() {
        // SET key value NX EX expireTime:原子操作
        Boolean success = redisTemplate.opsForValue().setIfAbsent(
            lockKey, lockValue, expireTime, TimeUnit.SECONDS
        );
        return Boolean.TRUE.equals(success);
    }

    // 释放锁
    public boolean unlock() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(
            script,
            Collections.singletonList(lockKey), // KEYS[1]
            lockValue // ARGV[1]
        );
        return result != null && result > 0;
    }
}
3. 成熟框架:Redisson(推荐)

手动实现需处理可重入、锁续期、集群高可用等细节,推荐使用 Redisson(Redis 官方推荐的 Java 客户端),它封装了完整的分布式锁实现:

  • 支持可重入锁(RLock)、公平锁、联锁、红锁等;
  • 自动实现锁续期(避免锁超时释放);
  • 适配 Redis 单机、主从、哨兵、集群等部署模式。

Redisson 示例代码

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonLockDemo {
    public static void main(String[] args) {
        // 1. 配置 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redissonClient = Redisson.create(config);

        // 2. 获取分布式锁(锁的唯一标识)
        RLock lock = redissonClient.getLock("stock_lock_1001");

        try {
            // 3. 尝试获取锁:等待10秒,10秒内未获取则失败;锁自动过期30秒
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (locked) {
                // 4. 执行业务逻辑(例如扣减库存)
                System.out.println("获取锁成功,执行核心业务...");
            } else {
                System.out.println("获取锁失败,当前锁被占用");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 5. 释放锁(仅持有锁的客户端能释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }

        // 6. 关闭客户端
        redissonClient.shutdown();
    }
}
4. 优缺点
  • 优点:高性能(Redis 单机 QPS 万级)、部署简单、支持多种锁类型;
  • 缺点:Redis 集群下可能出现 “主从切换丢失锁”(主节点锁未同步到从节点就宕机),需用红锁(RedLock)缓解(但红锁性能略低)。

方案 2:基于 ZooKeeper 的分布式锁

ZooKeeper 是分布式协调工具,基于 临时有序节点 实现分布式锁,天然支持高可用和公平锁。

1. 核心原理
  • 客户端在 ZooKeeper 的 /lock 节点下创建 临时有序子节点(例如 /lock/lock-xxx-00000001);
  • 客户端获取 /lock 下所有子节点,判断自己的节点是否是最小的:
    • 是:获取锁成功;
    • 否:监听前一个节点(Watcher 机制),前一个节点删除时触发通知,再次判断是否为最小节点;
  • 释放锁:客户端断开连接时,临时节点自动删除(天然支持超时释放),或主动删除节点。
2. 成熟框架:Curator

ZooKeeper 原生 API 复杂,推荐使用 Curator 框架(Apache 官方维护),它封装了分布式锁实现:

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;

public class ZkDistributedLockDemo {
    public static void main(String[] args) {
        // 1. 配置 Curator 客户端(连接 ZooKeeper 集群)
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("127.0.0.1:2181") // ZooKeeper 地址(集群用逗号分隔)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3)) // 重试策略
                .build();
        client.start();

        // 2. 创建分布式锁(锁的路径)
        InterProcessMutex lock = new InterProcessMutex(client, "/distributed/lock/stock_1001");

        try {
            // 3. 尝试获取锁:等待5秒,超时未获取则失败
            boolean locked = lock.acquire(5, TimeUnit.SECONDS);
            if (locked) {
                // 4. 执行业务逻辑
                System.out.println("获取锁成功,执行核心业务...");
            } else {
                System.out.println("获取锁失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 5. 释放锁
            if (lock.isAcquiredInThisProcess()) {
                try {
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 6. 关闭客户端
            client.close();
        }
    }
}
3. 优缺点
  • 优点:天然高可用(ZooKeeper 集群)、支持公平锁、无锁丢失问题(临时节点依赖会话);
  • 缺点:性能低于 Redis(ZooKeeper 写操作需集群同步,QPS 千级)、部署复杂。

方案 3:基于数据库的分布式锁

利用数据库的 唯一索引 或 悲观锁 / 乐观锁 实现,适合对性能要求不高、已有数据库的场景。

1. 实现方式 1:唯一索引(互斥锁)
  • 建表:创建锁表,lock_key 设为唯一索引;
  • 获取锁:插入一条 lock_key = 目标锁标识 的记录(唯一索引保证仅一条成功);
  • 释放锁:删除该记录;
  • 超时释放:给表加 expire_time 字段,定时任务删除过期记录。

表结构示例

CREATE TABLE distributed_lock (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    lock_key VARCHAR(64) NOT NULL COMMENT '锁的唯一标识',
    lock_value VARCHAR(128) NOT NULL COMMENT '客户端标识(UUID+线程ID)',
    expire_time DATETIME NOT NULL COMMENT '过期时间',
    UNIQUE KEY uk_lock_key (lock_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Java 代码示例(简化)

import org.springframework.jdbc.core.JdbcTemplate;
import java.util.UUID;
import java.util.Date;

public class DbDistributedLock {
    private final JdbcTemplate jdbcTemplate;
    private final String lockKey;
    private final String lockValue;
    private final long expireSeconds; // 过期时间(秒)

    public DbDistributedLock(JdbcTemplate jdbcTemplate, String lockKey, long expireSeconds) {
        this.jdbcTemplate = jdbcTemplate;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
        this.expireSeconds = expireSeconds;
    }

    // 获取锁
    public boolean tryLock() {
        Date expireTime = new Date(System.currentTimeMillis() + expireSeconds * 1000);
        try {
            String sql = "INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES (?, ?, ?)";
            jdbcTemplate.update(sql, lockKey, lockValue, expireTime);
            return true; // 插入成功=获取锁
        } catch (Exception e) {
            return false; // 唯一索引冲突=锁被占用
        }
    }

    // 释放锁
    public boolean unlock() {
        String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?";
        int rows = jdbcTemplate.update(sql, lockKey, lockValue);
        return rows > 0;
    }
}
2. 实现方式 2:悲观锁(SELECT FOR UPDATE)
  • 利用数据库的行级锁,通过 SELECT ... FOR UPDATE 锁定目标记录,其他事务需等待锁释放。
  • 缺点:性能差,容易导致死锁,不推荐高并发场景。
3. 优缺点
  • 优点:实现简单、无需额外中间件;
  • 缺点:性能低(数据库 IO 瓶颈)、容易产生死锁、不支持高并发。

方案 4:基于 Etcd 的分布式锁

Etcd 是云原生场景下的分布式键值存储(类似 ZooKeeper),基于 Raft 协议 保证一致性,支持原子操作和租约机制。

核心原理
  • 获取锁:用 PUT /lock/key value --lease=leaseId 原子操作(租约保证超时释放),同时监听前序节点;
  • 释放锁:删除键或租约过期;
  • 高可用:Raft 协议保证集群数据一致性。
Java 客户端:jetcd
import io.etcd.jetcd.*;
import io.etcd.jetcd.lock.LockResponse;
import io.etcd.jetcd.lock.UnlockResponse;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class EtcdDistributedLockDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建 Etcd 客户端
        Client client = Client.builder().endpoints("http://127.0.0.1:2379").build();
        Lock lockClient = client.getLockClient();

        // 2. 锁的标识和租约(租约10秒,超时自动释放)
        ByteSequence lockKey = ByteSequence.from("stock_lock_1001", StandardCharsets.UTF_8);
        long leaseId = client.getLeaseClient().grant(10).get().getID();

        try {
            // 3. 获取锁(异步转同步)
            CompletableFuture<LockResponse> lockFuture = lockClient.lock(lockKey, leaseId);
            LockResponse lockResponse = lockFuture.get(5, TimeUnit.SECONDS); // 等待5秒

            // 4. 执行业务逻辑
            System.out.println("获取锁成功,锁标识:" + lockResponse.getKey());
        } catch (Exception e) {
            System.out.println("获取锁失败:" + e.getMessage());
        } finally {
            // 5. 释放锁
            CompletableFuture<UnlockResponse> unlockFuture = lockClient.unlock(lockKey);
            unlockFuture.get();
            // 6. 关闭客户端
            client.close();
        }
    }
}
优缺点
  • 优点:强一致性(Raft 协议)、云原生场景适配好、支持租约机制;
  • 缺点:部署复杂、Java 生态不如 Redis/ZooKeeper 成熟。

三、方案对比与选型建议

方案性能高可用实现复杂度适用场景
Redis(Redisson)高(万级 QPS)支持(集群)低(框架封装)大多数分布式场景(秒杀、库存)
ZooKeeper(Curator)中(千级 QPS)支持(集群)中(框架封装)对公平锁、一致性要求高的场景
数据库低(百级 QPS)支持(主从)低(简单 SQL)低并发、已有数据库的场景
Etcd中(千级 QPS)支持(集群)高(部署复杂)云原生、K8s 生态场景

选型优先级

  1. 优先选 Redis(Redisson):平衡性能、易用性和高可用,覆盖绝大多数场景;
  2. 需公平锁 / 强一致性选 ZooKeeper(Curator):例如分布式任务调度;
  3. 低并发 / 无中间件选数据库:例如内部系统的简单并发控制;
  4. 云原生场景选 Etcd:例如 K8s 集群内的服务协调。

四、注意事项

  1. 锁的粒度:锁的 key 要精准(例如 “stock_lock_1001” 而非 “stock_lock”),避免大面积并发阻塞;
  2. 超时设置:锁超时时间需大于业务执行时间,避免业务未完成锁已释放;
  3. 锁续期:长耗时业务需手动续期(Redisson 自动支持),防止锁超时;
  4. 异常处理:必须在 finally 中释放锁,避免异常导致锁泄漏;
  5. 集群部署:Redis/ZooKeeper/Etcd 必须集群部署,避免单点故障。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

canjun_wen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值