Redisson分布式锁

Redisson

Redisson 是一款基于 Redis 的 Java 应用实现分布式锁、分布式对象、分布式集合等功能的开源框架。其中,Redisson 中的分布式锁提供了可靠、高效的分布式解决方案。下面详细介绍一下 Redisson 分布式锁的实现原理以及相关方法的使用。

实现原理

Redisson 分布式锁是通过创建一个名为 lock:{lockKey} 的字符串键来实现的,其中 lockKey 表示当前获取锁的线程 ID。具体的实现过程如下:

  1. 创建字符串键,一开始它的值为空字符串。
  2. 使用 Redis 的 SET key value NX PX expire 命令来获取分布式锁。其中,NX 表示只有在键不存在的情况下才能设置该键,PX 表示设置键的过期时间,expire 表示设置键过期时间的值(以毫秒为单位)。
  3. 如果命令执行成功,则当前线程成功获取到了分布式锁。而如果命令执行失败,说明其它线程已占用该分布式锁,当前线程不能获取到分布式锁,需要等待锁的释放。
  4. 释放锁时,使用 Redis 的 Lua 脚本来实现原子性的解锁操作。具体的实现过程就是检查 lock:{lockKey} 的值是否等于当前线程 ID,如果相等则删除该键,释放锁。

为什么释放锁时使用lua脚本

在Redisson中释放锁时,使用了Lua脚本的方式进行解锁,主要出于以下的考虑:

  1. 原子性:采用Lua脚本的方式,可以保证解锁操作的原子性。当我们使用Redisson获取锁时,我们会通过Redis脚本将锁对应的键和当前线程ID一并设置到Redis中,解锁时我们会先查询Redis中该锁是否是由当前线程ID所持有,如果是,那么就执行删除操作。采用Lua脚本的方式执行该操作,不会有并发情况下的数据竞争或者冲突发生。

  2. 性能:Lua脚本相对于Java代码还是更省时省力,这主要是因为Redis对于连续的相同脚本只需要编译一次,然后缓存起来在以后的使用中直接调用即可。这使得Redisson分布式锁在高并发情况下拥有了更好的性能。

接下来,我们看下Redisson释放锁的代码,该方法内部调用了eval()方法去执行Lua脚本来释放锁:

public void unlock(String lockKey, String uuid) {
    RedissonClient redisson = Redisson.create();
    RScript script = redisson.getScript();
 
    List<Object> keyList = new ArrayList<Object>();
    keyList.add(lockKey);
 
    Long result = script.eval(RScript.Mode.READ_WRITE, 
                "if redis.call(\"get\", KEYS[1]) == ARGV[1] then " +
                "   return redis.call(\"del\", KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end"
            , RScript.ReturnType.INTEGER, keyList, uuid);
 
    redisson.shutdown();
}

上述代码中的 unlock() 方法首先创建一个 Redisson 客户端,然后获取 RScript 实例。接下来,该方法会根据参数生成相应的键值列表,并通过 script.eval() 方法执行 Lua 脚本,从而删除分布式锁。

eval() 方法的第一个参数 Mode.READ_WRITE 表示脚本需要读写 Redis 键,因此需要获取写锁;第二个参数是 Lua 脚本内容;第三个参数 ReturnType.INTEGER 表示返回结果需要为整型数值类型;第四个参数 keyList 包含分布式锁的 key,参数列表为运行 Lua 脚本所需的参数,如 uuid。在运行完 Lua 脚本后,通过 shutdown() 方法关闭 Redisson 客户端连接。

总之,通过使用Lua脚本来删除分布式锁,能够确保默认的解锁操作是原子的,同时性能也得到了提升,这也是Redisson采用该方式来释放锁的主要原因。

Redisson释放锁的代码详细介绍

Redisson释放分布式锁的代码如下:

public void unlock(String lockKey, String uuid) {
    RedissonClient redisson = Redisson.create();
    RScript script = redisson.getScript();
 
    List<Object> keyList = new ArrayList<Object>();
    keyList.add(lockKey);
 
    Long result = script.eval(RScript.Mode.READ_WRITE, 
                "if redis.call(\"get\", KEYS[1]) == ARGV[1] then " +
                "   return redis.call(\"del\", KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end"
            , RScript.ReturnType.INTEGER, keyList, uuid);
 
    redisson.shutdown();
}

该方法包含了三个步骤:

  1. 创建Redisson客户端和获取RScript实例:
RedissonClient redisson = Redisson.create();
RScript script = redisson.getScript();

首先,我们需要创建一个RedissonClient实例,来构建客户端连接。然后,我们通过该实例的getScript()方法获取RScript实例,方便未来Lua脚本的执行操作。

  1. 构造键值列表和执行Lua脚本
List<Object> keyList = new ArrayList<Object>();
keyList.add(lockKey);

Long result = script.eval(RScript.Mode.READ_WRITE, 
            "if redis.call(\"get\", KEYS[1]) == ARGV[1] then " +
            "   return redis.call(\"del\", KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end"
        , RScript.ReturnType.INTEGER, keyList, uuid);

在此步骤中,我们需要构造键值列表,将待释放的锁的key值添加到list中。接下来,在eval()方法中,我们传入了Mode.READ_WRITE来表示脚本需要读写Redis缓存;继而将Lua脚本作为字符串传入第二个参数;ReturnType.INTEGERT则表示要求返回整型数值类型。注意,Lua脚本中的ARGV[1]表示的是锁的value值uuid,它保存了当前持有锁的线程ID。如果加锁的时候,我们对Redis中该锁的value进行了设置,表示该锁是被当前线程锁持有的,那么Redis中对应的value的值就是当前线程的uuid,此时,如果lua解锁的时候,去检查该锁的value是否等于ARGV[1],即锁的value=当前线程的uuid,如果相等则删除该锁。相反,如果锁的value与当前uuid不匹配,则不能删除该锁。最后,eval()方法会返回一个Long类型的结果值,表示该操作是否成功,0表示锁未被释放成功,非0则表示锁被成功释放。

  1. 关闭Redisson客户端连接:
redisson.shutdown();

最后,需要关闭Redisson客户端连接,释放资源。这里,有几点需要注意:

  1. Redisson的互斥锁其实并不是真正意义上的锁,Redis使用的是一个带有超时机制的key-value对来实现锁的功能。它的原理是:当该key没有被其他客户端占用时,客户端持有该key作为锁,锁的持有者会在一段时间之后主动释放该锁。

  2. 由于Redisson的锁是基于Redis的分布式机制来实现的,因此Lua脚本的执行保证了锁的原子性,避免了可能出现的数据竞争和冲突。

  3. 在执行Lua脚本之后,需要关闭Redisson客户端连接,这样能够有效地释放资源,避免内存泄露发生。需要注意的是,一定不要在多线程环境下创建Redisson客户端,这可能导致多个线程共享同一个客户端引用,从而引起线程安全问题。

KEYS[1]) 这是什么

Redis的Lua脚本语言实现了5个关键字:ARGV、KEYS、redis、math、table,其中,KEYS是其中一个。KEYS是一个表(table)类型的全局变量,它包含了Lua脚本所使用的所有可用的Redis键,可以在Lua脚本中访问这些键。

在Redisson释放锁的代码中,KEYS[1]表示的是可重入锁占用的Redis键(lockKey),该键是在加锁时被保存到Redis缓存中的,当需要释放锁时,就需要通过该键来查找对应的锁进行释放。

示例代码如下:

Long result = script.eval(RScript.Mode.READ_WRITE, 
            "if redis.call(\"get\", KEYS[1]) == ARGV[1] then " +
            "   return redis.call(\"del\", KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end"
        , RScript.ReturnType.INTEGER, keyList, uuid);

在上述代码中,我们可以看到,KEYS[1]被用作eval()方法中的第一个参数,即Lua脚本中的KEYS[1]。该键表示的是lockKey,当解锁时,Lua脚本会先通过该键在Redis缓存中寻找锁占用的信息,然后判断当前线程是否持有锁,如果是,则删除该锁并将Lua脚本的返回值值为1,否则直接返回0。因此,KEYS[1]在这里被用作了Redis缓存中lockKey键的表达式,如果该锁被当前uuid所占用,解锁成功删除该锁的时候就会返回1,否则则不会返回1。

缺点

假设我们的Redis有一个主节点和一个从节点,他们的数据是相同的。现在我们在主节点上维护一个分布式锁,当一个客户端占用了这个锁后,会往主节点存入一个key-value,其中key代表锁,value代表客户端标识。

但是,如果主节点突然宕机了,从节点会成为新的主节点。虽然数据是一样的,但是这个过程中会有个延迟,新的主节点可能还没有接收到旧的主节点上的数据。这时候,如果另一个客户端想要占用这个锁,它会去新的主节点上占用,而第一个客户端以为自己还在占用着这个锁,然后就会导致多个客户端同时占用这个锁。

维护分布式锁需要各个节点之间的数据同步,而Redis只有主从复制,没有多主节点。因此,为了避免这个问题,需要使用Redis集群方案,或者采用RedLock锁的方案来避免单点故障。

Redis的主备切换机制会导致分布式锁方案中存在的问题,这也是容易出现的一种情况。当Redis主机宕机时,从机会随机升为主机,并开始执行原主机的所有任务。此时,因为旧主机上的锁在新主机上没有同步,那么新主机和旧主机之间会存在数据不一致的问题,此时多个客户端在新主机和旧主机上同时发送加锁请求,新主机和旧主机上的加锁请求都能获得锁,从而导致锁的并发使用。为了避免这种问题,需要使用Redis集群的方案,使所有节点都维护全局的锁占用情况,并在所有节点之间同步锁相关的信息,从而避免以上问题。另外,Redisson也提供了RedLock锁的方案,它利用多个Redis实例来实现分布式锁,从而避免单点故障问题。

相关方法使用

下面介绍一些 Redisson 分布式锁中常用的方法:

1. org.redisson.api.RLock.lock()

该方法用于获取分布式锁,它会一直阻塞当前线程,直到获取到分布式锁。它会这样做是因为,在第一次尝试获取锁失败之后,需要等待其他线程释放锁,然后再次尝试获取锁。

RLock lock = redissonClient.getLock("myLock");
lock.lock();
try {
    // 执行业务代码块
} finally {
    lock.unlock();
}

上面的代码将首先获取 myLock 名称的分布式锁,然后调用 lock() 方法以获取锁。执行完业务代码块后,在 finally 块中释放锁。

2. org.redisson.api.RLock.tryLock()

该方法会尝试获取分布式锁,它不会阻塞当前线程,而是在获取锁失败时立即返回 false。它可以使用 waitTime 参数来设置等待时长。

RLock lock = redissonClient.getLock("myLock");
if (lock.tryLock(waitTime, TimeUnit.SECONDS)) {
    try {
        // 执行业务代码块
    } finally {
        lock.unlock();
    }
} else {
    // 获取锁失败
}

上面的代码首先尝试获取名称为 myLock 的分布式锁,等待时长为 waitTime(秒)。如果获取锁成功,则执行业务代码块并在 finally 块中释放锁;否则,获取锁失败。

3. org.redisson.api.RLock.tryLock(long time, TimeUnit unit)

该方法是另一个尝试获取锁的方法,与 tryLock() 方法类似,不同的是它可以设置等待的时间,如果超时还未获取到锁,则返回 false

RLock lock = redissonClient.getLock("myLock");
if (lock.tryLock(waitTime, TimeUnit.SECONDS)) {
    try {
        // 执行业务代码块
    } finally {
        lock.unlock();
    }
} else {
    // 获取锁失败
}

tryLock() 方法类似,上面的代码将首先获取名称为 myLock 的分布式锁,如果在等待时间内获取到锁,则执行业务代码块并在 finally 块中释放锁;否则,获取锁失败。

4. org.redisson.api.RLock.unlock()

该方法用于释放分布式锁,它必须在获取锁成功后调用,否则会抛出异常。

RLock lock = redissonClient.getLock("myLock");
lock.lock();
try {
    // 执行业务代码块
} finally {
    lock.unlock();
}

上面的代码将首先获取名称为 myLock 的分布式锁,然后调用 lock() 方法以获取锁,在执行业务代码块后,在 finally 块中释放锁。

5. org.redisson.api.RLock.isLocked()

该方法用于判断分布式锁是否被占用,如果分布式锁被占用,则返回 true;否则,返回 false

RLock lock = redissonClient.getLock("myLock");
if (lock.tryLock(10, TimeUnit.SECONDS)) {
    try {
        // 执行业务代码块
    } finally {
        lock.unlock();
    }
} else {
    if (lock.isLocked()) {
        // 锁被占用
    } else {
        // 获取锁失败
    }
}

上面代码中的 isLocked() 方法用于判断名称为 myLock 的分布式锁是否被占用。如果在等待时间内未能获取锁,则使用 isLocked() 方法检查锁是否被占用。

6. org.redisson.api.RLock.getHoldCount()

该方法用于获取当前线程持有指定分布式锁的次数。

RLock lock = redissonClient.getLock("myLock");
lock.lock();
try {
    // 第一次获取锁
    System.out.println("Hold Count : " + lock.getHoldCount());
    if (lock.tryLock(5, TimeUnit.SECONDS)) {
        try {
            // 第二次获取锁
            System.out.println("Hold Count : " + lock.getHoldCount());
        } finally {
            lock.unlock();
        }
    }
} finally {
    lock.unlock();
}

上面的代码首先获取名称为 myLock 的分布式锁,然后调用 lock() 方法以获取锁。获取到锁后,在业务代码块中调用 getHoldCount() 方法获取当前线程持有锁的次数,在执行完第一个业务代码块后,再次尝试获取锁,并再次调用 getHoldCount() 方法,最后完成释放锁操作。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路上阡陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值