数据库 #
索引 #
索引是什么,有什么作用,有何优缺点? #
索引是帮助Mysql高效获取数据的一种数据结构,通常用B树,B+树实现(Mysql不支持hash)
数据库索引,hash索引与B+树索引的适用场景,为什么用B+树索引 #
B+树是一个平衡的多叉树,从根结点到每个叶子结点的高度差不超过1,而且同层级的结点间有指针相互连接。
在B+树上的常规检索,从根结点到叶子结点的搜索效率基本相当,不会出现大幅的波动,而且基于索引的顺序扫描时,也可以利用双指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库、文件系统等场景。
Hash索引,就是采用一定的Hash算法,把键值换算成新的Hash值,检索时不需要类似B+树那样从根结点到叶子结点逐级查找,只需要一次Hash算法即可立即定位到相应的位置,速度非常快。
对比
- 如果是等值查询,那么Hash索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值。前提是键值唯一。
- 如果是范围查询检索,这时候Hash索引就毫无用武之地了。
- 同理,Hash索引也无法利用索引完成排序,以及Like这样的部分模糊查询,这种模糊查询本质上也是范围查询。
- Hash索引不支持复合索引,对于复合索引来说,Hash索引再计算Hash值的时候是将索引键合并后再一起计算Hash值,不会对每个索引单独计算Hash值。因此,如果用到复合索引的一个或者几个索引时,索引会失效。
- B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键的情况下,Hash索引的效率也是极低的,因为存在哈希冲突问题。
应用场景
- B+树索引结构适用于绝大多数场景
- 如果数据离散型高、基数大,且为等值查询的时候,Hash索引特别有优势
B 树与 B+ 树的对比
在单行查询的时候,B+ 树会自顶向下逐层查找结点,最终找到匹配的叶子结点。这看起来和 B 树差不多,但其实有两点不同。首先,B+ 树的中间结点没有具体数据,所以同样大小的磁盘页可以容纳更多的结点元素,这就意味着,数据量相同的情况下,B+ 树的结构比 B 树更加 “矮胖”,因此查询时 IO 次数也更少。其次,B+ 树的查询必须最终查找到叶子结点,而 B 树只要找到匹配元素即可,无论匹配元素处于中间结点还是叶子结点。因此,B 树的查找性能并不稳定(最好情况是只查根结点,最坏情况是查到叶子结点)。而 B+ 树的每一次查找都是稳定的
我们再来看看范围查询。B 树做范围查询只能依靠繁琐的中序遍历,而 B+ 树只需要在链表上做遍历即可:即先自顶向下找到范围的下限,再通过链表指针遍历到目标元素
除了查询,还有插入和删除操作,因为 B+ 树的叶子结点包含所有元素,并且以有序的链表结构存储,这样大大提高了增删结点的效率
综上,B+ 树相比 B 树的优势:
磁盘 IO 次数更少 查询性能稳定 范围查询简便 增删结点时,效率更高
主键与非主键和索引的关系 #
主键索引指的就是在主键上做索引,而非主键索引也就是在非主键上加索引。主键索引和非主键索引是有区别的,主键索引存放的值是整行字段的数据,而非主键索引上存放的值不是整行字段的数据,而存放主键字段的值。
因此在使用主键索引查询的时候,直接就可以获得想要的数据,而用非主键索引则会先查询到主键,之后根据主键查询到具体的信息。
非主键索引又称为二级索引,主键索引又称为聚簇索引。
聚簇索引定义:
- 索引和数据是放在一块的(一个文件存储,主键索引的B+树的叶子节点中存放了索引值和数据行所有字段) 索引的顺序和数据的物理存储一致(因为字段也在B+树的叶子节点中,因此索引按序则整个数据行也是按序的)
- 非聚簇索引定义: 索引和数据是分开存放的(两个文件存储,索引的B+树的叶子节点中只存放了索引值和指向对应数据行的物理地址) 索引的顺序和数据的物理存储不一致(B+树中的索引值是按序的,但指针中的对应数据行的物理地址并不是按序的)
记住一个结论:
- InnoDB使用的都是聚簇索引
InnoDB的主键索引是严格的聚簇索引,B+树叶子节点中存放主键索引值和对应数据行所有字段。非主键索引不是严格的聚簇索引但也归为其中,B+树叶子节点中存放的是非主键索引值和对应主键值。因此InnoDB中使用非主键索引来查询数据,需要查两棵B+树。
唯一索引 #
主键和唯一键都是关系数据库中的唯一键,他们保证一列或一组列上的值的唯一性。主键约束中已经存在预定义的唯一键约束。
唯一键是表的一个或多个列/字段的集合,它们唯一地标识数据库表中的记录。 UNIQUE KEY
约束确保一列中的所有值在数据库中都是唯一的。就像主键一样,唯一键也可以包含多个列。但是,唯一键只能接受一个空值。数据库表中没有两行具有相同的值。
唯一键与主键非常相似,可以在创建表的过程中进行定义。当一列或一组列在关系数据库系统中被标记为唯一时,它将在分配约束之前检查值的完整性,以防止两个记录在特定列中具有相同的值。
UNIQUE
是对非PRIMARY KEY
列的约束,其特征如下:
UNIQUE KEY
约束保证值的唯一性。- 可以在一个表上定义多个唯一键。
- 一列可以包含
NULL
值,但每列只允许一个NULL
值。 - 默认情况下,唯一键可能会创建非聚集索引。
主键 | 唯一键 |
---|---|
主键用于唯一标识数据库表中的记录/行。 | 唯一键用于唯一标识表中所有可能的行,而不仅仅是当前存在的行。 |
它不接受NULL值。 | 表中只能接受一个NULL值。 |
默认情况下,它是聚簇索引,数据按聚簇索引顺序组织。 | 默认情况下,它是唯一的非聚集索引。 |
一个表中只能有一列是主键。 | 一个表多列可以具有多个唯一键。 |
主键是通过使用PRIMARY KEY 约束定义的。 |
唯一键使用UNIQUE 约束表示。 |
用于标识表中的一行。 | 用于防止列中的重复值。 |
主键值不能更改或删除。 | 唯一键值可以修改。 |
Bloom Filter的特点 #
布隆过滤器(Bloom Filter)实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率而且删除困难。
位图(Bitmap) #
Redis
当中有一种数据结构就是位图,布隆过滤器其中重要的实现就是位图的实现,也就是位数组,并且在这个数组中每一个位置只有0
和1
两种状态,每个位置只占用1
个 bit,其中0
表示没有元素存在,1
表示有元素存在。
如下图所示就是一个简单的布隆过滤器示例(一个key
值经过哈希运算和位运算就可以得出应该落在哪个位置):
哈希碰撞 #
上面我们发现,lonely
和wolf
落在了同一个位置,这种不同的key
值经过哈希运算后得到相同值的现象就称之为哈希碰撞。发生哈希碰撞之后再经过位运算,那么最后肯定会落在同一个位置。
如果发生过多的哈希碰撞,就会影响到判断的准确性,所以为了减少哈希碰撞,我们一般会综合考虑以下2
个因素:
- 增大位图数组的大小(位图数组越大,占用的内存越大)。
- 增加哈希函数的次数(同一个
key
值经过1
个函数相等了,那么经过2
个或者更多个哈希函数的计算,都得到相等结果的概率就自然会降低了)。
上面两个方法我们需要综合考虑:比如增大位数组,那么就需要消耗更多的空间,而经过越多的哈希计算也会消耗cpu
影响到最终的计算时间,所以位数组到底多大,哈希函数次数又到底需要计算多少次合适需要具体情况具体分析。
布隆过滤器的 2 大特点 #
下图就是一个经过了2
次哈希函数得到的布隆过滤器,根据下图我们很容易看到:假如Redis
根本不存在,但是Redis
经过2
次哈希函数之后得到的两个位置已经是1
了(一个是wolf
通过f2
得到,一个是Nosql
通过f1
得到,这就是发生了哈希碰撞,也是布隆过滤器可能存在误判的原因)。
所以通过上面的现象,我们从布隆过滤器的角度可以得出布隆过滤器主要有2
大特点:
- 如果布隆过滤器判断一个元素存在,那么这个元素可能存在。
- 如果布隆过滤器判断一个元素不存在,那么这个元素一定不存在。
而从元素的角度也可以得出2
大特点:
- 如果元素实际存在,那么布隆过滤器一定会判断存在。
- 如果元素不存在,那么布隆过滤器可能会判断存在。
PS:需要注意的是,如果经过N
次哈希函数,则需要得到的N
个位置都是1
才能判定存在,只要有一个是0
,就可以判定为元素不存在布隆过滤器中。
介绍MySql的事务 #
- 事务是一个不可分割的执行单元
- 事务作为一个整体要么一起执行,要么一起回滚
事务的特性 #
- 原子性:事务是一个整体,不可再分,要么一起执行,要么一起不执行。
- 一致性:事务完成时,数据必须处于一致的状态
- 隔离性:每个事务都是相互隔离的
- 永久性:事务完成后,对数据对修改都是永久的
原子性:不能被进一步分割的最小粒子”,而原子操作意为 “不可被中断的一个或一系列操作”。
事务是如何实现的 #
事务的持久性是通过事务日志来保证的,包括重做日志(redo log)和回滚日志(undo log)。
当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志(redo log)中,然后再对 数据库中对应的进行修改。这样做的好处是,即使数据库系统奔溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事物具有持久性。
而当事务需要回滚的时候,就用到了回滚日志(undo log),从而使事物具有原子性和一致性。
简单整理下他们的关系:
- 事务的隔离性:由【锁机制】实现;
- 事务的原子性、一致性和持久性:由事务的 redo log和undo log日志来保证;
- redo log: 重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性;
- undo log:回滚日志,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
redolog,binlog,undolog #
redo log和undo log是InnoDB存储引擎层的日志,bin log是MySQL Server层记录的日志,两者都是记录了某些操作的日志(不是所有),自然有一些重复,但两者的记录格式不同。
redo log #
用于记录事物操作的变化,记录的是数据修改后的值,不管事务是否都会记录下来。
作用:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
内容:
redo log是物理格式的日志,记录的是物理数据页面的修改的信息,其redo log是顺序写入redo log file的物理文件中去的。
产生:
redo log是循环写,日志空间大小固定。
事务开始之后就产生redo log,redo log 的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中便开始写入redo log文件中。原因就是,重做日志有一个缓存区Innodb_log_buffer,Innodb_log_buffer的默认大小为8M,Innodb存储引擎先将重做日志写入innodb_log_buffer中。
释放:
当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志(redo log)占用的空间就可以重用(被覆盖)。
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。
内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
平时很快的更新操作,都是在写内存和日志,他并不会马上同步到磁盘数据,这时内存数据页跟磁盘数据页内容不一致,我们称之为“脏页”。
undo log #
undo log是在事务开始之前保存的被修改数据的一个版本,产生undo日志的时候,同样会伴随类似于保护事务持久化机制的redolog的产生。
作用:
保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。
产生:
事务开始之前,将当前是的版本生成undo log,undo 也会产生 redo 来保证undo log的可靠性。
释放:
当事务提交之后,undo log并不能立马被删除,而是放入待清理的链表,由purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。
bin log #
bin log 是MySQL Server层记录的日志,所有引擎都可以使用,这样在数据库用别的存储引擎时可以达到一致性的要求。
作用:
用于数据复制和数据还原。在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
内容:
逻辑格式的日志。包括了执行的sql语句(增删改)以及反向的信息,也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。
因此可以基于binlog做到类似于oracle的闪回功能,其实都是依赖于binlog中的日志记录。
产生:
bin log是追加写,是指一份写到一定大小的时候会更换下一个文件,不会覆盖。
事务提交的时候,一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到 bin log 中。这里与 redo log 很明显的差异就是 redo log 并不一定是在事务提交的时候刷新到磁盘,redo log是在事务开始之后就开始逐步写入磁盘。
因此对于事务的提交,即便是较大的事务,提交(commit)都是很快的,但是在开启了bin_log的情况下,对于较大事务的提交,可能会变得比较慢一些。这是因为 bin log 是在事务提交的时候一次性写入造成的。
释放:
bin log 的默认是保持时间由参数expire_logs_days配置,也就是说对于非活动的日志文件,在生成时间超过expire_logs_days配置的天数之后,会被自动删除。
MVCC机制 #
MVCC机制–全称multi version concurrent control,多版本并发控制机制
读已提交和可重复读都用到了MVCC机制
MVCC是处理读写冲突的手段,目的在于提高数据库高并发场景下的吞吐性能。
MVCC最大的优势:读不加锁,读写不冲突。读写不冲突是非常重要的,极大的增加了系统的并发性能。MVCC机制也是乐观锁的一种体现。
特点:
- 允许多个版本同时存在,并发执行
- 不依赖锁机制,性能高
- 只在读已提交和可重复读的事务隔离级别下工作
常用概念 #
-
ReadView
可以理解为数据库中某一时刻所有未提交事务的快照。
-
隐藏列
InnoDB存储引擎中,它的聚簇索引记录中都包含两个必要的隐藏列。
-
事务链
每次对记录进行修改时,都会记录一条undo log信息,每条undo log信息都包含一个roll_pointer属性,可以将这些undo日志都连起来,串成一个链表。
并发事务带来了哪些问题? #
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
- 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
- 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。
- 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
- 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读有什么区别呢?
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
幻读其实可以看作是不可重复读的一种特殊情况,单独把区分幻读的原因主要是解决幻读和不可重复读的方案不一样。
举个例子:执行 delete
和 update
操作的时候,可以直接对记录加锁,保证事务安全。而执行 insert
操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 insert
操作的时候需要依赖 Next-Key Lock(Record Lock+Gap Lock) 进行加锁来保证不出现幻读。
SQL标准定义了那些事务隔离级别? #
- READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读) :(默认级别)对同一字段段多次读取结果都是一致的,除非数据是被本事事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化) :最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
如何解决不可重复读问题(MVCC) #
MySQL的隔离级别是基于锁实现的吗? #
MySQL的隔离级别是基于锁和MVCC机制共同实现的。
可串行化隔离级别,是根据锁来实现的。其他是根据MVCC机制实现的。
不过,SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
锁 #
表级锁和行级锁有什么区别? #
MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性非常差。
InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。
表级锁和行级锁对比 :
- 表级锁: MySQL 中锁定粒度最大的一种锁,是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
- 行级锁: MySQL 中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
共享锁和排他锁呢? #
不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:
- 共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。
S 锁 | X 锁 | |
---|---|---|
S 锁 | 不冲突 | 冲突 |
X 锁 | 冲突 | 冲突 |
由于 MVCC 的存在,对于一般的 SELECT
语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。
# 共享锁
SELECT ... LOCK IN SHARE MODE;
# 排他锁
SELECT ... FOR UPDATE;
意向锁有什么作用? #
如果需要用到表锁的话,如何判断表中的记录没有行锁呢?一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。
意向锁是表级锁,共有两种:
- 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些加共享锁(S锁),加共享锁前必须先取得该表的IS锁。
- 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X锁),加排他锁之前必须先取得该表的IX锁。
意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
意向锁之间是互相兼容的。
IS 锁 | IX 锁 | |
---|---|---|
IS 锁 | 兼容 | 兼容 |
IX 锁 | 兼容 | 兼容 |
意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。
IS 锁 | IX 锁 | |
---|---|---|
S 锁 | 兼容 | 互斥 |
X 锁 | 互斥 | 互斥 |
InnoDB 有哪几类行锁? #
MySQL InnoDB 支持三种行锁定方式:
- 记录锁(Record Lock) :也被称为记录锁,属于单个行记录上的锁。
- 间隙锁(Gap Lock) :锁定一个范围,不包括记录本身。
- 临键锁(Next-key Lock) :Record Lock+Gap Lock,锁定一个范围,包含记录本身。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
InnoDB 的默认隔离级别 RR(可重读)是可以解决幻读问题发生的,主要有下面两种情况:
- 快照读(一致性非锁定读) :由 MVCC 机制来保证不出现幻读。
- 当前读 (一致性锁定读): 使用 Next-Key Lock 进行加锁来保证不出现幻读。
当前读和快照读有什么区别? #
快照读(一致性非锁定读)就是单纯的 SELECT
语句,但不包括下面这两类 SELECT
语句:
SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。
快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。
只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读:
- 在 RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。
- 在 RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本。
快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。
当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。
当前读的一些常见 SQL 语句类型如下:
# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...
乐观锁和悲观锁本质的区别是什么 #
乐观锁:指的是在操作数据的时候非常乐观,乐观地认为别人不会同时修改数据,因此乐观锁默认是不会上锁的,只有在执行更新的时候才会去判断在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。
冲突比较少的时候, 使用乐观锁(没有悲观锁那样耗时的开销) 由于乐观锁的不上锁特性,所以在性能方面要比悲观锁好,比较适合用在DB的读大于写的业务场景。
悲观锁:指的是在操作数据的时候比较悲观,悲观地认为别人一定会同时修改数据,因此悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能操作数据。
冲突比较多的时候, 使用悲观锁(没有乐观锁那么多次的尝试)对于每一次数据修改都要上锁,如果在DB读取需要比较大的情况下有线程在执行数据修改操作会导致读操作全部被挂载起来,等修改线程释放了锁才能读到数据,体验极差。所以比较适合用在DB写大于读的情况。
读取频繁使用乐观锁,写入频繁使用悲观锁。
数据库调优 #
- 查找、定位慢查询,并优化
- 创建索引:创建合适的索引提高查询速度
- 分表:当一张表的数据比较多或者一张表的某些字段的值比较多并且使用时改用水平分布和垂直分表来优化。
- 读写分离(集群):当一台服务器不能满足需要时,采用读写分离的方式进行集群
- 缓存:使用redis来进行缓存
- 库级优化
站在数据库的维度上进行优化,比如控制一个库中的数据表数量。或者采用主存架构来优化读写策略。
如果读写的业务量都很大,并且它们都在同一个数据库服务器中进行操作,那么数据库的性能就会出现瓶颈,这时为了提升系统的性能,优化用户体验,我们可以采用读写分离的方式降低主数据库的负载,比如用主数据库完成写操作,用从数据库完成读操作。
分库:
我们还可以对数据库分库分表。当数量级达到亿级以上的时,有时候我们需要把一个库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。
垂直切分和水平切分:
垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。
垂直分表:将一个表按照字段分成多表,每个表存储其中一部分字段
水平分库是把同一个表的数据按一定的规则拆到不同的数据库中,每个库可以放在不同的服务器上。
水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中。
采用垂直分表的形式,就是将一张数据表分拆成多张表,采用水平拆分的方式,就是将单张数据量大的表按照某个属性维度分成不同的小表。分拆在提升数据库性能的同时,也会增加维护和使用成本。
如果数据库中的数据表过多,可以采用垂直分库的方式,将关联的数据表部署在一个数据库上。如果数据表中的列过多,可以采用垂直分表的方式,将数据表分拆成多张,把经常一起使用的列放到同一张表里。
如果数据表中的数据达到了亿级以上,可以考虑水平分表,将大的数据表分拆成不同的子表,每张表保持相同的表结构。比如你可以按照年份来划分,把不同年份的数据放到不同的数据表中。2017 年、2018 年和 2019 年的数据就可以分别放到三张数据表中。
插入优化 #
插入数据的优化点:主要在于最大程度上利用每一次数据库连接,避免频繁创建数据连接。
常用的优化方式如下:
- 批量插入(单条插入需要每次都与数据库创建连接,存在比较大的消耗)
- 手动管理事务(可以将多个数据批量放入在一个事务中,减少开启、关闭事务的次数)
- 数据按照主键顺序插入(避免页分裂和重新指针指向)
- 大数据量时使用load指令(如初始化时需要几百甚至上千万数据(百万数据十几秒),此时则使用load命令来进行插入数据,mysql原生支持大数据量插入,性能非常高)
load命令的使用:
如果是命令行连接,需要指定客户端需要执行本地文件,在连接中添加:–local-infile
mysql –local-infile -u root -p
服务端开启load指令支持:set grobal local_infile=1
语法:load data local infile ‘文件路径’ into table ‘表名’ fields teminated by ‘字段分割符号’ lines teminated by ‘行分割符号’
主键优化
**页(Page):**存放的就是具体的行数据
特点:页可以为空、也可以填充一半,或者填充100%。每个页包含了2-N行数据(如果一行数据太大,会行溢出),页中数据根据主键排序(InnoDB中规定每页中至少大于2行,如果只有一行,证明形成了链表,在InnoDB中是允许的)。页与页之间页存在指针相互指向。
页分裂:
如果插入数据是数据的逐渐时乱序插入,因为InnoDB中数据是按照主键顺序存放在页中的,它会找到本应该插入的数据页50%的位置(改数据页因为乱序插入已经满了),然后将之后的元素以及新插入的元素放到新申请的页中。然后指针重新指向的现象。
页合并:
**注意:**在InnoDB中,当删除一个记录时,实际上记录并没有被物理删除,只是记录被标记为(flaged)删除,并且它的空间变得允许被其他记录声明使用。
**定义:**当页中数据被删除到MERGE_THRESHOLD(默认是页的50%),InnoDB会开四季寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用。
MERGE_THRESHOLD参数在创建表或者索引时可以进行指定,默认就是页的一半。
主键设计原则:
- 满足业务需求情况下,尽量降低主键的长度(因为二级索引叶子节点存储的是主键值,主键值越长,占用的空间越大,在搜索时需要耗费磁盘IO的次数就越多)
- 插入数据时,尽量顺序插入,选择使用AUTO_INCREMENT自增主键(乱序插入可能导致页分裂,消耗性能)
- 尽量不要使用UUID做主键或者其他自然主键如身份证(因为他们是无序的,还是会存在页分裂,同时因为他们的长度比较长,在检索时会消耗大量的磁盘IO)
- 业务操作时,尽量避免对主键对修改(修改了主键,需要重新维护对应的索引数据结构)
查询优化 #
1、Order by优化
使用explain关键字查看SQL语句的执行计划,注意:出现Using index的前提是有了覆盖索引,多字段排序时,也遵循最左前缀法则。
- Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓存区sort buffer中完成排序操作。所有不是通过索引直接返回排序结果的排序都叫Filesort排序。
- Using index:通过有序索引顺序扫描直接返回有序数据,这种情况称为using index,它不需要额外排序,操作效率高。
- Backward index scan;Using index:没有进行额外排序,但进行了反向扫描索引。
- Using index;Using filesort:没有直接通过索引返回有序数据,需要走sort buff进行排序,效率也是较低。
Using filesort优化方式:
- 给对应的字段创建联合索引(注意要根据排序的顺序或者倒叙指定索引的顺序)
- 如果不可避免出现filesort,在对大数据量排序时,可以适当增加排序缓冲区大小sort_buffer_size(默认时256k),查询方式:show variales like ‘sort_buffer_size’。
- 如果排序缓冲区被占满,则会在磁盘进行排序操作,性能会降低。
2、group by优化
分组操作中,主要是索引起了优化效果。使用explain关键字查看SQL语句的执行计划分组情况如下:
- Using temporary:使用了临时表,性能较低。
- Using index:用了索引,性能较高(案例:group by 和where中字段满足最左前缀法则)
- Using index;Using temporary:案例:如不遵循最左前缀法则,但命中索引覆盖时,可能出现这个值
优化技巧:通过索引来提高效率,注意是否满足最左前缀法则
3、Limit优化
现象:在大数量时分页时,越往后的数据,需要耗时越大,效率越大
优化:子查询(多表关联)+覆盖索引
方式:先查询到需要筛选数据的主键,然后再进行数据子查询或者表关联查询到需要的具体数据
4、Count优化
这个话题已经是老生常谈了,但是总有人争论不休,其实,最优权威的是官方的说法,官方是推荐使用count(*)而不是其他,下面来认识各种count用法的一个区别。
MyISAM引擎会把一个表中的总行数存储到磁盘中,在执行count(*)不带where条件时,可以直接拿到该数据,效率很高。
InnoDB在count时,需要将数据一行行从引擎读取出来,然后累计计数(大数量的情况下是比较耗时的,主要是由存储引擎决定的)。
优化思路:借助内存数据库手动维护总条数,插入时加1,删除时减1等
count的用法:
- count(*): 对返回的数据进行计数。逻辑:引擎做了专门优化,不取值,服务层直接按行进行累加。
- count(主键):主键不可能为NULL,InnoDB会遍历全表、将每行的主键ID取出来,返回给服务层进行累计操作,无需判断是否为NULL。
- count(1):对返回的每条数据都置1,然后进行累计。逻辑:引擎遍历全表,但是不取值,服务层对返回的每一行都放一个数字"1"进去,直接进行累加操作。
- count(列):统计字段值不为NULL的条数。统计逻辑:没有not null约束,idb引擎会遍历全表的每一行的字段值取出来,返回给服务层,服务层会判断是否为null,不为null则进行累加。如果有not null约束,则引擎会遍历全表返回每一行的字段值,返回给服务层,服务层直接进行累加操作。
推荐使用:count(*)
按照效率排序的话,count(字段)<count(主键id)<count约等于count**(),所以尽量使用count()**
修改优化 #
更新数据时where条件一定要使用索引字段,否则就会从行锁升级为表锁,并发情况下,性能低。
删除优化 #
跟插入语句类似,要利用批量删除的方式,最大程度减少数据库连接,事务提交的消耗。
基础 #
什么是关系型数据库? #
是指采用关系模型来组织数据的数据库,其以行和列的形式存储数据,以便于用户理解,关系型数据库这一系列的行和列被称为表,一组表组成了数据库。关系模式就是二位表模型。
关系型数据库的优势 #
- 易于理解
- 支持复杂查询,可以用SQL语句方便的在一个表以及多个表之间做非常复杂的数据查询
- 支持事务,可靠的处理事务并保持事务的完整性,使得对于安全性能很高的数据访问要求得以实现。
常见的关系型数据库? #
- MySql
- PostgreSQL
- Oracle
- SQL server
- SQLite
MySql存储引擎有哪些?默认使用那个? #
- InnoDB (默认) 支持事务,其他不支持
- MylSAM(只有表级锁,没有行级锁,不支持事务,不支持外键,不支持MVCC)
第一第二第三范式 #
1NF(第一范式)
属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。1NF 是所有关系型数据库的最基本要求 ,也就是说关系型数据库中创建的表一定满足第一范式。
2NF(第二范式)
2NF 在 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。如下图所示,展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列,这个列称为主键,非主属性都依赖于主键。
一些重要的概念:
- 函数依赖(functional dependency) :若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
- 部分函数依赖(partial functional dependency) :如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖与(学号,身份证号);
- 完全函数依赖(Full functional dependency) :在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级);
- 传递函数依赖 : 在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。。
3NF(第三范式)
3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。
总结
- 1NF:属性不可再分。
- 2NF:1NF 的基础之上,消除了非主属性对于码的部分函数依赖。
- 3NF:3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。