Redis 分布式锁与 Redisson 原理深度解析

Redis 分布式锁与 Redisson 原理深度解析

分布式锁是分布式系统中保证资源互斥访问的核心技术。本文深入剖析 Redis 分布式锁的实现原理、Redisson 的看门狗机制、RedLock 算法与争议、以及生产环境中的最佳实践与避坑指南。

一、分布式锁概述

1.1 为什么需要分布式锁

分布式锁要求
互斥性: 任意时刻只能一个客户端持有
可重入: 同一客户端可重入
死锁避免: 锁要能自动释放
性能: 高并发下性能要足够好
分布式问题
多进程/多机器竞争同一资源
JVM 锁失效
需要分布式锁
单机问题
多线程竞争同一资源
synchronized / ReentrantLock
只在单机有效

1.2 分布式锁的实现方式

实现方式
数据库
表记录锁
悲观锁
ZooKeeper
临时有序节点
CP 系统,强一致
Redis
SETNX + 过期时间
AP 系统,高性能


二、Redis 分布式锁基础

2.1 简单实现

释放锁


释放锁
unlink lock_key
是 owner?
释放成功
释放失败
加锁


SET lock_key unique_value NX EX 30
返回 OK?
获取锁成功
获取锁失败

2.2 基础实现代码

java 复制代码
// 基础分布式锁实现
public class SimpleRedisLock {
    
    private RedisTemplate<String, String> redisTemplate;
    
    public boolean tryLock(String lockKey, String requestId, int expireTime) {
        // SET key value NX EX seconds
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, requestId, 
                Duration.ofSeconds(expireTime));
        return Boolean.TRUE.equals(result);
    }
    
    public boolean releaseLock(String lockKey, String requestId) {
        // 使用 Lua 脚本保证原子性
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey),
            requestId
        );
        
        return result != null && result > 0;
    }
}

2.3 Lua 脚本释放锁

lua 复制代码
-- 释放锁的 Lua 脚本
-- KEYS[1]: 锁的 key
-- ARGV[1]: 请求的唯一标识

-- 只有当锁的值等于请求 ID 时才删除
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

2.4 为什么需要唯一值?

解决方案
每个客户端使用唯一值作为锁的值
释放时检查值是否匹配
只有自己的锁才能释放
问题场景
客户端A 获取锁,过期时间 30s
业务执行时间 > 30s
锁自动过期
客户端B 获取同一把锁
客户端A 业务执行完成
客户端A 释放锁
锁被错误释放!(B 的锁)


三、Redisson 分布式锁

3.1 Redisson 概述

优势
开箱即用
功能完善
社区活跃
Redisson核心
提供易用的分布式锁 API
实现可重入锁
看门狗自动续期
公平锁/读写锁等多种锁

3.2 看门狗机制

看门狗(Watchdog)是 Redisson 的核心特性,解决锁续期问题:
续期时机
业务执行中
锁快过期
自动续期
业务继续执行
业务完成后释放锁
看门狗流程


加锁时设置过期时间 30s
启动定时任务
每 10s 检查一次
锁还存在?
续期 30s
停止续期

3.3 看门狗源码解析

java 复制代码
// Redisson 看门狗核心代码
public class RedissonLock {
    
    // 默认锁过期时间
    private static final long lockWatchdogTimeout = 30 * 1000;
    
    // 续期间隔 = 过期时间 / 3
    private static final long internalLockLeaseTime = lockWatchdogTimeout / 3;
    
    // 续期定时任务
    private void scheduleExpirationRenewal(long threadId) {
        // 定时任务,每 10 秒执行一次
        renewalTask = commandExecutor.getConnectionManager()
            .getSchedulerService()
            .schedule(new Runnable() {
                @Override
                public void run() {
                    // 续期命令
                    renewExpiration();
                }
            }, internalLockLeaseTime, TimeUnit.MILLISECONDS);
    }
    
    // 续期
    private void renewExpiration() {
        // 使用 Lua 脚本续期
        Long result = evalWriteAsync(getName(), LongCodec.INSTANCE, RawValue.INSTANCE,
            "if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
            "    redis.call('pexpire', KEYS[1], ARGV[1]); " +
            "    return 1; " +
            "else " +
            "    return 0; " +
            "end",
            Collections.singletonList(getName()), 
            lockWatchdogTimeout, getLockName());
        
        if (result > 0) {
            // 续期成功,继续调度
            scheduleExpirationRenewal(getHolderId());
        }
    }
}

3.4 Redisson 使用示例

java 复制代码
// 引入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.0</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);
    }
}

// 使用可重入锁
@Service
public class OrderService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public void createOrder(Order order) {
        RLock lock = redissonClient.getLock("order:create");
        
        try {
            // tryLock() 尝试获取锁,最多等待 10s,锁自动续期
            boolean locked = lock.tryLock(10, TimeUnit.SECONDS);
            if (!locked) {
                throw new RuntimeException("获取锁失败");
            }
            
            // 业务逻辑
            doCreateOrder(order);
            
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

3.5 可重入锁实现

java 复制代码
// Redisson 可重入锁核心
// 使用 Redis Hash 存储锁信息
// key: 锁名
// field: 线程标识
// value: 重入次数

// 加锁时
HSET lock_name thread_id 1  // 首次加锁,次数为 1

// 重入时
HINCRBY lock_name thread_id 1  // 次数 +1

// 释放时
HINCRBY lock_name thread_id -1  // 次数 -1
// 如果次数为 0,删除锁

四、RedLock 算法

4.1 RedLock 原理

释放
向所有 Redis 实例释放锁
不管是否获取成功
RedLock算法


向 N 个 Redis 实例请求锁
使用相同的 key 和随机值
计算获取锁的时间
成功获取 >= N/2+1?
成功获取锁
获取锁失败

4.2 RedLock 代码实现

java 复制代码
public class RedLock {
    
    public String lock(String resourceName, int ttlMillis) {
        // 生成随机值
        String token = UUID.randomUUID().toString();
        
        int n = redissonServers.size();
        int成功 = 0;
        
        long startTime = System.currentTimeMillis();
        
        // 向所有实例请求锁
        for (RedissonInstance instance : redissonServers) {
            try {
                if (instance.tryLock(token, ttlMillis)) {
                    成功++;
                }
            } catch (Exception e) {
                // 继续尝试其他实例
            }
        }
        
        long elapsedTime = System.currentTimeMillis() - startTime;
        
        // 检查是否获取超过半数实例的锁
        if (成功 >= (n / 2 + 1)) {
            // 计算锁的有效期
            long clientClockTimeout = ttlMillis - elapsedTime - 重试时间;
            return token;
        } else {
            // 释放所有获取到的锁
            for (RedissonInstance instance : redissonServers) {
                try {
                    instance.unlock(token);
                } catch (Exception e) {
                    // 忽略
                }
            }
            return null;
        }
    }
}

4.3 RedLock 争议

替代方案
单机 Redis + 哨兵/集群
足够大多数场景
Redisson 单机模式足够
质疑点
时钟跳跃问题
各服务器时钟不同步
可能导致锁提前失效
性能问题
需要请求多个 Redis 实例
延迟增加
复杂度
部署多个 Redis 实例
运维成本增加


五、生产环境最佳实践

5.1 锁的粒度控制

示例
// 反例: 锁住整个方法
public void doSomething() {
lock.lock(); // 整个方法加锁
try { ... } finally { lock.unlock(); }
}
// 正例: 只锁关键代码
public void doSomething() {
// 前置处理
lock.lock();
try { 关键代码 } finally { lock.unlock(); }
// 后置处理
}
粒度过小
只锁必要的代码
性能好
并发能力强
粒度过大
锁住整个方法
性能差
并发能力差

5.2 最佳实践代码

java 复制代码
@Service
public class InventoryService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    // 最佳实践: 合理的锁粒度
    public void decreaseStock(Long productId, Integer count) {
        String lockKey = "stock:decrease:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        boolean locked = false;
        try {
            // 尝试获取锁,最多等待 5 秒,锁自动续期
            locked = lock.tryLock(5, -1, TimeUnit.SECONDS);
            
            if (!locked) {
                throw new BusinessException("系统繁忙,请稍后重试");
            }
            
            // 只在锁内执行关键操作
            Inventory inventory = inventoryMapper.selectByProductId(productId);
            if (inventory.getStock() < count) {
                throw new BusinessException("库存不足");
            }
            
            inventoryMapper.decreaseStock(productId, count);
            
        } finally {
            // 确保锁被释放
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

5.3 锁超时设置

java 复制代码
// 错误示例: 锁超时设置过短
lock.tryLock(1, TimeUnit.SECONDS);  // 只等待 1 秒
// 业务执行 30 秒,锁在 30 秒时自动释放

// 正确示例: 锁超时设置合理
lock.tryLock(10, 30, TimeUnit.SECONDS);  // 等待 10 秒,锁 30 秒过期
// 使用看门狗自动续期

// 正确示例: 不设置过期时间,使用看门狗
lock.tryLock();
// 看门狗每 10 秒检查一次,锁自动续期

// 正确示例: 设置合理的过期时间
lock.tryLock(10, 60, TimeUnit.SECONDS);  // 等待 10 秒,锁 60 秒过期
// 看门狗会自动续期

5.4 常见问题处理

问题3: 主从切换丢锁
Redis 主从切换
从库未同步锁信息
新主库丢失锁
解决方案: 使用 RedLock 或红锁
问题2: 锁重试风暴
获取锁失败后大量重试
Redis 压力增大
解决方案: 添加随机延迟
使用分布式信号量控制并发
问题1: 锁续期失败
看门狗定时任务失败
锁被提前释放
解决方案: 设置合理的过期时间
不要依赖看门狗作为唯一续期机制


六、面试高频问题

6.1 Redis 分布式锁需要注意哪些问题?

复制代码
1. 原子性
   - 使用 SETNX + EX 保证原子性
   - 释放锁使用 Lua 脚本保证原子性

2. 过期时间
   - 设置合理的过期时间
   - 防止死锁

3. 唯一值
   - 使用 UUID 等唯一值
   - 防止误删其他客户端的锁

4. 可重入
   - 同一线程可多次获取锁
   - 使用计数器实现

5. 锁续期
   - 看门狗机制
   - 防止业务未完成锁过期

6.2 如何实现可重入锁?

复制代码
实现方式:使用 Redis Hash

加锁时:
- key: 锁名
- field: 线程标识(UUID + 线程ID)
- value: 重入次数

- 首次加锁:HSET lock field 1
- 重入:HINCRBY lock field 1

释放时:
- HINCRBY lock field -1
- 如果值为 0,删除 key

6.3 Redisson 的看门狗机制原理?

复制代码
原理:
1. 加锁时设置默认过期时间 30 秒
2. 看门狗定时任务每 10 秒(30/3)执行一次
3. 检查锁是否还存在
4. 如果存在,续期 30 秒
5. 业务完成后释放锁,停止看门狗

注意:
- 只有使用 tryLock() 不指定过期时间时才启用看门狗
- tryLock(time, timeUnit) 指定过期时间时不启用看门狗

6.4 RedLock 算法的原理?

复制代码
原理:
1. 向 N 个独立的 Redis 实例请求锁
2. 计算获取锁的时间
3. 如果在有效期内,成功获取 >= N/2+1 个实例的锁
4. 获取锁成功,有效期 = TTL - 获取时间
5. 释放时向所有实例发送释放命令

为什么需要多个实例:
- 防止单点故障
- 提高可用性

注意:RedLock 有争议,主要问题是时钟同步假设

6.5 分布式锁 vs 分布式信号量?

复制代码
分布式锁:
- 同一时刻只有一个客户端持有
- 用于互斥访问

分布式信号量:
- 同一时刻可有 N 个客户端持有
- 用于限流控制

Redisson 示例:
RLock lock = redissonClient.getLock("resource");
// 互斥锁

RSemaphore semaphore = redissonClient.getSemaphore("resource");
// 信号量,允许 N 个并发
semaphore.trySetPermits(10);  // 设置 10 个许可
semaphore.acquire();  // 获取许可
semaphore.release();  // 释放许可

七、总结

7.1 分布式锁选择

选择建议
Redis 单机
足够大多数场景
Redis 集群 + Redisson
高可用要求
ZooKeeper
强一致性要求
etcd
配置中心 + 分布式锁

7.2 最佳实践

复制代码
分布式锁最佳实践:

1. 原子性
   ✅ SETNX + EX 原子性加锁
   ✅ Lua 脚本原子性释放

2. 过期时间
   ✅ 设置合理的过期时间
   ✅ 使用看门狗自动续期

3. 唯一标识
   ✅ 使用 UUID 作为锁值
   ✅ 释放前检查值是否匹配

4. 锁粒度
   ✅ 只锁关键代码
   ✅ 避免锁粒度过大

5. 异常处理
   ✅ finally 中释放锁
   ✅ 检查是否持有锁后再释放
相关推荐
胡楚昊2 小时前
BUU WEB之旅(1)
java·数据库·mybatis
摇滚侠3 小时前
基于 Redis 实现验证码登录
javascript·redis·bootstrap
牢七3 小时前
链条合集整理
java·开发语言
小雅痞3 小时前
[Java][Leetcode hard] 30. 串联所有单词的子串
java·leetcode
钝挫力PROGRAMER3 小时前
static final 指向可变集合的设计模式
java·设计模式
Filwaod3 小时前
互联网大厂Java面试实战:Spring+Redis+MySQL+JVM场景问答深度解析
jvm·spring boot·redis·mysql·java面试·技术面试·互联网大厂
青山师3 小时前
Java反射深度解析:运行时探查的艺术、代价与工程实践
java·开发语言·面试·反射·java程序员·java核心
安当加密3 小时前
Spring Boot应用接入国产安当凭据管理系统SMS Starter实战(附源码)
java·spring boot·后端
skilllite作者3 小时前
Deer-Flow 工作流引擎深度评测报告
java·大数据·开发语言·chrome·分布式·架构·rust