三、实战篇 优惠券秒杀

源码仓库地址:git@gitee.com:chuangchuang-liu/hm-dingping.git

1、全局唯一ID

数据库默认自增的存在的问题:

  • id增长规律明显
  • 受单表数据量的限制

场景一分析:id如果增长规律归于明显,容易被用户或者商业对手猜测出一些敏感信息,比如早上出的第一个单子的id是1,晚上再查看出的单子的id是1001,那别人就很容易猜测出你这一天的销售情况。
场景二分析:Mysql数据库的由于查询性能的考虑,单表数据量不建议超过500W。数据量更大时,需要进行分库分表,但从逻辑上来讲这两张表是同一个表,需要保证id的唯一性,增添了一定的维护成本。

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

特性含义
唯一性需要保证id全局唯一
高可用生成器需要保证所有线程来调用时都能提供服务来生成id
高性能很多业务要求执行时间不能过长如缓存重建,所以对生成器生成id的性能有一定要求
递增性为了不重复、便于管理和查询以及提高系统的性能和效率
安全性生成的id不易被猜测或篡改

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
1653363172079.png

符号位:永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

  • 编码实现

全局唯一id生成器

@Component
public class RedisIdWorker {/*** 开始时间戳*/private static final long BEGIN_TIMESTAMP = 1640995200L;/*** 序列号的位数*/private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix) {// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.生成序列号// 2.1.获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2.自增长long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}
}

单元测试

@SpringBootTest
class HmDianPingApplicationTests {@Autowiredprivate RedisIdWorker redisIdWorker;private ExecutorService es;/*** 初始化线程池*/@BeforeEachvoid setUp() {es = Executors.newFixedThreadPool(10);}@Testvoid testIdWorker() throws InterruptedException {CountDownLatch latch = new CountDownLatch(300);Runnable task = () -> {for (int i = 0; i < 100; i++) {long id = redisIdWorker.nextId("order");System.out.println("id = " + id);}latch.countDown();};long begin = System.currentTimeMillis();for (int i = 0; i < 300; i++) {es.submit(task);}latch.await();long end = System.currentTimeMillis();System.out.println("time = " + (end - begin));}
}

2、实现优惠券秒杀下单

秒杀需要思考的点:

  • 秒杀活动是否开始或结束
  • 库存是否足够

思路分析:
image.png

  • 编码实现
/*** 下单购买秒杀券* @param voucherId 秒杀券id* @return 订单id*/
@Override
@Transactional
public Result order(Long voucherId) {// 查询优惠券信息SeckillVoucher voucher = seckillVoucherService.getById(voucherId);if (ObjectUtil.isEmpty(voucher)) {return Result.fail("id为" + voucherId + "的秒杀券不存在");}// 判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀还未开始");}// 判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已结束");}// 判断秒杀券是否已售罄if (voucher.getStock() < 1) {return Result.fail("秒杀券已售罄");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!success) {return Result.fail("秒杀券已售罄");}// 生成订单VoucherOrder order = new VoucherOrder();long orderID = redisIdWorker.nextId("order");order.setId(orderID);order.setVoucherId(voucherId);order.setUserId(UserHolder.getUser().getId());save(order);return Result.ok(orderID);
}

3、超卖问题

超卖问题原因分析:假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁,而对于加锁通常有两种方案:

方案名称含义例子
悲观锁认为线程安全问题一定会发生,因此在操作数据前,添加互斥锁,保证操作是串行的Synchronized、Lock
乐观锁认为线程安全问题不一定会发生,因此就不添加锁,只有在做更新操作时,判断其他线程是否做过更新。如果没有改过,才去做更新CAS

什么是CAS?
CAS(Compare And Swap)是一种用于管理并发数据访问的无锁算法。CAS操作包含三个参数:内存位置(V)、预期原值(A)和新值(B)。执行CAS操作的基本步骤是:如果V的值和预期原值A相同,那么就用新值B替换V的值;如果V的值和预期原值A不相同,就不做任何操作。

乐观锁的两种常见方式:

  • 版本号法(version)
  • 利用业务数据本身来充当版本号

1653369268550.png

  • 编码实现解决超卖问题

一般是CAS+自旋组合来解决超卖问题:如果CAS失败,但只要库存仍大于0,就允许其继续尝试购买秒杀券
但这里可以简化为只要当前库存量大于0,就允许其继续尝试购买秒杀券

// 扣减库存,添加乐观锁
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId)// 这种方式反而会增加下单的失败率
//                .eq("stock", voucher.getStock())// 只要库存还大于0,就认为下单成功.gt("stock", 0).update();

4、一人一单

需求: 要求一个用户只能购买一张秒杀券
目前存在的问题: 一个用户可以无限制次数的抢优惠券。应当加一层判断逻辑,当用户成功下完单后,不允许其再次抢优惠券。
判断逻辑: 查询该用户和优惠券在订单表里是否已经存在,如果存在,说明其之前已经抢过优惠券了

  • 编码实现
// 保证一人一单
Integer count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
if (count > 0) {return Result.fail("id为:" + userId + "的用户已经购买过该秒杀券");
}

存在问题: 在并发情况下仍然会出现一人多单的问题
分析: 该问题和超卖的问题是一致的,因为在查询时,多个线程都进来查询,发现该用户没有下过单,因此都做创建订单操作。
解决方案: 和之前一样,通过添加锁来实现。乐观锁比较适合更新数据,而这里是插入数据操作,适合悲观锁。
注:添加悲观锁这里,存在诸多问题,一个个来分析:

  • 锁的存放位置:在方法上添加同步锁。这种方式下,锁的粒度太粗了,导致每一个线程进来都会被锁住,性能太差
@Transactional
/*将锁放在方法体上,那么这个方法就是一个同步方法,只有一个线程能够进入,会导致性能问题*/
public synchronized Result oneUserAndOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();// 保证一人一单Integer count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();if (count > 0) {return Result.fail("id为:" + userId + "的用户已经购买过该秒杀券");}// 扣减库存,添加乐观锁boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId)// 这种方式反而会增加下单的失败率
//                .eq("stock", voucher.getStock())// 只要我库存还大于0,就允许用户继续下单.gt("stock", 0).update();if (!success) {return Result.fail("秒杀券已售罄");}// 生成订单VoucherOrder order = new VoucherOrder();long orderID = redisIdWorker.nextId("order");order.setId(orderID);order.setVoucherId(voucherId);order.setUserId(UserHolder.getUser().getId());save(order);return Result.ok(orderID);
}
  • 锁的存放位置:在方法体内添加同步锁,且以用户id进行加锁,这样锁的粒度更细,同一个用户线程进来后会去争锁资源,而不会导致所有线程都被锁住。

存在的问题: 在方法体内添加同步代码块,代码块执行完毕后立即释放锁,但事务又是由Spring管理的,此时事务还未提交。其他线程进来后,查询用户未下单,执行创建订单操作。因此这种方式仍会出现多线程并发问题。
解决方案: 事务提交后再释放锁

  • 编码实现
<!--引入aspectJ-->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>
@Override
public Result order(Long voucherId) {// 查询优惠券信息SeckillVoucher voucher = seckillVoucherService.getById(voucherId);if (ObjectUtil.isEmpty(voucher)) {return Result.fail("id为" + voucherId + "的秒杀券不存在");}// 判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀还未开始");}// 判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已结束");}// 判断秒杀券是否已售罄if (voucher.getStock() < 1) {return Result.fail("秒杀券已售罄");}// 保证一人一单Long userId = UserHolder.getUser().getId();// 3、事务提交后,锁会被释放synchronized (userId.toString().intern()) {// 获取代理对象,代理对象才具备事务功能IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.oneUserAndOrder(voucherId);}
}
@Transactional
public Result oneUserAndOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();// 保证一人一单Integer count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();if (count > 0) {return Result.fail("id为:" + userId + "的用户已经购买过该秒杀券");}// 扣减库存,添加乐观锁boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId)// 这种方式反而会增加下单的失败率
//                .eq("stock", voucher.getStock())// 只要我库存还大于0,就允许用户继续下单.gt("stock", 0).update();if (!success) {return Result.fail("秒杀券已售罄");}// 生成订单VoucherOrder order = new VoucherOrder();long orderID = redisIdWorker.nextId("order");order.setId(orderID);order.setVoucherId(voucherId);order.setUserId(UserHolder.getUser().getId());save(order);Result.ok(orderID);
}
@MapperScan("com.hmdp.mapper")
// 启动aop,否则service获取不到aop代理类
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}
}
  • JMeter进行并发安全测试

image.png
image.png
image.png
image.png

可以发现,至此已成功添加了一人一单的限制!

5、集群环境下的并发问题

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
1、我们将服务启动两份,端口分别为8081和8082:
image.png
2、修改nginx的配置文件

upstream backend {server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
}  

具体操作:
1、通过IDEA克隆一份应用配置
image.png
2、添加 vm options
image.png
image.png
3、重启nginx

nginx -s reload

集群环境下锁失效的原因分析:
现在部署了多套tomcat服务器,每个tomcat内部都有一个jvm,jvm内部多个线程间可以实现锁互斥,但jvm间的线程的锁并不互斥,从而导致互斥锁失效,出现一人多单的问题,这就是集群环境下syn锁失效的原因,在这种情况下就需要使用分布式锁来解决该问题!
1653374044740.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/735614.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT画图功能

QT画图功能 每个QWidget都自带的功能&#xff0c;继承了QPainteDevice都可以使用QPainter来进行绘图。 画图需要调用paintEvent绘制事件&#xff0c;paintEvent事件时QWidget类自带的事件。 重写paintEvent事件。&#xff08;重写事件&#xff1a;如果父类有某个方法&#xff…

Spring Boot 面试题及答案整理,最新面试题

Spring Boot中的自动配置是如何工作的&#xff1f; Spring Boot的自动配置是其核心特性之一&#xff0c;它通过以下方式工作&#xff1a; 1、EnableAutoConfiguration注解&#xff1a; 这个注解告诉Spring Boot开始查找添加了Configuration注解的类&#xff0c;并自动配置它们…

22.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-加载配置文件到分析工具界面

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;21.配置数据保存…

加快代码审查的 7 个最佳实践

目录 前言 1-保持小的拉取请求 2-使用拉取请求模板 3-实施响应时间 SLA 4-培训初级和中级工程师 5-设置持续集成管道 6-使用拉取请求审查应用程序 7-生成图表以可视化您的代码更改 前言 代码审查可能会很痛苦软件工程师经常抱怨审查过程缓慢&#xff0c;延迟下游任务&…

什么是GoogLeNet,亮点是什么,为什么是这个结构?

GooLeNet 亮点 最明显的亮点就是引入了Inception&#xff0c;初衷是多卷积核增加特征的多样性&#xff0c;提高泛化能力 &#xff0c;比如&#xff0c;最下边是一个输入层&#xff0c;然后这个输入分别传递给1*1&#xff0c;3 * 3 &#xff0c;5 * 5和一个最大池化层&#xff…

2024春招和暑期实习全面启动!

大家好&#xff0c;我是小柠檬。2024春招和暑期实习全面启动&#xff01;最近&#xff0c;我注意到很多同学都在积极投递简历。 3D视觉求职星球 今天给大家推荐我们的3D视觉岗求职星球&#xff0c;里面时常发布大量3D视觉岗位和星球专属内推岗位。 篇幅有限&#xff0c;文节选…

最新全流程GMS地下水数值模拟及溶质(包含反应性溶质)运移模拟技术深度应用

本文以地下水数值模拟软件GMS操作&#xff0c;本文中强调模块化教学&#xff0c;分为前期数据收集与处理&#xff1b;三维地质结构建模&#xff1b;地下水流动模型构建&#xff1b;地下水溶质运移模型构建和反应性溶质运移构建5个模块&#xff1b;采用全流程模式将地下水数值模…

后端项目访问不了

问题&#xff1a; 后端启动不了&#xff0c;无法访问网站 原因&#xff1a; 1.防火墙没有关 2.有缓存 3、项目没有启动 4、docker没有启动 解决&#xff1a; 先查看进程&#xff1a;docker ps&#xff0c;必须有三个 详细查看&#xff1a;docker ps -a exited代表没有开启…

trunk

介绍&#xff1a; 在华为企业级网络模拟平台&#xff08;eNSP&#xff09;中&#xff0c;“trunk” 是指用于在交换机之间传送多个 VLAN 数据的端口。在华为设备中&#xff0c;“trunk” 端口实际上就是可以承载多个 VLAN 数据流的端口。 当两台交换机之间需要互相传送多个 VLA…

关于多权威属性加密论文阅读

来源于2007年Multi-authority Attribute Based Encryption 从单权威机构到多权威机构的意义是什么呢&#xff1f; 基础方案&#xff08;单权威方案SW&#xff09;支持数据持有者对数据进行加密使用指定的属性集合并且指定一个数值d。当一个用户需要使用该数据时&#xff0c;需…

【LLM】Advanced rag techniques: an illustrated overview

note 文章目录 noteAdvanced rag techniques: an illustrated overview基础RAG高级RAG分块和向量化(Chunking & Vectorisation)搜索索引(Search Index)1. 向量存储索引&#xff08;Vector Store Index&#xff09;2. 多层索引(Hierarchical Indices)3. 假设问题和HyDE(Hypo…

intel realsense D405 在 ROS2 使用示例

1.点云示例 此示例演示如何启动相机节点并使其使用点云选项发布点云。 ros2 launch realsense2_camera rs_launch.py pointcloud.enable:true 以下示例启动相机并同时打开 RViz GUI 以可视化发布的点云。它执行上面的 2 个示例。 ros2 launch realsense2_camera rs_pointcl…

第九个实验:一维数组和二维字符串数组的输入而输出

实验内容: 新建一维数组 新建二维字符串数组 输入内容,运行结果,在输出界面中显示输入的内容 第一步:新建项目 第二步:编程 添加一个INT数控件和字符串控件 修改控件: 复制前面板控件

基于C++和Qt Creator实现的仿制网易云音乐播放器

目录 总体介绍开发环境技术介绍项目目录项目介绍特殊说明Gitee地址 总体介绍 仿照网易云播放器界面实现&#xff0c;目的在于锻炼C编程能力&#xff0c;熟练掌握Qt Creator各种组件的使用及样式设置、界面布局、QtPlugin技术、QXml读写XML文件方法、Qss文件的编写及使用等。 …

协程库项目—协程类模块

ucontext_t结构体、非对称协程 协程类 ucontext_t结构体 头文件中定义的四个函数&#xff08;getcontext(), setcontext(), makecontext(), swapcontext()&#xff09;和两个结构类型&#xff08;mcontext_t, ucontext_t&#xff09;在一个进程中实现用户级的线程切换。 其中…

Spring Boot 中解决跨域的多种方式

Spring Boot 中解决跨域的多种方式 《踏过跨域障碍&#xff1a;Spring Boot 中解决跨域的多种方式》摘要引言正文何为跨域跨域问题出现特征方式一&#xff1a;使用 CrossOrigin 注解方式二&#xff1a;自定义 WebMvcConfigurer方式三&#xff1a;使用 Filter 进行跨域配置 结论…

免费无水印视频素材哪里下载?这几个地方您要知道

哟哟&#xff0c;切克闹&#xff0c;视频剪辑达人们&#xff0c;是不是在视频素材的海洋里迷航了&#xff1f;别着急&#xff0c;今天我就给大家分享几个超实用的无水印短视频素材合集网&#xff0c;让你的创作更加得心应手&#xff0c;从此素材不再是你的烦恼 1&#xff0c;蛙…

Vue3与Vue2:对比分析与迁移指南

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

从零学习Linux操作系统 第三十五部分 Ansible中的角色

一、理解roles在企业中的定位及写法 #ansible 角色简介# Ansible roles 是为了层次化&#xff0c;结构化的组织Playbookroles就是通过分别将变量、文件、任务、模块及处理器放置于单独的目录中&#xff0c;并可以便捷地include它们roles一般用于基于主机构建服务的场景中&…

002typeScript面试,1 理解TS类?2 类的继承 3 修饰符 4 抽象类理解 5 枚举类 enum

1 理解TS类&#xff1f; 2 类的继承 3 修饰符 3-1) private 3-2) protected 3-3) readonly 4 抽象类理解 5 枚举类 enum 5-1&#xff09;枚举模式 5-2&#xff09;数字枚举 5-3&#xff09;字符串枚举 5-4&#xff09;异构枚举