欢迎光临
我们一直在努力

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

在这里插入图片描述

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

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


目录

  • 一、什么是分布式锁?
  • 二、为什么需要分布式锁?
  • 三、分布式锁的实现方案
  • 四、Redis 分布式锁实战
  • 五、Zookeeper 分布式锁
  • 六、分布式锁的常见陷阱
  • 七、最佳实践与选型建议
  • 八、总结

一、什么是分布式锁?

1.1 从单机锁到分布式锁

在这里插入图片描述

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

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

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);
}
}
}

问题:setIfAbsent 和 expire 不是原子操作!

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 利用临时顺序节点实现分布式锁:

  • 创建锁节点:/lock/lock-
  • 创建临时顺序节点:/lock/lock-000001
  • 获取所有子节点,判断自己是否是最小的
  • 如果是最小的,获取锁成功
  • 如果不是最小的,监听比自己小的节点
  • 释放锁:删除自己的节点
  • 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 – 业务完成,尝试释放锁 → 释放了别人的锁!

    解决方案:

  • 使用 Watchdog 自动续期
  • 合理设置过期时间
  • 业务完成后检查锁是否还持有
  • 6.2 陷阱二:锁不可重入

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

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

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

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

    在这里插入图片描述

    问题:

  • 客户端 A 在主节点获取锁
  • 主节点宕机,从节点升级为主节点
  • 从节点没有同步锁信息
  • 客户端 B 在新主节点获取锁 → 两个客户端同时持有锁!
  • 解决方案:

  • 使用 RedLock 算法
  • 使用 Zookeeper(强一致性)
  • 接受极低概率的并发风险
  • 6.4 陷阱四:锁竞争激烈

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

    解决方案:

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

  • 七、最佳实践与选型建议

    7.1 最佳实践

    在这里插入图片描述

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

    7.2 选型建议

    在这里插入图片描述

    场景推荐方案理由
    高并发、低延迟 Redis + Redisson 性能最好
    强一致性要求 Zookeeper + Curator CP 系统
    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 核心要点

    在这里插入图片描述

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

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

    记住:

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

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

    赞(0)
    未经允许不得转载:171主机测评 » 深入浅出:分布式锁的原理与实战全解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址