黑马点评学习笔记10(优惠券秒杀下单优化(分布式锁的优化,Lua脚本))
前言
前面讨论了Redis实现优惠券秒杀系统中的线程安全问题,用悲观锁和乐观锁解决了多个线程同时查询并修改库存导致负数库存。代码示例展示了: 乐观锁实现库存扣减(CAS方式) 用户订单数量检查 订单创建流程 整个系统通过线程安全机制保证了高并发场景下的数据一致性,下面继续讨论线程安全的问题 :
只是前面的sychronized锁住的只是一个用户的访问,每一个JVM都有一个独立的锁监视器,因此我们要用到分布式锁:


分布式锁的常见实现方式:

基于Redis的分布式锁:


1. 分布式锁(SimpleRedisLock)
用于实现“一人一单”的并发控制,防止用户重复下单。
涉及的 Redis 命令:
- SET key value NX PX timeout
- 用于尝试获取锁。
- NX:只有当 key 不存在时才设置(保证互斥)。
- PX:设置过期时间(毫秒),防止死锁。
示例(伪命令):
SET order:123 "thread-id" NX PX 1200000
setIfAbsent(key, value, timeout) 是 Spring 对 SET key value NX PX 的封装。
返回 true 表示加锁成功,false 表示锁已被占用。
// 对应 Redis 命令: SET order:123 "lockValue" NX PX 1200
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent("order:" + userId, "anyValue", Duration.ofMillis(1200));
- DEL key(带校验)
1)来看看具体代码吧,创建分布式锁:
我们再新建一个类:
package com.hmdp.utils;import ...public class SimpleRedisLock implements ILock{/*锁的名称*/private String name;/*Redis操作客户端*/private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}/*尝试获取锁*/@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}/*释放锁*/@Overridepublic void unLock() {stringRedisTemplate.delete(KEY_PREFIX + name);}}
}
使用分布式锁:
package com.hmdp.service.impl;import .../*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;/*查询领取秒杀券*/@Overridepublic Result seckillVoucher(Long voucherId) {//1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}//3.判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已结束");}//4.判断库存是否充足if (voucher.getStock() < 1) {return Result.fail("库存不足");}// 3、创建订单(使用分布式锁)Long userId = UserHolder.getUser().getId();SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);boolean isLock = lock.tryLock(1200);if (!isLock) {// 索取锁失败,重试或者直接抛异常(这个业务是一人一单,所以直接返回失败信息)return Result.fail("一人只能下一单");}try {// 索取锁成功,创建代理对象,使用代理对象调用第三方事务方法, 防止事务失效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(userId, voucherId);} finally {lock.unLock();}}/*创建订单*/@Transactional //添加事务保证数据库操作和缓存操作的原子性public Result createVoucherOrder(Long voucherId) {//5.一人一单//5.1 查询订单Long userId = UserHolder.getUser().getId();//5.1 获取锁成功int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2.判断是否存在if (count > 0) {//用户购买过return Result.fail("用户已经买过了");}//6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0)//where id = ? and stock > 0.update();if (!success) {//扣减库存失败return Result.fail("库存不足");}//7.创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1.订单IDlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2.优惠券IDvoucherOrder.setVoucherId(voucherId);//7.3.用户IDvoucherOrder.setUserId(userId);//写入数据库save(voucherOrder);//6.返回订单IDreturn Result.ok(orderId);}}
分布式锁优化
1.分布式锁可能出现的问题1:


上面实现了一个简单的分布式锁,其实还存在一些问题,就像上面的线程一获取锁后,然后一些原因业务阻塞了,然后锁呢超时释放了,这时候线程二,趁虚而入,获取锁成功后,线程一完成了把锁给释放了,这时县城三又开始获取锁了,这就导致超卖问题了。
来看看怎么解决吧
我们在释放锁时检查一下是不是自己的锁不就行了,不是自己的线程锁就不可以释放,是自己的就可以释放。

来看一下代码怎么实现吧:
package com.hmdp.utils;import...public class SimpleRedisLock implements ILock{/*锁的名称*/private String name;/*Redis操作客户端*/private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}/***key的前缀*/private static final String KEY_PREFIX = "lock:";/*尝试获取锁*/@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unLock() {//获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);if (threadId.equals(id)) {//释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}}
}
分布式锁可能出现的问题2
分布式锁的原子问题:

当线程一获取锁成功,执行完业务获取锁标识成功后正好要释放锁时,线程阻塞了,这是线程二趁虚而入了,获得锁成功,这是线程一又好了,直接释放锁了(因为之前判断过了),又导致线程三趁虚而入了,又产生超卖问题了。
来看看怎么解决呢?
为了避免上书情况的发生,我们需要保证判断锁释放锁这两个方法的原子性,怎么保证原子性呢?
先来看看什么是Lua脚本:
一、什么是 Lua 脚本?
Lua 是一种轻量级、高效的脚本语言,常被嵌入到其他应用程序中。在 Redis 中,可以通过 EVAL 或 EVALSHA 命令执行 Lua 脚本。
在 Redis 中使用 Lua 脚本?
- 原子性(Atomicity):
- Redis 是单线程执行命令的。
- 当你通过 EVAL 执行一段 Lua 脚本时,整个脚本会在 Redis 服务器端以原子方式执行,期间不会被其他客户端的请求打断。
- 这对于实现复杂的逻辑(比如检查+删除)非常关键,可以避免竞态条件。
- 减少网络开销:
- 多个 Redis 操作可以封装在一个脚本中,只需要一次网络调用即可完成。
- 可复用性和安全性:
- 脚本可以预加载或通过 SHA 缓存重复执行。
- 在服务端执行,避免中间状态暴露给客户端。


1.编写Lua脚本:
在代码里新建一个Lua文件,记得装插件:Tarantool-EmmyLua

在lua脚本中编写要保证原子性的Redis命令
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Cecilia.
--- DateTime: 2025/10/24 11:30
---比较线程标识与锁中的标识是否一致
if(redis.call('get',KEY[1]) == ARGV[1]) then--释放锁 del keyreturn redis.call('del', KEYS[1])
end
-- 不一致,返回0
return 0
编写Java代码,使用Lua改进分布式锁:
package com.hmdp.utils;import...public class SimpleRedisLock implements ILock{/*锁的名称*/private String name;/*Redis操作客户端*/private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}/*key前缀*/private static final String KEY_PREFIX = "lock:";/*ID前缀*/private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";/*尝试获取锁*/@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}/*加载Lua脚本*/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),ID_PREFIX + Thread.currentThread().getId());}}
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
ID_PREFIX:每个 JVM 实例启动时生成一个唯一 UUID,加上线程 ID,构成锁的 value。
🔒 获取锁:tryLock(long timeoutSec)
@Override
public boolean tryLock(long timeoutSec) {String threadId = ID_PREFIX + Thread.currentThread().getId();Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}
对应的 Redis 命令:
SET lock:order:123 "a1b2c3-12345" EX 120 NX
✅ 1. Lua 脚本的声明与加载
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);
}
在类加载时,从 classpath 下加载名为 unlock.lua 的 Lua 脚本文件,并封装为 DefaultRedisScript 对象。
✅ 2. Lua 脚本的执行(释放锁)
public void unLock() {stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
}
- 调用方式:使用 StringRedisTemplate.execute() 执行预定义的 Lua 脚本。
- 参数说明:
- 第一个参数:UNLOCK_SCRIPT —— 已加载的 Lua 脚本对象
- 第二个参数:KEYS 列表 → 只传一个 key:lock:xxx
- 第三个参数:ARGV 值 → 当前线程的唯一标识(如 a1b2c3-12345)
✅ 3. Lua 脚本内容(unlock.lua 文件)
将三个参数传到Lua脚本中执行,Redis命令保证了其执行的原子性。
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Cecilia.
--- DateTime: 2025/10/24 11:30
---比较线程标识与锁中的标识是否一致
if(redis.call('get',KEY[1]) == ARGV[1]) then--释放锁 del keyreturn redis.call('del', KEYS[1])
end
-- 不一致,返回0
return 0
本文是学习黑马程序员—黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)
…。。。。。。。。。。。…

…。。。。。。。。。。。…
