第一时间收到文章更新
来源丨王中阳Go(ID:wangzhongyanggo)
下面就是这次二面的面试题详解:
自我介绍 挑一个项目介绍一下这个项目,主要做什么,你在里面承担什么,聊一下相关的技术。
3. 充值中心是一个一发多的系统,如何保障只有一个供应商抢到订单,如何保障由最快的供应商进行充值?
在设计一个充值中心时,确保只有一个供应商能够成功处理特定的订单是至关重要的。这通常涉及到两个方面:一是防止多个供应商同时处理同一订单,二是确保最合适的(如最快)供应商得到机会处理订单。
为了实现这一点,可以使用分布式锁机制或消息队列来控制订单的分配。分布式锁是一种保证在分布式系统中某些资源在同一时间只能被一个进程访问的技术。当有新订单到来时,所有感兴趣的供应商都会尝试获取这个锁,但只有第一个成功获取到锁的供应商才能继续处理该订单。这样就避免了多个供应商同时处理同一订单的情况。
对于选择“最快的”供应商,可以通过预评估供应商的历史表现来优化订单分发策略。例如,记录每个供应商完成订单的时间,并基于这些数据构建一个评分系统。每当有新订单时,系统可以根据评分优先向表现最好的供应商发送订单请求。此外,还可以引入竞价机制,让供应商竞争订单,从而激励他们提高服务速度。
4. 聊一聊数据库事务?
数据库事务是指一组操作作为一个整体被执行的过程,要么全部成功,要么全部失败。这种特性对于维护数据的一致性和完整性至关重要。事务具有四个关键属性,通常称为ACID特性:
原子性 (Atomicity): 指的是事务中的所有操作必须作为一个不可分割的整体执行,如果任何一个操作失败,则整个事务都将被撤销。 一致性 (Consistency): 确保事务前后数据库处于一致状态,即不会破坏数据库的数据规则。 隔离性 (Isolation): 即使有多个事务并发执行,它们之间的操作应该是独立的,互不干扰。 持久性 (Durability): 一旦事务提交,其结果就是永久性的,即使系统发生故障也不会丢失。
事务通过日志文件、检查点和其他机制来保证上述特性。例如,在MySQL的InnoDB存储引擎中,它会利用重做日志(redo log)来确保持久性,以及undo日志来支持回滚和MVCC(多版本并发控制),以实现快照读取和隔离性。
5. InnoDB是如何满足ACID特性的,做了哪些设计?
InnoDB作为MySQL的默认存储引擎,通过一系列精心设计的功能和技术来确保满足ACID特性:
原子性: InnoDB使用undo日志来跟踪事务所做的更改。如果事务未能完成,它可以使用undo日志来回滚未提交的变化,确保事务的原子性。 一致性: 通过外键约束、唯一性约束等机制,InnoDB确保数据始终符合预定义的规则。此外,它还使用两阶段提交协议来协调分布式事务,保持跨节点的一致性。 隔离性: InnoDB实现了四种标准的SQL事务隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。它主要依靠MVCC和行级锁定来提供高并发环境下的隔离性。 持久性: InnoDB依赖于redo日志来记录对数据页的修改。每次事务提交时,InnoDB会先将相应的redo日志写入磁盘,然后再确认事务完成。即使系统崩溃,也可以根据这些日志恢复未完成的事务。
6. 隔离性是通过什么满足的,除了MVCC?
除了MVCC(多版本并发控制),数据库还采用其他几种方法来满足隔离性要求:
锁机制: 包括表锁、行锁和页面锁等。锁可以限制其他事务对相同数据的访问,从而避免冲突。例如,行级锁定允许并发事务在不同行上执行更新操作,而不会互相干扰。 序列化执行: 最严格的隔离级别是串行化(Serializable),在这种模式下,事务依次执行,如同在一个单线程环境中一样,完全避免了任何并发问题。 乐观并发控制 (OCC): OCC假设冲突很少发生,并允许事务在没有显式锁定的情况下读取和修改数据。只有在提交时才检查是否有冲突;如果有冲突,则回滚并重新尝试。 快照隔离 (Snapshot Isolation, SI): 快照隔离提供了一种比传统MVCC更强大的隔离形式,它不仅提供了非阻塞读取,而且还能防止幻读现象的发生。
7. 为什么有了锁还需要MVCC?
锁主要用于保护数据免受并发修改的影响,确保数据的一致性和准确性。然而,单纯依靠锁会导致较高的争用率,特别是在高并发环境下,可能会严重影响性能。因此,需要一种既能提供足够隔离又能保持良好性能的方法。
MVCC(多版本并发控制)正好解决了这个问题。它允许读操作不必等待写操作完成,反之亦然,因为每个事务都可以看到数据的不同版本。具体来说,当一个事务正在修改某条记录时,其他读取该记录的事务仍然可以看到修改前的数据版本,而不必等待写事务结束。这种方式大大减少了因锁定而导致的阻塞情况,提高了系统的并发处理能力。
8. MVCC是如何实现既有隔离性,又能尽可能保障吞吐?
MVCC通过创建数据的不同版本来实现高效的并发控制。每个事务在读取数据时,实际上是在读取一个特定时间点的数据快照,而不是最新的数据。这意味着读取操作不会阻塞写入操作,反之亦然,从而提高了系统的吞吐量。
具体实现方式包括:
版本链: 每个数据行都包含指向旧版本的指针,形成一个版本链。当有新的写入发生时,不会直接覆盖现有数据,而是创建一个新的版本,并将其链接到当前版本之后。 隐藏字段: 数据行中包含一些额外的信息,如创建时间和删除时间戳,用于确定哪个版本应该对给定的事务可见。 视图控制: 数据库会为每个事务生成一个快照视图,决定哪些数据版本是可见的。例如,在InnoDB中,它会根据事务的启动时间和数据行的时间戳来判断是否显示某个版本的数据。
这种方法确保了即使在高并发情况下也能维持良好的隔离性和性能。
9. 说一下有哪些锁,如何分类?
数据库中的锁可以按照作用范围、锁定对象和锁类型等多个维度进行分类:
按作用范围分类:
共享锁 (Shared Locks, S): 多个事务可以同时持有同一个对象上的共享锁,适用于只读操作。 排他锁 (Exclusive Locks, X): 只有一个事务可以在同一时间持有排他锁,用于写入或更新操作。 按锁定对象分类:
表锁 (Table-Level Locks): 锁定整张表,影响较大,通常用于批量操作或DDL语句。 行锁 (Row-Level Locks): 只锁定特定的行,适用于细粒度控制,减少锁定范围,提高并发性。 页锁 (Page-Level Locks): 锁定数据库页面,介于表锁和行锁之间,提供了一种折衷方案。 按锁类型分类:
意向锁 (Intention Locks): 表明事务打算在更低级别的对象上获取锁。例如,意向共享锁(IS)表示事务计划在表内的某些行上获取共享锁。 间隙锁 (Gap Locks): 锁定两个行之间的间隙,防止其他事务插入新行。 临键锁 (Next-Key Locks): 结合了行锁和间隙锁,锁定一行及其之前的间隙,用于防止幻读。
此外,还有诸如死锁检测与预防、锁超时等机制来管理锁的行为,确保系统稳定运行。
10. 间隙锁的目的是什么?
间隙锁的主要目的是解决幻读问题,即一个事务在两次查询间看到了不同的数据集。例如,如果一个事务在第一次查询时得到了一组记录,而在第二次查询时发现中间插入了新的记录,这就构成了幻读。
通过应用间隙锁,数据库可以阻止其他事务在指定的范围内插入新行。具体来说,间隙锁锁定的是两个实际行之间的空间,而不是具体的行本身。这样做不仅可以防止幻读,还可以有效地减少锁定冲突,因为它并不直接锁定实际的数据行,而是锁定可能插入新行的位置。
例如,在InnoDB中,当你在一个范围查询上使用SELECT ... FOR UPDATE时,除了对匹配的行加锁之外,还会对行间的间隙加锁,从而确保其他事务无法在此期间插入新的行。
11. 说一下幻读的具体现象?
幻读是指在一个事务内,两次查询同一范围的数据时,由于其他事务的插入或删除操作导致返回的结果集发生变化的现象。简单来说,就是事务A在第一次查询时得到了一组记录,但在同一事务中的第二次查询时,发现出现了之前不存在的新记录,或者某些记录消失了。
例如,考虑一个图书库存系统。如果事务A开始时查询所有库存数量大于零的书籍,并列出了一份清单。随后,另一个事务B插入了一本新书,使得库存数量大于零。当事务A再次查询相同的条件时,它会发现列表中多了一本书,这就是幻读。
幻读不同于不可重复读(Non-repeatable read),后者指的是同一事务中两次读取同一行数据时,由于其他事务对该行进行了更新,导致读取结果不同。而幻读涉及的是整个结果集的变化,特别是新增或删除的行。
12. 死锁是什么导致的?
死锁发生在两个或更多事务相互等待对方持有的资源释放,从而形成循环等待的状态,导致这些事务都无法继续执行下去。这种情况通常是由于以下原因造成的:
循环等待: 当每个事务都在等待另一个事务释放它所需要的资源时,就会形成死锁。例如,事务T1持有资源R1并等待资源R2,而事务T2持有资源R2并等待资源R1。 资源分配顺序不当: 如果多个事务以不同的顺序申请相同的资源,那么很容易引发死锁。比如,两个事务分别以相反的顺序请求两个资源,就有可能陷入死锁。 长事务持有大量资源: 长时间运行的事务会占用较多资源,增加了与其他事务发生冲突的机会,进而提高了死锁的风险。
为了避免死锁,数据库管理系统通常会采用死锁检测和预防机制。死锁检测是通过定期扫描系统中所有等待锁的事务,寻找是否存在死锁环路。一旦发现死锁,系统会选择牺牲其中一个事务(通常是回滚最小的那个),以打破死锁环路。而死锁预防则是通过事先规定资源的分配顺序或限制事务持有资源的时间等方式来避免死锁的发生。
13. 如何避免死锁?死锁是一定可能避免的吗?
要完全避免死锁是非常困难的,尤其是在复杂的应用场景中。但是,可以通过一些策略和技术手段显著降低死锁发生的概率:
固定资源请求顺序: 确保所有事务按照固定的顺序请求资源,可以有效避免循环等待。例如,总是先锁定表A再锁定表B。 尽量缩短事务持续时间: 减少事务持有资源的时间,可以降低与其他事务产生冲突的可能性。为此,应该优化查询效率,尽早提交或回滚事务。 使用更高的隔离级别: 在某些情况下,提升事务的隔离级别可以减少死锁的发生。例如,使用可重复读代替读已提交。 设置锁超时: 给事务设置合理的锁超时时间,当超过时限仍未获得所需资源时自动放弃等待,避免长时间挂起。 死锁检测和回滚: 数据库系统内部会有死锁检测机制,一旦检测到死锁,可以选择性地回滚一个或多个事务来解决问题。
尽管采取了上述措施,但在高并发环境下,完全杜绝死锁几乎是不可能的。因此,重要的是要有应对死锁的预案,比如设计应用程序逻辑来优雅地处理回滚后的重试逻辑。
14. 如何提高数据库的吞吐上限?
提高数据库的吞吐上限意味着增加每秒可以处理的操作数量。这可以通过多种方法实现:
优化查询: 编写高效、针对性强的SQL查询,避免不必要的全表扫描,利用索引加速查询过程。 分片 (Sharding): 将大表拆分成多个较小的子表,并分布在不同的服务器上,以此分散负载。每个分片负责处理一部分数据,从而减轻单一服务器的压力。 读写分离: 实现主从复制架构,其中主服务器负责写入操作,而从服务器则专门用于读取查询。这有助于平衡读写比例,提高整体性能。 缓存: 使用内存缓存(如Redis)来存储频繁访问的数据,减少直接访问数据库的需求,加快响应速度。 连接池: 通过连接池管理数据库连接,减少建立和销毁连接的开销,提高资源利用率。 异步处理: 对于耗时较长的操作,可以考虑采用异步处理方式,例如使用消息队列解耦业务逻辑,使前端请求能够快速返回结果。 硬件升级: 增加CPU核心数、扩展RAM容量或采用更快的存储介质(如SSD),以增强数据库服务器的处理能力和I/O性能。
15. 输入一个SQL,会发生什么?
当输入一条SQL语句后,数据库管理系统(DBMS)会经历一系列步骤来解析、优化并执行这条语句:
解析 (Parsing): SQL语句首先被解析器接收,解析器会验证语法正确性,并将SQL文本转换成内部结构,如解析树。 绑定 (Binding): 解析后的语句会被绑定到数据库元数据,如表结构和列定义,以确定查询的实际含义。 优化 (Optimization): 查询优化器会分析各种可能的执行路径,并选择最优的执行计划。这一步骤非常关键,因为它直接影响到查询的性能。 执行 (Execution): 根据选定的执行计划,执行器开始真正处理SQL语句,可能涉及扫描表、应用过滤条件、排序、聚合等操作。 结果返回 (Result Retrieval): 执行完成后,查询结果会被格式化并返回给客户端应用程序。
在整个过程中,数据库还可能涉及到事务管理、锁控制、并发控制等多个方面的工作,以确保数据的一致性和安全性。
16. 了解数据库一主多从架构吗?
一主多从架构是一种常见的数据库复制模式,其中一台数据库服务器作为主服务器(Master),负责处理所有的写入操作;而其他服务器作为从服务器(Slave),主要用于读取查询。这种架构有几个优点:
提高读性能: 通过将读流量分散到多个从服务器上,可以显著提高系统的读取能力。 冗余和备份: 从服务器可以作为热备份,当主服务器出现故障时,可以从服务器接管服务,确保系统的高可用性。 地理分布: 主从架构还可以用来实现地理分布式的部署,使得用户可以从最近的服务器获取数据,降低延迟。
在实施主从架构时,需要注意同步机制的选择。异步复制是最简单的形式,它允许主服务器在写入完成后立即返回结果,而不需要等待从服务器确认。然而,这种方式可能会导致数据延迟。半同步复制则要求至少有一个从服务器确认接收到更新,才能认为写入成功。这种方式虽然稍微降低了性能,但提供了更强的数据一致性保障。
17. Binlog有哪些格式?
MySQL的二进制日志(binlog)主要用于数据库的主从复制和数据恢复,它有三种不同的格式:
Statement格式:
在这种模式下,每个修改数据的SQL语句都会记录到binlog中。 优点是日志量小,因为只记录了执行的SQL语句而非实际的数据变化。 缺点是在某些情况下可能导致主从不一致,例如当SQL语句是非确定性的或依赖于某些上下文信息时。
Row格式:
这种模式记录的是每一行数据的变化,即记录了每行数据修改前后的值。 优点是可以更精确地反映数据的变化,并且避免了一些非确定性问题。 缺点是日志量大,尤其是对于大量的写操作,会占用更多的存储空间。
Mixed混合格式:
混合模式结合了Statement和Row两种模式的优点。 默认情况下使用Statement格式来记录SQL语句,但在遇到无法安全地以Statement格式记录的情况时,会自动切换到Row格式。 这种方式可以在一定程度上保证数据的一致性同时减少日志量。
18. 数据库的函数有哪些?
数据库提供的内置函数种类繁多,涵盖了数学运算、字符串处理、日期时间操作等多个领域。以下是一些常见的函数类别:
数学函数: 如ABS()(绝对值)、CEIL()(向上取整)、FLOOR()(向下取整)、MOD()(取模)等。 字符串函数: 如CONCAT()(拼接字符串)、LOWER()(转换为小写)、UPPER()(转换为大写)、SUBSTRING()(提取子串)等。 日期时间函数: 如NOW()(当前时间)、DATE_ADD()(添加时间间隔)、TIMESTAMPDIFF()(计算两个时间点之间的差异)等。 聚合函数: 如COUNT()(计数)、SUM()(求和)、AVG()(平均值)、MAX()(最大值)、MIN()(最小值)等。 转换函数: 如CAST()(类型转换)、CONVERT()(字符编码转换)等。 加密函数: 如MD5()、SHA1()、AES_ENCRYPT()、AES_DECRYPT()等。 条件函数: 如IF()、CASE WHEN THEN ELSE END等。
这些函数可以直接在SQL查询中使用,帮助简化复杂的逻辑表达式,同时也增强了SQL语言的功能性。
19. 关系型数据库和非关系型数据库的区别?
关系型数据库(RDBMS)和非关系型数据库(NoSQL)在设计理念、适用场景等方面存在明显差异:
数据模型: RDBMS基于表格存储数据,强调规范化设计,即通过外键关联多个表来表达实体之间的关系;而NoSQL数据库则采用了更加灵活的数据模型,如文档型(MongoDB)、键值对型(Redis)、列族型(Cassandra)和图形型(Neo4j)等。 查询语言: RDBMS普遍支持结构化查询语言(SQL),这是一种标准化且功能强大的查询工具;相比之下,NoSQL数据库通常有自己的查询API,语法各异,灵活性更高但标准化程度较低。 扩展性: NoSQL数据库往往更容易水平扩展(scale out),即通过增加更多的机器来提升性能;而RDBMS更适合垂直扩展(scale up),即通过增强单台服务器的硬件配置来提高性能。 事务支持: RDBMS严格遵循ACID原则,提供完整的事务支持;部分NoSQL数据库仅提供较弱的事务保证,甚至完全不支持事务,但它们通常具有更好的分区容忍性和最终一致性。 应用场景: RDBMS适用于那些需要强一致性和复杂查询的应用,如金融交易系统;NoSQL数据库则更适合处理大规模、非结构化或半结构化的数据,如社交网络、实时分析平台等。
20. 关系型数据库的关系如何体现?
关系型数据库通过外键(Foreign Key)建立了表与表之间的联系,从而体现了所谓的“关系”。外键是一列或多列的组合,它的值必须存在于另一个表的相关列中。例如,一个订单表可以包含一个顾客ID列,作为指向顾客表中主键的外键。这样一来,就可以通过顾客ID将订单表中的记录关联到对应的顾客信息上。
此外,关系还可以通过以下几种方式体现:
一对一关系: 两个表之间有一对一的映射关系,通常通过双方的主键相互引用。 一对多关系: 一个表中的记录可以与另一个表中的多条记录相关联。例如,一个部门可以有多名员工,这时部门表中的部门ID可以作为员工表中外键。 多对多关系: 当两个表之间存在多对多关系时,通常需要引入一个中间表(也叫关联表)来保存两者之间的关系。例如,学生和课程之间可能存在多对多的关系,此时可以创建一个选课表来记录每个学生的选课情况。
21. Redis中如何表示关系?
Redis本身并不是一种关系型数据库,它没有像SQL那样的表结构和外键概念。不过,我们可以通过Redis的数据结构来模拟关系。以下是几种常见的方式:
哈希 (Hash): 可以用来存储对象的属性。例如,存储用户的个人信息,可以用哈希结构保存用户名、邮箱等字段。 集合 (Set) 和 有序集合 (Sorted Set): 适合表示一对多或多对多关系。例如,一个用户可以关注多个话题,这些话题可以用集合来存储。 列表 (List) 和 双端队列 (Stream): 可以用于表示有序关系,如聊天记录或事件流。 位图 (Bitmap) 和 布隆过滤器 (Bloom Filter): 这些特殊的数据结构可以用来高效地处理布尔值或集合成员关系的查询。
通过合理组合这些数据结构,可以在Redis中实现类似关系型数据库的功能,但要注意Redis本质上是非关系型的,因此它不具备事务、外键约束等功能。
22. Redis中的数据结构的底层实现?
Redis的数据结构底层实现依赖于其内部使用的多种数据结构,包括但不限于:
简单动态字符串 (Simple Dynamic String, SDS): Redis的字符串类型实际上是由SDS实现的。SDS是C语言中的一个自定义字符串实现,它提供了安全的字符串操作,并且支持二进制安全。 双向链表 (Double Linked List, DLL): 用于实现列表类型的底层存储。DLL中的每个节点都包含指向前后节点的指针,方便进行双向遍历。 字典 (Dictionary): 字典是Redis中最常用的数据结构之一,它基于哈希表实现,用于存储键值对。为了处理哈希冲突,Redis采用了拉链法,即在同一个桶中维护一个链表。 跳表 (Skip List): 用于实现有序集合(Sorted Set)。跳表是一种概率数据结构,它通过多层索引来加速查找过程,理论上可以达到O(log n)的时间复杂度。 压缩列表 (ZipList): 这是一种紧凑的内存表示形式,适用于小型集合或列表。它将多个元素连续地存储在一起,节省了额外的空间开销。 整数集合 (IntSet): 专为整数设计的小型集合,内部使用数组来存储唯一的整数值。当集合增长到一定程度时,会自动转换为哈希表以提高性能。 哈希表 (Hash Table): 实现了哈希映射,广泛应用于各种场景,如散列表、字典等。
这些数据结构共同构成了Redis的核心功能,使得它能够在高性能的同时保持灵活性。
23. 压缩链表是什么样的?
压缩链表(ZipList)是Redis内部的一种高效数据结构,主要用于存储小规模的列表或集合。它的特点在于将多个元素紧密打包在一起,以减少内存占用和提高访问速度。ZipList的设计理念是牺牲一定的灵活性换取更好的性能和更低的空间成本。
压缩链表的基本结构如下:
头部信息: 记录了整个链表的长度、最后一个元素的大小等元数据。 元素序列: 包含一系列连续的元素,每个元素都有自己的长度信息和内容。元素的内容可以是整数、字符串或其他类型的值。 尾部信息: 存储链表中最后一个元素的偏移量,便于快速定位末尾位置。
ZipList的优势在于它能有效地处理少量数据项,尤其适合那些经常变化但规模较小的数据集合。然而,随着数据量的增长,ZipList的性能会逐渐下降,因此Redis会在必要时自动将其转换为更合适的数据结构,如哈希表或跳表。
思考题:给你300个橙子,你可以向我要若干个箱子,把橙子装进箱子,满足当我说1-300的任意一个数字的时候,你可以吧整数个箱子给我,并且告诉我箱子里已经放好了对应的橙子数
这题很有意思,可以思考一下,然后在评论区给出你的答案。
早日上岸!
推荐阅读:
推荐阅读: