悲观锁和乐观锁
分布式锁的实现要求 – 互斥性、避免死锁、可重入性、高可用行、性能
分布式锁应该具有:
互斥性:任意时刻只能有一个客户端持有锁
锁超时释放: 锁超时会自动释放,防止死锁
可重入性: 一个线程获取锁之后可以再次对请求加锁
高可用、高性能:加锁和解锁需要开销尽可能低,同时要保证高可用
安全性:锁只能被持有客户端删除,不能被其他客户端删除
互斥-只能有一个客户端持有锁 – redis setnx
避免死锁
引入过期时间 – redis ttl
比如TTL为5秒,进程A获得锁
问题是5秒内进程A并未释放锁,被系统自动释放,进程B获得锁
刚好第6秒时进程A执行完,又会释放锁,也就是进程A释放了进程B的锁
仅仅加个过期时间会设计到两个问题:锁过期和释放别人的锁问题
锁过期问题 – 自动续期 – redis
锁过期问题的出现,是我们对持有锁的时间不好进行预估,设置较短的话会有【提前过期】风险,但是过期时间设置过长,可能锁长时间得不到释放。
这种情况同样有处理方式,可以开启一个守护进程(watch dog),检测失效时间进行续租,比如Java技术栈可以用Redisson来处理。
释放别人锁问题 – 锁附加唯一性 – 给每个客户端设置唯一ID?
锁key的值附加唯一性:针对释放别人锁这种问题,我们可以给每个客户端进程设置【唯一ID】,作为锁(所有客户端锁key相同)的值,这样我们就可以在应用层就进行检查唯一ID。
可重入 redis+lua计数器
一个线程获取了锁,但是在执行时,又再次尝试获取锁会发生什么情况?
是的,导致了重复获取锁,占用了锁资源,造成了死锁问题。
我们了解下什么是【可重入】:指的是同一个线程在持有锁的情况下,可以多次获取该锁而不会造成死锁,也就是一个线程可以在获取锁之后再次获取同一个锁,而不需要等待锁释放。
解决方式:比如实现Redis分布式锁的可重入,在实现时,需要借助Redis的Lua脚本语言,并使用引用计数器技术,保证同一线程可重入锁的正确性。
容错
容错性是为了当部分节点(redis节点等)宕机时,客户端仍然能够获取锁和释放锁,一般来说会有以下两种处理方式:
- 一种像etcd/zookeeper这种作为锁服务能够自动进行故障切换,因为它本身就是个集群
- 另一种可以提供多个独立的锁服务,客户端向多个独立锁服务进行请求,某个锁服务故障时,也可以从其他服务获取到锁信息,但是这种缺点很明显,客户端需要去请求多个锁服务。
主从一致性
redis集群(包括redis
Redis分布式缓存实现–Redisson
使用代码
1. Maven 依赖导入

2. 配置文件参数配置(需要根据你的情况进行修改)

3. 创建 Redisson 客户端

4. 使用分布式锁
在需要使用分布式锁的地方注入RedissonClient实例,并使用getLock方法创建一个分布式锁对象(RLock)。

方法说明
1. RLock.lock()
使用 Rlock.lock() 方法时 ,如果当前没有其他线程或进程持有该锁,那么调用线程将立即获得锁定,并继续执行后续的代码。如果其他线程或进程已经持有了该锁,那么调用线程将被阻塞,直到该锁被释放为止。
此外,Rlock.lock() 方法还具有以下特点:
- 可重入性: 同一个线程可以多次调用 Rlock.lock() 方法而不会造成死锁,只需确保每次 lock() 调用都有相应的 unlock() 调用与之匹配。
- 超时机制: 可以通过 lock() 方法中的参数设置等待锁的超时时间,避免因为无法获得锁而一直等待。
- 自动续期: 当线程持有锁的时间超过设置的锁的过期时间时,Redisson 会自动延长锁的有效期,避免因为业务执行时间过长而导致锁过期。
- 防止死锁: Redisson 通过唯一标识锁的 ID 来区分不同的锁,防止发生死锁。
2. RLock.unlock()
RLock.unlock()方法用于释放由Redission分布式锁所保护的资源。它允许持有锁的线程主动释放锁,从而允许其他线程获取该锁并访问共享资源。
注意事项:
- RLock.unlock()方法应该在保护的临界区代码执行完毕后进行调用,以确保锁的及时释放。
- 在多线程环境下,释放锁的顺序应该与获取锁的顺序相对应,以避免死锁或资源争用的问题。
- 如果当前线程没有持有锁,调用RLock.unlock()方法不会抛出异常,也不会影响其他线程。
- 如果Redisson客户端刚加锁成功,并且未指定leaseTime,后台会启动一个定时任务watchdog, 每隔10s检查key,key如果存在就为它⾃动续命到30s后;在watchdog定时任务存在的情况下,如果不是主动释放锁,那么key将会⼀直的被watchdog这个定时任务维持加锁。但是如果客户端宕机了,定时任务watchdog也就没了,也就没有锁续约机制了,那么过完30s之后,key会⾃动被删除、key对应的锁也自动被释放了。
互斥性 – SET lKey randId NX PX 30000
不能用setnx lkey lvalue expire lockKey 30
//保证原子性执行命令
SET lKey randId NX PX 30000
Redission实现-lua脚本:
redis锁数据结构(数据类型为HSet):<lockKey, <线程key, 可重入计数器>>
线程key=GUID+当前线程ID,value为可重入计数器
解决了以下问题:
- 互斥性:用lua脚本保证操作的原子性进而保证了互斥性
- 锁过期问题:设置超期时间+看门狗:看门狗默认将锁的过期时间设置为30秒,并且每10秒重新设置过期时间为30秒,直到线程终止后看门狗才不再运行。
- 释放别人锁(线程key唯一标识当前线程,释放锁时,先判断redis中存储的线程key是不是当前线程,若是才释放锁)
- 可重入性:用value作为可重用计数器,每次重新加(解)锁,可重入计数器便加(减)1
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"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; " +
"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]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
避免死锁
见上方解释。
可重入性
见上方
性能
最高,高于zookeeper
高可用性(主从一致性)
普通模式(前述方式)锁的问题
事实上上面的实现琐的方式的最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
- 在Redis的master节点上拿到了锁;
- 但是这个加锁的key还没有同步到slave节点;
- master故障,发生故障转移,slave节点升级为master节点;
- 导致锁丢失。
高可用环境下(sentinel或redis cluster)的锁实现–RedLock
RedLock实现思路
正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。笔者认为,Redlock也是Redis所有分布式锁实现方式中唯一能让面试官高潮的方式。
antirez提出的redlock算法大概是这样的:
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
- 获取当前Unix时间,以毫秒为单位。
- 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该


1615

被折叠的 条评论
为什么被折叠?



