分布式锁 — 概念、原理与实践

分布式锁 --- 概念、原理与实践


一、为什么需要分布式锁

1.1 单机锁的局限

复制代码
单机应用(1个JVM进程):
  线程A ─┐
  线程B ─┼→ synchronized / ReentrantLock → 资源
  线程C ─┘
  ✅ 有效:所有线程在同一个JVM内,共享同一把锁
分布式应用(多个JVM进程/多台服务器):

  服务器1(JVM-1):线程A → synchronized → 本地锁1
  服务器2(JVM-2):线程B → synchronized → 本地锁2
  服务器3(JVM-3):线程C → synchronized → 本地锁3

  ❌ 无效:三台服务器各自加的是各自的本地锁,互相不可见
  → 三个线程可以同时访问共享资源(数据库同一行)

1.2 类比理解

复制代码
本地锁 = 家门锁(只能锁住自己家门,邻居有自己的锁)
分布式锁 = 小区门禁(所有住户共享同一个门禁系统,一次只能一个人通过)

1.3 核心问题

多个服务实例同时操作同一条数据时,如何保证同一时刻只有一个实例能执行关键操作?


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、核心概念

2.1 什么是分布式锁

分布式锁是一种跨进程/跨服务器的互斥机制 ,通过一个所有实例都能访问的共享存储(Redis/ZooKeeper/数据库)来协调并发访问。

2.2 关键术语

概念 说明 类比
标记"当前资源正在被使用" 公共厕所的"使用中"牌子
获取锁(加锁) 尝试标记资源被自己占用 进门后翻转牌子为"使用中"
释放锁(解锁) 标记资源不再被自己占用 出门后翻转牌子为"空闲"
锁持有者标识 记录谁持有锁(requestId) 牌子上写了"3号隔间-张三"
锁过期时间(TTL) 锁自动释放的时间 超过30分钟自动解锁(防止人晕在里面)
自旋等待 获取锁失败后反复重试 门口排队等待
超时放弃 等待超过一定时间后放弃 等了10分钟还没轮到,走人

2.3 分布式锁必须满足的条件

条件 说明
互斥性 同一时刻只有一个客户端能持有锁
防死锁 持有者崩溃后锁能自动释放(TTL 过期)
防误释放 只有锁的持有者才能释放锁(A 加的锁 B 不能释放)
高可用 锁服务本身不能是单点故障

三、与本地锁的对比

3.1 Java 本地锁(JVM 内有效)

锁类型 特点 适用范围
synchronized JVM 内置,自动释放 同一 JVM 内的线程
ReentrantLock 可重入、可中断、可超时 同一 JVM 内的线程
ReadWriteLock 读写分离,读不互斥 同一 JVM 内的线程

3.2 分布式锁(跨 JVM 有效)

实现方式 存储介质 特点
Redis 内存 性能最高,但 Redis 宕机有风险
ZooKeeper 磁盘+内存 强一致性,但性能略低
数据库 磁盘 最简单,但性能最低
Etcd 磁盘+内存 强一致性,云原生场景

四、底层原理

4.1 基于 Redis 的分布式锁

加锁原理
复制代码
客户端A → Redis:SET lock_key requestId_A NX PX 30000

NX = Only set if Not eXists(key不存在时才设置成功)
PX 30000 = 30秒后自动过期

如果返回 OK → 加锁成功
如果返回 nil → 加锁失败(锁被别人持有)
为什么用 NX?
复制代码
时刻T1:客户端A执行 SET lock NX → 成功(key不存在)
时刻T2:客户端B执行 SET lock NX → 失败(key已存在)
→ 只有A获得锁,B被拒绝
为什么需要 PX(过期时间)?
复制代码
客户端A加锁成功后崩溃了(没来得及释放锁):
  没有过期时间 → 锁永远不释放 → 死锁(其他人永远无法获取)
  有过期时间   → 30秒后Redis自动删除key → 其他人可以获取锁
为什么需要 requestId?
复制代码
场景:A加的锁,B来释放

时刻T1:A 加锁成功(lock = requestId_A)
时刻T2:A 处理业务超时,锁过期自动释放
时刻T3:B 加锁成功(lock = requestId_B)
时刻T4:A 处理完毕,执行释放锁操作

如果不校验 requestId:
  A 直接 DEL lock → 把B的锁删了!B以为自己还持有锁,实际已经没了

如果校验 requestId:
  A 释放时检查 lock 的值是否是 requestId_A → 不是 → 不释放
  → B 的锁不受影响
加锁 Lua 脚本(原子操作)
lua 复制代码
-- KEYS[1] = 锁的key
-- KEYS[2] = requestId的key
-- ARGV[1] = requestId值
-- ARGV[2] = 过期时间(毫秒)

if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], KEYS[2], ARGV[1])
    redis.call('pexpire', KEYS[1], ARGV[2])
    return 1  -- 加锁成功
else
    return 0  -- 锁已存在,加锁失败
end

为什么用 Lua 脚本?

  • exists + hset + pexpire 三个命令需要原子执行
  • 如果分开执行,两条命令之间可能被其他客户端插入操作
  • Redis 保证 Lua 脚本原子性执行
解锁 Lua 脚本(原子操作)
lua 复制代码
-- KEYS[1] = 锁的key
-- KEYS[2] = requestId的key
-- ARGV[1] = requestId值

if redis.call('hget', KEYS[1], KEYS[2]) == ARGV[1] then
    redis.call('del', KEYS[1])
    return 1  -- 解锁成功
else
    return 0  -- 不是自己的锁,拒绝释放
end

4.2 基于数据库的分布式锁

原理

利用数据库的唯一约束排他锁实现互斥。

方式1:唯一索引(INSERT 方式)
sql 复制代码
-- 建表
CREATE TABLE distributed_lock (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    lock_key VARCHAR(100) NOT NULL UNIQUE,  -- 唯一约束
    request_id VARCHAR(64) NOT NULL,
    expire_time DATETIME NOT NULL,
    create_time DATETIME NOT NULL
);

-- 加锁:INSERT(唯一约束保证互斥)
INSERT INTO distributed_lock (lock_key, request_id, expire_time, create_time)
VALUES ('order_process_123', 'uuid-xxx', NOW() + INTERVAL 30 SECOND, NOW());
-- 成功 → 获得锁
-- 失败(Duplicate entry) → 锁被占用

-- 解锁:DELETE(校验 request_id)
DELETE FROM distributed_lock 
WHERE lock_key = 'order_process_123' AND request_id = 'uuid-xxx';
方式2:SELECT FOR UPDATE(悲观锁)
sql 复制代码
-- 加锁
BEGIN;
SELECT * FROM distributed_lock WHERE lock_key = 'order_process_123' FOR UPDATE;
-- 其他事务对同一行的 SELECT FOR UPDATE 会阻塞等待

-- 执行业务逻辑...

-- 解锁
COMMIT;  -- 事务提交后锁自动释放

4.3 基于 ZooKeeper 的分布式锁

原理

利用 ZooKeeper 的临时有序节点实现。

复制代码
/locks/order_process_123/
  ├── node_0000000001 (客户端A创建)  ← 序号最小,获得锁
  ├── node_0000000002 (客户端B创建)  ← 监听前一个节点
  └── node_0000000003 (客户端C创建)  ← 监听前一个节点

客户端A处理完毕 → 删除 node_0000000001
→ 触发 node_0000000002 的 Watch 通知
→ 客户端B检查自己是否最小 → 是 → 获得锁

优点: 临时节点在客户端断连时自动删除(防死锁),Watch 机制避免轮询。


五、JPA/数据库中的锁实现

5.1 乐观锁(Optimistic Locking)

思想: 假设冲突很少发生,不加锁,提交时检查是否被修改过。

java 复制代码
@Entity
@Table(name = "stock")
public class Stock {
    @Id
    private Integer id;

    private Integer itemSkuId;
    private Integer qty;

    @Version  // JPA 乐观锁注解
    private Integer version;
}

执行流程:

复制代码
线程A:SELECT * FROM stock WHERE id=1  → version=5, qty=100
线程B:SELECT * FROM stock WHERE id=1  → version=5, qty=100

线程A:UPDATE stock SET qty=90, version=6 WHERE id=1 AND version=5  → 成功(影响1行)
线程B:UPDATE stock SET qty=80, version=6 WHERE id=1 AND version=5  → 失败(影响0行,version已变为6)
→ JPA 抛出 OptimisticLockException
→ 线程B可以重试

适用场景: 并发冲突概率低,读多写少。

5.2 悲观锁(Pessimistic Locking)

思想: 假设冲突频繁,操作前先加锁。

java 复制代码
// JPA 悲观锁查询
@Repository
public interface StockRepository extends JpaRepository<Stock, Integer> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT s FROM Stock s WHERE s.itemSkuId = :itemSkuId")
    Stock findByItemSkuIdForUpdate(@Param("itemSkuId") Integer itemSkuId);
}

执行的 SQL:

sql 复制代码
SELECT * FROM stock WHERE item_sku_id = ? FOR UPDATE;
-- 其他事务对同一行的写操作会阻塞,直到当前事务提交

适用场景: 并发冲突概率高,写多读少。

5.3 乐观锁 vs 悲观锁 vs 分布式锁

维度 乐观锁 悲观锁 分布式锁
加锁时机 更新时检查 查询时加锁 业务操作前加锁
锁的范围 数据库行级 数据库行级 跨服务/跨资源
冲突处理 抛异常/重试 阻塞等待 阻塞等待/超时放弃
性能 高(无锁) 中(行锁等待) 取决于实现(Redis最快)
死锁风险 有(多表交叉锁) 有(靠TTL解决)
适用场景 低冲突 高冲突单表 跨服务/跨资源互斥
典型用法 @Version FOR UPDATE Redis SETNX

六、各技术框架中的分布式锁实现

6.1 Redisson(最流行的 Redis 分布式锁框架)

java 复制代码
// 依赖
// <dependency>
//     <groupId>org.redisson</groupId>
//     <artifactId>redisson-spring-boot-starter</artifactId>
//     <version>3.27.0</version>
// </dependency>

@Service
public class OrderService {

    @Resource
    private RedissonClient redissonClient;

    public void processOrder(String orderId) {
        RLock lock = redissonClient.getLock("order_lock_" + orderId);
        try {
            // 等待10秒,锁自动释放时间30秒
            boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (acquired) {
                doProcess(orderId);
            } else {
                throw new RuntimeException("获取锁超时");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Redisson 的高级特性:

  • 看门狗机制(Watchdog):自动续期,防止业务未完成锁就过期
  • 可重入锁:同一线程可以重复获取同一把锁
  • 红锁(RedLock):多 Redis 节点加锁,防单点故障

6.2 Spring Integration(数据库锁)

java 复制代码
// 依赖
// <dependency>
//     <groupId>org.springframework.integration</groupId>
//     <artifactId>spring-integration-jdbc</artifactId>
// </dependency>

@Configuration
public class LockConfig {

    @Bean
    public DefaultLockRepository lockRepository(DataSource dataSource) {
        DefaultLockRepository repository = new DefaultLockRepository(dataSource);
        repository.setPrefix("APP_LOCK_");
        repository.setTimeToLive(30000);  // 30秒过期
        return repository;
    }

    @Bean
    public JdbcLockRegistry lockRegistry(LockRepository lockRepository) {
        return new JdbcLockRegistry(lockRepository);
    }
}

@Service
public class OrderService {

    @Resource
    private LockRegistry lockRegistry;

    public void processOrder(String orderId) {
        Lock lock = lockRegistry.obtain("order_" + orderId);
        try {
            if (lock.tryLock(10, TimeUnit.SECONDS)) {
                try {
                    doProcess(orderId);
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

6.3 Curator(ZooKeeper 锁)

java 复制代码
// 依赖
// <dependency>
//     <groupId>org.apache.curator</groupId>
//     <artifactId>curator-recipes</artifactId>
//     <version>5.5.0</version>
// </dependency>

@Service
public class OrderService {

    @Resource
    private CuratorFramework curatorClient;

    public void processOrder(String orderId) {
        InterProcessMutex lock = new InterProcessMutex(
            curatorClient, "/locks/order_" + orderId);
        try {
            if (lock.acquire(10, TimeUnit.SECONDS)) {
                try {
                    doProcess(orderId);
                } finally {
                    lock.release();
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("获取锁失败", e);
        }
    }
}

七、通用示例代码

7.1 基于 Redis 的分布式锁

java 复制代码
/**
 * 分布式锁实现(基于Redis + Lua脚本).
 *
 * 设计要点:
 * 1. NX 保证互斥
 * 2. PX 防死锁(自动过期)
 * 3. requestId 防误释放
 * 4. Lua 脚本保证原子性
 * 5. 自旋等待 + 超时退出
 */
public class RedisDistributedLock implements AutoCloseable {

    private static final Logger log = LoggerFactory.getLogger(RedisDistributedLock.class);

    private final StringRedisTemplate redisTemplate;
    private final String lockKey;
    private final String requestId;
    private final long leaseTimeMillis;
    private volatile boolean locked = false;

    private static final String LOCK_PREFIX = "distributed_lock:";

    // 加锁Lua脚本
    private static final String LOCK_SCRIPT =
        "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
        "  redis.call('pexpire', KEYS[1], ARGV[2]); " +
        "  return 1; " +
        "else " +
        "  return 0; " +
        "end";

    // 解锁Lua脚本
    private static final String UNLOCK_SCRIPT =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "  redis.call('del', KEYS[1]); " +
        "  return 1; " +
        "else " +
        "  return 0; " +
        "end";

    public RedisDistributedLock(StringRedisTemplate redisTemplate,
                                String businessKey, long leaseTimeMillis) {
        this.redisTemplate = redisTemplate;
        this.lockKey = LOCK_PREFIX + businessKey;
        this.requestId = UUID.randomUUID().toString().replace("-", "");
        this.leaseTimeMillis = leaseTimeMillis;
    }

    /**
     * 尝试获取锁(支持超时等待).
     *
     * @param waitTimeMillis 最大等待时间(毫秒),0表示不等待立即返回
     * @return true-获取成功,false-超时失败
     */
    public boolean tryLock(long waitTimeMillis) {
        long deadline = System.currentTimeMillis() + waitTimeMillis;

        // 第一次尝试
        if (doLock()) {
            this.locked = true;
            log.debug("获取锁成功: key={}, requestId={}", lockKey, requestId);
            return true;
        }

        // 自旋等待
        while (System.currentTimeMillis() < deadline) {
            try {
                Thread.sleep(100);  // 每100ms重试一次
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
            if (doLock()) {
                this.locked = true;
                log.debug("获取锁成功(重试): key={}, requestId={}", lockKey, requestId);
                return true;
            }
        }

        log.warn("获取锁超时: key={}", lockKey);
        return false;
    }

    /**
     * 释放锁.
     */
    public boolean unlock() {
        if (!locked) {
            return true;
        }
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(script,
            Collections.singletonList(lockKey), requestId);
        boolean success = result != null && result == 1;
        if (success) {
            this.locked = false;
            log.debug("释放锁成功: key={}, requestId={}", lockKey, requestId);
        } else {
            log.warn("释放锁失败(非持有者): key={}, requestId={}", lockKey, requestId);
        }
        return success;
    }

    @Override
    public void close() {
        unlock();
    }

    private boolean doLock() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(script,
            Collections.singletonList(lockKey),
            requestId, String.valueOf(leaseTimeMillis));
        return result != null && result == 1;
    }
}

7.2 锁工厂

java 复制代码
/**
 * 分布式锁工厂.
 * 注入后直接使用,无需关心底层 Redis 操作.
 */
@Component
public class DistributedLockFactory {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 获取分布式锁实例.
     *
     * @param businessKey 业务锁标识(如 "order_123")
     * @param leaseTime   锁过期时间
     * @param unit        时间单位
     */
    public RedisDistributedLock getLock(String businessKey, long leaseTime, TimeUnit unit) {
        return new RedisDistributedLock(stringRedisTemplate, businessKey, unit.toMillis(leaseTime));
    }

    /**
     * 获取分布式锁(默认过期10分钟).
     */
    public RedisDistributedLock getLock(String businessKey) {
        return getLock(businessKey, 10, TimeUnit.MINUTES);
    }
}

7.3 使用示例

java 复制代码
@Service
public class StockDeductService {

    @Resource
    private DistributedLockFactory lockFactory;
    @Resource
    private StockRepository stockRepository;

    /**
     * 扣减库存(分布式锁保证并发安全).
     */
    public void deductStock(Integer itemId, Integer qty) {
        String lockKey = "stock_deduct_" + itemId;

        // try-with-resources 自动释放锁
        try (RedisDistributedLock lock = lockFactory.getLock(lockKey, 30, TimeUnit.SECONDS)) {
            // 尝试获取锁,最多等待5秒
            if (!lock.tryLock(5000)) {
                throw new RuntimeException("系统繁忙,请稍后重试");
            }

            // 获取锁成功,安全执行业务
            Stock stock = stockRepository.findByItemId(itemId);
            if (stock.getQty() < qty) {
                throw new RuntimeException("库存不足");
            }
            stock.setQty(stock.getQty() - qty);
            stockRepository.save(stock);
        }
        // 离开 try 块自动调用 close() → unlock()
    }
}

八、常见问题与解决方案

问题 原因 解决方案
死锁 持有者崩溃未释放 TTL 过期自动释放
误释放 A 释放了 B 的锁 requestId 校验
锁过期但业务未完成 业务执行时间超过 TTL Watchdog 自动续期(Redisson)
Redis 主从切换丢锁 主节点宕机,从节点升主但未同步锁 RedLock(多节点加锁)
锁饥饿 某些线程一直获取不到锁 公平锁(按申请顺序排队)
重入问题 同一线程再次获取同一把锁 可重入锁(计数器+线程ID)

九、关键设计总结

设计要点 Redis 实现 数据库实现 ZooKeeper 实现
互斥性 SETNX UNIQUE KEY / FOR UPDATE 临时有序节点
防死锁 PEXPIRE TTL 定时清理过期记录 临时节点自动删除
防误释放 Lua 校验 requestId WHERE request_id = ? 只能删除自己创建的节点
原子性 Lua 脚本 数据库事务 ZK 原子性保证
等待通知 轮询(sleep + retry) 无(需轮询) Watch 机制(事件驱动)
性能 ⭐⭐⭐⭐⭐(内存操作) ⭐⭐(磁盘IO) ⭐⭐⭐(网络+磁盘)
可靠性 ⭐⭐⭐(主从可能丢锁) ⭐⭐⭐⭐(事务保证) ⭐⭐⭐⭐⭐(强一致性)
相关推荐
JHC0000002 小时前
基于 DrissionPage 与 Xvfb 的分布式无头爬虫架构
分布式·kafka
梦云莓铃 脚后跟法3 小时前
结合领域驱动设计的SOA分布式软件架构
分布式
zcmodeltech3 小时前
火电厂模型多设备时序控制系统设计:基于STM32与Modbus RTU的燃煤-燃机全流程联动方案
分布式·stm32·单片机·嵌入式硬件·物联网
ACP广源盛1392462567321 小时前
DeepSeek‑V4‑Flash 公测@ACP#昇腾 950 国产算力组合落地,国产 PCIe 交换芯片 IX9104 有哪些硬件机会
大数据·数据库·人工智能·分布式·单片机·嵌入式硬件·microsoft
玉鸯1 天前
多 Agent 并行时如何保证状态一致性?
分布式·python·agent
CodeHackerBhx1 天前
Spring Boot 4 防重复提交:从接口幂等到 Redis 分布式锁的完整实践
java·数据库·spring boot·redis·分布式
城管不管1 天前
RabbitMQ死信队列
java·分布式·ai·面试·职场和发展·rabbitmq·agent
2601_956456341 天前
从研发到交付:嗨动视觉分布式KVM坐席系统的靠谱逻辑
分布式
小O的算法实验室1 天前
AAAI-26,解空间变换引导的节能分布式异构柔性作业车间协同进化
分布式