Redis-redission分布式锁
使用lua脚本执行setnx就可以实现分布式锁逻辑,经过通过调用RedisTemplate.execute可以执行改造过的lua脚本,保证命令执行的原子性。
1、通过设置过期时间可以防止服务器宕机导致的死锁。
2、通过setnx(不存在则插入)确保锁唯一。
3、通过线程标识可以防止误删他人的锁。(当setnx这种方式设置过期时间,时间一到就会释放锁。等业务执行完需要删除锁时通过线程标识判断是不是自己的锁,不是的话则不删除)
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,//脚本内容
Collections.singletonList(KEY_PREFIX + name),//key
ID_PREFIX + Thread.currentThread().getId());//value
}
**思考:**什么场景下才可以用setnx实现业务锁?
但是也存在以下问题:
重入问题:因为setnx是不存在时才进行后续操作,如果线程调用方法A时进行了加锁,期间它去调用方法B后想回到方法A,这个时候发现setnx已经存在了,因为它去调用方法B时也不会释放A的锁。这就导致了死锁的情况。
不可重试:setnx就是一条命令,若要实现重试需要自己写循环逻辑。SETNX 本身不提供任何 “等待 / 重试 / 排队” 机制
**超时释放:**我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果执行的时间与锁的时间不匹配,即使加了 Lua,不删别人锁,但业务还在执行,锁超时了 → 仍然不安全。
根本原因:SETNX + 过期时间,无法自动续期。此时两个线程同时操作共享资源 → 超卖、重复扣款、脏数据。
主从一致性: 因为redis的主从同步是异步的,如果当我们向集群写数据时,主机需要异步的将数据同步给从机,当一个线程获取到锁后去操作业务,但是如果同步完成之前主节点宕机,就会导致新主节点未获取到该锁消息,导致其他线程也获取到该锁。就会导致两个线程同时操作一个业务代码以及数据。
Redission
可重入锁(Reentrant Lock)
核心特性:同一线程可多次获取同一把锁,不会自己阻塞自己,底层通过线程标识 + 重入计数器实现。
看门狗机制:自动续期锁的过期时间,避免业务执行时间过长导致锁提前释放。
适用场景:分布式环境下的方法嵌套调用、循环加锁等需要重复获取锁的场景.
公平锁(Fair Lock)
核心特性:保证线程按请求锁的顺序获取锁(FIFO 队列),避免 “饥饿” 问题。
底层实现:通过 Redis 的 List + ZSet 结构模拟等待队列,按时间戳排序。
适用场景:对执行顺序有严格要求的业务,如订单处理、任务调度。
联锁(MultiLock)
核心特性:将多个独立的 RLock 组合为一个逻辑锁,必须所有子锁都获取成功才算加锁成功,释放时也需释放所有子锁。
适用场景:需要跨多个 Redis 实例 / 节点协同加锁的场景,如分布式事务、多资源互斥。
红锁(RedLock)
核心特性:Redisson 对 Redis 官方红锁算法的实现,向多个独立主节点加锁,超过半数成功则认为加锁成功,降低主从切换丢锁的概率。
本质:通过 “多数派” 思想提升分布式锁的一致性,缓解 Redis 主从异步复制的缺陷。
适用场景:对锁安全性要求极高、无法容忍锁失效的核心业务。
读写锁(ReadWriteLock)
核心特性:分为读锁和写锁,读读不互斥、读写互斥、写写互斥,提升读多写少场景的并发性能。
底层实现:基于 Redis 的 Hash 结构存储读写状态,通过 Lua 脚本保证原子性。
适用场景:缓存查询、配置读取等读远多于写的业务。
信号量(Semaphore)
核心特性:控制并发访问的最大许可数,允许多个线程同时获取资源,达到上限后阻塞等待。
适用场景:接口限流、数据库连接池、资源池等需要限制并发数的场景。
可过期性信号量(PermitExpirableSemaphore)
核心特性:在普通信号量基础上,为每个许可增加过期时间,超时后自动释放许可,避免许可泄露。
适用场景:需要临时占用资源且必须自动回收的场景,如临时锁、限时任务。
闭锁(CountDownLatch)
核心特性:设置一个计数器,线程调用 await() 等待,其他线程完成任务后调用 countDown() 递减计数器,计数器归零时唤醒所有等待线程。
适用场景:分布式环境下的任务协同,如等待所有服务启动完成、多节点数据汇总后再执行后续逻辑。
简单记:
普通业务 → 可重入锁
读多写少 → 读写锁
限流 → 信号量
高一致 → 红锁
排队顺序 → 公平锁
可重入锁ReentrantLock
final boolean tryLock() {
Thread current = Thread.currentThread();
int c = getState();//标识判断是第一次获取锁还是重入
if (c == 0) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}
ReentrantLock通过State属性进行记录重入次数,可重入锁底层 = 计数器 (state) + 线程标记。会通过调用以下方法将锁赋值一个线程:
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}

Redission看门狗机制
**核心源码:**在RedissonLock#tryLock
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
//waitTime 最大等待时间(抢不到锁,最多等多久)leaseTime锁自动释放时间(持有多久自动释放),且该值只有为-1时看门狗才工作 unit时间单位(秒/毫秒/分钟)
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();//获取当前时间
long threadId = Thread.currentThread().getId();//哪个线程
Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);//第一次获取锁如果ttl不为空则说明已经被持有,并返回过期锁的过期时间
if (ttl == null) {
return true;
} else {
//如果超时了直接false
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
return false;
} else {//如果没有超过等待时间
current = System.currentTimeMillis();
RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
//创建一个线程去等待,醒过来后看一下锁的过期时间到了
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
//取消订阅Redis锁释放的消息,如果取消失败则说明已经完成订阅
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
this.unsubscribe(subscribeFuture, threadId);
}
});
}
this.acquireFailed(waitTime, unit, threadId);
return false;
} else {
try {
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
boolean var20 = false;
return var20;
} else {
//睡醒还没超等待时间
boolean var16;
do {
long currentTime = System.currentTimeMillis();
ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
var16 = true;
return var16;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
var16 = false;
return var16;
}
currentTime = System.currentTimeMillis();
if (ttl >= 0L && ttl < time) {
((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
} while(time > 0L);
this.acquireFailed(waitTime, unit, threadId);
var16 = false;
return var16;
}
} finally {
this.unsubscribe(subscribeFuture, threadId);
}
}
}
}
}
假设线程出现宕机的情况下,由于没人去调用看门狗的续期方法renewExpiration()则到期后就会直接释放锁,因此不会导致死锁的情况。
总结: 开启看门狗模式下,TryLock方法会第一次获取锁失败后,根据返回的锁过期时间进行等待。如果唤醒后发现已经过了最长等待时间则直接返回false,否则就去再次去抢锁,只要等待时间还没到,就会一直抢。
抢到锁后会调用tryAcquireAsync方法Future线程去进行续约。
更多推荐



所有评论(0)