找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

149

积分

0

好友

17

主题
发表于 12 小时前 | 查看: 1| 回复: 0

在分布式系统中,协调多个服务或实例对共享资源的访问是一个核心挑战。简单使用Redis的SETNX命令实现的分布式锁,虽然基础,但在实际生产环境中往往面临几个关键问题。

一、基于SETNX的分布式锁存在哪些问题?

  1. 不可重入:同一个线程无法多次获取同一把锁。这在使用锁封装的方法相互调用时,极易引发死锁。
  2. 不可重试:获取锁仅尝试一次,失败便立即返回。在并发激烈的场景下,合理的重试机制是提升成功率的关键。
  3. 超时释放隐患:为避免死锁设置锁超时时间,若业务执行耗时超过锁有效期,锁会提前释放,存在安全风险。
  4. 主从一致性问题:在Redis主从架构下,锁信息异步同步到从节点。若在主节点写入锁后、同步完成前主节点宕机,锁信息可能丢失,导致多个客户端同时持有锁。

下图清晰对比了这几种锁机制的特性:
锁机制特性对比图

二、Redisson:Java分布式锁的强大框架

Redisson 是一个在Redis基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了基础的Redis操作,更封装了丰富的分布式对象和分布式服务,其中分布式锁是其最核心的功能之一。

Redisson的分布式锁功能完善,提供了多种锁和同步器以满足不同场景需求,具体列表如下:
Redisson分布式锁和同步器列表

三、快速集成与使用Redisson分布式锁

3.1 引入依赖

首先,在您的 pom.xml 中添加Redisson依赖。

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

3.2 配置Redisson客户端

通过一个配置类来创建 RedissonClient Bean,这是操作所有Redisson功能的基础。

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 使用单节点模式,根据实际情况可配置集群、哨兵等模式
        config.useSingleServer()
                .setAddress("redis://192.168.150.101:6379")
                .setPassword("123321");
        return Redisson.create(config);
    }
}

3.3 基础锁使用示例

以下是一个在测试环境中的基本使用示例,展示了获取锁、执行业务和释放锁的完整流程。

@Resource
private RedissonClient redissonClient;

@Test
void testRedisson() throws Exception {
    // 1. 获取锁对象(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");

    // 2. 尝试获取锁(支持等待重试和自动释放时间)
    // 参数:最大等待时间、锁持有时间、时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);

    // 3. 判断是否获取成功
    if (isLock) {
        try {
            System.out.println("成功获取锁,执行业务逻辑...");
            // 你的业务代码放在这里
        } finally {
            // 4. 无论如何,最终在finally块中释放锁
            lock.unlock();
        }
    }
}

3.4 业务场景实战:秒杀订单

在真实的业务场景,例如秒杀下单中,我们使用Redisson锁来防止用户重复下单,其高并发控制逻辑如下:

@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService {

    @Resource
    private RedissonClient redissonClient;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // ... 优惠券查询、时间校验、库存判断等前置逻辑 ...
        Long userId = UserHolder.getUser().getId();

        // 创建分布式锁对象,锁键以业务前缀+用户ID构成,实现细粒度锁
        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);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

四、Redisson可重入锁的实现原理

Redisson的可重入锁设计精巧,它利用Redis的Hash结构来存储锁状态,完美解决了线程重入问题。其加锁的核心逻辑通过一段Lua脚本来保证原子性执行。

加锁Lua脚本解析:

  • KEYS[1]:锁的键名。
  • ARGV[1]:锁的生存时间。
  • ARGV[2]:锁的客户端标识,通常为 客户端ID:线程ID,用作Hash结构中的field
-- 锁不存在时,加锁并设置过期时间
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;
-- 锁已存在,且是当前线程持有,则重入次数+1
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. 判断锁(大Key)是否存在。不存在则直接加锁,设置线程标识(小Key)和重入次数为1。
  2. 锁已存在,则检查是否由当前线程持有(即小Key是否存在)。如果是,则将重入次数加1(hincrby)。
  3. 以上都不是,说明锁被其他线程持有,返回锁的剩余有效期(pttl)。

下图展示了Redis中存储的锁数据结构:
Redisson可重入锁Redis存储结构

五、锁重试与WatchDog看门狗机制

Redisson不仅支持在 tryLock 时指定等待时间进行重试,其 lock() 方法还内置了强大的 WatchDog(看门狗) 机制,解决了锁超时释放而业务未执行完的安全隐患。

核心流程:

  1. 当使用 lock() 无参方法时,默认加锁时间为30秒。
  2. 加锁成功后,Redisson会启动一个后台定时任务(看门狗线程),每隔 锁有效期 / 3(即10秒)检查一次。
  3. 如果业务仍在执行(即客户端仍存活),看门狗会通过Lua脚本重置锁的过期时间,恢复到30秒。
  4. 此过程不断递归,直到业务完成并主动释放锁。如果客户端宕机,看门狗线程随之停止,锁最终会因超时而自动释放,避免了死锁。

这种机制确保了只要业务线程健康运行,锁就不会被意外释放,同时结合了 [数据库](https://yunpan.plus/f/23-1) 等组件的可靠性设计思路,为长事务提供了保障。

六、MultiLock联锁解决主从一致性危机

在Redis主从或哨兵模式下,主节点写入锁后,若在数据同步到从节点前发生故障切换,新的主节点可能不包含该锁信息,导致锁失效。这就是分布式锁的 主从一致性脑裂 问题。

Redis主从架构下的锁失效问题

Redisson提供的 MultiLock(联锁) 正是为解决此问题而生。其思想是将锁同时写入多个独立的Redis主节点(非主从关系),所有节点都加锁成功才算成功。

工作原理:

  1. 客户端需要向N个独立的Redis实例尝试加锁。
  2. 总加锁超时时间 = 锁数量 * 1500ms。例如3个锁,则总等待时间为4500ms。
  3. 在总时间内,必须成功获取所有锁。只要有一个实例加锁失败,则会在剩余时间内不断重试。
  4. 一旦失败或最终超时,已获取的锁会被全部释放。

下图展示了Redisson MultiLock的工作流程:
Redisson MultiLock加锁流程图

通过MultiLock,牺牲了一定的写入性能(需要写多个节点),换取了极高的锁可用性和数据一致性,适用于对可靠性要求极高的金融、交易等场景。




上一篇:RaindropEcho:JS逆向自动化工具,实现BurpSuite数据包加解密插件
下一篇:Python bup备份工具:基于Git技术的大文件高效增量备份方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 16:02 , Processed in 0.115974 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表