微服务系统面经之二: 以秒杀系统为例

16 微服务与集群部署

16.1 一个微服务一般会采用集群部署吗?

对于一个微服务是否采用集群部署,这完全取决于具体的业务需求和系统规模。如果一个微服务的访问压力较大,或者需要提供高可用性,那么采用集群部署是一种常见的策略。通过集群部署,可以在一定程度上提高服务的可用性和容错能力,因为当某个节点发生故障时,其他的节点可以继续提供服务。同时,通过负载均衡技术,可以将请求分散到各个节点,提高系统的处理能力。

16.2 为什么每一个微服务都可以提供读写服务?而不是像raft集群那样部署,只能leader节点提供读写能力,其他节点保证一致性和可用性

这是因为他们的读写的数据库是同一个,所以一致性保证是由独立部署的分布式数据库测提供。

在微服务架构中,每个微服务都可以提供读写服务,这主要是因为微服务架构的设计原则之一就是服务的自治。每个微服务都应该能够独立地处理请求,而不需要依赖其他的服务。对于数据存储,**一般来说,每个微服务都会有自己独立的数据库,以避免跨服务的数据依赖。**这样,即使是写操作,也可以由任何一个微服务节点来处理。当然,对于数据的一致性保证,通常是由数据库本身来提供。

对于你提到的Raft集群,它是一种强一致性的复制协议,通常用于实现分布式系统的状态机复制。在Raft中,只有Leader节点才能处理客户端的请求,这主要是为了保证数据的一致性。但是在微服务架构中,由于每个服务都有自己的数据库,所以一致性问题主要是由数据库来处理的,而微服务只需要关注如何处理业务逻辑即可。

16.3 每个微服务都会有自己独立的数据库,但是一个微服务的所有实例共享这个数据库对吧

对的,一个微服务的所有实例通常会共享一个数据库。这是因为在微服务架构中,每个微服务都应该是自治的,这意味着它们应该拥有自己的数据存储,并且能够独立地处理请求。

例如,假设你有一个订单服务,你可能会有多个实例来处理高并发的订单请求。尽管你有多个订单服务实例,但是他们会共享同一个订单数据库。

这样的设计可以让微服务自己管理和控制对自己数据的访问,以及如何实现业务逻辑,这有助于微服务的独立发展和快速迭代。同时,由于微服务的所有实例都访问同一个数据库,所以他们能够看到同一份数据,保证了数据的一致性。

然而,也需要注意,这样的设计可能会引发一些问题,比如数据库的并发访问控制,数据的一致性和隔离性问题等,这就需要我们在实际的系统设计和开发中去妥善处理。

17 请求的有序性问题(重点)

17.1 单机下的请求有序性问题秒杀及其解决方案

17.1.1 同一个应用先后发出的两个http请求,到达服务端的顺序可能发出的顺序不一致吧?不一致的话会有什么危害(当时暑期实习面滴滴的一个问题,现在才明白怎么回事)

是的,同一个应用发送的两个HTTP请求可能在到达服务器时的顺序和发送时的顺序不同。这是由于网络延迟、服务器的处理速度等因素都可能导致这种情况发生。如果你的应用逻辑依赖于请求的发送顺序,那么这可能会导致问题。

例如,如果一个请求是用来更新数据,另一个请求是用来读取这个数据,那么如果读取请求先于更新请求到达,那么就会读取到旧的数据。或者,如果你有两个请求,一个是用来创建一个资源,另一个是用来删除这个资源,如果删除请求先到达,那么就会出现无法删除一个不存在的资源的错误。

这种情况下,一种可能的解决方案是在服务器端实现某种形式的请求序列化,即按照一定的规则(例如请求的时间戳、或者一些业务相关的顺序)来处理请求。但是,这可能会增加服务器的复杂性和处理延迟。

另一种解决方案是让客户端处理这种情况。例如,如果客户端知道一个请求依赖于另一个请求的结果,那么它可以等待第一个请求完成后再发送第二个请求。或者,客户端可以包含一些信息在请求中,让服务器知道请求的相关性和顺序。

这个问题在分布式系统中是非常常见的,被称为"请求的有序性问题"。处理这个问题通常需要一些复杂的设计和编程技巧。

17.1.2 方案一:在客户端保证请求的有序性(业务层面保证)

以用户在秒杀场景中先下单后支付为例,可以在支付之前查询redis缓存或者mysql数据库判断订单是否已经完成,如果完成才允许进行支付操作,否则拒绝。

17.1.3 方案二:利用消息队列FIFO特性保证服务端消费请求的顺序一致性

为每个用户维护一个消息队列,所有该用户发出的请求都首先进入这个队列,然后由服务器按照队列的顺序(FIFO)来处理这些请求。以kafka为例,kafka为每一个用户新建一个topic,且保证这个topic的分区数量(等同于队列数量)和消费者数量一一对应,客户端在发送消息到队列时要指定具体的分区,对应的下单/支付(这俩必须是绑定在一个一起)消费者必须消费这个指定的分区,那么就能保证消费的顺序性

但是这里又存在一个问题,如何保证生产中发送的消息到达队列中的顺序和发送顺序是一致的呢?

答:利用停止-等待协议,产生同步阻塞,生产者在向消息到队列后会阻塞住,直到收到了队列所在服务器的确认才会继续发送下一个消息

但是这样设置完后,需要进行消息去重操作,同时还要保证消息的幂等性,这里会借鉴tcp协议在保证幂等性上的做法,在发送端,每次发送一条消息都会将序列号+1(但是重发的请求公用初始时分配的序列号),kafka端本身会有一个从客户端id到序列号的映射,存储的是最近一次该客户端成功发送的消息序列号,也就是说重发的请求序列号必然小于等于这个map中存储该客户端的序列号,通过对比就可以知道这个请求是否是旧的。

但是这种方案一般不推荐,因为众所周知停等协议是非常慢的,这样做只会浪费kafka的消费速度

大家可以参考这篇文章:Kafka如何做到发送端和接收端的顺序一致性?

17.1.4 真正的解决方案一:业务层硬编码保证有序

从业务层再做重排,典型的就是支付之前先查表判断是否抢购并且下单成功

17.1.4 真正的解决方案二:使用kafka

(1)正经解决就是不在需要严格保序的场景里用kafka,比如扣减库存操作,哪一个用户请求先扣都无所谓

(2)(待定)还有一种是使用kafka的分区特性,将互相依赖的消息放入到同一个消费分区的消费队列中,这个分区内的消息是顺序消费的,只需要保证放入分区队列中的顺序是符合依赖就行

17.2 多机下的请求有序性

17.2.1 一个微服务通常以集群方式部署,每一个实例都能提供全量的功能,如果下单请求打到了实例A,但是支付请求打到了实例B,最后B比A先到达数据库,这种顺序不一致如何保证呢?

实例B在支付之前查询redis缓存或者mysql数据库判断订单是否已经完成,如果完成才允许支付操作,否则fail掉,随后再重发,但是这里就需要保证支付操作的幂等性(多次请求的结果和第一次执行的结果状态相同),参考上面提到的tcp幂等性保证即可。

17.3 面试场景(重点)

17.3.4 秒杀场景中的消息公平性定义

消息的公平性在不同的上下文中可能有不同的含义。在一些情况下,它可能指的是保证消息被处理的顺序与它们到达的顺序相同(先来后到,比如AQS中的公平锁)。在其他情况下,它可能指的是保证每个发送者都有公平的机会让他们的消息被处理,而不是让某些先到的发送者的消息被优先处理。

在秒杀场景中,消息的公平性通常更关注后者,即确保所有的用户都有公平的机会参与秒杀,而不是让请求先到的用户有更高的机会。这通常通过随机选取、每人限购、等概率抽取等手段来实现。

至于先来后到的问题,由于网络延迟和服务器处理速度的差异,严格的先来后到在实际的网络环境中很难实现。同时,如果只优先处理请求先到的用户,那么有可能导致网络条件好或者手速快的用户总是抢先一步,不利于公平性。

17.3.5 面试官:消息的公平性如何保证,即保证每一个请求都有机会获取到商品秒杀的权利?即回答一下如何确认这1000w条里面有哪一百条才能得到商品数据?

答:以下四种方法中,1和2只能保证先到先得得公平性,不适用秒杀场景,3和4适合秒杀场景

理想的情况下,我们希望能完全公平地处理所有用户的请求。但在实际操作中,为了保证系统的高并发性能,一般的实践是在处理大量请求时只能保证一定的公平性,即只能保证大部分请求都有机会得到处理,而不能保证完全按照请求的顺序进行处理。

为了尽可能保证公平性,我们可以采取以下的一些策略:

  1. 预减库存:在用户请求秒杀时,我们可以在Redis等高速缓存中进行预减库存操作。如果预减成功,再将用户的请求发送到消息队列中,等待后续处理。这种方式可以有效防止库存超卖,但并不能完全保证公平性。

  2. 使用有序队列:如果我们想要更好的公平性,可以考虑使用有序的消息队列,比如Kafka。这样,我们可以按照用户请求的顺序进行处理。然而,这种方式可能会降低系统的并发处理能力。

  3. 分批处理:我们还可以考虑将用户的请求分批处理。例如,我们可以每秒处理一定数量的请求,然后在这些请求中随机选择一部分进行处理。这种方式既可以保证一定的公平性,又可以保证系统的并发处理能力。

  4. (这个可能真正回答了如何确认1000w条)使用抽签系统:对于超高并发的场景,我们甚至可以考虑使用抽签系统。在用户请求秒杀时,我们可以让用户参与一个抽签,只有抽中的用户才有资格进行秒杀。这种方式可以有效防止库存超卖,并且在一定程度上保证了公平性。

需要注意的是,上述的各种策略都只能在一定程度上保证公平性,并不能保证完全的公平。因为在高并发的环境下,完全的公平性往往意味着较低的性能。因此,在实际的系统设计中,我们需要在公平性和性能之间找到一个合适的平衡点。

17.3.6 如果使用抽签系统(可以单独做一个微服务,或者在本地生成也可以),如何设计这个系统呢,给出一个可行的高校方法,会用到什么算法

设计一个抽签系统,通常可以根据以下步骤进行:

  1. **用户报名:**当用户点击秒杀按钮时,不直接进行秒杀操作,而是标记该用户为参与抽签。这可以通过在数据库中为每个用户创建一条记录来实现,记录中包含用户ID和商品ID等信息。

  2. **生成抽签池:**在秒杀开始之前,从所有报名的用户中抽取一部分用户作为参与抽签的用户,形成一个抽签池。生成抽签池的方法可以有多种,例如随机抽取,或者根据用户的购买历史、信用等级等因素进行筛选。

  3. **进行抽签:**在秒杀开始时,从抽签池中随机选取一部分用户作为中签用户,这些用户有权进行秒杀操作。进行抽签的方式也可以有多种,例如可以使用简单的随机算法,或者使用更复杂的加权随机算法,对信用等级高的用户给予更高的中签概率。

  4. **通知中签用户:**抽签结束后,需要将抽签结果通知给所有参与抽签的用户。对于中签用户,可以直接发送一个包含秒杀链接的通知,用户点击链接后可以直接进行秒杀操作。

这个系统的关键在于如何进行抽签。最简单的方式是使用随机算法,例如可以使用Java的Random类生成随机数,然后根据随机数决定是否选取某个用户。对于更复杂的场景,例如需要根据用户的信用等级等因素进行抽签,可以使用加权随机算法,根据每个用户的权重生成随机数,权重大的用户生成的随机数范围更大,因此中签的概率也更高。

注意,以上只是一个抽签系统的基本设计方案,具体实现时可能需要考虑更多的细节,例如如何处理网络延迟、如何保证抽签的公平性、如何处理用户重复报名等问题。

17.3.7 介绍一下加权随机法的实现方式以及java代码

  1. 加权随机法的实现方式

加权随机法是一种根据权重进行随机选择的方法。具体实现如下:

  • 首先,我们需要一个列表来存储所有的用户和他们的权重。
  • 然后,我们计算所有用户权重的总和。
  • 然后,我们生成一个0到总权重之间的随机数。
  • 最后,我们从列表的第一个元素开始,累加每个元素的权重,当累加的权重大于或等于随机数时,就返回该元素。

这个算法的时间复杂度为O(n),空间复杂度也为O(n)。

以下是该方法的Java实现:

java 复制代码
public class WeightedRandom {
    private static class Node {
        int weight;
        String user;
        
        Node(String user, int weight) {
            this.weight = weight;
            this.user = user;
        }
    }
    
    private List<Node> nodeList = new ArrayList<>();
    private int totalWeight = 0;
    
    public void addUser(String user, int weight) {
        nodeList.add(new Node(user, weight));
        totalWeight += weight;
    }
    
    public String getRandomUser() {
        int randomWeight = new Random().nextInt(totalWeight);
        int weightSum = 0;
        for (Node node : nodeList) {
            weightSum += node.weight;
            if (randomWeight < weightSum)
                return node.user;
        }
        return null;
    }
}

在这段代码中,我们首先定义了一个内部类Node来存储用户和权重,然后在主类WeightedRandom中定义了一个列表nodeList来存储所有的Node,和一个变量totalWeight来存储所有权重的总和。然后,我们定义了两个方法,addUser用于添加用户,getRandomUser用于根据权重随机选择一个用户。

17.3.8 使用抽签方式后,是否需要使用redis预减库存

使用抽签方式后,流量可以直接打到消息队列,但是使用redis预减库存仍然是一个好的做法,它可以进一步提升系统的响应速度,并且减少数据库的压力。

17.3.9 抽签系统在高并发环境下的考虑

在高并发环境下,抽签系统需要考虑并发控制和性能优化。例如,当大量用户同时请求抽签时,可能会导致系统的负载过高。为了解决这个问题,可以使用限流算法,例如漏桶算法或者令牌桶算法,来控制请求的并发数;每次只放一批请求过来去抽签

17.3.10 如何对没有抽中的人进行去重操作(防止它持续性点击)

对于没有抽中的用户,可以在数据库或者缓存中为每个用户创建一个标记,标记用户是否已经参与过抽签。然后在用户请求抽签时,首先检查这个标记,如果用户已经参与过抽签,就直接返回一个提示信息,告诉用户不要重复点击。这种方法的效率很高,可以有效防止用户持续性点击。

18关于超卖问题

我写的一篇关于超卖问题的解决方案

18.1 秒杀系统中的超卖问题解决方案

**超卖问题主要是由于并发情况下,系统无法保证库存扣减的原子性。**解决超卖问题的常见方式如下:

  • 乐观锁:乐观锁实际上是一种思想,即假设系统在大部分时间里不会发生并发冲突,所以只在真正写入数据时才会检查是否存在冲突。在秒杀系统中,可以在数据库中为商品设定一个版本号(或时间戳),每次更新库存时,都对比版本号(或时间戳),只有当版本号(或时间戳)符合预期时,才进行更新,并将版本号(或时间戳)加一。

  • 悲观锁:相比于乐观锁的乐观假设,悲观锁假设每次去读数据都会发生并发冲突,所以在每次读数据的时候都会上锁,确保在读写数据时不会被其他线程影响。

  • 使用分布式锁:例如Redis的SETNX命令或者Zookeeper、etcd等分布式协调服务。

  • 数据库行级锁:例如MySQL InnoDB引擎的行级锁等。

  • 队列串行化:将请求放入消息队列,一个一个的处理。

18.2 关于使用etcd分布式锁的方案

可以采用etcd分布式锁来解决秒杀系统中的超卖问题。在请求打到数据库之前,每个请求都尝试获取锁,只有成功获取锁的请求才能去数据库扣减库存。这样可以保证对库存扣减的操作是串行的,从而避免了超卖的问题。

  • 优点:可以有效避免超卖问题,保证数据的一致性。

  • 缺点:并发性能可能会下降。因为所有的请求都需要尝试获取锁,如果请求量非常大,可能会导致大量的请求在等待获取锁,从而增加了系统的响应时间。同时,对于分布式锁的使用,也需要考虑锁的生命周期管理,例如避免死锁、锁超时自动释放等问题。

一种常见的优化方案是,结合缓存、消息队列和数据库三者,首先在缓存中进行库存检查和预减库存,然后将请求放入消息队列,最后由消费者线程去数据库中扣减真正的库存。这种方案既能避免超卖问题,又能提高系统的并发处理能力。

19 关于秒杀的微服务系统的多级限流问题

19.1 微服务系统的多级限流措施?各级之间的限流关系是怎么样的?

nginx,api网关,rpc框架以及接口本身都可能会做限流,这几种限流器允许同时存在

19.2 nginx的限流器是如何实现的

高并发下的Nginx限流实战

nginx使用一种名为Leaky Bucket(漏桶)的算法进行限流。基本思路是,所有进入的请求(作为水进入漏桶)都会被暂存,然后以固定的速度(漏洞的大小)处理和发送这些请求。如果进入的请求过多导致"桶"满了,新的请求将被拒绝或排队。

nginx的限流配置通常在nginx的配置文件中设置,像下面这样:

java 复制代码
nginx
Copy code
http {
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;

    server {
        location /request/ {
            limit_req zone=mylimit burst=5;
        }
    }
}

在这个例子中,limit_req_zone定义了一个名为mylimit的限流区域和速度1r/s(每秒一个请求)。burst=5表示可以容忍短时间的突发流量。

19.2.1 nginx的限流一般是针对哪个进行限流

Nginx的限流一般是针对请求的数量或者请求的速率进行限流,具体可以针对以下几个维度进行限流:

  1. IP地址限流:你可以设置每个IP地址在一定时间内只能发送多少个请求或者在一定时间内的请求速率。

  2. Location或URL限流:你可以根据特定的请求路径或URL进行限流。例如,对于某些特定的API路径,你可以设置不同的限流策略。

  3. 用户身份或认证限流:如果你的系统有用户身份认证,你可以根据用户身份进行限流,以限制每个用户的请求数量或速率。

  4. 后端服务器限流:有时候,你可能希望限制向后端服务器的请求数量或速率,以防止服务器过载。

  5. HTTP方法限流:你可以根据HTTP请求方法(GET、POST、PUT等)进行限流。

  6. 特定User-Agent限流:如果你希望限制特定User-Agent的请求,例如爬虫,你可以设置User-Agent为限流维度。

  7. Referer限流:有时你可能想根据请求中的Referer信息进行限流,以防止恶意请求。

总之,Nginx的限流非常灵活,可以根据不同的需求选择不同的限流维度,以保护服务器免受过多请求的影响,确保系统的稳定性和可用性。

19.3 api-gateway如何实现限流的,以zuul为例

Zuul是Netflix出品的一个API网关,通常用于微服务架构。限流可以通过过滤器来实现。

前置过滤器(Pre Filter): 在这里,你可以根据不同的规则(例如IP地址,请求路径等)来限流。

令牌桶算法: 通常用于实现高效的限流。

一个简单的Zuul过滤器示例可能是这样:

java 复制代码
@Component
public class RateLimitFilter extends ZuulFilter {
  
  private RateLimiter rateLimiter = RateLimiter.create(1000);  //每秒1000个令牌
  
  @Override
  public String filterType() {
    return "pre";
  }
  
  @Override
  public int filterOrder() {
    return -4;
  }
  
  @Override
  public boolean shouldFilter() {
    return true;
  }
  
  @Override
  public Object run() {
    if (!rateLimiter.tryAcquire()) {
      throw new RateLimitException();
    }
    return null;
  }
}

19.4 dubbo如何实现服务限流和熔断

dubbo如何实现服务限流和熔断

19.5 自定义的限流器是如何实现限流的?

一般常见的有三种算法,滑动窗口、令牌桶给和漏桶,从中选取一个作为限流算法,然后配合AOP的前置通知或者注解和redis,实现对不同粒度、不同接口以及不同用户的限流。

19.3 先流的粒度和方式方法以及各种方法的优劣权衡

可以查看下面这篇文章的第19点
系统设计类题目汇总二

20 秒杀模块的数据表如何设计的

20.1 为了避免影响正常的购物流量,为正在秒杀的活动单独创建了一套秒杀表,包括秒杀商品表、秒杀订单表,为什么?

秒杀活动(Flash Sales)通常会导致巨大的流量涌入,这对任何电子商务网站或应用来说都是一次巨大的压力测试。设计一套专门针对秒杀活动的数据库表结构有多个好处:

1. 系统性能

秒杀活动需要非常快速的读写操作。通过创建单独的秒杀表,可以对这些表进行特定的优化,例如:高速缓存、分区等,以提高性能。

2. 数据隔离

正常购物流量和秒杀流量通常有不同的业务规则和数据需求。通过隔离这些数据,你可以减少复杂性,并使得常规操作不受秒杀活动的影响。

3. 可扩展性

单独的秒杀表意味着你可以更容易地将秒杀相关的数据和逻辑迁移到独立的服务或数据库,以提高系统整体的可扩展性。

4. 简化问题排查

出现问题时,拥有专门的秒杀表可以更容易地定位和解决问题,因为你只需要关注一个相对较小和专门化的数据集。

5. 提高安全性

由于秒杀活动容易受到各种攻击(例如,刷单,机器人抢购等),单独的数据表结构使得可以更方便地实施特定的安全措施。

20.2 秒杀商品和订单表与普通的商品订单表是什么关系?

秒杀商品和订单表与普通的商品和订单表在业务逻辑和用途上有一定的关联性,但通常会被设计为相对独立的数据结构。下面是他们之间可能的关系:

20.2.1 数据模型

  1. 共享商品数据 :秒杀商品通常是普通商品表(Products)中的子集。在这种情况下,秒杀商品表(FlashSaleProducts)可能有一个字段,如 original_product_id,作为外键指向普通商品表的相应条目。

    sql 复制代码
    FlashSaleProducts
    - original_product_id (Foreign Key -> Products.id)
  2. 共享用户数据 :同样,秒杀订单表(FlashSaleOrders)和普通订单表(Orders)可能共享同一个用户表(Users)。

    sql 复制代码
    FlashSaleOrders
    - user_id (Foreign Key -> Users.id)
    
    Orders
    - user_id (Foreign Key -> Users.id)

20.2.2 数据流

  1. 商品信息同步:如果一个商品同时出现在普通商品表和秒杀商品表中,那么需要确保这两个表中的商品信息(如价格、库存等)能够同步或至少保持一致。

  2. 订单转换:在某些情况下,一个开始作为秒杀订单的订单可能最终会转换为普通订单,或者反过来。

20.2.3 数据隔离

尽管有一定的关联性,但通常推荐将秒杀相关的数据表与普通购物流程的数据表分开。这样做有助于:

  1. 性能优化:针对秒杀场景进行特定的性能优化。
  2. 降低复杂性:减少在一个单一的表中处理多种业务逻辑的复杂性。
  3. 安全性:秒杀活动通常需要额外的安全措施,如防刷单、防机器人等。

总体来说,尽管秒杀商品和订单表可能与普通的商品和订单表有一定的数据和业务逻辑重叠,但将它们设计为独立的表通常是更可取的。这样可以更灵活地应对不同的性能、安全和业务需求。

20.3 所以秒杀中的商品库存扣减了之后,还需要将这个库存的减少数量在商品表中也减一遍嘛?同理秒杀订单也需要同步到普通的订单表中嘛?

20.3.1 商品库存

  1. 同步方式: 如果秒杀商品和普通商品共享同一库存,那么在秒杀期间对商品库存的任何扣减确实需要在普通的商品表中也进行相应的扣减。

  2. 独立库存: 另一种方式是为秒杀活动分配独立的库存。这种情况下,在秒杀表中的库存扣减不会影响到普通商品表的库存。

选择哪种方式取决于业务需求。共享库存更符合现实世界的物流,但可能增加系统复杂性。独立库存则更容易管理,但可能与实际物流状态有偏差。

20.3.2 订单同步

  1. 不同步: 秒杀订单和普通订单可以被视为完全独立的实体,各自维护在各自的表中。这样做可以简化处理逻辑,并允许针对秒杀订单进行特定的优化。

  2. 同步至普通订单表: 在秒杀活动结束后,或者在订单状态发生变更(例如,从'未支付'变为'已支付')时,可以选择将秒杀订单同步到普通订单表中。这样做可以统一订单处理逻辑,但会增加系统复杂性。

  3. 标记或状态字段: 也可以在普通的订单表中加入字段,以标识是否为秒杀订单。这样,普通订单和秒杀订单就可以在同一张表中管理,但这也会增加查询和管理的复杂性。

是否需要同步取决于你的具体需求,例如,是否需要在一个统一的界面中管理所有类型的订单,或者是否有必要将所有订单数据集成到一个单一的分析系统中。

总体来说,如何处理库存和订单同步是一个业务决策,取决于你的具体需求和约束。不论选择哪种方式,都需要确保系统的一致性、可扩展性和性能。

20.4 下面是我创建的几张相关表

1 普通商品表和秒杀商品表

2 普通订单表和秒杀订单表


20.5 示例数据表设计

下面是一个非常基础的示例,用于描述可能的秒杀商品表和秒杀订单表:

20.5.1 秒杀商品表 (FlashSaleProducts)

  • id: 主键
  • product_name: 商品名称
  • original_price: 原价
  • flash_sale_price: 秒杀价
  • quantity_available: 可用数量
  • start_time: 秒杀开始时间
  • end_time: 秒杀结束时间

20.5.2 秒杀订单表 (FlashSaleOrders)

  • id: 主键
  • user_id: 用户ID
  • flash_sale_product_id: 秒杀商品ID(外键)
  • quantity: 购买数量
  • order_time: 下单时间
  • status: 订单状态(例如,完成、取消、失败等)

这样的设计提供了一个基础框架,可以根据特定需求进行进一步的定制和优化。

21 JMeter压测

21.1 讲一讲QPS,并发量之间的计算过程

QPS(Queries Per Second)是衡量数据库或系统性能的一种常用指标,它表示每秒钟能够处理的查询或请求数量。与之相关的指标包括并发量(Concurrency)和响应时间(Response Time)。

计算QPS和并发量之间的关系通常涉及以下几个因素:

  1. QPS(Queries Per Second):QPS是指每秒钟能够处理的查询或请求数量。它表示系统的处理能力,即在单位时间内可以处理多少个请求。

  2. 并发量(Concurrency):并发量是指同时存在于系统中的并发请求数量。在高并发的情况下,多个请求同时发送到系统,因此存在多个并发请求。

  3. 响应时间(Response Time):响应时间是指系统处理一个请求所需的时间。它可以是一个查询的执行时间,也可以是一个HTTP请求的响应时间。

计算QPS和并发量的关系通常可以使用以下公式:

QPS = 并发请求数 / 平均响应时间

这个公式中,QPS是需要计算的指标,而并发请求数和平均响应时间是已知的参数。

  • 并发请求数:这是系统同时处理的请求的数量。它可以根据系统的并发性设置、请求队列长度等来确定。例如,如果有100个请求同时发送到系统,那么并发请求数就是100。

  • 平均响应时间:这是系统处理一个请求所需的平均时间。通常以毫秒(ms)为单位表示。可以通过测量系统的响应时间来获得此值。例如,如果系统的平均响应时间是10毫秒,那么平均响应时间就是10ms。

通过将已知的并发请求数和平均响应时间代入上述公式,可以计算出系统的QPS。这个计算过程可以帮助评估系统的性能和负载情况,以确保系统能够处理预期的请求量。要注意,QPS和并发量是系统性能的两个关键方面,需要根据具体情况进行优化和监控。

21.2 TPS和吞吐量:TPS是指吞吐量嘛

是的,**TPS(Transactions Per Second)通常指事务每秒钟的处理数量,它是吞吐量(Throughput)的一种度量方式。**TPS用于衡量系统或应用程序在单位时间内能够成功完成的事务数量,这些事务可以是数据库事务、网络事务、交易等,具体取决于应用程序的性质。

TPS是一个重要的性能指标,它用于评估系统的处理能力和性能。高TPS表示系统能够在短时间内处理大量事务,而低TPS可能表示系统在单位时间内能够处理的事务较少。不同的应用程序和场景可能有不同的TPS需求,因此,TPS通常被用来确定系统是否能够满足业务需求。

需要注意的是,TPS通常关注成功完成的事务数量,不包括失败的事务。它与QPS(Queries Per Second)不同,QPS通常用于衡量查询或请求的数量,而TPS用于衡量事务的数量。

21.2 吞吐量(TPS)和qps的区别

tps的加载流程:用户发起一个请求到服务器,服务器内部进行多种处理,比如调用其他服务,查询数据,网络IO,写文件等等,然后再返回给用户,这么一个流程算一次TPS;例如,在一个电子商务网站中,一个用户购买商品的完整过程可以被视为一个事务,该事务包括用户浏览商品、将商品添加到购物车、进行结算、支付等多个步骤。这个完整的过程可以算作一个 TPS。如果一个TPS中只有一个任务,此时TPS和QPS等值;

举一个例子:访问一个页面,从访问到客户端得到响应,会产生一个TPS,但是页面本身的图片这些加载可能会产生向服务器发送的请求,产生额外多个qps操作。

吞吐量(Throughput)和 QPS(Queries Per Second)都是用于衡量系统性能的指标,但它们有一些关键区别:

  1. 定义

    • 吞吐量(Throughput) :吞吐量是指系统在单位时间内成功完成的请求或事务的数量。它可以表示系统处理能力的度量,不仅包括查询或请求的数量,还包括成功处理的事务。吞吐量通常用于描述系统的整体性能,包括数据传输、网络带宽、磁盘I/O等方面

    • QPS(Queries Per Second):QPS是指系统在单位时间内能够处理的查询或请求的数量。它通常用于衡量数据库或应用程序服务器等处理查询或请求的性能。QPS主要关注查询或请求的数量,不考虑事务或数据传输的成功与否。

  2. 应用领域

    • 吞吐量:吞吐量通常用于描述系统的整体性能,例如,一个Web服务器的吞吐量可以表示每秒钟处理的HTTP请求的数量,包括成功响应和失败响应。

    • QPS:QPS通常用于数据库或特定应用程序服务器的性能评估,它衡量的是在数据库引擎或应用程序中每秒处理的查询或请求的数量。

  3. 成功与失败

    • 吞吐量:吞吐量包括成功和失败的请求或事务。它表示系统在单位时间内完成的所有操作数量。

    • QPS:QPS通常只关注成功的查询或请求的数量。它不考虑失败的查询或请求。

  4. 用途

    • 吞吐量:吞吐量通常用于衡量系统的整体性能和稳定性。它可以用于评估系统的容量、负载测试以及性能优化。

    • QPS:QPS主要用于衡量特定组件或服务的性能,例如,数据库引擎的查询处理能力或Web服务器的请求处理速度。

总之,吞吐量和QPS都是用于衡量系统性能的重要指标,但它们关注的方面略有不同。吞吐量考虑了成功和失败的操作,而QPS通常只关注成功的查询或请求数量。选择使用哪个指标取决于你关心的性能方面以及你想要测量的系统组件。

21.3 如果一个TPS中只有一个任务,此时TPS和QPS等值;对吗?

是的,如果一个 TPS(Transactions Per Second,每秒事务数)中只包含一个任务或一个子操作,那么这个 TPS 与 QPS(Queries Per Second,每秒查询数)可以基本等值,因为它们都表示每秒钟的操作或查询数量。

具体来说,TPS 是一个更通用的性能指标,表示在单位时间内成功完成的事务数量,而事务可以包括各种不同类型的操作,包括查询、计算、文件操作等。如果一个 TPS 中只包含一个查询任务,那么它的数量与 QPS 基本相等,因为这个 TPS 只涉及一种操作类型,即查询。

QPS 通常用于衡量特定的查询或请求操作的性能,特别是在数据库领域。但如果系统或应用程序的性能测试中只涉及一个特定的查询操作,并且在测试中这个查询操作是主要的操作,那么这个 TPS 可以与 QPS 等值,因为它们都衡量了相同的操作频率。

总之,TPS 与 QPS 的关键区别在于 TPS 更通用,可以包括各种不同类型的操作,而 QPS 更专注于查询或请求的性能。在特定情况下,如果一个 TPS 中只包含一个任务,那么它可以等同于 QPS。

21.3 tps/qps测试

背景:因为JMeter只提供了TPS的测量工具,所以根据21.3中的理论,这里我使用JMeter测试只包含一个操作-查询数据库获取商品信息的接口,这样相当于完成了qps的测试操作。

21.3.1 在1GB内存下,单核cpu下,10MB的带宽下,开启1000个线程,使用缓存,测试商品列表页

15 qps

21.3.2 在2GB内存下,单核cpu下,10MB的带宽下,开启1000个线程,使用缓存,测试商品列表页

60qps

21.3.3 在16GB内存下,10MB的带宽,不使用缓存,开启10000个线程,使用同一个用户账号,测试一个返回用户输入信息的接口。

1400qps

21.3.4 在16GB内存下,10MB的带宽,不使用缓存,开启10000个线程,使用同一个用户账号,测试商品列表页

注:商品列表页面涉及到查询数据库的操作

1100qps

页面优化:下面使用了redis去缓存商品html页面,

页面缓存优化后,windows下的tps:2340

问题:前后端不分离,将整个页面传递给前端了,传输的数据量是非常大

再次优化:前后端分离,后端只需要传递给前端所需要的json数据即可,而不是html页面

页面静态化TPS:2560

21.3.5 1000线程,循环10次,总共5w线程,测试商品列表页

windows优化前:在16GB内存下,8核cpu,10MB的带宽,不使用缓存,1300 TPS

linux优化前:在2GB内存下,单核cpu,10MB的带宽,不使用缓存,207 TPS

21.3.6 测试不做任何优化包括mq和redis的秒杀接口:在16GB内存下,10MB的带宽,开启1w个线程(1w线程对应1w用户,测3次),模拟不同的用户,去秒杀(重点)

windows优化前:785 TPS

linux优化前:175 TPS

redis缓存订单优化后windows:1185tps

redis缓存订单+页面静态化后windows:1375tps

页面静态化+redis缓存订单+redis预减库存+rabbitMQ异步下单后+windows:2450tps

页面静态化+redis缓存订单+多级缓存扣减库存(redis预减库存+java本地缓存)+rabbitMQ异步下单后+windows:2650tps

21. 4 为什么加1GB内存,qps提升了几倍?

在性能测试中,增加内存通常会对系统的性能产生积极的影响,特别是在处理大量请求或数据时。在你的测试中,增加了1GB内存后,QPS(Queries Per Second,每秒查询数)从15提升到60,这种提升可以有以下一些可能的原因:

  1. 更大的内存缓存:增加内存可以提供更多的空间来缓存数据和查询结果。当内存较小时,系统可能需要频繁地从磁盘读取数据,而增加内存后,更多的数据可以在内存中缓存,减少了磁盘I/O操作,从而加快了查询的响应速度。

  2. 减少了内存压力:较小的内存可能导致系统不得不频繁地进行内存管理和数据清理,以释放内存空间。这些操作可能会降低系统的性能。增加内存后,系统更有可能保持数据在内存中,减少了这些开销。

  3. 更多的并发处理能力:内存的增加可以提高系统的并发处理能力,因为更多的线程可以同时在内存中工作,而不会受到内存限制的限制。这可以导致更多的并发查询和更高的QPS。

  4. 数据库性能优化:增加内存后,数据库服务器可能能够更好地利用内存缓存查询计划和数据。这可以加快查询的执行速度,从而提高QPS。

  5. 减少了内存交换:当内存不足时,操作系统可能会将部分数据交换到磁盘上,这被称为内存交换。内存交换会导致显著的性能下降,因为磁盘访问速度远远低于内存。增加内存可以减少内存交换的发生,提高系统性能。

需要注意的是,内存增加对系统性能的影响是复杂的,具体的效果取决于应用程序的性质、数据库设计、硬件配置等多个因素。在进行性能测试时,增加内存通常是一种有效的性能优化手段,但需要根据实际情况权衡资源成本和性能需求。

21.4 为什么10个线程发起的页面查询,qps是90多,但是1000个线程发起查询,qps只有200多?而不是原来的100倍?

这个情况可能涉及到多个因素,其中包括系统资源限制、并发控制、数据库性能、网络延迟等。以下是可能导致这种现象的一些原因:

  1. 系统资源限制:在大量线程同时发起查询时,系统的资源(如CPU、内存、磁盘IO)可能成为瓶颈。如果系统资源不足,系统可能无法有效地处理大量线程的请求,导致性能下降。这可能会导致QPS没有线性增长。

  2. 并发控制:数据库系统通常使用锁和事务来进行并发控制。当大量线程同时查询数据库时,可能会发生锁竞争,导致一些查询被阻塞,从而降低了QPS。

  3. 数据库性能:数据库本身的性能也是一个关键因素。如果数据库服务器的处理能力不足或未经过适当优化,它可能无法处理大量并发查询,导致性能下降。

  4. 网络延迟:在分布式系统中,网络延迟也可能成为性能瓶颈。大量线程同时查询时,网络传输的开销可能会增加,从而影响QPS。

  5. 线程管理开销:创建和管理大量线程本身也会产生一定的开销。线程的创建、销毁和上下文切换可能会消耗系统资源。

21.5 为什么秒杀接口的TPS只有商品列表接口TPS的一半?

因为秒杀接口是去数据库修改和新增数据,而商品列表接口只涉及到读取数据

21.6 百万级的并发

21.6.1 平常我们所说的百万级的并发,是指一秒内有上百万的请求到达后端吗?

通常情况下,当我们谈论 "百万级的并发" 时,是指系统在某个特定时间段(通常是每秒钟)内处理了上百万个并发请求。这并不一定要求每秒钟有精确百万级的请求到达后端,而是指在一个较短的时间窗口内(通常是1秒)系统要处理数百万个请求。

百万级的并发通常是一种非常高的负载,需要强大的硬件资源、优化的系统架构和高效的处理机制来支持。在一些需要应对大规模用户或数据的互联网应用、高性能计算、云计算、金融交易系统等领域,百万级的并发是一个重要的性能指标,对于系统的可扩展性和稳定性有着重要的挑战。

需要注意的是,这个数字通常是一个抽象的概念,实际系统的性能表现会受到多个因素的影响,包括硬件资源、网络延迟、负载均衡、并发控制、数据库性能等等。在实际应用中,需要进行性能测试和优化,以确保系统能够在实际负载下稳定运行。

21.6.2 通常情况下,当我们谈论 "百万级的并发" 时,是指系统在某个特定时间段(通常是每秒钟)内处理了上百万个并发请求。这里的处理应该是指整个微服务系统的处理吧

是的,通常情况下,当我们谈论 "百万级的并发" 时,是指整个微服务系统在某个特定时间段(通常是每秒钟)内处理了上百万个并发请求。这个并发请求可以涵盖系统中的所有微服务和组件,包括前端接口、后端服务、数据库访问、缓存操作等等。整个系统的性能和吞吐量都应该考虑在内。

在微服务架构中,系统通常由多个微服务组成,每个微服务都有自己的职责和功能。因此,百万级的并发是对整个系统综合性能的考量,包括了前端接口的响应、微服务之间的通信、数据库和缓存的访问,以及其他相关操作。系统需要能够有效地处理这一并发负载,以满足用户的需求并确保高可用性。

在实际应用中,需要对每个微服务和系统组件进行性能测试和优化,以确保整个系统能够在高并发负载下稳定运行。这包括了硬件资源的分配、负载均衡、缓存策略、数据库优化等多个方面的工作。

21.6.3 这个并发请求可以涵盖系统中的所有微服务和组件,包括前端接口、后端服务、数据库访问、缓存操作等等。整个系统的性能和吞吐量都应该考虑在内。意思是包括外部的http请求,也包括内部的服务调用请求吗?

是的,通常情况下,百万级的并发请求考虑的范围包括系统中的所有请求,包括但不限于:

  1. 外部 HTTP 请求:这包括来自客户端的请求,例如用户通过浏览器或移动应用发出的 HTTP 请求。这些请求通常由前端接口或API端点处理。

  2. 内部服务调用:在微服务架构中,系统通常由多个微服务组成,它们之间需要相互通信以完成各自的任务。因此,内部服务之间的调用也需要考虑在内,包括服务间的 REST API 调用、消息队列通信等。

  3. 数据库访问:数据库是系统的关键组件之一,因此数据库的访问也需要纳入考虑范围。这包括读取和写入数据库的操作。

  4. 缓存操作:缓存通常用于提高系统的性能,因此涉及到缓存的读取和写入操作也需要考虑在内。

  5. 其他系统组件:根据系统的复杂性,还可能涉及到其他组件的请求和操作,例如消息队列的生产者和消费者、文件系统访问等。

综合来看,百万级的并发请求通常是对整个系统的性能和吞吐量的综合考量,包括了系统内外的各种请求和操作。系统需要在高并发负载下保持高可用性和性能,因此对各个组件的性能和调优都非常重要。

21.6.4 针对21.3.6 中的秒杀接口,其tps只有2600多,如何抗住百万级的并发呢?

根据21.6.2和3可知,百万级的并发不都是针对单机的,来源也是多样的,也就是说可以通过整个的分布式系统来抗这个并发数,这个系统的机器可能包括几百个服务集群,每一个集群包括了多个实例。

假如说260w并发,每一个机器能抗住2600tps的吞吐量,假设微服务系统的吞吐量和机器数量成线性关系,则从线性角度讲,需要260w/2600=1000台机器,但是我们都知道,微服务系统的各个实例之间的rpc交互会产生损耗,所以假设会导致整个分布式系统的性能损耗达到百分之30,则实际需要260w/(2600*0.7)=1428台机器

相关推荐
工业甲酰苯胺8 小时前
分布式系统架构:服务容错
数据库·架构
Java程序之猿9 小时前
微服务分布式(一、项目初始化)
分布式·微服务·架构
Yvemil711 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
小蜗牛慢慢爬行12 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
思忖小下13 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
Yvemil715 小时前
《开启微服务之旅:Spring Boot Web开发》(二)
前端·spring boot·微服务
维李设论15 小时前
Node.js的Web服务在Nacos中的实践
前端·spring cloud·微服务·eureka·nacos·node.js·express
jwolf218 小时前
基于K8S的微服务:一、服务发现,负载均衡测试(附calico网络问题解决)
微服务·kubernetes·服务发现
Yvemil719 小时前
《开启微服务之旅:Spring Boot Web开发举例》(二)
前端·spring boot·微服务