redis-分布式锁
高并发场景秒杀抢购超卖BUG
场景: 多个请求同时读取到剩余库存,各自判断库存容量符合抢购条件,导致减库存操作执行异常。
public String deductStock() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" \+ realStock);
} else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
当多个请求同时扣减库存时导致最后库存不准确。
解决方式:jvm级锁与分布式锁
高并发场景下jvm级锁与分布式锁:
synchronized: 在分布式架构中,当请求分配到不同服务器上时,每个服务器上的jvm锁都会认为自己获得了锁,允许请求进入,导致锁失效。
分布式锁:把锁从每个进程内部移到所有进程都能访问的外部独立系统中,让所有服务器竞争同一把锁。
分布式锁简单版本
命令:SETNX key value:如果key不存在,将key的值设置为value,存在则不做任何操作。
public String deductStock() {
String lockKey = "lock:product_101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xxx");
if (!result)
return "error_code";
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
// try--catch此时作用-- 防止程序执行时出现异常导致锁未释放,导致死锁。此时未设置超时时间。
分布式锁设置超时时间
添加代码 :
stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
当前问题: 加锁与设置超时时间代码在redis执行性无法保证一起顺序执行。redis就执行命令单线程的,命令需要排队执行,执行这两个命令时可能被其他请求执行命令进行加塞(原子性问题)
原子性:单线程保证
使用组合指令
代码如下:
public String deductStock() {
String lockKey = "lock:product_101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xxx",10,TimeUnit.SECONDS);
if (!result)
return "error_code";
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" \+ realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
并发量大时,依然存在锁失效问题
假设3个线程 threadA,threadB,threadC,threadA获取锁后10s未执行完库存扣减逻辑,此时锁已经失效,threadB获取到锁开始执行5s,threadA同时继续执行5s且执行释放锁逻辑,此时ThreadA释放threadB的锁。threadC获取到锁开始执行5s,threadB同时继续执行5s且执行释放锁逻辑,此时ThreadB释放threadC的锁。导致锁一直是失效状态。
在高并发场景线程执行顺序完全不可预料,上述只是极端情况。
问题根本点:当前线程加的锁被其他线程释放 .
解决:给每个线程分配唯一ID。(不可以用ThreadId,因为每一台服务器可能存在相同的ThreadId)
代码如下
public String deductStock() {
String lockKey = "lock:product_101";
String clientId = UUID.randomUUID().toString(); //唯一id
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId,10,TimeUnit.SECONDS);
if (!result)
return "error_code";
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) { // 通过唯一id区分是否是当前线程的锁
stringRedisTemplate.delete(lockKey); //删除锁
}
}
return "end";
}
此时依然存在锁失效问题:
当ThreadA在执行区分是否是当前线程的锁逻辑后出现卡顿,在这个卡顿时间中锁失效,此时ThreadB获取到锁,THreadA继续执行依旧可以释放ThreadB的锁。(极端情况)
区分是否是当前线程的锁与删除锁不是原子性操作。且无原生命令解决。
根本点:超时时间过期问题
解决:
- 延长超期时间(治标不治本)
- 锁续命:主线程获取锁执行task任务时,分线程定时判断主线程是否执行结束,没有结束将key超时时间重新设置。
redisson 分布式锁原理:
核心特点:通过看门狗(Watchdog)自动续期机制,解决了锁超时释放与业务执行时间不匹配的难题,同时在加锁、解锁的各个环节都做了精密设计,确保原子性与安全性
如图:
线程1加锁成功后默认过期时间30s,redisson启动后台线程,每隔10s检查线程1是否持有锁,持有则延长线程1持有锁时间30s。
线程2获取所失败后堵塞,并订阅频道,在while循环中间歇性尝试加锁,当线程1释放锁后会像订阅的频道中发布消息,所有订阅该频道的线程都会被唤醒,尝试获取锁。
工作原理
- 初始设置:客户端加锁成功时,默认设置锁的过期时间为 30秒
- 启动续期:加锁成功后,Redisson 会为该锁启动一个看门狗线程。
- 定时检查:该线程会每隔 10秒(即
30秒 / 3)执行一次。 - 执行续期:每次执行时,它会检查锁是否仍被当前线程持有。如果是,它就通过 Lua 脚本将锁的过期时间重置为30秒
- 循环往复:只要业务还在运行,锁未被主动释放,这个“检查-续期”的过程就会无限循环下去
如何停止?
- 正常情况:当业务代码执行完毕,调用
unlock()释放锁时,Redisson 会同时取消这个看门狗定时任务 - 异常情况:如果持有锁的客户端宕机或网络中断,看门狗线程也随之消亡,无法进行续期。锁就会在最后一次续期后的30秒自动过期,避免了死锁
阻塞重试机制 不会消耗cpu资源
当一个客户端尝试获取锁失败时(锁已被他人持有),Redisson 并不会立刻返回失败,而是会进入一个高效的阻塞等待模式
- 订阅释放事件:获取锁失败的客户端会通过 Redis 的 发布/订阅(Pub/Sub) 功能,订阅一个与锁名称相关的特定频道
- 阻塞等待:线程会在一个
while(true)循环中等待,并定期尝试重新获取锁 - 收到唤醒信号:当持有锁的客户端释放锁时,它会向这个频道发布一条释放消息
- 再次尝试:所有订阅了该频道的客户端会收到通知,从等待状态中被唤醒,然后再次尝试获取锁
锁的释放
unlock() 方法的实现也至关重要,核心依然是 Lua 脚本,确保操作的原子性。
- 检查当前线程的锁是否存在
- 若不存在,说明锁已失效或未被持有,抛出异常。
- 若存在,则将重入次数(value)减1
- 如果减1后的值仍然大于0,表示线程仍然持有锁(重入),只是减少计数,并重新设置过期时间。
- 如果减1后的值等于0,表示锁需要彻底释放,执行
del命令删除锁的 Key,并发布一条释放锁的消息,以便通知(唤醒)其他阻塞等待的客户端。
代码如下:
public String deductStock3() {
String lockKey = "lock:product_101";
//获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//加分布式锁
redissonLock.lock(); // .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//解锁
redissonLock.unlock();
}
return "end";
}
备注:
tryLock(long waitTIme ,long leaseTime,TImeUnit unit); 尝试加锁时间 waitTIme,加锁成功后锁过期时间 lease Time
redis主从/集群架构分布式锁失效问题:
当主节点 key 写入成功后准备同步从节点,主节点宕机,从节点成为主节点并没有此key,其他线程也可以加锁成功,针对同一个商品减库存。
redLock 红锁
红锁使用要求:使用多个相互独立的 Redis 主节点,这些节点之间没有任何复制或协调关系。只有超过半数redis节点加锁成功才算加锁成功(也没有百分百解决)
代码如下
public String redlock() {
String lockKey = "product\_001";
//这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);
//根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//无论如何, 最后都要解锁
redLock.unlock();
}
return "end";
}
问题:依然存在主从/集群架构分布式锁失效(集群中从节点升级为主节点前未同步到主节点数据)
大促场景如何将分布式锁性能提升100倍
- 减小锁的粒度、
- 分段加锁
如一个库存容量1000,分成10段存入,每段100份。 此时可以通过10个分布式锁操作10段数据。 代码层面维护段位库记录总标记,可以循环进行锁数据,当段数据不够一次扣减时,通过逻辑进行额外处理。
传统方式:
库存 1000 件(一个 Key) + 1 把锁 = 单点竞争
优化方式:
库存 1000 件(逻辑库存) -> 分成 10 个物理 Key(stock_01 ~ stock_10)
每个 Key 100 件,配备 1 把独立锁。
10 把锁并行工作 = 理论吞吐量提升 10 倍。
使用 Redis 的 Lua 脚本,我们可以将“判断-扣减”操作封装为原子操作,完全不需要分布式锁,从而真正将性能提升 100 倍甚至更多。
Lua 脚本在 Redis 内部执行,天然原子,且避免了锁的上下文切换开销。
更多推荐


所有评论(0)