微服务系统面经之三: 以秒杀系统为例-多级缓存:用户订单状态机:增/全量库存:极限吞吐量提升

22 多级缓存(百度一面)

22.1 你用了什么缓存

答:本地内存+redis

22.2 内存标记是如何被使用被更新的

22.3 什么时候修改这个第一层缓存(内存标记),把它改为false

答:当redis预减库存为0的时候,这个就需要改为false

22.4 这个内存标记可以被称为第二级缓存吗?(我答错了)

22.4.1 内存标记和redis预减库存的操作,哪一个属于一级缓存哪一个属于二级

在秒杀系统设计中,"内存标记"和"Redis预减库存"都是优化手段,旨在减少对数据库的访问以提高系统的性能。但在缓存的层级分类中,这两者的位置可以如下解释:

  1. 内存标记:它直接在应用程序的内存中进行,速度非常快。因为这种操作不需要与任何外部系统通信,所以它通常被视为一级缓存

  2. Redis预减库存:Redis是一个外部的内存存储系统,虽然它非常快,但与直接在应用程序的内存中进行操作相比,访问Redis会涉及到网络通信的延迟。因此,从缓存的层级分类角度来看,Redis通常被视为二级缓存

所以,简而言之,内存标记是一级缓存,而Redis预减库存是二级缓存。

22.4.2 一级、二级缓存的区别标志是什么呢

一级缓存和二级缓存的区别通常基于以下特性或标志来定义:

  1. 存储位置

    • 一级缓存:通常存储在事务或会话的上下文中,这意味着它是局部的,仅与单个会话或线程相关。例如,在某些ORM框架中,一级缓存是与单个会话关联的。
    • 二级缓存:通常是全局的,它存储在进程或应用之外的位置,并可供多个会话或线程共享。例如,分布式缓存系统如Redis或Memcached。
  2. 生命周期

    • 一级缓存:生命周期通常限于单个会话或事务。会话结束时,缓存中的数据也可能被清除。
    • 二级缓存:生命周期超出单个会话,通常基于时间或容量失效策略来管理。
  3. 数据共享性

    • 一级缓存:因为它是局部的,所以不会被多个会话或线程共享。
    • 二级缓存:可以被多个会话、线程或甚至是多个应用实例共享。
  4. 访问速度

    • 一级缓存:访问速度非常快,因为它通常在应用程序的内存中。
    • 二级缓存:虽然也是非常快的,但由于可能涉及到进程间或网络通信,所以相对较慢。
  5. 失效策略

    • 一级缓存:通常在会话结束或事务完成后自动失效。
    • 二级缓存:通常基于具体的失效策略,如LRU、TTL等。
  6. 复杂性

    • 一级缓存:通常更简单,自动管理。
    • 二级缓存:可能需要更多的配置和管理,尤其是当涉及到分布式缓存系统时。

根据上述特点,开发者可以根据实际需要选择使用一级缓存、二级缓存或两者结合,以优化应用程序的性能。

22.4.3 我觉得可以根据距离cpu的逻辑远近来判断,比如本地内存标记不需要网络通信,所以是一级,redis需要网络通信,所以是二级 另外一种角度看,数据是首先同步到redis,然后再同步到内存标记的

确实,根据数据存取的速度和距离CPU的逻辑远近来判断缓存层级是非常直观的方式。在计算机架构中,这种分类方式常被用来描述CPU的缓存层级(L1、L2、L3等),其中距离CPU核心越近、速度越快的缓存被视为更低的层级。

对于应用层面的缓存:

  • 一级缓存:通常指直接存储在应用进程中的缓存,如Java对象在JVM堆上的存储。这种缓存的访问速度极快,不涉及任何跨进程或网络通信。

  • 二级缓存:涉及到跨进程通信或网络通信的缓存,如Redis、Memcached等分布式缓存系统。虽然它们仍然是内存存储,但由于涉及通信,访问速度相对较慢。

你的观点完全成立。如果数据首先同步到Redis然后再同步到内存标记,那么可以将Redis视为一种“前置缓存”或“缓冲层”,而应用内存中的标记则可以视为更接近实际数据处理和业务逻辑的一级缓存。

这种层级化的缓存策略使得应用能够更加灵活地处理高并发情况,同时确保数据的一致性和系统的响应速度。

22.5 内存标记和redis缓存的区别是什么呢?

答:使用内存标记就不需要和远程主机进行网络通信

23 一致性与缓存和mysql的更新机制(百度一面)

23.1 如果有一部分流量进来了之后,就卡在这个边界点,你怎么去判断?如果秒杀第10件商品的时候,有两个请求同时打到后端,有没有可能这两个请求都进到你的消息队列中去?

答:有可能的,因为redission可能会发生数据丢失,redis集群本身只能保证最终一致性

23.2 如果有一主多从的话,是不是可能存在多个流量进来呢?

答:对,但是这种概率很低,因为一个redis集群连续发生多次主从切换的可能性很小

23.3 前端这个时候,你会提示它在等待吗?

答:对,会显示排队中,客户端会不断轮询后端暴露出来的一个查询订单的接口,查看是否存在,这个接口会增加这个订单到redis缓存中

23.3.1 为什么这个时候数据库和redis的同步机制是先更新mysql,后更新缓存?难道不存在不一致问题吗?

答:这个时候,客户端只涉及到查询订单操作,而不涉及到修改,而且只有这一个服务端线程会修改redis缓存,所以可以采用先更新mysql,再删除redis

23.4 如果用户下单后没多久又取消订单了怎么办?如果没有支付怎么办(误区:(1)把这两个弄成了一个事情,库存回退没有说,两级缓存更新没有说)

答:

当用户下单后没多久又取消订单,或者下单后未支付,系统需要有一套相应的处理机制。这些机制通常涉及订单的生命周期管理、库存管理、支付超时等。以下是一些常见的处理方式:

  1. 订单取消

    • 库存回滚:当用户取消订单,前端会向指定的接口立即发送请求,系统应立即将对应的商品库存数量回滚。
    • 订单状态管理:订单应有多个状态,如“已创建”、“已支付”、“已取消”等。当用户取消订单时,系统应将订单状态更改为“已取消”。
    • 库存回退和缓存更新:会将这个库存回退到mysql的商品表中,然后会删除redis中相应的订单,同时删除redis的关于该库存信息的缓存,然后再将业务服务器中的该商品是否售罄的缓存更新为false
  2. 未支付的订单

    • 支付超时机制:为订单设定一个支付超时时间(例如15或30分钟)。如果用户在这个时间内没有完成支付,系统自动将订单状态更改为“已取消”并回滚库存。
    • 超时提醒:当订单即将到达超时时间时,可以通过短信、邮箱或应用内通知提醒用户支付。
    • 定期检查:后端可以使用定时任务或其他机制定期检查未支付的订单,并根据其创建时间处理超时订单。
    • 库存回退和缓存更新:(基本逻辑和订单取消的相应逻辑相同)

总之,对于用户的取消订单或未支付行为,系统应该有一套完整的策略和流程,确保商家的利益和其他用户的正常购买体验不受影响。

23.5 你的一级缓存如何更新呢?这是不是涉及到了多个本地内存机器的内存标记的更新呢?

答:

一级缓存通常指的是业务服务器本地的内存缓存,例如Java中的ConcurrentHashMap或其他本地缓存工具。更新一级缓存的方法和策略如下:

  • 数据变动触发:当有关键数据(如库存)变动时,我们需要同时更新一级缓存中的相关数据。
  • 多机器同步问题:在分布式环境中,可能有多个业务服务器实例,因此当一级缓存的数据在一个实例中被修改时,我们需要确保其他实例的缓存数据也被相应地更新。这通常通过消息中间件如Kafka、RabbitMQ等来实现,当一个实例更新了缓存,它会发送一个消息到消息中间件,其他实例监听到这个消息后会更新自己的一级缓存。
  • 定期刷新:一些数据可能不是经常变动,但为了确保数据的时效性和准确性,可以设置一个定期刷新的策略,例如每隔一段时间从数据库或二级缓存(如Redis)中重新加载数据。

23.6 你的缓存更新会不会造成并发的冲突呢?比如客户端线程在读,服务端线程在写

答:

缓存并发的问题确实是一个需要关注的点。根据不同的缓存类型和策略,处理方式也会有所不同。

  • Redis缓存:Redis是单线程模型,它确保了每次只有一个命令在执行,因此不会有并发冲突。但在分布式应用中,多个客户端同时发送命令到Redis可能会造成读旧数据的情况。为了避免这种情况,可以使用Redis的事务功能或乐观锁来确保数据的一致性。

  • 本地内存缓存:在多线程环境中,本地缓存可能会出现并发的问题。例如,当一个线程正在更新缓存数据时,另一个线程可能正在读取这些数据。为了解决这个问题,可以使用并发工具,如Java中的ConcurrentHashMap或ReadWriteLock。ConcurrentHashMap允许多个读线程并发进行,但写操作会被锁定,确保每次只有一个线程可以写入。ReadWriteLock也提供了类似的功能,但提供了更细粒度的控制。

综上所述,确保缓存的并发安全是关键,需要根据实际情况选择合适的工具和策略来实现。

24 (度小满金融校招一面)你的用户订单的状态机是怎么设计的呢,假设一条订单的初始值为0,表示用户下单了,1表示已经支付了,2表示…

答:假设一条订单的初始值为0,表示用户下单但是仍然未支付,1表示成功支付了,待发货中,2表示用户主动取消订单了,3表示过期没有支付所以取消订单,4是终态,不会再变化了。

24.1 你什么时候插入的订单记录?

答:消费者主动从kafka消息队列中拉取下单消息,进行消费的时候,会进行下单操作。

24.2 基于你刚刚设计的状态机,用户的哪一个行为会导致状态机由哪一个状态流转到哪一个状态,这个由用户导致的行为一般体现在后端系统中的哪一步

答:
(1)用户点击下单按钮会生成一笔订单,此时订单的状态是0,消费者主动从kafka消息队列中拉取下单消息,进行消费的时候,会进行下单操作,此时变成初始状态0

(2)用户主动调用取消订单的接口,会使得状态由0变为2

(3)用户调用支付接口,状态由0变为1

(4)用户过了三十分钟没有支付,延时消息会到期被消费,将订单状态由0变为3

(5)用户收到货之后点击收货按钮,就会变为终态

24.3 假设现在又新增了几个状态,退换货完成,退换货中,退款中,退款完成,交易关闭这几个状态,想一下连同新增的状态,各个状态之间怎么流转,在什么时间点流转。

答:考虑到新增的状态,我们可以定义以下状态值:

5 - 退换货中
6 - 退换货完成
7 - 退款中
8 - 退款完成
9 - 交易关闭

基于以上的状态定义,以下是各状态间的可能流转以及在什么时间点流转:

  1. 用户点击下单按钮
    用户行为:点击下单按钮
    状态流转:初始 -> 0 (未支付)

  2. 用户取消订单
    用户行为:主动调用取消订单的接口
    状态流转:0 (未支付) -> 2 (用户主动取消)

  3. 用户支付
    用户行为:调用支付接口
    状态流转:0 (未支付) -> 1 (已支付待发货)

  4. 用户超时未支付
    系统行为:处理延时消息
    状态流转:0 (未支付) -> 3 (过期未支付)

  5. 用户确认收货
    用户行为:点击收货按钮
    状态流转:1 (已支付待发货) -> 4 (交易完成)

  6. 用户申请退换货
    用户行为:点击申请退换货
    状态流转:4 (交易完成) -> 5 (退换货中)

  7. 退换货完成
    系统行为:确认退换货请求已处理
    状态流转:5 (退换货中) -> 6 (退换货完成)

  8. 用户申请退款
    用户行为:点击申请退款
    状态流转:4 (交易完成) 或 6 (退换货完成) -> 7 (退款中)

  9. 退款完成
    系统行为:确认退款已处理
    状态流转:7 (退款中) -> 8 (退款完成)

  10. 交易关闭
    系统或用户行为:在任何非终态下,由于某些原因(如长时间不活跃、交易异常等)交易关闭
    状态流转:任意状态 -> 9 (交易关闭)

以上流转可能因业务需求的具体性而有所不同,但大致能够涵盖大部分场景。

25 希音一面:你的秒杀系统做的是增量还是存量库存啊(覆盖还是加减)

答:加减

25.1 我可以为你解释增量库存和全量库存的概念:

  1. 增量库存

    • 增量库存是指对库存的增加或减少是基于当前库存量进行的。例如,如果当前库存为10,增量为+3,则新的库存为13;如果增量为-2,则新的库存为8。
    • 在实际操作中,增量库存通常是通过加减操作来实现的。例如,每次商品销售,库存就减少相应的数量;每次进货,库存就增加相应的数量。
    • 增量库存操作更适合于实时或高并发的场景,因为它可以很好地处理并发操作,并确保数据的一致性。
  2. 全量库存

    • 全量库存是指每次更新库存时,都是设置一个完整的库存值,而不是基于当前库存进行加减。
    • 例如,无论当前库存是多少,都可以直接设置库存为20或其他任意值。
    • 全量库存操作通常用于库存的初始化、盘点或其他需要完全覆盖当前库存的场景。

在实际的库存管理系统中,增量库存和全量库存都有其应用场景,具体使用哪种方式取决于业务需求和系统设计。

25.2 除了年终盘点,还有什么全量库存的应用场景呢?

  1. 除了年终盘点,还有什么全量库存的应用场景呢?

    • 新商品上架:当商店引入新的商品时,需要为这些商品设置初始的库存量。这是一个全量库存的操作,因为是直接设置商品的库存,而不是基于现有的库存进行加减。

    • 退货和换货:当客户退回或换货时,可能需要对库存进行调整。在某些情况下,这种调整可能是全量的,例如直接设置某个商品的库存为特定的数量。

    • 库存调拨:在大型的零售或仓储企业中,可能需要在不同的仓库之间进行库存调拨。当某个仓库收到调拨的商品时,可能会直接设置这些商品的库存,而不是基于现有的库存进行加减。

    • 库存纠错:如果发现之前的库存记录有误,可能需要进行库存纠错。这通常是一个全量库存的操作,因为是直接设置商品的库存,而不是基于现有的库存进行加减。

  2. 年终盘点:在零售或仓储行业中,通常在年底会进行一次全面的库存盘点。在这个过程中,员工会手工点验每一件商品,然后将实际的库存数量录入系统。由于这是一个全面的盘点,所以不考虑之前的库存数量,直接将系统中的库存值设置为盘点后的实际数量。这就是一个典型的全量库存的应用场景。

25.3 这两种库存方式的幂等性怎么保证

  1. 增量库存的幂等性怎么保证

    • 唯一标识:为每次的库存变动操作生成一个唯一的标识(如订单号、事务ID等)。在操作前检查该标识是否已经被处理过,如果已处理,则不再进行操作。
    • 数据库锁:使用数据库的行锁或乐观锁来确保同一时间只有一个操作能够更改库存。
    • 状态检查:在进行增量操作前,检查商品或订单的状态,确保不会重复处理。
    • 消息队列:使用支持幂等性的消息队列,如Kafka,确保消息只被消费一次。
  2. 全量库存的幂等性

    全量库存的幂等性相对容易保证,因为每次都是设置一个固定的值,不依赖于之前的状态。只要确保每次设置的值是相同的,那么多次操作的结果也是相同的。但在实际操作中,还需要考虑以下几点:

    • 唯一标识:同样可以为每次的库存设置操作生成一个唯一的标识,以确保不会重复设置。
    • 版本控制:为库存数据增加版本号或时间戳,每次更新时比较版本号或时间戳,只有当新的版本号或时间戳大于当前的版本号或时间戳时,才进行更新。
    • 数据库锁:使用数据库的行锁或乐观锁来确保同一时间只有一个操作能够设置库存。

25.4 增量库存可以使用下面这些全量库存版本控制的方法嘛?

是的,增量库存同样可以使用以下的版本控制方法:

  • 唯一标识:为每次的库存变动操作生成一个唯一的标识(如订单号、事务ID等)。这可以确保每次的库存变动操作不会被重复执行。
  • 版本控制:为库存数据增加版本号或时间戳是一个常见的策略,特别是在高并发的场景下。当多个请求同时尝试修改库存时,只有版本号或时间戳匹配的请求才会被执行,其他的请求则会被拒绝。这样可以确保库存的数据一致性和操作的幂等性。

26 吞吐量达到极限了怎么继续提升(度小满金融三面)

26.1 秒杀主要体现在哪个地方呢,吞吐量高吗还是

答:秒杀主要体现在系统的短时间内需要处理大量用户的并发请求,特别是在秒杀活动开始的那一刹那。因此,秒杀系统的吞吐量需求非常高,以确保在短时间内处理尽可能多的用户请求,同时确保系统的稳定性和可用性。

26.2 你的极限吞吐量怎么得来的

答:极限吞吐量通常通过压力测试得出。在控制条件下,我们会逐渐增加发向系统的请求,并监控系统的响应时间和错误率。当我发现进一步提高线程数或并发请求时,tps开始下降或系统响应时间显著增加,这时的吞吐量就是系统的极限吞吐量。

26.3 有没有可能是发压的地方扛不住了呢

答:有这种可能性。发压工具或者发压机本身也可能成为瓶颈,特别是当它所在的硬件资源不足或网络带宽受限时。这也是为什么在进行压力测试时,我们也要监控发压机的资源使用情况。

26.4 你在什么样机器上发出这1w个请求呢,为什么能抗住这1w线程或者更多的并发请求呢

答:本地,16GB的内存,8核心,首先我是在本地进行压测的,它不涉及到远程通信,此外它的秒杀接口返回的信息量很少,各个服务也是本地部署,也就是说本机进程之间的通信速度也很快,这样就是说开的这些线程并发数虽然很高,但是也会很快释放资源,或者说执行完的线程会被重用。

26.5 那你这里2600TPS没法提高的瓶颈大概是在什么地方呢?假设我想让他提到3000,4000,需要在哪些地方做优化呢

答:要提高TPS,首先需要识别当前的瓶颈。这可能涉及到CPU、内存、数据库、网络等多个方面。优化可能包括对Redis进行水平扩展,对各微服务进行集群部署和负载均衡,优化数据库查询和索引,甚至考虑使用更高效的硬件。

26.6 刚刚你说对redis做水平扩展,是默认了redis已经是瓶颈了是吗

答:不完全是。提到Redis的水平扩展是基于它常常是高并发系统中的一个瓶颈点。但真正的瓶颈可能涉及多个方面。为了确定真正的瓶颈,需要进行深入的性能分析和监控。

26.7 你觉得你写的这个系统在,是哪一个地方的资源或者哪一种资源不够,导致需要做水平扩展呢,比如IO,磁盘,网络,线程,内存和cpu

答:
(1)如果目的是提升整个微服务后端系统的总体tps,那么发压方的机器也需要进行集群部署,并在每个机器上随机访问不同的接口,以模拟真实的分布式环境。

(2)对于提升秒杀接口的tps,我认为订单处理服务可能是一个主要的瓶颈,因为它涉及到库存的扣减和订单的生成。因此,订单服务集群的水平扩展可能是个好选择,这样可以提高它的CPU处理能力,从而提高整体的tps。

27 支付业务场景(百度acg百度网盘一面)

27.1 但你这到下单这一步已经是异步了,用户怎么知道是否可以支付呢?怎么立即跳转到支付页面呢(重点)

答:

您提到的问题确实是在设计异步下单系统时需要考虑的关键点。即使下单操作是异步的,用户仍然需要得到一个明确的反馈来知道他们的下单请求是否被接受,以及是否可以继续支付。以下是一种可能的解决方案:

  1. 预下单响应:当用户点击下单按钮后,系统立即接收请求并将其放入消息队列中进行异步处理。同时,系统可以立即返回一个预下单的响应给用户,这个响应可能包含一个订单ID或一个跟踪号,以及一个提示信息,例如“您的订单正在处理中,请稍候”。

  2. 轮询或WebSocket(重点):在用户端,可以使用轮询或WebSocket等技术来定期查询订单的状态。当订单处理完成(无论成功还是失败),系统会更新订单状态。用户端在检测到订单状态变化后,可以根据新的状态给用户提供相应的反馈。例如,如果订单处理成功,可以引导用户到支付页面;如果订单处理失败,可以给用户显示一个错误消息。

  3. 通知机制:除了轮询或WebSocket,系统还可以使用其他通知机制,如推送通知,来告知用户订单的状态变化。

  4. 超时处理:为了避免用户长时间等待,可以设置一个合理的超时时间。如果在这个时间内订单仍然没有被处理,可以认为订单处理失败,并给用户显示一个超时错误消息。

  5. 优化异步处理速度:虽然下单操作是异步的,但仍然应该尽量优化其处理速度,确保大多数订单可以在很短的时间内被处理。这样,用户在等待订单处理的时间会尽量缩短,提高用户体验。

通过上述方法,即使下单操作是异步的,用户仍然可以得到明确的反馈,并在适当的时候被引导到支付页面。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《java面经-百度准入职老哥整理.pdf》是一份关于百度准入职面试的Java面经整理。这份面经是由百度准入职的老哥整理而成,其中记录了一些面试时可能会遇到的问题以及解答方法。 这份面经对于准备参加百度准入职面试的人来说非常有价值。首先,它列出了一些常见的面试问题,涵盖了Java语言的各个方面,包括基础知识、数据结构与算法、设计模式、多线程、网络编程等等。通过仔细研究和复习这些问题的答案,可以帮助面试者全面了解Java语言的特性和应用。 其次,这份面经还提供了问题的解答思路和方法,帮助面试者理清思路,正确回答问题。这对于很多面试者来说特别有帮助,因为在面试时有时会遇到一些棘手的问题,有了这份面经的指导,面试者可以更好地掌握应对策略。 不过需要注意的是,面经作为一份参考资料,不能完全依赖于它来准备面试。面试官可能会问一些不在面经中列出的问题,因此考生还是需要自己对Java语言有充分的了解,并能够熟练运用。同时,面试官还会关注考生的沟通能力、解决问题的能力以及对新技术的学习和掌握能力。 总体来说,《java面经-百度准入职老哥整理.pdf》是一份非常宝贵的资料,可以帮助面试者对Java面试中可能会遇到的问题有更深入的了解,提供了解答思路和方法。但记住,面试准备还需要多方面的知识积累和实践经验的积累,才能在面试中展现自己的优势。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值