谈一谈数据库的那些事

陌路散爱 提交于 2020-08-12 05:09:40

面向面试的博客,以问答式Q/A方式呈现。


Question1:介绍一下数据库存储引擎,以及罗列你所知道的几种不同的数据库存储引擎?

Answer1:

数据库存储引擎含义

数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。存储引擎主要有: 1. InnoDB,2. Federated ,3. MyIsam ,4. Memory。

存储引擎1:InnoDB ( B+树 )

InnoDB 底层存储结构为B+树, B树的每个节点对应innodb的一个page,page大小是固定的,一般设为 16k。其中非叶子节点只有键值,叶子节点包含完成数据

在这里插入图片描述
适用场景:
1)并发,经常更新的表,适合处理多重并发的更新请求。
2)容灾,可以从灾难中恢复(通过 bin-log 日志等)。
3)事务,支持事务。
4)外键,外键约束,只有它支持外键。
5)主键,支持自动增加列属性 auto_increment。





存储引擎2:TokuDB( Fractal Tree-节点带数据 )

TokuDB 底层存储结构为 Fractal Tree,Fractal Tree 的结构与 B+树有些类似, 在 Fractal Tree中,每一个 child 指针除了需要指向一个 child 节点外,还会带有一个 Message Buffer ,这个Message Buffer 是一个 FIFO 的队列,用来缓存更新操作

例如,一次插入操作只需要落在某节点的 Message Buffer 就可以马上返回了,并不需要搜索到叶子节点。这些缓存的更新会在查询时或后台异步合并应用到对应的节点中。

在这里插入图片描述

优点:TokuDB 在线添加索引,不影响读写操作, 非常快的写入性能, Fractal-tree 在事务实现上有优势。 他主要适用于访问频率不高的数据或历史数据归档。

存储引擎3:MyIASM

MyIASM是MySQL默认的引擎,不支持数据库事务,也不支持行级锁和外键。

优点:执行写操作(INSERT或UPDATE)时需要锁定整个表(即使用表级锁),效率便会低一些。

缺点:执行读取操作的速度很快,而且不占用大量的内存和存储资源。在设计之初就预想数据组织成有固定长度的记录,按顺序存储的。

存储引擎4:Memory

Memory(也叫 HEAP)堆内存:使用内存中的内容来创建表。每个 MEMORY 表只实际对应一个磁盘文件。

优点:访问快,MEMORY 类型的表访问非常得快,因为它的数据是放在内存中的,并且默认使用HASH 索引。
缺点:易失,一旦服务关闭,表中的数据就会丢失掉,因为内存是易失性的。

Memory 同时支持B树索引和散列索引,B树索引可以使用范围查询和通配查询,也可以使用<,>和>=等操作符方便数据挖掘,散列索引相等的比较快但是对于范围的比较慢很多。


Question2:谈一谈如何设计出高效的索引(或谈一谈你所知道的索引设计法则或如何在索引层面提高查询速度)?

Answer2:

索引(Index)是帮助 MySQL 高效获取数据的数据结构。常见的查询算法,顺序查找,二分查找,二叉排序树查找,哈希散列法,分块查找,平衡多路搜索树 B 树(B-tree)。

常见索引原则有:

  1. 选择唯一性索引。做法:唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。
  2. 重要的字段才索引。做法:为经常需要排序、分组和联合操作的字段建立索引 ;为经常作为查询条件的字段建立索引 ; 避免带函数的查询参与索引(索引列不能参与计算,保持列“干净”)。
  3. 索引列选择。尽量使用数据量少的索引;尽量选择区分度高的列作为索引,区分度的公式是表示字段不重复的比例。
  4. 限制索引的数目。理由:越多的索引,会使更新表变得很浪费时间。做法:删除不再使用或者很少使用的索引;尽量的扩展索引,不要新建索引。
  5. 限制索引长度。理由:如果索引的值很长,那么查询的速度会受到影响。做法:尽量使用前缀来索引,如果索引字段的值很长,最好使用值的前缀来索引;最左前缀匹配原则,非常重要的原则。

Question3:介绍一下数据库的三大范式?

Answer3:

范式是具有最小冗余的表结构。三大范式具体如下:

1、第一范式(1st NF -列都是不可再分,拆分列)

第一范式的目标是确保每列的原子性:如果每列都是不可再分的最小数据单元(也称为最小的原子单元),则满足第一范式(1NF)。

下图左边Address列同时描述国家和城市两个属性,将其优化以满足第一范式,分为Country City两个列,保证每个列描述一个属性。

在这里插入图片描述

2、第二范式(2nd NF -每个表只描述一件事情,拆分表)

首先满足第一范式,并且表中非主键列对主键的完全依赖。 第二范式要求每个表只描述一件事情。

下图左边Orders表既描述订单的意思(订单编号、订购日期两个列),又描述产品的意思(产品编号、价格两个列),将其优化以满足第二范式,分为Orders订单表和Products产品表,保证每一个表描述一个事情,非主键列对主键列完全依赖。
在这里插入图片描述

3、第三范式(3rd NF - 不存在对非主键列的传递依赖,去掉多余的列)

第三范式定义是,满足第二范式,并且表中任何非主键列不依赖于其它非主键列,除了主键订单编号外,顾客姓名依赖于非主键顾客编号。

下图左边Orders表,顾客姓名列除了依赖订单编号,也可以单独有顾客编号确定,也依赖顾客编号,将其优化以满足第三范式,去掉这个顾客姓名列。

在这里插入图片描述


Question4:介绍一下数据库事务ACID ?

Answer4:

事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行 。事务是一个不可分割的工作逻辑单元。

事务必须具备以下四个属性,简称 ACID 属性:

原子性( Atomicity )
事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。

一致性( Consistency )
当事务完成时,数据必须处于一致状态。

隔离性( Isolation )
对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。

永久性( Durability )
事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性。


Question5:介绍一下数据库事务隔离机制?

Answer5:

数据库事务隔离相关问题的含义

脏读:指一个线程中的事务读取到了另外一个线程中未提交的数据。
不可重复读(虚读):指一个线程中的事务读取到了另外一个线程中提交的update的数据。
幻读:指一个线程中的事务读取到了另外一个线程中提交的insert的数据。

四种隔离级别

READ UNCOMMITTED(未提交读)
在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读( Dirty Read),这个级别会导致很多问题,从性能上来说, READ UNCOMMITTED不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。

READ CONIIED(提交读)
大多数数据库系统的默认都是 READ COMITTED(但 MySQL不是),READ COMMITTED满足前面提到的隔离性的简单定义:一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的,这个级别有时候也叫做不可重复读( nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。

REPEATABLE READ(可重复读)
REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的,但是理论上,可重复读隔离级别还是无法解决另外一个幻读 ( Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行( Phantom Row). InnoDB和 XtraDB存储引擎通过多版本并发控制(MvC, Multiversion Concurrency Control)解决了幻读的问题,。可重复读是MySQL的默认事务隔离级别

SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了幻读问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别, 只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。

一表小结

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能

Question6:介绍一下数据库事务存储过程( 特定功能的SQL语句集)?

Answer6:

一组为了完成特定功能的 SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。

存储过程优化思路:

  1. 避免循环:尽量利用一些 sql 语句来替代一些小循环,例如聚合函数,求平均函数等。
  2. 避免循环:查找语句尽量不要放在循环内。
  3. 中间结果:中间结果存放于临时表,加索引。
  4. 游标Cursor:少使用游标。sql 是个集合语言,对于集合运算具有较高性能。而 cursors 是过程运算。比如对一个 100 万行的数据进行查询。游标需要读表 100 万次,而不使用游标则只需要少量几次读取。
  5. 事务:事务越短越好。sqlserver 支持并发操作。如果事务过多过长,或者隔离级别过高,都会造成并发操作的阻塞,死锁。导致查询极慢,cpu 占用率极地。
  6. 异常:使用 try-catch 处理错误异常。

Question7:谈一谈触发器(一段能自动执行的程序)?

Answer7:

触发器是一段能自动执行的程序,是一种特殊的存储过程。

触发器和普通的存储过程的区别是:触发器是当对某一个表进行操作时触发。
诸如:update、insert、delete 这些操作的时候,系统会自动调用执行该表上对应的触发器。

触发器能够自动响应某种行为,所以对于必须对某种行为做出业务级别响应的情况,触发器很合适。

触发器可以分为两类:DML 触发器和DDL 触发器。DML触发器包含了用于对表或视图的insert、update、delete操作做出响应的T-SQL代码;DDL触发器对服务器或数据库事件做出响应而不是数据修改。


Question8:介绍一下数据库并发策略?

Answer8:

并发控制一般采用三种方法,分别是乐观锁和悲观锁以及时间戳。

乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;悲观锁就刚好相反,觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;时间戳就是不加锁,通过时间戳来控制并发出现的问题。

1、乐观锁
乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据,所以不加锁,操作成功则无事,操作失败则回退。

2、悲观锁

悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,或者反过来说,就是自己修改某条数据的时候,不允许别人读取该数据,只有等自己的整个事务提交了,才释放自己加上的锁,才允许其他用户访问那部分数据。以上悲观锁所说的加“锁”,其实包括两种锁:排它锁(写锁)和共享锁(读锁)。

3、时间戳

时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”,每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量。


Question9:介绍一下数据库锁?

Answer9:

1、行级锁

行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时,Oracle 会自动应用行级锁:

  1. 锁定:INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT]; 语句允许用户一次锁定多条记录进行更新
  2. 解锁:使用 COMMIT 或 ROLLBACK 语句释放锁。

2、表级锁

表级锁定包括表共享读锁(共享锁)与表独占写锁(排他锁)。表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MYISAM 与 INNODB 都支持表级锁定。

3、页级锁

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB 支持页级锁。


Question10:介绍一下数据库基于Redis的分布式锁?

Answer10:

  1. 获取锁的时候,使用 setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0)加锁,锁的 value值为一个随机生成的 UUID,在释放锁的时候进行判断。并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁。
  2. 获取锁的时候调用 setnx,如果返回 0,则该锁正在被别人使用,返回 1 则成功获取锁。 还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。

Question11:介绍一下数据库分区、分表技术?

Answer11:

分库分表有垂直切分和水平切分两种。

垂直切分 ( 按照功能模块 )
将表按照功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立定义数据库 workDB、商品数据库 payDB、用户数据库 userDB、日志数据库 logDB 等,分别用于存储项目数据定义表、商品定义表、用户数据表、日志数据表等。

在这里插入图片描述

水平切分 ( 按照规则划分存储 )
当一个表中的数据量过大时,我们可以把该表的数据按照某种规则,例如 userID 散列,进行划分,然后存储到多个结构相同的表,和不同的库上。

在这里插入图片描述


Question12:谈一谈数据库两阶段提交协议?

Answer12:

分布式事务是指会涉及到操作多个数据库的事务,在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。

XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。

因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

1、准备阶段

事务协调者(事务管理器)给每个参与者(资源管理器)发送 Prepare 消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的 redo 和 undo 日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

2、提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

3、两阶段提交存在的缺点

(1)参与者:同步阻塞问题
执行过程中,所有参与节点都是事务阻塞型的。

(2)协调者:单点故障
由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。

(3)参与者:数据不一致 (脑裂问题)
在二阶段提交的阶段二中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,导致只有一部分参与者接受到了commit 请求。于是整个分布式系统便出现了数据部一致性的现象(脑裂现象)。

(4)协调者:二阶段无法解决的问题 (数据状态不确定)
协调者再发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。


Question13:介绍一下数据库三阶段提交协议?

Answer13:

三 阶 段 提 交 ( Three-phase commit ) , 也 叫 三 阶 段 提 交 协 议 ( Three-phase commit protocol),是二阶段提交(2PC)的改进版本。

与两阶段提交不同的是,三阶段提交有两个改动点。

1、引入超时机制。同时在协调者和参与者中都引入超时机制。

2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC 把 2PC 的准备阶段再次一分为二,这样三阶段提交就有 CanCommit、PreCommit、DoCommit 三个阶段。

(1)CanCommit 阶段
协调者向参与者发送 commit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。

(2)PreCommit 阶段
协调者根据参与者的反应情况来决定是否可以继续进行,有以下两种可能。假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行;假如有任何一个参与者向协调者发送了 No 响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

(3)doCommit 阶段
该阶段进行真正的事务提交,主要包含
a.协调者发送提交请求
b.参与者提交事务
c.参与者响应反馈( 事务提交完之后,向协调者发送 Ack 响应,告诉协调者自己提交完了)
d.协调者确定完成事务。





Question14:介绍一下CAP原则?

Answer14:

CAP 原则又称 CAP 定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

一致性(C)
在分布式系统中的所有数据备份,在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)。

可用性(A)
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)。

分区容忍性(P)
以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。


Question15:谈一谈对柔性事务的认识?

Answer15:

BASE 理论是在 CAP 理论的基础之上的延伸,包括 基本可用(Basically Available)、柔性状态(Soft State)、最终一致性(Eventual Consistency)。

通常所说的柔性事务分为:两阶段型、补偿型、异步确保型、最大努力通知型几种。

1、两阶段型

就是分布式事务两阶段提交,对应技术上的 XA、JTA/JTS。这是分布式环境下事务处理的典型模式。

2、补偿型

TCC 型事务(Try/Confirm/Cancel)可以归为补偿型。

WS-BusinessActivity 提供了一种基于补偿的 long-running 的事务处理模型。服务器 A 发起事务,服务器 B 参与事务,服务器 A 的事务如果执行顺利,那么事务 A 就先行提交,如果事务 B 也执行顺利,则事务 B 也提交,整个事务就算完成。但是如果事务 B 执行失败,事务 B 本身回滚,这时事务 A 已经被提交,所以需要执行一个补偿操作,将已经提交的事务 A 执行的操作作反操作,恢复到未执行前事务 A 的状态。这样的 SAGA 事务模型,是牺牲了一定的隔离性和一致性的,但是提高了 long-running 事务的可用性。

在这里插入图片描述

3、异步确保型
通过将一系列同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响。

在这里插入图片描述

4、最大努力通知型 (多次尝试)
这是分布式事务中要求最低的一种, 也可以通过消息中间件实现, 与前面异步确保型操作不同的一点是, 在消息由 MQ Server 投递到消费者之后, 允许在达到最大重试次数之后正常结束事务。


Question16:介绍一下分布式缓存(包括缓存雪崩、缓存穿透等)?

Answer16:

1、常见问题:缓存雪崩

缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。一般有三种处理办法:

  1. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  2. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
  3. 为 key 设置不同的缓存失效时间。

2、 常见问题:缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。

3、常见技巧:缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

4、常见技巧:缓存更新

缓存更新除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

5、常见技巧:缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在这里插入图片描述

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!