目录
2.3.3 自适应哈希索引(Adaptive Hash Index)
3.5.5 Index Condition Pushdown
1、Mysql 体系结构
1.1 数据库和数据库实例
- 数据库是文件的集合,是按照某种数据模型组织起来并存放于二级存储器中的数据集合;
- 数据库实例是程序,是位于用户于操作系统之间的一层数据管理软件;
- 用户的 select、insert 等操作不是直接操作文件来修改数据库内容,而是通过数据库实例来完成对数据的操作
1.2 存储引擎
MySQL使用插件式存储引擎,存储引擎提供了一系列标准的管理和服务支持。存储引擎是基于表的,而不是数据库的,即数据库中的每个表可以使用不同的存储引擎。
1.2.1 InnoDB 和 MyISAM 对比
InnoDB | MyISAM | ||
单表大小限制 | - 共享表空间:64TB - 独享表空间:由文件系统决定 | 由文件系统决定 | |
支持事务 | 支持 | 不支持 | |
锁粒度 | 行锁 | 表锁 | |
支持外键 | 支持 | 不支持,但在TODO列表中 | |
MVCC(多版本并发控制) | 支持 | 不支持 | |
索引 | B树索引 | 支持 | 支持 |
hash索引 | 不支持,但其内部通过hash算法实现自适应hash索引 | 不支持 | |
geo索引 | MySQL5.7及以后版本支持 | 支持 | |
全文索引 | InnoDB 1.2.x开始支持(MySQL5.6及以后版本) | 支持 | |
聚集索引 | 支持 | 不支持 | |
缓冲池 | 缓存索引+数据 | 只缓存索引 | |
压缩数据文件 | 否 | 是 | |
磁盘空间占用 | 高 | 低 | |
内存占用 | 高 | 低 | |
应用场景 | 在线事务处理(OLTP, On-Line Transaction Processing) 存在大量的 insert 和 update 操作 | 在线分析处理(OLAP, On-Line Analytical Processing) 存在大量的 select 操作 |
1.2.2 MVCC(多版本并发控制)
TODO
https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc
https://www.jianshu.com/p/8845ddca3b23
1.2.3 OLTP 和 OLAP
TODO
https://www.cnblogs.com/lingchen-liang/p/10690619.html
2、InnoDB 存储引擎
2.1 体系架构
InnoDB存储引擎主要包括两部分:后台线程和内存池。
2.1.1 后台线程
后台线程分为Master Thread、IO Thread、Purge Thread、Page Cleaner Thread。主要负责如下工作:
-
从磁盘刷新内存池中的数据,保证缓冲池中缓存的数据是最新的;
-
将缓冲池中已修改的数据文件刷新到磁盘;
-
保证数据库异常时InnoDB能恢复到正常运行状态;
(1)Master Thread
核心后台线程,负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括:脏页的刷新、合并插入缓冲(Insert Buffer)、undo页的回收。Master Thread内部有多个循环:主循环(loop)、后台循环(background loop)、刷新循环(flush loop)、暂停循环(suspend loop),Master Thread会根据数据库运行状态在这几个循环中切换。Master Thread每秒、每十秒根据条件会执行一些操作,伪代码如下:(建议看到2.2节后再回来看)
void master_thread(){
goto loop;
loop:
for(int i = 0; i < 10; i++){ //每秒执行的操作
thread_sleep(1);
do_log_buffer_flush_to_disk(); //日志缓冲刷新到磁盘,即使这个事务还没有提交
if (前一秒发生的IO次数 < 5% * innodb_io_capacity) //innodb_io_capacity表示磁盘IO的吞吐量,默认为200
do_merge_5%_innodb_io_capacity_insert_buffer(); //合并5%*innodb_io_capacity的插入缓冲
if (缓冲池中脏页的比例 > innodb_max_dirty_pages_pct){ //innodb_max_dirty_pages_pct表示缓冲池中的脏页比例,默认为90%
do_buffer_pool_flush_100%_innodb_io_capacity_dirty_page(); //刷新innodb_io_capacity个脏页到磁盘
}else if (enable_adaptive_flush()){
do_buffer_pool_flush_desired_amount_dirty_page(); //根据产生重做日志的速度来决定刷新脏页的数量
}
if (no_user_activity()) //没有用户活动
goto backgroup loop;
}
// 每10秒执行的操作
if (前10秒发生的IO次数 < innodb_io_capacity)
do_buffer_pool_flush_100%_innodb_io_capacity_dirty_page(); //刷新脏页到磁盘
do_merge_5%_innodb_io_capacity_insert_buffer(); //合并插入缓冲
do_log_buffer_flush_to_disk(); //日志缓冲刷新到磁盘
do_full_purge(); //删除无用的undo页
if (缓冲池中脏页的比例 > 70%){
do_buffer_pool_flush_100%_innodb_io_capacity_dirty_page(); //刷新innodb_io_capacity个脏页到磁盘
}else {
do_buffer_pool_flush_10%_innodb_io_capacity_dirty_page(); //刷新10%*innodb_io_capacity个脏页到磁盘
}
goto loop;
backgroup loop:
do_full_purge(); //删除无用的undo页
do_merge_100%_innodb_io_capacity_insert_buffer(); //合并插入缓冲
if (not_idle()){
goto loop;
}else {
goto flush loop;
}
flush loop:
do_buffer_pool_flush_100%_innodb_io_capacity_dirty_page(); //刷新100个脏页到磁盘
if (缓冲池中脏页的比例 > innodb_max_dirty_pages_pct) //innodb_max_dirty_pages_pct默认为90%
goto flush loop;
goto suspend loop;
suspend loop:
suspend_thread();
waiting_event(); //等待用户事件,重回主循环
goto loop;
}
(2)IO Thread
InnoDB 中使用了大量的AIO(Async IO)来处理写IO请求,以此提高数据库的性能。IO Thread负责这些IO请求的回调处理。InnoDB 1.0之前有4个 IO Thread,分别是write thread、read thread、inser buffer thread、log IO thread;InnoDB 1.0.x开始read thread和write thread分别增加到4个(即默认会有10个IO Thread),且可以通过参数配置调整read thread和write thread的数量。
(3)Purge Thread
Purge Thread用于回收已经分配且不再需要的undo页。InnoDB 1.1 之前undo页的purge回收操作是在Master Thread中完成;InnoDB 1.1开始purge操作可以独立到单独的Purge Thread中,以此减轻Master Thread的工作,提高CPU的利用率和存储引擎的性能;InnoDB 1.2 开始支持多个Purge Thread,加快undo页的回收,同时由于Purge Thread是离散读undo页,进一步利用磁盘的随机读性能。
(4)Page Cleaner Thread
Page Cleaner Thread是InnoDB 1.2.x开始引入,其作用是把原本Master Thread中的脏页刷新操作独立到单独的线程中,减轻Master Thread的工作以及对于用户查询线程的阻塞(为什么?),提高存储引擎的性能。
2.1.2 内存池
内存池大致可以分为缓冲池、重做日志缓冲、额外内存池,主要负责如下工作:
- 维护所有进程/线程需要访问的多个内部数据结构;
- 缓存磁盘上的数据,方便快速读取;
- 对磁盘文件的数据修改之前在这里缓存;
- 重做日志(redo log)缓存;
(1)缓冲池
缓冲池缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应hash索引、锁信息、数据字典信息。InnoDB 1.0.x 开始可以设置多个缓冲池实例,每个页根据hash值散列到不同的缓冲池实例中。
- 读取页操作:首先判断该页是否在缓冲池中,若存在则直接从缓冲池中读取该页,若不存在则从磁盘读取该页,并缓存到缓冲池中;
- 修改页操作:首先修改缓冲池中的页,然后再以一定的频率(checkpoint机制)刷新到磁盘;
(2)LRU List、Free List、Flush List
a. LRU List
基于LRU算法缓存页,但是不包括插入缓冲(insert buffer)、自适应hash索引、锁信息、数据字典信息。LRU List 采用了优化的LRU算法,优化点有二:一是新缓存的页不是放到队列头,而是放到设定的midpoint位置(默认是队列的5/8的位置),以此解决多页扫描时产生的缓存污染问题(多页扫描的页可能仅仅是本次使用,导致LRU List中缓存的页并不是热点数据);二是增加innodb_old_blocks_time参数,用于表示页插入到midpoint位置后需要等待多久才会被加入到LRU List的热端(midpoint之前的为热端);
b. Free List
用于存储空闲页。当需要缓存页到缓冲池中时,先检查Free List中是否有空闲页,当由空闲页时删除一个空闲页加入LRU List,若没有则执行flush LRU List操作。flush LRU List流程如下:遍历LRU List寻找其中的非脏页,将这些页从LRU List中删除,并插入Free List;若不存在非脏页,则从LRU List中flush一些脏页至磁盘,并删除Flush List中的对应记录,然后把flush完的非脏页插入Free List。
c. Flush List
用于存储脏页。Flush List中存的脏页都有一个oldest_modification字段,用于表示该页最早修改的时间,Flush List中的脏页按照oldest_modification排列,越大则说明修改时间越晚,排在越前边。对Flush List执行flush操作时,从Flush List的尾部开始执行,写出足够多的脏页,推进Checkpoint。
d. flush LRU List 和 flush Flush List 的区别
- flush LRU是为了在缓冲池满的时候释放足够的脏页,并使用户可以立即获得空闲页面,不用长时间等待;flush Flush是为了推进CheckPoint,使InnoDB再系统崩溃时能够快速恢复。
- flush LRU是先从LRU List中删除,同时flush Flush List中对应页,最后把非脏页放到Free List;flush Flush是直接删除Flush List中的页,无需修改LRU List和Free List。
- flush LRU释放的脏页较少,基本固定数量;flush Flush根据系统的繁忙程度,动态调整释放的脏页数量 。
- LRU List + Free List ≈ 缓冲池大小,因为缓冲池中还有insert buffer、锁信息等数据
(3)重做日志(redo log)缓冲池
InnoDB存储引擎首先将重做日志信息放到重做日志缓冲池中,然后按一定频率将其刷新到磁盘的重做日志文件中,重做日志缓存默认为8M。触发重做日志缓存刷新到磁盘重做日志文件有三种情况:
- Master Thread 每秒触发;
- 每个事务提交时触发;
- 当重做日志缓冲池的剩余空间小于一半时触发;
(4)额外内存池
InnoDB在对一些数据结构本身的内存进行分配时,需要从额外内存池中申请,当额外内存池内存不够时,会从缓冲池申请。例如分配了缓冲池,但是每个缓冲池中帧缓冲以及对应的缓冲控制对象(记录了LRU、锁、等待等信息)需要从额外内存池中申请。
2.2 CheckPoint机制
CheckPoint机制是为了解决以下几个问题:
- 缩短数据库的恢复时间:当数据库宕机时,不需要重做所有日志,只需要对CheckPoint之后的日志进行重做即可;
- 缓冲池不够用时,将脏页刷新到磁盘:当缓冲池不够用时,会执行flush操作,强制推进CheckPoint;
- 重做日志空间不足时,刷新脏页:重做日志是循环使用的,若此时重做日志空间不足时,需要强制推进CheckPoint,释放重做日志空间;
CheckPoint分为两种:Sharp CheckPoint 和 Fuzzy CheckPoint。
- Sharp CheckPoint发生在数据库关闭时将所有脏页都刷新回磁盘;
- Fuzzy CheckPoint分为四种情况,
- Master Thread CheckPoint:Master Thread每秒或没十秒从缓冲池的Flush List中刷新一定比例的脏页到磁盘,这个过程是异步的,不会阻塞用户查询线程。
- flush LRU List CheckPoint:InnoDB需要保证LRU List中有100个空闲页可被使用,若没有会执行 flush LRU List操作,释放一定数量的脏页,数量较少,推进CheckPoint。InnoDB 1.2.x 之前检查空闲页操作是放在Master Thread中,这会阻塞用户查询线程;InnoDB 1.2.x 开始放在Page Cleaner Thread中,不会阻塞用户查询线程。
- Async/Sync flush CheckPoint:重做日志空间不足时,强制刷新脏页,此时脏页是从Flush List中选取,数量较大。本操作InnoDB 1.2.x开始也放到Page Cleaner Thread中,不会阻塞用户查询线程。
- Dirty Page too much CheckPoint:脏页数量太多,触发刷新。InnoDB 1.0.x之前默认是缓冲池中的脏页达到90%时触发,之后是75%。
2.3 关键特性
2.3.1 插入缓冲(Insert Buffer)
插入缓冲是为了解决插入或者更新辅助索引带来的离散访问辅助索引页的问题,基本流程是先判断插入或更新的辅助索引页是否在缓冲池中,若在则直接插入或更新,若不在则放到一个Insert Buffer对象中,然后以一定的频率合并Insert Buffer,并更新辅助索引页。通俗的说就是把多次更新辅助索引页的操作合并成一次,以提高辅助索引的更新性能。InnoDB 1.0.x开始升级了Insert Buffer,变为Change Buffer,可以对插入、删除、更新操作都做缓冲。
插入缓冲的应用需要满足两个条件:一是必须是辅助索引,二是索引值不能唯一。聚集索引由于是顺序访问,不存在离散访问问题;索引值唯一的话,则插入前需要判断索引值是否已存在,而这一步操作是离散读取,导致插入缓冲失去意义。也就是说Insert Buffer的适用对象是非唯一辅助索引。
插入缓冲的内部实现就是一棵B+树(具体结构待补充),合并插入缓冲发生在以下三种情况:
- 辅助索引页被读取到缓冲池中;
- Insert Buffer Bitmap页追踪到该辅助索引页无可用空间(插入记录后可用空间小于1/32)时:此时会强制读取该辅助索引页到换缓冲池,Insert Buffer Bitmap记录了每个辅助索引页的可用空间,以保证合并插入缓冲一定成功;
- Master Thread(具体可见2.1.1节中的伪代码)
2.3.2 双重写(Double Write)
由于刷新脏页到磁盘的过程是离散写的,因此可能会出现写到一半数据库宕机,这种情况称之为部分写失效(partial page write)。某些情况下可以通过redo log恢复数据,但是如果页本身发生损坏(数据丢失等情况),redo log也无法恢复。Double Write就是为了解决redo log无法恢复的情况,提高数据库的可靠性。
Double Write的体系架构如下:
Double Write由两部分组成,一部分是内存中的Double Write Buffer(2MB),一部分是物理磁盘上共享表空间中连续的128页(2MB)。当缓冲池中的脏页需要刷新时,先将脏页复制到Double Write Buffer中,然后分两次顺序写入物理磁盘的共享表空间中(图中的Double Write),然后再写入磁盘的数据文件中。Double Write虽然会增加刷新脏页的耗时,但是由于共享表空间中是顺序写,而磁盘数据文件是随机写,所以整体性能不会有太大的下降。一句话总结就是,Double Write通过先写共享表空间的方式,把一次脏页刷新操作变成原子操作/事务,要么全部写入成功,要么全部失败。
2.3.3 自适应哈希索引(Adaptive Hash Index)
InnoDB会监控对表上各索引页的查询,如果观察到建立哈希索引可以带来速度提升,则建立哈希索引。自适应哈希索引是通过缓冲池的B+树页构造的,不需要对整张表建立。自适应哈希索引有一个要求,即对这个页的连续访问模式必须是一样的,即查询条件一样,例如where a = 1。
3、索引
InnoDB中B+树索引不能找到给定键值的具体行,只能找到被查找数据行所在的页,然后把该页读到内存中,再在内存中查找。也就是说B+树索引索引的是数据页,而不是数据行,这跟InnoDB管理的最小单元是页吻合。
3.1 B树与B+树区别
- B树的叶子节点和非叶子节点都会存储数据,B+树只有叶子节点存储数据;
- 由于B+树只有叶子节点存储数据,所以B+树的非叶子节点可以存储更多的值,降低树的高度。一般B+树的高度在2~4层,相比于B树可以减少磁盘IO;
- B+树的叶子节点间使用双向链表相连,方便顺序读取;
- B+树的查询时间复杂度是O(logN),而B树最优是O(1)、最差是O(logN);
3.2 聚集索引和辅助索引
- 聚集索引:按照主键构造的B+树,聚集索引是唯一的,且主键逻辑顺序与数据物理顺序相同。聚集索引的存储不是物理上连续而是逻辑上连续,原因有二:一如果是物理连续,那建表的时候需要留出足够空间,不然数据增加时会涉及到数据整体迁移,开销很大;二是B+树叶子节点通过双向链表相连(数据页间通过双向链表相连,每一页中的记录也是通过双向链表相连),如果是物理连续,那双向链表就没有意义。
- 辅助索引:辅助索引是聚集索引的索引,每张表上可以有多个辅助索引,且辅助索引的值可以不唯一。辅助索引也是B+树结构,叶子节点存储的是聚集索引的键,通过辅助索引查找数据时,最后还是要通过聚集索引拿到最终的行数据。
3.3 索引的DDL操作优化
MySQL5.5之前,对于索引的增加这类DDL操作的流程是这样的:创建一张临时表,把原表数据导入临时表,删除原表,把临时表更名为原表。整个过程需要对表加X锁,阻塞所有的读写操作。
InnoDB 1.0.X开始支持 Fast Index Creation(FCI),FCI不需要使用临时表,直接在原表上,整个过程会对表加S锁,阻塞所有的写操作。FCI只能作用于辅助索引,聚集索引的创建还是需要使用临时表。
MySQL 5.6开始支持Online DDL,允许在创建索引的同时,执行insert、update、delete等DML操作。其原理是把这些DML操作都写入缓存,在索引创建完成后,再通过redo log保证数据一致。Online DDL支持辅助索引和聚集索引。
3.4 Cardinality值
Cardinality值表示索引中不重复记录数量的预估值,这是一个预估值,不是准确值。Cardinality值与表中记录总数的比值越接近1,表示该索引的选择性越高。InnoDB中使用采样的方式统计Cardinality值,更新Cardinality值的策略为:表中1/16的数据已经发生变化或者表中数据修改次数超过20亿次。统计Cardinality值的流程如下:
- 获取B+树索引中叶子节点的数量,记为A
- 从B+树索引中随机选取8个叶子节点,分别统计每页中不重复记录的数量,记为P1、P2、...P8
- 计算Cardinality值,Cardinality = (P1+P2+...+P8)*A/8
3.5 索引实战优化
3.5.1 索引覆盖
索引覆盖是针对辅助索引而言,即从辅助索引中就能得到查询结果,不要接着去查询聚集索引,例如count操作或者只需要查询主键字段。
3.5.2 优化器不使用辅助索引
优化器不使用辅助索引一般发生在查找的数据量较大的情况,例如范围查找、join操作。辅助索引不能覆盖用户查询的字段,同时查询的数量又比较大时,在查询完辅助索引后去查询聚集索引时,会产生大量的离散读操作,其效率远不如直接从聚集索引查找,把大量的离散读操作转换成顺序读操作,提高查询效率。
3.5.3 索引提示
索引提示是指显式地告诉优化器使用哪个索引,用到这个功能一般是一下两种情况:
- 优化器错误地选择了索引,导致查询变慢,这种情况很少发生
- 可选择的索引过多,导致优化器选择执行计划的时间大于SQL语句本身,举个例子:
CREATE TABLE t (
a INT,
b INT,
KEY (a),
KEY (b)
) ENGINE=INNODB
# 执行如下语句
SELECT * FROM t WHERE a=1 AND b=2;
# 此时优化器会选择同时使用索引a和索引b,然后取交集得到结果,取交集操作明显会比较慢
# 可以使用 FORCE INDEX 强制使用某个索引
SELECT * FROM t FORCE INDEX(a) WHERE a=1 AND b=2;
3.5.4 Multi-Range Read
Multi-Range Read的目的是减少磁盘的随机访问,并将随机访问转化为较为顺序地访问,适用于range、ref、eq_ref类型的操作。其好处如下:
- 使得数据访问较为顺序:在查询辅助索引时,把查询的结果按主键排序,并按排序后的主键进行顺序查找
- 减少缓冲池中页被替换的次数:由于缓冲池大小有限,频繁地离散读操作可能会导致某些页不断地被替换出缓冲池,又不断地被读入缓冲池
- 批量处理对键值的查询操作:将某些范围查询操作,转换为批量的键值对查询,这样做的好处是在转换的过程中可以直接过滤一些不符合的数据,举个例子:
SELECT * FROM t
WHERE a >= 100 AND a < 200 AND b = 1000
# 表中有联合索引(a,b),则优化器会先取出所有a>=100且a<200的数据,然后再根据b=1000进行过滤
# 使用MRR后,优化器会把查询条件拆分为(100,1000)、(101,1000)、...(199,1000),然后再根据这些拆分出的条件进行查询
3.5.5 Index Condition Pushdown
Index Condition Pushdown是指数据库在去除索引的同时,判断是否可以进行WHERE条件的过滤,也就是把WHERE的部分过滤放到存储引擎层,大大减少了上层SQL对于数据的fetch,以提高数据库整体的性能。
4、锁
4.1 锁的类型
根据锁的粒度可以分为行锁和表锁。根据锁的兼容性可以分为共享锁(S锁)、排它锁(X锁)、意向共享锁(IS锁)、意向排它锁(IX锁),其中S锁和X锁可以作用于行级、表级,IS锁和IX锁可作用于表级。当需要对一个对象加锁时,需要先对其加粗粒度锁,再加细粒度锁。
意向锁的存在就是为了协调表锁和行锁的关系,支持多粒度的锁(表锁和行锁)并存,举个例子:事务A对某行加了X锁,而事务B对整表加了X锁,此时事务A和事务B就会发生冲突;有的同学可能会说事务B在对整表加X锁的时候,先去检测表中有没有其他X锁不就好了,没错是可以这样,但是如果这表很大,遍历完所有的行可能会需要很长时间。所以意向锁就出现了,事务A对某行加X锁前,需要对整表加一个IX锁,而事务B想要对整表加X锁时,发现表上有一个IX锁,就会加锁失败,等待该IX锁释放后,再对表加X锁。表级意向锁和行级锁的兼容性如下:
IS | IX | S | X | |
IS | Y | Y | Y | N |
IX | Y | Y | N | N |
S | Y | N | Y | N |
X | N | N | N | N |
4.2 非锁定的一致性读
非锁定的一致性读是指通过行多版本控制的方式来读取当前执行时间数据库中行的数据。当一个行被加了X锁时,仍然可以读取该行的数据,其原理是读取了该行的快照版本,而快照是通过undo log实现。在不同隔离级别下,读取到行数据快照是不一样的,在REPETABLE READ下,读取的是该事务开始前的行数据快照版本;在READ COMMITTED下,总是读取该行的最新一份快照,这违反了数据库的隔离性,导致出现不可重复读问题。
4.3 行锁算法
InnoDB行锁是通过对索引加锁实现的,不会锁行记录,如果没有显式的索引,则使用_ROWID的聚集索引。行锁算法分为以下三种:
- Record Lock:单个行记录上的锁。
- Gap Lock:间隙锁,锁定一个范围,但是不包含记录本身,左右都是开区间。
- Next-Key Lock:上述两种锁的结合,锁定一个范围,并锁定记录本身,左开右闭区间。当查询的索引为聚集索引时,InnoDB会对Next-Key Lock优化,降级为Record Lock;当查询的索引为辅助索引时,除了锁住该键值所在的区间外,还会锁住下一个键值所在的区间。举个例子:
CREATE TABLE t (a INT, b INT, PRIMARY KEY(a), KEY(b));
INSERT INTO t SELECT 1,1;
INSERT INTO t SELECT 3,1;
INSERT INTO t SELECT 5,3;
INSERT INTO t SELECT 7,6;
INSERT INTO t SELECT 10,8;
# 执行以下语句,for update的作用是强制加X锁
SELECT * FROM t WHERE b=3 FOR UPDATE;
# 上述语句会对a=5那行加上Record Lock,对索引b加上(1,3]的Next-Key Lock和(3,6)的Gap Lock,合并完就是对(1,6)加上Gap Lock
4.4 锁问题
- 脏读:是指当前事务可以读到其他事务没有提交的数据,发生在READ UNCOMMITTED下
- 不可重复读:是指一个事务内多次读取的同一数据得到的数据不一致,发生在READ COMMITTED下,在该隔离级别下,通过Next-Key Lock可以避免不可重复读
- 幻读:是指一个事务内读取的同一数据得到的数据条数不同,不可重复读针对的数据值的修改,而幻读针对的是数据条数的变化,发生在READ COMMITTED下,在该隔离级别下,通过Next-Key Lock可以避免幻读
- 丢失更新:事务A先读取然后更新,事务B在A更新前读取然后更新,导致AB中后更新的会覆盖掉先更新的,因为读取时没有加X锁,且后更新的事务无法感知先更新的事务对数据的操作,发生在READ REPEATABLE下。这个问题大部分是由于业务逻辑不合理而不是数据库的问题,数据库可以通过SERIALIZABLE隔离级别解决,业务中可以通过悲观锁或者乐观锁解决。
5、事务
5.1 ACID原则
- 原子性(Atomicity):整个事务流程视为一个原子操作,要么全部成功,要么全部失败。
- 一致性(Consistency):事务从一种一致性状态转变为下一种一致性状态,事务开始之前和事务结束之后数据库的完整性月数没有被破坏。例如:在表中有一个字段为姓名,为唯一约束,如果一个事务对姓名字段修改了,但是事务提交或者发生回滚后,表中姓名变得非唯一了,这就破坏了事务的一致性。
- 隔离性(Isolation): 要求每个事务的读写操作在提交前对其他事务都不可见,保证隔离性靠锁。
- 持久性(Durability):事务提交后其结果是永久的,及时发生数据库宕机也能恢复数据。
5.2 事务的实现
5.2.1 redo log
redo log 用来保证数据库的原子性和持久性。redo log由两部分组成,一个是内存中的重做日志缓冲(redo log buffer),一个是磁盘中的重做日志文件(redo log file)。InnoDB通过Force Log at Commit 机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的Commit操作完成才算事务完成。
重做日志与二进制日志(bin log)的区别:
- redo log是InnoDB存储引擎产生的;bin log是MySQL数据库上层产生的,bin log会记录所有存储引擎对数据库的更改。
- redo log记录的是物理日志,即对每个页的修改;bin log记录的是逻辑日志,即SQL语句。
- redo log的写入是在事务中多次写入,即可能会有多个事务交替写入;bin log只在事务提交完成后写入一次。
5.2.2 undo log
undo log用来保证数据库的一致性,用于数据回滚操作。undo log是逻辑日志,其通过逆向操作完成数据库回滚,而不是直接将数据库还原到事务执行前的状态,因为数据库是多事务并发操作的,如果直接还原会对其他事务产生影响。除了回滚操作,undo log的另一个作用是MVCC,可以通过undo log实现非锁定的一致性读。此外,undo log也会产生redo log。
5.3 隔离级别
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE