面试常见的场景设计题

发布时间:2024年01月14日

介绍常见的场景设计题思路,答案不唯一,自由发挥

文章目录

什么是海量数据?

所谓海量数据处理,就是指数据量太大,无法在较短时间内迅速解决,或者无法一次性装入内存。

而解决方案就是:

  • 针对时间,可以采用巧妙的算法搭配合适的数据结构,如 Bloom filter/Hashmap/bit-map//数据库/倒排索引/Trie树;
  • 针对空间,大而化小,分而治之(hash映射),把规模大化为规模小的,各个击破。

[海量数据]处理的基本方法总结起来分为以下几种:

  1. 分而治之/hash映射 + hash统计 + 堆/快速/归并排序;
  2. Trie树/Bloom filter/Bitmap
  3. 数据库/倒排索引(问题实例:文档检索系统,查询那些文件包含了某单词,比如常见的学术论文的关键字搜索。);
  4. 双层桶划分(其实本质上就是【分而治之】的思想,重在“分”的技巧上);
  5. 外排序
  6. 分布式处理之Hadoop/Mapreduce

1.海量日志数据,统计出某日访问[xx]次数最多的那个IP?

**解决方式1:**处理海量日志数据统计访问次数最多的IP,可以使用MapReduce框架。以下是基本的处理步骤:

  1. Map阶段:
    • 将日志数据按照日期作为键,IP地址作为值进行映射。
    • 输出的键值对格式为:(日期, IP地址)
  2. Shuffle阶段:
    • Map阶段输出的键值对会根据日期进行分组。
  3. Reduce阶段:
    • 对于每个日期分组,统计每个IP地址的出现次数。
    • 找到出现次数最多的IP地址。

解决方式2:IP地址最多有 2^32 = 4G 种取值情况,所以不能完全加载到内存中进行处理,采用 hash分解+ 分而治之 + 归并的方式:

(1)按照 IP 地址的 Hash(IP)%1024 值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;

(2)对于每一个小文件,构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址

(3)然后再在这1024组最大的IP中,找出那个频率最大的IP

2.有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

对于这个问题,由于内存限制是1M,无法将整个文件加载到内存中处理。因此,可以使用分块读取文件的方式,并使用哈希表(HashMap)来统计词频。具体步骤如下:

解决思路1:

  1. 分块读取文件: 将文件分割成多个小块,每个小块可以装载到内存中。可以逐块读取文件,处理每个小块的数据。
  2. 词频统计: 对于每个小块,使用哈希表统计词频。将词作为键,词频作为值,存储在哈希表中。由于内存限制,可以使用有限的空间来维护这个哈希表。
  3. 合并词频统计结果: 将所有小块的词频统计结果进行合并。最终得到整个文件的词频统计结果。
  4. 选取频数最高的100个词: 对合并后的词频统计结果进行排序,选择频数最高的100个词。

解决思想2:hash分解+ 分而治之 + 归并

(1)顺序读文件中,对于每个词x,按照 hash(x)/(1024*4) 存到4096个小文件中。这样每个文件大概是250k左右。如果其中有的文件超过了1M大小,还可以按照hash继续往下分,直到分解得到的小文件的大小都不超过1M。

(2)对每个小文件,可以采用 trie树/hashmap 统计每个文件中出现的词以及相应的频率,并使用 100个节点的小顶堆取出出现频率最大的100个词,并把100个词及相应的频率存入文件。这样又得到了4096个文件。

(3)下一步就是把这4096个文件进行归并的过程了

3.内存限制是4G,找出a、b文件共同的url?

如果内存限制为4GB,而文件a和文件b非常大,无法一次性加载到内存中处理,可以使用外部排序(External Sorting)的方法来解决这个问题。外部排序将大文件分成多个小块,每个小块可以适应内存并进行排序。然后,使用归并排序(Merge Sort)的思想将这些小块进行合并,找出共同的URL。

以下是一个基本的思路:

  1. 切分文件: 将大文件a和文件b切分成多个小块,每个小块可以适应内存。可以使用哈希函数将相同的URL哈希到同一个小块中。
  2. 内部排序: 对每个小块进行内部排序。在内存限制下,可以使用快速排序(QuickSort)等高效的排序算法。
  3. 归并排序: 将排序后的小块进行归并排序。可以使用归并排序算法将这些小块进行合并。在合并的过程中,找出共同的URL。
  4. 输出结果: 将共同的URL输出或者进行其他处理。

解决方案1:hash 分解+ 分而治之 + 归并 的方式:

如果内存中想要存入所有的 url,共需要 50亿 * 64= 320G大小空间,所以采用 hash 分解+ 分而治之 + 归并 的方式:

(1)遍历文件a,对每个 url 根据某种hash规则,求取hash(url)/1024,然后根据所取得的值将 url 分别存储到1024个小文件(a0a1023)中。这样每个小文件的大约为300M。如果hash结果很集中使得某个文件ai过大,可以再对ai进行二级hash(ai0ai1024),这样 url 就被hash到 1024 个不同级别的文件中。

(2)分别比较文件,a0 VS b0,…… ,a1023 VS b1023,求每对小文件中相同的url时:把其中一个小文件的 url 存储到 hashmap 中,然后遍历另一个小文件的每个url,看其是否在刚才构建的 hashmap 中,如果是,那么就是共同的url,存到文件中。

(3)把1024个文件中的相同 url 合并起来

解决方案2:Bloom filter

如果允许有一定的错误率,可以使用 Bloom filter。

4G内存大概可以表示 340 亿bit,n = 50亿。

如果按照出错率0.01算需要的大概是650亿个bit,现在可用的是340亿,相差并不多,这样可能会使出错率上升些。

将其中一个文件中的 url 使用 Bloom filter 映射为这340亿bit

然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)

4.有10个文件,每个文件1G,每个文件的每一行存放的都是用户的 query,每个文件的query都可能重复。要求你按照query的频度[排序]

王处理这么大规模的数据需要一种外部排序(External Sorting)的方法,因为无法一次性将所有数据加载到内存中进行排序。以下是一个可能的处理方法:

  1. 切分文件: 将每个1GB的文件切分成多个小块,每个小块适应内存大小。可以使用哈希函数将相同的query哈希到同一个小块中。
  2. 内部排序: 对每个小块进行内部排序。在内存限制下,可以使用快速排序(QuickSort)等高效的排序算法。
  3. 归并排序: 将排序后的小块进行归并排序。可以使用归并排序算法将这些小块进行合并。在合并的过程中,记录每个query的出现次数。
  4. 按频度排序: 使用堆(Heap)或者快速选择(QuickSelect)等方法,按照query的频度进行排序。

解决方案1:hash分解+ 分而治之 +归并

(1)顺序读取10个文件 a0~a9,按照 hash(query)%10 的结果将 query 写入到另外10个文件(记为 b0~b9)中,这样新生成的文件每个的大小大约也1G

(2)找一台内存2G左右的机器,依次使用 hashmap(query, query_count) 来统计每个 query 出现的次数。利用 快速/堆/归并排序 按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件c0~c9。

(3)对这10个文件 c0~c9 进行归并排序(内排序与外排序相结合)。每次取 c0~c9 文件的 m 个数据放到内存中,进行 10m 个数据的归并,即使把归并好的数据存到 d结果文件中。如果 ci 对应的m个数据全归并完了,再从 ci 余下的数据中取m个数据重新加载到内存中。直到所有ci文件的所有数据全部归并完成。

解决方案2:Trie树

如果query的总量是有限的,只是重复的次数比较多而已。

可能对于所有的query,一次性就可以加入到内存了。

在这种情况下,可以采用 trie树/hashmap 等直接来统计每个query出现的次数,

然后按出现次数做快速/堆/归并排序就可以了。

5.怎么在海量数据中找出重复次数最多的一个

在海量数据中找出重复次数最多的一个元素通常可以使用哈希表或者堆(优先队列)来解决。以下是两种可能的方法:

方法一:使用哈希表
  1. 遍历数据并建立哈希表: 遍历所有数据,将每个元素作为哈希表的键,出现的次数作为对应键的值,存储在哈希表中。
  2. 找出重复次数最多的元素: 遍历哈希表,找到值最大的键即为重复次数最多的元素。
方法二:使用堆(优先队列)
  1. 建立最小堆: 遍历所有数据,将每个元素及其出现次数存储在最小堆中。最小堆的大小保持在K(K为需要找的重复次数最多的元素的数量)。
  2. 维护最小堆: 如果当前元素的出现次数比堆顶元素的次数多,就将堆顶元素替换为当前元素。
  3. 找出重复次数最多的元素: 最终,堆中剩余的元素就是重复次数最多的元素。

这两种方法的选择取决于数据的规模和内存的限制。在数据量非常大的情况下,可以使用外部排序等更复杂的算法,将数据分块读入内存,进行处理。需要根据具体情况选择最合适的方法。

6.现有两个各有20亿行的文件,每一行都只有一个数字,求这两个文件的交集。

解决方案1:处理两个各有20亿行的文件并找出交集通常需要使用外部排序和合并的方法,因为内存无法容纳如此庞大的数据量。以下是一种可能的解决方案:

  1. 外部排序: 首先,对两个文件分别进行外部排序,将数据划分为多个小块,每个小块包含部分数据,但是可以完全载入内存进行排序。
  2. 合并算法: 排序完成后,使用合并算法(Merge Algorithm)将两个排序后的文件进行合并,找出交集。合并算法的基本思路是,维护两个指针,分别指向两个已排序的文件的当前行,逐行比较两个指针所指向的值。
  3. 输出交集: 当两个指针指向的值相同时,表示找到了交集元素,将该元素输出。然后分别移动两个指针,继续比较下一行。直到遍历完两个文件的所有行。

这种方法的关键在于外部排序和合并算法的设计。外部排序通常会使用归并排序(Merge Sort)或者基数排序(Radix Sort)等算法。在合并时,可以采用类似归并排序的方式,逐行比较两个文件的当前元素。

需要注意的是,这个过程可能需要大量的磁盘读写操作,因此效率会相对较低,但是在大规模数据处理中是一种可行的解决方案。

解决方案2:采用 bitmap 进行问题解决,int 的[最大数]是 2^32 = 4G。

用一个二进制的下标来表示一个 int 值,大概需要4G个bit位,即约4G/8 = 512M的内存,就可以解决问题了。

首先遍历文件,将每个文件按照数字的正数,负数标记到2个 bitmap 上,为:正数 bitmapA_positive,负数 bitmapA_negative

遍历另为一个文件,生成正数:bitmapB_positive,bitmapB_negative

③ 取 bitmapA_positive and bitmapB_positive 得到2个文件的正数的交集,同理得到负数的交集。

④ 合并,问题解决

这里一次只能解决全正数,或全负数,所以要分两次

7.如何把一个文件快速下发到100w个服务器?

将一个文件快速下发到大规模服务器集群通常需要一种高效的分发系统。以下是可能的一些方法和工具:

  1. 分布式文件系统: 使用分布式文件系统(如Hadoop HDFS、Ceph、GlusterFS等),将文件存储在集群中,然后通过文件系统的复制机制自动将文件分发到多台服务器。
  2. 分发工具: 使用专门的分发工具,例如rsyncscp(Secure Copy Protocol)或pscp(PuTTY Secure Copy Client)等,这些工具支持多线程、断点续传等特性,适合大规模分发。
  3. BitTorrent: 使用BitTorrent协议进行文件分发,该协议允许多台服务器同时下载文件的不同部分,加速文件分发过程。
  4. CDN(内容分发网络): 使用CDN服务,将文件缓存在全球各地的服务器上,用户请求文件时从最近的CDN节点获取,减轻源服务器的负载,提高文件分发速度。
  5. 分布式消息队列: 使用分布式消息队列(例如Apache Kafka、RabbitMQ等),将文件以消息的形式发送到队列中,然后多个消费者(服务器)从队列中获取文件数据进行保存。
  6. P2P网络: 建立一个点对点(P2P)网络,允许服务器之间直接交换文件,减轻源服务器的压力。
  7. 自定义分发系统: 根据需求设计并实现一个自定义的分发系统,该系统可以根据网络状况、服务器负载等动态调整分发策略,提供高效的文件分发服务。

在选择合适的方法之前,需要考虑网络带宽、服务器性能、安全性等因素,并根据具体需求选择最合适的分发方案。

8.给每个组分配不同的IP段,怎么设计一种结构可以快速得知IP是哪个组的?

设计时,可以将IP地址转化为二进制格式,并构建一个Trie树。每个节点代表一位二进制数,从根节点开始,每个节点有两个子节点,分别代表0和1。在Trie树中,从根节点到叶子节点的路径表示IP地址的二进制表示。

这样,当需要判断某个IP属于哪个组时,可以根据IP地址的二进制形式,在Trie树上进行快速查找。具体步骤如下:

  1. 将每个IP地址转化为32位二进制数。
  2. 将32位二进制数插入到Trie树中,从根节点开始,按照IP地址的每一位(0或1)选择左子节点或右子节点,直到达到IP地址的末尾,将IP地址所属的组信息存储在叶子节点中。
  3. 当需要查询某个IP地址所属的组时,按照相同的方式在Trie树中查找,最终到达叶子节点即可得知该IP所属的组信息。

这种方法的优点是可以在O(1)的时间复杂度内完成IP到组的映射,因为IP地址的二进制表示的位数是固定的,所以查找的时间复杂度是常数级别的。

需要注意的是,在实际应用中,可以根据具体需求对Trie树进行优化,例如使用压缩Trie树等,以节省空间和提高查询速度。

9.典型TOPk系列的问题:10亿个数,找出最大的10个。等(10万个数,输出从小到大?有十万个单词,找出重复次数最高十个?)

使用堆(Heap):
  1. 最大的10个数
    • 维护一个小顶堆(最小堆)来存储前10个数。
    • 遍历剩余的数,如果当前数比堆顶元素大,则将堆顶元素替换为当前数,并进行堆调整。
    • 遍历完成后,堆中的10个元素即为最大的10个数。
  2. 从小到大输出10万个数
    • 维护一个大顶堆(最大堆),首先将前10个数放入堆中。
    • 遍历剩余的数,如果当前数比堆顶元素小,则将堆顶元素替换为当前数,并进行堆调整。
    • 遍历完成后,堆中的元素即为从小到大的前10万个数。
  3. 找出重复次数最高的十个数
    • 使用哈希表记录每个数出现的次数。
    • 维护一个小顶堆(最小堆)来存储出现次数前10个的数。
    • 遍历哈希表,如果当前数的出现次数大于堆顶元素,替换堆顶元素并进行堆调整。
使用快速选择(QuickSelect):
  1. 最大的10个数
    • 使用快速选择算法,选择第10大的数作为分界点。
    • 将分界点左边的数视为大于等于它的数,右边的数视为小于它的数。
    • 如果左边的数的数量大于10,继续在左边进行快速选择。
    • 否则,在右边找出剩余的最大数。
  2. 从小到大输出10万个数
    • 使用快速选择算法,选择第10万小的数作为分界点。
    • 将分界点左边的数视为小于等于它的数,右边的数视为大于它的数。
    • 如果左边的数的数量小于10万,继续在右边进行快速选择。
    • 否则,在左边找出剩余的数,并进行排序。
  3. 找出重复次数最高的十个数
    • 使用哈希表记录每个数出现的次数。
    • 使用快速选择算法,选择第10大的出现次数作为分界点。
    • 将分界点左边的出现次数视为大于等于它的数,右边的数视为小于它的数。
    • 如果左边的数的数量大于10,继续在左边进行快速选择。
    • 否则,在右边找出剩余的出现次数,并进行排序。

以上方法中,堆排序适用于处理动态数据,而快速选择则适用于静态数据。选择方法取决于问题的具体要求和数据特点。

10.设计一个微信发红包的api,你会怎么设计,不能有人领到的红包里面没钱,红包数值精确到分。

设计微信发红包的API时,需要考虑以下几个关键点:

  1. 红包金额精确到分:确保红包金额是整数,避免出现小数精度问题。
  2. 避免空红包:红包总金额需要在发放时就确定,确保总金额大于0,避免出现空红包。
  3. 随机分配金额:将总金额随机分配给红包领取者。可以使用一些随机算法,确保每个人都有机会领取到红包,同时保持红包金额的随机性。
  4. 发红包者余额控制:确保发红包者账户余额足够支付红包金额,避免出现支付失败的情况。
  5. 红包领取记录:记录每个红包的领取情况,包括领取者和领取金额,便于查询和统计。
  6. 安全性:红包发放和领取过程需要进行合法性检查,避免恶意攻击和刷红包行为。

11.分布式多个机器生成id,如何保证不重复?

在分布式系统中确保生成的 ID 不重复通常可以使用以下几种方法:

  1. UUID(Universally Unique Identifier):UUID 是一个标准的 128 位长度的唯一标识符,通常以 32 个十六进制数字表示。使用 UUID 生成 ID 可以避免重复,但由于其较长的长度,可能不适合作为数据库主键。
  2. Snowflake 算法:Snowflake 是 Twitter 开源的分布式 ID 生成算法,可以生成 64 位的唯一 ID。该算法结合了时间戳、机器 ID 和序列号,保证了在不同机器和不同时间生成的 ID 唯一性。每个部分的位数可以根据需求进行调整。
  3. 数据库自增主键:如果系统中使用了数据库,并且数据库支持自增主键(如 MySQL 的 AUTO_INCREMENT),可以使用数据库的自增主键作为唯一 ID。在分布式系统中,不同节点访问不同的数据库,可以保证 ID 的唯一性。
  4. 全局唯一 ID 生成服务:使用专门的 ID 生成服务,如 Zookeeper、etcd 等,这些服务可以保证全局唯一性,各个节点从该服务获取 ID。
  5. 分布式锁:使用分布式锁来确保 ID 的生成过程是原子性的,避免多个节点同时生成相同的 ID。
  6. 数据库分区:将 ID 存储在多个数据库分区中,每个分区负责一部分 ID 范围,通过分区键进行路由。这样可以保证每个分区内的 ID 唯一,但需要额外的管理和维护。

选择合适的方法取决于系统的需求和架构。在实际应用中,Snowflake 算法是一个常用且可靠的选择,它可以在分布式系统中生成唯一的、有序的 ID。

12.扫码登录是如何实现的?

扫码登录通常是通过以下步骤实现的:

  1. 生成二维码:服务端生成一个唯一的标识符(如随机字符串或者 token),然后将该标识符嵌入到一个二维码中。这个二维码会显示在用户的登录页面上。
  2. 用户扫码:用户使用移动设备上的客户端应用(如手机上的微信、支付宝等)打开扫码功能,并扫描显示在网页上的二维码。
  3. 扫码确认:客户端应用将二维码中的标识符(token)发送到服务端。服务端接收到标识符后,将其与用户的会话信息(如用户 ID、登录状态等)关联。
  4. 轮询或者长连接:客户端应用定期(通常是几秒一次)向服务端发起请求,询问用户是否已经完成扫码。服务端会检查扫码标识符是否已经被用户关联,如果已经关联,则返回登录成功的消息,客户端应用即可根据服务端返回的信息进行相应操作。
  5. 登录成功:一旦服务端确认用户已经完成扫码,将二维码标识符与用户会话信息关联,用户即被视为已登录。客户端应用接收到登录成功消息后,可以进行相应的界面跳转或者其他操作,表示用户已经登录成功。

在上述流程中,服务端起到了协调和验证的作用,而客户端负责显示二维码、接收用户的扫码操作,并向服务端询问用户是否已经完成扫码。通常,为了增加用户体验,服务端和客户端之间采用了长连接或者轮询的方式,实时地获取用户的扫码状态。

13.分布式集群中如何保证线程安全?

在分布式集群中,保证线程安全通常不再依赖于单一计算机上的线程同步方法(如锁机制),而是依赖于分布式系统的设计和架构。以下是一些在分布式集群中保证线程安全的方法:

  1. 分布式锁:使用分布式锁(如基于Redis的分布式锁)来确保在多个节点上对共享资源的互斥访问。当一个节点获得锁时,其他节点将被阻塞,直到锁被释放。
  2. 事务性操作:使用事务来确保多个操作的原子性。如果一个操作失败,整个事务将会回滚,保持数据的一致性。
  3. 乐观锁:在数据库中使用乐观锁(例如CAS操作)来确保多个客户端对同一数据的并发修改不会导致冲突。如果两个客户端同时尝试更新同一数据,系统会根据版本号或时间戳判断哪个操作应该被接受。
  4. 消息队列:使用消息队列(如RabbitMQ、Kafka等)来进行异步处理。将需要并发处理的任务放入消息队列中,由多个消费者节点异步处理。这样可以避免直接在主服务节点上进行并发操作,减少了竞争和冲突。
  5. 分布式事务:在需要的情况下,可以使用分布式事务协议(如XA协议)来确保跨多个数据库或服务的多个操作的一致性。不过,分布式事务往往会引入更多的复杂性和性能开销,需要根据具体情况权衡。
  6. 无状态设计:尽量设计无状态的服务,避免在服务端保存客户端的状态信息。如果每个请求都是独立的,就不需要担心多个请求之间的状态问题。
  7. 幂等性设计:确保操作是幂等的,即多次执行相同操作的效果和一次执行的效果相同。这样即使发生重试或者多次执行,也不会引起问题。
  8. 分片和分区:将数据按照一定的规则分片或者分区,每个节点只负责处理部分数据。这样可以减少单个节点的负担,提高系统的可扩展性和并发处理能力。

以上方法通常会根据具体应用场景和需求的复杂程度来选择和组合使用。

14.某网站/app首页每天会从10000个商家里面推荐50个商家置顶,每个商家有一个权值,你如何来推荐?第二天怎么更新推荐的商家?

推荐系统是一个复杂的领域,可以使用多种方法来实现。在这个场景下,你可以使用以下方法来推荐商家并在第二天更新推荐:

推荐商家:

  1. 基于权值的推荐:为每个商家计算一个权值,可能包括商家的评分、热度、历史交易量等。根据权值排名,选择前50个商家作为推荐。
  2. 协同过滤推荐:根据用户的历史行为数据,找出与用户兴趣相似的用户,然后推荐这些相似用户喜欢的商家。
  3. 内容推荐:根据商家的详细信息,比如商家的类别、标签、描述等,和用户的兴趣进行匹配,推荐与用户兴趣相关的商家。
  4. 深度学习模型:使用深度学习模型,如神经网络,将用户和商家的特征进行嵌入,然后通过模型学习用户和商家之间的关系,从而进行推荐。

更新推荐:

  1. 每日刷新:每天固定时间,重新计算商家的权值,然后重新选择前50个商家进行推荐。
  2. 实时更新:当商家的权值发生变化时(比如有新的评价、新的交易等),立即更新商家的权值,并根据新的权值重新排序,选择前50个商家进行推荐。
  3. 在线学习:使用在线学习算法,当用户与商家发生交互时(比如用户点击商家、购买商品等),即时更新模型,然后基于更新后的模型进行推荐。
  4. 定期重训练:每隔一段时间,使用历史数据重新训练推荐模型,得到新的模型后,基于新模型进行推荐。

在选择推荐算法和更新策略时,需要考虑数据规模、实时性要求、用户体验等因素,通常会根据实际情况选择合适的方法。

15.如何设计一个本地缓存?需要考虑哪些方面?

设计一个本地缓存需要考虑以下方面:

  1. 缓存策略选择:选择合适的缓存替换策略,如LRU(Least Recently Used,最近最少使用)、LFU(Least Frequently Used,最不经常使用)等,根据业务需求选择最合适的策略。
  2. 缓存数据结构:选择合适的数据结构来存储缓存数据,通常使用哈希表或者平衡树(比如红黑树)来实现高效的查找。
  3. 缓存容量:确定缓存的最大容量,超出容量时采取何种策略,如删除最近最少使用的数据。
  4. 缓存过期策略:考虑缓存数据的有效期,实现缓存过期策略,定期清理过期数据。
  5. 并发访问控制:实现并发安全的缓存访问,避免多个线程同时读写缓存数据时出现问题。
  6. 内存管理:合理管理内存,防止缓存占用过多内存,可以考虑使用内存池等技术来优化内存使用。
  7. 缓存预热:可以在系统启动时将一部分数据预先加载到缓存中,提高系统的响应速度。
  8. 监控与统计:实现缓存的监控与统计功能,记录缓存命中率、缓存使用率等信息,方便性能优化和故障排查。
  9. 持久化:考虑缓存数据的持久化,可以选择定期将缓存数据写入持久化存储,以防止数据丢失。
  10. 分布式缓存:如果是分布式系统,可以考虑使用分布式缓存系统,如Redis、Memcached等,来实现高性能、高可用的缓存。
  11. 缓存失效处理:当缓存失效时,避免缓存雪崩效应,可以考虑使用加锁、限流等措施,避免大量请求直接打到数据库或其他后端服务。
  12. 异常处理:处理缓存访问可能出现的异常情况,如网络超时、缓存服务宕机等,采取合适的容错机制。

以上是设计本地缓存时需要考虑的一些方面,具体的设计方案需要根据系统需求和架构进行灵活选择和调整。

16.搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。 假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。

**解决方案1:**第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计(之前写成了排序,特此订正。July、2011.04.27);
第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比所以,我们最终的时间复杂度是:O(N) + N’*O(logK),(N为1000万,N’为300万)。ok,更多,详情,请参考原文。

**解决方案2:**采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。

17.分布式锁有哪些实现方式?你项目中用到的是哪一种?

分布式锁是在分布式系统中用来协调多个节点并发访问共享资源的一种机制。以下是常见的分布式锁实现方式:

  1. 基于数据库的分布式锁:将分布式锁的状态存储在数据库中,通过数据库事务的特性来实现锁的互斥。常用的数据库有MySQL、PostgreSQL等。但是基于数据库的分布式锁可能存在数据库性能瓶颈的问题。
  2. 基于缓存的分布式锁:使用分布式缓存如Redis、Memcached等,在缓存中设置一个键值对,当键不存在时创建,表示获取锁。由于缓存具备高性能和高并发的特性,因此基于缓存的分布式锁是比较常用的方式。
  3. ZooKeeper/etcd/consul分布式锁:ZooKeeper是一个分布式协调服务,在ZooKeeper中可以创建临时有序节点,通过节点的顺序来确定锁的获取顺序。ZooKeeper分布式锁具有高可用性和强一致性,但是相比缓存和数据库,它的性能较低。
  4. 基于文件的分布式锁:在分布式文件系统中创建一个文件,通过文件的存在与否来表示锁的状态。这种方式简单,但是在分布式环境中需要考虑文件系统的一致性问题。
  5. 基于乐观锁的分布式锁:在数据库中使用版本号或者时间戳等字段来实现乐观锁,通过比较版本号或者时间戳的方式来判断是否能获取锁。
  6. 基于硬件的分布式锁:使用专门的硬件设备来实现分布式锁,如基于网络的硬件设备。

在我的项目中,具体使用哪种分布式锁的方式通常取决于项目的特点和需求。在较为简单的场景下,我可能会选择基于Redis的分布式锁,因为Redis性能高,易于部署和维护。在需要更高一致性和可靠性的场景下,我可能会选择ZooKeeper/etcd/consul分布式锁。不同的项目可能会选择不同的实现方式来满足具体的需求。

18.谈谈你对分布式事务的理解,你觉得重要吗?

分布式事务是指涉及到多个分布式系统之间的交互操作,需要保证这些操作的一致性和原子性。在传统的单体应用中,事务的处理相对简单,但在分布式系统中,涉及到多个服务、数据库等组件,事务处理变得更加复杂。

为什么分布式事务重要?
  1. 数据一致性:在分布式系统中,如果不同的服务或模块之间的操作不具备原子性,可能导致数据不一致,破坏了系统的可靠性和正确性。
  2. 业务操作的原子性:在一些业务场景下,需要确保多个操作要么全部成功,要么全部失败,这就需要分布式事务来保障这些操作的原子性。
  3. 系统可靠性:分布式系统中,一个服务的失败不能影响其他服务的正常运行。分布式事务可以确保在一个服务失败的情况下,事务可以回滚,避免了分布式系统的“脑裂”问题。
分布式事务的实现方式
  1. 两阶段提交(2PC):2PC是一种分布式事务的协议,它通过两个阶段的提交来保证事务的一致性。然而,2PC存在阻塞、单点故障等问题,不够灵活。
  2. 补偿事务:补偿事务是指在分布式系统中,通过执行补偿操作来达到一致性的目的。例如,在某个服务执行失败时,通过调用其他服务的接口来撤销之前的操作。
  3. 本地消息表(TCC):TCC(Try-Confirm-Cancel)是一种基于本地消息表的分布式事务处理方式,它通过预留资源、确认操作和撤销操作来保证事务的一致性。
  4. 分布式事务中间件:像阿里的Seata、开源的LRA(Last Resource Commit)等分布式事务中间件,提供了相对完备的分布式事务解决方案。
重要性总结

分布式事务在分布式系统中是非常重要的,它保障了数据的一致性,确保了系统的稳定性和可靠性。在设计和开发分布式系统时,必须考虑事务的一致性,选择合适的分布式事务方案,以确保系统的正确运行。

19.流量削峰在秒杀场景下有考虑过存在的问题和解决方案吗?

是的,在秒杀场景中,流量削峰是一个非常关键的问题。当秒杀活动开始时,会出现非常高的并发请求,如果不进行流量削峰,容易导致系统崩溃。以下是秒杀场景下流量削峰存在的问题和解决方案:

问题:
  1. 高并发冲击: 秒杀开始时,大量用户同时发起请求,导致数据库、缓存等后端资源瞬间承受巨大压力。
  2. 数据库压力: 大量用户同时查询库存,下单等操作,增加数据库负载,容易造成数据库宕机。
  3. 缓存雪崩: 缓存中秒杀商品信息瞬间失效,导致大量请求直接打到数据库上,引发数据库压力过大。
  4. 订单并发: 大量用户秒杀成功后,订单的并发写入可能会超出数据库的承受能力,导致订单数据异常。
解决方案:
  1. 限流控制: 使用限流算法(如令牌桶、漏桶等)对用户的请求进行限制,防止瞬间并发请求过多。
  2. 缓存策略: 合理设置秒杀商品信息的缓存失效时间,避免在短时间内大量请求打到数据库上。
  3. 队列处理: 使用消息队列(如Kafka、RabbitMQ)将秒杀请求异步处理,削峰的同时实现异步下单,减小数据库压力。
  4. 数据库优化: 对数据库进行优化,使用索引、合理的SQL语句,提高数据库的查询和写入效率。
  5. 分布式架构: 使用分布式数据库、分布式缓存等技术,将请求分散到不同的节点上,提高系统的整体承受能力。
  6. 预热机制: 在秒杀开始前,提前将商品信息加载到缓存中,避免瞬间并发请求导致缓存失效。
  7. 数据库水平拆分: 将数据库按照商品ID等维度进行水平拆分,降低单表的数据量,提高数据库的并发处理能力。

以上方案通常需要根据具体的业务场景和系统架构来选择和组合使用,以保障系统在秒杀活动中的稳定性和高可用性。

20.如何实现在秒杀场景下的限流服务?

在秒杀场景下,限流是确保系统稳定性的关键措施。以下是一些常见的秒杀场景下限流服务的实现方法:

  1. 令牌桶算法: 令牌桶算法是一种简单而有效的限流算法。在秒杀场景中,可以将令牌桶算法应用于接口的请求上。系统以固定的速率往令牌桶中放入令牌,用户的请求需要获取令牌才能继续执行。如果令牌桶中没有足够的令牌,则请求被拒绝,实现了限流效果。
  2. 漏桶算法: 漏桶算法也是一种常见的限流算法。漏桶算法固定容量的桶,按照固定的速率流出水滴。当请求到来时,将请求放入漏桶中,如果漏桶已满,则请求被拒绝。漏桶算法可以平滑请求的流量。
  3. 分布式限流: 使用分布式限流组件,例如Redis或ZooKeeper,将限流规则存储在共享的分布式存储中,各个服务节点从分布式存储中获取限流规则,实现统一的限流策略。
  4. Guava RateLimiter: Google的Guava库提供了一个RateLimiter类,可以用来实现令牌桶限流算法。RateLimiter允许你以一个固定的速率获取令牌,当令牌用完时,请求将被阻塞。
  5. Nginx限流: 如果秒杀服务是通过HTTP请求提供的,可以使用Nginx的限流功能,通过配置limit_req指令实现请求的限速。
  6. 熔断器: 在高并发时,可以使用熔断器(例如Hystrix)来处理限流。熔断器能够实时监控系统的流量,当流量超过阈值时,自动打开熔断,拒绝新的请求,避免系统崩溃。当系统稳定后,熔断器会逐渐放开,恢复对新请求的处理。

以上方法可以单独使用,也可以组合使用,根据具体场景和需求选择适合的限流策略。

21.如何处理大型日志文件

  1. 并发处理: 使用Go协程(goroutine)进行并发处理,可以充分利用多核CPU的性能。可以将大文件拆分为小块,每个协程处理一个小块,然后将处理结果合并。

  2. 缓冲读取: 使用缓冲读取(Buffered IO)可以减少IO操作的次数,提高读取速度。可以使用bufio包提供的缓冲读取功能。

  3. 内存映射文件: 使用os包中的mmap功能,将大文件映射到内存中,然后在内存中进行操作。这种方法可以减少文件读取和写入的系统调用,提高IO性能。

    goCopy codefile, err := os.Open("largefile.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()
    
    // 将文件映射到内存中
    data, err := mmap.Map(file, mmap.RDONLY, 0)
    if err != nil {
        log.Fatal(err)
    }
    defer data.Unmap()
    
    // 在data中进行操作,data是一个字节切片
    
  4. 分块读取: 将大文件分成小块,逐块读取并处理。这种方法可以降低单次读取的数据量,减小内存占用,提高处理速度。

  5. 使用内存池: 对于需要频繁创建和销毁大量字节切片的情况,可以使用sync.Pool来创建一个字节切片的池,避免频繁的内存分配和释放。

  6. 使用高性能IO库: 对于特定的文件格式,可以选择使用专门的高性能IO库,例如encoding/csv用于CSV文件的读写,encoding/json用于JSON文件的读写等。

  7. 合理使用缓存: 如果文件的内容可以被缓存,可以考虑使用内存缓存,将文件内容加载到内存中,然后在内存中进行操作。但要注意内存占用和缓存一致性的问题。

  8. 使用多级缓存: 如果文件内容非常大,可以考虑使用多级缓存,例如内存缓存、磁盘缓存等,根据访问频率和数据大小选择合适的缓存级别。

21.一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析

方案1:如果文件比较大,无法一次性读入内存,可以采用hash取模的方法,将大文件分解为多个小文件,对于单个小文件利用hash_map统计出每个小文件中10个最常出现的词,然后再进行归并处理,找出最终的10个最常出现的词。

方案2:通过hash取模将大文件分解为多个小文件后,除了可以用hash_map统计出每个小文件中10个最常出现的词,也可以用trie树统计每个词出现的次数,时间复杂度是O(n*le)(le表示单词的平准长度),最终同样找出出现最频繁的前10个词(可用堆来实现),时间复杂度是O(nlog10)

22.1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现

方案1:这题用trie树比较合适,hash_map也行。

方案2:分而治之/hash映射 + hashmap

23、2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。

有点像鸽巢原理,整数个数为232,也就是,我们可以将这232个数,划分为2^8个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap就可以直接解决了。也就是说只要有足够的磁盘空间,就可以很方便的解决。

24、5亿个int找它们的中位数。

  1. 思路一:这个例子比上面那个更明显。首先我们将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
    实际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。即可以先将int64分成224个区域,然后确定区域的第几大数,在将该区域分成220个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有2^20,就可以直接利用direct addr table进行统计了。
  2. 思路二@绿色夹克衫:同样需要做两遍统计,如果数据存在硬盘上,就需要读取2次。
    方法同基数排序有些像,开一个大小为65536的Int数组,第一遍读取,统计Int32的高16位的情况,也就是0-65535,都算作0,65536 - 131071都算作1。就相当于用该数除以65536。Int32 除以 65536的结果不会超过65536种情况,因此开一个长度为65536的数组计数就可以。每读取一个数,数组中对应的计数+1,考虑有负数的情况,需要将结果加32768后,记录在相应的数组内。
    第一遍统计之后,遍历数组,逐个累加统计,看中位数处于哪个区间,比如处于区间k,那么0- k-1的区间里数字的数量sum应该<n/2(2.5亿)。而k+1 - 65535的计数和也<n/2,第二遍统计同上面的方法类似,但这次只统计处于区间k的情况,也就是说(x / 65536) + 32768 = k。统计只统计低16位的情况。并且利用刚才统计的sum,比如sum = 2.49亿,那么现在就是要在低16位里面找100万个数(2.5

外排序

适用范围:大数据的排序,去重

基本原理及要点:外排序的归并方法,置换选择败者树原理,最优归并树

扩展:

问题实例:

1).有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16个字节,内存限制大小是1M。返回频数最高的100个词。

这个数据具有很明显的特点,词的大小为16个字节,但是内存只有1m做hash有些不够,所以可以用来排序。内存可以当输入缓冲区使用。

trie树

适用范围:数据量大,重复多,但是数据种类小可以放入内存

基本原理及要点:实现方式,节点孩子的表示方式

扩展:压缩实现。

问题实例:

1).有10个文件,每个文件1G,每个文件的每一行都存放的是用户的query,每个文件的query都可能重复。要你按照query的频度排序。

2).1000万字符串,其中有些是相同的(重复),需要把重复的全部去掉,保留没有重复的字符串。请问怎么设计和实现?

3).寻找热门查询:查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个,每个不超过255字节。

分布式处理 mapreduce

适用范围:数据量大,但是数据种类小可以放入内存

基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约。

扩展:

问题实例:

1).The canonical example application of MapReduce is a process to count the appearances ofeach different word in a set of documents:

2).海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。

3).一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到N^2个数的中数(median)?

经典问题分析

上千万or亿数据(有重复),统计其中出现次数最多的前N个数据,分两种情况:可一次读入内存,不可一次读入。

可用思路:trie树+堆,数据库索引,划分子集分别统计,hash,分布式计算,近似统计,外排序

所谓的是否能一次读入内存,实际上应该指去除重复后的数据量。如果去重后数据可以放入内存,我们可以为数据建立字典,比如通过 map,hashmap,trie,然后直接进行统计即可。当然在更新每条数据的出现次数的时候,我们可以利用一个堆来维护出现次数最多的前N个数据,当然这样导致维护次数增加,不如完全统计后在求前N大效率高。

如果数据无法放入内存。一方面我们可以考虑上面的字典方法能否被改进以适应这种情形,可以做的改变就是将字典存放到硬盘上,而不是内存,这可以参考数据库的存储方法。

当然还有更好的方法,就是可以采用分布式计算,基本上就是map-reduce过程,首先可以根据数据值或者把数据hash(md5)后的值,将数据按照范围划分到不同的机子,最好可以让数据划分后可以一次读入内存,这样不同的机子负责处理各种的数值范围,实际上就是map。得到结果后,各个机子只需拿出各自的出现次数最多的前N个数据,然后汇总,选出所有的数据中出现次数最多的前N个数据,这实际上就是reduce过程。

实际上可能想直接将数据均分到不同的机子上进行处理,这样是无法得到正确的解的。因为一个数据可能被均分到不同的机子上,而另一个则可能完全聚集到一个机子上,同时还可能存在具有相同数目的数据。比如我们要找出现次数最多的前100个,我们将1000万的数据分布到10台机器上,找到每台出现次数最多的前 100个,归并之后这样不能保证找到真正的第100个,因为比如出现次数最多的第100个可能有1万个,但是它被分到了10台机子,这样在每台上只有1千个,假设这些机子排名在1000个之前的那些都是单独分布在一台机子上的,比如有1001个,这样本来具有1万个的这个就会被淘汰,即使我们让每台机子选出出现次数最多的1000个再归并,仍然会出错,因为可能存在大量个数为1001个的发生聚集。因此不能将数据随便均分到不同机子上,而是要根据hash 后的值将它们映射到不同的机子上处理,让不同的机器处理一个数值范围。

而外排序的方法会消耗大量的IO,效率不会很高。而上面的分布式方法,也可以用于单机版本,也就是将总的数据根据值的范围,划分成多个不同的子文件,然后逐个处理。处理完毕之后再对这些单词的及其出现频率进行一个归并。实际上就可以利用一个外排序的归并过程。

文章来源:https://blog.csdn.net/qq_42009262/article/details/135580493
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。