【得物】春招后端一面,这么喜欢八股吗?|0403

  1. 讲了一下实习怎么SQL慢查询优化的

  2. 索引什么情况会失效 (1/3什么的)

  3. 为什么索引用B+树

  4. TCP的三次握手和四次挥手

  5. 讲一下TIME_WAIT

  6. HTTPS加密过程

  7. git rebase和merge的区别

  8. 线程和进程区别

  9. Redis单线程模型

>alt

作者:安安熊

面经专栏直通车,欢迎订阅:https://www.nowcoder.com/creation/manager/columnDetail/0xKkDM

面经专栏下载, 点击跳转:https://github.com/Li-CW/Job-Hunter

1. SQL慢查询是怎么优化?

SQL慢查询优化通常涉及以下几个关键步骤:

  1. 识别慢查询
    • 使用数据库的慢查询日志(如MySQL的slow query log)来识别执行时间较长的查询。
    • 监控数据库性能工具(如Prometheus, Grafana, 或数据库自带的监控工具)。
  2. 分析查询
    • 对慢查询进行EXPLAIN分析,理解查询执行计划,查看是否有全表扫描、缺失索引等问题。
    • 分析查询的复杂性,查找是否可以进行简化或分解。
  3. 优化索引
    • 根据查询条件和WHERE子句,添加或优化索引以提高查询效率。
    • 避免过度索引,因为这会影响写入性能和存储空间。
  4. 优化查询语句
    • 重写或修改查询语句,避免使用SELECT *,只选择必要的字段。
    • 使用JOIN代替子查询,或者优化子查询的使用。
    • 避免在WHERE子句中使用函数,这可能导致索引失效。

学习指引字节面试官:一条sql执行慢的原因?如何优化?

2. 索引什么情况会失效?

索引在数据库中用于提高查询性能,但在某些情况下,索引会“失效”。以下是一些导致索引失效的常见情况:

  1. 查询条件与索引不匹配:如果查询条件没有正确使用索引列,或者查询条件中的数据类型与索引列的数据类型不一致,索引可能不会被使用。
  2. 函数或表达式的使用:在查询条件中对索引列使用函数或表达式(如 LOWER(column_name)column_name + 10),这通常会阻止索引的有效使用,因为数据库需要计算每行的值,而不是直接查找索引。
  3. 隐式类型转换:当查询条件中的数据类型与索引列的数据类型不匹配,且数据库进行了隐式类型转换时,索引也可能失效。例如,将字符串与数字列进行比较。
  4. 非最左前缀原则:对于复合索引,如果不从最左侧的列开始查询,或者跳过了某些列,索引可能不会被充分使用。例如,有一个基于 (col1, col2, col3) 的复合索引,但查询只涉及 col2col3
  5. 选择性低的列:如果索引列的值非常重复(选择性低),数据库优化器可能会认为全表扫描比使用索引更有效。
  6. OR 运算符的使用:在某些情况下,使用 OR 运算符连接多个条件可能导致索引失效,特别是当这些条件涉及不同的索引列时。
  7. LIKE 语句的使用:当使用 LIKE 语句进行模糊查询时,如果通配符(%)出现在字符串的开始位置(如 %value),索引通常会失效。因为这种情况下,数据库需要进行全表扫描来匹配以任意字符开头的值。

学习指引索引失效的七种情况

3. 为什么索引用B+树?

B+树的特性和优势

  1. 磁盘读写代价低:B+树的内部节点不存储指向数据具体信息的指针,只存储键值和子节点的指针,因此其内部节点相对更小。这种设计使得在相同大小的盘块中可以容纳更多的关键字,进而减少磁盘I/O次数。在数据库系统中,由于数据通常存储在磁盘上,而磁盘I/O操作的成本远高于内存操作,因此降低磁盘读写代价对于提高查询性能至关重要。
  2. 查询效率稳定:在B+树中,无论查询哪个关键字,都需要从根节点走到叶子节点。这种固定的查询路径长度保证了每个数据的查询效率相当稳定。此外,由于所有关键字数据都存储在叶子节点上,使得每次查找的次数相同,进一步增强了查询效率的稳定性。
  3. 范围查询效率高:B+树的叶子节点通过指针相连,形成一个有序链表。当进行范围查询时,只需遍历叶子节点即可,无需像B树那样遍历整棵树。这种特性使得B+树非常适合于区间查询和遍历操作,特别是在数据库系统中频繁进行的范围查询操作。
  4. 空间利用率高:与B树相比,B+树的非叶子节点不存储数据,只存储键值信息和指针信息。这种设计使得B+树在相同空间内可以存储更多的关键字信息,提高了空间利用率。在数据库系统中,空间利用率是一个重要的考虑因素,因为它直接影响到存储成本和性能。

与其他数据结构的对比

  1. 与B树对比:虽然B树也具有良好的查询性能,但B+树在某些方面进行了优化。首先,B+树的磁盘读写代价更低,因为它减少了内部节点的数据量。其次,B+树的查询效率更加稳定,因为所有关键字数据的查询路径长度相同。最后,B+树更适合进行范围查询,因为其叶子节点形成了一个有序链表。然而,如果经常访问的数据离根节点很近,那么B树可能会比B+树更快,因为B树的非叶子节点存储了关键字数据的地址。
  2. 与二叉搜索树对比:二叉搜索树虽然具有简单的结构和较低的维护成本,但在面对大数据量时可能会变得非常不平衡,导致查询性能下降。相比之下,B+树通过引入平衡因子和分裂合并机制来保持树的平衡性,从而确保稳定的查询性能。此外,B+树还优化了磁盘I/O操作和空间利用率,使其更适合于数据库系统。
  3. 与散列结构对比:散列结构如哈希表可以提供快速的点查询性能,但对于范围查询则效率较低。此外,哈希表在数据更新时可能需要重新哈希和重新分配空间,导致性能下降。相比之下,B+树既支持快速的点查询也支持高效的范围查询,并且在数据更新时能够保持较好的性能稳定性。

学习指引数据库索引采用B+树的主要原因

4. TCP的三次握手和四次挥手?

三次握手过程

三次握手是TCP协议建立连接的过程,其主要目的是在客户端和服务器之间建立可靠的连接,并确认双方的接收和发送能力。具体步骤如下:

  1. 客户端发送SYN包:客户端向服务器发送一个SYN包,其中包含客户端的初始序列号。这一步是客户端请求建立连接。
  2. 服务器回复SYN+ACK包:服务器收到SYN包后,会回复一个SYN+ACK包。其中,ACK是对客户端SYN包的确认,SYN则是服务器请求建立连接的标志。此外,服务器还会生成自己的初始序列号。
  3. 客户端发送ACK包:客户端收到服务器的SYN+ACK包后,会再次发送一个ACK包,确认收到服务器的SYN包和初始序列号。至此,三次握手完成,客户端和服务器之间建立了可靠的连接。

通过这三次握手,客户端和服务器能够确认双方的接收和发送能力,以及初始序列号,从而确保后续数据传输的正确性。

四次挥手过程

四次挥手则是TCP协议关闭连接的过程,其目的是确保双方的数据传输完全结束后再关闭连接。具体步骤如下:

  1. 客户端发送FIN包:当客户端想要关闭连接时,会向服务器发送一个FIN包,表示数据传输结束。
  2. 服务器回复ACK包:服务器收到FIN包后,会回复一个ACK包,确认收到客户端的关闭请求。此时,服务器可能还有数据需要传输给客户端,因此不会立即关闭连接。
  3. 服务器发送FIN包:当服务器确保所有数据都已经传输给客户端后,会发送一个FIN包,表示服务器也准备关闭连接。
  4. 客户端回复ACK包:客户端收到服务器的FIN包后,会回复一个ACK包,确认收到服务器的关闭请求。至此,四次挥手完成,客户端和服务器之间的连接被关闭。

通过这四次挥手,客户端和服务器能够确保双方的数据传输完全结束后再关闭连接,从而避免数据的丢失或损坏。

学习指引TCP连接的四次挥手全过程

5. 讲一下TIME_WAIT?

TIME_WAIT状态是TCP协议中的一个重要环节,它出现在一个TCP连接关闭的过程中。具体来说,当TCP连接的一方(通常是客户端)主动发起关闭连接请求,并在收到对方的确认后,会进入TIME_WAIT状态。

  1. 确保关闭请求的可靠性:TIME_WAIT状态允许主动关闭方等待一段时间,以确保被动关闭方已经收到了关闭连接的最后一个ACK报文。这避免了因为网络延迟或丢包导致的连接异常终止。
  2. 防止旧连接的数据包干扰新连接:在网络中,可能存在延迟的数据包,这些数据包可能属于已经关闭的连接。TIME_WAIT状态的存在可以确保这些旧数据包在网络中自然消失,从而避免它们对新建立的连接造成干扰。
  3. 保证端口资源的正确释放:在TIME_WAIT状态期间,主动关闭方会保持对端口的占用,直到状态超时。这确保了在该端口上不会立即建立新的连接,从而避免了潜在的端口冲突和资源争用问题。

TIME_WAIT状态出现的原因

TIME_WAIT状态的出现主要是由于TCP协议的设计需要和网络环境的复杂性。

  1. 主动关闭连接的一方需要确认被动关闭方已经接收到了关闭连接的请求
  2. 网络环境中可能存在的延迟和丢包现象。这些网络问题可能导致关闭连接的ACK报文无法及时到达被动关闭方,从而需要TIME_WAIT状态来确保连接的正确关闭。
  3. 防止已失效的报文段干扰新的连接。通过等待一段时间再释放端口资源,可以降低这种干扰发生的概率。

学习指引TCP协议中为什么有TIME_WAIT状态?

6. HTTPS加密过程?

1. SSL/TLS握手过程

SSL/TLS握手是HTTPS加密过程的初始阶段,用于在客户端和服务器之间建立安全连接。

  • 验证身份:通过交换证书,验证服务器(有时也验证客户端)的身份。
  • 协商加密协议:双方协商确定将使用的加密套件(包括加密算法、密钥交换算法和消息认证码算法)。
  • 交换密钥:生成并交换用于加密通信的会话密钥。

具体握手步骤如下:

  1. 客户端发送Client Hello:包含客户端支持的加密套件列表和随机数。
  2. 服务器响应Server Hello:选择客户端支持的加密套件之一,并发送自己的随机数。
  3. 服务器发送证书:服务器发送其公钥证书,以证明其身份。
  4. 服务器发送Server Key Exchange(可选):如果密钥交换算法需要,服务器会发送额外的密钥交换信息。
  5. 服务器发送Certificate Request(可选):如果服务器需要验证客户端身份,会请求客户端证书。
  6. 服务器发送Server Hello Done:表示服务器握手消息发送完毕。
  7. 客户端发送Certificate(可选):如果服务器请求了客户端证书,客户端会发送其证书。
  8. 客户端发送Client Key Exchange:客户端生成预主密钥,并使用服务器的公钥进行加密后发送给服务器。
  9. 客户端发送Certificate Verify(可选):如果发送了客户端证书,客户端会发送一个签名,以证明其拥有证书对应的私钥。
  10. 客户端发送Change Cipher Spec:通知服务器后续通信将使用协商好的加密套件和会话密钥。
  11. 客户端发送Finished:发送加密后的握手摘要,供服务器验证。
  12. 服务器发送Change Cipher Spec:通知客户端后续通信将使用协商好的加密套件和会话密钥。
  13. 服务器发送Finished:发送加密后的握手摘要,供客户端验证。

2. 密钥交换

在SSL/TLS握手过程中,密钥交换是一个关键步骤。它允许客户端和服务器安全地协商出一个共享的会话密钥。这个会话密钥将在后续的通信中用于加密和解密数据。具体的密钥交换算法取决于在握手过程中协商的加密套件。例如,Diffie-Hellman(DH)算法就是一种常用的密钥交换算法。

3. 数据加解密

一旦SSL/TLS握手完成,并且双方已经协商出了会话密钥,就可以开始进行数据的加解密了。在HTTPS通信中,所有的数据(包括HTTP请求和响应)都会使用会话密钥进行加密后再传输。接收方在收到加密数据后,会使用相同的会话密钥进行解密,以还原出原始数据。这样,即使数据在传输过程中被截获,攻击者也无法轻易地读取其中的内容。

学习指引彻底搞懂HTTPS的加密原理

7. git rebase和merge的区别?

git rebase的主要作用是将一个分支的基底更新为另一个分支的末尾,从而使两条分支最终变得相似。这通常用于保持一个干净、线性的提交历史。

当执行rebase操作时,Git会尝试将当前分支上的每个提交重新应用到目标分支上。它会取消当前分支上的提交,保存这些提交所做的更改,然后在目标分支上逐个重新应用这些更改。这个过程可能会产生冲突,需要手动解决。但一旦冲突被解决,提交历史就会看起来像是一系列连续的、没有分叉的提交。

git merge会将两个或两个以上的开发历史合并在一起。它创建一个新的提交,该提交包含了两个分支的所有更改,并保留了完整的提交历史。这意味着合并操作会在提交历史中创建一个新的分叉点,表示两个分支曾经分开但现在又合并到了一起。merge操作通常用于整合不同分支上的工作,尤其是当这些分支包含了相互独立的、无法简单重新应用的更改时。

rebasemerge的主要区别在于它们如何处理分支和提交历史。rebase通过重新应用提交来创建一个线性的提交历史,而merge则通过创建一个新的合并提交来整合不同分支的工作。

学习指引:[git的rebase命令的作用是什么,怎么去使用,举例说明](https://www.5axxw.com/questions/simple/cpytda)

8. 线程和进程区别?

  1. 定义与本质
    • 进程:进程是操作系统分配资源的基本单位。它包含了一个程序的执行实例,包括代码、数据和系统资源(如内存、文件、设备等)。每个进程都有独立的内存空间和系统资源,相互之间互不干扰。
    • 线程:线程是操作系统调度的基本单位,也是程序执行的基本单位。一个进程内可以包含多个线程,这些线程共享该进程的内存空间和系统资源。线程之间可以并发执行,通过共享内存来直接通信。
  2. 资源占用
    • 进程:每个进程都有独立的内存空间和系统资源,因此进程切换时开销较大,需要保存和恢复较多的上下文信息。
    • 线程:线程共享进程的内存空间和系统资源,线程切换时只需保存和恢复线程的上下文信息,开销相对较小。
  3. 并发性
    • 进程:进程之间是相互独立的,只能通过进程间通信(IPC)机制来交换信息,并发性相对较低。
    • 线程:线程之间共享内存空间,可以直接读写共享数据,因此线程间的并发性更高,通信也更高效。
  4. 独立性
    • 进程:进程是独立的,一个进程出现问题不会影响其他进程的执行。
    • 线程:线程是进程的一部分,一个线程的错误可能导致整个进程的崩溃。
  5. 系统开销
    • 进程:由于进程拥有独立的内存空间和资源,因此创建、销毁和切换进程的开销较大。
    • 线程:线程的创建、销毁和切换开销相对较小,因为线程共享进程的内存和资源。
  6. 使用场景
    • 进程:适用于需要独立资源管理和保护的任务,如多用户或多应用程序环境。
    • 线程:适用于需要高并发和共享内存的任务,如网络服务器、图形界面程序等。

学习指引进程和线程的区别(超详细)

9. Redis为什么选择单线程模型?

  1. 设计目标
    • 简单性:Redis的设计哲学之一就是保持简单。单线程模型避免了多线程同步、死锁、线程间通信等复杂问题,使得代码更容易理解和维护。
    • 快速响应:由于Redis主要作为缓存使用,因此快速响应是非常重要的。单线程模型可以避免多线程切换带来的开销,确保每个操作都能得到快速的处理。
  2. 性能考虑
    • 避免上下文切换:多线程环境中,线程之间的切换会带来额外的性能开销。而在Redis的单线程模型中,由于不存在线程切换,因此可以避免这种开销,从而提高性能。
    • 利用I/O等待时间:Redis的许多操作都是I/O密集型的,如读写磁盘、网络通信等。在这些操作期间,线程通常会处于等待状态。在单线程模型中,Redis可以利用这些等待时间处理其他请求,从而实现高效的并发处理。
  3. 内存使用效率
    • 减少内存占用:多线程环境中,每个线程都需要独立的栈空间,这会增加内存的使用量。而在Redis的单线程模型中,只需要一个线程的栈空间,从而降低了内存占用。
    • 避免内存竞争:多线程环境中,多个线程可能同时访问和修改同一块内存区域,导致数据不一致和性能下降。而在Redis的单线程模型中,由于只有一个线程在执行操作,因此不存在内存竞争的问题。
  4. 多线程的复杂性
    • 同步问题:多线程环境中需要解决同步问题,如互斥锁、读写锁等,以确保数据的一致性和正确性。这增加了代码的复杂性和出错的可能性。而在Redis的单线程模型中,由于不存在并发操作,因此无需解决同步问题。
    • 调试和测试难度:多线程环境中的调试和测试通常比单线程环境更加困难。因为多线程环境中可能存在各种竞态条件和时序问题,这些问题很难在测试和调试过程中被发现和修复。而在Redis的单线程模型中,调试和测试相对简单得多。

Redis选择单线程模型主要是出于设计目标、性能考虑、内存使用效率以及多线程的复杂性等方面的考虑。单线程模型在Redis中具有简单性、高性能、低内存占用和易于调试测试等优势。

Redis 6.0引入了多线程I/O处理功能来进一步提高性能表现。

学习指引为什么 Redis 选择单线程模型

10. RDB和AOF持久化?

1. RDB(Redis DataBase)

RDB持久化是通过将Redis在内存中的数据生成一个快照文件(RDB文件),并保存到硬盘上。当Redis重启时,可以通过加载该快照文件将数据恢复到内存中。这个过程也称为“快照”或“拍快照”。

特点与适用场景

  • 高性能:由于RDB文件是紧凑的二进制格式,且写入时采用了优化策略,因此具有很高的写入和读取性能。
  • 空间效率:相比AOF文件,RDB文件更小,更节省磁盘空间。
  • 适用于备份和灾难恢复:定期生成的RDB快照可以作为数据备份,用于灾难恢复。

优缺点

  • 优点:性能高、文件小、恢复速度快(相对于AOF的重写机制)。
  • 缺点:数据丢失风险较高(由于是定期生成快照,可能会丢失最后一次快照后的数据)、恢复速度可能较慢(如果数据量很大,从磁盘加载到内存可能需要较长时间)。

配置方法

在Redis的配置文件(通常是redis.conf)中,可以设置相关的RDB持久化参数,如save指令定义了触发RDB持久化的条件,dbfilename指定了RDB文件的名称等。完成配置后,需要重启Redis服务器使新的配置生效。

2. AOF(Append Only File)

工作原理

AOF持久化是通过将Redis的写操作以追加的方式保存到一个文件中(即AOF文件)。这个文件包含了所有对Redis的写入操作。当Redis重启时,可以重新执行这些写操作来重建原始数据。这个过程也称为“命令重演”或“日志重放”。

特点与适用场景

  • 数据安全性更高:由于AOF记录了所有的写操作,因此数据丢失的可能性更低。
  • 更适合需要频繁写入的应用:如实时日志记录、聊天应用等。

优缺点

  • 优点:数据安全性高、持久化能力强(可以配置每秒持久化或每个命令执行完就持久化)。
  • 缺点:文件较大(记录了每一个写操作,文件通常比RDB大得多)、性能略低(相对于RDB的快照机制,AOF需要更频繁地写入磁盘)、重写机制可能导致较长时间的延迟。

配置方法

在Redis的配置文件中,可以启用AOF持久化并设置相关参数,如appendonly yes启用AOF,appendfilename指定了AOF文件的名称等。同样地,完成配置后需要重启Redis服务器。

RDB和AOF各有其特点和适用场景。如果更关注数据的安全性和完整性,AOF是更好的选择;如果更关注性能和磁盘空间利用率,RDB可能更合适。

学习指引Redis持久化机制详解

11. Redis的ZSet底层实现?

Redis的有序集合(ZSet)是一种非常有用的数据结构,它允许根据元素的分数(score)进行排序,并且每个元素在集合中是唯一的。ZSet结合了集合(Set)和哈希表(Hash)以及跳跃表(Skip List)或压缩列表(Zip List)的优点,实现了高效的有序数据操作。

数据结构

Redis的ZSet底层主要使用了**跳跃表(Skip List)哈希表(Hash Table)**两种数据结构。

  1. 跳跃表(Skip List)
    • 跳跃表是一种可以用于搜索、插入、删除等操作的数据结构,其性能可以与平衡树相媲美,但实现起来更为简单。
    • 跳跃表由多个“层”组成,每个层都是一个有序链表。最底层(Level 0)包含所有元素,每个高层都是下面一层的子集,且高层中的元素通过某种策略(如随机化)选择。
    • 通过在不同层上进行搜索,跳跃表可以在对数时间复杂度内找到目标元素。
    • 在Redis中,跳跃表用于维护ZSet元素的有序性。
  2. 哈希表(Hash Table)
    • 哈希表用于存储ZSet中的元素与分数之间的映射关系,以及元素与跳跃表节点之间的映射关系。
    • 通过哈希表,Redis可以在常数时间复杂度内查找到元素的分数或跳跃表节点。

插入操作

  1. 计算元素的哈希值,并将其与分数一起存储在哈希表中。
  2. 在跳跃表中插入元素:从最高层开始,逐层向下遍历,直到找到合适的位置插入新元素。如果插入位置所在的层数过低(即该层元素过多),可能会通过随机化策略增加新的层。
  3. 更新哈希表与跳跃表之间的映射关系

删除操作

  1. 在哈希表中查找元素,获取其对应的分数和跳跃表节点。
  2. 在跳跃表中删除该节点:从最高层开始,逐层删除包含该元素的节点,并更新相邻节点的指针。
  3. 更新哈希表与跳跃表之间的映射关系,并删除元素在哈希表中的记录。

查找操作

  1. 根据元素的哈希值在哈希表中查找其对应的分数和跳跃表节点
  2. 如果需要按照分数范围查找元素,则可以在跳跃表中进行范围搜索,利用跳跃表的有序性高效地找到符合条件的元素。

有序性保证

ZSet的有序性主要依赖于跳跃表的有序性。当插入新元素时,Redis会在跳跃表中找到合适的位置插入该元素,确保每层链表仍然保持有序。同时,由于哈希表记录了元素与分数之间的映射关系,因此无论元素在跳跃表中的位置如何变化,都可以通过哈希表快速找到其对应的分数。这种设计使得ZSet在插入、删除和查找操作中都能保持高效且有序。

优化与变体

在某些情况下,当ZSet的大小较小时,Redis可能会使用一种更紧凑的数据结构——**压缩列表(Zip List)**来代替跳跃表和哈希表的组合。压缩列表是一种连续内存结构,它减少了指针的开销,因此在存储小量数据时更加高效。但是,随着数据量的增加,压缩列表的性能会下降,此时Redis会将其转换为跳跃表和哈希表的组合结构。

学习指引【Redis】底层探析 I - Redis 有序集合(ZSet)是如何实现的?

12. Redis主从复制流程?

Redis主从复制允许数据从一个Redis节点(主节点)复制到一个或多个Redis节点(从节点)。可以扩展读性能、进行数据备份和故障恢复。

Redis主从复制的流程

  1. 建立连接阶段
    • 从节点执行SLAVEOF命令或配置文件中设置slaveof选项,指向主节点的IP地址和端口。
    • 从节点向主节点发送一个SYNC命令请求同步数据。
    • 主节点接收到SYNC命令后,使用BGSAVE命令生成一份当前数据库的快照(RDB文件),并同时记录期间的所有写命令。
  2. 数据同步阶段
    • 主节点将生成的RDB快照文件发送给从节点,从节点接收并加载快照文件,这样从节点就有了主节点某一时间点的数据快照。
    • 接着,主节点将快照生成后记录的所有写命令发送给从节点,从节点执行这些写命令,将数据更新至与主节点最新状态一致。
  3. 持续复制阶段
    • 一旦主从节点完成初始数据同步,它们就进入持续复制阶段。
    • 在这个阶段,主节点将其执行的每一个写命令都发送给从节点,从节点实时接收并执行这些命令,从而保持与主节点的数据一致。

写操作在主从节点间的处理机制

  • 写操作(如SET、LPUSH等)只会在主节点上执行。
  • 当主节点接收到写命令并成功执行后,它会将该命令广播给所有从节点。
  • 从节点接收到写命令后,会在自己的数据集中执行该命令,以保持与主节点的数据一致。
  • 这种机制确保了主从节点的数据一致性,并允许从节点扩展读操作的能力。

故障情况下的主从切换

  • Redis本身不提供自动故障切换功能,但可以通过一些外部工具(如Redis Sentinel或Redis Cluster)来实现。
  • Redis Sentinel
    • Sentinel是一个独立的监控和故障切换系统,用于监视Redis主从节点。
    • 当主节点出现故障时,Sentinel会自动选举一个新的主节点,并将其他从节点重新配置为指向新的主节点。
    • Sentinel还提供了通知机制,可以在主从切换时发送通知。
  • Redis Cluster
    • Redis Cluster是一个分布式Redis实现,它提供了自动分区和故障转移功能。
    • 在Cluster中,节点被组织成集群,并且每个节点都持有部分数据。
    • 当某个节点出现故障时,Cluster会自动将其数据迁移到集群中的其他节点,并重新分配数据槽。

学习指引Redis——Redis主从复制(工作流程详解)

13. 布隆过滤器底层实现 , 如何评估大小?

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是不是属于这个集合。布隆过滤器具有误报率(false positive rate),即它可能会错误地判断某个元素属于集合,但肯定不会误报某个元素不属于集合,也就是不存在假阴性(false negative)。

底层实现原理

  1. 初始化:布隆过滤器本质上是一个很长的二进制向量(位数组)和一系列随机映射函数。在初始化时,这个二进制向量每一位都设置为0。
  2. 添加元素:当要向布隆过滤器中添加一个元素时,会使用多个不同的哈希函数对这个元素进行哈希运算,得到多个哈希值。这些哈希值对应于位数组中的位置,随后将这些位置上的二进制位设置为1。
  3. 查询元素:在查询某个元素是否存在于布隆过滤器中时,同样使用相同的哈希函数对该元素进行哈希运算,得到对应的哈希值。然后检查这些哈希值在位数组中对应的位置是否都为1。如果都为1,则认为该元素可能存在于集合中(有概率是误报);如果有任何一个位置不为1,则肯定该元素不在集合中。

评估布隆过滤器所需的大小

评估布隆过滤器所需的大小通常涉及以下几个因素:

  1. 预计插入元素的数量(n):这是决定布隆过滤器大小的主要因素。插入的元素越多,需要的空间就越大。
  2. 可接受的误报率(p):误报率是指布隆过滤器错误地声称某个元素属于集合的概率。误报率越低,需要的空间通常就越大。
  3. 哈希函数的数量(k):哈希函数的数量也会影响到布隆过滤器的大小和性能。哈希函数太少会导致误报率增加,太多则会浪费空间并降低性能。通常需要根据实际情况选择一个合适的k值。
  4. 位数组的长度(m):这是布隆过滤器实际占用的空间大小

学习指引布隆过滤器

14. 用Redis ZSet实现排行榜先用分数再用时间排序怎么实现?

在有序集合中,每个元素都关联着一个分数,通过分数来为集合中的元素提供排序。在这个场景中,可以将这个分数看作是排行榜的排名依。

可以将用户的实际得分和时间戳结合起来,构造一个新的分数。为了避免分数相同的情况,即使两个用户的实际得分相同,也可以通过时间戳来确保他们在有序集合中的唯一性。

构造分数的一个常见方法是使用一个大整数来将实际得分和时间戳结合起来。例如,如果用户的实际得分是一个整数,可以将这个整数与时间戳(单位可以是秒或者毫秒)相乘或者相加,再加上一个适当的偏移量,以确保生成的分数是唯一的。

假设用户的实际得分为score,时间戳为timestamp,可以这样构造新的分数:

new_score = score * 1000000000 + (MAX_INT - timestamp)

这里MAX_INT是一个足够大的整数,用来保证时间戳的位数不会影响到实际得分的比较。选择(MAX_INT - timestamp)是为了让时间戳较小的元素(即较早的元素)在分数相同时排在前面,从而实现按时间升序排序的效果。

当用户产生一个新的得分时,按照上述方法计算出新的分数,然后使用ZADD命令将这个分数和用户ID一起插入到有序集合中:

ZADD ranking_key new_score user_id

这里ranking_key是排行榜的键名,new_score是按照上述方法计算出的新分数,user_id是用户的唯一标识。

由于已经通过构造分数的方式将实际得分和时间戳信息融合到了一个分数中,因此Redis会自动根据这个分数对有序集合中的元素进行排序。只需要使用ZREVRANGE命令就可以获取排行榜的前N名用户:

ZREVRANGE ranking_key 0 N WITHSCORES

这里N是你想要获取的用户数量。WITHSCORES选项表示同时返回用户的分数。

如果你想要查询某个特定用户在排行榜中的位置,可以使用ZREVRANK命令:

ZREVRANK ranking_key user_id

这将返回用户在排行榜中的排名(从0开始计数)。如果用户不在排行榜中,命令将返回nil

15. 如何设计秒杀场景处理高并发以及超卖现象?

秒杀场景是电商系统中常见的营销活动,其特点是用户请求量大、并发度高、时间集中。为保证秒杀活动的顺利进行,需设计一套高效稳定的处理方案,确保系统能够应对高并发请求,同时防止商品超卖。

处理高并发

  1. 前端优化:通过前端技术减少无效请求,如使用倒计时按钮避免用户提前点击,利用本地缓存和懒加载提升页面响应速度。
  2. 流量削峰:引入队列(如Kafka、RabbitMQ)进行流量缓冲,将用户请求异步化处理,平滑瞬时高并发流量。
  3. 分布式限流:利用令牌桶、漏桶等算法在接入层进行限流,保护后端服务不被突发流量冲垮。
  4. 异步下单:用户提交秒杀请求后,立即返回受理成功,后台异步处理下单逻辑,提升用户体验。

库存控制

  1. 库存预热:将秒杀商品库存提前加载到缓存(如Redis)中,减少数据库访问压力。
  2. 库存扣减:采用乐观锁或基于Redis的Lua脚本实现原子性扣减,确保库存数据的一致性。
  3. 库存分层:将库存分为多层,如总库存、渠道库存、活动库存等,各层之间通过同步机制保持数据一致。
  4. 库存回滚:若下单过程中出现异常(如支付失败),需设计库存回滚机制,确保库存数据的准确性。

数据一致性

  1. 最终一致性:在秒杀场景下,可牺牲部分实时性换取系统的高可用和扩展性,通过异步补偿、对账等方式确保数据的最终一致性。
  2. 分布式事务:利用分布式事务框架(如Seata)管理跨服务的事务,保证多个微服务之间的数据一致性。
  3. 数据库主从同步:采用数据库主从复制架构,确保读操作不会受到写操作的阻塞,同时保证数据的一致性。
  4. 监控与告警:对关键指标进行实时监控,如库存变化、请求失败率等,一旦发现问题立即触发告警,及时人工介入处理。

16. 如果对热点数据设置过期时间,活动结束后删除可能会阻塞主线程,怎么解决?

  1. 异步处理
    • 使用异步编程模型,如事件驱动、回调函数、Promises、Futures、async/await等,将删除操作移至后台线程或工作队列中执行。
    • 当主线程需要触发删除操作时,它可以将任务提交给后台线程池,并立即返回,继续处理其他任务。后台线程池将负责在适当的时间执行删除操作,并可以在完成后通知主线程。
  2. 定时任务
    • 使用定时任务(如Cron作业、计划任务或定时服务)来定期清理过期的热点数据。这种方法将删除操作与主线程的运行解耦,确保主线程不会被阻塞。
    • 定时任务可以在系统负载较低的时候执行,以进一步减少对系统性能的影响。
  3. 延迟删除
    • 采用延迟删除策略,即不是立即删除过期的数据,而是在数据被再次访问时检查其过期状态,并进行删除。这种方法可以减少不必要的删除操作,并分散删除操作的负载。
    • 可以使用懒加载的思想,在数据被访问时再进行过期检查和数据清理。
  4. 批量删除
    • 如果必须一次性删除大量数据,可以考虑将数据分批处理。每次只删除一小部分数据,然后释放控制权,让主线程有机会处理其他任务。通过多次迭代完成整个删除过程。
  5. 使用数据库特性
    • 如果数据存储在数据库中,可以利用数据库的内置特性来处理过期数据。例如,许多数据库支持设置数据的TTL(生存时间),在数据过期时自动删除。
    • 数据库通常具有高效的内部机制来处理这类操作,而无需在应用程序层面进行复杂的编程。
  6. 监控和调优
    • 对删除操作进行监控,以便及时发现性能瓶颈或问题。使用性能分析工具来评估删除操作对系统性能的影响。
    • 根据监控结果进行调优,例如调整后台线程池的大小、优化数据库查询、减少不必要的删除等。
  7. 回退策略
    • 设计一个回退策略,以防万一删除操作仍然对系统造成不可接受的影响。这可能包括暂时禁用某些功能、降低服务级别或向用户显示友好的错误消息。

17. 如果用Hash存商品ID和商品数量,当大量请求打过来的时候,商品数量可能变负,还可以用什么数据结构?

在高并发场景下,使用Hash结构(如Redis的Hash)来存储商品ID和商品数量实存在商品数量可能变负的风险。

  1. 原子操作
    • 使用Redis的原子操作命令,如HINCRBY,来递增或递减商品数量。这可以确保即使在多个请求同时到达的情况下,库存更新也是原子的,从而避免竞态条件。
  2. Lua脚本
    • 将多个操作封装到一个Lua脚本中,并在Redis中执行。由于Redis保证Lua脚本的原子性执行,这可以防止在脚本执行过程中被其他命令打断,从而确保数据的一致性。
  3. 分布式锁
    • 使用分布式锁(如Redis的SETNX命令或RedLock算法)来确保同一时间只有一个请求能够更新特定商品的库存。这可以避免并发更新导致的问题,但可能会降低系统的吞吐量。
  4. 消息队列
    • 引入消息队列(如Kafka、RabbitMQ等),将库存更新请求放入队列中,并由后台工作线程按顺序处理。这可以将并发请求转换为串行处理,从而消除竞态条件,但可能会增加系统的复杂性和延迟。
  5. 数据库事务
    • 如果你的应用使用了关系型数据库,并且库存信息也存储在数据库中,可以考虑使用数据库事务来确保库存更新的原子性。这通常涉及到开启一个事务,进行库存检查和更新,然后提交或回滚事务。
  6. 使用专门的库存服务
    • 将库存管理逻辑封装到一个独立的服务中,该服务负责处理所有的库存更新请求。这可以通过使用微服务架构来实现,其中库存服务是一个高度可用和可扩展的服务,能够处理大量的并发请求。
全部评论

相关推荐

8 55 评论
分享
牛客网
牛客企业服务