当前位置: 首页 > 后端技术 > Java

Redis架构实践:高并发下的并发扣库存

时间:2023-04-01 14:51:13 Java

相信大家是从网上学来的。大多数人的第一个项目都是电商,在生活中会无时无刻不在使用电商APP,比如淘宝、京东等等。做技术的都知道电子商务的业务逻辑很简单,但是大部分电子商务都会涉及到高并发和高可用,对并发和数据处理的要求非常高。这里我就说说今天高并发的情况下如何扣库存?我们在扣除库存时需要注意的技术点是:当前剩余数量大于或等于当前需要扣除的数量,不允许超卖。同样的数据量,存在用户并发扣费,需要保证并发的一致性。可用性和性能,性能至少是二级推演包含多个目标数量当推演有多个数量时,如果其中一个推演不成功,则不成功,需要回滚。退款前必须先扣款。数量必须加回去,不能丢。一次扣款可以多次返还。必须保证返回的幂等性。第一种方案:纯MySQL推演实现顾名思义,推演业务完全依赖MySQL等数据库来完成。无需依赖其他一些中间件或缓存。纯数据库实现的优点是逻辑简单,开发部署成本低。(适用于中小电商)。纯数据库的实现之所以能够满足推演业务的功能需求,主要取决于两点:基于数据库的乐观锁方式保证了并发推演的强一致性。该方案,包含一个推算服务和一个量库,如果单库压力很大,也可以做主从,分库分表,服务可以做集群.一个完整的流程就是先进行数据校验,在里面做一些参数格式校验。这里做界面开发的时候,必须要保持一个原则,就是不信任原则。不要相信所有的数据,需要做验证判断。其次,还可以进行库存抵扣的预验证。比如当前库存只有8只,用户想买10只,这时候可以预先拦截数据校验,减少对数据库的写操作。纯读不锁,性能高。这个方法可以用来增加并发量。updatexxxsetleftAmount=leavedAmount-currentAmountwhereskuid='xxx'andleftAmount>=currentAmount这个SQL使用了类似乐观锁的方法来实现原子性。where后判断剩余数量大于等于要求数量则成功,否则失败。扣款完成后,需要记录流量数据。每次扣款,外部用户都需要传入一个uuid作为流水号,全局唯一。用户在扣款时传入的唯一编号有两个作用:当用户退货数量时,需要带回此码,以识别本次退货属于哪个历史扣款。执行幂等控制。当用户调用扣费接口超时时,由于用户不知道是否成功,用户可以使用这个号码重试或者反查。重试时,使用此编号来识别并防止重复。当用户只购买一件商品时,如果验证时库存还剩8件,则验证通过。但是,在后续的实际扣款中,由于其他用户也在同时扣款,可能会出现幻读。此时用户实际扣除的小于1,导致失败。这种场景会导致多查询一次数据库,降低整体的推演性能。这时候就可以升级MySQL架构了。如果升级MySQL架构多一个查询,会增加数据库的压力,对整体性能有一定的影响。另外,对外提供查询库存数量的接口也会对数据库造成压力,同时读的请求比写的大很多。根据业务场景分析,读取库存的请求一般是在客户浏览商品时产生的,而调用扣减库存的请求基本都是在用户购买时触发的。用户购买请求的商业价值大于读取请求,因此需要保护写入。针对以上问题,可以升级MySQL的整体架构。整体升级策略采用读写分离的方式。另外,主从复制直接利用了MySQL等数据库已有的功能。变化非常小。只需配置两个数据源。当客户在服务中查询剩余库存和扣除预支票时,从数据库中读取即可。真正的数据推演还是使用主库。读写分离后,根据28原则,80%的流量是读流量,主库压力降低80%。但是使用读写分离也会导致读取数据不准确的问题。但库存本身是实时变化的,业务上的短期差异是可以容忍的。最终实际扣除将保证数据的准确性。在上面的基础上,还可以升级。虽然加入纯数据库缓存的方案可以避免超卖和欠卖的情况,但是并发度确实很低,性能也不是很乐观。所以这里是升级的第二个方案:缓存实现扣减这个其实和前面的扣减库存是一样的。但是此时扣款服务依赖的是Redis,而不是数据库。这里针对Redis的hash结构不支持多key批量操作的问题,我们可以使用Redis+lua脚本来实现单线程请求的批量推算。升级到纯Redis实现扣费也会出问题。Redis会挂掉。如果Redis中的扣库存操作还没有执行,只需要返回给客户端失败即可。如果已经执行,Redis扣除库存后挂掉。然后需要一个和解程序。通过比较Redis中的数据与数据库中的数据是否一致,结合推演服务的日志。Java培训当发现数据不一致导致日志记录推算失败时,可以将比Redis多的数据库的库存数据回加到Redis中。Redis扣款完成,但异步数据库刷新失败。这时候Redis中的数据是准确的,数据库有很多库存。结合扣费服务的日志确定Redis扣费成功但异步数据记录失败后,数据库中大于Redis的库存数据可以在数据库中扣减。虽然使用纯Redis方案可以提高并发量,但由于Redis不具备事务特性,在极端情况下,Redis数据无法回滚,导致销量下降。也可能会出现异步写入数据库失败,导致超额订阅的数据无法再取回的情况。第三种方案:数据库+缓存顺序写性能更好。在对磁盘进行数据操作时,不断追加写入到文件末尾的性能远大于随机修改的性能。因为对于传统的机械硬盘来说,每次随机更新都需要机械键盘的磁头对硬盘的盘面进行寻址,然后更新目标数据,性能消耗很大。追加到文件末尾,每次写入只需要磁头寻址一次,将磁头定位到文件末尾,后续的顺序写入可以继续追加。对于固态硬盘,虽然避免了磁头移动,但还是有一定的寻址过程。另外,文件内容的随机更新类似于数据库的表更新,都有加锁带来的性能消耗。数据库的插入性能也优于更新性能。对于数据库的更新,为了保证同一块数据并发更新的一致性,在更新的时候会加锁,但是加锁对性能的消耗很大。另外,对于没有索引的更新条件,为了找到需要更新的那条数据,需要遍历整张表,时间复杂度为O(N)。虽然最后只插入追加,但性能非常好。顺序写入架构基于上述理论,可以得到兼具性能和高可靠性的推导架构。上述架构与纯缓存架构的区别在于,写入数据库不是异步写入,而是推导时同步。写。同步写数据库使用的是insert操作,是顺序写,而不是update去修改数据库的个数,所以性能会更好。insert的数据库称为任务库,只存储每次推导的原始数据,不进行真正的推导(即不更新)。其表结构大致如下:createtabletask{idbigintnotnullcomment"tasksequencenumber",task_idbigintnotnull}task表存储的内容格式可以是JSON、XML等结构化数据。以JSON为例,数据内容可以大致如下:{"扣号":uuid,"skuid1":"数量","skuid2":"数量","xxxx":"xxxx"}复制代码到这里我们一定还有一个记录商家数据的库,里面存放着名企真实扣款和SKU汇总数据。对于其他库中的数据,只需要通过这张表进行异步同步即可。推演过程与纯缓存的区别在于增加了事务启动和回滚的步骤,同步数据库写过程任务库存储的是纯文本JSON数据,不能直接使用。其中的数据需要转储到实际的业务库中。业务库中存储了两种数据,一种是每次扣款的流水数据,与任务表中的数据不同的是,它是结构化的,而不是JSON文本的大字段内容。另一类是汇总数据,即每个SKU的当前总金额,以及当前剩余金额(即从任务库同步时需要扣除的金额)。表结构大致如下:createtableflowtable{idbigintnotnull,uuidbigintnotnullcomment'扣款数',sku_idbigintnotnullcomment'货品编号',numintnotnullcomment'本次扣款数量'}comment'扣流表'copycode产品表实时数据汇总,结构如下:createtablesummarytable{idbitintnotnull,sku_idunsignedbigintnotnullnullcomment'totalquantity',left_numunsignedintnotnullcomment'当前商品剩余数量'}comment'Recordtable'整体流程还是复用了之前的纯缓存架构流程。当新增商品或补货已有商品时,对应的新增商品数量会通过Binlog同步到缓存中。扣除时,仍以缓存中的金额为准