03事务隔离:为什么你改了我还看不见?
事务:就是要保证一组数据库操作,要么全部成功,要么全部失败。在Mysql中,事务支持在引擎层中实现的。由此可见,Mysql支持多引擎,但是不是所有引擎都支持事务。本文以InnoDB为例子,剖析事务支持方面的特定实现。
隔离性与隔离级别
提到事务,会想到ACID(Atomicity原子性、Consistency一致性、Isolation隔离性、Durability持久性)。今天我们先看隔离性Isolation。
当数据库上有多个事务同时执行的时候,可能出现脏读(dirty read)、不可重复读(nonrepeatable read)、幻读(phantom read)等问题。
- 脏读(Dirty Read):读取到了未提交的数据(如果事务这时候回滚了,那么第二个事务就读到了脏数据)。
- 不可重复读(nonrepeatable read)同一个事务中,对于同一数据,执行完全相同的select语句时可能看到不一样的结果。
- 幻读(phantom read)当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。
幻读和不可重复读的区别
不可重复读
重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(一个事务多次读取同一范围内数据时,另外一个发生了事务发生insert操作并提交了)。数据变化。
幻读
重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(一个事务多次读取同一条数据时,另外一个发生了事务发生update,delete操作并提交了。insert操作一条新数据并不会影响刚才被读的那条数据。)。行数变化。
为了解决这些问题,就有了隔离级别的概念。所谓隔离,隔离的越严,效率就会越低,所以我们要找到一个balance。SQL标准的事务隔离级别有:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)。
- 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交:一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化:顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
举个例子来解释。假设表T中只有一列,其中一行的值为1,下面按照时间顺序执行两个事务的行为。
mysql>create table T(c int) engine=InnoDB;
insert into T(c) values(1);
来分析一下不同隔离级别下返回的结果有什么区别?即V1,V2,V3的返回值是什么?
- “读未提交”,则V1=2。这时事务B虽然还没有提交,但是结果已经被A看到了,所以V2、V3也是2。
- “读提交”,则V1=1,V2的值是2。事务B的更新在提交后才能被A看到,所以V3的值也是2。
- “可重复读”,则V1、V2的值是1,V3是2。事务在执行期间看到的数据必须前后保持一致。
- “串行化”,当事务A执行查询时,执行读锁;在事务B执行“将1改成2”时,会被锁住,直到事务A提交后,事务B才能继续执行,从A的角度来看,V1、V2的值是1,V3的值是2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- 在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图;可以认为事务启动时的视图是静态的,不受其他事务更新的影响。
- 在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。注意:“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念.
- “串行化”隔离级别下直接用加锁的方式来避免并行访问。
事务隔离的实现
这里展开说明“可重复读”。
在Mysql中,实际上每条记录在更新的同时会记录一条回滚操作,通过该操作可以得到前一个状态的值。
假设一个值1被按顺序改成了2、3、4,在回滚日志里就会有类似下图的记录。
当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于read-viewA,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。
同时你会发现,即使现在有另外一个事务正在将4改成5,这个事务跟read-viewA、B、C对应的事务是不会冲突的。
回滚日志什么时候删除呢? 系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。也就是当系统里没有比这个回滚日志更早的read-view的时候。
尽量不要使用长事务的原因? 长事务意味着系统中会存在很老的事务视图,在这个事务提交前,数据库里它可能用到的回滚记录都必须保留,这就占据了大量存储空间;此外,长事务还会占用锁资源。
事务的启动方式
- 显式启动事务语句,begin或start transaction。配套的提交语句是commit,回滚语句是rollback。
- set autocommit=0,这意味着你将自动提交关闭,如果你只执行一个select语句,此事务启动且不会自动提交。这个事务持续存在直到你主动执行commit或rollback语句,或者断开连接。
执行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