在 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 必须集群部署,避免单点故障。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐