秒杀系统(高并发系统)的架构设计

存在的问题和需要注意的事情:

  1. 超卖和少卖
    1. 用户请求处理可以分为“接收用户请求”、“创建用户订单”、“扣减库存”、“用户支付”这几个部分
    2. 少卖问题:如果用户支付之前就扣减库存,即按照“接收请求——扣库存——创建订单——支付”的顺序,如果很多恶意用户发了很多下订单请求(下订单请求即扣库存——原子操作)却不支付,会存在少卖问题。
    3. 超卖问题:如果用户支付成功了才扣,也就是按照“接收请求——创建订单——支付——扣库存”,就会存在更大的问题(大忌):如果进行到支付这一步了,用户会认为自己已经下单成功了,如果支付之后扣库存时才告诉用户库存没了,用户就会生气,另外,一般支付之前要经历创建订单这个步骤,也就是说创建的订单量大于库存量,也就是导致了超卖问题。
    4. 2、3中提到的两种处理顺序以及引发的问题进行对比,发现虽然“接收请求——扣库存——创建订单——等待支付”这中顺序,可能会因为部分用户后续没有支付而导致少卖,但是通过设置支付时间限制可以解决这个问题。相比与超卖对用户体验带来的影响,还是这种先扣库存才创建订单的顺序更加合理。
    5. 库存量是一定的,如果在“用户订单创建完成后”或者支付完成之后再扣库存,用户
  2. 响应时间
    1. 之前说扣库存成功之后要创建订单,但是实际上可以在扣库存成功之前直接向用户返回成功响应,通过消息队列完成后续的创建订单步骤。也就是通过消息队列将这两步事务进行了解耦,缩短了用户等待响应的时间。
  3. 服务器访问压力:在秒杀环境下,虽然买的东西只有1000件,但由于其优惠力度,会吸引百万用户进行抢购。因此需要利用 CDN和反向代理、负载均衡、前端限流、后端限流、防止秒杀脚本、等手段减少服务器的请求访问压力。
    1. CDN和反向代理:静态资源提前部署在用户侧的 CDN 服务器、或者服务器侧的反向代理服务器
    2. 负载均衡:使用10台服务器分解访问压力,并由负载均衡服务器(能够承载几万并发量的 Ngnix,而 Tomcat 只能接收几百)率先接收用户请求,然后均衡的分配到不同的 tomcat 服务器上
    3. 前端限流:为了避免同一用户多次发起请求,可以设置一次发起之后,过一段时间之后才能再次点击(按键置灰)。另外,秒杀开始之前按键也置灰,正常用户无法提前点击,恶意用户无法通过 http 请求提前获取秒杀链接而编写秒杀脚本(另外,通过不断修改秒杀链接也能避免秒杀脚本)。
    4. 后端限流
      1. 风控:收集用户信息,对存在黄牛、机器人行为的用户进行标识,当秒杀中收到该类用户的请求,直接丢弃(如果卖1000件,收到10万条请求,只放进服务器内部1万条,而不是1000条,这就是为了后续从中过滤掉恶意用户请求)
  4. 数据库访问压力
    1. 如果用户请求来了,那么就要扣库存了,如果每次都对数据库直接进行操作,数据库是承载不了那么多的并发量的。
    2. 因此扣库存操作应该在内存中进行,如果原始库存为1000,由10台服务器接收用户请求。那么就可以提前给每个服务器的内存中写入可支配库存为100,服务器接收用户请求之后,就对自身的内存中的库存量进行扣减即可
    3. 另外可以使用一个 redis 服务器,负责全局库存的扣减,也就是说没台服务器先判断自己内存中的库存量是否大于0,(1)如果库存为0,显然直接向用户返回失败相应;(2)如果大于0,在对自己的内存进行扣减之后,还需要向 redis 服务器发起内存扣减,当 redis 服务器完成扣减之后,才向用户返回成功响应。
    4. 增加 redis 的好处是:如果有部分服务器宕机了,为了避免由于这部分服务器无法提供服务,而导致其分配到的库存“少卖”,同意预先给各个服务器设置预售库存量。
      1. 服务器在将自身分配到的库存量扣减完之后,不直接向用户返回失败响应,而是进一步向 redis 查询,如果全局库存量大于0,则向用户返回成功响应,如果全局库存量也为0,才向用户返回失败响应。也就是说,通过 redis 服务器(全局库存)最终即可以避免少卖,也可以避免超卖。
      2. 当本地库存和预备库存都减完了,显然直接向用户返回失败响应。
      3. 为了有效保证不超卖,必须在本地扣库存和远程扣库存都完成了之后,才向用户返回成功响应。因此实际上就相当于卖1000张,有10个服务器,给每个服务器分配的可扣库存量大于1000/10。

 

 seckill系统部署

部署运行时bug:

  1. 修改 jdk
  2. redis 远程访问配置
  3. rabbitmq web 端 手动 add seckill.queue 队列

 

AirCraft

部署运行时bug:

  1. redis 注释掉密码那一行(如果设置了密码,就算在 springboot 配置文件中写了密码也还是会报错:nested exception is redis.clients.jedis.exceptions.JedisDataException: NOAUTH Authentication required.)
  2. 前端要求输入的是密码,而后端的登录校验却查找的是昵称。。。。,然后我就把前端改了,后端对前端输入的loginVo手机号校验注释@CheckMobile给删掉了

访问地址:http://localhost:8080/login/to_login

 

【秒杀系统】秒杀系统实战(有文档有代码)

https://blog.csdn.net/qqxx6661/article/details/107298304

总结:

  1. 如何实现一个秒杀系统呢?首先想到最简单的设计:
    1. 写一个接收用户购买请求的 Controller接口,其中包含商品 id 和用户 id,收到请求之后调用负责创建订单的 Service方法,在 Service 方法内,从数据库中读取商品库存表,获取当前商品 id对应的的库存,如果库存为0,则返回失败,否则更新数据库商品库存(-1),并在数据库的订单表中写入(用户 id,商品 id),并返回成功信息,Controller 根据返回结果向前端返回失败或者成功抢购。
    2. 那么其中存在的问题有哪些呢?首先是数据库库存量的读取与更新,这里存在读取与更新的原子问题,具体来说,如果负责处理用户 A 和 B 的线程,都读取到库存量为1,然后 A 先按照逻辑发现库存量不为0,并将库存量更新为(1-0),并返回抢购成功,而 B 也在 A 更新库存之前读取到了库存量为1,并且按照相同逻辑进行了处理,这就导致了超卖。
      1. 因此库存表的读取与更新应该作为一个事务(要么都进行要么都不进行),在 springboot 中,为创建订单服务(包含读取库存+更新库存+创建订单的操作)方法添加事务注解即可
      2. 为进行“库存读取、库存更新”的方法添加事务注解,从而为库存表添加行锁,具体来说就是当有连接对某商品 id 对应的行进行读取、更新操作的时候,其他连接无法进行操作(悲观锁,认为竞争一定会发生,因此使用锁的方法,提前保证竞争者只能排队);
      3. 或者就是使用一个 version 版本标识,每次读库存操作的时候还会读取当前 version,当进行库存更新的时候会对比更新时 version 是否和读到的 version 一致,只有一致了,才说明之前读到的库存是最新值,可以进行库存更新(并同时将 version+1),否则就说明已经有其他人进行过库存更新操作了,也就是之前读到的库存不是最新值了
  2. 除了库存读写的原子问题,还存在哪些问题呢?——系统和数据库压力过大
    1. 在之前的设计中,如果有1000个人参与秒杀的话,那么这1000个人都会连接数据库,读取库存,即使只有10个人能够抢到商品(更新库存),这会导致数据库瞬间压力过大。
    2. 因此想到采用限流方法,也就是说将用户请求按照一定的缓慢速率放进来,保证系统和数据库的压力都比较小且均匀。
      1. 可以使用 springboot 的 guava 库,相当于创建一个令牌桶对象,如设置每秒钟能够进来10个请求(相当于十个令牌,只有拿到令牌的请求能够被系统处理)。
      2. 对没有拿到令牌请求,可以将他们设置为阻塞(排队等待令牌),或者等待一定时间,如果一定时间之后仍然没有拿到令牌,就直接向用户前端返回失败响应。
  3. 使用了锁和令牌桶之后,还有什么问题呢?——数据库读写速度太慢
    1. 可以将库存量放在缓存中
      1. 可以读写都对缓存进行操作,等秒杀活动结束了再同一更新数据库
      2. 也可以在秒杀中也对数据库同步更新,当读到缓存中库存为0,直接返回秒杀失败,若不是0,则需要对数据库的缓存同步更新。
        1. 先删除缓存,再更新数据库。存在 A 删除了缓存,要去更新数据库(将1变0)之前,B 发现缓存没了,就去读数据库,读到了1,将1写入缓存。这种可能比较大,但是也可以使用延迟双删(也就是更新之后再延迟一段时间再删除一次缓存)的方法来避免。
        2. 先更新数据库,再删除缓存。存在:(1)A 读到了1,(2)B将数据库从1更新为0,(3)B 删除了缓存,(4)A 将1写入了缓存(脏缓存)。这种可能不大(因为2的时间一般比较长,所以3发生在4之前的可能不大),且可以使用延迟删除的方法来避免脏数据的出现,也就是再 B 对数据库更新之后,等待一段设定的时间,然后再次主动将缓存删除,这样当有了新的读、写请求之后,就会再次从数据库读到新值,从而更换掉缓存中的脏数据。
      3. 延迟双删的最后一次失败了怎么办?
        1. 可以将延迟双删作为消息写入消息队列,异步执行,消息队列能够保证消息最终一定被执行
        2. 这个时候的业务代码长这样:读库存,更新库存,插入订单行,删除库存缓存)。
          1. 但是实际上,为了避免业务代码复杂化,可以将删除库存缓存(等待一段延迟时间、写入消息队列等)的操作另起一段非业务代码来负责,业务代码中只需要在数据库的 binlog 日志中写入要做的事情,由非业务代码监控 binlog,读取 binlog 中的数据,然后进行操作即可。
          2. 可以利用阿里的 canal 作为中间件读取 binlog
  4. 使用消息队列优化用户响应时间和吞吐量
    1. 之前逻辑中,是在向数据库插入用户订单之后,才向用户返回成功信息,但是实际上可以(最终逻辑):
      1. 收到用户请求
      2. 查询缓存中是否存在(用户 id_商品 id 的 key),如果存在返回“已经参与过秒杀,不能贪心”,否则继续下一步
      3. 查询缓存中库存量,如果为0,则返回“商品售罄”,否则继续下一步
      4. 将包含用户 id 和商品 id 的消息插入 order 队列中,直接向用户返回“秒杀成功,请等待订单生成”或者“排队中”,不直接向用户返回秒杀成功消息,避免用户在获取成功之后,结果去订单列表内查询,却查不到该订单(因为消息队列中的消息可能还未被处理到,也就是说订单还没写入数据库,当然就查不到订单啦)
      5. 创建order 队列的消费者类
        1. 负责从队列中获取消息中的用户 id 和商品 id
        2. 再次查询缓存,如果缓存为0则返回“秒杀失败”,否则下一步
        3. 查询数据库库存表,获取商品 id 对应的库存量,并更新库存(原子操作)
        4. 向数据库订单表,插入(商品 id、用户 id)的行
        5. 向缓存中的“商品 id购买情况set 集合”( key:商品 id,set:购买过该商品的用户 id集合)插入该用户 id
      6. 前端不断向后端发送“订单生成情况查询”请求,如果缓存的“购买情况 set 集合”包含 该用户 id,则向用户展示“订单生成成功”、“恭喜您,抢购成功”界面。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值