Preview
Preview
Redisson 分布式锁设计与实战
返回文章列表
JavaDistributed SystemsRedisRedissonConcurrency

Redisson 分布式锁设计与实战

2026-02-09

Redisson 分布式锁设计与实战

在分布式环境下,多个服务实例同时操作共享资源(如库存扣减、订单创建、文件处理)时,如何保证数据一致性?这正是分布式锁要解决的核心问题。

本文将深入讲解基于 Redisson 的分布式锁实现原理,包括 Lua 脚本的加锁/解锁逻辑、Watchdog 自动续期机制、可重入锁设计,以及生产环境中的最佳实践与故障排查指南。


1. 分布式锁的核心设计理念

1.1 四大核心特性

互斥性(Mutual Exclusion)

  • 在任意时刻,只有一个客户端能持有锁
  • 其他客户端必须等待锁释放才能获取

防死锁(Deadlock Prevention)

  • 锁必须设置超时时间,避免因程序崩溃导致锁永远无法释放
  • 支持自动续期机制,防止业务未完成时锁提前释放

可重入性(Reentrant)

  • 同一个线程可以多次获取同一把锁
  • 通过重入计数器实现,解决方法嵌套调用场景

高可用性(High Availability)

  • 锁服务本身不能成为单点故障
  • 支持主从切换、集群部署

1.2 技术选型对比

方案优势劣势适用场景
Redisson(推荐)成熟稳定、API友好、自动续期、可重入依赖Redis大部分业务场景
自研Redis锁轻量级、可定制需要处理各种边界情况,维护成本高学习研究
ZooKeeper强一致性、天然支持临时节点性能较低、运维复杂对一致性要求极高的场景
数据库悲观锁无需额外组件性能差、可能死锁轻量级场景

2. 核心技术原理

2.1 Redis 分布式锁实现原理

Redisson 基于 Redis 的 SET 命令和 Lua 脚本实现分布式锁,保证操作的原子性。

加锁脚本

lua
-- 加锁脚本
if (redis.call('exists', KEYS[1]) == 0) then
    -- 锁不存在,设置锁并设置过期时间
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 锁存在且是当前线程持有,增加重入计数
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
-- 锁被其他线程持有,返回剩余过期时间
return redis.call('pttl', KEYS[1]);

工作原理:

  1. 检查锁是否存在(exists)
  2. 如果不存在,设置锁并设置过期时间(hset + pexpire)
  3. 如果锁已存在且是当前线程持有,增加重入计数(hincrby)
  4. 使用 Hash 结构存储,key 为锁名,field 为线程ID,value 为重入次数

解锁脚本

lua
-- 解锁脚本
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    -- 不是当前线程持有的锁,直接返回
    return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
    -- 重入计数 > 0,刷新过期时间
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    -- 重入计数 = 0,删除锁
    redis.call('del', KEYS[1]);
    return 1;
end;

工作原理:

  1. 检查锁是否由当前线程持有
  2. 减少重入计数
  3. 如果计数 > 0,刷新过期时间
  4. 如果计数 = 0,删除锁

2.2 Watchdog 自动续期机制

Watchdog 是 Redisson 的核心特性之一,用于防止业务执行时间超过锁的过期时间导致锁提前释放。

工作流程

text
1. 获取锁成功
   ↓
2. 启动 Watchdog 定时任务
   ↓
3. 每隔 leaseTime/3 时间检查一次
   ↓
4. 如果业务还在执行,自动续期
   ↓
5. 业务执行完成,释放锁,停止 Watchdog

续期时机

  • 默认配置:leaseTime = 30秒,每 10秒 续期一次
  • 自定义配置:可以通过参数调整
  • 自动停止:业务完成或发生异常时自动停止续期

注意:如果指定了 leaseTime 参数,Watchdog 将不会启动,需要手动管理锁的过期时间。

2.3 锁的可重入性实现

可重入原理

java
// 外层方法
public void outerMethod() {
    distributedLockUtil.executeWithLock("myLock", 3, 10, () -> {
        // 第一次获取锁,重入计数 = 1
        innerMethod();  // 调用内层方法
        return null;
    });
}

// 内层方法
public void innerMethod() {
    distributedLockUtil.executeWithLock("myLock", 3, 10, () -> {
        // 第二次获取锁,重入计数 = 2(可重入)
        // 业务逻辑
        return null;
    });
    // 释放一次,重入计数 = 1
}
// 释放一次,重入计数 = 0,锁被完全释放

Redis 存储结构

text
Key: lock:order:123
Field: UUID_ThreadID
Value: 2  (重入次数)

3. 关键代码实现

3.1 Redisson 配置类

java
@Configuration
public class RedissonConfig {

    @Value("${spring.data.redis.host:127.0.0.1}")
    private String redisHost;
    
    @Value("${spring.data.redis.port:6379}")
    private int redisPort;
    
    @Value("${spring.data.redis.password}")
    private String redisPassword;
    
    @Value("${spring.data.redis.database:0}")
    private int redisDatabase;
    
    @Value("${spring.redisson.connection.minimum-idle-size:5}")
    private int minimumIdleSize;
    
    @Value("${spring.redisson.connection.pool-size:10}")
    private int poolSize;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        // 单节点配置
        config.useSingleServer()
                .setAddress("redis://" + redisHost + ":" + redisPort)
                .setPassword(!redisPassword.isEmpty() ? redisPassword : null)
                .setDatabase(redisDatabase)
                .setConnectionMinimumIdleSize(minimumIdleSize)
                .setConnectionPoolSize(poolSize);
        
        return Redisson.create(config);
    }
}

3.2 分布式锁工具类

java
@Slf4j
@Component
@RequiredArgsConstructor
public class DistributedLockUtil {

    private final RedissonClient redissonClient;

    /**
     * 尝试获取锁并执行操作
     * 
     * @param lockKey   锁的 key
     * @param waitTime  等待获取锁的最大时间(秒)
     * @param leaseTime 持有锁的最长时间(秒)
     * @param supplier  要执行的操作
     * @return 操作返回值
     */
    public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, Supplier<T> supplier) {
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
            
            if (!acquired) {
                throw new RuntimeException("获取锁失败: " + lockKey);
            }
            
            log.debug("获取锁成功: {}", lockKey);
            return supplier.get();
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断: " + lockKey, e);
            
        } finally {
            // 确保锁被正确释放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.debug("释放锁: {}", lockKey);
            }
        }
    }

    /**
     * 简化方式:使用默认超时时间
     * waitTime = 3秒,leaseTime = 10秒
     */
    public <T> T executeWithSimpleLock(String lockKey, Supplier<T> supplier) {
        return executeWithLock(lockKey, 3, 10, supplier);
    }

    /**
     * 简化方式:无返回值
     */
    public void executeWithSimpleLock(String lockKey, Runnable runnable) {
        executeWithLock(lockKey, 3, 10, () -> {
            runnable.run();
            return null;
        });
    }
}

3.3 锁 Key 设计规范

命名格式:业务模块:操作类型:业务标识

java
// 订单锁
String lockKey = "order:create:" + orderId;

// 库存锁
String lockKey = "inventory:deduct:" + skuId;

// 用户操作锁
String lockKey = "user:like:" + userId + ":" + targetId;

4. 业务应用案例

4.1 防止重复提交

java
@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final DistributedLockUtil lockUtil;
    
    public Order createOrder(CreateOrderRequest request) {
        String lockKey = "order:create:" + request.getUserId();
        
        return lockUtil.executeWithLock(lockKey, 3, 10, () -> {
            // 检查是否重复提交
            if (orderRepository.existsByRequestId(request.getRequestId())) {
                throw new BusinessException("订单已存在,请勿重复提交");
            }
            
            // 创建订单
            Order order = new Order();
            order.setRequestId(request.getRequestId());
            order.setUserId(request.getUserId());
            // ... 其他业务逻辑
            
            return orderRepository.save(order);
        });
    }
}

4.2 库存扣减

java
@Service
@RequiredArgsConstructor  
public class InventoryService {
    
    private final DistributedLockUtil lockUtil;
    
    public void deductStock(String skuId, int quantity) {
        String lockKey = "inventory:deduct:" + skuId;
        
        lockUtil.executeWithLock(lockKey, 5, 30, () -> {
            // 1. 查询当前库存
            Inventory inventory = inventoryRepository.findBySkuId(skuId);
            
            // 2. 检查库存是否充足
            if (inventory.getStock() < quantity) {
                throw new BusinessException("库存不足");
            }
            
            // 3. 扣减库存
            inventory.setStock(inventory.getStock() - quantity);
            inventoryRepository.save(inventory);
            
            return null;
        });
    }
}

5. 最佳实践

5.1 锁粒度设计

粒度级别示例优点缺点
粗粒度lock:order实现简单并发度低
中粒度lock:order:userId平衡并发与复杂度需要合理设计
细粒度lock:order:orderId并发度高锁数量多

5.2 超时时间选择

业务类型建议配置说明
快速操作waitTime=3s, leaseTime=10s点赞、收藏等
一般操作waitTime=5s, leaseTime=30s订单创建、库存扣减
复杂操作waitTime=10s, leaseTime=60s批量处理、文件上传
长时间操作不指定leaseTime,使用Watchdog需要自动续期

5.3 异常处理规范

java
public void businessOperation(String businessId) {
    String lockKey = "business:operation:" + businessId;
    
    try {
        lockUtil.executeWithLock(lockKey, 3, 10, () -> {
            // 业务逻辑
            return null;
        });
    } catch (RuntimeException e) {
        if (e.getMessage().contains("获取锁失败")) {
            // 锁获取失败,给用户友好提示
            throw new BusinessException("系统繁忙,请稍后重试");
        }
        throw e;
    }
}

6. 故障排查

6.1 常见问题与解决方案

问题可能原因解决方案
锁获取超时其他线程持有锁时间过长1. 检查 leaseTime 配置
2. 优化业务逻辑执行时间
锁提前释放业务执行时间超过 leaseTime1. 增加 leaseTime
2. 使用 Watchdog 自动续期
死锁程序异常未释放锁1. 确保 finally 中释放锁
2. 设置合理的过期时间
锁重入失败不同线程尝试重入检查业务逻辑,确保是同一线程

6.2 监控指标

建议监控以下指标:

java
@Component
@RequiredArgsConstructor
public class LockMetrics {
    
    private final MeterRegistry meterRegistry;
    
    public void recordLockAcquired(String lockKey, long waitTime) {
        Timer.builder("distributed.lock.acquire.time")
            .tag("lock_key", lockKey)
            .register(meterRegistry)
            .record(waitTime, TimeUnit.MILLISECONDS);
    }
    
    public void recordLockFailed(String lockKey) {
        Counter.builder("distributed.lock.acquire.failed")
            .tag("lock_key", lockKey)
            .register(meterRegistry)
            .increment();
    }
}

7. 总结

Redisson 分布式锁是一个成熟、可靠的解决方案,具备以下核心优势:

  • 开箱即用:无需关心底层实现细节,API 简洁易用
  • 自动化机制:Watchdog 自动续期、异常自动释放锁
  • 可重入支持:支持同一线程多次获取锁
  • 高性能:基于 Redis 内存操作,响应速度快(通常 < 5ms)

在使用时,需要注意:

  1. 合理设计锁粒度,平衡并发度和复杂度
  2. 根据业务特点选择合适的超时时间
  3. 确保异常情况下锁能被正确释放
  4. 建立监控指标,及时发现锁相关问题

分布式锁不是银弹,在高并发场景下,还需要结合消息队列、乐观锁等技术方案,构建完整的并发控制体系。