订单减库存设计
$goods->query('update order set = store- num where store>=num and goodID = 12345');
$goods->query('update order set = store- num where store>=num and goodID = 12345');
一、扣减库存的三种方案
(1)下单减库存(秒杀商品这种方式最好)
用户下单(确认订单)时减库存
优点:实时减库存,避免付款时因库存不足减库存的问题
缺点:恶意买家大量下单,将库存用完,但是不付款,真正想买的人买不到
(2)付款后减库存(支付成功回调时减库存)
下单页面显示最新的库存,下单时不会立即减库存,而是等到支付时才会减库存。
优点:防止恶意买家大量下单用光库存,避免下单减库存的缺点
缺点:下单页面显示的库存数可能不是最新的库存数,而库存数用完后,下单页面的库存数没有刷新,出现下单数超过库存数,若支付的订单数超过库存数,则会出现支付失败。(其他用户可能提示库存不足,可能出现超卖问题) 因第三方支付返回结果存在时差,同一时间多个用户同时付款成功,会导致下单数目超过库存,商家库存不足容易引发断货和投诉,成本增加
(3)预扣库存(确认订单之后与支付之间保留库存,调起支付界面前锁定库存, 业务系统中最常见的就是预扣库存方案)
下单页面显示最新的库存,下单后保留这个库存一段时间(比如10分钟),超过保留时间后,库存释放。若保留时间过后再支付,如果没有库存,则支付失败。例如:要求30分钟内支付订单。(①.支付前预占库存,②.限制支付时间:若订单创建成功N分钟不付款,则订单取消,库存回滚,③.检测恶意下单用户加入到店铺黑名单,④.加入限购):设置了“付款减库存”后,买家拍下商品待付款,这是系统会预扣库存出来,就叫“预扣库存”。若买家30分钟后依然未付款,预扣库存释放,回增到总库存里。若买家30分钟内付款了,库存就真正被扣除 ;存在预扣库存时,无法编辑商品库存,也无法更改“库存计数”的设置
预扣库存须知:
1、问:为什么我在商品发布页面看到了预扣库存? 答:预扣库存是指付款减库存商品被拍下30分钟未付款的数量。 2、问:为什么我有的商品发布页面有预扣库存数量设置,有的又没有? 答:目前该功能在测试期间,只针对部分商品开通此功能。 3、问:我修改商品数量的时候,为什么报错“不能低于预扣库存”? 答:为了保障已经拍下宝贝的买家在30分钟内付款能够买到商品,不允许卖家修改商品数量小于预扣库存数量。 4、问:为什么我发布的商品数和宝贝页面看到的可售数量不一样? 答:宝贝页面展示的可售数量=卖家维护的商品数量-预扣库存数量。 5、问:为什么我要删除部分sku库存无法删除? 答:该宝贝有未付款订单,请联系买家进行支付或者取消订单后才能删除sku库存。
高并发下减库存操作避免超卖:
1:事务+行锁(悲观锁)
for update:这是数据库行锁,也是我们常用的悲观锁,可用于针对某商品的秒杀操作,但是当出现主键索引和非主键索引同时等待对方时,会造成数据库死锁
select * from goods where ID=1 for update
for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效
2:设置无符号:性能是1种的三倍 try sql语句 当库存不够时catch捕捉错误 返回数量不足
3:乐观锁:先比较在更新
方式1:case:该方式在库存小于购买商品数量时会冗余的更新一遍库存且不能正确获取是否扣减成功,所以还得查询一次数据库:
UPDATE goods SET store = CASE WHEN store>= num THEN store-num ELSE store END
方式2:where语句添加条件判断:性能又略微优于设置无符号
UPDATE goods SET store = store-num where id=1 and store>num
数据表上设计一个数据更新字段作递增的版本号, 每次修改时修改数据版本号或者时间戳;修改数据的时候首先把这条数据的版本号查出来,update时判断这个版本号是否和数据库里的一致,如果一致则表明这条数据没有被其他用户修改,若不一致则表明这条数据在操作期间被其他客户修改过,此时需要在代码中抛异常或者回滚等
update tb set name='yyy' and version=version+1 where id=1 and version=version;
1. SELECT name AS old_name, version AS old_version FROM tb where ...; 2. 根据获取的数据进行业务操作,得到new_name和new_version 3. UPDATE SET name = new_name, version = new_version WHERE version = old_version if (updated row > 0) { // 乐观锁获取成功,操作完成 } else { // 乐观锁获取失败,回滚并重试 }
4:阻塞队列
请求过来的时候放到固定大小的阻塞队列,请求结束后或者秒杀时间结束后,遍历队列,做减操作,这需要显示购买数量,因为100个请求不等于100个库存
建议采用无符号加乐观锁
优点:结合下单减库存的优点,实时减库存,且缓解恶意买家大量下单的问题,保留时间内未支付,则释放库存。
缺点:保留时间内,恶意买家大量下单将库存用完。并发量很高的时候,依然会出现下单数超过库存数。
二、如何解决恶意买家下单的问题
这里的恶意买家指短时间内大量下单,将库存用完的买家。
(1)限制用户下单数量
优点:限制恶意买家下单
缺点:用户想要多买几件,被限制了,会降低销售量
(2)标识恶意买家
优点:卖家设定一个备用库存,当支付时,库存已用完,扣减备用库存数,这就是常见的补货场景
缺点:因高并发场景下,数据可能存在不一致性的问题
三、如何解决下单成功而支付失败(库存不足)的问题
(1)备用库存
商品库存用完后,如果还有用户支付,直接扣减备用库存。
优点:缓解部分用户支付失败的问题
缺点:备用库存只能缓解问题,不能从根本上解决问题。另外备用库存针对普通商品可以,针对特殊商品这种库存少的,备用库存量也不会很大,还是会出现大量用户下单成功却因库存不足而支付失败的问题。
四、如何解决高并发下库存超卖的场景
库存超卖最简单的解释就是多成交了订单而发不了货。
场景:
用户A和B成功下单,在支付时扣减库存,当前库存数为10。因A和B查询库存时,都还有库存数,所以A和B都可以付款。
A和B同时支付,A和B支付完成后,可以看做两个请求回调后台系统扣减库存,有两个线程处理请求,两个线程查询出来的库存数 inventory=10,
然后A线程更新最终库存数 lastInventory=inventory - 1 = 9,
B线程更新库存数 lastInventory=inventory - 1 = 9。
而实际最终的库存应是8才对,这样就出现库存超卖的情况,而发不出货。
那如何解决库存超卖的情况呢?
1.SQL语句更新库存时,如果扣减库存后,库存数为负数,直接抛异常,利用事务的原子性进行自动回滚。
2.利用SQL语句更新库存,防止库存为负数
UPDATE [库存表] SET 库存数 - 1 WHERE 库存数 - 1 > 0
五、秒杀场景下如何扣减库存
(1)下单减库存
因秒杀场景下,大部分用户都是想直接购买商品的,可以直接用下单减库存。
大量用户和恶意用户都是同时进行的,区别是正常用户会直接购买商品,恶意用户虽然在竞争抢购的名额,但是获取到的资格和普通用户一样,所以下单减库存在秒杀场景下,恶意用户下单并不能造成之前说的缺点。
而且下单直接扣减库存,这个方案更简单,在第一步就扣减库存了。
(2)将库存放到redis缓存中
查询缓存要比查询数据库快,所以将库存数放在缓存中,直接在缓存中扣减库存。然后在通过MQ异步完成数据库处理。
(3)使用量自增方式
可以先增加已使用量,然后与设定的库存进行比较,如果超出,则将使用量减回去。
项目中用到了很多机制,但是没有总结出来,学习架构需要不断地总结。
六 第三方支付
1 支付成功多次回调:把减库存放在微信支付的成功回调URL的方法里面。但是微信支付成功之后微信支付平台会发送8次请求到回调地址。这样的做法就会导致库存减少,一定要验证返回的编号是否已经完成扣减。
避免超卖:
主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行SQL语句来报错;再有一种就是使用CASE WHEN判断语句,例如这样的SQL语句:
UPDATE item SET store = CASE WHEN store>= num THEN store-num ELSE store END
秒杀库存处理:
秒杀前将库存放缓存中(redis,mamecache) (没有复杂的SKU库存和总库存这种联动关系)SKU库存:比如淘宝商品的属性,颜色,尺寸等没个属性对应的商品数量都不同
$store=10;//库存10个
for($i=0;$i<$store;$i++){
$redis->lpush('goods_store',1);
}
秒杀活动开始后:
$res=$redis->lpop('goods_store');//移除并返回列表
if($res){
$where['g_id'] =1;
$res=$pdo->table('good')->where($where)->setDec('goods_store',1);
}else{
echo '秒杀失败';
}
秒杀复杂的SKU库存方案:redis hash能解决吗 或者事务处理
其他问题:
由于MySQL存储数据的特点,同一数据在数据库里肯定是一行存储(MySQL),因此会有大量线程来竞争InnoDB行锁,而并发度越高时等待线程会越多,TPS(Transaction Per Second,即每秒处理的消息数)会下降,响应时间(RT)会上升,数据库的吞吐量就会严重受影响。 这就可能引发一个问题,就是单个热点商品会影响整个数据库的性能, 导致0.01%的商品影响99.99%的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。 而分离热点商品到单独的数据库还是没有解决并发锁的问题,我们应该怎么办呢?要解决并发锁的问题,有两种办法: 应用层做排队。按照商品维度设置队列顺序执行,这样能减少同一台机器对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用太多的数据库连接。 数据库层做排队。应用层只能做到单机的排队,但是应用机器数本身很多,这种排队方式控制并发的能力仍然有限,所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种MySQL的InnoDB层上的补丁程序(patch),可以在数据库层上对单行记录做到并发排队。 你可能有疑问了,排队和锁竞争不都是要等待吗,有啥区别? 如果熟悉MySQL的话,你会知道InnoDB内部的死锁检测,以及MySQL Server和InnoDB的切换会比较消耗性能,淘宝的MySQL核心团队还做了很多其他方面的优化,如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的补丁程序,配合在SQL里面加提示(hint),在事务里不需要等待应用层提交(COMMIT),而在数据执行完最后一条SQL后,直接根据TARGET_AFFECT_ROW的结果进行提交或回滚,可以减少网络等待时间(平均约0.7ms)。据我所知,目前阿里MySQL团队已经将包含这些补丁程序的MySQL开源。 另外,数据更新问题除了前面介绍的热点隔离和排队处理之外,还有些场景(如对商品的lastmodifytime字段的)更新会非常频繁,在某些场景下这些多条SQL是可以合并的,一定时间内只要执行最后一条SQL就行了,以便减少对数据库的更新操作。
超级详细的博客: https://blog.csdn.net/qq_33862644/article/details/79434146
问题1、按你的架构,其实压力最大的反而是服务端,假设真实有效的请求数有1000万,不太可能限制请求连接数吧,那么这部分的压力怎么处理?
答:每秒钟的并发可能没有1kw,假设有1kw,解决方案2个:
(1)服务层(web服务器)是可以通过加机器扩容的,最不济1k台机器来呗。
(2)如果机器不够,抛弃请求,抛弃50%(50%直接返回稍后再试),原则是要保护系统,不能让所有用户都失败。
问题2、“控制了10w个肉鸡,手里有10w个uid,同时发请求” 这个问题怎么解决哈?
答:上面说了,服务层(web服务器)写请求队列控制
问题3:限制访问频次的缓存,是否也可以用于搜索?例如A用户搜索了“手机”,B用户搜索“手机”,优先使用A搜索后生成的缓存页面?
答:这个是可以的,这个方法也经常用在“动态”运营活动页,例如短时间推送4kw用户app-push运营活动,做页面缓存。
问题4:如果队列处理失败,如何处理?肉鸡把队列被撑爆了怎么办?
答:处理失败返回下单失败,让用户再试。队列成本很低,爆了很难吧。最坏的情况下,缓存了若干请求之后,后续请求都直接返回“无票”(队列里已经有100w请求了,都等着,再接受请求也没有意义了)
问题5:服务端过滤的话,是把uid请求数单独保存到各个站点的内存中么?如果是这样的话,怎么处理多台服务器集群经过负载均衡器将相同用户的响应分布到不同服务器的情况呢?还是说将服务端的过滤放到负载均衡前?
答:可以放在内存,这样的话看似一台服务器限制了5s一个请求,全局来说(假设有10台机器),其实是限制了5s 10个请求,解决办法:
1)加大限制(这是建议的方案,最简单)
2)在nginx层做7层均衡,让一个uid的请求尽量落到同一个机器上
问题6:服务层(web服务器)过滤的话,队列是服务层(web服务器)统一的一个队列?还是每个提供服务的服务器各一个队列?如果是统一的一个队列的话,需不需要在各个服务器提交的请求入队列前进行锁控制?
答:可以不用统一一个队列,这样的话每个服务透过更少量的请求(总票数/服务个数),这样简单。统一一个队列又复杂了。
问题7:秒杀之后的支付完成,以及未支付取消占位,如何对剩余库存做及时的控制更新?
答:数据库里一个状态,未支付。如果超过时间,例如45分钟,库存会重新会恢复(大家熟知的“回仓”),给我们抢票的启示是,开动秒杀后,45分钟之后再试试看,说不定又有票哟~
问题8:不同的用户浏览同一个商品 落在不同的缓存实例显示的库存完全不一样 请问老师怎么做缓存数据一致或者是允许脏读?
答:目前的架构设计,请求落到不同的站点上,数据可能不一致(页面缓存不一样),这个业务场景能接受。但数据库层面真实数据是没问题的。
问题9:就算处于业务把优化考虑“3k张火车票,只透3k个下单请求去db”那这3K个订单就不会发生拥堵了吗?
答:(1)数据库抗3k个写请求还是ok的;(2)可以数据拆分;(3)如果3k扛不住,服务层(web服务器)可以控制透过去的并发数量,根据压测情况来吧,3k只是举例;
问题10;如果在服务端或者服务层(web服务器)处理后台失败的话,需不需要考虑对这批处理失败的请求做重放?还是就直接丢弃?
答:别重放了,返回用户查询失败或者下单失败吧,架构设计原则之一是“fail fast”。
问题11.对于大型系统的秒杀,比如12306,同时进行的秒杀活动很多,如何分流?
答:垂直拆分
问题12、额外又想到一个问题。这套流程做成同步还是异步的?如果是同步的话,应该还存在会有响应反馈慢的情况。但如果是异步的话,如何控制能够将响应结果返回正确的请求方?
答:用户层面肯定是同步的(用户的http请求是夯住的),服务层(web服务器)面可以同步可以异步。
问题13、秒杀群提问:减库存是在那个阶段减呢?如果是下单锁库存的话,大量恶意用户下单锁库存而不支付如何处理呢?
答:数据库层面写请求量很低,还好,下单不支付,等时间过完再“回仓”,之前提过了。