Redisson分布式锁:从入门到实战

Redisson分布式锁:从入门到实战

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

在单体应用中,我们使用Java的synchronized或ReentrantLock就能解决并发问题。但在微服务架构下,多个实例同时运行,单机的锁机制就失效了。这时就需要分布式锁来保证跨JVM的互斥访问。

分布式锁的核心需求

  1. 互斥性:同一时刻只能有一个客户端持有锁
  2. 防止死锁:客户端崩溃后能自动释放锁
  3. 容错性:Redis节点故障时仍能正常工作
  4. 可重入性:同一个线程可以多次获取同一个锁

二、Redis实现分布式锁的基础方案

2.1 最简单的实现:SET NX

最早期的实现方式是使用SET命令的NX选项:

bash 复制代码
SET key value NX PX 30000
  • NX:只在key不存在时设置
  • PX:设置过期时间(毫秒)

这种方式的缺点:

  • 无法实现可重入锁
  • 无法获取锁的持有人信息
  • 无法实现锁的自动续期

2.2 完整实现方案

一个相对完善的Redis分布式锁实现需要包含以下逻辑:

  1. 加锁:SET key value NX PX timeout
  2. 解锁:Lua脚本保证原子性
  3. 看门狗:定时续期防止业务执行时间超过锁过期时间

三、Redisson分布式锁

Redisson是Redis的Java客户端,它提供了完善的分布式锁实现,解决了原生Redis锁的各种问题。

3.1 核心特性

  1. 可重入锁:同一个线程可多次获取锁
  2. 公平锁:按请求顺序获取锁
  3. 读写锁:允许多个读操作或单个写操作
  4. 红锁:跨多个Redis节点的分布式锁
  5. 自动续期:看门狗机制自动延长锁时间
  6. 锁释放:Lua脚本保证原子性操作

3.2 基本使用

java 复制代码
// 获取锁对象
RLock lock = redisson.getLock("myLock");

try {
    // 加锁(支持等待时间和自动续期)
    boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
    if (locked) {
        // 执行业务逻辑
        doBusiness();
    }
} finally {
    // 释放锁
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

四、Redis实现 vs Redisson对比

特性 Redis原生实现 Redisson
实现复杂度 高,需要处理多种边界情况 低,开箱即用
可重入锁 需要自己实现 原生支持
自动续期 需要后台线程续期 看门狗自动续期
公平锁 难以实现 开箱即用
读写锁 难以实现 开箱即用
红锁 需要自己协调 开箱即用
异常处理 需要仔细处理 完善的异常体系

结论:生产环境推荐使用Redisson,它已经处理了各种边界情况和异常场景。

五、Redisson核心原理解析

5.1 加锁机制

Redisson使用Lua脚本保证加锁的原子性:

lua 复制代码
-- 检查key是否存在
if redis.call('exists', KEYS[1]) == 0 then
    -- 不存在则设置,使用hash结构存储
    redis.call('hset', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end

-- key已存在,检查是否是当前线程
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])

锁的数据结构使用Hash类型:

  • Key:锁名称
  • Field:线程标识(UUID:threadId)
  • Value:重入次数

5.2 看门狗机制

看门狗(Watchdog)是Redisson的核心特性,用于解决锁超时问题:

  1. 业务获得锁时,启动后台定时任务
  2. 每隔lockWatchdogTimeout/3时间检查锁是否还持有
  3. 如果还持有则重置过期时间
  4. 业务释放锁时停止看门狗

默认情况下,看门狗每10秒续期一次,锁的过期时间为30秒。

5.3 解锁机制

解锁同样使用Lua脚本保证原子性:

lua 复制代码
-- 检查锁是否是当前线程持有
if redis.call('hexists', KEYS[1], ARGV[2]) <= 0 then
    return nil
end

-- 减少重入次数
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1)

-- 如果重入次数大于0,重置过期时间
if counter > 0 then
    redis.call('pexpire', KEYS[1], ARGV[1])
    return 0
end

-- 重入次数为0,删除锁
redis.call('del', KEYS[1])
-- 发布解锁消息
redis.call('publish', KEYS[2], ARGV[3])
return 1

六、生产环境实战案例

6.1 库存扣减

java 复制代码
@Service
public class InventoryService {

    @Autowired
    private RedissonClient redisson;

    public boolean deductStock(String productId, int quantity) {
        RLock lock = redisson.getLock("stock:" + productId);

        try {
            // 等待5秒,锁过期30秒
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                // 查询库存
                int stock = getStock(productId);
                if (stock >= quantity) {
                    // 扣减库存
                    updateStock(productId, stock - quantity);
                    return true;
                }
                return false;
            }
            throw new BusinessException("系统繁忙,请稍后重试");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

6.2 订单幂等性控制

java 复制代码
@Service
public class OrderService {

    @Autowired
    private RedissonClient redisson;

    public Order createOrder(OrderRequest request) {
        String lockKey = "order:create:" + request.getUserId();
        RLock lock = redisson.getLock(lockKey);

        try {
            if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
                // 检查是否已有未完成订单
                Order existOrder = getUnfinishedOrder(request.getUserId());
                if (existOrder != null) {
                    return existOrder;
                }

                // 创建新订单
                Order order = buildOrder(request);
                saveOrder(order);
                return order;
            }
            throw new BusinessException("操作频繁,请稍后重试");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

6.3 定时任务防止重复执行

java 复制代码
@Component
public class DataSyncJob {

    @Autowired
    private RedissonClient redisson;

    @Scheduled(cron = "0 */5 * * * *")
    public void syncData() {
        String lockKey = "job:data:sync";
        RLock lock = redisson.getLock(lockKey);

        try {
            // 尝试获取锁,不等待
            if (lock.tryLock(0, TimeUnit.SECONDS)) {
                // 执行数据同步
                doSync();
            } else {
                log.info("上次同步任务尚未完成,跳过本次执行");
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

七、高级特性:读写锁

在读多写少的场景下,使用读写锁可以大幅提升并发性能:

java 复制代码
@Service
public class ConfigService {

    @Autowired
    private RedissonClient redisson;

    public String getConfig(String key) {
        RReadWriteLock rwLock = redisson.getReadWriteLock("config:" + key);
        RLock readLock = rwLock.readLock();

        try {
            readLock.lock();
            // 多个线程可以同时读取
            return queryFromDB(key);
        } finally {
            readLock.unlock();
        }
    }

    public void updateConfig(String key, String value) {
        RReadWriteLock rwLock = redisson.getReadWriteLock("config:" + key);
        RLock writeLock = rwLock.writeLock();

        try {
            // 写操作独占锁
            writeLock.lock();
            updateDB(key, value);
            // 清除缓存
            clearCache(key);
        } finally {
            writeLock.unlock();
        }
    }
}

八、最佳实践

8.1 锁的粒度选择

  1. 粗粒度:锁整个表或模块,简单但并发度低
  2. 细粒度:锁单个记录,并发度高但实现复杂

推荐:根据业务场景选择合适的粒度,库存扣减用商品维度,订单创建用用户维度。

8.2 锁的过期时间设置

  1. 不要设置过短,避免业务未执行完就释放
  2. 不要设置过长,避免故障时长时间阻塞
  3. Redisson的看门狗会自动续期,但要设置合理的初始时间

推荐:根据业务执行时间的P99值设置,预留50%余量。

8.3 异常处理

  1. 捕获中断异常,确保锁能被释放
  2. 使用tryLock而不是lock,避免无限等待
  3. 检查锁的持有状态,避免误释放

九、常见问题

Q1:Redisson锁会丢失吗?

不会。Redisson使用看门狗机制,即使业务执行时间超过锁的过期时间,也会自动续期,直到业务主动释放锁。

Q2:Redis主从切换会影响锁吗?

会。单节点Redis在主从切换时可能丢失锁。需要使用Redisson的红锁(RedLock)或Redis Cluster。

Q3:如何实现公平锁?

Redisson提供了公平锁实现:

java 复制代码
RLock fairLock = redisson.getFairLock("myFairLock");
fairLock.lock();

Q4:性能如何?

Redisson的分布式锁性能很好,单机QPS可达5万+,足以应对大多数场景。如有更高性能需求,可以考虑分段锁或本地锁+分布式锁的组合方案。

十、总结

Redisson分布式锁是Java领域最成熟的Redis分布式锁解决方案:

  1. 使用简单:API友好,学习成本低
  2. 功能完善:支持多种锁类型和场景
  3. 稳定可靠:经过大量生产环境验证
  4. 性能优异:看门狗机制和Lua脚本保证性能

在实际项目中,建议优先使用Redisson,它已经帮你处理了各种复杂的边界情况,让你专注于业务逻辑的实现。


相关推荐
3 小时前
java关于内部类
java·开发语言
好好沉淀3 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin3 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder3 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~3 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟3 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日3 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du3 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea
东东5163 小时前
高校智能排课系统 (ssm+vue)
java·开发语言