场景题

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

  • 唯一性:订单号必须保持唯一,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点的时候,支付成功了,这时候该如何处理?

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

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

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

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

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

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