dp秒杀优惠券

news2024/11/15 9:03:36

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 {

    @Resource
    private SeckillVoucherServiceImpl seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public 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.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 5.2.用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 5.3.代金券id
        voucherOrder.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.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
}

存在两个问题:

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

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

@Override
    public 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);
        }
    }

    @Transactional
    public 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.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 5.2.用户id
        voucherOrder.setUserId(userId);
        // 5.3.代金券id
        voucherOrder.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锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

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

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

相关文章

【机器学习】解锁AI密码:神经网络算法详解与前沿探索

&#x1f440;传送门&#x1f440; &#x1f50d;引言&#x1f340;神经网络的基本原理&#x1f680;神经网络的结构&#x1f4d5;神经网络的训练过程&#x1f686;神经网络的应用实例&#x1f496;未来发展趋势&#x1f496;结语 &#x1f50d;引言 随着人工智能技术的飞速发…

设计模式六大原则之依赖倒置原则

文章目录 概念逻辑关系 小结 概念 依赖倒置原则指在设计代码架构时&#xff0c;高层模块不应该依赖底层模块&#xff0c;二者都应该依赖抽象。抽象不应该依赖于细节&#xff0c;细节应该依赖于抽象。 逻辑关系 如上图所示&#xff0c;逻辑应该就是这样&#xff0c;高层依赖于…

深度学习-语言模型

深度学习-语言模型 统计语言模型神经网络语言模型语言模型的应用序列模型&#xff08;Sequence Model&#xff09;语言模型&#xff08;Language Model&#xff09;序列模型和语言模型的区别 语言模型&#xff08;Language Model&#xff09;是自然语言处理&#xff08;NLP&…

web自动化-数据驱动与失败用例截图、失败重新运行

因为只有失败的用例需要截图&#xff0c;那么问题就是&#xff1a; 什么时候用例会失败&#xff1f; 数据驱动测试 我们前面覆盖到的用例都是正常的用例&#xff0c;如果要测试异常的用例呢&#xff1f; 我们来写一下登录的异常 场景&#xff1a;【login_page】 # 用户输入框…

Adobe AntiCC 简化版 安装教程

Adobe AntiCC 简化版 安装教程 原文地址&#xff1a;https://blog.csdn.net/weixin_48311847/article/details/139277743

opencascade V3d_RectangularGrid 源码学习

类V3d_RectangularGrid V3d_RectangularGrid() V3d_RectangularGrid::V3d_RectangularGrid(const V3d_ViewerPointer &aViewer, const Quantity_Color &aColor, const Quantity_Color &aTenthColor) // 构造函数 ◆ ~V3d_RectangularGrid() virtual V3d_Rectang…

华为诺亚等发布MagicDrive3D:自动驾驶街景中任意视图渲染的可控3D生成

文章链接&#xff1a;https://arxiv.org/pdf/2405.14475 项目链接&#xff1a;https://flymin.github.io/magicdrive3d 虽然可控生成模型在图像和视频方面取得了显著成功&#xff0c;但在自动驾驶等无限场景中&#xff0c;高质量的3D场景生成模型仍然发展不足&#xff0c;主…

NDIS小端口驱动开发(三)

微型端口驱动程序处理来自过度驱动程序的发送请求&#xff0c;并发出接收指示。 在单个函数调用中&#xff0c;NDIS 微型端口驱动程序可以指示具有多个接收 NET_BUFFER_LIST 结构的链接列表。 微型端口驱动程序可以处理对每个NET_BUFFER_LIST结构上具有多个 NET_BUFFER 结构的多…

树莓派部署harbor_arm64

文章目录 树莓派4b部署Harbor-arm64版本docker-compose维护命令访问harbor 192.168.1.111认用户名密码admin/Harbor12345 树莓派4b部署Harbor-arm64版本 harbor-arm版本 部署&#xff1a;参考 wget https://github.com/hzliangbin/harbor-arm64/releases/download/v1.9.3/ha…

NFS p.1 服务器的部署以及客户端与服务端的远程挂载

目录 介绍 应用 NFS的工作原理 NFS的使用 步骤 1、两台机子 2、安装 3、配置文件 4、实验 服务端 准备 启动服务&#xff1a; 客户端 准备 步骤 介绍 NFS&#xff08;Network File System&#xff0c;网络文件系统&#xff09;是一种古老的用于在UNIX/Linux主…

redis数据类型之string,list

华子目录 key操作说明SCAN cursor [MATCH pattern] [COUNT count]dump与restorekeys 通配符 示例演示 string说明setbit key offset valuegetbit key offsetsetrange key offset value List结构图相关命令lrem key count valueltrim key count value示例&#xff1a;使用 LTRIM…

Blazor入门-svg绘制-碰撞检测和图形坐标调整

上一篇&#xff1a; Blazor入门-简单svg绘制导出图像_blazor 画图-CSDN博客 https://blog.csdn.net/pxy7896/article/details/139003443 注意&#xff1a;本文只给出思路和框架&#xff0c;对于具体的计算细节&#xff0c;考虑到日后会写入软件著作权和专利文书&#xff0c;因…

被追着问UUID和自增ID做主键哪个好,为什么?

之前无意间看到群友讨论到用什么做主键比较好 其实 UUID 和自增主键 ID 是常用于数据库主键的两种方式&#xff0c;各自具有独特的优缺点。 UUID UUID 是一个由 128 位组成的唯一标识符&#xff0c;通常以字符串形式表示。它可以通过不同的算法生成&#xff0c;例如基于时间…

绝招曝光!3小时高效利用ChatGPT写出精彩论文

在这份指南中&#xff0c;我将深入解析如何利用ChatGPT 4.0的高级功能&#xff0c;指导整个学术研究和写作过程。从初步探索研究主题&#xff0c;到撰写结构严谨的学术论文&#xff0c;我将一步步展示如何在每个环节中有效运用ChatGPT。如果您还未使用PLUS版本&#xff0c;可以…

C++入门 ros自定义msg话题通信

一、 开发环境 ubuntu20.04 ros版本noetic 参考视频 https://www.bilibili.com/video/BV1Ci4y1L7ZZ/?p52&spm_id_from333.1007.top_right_bar_window_history.content.click&vd_source4cd1b6f268e2a29a11bea5d2568836ee 二、 编写msg文件 在功能包下面创建msg文件夹…

【源码】6语言跨境电商PHP源码 精美UI+功能强大开源无授权

6语言跨境电商PHP源码 精美UI功能强大开源无授权 英文&#xff0c;简体中文&#xff0c;繁体中文&#xff0c;日语、泰语、越南语6语言。功能非常强大&#xff0c;UI也很漂亮的跨境电商源码。基于国外成熟电商系统二开的源码&#xff0c;带POS系统。 系统采用Laravel框架开发…

盘点好用的国产传输软件,看看哪个适合你

流动让数据释放价值&#xff0c;无论什么企业&#xff0c;什么行业&#xff0c;业务的正常开展均是以数据和文件的传输为基础&#xff0c;因此&#xff0c;对企业来说&#xff0c;文件传输工具是最基础但也是最举重若轻的。在琳琅满目的多种国产传输软件中&#xff0c;哪个是最…

【运筹学】前言:基础知识

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

CISCN --EzHeap

当时有点着急了&#xff0c;这题没写出来&#xff0c;结束后在ctfshow上做了一下。 使用的方法是environ泄露栈地址&#xff0c;然后在栈上构造orw的rop链。 以下是过程&#xff1a; 只能orw。 堆体开沙盒模式会在heap和bin一开始构造很多垃圾堆。所以分配和free的时候要注意…

1806 jsp防疫物资销售管理系统 Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 jsp 防疫物资销售管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助采用了java设计&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统采用web模式&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.…