深入浅出:分布式锁的原理与实战全解析

在这里插入图片描述

深入浅出:分布式锁的原理与实战全解析

在分布式系统中,多个节点同时访问共享资源时,如何保证数据一致性?分布式锁就是你的答案。


目录


一、什么是分布式锁?

1.1 从单机锁到分布式锁

在这里插入图片描述

在单机应用中,我们使用 synchronizedReentrantLock 等来保证线程安全。但在分布式系统中,多个进程运行在不同的机器上,这些单机锁就失效了。

场景单机锁分布式锁
单机多线程✅ 有效❌ 不需要
多机单进程❌ 无效✅ 需要
多机多进程❌ 无效✅ 需要

1.2 分布式锁的定义

分布式锁是一种在分布式系统中协调多个节点对共享资源访问的机制。它保证在同一时刻,只有一个节点能持有锁并访问资源。

在这里插入图片描述

1.3 分布式锁的三个核心要素

要素说明要求
互斥性同一时刻只有一个客户端持有锁严格保证
安全性锁只能被持有者释放不能被其他客户端解锁
活性持有锁的客户端宕机后锁能自动释放避免死锁

二、为什么需要分布式锁?

2.1 经典场景:库存扣减

在这里插入图片描述

假设有一个商品库存为 100,两个用户同时下单:

用户A: 查询库存 = 100
用户B: 查询库存 = 100
用户A: 扣减库存 → 99
用户B: 扣减库存 → 99  (超卖!)

使用分布式锁后:

用户A: 获取锁 → 查询库存 = 100 → 扣减 → 99 → 释放锁
用户B: 等待锁...
用户B: 获取锁 → 查询库存 = 99 → 扣减 → 98 → 释放锁

2.2 常见应用场景

在这里插入图片描述

场景说明风险
库存扣减防止超卖数据不一致
订单创建防止重复下单重复扣款
定时任务防止多节点重复执行资源浪费
缓存更新防止缓存击穿数据库压力
Leader 选举选出主节点脑裂问题

2.3 不使用分布式锁的后果

// ❌ 危险代码:没有分布式锁
public void deductStock(Long productId, int quantity) {
    Stock stock = stockMapper.selectById(productId);
    if (stock.getQuantity() >= quantity) {
        stock.setQuantity(stock.getQuantity() - quantity);
        stockMapper.updateById(stock);
    }
}

问题:并发时可能出现超卖!


三、分布式锁的实现方案

3.1 方案对比

在这里插入图片描述

方案性能可靠性复杂度适用场景
Redis⭐⭐⭐⭐⭐⭐⭐⭐高并发、低延迟
Zookeeper⭐⭐⭐⭐⭐⭐⭐⭐强一致性要求
数据库⭐⭐⭐⭐⭐简单场景
etcd⭐⭐⭐⭐⭐⭐⭐⭐⭐Kubernetes 生态

3.2 基于数据库的分布式锁

最简单的实现方式,利用数据库的唯一索引:

CREATE TABLE distributed_lock (
    lock_key VARCHAR(255) PRIMARY KEY,
    lock_value VARCHAR(255),
    expire_time DATETIME,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

加锁

INSERT INTO distributed_lock (lock_key, lock_value, expire_time)
VALUES ('order_lock', 'uuid_xxx', NOW() + INTERVAL 30 SECOND)
ON DUPLICATE KEY UPDATE
    lock_value = IF(expire_time < NOW(), VALUES(lock_value), lock_value),
    expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time);

解锁

DELETE FROM distributed_lock
WHERE lock_key = 'order_lock' AND lock_value = 'uuid_xxx';

缺点:性能差、依赖数据库可用性。


四、Redis 分布式锁实战

4.1 基础实现:SETNX + EXPIRE

在这里插入图片描述

public class RedisLock {
    private StringRedisTemplate redisTemplate;
    private String lockKey = "distributed_lock:";
    private String lockValue;  // UUID,保证唯一性
    private long expireTime = 30000;  // 过期时间 30 秒

    public boolean tryLock(String businessKey) {
        String key = lockKey + businessKey;
        lockValue = UUID.randomUUID().toString();

        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, lockValue, expireTime, TimeUnit.MILLISECONDS);

        return Boolean.TRUE.equals(result);
    }

    public void unlock(String businessKey) {
        String key = lockKey + businessKey;
        // 只删除自己的锁
        if (lockValue.equals(redisTemplate.opsForValue().get(key))) {
            redisTemplate.delete(key);
        }
    }
}

问题setIfAbsentexpire 不是原子操作!

4.2 改进:原子操作

// ✅ 正确:使用 Lua 脚本保证原子性
public boolean tryLock(String businessKey) {
    String key = lockKey + businessKey;
    lockValue = UUID.randomUUID().toString();

    // SET key value NX PX milliseconds
    String script = 
        "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then " +
        "  return 1 " +
        "else " +
        "  return 0 " +
        "end";

    Long result = redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        List.of(key),
        lockValue,
        String.valueOf(expireTime)
    );

    return result != null && result == 1;
}

4.3 安全解锁:Lua 脚本

在这里插入图片描述

// ✅ 正确:使用 Lua 脚本保证原子性解锁
public void unlock(String businessKey) {
    String key = lockKey + businessKey;

    String script = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "  return redis.call('del', KEYS[1]) " +
        "else " +
        "  return 0 " +
        "end";

    redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        List.of(key),
        lockValue
    );
}

4.4 完整实现:可重入锁

public class RedisReentrantLock {
    private StringRedisTemplate redisTemplate;
    private String lockKey = "distributed_lock:";
    private String lockValue;
    private long expireTime = 30000;
    private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);

    public boolean tryLock(String businessKey) {
        String key = lockKey + businessKey;
        String currentLockValue = redisTemplate.opsForValue().get(key);

        // 可重入:如果已经持有锁,增加计数
        if (lockValue != null && lockValue.equals(currentLockValue)) {
            lockCount.set(lockCount.get() + 1);
            return true;
        }

        lockValue = UUID.randomUUID().toString();

        String script = 
            "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then " +
            "  return 1 " +
            "else " +
            "  return 0 " +
            "end";

        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            List.of(key),
            lockValue,
            String.valueOf(expireTime)
        );

        if (result != null && result == 1) {
            lockCount.set(1);
            return true;
        }
        return false;
    }

    public void unlock(String businessKey) {
        String key = lockKey + businessKey;

        // 减少计数
        int count = lockCount.get() - 1;
        if (count > 0) {
            lockCount.set(count);
            return;
        }

        // 计数为 0,真正释放锁
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "  return redis.call('del', KEYS[1]) " +
            "else " +
            "  return 0 " +
            "end";

        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            List.of(key),
            lockValue
        );

        lockCount.remove();
    }
}

4.5 锁续期:Watchdog 机制

在这里插入图片描述

public class RedisLockWithWatchdog {
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private ScheduledFuture<?> watchdogTask;

    public boolean tryLock(String businessKey) {
        // ... 加锁逻辑 ...

        if (locked) {
            // 启动 Watchdog,每 10 秒续期一次
            watchdogTask = scheduler.scheduleAtFixedRate(() -> {
                extendLock(businessKey);
            }, 10, 10, TimeUnit.SECONDS);
        }

        return locked;
    }

    private void extendLock(String businessKey) {
        String key = lockKey + businessKey;
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "  return redis.call('pexpire', KEYS[1], ARGV[2]) " +
            "else " +
            "  return 0 " +
            "end";

        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            List.of(key),
            lockValue,
            String.valueOf(expireTime)
        );
    }

    public void unlock(String businessKey) {
        // 停止 Watchdog
        if (watchdogTask != null) {
            watchdogTask.cancel(false);
        }

        // ... 解锁逻辑 ...
    }
}

4.6 使用 Redisson(推荐)

在这里插入图片描述

Redisson 是 Redis 官方推荐的 Java 客户端,内置了完善的分布式锁实现:

// 引入依赖
// <dependency>
//     <groupId>org.redisson</groupId>
//     <artifactId>redisson</artifactId>
//     <version>3.27.0</version>
// </dependency>

// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

// 使用分布式锁
RLock lock = redisson.getLock("myLock");

try {
    // 尝试加锁,最多等待 10 秒,自动释放 30 秒
    boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);

    if (locked) {
        // 执行业务逻辑
        doBusinessLogic();
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    // 释放锁
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

Redisson 的优势

  • ✅ 自动续期(Watchdog)
  • ✅ 可重入锁
  • ✅ 公平锁
  • ✅ 读写锁
  • ✅ 联锁(MultiLock)
  • ✅ 红锁(RedLock)

五、Zookeeper 分布式锁

5.1 Zookeeper 锁原理

在这里插入图片描述

Zookeeper 利用临时顺序节点实现分布式锁:

  1. 创建锁节点/lock/lock-
  2. 创建临时顺序节点/lock/lock-000001
  3. 获取所有子节点,判断自己是否是最小的
  4. 如果是最小的,获取锁成功
  5. 如果不是最小的,监听比自己小的节点
  6. 释放锁:删除自己的节点

5.2 Curator 实现

// 引入依赖
// <dependency>
//     <groupId>org.apache.curator</groupId>
//     <artifactId>curator-recipes</artifactId>
//     <version>5.6.0</version>
// </dependency>

// 创建客户端
CuratorFramework client = CuratorFrameworkFactory.newClient(
    "localhost:2181",
    new ExponentialBackoffRetry(1000, 3)
);
client.start();

// 创建锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/my-lock");

try {
    // 获取锁,最多等待 10 秒
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        try {
            // 执行业务逻辑
            doBusinessLogic();
        } finally {
            // 释放锁
            lock.release();
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

5.3 Zookeeper vs Redis

在这里插入图片描述

特性RedisZookeeper
性能极高中等
一致性AP(最终一致)CP(强一致)
锁释放需要 Watchdog临时节点自动释放
适用场景高并发、低延迟强一致性要求
运维成本

六、分布式锁的常见陷阱

6.1 陷阱一:锁过期但业务未完成

在这里插入图片描述

问题:锁过期时间是 30 秒,但业务执行了 40 秒。

时间线:
0s   - 获取锁
30s  - 锁自动过期
30s  - 其他客户端获取锁
40s  - 业务完成,尝试释放锁 → 释放了别人的锁!

解决方案

  1. 使用 Watchdog 自动续期
  2. 合理设置过期时间
  3. 业务完成后检查锁是否还持有

6.2 陷阱二:锁不可重入

// ❌ 问题代码:不可重入锁
public void methodA() {
    lock.lock();
    methodB();  // 死锁!
    lock.unlock();
}

public void methodB() {
    lock.lock();  // 等待 methodA 释放锁
    // ...
    lock.unlock();
}

解决方案:使用可重入锁(如 Redisson)。

6.3 陷阱三:Redis 主从切换丢锁

在这里插入图片描述

问题

  1. 客户端 A 在主节点获取锁
  2. 主节点宕机,从节点升级为主节点
  3. 从节点没有同步锁信息
  4. 客户端 B 在新主节点获取锁 → 两个客户端同时持有锁!

解决方案

  1. 使用 RedLock 算法
  2. 使用 Zookeeper(强一致性)
  3. 接受极低概率的并发风险

6.4 陷阱四:锁竞争激烈

问题:大量请求竞争同一把锁,导致性能下降。

解决方案

  1. 锁粒度细化(如按商品 ID 分锁)
  2. 使用队列排队
  3. 考虑无锁方案(如乐观锁)

七、最佳实践与选型建议

7.1 最佳实践

在这里插入图片描述

实践说明
合理设置过期时间根据业务耗时设置,留有余量
使用 UUID 作为锁值防止误删其他客户端的锁
使用 Lua 脚本保证加锁/解锁的原子性
实现 Watchdog自动续期,防止业务未完成锁过期
锁粒度细化按业务 ID 分锁,减少竞争
异常处理确保锁能被正确释放

7.2 选型建议

在这里插入图片描述

场景推荐方案理由
高并发、低延迟Redis + Redisson性能最好
强一致性要求Zookeeper + CuratorCP 系统
Kubernetes 生态etcd原生支持
简单场景数据库无需额外组件
云服务云厂商分布式锁托管服务

7.3 使用 Redisson 的完整示例

@Service
public class OrderService {

    @Autowired
    private RedissonClient redisson;

    @Autowired
    private StockMapper stockMapper;

    public Result createOrder(Long userId, Long productId, int quantity) {
        String lockKey = "order:" + productId;
        RLock lock = redisson.getLock(lockKey);

        try {
            // 尝试获取锁,最多等待 5 秒,自动释放 30 秒
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                try {
                    // 1. 查询库存
                    Stock stock = stockMapper.selectById(productId);
                    if (stock.getQuantity() < quantity) {
                        return Result.fail("库存不足");
                    }

                    // 2. 扣减库存
                    stock.setQuantity(stock.getQuantity() - quantity);
                    stockMapper.updateById(stock);

                    // 3. 创建订单
                    Order order = new Order();
                    order.setUserId(userId);
                    order.setProductId(productId);
                    order.setQuantity(quantity);
                    orderMapper.insert(order);

                    return Result.success(order);
                } finally {
                    // 释放锁
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                    }
                }
            } else {
                return Result.fail("系统繁忙,请稍后重试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Result.fail("系统异常");
        }
    }
}

八、总结

8.1 核心要点

在这里插入图片描述

  1. 分布式锁的本质:协调多节点对共享资源的访问
  2. 三大要素:互斥性、安全性、活性
  3. 主流方案:Redis(高性能)、Zookeeper(强一致)、etcd(云原生)
  4. 推荐工具:Redisson(Redis)、Curator(Zookeeper)
  5. 常见陷阱:锁过期、不可重入、主从切换、竞争激烈

8.2 最后的话

💡 核心观点:分布式锁不是万能的,但没有分布式锁是万万不能的。选择合适的方案,遵循最佳实践,才能让分布式系统既高效又可靠。

记住

  • 优先考虑无锁方案(乐观锁、队列)
  • 必须用锁时,选择成熟的框架(Redisson、Curator)
  • 合理设置锁粒度和过期时间
  • 做好异常处理和监控

标签:分布式锁, Redis, Zookeeper, Redisson, 分布式系统, Java, 并发编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值