分布式锁的工作原理

  • JVM锁的局限性:synchronized锁只能保证单个JVM内部的线程互斥,无法实现集群模式下多JVM进程间的互斥
  • 核心解决思路:需要让多个JVM进程都能访问同一个外部锁监视器
  • 工作流程:
    • 线程1从外部锁监视器获取锁成功并记录持有者信息
    • 其他线程(无论是否同JVM)获取锁失败进入等待
    • 线程1执行业务(查询订单→判断存在→插入新订单)
    • 线程1释放锁后,等待线程获取锁并执行业务
    • 由于订单已存在,后续线程查询后会直接报错

分布式锁的概念

  • 基本定义:满足分布式系统或集群模式下多进程可见且互斥的锁
  • 五大核心特性:
    • 多进程可见:所有JVM都能访问同一个锁资源(如Redis、MySQL等)
    • 互斥性:同一时刻只有一个线程能获取锁
    • 高可用:锁服务应保持高可用性
    • 高性能:获取/释放锁的操作要高效
    • 安全性:异常情况下能自动释放锁,避免死锁
  • 扩展特性(非必需):
    • 可重入性
    • 阻塞/非阻塞
    • 公平/非公平锁

分布式锁的实现

MySQLRedisZookeeper
互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放
  • MySQL实现方案:
    • 互斥原理:利用数据库事务的排他锁
    • 优点:自动释放锁(连接断开时事务回滚)
    • 缺点:性能一般,依赖数据库可用性
  • Redis实现方案:
    • 互斥原理:使用SETNX命令
    • 优点:高性能,支持集群模式
    • 安全缺陷:需配合过期时间避免死锁(expire)
    • 挑战:过期时间设置需权衡业务执行时间
  • Zookeeper实现方案:
    • 互斥原理:利用节点唯一性和有序性
    • 优点:自动释放(临时节点),高可用
    • 缺点:性能低于Redis(强一致性导致)
  • 方案选择建议:
    • 优先考虑Redis方案(性能优势)
    • 对安全性要求极高时考虑Zookeeper
    • MySQL方案适合已有数据库依赖的场景

基于Redis实现分布式锁

image-9

获取锁

  • 互斥机制: 利用Redis的SETNX命令实现,当多个线程同时执行时,只有第一个执行成功的线程能获取锁
  • 业务隔离: 不同业务可以使用不同的key作为锁标识,例如”lock:order”和”lock:payment”
  • 示例操作: SETNX lock thread1,成功返回1,失败返回0

释放锁

  • 手动释放: 使用DEL命令删除对应的key,如DEL lock
  • 释放效果: 删除后其他线程可以再次获取该锁
  • 示例流程:
    • 线程1执行SETNX lock thread1获取锁
    • 线程2尝试获取相同锁会失败
    • 线程1执行DEL lock释放锁
    • 线程2可以重新尝试获取锁

超时释放

  • 问题场景: 服务获取锁后宕机,导致锁无法释放形成死锁
  • 解决方案: 使用EXPIRE命令设置锁的过期时间,如EXPIRE lock 10
  • 时间设置原则: 应比实际业务执行时间长,通常设置为10秒左右
  • 自动释放: 过期后Redis会自动删除key,其他线程可重新获取

原子性操作

  • 问题场景: SETNX和EXPIRE两个命令之间服务宕机,仍会导致死锁
  • 解决方案: 使用Redis的SET命令同时实现SETNX和EXPIRE功能
  • 原子命令: SET lock thread1 EX 10 NX
  • 参数说明:
    • EX: 设置过期时间(秒)
    • NX: 仅当key不存在时设置
  • 执行效果: 要么全部成功,要么全部失败,保证原子性

获取锁失败的处理

  • 返回结果: 成功返回”OK”,失败返回nil
  • 非阻塞机制: 获取失败立即返回false,不进行重试
  • 优点: 避免CPU资源浪费,实现简单
  • 业务决策: 由调用方决定失败后的处理策略(重试/放弃)
  • 适用场景: 适用于对实时性要求不高或可降级的业务场景

代码实现

public class SimpleRedisLock implements ILock {  
  
    private String name;  
    private StringRedisTemplate stringRedisTemplate;  
    private static final String KEY_PREFIX = "lock:";  
  
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {  
        this.name = name;  
        this.stringRedisTemplate = stringRedisTemplate;  
    }  
  
    /**  
     * 尝试获取锁  
     *  
     * @param timeoutSec 锁持有的超时时间,过期后自动释放  
     * @return true代表获取锁成功,false代表获取锁失败  
     */  
    @Override  
    public boolean tryLock(Long timeoutSec) {  
        String key = KEY_PREFIX + name;  
        // 获取线程标识  
        String value = Thread.currentThread().getId() + "";  
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSec, TimeUnit.SECONDS);  
        // 排除空指针的风险  
        return Boolean.TRUE.equals(flag);  
    }  
  
    /**  
     * 释放锁  
     */  
    @Override  
    public void unLock() {  
        String key = KEY_PREFIX + name;  
        stringRedisTemplate.delete(key);  
    }  
}

然后就可以更新核心代码中的代码来解决 集群下的线程安全并发问题

public Result seckillVoucher(Long voucherId) {  
    // 查询优惠券  
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);  
    if (voucher == null) {  
        return Result.fail("优惠券不存在!");  
    }  
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {  
        return Result.fail("优惠券尚未开始!");  
    }  
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {  
        return Result.fail("优惠券已过期!");  
    }  
    // 判断库存是否充足  
    if (voucher.getStock() < 1) {  
        return Result.fail("库存不足!");  
    }  
    Long userId = UserHolder.getUser().getId();  
    // 创建锁对象  
    SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);  
    // 获取锁  
    boolean isLock = lock.tryLock(10L);  
    if (!isLock) {  
        return Result.fail("不允许重复下单!");  
    }  
    try {  
        // 获取代理对象(事务)  
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();  
        return proxy.createVoucherOrder(voucherId);  
    } catch (Exception e) {  
        throw new RuntimeException(e);  
    } finally {  
        lock.unLock();  
    }  
}

Redis分布式锁误删问题

极端情况

  • 业务阻塞导致锁超时:当线程1业务执行时间超过锁的超时时间(10秒),锁会被自动释放
  • 误删他人锁:线程1恢复后,会误删此时持有锁的线程2的锁
  • 并发安全问题:线程3趁机获取锁,导致线程2和线程3同时持有锁,出现并发问题

image-10

问题根源

  • 直接删除机制:释放锁时未验证锁的归属权
  • 无状态判断:线程无法识别当前锁是否属于自己

解决方案

  • 获取锁时存储标识:将线程唯一标识存入锁
  • 释放锁前验证:
    • 获取锁中存储的线程标识
    • 与当前线程标识比较
    • 只有匹配时才执行删除操作

image-11

Warning

但这里依然存在不足,因为线程1阻塞恢复正常后会继续执行,此时线程2获取到锁也在执行,也就是说同时有两个线程在执行。参考功能介绍中超时释放的描述。解决方案建议如下:

  1. 增加锁的超时时间 :确保业务执行时间不会超过锁的超时时间
  2. 使用看门狗机制 :定期续期锁的超时时间,Redisson分布式锁源码分析中有所体现
  3. 业务幂等性设计 :即使出现并发,也要保证业务结果的正确性
  4. 数据库层面的约束 :利用数据库的唯一约束来防止重复数据

image-12

Redis分布式锁改进实现

需求分析

  • 改进需求:
    • 获取锁时存入线程标识(使用UUID)
    • 释放锁时先获取锁中的线程标识进行比对
      • 一致则释放锁
      • 不一致则不释放锁
  • 线程标识设计:
    • 避免直接使用线程ID(JVM内部递增数字,集群环境下可能冲突)
    • 采用UUID+线程ID组合方式:
      • UUID区分不同JVM实例
      • 线程ID区分同一JVM内的不同线程
      • 确保不同线程标识唯一,相同线程标识一致

Tip

这里UUID来自cn.hutool.core.lang.UUID;

public class SimpleRedisLock implements ILock {  
  
    private String name;  
    private StringRedisTemplate stringRedisTemplate;  
    private static final String KEY_PREFIX = "lock:";  
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";  
  
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {  
        this.name = name;  
        this.stringRedisTemplate = stringRedisTemplate;  
    }  
  
    /**  
     * 尝试获取锁  
     *  
     * @param timeoutSec 锁持有的超时时间,过期后自动释放  
     * @return true代表获取锁成功,false代表获取锁失败  
     */  
    @Override  
    public boolean tryLock(Long timeoutSec) {  
        String key = KEY_PREFIX + name;  
        // 获取线程标识  
        String value = ID_PREFIX + Thread.currentThread().getId() + "";  
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSec, TimeUnit.SECONDS);  
        // 排除空指针的风险  
        return Boolean.TRUE.equals(flag);  
    }  
  
    /**  
     * 释放锁  
     */  
    @Override  
    public void unLock() {  
        // 获取线程标识  
        String value = ID_PREFIX + Thread.currentThread().getId() + "";  
        // 获取锁中的值  
        String key = KEY_PREFIX + name;  
        String currentValue = stringRedisTemplate.opsForValue().get(key);  
        // 判断是否一致  
        if (value.equals(currentValue)) {  
            stringRedisTemplate.delete(key);  
        }  
  
    }  
}

分布式锁的原子性问题

极端场景问题

  • 典型场景:
    • 线程1获取锁成功并完成业务
    • 判断锁标识通过后,在释放前发生JVM垃圾回收(FGC)阻塞
    • 阻塞期间锁超时释放,线程2成功获取锁
    • 线程1恢复后继续执行释放操作,误删线程2的锁
  • 根本原因:判断标识和释放锁两个操作非原子性,中间可能被阻塞
  • 关键发现:即使添加了标识验证,操作间隔仍可能导致并发问题

image-13

解决方案

  • 核心要求:必须保证判断标识和释放锁的原子性
  • 技术本质:需要将两个操作合并为一个不可分割的执行单元
  • 待解决问题:如何在分布式环境下实现多操作的原子性保证

Lua脚本解决多条命令原子性问题

Lua脚本介绍

  • 功能特点:在一个脚本中编写多条Redis命令,确保多条命令执行的原子性
  • 语言特性:Lua是一种轻量级脚本语言,语法简单易学,适合嵌入应用程序中。
  • 学习资源:基本语法可参考Lua教程网站

image-14

image-15

业务流程

  • 获取锁中的线程标识
  • 将获取的标识与当前线程标识比较
  • 若一致则释放锁,不一致则不执行任何操作
-- 锁的jey
local key = KEYS[1]
-- 当前线程标识
local threadId = ARGV[1]
 
-- 获取锁的线程标识
local id = redis.call('get', key)
-- 比较线程标识与锁中是否一致
if id == threadId then
    -- 释放锁
    return redis.call('del', key)
end   
return 0

代码实现

  • 脚本位置:存放在resources/unlock.lua文件中,便于维护修改
  • 核心方法:stringRedisTemplate.execute()
  • 参数配置:
  • 脚本对象:DefaultRedisScript<Long>类型
    • key列表:Collections.singletonList()包装单个key
    • 参数数组:线程标识作为可变参数传入

性能优化

使用静态代码块预加载脚本,避免每次释放锁都读取文件

public class SimpleRedisLock implements ILock {  
  
    private String name;  
    private StringRedisTemplate stringRedisTemplate;  
    private static final String KEY_PREFIX = "lock:";  
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";  
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;  
  
    static {  
        UNLOCK_SCRIPT = new DefaultRedisScript<>();  
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));  
        UNLOCK_SCRIPT.setResultType(Long.class);  
    }  
  
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {  
        this.name = name;  
        this.stringRedisTemplate = stringRedisTemplate;  
    }  
  
    /**  
     * 尝试获取锁  
     *  
     * @param timeoutSec 锁持有的超时时间,过期后自动释放  
     * @return true代表获取锁成功,false代表获取锁失败  
     */  
    @Override  
    public boolean tryLock(Long timeoutSec) {  
        String key = KEY_PREFIX + name;  
        // 获取线程标识  
        String value = ID_PREFIX + Thread.currentThread().getId() + "";  
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSec, TimeUnit.SECONDS);  
        // 排除空指针的风险  
        return Boolean.TRUE.equals(flag);  
    }  
  
    /**  
     * 释放锁  
     */  
    @Override  
    public void unLock() {  
        // 调用lua脚本  
        stringRedisTemplate.execute(UNLOCK_SCRIPT,  
                Collections.singletonList(KEY_PREFIX + name),  
                ID_PREFIX + Thread.currentThread().getId()  
        );  
    } 
}

Redisson

基于setnx实现的分布式锁存在的问题

  • 不可重入问题:同一个线程无法多次获取同一把锁,会导致死锁。例如方法A调用方法B时,若方法A已获取锁,方法B再次尝试获取同一把锁就会阻塞等待。
  • 不可重试问题:当前实现是非阻塞式的,获取锁失败会立即返回false,缺乏重试机制。实际业务中往往需要等待或重试获取锁。
  • 超时释放问题:虽然通过Lua脚本解决了误删问题,但业务执行时间超过锁超时时间仍会导致并发风险。设置时间过长又会影响故障恢复效率。
  • 主从一致性问题:主从同步延迟可能导致主节点宕机时,从节点未同步锁信息,其他线程可能获取到同一把锁。

功能介绍

Redisson Reference Guide

  • 核心定位:基于Redis实现的Java驻内存数据网格(In-Memory Data Grid),提供分布式Java对象和服务集合。
  • 功能特点:不仅包含分布式锁,还提供分布式集合、对象、服务等完整解决方案。
  • 架构优势:支持独立节点、主从、哨兵、集群等多种部署模式,兼容AWS、Azure等云服务。

快速入门

  • 锁特性:
    • 可重入性:支持同一线程多次获取同一把锁
    • 自动释放:防止死锁机制
  • tryLock参数:
    • 等待时间:最大等待获取锁时间(期间会重试)
    • 释放时间:锁自动释放时间(默认30秒)
    • 时间单位:灵活指定时间单位
  • 使用规范:
    • 获取锁:boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS)
    • 业务处理:在try块中执行业务代码
    • 释放锁:必须在finally块中调用lock.unlock()

添加依赖

<dependency>  
    <groupId>org.redisson</groupId>  
    <artifactId>redisson</artifactId>  
    <version>3.13.6</version>  
</dependency>

配置类

@Configuration  
public class RedissonConfig {  
    @Bean  
    public RedissonClient redissonClient() {  
        Config config = new Config();  
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");  
        return Redisson.create(config);  
    }  
}

修改秒杀业务代码

@Resource  
private RedissonClient redissonClient;
 
public Result seckillVoucher(Long voucherId) {  
    // 查询优惠券  
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);  
    if (voucher == null) {  
        return Result.fail("优惠券不存在!");  
    }  
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {  
        return Result.fail("优惠券尚未开始!");  
    }  
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {  
        return Result.fail("优惠券已过期!");  
    }  
    // 判断库存是否充足  
    if (voucher.getStock() < 1) {  
        return Result.fail("库存不足!");  
    }  
    Long userId = UserHolder.getUser().getId();  
    // 创建锁对象  
    RLock lock = redissonClient.getLock("lock:order:" + userId);  
    // 获取锁  
    boolean isLock = lock.tryLock();  
    if (!isLock) {  
        return Result.fail("不允许重复下单!");  
    }  
    try {  
        // 获取代理对象(事务)  
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();  
        return proxy.createVoucherOrder(voucherId);  
    } catch (Exception e) {  
        throw new RuntimeException(e);  
    } finally {  
        lock.unlock();  
    }  
}

可重入锁原理

获取锁的流程

  • 基础实现:采用Redis的string数据类型,通过SET命令加NX(互斥)、EX(超时)参数实现
  • 线程标识:获取锁时存入线程标识,释放锁时验证标识避免误删
  • 缺陷分析:无法实现可重入,因为NX参数导致同一线程二次获取必然失败

  • 案例演示:method1获取锁后调用method2,同一线程内二次获取锁失败
  • 执行流程:
    • method1执行SET lock thread1 NX EX 10成功
    • method2执行相同SET命令因NX参数失败
    • 证明string结构无法实现重入

原理分析

image-16

  • JDK实现参考:ReentrantLock通过state计数器记录重入次数

    • 获取锁:同一线程重入时state++
    • 释放锁:每次释放state—,归零时才真正释放
  • Redis改造方案:

    • 数据结构:改用hash结构,field存线程ID,value存重入次数
    • 获取锁流程:
      • 判断key是否存在(EXISTS命令)
      • 不存在时:HSET key threadID 1 + 设置过期时间
      • 存在时:验证线程ID,相同则HINCRBY + 重置过期时间
    • 释放锁流程:
      • 验证线程ID
      • HINCRBY -1
      • 判断值是否为0,是则DEL键
  • 原子性保障:必须使用Lua脚本保证多步骤操作的原子性

  • 关键改进点:

    • 过期时间重置:每次重入操作都需要重置有效期
    • 释放时机:只有最外层释放时才实际删除key
    • 错误处理:非持有线程尝试释放应抛出异常

获取锁的lua

local key = KEYS[1]; -- 锁的key  
local threadId = ARGV[1]; -- 线程唯一标识  
local releaseTime = ARGV[2]; -- 锁的自动释放时间  
-- 判断是否存在  
if(redis.call('exists', key) == 0) then
    -- 不存在, 获取锁    
    redis.call('hset', key, threadId, '1');
    -- 设置有效期    
    redis.call('expire', key, releaseTime);    
    return 1; -- 返回结果  
end;  
-- 锁已经存在,判断threadId是否是自己  
if(redis.call('hexists', key, threadId) == 1) then
    -- 不存在, 获取锁,重入次数+1    
    redis.call('hincrby', key, threadId, '1');
    -- 设置有效期
    redis.call('expire', key, releaseTime);    
    return 1; -- 返回结果  
end;  
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的lua

local key = KEYS[1]; -- 锁的key  
local threadId = ARGV[1]; -- 线程唯一标识  
local releaseTime = ARGV[2]; -- 锁的自动释放时间  
-- 判断当前锁是否还是被自己持有  
if (redis.call('HEXISTS', key, threadId) == 0) then  
    return nil; -- 如果已经不是自己,则直接返回  
end;  
-- 是自己的锁,则重入次数-1  
local count = redis.call('HINCRBY', key, threadId, -1);  
-- 判断是否重入次数是否已经为0  
if (count > 0) then    
-- 大于0说明不能释放锁,重置有效期然后返回    
	redis.call('EXPIRE', key, releaseTime);    
	return nil;  
else  -- 等于0说明可以释放锁,直接删除    
	redis.call('DEL', key);    
	return nil;  
end;

可重入问题解决方案

  • 哈希结构实现: 采用哈希结构替代原始的String结构,能够同时保存线程标识和重入次数两个关键信息。
  • 重入判断逻辑:
    • 获取锁时先判断锁是否存在
    • 若不存在则直接获取
    • 若存在则检查线程标识是否为当前线程
  • 重入计数机制:
    • 当确认是当前线程时,将重入次数加一
    • 释放锁时每次将重入次数减一
    • 当重入次数减至零时才真正释放锁
  • 与JDK对比: 该实现原理与JDK中的ReentrantLock基本一致。

不可重试问题

  • 问题表现:当前自定义分布式锁在获取锁失败时会立即结束,缺乏重试机制
  • Redisson解决方案:通过tryLock(long waitTime, TimeUnit unit)方法实现
    • 参数说明:第一个参数waitTime表示最大等待时长,在该时间段内会持续尝试获取锁
    • 工作机制:首次获取失败不会立即返回,而是在等待时间内不断重试,超时后才返回false
    • 使用示例:lock.tryLock(1L, TimeUnit.SECONDS)表示最长等待1秒

底层原理可参考Redisson分布式锁源码分析

2)锁失败重试问题解决方案

  • 信号量机制: 基于Redis的发布订阅(pub/sub)功能实现
  • 等待唤醒流程:
    • 首次获取锁失败后不立即返回失败
    • 转为等待释放锁的消息通知
    • 通过订阅释放锁的频道进行监听
  • 消息触发机制:
    • 获取锁成功的线程在释放时会发布消息
    • 等待线程收到消息后重新尝试获取锁
  • 重试限制:
    • 设置最大等待时间
    • 超时后停止重试
  • 性能优势: 采用等待唤醒方案,不会过多占用CPU资源

超时释放问题

  • 问题表现:锁超时自动释放时若业务尚未完成,会导致线程安全问题
    • 产生原因:业务执行时间过长或被阻塞,超过预设的锁有效期
    • 风险场景:其他线程在业务未完成时获取到锁,造成数据不一致
  • Redisson参数:leaseTime参数控制锁自动释放时间
    • 注意事项:该参数可不传,系统会提供默认值

3)锁超时释放问题解决方案

  • 看门狗机制: 通过定时任务自动续期锁的超时时间
  • 实现细节:
    • 获取锁成功后启动定时任务
    • 定期重置锁的过期时间(expire)
    • 使锁的生存时间得到延续
  • 效果描述: 该机制使锁能够”满血复活”,避免业务未完成时锁自动释放的问题

主从一致性问题

  • 问题本质:分布式环境下保证数据一致性的挑战
  • 研究方法:需要通过跟踪Redisson源码分析其实现机制
    • 关键方法:重点关注tryLock方法的不同重载形式
    • 参数组合:最简实现只需传入等待时间和时间单位两个参数
  • 参数选择建议:
    • 必须指定waitTime以实现重试机制
    • leaseTime可根据业务场景选择是否显式设置
    • 时间单位需与业务时间尺度匹配(秒/毫秒等)

产生原因

  • 单节点风险:采用单节点Redis时,若该节点发生故障,所有依赖Redis的业务(包括分布式锁)都会出现问题
  • 核心业务需求:在核心业务场景中,这种单点故障的情况是不可接受的,因此需要提高Redis的可用性

主从模式介绍

  • 角色划分:由多台Redis组成,其中一台作为主节点(master),其余作为从节点(slave)
  • 职责分离:
    • 主节点:处理所有写操作(增删改)
    • 从节点:只处理读操作
  • 数据同步机制:主节点会持续将自己的数据同步给从节点,确保主从数据一致性

延时问题

  • 同步延迟:由于主从节点不在同一台机器,数据同步存在一定延迟
  • 故障场景示例:
    • 线程A执行set lock thread1 ax EX命令获取锁
    • 主节点保存锁标识后,在同步完成前发生故障
    • 哨兵选举新主节点时,因同步未完成导致锁丢失
    • 其他线程可以获取相同锁,导致并发安全问题

解决方法

  • 核心思路:取消主从关系,所有节点独立运行
  • 获取锁机制:
    • 必须依次向多个Redis节点获取锁
    • 所有节点都保存锁标识才算获取成功
  • 优势分析:
    • 消除主从同步延迟问题
    • 提高可用性:部分节点宕机不影响整体功能
    • 可扩展性:节点数量越多,可用性越高

Multi Lock方案介绍

参考Redisson使用连锁

特点总结:

  • 原子性保证:必须获取所有锁才算成功,否则释放已获取的锁
  • 重试机制:在waitTime内会不断重试获取失败的锁
  • 有效期同步:当指定leaseTime时,会统一所有锁的有效期
  • 失败限制:通过failedLocksLimit控制允许失败的锁数量