一、事务
1、操作事务
1.1、手动提交
set autocommit=0
:当前session禁用自动提交事物.
begin
或start transaction
:显式的开启一个事务.
commit
:提交事务,并使已对数据库进行的所有修改成为永久性的。
rollback
:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改.
1.2、自动提交
set autocommit=1
开启自动提交
1 | MySQL的每⼀条DML(增删改)语句都是⼀个单独的事务,每条语句都会⾃动开启⼀个事务,执⾏完毕⾃ |
1.3、回滚点
1 | 在某些成功的操作完成之后,后续的操作有可能成功有可能失败,但是不管成功还是失败,前⾯操作都已经成功,可以在当前成功的位置设置⼀个回滚点。 |
设置回滚点语法: savepoint 回滚点名字
回到回滚点语法:rollback to 回滚点名字
总结:设置回滚点可以让我们在失败的时候回到回滚点,⽽不是回到事务开启的时候。
2、隔离级别
2.1、查看
show variables like '%isolation%'
2.2、设置
set global transaction isolation level '级别字符串'
2.3、级别对比
名字 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现 | 默认级别 |
---|---|---|---|---|---|---|
读未提交 | READ UNCOMMITTED | √ | √ | √ | 每次都读取最新的数据 | |
读已提交 | READ COMMITTED | × | √ | √ | 每次读取前都创建read view, 通过mvcc控制 |
Oracle、Sql Server |
可重复读 | REPEATABLE READ | × | × | √ | 第一次读取之前创建read view, 通过mvcc控制 |
Mysql |
串行化 | SERIALIZABLE | × | × | × | 加锁 |
2.4、不同级别出现的问题
脏读
一个事务读到了另一个事务未提交的事务
不可重复读
一个事务多次读取数据结果不一致
幻读
一个事务(同一个read view)在前后两次查询同一范围的时候,后一次查询看到了前一次查询没有看到的行。
图中第四步的update是当前读,更新了数据的同时也更新了隐藏字段的DB_TRX_ID
,之后在进行select快照读时,此数据对于当前事务课件,所以会将该数据读出。
如果事务中都是用快照读,那么不会产生幻读的问题,但是快照读和当前读一起使用的时候就会产生幻读。
时间 | 事务1 | 事务2 |
---|---|---|
begin; | ||
T1 | select * from user where age =20 for update; | |
T2 | insert into user values(25,’25’,20);此时会阻塞等待锁 | |
T3 | select * from user where age =20 for update; |
此时,可以看到事务2被阻塞了,需要等待事务1提交事务之后才能完成,其实本质上来说采用的是间隙锁的机制解决幻读问题。
2.5、如何实现?
1 | 读未提交,每次都读取最新的数据 |
总的来说,数据库的隔离级别依靠MVCC
和锁
实现;而MVCC
依靠隐藏字段
、read view
、undo log
实现。
3、事务四大特点
事务特性 | 含义 | 实现 |
---|---|---|
A-原子性 | 事务中的多条SQL语句是⼀个整体,不可再分割, 一起成功,一起失败。 |
通过undo log来保证 |
C-一致性 | 事务前后的数据要保证⼀致 | 通过原子性、隔离性、持久性来保证 |
I-隔离性 | ⼀个事务执⾏的结果不能影响另⼀个事务。 | 通过mvcc、间隙锁来保证 |
D-持久性 | 事务⼀旦提交,对数据库的数据影响是永久了。 | 通过redo log来保证 |
二、log
1、bin log(二进制日志、归档日志)
server层面的日志,属于逻辑日志,以二进制形式记录相关语句的原始逻辑,没有crash-safe能力
2、redo log(重做日志)
- 是innodb存储引擎的日志,当发生数据修改时,先写到redolog中并更新内存;
- 该文件的大小是固定的,所以当记录满后从头循环继续写;
- 有了redolog日志,可以保证异常重启时数据不丢失,具有crash-safe能力
1 | checkpoint,在检查点之前的日志已经被记录到磁盘并擦除; |
redo log刷盘
MySQL
中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool
中。后续的查询都是先从 Buffer Pool
中找,没有命中再去硬盘加载,减少硬盘 IO
开销,提升性能。
更新表数据的时候,也是如此,发现 Buffer Pool
里存在要更新的数据,就直接在 Buffer Pool
里更新。然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer
)里,接着刷盘到 redo log
文件里。
InnoDB
存储引擎有一个后台线程,每隔1
秒,就会把 redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用 fsync
刷盘。
如右图所示,redo log的刷盘存在三种方式,可以通过innodb_flush_log_at_trx_commit
参数进行调整,主要控制innodb将log buffer中的数据写入日志文件并flush磁盘的时间点,值分别为0,1,2
值0:在提交事务时,InnoDB不会立即触发将缓存日志写到磁盘文件的操作,而是等待后台线程每秒将缓存日志回写入文件缓存系统(OS Cache),并fsync()刷盘。
为
0
时,如果MySQL
挂了或宕机可能会有1
秒数据的丢失。值1:在事务提交时,InnoDB立即将缓存日志写入文件缓存系统(OS Cache),并fsync()刷盘。
为
1
时, 只要事务提交成功,redo log
记录就一定在硬盘里,不会有任何数据丢失。如果事务执行期间
MySQL
挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。值2:在每个事务提交时,InnoDB立即将缓存中的redo日志回写到文件缓存系统(OS Cache),但后台线程刷盘。
为
2
时, 只要事务提交成功,redo log buffer
中的内容只写入文件系统缓存(page cache
)。如果仅仅只是
MySQL
挂了不会有任何数据丢失,但是宕机可能会有1
秒数据的丢失。
执行SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'
命令可知,mysql默认值为1
总结如下:
1 | 默认设置为1,这是为了保证数据的持久性。当然,我们可以将这个配置项设置为1以外的值来换取更高的性能,但是在系统崩溃的时候,将会丢失1秒的数据。 |
3、relo log两阶段提交
流程
a、执行器先从引擎中找到数据,如果在内存中直接返回,如果不在内存中,查询后返回
b、执行器拿到数据之后会先修改数据,然后调用引擎接口重新写入数据
c、引擎将数据更新到内存,同时写数据到redo中,此时处于prepare
阶段,并通知执行器执行完成,随时可以操作
d、执行器生成这个操作的bin log
f、执行器调用引擎的事务提交接口,引擎把刚刚写完的redo改成commit
状态,更新完成
问题
先写redo log后写bin log
1 | 假设在redo log写完,bin log还没写完的时候,mysql进程异常启动。 |
先写bin log后写redo log
1 | 如果在bin log写完之后creash,由于redo log还没完,崩溃恢复以后这个事务无效,所以这样记录的值没有恢复。 |
4、bin log和redo log区别
1 | redo log是属于innoDB层面; |
5、undo log(回滚日志)
1 | 备份数据,实现事务的回滚操作保证原子性,当出现错误或者执行rollback语句,系统利用undo log中的备份数据恢复到事务开始之前的状态 |
三、MVCC
1 | MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。 |
当前读
1 | 像select lock in share mode(共享锁), select...for update; update, insert ,delete(排他锁)这些操作都是一种当前读; |
快照读
1 | 像不加锁的select操作就是快照读,即不加锁的非阻塞读; |
当前读、快照读、MVCC关系
1 | MVCC多版本并发控制指的是维持一个数据的多个版本,使得读写操作没有冲突,快照读是MySQL为实现MVCC的一个非阻塞读功能。 |
3个隐式字段
1 | DB_TRX_ID:6byte,最近修改事务ID,记录最后一次操作(首次创建、最后一个更新)这条记录的事务id |
read view
概念
Read View是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。
其实Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据
Read View遵循的可见性算法主要是将要被修改的数据的最新记录中的DB_TRX_ID(当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DB_TRX_ID跟Read View的属性做了比较,不符合可见性,那么就通过DB_ROLL_PTR回滚指针去取出undolog中的DB_TRX_ID做比较,即遍历链表中的DB_TRX_ID,直到找到满足条件的DB_TRX_ID,这个DB_TRX_ID所在的旧记录就是当前事务能看到的最新老版本数据。
read view生成时机
1 | RC-读已提交:每次读取数据前,都生成read view |
在RR(可重复读)级别下的某个事务的对某条记录的第一次快照读会创建一个快照即Read View,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View;只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见
在RR(可重复读)级别下,快照读生成Read View时,Read View会记录此时所有其他活动和事务的快照,这些事务的修改对于当前事务都是不可见的,而早于Read View创建的事务所做的修改均是可见
在RC(可重复读)级别下,事务中,每次快照读都会新生成一个快照和Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
总结:在RC(可重复读)隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR(可重复读)隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View.
Read View的全局属性
trx_list
:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
up_limit_id
:记录trx_list列表中事务ID最小的ID,以下用min_trx_id 来表示方便理解
low_limit_id
:Read View生成时刻系统尚未分配的下一个事务ID,以下用max_trx_id来表示方便理解
create_trx_id
:当前事务id
数据可见性判断
情况一:DB_TRX_ID
与create_trx_id
相等
- 表明该数据是当前事务修改的记录
当出现数据事务id不等于当前事务id时,根据undo log中的数据版本链往上追溯进行判断
情况二:DB_TRX_ID
<min_trx_id
- 当前
DB_TRX_ID
(记录的事务id)小于min_trx_id
(最小活跃事务id),这表明这条记录在其他活跃事务事前已经提交,所以数据对于当前事务可见
情况三:DB_TRX_ID
>= max_trx_id
- 当前DB_TRX_ID (记录的事务id)大于
max_trx_id
(未分配事务id),这表明在当前事务开启之后又有新的事务开启,并且修改提交了该数据,所以数据对于当前事务不可见
情况四:min_trx_id
<= DB_TRX_ID
< max_trx_id
,数据在两者之间需要分情况分析
在区间内,
DB_TRX_ID
也在trx_list
中,那么当前事务不能读取。这表明该行数据是与当前事务一起开启的事务提交后生成的数据,所以数据对当前事务不可见在区间内,但是
DB_TRX_ID
不在trx_list
中,在区间内表明之前存在多个开启事务,不在DB_TRX_ID
表明在之前的多个热点事务中,id处于中间的某个事务提交,所以当前的事务读取的行数据为之前提交的事务数据,所以数据对于当前事务可见总得来说就是判断
DB_TRX_ID
是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,未修改的数据,当前事务也是看不到;如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。
purge与read view
为了节省磁盘空间,innodb有专门的purge
线程来清理delete_bit为true的记录。为了不影响mvcc的正常工作,purge
线程自己维护一个read view;如果某个记录的delete_bit为true,同时它的DB_TRX_ID(事务id)存在于purge
线程的read view中,那么该记录可以被安全删除