0.基础概念
a.MyISAM VS InnoDB
简单来说:
- MyISAM是5.5版本之前的默认引擎,不支持事务;不支持行级锁;崩溃后无法安全恢复;适合读密集的场景;
- InnoDB是5.5版本之后的默认引擎,支持事务;支持行级锁;崩溃后可以安全恢复(crash-safe)。
详细介绍
- 是否支持行级锁?
MyISAM只支持表级锁,InnoDB支持表级锁和行级锁,默认为行级锁; - 是否支持事务和奔溃后的安全恢复?
MyISAM强调的是性能,每次查询都是原子操作,其执行速度比Innodb更快,但是不支持事务。
InnoDB支持事务和奔溃后的修复能力; - 是否支持外键?
MyISAM不支持外键;InnoDB支持外键; - 是否支持MVCC?
仅InnoDB支持。 - MyISAM一定比InnoDB快吗?
不一定,某些使用了聚簇索引或者访问的数据都在内存中的时候,InnoDB比MyISAM要快。
1.索引
1.1 基础知识
1.1.1索引的分类
索引也可以分为普通索引和唯一性索引
- 普通索引:最基本的索引,它没有任何限制
- 唯一性索引:与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
聚集索引和非聚集索引
a.聚集索引
定义:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。
数据行的物理顺序与列值的顺序相同,如果我们查询id比较靠后的数据,那么这行数据的地址在磁盘中的物理地址也会比较靠后。而且由于物理排列方式与聚集索引的顺序相同,所以也就只能建立一个聚集索引了。
索引的叶子节点就是对应的数据节点,可以直接获取到对应的全部列的数据,而非聚集索引在索引没有覆盖到对应的列的时候需要进行二次查询.
b.非聚集索引
定义:该索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。
叶子节点存的是字段的值,通过这个非聚集索引的键值找到对应的聚集索引字段的值,再通过聚集索引键值找到表的某行
1.1.2 索引的缺点
- 虽然索引大大提高了查询速度,但是却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要维护索引文件。
- 建立索引会占用磁盘空间。
1.2 B+树
索引结构有BTree索引和哈希索引,在绝大多数需求为单条记录查询的时候,选择哈希索引,查询性能最快,其他场景还是使用BTree索引。
1.2.1 B+树的诞生过程
1.需求
根据某个值查找数据,比如 select * from user where id=1234;
根据区间值来查找某些数据,比如 select * from user where id > 1234 and id < 2345。
2.数据结构选型
- 散列表。散列表的查询性能很好,时间复杂度是 O(1)。但是,散列表不能支持按照区间快速查找数据。
- 平衡二叉查找树。尽管平衡二叉查找树查询的性能也很高,时间复杂度是 O(logn)。而且,对树进行中序遍历,我们还可以得到一个从小到大有序的数据序列,但这仍然不足以支持按照区间快速查找数据。
- 跳表。跳表是在链表之上加上多层索引构成的。它支持快速地插入、查找、删除数据,对应的时间复杂度是 O(logn)。并且,跳表也支持按照区间快速地查找数据。我们只需要定位到区间起点值对应在链表中的结点,然后从这个结点开始,顺序遍历链表,直到区间终点对应的结点为止,这期间遍历得到的数据就是满足区间值的数据。
3.推演过程
3.1 二叉树的改造
为了让二叉查找树支持按照区间来查找数据,对它进行改造:
- 树中的节点并不存储数据本身,而是只作为索引。
-
我们把每个叶子节点串在一条链表上,链表中的数据是从小到大有序的。
改造之后,如果我们要求某个区间的数据。我们只需要拿区间的起始值,在树中进行查找,当查找到某个叶子节点之后,我们再顺着链表往后遍历,直到链表中的结点数据值大于区间的终止值为止。所有遍历到的数据,就是符合区间值的所有数据。
3.2 B+树的诞生
- 二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上。
- 我们要为几千万、上亿的数据构建索引,如果将索引存储在内存中,尽管内存访问的速度非常快,查询的效率非常高,但是,占用的内存会非常多。
- 为了节省内存,如果把树存储在硬盘中,那么每个节点的读取(或者访问),都对应一次磁盘 IO 操作。树的高度就等于每次查询数据时磁盘 IO 操作的次数。比起内存读写操作,磁盘 IO 操作非常耗时,所以我们优化的重点就是尽量减少磁盘 IO 操作,也就是尽量降低树的高度。
- 为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用“N 叉”树。这里,“N 叉”树中的“N”取决于数据块的大小。对于相同个数的数据构建 N 叉树索引,N 叉树中的 N 越大,那树的高度就越小,那 N 叉树中的 N 是不是越大越好呢?到底多大才最合适呢?
不管是内存中的数据,还是磁盘中的数据,操作系统都是按页(一页大小通常是 4KB)来读取的,一次会读一页的数据。如果要读取的数据量超过一页的大小,就会触发多次 IO 操作。所以,我们在选择 N 大小的时候,要尽量让每个节点的大小等于一个页的大小。读取一个节点,只需要一次磁盘 IO 操作。
1.2.2 索引的分析
数据的写入过程,会涉及索引的更新,这是索引导致写入变慢的主要原因。
对于一个 B+ 树来说,N 值是根据页的大小事先计算好的,也就是说,每个节点最多只能有 N 个子节点。在往数据库中写入数据的过程中,这样就有可能使索引中某些节点的子节点个数超过 N,这个节点的大小超过了一个页的大小,读取这样一个节点,就会导致多次磁盘 IO 操作。我们该如何解决这个问题呢?
我们只需要将这个节点分裂成两个节点。但是,节点分裂之后,其上层父节点的子节点个数就有可能超过 N 个。不过这也没关系,我们可以用同样的方法,将父节点也分裂成两个节点。这种级联反应会从下往上,一直影响到根节点。这个分裂过程,你可以结合着下面这个图一块看,会更容易理解(图中的 B+ 树是一个三叉树。我们限定叶子节点中,数据的个数超过 2 个就分裂节点;非叶子节点中,子节点的个数超过 3 个就分裂节点)。
实际上,不光写入数据会变慢,删除数据也会变慢。这是为什么呢?
我们在删除某个数据的时候,也要对应的更新索引节点。频繁的数据删除,就会导致某些节点中,子节点的个数变得非常少,长此以往,如果每个节点的子节点都比较少,势必会影响索引的效率。在 B+ 树中,这个阈值等于 N/2。如果某个节点的子节点个数小于 N/2,我们就将它跟相邻的兄弟节点合并。不过,合并之后节点的子节点个数有可能会超过 N。针对这种情况,我们可以借助插入数据时候的处理方法,再分裂节点。
1.2.3 B+树 VS B树
B+ 树的特点:
- 每个节点中子节点的个数不能超过 N,也不能小于 N/2;
- 根节点的子节点个数可以不超过 N/2,这是一个例外;
- N 叉树只存储索引,并不真正存储数据,这个有点儿类似跳表;
- 通过链表将叶子节点串联在一起,这样可以方便按区间查找;
- 一般情况,根节点会被存储在内存中,其他节点存储在磁盘中。
B 树跟 B+ 树的不同点主要集中在这几个地方:
- B+ 树中的节点不存储数据,只是索引,而 B 树中的节点存储数据;
- B 树中的叶子节点并不需要链表来串联。也就是说,B 树只是一个每个节点的子节点个数不能小于 N/2 的 N 叉树。
1.3 InnoDB索引模型
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。
InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。每一个索引在 InnoDB 里面对应一棵 B+ 树。每张表都对应了好几课B+树。
根据叶子节点的内容,索引类型分为 主键索引和 非主键索引。
- 主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引。
- 非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
基于主键索引和普通索引的查询有什么区别?
如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。也就是说,基于非主键索引的查询需要多扫描一棵索引树。
因此,我们在应用中应该尽量使用主键查询。
1.4 索引优化
1.4.1 覆盖索引
在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
案例:在一个市民信息表上,是否有必要将身份证号和名字建立联合索引?
答:如果有一个高频的查询需求,根据身份证号查询名字,那么建立联合索引,利用覆盖索引的优势,就不需要再回表了,效率更高
1.4.2 最左前缀
不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。
这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符
1.4.3 索引下推
对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
2.事务
2.1 ACID
- 原子性(A):事务执行的最小单位,不允许分割;要么全部执行,要么都不执行;
- 一致性(C):执行数据前后,数据保持一致;事务应确保数据库的状态从一个一致状态转变为另一个一致状态。
- 隔离性(I):并发访问数据库的时候,一个用户的事务不被其他事务所干扰,各并发事务之间的数据库是独立的。
- 持久性(D):一个事务提交以后,对数据库的影响是持久的,即使数据库发生故障也不会对其有影响。
2.2 并发带来的问题
- 脏读(Dirty read ):一个事务读到了另外一个事务还没有提交的事务;
- 不可重复读(Unrepeatableread):一个事务内多次读取某个数据,但是另外一个事务修改了数据,导致第一个事务前后两次读到的数据不一致。
不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。
- 幻读(Phantom read):T1读取了几行数据后,T2插入了几条数据,T1在后续的查询中就会发现多了原本不存在的数据,就好像发生了幻觉一样。
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检锁范围为只读,这样就避免了幻读。
总结:在并发访问情况下,可能会出现脏读、不可重复读和幻读等读现象,为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念。
2.2隔离性与隔离级别
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行,所有事务依次执行。
MySQL的隔离级别是可重复读,但是对于InnoDB引擎在可重复读的隔离级别下,使用的算法是Next-Key Lock锁算法,避免了幻读,且不会有任何的性能损失。
如何实现?
数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- 在“可重复读”隔离级别下:这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
- 在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
- “读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;
- “串行化”隔离级别下直接用加锁的方式来避免并行访问。
什么时候需要“可重复读”的场景呢?
假设你在管理一个个人银行账户表。
一个表存了账户余额,一个表存了账单明细。
到了月底你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。
你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
事务隔离级别的实现
在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录的最新值,通过回滚操作,都可以得到前一个状态的值。
在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。
3.SQL语句的执行
3.1 查询语句
大体来说,MySQL分为两层架构:server层和存储引擎层:
server:包括连接器,查询缓存,分析器,优化器和执行器,涵盖MySQL的大多数核心服务功能。所有跨存储引擎的功能都在这一层实现。
存储引擎层:负责数据的提取和存储。架构模式是可插拔式的,InnoDB为默认的存储引擎。
1.连接器
作用划分:建立连接、获取权限、维持和管理连接。
1.1权限
- 一个用户成功建立连接以后,即使用管理员账户对这个用户的权限进行修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
1.2 长连接
- 建立连接的过程是比较消耗资源的,所以在使用中尽量减少建立连接的动作,即尽量使用长连接。
- 使用长连接后内存涨的特别快的原因?
-- MySQL在执行过程中临时使用的内存是管理在连接对象里面的,这些资源会在连接断开的时候才释放。如果长连接累积下来,可能会导致内存占用过大,被系统强行杀掉(OOM),从现象上看就是MySQL异常重启。 - 如何解决长连接造成的大内存呢?
- 定期断开长连接。使用一段时间或者判断程序里面执行一个占用内存的大查询后,断开连接,之后要查询再重新连接。
- 在每次执行一个比较大的操作后,执行mysql_reset_connection 来初始化连接资源。这个过程不需要重连和重新做权限校验,但是会将连接恢复到刚刚创建完的状态。
2.查询缓存
弊端
- 查询缓存的失效非常频繁,只要有对表的更新,这个表上所有的查询缓存都会被清空。
- 对于更新频繁的数据库来说,查询缓存的命中率会非常的低;
适合场景
适合静态表,很长时间才会更新一次,比如一个系统的配置表;
3.分析器
词法分析
你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。
MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。
语法分析
根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。
如果表 T 中没有字段 k,而你执行了这个语句 select * from T where k=1, 那肯定是会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。
这个就是在分析器中的报错。
4.优化器
经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
- 优化器是在表里面有多个索引的时候,决定使用哪个索引;
- 或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
5.执行器
MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
- 开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误。
- 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
3.2 更新语句
1.整体流程
- 执行器先找引擎取 ID=2 这一行。(ID 是主键,引擎直接用树搜索找到这一行)。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
2. redo log vs binlog
2.1 redo log
问题:如果MySQL每一次更新操作都写进磁盘,同时磁盘也要找到对应的那条记录,然后更新。整个过程的IO成本、查找成本都很高;
如何解决:WAL(Write Ahead Logging):先写日志,再写磁盘
- 当有一条记录需要更新的时候,InnoDB首先会把记录写到redo log,然后更新到内存中,整个过程就算完成了。
- InnoDB会在适当的时候,将redo log中的数据更新写入磁盘。(适当的时候:1.系统比较空闲的时候;2.redo log空间已满的时候)
crash-safe的能力
- 有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe
2.2 bin log
redo log是InnoDB独有的日志。
server层也有自己的日志,binlog,即归档日志,没有crash-safe的能力。
2.3 二者对比
- redo log 是InnoDB独有的日志,具有crash-safe的能力;binlog 即归档日志,是所有搜索引擎都可以使用的,没有crash-safe的能力;
- redo log 是物理日志,是某个数据页上面做了什么修改,记录这个页 “做了什么改动”;bin log是逻辑日志,记录的是这个语句的原始逻辑,比如某表某行的某个字段加1
- redo log 大小的固定的,可能会存储满;bin log是可以追加写的,没有大小的限制,即有归档的概念;
3. 相关问题
3.1怎样让数据库恢复到半个月内任意一秒的状态?
前提:binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。
- 备份系统中一定会保存最近半个月的所有 binlog;
- 同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
如何操作:
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做: - 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻;
- 这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去;
3.2 为什么日志需要“两阶段提交”?
一句话总结:简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
- 先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复的话,由于这个语句的 binlog 丢失,就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
- 先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。
3.3各个时间点奔溃的问题引申:
1 prepare阶段 2 写binlog 3 commit
当在2之前崩溃时
重启恢复:后发现没有commit,回滚。备份恢复:没有binlog 。
一致
当在3之前崩溃
重启恢复:虽没有commit,但满足prepare和binlog完整,所以重启后会自动commit。备份:有binlog. 一致
3.4 bin log与crash safe的关系
CrashSafe指MySQL服务器宕机重启后,能够保证:
- 所有已经提交事务的数据仍然存在。
- 所有没有提交事务的数据自动回滚。
Innodb通过Redo Log和redo Log可以保证以上两点。
为了保证严格的CrashSafe,必须要在每个事务提交的时候,将Redo Log写入硬件存储。这样做会牺牲一些性能,但是可靠性最好
使用2PC协议。
事务的协调者Binlog
Binlog在2PC中充当了事务的协调者。由Binlog来通知InnoDB引擎来执行prepare,commit或者rollback的步骤。事务提交的整个过程如下:
- 协调者准备阶段(Prepare Phase)
告诉引擎做Prepare,InnoDB更改事务状态,并将Redo Log刷入磁盘。 - 协调者提交阶段(Commit Phase)
2.1 记录协调者日志,即Binlog日志。
2.2 告诉引擎做commit。
注意:记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。
恢复前事务的状态
在恢复开始前事务有以下几种状态:
- InnoDB中已经提交
根据前面2PC的过程,可知Binlog中也一定记录了该事务。所以这种事务是一致的不需要处理。 - InnoDB中是prepared状态,Binlog中有该事务的Events。
需要通知InnoDB提交这些事务。 - InnoDB中是prepared状态,Binlog中没有该事务的Events。
因为Binlog还没记录,需要通知InnoDB回滚这些事务。
CrashSafe的写盘次数
保证CrashSafe就要设置下面两个参数为1:
sync_binlog=1
innodb_flush_log_at_trx_commit=1
- sync_binlog
sync_binlog是控制Binlog写盘的,1表示每次都写。由于Binlog使用了组提交(Group Commit)的机制,它代表一组事务提交时必须要将Binlog文件写入硬件存储1次。 - innodb_flush_log_at_trx_commit的写盘次数
这个变量是用来控制InnoDB commit时写盘的方法的。现在commit被分成了两个阶段,到底在哪个阶段写盘,还是两个阶段都要写盘呢? - Prepare阶段时需要写盘
2PC要求在Prepare时就要将数据持久化,只有这样,恢复时才能提交已经记录了Xid_log_event的事务。 - Commit阶段时不需要写盘
如果Commit阶段不写盘,会造成什么结果呢?已经Cmmit了的事务,在恢复时的状态可能是Prepared。由于恢复时,Prepared的事务可以通过Xid_log_event来提交事务,所以在恢复后事务的状态就是正确的。因此在Commit阶段不需要写盘。
4.锁
当并发事务同时访问一个资源时,有可能导致数据不一致,因此需要一种机制来将数据访问顺序化,以保证数据库数据的一致性。锁就是其中的一种机制。在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥的要求。
MyISAM采用为表级锁;
InnoDB支持表级锁和行级锁,默认为行级锁;
4.1表级锁与行级锁
- 表级锁:粒度最大的锁,实现简单,资源消耗比较小,不会出现死锁;但是并发度最低,不适合高并发场景。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
- 行级锁:粒度最小的锁,只对当前操作的行进行加锁,适合并发场景,降低冲突的概率;加锁开销大,加锁慢,容易出现死锁。行级锁分为共享锁 和 排他锁。
4.1.1行级锁
1.行级锁
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。
InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
行级锁的算法有三种:
- Record Lock:对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
- Gap Lock:间隙锁,锁定一个范围,不包括记录本身;
- Next-key Lock:行锁+间隙锁,包含记录本身,解决了幻读;
2.死锁
当出现死锁以后,有两种策略:
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout (时间默认是50s)来设置。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
主动死锁检测:具有额外的负担,每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
有多种方法可以避免死锁,这里介绍常见的三种
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
3.行级锁的小结
- InnoDB查询使用的是Next-key Lock;
- Next-Key Lock为了解决幻读的问题;
- 当查询的索引有唯一属性时,Next-key 降级为Record Key
- GAP 锁的目的就是为了防止幻读,阻止多个事务将数据插入到同一范围内;
4.1.2 表级锁使用场景
- 事务更新大表中的大部分数据直接使用表级锁效率更高;
- 事务比较复杂,使用行级索很可能引起死锁导致回滚。
4.2 共享锁(s)和排他锁(X)-->行锁
行级锁可以进一步划分为共享锁(s)和排他锁(X)。
共享锁(Share Lock)
共享锁又称读锁,是读取操作创建的锁。其他事务可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
用法
SELECT ... LOCK IN SHARE MODE
;
在查询语句后面增加LOCK IN SHARE MODE
,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。
排他锁(eXclusive Lock)
排他锁又称写锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的锁。获准排他锁的事务既能读数据,又能修改数据。其它用户只能查询但不能更新被加锁的数据行。
用法
SELECT ... FOR UPDATE
;
在查询语句后面增加FOR UPDATE
,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。
对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。
共享锁:SELECT ... LOCK IN SHARE MODE
;
排他锁:SELECT ... FOR UPDATE
;
4.3 读锁和写锁--》表锁
读锁:共享锁
写锁:排它锁,排它锁,互斥锁
本文提到的读锁和写锁都是MySQL数据库的MyISAM引擎支持的表锁的。
MyISAM 存储引擎只支持表锁,MySQL 的表级锁有两种模式:表共享读锁(Table Read Lock
)和表独占写锁(Table Write Lock
)。
对于读操作,可以增加读锁,一旦数据表被加上读锁,其他请求可以对该表再次增加读锁,但是不能增加写锁。(当一个请求在读数据时,其他请求也可以读,但是不能写,因为一旦另外一个线程写了数据,就会导致当前线程读取到的数据不是最新的了。这就是不可重复读现象)
对于写操作,可以增加写锁,一旦数据表被加上写锁,其他请求无法在对该表增加读锁和写锁。(当一个请求在写数据时,其他请求不能执行任何操作,因为在当前事务提交之前,其他的请求无法看到本次修改的内容。否则就可能产生脏读、不可重复读和幻读)
读锁和写锁都是阻塞锁。
如果t1对数据表增加了写锁,这时t2请求对数据表增加写锁,这时候t2并不会直接返回,而是会一直处于阻塞状态,直到t1释放了对表的锁,这时t2便有可能加锁成功,获取到结果。
另外两个表级锁:IS和IX(InnoDB)
意向锁:当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以在需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。
InnoDB另外的两个表级锁:
意向共享锁(IS): 表示事务准备给数据行记入共享锁,事务在一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX): 表示事务准备给数据行加入排他锁,事务在一个数据行加排他锁前必须先取得该表的IX锁。
注意:
这里的意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正加行锁时才会判断是否冲突。意向锁是InnoDB自动加的,不需要用户干预。
IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突。
4.4 悲观锁和乐观锁
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
乐观锁与悲观锁只是人们定义出来的概念。针对于不同的业务场景,应该选用不同的并发控制方式。在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的。
4.4.1 悲观锁
这种借助数据库锁机制在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)。
加锁流程:
- 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
4.3.2乐观锁
在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。
实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。
版本号的实现:
数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
//修改商品库存
update item set quantity=quantity - 1 where id = 1 and quantity - 1 > 0
如何选择
在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。
1、乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
2、悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景。
4.5
MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都 select 出来存成文本。
官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction
的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
你一定在疑惑,有了这个功能,为什么还需要 FTWRL 呢?
single-transaction 方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。
5.其他
5.1大表优化
1. 限定数据的范围
务必禁止不带任何限制数据范围条件的查询语句;
2. 读写分离
主库写,从库读
一主多从,读写分离,主动同步,是一种常见的数据库架构,一般来说:
- 主库,提供数据库写服务
- 从库,提供数据库读服务
- 主从之间,通过某种机制同步数据,例如mysql的binlog
本质上解决的问题: - 大部分互联网业务读多写少,数据库的读往往最先成为性能瓶颈,从而可以线性提升数据库读性能;
- 通过消除读写锁冲突提升数据库写性能;
小结
一句话,主要解决“数据库读性能瓶颈”问题,在数据库扛不住读的时候,通常读写分离,通过增加从库线性提升系统读性能。
缺点
如果数据库读写分离:
- 数据库连接池需要区分:读连接池,写连接池
- 如果要保证读高可用,读连接池要实现故障自动转移
- 有潜在的主库从库一致性问题
优化建议
- 如果面临的是“读性能瓶颈”问题,增加缓存可能来得更直接,更容易一点
- 关于成本,从库的成本比缓存高不少
2垂直划分
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
优点:优化表的结构,易于维护;可以使列的数据变小,查询时候的block块减少,减少I/O数;
缺点:主键冗余;引起Join操作(应用层完成);垂直分区让事务更加复杂;
主要依据以下几点:
(1)将长度较短,访问频率较高的属性尽量放在一个表里,这个表暂且称为主表;
(2)将字段较长,访问频率较低的属性尽量放在一个表里,这个表暂且称为扩展表;
(3)经常一起访问的属性,也可以放在一个表里;
为何这么垂直拆分可以提升性能?
(1)数据库有自己的内存缓冲池,会将磁盘上的数据load到缓冲池里;
(2)数据库缓冲池,以row为单位缓存数据;
(3)在内存有限的情况下,在数据库缓冲池里缓存短row,就能缓存更多的数据;
(4)在数据库缓冲池里缓存高频访问row,就能提升缓存命中率,减少磁盘的访问;
3.水平划分
水平切分,也是一种常见的数据库架构,一般来说:
- 每个数据库之间没有数据重合,没有类似binlog同步的关联;
- 所有数据并集,组成全部数据;
- 会用算法,来完成数据分割,例如“取模”,或者根据业务标识;
水平切分架构究竟解决什么问题?
大部分互联网业务数据量很大,单库容量容易成为瓶颈,如果希望:
- 线性降低单库数据容量
- 线性提升数据库写性能
小结:
一句话总结,水平切分主要解决“数据库数据量大”问题,在数据库容量扛不住的时候,通常水平切分。
一个水平切分集群中的每一个数据库,通常称为一个“分片”。
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。
水平拆分能够支持非常大的数据量存储,拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。
数据库分片的两种常见方案:
客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
5.2 todoread
- 一条SQL语句执行得很慢的原因有哪些
- 数据库调优
- 缓冲池(buffer pool)
4.面试问题100问
5.https://mp.weixin.qq.com/s/2A14OKApo-DDKAVaRbnkHg