场景题

如何设计一个订单号生成服务? #

  • 唯一性:订单号必须保持唯一,uuid,Snowflake等。
  • 数据量:在设计订单号的时候,需要充分的考虑到后续数据量变大的情况下该如何兼容。所以需要提前预留出足够的位数。
  • 可读性:订单号应该易于理解和记忆,可以根据业务需求自定义订单号的格式和组成方式,例如使用时间戳、随机数、用户ID等信息来构造订单号。
  • 基因法:订单系统到最后都可能会考虑分库分表,所以在最初设计订单号的时候,需要考虑将和分表有关的字段编码到订单号中,如买家ID等。
  • 可扩展性:订单号生成服务需要支持高并发、分布式部署和横向扩展等特性,可以采用分布式ID生成器、Redis等技术来实现。
  • 高性能:订单号生成服务需要具有高性能和低延迟的特点,可以使用内存缓存、异步处理等技术来优化性能。
  • 高可用:订单号生成服务需要保证高可用性,可以使用多节点部署、负载均衡、健康检查等技术来提高系统的可靠性和稳定性。

通过2位数字表示业务类型,如交易订单、支付单、结算单等都是不同的业务类型,可以有不同的编号。中间的18-20位用一个唯一的ID来表示,可以用雪花算法,也可以用Leaf,总之就是他需要保证唯一性。最后4位,基于基因法,将分表后的结果获取到,把他也编码到订单号中。

订单到期关闭如何实现? #

定时任务,最简单,最方便

redis过期监听不推荐,因为Redis官网上明确的说过,Redis并不保证Key在过期的时候就能被立即删除,更不保证这个消息能被立即发出。所以,消息延迟是必然存在的,随着数据量越大延迟越长,延迟个几分钟都是常事儿。

如何设计一个购物车功能? #

  • 未登陆购物车

    对于未登录的用户,其实他的购物车的信息没必要存储在后端,只需要在客户端做临时缓存就行了。客户端存储可以选择Cookie 和 LocalStorage等技术。

    在存储时,只需要设计一个JSON格式就可以了,因为用户没登录,所以也就不需要标识数据属于谁,那么只需要如下存储即可:

    {
        "cart": [
            {
                "SKUID": 10086,
                "timestamp": 1666513990,
                "count": 2
            },
            {
                "SKUID": 10010,
                "timestamp": 1666513990,
                "count": 10
            }
        ]
    }
    
  • 已登陆

    如果是使用数据库,那么就直接建表存储就行了,表中主要需要包含user_id、sku_id、count、time_stamp等几个业务字段就可以了。这样每一个加过购物车的用户都有一条记录。

    如果使用Redis来保存的话,其实也简单,只需要在上面的未登录用户的购物车的基础上增加一个user_id作为key就行了:

    {
        "KEY": 12343123,
        "VALUE": [
             {
                "SKUID": 10086,
                "timestamp": 1666513990,
                "count": 2
            },
            {
                "SKUID": 10010,
                "timestamp": 1666513990,
                "count": 10
            }
        ]
    }
    

如果你的业务量突然提升100倍QPS,怎么做? #

  • 正常情况

    最简单的就是扩容,增加集群的服务器数量,提升机器的硬件资源配置,让整体的吞吐量提升。

  • 被DDOS攻击了

    上一些降级、限流、熔断等等的机制

不用redis分布式锁,如何防止用户重复点击? #

  • 前端点击后置灰
  • 可以通过token的机制避免重复提交,当用户访问页面的时候,请求后端服务拿到一个token,然后下一次接口点击的时候把token带过来,服务端对token进行验证,验证该token是否被使用过,如果没有被使用过才可以进行点击。验证的逻辑可以放在数据库中,通过数据库的悲观锁或者乐观锁都可以实现。
  • 滑动窗口限流,滑动窗口限流是一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。我们可以限制一分钟或者一秒钟内用户只能发起一次请求来防止重复点击。
  • 可以使用布隆过滤器,他可以快速判断某个元素是否存在于集合中。可以在服务器端使用布隆过滤器记录某个操作是否已经被执行过,从而防止重复执行。
  • Redis其实也是一个集中式的存储服务,在特殊情况下,如果无法使用,一般的做法都是降级成直接使用数据库。

设计一个秒杀系统,你会考虑哪些因素? #

  • 高并发瞬时流量

    在整体架构设计上面,会做逐层的流量过滤 ,一次用户的秒杀请求会经过客户端、CDN、Nginx、Web应用、缓存、数据库等等。

    尽量在离用户更近的地方做流量的过滤,例如:前端随机丢弃,直接返回失败,或防止重复点击。

    在服务端接受请求之前,还会先经过Nginx做统一接入,Nginx不仅可以用来做负载均衡和流量的分发,其实他也是可以做流量的过滤的,这里面可以配置一些黑白名单、可以通过IP进行限流、也可以做一些业务校验都是可以的。

    服务器层面也是配置很多限流策略

    还有就是,服务器中有一些查询操作,和一部分写操作,其实是可以用缓存来抗一下的。在缓存上,本地缓存要比分布式缓存的性能更高,近端缓存要好于远端缓存。

  • 热点数据

    秒杀系统另外一个比较典型的特点就是会存在热点数据,因为大家都会抢购同一件商品,那么这个商品就会变成热点数据。

    解决方案主要就是拆分+缓存。

    首先说拆分,就是把一个热点的数据,拆分开,拆成没那么热的多个数据,在通过负载均衡让不同的请求分散到不同的数据上。

    还有就是用缓存,一般来说,秒杀是可以提前预知哪些数据会变成热点的,所以可以提前做一些缓存的预热,对于热点数据,不仅需要在Redis中做预热,还需要在本地缓存也做预热,避免Redis的热key问题。

  • 数据量大

    秒杀系统会有高频的下单,那么就会导致最终数据量也会很多,那么最终产生的订单量可能就会很大。

    数据量一大,就会带来查询效率低的问题。

    这时候就可以考虑**要么就加缓存、要么就用ES、要么就做分库分表。**还有就是做数据归档,把历史数据归档掉,无非就是这么几个方案了。

  • 库存的正确扣减

  • 黄牛抢购

    一般都是需要借助算法模型,根据用户的IP、设备信息、网络信息、行为数据等进行分析。

    这部分用户的ID直接可以加入黑名单中,然后黑名单可以在Nginx中,以及业务系统中都可以做过滤,如果发现用户在黑名单中,就直接拒绝请求。

    除了用户ID以外,还需要对他的IP地址、设备等进行限流,比如限制某个IP一段时间内只能下单几次,基于令牌桶、漏桶等限流算法都能实现。

    还有一种防刷,是防止别人直接通过脚本的方式直接调我们的接口,这种的话,我们可以借助token的方式实现防刷。

    当用户访问页面时,发放一个token,请求过来时,需要把token带过来,这时候我们做校验,token合法则接受请求,并且让token失效。token不合法或者已失效则直接拒绝请求即可。

  • 重复下单

    首先,基于我们前面提到的token,是可以做重复下单的检测的,也就是说如果用户在一个页面上,没刷新页面的话,token是一样的,那么我们基于token做重复下单检测就行了。这样可以避免重复下单。

    另外,秒杀业务中,其实通常都是限购的,所以我们可以结合业务场景,判断用户是否已有在途订单,通过限购方式避免重复下单。

    那么,在以上过程中,都可能出现token检测、以及订单重复性判断时因为并发导致重复的,那么就需要引入锁机制来保证下单操作的幂等。

  • 对普通交易的影响

    秒杀一般是电商网站中的一个功能,大多数情况下是和其他的业务在一起部署的,不仅是服务器,还包括缓存、数据库这些。

    隔离有两种,逻辑隔离和物理隔离。想要彻底的话,那肯定是物理隔离。

    所谓物理隔离,就是前后端服务、包括数据存储都彻底分开。

    或者应用上面做物理隔离,数据上做逻辑隔离。数据的逻辑隔离就是在订单、商品上面打标,标记出来是秒杀订单,方便后续查询及数据分析等。

  • 业务手段

    通过提前预约、预售等功能减少高并发流量

    通过验证码、问题等减少瞬时流量。

如果让你实现消息队列,会考虑哪些问题? #

基础架构和功能 #

  • 生产者、消费者、Broker:生产者负责发送消息,消费者负责接收消息,Broker作为服务端,处理消息的存储、备份、删除和消费关系维护。
  • 主题和分区:主题(Topic)是消息分类的标识,而分区是主题的物理分割,有助于提高消息队列的吞吐量。

基本功能 #

  • 消息存储方式:消息队列需要将消息存储在某种媒介中,一般采用内存或者磁盘存储。在内存存储的情况下,可以快速的读写消息,但是可能会丢失消息,因为内存中的消息没有持久化。而采用磁盘存储,可以持久化消息,但是读写速度相对慢一些。
  • 消息传递协议:消息队列需要定义消息传递的协议,包括消息格式、消息队列的地址等信息。我们可以使用成熟的RPC框架(如Dubbo或Thrift)实现生产者和消费者与Broker之间的通信。
  • **消息的持久化和确认机制:**在消息队列中,需要实现消息的持久化和确认机制,确保消息不会丢失或重复消费。一般的做法是将消息存储在磁盘中,并在消费者确认消费完成后再删除消息。
  • 消息的分发方式:消息队列需要实现消息的分发方式,包括点对点和广播两种方式。在点对点方式下,每个消费者只会接收到自己订阅的消息;在广播方式下,每个消费者都会接收到所有的消息。
  • 消息的传递方式:在消息队列中,有多种消息的传递方式,如轮询、长连接,还有长轮询。一般都是支持推拉结合的方式。或者基于拉实现推的机制。

消息的可靠性保证 #

  • 消息队列的容错性和可用性:消息队列需要实现高可用和容错机制,以确保消息的可靠传输。一般的做法是采用主从复制、集群模式或者分布式架构来实现。

高性能设计 #

高性能这部分可以参考kafka,引入一些批量操作、顺序写入、零拷贝等技术。

功能扩展 #

  • 顺序消息
  • 延迟消息
  • 事务消息
  • 重复消费
  • 消息堆积

库存扣减如何避免超卖和少卖? #

最终就是要实现库存扣减过程中的原子性和有序性。

原子性:库存查询、库存判断以及库存扣减动作,作为一个原子操作,过程中不会被打断,也不会有其他线程执行。

有序性:多个并发操作需要排队执行。

数据库扣减 #

不建议

正常来说,MySQL的热点行更新最多也就抗200-300的并发更新,如果想要抗的更多,要么就是提升硬件水平,要么就是做一些技术改造

Redis扣减 #

我们可以基于Redis做库存扣减的,借助Redis的单线程执行的特性,再加上Lua脚本执行过程中的原子性保障,我们可以在Redis中通过Lua脚本进行库存扣减。

先从Redis中取出当前的剩余库存,然后判断是否足够扣减,如果足够的话,就进行扣减,否则就返回库存不足。

因为lua脚本在执行过程中,可以避免被打断,并且redis执行的过程也是单线程的,所以在脚本中进行判断,再扣减,这个过程是可以避免并发的。所以也就可以实现前面我们说的原子性+有序性了。

一致性保证 #

现实中都是数据库扣减和Redis扣减结合使用。

先在Redis中做扣减,利用Redis来抗高并发流量,然后再同步到数据库中,在数据库中做扣减并进行持久化存储,避免Redis挂了导致数据丢失。

一般的做法是,先在Redis中做扣减,然后发送一个MQ消息,消费者在接到消息之后做数据库中库存的真正扣减及业务逻辑操作。

但是,这个方案有个问题,就是可能导致少卖

少卖 #

假设,上面的流程中,第1步执行成功了,Redis中库存成功扣减了,但是后续第2步的消息没有发出去,或者后面的消费过程中消息丢了或者失败了等情况。

那么,想要解决这类问题呢,就需要引入一些对账的机制,做一些准实时的核对,针对这类情况及时发现,如果少卖很多的话,那么就需要再把这些库存加回去。(比如用zset在redis中添加流水记录,然后定时拉一段时间内的所有记录,和数据库比对,发现不一致,则进行补偿处理)

如何用Redis实现朋友圈点赞功能? #

首先我们需要分析下朋友圈点赞需要有哪些功能,首先记录某个朋友圈的点赞数量,并且支持点赞数数量的查看,支持点赞和取消点赞操作。并且支持查看哪些人点过赞,并且点赞的顺序是可以看得到的。

在数据结构上,我们可以采用ZSet来实现,KEY就是这个具体的朋友圈的ID,ZSET的value表示点赞用户的ID,score表示点赞时间的时间戳。这样可以方便地按照时间顺序查询点赞信息,并支持对点赞进行去重

  • 使用字符串存储每篇朋友圈的ID,作为有序集合的KEY。
  • 使用zset存储每篇朋友圈的点赞用户信息,其中value为点赞用户的ID,score为点赞时间的时间戳。
  • 点赞操作:将用户的ID添加到zset中,score为当前时间戳。如果用户已经点过赞,则更新其点赞时间戳。
  • 取消点赞操作:将用户的ID从有序集合中删除。
  • 查询点赞信息:使用有序集合的ZREVRANGEBYSCORE命令,按照score(即时间戳)逆序返回zset的value,即为点赞用户的ID。

Redis的zset实现排行榜,实现分数相同按照时间顺序排序,怎么做? #

zset可以实现,将每个用户的得分作为zset中元素的score,将用户ID作为元素的value。使用zset提供的排序功能,可以按照分数从高到低排序,但是如果分数相同,按照默认的排序规则会按照value值排序,而不是按照时间顺序排序。

为了实现分数相同按照时间顺序排序,我们可以将分数score设置为一个浮点数,其中整数部分为得分,小数部分为时间戳,如下所示:

score = 分数 + 1-时间戳/1e13

因为时间戳是这种形式1708746590000 ,共有13位,而1e13是10000000000000,即1后面13个0,所以用时间戳/1e13就能得到一个小数

假设现在的时间戳是1680417299000,除以1e13得到0.1680417299000,再加上一个固定的分数(比如10),那么最终的分数就是10.1680417299000,可以将它作为zset中某个成员的分数,用来排序。

如何实现“查找附近的人”功能? #

实现"查找附近的人"功能,可以利用Redis的Geospatial数据类型,结合用户经纬度信息进行存储和查询。基于zset,有序集合实现,并通过一系列专门的命令(以 GEO开头的命令)来存储和操作地理空间数据。

使用Redis的GEOADD命令将用户经纬度信息存储在一个指定的键值中,然后再使用Redis的GEORADIUS命令可以查询指定经纬度附近一定范围内的用户信息就能简单实现这个功能了。

GEOADD user_location 121.57465 25.04100 user1

GEORADIUS user_location 121.57465 25.04100 1000 km

消息队列使用拉模式好还是推模式好?为什么? #

推和拉是两种消息传递的方式

推的模式就消费者端和消息中间件建立TCP长链接或者注册一个回调,当服务端数据发生变化,立即通过这个已经建立好的长连接(或者注册好的回调)将数据推送到客户端。

拉的模式就是消费者轮询,通过不断轮询的方式检查数据是否发生变化,变化的话就把数据拉回来。

如果使用拉的模式来实现消息队列的话,消费者可以完全自己掌控消息的数量及速度,这样可以大大的避免消息堆积的情况。但是,这种方案也有缺点,首先就是消费者需要不断的进行轮询,这种轮询也会对消息中间件造成一定的压力。

如果使用推的模式来实现,好处就是消息是实时的, 一旦有消息过来消费者马上就能感知到。而且对于消费者来说也比较简单,不需要轮询,只需要等推送就行了。但是缺点也比较明显,那就是如果消息的生产速度大于消费速度,可能会导致消息大量堆积在消费者端,会对消费者造成很大的压力,甚至可能把消费者压垮。

一般来说,推的模式适合实时性要求比较高的场景。而拉的模式适合实时性要求没那么高的场景。

还有需要注意的就是,在有些生产环境下,服务器环境只能单向通信,也就是只能通过一端访问另外一端,而不能在反方向通信,此时就需要消费方,使用拉模式,推模式是长链接,是双向通信,所以不行。

在很多中间件的实现上,可能并没有在直接用长连接或者轮询,而是把二者结合了一下,使用长轮询的方式进行拉消息的。

长轮询,就是消费者向消息中间件发起一个长轮询请求,消息中间件如果有消息就直接返回,如果没有也不会立即断开连接,而是等待一会,等待过程中如果有新消息到达,再把消息返回。如果超时还没有消息,那就结束了,等下次长轮询。

比如Kafka和RocketMQ都是支持基于长轮询进行拉取消息的。

如果让你实现一个RPC框架,会考虑用哪些技术解决哪些问题? #

  • RPC调用,需要通信吧,那就需要一个通信协议。
  • 通信的过程中,需要做参数的序列化吧,那就需要一个序列化协议。
  • RPC框架中需要有注册中心吧,服务提供者和调用者需要和注册中心交互吧,这就需要解决服务的注册和发现的问题吧。
  • 有了多个服务提供者之后,调用者在调用的时候,需要选择一个具体的服务调用吧,这时候是不是又需要负载均衡了。
  • RPC重要的是像调用本地服务一样调用远程服务,那么这就涉及到动态代理的技术实现了吧
  • 除了以上这些重要的核心功能以外,还可以考虑,比如缓存、比如服务降级、比如泛化调用、比如优雅上下线、比如服务的高可用、比如异步回调、比如服务治理之类的了。

一个订单,在11:00超时关闭,但在11:00也支付成功了,怎么办? #

假如,有一笔订单,在10:00下单成功,超时时间是1小时,那么在11点的时候,支付成功了,这时候该如何处理?

在"支付中"的一笔支付单 ,是有可能推进到支付成功的状态,并且也可能推进到已取消的状态的。

一般来说,正常的支付业务中,支付成功和已取消,都应该是终态,也就是状态机中的最终状态,终态是不能再变化的。 如果一个模型没有明确的终态,或者已经终态的终态数据状态还能随便变化,那么他的设计一定是不合理的。

一个支付单,多个渠道同时支付成功了怎么办? #

在电商场景中,创建了一个支付单之后,会通过支付工具进行支付,比如微信支付、支付宝、银行卡等,那么在特殊场景中,会出现,用户先通过微信支付尝试支付,因为网络延迟导致一直未支付成功,后面用户又再用支付宝支付成功了,可是过了一会,微信支付那面又回调通知支付成功了,这种该怎么处理呢?

好的做法是,在支付单中冗余一个支付渠道和渠道支付单号,在支付成功的时候,把支付渠道返回的渠道支付单号记录下来。

在接收到支付成功回调的时候,先检查状态,如果发现已经支付成功,则比对支付渠道和渠道单号是否一致,如果一致,则幂等成功。如果不一致,说明发生了重复支付,则执行退款流程。

海量数据,如何查询一条数据? #

如何解决重复消费和重复下单问题? #

对于服务的提供者来说,我们需要在接口中做好幂等控制,来避免因为重复而导致的脏数据。

比较常见的解决方式,就是通过消息中定义的一个幂等号,来做防重判断。这个幂等号一般是约定好的一个业务字段,如果没有这样一个字段的话,也可以用消息中间件的msg_id来做幂等控制,但是可能存在一个情况,那就是发送者重复发送了多次消息,这就会导致几次消息的msg_id不一样,但是消息内容一致。所以,一般都需要在消息中约定一个唯一的幂等字段或者业务字段。

一般用token

可以用Redis实现token验证,并且让一个token只能用一次。

这个逻辑存在一个问题,那就是如果高并发场景,可能会导致多个线程同时,导致token同时校验通过。要解决这个问题,有两个办法,第一个是把get和del放到一个事务中,或者用lua脚本,或者用分布式锁也可以。

但是,这个方案还存在一个问题,那就是如果用户提前先通过token获取的接口拿到一批token,然后再风控下单,也能绕过这个防重校验了。那么解决方案就是可以针对<同一个商品+同一个用户>维度来生成token,确保同用户在同商品下同时只有一个token。避免大量刷token的情况。

40亿个QQ号,限制1G内存,如何去重? #

问题分析

  • 40亿个QQ号 ≈ 4 × 10^9 个数字
  • QQ号范围:一般是5-11位数字,最大值约10^11
  • 1G内存 = 1024^3 ≈ 10^9 bytes

方案1:Bitmap位图法(最优解)

计算空间需求

QQ号最大以10^11(1000亿)计算:

  • 需要1000亿个bit = 10^11 bits
  • 转换为bytes:10^11 / 8 = 12.5 × 10^9 bytes ≈ 12.5GB

12.5GB > 1GB,直接Bitmap不可行

改进方案:分段Bitmap + 多趟扫描

// ByteBitmap 基于byte的实现
type ByteBitmap struct {
    bits []byte //每个byte可以存储8个bit
    size uint64
}

func NewByteBitmap(size uint64) *ByteBitmap {
    return &ByteBitmap{
        bits: make([]byte, (size+7)/8),
        size: size,
    }
}
(size+7)/8:计算需要多少个byte来存储size个bit
例如:size=10 → (10+7)/8=17/8=2.125 → 取整为2个byte(可以存储16位)
这种计算方式确保有足够的byte存储所有位
func (b *ByteBitmap) SetBit(pos uint64, value bool) {
    if pos >= b.size {
        return
    }
    index := pos / 8  //计算在哪个byte
    offset := pos % 8  // 计算在byte中的哪一位
    
    if value {
        b.bits[index] |= 1 << offset // 设置位为1
    } else {
        b.bits[index] &^= 1 << offset// 清除位(设为0)
    }
}

func (b *ByteBitmap) GetBit(pos uint64) bool {
    if pos >= b.size {
        return false
    }
    index := pos / 8
    offset := pos % 8
    return (b.bits[index] & (1 << offset)) != 0
}
位操作解释:
1 << offset:创建一个掩码,只有offset位为1
offset=3 → 00001000
|=:按位或运算,设置特定位为1
原有byte: 00100000
掩码:    00001000
结果:    00101000
&^=:按位清除运算(AND NOT)
原有byte: 00101000
掩码:    00001000
结果:    00100000

说一说多级缓存是如何应用的? #

一个真实的秒杀业务,从前到后,可能涉及到5层缓存。

  • 客户端缓存

    秒杀场景中,比较典型的客户端缓存的例子就是秒杀页面的倒计时。

  • CDN缓存

    秒杀活动中,有很多商品相关的信息,比如商品的图片信息,这些前端的静态资源其实变化的频率并不是很高,那么其实就可以通过CDN来做缓存。

    CDN (Content Delivety Network)内容分发网络,它主要是通过将内容存储在全球各地的边缘节点上,以就近原则向用户提供内容。

    CDN可以做缓存是因为它在全球范围内部署了多个边缘节点,这些节点分布在不同的地理位置,靠近用户所在的区域。当用户请求某个资源(例如网页、图片、视频等),CDN会根据用户的位置,将资源从最近的边缘节点提供给用户。

  • Nginx缓存

    比如一些静态资源,除了放到CDN中,Nginx中也可以缓存一份,当没有CDN的时候,可以优先到Nginx中获取。

    在秒杀场景中,我们需要做一些用户的鉴权,比如用户是否登录,是否是黄牛用户,用户的IP是否被封禁。这些信息一般是放在Nginx中的,可以在这里面做一些前置的校验,把一些非法的请求就直接都给拒绝掉了。

  • Web APP缓存

    应用当中也会经常使用本地缓存来提升查询效率。但是本地缓存我们都知道,可能存在不一致的问题,所以这里面存储的信息一般是变化没那么频繁的。

    比如一次秒杀活动的开始和结束时间、比如秒杀的用户的用户等级这些信息,基本都是变化没那么频繁的,就可以放到本地缓存中,可以快速的读取。

  • 分布式缓存

    有些数据因为要考虑他的一致性,所以没办法放到本地缓存中,比如商品库存,这种就需要放到分布式缓存中,比如Redis。

    在秒杀场景中,我们经常使用Redis来做库存扣减,其实这就是利用了Redis来做缓存的一种具体案例。

从B+树的角度分析为什么单表2000万要考虑分表?? #

对于B+树的高度限制,一般建议将B+树的高度控制在3到4层以内,以获得更快的查询性能。

我们已知一个根节点的存储量是16KB,并且他作为非叶子节点,他只需要存储一个主键+一个指针就行了。假设是一个bigint类型的主键(8字节),和默认6字节的指针。那么可以存储:16 * 1024 / (8+6) ≈ 1170根节点可以扩展出1170个二层高度的子节点,而三层的B+树则会有两层非叶子节点。那么最终就能关联出 1170 * 1170 = 1,368,900个叶子节点。

已知一个叶子节点有16KB,那么它能存储多少数据量就取决于单行数据的大小了。假设单行数据量1KB,那么他就能存储16条,假设单条数据量500 B 那么他就能存储32条。

假设单条数据的存储空间是1KB,那么3层高度的B+树最终的可存储数据量为:1170 * 1170 * 16 = 21,902,400,即2000万

InnoDB为什么不用跳表,Redis为什么不用B+树? #

跳表是一种概率性的有序数据结构,它通过构建多级索引来实现快速查找,可以看作是在单链表基础上增加了多级索引的"多维链表"。

MySQL是基于磁盘存储的,Redis是基于内存存储的。而B+树是一种磁盘IO友好型的数据结构,跳表则是一种内存友好型的数据结构。

B+树的叶子节点形成有序链表,可以方便地进行范围查询操作。对于磁盘存储来说,顺序读取的效率要高于随机读取,因为它可以充分利用磁盘预读和缓存机制,减少磁盘 I/O 的次数。

其次,由于B+树的节点大小是固定的,因此可以很好地利用磁盘预读特性,一次性读取多个节点到内存中,这样可以减少IO操作次数,提高查询效率。

还有就是,B+树的叶子节点都存储数据,而非数据和指针混合,所以叶子节点的大小是固定的,而且节点的大小一般都会设置为一页的大小,这就使得节点分裂和合并时,IO操作很少,只需读取和写入一页。

因为Redis是一种基于内存的数据结构,他不需要考虑磁盘IO的性能问题,所以,他完全可以选择一个简单的数据结构,并且性能也能接受的 ,那么跳表就很合适

线上接口如果响应很慢如何去排查定位问题呢? #

  • 先通过各种监控系统,发现慢的现象或者问题

  • 安装arthas

  • 运行命令

    sh as.sh
    
  • 查看接口耗时

  • 分析耗时

  • 耗时优化

MySQL千万级大表如何做数据清理? #

错误做法 #

通过创建时间直接删

若删的字段没有索引会进行全表扫描

正确做法 #

  • 获取主键最大值和最小值
  • 在主键之间,分批进行处理

高并发的库存系统,在数据库扣减库存怎么实现? #

排队 #

在数据库层面做改造。

当开启热点更新自动探测时,系统会自动探测是否有单行的热点更新,如果有,则会让大量的并发update排队执行,以减少大量行锁造成的并发性能下降。

拆分 #

把大库拆分成多个小库存,拆分后,一次扣减动作就可以分散到不同的库和表中进行。

缺点:

  • 存在碎片
  • 没办法一次扣很多个,比如一共1000,扣999个

合并 #

把多个update请求合并成一个,这样数据库只需要做一次更新就行。

其它 #

  • 异步扣除
  • 把update改成insert,只需要统计数量就行

MySQL热点数据更新会带来哪些问题? #

  • 锁竞争
  • 占用数据库连接
  • 耗尽数据库CPU
  • 死锁风险高
  • 索引维护开销大
  • 主从不一致

MySQL里有2000w数据,Redis中只存20w,如何保证Redis中的数据都是热点数据? #

  • 数据预热
  • 热点数据更新(监测热点数据)
  • 缓存过期策略(最近最少使用,最近不经常使用)
  • 缓存淘汰策略(过期时间设置)

在100M内存下存储一亿个整数,其范围在1到2亿,如何快速判断给定到一个整数值是否存在? #

内存不够

使用bitmap

因为范围是1-2亿

优化思路 #

尝试对数据集中的整数进行压缩,比如将32位整数拆分成高16位和低16位,将高16位相同的数字放在一起(一个container),低16位形成一个bitmap(本质上也是个bit数组可称作bucket)。可以简单理解成一个hashmap下挂了bitmap,这样对32位整形进一步压缩。对于稀疏的数据集,大幅度降低内存占用。

电商下单场景,如何设计一个数据一致性方案? #

分布式事务、事务消息

如果表单数据量大, 只能考虑分库分表吗? #

  • 先考虑数据库优化
  • 要不要上缓存
  • 数据归档
  • 分布式数据库

如何实现缓存预热? #

  • 启动时预热
  • 定时加载
  • 用时加载
  • 缓存加载器(一些缓存框架提供)

如何实现百万级数据从Excel导入到数据库? #

  • 逐行并发读取(防止内存溢出)
  • 批处理插入(减少数据库压力)
  • 解决错误问题(兜底)

应用占用内存持续增长,但是堆内存、元空间都没变化,可能是什么原因? #

嫌疑方向 说明 排查手段
CGO 内存泄漏 Go 通过 CGO 调用 C 代码,C 分配的内存不在 Go runtime 管理范围内。Go 的 GC 不知道这块内存的存在。 valgrindAddressSanitizer (ASan)mprof 追踪 native 分配
Goroutine 泄漏 大量 goroutine 阻塞未退出(如 channel 死锁、HTTP body 未关闭)。每个 goroutine 初始栈 2KB~8KB,百万级 goroutine 可占数 GB。 runtime.NumGoroutine()、pprof /debug/pprof/goroutine
mmap / os.File 未关闭 直接 syscall 映射的文件或匿名内存,Go heap profile 看不到。 lsof -p <pid>/proc/<pid>/maps、strace
Go Runtime 内部碎片 Go 的 mcache/mcentral/span 机制在某些版本存在碎片问题;或 tcmalloc/jemalloc 替换后的行为差异。 GODEBUG=gctrace=1go tool pprof -inuse_space vs RSS 对比
cgo 回调导致的栈膨胀 C 调用 Go 函数时,goroutine 栈可能需要扩容到很大,且不易缩回。 goroutine profile 查看栈大小分布

如何预估一个系统的QPS? #

从产品需求、用户量、运营活动等业务维度反推技术 QPS,适用于新系统上线前大促备战

1. 核心公式 #

QPS=总请求量/时间窗口(秒)×峰值系数

2. 常见推演模型 #

场景 推演方式 示例
日活驱动 DAU × 人均请求数 ÷ 有效时长(s) × 峰值倍率 DAU=100万, 人均50次, 集中在10h, 峰值3倍 → 100w×50÷36000×3 ≈ 4,167 QPS
秒杀/抢购 参与人数 × 人均点击次数 ÷ 抢购窗口(s) 10万人抢, 每人点5次, 1秒内 → 500,000 QPS
订单转化 GMV目标 ÷ 客单价 ÷ 转化率 ÷ 时间窗 需倒推到下单接口调用量
API 调用方 下游调用方数量 × 单调用方频率 微服务间依赖评估

3. 关键修正因子 #

  • 峰值系数:日常流量通常服从"双峰曲线"(午高峰+晚高峰),峰值一般是日均的 2~5 倍;大促可达 10~50 倍
  • 读写比:电商典型读写比 10:1 ~ 100:1,读 QPS 远高于写 QPS,需分开估算。
  • 漏斗衰减:首页 → 列表 → 详情 → 下单,每一层都有流失,不能简单用入口 QPS 代表全链路。
  • 二八法则:80% 的请求集中在 20% 的热点数据/接口上,热点接口需单独放大预估。

⚠️ 经验法则:业务推演的结果通常作为"上限参考",实际值往往因缓存命中、前端防抖、限流等因素低于理论值。务必留出 30%~50% 的安全余量。

如何做SQL调优:用了主键索引反而查询很慢? #

  • 你根本没用到主键,例如sql写法错误
  • 数据量太大
  • uuid主键
  • SELECT * 导致行数据太大,主键查询触发大量 IO
  • 主键索引无法缓存到内存,大量随机磁盘 IO

如果要存IP地址,用什么数据类型比较好? #

  • ipv4 字符串 32位整数
  • ipv6 字符串 二进制格式存储

一次RPC请求,客户端显示超时,但是服务端不超时,可能是什么原因? #

  • 网络延迟或丢包
  • 客户端服务端超时配置不一致
  • 客户端处理能力到瓶颈(次要)

为什么不建议使用MQ实现订单到期关闭? #

  • 大量无效消息,资源严重浪费(最核心)

    不能一个订单就一个延迟消息

  • 延迟消息能力有硬限制,不灵活

    例如RocketMQ延迟时间有等级固定,不能随意设置

  • 有风险 消息可能丢失

  • 精度与抖动问题:关单时间不准

  • 系统复杂,运维成本高

实用方案 #

  • 数据库定时任务
  • Redis zset:订单到期时间作为 score,定时拉取到期任务(轻量、高效)
  • 被动触发:用户查订单时,顺便检查是否超时并关闭

如何实现敏感词过滤?如何快速判断此文本有敏感词? #

AC 自动机(Aho–Corasick)

核心特点

  1. 一次扫描,全部匹配
  2. 支持前缀过滤、模糊匹配、跳过干扰字符
  3. 支持敏感词库热更新
  4. 内存占用极低

字典树 Trie(前缀树) #

  • 每个节点存单个字符,边代表字符走向
  • 公共前缀共享节点,压缩存储
  • 标记结束节点:代表走到当前位置,命中一个完整敏感词

失败指针 Fail(核心) #

类比 KMP 算法的部分匹配表

当前字符匹配失败时,不用回退文本指针,直接跳转到另一个最长可匹配后缀节点,继续匹配。

作用:保证文本只遍历一遍,整体时间复杂度 (O(\text{文本长度}))。

输出节点 #

节点附加标记:是否为敏感词结尾,以及对应敏感词。

使用分布式锁时,分布式锁加在事务外面还是里面,有什么区别? #

外面

如果加在里面,锁释放了,事务还没提交!

导致:

  1. 锁释放
  2. 其他线程立刻进来
  3. 读取到未提交的旧数据
  4. 超卖、超发、数据不一致

加分布式锁之后影响并发了怎么办? #

加锁的目地不是为了不让并发,而是解决一些系统性问题,我们加锁把一些重复性请求拦住了,让系统有更多的资源响应用户的请求,反而提升了并发。

为什么很多公司数据库不允许物理删除(delete) 数据 #

  • 数据留痕
  • 合规要求
  • 数据完整性
  • 性能
  • 没有内存碎片

为什么不用分布式锁来实现秒杀? #

没必要

我们要实现秒杀的库存扣减,最重要的是两个点:1、抗更高的并发。2、避免超卖 。尤其重要的就是这个防止超卖,这也是我们加锁的目的。

如果使用分布式锁,在不使用 lua 脚本的情况下,每次库存扣减操作都需要多次与 Redis 服务器通信(例如,加锁、读取库存、扣减库存、释放锁等)。这不仅增加了网络延时,还增加了系统的复杂性。

如何实现百万级排行榜功能? #

百万级排行榜 = Redis Sorted Set (ZSet) + 分桶 + 定时落库 + 预热缓存

为什么一定要做限流?不应该服务好客户吗?不应该是加机器吗? #

  • 客户第一
  • 突发流量 防止攻击
  • 自我保护
  • 保护别人 下游服务
  • 加机器

Redis 如果挂了,你怎么办? #

  • 限流&降级 不让它挂
  • 做好备份
  • 本地缓存

大型电商的订单系统,如何设计分库分表方案? #

水平分库 + 水平分表

分片键 = user_id(用户 ID)

分片算法 = 哈希取模 / 一致性哈希

分库:8~32 个库

分表:每个库 8~16 张表

总表数 = 库数 × 表数(64~512 张)

为什么用 user_id?

90% 订单查询都是按用户查订单,能保证 90% 请求都命中单库单表,性能最高。

分库分表时,每个城市的人口不一样,有的密集,有的稀疏,如何实现均匀分布? #

  • 换一个分表算法,比如出生日期
  • 人工干预分表算法

项目中,如果日志打印成为瓶颈,该如何优化? #

  • 控制日志输出量
  • 日志级别
  • 异步打印
  • 日志文件拆分

分布式系统,用户登录信息保存在服务器A上,服务器B如何获取到共享Session #

a,b 的联合索引,select b where a = xx,无法走索引覆盖什么原因? #

  • 索引中 a不在左边,不满足最左匹配原则
  • a的类型发生了转换

把商品加入购物车时断网了,该怎么在重新联网时同步? #

  • 本地客户端缓存
  • 重试机制

进入电梯里断网后又恢复刚开始为什么网络慢? #

TCP的拥塞控制

在网络断开并重新恢复连接后,TCP协议的拥塞控制机制会起作用。在网络断开的过程中,TCP的超时重传机制会导致其认为网络发生了拥塞,因此TCP发送窗口(Congestion Window,cwnd)会被缩小。

全国的酒店价格(千万级)需要在某个瞬间比如7点发生变动,怎样高性能准点去进行变更 #

  • 前提、预计算价格
  • 多版本价格 七点时,查询接口加上参数,获取另一个价格
  • 缓存失效

账户里面只有十块钱,同时发来两笔订单一共大于十块钱,怎么保证不超花? #

  • 分布式锁
  • 乐观锁先提交订单,再失败
  • Redis扣减