数据库基础知识

另可参考:http://www.stevenwash.xin/2018/04/21/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/

存储引擎MyISAM和InnoDB的区别

InnoDB

1、支持事务、支持外键、支持行锁
2、不支持全文索引
3、用来处理巨大量数据,CPU效率非常高

MyISAM

1、不支持事务、不支持外键、不支持行锁(支持表锁)
2、支持全文索引
3、MyISAM相对简单,所以在效率上要由于InnoDB
4、在大并发下的读写可能出现表损坏

InnoDB的原理

所有的数据库都被逻辑的存放在表空间里面,每个表空间含有多个段空间(Segment),每个段空间又含有多个区空间(Extent),然后在每个区空间里面含有多个页(Page),在页空间里面存着每一行的数据。

页是Innodb管理的最小单位,最小为4KB,最大为64KB,默认为16KKB。

如何存储数据

Innodb会将表的定义和数据索引等信息分开来进行存储,其中表的结构存储在.frm文件中,数据索引文件存储在.ibd文件(即表数据和相关的索引数据)中。

在每一页中的数据都是用行来进行存储的,所以存储的时候会有固定的行格式。

行格式:Barracuda在向下兼容Antelope(COMPACT和REDUNDANT)的同时增加了COMPRESSED和DYNAMIC两种行格式

在处理行溢出数据的时候,比如存储VARCHAR或者BLOB的大对象的数据类型的时候:
对于Compact 和 Redundant的格式来说:先将行数据中的前768个字节存储在数据页中,然后后面会有一个偏移量指针指向溢出页的地址。

对于Comparessed 和 Dynamic的格式来说:都只会在行记录中保存20字节的指针,然后这个指针指向溢出页面,实际的数据都会存储在溢出页面中。

在实际存储数据的时候,为了插入和删除的效率,整个页面并不会对数据按照主键进行排序,它会自动从左侧到右寻找空白节点进行插入操作,所以行记录在物理存储上并不是连续的,而是通过next_record指针来进行指示下一条记录所在的位置。

数据库索引

是什么

索引是对数据表中的一列或者多列数据进行排序的的数据结构,使用索引客以快速访问到数据库的相关数据。

什么数据结构实现

Innodb在绝大多数的情况下使用的是B+树来建立索引。在B+树中查找到的并不是给定key所对应的value,而是数据行所对应的页,然后Innodb会将整个页加载到内存中,然后通过存储在页中的PageDirectory中的稀疏索引和next_record等属性取出记录。

B+树特点:
1、n个关键字就有n个分支
2、每个非叶子结点都不保存数据,只是用来做索引,所有数据都保存在叶子结点上
3、叶子结点包含所有信息,而且由小到大排序
4、叶子结点都通过指针相连,便于当做链表直接顺序访问
5、所有的中间元素都同时存在于子节点中,是子节点中的最大值(或者最小值)

B树的特点:
1、n个关键字有n+1和分支
2、每个节点都存有数据元素,每个关键字的左子树的元素小于该关键字的值,右边元素大于该关键字的值
3、节点内的各个关键字之间是按照从小到大的顺序排序
4、不必每一次查询都查到叶子结点(在一定程度上会快一点,但是就降低查询的稳定性)
5、B树做范围查找相比B+树会慢很多

B+树优势:
1、单一节点存储更多的元素,使得查询的IO次数更少
2、由于所有的查找都会查找到叶子结点,所以查找性能稳定
3、所有叶子结点形成有序链表,便于做范围查找

聚簇索引和非聚簇索引

一张表中包含了一个聚簇索引构成的B+树和若干个辅助索引构成的B+树

聚簇索引

索引项的顺序与表记录的物理存储顺序是一致的,在一张表上只能建一个聚簇索引(一般情况下是主键),因为真实数据的物理顺序只有一种。因为跟物理地址是一一对应的,所以没有地址映射的问题,直接对应数据,即在B+树的叶子结点直接存放的就是记录信息。

非聚簇索引

索引项的顺序与表记录的顺序不一致,在一张表中可以有多个非聚簇索引。通过非聚簇索引(又叫辅助索引)查找到对应的主键,然后通过这个主键在聚簇索引中查找到对应的行记录。

什么时候建立索引

1、经常出现在关键字order by、group by、 distinct后面的字段建立索引
2、在使用union来连接两个结果集的时候,对进行连接的结果集进行建立索引
3、为经常做查询选择的属性建立索引
4、为经常进行表间连接的字段进行索引

Note :
1、索引的创建应该根据具体的应用来设计,找到一个最合适的,没有最好的。因为建立索引了必然就回导致性能下降,所以要控制好索引建立的字段和数量。
2、针对插入很少、查询很多的数据表甚至可以考虑建立索引覆盖

数据库事务

事务的四个特点ACID

1、原子性(A):事务作为整体执行,出错之后会进行事务回滚
2、一致性(C):数据库从一个一致状态转化到另一个一致状态
3、隔离性(I):多个事务并发执行的时候,互不影响
4、持久性(D):一个事务一旦提交对数据库的修改应该是持久的

事务的并发问题

1、丢失更新

当A事务和B事务并发的时候,B事务完成了提交,而A事务执行一半发生了事务回滚,此时会把B事务的修改覆盖了

2、脏读

是指在A事务进行修改的数据库的信息的时候,此时还没有提交事务,B事务读取了A事务修改之后的数据,然后此时A事务又进行了事务的回滚,然后此时B事务读取到底的就不是真实的数据库的数据,这个数据就是脏数据

3、不可重复读

当A事务读取到某个数据为100,B事务读取到为100,然后A事务修改了数据为90,此时B事务又读取的时候发现又是90,此时这个数据经过B事务重复读取之后发现除了问题

特例:当上述的过程中,发生到了B读取100之后,将数据修改为110,然后B提交事务,此时这个数据变为了100,就是相当于把A的修改给覆盖了。

4、幻读

假设A事务统计数据有500条,此时B事务向其中插入了一条数据,然后A事务在进行数据读取的时候发现数据又多了一条,出现了幻读

//个人感觉也是一种不可重复读

数据库的锁有几种

基本的锁类型分为三类:排它锁(X)、共享锁(S)、更新锁(U)

排它锁(X):一次只允许一个事务获取相应的锁,此时该事务可以进行读取和修改数据的操作。其他的事物必须等到X锁释放之后才能进行访问,在事务结束之后就会释放X锁。

共享锁(S):多个事务可以锁定一个共享页,但是获得共享锁之后所有的事务都只能进行读操作,不能进行修改操作。其他的事务都只能再对该记录施加S锁,直到之前的事务释放了S锁才能施加X锁。

更新锁(U):对一个记录实施更新锁的时候,开始的时候允许其他的事务进行读操作,但是此时不允许再施加X锁或者U锁,此时相当于S锁,当读取的记录要进行更新的时候,此时的更新锁将会升级为X锁。

Note :
1、更新锁是相当于是排它锁和共享锁的结合,但是更新锁客以避免出现死锁,而使用共享锁的时候是有可能出现死锁的。

2、所以,共享锁之间是兼容的,但是不和排它锁兼容,排它锁和其他任何锁都不兼容。因为排它锁代表的是写操作,共享锁代表的是读操作,所以在数据库中可以并行读,但是只能串行写。

锁的粒度

在Innodb中实现了页级锁、表锁和行锁,为了实现多粒度的锁机制,添加了意向锁。

意向锁的特点:

1、意向锁是为了表示是否有事务请求锁定表或者表中的某一行数据。
2、意向锁是一种表级锁
3、分为意向互斥锁和意向共享锁

实现方式

没有意向锁的情况:
假设当一个事务A想要对某一个表中的一行数据进行修改,则该事务对这个一行数据加上行级的排它锁,当另一个事务B需要对这个表中所有的数据进行修改的时候,这个事务B则需要扫描表中所有的行,检查每一行的锁情况,这样很耗时。

有意向锁的情况:
同样,当一个事务A想要对一个表中的某一行数据进行修改的时候,会先申请表的意向排它锁,然后再申请行的排它锁。此时,另一个事务B想要对表中所有的记录进行修改的时候,只需要等待加在表上的意向互斥锁释放即可(因为意向互斥锁没有释放就说明有事务在进行表数据的修改操作)。

悲观锁和乐观锁

是两种加锁的思想,比如其中的悲观锁,就相当于是利用排它锁的方式实现。

悲观锁

对于加锁处于一种悲观的态度,即认为冲突是一直会发生的,所以只要要修改数据就会先尝试加上排它锁,如果加锁失败,要么等待要么抛出异常,如果加锁成功,则开始进行相关的操作。由于是排它锁,所以当一个事务对某一条记录加了锁,其他的事务是会被阻塞等待或者出异常。

优缺点:
1、通过先加锁再访问的方式,这样可以保证操作的安全
2、但是频繁加锁会是的开销很大
3、在像只读事务这类只会读取不会修改的时候,用悲观锁都进行加锁会造成很大的性能浪费

乐观锁

对于加锁保持一种乐观的态度,认为一般情况下不会发生冲突,只有在提交修改数据的时候判断是否有冲突,如果有冲突,就返回用户错误信息,没有冲突的话就会直接进行更新操作。

如何判断是否有冲突,就看在数据修改的时候是否有其他的事务对数据进行了修改,由此引出数据版本的信息,通过判断数据版本来判断当前的数据是否在其他大方被更新了。

数据版本的实现方式:
1、通过版本号实现
在数据表中增加一个数据版本标识,每当进行一次更新操作的时候就将当前的版本信息进行增加一个。所以,在读取记录的时候将version读取出来,然后在最后准备要进行更新操作的时候,将当时的数据库中该条记录中的版本信息查询出来跟之前保存的version进行比较,如果一样,这说明没有其他的事务对这个记录进行了修改,则这个时候是安全的,可以直接进行修改操作。如果不一样,则说明数据出现过了修改,发生了冲突,这是将错误信息返回给用户,由用户决定应该怎么处理(稍后重试或者出错)。

2、时间戳
实际上跟上面的方式差不多,只是在增加的字段里面存放的是时间戳的数据,一样进行比较。

Note : 乐观锁的一种具体实现:CAS(比较和交换)

锁的算法

三种锁的实现算法:Record Lock、Gap Lock 和 Next-Key Lock

Record Lock(行锁)

这个锁是直接加在索引记录上的,锁住的是key而不是行数据。

Gap Lock(间隙锁)

间隙锁是对索引记录中一段连续区域的锁,比如,当时用sql语句中范围查找的时候,在这个范围内的记录就会被锁住(但是不包括这个范围本身),若有其他的事务对这个间隙内插入数据就会被阻止。比如:where id < 6,那么就会锁住(-OO,6)区间内,如果是where id between 3 and 6,则就会锁住(3,6)之间的记录,同理id > 6就会锁住(6, +OO)之间了。

主要是解决可重复度模式下的幻读问题。

间隙锁就是存储引擎对性能和并发做出的权衡,因为一次对一个范围内的行数据都加上锁会影响性能,但是不加上锁又会影响并发。

间隙锁可以是共享锁,也可以是排它锁,它们之间是不排斥的,也就是允许同时有不同的事务对一段相同范围的间隙加上共享锁或者互斥锁,唯一阻止的是其他事务向这个范围插入数据。

Next-key Lock

是Record Lock和Gap Lock的结合,锁定一个记录本身(行记录锁),并且锁定一个范围(间隙锁)。

因为在Innodb中对于行的查询都是Next-key Lock的方式,假设有以下的一个记录:
id : 2, 4, 6, 8, 10

因此划分出了如下范围:左开右闭
(-OO,2],(2,4],(4,6],(6,8],(8,10],(10,+oo)

例如:select * from user where id = 8,此时Next-key Lock不仅会将当前值和前面的范围锁住,还会将当前值的后面范围加上Gap Lock,即不仅在(6,8]上加了Next-key Lock锁,还会在(8,10]上加上Gap Lock ,所以整个加锁的区域为:(6,10],此时在这个区间内进行插入的时候就会被锁定。

能有效的防止幻读的发生。

事务的隔离级别

有四个等级,由低到高分别是:读未提交、读已提交、可重复度、序列化

1、Read Uncommitted(可以避免丢失更新):使用查询语句不会加锁,读那些未提交的数据,可能会读到脏数据。
2、Read Committed(可以避免丢失更新、读脏数据):只对记录加记录锁(所以保证读取某一行数据的时候都是已经提交的数据,所以不会读到脏数据),而不会对记录加间隙锁,所以允许新的记录插入到被锁定的记录的附近,所以会导致再次使用查询语句的时候查询的结果不一样,这就导致了不可重复读。
3、Repeatable Read(可以避免丢失更新、读脏数据、不可重复读):多次读取相同范围的数据不会返回不同的数据行,但是可能发生幻读
4、Serializable(可以避免丢失更新、读脏数据、不可重复度和幻读):Innodb隐式的将所有的查询语句加上共享锁,解决了幻读的问题。

Note : MySQL默认的是第三种可重复读,是在Innodb中通过Next-key Lock在某种程度上解决幻读问题

分布式事务(DT)

CAP定理

一致性(Consistency):同样的数据在分布式的各个节点上都是一致的
可用性(Availability):所有在分布式系统的节点都可以处理操作并且进行相应
分区容忍性(Partition Tolerance):即时一部分节点出现了故障导致无法通信,整个系统任然可以正常运行

任何数据库设计中,一个应用只能同时满足上面的两个条件。

BASE理论

在分布式系统更加追求的是可用性,于是有BASE理论:

Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)

一致性方面可以根据具体应用的特点来追求最终一致性。

分布式事务解决方案

2PC协议

主要是处理数据一致性的解决方案。

第一阶段:(准备阶段)事务协调器向每个涉及到事务的不同服务器上的数据库提交准备请求,并且要求返回是否可以进行提交的回复。

第二阶段:(提交/回滚阶段)是真正的事务提交的阶段。此时事务协调者如果发现有一个事务参与者返回了在准备阶段失败了的消息(即不能进行提交的消息),则会要求所有的参与者进行当前事务的回滚操作。如果所有的事务参与者返回的都是可以提交的信息,则事务协调者会通知所有的事务进行事务的提交。

这样就可以保证要么所有节点上的数据库的事务都进行提交,或者都进行回滚,这就保证了一致性,但是在可用性方面还是会有所下降的。

有两个缺点:
1、单点问题:由于协调者在整个二阶段中非常重要,所以一段部署完成协调者所在组件的服务器节点出现不可用宕机的情况,那会对整个分布式系统产生很大的影响。
2、同步阻塞:在二阶段的提交的过程中,所有的服务的参与者都要服从事务协调者完成统一提交,这个过程是会处于阻塞的状态的,此时会有一定的性能影响。

所以相当于是牺牲了一部分的可用性来换取一致性

补偿事务(TCC)

核心:每一个操作,都有一个对应的补偿操作(撤销操作), 相当于是在应用层面的2PC

三个阶段:
1、Try阶段:对业务系统进行检测以及预留资源
2、Confirm阶段:对业务系统进行确认提交
3、Cancel阶段:在业务执行错误的时候,进行事务回滚,并将预留的资源进行释放

在try阶段是进行资源的申请,当try阶段成功的时候,在进行confirm的时候如果发生问题,将会不断进行重试(因为前置条件,即try都已经成功了,所以不断重试就可以保证成功)

本地消息表

核心是通过消息日志的方式来异步执行,这个思路来源于eBay。消息日志可以存储在本地文本、数据库或者消息队列

步骤:
1、首先,消息产生的这一方A,在A中,先完成事务内容(此时包含写业务数据),然后将所执行的操作写入消息日志表中,通过MQ将消息日志信息发送出去(发送的事务由MQ完成,失败重试)。
2、订阅了这个消息的接收方B,接收到消息之后,开始根据消息日志中的信息进行事务的执行操作(此时包含写业务数据)。
3、在第2步中,为了完成消息的幂等性(即不会重复进行同一个事务的操作,见 https://www.cnblogs.com/leechenxiang/p/6626629.html ),需要在执行事务的一方添加一个表trans_recv_log用来记录相关的事务是否被执行了,比如说事务T1执行成功了,则在trans_recv_log中记录T1 success,当用户再次点击(触发)T1事务的执行的时候,会在trans_recv_log中先查找相应的T1事务是否已经执行完了,执行完之后则直接返回,否则就完成该事务。

消息中间件

阿里的RocketMQ支持消息事务,大致流程:
1、先发送prepared消息,获取消息的地址
2、执行本地事务
3、发送确认消息发送的消息,通过第一阶段中拿到的地址进行发送。并修改消息的状态(已提交、已回滚)
4、假设消息的生产方已经 完成了自己的本地事务,并且将消息已经成功发送到消息队列中了,此时对与消息的消费方来说,会出现两个问题:消费超时和消费失败。对于消费超时,解决的方式就是不断的重试(这就需要实现消息的重复问题:1、消费端实现消息幂等 2、业务端利用日志表记录成功处理的消息ID)。另一个问题就是消息的消费失败,RocketMQ的解决方案是人工解决,因为实际上出现消费失败的概率非常小,再加上消费失败的时候如果进行所有的回滚,实现起来的复杂度和消耗非常大,所以没有实现的必要。

Redis和Memcached

Memcached

是一款高性能的分布式内存缓存服务器,缓存数据库的查询,提高再次查询的命中率。

使用过程

1、在使用的时候,先是从缓存中获取数据,如果缓存中存在要查询的数据的时候,则直接将数据读取出来并且返回,如果缓存中不存在要插叙的数据,则将查询的请求发给数据库。
2、数据库进行查询,将查询的结果返回给用户,并且将查询到的数据在缓存中也存一份。
3、为了保证缓存中数据的有效性,每当进行改变数据的操作时(比如修改或者删除数据的时候),就会相应的更新缓存中的相关数据的索引信息。

特点

1、协议简单
2、基于libevent的事件处理
3、内置内存存储方式(不能持久化)
4、memcache是不互相通信的分布式

如何分布式

因为每个服务器中的memcache之间是不互相通信的,那么如何实现的分布式呢?

缓存的流程:客户端的算法将根据存入数据的键来判断将要进行保存的memcahce服务器,选定服务器之后就让相应的服务器进行数据的保存。读数据的时候也是一样的,只要使用跟存入数据的时候一样的算法,计算出相应的key值,然后找到相应的服务器,然后从中取出数据即可。

余数计算法

是标准的memcache标准的分布式方法,步骤如下:
1、先根据key的值计算出crc(循环冗余检验码)值
2、然后使用上面计算出来的值对服务器的个数N进行取模运算:CRC($key)%N

两个问题:
1、当通过上面的计算找到应该做出处理的服务器最后,发现无法连接上这个服务器,此时有一种解决办法是将重试的次数加到key后面,然后对更新之后的key的值进行rehash算法
2、尽管余数计算分散发相当简单,数据分散也很优秀,当添加或者移除服务器的时候,缓存重组的代价相当大。

解释第二个问题:
1、由于hash值是通过%N来进行计算的,所以当增加一个节点或者删除一个节点的时候,N的值都会发生变化,在之后访问缓存中的值的时候如果还是使用一样的%N,此时的到的地址就不是之前存数据的地址了。
2、所以,在增加了节点或者删除节点之后,需要将所有的缓存的值进行rehash,重新映射,这个将是一个灾难性的操作

一致性hash算法

过程:
1、首先计算每个memcache服务器的hash值,将这些值映射在0-2^32-1的数值空间上(当做是一个圆环)
2、然后通过相同的hash算法将要进行缓存的数据进行hash计算得到值并映射在上面的圆上
3、然后从数据映射的位置开始顺时针的查找,将数据保存到第一台找到的memcache的服务器上,如果超过了2^32-1还没有找到则会直接保存在第一台服务器上。

一致性hash的方法实际上第3步已经相当于解决了上面余数算法的第一个问题。在第二个问题中,如果增加或者移除节点,只会影响到相关的两个节点的映射,比如在节点2和3之间插入节点4,此时会将原本2,3之间的数据都映射到3上,变为一部分映射到3上,一部分映射到4上。

优化后的一致性hash算法:
如果采用一般的HASH算法的话,key经过HASH之后的分布会很不均匀,从而导致数据库访问倾斜(出现热点数据集中在某一个服务器上),为了避免这个问题,引入了虚拟节点的机制,即,为每一台服务器计算出多个hash地址,每个值对应一个节点位置,这些节点就是虚拟的节点,虚拟节点中不是具体的服务器节点,而是到服务器的映射值。这种机制下,即使物理的服务器节点很少,只要虚拟节点足够多,也能是的key的分布相对均匀。

应用

1、完整数据缓存

对于不会轻易发生变化的数据,提前读取到memcache中进行数据预热,这个时候只需要读取缓存,访问快速。而且由于这类数据不会经常发生变化,所以,当后台有操作改变数据的时候,只需要更新一下memcache中的缓存数据即可。

另一种做法:以商品的分类为例,分类信息由后台管理进行维护,一般情况下不会轻易发生变化。此时可以将商品分类的数据做成静态化文件,然后再使用前端的WEB缓存或者CDN加速,会有更好的效果。同样,也是在后台发生分类信息修改的时候,再将静态文件进行更新。

2、热点数据缓存

用缓存来存储经常被访问的节点,比如用来存储商品更新信息,将商品等热点数据添加到memcache中,查询的时候先从内存查找,然后直接取走数据。

3、几大电商双11秒杀场景:

这种高并发的环境下,必须先预热各种缓存:前端web缓存、后端数据库缓存

使用的步骤是一样的,先读,有的话就直接返回,没有就读去数据库,且同步到缓存中,并且当数据库的数据更新之后即时更新缓存信息

4、可以用作集群节点共同存储session信息

即将客户端请求的多个应用集群产生的会话信息(多个集群上的会话信息是一致的)存储到Memcache中,这个session的获取也非常的快。

配合Memcached和session来实现分布式session管理可以使用MSM.

你可能感兴趣的:(计算机基础,数据库,面试,数据库,MySQL,存储引擎,InnoDB)