库存超卖问题
分析:
超卖问题属于多线程安全问题,解决方案是加锁,如下
悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行
- 例如:Synchronized、Lock都属于悲观锁
乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改
- 如果没有修改则认为是安全的,自己才更新数据。
- 如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:
虽然乐观锁解决了超卖问题,但是存在成功率降低的问题
解决:将SQL语句where id = ?and stock = 1 改成 where id = ?and stock > 0
这样的话,使用的不是乐观锁,而是行锁中的排他锁解决超卖
分布式锁基本原理:
概念:满足分布式系统或集群模式下多进程可见并且互斥的锁
实现方式对比:
Redis分布式锁实现思路:由于redis是单线程的,使用命令之后,只能有⼀个客户端对某一个key设置值,在没有过期或删除key的时候,其他客户端是不能设置这个值的。
获取锁:SetNX命令
- 互斥:确保只能有一个线程获取锁
- 非阻塞:尝试一次,成功返回true,失败返回false
# 添加锁,NX是互斥、EX是设置超时时间
SET lock thread1 NX EX 10
释放锁:
- 手动释放
- 超时释放:获取锁时添加一个超时时间
# 释放锁,删除即可
DEL key
上边Redis分布式锁存在误删问题:
解决Redis分布式锁误删问题:
上述分布式锁存在原子性问题:
Lua脚本解决多条命令原子性问题:Redis与Lua预防库存超卖,Lua可以将redis的多个操作合成⼀个脚本,然后整体执行,在脚本的执行中,不会出现资源竞争的情况,即保证了操作的原子性
释放锁的业务流程是这样的:
1.获取锁中的线程标示
2.判断是否与指定的标示(当前线程标示)一致
3.如果一致则释放锁(删除)
4.如果不一致则什么都不做
如果用Lua脚本来表示则是这样的:
这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
获取锁中的标示,判断是否与当前线程标示一致
if(redis.call('GET',KEYS[1])== ARGV[1]) then
一致,则删除锁
return redis.call('DEL', KEYS[1])
end
不一致,则直接返回
return 0
Redisson
基于setnx实现的分布锁存在以下问题:
- 不可重入:同一个线程无法多次获取同一把锁
- 不可重试:获取锁只尝试一次就返回false,没有重试机制
- 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
- 主从一致性:如果Redis提供了主从集群主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
Redisson可重入锁原理:
获取锁Lua脚本:
local key = KEYS[1];-- 锁的key
local threadId = ARGV[1];-- 线程唯一标识
local releaseTime=ARGV[2];--锁的自动释放时间
--判断是否存在
if(redis.call('exists',key)==0)then
-- 不存在,获取锁
redis.call('hset',key,threadId,'1');
-- 设置有效期
redis.call('expire', key, releaseTime);
return1;--返回结果
end ;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists",key,threadId).== 1)then
--不存在,获取锁,重入次数+1
redis.call('hincrby", key, threadId,"1');
--设置有效期
redis.call('expire",key, releaseTime);
return1;--返回结果
end ;
return 0;--代码走到这里,说明获取锁的不是自己,获取锁失败
释放锁Lua脚本:
local key = KEYS[1];--锁的key
local threadId= ARGV[1];-- 线程唯一标识
local releaseTime = ARGV[2];--锁的自动释放时间
--判断当前锁是否还是被自己持有
if(redis.call('HEXISTs',key,threadId)== 0)then
return nil;如果已经不是自己,则直接返回
end;
--是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
--判断是否重入次数是否已经为0
if(count>0)then
--大于0说明不能释放锁,重置有效期然后返回
redis.call('EXPIRE', key, releaseTime);
return nil;
else --等于0说明可以释放锁,直接删除
redis.call('DEL',key);
return nil;
end ;
Redisson可重试锁和WatchDog原理:
Redisson分布式锁原理总结:
- 可重入:利用hash结构记录线程id和重入次数
- 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
- 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间
Redisson分布式锁主从一致问题:
解决方法:Redisson的MultLock原理指多个独立的Redis节点,必须在所有节点都获取重入
锁,才算获取锁成功