如何设计一个高性能秒杀系统?
-
秒杀系统是一种用于处理短时间内大流量的高并发系统,需要支持高性能、一致性和可用性等要求,避免少卖和超卖。
-
动静分离:将客户端数据分为动态数据(库存)和静态数据(名称、描述和图片),静态数据做缓存处理,减轻服务器压力,并且提高客户端体验,动态数据才需要作一致性保证,将请求落到服务器上。这里要注意一点,静态数据只是相对静态,并不是一直都不会变的数据,因此对静态数据的缓存需要做到秒级失效的保证。
-
CDN缓存:静态数据可以缓存到二级CDN中(二级缓存的存储容量更大,访问量更集中),直接使用URL(需要做唯一化处理)作为key,缓存整个HTTP响应,无需解析HTTP请求头。如果将数据缓存到客户端,则难以控制失效时间(需要用户主动刷新)。反之,如果缓存到服务器,则会加大服务器负载。
-
动态数据的加载处理方案:
- ESI 方案:在CDN节点或Web反向代理服务器上请求动态数据,并将动态数据插入到静态页面中,用户看到页面时已经是一个完整的页面。这种方式对服务端性能要求高,但用户体验较好。
- CSI 方案:CDN节点或Web反向代理服务器上只返回静态页面,前端单独发起一个异步 JS 请求动态数据。这种方式对服务端性能友好,但用户可能先看到不完整的页面,直到动态内容被加载出来,对用户体验稍差。
-
热点识别:
- 对一些热点操作,如零点刷新、零点下单、零点添加购物车作限制保护,比如用户频繁刷新页面时进行提示阻断。
- 提前识别静态热点数据,如问卷调查,对商品访问量进行提前计算并筛选。
- 在服务过程中异步地进行动态热点数据识别,如采集交易产生时各个环节的热点Key信息,聚合分析热点数据,通过订阅分发推送到各个中间件,然后它们根据自身需求决定如何处理热点数据。
-
热点隔离:将热点数据存储到单独的数据库或缓存集群,不要让 1% 影响到另外的 99%。
-
库存一致性:
- 下单减库存的优点是用户体验好,但缺点是容易引起恶意下单,然后导致少卖
- 付款减库存的优点是可以避免恶意下单,但用户体验差,容易引起下单完无法成功付款的现象。
- 预减库存:买家下单后,库存为其保留一定的时间(如 15 分钟),超过这段时间,库存自动释放,释放后其他买家可以购买。结合了以上两种方法的优点。
-
少卖和超卖的解决方法:
- 少卖:可以结合安全和反作弊措施来制止,一人最多只能买 N 件商品,或者是验证码和黑名单。
- 超卖:设置数据库字段不能为负,比如设置数据库字段为无符号整数,或者是通过事务来检查。有些场景下,超卖可能不是个很严重的问题,可以通过补货来解决。
-
高并发读:读数据时可以事先进行不影响性能的分层校验,如请求是否有效,商品状态是否正常等,提前过滤一些无效请求。此外,还可以允许用户读取脏数据,等到真正写数据时再保证最终一致性。
-
高并发写:写数据时也可以进行不影响性能的分层校验,提前过滤一些无效请求。如果减库存逻辑非常单一的话,可以将库存直接放到缓存中。如果有比较复杂的减库存逻辑,比如有总库存和具体库存,则必须要到数据库中完成减库存操作,此时可以使用消息队列等中间件来提高写性能。
-
流量削峰:使用答题、验证码等方式来防止作弊,并将流量均匀分布到一定时长,减少瞬时的高并发压力。另外,由于请求具有先后顺序,后面的请求到来时可能已经没有库存了,因此根本无法下单,此阶段落到数据层真正的写也就非常有限了。
-
预扣库存的回补:
- 定时任务扫描超时订单,比如每隔1分钟扫描数据库中的超时订单,批量回补库存。优点简单直接,缺点是延迟高,数据库压力过大。
- 下单时同时设置redis过期键,通过redis监听过期键状态,触发回补。优点是精准,数据库压力较小。缺点是需要保证redis和数据库的连接稳定性和一致性。
- 下单时发送延迟消息到消息队列中,消费者收到消息后检查订单状态,未支付则回补。优点是支持大规模并发,缺点是消息队列本身需要支持延迟消息,比如Kafka。
-
第三方支付的状态一致性保证:下单时先预扣库存,用户完成支付后,第三方支付会回调系统(需要保证幂等性),系统再真正扣减库存,并修改订单状态为已支付。如果支付失败或超时,系统自动把冻结的库存释放回可用库存,不影响其他用户购买导致少卖。此外,还需要设置一个对账系统,每天负责定时对比支付系统的交易记录和库存系统的扣减记录,进行兜底。
-
系统优化:
- 减少序列化:减少序列化操作可以很好的提升系统性能。序列化大部分是在 RPC 阶段发生,因此应该尽量减少 RPC 调用,一种可行的方案是将多个关联性较强的应用进行 “合并部署”,从而减少不同应用之间的 RPC 调用。
- 裁剪日志异常堆栈:无论是外部系统异常还是应用本身异常,都会有堆栈打出,超大流量下,频繁的输出完整堆栈,只会加剧系统当前负载。可以通过日志配置文件控制异常堆栈输出的深度。
- 去组件框架:极致优化要求下,可以去掉一些组件框架,比如去掉传统的 MVC 框架,让客户端直接与服务对接,绕过一大堆复杂且用处不大的处理逻辑,节省时间。
-
检查和兜底:定期检查应用基线来进行优化。比如性能和成本等等。为了保证系统的高可用,必须设计一个备选方案来进行兜底。
如何设计一个高性能的分布式日志系统?
- 轻量级日志收集客户端:
- 提供Go/Java/Python/C++等主流语言的SDK,核心部分用C++实现以获得最佳性能。
- 采⽤线程局部存储、双缓冲和动态扩容/缩容机制来优化内存管理,并设计智能采样算法,根据当前负载。
- 使⽤Protobuf来确保⾼性能⽹络传输,并结合令牌桶算法限制⽇志发送速率。
- 高性能消息队列缓冲层:配置Kafka集群,将数据分散到不同分区,多个生产者可并行写入,多个消费者可并行读取。
- 日志实时处理与分析层:自动识别IP、URL、时间戳等信息,并对敏感数据进行脱敏处理,如手机号和身份证。
- 日志高可用存储层:3天内的热数据会被发送到NVMe存储,7天内的温数据会被发送到SSD存储,30天内的冷数据会被发送到HDD存储。
- 日志查询与可视化层:该层主要面向开发者,需要进行一些查询功能的优化,比如自动补全和查询缓存等,尽可能对开发者友好。
如何在海量数据中筛选出数据?
如果是找出最大/最小的几个数,则考虑用堆解决,比如从中数十亿的数据中筛选出最大的K个数据:
- 先建立一个小顶堆,并从原数据集中放入K个数。
- 逐个添加原数据集中的剩余元素,如果当前元素小于堆顶则跳过,大于堆顶则加入堆,并弹出堆顶元素。
- 遍历完成后,堆中剩余的K个数就是最大的K个数。
- 在内存受限的情况下,可以使用将原数据集划分为n个子数据集,然后用不同线程去建堆筛选,最后由一个线程将结果合并。
- 如果原数据集的重复元素过多,可以使用hash法或位示图法去重后再进行筛选。
如何对海量数据进行排序?
- 当数据量远大于可用内存时,采用外部排序算法,将数据分割成子数据集装入内存中,使用高效的排序算法排序,然后将结果写入临时文件,最后再归并所有临时文件,完成排序。
- 当数据量可以完全写入内存时,就需要考虑排序算法的使用,通常使用快速排序即可。如果需要确保稳定性,则使用归并排序。如果需要节省空间,则使用堆排序进行原地排序。如果数据的范围。
如何对海量数据进行统计?
在实际业务场景中,我们常常需要对海量数据进行元信息统计,比如显示用户某个月的签到次数和首次签到实际,或是统计7天内连续签到的用户总数。对于这类只有两种状态(有签到和没签到)的海量数据,我们可以使用位图来进行统计:
- 判断用户是否在线:创建一个表示用户登陆状态的位图,用户ID作为下标,在线值就设置为1,下线值就设置为0。
- 判断用户的签到情况:针对单个用户创建一个表示签到情况的位图,这样一年的签到也仅需365个bit位。而且Redis也提供了可以快速检索位图中第一个不为0的命令,这样就可以快速获取用户的首次打卡时间。
- 判断用户的连续签到情况:创建表示用户是否打卡的位图,用户ID作为下标,如果打卡则设置为1,没有打卡则设置为0,并且该位图要创建多张,每张表示不同的打卡日期。假设要统计7天连续打卡情况,则将7个位图进行与操作,然后在其中检索哪些位为1就可以得知连续签到情况了。
如何制作一个排行榜?
- 如果只是简单的排行榜,可以使用redis中的zset(有序集合)去实现,将得分设置为score,用户ID设置为value,利用zset的排序功能,可以按照分数从高到低排序。
- 如果分数相同,按照默认的排序规则会按照value值排序,但目前希望按照时间顺序排序,就是分数相同的情况下先上榜的排在前面。一个可行的方案是将分数score设置为一个浮点数,其中整数部分为得分,小数部分为时间戳,即
score= 分数 + 1 - 时间戳/1e13
。此时在分数相同的情况下,时间早的时间戳小,除以一个很大的数后得到的数字就小,被1减去以后得到的差就会大,也就实现了分数相同的情况下先上榜的排在前面。 - 大体上排行榜由两部分组成,排序和信息汇总,排序可以让用户看到当前排行靠前的人有谁,而信息汇总则是点击某个用户的头像可以看到具体的用户信息。对于并发量不大的情况,可以直接对接数据库,如果是并发量大的情况,则考虑使用缓存、消息队列等机制。
如何设计一个预约系统?
预约系统的核心目标是允许用户预约特定的时间段,并确保这些时间段不会与其他预约发生冲突。换句话说,系统需要管理一组时间资源,并确保在任何给定的时间段内,同一资源不会被多次预约。
- 资源定义:明确哪些资源可以被预约,例如,会议室、医生、服务人员等,然后创建一个数据库表描述这些资源。
- 时间粒度:确定预约的最小时间单位(如15分钟、30分钟、1小时等),然后创建一个数据库表描述具体的预约,比如用户预约了哪个资源(对应资源表中的主键),预约开始时间和结束时间,以及预约状态(是否预约)。
- 冲突检测:在创建或更新预约时,需要检查该资源在请求的时间段内是否已有其他预约。
- 用户界面:用户可以查看可用时间并进行预约。
- 并发控制:对于如何处理多个用户同时尝试预约同一时间段的情况,可以使用悲观锁和乐观锁去解决。对于高并发来说,同样也是可以使用缓存和消息队列去解决。
- 高级功能:对于一些高级功能,比如通知系统,则可以使用合适的微服务去解决。
如何去计算一篇文章的热度?
- 影响因素:对于文章热度来说,有很多需要考虑的因素,如浏览量、评论数、转发量、点赞数、收藏数、阅读时长。
- 权重分配:不同平台对热度的定义可能不同。例如,新闻平台可能更看重转发和评论,而博客平台可能更看重阅读时长和收藏。
- 归一化处理:不同因素的数值范围可能差异很大。例如,浏览量可能是数千,而评论数可能是几十。为了公平比较,需要进行归一化。
- 时间衰减:热度通常会随时间下降。可以引入时间衰减因子。
- 动态调整:不同时期用户可能更倾向于评论而非转发(如节日话题易引发讨论)、平台可能阶段性鼓励评论(如推出"优质评论奖励"活动)、以及发现刷量行为时自动降低对应指标的权重。这些原因导致了文章热度的权重分配需要进行动态调整,这种调整通常基于时间或基于业务等。
如何设计一个评论系统?
- 评论对于文章来说是非常常见的一种系统,通常需要支持快速查看某篇文章的评论数、支持楼中楼回复、查看某个评论有多少条回复,不一次性加载所有评论等特性。
- 为了满足这些要求,可以设计三个表:文章),评论索引表和评论内容表。
- 文章表包含文章ID、标题和文章的根评论数
- 评论索引表包含评论ID、所属文章ID、父评论ID(顶级评论为0)、用户ID、回复数量和点赞数量。
- 评论内容表存储评论ID(关联评论索引表的评论ID的外键)和评论内容。
- 这种设计方式通过将评论内容和评论索引分开存储,提高了查询性能,同时也便于数据维护,并且通过文章表记录文章的根评论数,方便快速统计。并且可以设计触发器用于在更改评论时的多表级联更新。
- 添加评论:
- 前端发送评论到服务端。
- 服务端收到评论消息之后,将之发给MQ去处理,然后直接返回评论成功的结果。
- 消费者从MQ上消费消息,向数据添加评论。
- 添加完成后,将添加成功的消息通过websocket推送给客户端。也就是说实际添加评论到数据库这一动作是异步的,用于应对高并发。
- 查询评论:
- 添加缓存
- 主从复制和读写分离(评论系统倾向于读多写少)
如何实现客户端在登陆后30分钟内没有任何操作,就立即强制下线只能重新登陆的功能?
- 登录时,前端从后端获取一个jwt放在cookie中,jwt包括用户的实际token(该token也会保存到后端,可以用数据库保存),过期时间为40分钟。
- 前端无操作时启动一个30分钟的定时器,有操作时取消,定时器到期自动发登出请求。
- 后端在收到登出请求后,需要将后端所保存的对应token删除。
- 在登录状态下,前端每20分钟使用这个token向后端交换新的jwt,新的jwt保存了新的token,过期时间也还是40分钟,以此防止token一直可用。
- 后端对所有的请求,都走一遍校验jwt的流程,jwt合法的话,再检查token是否存在。
有 m 匹马,n 条跑道(每次最多 n
匹马比赛),如何用最少的比赛次数找出第 k 快的马?
- 分组比赛:
- 把
m
匹马分成⌈m/n⌉
组,每组最多n
匹。 - 每组比赛一次,记录每组的排名(如
A1 > A2 > ... > An
)。 - 比赛次数:
⌈m/n⌉
场。
- 把
- 冠军赛:
- 让每组的第一名(
A1, B1, C1, ...
)再比一场,排出所有冠军的排名(如A1 > B1 > C1 > ...
)。 - 比赛次数:
+1
场。
- 让每组的第一名(
- 筛选候选马:
- 根据冠军赛结果,可以排除一些马:
- 比如
A1
是最快的,B1
可能是第二快,C1
可能是第三快,等等。 - 第
k
快的马只可能来自:- 冠军赛前
k
名的马(A1, B1, ..., K1
)。 - 以及它们所在组的前
k - i
名的马(i
是该冠军的排名)。
- 冠军赛前
- 比如
- 例子(
k=3
):- 候选马:
A2, A3, B1, B2, C1
(因为A1
最快,B1
可能第二快,C1
可能第三快,A2
可能比B1
慢但比C1
快,等等)。
- 候选马:
- 根据冠军赛结果,可以排除一些马:
- 决赛:
- 让候选马再比一场,直接选出第
k
快的。 - 比赛次数:
+1
场。
- 让候选马再比一场,直接选出第
业务中生成的日志如何实现可靠地落盘?如果断电了怎么办?
- 策略1:每条日志都写入并使用flush持久化磁盘,性能最慢,安全性最高。
- 策略2:每 100 条日志 flush 一次,比较平衡。如果断电,最后一批 buffer 中的数据会丢失。
- 策略3:使用WAL(Write-Ahead Logging)机制,先写日志到一个 append-only 文件然后立即flush,再处理主流程。类似于MySQL的binlog。
客户端发送到服务端的数据没有到达,有哪些可能的原因,如何排查?
- 网络通信失败,可能是因为防火墙策略、端口没有设置好、DNS解析失败,网络拥塞等问题造成的。其排查方法有如下几种方式:
- 客户端
ping
或telnet
测试连接 - 使用
curl
模拟客户端请求,确认可以接通 - 检测客户端和服务端的防火墙策略和端口设置
- 客户端
- 客户端问题,查看日志是否报错,确认日志系统正常运行。
- 服务端问题,查看日志是否报错,查看服务端运行情况是否正常,资源有没有被耗尽。
- 整体排查顺序:确认客户端有发请求、确认客户端到服务端的网络畅通、查看服务端日志、确认数据格式正确、开启双端debug日志、重放请求并开启抓包。
如何解决2个鸡蛋扔楼问题?
你有两个鸡蛋,假设在第N层扔下,鸡蛋没碎,在第N+1层扔下,鸡蛋碎了,则鸡蛋的硬度标记为N
现在有一栋楼100层高,请问你最少多少次能够测出鸡蛋的硬度?
- 最佳策略是使用“等差数列求和”的方式,假设你第一次从
x
层开始扔,然后往上每次少 1 层(即 x, x+(x−1), x+(x−1)+(x−2), ...),所有这些值求和满足小于100。最终解得这个不等式的x值为14。 - 具体方式为:让第一颗鸡蛋每次从更高的楼层扔下,但层数递减(如第1次扔第14层,第2次扔第27层,再扔第39层……),一旦碎了就用第二颗鸡蛋从上一次安全的楼层起一层层向上试。这样最多只需14次,就能在100层楼中找出鸡蛋的临界硬度层。
如何设计一个朋友圈的刷新功能?
- 刷新朋友圈的本质是,用户点击刷新后,获取自己和朋友们发布的最新动态,按时间倒序排列,分页返回。其主流方式有拉模式(Pull)、推模式(Push)和混合模式(Push + Pull)
- 拉模式:用户刷新时读取好友列表中所有人的最新动态,然后合并排序并返回。
- 优点:实时性高、存储简单
- 缺点:查询代价高、朋友圈好友多时性能差
- 推模式:每个人发动态时,都会自动推送到所有好友的收件箱,其他人刷新时直接从自己的收件箱中读取。
- 优点:刷新非常快
- 缺点:存储开销大、用户变动后要维护收件箱一致性
如何实现一个支持单机百万级连接推送通知系统,支持在分钟级别实现亿级客户端的消息推送?
- 连接层优化:采用高性能网络框架如Java的Netty或C++的workflow,通过epoll多路复用管理TCP连接,优化内核参数支持百万级文件描述符,使用心跳机制保持连接活性,并设计连接状态轻量级存储。
- 消息分发架构:基于发布/订阅模式,使用Kafka分区存储消息,按设备ID哈希分配到不同推送节点。采用多级推送策略——先批量聚合消息,再通过树状广播方式分发到连接节点,最后并行推送给终端设备。
- 性能保障措施:实现零拷贝网络传输,采用Protocol Buffers压缩消息,建立优先级队列处理重要消息,通过一致性哈希实现水平扩展,并设置熔断机制防止雪崩。单机通过连接复用和批量推送(如每100条合并发送)将亿级推送分解为可管理的吞吐量目标。
如何设计一个支持多种音频格式(mp3/wav/ogg)的音乐格式的播放器,该播放器支持随机播放、单曲循环、顺序播放等几种播放模式。
- PCM(Pulse Code Modulation,脉冲编码调制)是一种表示模拟音频信号的数字形式,我们常听的音乐文件(如 MP3、OGG)是经过压缩和编码的,而要把它们真正播放出来,设备(如声卡)只能识别最原始的音频数据,也就是 PCM 数据流。
- 首先,要实现一个支持多种音频格式(如 MP3、WAV、OGG)的音乐播放器,核心在于音频解码模块的设计。我们可以通过集成成熟的解码库(如 FFmpeg)来实现格式的统一解析,这样可以屏蔽底层的格式差异,只输出统一的 PCM 数据流,供播放模块使用。这一层是播放器跨格式支持的关键。
- 其次,在播放控制模块中,我们需要实现基本的播放功能,如播放、暂停、停止、跳转等操作。这部分可以基于跨平台的音频输出库(如 PortAudio 或 SDL Audio)来完成,负责将 PCM 数据实时送入音频设备进行播放。该模块还需要负责处理播放进度管理和状态维护。
- 第三,播放模式的支持(如顺序播放、随机播放、单曲循环)主要依赖于一个播放队列管理模块。我们设计一个播放队列调度器,根据当前模式动态生成播放索引顺序。例如顺序播放按列表顺序,随机播放预先打乱索引,单曲循环则保持当前索引不变。通过该调度器对播放顺序进行统一管理,能够实现模式切换时的无缝过渡。
- 最后,还需要一个文件与播放列表管理模块,用于扫描本地音频文件、提取元数据(如标题、艺术家、时长等),并构建播放列表供调度器使用。这个模块同时可以为 UI 或控制台界面提供歌曲列表展示和选择支持。
什么是DevOps?
DevOps 是“Development + Operations”的缩写,是一种强调开发(Dev)和运维(Ops)紧密协作的软件工程文化与实践方法,目标是提高软件开发效率、交付速度和系统稳定性。包括:
- 持续集成(CI):代码合并后自动构建、自动测试
- 持续交付/部署(CD):代码通过自动流程快速上线
- 自动化运维:使用脚本或平台进行部署、监控、回滚
当一个复杂系统出现bug时,怎么去快速定位bug?
- 优先考虑复现问题,如果不能复现再进行分析问题。
- 获取问题场景信息:
- 首先确认问题发生的时间、用户操作、输入参数、上下文环境等。
- 收集能还原问题的日志、截图、错误码、接口响应、监控数据。
- 定位错误位置:
- 日志穿透:检查是否全链路打点,系统可以关联起某个请求在各个服务的所有日志,以此使用 Trace ID 找出请求从入口到出错点的完整调用链。
- 分层排查:每一层验证:是否收到了请求?是否处理成功?是否向下游正确传参?
- 查看异常栈和错误码:若程序抛出了异常,分析异常类型、错误码和调用栈,辅助判断代码路径。
- 对比正常和异常请求:比如对比两个相同接口、不同 Trace ID 的调用过程,能快速缩小排查范围。
- 验证定位点:通过增加日志、构造测试数据、使用断点调试、上线灰度验证(将新版本只开放给部分用户,及时发现潜在问题,降低上线风险)来确认问题是否确实出在某一层或某个变量。
说一下常见的风控(风险控制)?
- 风控是指对系统可能遭受的恶意攻击、异常操作、滥用行为等风险进行检测、拦截和限制,以保障业务安全。
- 常见的风控场景:
- 异常登录(异地登录、频繁登录、账号撞库)
- 恶意刷接口、刷优惠券、薅羊毛
- 自动化脚本操作、模拟器登录
- 支付风险(大额转账、频繁提现)
- 常见的风控手段:
- 登录失败次数过多 → 暂时封号或滑块验证
- 同 IP 多账号登录 → 限制或打标签
- 登录地突变(上次北京,这次俄罗斯)→ 发短信验证
- 请求行为特征异常(点击速率、人类不可能操作速度)→ 加验证码或拒绝
多线程分片下载文件,如果某个线程下载失败了怎么办?
- 失败重试机制:每个线程内置重试逻辑,例如失败最多重试 3 次,并加上退避时间(如指数退避)。重试可以使用 try-catch 包裹核心下载逻辑,判断异常类型(超时、连接失败等)再重试。
- 失败上报与监控:如果重试多次仍失败,线程应上报失败状态到主线程或控制器,主线程负责统一处理。主线程可以选择调度其他空闲线程重试该分片,或者中止整个下载任务并报错。
- 断点续传:每个线程下载成功后将分片保存到临时文件,失败不会影响已完成部分。下次下载可以从失败片段重新开始,而不是重头再来。
多线程分片下载文件,如果文件合并失败了怎么办?
- 文件分片多线程下载后,每个线程将各自负责的片段保存为临时文件(如
part0.tmp
、part1.tmp
…),最后需要按顺序合并这些片段形成完整文件(如final.mp4
) - 如果在合并过程中发生失败(如磁盘满、写失败、中途崩溃等),就可能导致:合并失败、目标文件不完整和临时文件未清理。
- 可以使用以下解决方法:
- 使用“临时文件 + 重命名”机制:合并过程不直接写入目标文件,而是先写到临时文件(如
final.mp4.tmp
),合并成功后再一次性重命名为最终文件名。 - 添加恢复机制:保留所有分片文件,不立即删除,合并失败后输出错误信息,并允许用户/系统重新尝试合并,无需重新下载。
- 记录进度:合并时可以记录已写入的片段索引,失败后重新启动合并任务时,从上次的 checkpoint 继续,不必全重新合并。
- 空间预检查:在合并前预估目标文件大小(各分片总和),提前检查磁盘是否有足够空间,防止写到一半挂掉
- 使用“临时文件 + 重命名”机制:合并过程不直接写入目标文件,而是先写到临时文件(如
现在有一个1-5的随机数生成器,如何实现一个1-7的随机数生成器?
- 核心思想:多次使用
rand5()
构造出一个比 7 大的范围,再通过模运算 + 拒绝超出范围的值来实现等概率采样。 - 用
rand5()
构造一个 1~25 的整数。int x = (rand5() - 1) * 5 + rand5(); // 得到 1~25 的等概率整数
- 如果这个数 ≤ 21,就取模转成 1~7 返回,因为 21 是 7 的倍数,所以前 21 个数可以平均分成 7 组,每组 3 个数 → 等概率。
- 如果 x > 21(即 22~25),就丢弃重来。它们根本不会被映射到任何结果上,因此它们不会造成某些数字比其他数字更常出现。
一个记录上千万键值对的文件被程序读取并存到map里,然后外部请求会读这个map,这会有什么问题?
- 问题:启动/加载时间长、内存占用大、GC压力过大(比如Go需要经常扫描大量堆内存)、并发访问下的同步控制和持久化更新问题。
- 缓解方法:
- 启动时分批加载,或使用内存映射(mmap)减少加载时间
- 使用更高效的数据结构或压缩存储
- 读多写少时,采用读写锁或无锁结构保证并发安全
- 尽量减少map的动态增删,保持稳定容量
- 结合缓存和索引技术减少内存压力
- 如果数据量超大,考虑外部KV存储(如 RocksDB、Redis)替代全加载
有海量视频和用户,每个用户可以反复观看视频,如何实时统计观看量最高的100个并显示?
这是一个实时TOP-K排序问题,具有数据量极大、更新频率高、实时性强和读写压力大的特点,可以采用以下方案去解决:
- 分布式计数系统:将所有视频按照ID进行哈希分片,分属在不同的数据库节点中。每个视频维护了一个计数器,每次用户观看某个视频时会自动加1。
- 热点检测与Top-K维护:每个节点单独维护一个TOP集合,不一定是K,然后中心节点会周期性地拉取其他节点的TOP集合。中心节点使用最小堆来筛选出当前Top 100。
- 数据展示:将Top 100结果写入高性能缓存如 Redis 的
ZSET
。展示服务(Web 或接口)只读取缓存,保证查询延迟极低。缓存更新由 Top-K 模块驱动,不依赖用户请求触发。
有1000个一模一样的瓶子,其中有999瓶是普通的水,有一瓶是毒药。任何喝下毒药的生物都会在一星期之后死亡。现在,你只有10只小白鼠和一星期的时间,如何检验出那个瓶子里有毒药?
- 这是用二进制编码做组合测试的经典思路。首先给1000个瓶子编号,转成10位二进制数,因为2的10次方为1024,能覆盖所有瓶子。
- 让10只老鼠分别负责检测对应二进制位为1的那些瓶子,也就是每只老鼠喝所有对应位上是1的瓶子的水。
- 一周后观察老鼠的生死情况,死亡的老鼠对应的二进制位为1,存活的对应0。将10只老鼠的生死状态组合成一个二进制数,就是有毒瓶子的编号。
烧一根不均匀的绳,从头烧到尾总共需要1个小时。现在有若干条材质相同的绳子,问如何用烧绳的方法来计时一个小时十五分钟呢?
- 准备A,B,C三绳
- A,C一头烧,B两头烧
- B烧完时半小时,此时掐断C,A继续烧。
- A烧完时一小时,然后此时从两头烧C。
- C烧完时时间为一小时十五分钟
如何解决过桥问题?
- 四人一起过桥,总时间最短。限制是:
- 桥一次只能两人通过,且必须有手电筒;
- 两人过桥时间取决于慢者;
- 只有一只手电筒,必须有人带回;
- 四人过桥时间分别是1、2、5、8分钟。
- 最优步骤:
- 1和2先过桥(耗时2分钟),手电筒由1带回(耗时1分钟),总用时3分钟。
- 5和8过桥(耗时8分钟),手电筒由2带回(耗时2分钟),总用时3 + 8 + 2 = 13分钟。
- 1和2再次过桥(耗时2分钟),总用时13 + 2 = 15分钟。
给你一个容量为5升的桶和一个容量为3升的桶,水不限使用,要求精确得到4升水?
- 本问题的解题基本思想是用:用小桶容量的倍数对大桶的容量进行取余。即不断用小桶装水倒入大桶,大桶满了立即清空,每次判断下两个桶中水的容量是否等于指定容量
- 比如3升的桶和5升的桶得到4升水可以这样做:
- 3 % 5 = 3
- 6 % 5 = 1
- 9 % 5 = 4
如何设计一个图片加载APP?
- 页面布局:
- 首页:展示图片列表,支持瀑布流、分页加载或滑动切换。
- 图片详情页:可缩放查看大图、点赞、保存等功能。
- 图片加载策略:进入页面时,只加载当前屏幕内图片。上滑时提前预加载下一屏内容。可使用占位图(placeholder)减少白屏时间。在空闲时调度后台预加载。
- 图片缓存策略:在内存中使用LRU缓存策略,在磁盘使用LRU+时间维度的缓存策略,超过太长时间为加载的图片自动删除。
- 图片下载策略:下载线程池控制并发数量。异步下载,避免阻塞主线程。仅在WiFi下下载高清图,4G下降级加载。支持断点续传/重试机制。
- 图片清晰度策略:首页加载缩略图、点开后再加载原图或高清图、服务器提供多种尺寸图片(适配不同分辨率)。
- 图片格式策略:使用 WebP、AVIF 等高压缩比格式,可动态选择合适格式(服务器通过 Accept 判断)。
当用户连续进入多个页面,如何确保只加载当前页面?
- 我们可以给每一个页面或者每一个加载任务分配一个唯一的 tag,所有发起的资源请求都带上这个 tag。这样我们就可以在页面销毁、被覆盖或跳转时,统一取消这个页面对应的所有资源加载任务。
- 即使我们取消了旧页面的请求,但由于网络是异步的,总可能有“漏网之鱼”。所以我们还需要加一层防护——在资源异步加载完成、即将显示到界面上时,先检查目标控件是否仍然属于当前页面、仍然活跃。
如何设计一个合理的客户端缓存?
- 在设计缓存策略时,我会采用两级缓存机制:内存缓存 + 磁盘缓存,目标是在保证加载速度的同时尽可能节省带宽和存储资源。
- 首先,内存缓存主要用于保存当前会话中频繁访问的资源,比如用户正在浏览的资源。内存缓存读写速度快,适合短期高频使用的资源。我会采用LRU(最近最少使用)算法,确保优先保留那些刚使用过或频繁使用的资源,避免无用资源占用内存。
- 其次,磁盘缓存用于存储访问频率中等但具有“回访”可能性的资源,比如用户上一次打开APP时看过的资源。磁盘空间较大,可以设置合理的上限(比如 200MB),同样采用 LRU 结合时间过期策略,比如资源超过 7 天未访问就清除,避免无限增长。
- 优先缓存策略:高频优先,小资源优先,网络状态决定缓存积极程度
如何实现扫码登录?
- 网页端生成一个唯一的登录会话标识(如
login_token
),存入服务器(状态设为“未确认”,然后网页将这个标识转成二维码,展示给用户扫码。 - 用户用登录状态的 APP 扫码,APP 解析出二维码中的
login_token
,然后将它发到后台,并附带用户的身份凭证(如 token),表示“我这个人要在网页上登录”。 - 后台检查 APP 用户身份是否合法,如果合法,就将之前那个
login_token
的状态设为“已确认 + 绑定用户ID”。 - 网页端通过定时轮询或者 WebSocket,不断询问服务器“这个 token 状态如何?”。一旦发现状态是“已确认”,就让网页登录该用户账号。
评论