Fork me on Gitee

电商系统设计

电商系统设计

第一步 前端限制请求量

可以做提前预约,比如公司要在下周一10点开启抢购20万条新疆长绒棉毛巾,那么在之前的一周时间内,可以向所有活跃用户推送预约通知。然后根据预约量和浏览量预估下周一的参与抢购人数有500万。但是其实没必要让这500万个请求都到后台的,我最多放200万个请求到后台,其他的300万直接就在前端网页看动画就好了。我说下我是怎么设计这个的?因为我准备放行200w个请求,所以我提前生成200万个token,在用户点击预约、或者浏览该商品时,就按规则发放出去。(规则可以设计成公平模式,比如某个用户ID已经预约多次了,还没抢到,那么就给他token,也可以做成随机发放的,5天的预热时间,每天发40万个就好)前端浏览器接收到这个token后,就提前保存本地,当秒杀开始时,没有token的用户,就直接看动画了,过几秒告诉他商品不足就好了,并且前端也要加一些限制,就算用户一直点,也保证实际请求不会重复发送。但是这个token只能限制普通人,对于程序员们就不行了,即便这个网站在抢购开始前没有暴露抢购的接口,但在抢购开始的一瞬间,他依旧能搞到下单接口地址,然后用脚本频繁提交下单请求,这么算下来200万请求肯定是超了

第二步 网关过滤非法请求和再限流

然后到后台网关,网关要以最快的速度判断出当前请求是否需要放行,也就是再次增加一些过滤条件

  1. 过滤没有携带token的请求
  2. 过滤掉黑名单里的IP或用户ID,为了加快速度可以直接放内存里
  3. 过滤重复请求(可以采用Redis计数,对同一个IP、用户ID发起的重复请求给予拒绝)

做完过滤后,假如还剩100万个请求,这依旧是个非常大的数字,所以最好是不让其中的70万个去触碰后面的逻辑,所以还需要再次做限流。可以通过Redis实现令牌桶,开20个Redis,每个Redis服务里分配1.5万个令牌桶,在1-3秒内放完,得不到令牌桶的就直接返回抢购失败

第三步 极速下单

网关会将请求转发到下单服务,这里需要快速的生成订单,不然又会导致大量超时,这里要考虑订单应该存到哪里?Mysql肯定是指望不上了,Mysql一秒2千的写入都已经比较艰难,即便是集群,想要达到万的量级也是比较困难,等你入库完毕,都半分钟过去了。所以可以考虑存到Redis,在订单请求到达后将订单下到Redis里,如果Redis有压力,可以做集群分片,将用户订单,保存到不同的Redis实例中。放到Redis的目的一是速度快,二是为了做订单查询用,因为用户下单后还是会查询订单的。在保存到Redis的同时也向MQ消费队列中投递一份,这么做是为了让后端消费,做并订单入库的。入库的过程后端就可以从MQ慢慢消费了。入库成功后,就可以把Redis的订单删掉了。

第四步 防超卖

但是上面的流程里我们通过令牌桶放出去的令牌数是大于商品数量的,那么就面临超卖问题。在分布式环境下,最简单方案就是使用分布式锁,可以针对商品ID加分布式锁,如果商品数量很少,几百几千个,通过分布式锁也能很快的处理完。通过实测,Redis加锁和释放锁总耗时约1ms,再加上客户端逻辑处理时间,就按下一单要5-10ms,那么一秒的时间也顶多处理100-200个请求。为了应对这个20-30w个请求,我们可以对商品数量分片,比如20W个商品,那么我可以给它拆成100组2000的,然后对这100组分别使用分布式锁,当请求来时,按照Hash将分布式锁加到不同的分组上。总的来说这个方案貌似比较复杂,可能也不太好控制,也可以直接在后端的每个服务实例里写明商品数量,这样直接就可以直接在本地判断商品剩余量,谁也不通信了,从而性能达到极致。比如我们部署了40个订单服务,然后接入配置中心,在抢购开始前配置中心给每个实例分配5000个商品,如果这个实例的商品数量消耗就算售罄,但是由于服务的请求的不均匀和处理速度不同,可能会出现某个实例已经售罄,而别的实例还有大量剩余,造成来得晚的人还能抢到了商品。这种高并发情况下难免有的实例可能挂掉,切实出现这种情况下也不需要太多考虑,大不了少卖一些而已,卖20万个,和卖了19万3千个,区别也不大,可以等其他实例全卖光之后,统计Redis总的订单量,比如卖了19万个3千,那么就再把挂掉的服务启动起来,或者直接给其他19台实例再分配7000个剩余量,如果要这样做就需要前端在显示的时候不要直接说卖完了,要改成"可能有部分用户还没有付款你还有机会"

一、分库分表

随着电商系统订单量的增长,订单管理系统的数据库主要经历以下几个步骤:

- 1主-1从架构

- 双主-多从架构,读写分离

- 水平分表,提高并发

- 垂直分表,提高并发

- 水平分库,提高并发

- 垂直分库,提高并发

分库分表实现过程

电商系统订单分成16个库,每个库64个表进行存储,总共1024个表,Mysql单表性能超过千万级别会导致性能严重下降,假设按千万计算,最高可以存储百亿级订单。随着存储问题的解决,但复杂度会随着增加:

二、全局唯一订单号

这里采用雪花方案,全局唯一ID生成由:时间戳+机器ID+自增序列(+userid后两位),可以直接在服务实例中生成订单,在内存中计算,解决性能问题,userid后两位在后面解释。

三、数据库连接问题

分库分表后,连接数据库变的复杂起来,主要由两种方案:

1、ORM直连

需要自己计算订单应该进入哪个库,可以取订单的后两位,先对库16进行取模,再对表64取模,就能知道存到那张表。优点是直连数据库性能更好,缺点是代码复杂度增加。

2、通过中间价连接

可以使用阿里的Mycat连接。优点:代码实现简单,跟分库前差不多。

四、订单查询

1、买家查询订单

电商系统订单成交后,如果买家需要查询订单,但是只有userid,全部表都遍历一遍不现实。所以还需要改进订单号,之前是【时间戳+机器ID+自增序列】。现在是【时间戳+机器ID+自增序列+userid后两位】。并且订单具体存储到那个数据库也是根据后两位取模而来,所以同一个买家的所有订单都会存入同一个表中,只需要去这表中查找就可以了。

2、卖家查询订单

卖家的订单可能分散在各个表中,查询起来很费劲,所以订单存储的同时,还需按照卖家维度再存入到别的库和表中,这个表专门提供卖家查询订单。

五、扩容问题

针对电商系统的订单其实是具有时间特性,用户查询的大部分都是最近的订单,3月前的订单很少会查看,所以不适合进行扩容,而是做迁移,将3个月前的数据迁移到历史数据库中,从而解决容量增长的问题。

六、业务拆分

下电商系统订单过程,业务极其复杂,不只是电商系统订单号的生成插入等,还要减库存、支付等一系列的操作。所以应该通过消息队列将业务进行拆分,本步骤只做电商系统订单生成的操作,通过消息队列实现数据的最终一致性。

浮点类型精度丢失问题

因为计算机的存储是二进制的,所以任何数据类型最终存储时都需要会被保存为二进制格式,但是浮点数转二进制是有损失的无法精确转换。比如我保存一个float类型的2.4的,但是它在二进制存储时可能就变为了2.399999,如果根据这个去做数学运算就会出现精度丢失

posted @ 2021-06-04 16:25  明叶师兄。  阅读(389)  评论(0编辑  收藏  举报