mysql事务
事务就是要保证一组数据库操作,要么全部成功,要么全部失败。
在 MySQL 中,事务支持是在引擎层实现的。
MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
事务的四大属性ACID
ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)
隔离性与隔离级别
当数据库上有多个事务同时执行的时候,就可能出现以下问题:
- 脏读(dirty read)
- 不可重复读(non-repeatable read)
- 幻读(phantom read)
为了解决这些问题,就有了“隔离级别”的概念。
SQL标准的事务隔离级别包括:
- 读未提交(read uncommitted):
一个事务还没提交时,它做的变更就能被别的事务看到。 - 读已提交(read committed):
一个事务提交之后,它做的变更才会被其他事务看到。 - 可重复读(repeatable read):
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。 - 串行化(serializable ):
顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
测试案例
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
下面是一个模拟两个事务的过程:
下面分析一下不同隔离级别下出现的情况:
- 若隔离级别是读未提交,则V1结果是2,V2,V3也都是2。
- 若隔离级别是读已提交,则V1结果是1,V2,V3都是2。
- 若隔离级别是可重复读,则V1,V2结果是1,V3是2,之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的
- 若隔离级别是串行化,则在事务 B 执行“将 1 改成 2”的时候,会被锁住,直到事务 A 提交后,事务 B 才可以继续执行,所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
TIPS:
RC(读已提交)级别下,MVCC视图会在每一个语句前创建一个,所以在RC级别下,一个事务是可以看到另外一个事务已经提交的内容,因为它在每一次查询之前都会重新给予最新的数据创建一个新的MVCC视图。 RR(可重复读)级别下,MVCC视图实在开始事务的时候就创建好了,这个视图会一直使用,直到该事务结束。 这里要注意不同的隔离级别他们的一致性事务视图创建的时间点是不同的。 RU(读未提交):没有视图的概念,直接返回最小行数据。
RC(读已提交):在每一行SQL语句执行的时候创建。
RR(可重复读):在事务启动的时候创建。
Serial(串行化):通过锁来实现数据访问,没有视图的概念。
在不同的隔离级别下,数据库行为是有所不同的,Oracle 数据库的默认隔离级别其实就是“读提交”,因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为“读提交”。
配置的方式是,将启动参数 transaction-isolation 的值设置成
READ-COMMITTED
。你可以用 show variables 来查看当前的值。
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)
-- 或者
mysql> show variables like '%isolation%'
-> ;
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| tx_isolation | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec)
注意:mysql默认的事务处理级别是’REPEATABLE-READ’,而Oracle和SQL Server是READ_COMMITED
设置会话的隔离级别,隔离级别由低到高设置依次为:
set session transacton isolation level read uncommitted;
set session transacton isolation level read committed;
set session transacton isolation level repeatable read;
set session transacton isolation level serializable;
设置当前系统的隔离级别,隔离级别由低到高设置依次为:
set global transacton isolation level read uncommitted;
set global transacton isolation level read committed;
set global transacton isolation level repeatable read;
set global transacton isolation level serializable;
每种隔离级别都有自己的使用场景,当你查看数据时不希望受到现在正在发生的数据变化对你的影响,这时候使用“可重复读”隔离级别就很方便。
事务隔离的实现
在Mysql中实际上每条记录在更新时都会同时记录一条回滚操作,记录上的最新值通过回滚操作都可以得到前一个状态的值。
下面重点说明可重复读。
假设一个值从1被按顺序改为2,3,4。那么在回滚日志里面就会有类似的下面的记录。
当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view,在视图A、 B、 C里面, 这一个记录的值分别是1、 2、 4,同一条记录在系统中可以存在多个版本, 就是数据库的多版本并发控制(MVCC), 对于read-view A, 要得到1, 就必须将当前值依次执行图中所有的回滚操作得到,即使现在有另外一个事务正在将4改成5, 这个事务跟read-view A、 B、 C对应的事务是不会冲突的。
当系统判断没有事务再需要用到这些回滚日志时,就会删除这些回滚日志,就是当系统里没有比这个回滚日志更早的read-view的时候。
建议尽量不要使用长事务,因为长事务意味着在事务提交之前,就会保存很久的回滚日志,会导致占用大量存储空间。
在MySQL 5.5及以前的版本,回滚日志是跟随着数据字典一起放在ibdata文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小,除此之外,长事务还占用锁资源。
事务的启动方式
-
显式启动
begin 或者 start transaction,配套的提交语句时 commit,回滚语句时rollback。 -
set autocommit=0,该命令会将这个线程的自动提交关掉,意味着只要执行一个select语句这个事务就启动了,而且不会自动提交,该事务会持续存在,直到你主动执行了commit 、rollback语句或者断开连接。
注意,有的客户端框架会默认连接成功后先执行一个set autocommit=0的命令,这样就导致了长事务。
对于一个频繁使用事务的业务,建议使用commit work and chain
语法。
在autocommit为1的情况下, 用begin显式启动的事务, 如果执行commit则提交事务。 如果执行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
总结
这篇文章介绍了MySQL的事务隔离级别的现象和实现, 根据实现原理分析了长事务存在的风险, 以及如何用正确的方式避免长事务,希望能够帮助理解事务,并更好地使用MySQL的事务特性。
2020-12-24 10:06:45补充事务保留点的概念: