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 {

    @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锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在HTML和CSS当中运用显示隐藏

1.显示与隐藏 盒子显示:display:block;盒子隐藏: display:none:隐藏该元素并且该元素所占的空间也不存在了。 visibility:hidden:隐藏该元素但是该元素所占的内存空间还存在&#xff0c;即“隐身效果”。 2.圆角边框 在CSS2中添加圆角&#xff0c;我们不得不使用背景图像&am…

基于51单片机的DS18B20温度报警器

摘 要&#xff1a;单片机技术已经普及到我们生活&#xff0c;工作&#xff0c;科研&#xff0c;各个领域&#xff0c;已经成为一种比较成熟的技术,本文将介绍一种基于单片机控制的数字温度器&#xff0c;本温度计属于多功能温度计&#xff0c;可以设置上下报警温度&#xff0c;…

生产者延迟消息和重试机制

messageDelayLevel1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 延迟消息级别public PutMessageResult putMessage(final MessageExtBrokerInner msg) {//事务消息处理if (tranType MessageSysFlag.TRANSACTION_NOT_TYPE|| tranType MessageSysFlag.TRANSACT…

静态测试---基于WorkList的活跃变量分析

本文主要用于记录在活跃变量分析实验中的报错及解决&#xff0c;涉及静态测试的详细原理内容较少&#xff0c;编译运行底层逻辑偏多。 一、实验要求 1&#xff09;使用llvm基于框架实现一个基于WorkList的活跃变量分析demo。变量在某个程序点有两种状态&#xff0c;live 或 dea…

如何用俄语预定酒店,柯桥外贸俄语培训

-Привет, Алекс! Как твои дела? 你好&#xff0c;阿列克斯&#xff01;你最近好吗&#xff1f; -Отлично! Я скоро уезжаю на 10 дней в Санкт-Петербург, но никак не могу найти там…

基于高光谱数据集的创新点实现-高斯核函数卷积神经网络

一、高光谱数据集简介 1.1 数据集简介 数据集链接在这:高光谱数据集(.mat.csv)-科研学术 数据集包含下面三个文件&#xff1a; 文件中包含.mat与.csv,145x145x220, 其实主要使用avirissub.csv文件&#xff0c;在代码上只是将mat文件转成了csv文件。具体avirissub.csv如下&am…

第二十四章多栏布局解决方案(什么是自适应?/)

什么是自适应? 指能使网页自适应显示在不同大小终端设备上新网页设计方式及技术.简单的来说自适应就是让同一个页面自动适应不同大小的设备&#xff0c;从而解决为不同设备提供不同版本的页面问题。 1&#xff0e;两列自适应 两列自适应布局是指左侧固定宽度&#xff0c;右…

小程序使用Canvas设置文字竖向排列

在需要使用的js页面引入js文件,传入对应参数即可 /** * 文本竖向排列 */ function drawTextVertical(context, text, x, y) {var arrText text.split();var arrWidth arrText.map(function (letter) {return 26;});var align context.textAlign;var baseline context.…

河北奥润顺达集团研究院PMO经理常江南受邀为第十三届中国PMO大会演讲嘉宾

全国PMO专业人士年度盛会 河北奥润顺达集团研究院PMO经理、研发部运营管理办负责人常江南先生受邀为PMO评论主办的2024第十三届中国PMO大会演讲嘉宾&#xff0c;演讲议题为“初建PMO的体系宣贯和人员培养实践总结”。大会将于6月29-30日在北京举办&#xff0c;敬请关注&#xf…

质量工具系列之Dependency-Track

项目开发中依赖了很多第三方开源工具&#xff0c;对于其版本&#xff0c;漏洞等因为时间或者是数量太多而无法关注到&#xff0c;Dependency-Track解决这些问题。 Dependency-Track 是一个开源组件分析平台&#xff0c;是开放网络应用安全项目&#xff08;OWASP&#xff09;的一…

【自动化运营】PlugLink 1.0开源版发布

什么是PlugLink&#xff1f; PlugLink&#xff0c;顾名思义&#xff0c;就是插件的链接。它旨在帮助个人和小微企业实现运营自动化&#xff0c;通过链接脚本、API、AI大模型等&#xff0c;实现全自动工作流程。你可以把PlugLink看作一个巨大的拼装积木&#xff0c;每一个插件都…

Java解析JSON并修改属性值:从JSON到JsonObject的逐层解析

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 在Java中&#xff0c;可以使用各种库来解析和操作JSON数据。其中&#xff0c;Gson和Jackson是两个非常流行且功能强大的库。在这篇文章中&#xff0c;将使用Gson库来解析给定的JSON字符串&#xff0c;修改operationB…

加氢站压缩液驱比例泵放大器

加氢站压缩液驱液压系统的要求是实现换向和速度控制&#xff0c;对液压动力机构而言&#xff0c;按原理可区分为开式&#xff08;阀控&#xff09;- 节流控制系统和闭式&#xff08;泵控&#xff09;- 容积控制系统&#xff1a; 阀控系统 – 节流调速系统&#xff1a;由BEUEC比…

Sui新共识协议刷新了区块链交易速度的标准

Sui是提供业界领先性能和无限水平扩展的创新Layer 1区块链&#xff0c;今日在官推上宣布其最新共识协议Mysticeti已成功部署到测试网。这一重大突破将Sui测试网的共识时间减少了80%&#xff0c;至390毫秒&#xff0c;同时保持协议的行业领先吞吐量。这一令人印象深刻的演示证明…

英国金融时报:波场TRON出席康奈尔大学区块链会议

近日,康奈尔区块链大会的白金赞助商波场TRON在罗斯福岛的康奈尔大学科技校区(Cornell Tech)举办多项活动,消息得到英国金融时报,费加罗报和Benzinga等权威外媒报道,这全面彰显了波场TRON的领导力。大会吸引了包括学生、学者和行业领袖等在内的 800 多名参与者,凸显了波场TRON致…

【教程】Linux部署Android安卓模拟器

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 未完成&#xff0c; 先简单记录下指令。 docker-android https://github.com/budtmo/docker-android 检查系统是否支持&#xff1a; sudo apt instal…

【ARM+Codesys案例】树莓派+Codesys软PLC方案在包装行业灌装旋盖机的应用

ARM系列支持&#xff1a;全志T3、RK3568、树莓派 机型定义&#xff1a;双工位旋盖机 旋盖机主要适用于不同规格的材质及不同规格的盖、旋&#xff08;轧&#xff09;盖。适用螺旋盖、防盗盖、防撞盖、压入盖等。压力可方便调整&#xff0c;根据瓶盖大小设置取盖位。结构紧凑、…

【Springboot】注解@JsonIgnore、@TableField、@TableId

文章目录 1.JsonIgnore2.TableField(exist false)3.TableId 1.JsonIgnore 2.TableField(exist false) 新增一个字符串字段&#xff0c;并且这个字段是不用到数据库查询的 3.TableId Mybatis-plus 出现 WHERE null?&#xff0c;此时就要加这个注解。

全球十大体育赛事API服务

体育赛事API汇总&#xff1a; Broadage全球橄榄球赛事数据Broadage全球棒球赛事数据Broadage全球篮球实时数据Broadage全球冰球赛事数据Broadage全球排球实时数据TennisApi全球网球赛事讯息Broadage全球足球实时数据棒球数据【纳米数据】

AACR美国癌症研究学会文献及补充文献下载的途径

美国癌症研究学会American Association for Cancer Research&#xff08;AACR&#xff09;创建于1907年&#xff0c;是世界上成立最早、规模最大的致力于全面、创新和高水准癌症研究的科学组织。其出版物包括7种正式出版的期刊&#xff08;其中五种经同行评议*&#xff09;和其…