在Mysql中,是否支持事务是由存储引擎决定的,以下是Mysql官网关于部分存储引擎特点的摘抄
Feature | MyISAM | Memory | InnoDB | Archive | NDB |
---|---|---|---|---|---|
Transactions | No | No | Yes | No | Yes |
Locking granularity | Table | Table | Row | Row | Row |
MVCC | No | No | Yes | No | No |
从表中可以看出
本篇将主要讲解InnoDB,MySQL 在 5.1 之前版本默认存储引擎是 MyISAM,5.1 之后版本默认存储引擎是 InnoDB.
InnoDB
: The default storage engine in MySQL 8.0. InnoDB
is a transaction-safe (ACID compliant) storage engine for MySQL that has commit, rollback, and crash-recovery capabilities to protect user data. InnoDB
row-level locking (without escalation to coarser granularity locks) and Oracle-style consistent nonlocking reads increase multi-user concurrency and performance. InnoDB
stores user data in clustered indexes to reduce I/O for common queries based on primary keys. To maintain data integrity, InnoDB
also supports FOREIGN KEY
referential-integrity constraints. For more information about InnoDB
, see Chapter 15, The InnoDB Storage Engine.
特点如下:
These tables have a small footprint. Table-level locking limits the performance in read/write workloads, so it is often used in read-only or read-mostly workloads in Web and data warehousing configurations.
特点:
数据库事务必须保存ACID特性,ACID是指:
接下来看在MySQL中是如何实现事务的.
MySQL官方对InnoDB事务的描述:
The InnoDB
transaction model aims to combine the best properties of a multi-versioning database with traditional two-phase locking. InnoDB
performs locking at the row level and runs queries as nonlocking consistent reads by default, in the style of Oracle. The lock information in InnoDB
is stored space-efficiently so that lock escalation is not needed. Typically, several users are permitted to lock every row in InnoDB
tables, or any random subset of the rows, without causing InnoDB
memory exhaustion.
我们从中可以提取几个关键字:
那事务到底是怎么回事?
假设你已经知道了MySQL的一些知识,如binlog,redo log, undo log这些.
如图:
当客户端开启了一个事务:
那么这里有几个问题需要思考一下?
用户事务操作本质是对MySQL内存数据的读写
由于MySQL对数据的操作都是由存储引擎处理,但是MySQL的主从复制是通过binlog来实现的
接下来,我们逐个分析这些问题.
本节我们先回答1.1这个问题,如何正确的读取数据.
摘抄SQL:1992中的描述.
The isolation level of an SQL-transaction defines the degree to which the operations on SQL-data or schemas in that SQL-transaction are affected by the effects of and can affect operations on SQL-data or schemas in concurrent SQL-transactions.
一个SQL 事务的隔离级别定义了:
在并发SQL 事务环境下,一个SQL 事务中操作的 SQL data或数据库schema受其他SQL事务操作SQL data或schema影响的程度.
大白话就是:在并发SQL 事务的情况下,当前事务操作的数据 或者数据库schema受其事务的影响.
我们先来看下SQL:1992中定义了哪些隔离级别?
地址:https://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt
__Table_9-SQL-transaction_isolation_levels_and_the_three_phenomena_
_Level__________________P1______P2_______P3________________________
| READ UNCOMMITTED | Possib|e Possib|e Possible |
| | | | |
| READ COMMITTED | Not | Possibl| Possible |
Possible
| REPEATABLE READ | Not | Not | Possible |
| | Possib|e Possib|e |
| | | | |
| SERIALIZABLE | Not | Not | Not Possible |
|______________________|_Possib|e_Possib|e_________________________|
The isolation level specifies the kind of phenomena that can occur
during the execution of concurrent SQL-transactions. The following
phenomena are possible:
1) P1 ("Dirty read"): SQL-transaction T1 modifies a row. SQL-
transaction T2 then reads that row before T1 performs a COMMIT.
If T1 then performs a ROLLBACK, T2 will have read a row that was
never committed and that may thus be considered to have never
existed.
2) P2 ("Non-repeatable read"): SQL-transaction T1 reads a row. SQL-
transaction T2 then modifies or deletes that row and performs
a COMMIT. If T1 then attempts to reread the row, it may receive
the modified value or discover that the row has been deleted.
3) P3 ("Phantom"): SQL-transaction T1 reads the set of rows N
that satisfy some <search condition>. SQL-transaction T2 then
executes SQL-statements that generate one or more rows that
satisfy the <search condition> used by SQL-transaction T1. If
SQL-transaction T1 then repeats the initial read with the same
<search condition>, it obtains a different collection of rows.
在SQL:1992中定义了四种隔离级别:
在上表中说明了在并发操作sql的情况下,每个隔离状态可能会发生的异常情况,主要分为以下三种情况:
那么在MySQL的InnoDB引擎中的隔离级别如何?
InnoDB
offers all four transaction isolation levels described by the SQL:1992 standard: READ UNCOMMITTED
, READ COMMITTED
, REPEATABLE READ
, and SERIALIZABLE
. The default isolation level for InnoDB
is REPEATABLE READ
. 来自MySQL官网.
在MySQL的InnoDB存储引擎中提供了四种SQL:1992规定的事务隔离级别:
InnoDB默认的隔离级别是REPEATABLE READ.
在SQL:1992定义中,REPEATABLE READ 可重复读这一隔离级别还存在幻读的问题,但是在MySQL InnoDB存储引擎中通过gap lock 解决了幻读这个问题,后面会说到.
那么在MySQL中是如何实现这些隔离级别的?
在MySQL中
先说下一条记录的结构,记录中一定包含以下三个属性:
有了数据的版本链,那么要想实现不同隔离级别下数据的可见性,主要分为以下三种:
一个ReadView中主要包括以下四部分内容:
在访问某一条记录时,需要按照以下规则判断当前记录的版本链中那一条数据是对当前事务可见的:
那么基于ReadView怎么实现READ COMMITTED 和REPEATABLE READ这两种隔离级别?
它们两个的主要区别是,当前事务是否能读取到,刚提交事务的数据(当前事务开始时,该事务还未提交).
所以在两个隔离级别下通过ReadView的生成时机,来实现这两种隔离级别.
READ COMMITTED 是在每次读取数据时,生成一次ReadVIew,那么其中m_ids会包括当前正在活跃的事务id.
例如当前事务trx_id=100,
第一次查询:m_ids [91,92,98]
第二次查询前,92事务提交,并且新生成了两个事务,trx_id = 110,120,
那么新生成的ReadView中m_ids为:[91,98,110,120].
所以刚提交的92事务的数据,根据ReadView访问规则,对当前事务是可见的.
REPEATABLE READ在事务第一次读取数据时生成ReadView,一直到事务提交,该事务的ReadView中的m_ids、min_trx_id,max_trx_id都不会变.
但是当前ReadView的creator_trx_id为0,因为事务的id生成必须满足以下条件之一:
小贴士:
我们前边说过对某个查询语句执行EXPLAIN分析它的查询计划时,有时候在Extra列会看到Using temporary的提示,这个表明在执行该查 询语句时会用到内部临时表。这个所谓的内部临时表和我们手动用CREATE TEMPORARY TABLE创建的用户临时表并不一样,在事务回滚时并 不需要把执行SELECT语句过程中用到的内部临时表也回滚,在执行SELECT语句用到内部临时表时并不会为它分配事务id。
有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务 id。
undo log日志主要是为事务回滚和MVCC中的版本链服务.
上图为MySLQ内存中,undo log相关的数据结构:
在Mysql中会存在128个回滚段rollback segment,每一个回滚段对应就是一个Rollback Segment Header类型的页面,该页面中记录了两个重要的信息:
在MySQL中对数据的操作主要分为两种insert操作和update操作,另外delete操作只是在记录上修改delete flag标识.
数据主要分为两种,一是普通的用户表,而是在操作用户表的过程中,MySQL自己可能创建临时表,并对其进行insert\update\delete操作.
所以一个事务操作最多可能有四个undo log链:
这也是为什么将回滚段分为三部分的原因
下面梳理一下,事务开始时,undo log日志如何生成?
这里需要说明一下,在生成undo log日志的过程,
当innodb更新压力大时,purge操作不能高效进行history list就会变的很长.
innodb_max_purge_lag:用于设置history链表长度,默认为0,不做限制.
如果做了限制,当history长度达到后,就会延缓DML操作,延缓算法为:
Delay = ( (length(history_list) -innodb_max_purge_lag)*10)-5
delay单位为毫秒. delay的对象是行,而不是DML操作. 当一个update 操作假如需要更新5行数据,那么总延迟时间为5*delay .
innodb1.2引入全局动态参数innodb_max_purge_lag_delay用来设置最大延迟时间,当delay大于该值后,使用该值
系统默认设置使用128个回滚段,但是用户可以配置,但是临时表回滚段数量不会改变一直为32
参数: innodb_rollback_segements ,
我们上面说到,普通表回滚段可以在系统表空间,也可以在回滚表空间.
如果设置,参数如下:
redo log成为重做日志,是用来实现事务持久性的.
它主要有两部分构成:
Redo log 是WAL技术的体现,下面对WAL技术做一个简单介绍.
WAL:write ahead log 预写式日志.
WAL要求一个页操作在写入到持久存储设备时,首先必须将其内存中的日志写入到持久化存储. 就是说,我想将一个数据从内存刷到磁盘上,我必须先记录日志,在mysql中,我的理解是,对一个数据的操作,在从内存刷到磁盘前,必须先写到日志,保证系统突然崩溃,能够恢复,内存中的数据.
Redo log整体流程如下:
当用户开启事务操作数据过程中,innodb做了以下内容:
在整个过程,有下面几个问题需要思考:
接下来,就来回答这几个问题.
在MySQL中引入了XA事务,来保证binlog和redo log的一致性问题.
两阶段操作如下:
可能发生的情况如下:
对于redo log,MySQL做了以下两个优化:
redo log buffer的存在,提高了redo log记录写入的效率.MySQL不会对每条记录,进行刷盘操作,而是等到事务提交时,将该事务的redo log刷到磁盘.
还有以下几种情况,会将redo log刷到磁盘:
innodb_flush_log_at_trx_commit: 该参数用来控制redo log的刷盘策略,它有以下几种选择:
由于在linux系统中一个数据页最小时4Kb,而MySQL默认的数据页是16Kb,为了保证每次刷新时一个MySQL数据页的一致性,MySQL提供了Double Write机制.
在每次刷盘开始时,对与每个MySQL数据页,MySQL会将该数据页数据写入到double write 数据区,然后再将MySQL的redo log数据页刷新到磁盘,这样就算再redo log写入磁盘文件时发生崩溃,也能从double write区域恢复当前数据.
Mysql的redo log文件是会被循环使用的,
MySQL的数据目录(使用SHOW VARIABLES LIKE 'datadir’查看)下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个
磁盘文件中。如果我们对默认的redo日志文件不满意,可以通过下边几个启动参数来调节:
从上边的描述中可以看到,磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile_数字(数字可以是0、1、2…)的形式进行命名。 在将redo日志写入日志文件组时,是从ib_logfile0开始写,如果ib_logfile0写满了,就接着ib_logfile1写,同理,ib_logfile1写满了就去写ib_logfile2,依此类推。如 果写到最后一个文件该咋办?
那就重新转到ib_logfile0继续写.但是这会造成最后写的redo日志与最开始写的redo日 志追尾.
如何判断能否覆盖,这是就用到了checkpoint机制了
在说checkpoint之前,需要先说下LSN.
LSN: Log Sequeue Number 日志序列号.
初始值为8704.
LSN代表了在mtr(mini-transaction)过程中产生的redo log信息,保存到buffer pool 是所产生的位移,是递增的.
假如一个mtr 产生的日志数量为1000字节,那么新的LSN = LSN +1000 + (当前页的管理数据).
页的管理数据是指,每个数据页,即redo log buffer 中的数据页,都会有头部/尾部信息,这块也要算上.
总之记住以下两点:
在上面的Redo log整体流程 中,当mtr 修改buffer pool 中的数据页时,会将当前数据页假如到flush列表,
并且记录当前数据页的最早lsn(old_lsn),和最新lsn(new_lsn)
flush列表中的记录是按照最早lsn排列的,末尾的old_lsn最小,所以最后的这个数据页的数据从buffer pool 刷新到磁盘时,这时redo log 中lsn小于 该页old_lsn的日志都是无用的,就可以被覆盖了.
MySQL中定义了一个全局变量checkpoint_lsn来代表当前系统中可以被覆盖 的redo日志总量是多少,这个变量初始值也是8704。
所以将buffer pool 中的数据刷新到磁盘文件,并且更新当前系统的checkpoint_lsn,并将其写入到redo log文件头部里面,的过程称为checkpoint.
主要包括两步:
checkpoint_no每做一次checkpoint,该值就会加1.
当checkpoint_no的值是偶数时,就写到checkpoint1中,是奇数时,就写到checkpoint2中。
那么什么情况下会将buffer pool中的数据页,刷新到磁盘?
情况有两种:
在并发事务的情况下,访问相同记录情况大致分为以下三种:
对于第一种情况读-读:在并发环境下,并不会引起什么问题,所以允许这种情况.
对于第二种情况,如果同时对同一条记录进行修改,会引起脏写的发生,而这是不允许的,所以一般是通过加锁,进行排序.
对于第三种情况,读写,对于这种情况有两种方式:
下面主要分析第三种情况.
事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读,有的地方也称之为快照读。所有普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读,比方
说:
SELECT * FROM t;
SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2
一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动。
在Mysql中从锁的阻塞情况来看分为以下两种:
兼容性如下:
兼容性 | X | S |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
锁定读的方式:
两者的区别:
一个事务对一条记录加了S锁,那么其他事务还可以对该记录加S锁,但会阻塞X锁.
一个事务对一条记录加了X锁,会阻塞其他事务对该记录加的任何锁.
平常所用到的写操作无非是DELETE、UPDATE、INSERT这三种:
上面提到的都是对记录加锁.
在MySQL中还提供了两种级别的锁:
表锁,就是给整张表进行加锁,也可以分为共享锁(S锁)和排它锁(X锁).
当一个事务给表加S锁或者X锁,会有以下特点:
那么当给表加S锁或者X锁时,如何判断表中的记录又没又加锁,或者加锁的类型是S锁或X锁哪一种.
这时就需要意向锁了.
意向锁分为两种
IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是 兼容的,IX锁和IX锁是兼容的
兼容性 | X | IX | S | IS |
---|---|---|---|---|
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
IX | 不兼容 | 兼容 | 不兼容 | 兼容 |
S | 不兼容 | 不兼容 | 兼容 | 兼容 |
IS | 不兼容 | 兼容 | 兼容 | 兼容 |
接下来主要分析InnoDB引擎中的锁.
一个事务在对表执行如alter table 、drop table这类DDL语句时,会对表加X锁,阻塞其他事务的SELECT、INSERT、UPDATE、DELETE操作.
在系统变 量autocommit=0,innodb_table_locks = 1时,手动获取InnoDB存储引擎提供的表t的S锁或者X锁可以这么写:
LOCK TABLES t READ:InnoDB存储引擎会对表t加表级别的S锁。
LOCK TABLES t WRITE:InnoDB存储引擎会对表t加表级别的X锁。
当我们在对使用InnoDB存储引擎的表的某些记录加S锁之前,那就需要先在表级别加一个IS锁,当我们在对使用InnoDB存储引擎的表的某些记录加X锁之前,那就需要先在表级别加一个IX锁。IS锁和IX 锁的使命只是为了后续在加表级别的S锁和X锁时判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录.
当为表的某一列添加AUTO_INCREMENT属性,之后在插入记录时,可以不指定该列的值,系统会自动为它赋上递增的值.在获取递增值时,就会使用该锁.
系统实现这种自动给AUTO_INCREMENT修饰的列递增赋值的原理主要是两个:
需要注意一下的是,这个AUTO-INC锁的作用范围只是单个插入语句,插入语句执行完成后,这个锁就被释放了,跟我们之前介绍的锁在事务结束时释放是不一样的。
innoDB中的行锁,顾名思义就是给记录加上锁.
innoDB中的行锁算法有三种:
那么我们下面来思考两个问题:
先回答第一个问题:
第二个问题分析如下:
对于第二个问题,我以REPEATABLE READ模式为主分析:
这四种情况下加锁的分析,顺带分析READ COMMITTED模式下对应的情况.
我们知道,在innodb中一定会存在聚簇索引的.
背景,表结构:
CREATE TABLE `t_lock` (
`id` bigint(11) ,
`name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入数据:
(1,‘name1’),
(10,‘name10’),
(20,‘name20’),
(30,‘name30’),
本文以for update 写锁为例,可以自己加上读锁思考.
如果id为主键,该主键索引就是聚簇索引.
BEGIN ;
SELECT * from t_lock where id =10 for UPDATE;
则只会在聚簇索引上对id=10的记录加锁,
这时别的事务可以操作其他事务,但是阻塞对该记录的读、写.
BEGIN ;
SELECT * from t_lock where id =15 for UPDATE;
执行这个操作,就会在(10,20)添加gap锁.它具有以下特性:
这里的语句也可以换成upadte/delete id=15 也会加gap ,insert比较特殊,后面会说.
BEGIN ;
SELECT * from t_lock where id <=15 for UPDATE;
当使用id查询或者更新、删除.
在唯一索引上的加锁情况与主键索引相同,不同的时,由于这时唯一索引和聚簇索引是两个索引.
所以还会在聚簇索引对应的记录上加锁.
当唯一索引为非空唯一索引时,就和主键索引一摸一样了,只加一把锁.
id不建立索引,name上建立普通索引.
BEGIN ;
SELECT * from t_lock where name ='name10' for UPDATE;
执行该操作,
但是如果你执行:
update t_lock set name = '30' where id=30
发现,也会阻塞.
其原因是:
你在执行该操作时,发现无法使用name索引,然后就会通过聚簇索引遍历整张表,当遍历到加锁的记录,就会阻塞.
BEGIN ;
SELECT * from t_lock where name = 'name11' for UPDATE;
当你执行该操作时,只会对name索引上的(‘name10’,‘name20’)区间加gap锁,阻塞插入.
id、name都不建立索引.
BEGIN ;
SELECT * from t_lock_id where id =10 for UPDATE;
-- 或
SELECT * from t_lock_id_name where name = 'name11' for UPDATE;
执行上面的其中一个,都会进行全表扫描,然后锁主全部记录和gap,即表锁,阻塞所有的加锁操作和插入操作,
但是可以执行正常的select.
个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的gap锁(next-key锁也包含gap锁,后边就不强调了),如果有的话,插入操作需要等待,直到拥有gap锁的那个 事务提交。但是设计InnoDB的大叔规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。把这种类型的锁命名 为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION,我们也可以称为插入意向锁。
一个事务在执行INSERT操作时,如果即将插入的间隙已经被其他事务加了gap锁,那么本次INSERT操作会阻塞,并且当前事务会在该间隙上加一个插入意向锁,否则一般情况下INSERT操作是 不加锁的。那如果一个事务首先插入了一条记录(此时并没有与该记录关联的锁结构),然后另一个事务:
立即使用SELECT … LOCK IN SHARE MODE语句读取这条事务,也就是在要获取这条记录的S锁,或者使用SELECT … FOR UPDATE语句读取这条事务或者直接修改这条记录,也就是要获取这条 记录的X锁,该咋办?
我们把聚簇索引和二级索引中的记录分开看一下:
通过上边的叙述我们知道,一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id这个牛逼的东东的存在,相当于加了一个隐式锁。别的事务在对这条记录加S锁或者X 锁时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象.
解决死锁最简单的方式就是不要有等待,将任何的等待都转化为回滚,并且事务重新开始.innodb_lock_wait_timeout可以设置超市时间.
但是如果这样,将降低并发性能.
除了超时机制外,当前数据库提供了wait-for graph(等待图)的方式来进行死锁检测,innodb也采用了这种方式,wait-for graph要求数据库保存以下两种信息:
通过上述链表可以构造一张图,而在这个图中若存在回路,就代表存在死锁.
一个死锁,简单的例子:
-- 还是上面的表t_lock ,id为主键
-- 事务1 | 事务2
begin ; |
select * from t_lock where id = 10 for update; | begin;
| select * from t_lock where id = 20 for update;
|
select * from t_lock where id = 20 for update; |
| select * from t_lock where id = 10 for update;