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
列上的哈希索引将帮助快速查找特定名字的记录。
哈希索引的优势
- 查询速度快:由于哈希索引直接利用哈希表进行查找,理想情况下可以在常数时间内(O(1))找到目标数据行,这使得等值查询非常高效。
- 索引结构紧凑:哈希索引仅存储哈希码和行指针,不存储实际的字段值,因此索引结构非常紧凑,占用的存储空间较少。
哈希索引的局限性
- 不支持范围查询:哈希索引不支持范围查询(如
>
、<
、BETWEEN
等),因为哈希表中的数据不是按照索引值的顺序存储的。 - 不支持部分索引列匹配:哈希索引要求查询条件必须完全匹配索引列的所有列,部分匹配查询(如只使用索引列的一部分)无法利用哈希索引。
- 哈希冲突:当不同的索引列值产生相同的哈希码时,会发生哈希冲突。虽然哈希索引通过链表解决了冲突问题,但冲突越多,查询效率越低。
- 不支持排序:由于哈希表中的数据不是按顺序存储的,哈希索引无法用于排序操作。
哈希索引的应用场景
- 等值查询:当查询主要涉及等值比较(如
=
、IN
)时,哈希索引可以显著提升查询性能。 - 长字符串查询:对于包含大量长字符串的表,可以使用哈希索引优化查询性能。例如,存储大量URL信息的表可以通过计算URL的哈希值并建立哈希索引来加速查询。
- 高并发读取:在高并发读取场景中,哈希索引可以提供快速的查询响应时间。
自适应哈希索引
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 语句,如 INSERT
、UPDATE
、DELETE
等。这些操作会被暂存,直到事务提交或回滚。在这个阶段,MySQL 会进行以下步骤:
- 连接器:验证用户的身份和权限,确保用户有权限执行这些操作。
- 分析器:解析 SQL 语句,生成解析树。
- 优化器:生成执行计划,选择最优的执行路径。
- 执行器:根据执行计划,调用存储引擎的 API 执行 SQL 语句。
2.3 事务的提交
事务的提交可以通过以下命令显式地提交:
COMMIT;
或COMMIT WORK;
当事务提交时,MySQL 会执行以下步骤:
- 两阶段提交:
- Prepare 阶段:将事务的 XID(内部 XA 事务的 ID)写入 redo log,并将 redo log 的事务状态设置为 prepare,然后将 redo log 持久化到磁盘。
- Commit 阶段:将 XID 写入 binlog,然后将 binlog 持久化到磁盘;调用事务引擎的提交事务接口,将 redo log 状态设置为 commit。
- 持久化:确保事务的更改被永久保存到数据库中。即使系统发生故障,事务的更改也不会丢失。
2.4 事务的回滚
事务的回滚可以通过以下命令显式地回滚:
ROLLBACK;
或ROLLBACK WORK;
当事务回滚时,MySQL 会撤销所有未提交的更改,并将数据库恢复到事务开始之前的状态。在这个阶段,MySQL 会执行以下步骤:
- 撤销更改:撤销事务中所有已执行的 SQL 语句的更改。
- 释放资源:释放事务持有的锁和其他资源。
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 UPDATE
、SELECT ... LOCK IN SHARE MODE
、INSERT
、UPDATE
和DELETE
语句。
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 COMMITTED
和REPEATABLE READ
。 - 减少锁竞争:减少读写操作之间的锁竞争,提高系统的响应能力。
4.2 缺点
- 存储开销:需要存储多个版本的数据,占用更多的存储空间。
- 复杂性:实现和管理多版本数据增加了系统的复杂性。
5、SIX lock是什么,可以在哪些级别上锁,MySQL原子性怎么实现?
SIX 锁(意向排他共享锁)是数据库中一种特殊的锁类型,主要用于管理对共享资源的并发访问。它结合了共享锁(S锁)和排他锁(X锁)的特性,允许事务在不同层级上对资源进行加锁,从而提高并发性能和资源利用率。SIX 锁在数据库中通常用于以下场景:
- 意向锁:意向锁用于表示事务对某个资源有进一步加锁的意图。SIX 锁是一种意向锁,表示事务对某个资源有获取共享锁和排他锁的意图。
- 资源层次结构: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(数据操作语言)语句,但不包括数据查询语句(如 SELECT
、SHOW
等)。Binlog 以二进制格式存储,包含对数据库执行的所有修改操作的详细信息,如插入、更新、删除等。
2. Binlog 的作用
Binlog 在 MySQL 中有以下几个主要作用:
- 数据恢复:通过重放 Binlog 中的事件,可以将数据库恢复到特定的时间点。这对于恢复误删数据、应对错误的批量操作等情况非常有用。
- 主从复制:在主从复制中,主服务器将所有的更改记录到 Binlog 中,而从服务器通过读取主服务器的 Binlog 并执行相同的更改来保持数据同步。
- 审计:用户可以通过 Binlog 中的信息进行审计,判断是否有对数据库进行注入攻击或其他恶意操作。
3. Binlog 的存储格式
MySQL 支持三种 Binlog 格式,每种格式有不同的特点和适用场景:
- Statement 格式:基于 SQL 语句的复制(statement-based replication, SBR)。每个会修改数据的 SQL 语句都会记录在 Binlog 中。优点是日志文件小,可读性强,但某些复杂问题可能会导致主从不一致。
- Row 格式:基于行的复制(row-based replication, RBR)。记录的是数据行的变更,会包含变更前后的内容。优点是可以精确记录行数据,避免 Statement 格式可能出现的数据不一致的问题,但日志文件大,可读性差。
- Mixed 格式:混合模式复制(mixed-based replication, MBR)。根据具体情况自动选择 Statement 或 Row 类型来记录数据更改。兼顾了 Statement 和 Row 两种类型的优点,既可以节约空间,也可以精确记录数据变更。
4. Binlog 的配置和管理
-
开启 Binlog:在 MySQL 配置文件(通常是
my.cnf
或my.ini
)中添加以下内容:[mysqld] log-bin=mysql-bin server-id=1
其中
log-bin
参数表示启用 Binlog,并指定 Binlog 文件的名称前缀。server-id
参数表示服务器的唯一标识符,必须在主从复制中设置为不同的值。 -
设置 Binlog 文件大小和保留时间:
max_binlog_size=100M expire_logs_days=7
max_binlog_size
参数表示单个 Binlog 文件的最大大小,当文件大小超过此值时,MySQL 会创建一个新的 Binlog 文件。expire_logs_days
参数表示 Binlog 文件的保留天数,超过此天数的 Binlog 文件将被自动删除。 -
查看 Binlog 状态:
SHOW MASTER STATUS; SHOW BINARY LOGS;
SHOW MASTER STATUS
命令显示当前 Binlog 文件的状态,包括文件名和位置。SHOW BINARY LOGS
命令显示所有 Binlog 文件的列表。 -
解析 Binlog 内容:使用
mysqlbinlog
工具可以解析 Binlog 文件的内容,将其转换为可读的 SQL 语句:mysqlbinlog /path/to/mysql-bin.000001
通过解析 Binlog 内容,可以进行数据恢复、审计等操作。
5. Binlog 的实际应用
-
数据恢复:通过重放 Binlog 中的事件,可以将数据库恢复到特定的时间点。例如,可以使用
mysqlbinlog
工具导出 Binlog 文件,然后将其应用于数据库:mysqlbinlog /path/to/mysql-bin.000001 | mysql -u root -p
这种方法适用于恢复误删数据或应对错误的批量操作。
-
主从复制:在主从复制中,主服务器将所有的更改记录到 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;
通过这些步骤,可以实现主从复制,确保数据的一致性。
-
-
审计:通过解析 Binlog 内容,可以进行审计,判断是否有对数据库进行注入攻击或其他恶意操作。例如,可以定期检查 Binlog 文件,查找可疑的 SQL 语句。
6. Binlog 的性能影响
开启 Binlog 会稍微降低 MySQL 的性能,大约有 1% 的性能损耗。因此,在生产环境中,需要权衡数据安全性和性能之间的关系。
7. Binlog 的安全性和管理
- 安全性:Binlog 文件包含敏感的数据库操作信息,因此需要妥善保管,防止未授权访问。可以通过设置文件权限和加密等方式增强安全性。
- 管理:定期检查和清理 Binlog 文件,避免占用过多的磁盘空间。可以通过设置
expire_logs_days
参数自动删除过期的 Binlog 文件。
7、数据库主从复制怎么实现?
1. 概述
主从复制(Master-Slave Replication)是数据库管理系统中一种常见的高可用性和数据冗余策略。其核心思想是将主数据库(Master)的操作日志同步到从数据库(Slave),从而确保从数据库的数据与主数据库保持一致。这种机制不仅提高了系统的可用性和数据安全性,还可以通过读写分离来提高数据库的性能。
2. 工作原理
主从复制涉及以下几个主要步骤:
- 主库记录日志:主库在执行任何数据更改操作(如
INSERT
、UPDATE
、DELETE
)时,会将这些操作记录到二进制日志(Binary Log)中。 - 从库获取日志:从库通过一个 I/O 线程连接到主库,请求并接收主库的二进制日志。主库会为每个从库创建一个 Binlog Dump 线程,负责发送二进制日志内容给从库。
- 从库写入中继日志:从库的 I/O 线程将接收到的二进制日志内容写入到本地的中继日志(Relay Log)中。
- 从库应用日志:从库通过一个 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 选举过程
- 转变为 Candidate:
- Follower 在选举超时时间内没有收到 Leader 的心跳信息,会将自己的任期编号(Term)加 1,并转变为 Candidate。
- Candidate 会给自己投票,并向其他节点发送请求投票(RequestVote)RPC。
- 请求投票:
- Candidate 向其他节点发送 RequestVote RPC,请求投票。
- RequestVote RPC 包含当前任期编号、Candidate 的日志信息(最后一个日志条目的索引和任期编号)。
- 处理投票请求:
- 接收节点在收到 RequestVote RPC 后,会根据以下条件决定是否投票:
- 如果接收节点的当前任期编号小于 RequestVote RPC 中的任期编号,会更新自己的任期编号。
- 如果接收节点已经投票给其他节点,会拒绝投票。
- 如果接收节点的日志至少和 Candidate 的日志一样新(即最后一个日志条目的任期编号更大或相等,且索引更大或相等),会投票给 Candidate。
- 接收节点在收到 RequestVote RPC 后,会根据以下条件决定是否投票:
- 赢得选举:
- 如果 Candidate 收到大多数节点的投票,会成为新的 Leader。
- 新 Leader 会立即发送心跳信息,告知其他节点自己是新的 Leader。
- 选举失败:
- 如果没有节点赢得大多数投票,选举会失败,所有节点会重置选举超时时间并重新开始选举。
3.3 随机选举超时
- 目的:通过随机选举超时时间,降低多个节点同时发起选举的概率,避免选举冲突。
- 实现:每个 Follower 的选举超时时间在 150ms 到 300ms 之间随机生成。
4. 日志复制
4.1 日志结构
- 日志条目:每个日志条目包含索引(Index)、任期编号(Term)和命令(Command)。
- 索引:严格递增的数字,表示日志条目的位置。
- 任期编号:表示日志条目在哪个任期内创建。
- 命令:表示要执行的操作。
4.2 日志复制过程
-
客户端请求:
- 客户端向 Leader 发送请求。
- Leader 将请求作为日志条目记录到自己的日志中。
-
发送日志条目:
- Leader 通过 AppendEntries RPC 将日志条目发送给所有 Follower。
- AppendEntries RPC 包含当前任期编号、前一个日志条目的索引和任期编号、新的日志条目。
-
处理 AppendEntries RPC:
Follower 在收到 AppendEntries RPC 后,会根据以下条件决定是否接受新的日志条目:
- 如果前一个日志条目的索引和任期编号与 Follower 的日志匹配,会接受新的日志条目。
- 如果前一个日志条目的索引和任期编号不匹配,会拒绝新的日志条目,并返回前一个日志条目的索引。
-
提交日志条目:
- 当 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_mutex
和std::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 生成流程
-
时间戳生成:获取当前的时间戳(以毫秒为单位),减去起始时间戳(例如系统开始运行时的纪元时间),生成相对的毫秒差值,填入41-bit时间戳部分。
-
机器节点标识生成:系统中的每个节点(或进程)需要有一个唯一的标识,通常是通过预先分配或者动态生成的 Worker ID 来区分节点。这个标识是10-bit,支持最多 1024 个节点同时工作。
-
序列号生成:在同一台机器上,在同一毫秒内生成多个ID时,需要使用一个递增的序列号。该序列号是12-bit,支持同一节点在同一毫秒内生成 4096 个唯一ID。如果在同一毫秒内生成超过 4096 个ID,则需要等待到下一毫秒。
-
组合ID:
将时间戳、机器节点标识、序列号组合成一个 64-bit 的整数。具体操作是通过位运算将各部分信息放入相应的位置:
- 时间戳左移 22 位,填充到ID的高位;
- 机器ID左移 12 位,填充到中间位;
- 序列号直接放在ID的低12位。
Snowflake 算法实现步骤
-
初始化起始时间戳:
- 确定一个纪元时间(通常是系统上线的时间,比如 2024-01-01 00:00:00)。
- 计算当前时间与纪元时间的差值(以毫秒为单位)。
-
生成唯一的 Worker ID:
每个分布式节点(或生成ID的服务)必须有一个唯一的 Worker ID。可以通过以下方式分配:
- 通过配置文件手动分配给每个节点;
- 使用服务发现工具(如 Zookeeper、Consul)动态生成唯一的 Worker ID。
-
生成序列号:
每个节点在同一毫秒内生成ID时,使用一个12-bit的序列号。序列号会在同一毫秒内递增,如果超出最大值(4095),则等待下一毫秒再生成。
-
处理时间回拨问题:
如果节点的时钟发生回拨(即当前时间小于上次生成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)
动态规划
- 定义状态:用
dp[i]
表示跳到第i
级台阶的跳法数。 - 状态转移方程:
dp[i] = dp[i-1] + dp[i-2]
。 - 初始条件:
dp[1] = 1
,dp[2] = 2
。 - 最终结果:
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的基本理解?
-
定义:
事务是一系列数据库操作的集合,这些操作被视为一个单一的工作单元。事务的目的是确保所有操作都成功完成,或者在发生错误时撤销所有已经完成的操作,以保持数据的一致性。
-
ACID特性:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的任何一个操作失败,整个事务都会被回滚。
- 一致性(Consistency):事务执行前后,数据库必须保持一致状态。事务不能破坏数据库的完整性约束。
- 隔离性(Isolation):事务的执行是独立的,不受其他事务的干扰。即使多个事务并发执行,每个事务看到的数据库状态应该是与其他事务隔离的。
- 持久性(Durability):一旦事务提交,其对数据库的更改将是永久的,即使系统发生故障也不会丢失。
事务的生命周期
- 开始:事务的开始通常通过显式或隐式的开始事务命令来标记。
- 执行:事务中的各个操作依次执行。
- 提交:如果所有操作都成功完成,事务将被提交,所有更改将被永久保存到数据库中。
- 回滚:如果在事务执行过程中发生错误,事务将被回滚,所有已执行的操作将被撤销。
事务的并发控制
-
锁定:
- 共享锁(Shared Locks):允许多个事务同时读取同一数据项,但不允许写入。
- 排他锁(Exclusive Locks):只允许一个事务读取或写入数据项,其他事务必须等待。
-
时间戳排序(Timestamp Ordering):
为每个事务分配一个时间戳,确保事务的执行顺序与时间戳顺序一致。
-
多版本并发控制(MVCC):
通过为数据项创建多个版本,允许多个事务并发读取不同版本的数据,从而提高并发性能。
事务的实现
-
日志记录:
事务的每个操作都会被记录到日志中,以便在系统故障时进行恢复。
-
两阶段提交(Two-Phase Commit, 2PC):
用于分布式事务,分为准备阶段和提交阶段,确保所有参与节点一致地提交或回滚事务。
事务的使用场景
- 银行转账:确保从一个账户扣款和向另一个账户存款的操作要么全部成功,要么全部失败。
- 电子商务:确保订单创建和库存减少的操作要么全部成功,要么全部失败。
- 医疗记录:确保患者信息的更新操作要么全部成功,要么全部失败,以保持数据的一致性。
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,分析一下他的特点,还有时间/空间复杂度?
特点
- 多路平衡查找树:每个节点可以有多个子节点,且所有叶子节点都位于同一层,保证了树的高度相对较小,提高了查询效率。
- 非叶子节点只存储键值信息:非叶子节点只存储键值信息,不存储实际数据,这使得非叶子节点可以容纳更多的键值,减少了树的高度,提高了查询效率。
- 所有叶子节点包含所有关键字信息:所有叶子节点包含所有关键字信息以及指向关键字记录的指针,关键字自小到大顺序连接,形成了一个有序链表。这使得范围查询和排序查询非常高效。
- 叶子节点之间的链指针:所有叶子节点之间都有一个链指针,方便遍历。这使得范围查询和排序查询更加高效。
- 数据记录都存放在叶子节点中:数据记录都存放在叶子节点中,而非叶子节点只起到索引的作用。这使得查询路径更加稳定,每次查询都需要到达叶子节点,确保了查询的稳定性和效率。
时间复杂度
- 查找时间复杂度:B+ 树的查找时间复杂度为O(logmn)(m为底,n的对数),其中m是树的阶数,n是关键字的数量。这是因为 B+ 树的高度较低,且每个节点可以容纳多个关键字,减少了查询的层次。
- 插入和删除时间复杂度:插入和删除操作的时间复杂度也为O(logmn)(同上)。插入操作需要找到合适的叶子节点并插入关键字,如果节点满了,需要进行分裂。删除操作需要找到关键字并删除,如果节点不满,可能需要进行合并。
空间复杂度
- 节点存储:每个节点可以存储多个关键字,非叶子节点只存储键值信息,不存储实际数据,这使得每个节点的空间利用率较高。
- 磁盘读写:B+ 树的磁盘读写代价较低。由于非叶子节点不存储数据,每个节点可以容纳更多的键值,减少了磁盘 I/O 次数。
为什么 B+ 树适合数据库和文件系统
- 磁盘 I/O 效率高:B+ 树的非叶子节点不存储数据,每个节点可以容纳更多的键值,减少了磁盘 I/O 次数,提高了查询效率。
- 查询稳定:每次查询都需要到达叶子节点,确保了查询的稳定性和效率。
- 范围查询和排序查询高效:由于叶子节点之间有链指针,范围查询和排序查询非常高效。
4、设计题目,100亿行的数据,如何快速获取某一行的结果?
这里给大家提供一个参考:
- 数据分片:
- 将100亿行数据分成多个小文件,每个文件包含1000万行数据。
- 使用哈希函数将数据均匀分配到不同的文件中。
- 索引构建:
- 为每个文件构建B+树索引,索引文件存储在内存中。
- 索引文件包含每个数据行的唯一标识符和对应的文件偏移位置。
- 分布式存储:
- 使用HDFS存储分片后的数据文件。
- 使用HBase存储数据,HBase支持高效的行级查询和范围查询。
- 分布式计算:
- 使用Spark或MapReduce处理数据。
- Map阶段用于数据分片和索引构建,Reduce阶段用于数据查询和结果合并。
- 查询优化:
- 使用索引文件快速定位到目标数据所在的文件和位置。
- 使用缓存机制存储热点数据,减少对磁盘的访问次数。
- 并行查询多个文件,提高查询效率。
- 数据压缩:
- 对数据进行压缩存储,减少磁盘空间占用。
- 使用高效的压缩算法对数据进行压缩和解压。
5、C++malloc原理?
1. 基本概念
-
函数原型:
void *malloc(size_t size);
size_t size
:要分配的内存大小,以字节为单位。- 返回值:成功时返回指向已分配内存的指针,类型为
void*
,如果分配失败则返回NULL
。
-
头文件:
#include <stdlib.h>
或#include <malloc.h>
(在某些系统中,这两个头文件的内容是相同的)。
2. 内存分配方式
malloc
通过两种主要方式向操作系统申请内存:
- 使用
brk
系统调用:brk
系统调用用于调整数据段的末尾指针_enddata
,从而增加或减少堆的大小。- 当请求的内存小于某个阈值(通常是128KB)时,
malloc
会使用brk
系统调用从堆中分配内存。
- 使用
mmap
系统调用:mmap
系统调用用于将文件或设备映射到内存中,也可以用于分配匿名内存。- 当请求的内存大于某个阈值(通常是128KB)时,
malloc
会使用mmap
系统调用在文件映射区域分配内存。
3. 内存管理
-
内存池:
malloc
维护一个内存池,用于管理和复用已分配的内存块。- 内存池通常是一个链表结构,每个节点表示一个内存块,包含块的大小、是否空闲等信息。
-
内存块管理:
- 内存块通常由元数据区和数据区组成。
- 元数据区记录数据区的大小、是否空闲等信息。
- 数据区是实际分配给用户的内存区域。
-
分配算法:
malloc
使用多种算法来管理内存块,常见的算法包括:- 首次适配(First Fit):从头开始查找第一个足够大的空闲块。
- 最佳适配(Best Fit):查找所有空闲块,选择大小最接近请求大小的块。
- 下次适配(Next Fit):从上次查找的位置开始查找第一个足够大的空闲块。
4. 内存分配流程
- 请求内存:用户调用
malloc
函数,传入需要分配的内存大小size
。 - 检查内存池:
malloc
首先检查内存池中是否有合适的空闲块。- 如果找到合适的空闲块,直接返回该块的地址。
- 如果没有找到合适的空闲块,继续下一步。
- 分配内存:
- 如果请求的内存小于阈值(128KB),使用
brk
系统调用从堆中分配内存。 - 如果请求的内存大于阈值(128KB),使用
mmap
系统调用在文件映射区域分配内存。
- 如果请求的内存小于阈值(128KB),使用
- 更新内存池:将新分配的内存块信息更新到内存池中,包括块的大小、是否空闲等信息。
- 返回内存地址:成功分配内存后,返回指向已分配内存的指针。
5. 内存释放
-
函数原型:
void free(void *ptr);
void *ptr
:要释放的内存块的指针。 -
释放流程:
- 检查指针:
- 确认指针
ptr
是否为NULL
,如果是NULL
,直接返回。 - 确认指针
ptr
是否有效,即是否指向通过malloc
分配的内存块。
- 确认指针
- 更新内存池:将释放的内存块标记为空闲,并更新内存池中的相关信息。
- 合并空闲块:如果释放的内存块与相邻的空闲块相邻,可以将它们合并成一个更大的空闲块,以减少内存碎片。
- 释放内存:
- 如果内存块是通过
mmap
分配的,使用munmap
系统调用将内存返回给操作系统。 - 如果内存块是通过
brk
分配的,通常不会立即将内存返回给操作系统,而是保留在内存池中,以便后续重用。
- 如果内存块是通过
- 检查指针:
6. 内存碎片管理
- 内存碎片:随着程序的运行,频繁的内存分配和释放会导致内存碎片,即内存中存在许多小的、不连续的空闲块,使得无法分配大块的连续内存。
- 碎片管理:
malloc
通过合并相邻的空闲块来减少内存碎片。- 使用内存池管理技术,预分配较大块的内存,然后在需要时从中分配小块内存,减少频繁的系统调用开销。
6、说说计算机网络的分层哪些层?
OSI 七层模型
OSI(Open Systems Interconnection)七层模型是一个理论上的网络通信模型,虽然在实际应用中不如TCP/IP模型广泛,但它的概念清晰,理论完整,对理解网络通信有很好的指导作用。OSI模型从下至上分为以下七层:
- 物理层(Physical Layer):
- 功能:负责定义物理接口的机械、电气、功能和过程特性,确保原始比特流能够在物理介质上正确传输。
- 协议:无具体协议,但涉及传输介质(如双绞线、光纤等)和信号编码(如NRZ、Manchester编码)。
- 数据链路层(Data Link Layer):
- 功能:负责将物理层传输的比特流组织成帧,并提供可靠的数据传输服务,包括错误检测和纠正、流量控制等。
- 协议:以太网协议(Ethernet)、PPP(点对点协议)、HDLC(高级数据链路控制)等。
- 网络层(Network Layer):
- 功能:负责将数据从源主机传输到目的主机,包括路由选择、逻辑寻址和拥塞控制。
- 协议:IP(互联网协议)、ICMP(互联网控制消息协议)、ARP(地址解析协议)等。
- 传输层(Transport Layer):
- 功能:负责提供端到端的可靠数据传输服务,包括连接管理、错误恢复、流量控制和拥塞控制。
- 协议:TCP(传输控制协议)、UDP(用户数据报协议)。
- 会话层(Session Layer):
- 功能:负责建立、管理和终止应用程序之间的会话,包括会话的同步和恢复。
- 协议:无具体协议,但涉及会话管理和服务。
- 表示层(Presentation Layer):
- 功能:负责数据的表示、加密和压缩,确保数据在发送方和接收方之间的一致性。
- 协议:MIME(多用途互联网邮件扩展)、SSL/TLS(安全套接字层/传输层安全)。
- 应用层(Application Layer):
- 功能:负责提供应用程序访问网络的接口,实现特定的网络应用。
- 协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)等。
TCP/IP 四层模型
TCP/IP模型是一个实际运行的网络协议模型,虽然层次较少,但更简洁实用,广泛应用于互联网中。
- 网络接口层(Network Interface Layer):
- 功能:负责实际的数据传输,对应OSI模型的物理层和数据链路层。
- 协议:以太网协议、PPP(点对点协议)、SLIP(串行线路互联网协议)等。
- 网络层(Internet Layer):
- 功能:负责网络间的寻址和数据传输,对应OSI模型的网络层。
- 协议:IP(互联网协议)、ICMP(互联网控制消息协议)、ARP(地址解析协议)等。
- 传输层(Transport Layer):
- 功能:负责提供端到端的可靠数据传输服务,对应OSI模型的传输层。
- 协议:TCP(传输控制协议)、UDP(用户数据报协议)。
- 应用层(Application Layer):
- 功能:负责实现一切与应用程序相关的功能,对应OSI模型的应用层、表示层和会话层。
- 协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)等。
五层模型
五层模型是为了方便学习,综合了OSI七层模型和TCP/IP四层模型的优点,既简洁又能将概念讲清楚。五层模型从下至上分为以下五层:
- 物理层(Physical Layer):
- 功能:负责定义物理接口的机械、电气、功能和过程特性,确保原始比特流能够在物理介质上正确传输。
- 协议:无具体协议,但涉及传输介质和信号编码。
- 数据链路层(Data Link Layer):
- 功能:负责将物理层传输的比特流组织成帧,并提供可靠的数据传输服务。
- 协议:以太网协议、PPP、HDLC等。
- 网络层(Network Layer):
- 功能:负责将数据从源主机传输到目的主机,包括路由选择和逻辑寻址。
- 协议:IP、ICMP、ARP等。
- 传输层(Transport Layer):
- 功能:负责提供端到端的可靠数据传输服务。
- 协议:TCP、UDP。
- 应用层(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、算法:最长公共子序列
动态规划解法
- 状态定义
定义一个二维数组 dp[i][j]
,表示字符串 text1
的前 i
个字符和字符串 text2
的前 j
个字符的最长公共子序列的长度。
- 状态转移方程
- 如果
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])
。
-
边界条件
当
i = 0
或j = 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;
}