当前位置: 首页 > news >正文

黑马点评学习笔记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

本文是学习黑马程序员—黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)

…。。。。。。。。。。。…
请添加图片描述

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

http://www.dtcms.com/a/598127.html

相关文章:

  • 单页面网站有哪些内容吗电子商务就业岗位
  • 亚马逊云科技 Amazon Bedrock 持续壮大模型阵营:Qwen3 和 DeepSeek-V3.1 重磅上线
  • 同一个空间可以做两个网站么wordpress建设购物网站
  • 小县城做网站福田做商城网站建设多少钱
  • 东莞网站制作方案定制无锡网站制作哪家服务好
  • 做的的网站怎样上传如何做网站关键字优化
  • 【OpenCV + VS】图像像素的加减乘除操作
  • 那里可以做网站的吗wordpress修改文章链接
  • [Java EE] 多线程 -- 初阶(2)
  • 蓝牙钥匙 第89次 蓝牙钥匙未来发展趋势篇:与汽车电子架构演进深度融合
  • 网站建设解析2345网址导航开创中国
  • 营销型网站模版vi设计的目的
  • U++工程提取二进制工程
  • Zabbix告警配置全攻略:邮件+钉钉双保险
  • qq钓鱼网站制作微盟集团
  • 中国人做跨电商有什么网站自建冷库费用
  • 03|Langgraph | 从入门到实战 | 进阶篇 | 持久化
  • 如何将网站挂载域名网站建设的定位是什么意思
  • 知识管理工具sward上手指南:安装、配置与入门
  • 在Vivado中添加HLS IP后只显示路径无法显示可例化的IP核解决方式
  • 二分查找专题(十):“Z字形”的降维!当二分查找“失效”时
  • 响应式网站源码.net互联网行业的开发网站
  • Windows10 使用 dynv6 + ddns-go 实现 IPv6 固定域名远程访问指南
  • 如何自己做彩票网站免费自动回收的传奇手游
  • 教育培训网站建站网络营销是以什么为中心
  • 专家编程 | 提升编程技能的有效策略
  • 达州科创网站建设公司山东省建设备案网站审批表
  • ui设计的推荐网站及网址本公司经营网站建设
  • 项目分享|SD-Trainer:Stable Diffusion 训练集成工具
  • 专业的无锡网站建设网络贷款公司哪个好