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

dp秒杀优惠券

1、全局id生成器

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显
  • 受单表数据量的限制

场景分析:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。

场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的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);// 2.生成序列号long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.1.获取当前日期,精确到天// 2.2.自增长String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}}

利用线程池创建300个并发线程,每个线程生成100个id,总耗时time = 2629ms

@Test
void 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、添加优惠券,实现秒杀下单

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

@Service
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate SeckillVoucherServiceImpl seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1、查询优惠券信息SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2、判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now()) || voucher.getEndTime().isBefore(LocalDateTime.now())){return Result.fail("秒杀未开始");}// 3、判断库存是否充足if(voucher.getStock()<1) {return Result.fail("库存不足");}// 4、扣减库存voucher.setStock(voucher.getStock()-1);boolean success = seckillVoucherService.updateById(voucher);if(!success) {return Result.fail("库存不足");}// 5、创建订单VoucherOrder voucherOrder = new VoucherOrder();// 5.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 5.2.用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 5.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}
}

3、解决超卖问题

jmeter分析时记得在HTTP信息头管理器中加上token

测试1秒200个并发量,发现会出现超卖问题,100个订单扣减,库存剩下-9个。

乐观锁解决超卖问题

// 4、扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();

4、一人一单

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

// 根据用户id与优惠券id,判断订单是否存在
Long useId = UserHolder.getUser().getId();
Integer count = query().eq("user_id", useId).eq("voucher_id", voucherId).count();
if(count > 0) {return Result.fail("用户已经购买过一次!");
}
// 4、扣减库存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();

但是上述代码涉及到查询与修改,因此还是会有多线程安全问题。用jmeter测试,发现还是有相同用户id与优惠券id的订单超卖,没有达到一人一单的需求。

因此尝试加锁。由于存在较多的写操作,因此采用悲观锁。但如果对后面一大段设计增删改查的代码加锁,锁粒度太大。如下代码所示。

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {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") // 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);// 7.返回订单idreturn Result.ok(orderId);
}

存在两个问题:

1)且由于在createVoucherOrder代码外加上事务,事务包含锁,因此会导致加锁读操作后事务还未提交,就提前将所释放,下一个线程获取锁时,读取到的数据库的值为旧值,造成数据不一致性,因此需要在事务外加锁。

2)将synchronized加在方法上,相当于是对this加锁,因此多线程过来加的是一把锁,串行化,性能差。由于需求是一人一单,因此只需要对同一用户加锁。因此去除ThreadLocal中的userId进行加锁。但每次线程进入createVoucherOrder方法都会新建一个userId对象,所以其实本质上还是对不同的对象进行了加锁,userId.toString()的底层也是new一个string对象,但我们需要的是同一用户只有一把锁,因此需要intern() 这个方法从常量池中拿到数据。修改代码:

@Overridepublic Result seckillVoucher(Long voucherId) {// 1、查询优惠券信息SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2、判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now()) || voucher.getEndTime().isBefore(LocalDateTime.now())){return Result.fail("秒杀未开始");}// 3、判断库存是否充足Integer stock = voucher.getStock();if(voucher.getStock()<1) {return Result.fail("库存不足");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {return createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();// 根据用户id与优惠券id,判断订单是否存在int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0) {return Result.fail("用户已经购买过一次!");}// 4、扣减库存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!success) {//扣减库存return Result.fail("库存不足!");}// 5、创建订单VoucherOrder voucherOrder = new VoucherOrder();// 5.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 5.2.用户idvoucherOrder.setUserId(userId);// 5.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}

但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务

1)在启动类上加上

2)在pom.xml文件里加上依赖

<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>

3)修改代码

synchronized (userId.toString().intern()) {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);
}

记得将seckillVoucher方法上的事务注解取消,否则还是会出现上述问题。

查看数据库,成功实现一人一单

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

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。

开启两份服务,同一用户下单两次(负载均衡算法采用轮询),库存扣减两次。

有关锁失效原因分析

由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

相关文章:

  • k8s部署calico遇到的问题
  • python -【四】函数
  • 2024华为OD机试真题-素数之积-C++-OD统一考试(C卷D卷)
  • Textual for Mac:轻量级IRC客户端
  • 安卓赤拳配音v1.0.2Ai配音神器+百位主播音色
  • Rust一维Vec垂直方向拼接、水平方向拼接,多个二维Vec垂直方向拼接
  • STM32-13-MPU
  • Linux内核编译流程3.10
  • 24V_2A_1.2MHZ|PCD0303升压恒频LCD背光源专用电路超小体积封装
  • 前端学习--React部分
  • ROS | C++和python实现发布结点和订阅结点
  • 目标检测 | R-CNN、Fast R-CNN与Faster R-CNN理论讲解
  • 基于Python实现 HR 分析(逻辑回归和基于树的机器学习)【500010104】
  • k8s ceph(静态pvc)
  • 【漏洞复现】大华智能物联综合管理平台 log4j远程代码执行漏洞
  • JavaScript-如何实现克隆(clone)函数
  • 【编码】-360实习笔试编程题(二)-2016.03.29
  • 【每日笔记】【Go学习笔记】2019-01-10 codis proxy处理流程
  • angular2 简述
  • canvas 高仿 Apple Watch 表盘
  • CentOS7简单部署NFS
  • CentOS学习笔记 - 12. Nginx搭建Centos7.5远程repo
  • C学习-枚举(九)
  • HashMap ConcurrentHashMap
  • Java知识点总结(JDBC-连接步骤及CRUD)
  • java中具有继承关系的类及其对象初始化顺序
  • leetcode98. Validate Binary Search Tree
  • Mysql优化
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • php中curl和soap方式请求服务超时问题
  • Quartz初级教程
  • SegmentFault 技术周刊 Vol.27 - Git 学习宝典:程序员走江湖必备
  • 容器服务kubernetes弹性伸缩高级用法
  • 如何实现 font-size 的响应式
  • 我从编程教室毕业
  • 一起来学SpringBoot | 第三篇:SpringBoot日志配置
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • 职业生涯 一个六年开发经验的女程序员的心声。
  • 策略 : 一文教你成为人工智能(AI)领域专家
  • 如何用纯 CSS 创作一个货车 loader
  • ​2021半年盘点,不想你错过的重磅新书
  • #、%和$符号在OGNL表达式中经常出现
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • (2.2w字)前端单元测试之Jest详解篇
  • (3) cmake编译多个cpp文件
  • (delphi11最新学习资料) Object Pascal 学习笔记---第13章第1节 (全局数据、栈和堆)
  • (八十八)VFL语言初步 - 实现布局
  • (第一天)包装对象、作用域、创建对象
  • (数据大屏)(Hadoop)基于SSM框架的学院校友管理系统的设计与实现+文档
  • (数据结构)顺序表的定义
  • (太强大了) - Linux 性能监控、测试、优化工具
  • (小白学Java)Java简介和基本配置
  • (一)Spring Cloud 直击微服务作用、架构应用、hystrix降级
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .Net Redis的秒杀Dome和异步执行