Redis:基于SETNX解决分布式锁误删问题

文章介绍了使用Redis的SETNX命令实现分布式锁来防止一人一单问题时可能出现的锁误删现象。通过在锁中添加线程标识并在释放锁时进行校验来改进,但此方法仍存在误删风险。进一步优化是使用Lua脚本来保证释放锁操作的原子性,确保判断和释放的完整性。最后,提出了Redission分布式锁作为更完善的解决方案,它支持可重入、重试机制和超时续约,以提高系统的高可用性和一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.概述

前提:解决“一人一单”问题时,使用Redis互斥锁 锁住联合查询、扣库存、创建订单的步骤,防止同一个userID的多个线程去创建订单,最后在finally{ } 释放锁;

问题:出现锁的误删现象;

分析
当线程1获取锁,而此时线程1执行的业务因为某些情况被 阻塞,阻塞的时间太长,TTL到期导致锁被释放;
这时线程2就能获取锁;线程2执行自己的业务;
假设此时线程1 阻塞完了,业务完成,执行DEL 释放锁的逻辑!则线程2的锁被释放了!(误删)
由于锁被删除,则线程3也能获取锁;此时就有两个线程并行执行!
在这里插入图片描述

原因
1.业务阻塞导致线程1的锁提前释放;
2.线程1释放了线程2的锁;

解决
上锁时 添加 线程标识;
解锁时 判断 线程标识
在这里插入图片描述

二. 分布式锁(初级)

需求
1.获取锁时的value使用 UUID + 线程ID 作为线程标识
2.释放锁时先获取锁中的线程标识,判断 于当前线程标识是否一致!
一致则释放锁;
不一致则不释放;

(1)锁接口

在这里插入图片描述

(2)锁实现类+上锁

使用 SETNX / setIfAbsent 作为互斥锁;
key 是传入的name;
value =UUID+线程ID(保证不易冲突),UUID使用hutool中的UUID.randomUUID.toString,会去掉默认的横线 “-”;

在这里插入图片描述

(3)释放锁

先通过key获取当前线程的线程标识(value);
然后判断当前锁中的线程标识是否等于锁的线程标识即value;
在这里插入图片描述

注意:这里的UUID是静态static修饰的,类加载的时候就会产生,同一个项目(JVM)内UUID相同,不同的项目(JVM)UUID则不同,这样就能防止线程ID作为标识存在不同JVM之间重复冲突的问题;

总结:通过上锁时添加线程标识解锁时判断线程标识,解决了锁误删,提高健壮性!

(4)存在的问题

依然可能产生误删!
线程1判断成功准备释放锁,此时线程1 阻塞,如GC中的stw;(判断锁和释放锁是两个动作)
当阻塞时间够长超过TTL,则锁超时释放
此时线程2可以获取锁,并执行业务;
此时线程1阻塞结束,而之前已经判断过锁了,线程1认为锁是自己的,所以直接释放锁,再次造成误删
在这里插入图片描述
原因:判断锁和释放锁是两个动作!应该有原子性!

三. 改进释放锁

由于判断锁和释放锁是两个动作,【当判断锁和释放锁之间】产生了阻塞,则会导致误删;

解决
使判断锁释放锁具有 原子性

需求:使用基于Lua脚本实现分布式锁的释放锁逻辑;
使用RedisTemplate中的Excute() 方法来执行Lua脚本,参数分别是:脚本、KEYS参数集合、ARGS参数集合;(不需要指定key的个数)

(1)准备unlock.lua脚本

KEY参数传入锁的key;
AVG参数传入当前线程标识;
在这里插入图片描述

并将unlock.lua 脚本放到项目的 resources 中下;
在这里插入图片描述

(2)提前读取脚本

为了避免在使用脚本的时候才去读脚本,造成IO流影响性能,使用DefaultRedisScript 读取脚本,使用静态代码块提前读取;
在这里插入图片描述

(3)实现释放锁

执行脚本实现释放锁,此时只有一行代码(在Lua脚本中),可以保证判断线程标识和释放锁的 原子性
使用RedisTemplate的 execute() 方法执行Lua脚本;
KEYS参数必须是集合,使用Collections.singletonList快速生成;

在这里插入图片描述

总结
利用SETNX 实现互斥性
利用EXPIRE 保证超时释放
使用Redis集群保证高可用,高并发;
运行Lua脚本保证原子性

key:将用户ID作为锁的key,保证同一个用户ID获取同一把锁,而不同用户之间不阻塞!
value:为了防止锁误删,使用线程标识作为value 以区别不同的线程,释放锁的时候先判断再释放;

四. 基于SETNX实现的分布式锁的问题

  1. 不可重入(同一个线程多次获取同一把锁,如A方法调用方法B,A中获取了锁,而B又要获取同一把锁,就需要可重入锁)

  2. 没有重试机制(自旋),没有获取到锁就立即失败;

  3. 超时释放 虽然可以防止死锁,但是会导致业务还没执行完锁就被释放/误删

  4. 主从一致性(读写分离模式):Redis有一个主节点和多个从节点,当执行写操作访问主节点,读操作访问从节点;(主从会同步但是有延迟)
    当主节点正在将数据同步到从节点时突然宕机,此时会选择一个从节点作为主节点,客户端再向新的主节点发送请求,但由于数据未同步完成,此时锁失效了

解决:使用 Redission分布式锁;

  1. 可重入:基于Hash结构,使用Hash的value来记录锁重入的次数(类似Reentrantlock的state

  2. 可重试:借助发布订阅模式,第一次尝试获取锁失败以后不会立即失败,而是会等待释放锁的消息,而获取锁成功的线程在释放的时候会发送消息,则等待的线程捕获到消息会再次尝试获取锁;如果再失败,则继续等待释放锁的信号,捕获信号后再次尝试获取锁;超过设置的持续时间就不再重试;

  3. 超时续约:利用watchDog看门狗,【获取锁成功后】会开启一个定时任务,这个任务每隔一段时间就去重置锁的超时时间

  4. 主从一致性:使用 MultiLock 联锁
    不要主从模式,节点都是独立的读写;
    之前客户端获取锁只需要找到Master节点,而现在给每个Redis节点都配置一个Redsission锁,然后使用getMultiLock()将其连起来,形成联锁
    假设有节点宕机,因为锁依然存在,所以锁依然有效;
    在这里插入图片描述
    还可以再使用主从同步,进一步提高可靠性:
    在这里插入图片描述
    此时假设一个Master宕机,则其他线程需要获取每一个节点的锁才能成功! 所以是安全的;
    之前客户端获取锁只需要找到Master节点,而现在需要依次向多个Redis节点都去获取锁,都要保存锁的标识,全部获取才能成功;
    假设有节点宕机,因为锁依然存在,所以锁依然有效;

此时实现了高可用,同时避免了主从一致导致的锁时效问题;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值