mysql系列往期文章:
事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在MySQL中,事务支持是在引擎层实现的。
提及事务想必大家并不陌生,在平时的开发中,打交道最多的就是数据库了,在操作数据库时,我们就经常用到事务。如果某次业务开发中因为你的没有把握好"事务的度",最直接的影响可能就是不该写的数据写的,要写的数据没写,或者干脆是数据写串了,你自认为没问题的代码在线上跑一段时间后,可能就是大量的客诉,给公司造成不可挽回的损失。或者从比较功利的角度说,MySQL事务隔离机制也是面试经常问的吧,所以我们很有必要了解一下MySQL事务的原理。
1.1 隔离性与隔离级别
ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)中的I
多个事务同时执行时,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,
"隔离级别"的出现就是为了解决这些问题的。
1.1.1 脏读、不可重复读、幻读
-
脏读:一个事务读取了另一个事务未提交的数据
A事务读取了B事务还没提交的数据,B事务因发生错误导致回滚,那么A事务读取的就是脏数据了。
-
不可重复读:一个事务读取同一数据,前后数据内容不一致
A事务前后两次读取同一事物间隔时间比较长,这段时间内B事务对这条数据做了更改操作,那前后读取的结果就不一样了。
-
幻读:一个事务内对数据总量进行读取,前后不一致
事务A中需要执行两次数据总量的统计,两次读取时间间隔内事务B执行了新增操作并提交了,这时候前后读取的数据总量不一样了,就像出现幻觉一样,平白无故多出了几条数据。
1.1.2 隔离级别
读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )
说到隔离级别,首先得知道,你隔离得越"严格",效率就会越低。很多时候,我们需要在之间寻找一个平衡点。
- 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。
- 读已提交:一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然未提交变更对其他事务也是不可见的。
- 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能(出现) | 可能 | 可能 |
读已提交 | 不可能(出现) | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
可串行化 | 不可能 | 不可能 | 不可能 |
下面举一个具体的实例,深刻理解一下这几种隔离级别。
定义表T,只有一列字段为c,其中一行的值为1:
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
下图是按照时间顺序两个事务中的行为,我们看看不同的隔离级别事务A的查询会有哪些不同的返回结果
- 若为读未提交,因为事务A可以读到事务B未提交变更记录的结果,所以V1,V2,V3的值都是2。
- 若为读已提交,事务B的更新操作要在提交后才能被A看见,所以V1=1,V2,V3=2。
- 若为可重复读,事务A就应该在事务从启动时一直到提交事务读取的结果都应该和启动时一致,所以V1,V2=1,V3=2。(事务在执行期间看到的数据前后必须是一致的)
- 若为串行化,事务B执行“将1改成2”的时候,与事务A出现了锁冲突,直到事务A提交后,事务B才可以继续执行。所以V1,V2=1,V3=2。
在隔离级别的实现上,数据库里会创建一个视图,访问的时候以视图的逻辑结果为准。
- 可重复读,视图是在事务启动时创建的,整个事务存在期间都用这个视图。
- 读已提交,视图是在每个SQL语句开始执行的时候创建的。
- 读未提交,直接返回记录上的最新值,没有视图概念。
- 串行化,直接用加锁的方式来避免并行访问。
从上面的分析可以看到,不同隔离级别下,数据库的行为是有区别的。
查看隔离级别方式:
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
一般大多数业务场景MySQL的隔离级别都设置为读已提交,但每种隔离级别都有存在的意义,具体得根据具体的业务场景分析,比如举一个可重复读的业务场景。
一个银行账户,一个表存了每个月月底的余额,一个表存了账单明细。你有个业务需求是校对当前余额与上个月余额差值是否能和流水对上,但在你比对过程中,用户可能不断产生交易,你如果不希望用户的交易影响你的校对结果,就可以使用可重复读。
1.2 事务隔离的实现
以可重复读为说明,在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作,类似于链表,通过回滚操作可以得到历史某个快照版本的值。
假设一个值从1被按顺序改成了2、3、4,回滚日志(update undo log)就会出现下面类似的记录:
在查询这个值时,不同时刻启动事务会有不同的read-view,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。
前面说过,可重复读会在事务创建时产生一个read view,这个read view里面维护着系统当前没有commit的活跃事务ID(系统中当前不应该被本事务
看到的其他事务id列表
),当进行快照读时会把这个列表作为条件判断当前事物能看到哪个版本的数据。
到这你肯定想问,回滚日志维护的版本链不可能无限长吧?会在什么时候删除呢?
其实回滚日志主要分两种:
-
insert undo log
代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
-
update undo log
事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要;
只有在快照读或事务回滚不涉及到该日志,才会被清除,换句话说,当前系统没有比这个回滚日志更早的read-view
1.3 事务的启动方式
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。
- set autocommit=0,关闭自动提交直到你主动执行commit 或 rollback 语句,或者断开连接。(不建议使用,如果是长连接,就可能导致意外的长事务)
你可以在information_schema库的innodb_trx这个表中查询长事务,如查询超过60秒的长事务:
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
1.4 为什么建议你尽量不要使用长事务?
- 长事务意味系统中会存在很老的事务视图,由于这些事务可能随时访问你数据库中任何数据,所以在事务提交前,数据库中可能用到的回滚段都必须保留,这会导致大量占用存储空间。
- MySQL5.5及以前版本,回滚日志是跟数据字典一起放在ibdata文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。(MySQL 5.5之前的版本在回滚操作时并不会立即释放空间,而是将被修改的数据记录标记为可重用。这样做的目的是为了提高性能,因为不需要在每次回滚操作时重新分配空间。然而,这也意味着即使回滚操作后文件大小没有变小,但这些空间实际上是可重用的)
- 长事务还占用锁资源,可能拖垮整个库。
平常我们如何去避免长事务呢?
-
确认是否使用了set autocommit=0。
-
开启mysql的general_log日志;
SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';
-
随便跑一个业务,查看日志,确定是否默认关闭了提交;
-
想办法去修改默认逻辑,一般框架会提供某些参数来控制这个行为。
-
-
确认是否有不必要的只读事务。
-
业务连接数据库的时候,根据业务本身的预估,通过SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。