范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

SpringBoot中对于超卖现象的问题分析和解决方案

  本文只针对单体应用的高并发导致超卖的处理方案。
  超卖是指商品本来只有固定的数量比如10个,但是在某一时刻有大量的并发请求涌入,导致商品卖出去了比如100个,这就是超卖现象。
  本文以7种方案来实现减库存操作,然后分析每个方案有什么问题,哪个方案可以解决超卖。场景设计
  创建数据库:create database mytest charset=utf8; 复制代码
  创建一个商品表:USE mytest; DROP TABLE IF EXISTS `tb_product`; CREATE TABLE `tb_product`  (   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT "主键id",   `name` varchar(64) NOT NULL COMMENT "用户名,唯一",   `price` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT "价格",   `stock` int(10) NOT NULL DEFAULT 0 COMMENT "库存",   PRIMARY KEY (`id`) USING BTREE, ) ENGINE = InnoDB CHARACTER SET = utf8; 复制代码
  然后插入一条数据:INSERT INTO `mytest`.`tb_product`(`id`, `name`, `price`, `stock`) VALUES (1, "iPhone6S", 5000.00, 1); 复制代码
  现在,我们有了一个商品,且它的库存stock是1,即只有一个。JMeter模拟高并发
  JMeter可以模拟高并发场景,具体的使用请看我的这篇文章:JMeter的下载和使用
  模拟一下子进来500个请求。方案一(事务)
  先来看看一个商品减库存函数,分析在高并发下会出现的问题:/**  * 简单的减库存操作,不支持高并发  * @author cc  * @date 2021-12-30 15:04 */ @Transactional(rollbackFor = Exception.class) public void sampleSale(Long productId) {     TbProduct product = productDao.selectByPrimaryKey(productId);     if (product == null) {         throw new RuntimeException("没有找到该商品");     }     int stock = product.getStock() - 1;     if (stock >= 0) {         product.setStock(stock);         int r = productDao.updateByPrimaryKeySelective(product);         if (r <= 0) {             throw new RuntimeException("商品减库存失败");         }     } else {         throw new RuntimeException("库存不足");     } } 复制代码
  在上面的函数中,先获取该商品的信息,拿到库存数,当库存数足够,就进行减库存操作。
  但是问题是,在高并发下,会有多个线程同时读到商品的库存为1,然后就都进行了减库存操作。假如同一时刻有10个线程,那么减库存操作就会执行10次,商品库存数由1变成了-9。
  所以该方案是不行的。方案二(事务 + 方法锁)/**  * 事务 + synchronized,也不能解决高并发  * 因为AOP会在方法执行前开启事务,所以有可能在开启事务后执行方法的间隙中,有其他的线程同时开启了事务,只是概率很小,多试几次就能试出来  * 所以这种方式仍然不能解决超卖问题  * @author cc  * @date 2021-12-30 15:05 */ @Transactional(rollbackFor = Exception.class) public synchronized void syncSale(Long productId) {     TbProduct product = productDao.selectByPrimaryKey(productId);     if (product == null) {         throw new RuntimeException("没有找到该商品");     }     int stock = product.getStock() - 1;     if (stock >= 0) {         product.setStock(stock);         int r = productDao.updateByPrimaryKeySelective(product);         if (r <= 0) {             throw new RuntimeException("商品减库存失败");         }     } else {         throw new RuntimeException("库存不足");     } } 复制代码
  和方案一类似,但是在方法前面加了synchronized,经过测试方案二比方案一要好得多,但是多测几遍,会发现超卖问题依然存在,只是概率低了一些。
  这是因为Spring的AOP会在方法执行前开启事务,然后再进入加锁的方法。问题在开启事务和执行加锁方法的间隙有可能有其他线程同时开启了事务,只是这个概率比较低。
  所以这种方式仍然不能解决超卖问题。方案三(事务 + 代码块锁)/**  * 解决上面多个线程同时开启了事务的问题,将synchronized放到函数块里面  * 可以解决超卖,但是性能比较影响,并且多个请求要排队等待,不建议使用  * @author cc  * @date 2021-12-30 15:10 */ public void manualSale(Long productId) {     synchronized (this) {         sampleSale(productId);     } } 复制代码
  这种是方案二的优化版,将锁放到代码块,解决了方案二的问题。
  缺点是整个代码块都加锁,如果减库存之后还有其他的耗时操作,其他的请求就需要排很久的队。方案四(手撸SQL)
  通过这样的SQL也可以解决超卖问题:update `tb_product` set stock = stock - #{amount} WHERE id = #{productId} AND stock > 0 复制代码/**  * 手撸sql的方式解决超卖问题  * InnoDB会自动给UPDATE、DELETE、DELETE语句添加排他锁  * @author cc  * @date 2021-12-30 15:03 */ public void sqlSale(Long productId) {     int amount = 1; // 要扣减的数量     int r = productDao.updateStockById(productId, amount);     if (r <= 0) {         throw new RuntimeException("商品减库存失败");     } } 复制代码
  这是因为InnoDB引擎会自动给UPDATE、DELETE、DELETE语句添加排他锁,所以通过这样的语句可以防止超卖。
  优点很明显,简单方便。
  缺点仍然很明显,每一次都要操作数据库,对系统会造成很大的压力。
  所以在高并发这种场景下这个方案不适用。方案五(Redis缓存)
  方案四的缺点在IO,那么就用Redis在内存中处理好了。
  关于Redis可以看我的这篇文章:Spring Boot中Redis的基本使用和优雅的接口数据缓存
  使用Redis,我们要提前将商品数据缓存起来:redisTemplate.opsForHash().increment("stock", "product_1", 1); 复制代码
  缓存的方式有很多种,不一定用hash的incr,这里只是做一个示例。
  现在我们在Redis中有一个库存为1的商品,来看看代码示例:/**  * 普通的redis策略,将库存放到缓存中,不做其他处理  * 缺点:不支持高并发,会出现超卖  * @author cc  * @date 2021-12-30 14:55 */ public void redisNormal(Long productId) {     String productKey = "product_" + productId;     // 获取缓存中商品的库存量     int stock = Integer.parseInt(redisTemplate.opsForHash().get("stock", productKey).toString());     // 扣减库存     if (stock > 0) {         redisTemplate.opsForHash().increment("stock", productKey, -1);     } else {         throw new RuntimeException("库存不足");     }      // 模拟商品下单的耗时操作     try {         Thread.sleep(2000);     } catch (Exception e) {         // 商品下单失败         System.out.println("商品下单失败");     } } 复制代码
  我们将商品库存的查询放到了内存中,速度更快,但是上面的代码在高并发下会出现超卖现象,所以我们要对查询操作进行加锁。方案六(Redis + synchronized)/**  * redis策略升级版,用synchronized给库存操作上锁  * 优点:支持高并发  * @author cc  * @date 2021-12-30 14:57 */ public void redisBySync(Long productId) {     synchronized (this) {         String productKey = "product_" + productId;         // 获取缓存中商品的库存量         int stock = Integer.parseInt(redisTemplate.opsForHash().get("stock", productKey).toString());         // 扣减库存         if (stock > 0) {             redisTemplate.opsForHash().increment("stock", productKey, -1);         } else {             throw new RuntimeException("库存不足");         }     }      // 模拟商品下单的耗时操作     try {         Thread.sleep(2000);     } catch (Exception e) {         System.out.println("商品下单失败");     } } 复制代码方案七(Redis + Lock)private Lock lock = new ReentrantLock();  /**  * redis策略升级版,用lock给库存操作上锁  * 优点:支持高并发,使用起来比synchronized更灵活  *  * @author cc  * @date 2021-12-30 14:59 */ public void redisByLock(Long productId) {     String result = null;     lock.lock();     try {         String productKey = "product_" + productId;         // 获取缓存中商品的库存量         int stock = Integer.parseInt(redisTemplate.opsForHash().get("stock", productKey).toString());         System.out.println("stock: " + stock);          // 扣减库存         if (stock > 0) {             redisTemplate.opsForHash().increment("stock", productKey, -1);         } else {             result = "库存不足";         }     } catch (RuntimeException e) {         e.printStackTrace();     } finally {         lock.unlock();     }     if (result != null) {         throw new RuntimeException(result);     }      // 模拟商品下单的耗时操作     try {         Thread.sleep(2000);     } catch (Exception e) {         System.out.println("商品下单失败");     } } 复制代码
  方案六和方案七只是加锁的方式不一样,Lock比起synchronized,在使用上更加灵活,所以在使用上可以看场景来决定。
  两个方案都可以解决高并发下导致的超卖问题,并且是将锁加到库存查询操作中,不影响商品下单的操作,而且使用的是内存,所以速度更快。
  作者:失败的面
  链接:https://juejin.cn/post/7047681777036427271

1月24日晚间股市利好消息一览一板块利好1住建部支持上海无锡武汉长沙等城市推进智能网联汽车协同发展技术创新住房和城乡建设部印发十四五推动长江经济带发展城乡建设行动方案。其中提到,推进智慧城市基础设施与智能网联汽相距2米!红外隔空充电成功(记者袁一雪)日前,同济大学电子与信息工程学院教授刘庆文团队研发的红外隔空充电技术,将智能手机和AR眼镜的移动传能距离拓展到2米。该技术不仅可以在室内为智能手机智能眼镜等移动终端远充电5分钟续航200公里更有首款飞行汽车!小鹏汽车太牛了!小鹏汽车创新如今已成为中国的代名词,数不尽的新技术为我们带来了便捷的生活体验,其中在汽车行业中,小鹏汽车便是一直以创新为引领,一跃成为国内汽车智能体验方面的佼佼者。但小鹏汽车并不满Google第一款智能手表PixelWatch最早或将于5月26日发布Google公司的第一款智能手表在互联网上流传时日已久,但到目前为止还没有任何官方消息发布。不过据悉,这款手表的研发已接近最后阶段,JonProsser在一条推文中说,这是他们第一魅族18s推送更新修复蓝牙无法发送微信语音消息问题魅族18s手机于1月21日起,陆续获得了Flyme9。2。2。2A更新推送。本次更新优化了整体稳定性,修复了以下问题修复连接蓝牙耳机或车载蓝牙播放音乐时,无法发送微信语音消息的问题努比亚新音C1蓝牙耳机评测百元价格也能有千元体验?近年来,TWS耳机逐渐火爆,成为目前真蓝牙无线耳机领域中的主流趋势,而笔者作为数码爱好者,自然而然也是紧跟时代的趋势,先后入手了多款TWS蓝牙耳机,不得不说,有些耳机虽说价格亲民,汉王科技(002362。SZ)拟向中科阅深增资以促进文本大数据业务的整体发展智通财经APP讯,汉王科技(002362。SZ)公告,为更好支持子公司进行手写体识别复杂图像文档分析与识别技术的研发,经各方友好协商,公司控股子公司北京汉王数字科技有限公司(汉王数美媒腋窝传感器智能餐饮北京用这些措施保障冬奥来源环球时报美国全国公共广播电台1月22日文章,原题冬奥会开幕在即,中国旨在通过腋窝传感器机器人和更多装备来遏制新冠疫情煮好的面条将由机器人端上餐桌随着约1。3万名各国运动员和记者早知道十部门联合发文推动充换电中国移动实控人拟30亿至50亿元增持股份摘要最高法发布审理证券市场虚假陈述侵权民事赔偿案件的若干规定。十部门联合发文推动充换电,十四五末能满足超过2000万辆电动汽车需求。人社部等再次对美团等11家平台企业保障新就业形态虚拟机之战WASM与EVM万向区块链小课堂什么是EVM(EthereumVirtualMachine,以太坊虚拟机)?以太坊虚拟机是一种轻量级虚拟机,用于在以太坊网络上运行各种智能合约。EVM的功能不同于传统的操作系统,例5G发展持续向纵深挺进,中国手机行业未来可期1月20日,5G发展迎来又一里程碑工信部宣布,截至2021年年底,中国5G手机终端连接数达5。18亿户。这相当于自5G牌照发放之日起,平均每天新增入网手机达54万部之多。在网络部署
aampampamps2020年度AI创新产品评选公告as深耕安防行业20多年,见证了产业由小到大由弱到强的发展过程,经历了从引进贴牌到自主开发再到全面赶超的产品时代沿革,为了这个产业的壮大,新老安防人前赴后继扑在产品开发与技术钻研的试驾广汽埃安AIONVPlus,702公里工况续航,还有高速领航辅助?关注并标星电动星球News每天打卡阅读更深刻理解汽车产业变革出品电动星球News作者毓肥昨天,我们在佛山首批试驾了广汽埃安的新款SUV,AIONVPlus。和前不久的SPlus一样自动售货机行业怎样获客,发展新模式,这六个方法你知道几个在5G时期,24小时无人自动售卖机将更好像一个现实与数据连接的对话框,每一台终端设备,每一个顾客,都能够在最方便快捷的情况下,得到特殊的服务,针对品牌企业来讲,自动售卖机将变成一种招商加盟小白,如何探索营销新模式,获取高质量精准客户从往年迄今,肺炎疫情对许多领域而言都是挺大的冲击性,尤其是对许多靠线下推广运营的领域来说是严重的严厉打击。许多公司都面对着破产倒闭的困境,毫无疑问针对许多加盟招商领域而言也还有较大深圳规模最大的跨境电商展即将启航,安防企业注意了2020年受疫情暴发影响,线下消费及活动受到了许多因素的阻碍,全球消费者正主动或被动地将消费行为转移到线上,宅经济需求爆发,让跨境电商平台成为全球消费者采购的重要渠道。据海关总署发集装箱运价开启暴涨的模式,原因是什么?以上海港为例,上海港到美西美东航线船舶平均舱位利用率均在95以上,多数班次都处于满载水平,部分班次甚至出现暴舱。受基本面支持,大部分航商提高了各自订舱价格,即期市场运价上涨。那么华每天都用的卫生间,如何布局才合理?卫生间是我们每日起居用到最频繁的空间,关于它的设计,除了保证淋浴如厕洗手三个功能的齐全外,动线是否合理,布局是否恰当也十分重要。今天,我们就从卫生间的布局入手,来看看不同形状的卫生圆形vs方形餐桌,究竟哪种更好?餐桌,是每个家庭必不可少的用餐家具,而且从材质上分类,餐桌就有木质石材金属等选择而在外观上,餐桌就有圆形与方形的可选。从餐桌材质来说,可以根据喜好来选择,但从外观来说,圆形的餐桌与郝记者试驾全新凌派省油省钱又智能,日常家用的首选推荐在日常家用中,全新凌派的出现,无疑给了消费者一种更聪明的选择。自2013年上市,它已收获超95万消费者的支持。时间线来到2021年,全新凌派售价区间已经敲定,10。9816。98万选冰箱就是选生活!卡萨帝聚焦生活被首选2007年1月9日,乔布斯掏出了他的宝物(第一代iphone)说今天,苹果要重新发明手机。一个新时代的大幕开启,2020年数据显示,苹果市值1。22万亿美元。而在2020年,卡萨帝先别急着吊顶,4份吊顶装饰线设计先看看再说吊顶,一道普及度非常高的天花板装饰,除非没有需求或者预算不足才不考虑。虽然吊顶可以强化空间层次的美感可以起到隔音作用可以隐藏中央空调内机筒灯等等,但改变不了大白吊顶单调的事实,它唯