后端开发面经系列--Shopee秋招C++一二面面经

Shopee秋招C++一二面面经

公众号:阿Q技术站

https://www.nowcoder.com/feed/main/detail/f3b7e9fa48854f34b3647fcb9168c67b

一面

1、数据库中的hash索引?

哈希索引(Hash Index)是其中一种特殊的索引类型,它基于哈希表(Hash Table)的数据结构实现。哈希索引主要用于加速等值查询(例如使用 =IN 等操作符的查询),但在其他类型的查询(如范围查询、排序等)中表现不佳。

哈希索引的工作原理

哈希索引的核心在于哈希函数(Hash Function)。哈希函数将索引列的值转换成一个固定长度的哈希码(Hash Code)。这个哈希码随后被用作哈希表中的键,哈希表中的值则是指向数据行的指针。当执行查询时,数据库会先计算查询条件的哈希值,然后在哈希表中查找对应的哈希码,进而快速定位到目标数据行。如果多个不同的索引列值产生了相同的哈希码(即发生了哈希冲突),这些行会被存储在一个链表中,查询时需要遍历链表以找到符合条件的行。

例如,考虑一个简单的哈希索引示例:

CREATE TABLE testhash (
  fname VARCHAR(50) DEFAULT NULL,
  lname VARCHAR(50) DEFAULT NULL,
  KEY `fname` (`fname`) USING HASH
) ENGINE=MEMORY;

在这个例子中,fname 列上的哈希索引将帮助快速查找特定名字的记录。

哈希索引的优势
  1. 查询速度快:由于哈希索引直接利用哈希表进行查找,理想情况下可以在常数时间内(O(1))找到目标数据行,这使得等值查询非常高效。
  2. 索引结构紧凑:哈希索引仅存储哈希码和行指针,不存储实际的字段值,因此索引结构非常紧凑,占用的存储空间较少。
哈希索引的局限性
  1. 不支持范围查询:哈希索引不支持范围查询(如><BETWEEN等),因为哈希表中的数据不是按照索引值的顺序存储的。
  2. 不支持部分索引列匹配:哈希索引要求查询条件必须完全匹配索引列的所有列,部分匹配查询(如只使用索引列的一部分)无法利用哈希索引。
  3. 哈希冲突:当不同的索引列值产生相同的哈希码时,会发生哈希冲突。虽然哈希索引通过链表解决了冲突问题,但冲突越多,查询效率越低。
  4. 不支持排序:由于哈希表中的数据不是按顺序存储的,哈希索引无法用于排序操作。
哈希索引的应用场景
  1. 等值查询:当查询主要涉及等值比较(如=IN)时,哈希索引可以显著提升查询性能。
  2. 长字符串查询:对于包含大量长字符串的表,可以使用哈希索引优化查询性能。例如,存储大量URL信息的表可以通过计算URL的哈希值并建立哈希索引来加速查询。
  3. 高并发读取:在高并发读取场景中,哈希索引可以提供快速的查询响应时间。
自适应哈希索引

InnoDB存储引擎提供了一种名为自适应哈希索引(Adaptive Hash Index)的功能。当存储引擎注意到某列频繁被访问时,会自动在该列上建立哈希索引。这样,InnoDB引擎就同时拥有了B-Tree索引和哈希索引,可以在不同类型的查询中选择最优的索引类型。自适应哈希索引是一个无需人工干预的自动行为,可以进一步提升查询性能。

2、B+Tree与hash区别、对比?

1. B+Tree 索引
1.1 结构

B+Tree 是一种自平衡的多路搜索树,通常用于文件系统和数据库中。B+Tree 的主要特点包括:

  • 节点结构:每个节点可以包含多个关键字和子节点指针。关键字的数量和子节点的数量相等,且关键字按顺序排列。
  • 叶子节点:所有叶子节点都位于同一层,并且通过指针相互连接,形成一个链表。叶子节点包含实际的数据记录指针。
  • 非叶子节点:非叶子节点仅用作索引,不存储实际数据,只存储关键字和子节点指针。
1.2 查询过程
  • 等值查询:从根节点开始,逐层向下查找,直到找到目标关键字所在的叶子节点。
  • 范围查询:由于叶子节点按顺序排列并通过指针连接,可以高效地进行范围查询。
  • 排序:B+Tree 的结构使得数据自然有序,支持高效的排序操作。
1.3 优点
  • 支持多种查询:B+Tree 支持等值查询、范围查询和排序操作。
  • 数据有序:叶子节点按顺序排列,支持高效的范围查询和排序。
  • 稳定性:插入和删除操作的时间复杂度为 O(log n),保证了索引的稳定性。
1.4 缺点
  • 查询速度:相对于哈希索引,B+Tree 的查询速度较慢,因为需要逐层遍历树结构。
  • 存储开销:B+Tree 需要存储关键字和子节点指针,占用更多的存储空间。
2. 哈希索引
2.1 结构

哈希索引基于哈希表(Hash Table)的数据结构实现,主要特点包括:

  • 哈希函数:将索引列的值通过哈希函数转换为一个固定长度的哈希码。
  • 哈希表:哈希码作为键,存储指向数据行的指针。
  • 链表:当多个不同的索引列值产生相同的哈希码时,这些行会被存储在一个链表中。
2.2 查询过程
  • 等值查询:计算查询条件的哈希值,直接在哈希表中查找对应的哈希码,进而快速定位到目标数据行。
  • 哈希冲突:如果发生哈希冲突,需要遍历链表以找到符合条件的行。
2.3 优点
  • 查询速度快:在理想情况下,哈希索引可以在常数时间内(O(1))找到目标数据行,适用于等值查询。
  • 索引结构紧凑:哈希索引仅存储哈希码和行指针,不存储实际的字段值,占用的存储空间较少。
2.4 缺点
  • 不支持范围查询:哈希索引不支持范围查询(如 ><BETWEEN 等),因为哈希表中的数据不是按顺序存储的。
  • 不支持部分索引列匹配:哈希索引要求查询条件必须完全匹配索引列的所有列,部分匹配查询无法利用哈希索引。
  • 哈希冲突:当不同的索引列值产生相同的哈希码时,会发生哈希冲突,导致查询效率下降。
  • 不支持排序:哈希表中的数据不是按顺序存储的,哈希索引无法用于排序操作。
3. B+Tree 索引与哈希索引的对比
3.1 查询性能
  • 等值查询:哈希索引在等值查询中表现更好,查询速度更快,时间复杂度为 O(1)。B+Tree 索引的等值查询需要逐层遍历树结构,时间复杂度为 O(log n)。
  • 范围查询:B+Tree 索引支持高效的范围查询,因为叶子节点按顺序排列并通过指针连接。哈希索引不支持范围查询。
  • 排序:B+Tree 索引支持高效的排序操作,因为数据自然有序。哈希索引不支持排序操作。
3.2 存储开销
  • B+Tree 索引:需要存储关键字和子节点指针,占用更多的存储空间。
  • 哈希索引:仅存储哈希码和行指针,占用的存储空间较少。
3.3 插入和删除操作
  • B+Tree 索引:插入和删除操作的时间复杂度为 O(log n),保证了索引的稳定性。
  • 哈希索引:插入和删除操作的时间复杂度为 O(1),但在哈希冲突较多的情况下,性能会受到影响。
3.4 适用场景
  • B+Tree 索引:适用于需要支持多种查询(等值查询、范围查询、排序)的场景,如数据库中的主键索引和二级索引。
  • 哈希索引:适用于主要进行等值查询的场景,如缓存系统、高并发读取场景

3、MySQL一个txn的执行流程?

在 MySQL 中,事务处理是一种确保数据库中一系列操作要么全部成功执行,要么全部回滚的机制。事务处理的关键在于保证数据的一致性和完整性。

1. 事务的基本概念

事务是 MySQL 中一种重要的机制,它允许将一系列操作作为一个整体执行,要么全部成功,要么全部失败。事务处理的目的是确保数据库的一致性和完整性。事务具有 ACID 特性:

  • 原子性(Atomicity):事务是一个不可分割的工作单位,事务中的所有操作要么全部执行,要么全部不执行。
  • 一致性(Consistency):事务必须使数据库从一个一致状态转换到另一个一致状态。
  • 隔离性(Isolation):事务的执行不受其他事务的干扰,每个事务都独立运行。
  • 持久性(Durability):一旦事务提交,其对数据库的更改将是永久的,即使系统发生故障也不会丢失。
2. 事务的执行流程
2.1 开始事务

事务的开始可以通过以下命令显式地开启:

  • BEGIN;START TRANSACTION;
  • SET autocommit = 0; (禁止自动提交)

当事务开始时,MySQL 会创建一个事务上下文,用于跟踪事务中的所有操作。

2.2 执行 SQL 语句

在事务中,可以执行各种 SQL 语句,如 INSERTUPDATEDELETE 等。这些操作会被暂存,直到事务提交或回滚。在这个阶段,MySQL 会进行以下步骤:

  1. 连接器:验证用户的身份和权限,确保用户有权限执行这些操作。
  2. 分析器:解析 SQL 语句,生成解析树。
  3. 优化器:生成执行计划,选择最优的执行路径。
  4. 执行器:根据执行计划,调用存储引擎的 API 执行 SQL 语句。
2.3 事务的提交

事务的提交可以通过以下命令显式地提交:

  • COMMIT;COMMIT WORK;

当事务提交时,MySQL 会执行以下步骤:

  1. 两阶段提交:
    • Prepare 阶段:将事务的 XID(内部 XA 事务的 ID)写入 redo log,并将 redo log 的事务状态设置为 prepare,然后将 redo log 持久化到磁盘。
    • Commit 阶段:将 XID 写入 binlog,然后将 binlog 持久化到磁盘;调用事务引擎的提交事务接口,将 redo log 状态设置为 commit。
  2. 持久化:确保事务的更改被永久保存到数据库中。即使系统发生故障,事务的更改也不会丢失。
2.4 事务的回滚

事务的回滚可以通过以下命令显式地回滚:

  • ROLLBACK;ROLLBACK WORK;

当事务回滚时,MySQL 会撤销所有未提交的更改,并将数据库恢复到事务开始之前的状态。在这个阶段,MySQL 会执行以下步骤:

  1. 撤销更改:撤销事务中所有已执行的 SQL 语句的更改。
  2. 释放资源:释放事务持有的锁和其他资源。
2.5 事务的隔离级别

事务的隔离级别决定了事务之间的可见性和并发性。MySQL 支持以下四种隔离级别:

  • Read Uncommitted:允许脏读,事务可以读取其他事务未提交的数据。
  • Read Committed:允许不可重复读,事务只能读取其他事务已提交的数据。
  • Repeatable Read:默认隔离级别,允许幻读,事务在同一个事务中多次读取同一数据时,结果是一致的。
  • Serializable:最高隔离级别,事务完全隔离,不允许任何并发操作。

4、并发一致性,MVCC怎么实现?

1. 并发一致性概述

并发一致性是指在多个事务并发执行时,保证数据库的一致性状态。传统的锁机制(如行锁、表锁)虽然可以防止并发冲突,但会严重影响系统的并发性能。MVCC 通过维护数据的多个版本,允许读操作不加锁,从而提高并发性能。

2. MVCC 的基本原理
2.1 多版本数据

MVCC 的核心思想是通过保存数据的多个版本来支持并发读写。每个事务看到的数据版本是不同的,这取决于事务的开始时间。当一个事务对数据进行修改时,它不会直接覆盖原始数据,而是创建一个新的版本。其他事务可以继续访问旧版本的数据,而不会受到正在进行的修改的影响。

2.2 读视图(Read View)

为了实现多版本数据的管理和访问,MVCC 引入了读视图(Read View)的概念。读视图记录了事务在开始时系统中活跃事务的快照,包括事务 ID 列表。读操作根据读视图来决定可以看到哪些版本的数据。

2.3 版本链

每个数据行都有一个版本链,记录了该行的所有历史版本。每个版本包含以下信息:

  • 事务 ID:创建该版本的事务 ID。
  • 回滚指针:指向该版本的上一个版本。

版本链通过回滚指针连接,形成一个链表结构。读操作通过版本链找到符合读视图的数据版本。

3. MVCC 在 MySQL 中的实现
3.1 隐藏字段

在 MySQL 的 InnoDB 存储引擎中,每个数据行包含几个隐藏字段,用于支持 MVCC:

  • DB_TRX_ID:6 字节,记录最近修改该行的事务 ID。
  • DB_ROLL_PTR:7 字节,回滚指针,指向该行的上一个版本。
  • DB_ROW_ID:6 字节,隐含的自增 ID,如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引。
3.2 快照读与当前读
  • 快照读:不加锁的读操作,通过读视图和版本链找到符合读视图的数据版本。快照读主要用于 SELECT 语句。
  • 当前读:加锁的读操作,读取数据的最新版本,并对其进行加锁。当前读主要用于SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEINSERTUPDATEDELETE语句。
3.3 读视图的生成

当一个事务开始时,InnoDB 会生成一个读视图,记录系统中所有活跃事务的事务 ID 列表。读视图包含以下信息:

  • m_ids:事务开始时系统中活跃事务的事务 ID 列表。
  • min_trx_id:事务开始时系统中最小的未提交事务 ID。
  • max_trx_id:事务开始时系统中最大的未提交事务 ID。
  • creator_trx_id:生成读视图的事务 ID。
3.4 版本链的管理
  • 插入操作:插入新行时,生成一个新的版本,记录当前事务的事务 ID。
  • 更新操作:更新行时,生成一个新的版本,记录当前事务的事务 ID,并将回滚指针指向旧版本。
  • 删除操作:删除行时,标记行的删除标志,并生成一个新的版本,记录当前事务的事务 ID。
3.5 读操作的处理
  • 快照读:根据读视图和版本链找到符合读视图的数据版本。
    • 如果数据行的 DB_TRX_ID 小于 min_trx_id,说明该版本在事务开始前已经提交,可以读取。
    • 如果数据行的 DB_TRX_ID 大于 max_trx_id,说明该版本在事务开始后生成,不能读取。
    • 如果数据行的 DB_TRX_ID 在 m_ids 列表中,说明该版本由其他未提交的事务生成,不能读取。
    • 如果数据行的 DB_TRX_ID 等于 creator_trx_id,说明该版本由当前事务生成,可以读取。
  • 当前读:读取数据的最新版本,并对其进行加锁。
3.6 版本的回收

为了节省存储空间,InnoDB 会定期回收不再需要的旧版本数据。回收的条件是:

  • 旧版本数据对所有活跃事务都不再可见。
  • 旧版本数据没有被其他事务的回滚操作使用。
4. MVCC 的优缺点
4.1 优点
  • 提高并发性能:读操作不加锁,允许读写操作同时进行,提高系统的并发性能。
  • 支持多种隔离级别:通过读视图和版本链,支持不同级别的事务隔离,如 READ COMMITTEDREPEATABLE READ
  • 减少锁竞争:减少读写操作之间的锁竞争,提高系统的响应能力。
4.2 缺点
  • 存储开销:需要存储多个版本的数据,占用更多的存储空间。
  • 复杂性:实现和管理多版本数据增加了系统的复杂性。

5、SIX lock是什么,可以在哪些级别上锁,MySQL原子性怎么实现?

SIX 锁(意向排他共享锁)是数据库中一种特殊的锁类型,主要用于管理对共享资源的并发访问。它结合了共享锁(S锁)和排他锁(X锁)的特性,允许事务在不同层级上对资源进行加锁,从而提高并发性能和资源利用率。SIX 锁在数据库中通常用于以下场景:

  1. 意向锁:意向锁用于表示事务对某个资源有进一步加锁的意图。SIX 锁是一种意向锁,表示事务对某个资源有获取共享锁和排他锁的意图。
  2. 资源层次结构:SIX 锁可以在资源的多个层次上加锁,例如表级、页级和行级。
1. SIX 锁的定义

意向排他共享 (SIX):保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁以及针对某些(而并非所有)低层资源请求或获取的意向排他锁。顶级资源允许使用并发 IS 锁。

2. SIX 锁的用途

SIX 锁的主要用途是在事务中对资源进行多层次的加锁,确保事务在不同层级上对资源的访问不会发生冲突。具体来说:

  • 共享锁(S锁):允许事务读取资源,但不允许修改。
  • 排他锁(X锁):允许事务修改资源,但不允许其他事务读取或修改。
  • 意向锁:表示事务对资源有进一步加锁的意图,可以是共享锁或排他锁。

SIX 锁允许事务在顶级资源上加共享锁的同时,对某些低层资源加排他锁,从而实现更细粒度的并发控制。

3. SIX 锁的加锁级别

SIX 锁可以在多个级别上加锁,常见的加锁级别包括:

  • 表级:对整个表加锁,通常用于防止其他事务对表进行修改。
  • 页级:对表的某个页面加锁,通常用于防止其他事务对页面中的数据进行修改。
  • 行级:对表的某一行加锁,通常用于防止其他事务对行中的数据进行修改。
MySQL 原子性的实现

原子性(Atomicity)是数据库事务的四个基本特性之一(ACID),确保事务中的所有操作要么全部完成,要么全部不完成。MySQL 通过以下机制实现事务的原子性:

1. 事务管理

MySQL 使用事务来管理多个操作作为一个逻辑单元。事务是一系列的数据库操作,被视为一个单一的逻辑工作单元。事务的原子性确保这些操作全部成功完成,或者在出现错误时全部回滚。

2. 回滚日志(Undo Log)
  • Undo Log:回滚日志记录了事务执行前的数据状态,当事务执行过程中出现错误,或者用户执行 ROLLBACK 语句进行事务回滚时,可以利用 Undo Log 中的信息将数据库恢复到事务开始前的状态。
  • 记录内容:Undo Log 记录了事务对数据的修改操作,包括修改前的数据值和修改后的数据值。
  • 存储位置:Undo Log 通常存储在 InnoDB 的系统表空间中,或者在独立的 Undo 表空间中。
3. 重做日志(Redo Log)
  • Redo Log:重做日志记录了事务中所有的修改操作,用于在数据库异常宕机后恢复正在执行中的事务。
  • 记录内容:Redo Log 记录了事务对数据的物理修改操作,包括修改的数据页号、偏移量和修改后的数据值。
  • 存储位置:Redo Log 通常存储在 InnoDB 的重做日志文件中,这些文件位于 MySQL 的数据目录中。
4. 事务提交与回滚
  • 事务提交:当事务执行成功并调用 COMMIT 时,MySQL 会将 Redo Log 中的记录写入磁盘,确保数据的一致性和持久性。
  • 事务回滚:当事务执行过程中出现错误,或者用户执行 ROLLBACK 时,MySQL 会利用 Undo Log 中的信息将数据恢复到事务开始前的状态。

6、MySQL中的Binlog说说?

1. Binlog 简介

Binlog(二进制日志)是 MySQL 数据库中非常重要的日志文件,用于记录数据库的所有更改操作,包括 DDL(数据定义语言)和 DML(数据操作语言)语句,但不包括数据查询语句(如 SELECTSHOW 等)。Binlog 以二进制格式存储,包含对数据库执行的所有修改操作的详细信息,如插入、更新、删除等。

2. Binlog 的作用

Binlog 在 MySQL 中有以下几个主要作用:

  1. 数据恢复:通过重放 Binlog 中的事件,可以将数据库恢复到特定的时间点。这对于恢复误删数据、应对错误的批量操作等情况非常有用。
  2. 主从复制:在主从复制中,主服务器将所有的更改记录到 Binlog 中,而从服务器通过读取主服务器的 Binlog 并执行相同的更改来保持数据同步。
  3. 审计:用户可以通过 Binlog 中的信息进行审计,判断是否有对数据库进行注入攻击或其他恶意操作。
3. Binlog 的存储格式

MySQL 支持三种 Binlog 格式,每种格式有不同的特点和适用场景:

  1. Statement 格式:基于 SQL 语句的复制(statement-based replication, SBR)。每个会修改数据的 SQL 语句都会记录在 Binlog 中。优点是日志文件小,可读性强,但某些复杂问题可能会导致主从不一致。
  2. Row 格式:基于行的复制(row-based replication, RBR)。记录的是数据行的变更,会包含变更前后的内容。优点是可以精确记录行数据,避免 Statement 格式可能出现的数据不一致的问题,但日志文件大,可读性差。
  3. Mixed 格式:混合模式复制(mixed-based replication, MBR)。根据具体情况自动选择 Statement 或 Row 类型来记录数据更改。兼顾了 Statement 和 Row 两种类型的优点,既可以节约空间,也可以精确记录数据变更。
4. Binlog 的配置和管理
  1. 开启 Binlog:在 MySQL 配置文件(通常是 my.cnfmy.ini)中添加以下内容:

    [mysqld]
    log-bin=mysql-bin
    server-id=1
    

    其中 log-bin 参数表示启用 Binlog,并指定 Binlog 文件的名称前缀。server-id 参数表示服务器的唯一标识符,必须在主从复制中设置为不同的值。

  2. 设置 Binlog 文件大小和保留时间:

    max_binlog_size=100M
    expire_logs_days=7
    

    max_binlog_size 参数表示单个 Binlog 文件的最大大小,当文件大小超过此值时,MySQL 会创建一个新的 Binlog 文件。expire_logs_days 参数表示 Binlog 文件的保留天数,超过此天数的 Binlog 文件将被自动删除。

  3. 查看 Binlog 状态:

    SHOW MASTER STATUS;
    SHOW BINARY LOGS;
    

    SHOW MASTER STATUS 命令显示当前 Binlog 文件的状态,包括文件名和位置。SHOW BINARY LOGS 命令显示所有 Binlog 文件的列表。

  4. 解析 Binlog 内容:使用 mysqlbinlog 工具可以解析 Binlog 文件的内容,将其转换为可读的 SQL 语句:

    mysqlbinlog /path/to/mysql-bin.000001
    

    通过解析 Binlog 内容,可以进行数据恢复、审计等操作。

5. Binlog 的实际应用
  1. 数据恢复:通过重放 Binlog 中的事件,可以将数据库恢复到特定的时间点。例如,可以使用 mysqlbinlog 工具导出 Binlog 文件,然后将其应用于数据库:

    mysqlbinlog /path/to/mysql-bin.000001 | mysql -u root -p
    

    这种方法适用于恢复误删数据或应对错误的批量操作。

  2. 主从复制:在主从复制中,主服务器将所有的更改记录到 Binlog 中,从服务器通过读取主服务器的 Binlog 并执行相同的更改来保持数据同步。配置主从复制的步骤如下:

    • 主库操作:

      CREATE USER 'repl' IDENTIFIED BY 'password';
      GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%' IDENTIFIED BY 'password';
      FLUSH PRIVILEGES;
      SHOW MASTER STATUS;
      
    • 从库操作:

      CHANGE MASTER TO
        MASTER_HOST='master_host_name',
        MASTER_USER='repl',
        MASTER_PASSWORD='password',
        MASTER_LOG_FILE='recorded_log_file_name',
        MASTER_LOG_POS=recorded_log_position;
      START SLAVE;
      SHOW SLAVE STATUS;
      

    通过这些步骤,可以实现主从复制,确保数据的一致性。

  3. 审计:通过解析 Binlog 内容,可以进行审计,判断是否有对数据库进行注入攻击或其他恶意操作。例如,可以定期检查 Binlog 文件,查找可疑的 SQL 语句。

6. Binlog 的性能影响

开启 Binlog 会稍微降低 MySQL 的性能,大约有 1% 的性能损耗。因此,在生产环境中,需要权衡数据安全性和性能之间的关系。

7. Binlog 的安全性和管理
  1. 安全性:Binlog 文件包含敏感的数据库操作信息,因此需要妥善保管,防止未授权访问。可以通过设置文件权限和加密等方式增强安全性。
  2. 管理:定期检查和清理 Binlog 文件,避免占用过多的磁盘空间。可以通过设置 expire_logs_days 参数自动删除过期的 Binlog 文件。

7、数据库主从复制怎么实现?

1. 概述

主从复制(Master-Slave Replication)是数据库管理系统中一种常见的高可用性和数据冗余策略。其核心思想是将主数据库(Master)的操作日志同步到从数据库(Slave),从而确保从数据库的数据与主数据库保持一致。这种机制不仅提高了系统的可用性和数据安全性,还可以通过读写分离来提高数据库的性能。

2. 工作原理

主从复制涉及以下几个主要步骤:

  1. 主库记录日志:主库在执行任何数据更改操作(如 INSERTUPDATEDELETE)时,会将这些操作记录到二进制日志(Binary Log)中。
  2. 从库获取日志:从库通过一个 I/O 线程连接到主库,请求并接收主库的二进制日志。主库会为每个从库创建一个 Binlog Dump 线程,负责发送二进制日志内容给从库。
  3. 从库写入中继日志:从库的 I/O 线程将接收到的二进制日志内容写入到本地的中继日志(Relay Log)中。
  4. 从库应用日志:从库通过一个 SQL 线程读取中继日志中的内容,并在本地执行相同的更改操作,从而实现数据同步。

示例可以上一道题。

8、数据库dump log的时间节点(异步或同步?什么是异步,具体说说)?

1. Dump Log 的时间节点

在数据库主从复制过程中,Dump Log 是指主库将二进制日志(Binary Log)发送给从库的过程。这个过程的时间节点非常重要,因为它直接影响到主从复制的延迟和数据一致性。

1.1 异步复制

在异步复制(Asynchronous Replication)中,主库在执行完客户端提交的事务后,会立即将结果返回给客户端,并不关心从库是否已经接收并处理了这些日志。具体来说,主库的 Binlog Dump 线程会将二进制日志发送给从库,但主库不会等待从库的确认。

时间节点

  • 主库提交事务:主库在执行完客户端提交的事务后,将事务记录到二进制日志中。
  • 发送日志:主库的 Binlog Dump 线程将二进制日志发送给从库。
  • 返回结果:主库立即将结果返回给客户端,不等待从库的确认。
  • 从库接收日志:从库的 I/O 线程接收主库发送的二进制日志,并将其写入中继日志(Relay Log)。
  • 从库应用日志:从库的 SQL 线程读取中继日志中的内容,并在本地执行相同的更改操作。

优点

  • 性能高:主库不需要等待从库的确认,可以快速返回结果,提高了事务的处理速度。
  • 简单:实现和配置相对简单,对网络延迟的容忍度较高。

缺点

  • 数据不一致风险:如果主库在发送日志后立即宕机,而从库尚未接收到这些日志,可能会导致数据不一致。
  • 延迟:从库的数据可能会滞后于主库,尤其是在网络延迟较大的情况下。
1.2 半同步复制

半同步复制(Semi-Synchronous Replication)是一种改进的复制模式,它要求主库在提交事务之前,至少有一个从库确认收到了日志并写入到中继日志中。这种方式可以减少主从延迟,提高数据的一致性。

时间节点

  • 主库提交事务:主库在执行完客户端提交的事务后,将事务记录到二进制日志中。
  • 发送日志:主库的 Binlog Dump 线程将二进制日志发送给从库。
  • 等待确认:主库等待从库的确认,确认从库已经接收到日志并写入中继日志。
  • 返回结果:主库在收到从库的确认后,将结果返回给客户端。
  • 从库接收日志:从库的 I/O 线程接收主库发送的二进制日志,并将其写入中继日志。
  • 从库应用日志:从库的 SQL 线程读取中继日志中的内容,并在本地执行相同的更改操作。

优点

  • 数据一致性:主库在返回结果前确保至少有一个从库接收到日志,减少了数据不一致的风险。
  • 低延迟:主从之间的延迟较低,适合对数据一致性要求较高的场景。

缺点

  • 性能略低:主库需要等待从库的确认,增加了事务的处理时间。
  • 复杂性:实现和配置相对复杂,对网络延迟的容忍度较低。
2. 什么是异步?

异步(Asynchronous)是与同步(Synchronous)相对的概念。在计算机编程中,异步操作指的是可以与当前程序同时执行的操作。具体来说,异步操作的特点是:

  • 不阻塞:异步操作不会阻塞当前程序的执行,调用方可以在发起异步操作后立即继续执行其他任务。
  • 回调机制:通常通过回调函数、事件监听器、Promise 等机制来处理异步操作的结果。
  • 并发性:异步操作允许多个任务并发执行,提高了程序的效率和响应速度。

举例

  • 文件读取:在同步调用中,文件读取操作会阻塞程序的执行,直到读取完成。而在异步调用中,文件读取操作会立即返回,程序可以继续执行其他任务,当文件读取完成后通过回调函数处理结果。
  • 网络请求:在同步调用中,网络请求会阻塞程序的执行,直到请求完成。而在异步调用中,网络请求会立即返回,程序可以继续执行其他任务,当请求完成后通过回调函数处理结果。

应用场景

  • I/O 操作:如文件读写、网络通信等耗时操作,适合使用异步方式,避免阻塞主线程。
  • 高并发系统:在高并发系统中,异步操作可以提高系统的吞吐量和响应速度。
3. 异步在数据库主从复制中的应用

在数据库主从复制中,异步复制是一种常见的实现方式。主库在提交事务后立即将结果返回给客户端,不等待从库的确认。这种方式的优点是性能高、实现简单,但缺点是数据不一致风险较高。为了平衡性能和数据一致性,可以使用半同步复制,它在一定程度上保证了数据的一致性,同时保持了较高的性能。

9、详细解释Raft原理(leader、follower、选举那些)?

Raft 是一种用于管理分布式系统中多副本状态机的日志复制算法。它旨在通过简化 Paxos 等经典一致性算法的设计,使其更易于理解和实现。Raft 算法的核心目标是确保分布式系统中的所有节点在面对网络分区、节点故障等异常情况时,能够达成一致的状态。

1. Raft 算法概述

Raft 算法将分布式一致性问题分解为以下几个子问题:

  • Leader 选举(Leader Election)
  • 日志复制(Log Replication)
  • 安全性保证(Safety)

Raft 算法通过将系统中的节点分为三种角色来实现这些子问题:

  • Leader:负责处理客户端请求,并将日志条目复制到其他节点。
  • Follower:被动接收 Leader 的日志复制和心跳信息,不主动参与请求处理。
  • Candidate:在选举过程中尝试成为 Leader。
2. 节点角色
2.1 Leader
  • 职责
    • 处理客户端请求。
    • 将日志条目复制到其他节点。
    • 定期发送心跳信息(Heartbeat)以维持其 Leader 地位。
    • 确保日志的一致性。
  • 特点
    • 一个任期内只能有一个 Leader。
    • Leader 通过心跳信息来防止其他节点发起新的选举。
    • 如果 Leader 故障,系统会通过选举产生新的 Leader。
2.2 Follower
  • 职责
    • 被动接收 Leader 和 Candidate 的消息。
    • 处理 Leader 的日志复制和心跳信息。
    • 如果在选举超时时间内没有收到 Leader 的心跳信息,会转变为 Candidate。
  • 特点
    • 不主动发送消息。
    • 依赖 Leader 的心跳信息来维持其 Follower 状态。
2.3 Candidate
  • 职责
    • 在选举超时时间内没有收到 Leader 的心跳信息时,转变为 Candidate。
    • 发起选举,请求其他节点的投票。
    • 如果赢得大多数节点的投票,成为新的 Leader。
  • 特点
    • 选举过程中临时的角色。
    • 每个节点在一个任期内只能投一票。
3. Leader 选举
3.1 选举超时
  • 定义:每个 Follower 都有一个随机的选举超时时间(Election Timeout),通常在 150ms 到 300ms 之间。
  • 作用:如果 Follower 在选举超时时间内没有收到 Leader 的心跳信息,会转变为 Candidate 并发起选举。
3.2 选举过程
  1. 转变为 Candidate:
    • Follower 在选举超时时间内没有收到 Leader 的心跳信息,会将自己的任期编号(Term)加 1,并转变为 Candidate。
    • Candidate 会给自己投票,并向其他节点发送请求投票(RequestVote)RPC。
  2. 请求投票:
    • Candidate 向其他节点发送 RequestVote RPC,请求投票。
    • RequestVote RPC 包含当前任期编号、Candidate 的日志信息(最后一个日志条目的索引和任期编号)。
  3. 处理投票请求:
    • 接收节点在收到 RequestVote RPC 后,会根据以下条件决定是否投票:
      • 如果接收节点的当前任期编号小于 RequestVote RPC 中的任期编号,会更新自己的任期编号。
      • 如果接收节点已经投票给其他节点,会拒绝投票。
      • 如果接收节点的日志至少和 Candidate 的日志一样新(即最后一个日志条目的任期编号更大或相等,且索引更大或相等),会投票给 Candidate。
  4. 赢得选举
    • 如果 Candidate 收到大多数节点的投票,会成为新的 Leader。
    • 新 Leader 会立即发送心跳信息,告知其他节点自己是新的 Leader。
  5. 选举失败
    • 如果没有节点赢得大多数投票,选举会失败,所有节点会重置选举超时时间并重新开始选举。
3.3 随机选举超时
  • 目的:通过随机选举超时时间,降低多个节点同时发起选举的概率,避免选举冲突。
  • 实现:每个 Follower 的选举超时时间在 150ms 到 300ms 之间随机生成。
4. 日志复制
4.1 日志结构
  • 日志条目:每个日志条目包含索引(Index)、任期编号(Term)和命令(Command)。
  • 索引:严格递增的数字,表示日志条目的位置。
  • 任期编号:表示日志条目在哪个任期内创建。
  • 命令:表示要执行的操作。
4.2 日志复制过程
  1. 客户端请求:

    • 客户端向 Leader 发送请求。
    • Leader 将请求作为日志条目记录到自己的日志中。
  2. 发送日志条目:

    • Leader 通过 AppendEntries RPC 将日志条目发送给所有 Follower。
    • AppendEntries RPC 包含当前任期编号、前一个日志条目的索引和任期编号、新的日志条目。
  3. 处理 AppendEntries RPC:

    Follower 在收到 AppendEntries RPC 后,会根据以下条件决定是否接受新的日志条目:

    • 如果前一个日志条目的索引和任期编号与 Follower 的日志匹配,会接受新的日志条目。
    • 如果前一个日志条目的索引和任期编号不匹配,会拒绝新的日志条目,并返回前一个日志条目的索引。
  4. 提交日志条目:

    • 当 Leader 收到大多数节点的确认后,会将日志条目标记为已提交(Committed)。
    • Leader 会通过 AppendEntries RPC 通知其他节点日志条目已提交。
    • Follower 在收到 AppendEntries RPC 后,会将已提交的日志条目应用到状态机。
5. 安全性保证
5.1 选举限制
  • 最新日志条目:只有拥有最新已提交日志条目的 Follower 才有资格成为 Candidate。
  • 日志比较:在 RequestVote RPC 中,接收节点会比较自己和 Candidate 的日志,确保 Candidate 的日志至少和自己一样新。
5.2 日志一致性
  • 日志匹配:AppendEntries RPC 通过前一个日志条目的索引和任期编号来确保日志的一致性。
  • 日志覆盖:如果 Follower 的日志与 Leader 的日志不一致,Leader 会通过 AppendEntries RPC 覆盖 Follower 的日志,确保日志的一致性。
6. 异常处理
6.1 网络分区
  • 多数派原则:Raft 通过多数派原则来保证一致性。即使发生网络分区,只要大多数节点能够达成一致,系统仍然可以正常工作。
  • 脑裂问题:在网络分区的情况下,可能会出现多个 Leader 同时工作的现象,Raft 通过任期编号和日志一致性来解决脑裂问题。
6.2 节点故障
  • Leader 故障:如果 Leader 故障,系统会通过选举产生新的 Leader。
  • Follower 故障:如果 Follower 故障,Leader 会继续发送 AppendEntries RPC,直到 Follower 恢复。

10、C++的新特性?

1. C++11 新特性
  • 右值引用和移动语义:通过引入右值引用和移动构造函数/赋值运算符,提高了资源管理的效率,特别是在涉及临时对象的场景中。
  • 并发支持:引入了 C++11 内存模型,支持原子类型和无锁编程,以及线程支持库,包括线程管理、同步机制等。
  • 统一的初始化列表:提供了一种统一的语法来初始化任何对象,使得初始化过程更加简洁和一致。
  • 枚举类(enum class):引入了强类型枚举,提高了类型安全,避免了传统枚举类型的一些潜在问题。
  • constexpr 关键字:允许将变量、函数等声明为编译时常量,有助于提高程序的性能。
  • 委托构造函数:允许一个构造函数调用同一个类中的另一个构造函数,简化了构造函数的编写和维护。
  • 模板增强:包括外部模板、变参模板等特性,使模板编程更加强大和灵活。
  • 类型推导(decltype 和 auto 关键字):decltype关键字允许从表达式中推导出类型,auto关键字用于自动推断变量的类型,简化了代码编写过程。
  • 用户自定义字面量:允许定义自己的字面量运算符,为字面量的使用提供了更多的灵活性。
  • 范围 for 循环(range-based for loop):这是一种更简洁、更直观的遍历容器或数组的方式。
  • nullptr:这是一个新的空指针常量,用于取代传统的 NULL 或 0 表示法,提高了代码的可读性和类型安全性。
  • lambda 表达式:允许在代码中定义匿名函数,大大简化了函数对象的使用和编写。
2. C++14 新特性
  • Lambda 初始化捕获:在 C++14 中,lambda 表达式的捕获列表支持直接初始化捕获的变量。
  • 泛型 Lambda 参数:C++14 允许 lambda 函数参数类型使用auto关键字,使 lambda 更加灵活和通用。
  • 函数返回类型推导:C++14 为所有函数提供了返回类型推导的能力,而不仅仅是 lambda 函数。
  • 另一种类型推断:C++14 进一步增强了类型推断的能力,包括使用decltype(auto)来推断表达式的类型和引用性质。
  • 放松的 constexpr 限制:相较于 C++11,C++14 放宽了constexpr函数的限制,允许其函数体包含更多的控制流语句。
  • 变量模板:C++14 引入了变量模板,允许用户定义模板化的全局或静态变量。
  • 聚合体成员初始化:C++14 允许对聚合体的成员进行直接初始化,简化了聚合体的初始化过程。
  • 二进制字面量和数字分位符:C++14 引入了二进制字面量的表示方法,以及数字中的分位符,提高了代码的可读性。
  • 新的标准库特性:C++14 还引入了一些新的标准库特性,如共享的互斥体和锁(std::shared_timed_mutexstd::shared_lock),以及标准自定义字面量等。
3. C++17 新特性
  • 结构化绑定:允许将结构体、数组、元组等复合类型的成员解构到独立的变量中,简化了代码书写,提高了可读性。
  • if 和 switch 的初始化器:在条件语句中可以直接初始化变量,有助于提高代码的可读性。
  • 折叠表达式:用于精简泛型编程,使模板参数包的处理更加灵活。
  • constexpr if:在编译时进行条件判断,提高了模板代码的可读性和效率。
  • std::optional:这是一个标准库类型,用于表示一个可能包含值也可能不包含值的对象,增加了代码的安全性。
  • 改变 auto 表达式的推导规则:C++17 对auto表达式的推导规则进行了改进,使其更加直观和一致。
  • Lambda 表达式的改进:在 C++17 中,lambda 表达式可以捕获*this,即当前对象的一个拷贝,从而确保在当前对象被释放后,lambda 表达式仍然能安全地调用 this中的变量和方法。
  • 新增 inline 变量:允许直接将全局变量定义在头文件中,而不会导致多重定义错误。
  • 嵌套命名空间的定义:C++17 允许使用更简洁的语法来定义嵌套命名空间。
  • 标准属性的增加:引入了新的标准属性,如[[fallthrough]][[maybe_unused]][[nodiscard]],这些属性为编译器提供了额外的信息,有助于优化和错误检查。
4. C++20 新特性
  • 概念(Concepts):使模板编程变得更加直观、可靠和易于使用。
  • 模块(Modules):避免了传统头文件机制的诸多缺点,提供了一种更高效、更模块化的编译方式。
  • 协程(Coroutines):引入了协程支持,使得异步编程更加简单和高效。
  • 范围(Ranges):提供了一种更强大的迭代和操作集合的方式。
  • if consteval:在编译时判断一个常量表达式是否正在求值,从而增强了编译时计算的能力。
  • 多维下标运算符:增强了对多维数组的支持,使代码更加直观和简洁。
  • 明确的对象参数(Deducing this):允许在非静态成员函数中明确指定对象参数,简化了某些复杂的 C++ 编程模式。
5. C++23 新特性
  • 明确的对象参数(Deducing this):允许在非静态成员函数中明确指定对象参数,简化了某些复杂的 C++ 编程模式。
  • if consteval:在编译时判断一个常量表达式是否正在求值,从而增强了编译时计算的能力。
  • 多维下标运算符:增强了对多维数组的支持,使代码更加直观和简洁。
  • 内建衰减复制支持:简化了某些复杂的 C++ 编程模式。
  • 标记不可达代码(std::unreachable):提供了一种标记不可达代码的机制,有助于优化和错误检查。
  • 平台无关的假设([[assume]]):为编译器提供额外的信息,有助于优化。
  • 命名通用字符转义:提供了更强大的字符转义机制。
  • 扩展基于范围的 for 循环中临时变量的生命周期:提高了代码的可读性和可维护性。
  • constexpr 增强:进一步增强了编译时计算的能力。
  • 简化的隐式移动:简化了某些复杂的 C++ 编程模式。
  • 静态运算符 static operator[]:提供了更强大的数组操作机制。
  • 类模板参数推导(Class Template Argument Deduction from Inherited Constructors):简化了类模板的使用。

11、设计题:如何在分布式场景下生成唯一id,怎么生成的流程细讲?

在分布式场景下生成唯一ID,是分布式系统中的常见需求。一个好的唯一ID生成方案需要具备高可用性、高性能、全球唯一性和趋势递增性等特性。常见的唯一ID生成方案包括UUID数据库自增ID、以及基于时间的方案(如 Twitter 的 Snowflake 算法)。

Snowflake 算法简介

Snowflake 是一种高效、可扩展的分布式ID生成算法,由 Twitter 提出。它生成的ID是一个64位的整数(即 long 类型),确保在分布式环境中唯一,并且大部分情况下按照时间递增。

ID 结构

一个64位的 Snowflake ID 分为几个部分:

| 1-bit | 41-bit timestamp | 10-bit workerID | 12-bit sequence |
  • 1-bit 固定为 0:最高位保留,保持正数。
  • 41-bit 时间戳:表示时间戳的差值,通常以毫秒为单位,表示从某个固定时间(例如 纪元时间 2024-01-01 00:00:00)到当前时间的毫秒数,最多支持大约 69 年的时间跨度。
  • 10-bit worker ID:用于标识当前生成ID的机器节点,最多支持 1024 个机器节点。
  • 12-bit sequence:表示在同一毫秒内的计数器,用于在同一节点的同一毫秒内生成多个ID,最多支持每毫秒生成 4096 个唯一ID。
Snowflake ID 生成流程
  1. 时间戳生成:获取当前的时间戳(以毫秒为单位),减去起始时间戳(例如系统开始运行时的纪元时间),生成相对的毫秒差值,填入41-bit时间戳部分。

  2. 机器节点标识生成:系统中的每个节点(或进程)需要有一个唯一的标识,通常是通过预先分配或者动态生成的 Worker ID 来区分节点。这个标识是10-bit,支持最多 1024 个节点同时工作。

  3. 序列号生成:在同一台机器上,在同一毫秒内生成多个ID时,需要使用一个递增的序列号。该序列号是12-bit,支持同一节点在同一毫秒内生成 4096 个唯一ID。如果在同一毫秒内生成超过 4096 个ID,则需要等待到下一毫秒。

  4. 组合ID:

    将时间戳、机器节点标识、序列号组合成一个 64-bit 的整数。具体操作是通过位运算将各部分信息放入相应的位置:

    • 时间戳左移 22 位,填充到ID的高位;
    • 机器ID左移 12 位,填充到中间位;
    • 序列号直接放在ID的低12位。
Snowflake 算法实现步骤
  1. 初始化起始时间戳:

    • 确定一个纪元时间(通常是系统上线的时间,比如 2024-01-01 00:00:00)。
    • 计算当前时间与纪元时间的差值(以毫秒为单位)。
  2. 生成唯一的 Worker ID:

    每个分布式节点(或生成ID的服务)必须有一个唯一的 Worker ID。可以通过以下方式分配:

    • 通过配置文件手动分配给每个节点;
    • 使用服务发现工具(如 Zookeeper、Consul)动态生成唯一的 Worker ID。
  3. 生成序列号:

    每个节点在同一毫秒内生成ID时,使用一个12-bit的序列号。序列号会在同一毫秒内递增,如果超出最大值(4095),则等待下一毫秒再生成。

  4. 处理时间回拨问题:

    如果节点的时钟发生回拨(即当前时间小于上次生成ID的时间),需要做额外处理:

    • 可以抛出异常,拒绝生成ID;
    • 或者等待,直到时间恢复正常;
    • 也可以在回拨时间内生成ID时增加额外的机制(如启用备用序列号或调整 Worker ID)。

12、算法:n个台阶跳到最后几种跳法?

思路
递归

递归法的基本思路是将问题分解为更小的子问题。假设 f(n)f(n) 表示跳上 nn 个台阶的跳法数,那么:

  • 如果最后一步跳了1个台阶,那么前面的 n−1n−1 个台阶有 f(n−1)f(n−1) 种跳法。
  • 如果最后一步跳了2个台阶,那么前面的 n−2n−2 个台阶有 f(n−2)f(n−2) 种跳法。

因此,递推公式为: f(n)=f(n−1)+f(n−2)f(n)=f(n−1)+f(n−2)

边界条件:

  • f(0)=1f(0)=1(0个台阶也算一种跳法,即不动)
  • f(1)=1f(1)=1(1个台阶只有1种跳法)
  • f(2)=2f(2)=2(2个台阶有2种跳法:1+1 或 2)
动态规划
  1. 定义状态:用 dp[i] 表示跳到第 i 级台阶的跳法数。
  2. 状态转移方程:dp[i] = dp[i-1] + dp[i-2]
  3. 初始条件:dp[1] = 1dp[2] = 2
  4. 最终结果:dp[n]
参考代码
递归
#include <iostream>
using namespace std;

int climbStairs(int n) {
    if (n == 0) return 1;
    if (n == 1) return 1;
    if (n == 2) return 2;
    return climbStairs(n - 1) + climbStairs(n - 2);
}

int main() {
    int n;
    cout << "请输入楼梯的阶数:";
    cin >> n;
    int ways = climbStairs(n);
    cout << n << " 阶楼梯一共有 " << ways << " 种跳法。" << endl;
    return 0;
}
动态规划
#include <iostream>
using namespace std;

int climbStairs(int n) {
    if (n == 0) return 1;
    if (n == 1) return 1;
    if (n == 2) return 2;

    int dp[n + 1];
    dp[0] = 1;
    dp[1] = 1;
    dp[2] = 2;

    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    return dp[n];
}

int main() {
    int n;
    cout << "请输入楼梯的阶数:";
    cin >> n;
    int ways = climbStairs(n);
    cout << n << " 阶楼梯一共有 " << ways << " 种跳法。" << endl;
    return 0;
}

二面

1、说说对txn的基本理解?

  1. 定义

    事务是一系列数据库操作的集合,这些操作被视为一个单一的工作单元。事务的目的是确保所有操作都成功完成,或者在发生错误时撤销所有已经完成的操作,以保持数据的一致性。

  2. ACID特性

    • 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的任何一个操作失败,整个事务都会被回滚。
    • 一致性(Consistency):事务执行前后,数据库必须保持一致状态。事务不能破坏数据库的完整性约束。
    • 隔离性(Isolation):事务的执行是独立的,不受其他事务的干扰。即使多个事务并发执行,每个事务看到的数据库状态应该是与其他事务隔离的。
    • 持久性(Durability):一旦事务提交,其对数据库的更改将是永久的,即使系统发生故障也不会丢失。
事务的生命周期
  1. 开始:事务的开始通常通过显式或隐式的开始事务命令来标记。
  2. 执行:事务中的各个操作依次执行。
  3. 提交:如果所有操作都成功完成,事务将被提交,所有更改将被永久保存到数据库中。
  4. 回滚:如果在事务执行过程中发生错误,事务将被回滚,所有已执行的操作将被撤销。
事务的并发控制
  1. 锁定:

    • 共享锁(Shared Locks):允许多个事务同时读取同一数据项,但不允许写入。
    • 排他锁(Exclusive Locks):只允许一个事务读取或写入数据项,其他事务必须等待。
  2. 时间戳排序(Timestamp Ordering):

    为每个事务分配一个时间戳,确保事务的执行顺序与时间戳顺序一致。

  3. 多版本并发控制(MVCC):

    通过为数据项创建多个版本,允许多个事务并发读取不同版本的数据,从而提高并发性能。

事务的实现
  1. 日志记录:

    事务的每个操作都会被记录到日志中,以便在系统故障时进行恢复。

  2. 两阶段提交(Two-Phase Commit, 2PC):

    用于分布式事务,分为准备阶段和提交阶段,确保所有参与节点一致地提交或回滚事务。

事务的使用场景
  1. 银行转账:确保从一个账户扣款和向另一个账户存款的操作要么全部成功,要么全部失败。
  2. 电子商务:确保订单创建和库存减少的操作要么全部成功,要么全部失败。
  3. 医疗记录:确保患者信息的更新操作要么全部成功,要么全部失败,以保持数据的一致性。

2、说说ACID?

1. 原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的所有操作要么全部成功,要么全部失败。如果事务中的任何一个操作失败,整个事务将被回滚,所有已经完成的操作都将被撤销,确保数据库回到事务开始之前的状态。

实现机制

  • 日志记录:事务的每个操作都会被记录到日志中,以便在系统故障时进行恢复。
  • 回滚:如果事务中某个操作失败,系统会根据日志记录进行回滚,撤销所有已执行的操作。

示例: 假设有一个银行转账事务,从账户A向账户B转账100元。这个事务包含两个操作:从账户A扣款100元和向账户B存款100元。如果在执行过程中,账户A扣款成功,但向账户B存款失败,那么整个事务将被回滚,账户A的扣款也将被撤销,确保数据的一致性。

2. 一致性(Consistency)

一致性确保了事务执行前后,数据库必须保持一致状态。事务不能破坏数据库的完整性约束,包括主键、外键、唯一性约束等。事务执行的结果必须符合所有预定义的规则和约束。

实现机制

  • 约束检查:在事务执行过程中,数据库系统会检查所有相关的完整性约束,确保数据的一致性。
  • 事务日志:通过事务日志记录每个操作,确保在事务提交时数据的一致性。

示例: 假设有一个库存管理系统,库存数量不能为负。如果一个事务试图将库存数量减少到负数,系统将拒绝该操作,确保数据的一致性。

3. 隔离性(Isolation)

隔离性是指多个事务并发执行时,每个事务都能被隔离开,彼此之间互不干扰。事务的隔离性确保了并发事务的执行不会相互影响,避免了数据的不一致性和错误。

实现机制

  • 锁定:通过共享锁和排他锁来控制并发访问,确保数据的一致性。
  • 多版本并发控制(MVCC):通过为数据项创建多个版本,允许多个事务并发读取不同版本的数据,提高并发性能。
  • 事务隔离级别:不同的隔离级别提供了不同程度的隔离性,常见的隔离级别包括读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

示例: 假设有两个事务同时读取和修改同一个数据项。事务A读取数据项的值,事务B修改数据项的值,然后事务A再次读取数据项的值。在不同的隔离级别下,事务A读取的结果会有所不同:

  • 读未提交:事务A可以看到事务B未提交的修改。
  • 读已提交:事务A只能看到事务B已提交的修改。
  • 可重复读:事务A在事务B提交之前,多次读取数据项的值都是相同的。
  • 串行化:事务A和事务B按顺序执行,确保数据的一致性。
4. 持久性(Durability)

持久性是指一旦事务提交,其对数据库的更改将是永久的,即使系统发生故障也不会丢失。持久性确保了数据的可靠性和稳定性。

实现机制

  • 日志记录:事务的每个操作都会被记录到日志中,确保在系统故障时可以恢复。
  • 数据同步:在事务提交时,将数据同步到磁盘,确保数据的持久性。
  • 备份和恢复:通过定期备份和恢复机制,确保数据在系统故障后可以恢复。

示例: 假设有一个事务提交后,系统突然断电。在系统重新启动后,通过日志记录和数据同步机制,可以恢复事务提交前后的数据状态,确保数据的持久性。

3、B+ Tree,分析一下他的特点,还有时间/空间复杂度?

特点
  1. 多路平衡查找树:每个节点可以有多个子节点,且所有叶子节点都位于同一层,保证了树的高度相对较小,提高了查询效率。
  2. 非叶子节点只存储键值信息:非叶子节点只存储键值信息,不存储实际数据,这使得非叶子节点可以容纳更多的键值,减少了树的高度,提高了查询效率。
  3. 所有叶子节点包含所有关键字信息:所有叶子节点包含所有关键字信息以及指向关键字记录的指针,关键字自小到大顺序连接,形成了一个有序链表。这使得范围查询和排序查询非常高效。
  4. 叶子节点之间的链指针:所有叶子节点之间都有一个链指针,方便遍历。这使得范围查询和排序查询更加高效。
  5. 数据记录都存放在叶子节点中:数据记录都存放在叶子节点中,而非叶子节点只起到索引的作用。这使得查询路径更加稳定,每次查询都需要到达叶子节点,确保了查询的稳定性和效率。
时间复杂度
  1. 查找时间复杂度:B+ 树的查找时间复杂度为O(logmn)(m为底,n的对数),其中m是树的阶数,n是关键字的数量。这是因为 B+ 树的高度较低,且每个节点可以容纳多个关键字,减少了查询的层次。
  2. 插入和删除时间复杂度:插入和删除操作的时间复杂度也为O(log⁡mn)(同上)。插入操作需要找到合适的叶子节点并插入关键字,如果节点满了,需要进行分裂。删除操作需要找到关键字并删除,如果节点不满,可能需要进行合并。
空间复杂度
  1. 节点存储:每个节点可以存储多个关键字,非叶子节点只存储键值信息,不存储实际数据,这使得每个节点的空间利用率较高。
  2. 磁盘读写:B+ 树的磁盘读写代价较低。由于非叶子节点不存储数据,每个节点可以容纳更多的键值,减少了磁盘 I/O 次数。
为什么 B+ 树适合数据库和文件系统
  1. 磁盘 I/O 效率高:B+ 树的非叶子节点不存储数据,每个节点可以容纳更多的键值,减少了磁盘 I/O 次数,提高了查询效率。
  2. 查询稳定:每次查询都需要到达叶子节点,确保了查询的稳定性和效率。
  3. 范围查询和排序查询高效:由于叶子节点之间有链指针,范围查询和排序查询非常高效。

4、设计题目,100亿行的数据,如何快速获取某一行的结果?

这里给大家提供一个参考:

  1. 数据分片:
    • 将100亿行数据分成多个小文件,每个文件包含1000万行数据。
    • 使用哈希函数将数据均匀分配到不同的文件中。
  2. 索引构建:
    • 为每个文件构建B+树索引,索引文件存储在内存中。
    • 索引文件包含每个数据行的唯一标识符和对应的文件偏移位置。
  3. 分布式存储:
    • 使用HDFS存储分片后的数据文件。
    • 使用HBase存储数据,HBase支持高效的行级查询和范围查询。
  4. 分布式计算:
    • 使用Spark或MapReduce处理数据。
    • Map阶段用于数据分片和索引构建,Reduce阶段用于数据查询和结果合并。
  5. 查询优化:
    • 使用索引文件快速定位到目标数据所在的文件和位置。
    • 使用缓存机制存储热点数据,减少对磁盘的访问次数。
    • 并行查询多个文件,提高查询效率。
  6. 数据压缩:
    • 对数据进行压缩存储,减少磁盘空间占用。
    • 使用高效的压缩算法对数据进行压缩和解压。

5、C++malloc原理?

1. 基本概念
  • 函数原型:

    void *malloc(size_t size);
    
    • size_t size:要分配的内存大小,以字节为单位。
    • 返回值:成功时返回指向已分配内存的指针,类型为 void*,如果分配失败则返回 NULL
  • 头文件:

    #include <stdlib.h>#include <malloc.h>(在某些系统中,这两个头文件的内容是相同的)。

2. 内存分配方式

malloc 通过两种主要方式向操作系统申请内存:

  1. 使用 brk 系统调用
    • brk 系统调用用于调整数据段的末尾指针 _enddata,从而增加或减少堆的大小。
    • 当请求的内存小于某个阈值(通常是128KB)时,malloc 会使用 brk 系统调用从堆中分配内存。
  2. 使用 mmap 系统调用
    • mmap 系统调用用于将文件或设备映射到内存中,也可以用于分配匿名内存。
    • 当请求的内存大于某个阈值(通常是128KB)时,malloc 会使用 mmap 系统调用在文件映射区域分配内存。
3. 内存管理
  • 内存池

    • malloc 维护一个内存池,用于管理和复用已分配的内存块。
    • 内存池通常是一个链表结构,每个节点表示一个内存块,包含块的大小、是否空闲等信息。
  • 内存块管理

    • 内存块通常由元数据区和数据区组成。
    • 元数据区记录数据区的大小、是否空闲等信息。
    • 数据区是实际分配给用户的内存区域。
  • 分配算法

    malloc使用多种算法来管理内存块,常见的算法包括:

    • 首次适配(First Fit):从头开始查找第一个足够大的空闲块。
    • 最佳适配(Best Fit):查找所有空闲块,选择大小最接近请求大小的块。
    • 下次适配(Next Fit):从上次查找的位置开始查找第一个足够大的空闲块。
4. 内存分配流程
  1. 请求内存:用户调用 malloc 函数,传入需要分配的内存大小 size
  2. 检查内存池
    • malloc 首先检查内存池中是否有合适的空闲块。
    • 如果找到合适的空闲块,直接返回该块的地址。
    • 如果没有找到合适的空闲块,继续下一步。
  3. 分配内存
    • 如果请求的内存小于阈值(128KB),使用 brk 系统调用从堆中分配内存。
    • 如果请求的内存大于阈值(128KB),使用 mmap 系统调用在文件映射区域分配内存。
  4. 更新内存池:将新分配的内存块信息更新到内存池中,包括块的大小、是否空闲等信息。
  5. 返回内存地址:成功分配内存后,返回指向已分配内存的指针。
5. 内存释放
  • 函数原型

    void free(void *ptr);
    

    void *ptr:要释放的内存块的指针。

  • 释放流程

    1. 检查指针:
      • 确认指针 ptr 是否为 NULL,如果是 NULL,直接返回。
      • 确认指针 ptr 是否有效,即是否指向通过 malloc 分配的内存块。
    2. 更新内存池:将释放的内存块标记为空闲,并更新内存池中的相关信息。
    3. 合并空闲块:如果释放的内存块与相邻的空闲块相邻,可以将它们合并成一个更大的空闲块,以减少内存碎片。
    4. 释放内存:
      • 如果内存块是通过 mmap 分配的,使用 munmap 系统调用将内存返回给操作系统。
      • 如果内存块是通过 brk 分配的,通常不会立即将内存返回给操作系统,而是保留在内存池中,以便后续重用。
6. 内存碎片管理
  • 内存碎片:随着程序的运行,频繁的内存分配和释放会导致内存碎片,即内存中存在许多小的、不连续的空闲块,使得无法分配大块的连续内存。
  • 碎片管理:
    • malloc 通过合并相邻的空闲块来减少内存碎片。
    • 使用内存池管理技术,预分配较大块的内存,然后在需要时从中分配小块内存,减少频繁的系统调用开销。

6、说说计算机网络的分层哪些层?

OSI 七层模型

OSI(Open Systems Interconnection)七层模型是一个理论上的网络通信模型,虽然在实际应用中不如TCP/IP模型广泛,但它的概念清晰,理论完整,对理解网络通信有很好的指导作用。OSI模型从下至上分为以下七层:

  1. 物理层(Physical Layer)
    • 功能:负责定义物理接口的机械、电气、功能和过程特性,确保原始比特流能够在物理介质上正确传输。
    • 协议:无具体协议,但涉及传输介质(如双绞线、光纤等)和信号编码(如NRZ、Manchester编码)。
  2. 数据链路层(Data Link Layer)
    • 功能:负责将物理层传输的比特流组织成帧,并提供可靠的数据传输服务,包括错误检测和纠正、流量控制等。
    • 协议:以太网协议(Ethernet)、PPP(点对点协议)、HDLC(高级数据链路控制)等。
  3. 网络层(Network Layer)
    • 功能:负责将数据从源主机传输到目的主机,包括路由选择、逻辑寻址和拥塞控制。
    • 协议:IP(互联网协议)、ICMP(互联网控制消息协议)、ARP(地址解析协议)等。
  4. 传输层(Transport Layer)
    • 功能:负责提供端到端的可靠数据传输服务,包括连接管理、错误恢复、流量控制和拥塞控制。
    • 协议:TCP(传输控制协议)、UDP(用户数据报协议)。
  5. 会话层(Session Layer)
    • 功能:负责建立、管理和终止应用程序之间的会话,包括会话的同步和恢复。
    • 协议:无具体协议,但涉及会话管理和服务。
  6. 表示层(Presentation Layer)
    • 功能:负责数据的表示、加密和压缩,确保数据在发送方和接收方之间的一致性。
    • 协议:MIME(多用途互联网邮件扩展)、SSL/TLS(安全套接字层/传输层安全)。
  7. 应用层(Application Layer)
    • 功能:负责提供应用程序访问网络的接口,实现特定的网络应用。
    • 协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)等。
TCP/IP 四层模型

TCP/IP模型是一个实际运行的网络协议模型,虽然层次较少,但更简洁实用,广泛应用于互联网中。

  1. 网络接口层(Network Interface Layer)
    • 功能:负责实际的数据传输,对应OSI模型的物理层和数据链路层。
    • 协议:以太网协议、PPP(点对点协议)、SLIP(串行线路互联网协议)等。
  2. 网络层(Internet Layer)
    • 功能:负责网络间的寻址和数据传输,对应OSI模型的网络层。
    • 协议:IP(互联网协议)、ICMP(互联网控制消息协议)、ARP(地址解析协议)等。
  3. 传输层(Transport Layer)
    • 功能:负责提供端到端的可靠数据传输服务,对应OSI模型的传输层。
    • 协议:TCP(传输控制协议)、UDP(用户数据报协议)。
  4. 应用层(Application Layer)
    • 功能:负责实现一切与应用程序相关的功能,对应OSI模型的应用层、表示层和会话层。
    • 协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)等。
五层模型

五层模型是为了方便学习,综合了OSI七层模型和TCP/IP四层模型的优点,既简洁又能将概念讲清楚。五层模型从下至上分为以下五层:

  1. 物理层(Physical Layer)
    • 功能:负责定义物理接口的机械、电气、功能和过程特性,确保原始比特流能够在物理介质上正确传输。
    • 协议:无具体协议,但涉及传输介质和信号编码。
  2. 数据链路层(Data Link Layer)
    • 功能:负责将物理层传输的比特流组织成帧,并提供可靠的数据传输服务。
    • 协议:以太网协议、PPP、HDLC等。
  3. 网络层(Network Layer)
    • 功能:负责将数据从源主机传输到目的主机,包括路由选择和逻辑寻址。
    • 协议:IP、ICMP、ARP等。
  4. 传输层(Transport Layer)
    • 功能:负责提供端到端的可靠数据传输服务。
    • 协议:TCP、UDP。
  5. 应用层(Application Layer)
    • 功能:负责提供应用程序访问网络的接口,实现特定的网络应用。
    • 协议:HTTP、FTP、SMTP、DNS等。

7、TCP在哪一层?TCP/UDP,TCP是怎么可靠的?

不管是OSI七层模型还是TCP/IP模型,TCP都在传输层。

区别
1. 连接性
  • TCP:面向连接的协议。在数据传输之前,需要通过三次握手建立连接,数据传输结束后需要通过四次挥手关闭连接。这种连接机制确保了数据传输的可靠性。
  • UDP:无连接的协议。发送数据前不需要建立连接,可以直接发送数据报。这种无连接机制使得 UDP 的开销较小,数据传输效率高,但缺乏可靠性保障。
2. 可靠性
  • TCP:提供可靠的数据传输服务。通过校验和、序列号、确认应答、超时重传、流量控制和拥塞控制等机制,确保数据无差错、不丢失、不重复且按序到达。
    • 校验和:用于检测数据包是否在传输过程中损坏。
    • 序列号:用于确保数据包按顺序到达。
    • 确认应答:接收方发送 ACK 包确认收到的数据包。
    • 超时重传:发送方在指定时间内未收到确认应答时会重传数据包。
    • 流量控制:通过滑动窗口机制控制发送速率,防止接收方被淹没。
    • 拥塞控制:通过慢启动、拥塞避免、快重传和快恢复等算法防止网络拥塞。
  • UDP:提供不可靠的数据传输服务。不保证数据包的顺序、错误或重传。如果数据包在传输过程中丢失或损坏,UDP 不会采取任何补救措施。
3. 头部开销
  • TCP:头部开销较大,最小20字节,最大60字节。头部包含更多的控制信息,如序列号、确认号、窗口大小等。
  • UDP:头部开销较小,只有8字节。头部包含源端口、目的端口、长度和校验和。
4. 传输效率
  • TCP:由于需要建立连接、确认数据、处理重传等步骤,传输效率相对较低。但这种机制确保了数据传输的可靠性。
  • UDP:不需要这些额外的步骤,传输效率较高。但由于缺乏可靠性机制,数据传输过程中可能会丢失数据。
5. 应用场景
  • TCP:适用于需要可靠传输的场景,如文件传输、电子邮件、远程登录、网页浏览等。这些应用对数据的完整性要求较高,不允许数据丢失或错误。
  • UDP:适用于对实时性要求较高、但对数据可靠性要求不高的场景,如视频流、音频流、实时游戏、VoIP(网络电话)、DNS查询等。这些应用对数据传输的延迟敏感,可以容忍一定程度的数据丢失。
6. 一对一、一对多、多对一和多对多通信
  • TCP:通常用于一对一的通信,即一个TCP连接只能有一个发送方和一个接收方。
  • UDP:支持一对一、一对多、多对一和多对多的通信模式,可以实现广播和组播功能。
7. 面向字节流 vs 面向报文
  • TCP:面向字节流。TCP将应用层发下来的报文看成字节流,不区分应用层发下来的数据包。TCP把数据包封装成TCP报文段并添加TCP头部,然后交付给IP层发送。
  • UDP:面向报文。UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文。

8、算法:最长公共子序列

动态规划解法
  1. 状态定义

​ 定义一个二维数组 dp[i][j],表示字符串 text1 的前 i 个字符和字符串 text2 的前 j 个字符的最长公共子序列的长度。

  1. 状态转移方程
  • 如果 text1[i-1] == text2[j-1],则 dp[i][j] = dp[i-1][j-1] + 1
  • 如果 text1[i-1] != text2[j-1],则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  1. 边界条件

    i = 0j = 0 时,dp[i][j] = 0,因为空序列与任何序列的最长公共子序列长度为0。

参考代码
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size();
        int n = text2.size();

        // 创建 dp 数组,大小为 (m+1) x (n+1),所有元素初始化为0
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

        // 填充 dp 数组
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (text1[i - 1] == text2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        // 返回 dp[m][n],即最长公共子序列的长度
        return dp[m][n];
    }
};

int main() {
    Solution solution;
    string text1, text2;
    cout << "请输入两个字符串: ";
    cin >> text1 >> text2;

    int result = solution.longestCommonSubsequence(text1, text2);
    cout << "最长公共子序列的长度为: " << result << endl;

    return 0;
}
### 字节跳动后端开发面试经验 #### 时间管理与代码编效率 在实际面试场景中,候选人需注意时间分配。有案例显示,在一次面试中,候选人在超过20分钟的时间内完成了编码题目,但由于剩余时间不足未能运行测试用例[^1]。这表明合理安排解题时间和预留调试时间的重要性。 #### 基础知识考察 基础知识掌握情况也是评估重点之一。一位候选人提到自己除了一道关于QQ视频的问题外,大部分基础知识点都回答得较为满意;然而对于线程池的理解不够深刻,导致这部分的回答质量不高。因此准备期间应确保对常见并发编程概念有足够的理解。 #### 推荐学习资源 为了帮助提升技术水平并更好地应对技术面试中的挑战,以下是几本被推荐的经典书籍: - *JavaScript高级程序设计*:提供广泛的知识覆盖,适合作为长期参考资料; - *JavaScript DOM编程艺术*: 对于前端发者来说非常重要的一本书籍,有助于深入了解文档对象模型的操作方法; - *你不知道的故事背后的JavaScript* : 聚焦于JavaScript的核心特性如原型继承机制以及闭包等主题[^2]. 虽然上述书籍主要针对Web发领域内的技能培养,但对于全栈工程师而言同样具有参考价值。 #### 技术选型交流 当被问到日常工作中使用的编程语言时,应当基于个人经历如实作答。例如如果常用Java,则可以分享一些具体的应用实例及其带来的便利之处[^3]。 ```java public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, ByteDance!"); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值