范式

在关系型数据库中,关于数据表设计的基本原则或规则就称为范式。可以理解为,一张数据表的设计结构需要满足的某种设计标准的级别。要想设计一个结构合理的关系型数据库,必须满足一定的范式。

按照“数据库规范化”对表进行设计,其目的就是减少数据库中的数据冗余,以增加数据的一致性。

常见的范式有1NF、2NF、3NF、BCNF。下面对这几种常见的范式进行简要分析。

1NF(第一范式)

第一范式,就是数据表的列不可再分,数据库表中的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。

看下面数据表,对于选课列明显是可以再分的,所以它是违反第一范式的。

学号 姓名 选课
10001 张三 数学,语文,英语
10002 李四 语文,英语
10003 王五 语文,英语,历史

2NF(第二范式)

第二范式必须先满足第一范式。另外包含两部分的内容:一是表必须有一个主键;二是表中非主键列必须完全依赖于主键,而不能只依赖于主键的一部分。

学号 课程 成绩 课程学分
10001 数学 100 6
10001 语文 90 2
10001 英语 85 3
10002 数学 90 6
10003 数学 99 6
10004 语文 89 2

​ 表中主键为(学号,课程),可以表示为(学号,课程)->(成绩,课程学分),表示所有非主键列(成绩,课程学分)都依赖于主键(学号,课程)。但是,表中还存在另外一个依赖(课程)->(课程学分)。这样非主键列'课程学分'依赖于部分主键列'课程',所以上表是不满足第二范式的。

我们把它拆成如下2张表:

学生选课表:

学号 课程 成绩
10001 数学 100
10001 语文 90
10001 英语 85
10002 数学 90
10003 数学 99
10004 语文 89

课程信息表:

课程 课程学分
数学 6
语文 3
英语 2

那么上面两个表,学生选课表主键为(学号,课程)。课程信息表主键为(课程),表中所有非主键列都完全依赖主键。不仅符合第二范式,还符合第三范式。

3NF(第三范式)

定义:首先是满足2NF,另外非主键列必须直接依赖于主键,表中的列不存在对非主键列的传递依赖。即不能存在:非主键列A依赖于非主键列B,非主键列B依赖于主键的情况。

再看这样一个学生信息表:

学号 姓名 性别 班级 班主任
10001 张三 一班 小王
10002 李四 一班 小王
10003 王五 二班 小李
10004 张小三 二班 小李

上表中,主键为(学号),所有字段(姓名,性别,班级,班主任)都依赖于主键(学号),不存在对主键的部分依赖。所以是满足第二范式。但是,表中存在一个传递依赖,(学号)->(班级)->(班主任)。也就是说,(班主任)这个非主键列依赖于另外一个非主键列(班级),而(班级)又直接依赖于主键,导致(班主任)间接依赖于主键,产生了传递依赖,所以不符合第三范式。

把这个表拆分成如下两个表:

学生信息表:

学号 姓名 性别 班级
10001 张三 一班
10002 李四 一班
10003 王五 二班
10004 张小三 二班

班级信息表:

班级 班主任
一班 小王
二班 小李

这样,对主键的传递依赖就消失了。上面的两个表都符合第三范式。

2NF和3NF的概念很容易混淆,区分它们的关键点在于,2NF:非主键列是否完全依赖于主键,还是依赖于主键的一部分。3NF:非主键列是直接依赖于主键,还是直接依赖于非主键列,产生依赖传递。

BCNF(BC范式)

定义:在第三范式的基础上,消除主属性对于码部分的传递依赖。

假设仓库管理关系表(仓库号,存储物品号,管理员号,数量),满足一个管理员只在一个仓库工作;一个仓库可以存储多种物品,则存在如下关系:

(仓库号,存储物品号)->(管理员号,数量)

(管理员号,存储物品号)->(仓库号,数量)

所以,(仓库号,存储物品号)和(管理员号,存储物品号)都是仓库管理关系表的候选码,表中唯一非关键字段为数量,它是符合第三范式的。但是,由于存在如下决定关系:

(仓库号)->(管理员号)

(管理员号)->(仓库号)

存在关键字段决定关键字段的情况,因此其不符合BCNF。把仓库管理关系表分解为两个关系表:仓库管理表(仓库号,管理员号)和仓库表(仓库号,存储物品号,数量),这样这个数据库表是符合BCNF的,并消除了删除异常、插入异常和更新异常。

参考:

事务的基础知识

数据库的事务是并发控制的基本单位,是逻辑上的一组操作,要么全部执行,要么全部不执行。(在MySQL中,只有InnoDB是支持事务的)

事务处理的原则:

​ 保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交commit,那么这些修改就被永久的保存;要么数据库管理系统将放弃所做的所有修改,整个事务回滚rollback到最初状态。

事务的ACID特性

  1. **原子性:**事务是一个不可分割的工作单元,事务里的操作要么都成功,要么都失败,如果事务执行失败,则需要进行回滚操作。
  2. **一致性:**执行事务前后,数据保持一致。事务不能破坏数据的完整性和业务的一致性。例如在转账时,不管事务成功还是失败,双方钱的总额不变。
  3. **隔离性:**指一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相干扰。
  4. **持久性:**一旦事务被提交后,它对数据库中数据的改变就是永久性的,之后的其它操作和数据库故障不应该对其有任何影响。

ACID是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是目的。

并发事务带来的问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题:

  • **脏写:**指一个事务修改了另一个未提交事务修改过的数据,这样第一个事务修改的结果就丢失了,那就意味着发生了脏写。

    在这里插入图片描述

  • **脏读:**指当事务B正在访问数据,并且对数据进行了修改(但是此事务未提交),此时事务A查询了相同的数据,后来事务B撤销回滚,则修改的数据恢复原值,A读到的数据就与数据库中的数据不一致,即A读到的数据是脏数据。

    在这里插入图片描述

  • **不可重复读:**在一个事务内,多次读取同一个数据,但是由于另一个事务在此期间对这个数据做了修改并提交,导致前后读取到的数据不一致。

    在这里插入图片描述

  • **幻读:**在一个事务中,先后两次进行读取相同的数据(一般是范围查询),但由于另一个事务新增或者删除了数据,导致前后两次读取的结果不一致。

    在这里插入图片描述

1.不可重复读和幻读的区别:
不可重复读侧重于读取到其它事务修改的数据,幻读侧重于读取到其它事务新增或者删除的数据。
2.可以采用锁机制来解决不可重复读和幻读:
对于不可重复读,只需对操作的数据添加行级锁,防止操作的数据发生变化;而对于幻读,需要添加表级锁,将整张表锁定,防止新增或者删除数据。也可添加临键锁来解决幻读问题。

事务隔离级别

​ 为保证在并发环境下读取数据的完整性和一致性,数据库提供了四种事务隔离级别,隔离级别越高,越能保证数据的完整性和一致性,但对高并发性能运行也越差,执行效率越低。

  • READ UNCOMMITTED(读未提交):允许事务在执行过程中,读取其它事务尚未提交的数据。不能避免脏读、不可重复读、幻读。
  • READ COMMITTED(读已提交):允许事务在执行过程中读取其它事务已经提交的数据。可以避免脏读,但不可重复读、幻读问题仍然存在。
  • REPEATABLE READ(可重复读):在同一个事务内,任意时刻的查询结果都是一致的。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。
  • SERIALIZABLE:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其它事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。

在这里插入图片描述

参考:

MySQL事务日志

事务的四种特性到底是基于什么机制实现的呢?

  • 事务的隔离性由锁机制实现
  • 事务的原子性、一致性和持久性由事务的redo log和undo log日志来保证
    • REDO LOG称为重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
    • UNDO LOG称为回滚日志,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。

REDO LOG

为什么需要REDO LOG

如果没有redo log,则需要在事务提交完成之前把该事物所修改的所有页面都刷新到磁盘中,否则一旦发生宕机会导致数据丢失。但是这个做法会导致一些问题:

  • 修改量与刷新磁盘工作总量严重不成比例

    有时候仅仅修改了某个页面中的一个记录,但是InnoDB中是以页为单位来进行磁盘IO的,也就是说在该事物提交时不得不将一个完整的页面从内存中刷新到磁盘,一个页面默认是16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是太小题大做了。
    
  • 随机IO刷新较慢

    一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,假如该事务修改的这些页面可能并不相邻,这就意味着将某个事务修改的Buffer Pool中的页面刷新到磁盘时,需要进行很多的随机IO,随机IO比顺序IO要慢,尤其对于传统的机械硬盘来说。
    

InnoDB引擎的事务采用了WAL技术(Write-Ahead Logging),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是redo log。当发生宕机且数据未刷到磁盘的时候,可以通过redo log来恢复,保证ACID中的D,这就是redo log的作用。

在这里插入图片描述

redo log降低了刷盘频率且占用空间非常小,存储表空间ID、页号、偏移量以及需要更新的值,所需的存储空间是很小的,刷盘快。

redo log是顺序写入磁盘的:在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO,效率比随机IO快。

事务执行过程中,redo log不断记录:redo log是存储引擎层产生的,而bin log是数据库层产生的。假设一个事务,对表做10万行的记录插入,在这个过程中,会一直不断的往redo log顺序记录,而bin log不会记录,直到这个事务提交,才会一次写入到bin log中。

REDO LOG的组成

  • 重做日志的缓冲(redo log buffer),保存在内存中,是易失的。innodb_log_buffer_size:redo log buffer大小,默认是16M。
  • 重做日志文件(redo log file):保存在硬盘中,是持久的。

REDO LOG的整体流程

alt

  • 先将原始数据从磁盘中读到内存,修改数据的内存拷贝
  • 生成一条redo log重做日志并写入redo log buffer。记录的是数据被修改后的值
  • 当事务commit时,将redo log buffer中的内容刷盘到redo log file,对redo log file采用追加的方式
  • 定期将内存中修改的数据刷新到磁盘中。当发生宕机且数据未刷到磁盘的时候,可以使用redo log来恢复,保证ACID的持久性。

REDO LOG的刷盘策略

redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以一定的频率刷入到真正的redo log file中。

注意:redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到文件系统缓存(page cache)中去,真正的写入会交给系统决定。如果交给系统来同步,同样如果系统宕机,数据也会丢失。

InnoDB给出innodb_flush_log_at_trx_commit参数,该参数控制commit提交事务时,如何将redo log buffer中的日志刷新到redo log file中。它支持三种策略:

  • 设置为0:表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)

    在这里插入图片描述

  • 设置为1:表示每次事务提交时都将进行同步,刷盘操作(默认值)

    image

  • 设置为2:表示每次事务提交时都只把redo log buffer内容写入到page cache,不进行同步。由os决定什么时候同步到磁盘文件

    在这里插入图片描述

REDO LOG FILE相关参数

  • innodb_log_group_home_dir:指定redo log文件组所在的路径,默认值为./,表示在数据库的数据目录下。MySQL的默认数据目录(var/lib/mysql)下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。此redo日志文件位置还可以修改。
  • innodb_log_files_in_group:指明redo file的个数。默认2个,最大100个。
  • innodb_flush_log_at_trx_commit:控制redo log刷新到磁盘的策略,默认为1。
  • innodb_log_file_size:单个redo log文件设置大小,默认值为48M。

日志文件组

从上边的参数描述中可以看出,磁盘上的redo日志文件不止一个,而是以一个日志文件组的形式出现的。总共的redo日志文件大小其实就是:innodb_log_file_size * innodb_log_files_in_group。采用循环使用的方式向redo日志文件组里写数据。如下如所示:

alt

在日志文件组中有两个重要的属性:

  • write pos是当前记录的位置,一边写一边后移
  • checkpoint是当前要擦除的位置,也是往后推移

每次刷盘redo log记录到日志文件组,write pos位置就会后移更新。每次MySQL加载日志文件组恢复数据时,会清空加载过的redo log记录,并把checkpoint后移更新。write pos和checkpoint之间还空着的部分可以用来写入新的redo log记录。

alt

如果write pos追上checkpoint,表示日志文件组满了,这时候不能再写入新的redo log记录,MySQL得停下来,清空一些记录,把checkpoint推进一下。

alt

UNDO LOG

redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中更新数据的前置操作其实是要先写入undo log中。

MySQL把为了回滚而记录的内容称为撤销日志或者回滚日志。注意:由于查询并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。此外,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。

UNDO的类型

在innodb存储引擎中,undo log分为:

  • insert undo log:是指在insert操作中产生的undo log。因为insert操作的记录,只对事务本身可见,对其它事务不可见,故该undo log可以在事务提交时直接删除。不需要purge线程删除。
  • update undo log记录的是对删除和更新操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交事务时放入undo log链表,等待purge线程进行最后的删除。

UNDO的存储结构

回滚段:InnoDB对undo log的管理采用段的方式,也就是回滚段(rollback segment)。每个回滚段记录了1024个undo log segment,可在每个undo log segment段中进行undo页的申请。

  • innnodb_undo_directory:设置rollback segment文件所在的路径。默认值为“./”,表示在当前数据目录下。
  • innodb_undo_logs:设置rollback segment的个数,默认值为128。
  • innodb_undo_tablespaces:设置存储rollback segment的文件数量。

回滚段与事务的关系:

  1. 每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
  2. 当一个事务开始的时候,会指定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
  3. 在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用。

回滚段中的数据分类:1.未提交事务的回滚数据 2.已经提交事务但未过期的回滚数据 3.已经提交事务并过期的数据

当事务使用undo日志和redo日志的示意图:

在这里插入图片描述

对于InnoDB引擎来说,每个行记录除了记录本身的数据之外,还有几个隐藏列:

  • DB_ROW_ID:如果没有为表显示的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键。

  • DB_TRX_ID:每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入trx_id中。

  • DB_ROLL_PTR:回滚指针,本质上就是指向undo log的指针

    在这里插入图片描述

UNDO的执行流程

  1. 当执行插入操作时,插入的数据会生成一条insert undo log,并且数据的回滚指针会指向它。undo log会记录日志的序号、插入的主键的列和值等。在进行rollback时,通过主键直接把对应的数据删除即可。

    在这里插入图片描述

  2. 当执行更新或删除操作时,会产生update undo log,并且会分更新主键的和不更新主键的,假设现在执行:

    update user set name='sun' where id = 1;

    在这里插入图片描述

    当更新主键时

    在这里插入图片描述

    对于更新主键的操作,会先把原来的数据deletemark标识打开,这时并没有真正的删除数据,真正的删除会交给清理线程去判断,然后在后面插入一条新的数据,新的数据也会产生undo log,并且undo log的序号会递增。

  3. undo log的回滚操作:

    1. 通过undo no=3的日志把id=2的数据删除
    2. 通过undo no=2的日志把id=1的deletemark还原成0
    3. 通过undo no=1的日志把id=1的数据的name还原成Tom
    4. 通过undo no=0的日志把id=1的数据删除

总结

在这里插入图片描述

  • undo log是逻辑日志,对事物回滚时,只是将数据库逻辑地恢复到原来的样子。
  • redo log是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程。

参考:

锁机制

​ 当数据库中多个事务并发存取同一数据的时候,若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。MySQL锁机制的基本工作原理就是:事务在修改数据库之前,需要先获得相应的锁,获得锁的事务才可以修改数据;在该事务操作期间,这部分的数据是锁定的,其它事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

按照不同的分类方式,锁的种类可以分为以下几种:

  1. 按锁的粒度划分:表级锁、行级锁、页级锁;
  2. 按锁的类型划分:共享锁(S锁)、排它锁(X锁);
  3. 按锁的使用策略划分:乐观锁、悲观锁;

MySQL中的表级锁、行级锁、页级锁:

  1. 表级锁:最大粒度的锁级别,发生锁冲突的概率最高,并发度最低,但开销小,加锁快,不会出现死锁;
  2. 行级锁:最小粒度的锁级别,发生锁冲突的概率最小,并发度最高,但开销大,加锁慢,会发生死锁,且行锁是作用在索引的;
  3. 页级锁:锁粒度介于表级锁和行级锁之间,并发度一般。开销和加锁时间也介于表锁和行锁之间,会出现死锁。
不同的存储引擎支持不同的锁机制:
	1.InnoDB存储引擎支持行级锁和表级锁,默认情况下使用行级锁,但只有通过索引进行查询数据,才使用行级锁,否则使用表级锁。
	2.MyISAM和Memory存储引擎采用的是表级锁。

读锁、写锁

  • 共享锁(S锁、读锁):多个事务可以对同一数据共享一把S锁,但只能进行读取不能修改;
  • 排它锁(X锁、写锁):一个事务获取排它锁之后,可以对锁定范围内的数据执行写操作,在锁定期间,其它事务不能再获取这部分数据的锁(共享锁、排它锁),只允许拥有排它锁的事务进行更新数据。

对于update,delete,insert操作,InnoDB会自动给涉及的数据加排它锁;对于普通SELECT语句,InnoDB不会加任何锁。

需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。

在这里插入图片描述

InnoDB的加锁方法:

对于普通的select语句,InnoDB不会加任何锁,但是事务可以通过以下语句显式的给记录集添加共享锁或排它锁:

  1. select ...... for update:排它锁

    目的是在执行这个select查询语句的时候,会将对应的索引访问条目进行上排它锁,并且查到后的数据只允许自己来修改。

    使用场景:为了确保自己查找到的数据一定是最新数据,并且查找到的数据值只允许自己修改,此时就需要用到此语句。

  2. select ...... lock in share mode:共享锁

    给查找的数据加一个共享锁的功能,允许其它的事务页对该数据上共享锁,但是不能够允许对该数据进行修改。

    使用场景:为了确保自己查询的数据不会被其它事务修改,也就是确保自己查询到的数据是最新的数据,并且不允许其它事务来修改数据。

表锁

​ 该锁会锁定整张表,它是MySQL中最基本的锁策略,并不依赖于存储引擎,并且表锁是开销最小的锁。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。当然,锁的粒度大所带来的最大负面影响就是会出现更频繁的锁资源争用,导致并发率大打折扣。

意向锁

​ 因为InnoDB引擎允许行锁和表锁共存,实现多粒度的锁机制,表锁和行锁虽然锁定范围不同,但是会相互冲突。当你要加表锁时,势必要先遍历该表的所有记录,判断是否有排它锁。这种遍历检查的方式显然是一种低效的方式,MySQL引入了意向锁,来检测表锁和行锁的冲突。

​ 意向锁也是表级锁,分为读意向锁(IS锁)和写意向锁(IX锁)。当事务要在记录上加行锁时,则先在表上添加对应的意向锁。之后事务如果想进行锁表操作,只要先判断是否有意向锁存在,存在时则可快速返回该表不能启用表锁,提高了效率。

注意:意向锁是由存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排它锁之前,InnoDB会先获取该数据行所在数据表的对应意向锁

在这里插入图片描述

总结:

  • IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S锁发生冲突。
  • 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。

自增锁

​ AUTO-INC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁。在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。一个事务在持有AUTO-INC锁的过程中,其它事务的插入语句都要被阻塞。

元数据锁(MDL锁)

​ MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL的作用是保证读写的正确性。比如:如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结果做变更,增加了一列,那么查询线程就会出现错误。

因此,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。

读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显示使用,在访问一个表的时候会被自动加上。

行锁

​ 行锁(Row lock)也称为记录锁,就是锁住某一行记录。需要注意的是,MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。

记录锁

记录锁:当使用唯一索引,且记录存在的精准查询时,使用记录锁。且记录锁是有S锁和X锁之分的,称之为S型记录锁和X型记录锁。

  • 当一个事务获取了一条记录的S型记录锁后,其它事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
  • 当一个事务获取了一条记录的X型记录锁后,其它事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。

间隙锁

​ 当使用范围查询而不是精准查询进行检索数据,并请求共享或排它锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做间隙(GAP)。

临键锁

​ 在InnoDB中,为了解决幻读的现象,引入了临键锁(next-key)。根据索引,划分为一个个左开右闭的区间。其本质就是一个记录锁和一个间隙锁的合体,既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙。

其具体实现操作如下图所示:

  1. 范围查询,记录存在,使用临键锁:

    img

  2. 当记录不存在时(不论是等值查询,还是范围查询),临键锁将退化成间隙锁:

    img

  3. 当条件是精准匹配(即为等值查询时)且记录存在时,并且是唯一索引,临键锁退化成记录锁:

    img

  4. 当条件是精准匹配(即为等值查询时)且记录存在,但不是唯一索引时,使用临键锁,有精准值的数据会增加记录锁并且精准值前后的区间的数据会增加间隙锁:

    img

插入意向锁

​ 一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了gap锁(临键锁也包含gap锁),如果有gap锁的情况下,插入操作就需要等待,同时对该insert插入操作生成一个插入意向锁。

  • 插入意向锁是一种特殊的间隙锁——间隙锁可以锁定开区间内的部分记录。
  • 插入意向锁之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键、唯一索引)不冲突,那么事务之间就不会出现冲突等待。

事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。

**锁升级:**每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB中行锁升级为表锁,这样做的好处就是占用的锁空间降低了,但同时数据的并发度也下降了。

乐观锁、悲观锁

需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想。

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到别人释放锁。(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)

注意:select ...... for update语句执行过程中所有扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住。

乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现的。在程序上,可以采用版本号机制CAS机制实现。乐观锁适用于多读的应用类型,能提高吞吐量。

两种锁的使用场景:

  1. 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题。
  2. 悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其它事务对该数据的操作权限。

隐式锁、显式锁

**隐式锁:**一个事务对新插入的记录可以不显示的加锁(生成一个锁结构),但是由于事务id的存在,相当于加了一个隐式锁。其它事务在对这条记录加S锁或者X锁时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。隐式锁是一种延迟加锁的机制,从而来减少加锁的数量。

**显式锁:**通过特定的语句进行加锁,一般称之为显式锁。

全局锁

​ 全局锁就是对整个数据库实例加锁。当需要让整个库处于只读状态的时候,可以使用这个命令,之后其它线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用场景是:全库逻辑备份。

全局锁的命令:Flush tables with read lock

死锁

​ 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。 alt

如何处理死锁:

  1. 等待,直到超时:

    当两个或多个事务产生死锁时,互相等待其它事务释放资源。当一个事务等待时间超过设置的阈值时,就将其回滚,另外的事务继续进行。在InnoDB中,参数Innodb_lock_wait_timeout用来设置超时时间。

  2. 使用死锁检测进行死锁处理:

    这是一种较为主动的死锁检测机制,要求数据库保存锁的信息链表和事务等待链表两部分信息。一旦检测到存在死锁,这时候InnoDB存储引擎会选择回滚UNDO量最小的事务,让其它事务继续执行。

参考:

多版本并发控制

MVCC(Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC就是通过数据行的多个版本管理来实现数据库的并发控制。

快照读与当前读

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读,而非当前读。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。

**快照读:**又叫一致性读,读取的是快照数据。不加锁的简单的SELECT都属于快照读,即不加锁的非阻塞读。既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

**当前读:**读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其它并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的SELECT,或者对数据进行增删改都会进行当前读。

MVCC可以不采用锁机制,而是通过乐观锁的方式来解决不可重复读和幻读问题!它可以在大多数情况下替代行级锁,降低系统开销。

回顾一下undo日志的版本链,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

alt

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录没有更早的版本),可以将这些undo日志都连起来,连成一个链表: alt

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头结点就是当前记录最新的值。每个版本中还包含生成该版本对应的事务id。

MVCC实现原理之ReadView

​ ReadView就是事务A在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID(活跃事务指的是启动了但还没提交的事务)。

ReadView中主要包含4个比较重要的内容,分别如下:

  1. creator_trx_id:创建这个ReadView的事务ID。

    说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
    
  2. trx_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。

  3. up_limit_id:活跃的事务中最小的事务ID。

  4. low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的id值。low_limit_id是系统最大的事务id值,这里要注意的是系统中的事务id,需要区别于正在活跃的事务ID。

    注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,tex_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
    

    ReadView的规则:

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的up_limit_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的low_limit_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的up_limit_id和low_limit_id之间,那就需要判断一下trx_id属性值是不是在trx_ids列表中。
    • 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
    • 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

MVCC整体流程操作:

  1. 首先获得事务自己的版本号,也就是事务ID;
  2. 获取ReadView;
  3. 查询得到的数据,然后与ReadView中的事务版本号进行比较;
  4. 如果不符合ReadView规则,就需要从Undo log中获取历史快照;
  5. 最后返回符合规则的数据;

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本页不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

InnoDB中,MVCC是通过Undo log + ReadView进行数据读取,Undo log保存了历史快照,而ReadView规则帮助我们判断当前版本中的数据是否可见。

在隔离级别为读已提交时,一个事务中的每一次SELECT查询都会重新获取一次ReadView。注意,此时同样的查询语句都会重新获取一次ReadView,这时如果ReadView不同,就可能产生不可重复读或者幻读的情况。

在这里插入图片描述

当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次SELECT的时候会获取一次ReadView,而后面所有的SELECT都会复用这个ReadView,如下表所示:

在这里插入图片描述

在并发访问数据库时,对正在事务中的数据做MVCC多版本的管理,以避免写操作阻塞读操作,并且可以通过比较版本解决快照读方式的幻读问题,但对于当前读的幻读,MVCC并不能解决,需要通过临键锁来解决。

总结:

这里介绍了MVCC在READ COMMITTED、REPERTABLE READ这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的读-写、写-读操作并发执行,从而提升系统性能。

核心点在于ReadView的原理,READ COMMITTED、REPERTABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同:

  • READ COMMITTED在每一次进行普通SELECT操作前都会生成一个ReadView。
  • REPERTABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView。
说明:之前说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的。

通过MVCC可以解决:

  1. 读写之间阻塞的问题。通过MVCC可以让读写互相不阻塞,这样就可以提升事务并发处理能力。
  2. 降低了死锁的概率。这是因为MVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
  3. 解决快照读的问题。当我们查询数据库在某个时间点的快照读时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。

参考:

六大日志文件

日志类型:

  • 慢查询日志:记录所有执行时间超过long_query_time的所有查询,方便对查询进行优化。
  • 通用查询日志:记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。
  • 错误日志:记录MySQL服务的启动、运行或停止MySQL服务时出现的问题,方便我们了解服务器的状态,从而对服务器进行维护。
  • 二进制日志:记录所有更改数据的语句,可以用于主从服务器之间的数据同步,以及服务器遇到故障时数据的无损失恢复。
  • 中继日志:用于主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。
  • 数据定义语句日志:记录数据定义语句执行的元数据操作。

除二进制日志之外,其它日志都是文本文件。默认情况下,所有日志创建于MySQL数据目录中。

日志的弊端:

  • 日志功能会降低MySQL数据库的性能。例如,在查询非常频繁的MySQL数据库系统中,如果开启了通用查询日志和慢查询日志,MySQL数据库会花费很多时间记录日志。
  • 日志会占用大量的磁盘空间。对于用户量非常大、操作非常频繁的数据库,日志文件需要的存储空间甚至比数据库文件需要的存储空间还要大。

二进制日志

​ binlog即binary log,二进制日志文件,也叫做变更日志。它记录了数据库所有执行的DDL和DML等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等)它以事件形式记录并保存在二进制文件中。通过这些信息,可以再现数据更新操作的全过程。

binlog主要应用场景:

  • 一是用于数据恢复,如果MySQL数据库意外停止,可以通过二进制日志文件来查看用户执行了哪些操作,对数据库服务器文件做了哪些修改,然后根据二进制日志文件中的记录来恢复数据库服务器。
  • 二是用于数据复制,由于日志的延续性和时效性,master把它的二进制日志传递给slaves来达到master-slave数据一致性的目的。

binlog日志的存储结构:

  1. 当MySQL创建二进制日志文件时,先创建一个以“filename”为名称、以“.index”为后缀的文件,再创建一个以“filename”为名称、以“.000001”为后缀的文件。
  2. MySQL服务重新启动一次,以“.000001”为后缀的文件就会增加一个,并且后缀名逐次增1。即日志文件的个数与MySQL服务启动的次数相同;如果日志长度超过了max_binlog_size的上限(默认是1GB),就会创建一个新的日志文件。

binlog日志的格式:

  • statement:每一条修改数据的SQL语句都会记录在binlog中。

    优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。

  • row:5.1.5版本的MySQL才开始支持,不记录每条SQL语句的上下文信息,仅记录哪些数据被修改了。

    优点:row level的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程或function,以及trigger的调用和触发无法被正确复制的问题。

  • mixed:在Mixed模式下,一般的语句修改使用statment格式保存binlog,一些函数和存储过程采用row格式保存binlog。

binlog的写入机制

​ binlog的写入实际和redo log相似,事务执行过程中先把binlog日志写到binlog cache中;事务提交的时候先把binlog cache中的数据写入page cache中,再把page cache中的数据刷盘到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一块内存作为binlog cache。 alt

binlog的刷盘策略:由参数sync_binlog控制(默认为0)

  • 当参数sync_binlog为0时,表示每次提交事务时都先写入到page cache,由系统自行判断什么时候执行fsync,如果机器宕机,page cache中的binlog日志会丢失。 alt

  • 当参数sync_binlog为1时,表示每次提交事务时都会写入page cache并执行刷盘操作。

alt

  • 当参数sync_binlog为2时,表示每次提交事务时都会执行写入page cache操作,但是只有往page cache写入了N个事务之后才会执行刷盘操作(N值可自定义)如果机器宕机,page cache中最近N个事务的binlog日志会丢失。

alt

binlog和redo log的区别

  • redo log是物理日志,记录的内容是“在某个表空间上的某个数据页上的某个字节做了什么修改”,属于InnoDB存储引擎层产生的。
  • binlog是逻辑日志,记录的内容是“语句的原始逻辑”,类似于“给ID=2这一行的c字段加1“,属于MySQL Server层。
  • 当数据库宕机后,如果已提交的事务的信息还没有刷新到磁盘上,再重启后重新执行redo log文件把数据刷到磁盘上,保证了事务的持久性。
  • binlog主要保证了MySQL集群架构数据的一致性和数据误删之后的数据恢复。

binlog和redo log的写入时机

  1. 在执行更新操作时,会记录redo log与binlog两块日志,都是以基本的事务为单位的:

    • redo log:在事务执行过程中可以不断写入redo log file(每1秒写入一次)
    • binlog:只有在提交事务时才会写入binlog file文件组(每次事务提交时写入)
  2. 写入时机不一致会出现的问题:

    • 如果在执行过程中已经把redo log写入到redo log file文件,但是在提交事务时binlog未写入到binlog file就出现了宕机,这时就会出现问题。

alt

  • 由于出现了宕机,因此binlog没有对应的修改记录,再重启之后主服务器会使用redo log进行数据的恢复,从服务器会使用binlog进行数据恢复,因此会出现主从服务器的数据不一致的问题。

alt

  • 为了解决上述数据不一致的问题,InnoDB存储引擎使用两阶段提交方案(将redo log写入redo log file文件分为两阶段:prepare和commit)

alt

  • 两阶段提交的原理:在写入binlog时若mysql程序出现了异常,重启之后mysql主服务器根据redo log进行数据恢复时,发现redo log处于prepare阶段,然后就会判断是否有binlog日志,如果没有就会进行回滚事务,如果有就会进行提交事务。

alt

**总结:**其实MySQL在提交事务时,不仅会将redo log buffer中的数据写入到redo log文件中,也会将本次修改的数据记录到binlog文件中,同时会将本次修改的binlog文件名和修改的内容在binlog中的位置记录到redo log中,最后还会在redo log最后写入commit标记,这样就表示本次事务被成功的提交了。(对应两阶段提交的最后一步redo log设置commit)

如果数据在写入binlog文件时,刚写完数据库就宕机了,数据会丢失吗?
首先可以确定的是,只要redo log最后没有commit标记,说明本次的事务一定是失败的。但是数据是没有丢失的,因为已经被记录到redo log的磁盘文件了,在MySQL重启的时候,就会将redo log中的数据恢复(加载)到buffer Pool中。

img

MySQL会有一个后台线程,它会在某个时机将Buffer Pool中的数据刷到MySQL数据库中,这样就将内存和数据库数据保持一致了。刷新脏页的时机如下:

  1. redo log写满时,停止所有更新操作,将checkpoint向前推进,推进那部分日志的脏页更新到磁盘;
  2. 需要将一部分数据页淘汰,如果是干净页,直接淘汰就行了,脏页的话,需要全部同步到磁盘;
  3. MySQL自认为空闲时或MySQL正常关闭之前。

img

1.首先MySQL执行器根据执行计划调用存储引擎的API查询数据
2.存储引擎先从缓存池buffer pool中查询数据,如果没有就会从磁盘查询,如果查询到了就将其所在页放到缓存池中
3.在数据加载到Buffer Pool的同时,会将这条数据的原始记录保存到undo log日志文件中
4.InnoDB会在Buffer Pool中执行更新操作
5.更新后的数据会记录在redo log buffer中
6.提交事务的同时会做以下三件事(其实会按照刷盘策略进行)
7.(第一件事)将redo log buffer中的数据刷入到redo log文件中
8.(第二件事)将本次操作记录写入到binlog文件中
9.(第三件事)将binlog文件名字和更新内容在binlog中的位置记录到redo log中,同时在redo log最后添加commit标记
10.使用一个后台线程,它会在某个时机将Buffer Pool中的更新后的数据刷到MySQL数据库中,这样就将内存和数据库的数据保持一致了

参考:

中继日志

​ 中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个服务器本地的日志文件就叫中继日志。

从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。搭建好主从服务器之后,中继日志默认会保存在从服务器的数据目录下。

文件名的格式是:从服务器-relay-bin.序号。中继日志还有一个索引文件:从服务器名-relaybin.index,用来定位当前正在使用的中继日志。

使用中继日志应注意的问题

​ 如果从服务器宕机,有的时候为了系统恢复要重装操作系统,这样就可能会导致你的服务器名称与之前不同。而中继日志里是包含从服务器名的。在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里获取数据,以为是日志文件损坏了,其实只是名称不一致,把中继日志中的从服务器改为现在的服务器名,就可以恢复数据了。

主从复制

​ 一般应用对数据库而言都是读多写少,也就是对数据库读取数据的压力比较大,有一个思路就是采用数据库集群的方案,做主从架构、进行读写分离,这样也可以提升数据库的并发处理能力。

主从复制的作用:

  • 读写分离:主服务器master负责写入数据,从服务器slave负责读取数据,当主服务器进行更新的时候,会自动将数据复制到从服务器中,当客户端进行读取数据的时候会向从服务器进行读取。
    1. 从服务器提供负载均衡,让不同的请求按照策略均匀分发到不同的从服务器上,提升了读取的效率。
    2. 读写分离可以减少锁表的影响。
  • 数据备份:通过主从复制机制将主库中的数据复制到从库上,相当于是一种热备份机制,也就是在主库正常运行的情况下进行的备份,不会影响到服务。
  • 高可用性:数据备份实际上是一种冗余的机制,通过这种冗余的方式可以换取数据库的高可用性,也就是当服务器出现故障或宕机的情况下,可以切换到从服务器上,保证服务的正常运行。

**主从复制的原理:**slave从服务器会从master主服务器上读取binlog日志进行数据的同步。

原理剖析:

实际上主从复制的原理就是基于binlog进行数据同步,通过以下三个线程来操作:

alt

  • 二进制日志转储线程(Binlog dump thread)是一个主库线程。当从库线程连接的时候,主库可以将二进制日志发送给从库,当主库读取事件的时候,会在Binlog上加锁,读取完成之后,再将锁释放掉。
  • 从库I/O线程会连接主库,向主库发送请求更新Binlog。这时从库的I/O线程就可以读取到主库的二进制日志转储线程发送的Binlog更新部分,并且拷贝到本地的中继日志中。
  • 从库SQL线程会读取从库中的中继日志,并且执行日志中的事件,将从库中的数据与主库保持同步。

复制三步骤:

  1. Master将写操作记录到二进制日志中,这些记录叫做二进制日志事件;
  2. Slave将Master的二进制日志事件拷贝到它的中继日志中;
  3. Slave重做中继日志的事件,将改变应用到自己的数据库中。MySQL复制是异步且串行化的,而且重启后从接入点开始复制。

复制的最大问题:延时。

同步数据一致性问题

​ 进行主从同步的内容是二进制日志,它是一个文件,在进行网络传输的过程中就一定会存在主从延迟,这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的数据不一致问题。

解决一致性问题

  • 异步复制:异步模式就是客户端提交COMMIT之后不需要等从库返回任何结果,而是直接将结果返回给客户端,这样做的好处是不会影响主库写的效率,但可能会存在主库宕机,而Binlog还没有同步到从库的情况,也就是此时的主库和从库数据不一致。这时候从从库中选择一个作为新主,那么新主则可能缺少原来主服务器中已经提交的事务。所以,这种复制模式下的数据一致性是最弱的。

    在这里插入图片描述

  • 半同步复制:MySQL5.5版本之后开始支持半同步复制的方式。原理是客户端提交COMMIT之后不直接将结果返回给客户端,而是等待至少有一个从库接收到了Binlog,并且写入到中继日志中,再返回给客户端。

    这样做的好处就是提高了数据的一致性,当然相比于异步复制来说,至多增加了一个网络连接的延迟,降低了主库写的效率。

    在MySQL5.7版本中还增加了一个rpl_semi_sync_master_wait_for_slave_count参数,可以对应答的从库数量进行设置,默认为1。

    在这里插入图片描述

  • 组复制:组复制技术,简称MGR。是MySQL5.7.17版本中推出的一种新的数据复制技术,这种复制技术是基于Paxos协议的状态机复制。

参考:

MySQL数据库常见面试题总结

MySQL数据库常见面试题总结_张维鹏的博客-CSDN博客

游标Cursor

详情请阅读以下博客:

游标Cursor_张维鹏的博客-CSDN博客

触发器Trigger

详情请阅读以下博客:

触发器Trigger_张维鹏的博客-CSDN博客

存储过程Procedure

详情请阅读以下博客:

存储过程Procedure_张维鹏的博客-CSDN博客

视图View

详情请阅读以下博客:

视图View_张维鹏的博客-CSDN博客