`
huangyongxing310
  • 浏览: 506409 次
  • 性别: Icon_minigender_1
  • 来自: 广州
文章分类
社区版块
存档分类
最新评论

库存扣减和锁、支付转帐

 
阅读更多
库存扣减和锁

2018天猫双十一的每秒订单创建峰值达到 49.1 万笔,就是每秒才处理49.1 万笔数据.


在对数据库的值进行修改时,如果在多线程情况下会出现,修改不一致的情况出现,当然可以通过设值的方式来保证一致性(因为就算在同一事务中进行加减操作都不定保证数据的正确性,设置就可以保证多次重次都是一致的,(因为有可能数据提次后网络中断使客户端认为出错重试(就会多减,设值可以保证不会多减)))


所以使用设值会更好一点,
方式:
1.代码同步, 例如使用 synchronized ,lock 等同步方法,单机时可以分布式时不行
2.不查询,直接更新  update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0, 可以使用加减进行操作(单机、分布式都行,因为是条目进行上锁的),会存在重试多减的问题
3.使用CAS, update table set surplus = aa where id = xx and version = y,,
单机、分布式都行,但分布式时会存在更新不成功的问题(因版权本不同),要重试,多写的情况下,效率会低下
4.使用数据库锁, select xx for update,单机、分布式都行,因为是条目进行上锁的(悲观锁),只会对有for update的select进行互斥,没有for update的select不会互斥,事务级别为串行化也是可以实现同样的功能,但效率不高
5.使用分布式锁(zookeeper,redis等),,单机、分布式都行


第3、4种方式可以实现得比较好,分布式时也是可以的,但还是会有扣减成功但返回失败的问题,但最后没卖出可以最后由管理员管理写入重新卖(出错机率非常小),第4种方式可以减小用户抢到但买不到的问题,但效率会因为GC(JVM内存管理)降低


如果是支付数据,一定要先生成支付单,再在同一个事内完成状态和扣减的动作。



如果是库存的话,可以多减,因为最后会按实际发货或订单的数量更改最后的库存.
但如果是钱的扣减就不能用这种方式了,但钱的扣减一般不会有如此大的访问量


redis incr可以作为是否有抢购机会的判断,但值要在抢购前写入,否则抢购开始时多个插入会存在问题



redis中incr、incrby、decr、decrby属于string数据结构,它们是原子性递增或递减操作。
incr递增1并返回递增后的结果;
incrby根据指定值做递增或递减操作并返回递增或递减后的结果(incrby递增或递减取决于传入值的正负);
decr递减1并返回递减后的结果;
decrby根据指定值做递增或递减操作并返回递增或递减后的结果(decrby递增或递减取决于传入值的正负);

这类命令一般会应用到计数器场景
单号生成:根据业务生成key,每当需要单号时可以使用incr获得一个新的序列号。
错误拦截:比如有的网站账号密码输入错误N次之后,会做一些特殊处理;使用incr是实现这种功能的方式之一,可以根据用户的标识表示key,每当账号密码输错时使用incr命令做递增。
非法拦截:某段时间限制同IP请求同一接口次数


https://www.cnblogs.com/sealedbook/p/6194047.html(incr、incrby、decr、decrby命令的作用和用法)


缓存MEMCACHE 使用原子性操作add,实现并发锁
memcache中Memcache::add()方法在缓存服务器之前不存在key时, 以key作为key存储一个变量var到缓存服务器。我们使用add来向服务器添加一个键值对应,如果成功则添加,否则说明存在另一个并发作业在进行操作。
https://www.cnblogs.com/dluf/p/3849075.html(缓存MEMCACHE 使用原子性操作add,实现并发锁)




//=============库存扣减总结
1.用redis的原子性操作,表示是否可以得到购买的机会,先减去要的个数,返回大于0就可以,返回小于0就要增加回去,也可以另一个key保存失败的库存数,回仓时再加回去
2.查库存看足够应付订单的数据不,足够就生成订单和设置库存新值和新的版本号,记住旧的版本号,调用RPC接口,不够就redis增加回去
3.RPC接口开启事务保证订单和库存数据一起成功,(会出现事务完成但返回失败的异常,所以异常返回进行redis增加回去(因为每次保存前都会查一下数据库的值是否足够应付这个订单的)),成功返回代表购买成功.可以跳到支付页面进行支付了.
4.写入时会对比版本号,一致才会写入成功的.
5.3中返回异常的情况是非常小的概率,就算出现了,只不过是没卖出几个货,不会出现超卖的现象,后期可以商家更新库存,这种情况用户是可以通过查询得到订单信息的
6.回仓操作时也要使用版本号进行操作,查询所有没支付订单,超过时间的订单与库存值同一个事务中进行。
7.回仓后要更新redis的值,回仓时间最好间隔少点,如5分钟或10分钟
8.关键问题是redis的值如何写入,一定要抢购前写入的,可以在设置抢购活动时进行写入,或设置定时在抢购前几分钟进行写入
9.抢购时间要在设置抢购活动时写入,没有值要到数据库中查找设置进redis,防止没有开始就有请求抢购
10.当然抢购时也要判断时间到达没有才能进行后面的操作.
11.如果抢购时redis没有库存和时间信息可以使用锁的机制,没有获得锁的直接返回失败,防止多个请求进行库存和时间信息的获取和设置.这样就可以不用定时去处理库存和时间信息了,当然可以结合使用。(返回稍后重试的信息,或前端对这个信息不进行处理就可以了)
12.可以通过设置redis的失效时间使redis的信息在一次请求中可以重新建立,同在建立时进行回仓的逻辑处理,当然能在开抢前要进行一次就更好了(没有关系,大不了开抢前的一批人抢不了,1秒左右什么都应该建立好了)
13.redis库存的更新要有一把锁,不一定要用分布式锁,大不了多更新几次,扣减是以实际库存为准的.
14.缓存中的库存不能失败就加回去,因为会有逻辑问题,应该回仓进行设置。


如果是小库存,直接返回商品已经售罄即可,但是多大十几万的库存,让用户看得到,买不到,心里痒痒的似乎不太友好,并且运营策略上也希望能够快速消完这些库存好制造噱头。


小库存秒杀可以出现冲突就返回抢购完成,但大库存会出现冲突数更大,请求量也会很多,压跨系统(服务要做限流),大库存可以分批进行抢购,没有必要一次进行


为了减少秒杀减库存冲突,可以使用redis存放库存量,当数据大于数据库库存量时就表示卖完,最后再把库存写回数据库中


如果库存放在redis就只要插入订单就可以了,不用扣减库存,通过回仓方式进行重新抢购

//----------------SQL回仓
1.先设置redis缓存库存为空,应用不再有请求进来。(可以批量进行)
2.等待10秒,不一定要线程等待,可以定时到后再去进行操作。
3.查询还没有支付的订单,时间超过回仓时间+3分钟的就认为超时未支付,进行回仓操作,可以拭量或一个个进行回仓。
4.重新设置redis库存数.

//----------------redis回仓
0.先设置redis缓存库存总数,开始时间和回仓标志数据中的回仓标志为回仓标志.
1.先设置redis缓存库存为回仓标志,应用不再有请求进来。(可以批量进行(多个商品))
2.等待10秒,不一定要线程等待,可以定时到后再去进行操作。读取redis的剩余库存数
3.查询还没有支付的订单,时间超过回仓时间+3分钟的就认为超时未支付,进行回仓操作,可以拭量或一个个进行回仓要加上redis的剩余库存数。
4.回仓时间可以15分钟进行一次,回仓两次就可以了,两次后标记为抢购结束,不再进行
5.重新设置redis库存数.

不过redis一致性不太好,虽然不会超卖,但会有少卖现象。

//------也可以redis保存大概的库存,得到的就进入进行下单和减库存(数据库扣减),回仓后再重设一下redis保存的大概的库存,(其实就是限进入系统的人数)



//-------------库存、订单、支付(帐号)、第三方支付、发货流程
1.库存、订单同一库
2.帐号帐单同一库
3.生成订单减小库存保存在订单库,一致性同库事务完成,返回订单ID
4.订单ID以申请支付帐单保存在帐号库,(支付方式、有第三方支付、同时有第三方支付帐单ID)
5.支付发起,如果是帐号支付就余额与帐单状态、发送订单状态消息入库在同一个事务中完成。如果是第三方就调用接口进行支付。那个一失败都可以通过相关ID查询状态确定回来。
6.订单接收MQ订单状态消息更新状态和发送MQ消息到发货在同一事务中完成.(同时)
7.到此流程结束.

8.删除订单标状态,帐单不支付就可以了,系统删除(标状态)
9.如果有回仓操作的,定义过期时间,过期不让支付,



//-------------回仓
1.回仓应该由一个单独的服务进行库存回仓处理(时间到时后3~5分钟进行回仓,防止有些MQ没有处理完),防止分布式又要引入分布式锁。
2.更改库存和刷新redis的库存值
3.刷新redis的库存值也可以专门使用一个服务实现,防止分布式又要引入分布式锁。(毕竟商品数量不会很多)
4.回仓处理可以一个个加回库存里,加版权本号,失败重试方式,做完回仓后再清一下redis的库存,让redis重新获取一下,每个服务获取一次没有问题的,因为最终会以数据库的库存为准确定是否能进行购买。
5.手动设置库存也可以最后清一下redis库存方式实现(也要加版权本号进行防些同时操作)
6.回库时机可以是手动或定时器进行,超过一定时间的就回仓处理(也要加版权本号进行防些同时操作)



//-------------退货\退款
1.商家同意,申请生成帐单,向MQ发送退货消息,
2.帐号服务接收消息,对帐单进行支付,就完成了退款操作了。
3.操作完成发送MQ消息更新订单状态和退款单状态









//=============支付总结
1.发起转帐,生成支付订单,跳到支付页面
2.扣减(足够支付判断,支付订单状态判断)和支付订单状态更新、订单保存在同一事务中处理,出现数据保存成功,返回失败出没问题,支付订单状态可以查询到,再次支付单不允许
3.发送MQ,成功就删除,不成功就让后能重发(也可惟重发多次再让后台重发,也可以不删除更改状态)
4.消费MQ,版本、流水订单(订单号要可以重复计算出来)、更新流水订单和余额在一事务中,出现数据保存成功,返回失败出没问题,因为有流水就表明已经处理了这个收款了
5.通过订单的状态就可以达到保证余额最终的一致性了
6.退款也是一样的原理
7.要求帐号与帐单在同一个库中,会出现海量数据,可以通过主表只何存一定时间内的数据,其他数据保存到后备库中,查询要提申请才可以查询的方式



//=============转帐总结(A转B)
1.A生成订单,对订单支付(同时生成入帐订单号(与B帐号有关联)),查询A是否够钱支付,不够不成功,成功同一事务内更新订单状态和A帐号余额、插入MQ消息(有ID(与A帐号有关联))到表中(保证一致性),发送MQ,发不成功MQ可以后台重发,A阶段结束.(MQ消息表一段时间后删除保证安全一点,防止MQ数据丢失)
2.生成B订单(ID为发过来的入帐ID),不成功不做处理,成功同一事务内更新订单状态和B帐号余额,如果要发送到下一个MQ(与A一样的处理),如发消息通知收方
3.A生成的过程中要先确何双方帐号的可用性。
4.B成功后MQ通知A进行订单状态更新,A更新后发送通知消息到MQ
5.A接收到确认消息更新订单状态为完成状态。

6.主要重点是发送处理同库,接收和处理同库.

7.B可以发送MQ通知A成功,使A更新状态。也可以通过接口通知(对于第三方使用,第三方还可以提供查询的接口进行状态查询)



//------------------向外提供API
1.也是要求先申请帐单再支付帐单的方式,不能对帐单进行重复支付。
2.支付处理成功就算API成功.
3.支持帐单状态查询(入帐),可批量查询。对支付方无意义,对收款方才有意义
4.支持帐单状态查询对支付方只有调用返回失败时用于确认是否已经扣减成功用。




//====================通知消息发送
1.因通知消息重新发送不会造成业务不一致,所以消息端不用去重处理,直接通知就是了
2.生前端就要确何消息一定发到MQ才行。(如果不一定要发送成功可以不用确保)




//====================数据沉余
1.就是按不同主键重构数据(因为分库分表的存在),不同的查询有不同的目的(如买家、卖家,后台管理等)
2.数据量运行库可以只保存一年内的,其他的移到备份库中进行保存.






//====================秒杀系统
秒杀系统架构优化思路
1.将请求尽量拦截在系统上游
2.充分利用缓存
浏览器和APP:做限速,每5秒只准请求一次
站点层:按照uid做限速,做页面缓存,返回5S内相同的内容
服务层:按照业务做写请求队列控制流量,做数据缓存,按数据进行后面罗辑的处理,其他返回结柬信息

回仓,未支付的回仓,告诉客户多少分钟后进行重次(如45分钟)

架构设计原则之一是“fail fast”。


做请求队列控制流量(LockSupport)
http://shift-alt-ctrl.iteye.com/blog/2315008
单线程


LinkedBlockingQueue、信号量(指定数据库能接受的瓶颈)(能得到信号量或能入队列的继续执行,不能的返回结束信息)

系统并发数量限制可以使用队列LinkedBlockingQueue、ConcurrentLinkedQueue(非阻塞队列),请求到来时入队列,处理完毕出队列的方式实现,信号量也可以实现并发数量限制.
信号量、或锁也可以获取到当前等待队列大小的,所以也可以实现队列大到一定范围直接返回系统繁忙


同一个账号,一次性发出多个请求.解决为写入标志位
多个账号,一次性发送多个请求.解决为频率高就用验证码或同IP数据限制
多个账号,不同IP发送不同请求.解决为提高帐号的等级要求才可以参加


1.库存数据在读时可以在本地服务中进行缓存,一定时间再去redis中获取最新的库存数。
2.加入验证码的方式进行拉长抢购的时间.减少并发的峰值
3.同一数据在数据库里肯定是一行存储(MySQL),所以会有大量的线程来竞争InnoDB行锁,当并发度越高时等待的线程也会越多,TPS会下降RT会上升,数据库的吞吐量会严重受到影响。说到这里会出现一个问题,就是单个热点商品会影响整个数据库的性能,就会出现我们不愿意看到的0.01%商品影响99.99%的商品,所以一个思路也是要遵循前面介绍第一个原则进行隔离,把热点商品放到单独的热点库中。但是无疑也会带来维护的麻烦(要做热点数据的动态迁移以及单独的数据库等)。
4.每个服务对每一个热点商品进行队列化处理,减少数据库连接,不让太多请求落到数据库层面上。
5.服务器的保存可以使用限制QPS(令牌桶),信号量(处理数量),进行服务的保护。
6.也可以通过封装请求将请求放到MQ中,另一个服务进行请求处理,但要求客户端使用轮询的方式查询订单的情况,体验有点不太好。
https://blog.csdn.net/i_will_try/article/details/76299414(通过请求队列的方式来缓解高并发抢购)
7.请求队列化也可以使用锁或信号量的方式来实现,获得锁的进行处理,没有获取到和阻塞,当然要使用另一个信号量来限制阻阻力塞的个数,当然最好也在获取锁中定义超时时间。
8.可以为每个商品进行队列化操作。当然平时的下单流程也可以做成一样,只不过是多了一些保护功能,平时性能影响不大。


//-----秒杀系统应该是单独的服务系统,不要影响固定的业务,关键是如何进行业务区分
可以通过用秒杀页面与常规购买页面不同的方式路由到不同的服务器


//RPC服务端限流(队列化)可以使用AOP方式,WEB服务器还可以使用filter方式来实现


//正常下单,扣库存失败报系统繁忙,因为冲突机率会很少,平时很少机会会两个人同时一下样商品的订单。

//秒杀过程中扣减冲突会很高,用缓存的方式,做库存一致性又比较难处理(如扣减失败增加回去也难,因为写冲突比较高),当然可以用别一个key的值通过原子性加操作在保存失败时的库存数。回仓时再加上这个key的值.


//缓存自减方式做库存扣减(原子性),要用另一个key保存操作后失败的库存数(原子性),最后做回仓处理要加上这个key的值,最后有一个服务将库存数写回库存数据库。这种方式可以保证扣减的性能最好,服务层也不用做队列化处理。如果改数库的话要做队列化,因为数据库数据只能一个处理成功(队列化提高成功操作概率),当然缓存方式做队列化也可以减少缓存的请求数,同时接口层也可以限流和起到保护的作用,超过限流数快速返回失败。

//上述的操作出现失败的机率比较低,可以不用存失败的个数,因为有可能会实现成功但网络超时出现的错,这样就会出现超卖.



//-------------------保护方式
1.前端每5秒透过一个请求
2.站点页面缓存
3.服务控制流量,多过的直接返回无货
4.同一IP透过数据限制,
5.同一用户透过数据限制。
6.提高用户参加的等级


//
服务处理不过来的,要快速返回系统繁忙,不能让太多的连接连在nginx或tomcat中
信号量可以进行限量,队列也可以,流桶可以限流


//
商家商品放在同一个库,方便订单的商品合并


//
订单的写入可以通过批量的方式提高写入的速度

一个客户端进行测试:
INSERT test (id,name,value,sex,createDate)VALUES(UUID(),'xingxingxingxing','huangyongxinghuangyongxing',71,NOW());

1条插入:48条/s
10条插入:400条/s
20条插入:700条/s
40条插入:1350条/s
120条插入:3500条/s
200条插入:5500条/s
240条插入:7000条/s
480条插入:11000条/s
960条插入:15000条/s


在系统中可以使用队列(MQ或Java的队列)的方式先保存下来,另一个线程进行获取并批量写入,提高写入的速度
前端进行订单(轮询)是否插入成功,轮询缓存,线程插入后要向缓存写入相关信息便于查询



//库存扣减(大并发),抢购,订单写入库存扣减,(购买资格数在redis进行原子性扣减,lural脚本运行)
1.上游抢购请求间隔(如5S),请求频率不能太大,后台也可做频率检测,超过的进不去。
2.后台接收到后,生成订单数据放到队列中就返回订单ID,另一个线程从队列中获出订单数据批量插入订单库和减少库存,批量插入加快插入的速度。数据库要加版权本号,事务要是可重复读。
3.前端接收到订单ID后,等2s后查询订单是否已经插入数据库,插进去就可以去付款了,没有就再查一到二次,还没有就输出抢购失败(查询期间页面在等待抢到)。

//订单状态,
1.前端先查看定单是否已经支付,已经支付过不进行重复支付。
2.发起付款时,先查看帐号付款记录中是否已经存在这个订单,没有就插入和扣减帐号余额同一个事务内完成,并更改订单状态。已经存在,则更改订单状态.
3.也可以在发起支付时先查一次付款了没有,没有就更进行步骤2,否则就更改订单状态.

//帐号内转帐(A转100到总帐号)()
1.A帐号减100,帐单记录(里面有MQ状态)同一事务完成。发送MQ,另一个任务定时读取没有发送MQ的帐单记录进行MQ发送(5-10秒),发送成功的更改帐单记录里的MQ状态。
2.接收MQ信息,入帐单与总帐号加100同一事务完成.可以先查询一下有没有,有就不进行处理,没有就进行处理。(MQ里有入帐单ID,建议这样做否则逻辑会稍复杂(要考虑到分库和一致性问题))


//存在第三言支付的转帐(参考微信)
1.申请支付单
2.进行支付,
3.支付成功推送一条信息到你服务器或MQ发送给你,或提供批量查询接口,






//抢购方面
1.redis 一个大体的库存,一定时间内进行更新(如10S)
2.进行队列进行限流,
3.订单进行批量插入库存,库存也是多个一起扣减




















https://blog.csdn.net/qq315737546/article/details/76850173(浅谈库存扣减和锁)

https://blog.csdn.net/qq315737546/article/details/74532357(基于redis setnx的简易分布式锁)

https://www.iteye.com/news/32768(大促场景下热点数据写(库存扣减)技术难题解决方案)


https://www.cnblogs.com/jifeng/p/5264268.html(淘宝大秒系统设计详解)


http://www.mamicode.com/info-detail-2383504.html(秒杀系统优化方案)
分享到:
评论

相关推荐

    阿里云 专有云企业版 V3.8.2 全局事务服务 产品简介 20200417.pdf

    - **电商系统**:在订单处理中,涉及到库存扣减、支付等多个子事务,GTS可以确保这些操作的原子性,避免出现库存超卖或支付未完成的情况。 - **金融系统**:在转账操作中,需要同时更新两个账户的状态,GTS确保这...

    [精选]某市场批发商品管理与财务会计分析流通.pptx

    批发业务因其大规模经营的特性,往往涉及众多专业化商品种类,且伴随着大额的交易和频繁的库存变动。为了保持资金流动的合理性与成本控制的有效性,企业必须建立起一套完善的核算体系。 在批发商品购进环节,核算...

    阿里云 专有云企业版 V3.12.0 全局事务服务 GTS 技术白皮书 20200622.pdf

    GTS适用于需要强一致性保证的分布式业务场景,如电商平台的订单支付、库存扣减、物流更新等多步骤操作,以及金融行业的账户转账、保险理赔等业务。 5. **安全与合规** 文档强调了法律声明,用户需遵循保密协议,...

    施工企业会计核算与常用会计分录.doc

    现金作为企业中流动性最强的资产,在日常运营中的每一次收支都需详细记录,包括提取现金、支付各项费用、处理废旧物资收入,以及现金超过库存限额后的存入银行操作。这些活动都应运用借贷记账法进行会计分录。当出现...

    分布式事务在Sharding-Sphere中的实现.docx

    - **订单处理**:涉及支付、库存扣减等多个系统的联动操作,需要确保整个流程的完整性。 #### 二、ACID特性与CAP/BASE理论 **ACID**特性是指本地事务所具备的四大特征: - **原子性(Atomicity)**:事务作为一个...

    分布式事务处理

    - **电商订单**:订单创建、库存扣减、支付等操作必须保证一致性。 - **物流配送**:多个环节的数据同步,如发货、签收等。 #### 接入方式 针对不同的应用场景,可以选择不同的接入方式,主要包括: - **同库模式*...

Global site tag (gtag.js) - Google Analytics