【Redis基础和应用】(六)分布式锁

分布式锁

1. 基本实现

分布式锁本质上要实现的目标就是在Redis里面占一个"坑",当别的进程也要来占坑时,如果发现已经有一个坑了,就要放弃或者稍后再试。

分布式锁的实现主要依赖setnx(set if not exists)这个指令实现,只允许一个客户端占坑。先来占坑,用完了,再调用del指令释放坑。

相关指令

# 为了方便理解, 这里的Key就是“lock:codehole"
> setnx lock:codehole true
OK
# do something,执行业务逻辑
> del lock:codehole
(integer) 1

但是这里有一个问题,如果逻辑执行中出现异常,可能会导致del lock:codehole这个指令没有被调用,那么就会陷入死锁,锁永远无法释放。

为了解决这个问题,需要在拿到锁以后,在给锁加上一个过期时间,比如5s,这样即使执行过程中出现了问题,锁过了过期时间也会自动释放

> setnx lock:codehole true
OK
> expire lock:codehole 5
# do something
> del lock:codehole
(integer) 1

但是以上逻辑还有问题,如果在setnx和 设置expire指令之间,服务器突然发生异常,就可能导致expire得不到执行,也会造成死锁。

这种问题的根源就在于setnxexpire不是原子指令。但是这里不能使用redis的事务来解决,因为expire是依赖setnx的执行结果的,如果setnx没有抢到锁,expire是不应该执行的。(如果没有锁,怎么设置锁的过期时间呢?)事务里面没有if-else分支,要么全部执行,要么一个都不执行。

为了解决这个难题,Redis开源社区出现了很多分布式锁的工具包。这意味着不仅仅要学会使用Jedis,同时还需要引入分布式锁的library,随后,Redis2.8版本中,作者加入了set指令的扩展参数,使得setnx可以和expire一起执行,解决了分布式锁的乱象。

> set lock:codehole true ex 5 nx # set lock:codehole = true expire is 5s if key is not sxists
OK
# do something
> del lock:codehole

其中,set <key> <value> ex <time> nx就是分布式锁的奥义所在。

2. 超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的业务逻辑执行时间太长,长到超过锁的过期时间,那么就会出现问题。

因为在执行业务逻辑的时候,锁过期了,那么就会有其他线程重新获取到这把锁,导致临界区同时有两个线程在执行,那么就会导致执行结果出现错误。

因此,Redis分布式锁不适合较长时间的任务,如果真的出现了错误,造成较小的数据错乱就可能需要人工介入。

tag = random.nextint()
if redis.set(key, tag, nx=True, ex=5):
    do_something()
    redis.delifequals(key, tag) # 假象的del if equals

有一个稍微安全的方案是,将set的指令的value参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再(删除Key)释放锁,这是为了确保当前线程占有的锁不会被其他线程释放,除非这个锁是因为过期被服务器释放的。

但是匹配随机数和删除key不是一个原子操作,Redis没有提供类似于delifequals的指令,这时就需要Lua脚本来处理了,因为Lua脚本可以保证连续多个指令的原子性执行。

3. 可重入性

可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么这个锁就是可重入的。比如Java中的ReentrantLock。但是Redis分布锁如果要支持可重入,就需要对客户端的set方法进行包装,使用线程的Threadlocal变量存储当前持有锁的计数。

在此,作者不推荐使用可重入锁,因为它加重了客户端的复杂度,在编写业务方法时注意在逻辑结构上进行调整,避免使用可重入锁。

4. 集群环境下的分布式锁

上述的分布式锁的实现是使用一条指令就可以完成,不过在集群环境下,这种方式是有缺陷的,它不是绝对安全的。

比如:在Sentinel集群环境中,当主节点挂掉时,从节点会取而代之,但是客户端上并没有明显感知。比如,原先第一个客户端在主节点上申请成功了一把锁,但是这把锁还没来得及同步到从节点,主节点突然挂掉了,然后从节点变成了主节点,这个新的主节点没有这把锁,所以当另一个客户端就可以立即获取锁,导致了一定的不安全性。

不过这种安全尽在主从发生failover的情况下才会产生,而且持续时间很短,业务系统多数情况下可以容忍。

Redlock算法

为了解决这个问题,Antirez发明了Redlock算法,已经有很多开源的library做了良好的封装,用户可以直接使用redlock-py

import redlock

addrs = [{
    "host":"localhost",
    "port": 6379,
    "db":0
},{
    "host":"localhost",
    "port": 6479,
    "db":0
},{
    "host":"localhost",
    "port": 6579,
    "db":0
}]
dlm = redlock.Redlock(addrs)
success = dlm.lock("user-lck-leesure", 5000)
if success:
    print("lock success")
    dlm.unlock("user-lck-leesure")
else:
    print("lock failed")

为了使用Redlock,需要提供多个Redis实列,这些实列之间相互独立,没有主从关系,同很多分布式算法一样,Redlock也是用大多数机制

加锁时,他会向过半结点发送set(key,value, nx=True, ex=xxx)指令,只要过半的结点set成功,就认为加锁成功。

释放锁时,需要向所有结点发送del指令,不过Redlock算法还需要考虑出错重试,时钟漂移等很多细节问题,同时,因为Redlock需要向多个结点进行读写,也就是相比单个实例Redis的性能会下降一些。

Redlock使用场景

如果你很在乎高可用性,希望即时挂了一台Redis后也完全不受影响,就应该考虑Redlock,不过代价也是有的,需要更多的Redis实列,性能也下降了。代码上还需要引入额外的library,运维上也需要特殊对待,这些都是需要考虑的成本,使用前请再三斟酌。

### Redis 分布式锁Redisson 的实现原理 #### 1. **Redis 分布式锁** Redis 分布式锁的核心思想是通过 `SETNX` `EXPIRE` 来实现加锁操作。在早期版本(Redis 2.6.12之前),由于 `SETNX` 不支持设置过期时间,因此需要分两步完成:先调用 `SETNX` 创建键值对表示锁定状态,再调用 `EXPIRE` 设置过期时间以防止死锁[^3]。 然而这种两步操作无法保证原子性,可能会因网络延迟或其他异常导致锁未成功创建却设置了过期时间。为此,Redis 2.6.12引入了新的命令 `SET key value NX PX milliseconds`,其中 `NX` 表示只有当键不存在时才执行设置操作,`PX` 则用于指定毫秒级的过期时间。这种方式能够在一个命令内完成加锁并设定超时,从而有效解决了上述问题。 #### 2. **Redisson 实现分布式锁** Redisson 是基于 Redis 开发的一个 Java 客户端库,它不仅实现了更高级别的抽象接口,还提供了多种类型的分布式对象服务功能。对于分布式锁而言,Redisson 提供了一种更加健壮可靠的解决方案——`RLock` 接口及其子类实例化方式。 - **加锁逻辑** Redisson 使用 Lua 脚本来确保整个加锁过程具有原子性。该脚本会检查目标资源是否已被占用;如果未被占用,则尝试获取锁并将当前线程 ID 记录下来作为持有者的唯一标识符[^1]。 - **续命机制** 当某个客户端成功获得锁之后,Redisson 会在后台启动一个定时器任务定期向服务器发送续约请求延长锁的有效期限,直到显式解锁为止。此设计可以避免因长时间运行的任务而导致锁提前失效的情况发生[^2]。 - **自旋重试策略** 如果初次未能取得所需资源,则按照预定义间隔不断重复尝试直至达到最大等待时限或最终放弃争夺控制权。 - **公平性可靠性保障措施** 在某些特殊情况下(比如网络分区), 可能会出现部分节点认为自己已经拿到了全局唯一的锁,但实际上其他地方也有竞争者存在的情形下, redisson 还特别考虑到了这一点并通过内部复杂的协调算法尽可能减少冲突概率[^4]. #### 性能对比分析 | 特性 | Redis 原生分布锁 | Redisson | |-------------------------|------------------------------------------|----------------------------------| | 加锁效率 | 较高 | 略低 | | 锁安全性 | 存在网络抖动等问题 | 更安全可靠 | | 功能扩展能力 | 单纯提供基础加解鎖功能 | 支持更多特性如自动续租、可重入等 | | 易用程度 | 需要开发者手动处理很多细节 | API 封装良好易于集成 | 从表中可以看出虽然原生态方法简单高效但在实际应用过程中往往面临诸多挑战;而借助第三方工具包则可以在一定程度上弥补这些不足之处. ```java // 示例代码展示如何利用Redisson进行分布式锁管理 import org.redisson.api.RLock; import org.redisson.api.RedissonClient; public class DistributedLockExample { private final RedissonClient redissonClient; public void acquireAndReleaseLock(String lockName) throws InterruptedException{ RLock lock = redissonClient.getLock(lockName); try { boolean isLocked = lock.tryLock(10, TimeUnit.SECONDS); // 尝试获取锁最长等待时间为10秒 if(isLocked){ System.out.println(Thread.currentThread().getName()+" acquired the lock."); Thread.sleep(5000L); // Simulate some work }else{ System.err.println("Failed to get lock after waiting..."); } } finally { if(lock.isHeldByCurrentThread()){ lock.unlock(); System.out.println(Thread.currentThread().getName()+ " released the lock."); } } } } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

企鹅宝儿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值