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

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


一、为什么需要分布式锁

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) ⭐⭐⭐(网络+磁盘)
可靠性 ⭐⭐⭐(主从可能丢锁) ⭐⭐⭐⭐(事务保证) ⭐⭐⭐⭐⭐(强一致性)
相关推荐
肠畔码农19 小时前
深入分布式事务内核:从 2PC/XA 到 Seata AT 模式的架构演进与权衡
分布式·架构
ai小陈19 小时前
PyTorch多GPU分布式训练实战:从单卡脚本迁移到DDP
服务器·人工智能·pytorch·分布式·深度学习·ai·gpu算力
伟大的大威20 小时前
三台 DGX Spark 部署 DeepSeek V4 Flash NVFP4:从零到可用教程
大数据·分布式·spark
阿里云云原生21 小时前
企业级实时数据平台建设:利用存算分离 Kafka 实现低成本、高可靠入湖
分布式·阿里云·云原生·kafka
国科安芯21 小时前
星载CANFD总线通信网络中抗辐射微控制器MCU的失效机理与容错设计研究
网络·人工智能·分布式·单片机·嵌入式硬件·架构·抗辐射加固
zcmodeltech21 小时前
污水处理设备沙盘模型控制系统设计与实现:多单元协同联动方案
网络·分布式·stm32·单片机·嵌入式硬件·交互
roman_日积跬步-终至千里1 天前
Spark 资源配置与 Shuffle 故障排查生产手册
大数据·分布式·spark
roman_日积跬步-终至千里1 天前
常驻 Spark 引擎的稳定性风险:从一次 Linkis 任务失败说起
大数据·分布式·spark
JAVA面经实录9171 天前
Kafka面试题标准答案(面试背诵版)
分布式·面试·kafka
hh9501 天前
Agent Plan x DeepSeek Harness:分布式Agent集群调度与负载均衡
运维·分布式·负载均衡·adg·agent plan·adg成都社区