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

JAVA-实战8 Redis实战项目—雷神点评(6)秒杀优化

遠くへと広がる海の色暖かく

秒杀优化

判断优惠券库存以及一人一单都涉及到数据库操作,为了提高效率,将数据库操作优化为Redis操作。判断是否可以下单以及下单操作均在Redis中完成,而下单操作会存入异步阻塞队列中,后台服务器单开异步线程读取异步阻塞队列,完成数据库同步。示意图如下:
image

使用Redis维护每款优惠券库存,使用优惠券Redis中的Set结构维护每款优惠券已经被哪些用户下单,使用Lua脚本保证判断库存是否充足、判断一人一单、扣减库存、将当前用户Id存入Set等一系列操作的原子性。示意图如下:
image

改进秒杀业务

需求:

①新增秒杀优惠券的同时,将优惠券信息保存到Redis中
②基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
③如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
④开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

新增秒杀优惠券并保存到Redis

修改新增秒杀优惠券代码,新增添加Redis缓存添加操作

@Service
public class VoucherSecondKillServiceImpl implements VoucherSecondKillService {@Autowiredprivate VoucherSecondKillMapper voucherSecondKillMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void AddVoucherSecondKill(VoucherData NewVoucher) {VoucherSecondKillData NewVoucherSecondKillData = new VoucherSecondKillData();NewVoucherSecondKillData.setVoucherId(NewVoucher.getId());NewVoucherSecondKillData.setStock(NewVoucher.getStock());NewVoucherSecondKillData.setBeginTime(NewVoucher.getBeginTime());NewVoucherSecondKillData.setBeginTime(NewVoucher.getEndTime());voucherSecondKillMapper.insert(NewVoucherSecondKillData);// Redis缓存添加操作stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+NewVoucher.getId(),NewVoucher.getStock().toString());}
}

封装常量工具类

public class RedisConstants {public static final String SECKILL_STOCK_KEY = "seckill:stock:";
}

编写Lua脚本

Lua脚本执行行程图如下:
image

使用Redis中的sismember操作来判断key对应的set类型value是否存在相关成员

sismember key member

image
image

Lua脚本编写如下,其中..是Lua脚本的字符串拼接操作:

local VoucherId = ARGV[1]
local UserId = ARGV[2]local StockKey = 'seckill:stock:' .. VoucherId
local OrderKey = 'seckill:order:' .. VoucherId-- 判断库存是否充足
if(tonumber(redis.call('get',StockKey)) <=0) then-- 库存不足返回1return 1
end-- 判断用户是否下单
if(redis.call('sismember',OrderKey,UserId) ==1) then-- 用户存在说明重复下单,返回2return 2
end-- Redis库存-1
redis.call('incrby',StockKey,-1)
-- Redis下单用户集合添加当前用户
redis.call('sadd',OrderKey,UserId)
-- 成功返回0
return 0;

服务层方法修改如下,其中使用stringRedisTemplate.execute方法调用Lua脚本执行,Objects.requireNonNull().intValue()进行类型转换:

@Slf4j
@Service
public class VoucherOrderServiceImpl implements VoucherOrderService {@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 使用静态代码块初始化DefaultRedisScript,添加执行Lua脚本路径private static final DefaultRedisScript<Long> SecondKill_SCRIPT;static {SecondKill_SCRIPT = new DefaultRedisScript<>();SecondKill_SCRIPT.setLocation(new ClassPathResource("Lua/SecondKill.lua"));SecondKill_SCRIPT.setResultType(Long.class);}@Override@Transactionalpublic ResultData VoucherSecondKill(Long VoucherId) {Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());// 调用脚本int LuaResult = Objects.requireNonNull(stringRedisTemplate.execute(SecondKill_SCRIPT,Collections.emptyList(),VoucherId.toString(),UserId.toString())).intValue();// 错误情况判断if(LuaResult==1) {return ResultData.error("Stock is not Enough!");} else if (LuaResult==2) {return ResultData.error("You have already purchased this voucher!");}// 新增订单记录long VoucherOrderId = redisIdWorker.NewId("Order");return ResultData.success();}
}

执行测试:

分别测试两种错误情况
image
image

查看Redis相关,库存减少,优惠券购买用户添加完成,新增下单记录
image
image
image

开启异步队列,封装下单信息

将下单信息封装成为实体类,存入异步队列中,使用BlockingQueue<T>声明阻塞队列,容量为\(1024^2\)(约100w作用)

private BlockingQueue<VoucherOrderData> OrderTasksQueue = new ArrayBlockingQueue<>(1024*1024);@Override
@Transactional
public ResultData VoucherSecondKill(Long VoucherId) {Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());int LuaResult = Objects.requireNonNull(stringRedisTemplate.execute(SecondKill_SCRIPT,Collections.emptyList(),VoucherId.toString(),UserId.toString())).intValue();if(LuaResult==1) {return ResultData.error("Stock is not Enough!");} else if (LuaResult==2) {return ResultData.error("You have already purchased this voucher!");}long VoucherOrderId = redisIdWorker.NewId("Order");AddNewVoucherOrderToQueue(VoucherId,VoucherOrderId);proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();return ResultData.success("Now OrderId is "+VoucherOrderId);}@Override
@Transactional
public void AddNewVoucherOrderToQueue(Long VoucherId,Long VoucherOrderId) {Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());VoucherOrderData NewVoucherOrder = new VoucherOrderData();NewVoucherOrder.setId(VoucherOrderId);NewVoucherOrder.setUserId(UserId);NewVoucherOrder.setVoucherId(VoucherId);OrderTasksQueue.add(NewVoucherOrder);
}

开启线程任务,实现异步下单

这一步非常复杂

首先实现异步队列的读取:

使用Executors.newSingleThreadExecutor()初始化一步处理线程池SECONDKILL_ORDER_EXECUTOR;这是一个单线程线程池,内部只维护一个工作线程,其可以提供独立的执行环境,专门用于从队列中取出订单并执行耗时的数据库操作。

VoucherOrderHandler是内部私有类,实现了Runnable接口,封装了具体的异步消费逻辑,是实际执行数据库写入的操作者。

init()函数添加了@PostConstruct注解,确保在SpringBean初始化完成后立即执行;其作用是将VoucherOrderHandler实例提交给线程池。此时,后台线程启动并进入while(true)循环,开始监听队列。

private static final ExecutorService SECONDKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();@PostConstruct
private void init() {SECONDKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while(true) {try {VoucherOrderData NewVoucherOrder = OrderTasksQueue.take();AddVoucherOrderFromQueueToDatabase(NewVoucherOrder);} catch (InterruptedException e) {log.error("Calc Order Failed:",e);}}}
}

接下来实现提取出数据后存入数据库

private VoucherOrderService proxy;
@Override
public void AddVoucherOrderFromQueueToDatabase(VoucherOrderData NewVoucherOrder) {// 使用RedissonClient优化锁RLock lock = redissonClient.getLock(REDISSON_KEY_PREFIX+NewVoucherOrder.getUserId());boolean IsLock = lock.tryLock();if(!IsLock) {log.error("Add Database Get Lock Failed!");}try {proxy.CreateNewVoucherOrder(NewVoucherOrder);} finally {lock.unlock();}
}@Override
@Transactional
public void CreateNewVoucherOrder(VoucherOrderData NewVoucherOrder) {Long VoucherId = NewVoucherOrder.getVoucherId();Long UserId = NewVoucherOrder.getUserId();QueryWrapper<VoucherOrderData> orderlwp = new QueryWrapper<VoucherOrderData>();orderlwp.eq("voucher_id",VoucherId).eq("user_id",UserId).select("count(*) as Number");List<Map<String,Object>> Result = voucherOrderMapper.selectMaps(orderlwp);Long Count = (Long) Result.get(0).get("Number");if(Count>0) {log.info("You have already purchased this voucher!");}LambdaUpdateWrapper<VoucherSecondKillData> lwp = new LambdaUpdateWrapper<VoucherSecondKillData>();lwp.eq(VoucherSecondKillData::getVoucherId,VoucherId);voucherSecondKillMapper.UpdateSecondKillVoucher(lwp,1L);voucherOrderMapper.insert(NewVoucherOrder);
}

其实际执行的流程为:

启动阶段‌:Spring容器初始化VoucherOrderServiceImpl完成后,执行@PostConstruct,启动SECONDKILL_ORDER_EXECUTOR中的 VoucherOrderHandler线程,该线程阻塞在orderTasks.take()处等待。

请求阶段:前端发起优惠券秒杀下单请求,在经过处理和校验后最终订单信息存入OrderTasksQueue队列

处理阶段VoucherOrderHandler线程从OrderTasksQueue取出订单,获取分布式锁,获取成功则通过代理对象proxy调用CreateVoucherOrder方法执行数据库相关操作


Q:为什么proxy从之前的局部变量变成类成员变量

A:因为AopContext.currentProxy()强依赖于当前线程的ThreadLocal上下文。

使用局部变量声明,在‌主线程‌(如Controller调用的Service层方法)中获取proxy并传递给子线程使用,这是可行的,因为传递的是代理对象的‌引用‌;但如果试图在‌子线程内部‌直接调用AopContext.currentProxy(),通常会抛出IllegalStateException,因为子线程没有继承主线程的 AOP 上下文栈

使用类成员变量声明,一旦在主线程初始化阶段(如@PostConstruct或首次请求时)将代理对象赋值给成员变量this.proxy,该引用就脱离了ThreadLocal的束缚。在异步线程(如秒杀队列消费者VoucherOrderHandler)中,直接使用this.proxy调用方法是‌安全且有效‌的。因为此时只是通过持有引用的普通Java对象调用方法,Spring 的事务拦截器(Interceptor)会根据方法调用触发,并在当前子线程中开启新事务,而不需要访问 AopContextThreadLocal

‌因此,对于异步秒杀场景,‌成员变量方式优于在子线程中动态获取‌,它避免了子线程无法获取AOP上下文的问题

值得注意的是,在异步秒杀场景中,‌推荐使用成员变量方式,但应避免使用AopContext.currentProxy()进行初始化‌,而是采用‌自我注入‌的方式,这样更稳健,确实但是抱歉我实在是不想写了

@Service
public class VoucherOrderServiceImpl implements VoucherOrderService {@Autowiredprivate VoucherOrderService self; // Spring 会自动注入代理对象// ... 其他代码@Overridepublic void AddVoucherOrderFromQueueToDatabase(VoucherOrderData NewVoucherOrder) {// ... 锁逻辑// 直接使用注入的代理对象,无需担心 ThreadLocal 上下文问题self.CreateNewVoucherOrder(NewVoucherOrder);// ... 解锁逻辑}
}

基于阻塞队列的异步秒杀存在的问题

①阻塞队列OrderTasksQueue基于内存实现,存在大小限制
②一旦出现环境异常(例如宕机),基于内存的Redis和阻塞队列数据全部丢失,从而引发数据安全问题

http://www.jsqmd.com/news/765834/

相关文章:

  • MCP 2026多租户资源隔离配置终极手册:CPU/内存/网络/I/O四维配额联动公式(含YAML黄金模板)
  • 手把手教你用Vivado 2019.1在Kintex-7上搭建10G UDP协议栈(附12套源码)
  • APP加固防Hook效果哪家强?实测RASP与代码虚拟化技术差距
  • 告别92M下载!用bsdiff为你的Android App瘦身,增量更新实战避坑指南
  • QMT更新后xtdata报错?手把手教你手动激活隐藏的download_history_data2批量下载接口
  • 我的世界镜像下载
  • 避开新手大坑:在eNSP中用AC6605配置AP无认证上线的3个关键点与常见错误
  • 横向评测:主流AI培训技术机构的核心优势对比
  • 2026 四川创意设计服务排名:可视化、UI、品牌 VI 与 3D 数字内容优选
  • 如何快速掌握大疆无人机固件自由:DankDroneDownloader终极指南
  • 基于深度学习的暴力行为检测系统(YOLOv12完整代码+论文示例+多算法对比)
  • 大模型提示词安全攻防实战:从ClawSec项目看AI应用安全防线构建
  • 智能编程搭档:如何用快马平台的AI模型优化你的蓝桥杯嵌入式代码
  • MCP 2026时间敏感网络(TSN)工业部署避坑指南:从拓扑设计到微秒级同步校准的11个致命误区
  • CUDA核函数里的‘双线性插值’到底怎么算?一个像素的奇幻漂流
  • 解锁AI辅助开发:用快马让资料应用学会自动摘要与智能推荐,打造下一代信息工具
  • 【4】优化提示词与微调功能和数据库
  • 从游戏开发视角看OpenGL:在VS2022中快速搭建你的第一个3D渲染窗口(附完整代码)
  • 农业IoT数据“看不见、看不懂、来不及”?用这3个PHP类库+2个CSS技巧,3小时上线可交互作物生长看板
  • 基于事件驱动的Python量化交易框架Minitrade:从架构解析到实盘部署
  • 磁力链接转种子文件终极指南:Magnet2Torrent让下载管理更简单
  • 实战mysql应用:基于快马ai生成spring boot用户权限管理系统
  • Punica系统解析:基于SGMV内核实现多LoRA模型高效并发推理
  • GD32C103RBT6 单片机串口控制 TJC3224T124 串口屏实战教程(完整代码 + 驱动)
  • 调试NVMe SSD时,如何像‘破译密码’一样解读Completion Queue里的状态码(SCT/SC)?
  • 等了两年,Cloudflare 终于给规则引擎加上了通配符
  • 第113篇:AI伦理与治理框架——企业如何负责任地开发与部署AI系统?(概念入门)
  • 从零开始:用STM32F103C8T6和HAL库打造你的第一台四轴无人机飞控(附完整原理图与代码)
  • 用Python模拟三国杀王荣的‘吉占’技能,看看平均能摸几张牌?
  • AISMM评估结果差异超41.6%?揭秘2026奇点大会隐藏测试集构造逻辑(含3个未公开对抗样本生成规则)