MySQL事务隔离
Ⅰ.四种隔离级别
1.读未提交
一个事务还没提交时,它做的变更就能被别的事务看到。
2.读已提交
一个事务提交之后,它做的变更才会被其他事务看到。
3.可重复读
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。
当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
4.串行化
顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
Ⅱ.隔离机制分析
读未提交
直接返回记录上的最新值
读已提交
每个SQL语句执行时会创建一个视图,访问时以视图的逻辑为准
可重复读
在事务启动时创建一个视图,整个事务存在期间都使用这个视图中的数据
PS:可重复读的状态下,数据库中同一行势必会存在不同的版本,也被称作数据库多版本并发控制(MVCC),视图通过回滚日志和当前值来得到指定版本的数据。
串行化
直接用加锁的形式来避免并发访问
Ⅲ.一些问题以及解决方法
Oracle迁移到MySQL
Oracle数据库的默认隔离级别其实就是“读提交”,因此对于一些从Oracle迁移到MySQL的应用,为保证数据库隔离级别的一致,你一定要记得将MySQL的隔离级别设置为“读提交”。
配置的方式是,将启动参数transaction-isolation
的值设置成READ-COMMITTED
。你可以用show variables
来查看当前的值。
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
回滚日志体积过大
回滚日志删除的条件:
当没有事务需要用到这些回滚日志时(系统里没有比这个回滚日志更早的read-view视图),回滚日志将被删除。
基于上面的规则,建议尽量不要使用长事务。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。同时,长事务还占用锁资源,也可能拖垮整个库。
在MySQL 5.5及以前的版本,回滚日志是跟数据字典一起放在ibdata文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。
事务的启动方式
事务启动方式有以下几种:
-
set autocommit=1
显式启动事务语句,begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。 -
set autocommit=0
,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行commit 或 rollback 语句,或者断开连接。
PS:为了避免长连接,建议总是使用set autocommit=1, 通过显式语句的方式来启动事务。
对于频繁使用事务的业务,可以使用
commit work and chain
提交事务并自动开启下一个事务。
这样会省去后面事务执行begin语句的开销。
查找长事务
你可以在information_schema
库的innodb_trx
这个表中查询长事务。
#用于查找持续时间超过60s的事务
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
Ⅳ.事务隔离的底层实现
一些概念说明
事务开始 :begin/start transaction
只是表示事务开始,并不代表事务启动。
事务启动 :执行事务中第一个读/写语句,或者使用 start transaction with consistent snapshot
命令。
transaction_id :事务ID。每个事务都有一个唯一的事务ID,它是事务开始时向InnoDB的事务系统申请的,是严格按照申请顺序递增的。
row trx_id :数据行的版本号,一行有多个row trx_id
分别对应多个版本。事务在更新数据时,会生成一个新的数据版本,并把自己的transaction_id
赋值给新数据版本的row trx_id
。
更新数据示意图:
PS :U1、U2、U3是 undo log
回滚日志。
V1、V2、V3并不是真实存在的,是通过最新版本和undo log
日志恢复而来的版本。
可重复读快照的底层实现
InnoDB会在每个事务启动的时候,为该事务构造一个数组,用来保存这个事务启动瞬间,当前启动了但还没提交的事务ID
注意这里数组保存的是启动事务的ID,不是开始事务的ID
数组里面 事务ID的最小值
记为低水位,当前系统里面已经创建过的 事务ID的最大值
加1记为高水位。
由于事务开始时申请事务ID,事务启动时构造数组,中间有一定的时间差,所以当前事务的ID只能在低水位和高水位之间
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。
这个视图数组把所有的row trx_id 分成了几种不同的情况:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
- 如果落在黄色部分,那就包括两种情况
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见
读已提交快照的底层实现
读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
事务的更新逻辑
上面说了可重复读级别下,事务的数据可以互不影响。但是当事务中有更新(update)操作时,规则会有一定变化。
事务更新必须在最新的版本更新,更新需要先读到最新的数据,然后写数据。
更新前的读数据叫做“当前读” ,当前读要获取的是写锁(行锁),如果要读取的行上已经有写锁,那么当前读这个操作会进入锁等待。
#除了事务的更新会当前读以外,也可以手动 "当前读"
select k from t where id=1 lock in share mode;# 加了读锁(S锁,共享锁)
select k from t where id=1 for update; # 加写锁(X锁,排他锁)
事务更新实例