Redis实战—优惠卷秒杀(锁/事务/代理对象的应用)
本博客为个人学习笔记,学习网站与详细见:黑马程序员Redis入门到实战 P50 - P54
目录
优惠卷秒杀下单功能实现
超卖问题
悲观锁与乐观锁
实现CAS法乐观锁
一人一单功能实现
代码优化
代码细节分析
优惠卷秒杀下单功能实现
Controller层代码实现
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Resourceprivate IVoucherOrderService voucherOrderService;@PostMapping("seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}
}
Service层代码实现
//接口类
public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);
}//实现类
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic 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("库存不足!");}// 5.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!success) {// 扣减失败return Result.fail("库存不足!");}// 6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3代金卷idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);}
}
超卖问题
假设秒杀优惠卷库存为1,正常情况抢票逻辑如下图所示。
假设秒杀优惠卷库存为1,高并发情况抢票逻辑如下图所示。
悲观锁与乐观锁
由于悲观锁采用串行执行线程的方式,因此性能劣于乐观锁。
乐观锁的关键是判断之前查询到的数据是否被修改过,常见的方式有以下两种:
1.版本号法
为每种秒杀优惠卷添加版本号属性,每当该优惠卷的库存被更改时,令版本号也发生变化。因此只需要在查询库存的同时查询并记录版本号,在修改库存时,通过查询当前版本号并判断其是否与先前记录的版本号一致来判断查询到的库存是否已经被其他线程修改过。
2.CAS法(即版本号法的简化,利用库存代替版本号)
实现CAS法乐观锁
首先查询秒杀优惠卷信息并保存到变量voucher,当判断该优惠卷可以被抢购后,在修改该优惠卷库存时,需要满足当前优惠卷库存与voucher.getStock()相等的条件。
尽管添加了乐观锁,但在测试时发现用户抢购优惠卷的成功率过低。原因在于,在高并发的情况下,假设10个用户同时抢购一张优惠卷,那么只有一个用户能够抢购成功,其他用户均抢购失败。而在抢票的业务场景下,当用户同时进行抢购时,只要优惠卷库存大于0即可进行抢购,因此只需修改判断条件,在修改库存时,判断当前库存是否大于0即可。
一人一单功能实现
修改Service层实现类中优惠卷秒杀下单seckillVoucher方法,代码(未优化版本)如下:
@Override
@Transactional
public 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("库存不足");}//5.一人一单Long userId = UserHolder.getUser().getId();//5.1查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2判断是否存在if(count>1){//用户已经购买过了return Result.fail("用户已经购买过一次");}//6.扣减库存boolean success=seckillVoucherService.update().setSql("stock = stock - 1") //set 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.setUserId(userId);//7.3代金券IdvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//8.返回订单IDreturn Result.ok(orderId);
}
代码优化
存在的问题:尽管代码经过修改,但在高并发的情况下仍然存在问题,即单个用户在同一刻多次下单,多个线程均查询到count=0(该用户未购买过当前优惠券),此时多个线程同时创建订单,从而导致一人多单问题。
实现:为实现一人一单功能,我们需要加锁,相比超卖问题中每个线程只是对数据进行修改,所以可以使用乐观锁,通过判断数据值是否被修改来判断其他线程是否已经对当前数据进行操作。但一人多单的问题中,每个线程需要进行添加数据而非修改数据的操作,即创建新的订单,所以这种情况下,我们需要使用悲观锁。
优化后的代码如下所示:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@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("库存不足!");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 获取当前代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic synchronized Result createVoucherOrder(Long voucherId) {// 5.一人一单模式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") //使用MP,设置sql语句.eq("voucher_id", voucherId).gt("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.setUserId(userId);// 7.3代金卷idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 8.返回订单idreturn Result.ok(orderId);}
}
代码细节分析
有以下几个细节我们需要特别注意:
1. 首先,我们将查询、判断、创建订单等过程封装成createVoucherOrder方法,但是我们并不能直接对这个方法加锁,如果对这个方法加锁的话将导致创建订单的业务是串行执行的,在高并发多用户的情况下,性能会很差。
2. 因为是一人一单功能,我们需要做的是对同一个用户创建订单的流程进行加锁,所以我们根据用户ID来加锁。我们为相同的用户ID配置一把锁,从而将锁的范围缩小,因此我们将createVoucherOrder方法进行修改,如下所示。
3. 然而,我们需要确保synchronized()传入的参数对象是相同的,synchronized锁才能起作用。因此我们不传入查询得到的userId对象,因为我们每次查询后都会赋值给新的userId对象,这样会导致传入synchronized()的参数始终不是同一个对象,因此我们采用的是synchronized(userId.toString())。
4.但是,如果我们直接使用userId.toString() 方法,拿到的对象实际上是不同的,toString() 的底层逻辑是new出一个新对象,因此我们需要使用intern()方法,该方法是从常量池中拿到数据,能确保我们每次取得的对象都是相同的。得到的代码如下所示。
@Transactional
//将创建订单封装成一个方法
public Result createVoucherOrder(Long voucherId) {//获取用户IdLong userId = UserHolder.getUser().getId();synchronized(userId.toString().intern()) {查询、判断、创建订单等流程...}
}//错误情况:synchronized(userId.toString())
5.由于我们是在方法里面加锁,因此代码的逻辑是先释放锁,再提交事务,因此在高并发的环境下可能会出现安全问题(即释放锁后,提交事务还未完成,其他线程又进来了)。所以我们应该先提交事务,再释放锁。因此,我们应该在方法外加锁,代码如下。
@Override
public Result seckillVoucher(Long voucherId) {...Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {return createVoucherOrder(voucherId);}
}
6.在以上代码中,我们调用createVoucherOrder方法,其实是通过this.的方式调用的,而this代表的是当前VoucherOrderServiceImpl对象,并不是createVoucherOrder的代理对象,因此事务是不生效的。要想让事务生效,我们需要利用代理对象调用createVoucherOrder方法,才能使事务生效,代码如下。
@Override
public Result seckillVoucher(Long voucherId) {...Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 获取当前代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}
}
此外,我们还需做以下三步。
1.添加动态代理依赖
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>
2.在接口中添加createVoucherOrder(Long voucherId);
public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);Result createVoucherOrder(Long voucherId);
}
3.在启动类中添加注解,暴露该代理对象
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}}
相关文章:
Redis实战—优惠卷秒杀(锁/事务/代理对象的应用)
本博客为个人学习笔记,学习网站与详细见:黑马程序员Redis入门到实战 P50 - P54 目录 优惠卷秒杀下单功能实现 超卖问题 悲观锁与乐观锁 实现CAS法乐观锁 一人一单功能实现 代码优化 代码细节分析 优惠卷秒杀下单功能实现 Controller层…...
HTML星空特效
目录 写在前面 完整代码 代码分析 运行效果 系列文章 写在后面 写在前面 100行代码实现HTML星空特效。 完整代码 全部代码如下。 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"&g…...
银行数仓项目实战(四)--了解银行业务(存款)
文章目录 项目准备存款活期定期整存整取零存整取存本取息教育储蓄定活两便通知存款 对公存款对公账户协议存款 利率 项目准备 (贴源层不必写到项目文档,因为没啥操作没啥技术,只是数据。) 可以看到,银行的贴源层并不紧…...
MySQL版本发布模型
MySQL 8.0 之后使用了新的版本控制和发布模型,分为两个主线:长期支持版(LTS)以及创新版。这两种版本都包含了缺陷修复和安全修复,都可以用于生产环境。 下图是 MySQL 的版本发布计划: 长期支持版 MySQL…...
java: 不兼容的类型: org.apache.xmlbeans.XmlObject无法转换为x2006.main.CTRow
我使用的xmlbeans版本是5.0,使用xmlbeans包做转换时,报错,正如标题显示得那样 解决办法 额外再引入下面的jar包 <dependency><groupId>org.apache.xmlbeans</groupId><artifactId>xmlbeans</artifactId><…...
内容时代:品牌如何利用社交平台精准触达用户
还记得学生时代老师教写作文的时候常说的一句话就是“开头质量决定了阅卷老师想不想花精力去读,而内容质量决定了她愿不愿意给你判高分”这个世界仿若一个巨大的圆,同样的逻辑放在任何地方好像都能适用。在品牌营销中,内容已成为品牌与消费者…...
推荐4款PC端黑科技工具,快来看看,建议收藏
Thunderbird Thunderbird 是由 Mozilla 基金会开发的一款免费且开源的电子邮件客户端,支持 Windows、macOS、Linux 等多种操作系统。它不仅可以用于发送和接收电子邮件,还可以作为新闻阅读器、聊天工具以及日历应用。 Thunderbird 提供了丰富的功能&…...
汉化版PSAI全面测评,探索国产AI绘画软件的创新力量
引言 随着AI技术的飞速发展,图像处理和绘画领域迎来了新的变革。作为一名AIGC测评博主,今天我们测评的是一款国产AI绘画软件——StartAI,一句话总结:它不仅在技术上毫不逊色于国际大牌,更在用户体验和本地化服务上做到…...
LeetCode | 709.转换成小写字母
这道题可以用api也可以自己实现,都不难,大小字母之前相差了32,检查到大写字母时加上32即可 class Solution(object):def toLowerCase(self, s):""":type s: str:rtype: str"""return s.lower()class Solution…...
洗地机哪个品牌比较好?四款好用靠谱的优质洗地机推荐
随着现代生活节奏的加快,家庭清洁成了一项耗时且繁琐的任务。洗地机凭借其智能化和高效的清洁能力,越来越受到大家的青睐。然而,市场上各种品牌和型号琳琅满目,让人眼花缭乱。为了帮助大家在众多选择中找到心仪的产品,…...
java:spring actuator添加自定义endpoint
# 项目代码资源: 可能还在审核中,请等待。。。 https://download.csdn.net/download/chenhz2284/89437274 # 项目代码 【pom.xml】 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId&…...
LeetCode88-删除有序数组中的重复项
题目 给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 代…...
SpringBoot Starter 通用接口加密组件(防篡改)+ RequestBodyAdvice和ResponseBodyAdvice原理
防篡改: 如何保证接口安全,做到防篡改防重放?_接口防止串改-CSDN博客 接口安全设计之防篡改和防重放_接口防篡改机制-CSDN博客 参考博客: RequestBodyAdvice和ResponseBodyAdvice原理详解-CSDN博客 SpringBoot Starter 通用接口…...
delphi 如何使用TEdgeBrowser组件以及打包环境在其他主机上运行
不管开发环境还是第三方环境先安装运行时库:Microsoft Edge WebView2 | Microsoft Edge Developer 开发环境可以直接通过: delphi IDE安装 安装完毕后进入到指定路径,复制里面的WebView2Loader.dll到你要开发的程序根目录: 大致路…...
Sui的Fastcrypto加密库刷新速度记录
Sui使用的加密库Fastcrypto打破了许多速度记录,Mysten Labs在基准测试和安全分析中的工作修复了许多安全漏洞,同时通过识别新的优化技巧为创新开辟了道路。 最近在伦敦帝国理工学院举行的国际性能工程会议(ICPE)基准测试研讨会上…...
Malformed \uxxxx encoding或Maven server structure problem问题解决
问题描述: idea运行项目时, 报错如下: [ERROR] Malformed \uxxxx encoding. [ERROR] Maven server structure problem [ERROR] Malformed \uxxxx encoding. 解决方法总结 先说一下解决方法无非是下面几种 1、先检查项目的.properties、.yml 、pom.xml、logback等…...
Sui主网升级至V1.27.2版本
其他升级要点如下所示: 重点: #17245 增加了一个新的协议版本,并在开发网络上启用了Move枚举。 JSON-RPC #17245: 在返回的JSON-RPC结果中增加了对Move枚举值的支持。 GraphQL #17245: 增加了对Move枚举值和类型的支持。 CLI #179…...
Cheat Engine 学习
文章目录 Exact Value scanning任务实现步骤Unknown initial value任务实现步骤原理说明Floating points任务实现步骤原理说明Code finder任务实现步骤原理说明Pointers任务实现步骤原理说明Change Pointer 操作:Active(活跃状态)和数值修改:Code Injection任务概述实现步骤…...
【千帆AppBuilder】你有一封邮件待查收|未来的我,你好吗?欢迎体验AI应用《未来信使》
我在百度智能云千帆AppBuilder开发了一款AI原生应用,快来使用吧!「未来信使」:https://appbuilder.baidu.com/s/Q1VPg 目录 背景人工智能未来的信 未来信使功能介绍Prompt组件 千帆社区主要功能AppBuilderModelBuilder详细信息 推荐文章 未来…...
【案例分析】一文讲清楚SaaS产品运营的六大杠杆是什么?具体怎么运用?
在SaaS(软件即服务)行业,如何快速获取用户并实现持续增长一直是企业关注的重点。近年来,分销裂变策略因其高效性和低成本特性,成为许多SaaS企业实现快速增长的秘诀。下面,我们将通过一个具体的案例来剖析成…...
如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...
以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...
嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...
ubuntu22.04有线网络无法连接,图标也没了
今天突然无法有线网络无法连接任何设备,并且图标都没了 错误案例 往上一顿搜索,试了很多博客都不行,比如 Ubuntu22.04右上角网络图标消失 最后解决的办法 下载网卡驱动,重新安装 操作步骤 查看自己网卡的型号 lspci | gre…...
【1】跨越技术栈鸿沟:字节跳动开源TRAE AI编程IDE的实战体验
2024年初,人工智能编程工具领域发生了一次静默的变革。当字节跳动宣布退出其TRAE项目(一款融合大型语言模型能力的云端AI编程IDE)时,技术社区曾短暂叹息。然而这一退场并非终点——通过开源社区的接力,TRAE在WayToAGI等…...
[QMT量化交易小白入门]-六十二、ETF轮动中简单的评分算法如何获取历史年化收益32.7%
本专栏主要是介绍QMT的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写100篇左右。 QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。 文章目录 相关阅读1. 策略概述2. 趋势评分模块3 代码解析4 木头…...
