天津图书大厦,Hola Zumba Studio,中午休息,下午考试。培训的强度太大了,但好处就是你一眼就能区分出每个人的年龄段,喝一瓶水就能蹦一天的是 00 后,吃两口巧克力就恢复体力的是 95 后,90 年的我只能掏出保温杯躲在角落里瑟瑟发抖地吃煎饼果子。南楼煎饼也太好吃了叭!吃完码字!
学计算🐔的童鞋们都知道,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中事务支持是在引擎层实现的。还记得前面画的那张图吗(原谅我画的太糟糕,就不放了,前两篇笔记里都有),MySQL 是一个支持多引擎的系统,但并不是所有大引擎都支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的原因之一吧。
所以就以 InnoDB 为例,一起学习一下 MySQL 在事务支持方面的特定实现,并基于原理给出生产场景的实践建议。
ACID(Atomicity, Consisitency, Isolation, Durability),今天关注的是其中的 Isolation,隔离性。
当数据库上有多个事务同时执行时,就可能出现 dirty read 的情况,也就是所谓的脏读,而且也有不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就要引入隔离级别的概念。
当然,我们知道,隔离性越好,效率也就越低。所以通常要在两者之间找平衡。SQL 标准的事务隔离级别包括:
举一个栗子🌰,假设数据表 T 只有一列,其中一行的值为 1, 下面是按照时间顺序执行两个事务的行为。
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
timeline:
事务 A | 事务 B |
---|---|
启动事务 查询得到值 1 | 启动事务 |
查询得到值 1 | |
将 1 改成 2 | |
查询得到值 V1 | |
提交事务 B | |
查询得到值 V2 | |
提交事务 A | |
查询得到值 V3 |
我们看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,即 V1、V2、V3 的值都是什么。
它们都是怎么实现的呢?实际上,数据库里会创建一个视图,访问的时候以视图的逻辑结果为准。在”可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;“串行化”隔离级别下直接用加锁方式来避免并行访问。
所以在不同的隔离级别下,数据库行为是有所不同的,公司有些应用使用的是 Oracle 数据库,Oracle 默认隔离级别就是”读提交”,因此在去 IOE 时,这些应用从 Oracle 迁移到 MySQL,为保证数据隔离级别的一致,一定要将 MySQL 的隔离级别设置为”读提交”,或通过业务逻辑评估隔离级别是否对应用迁移有影响。公司另外相当一部分重要的应用跑在 DB2 数据库上,DB2 默认隔离级别为”游标稳定性”。
当使用游标稳定性隔离级别时,事务通过游标从表中检索行时,其他事务不能更新或删除游标所引用的行。但是,如果被锁定的行本身不是用索引访问的,那么其他事务可以将新的行添加到表中,以及对被游标锁定行前后的行进行更新和删除操作。所获取的锁一直有效,直到游标重定位或事务终止为止(如果游标重定位,原来行上的锁就被释放,并获得游标现在引用的行上的锁)
这个还有些细节不甚清楚,等研究明白了再填坑。
说回在 MySQL 中的配置方式,通过启动参数 transaction_isolation 进行设置。比如”读提交”,参数值设置为 READ-COMMITTED。
MySQL 中每条记录在更新时都会记录一条回滚操作,记录上的最新值通过回滚操作,都可以得到前一个状态的值。
在查询一条记录时,不同时刻启动的事务会有不同的 read-view,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制 MVCC。
回滚日志肯定也不是永远保留的,系统会判断当没有事务需要用到这些回滚日志时,回滚日志会被删除。也就是当系统里没有比这个回滚日志更早的 read-view 的时候。
所以有个问题,长事务为什么不建议用?长事务意味着系统里存在很老的事务视图,由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,无疑会导致空间被大量占用。
项目中应用开发人员一般也并不会故意使用长事务(能了解原理并故意使用的开发人员,我们一般也留不住…当然数据库开发人员除外),所以实际情况是误用的可能性会大一点。事务启动方式在 MySQL 是这样的:
有些客户端连接会默认连接成功后先执行一个 set autocommit = 0 的命令。这就导致接下来的查询都在事务中,如果是长连接就导致了意外的长事务。
所以可以总是通过显示语句 set autocommit = 1 来启动事务来避免。但缺点就是产生了”多一次交互”问题,因为在第 2 种方法中不用每次都主动执行 begin 了,减少了语句的交互次数。解决办法是使用 commit work and chain 的语法:用 begin 显示启动的事务,commit work and chain 提交事务并自动启动下一个事务,这样就省去的再次执行 begin 语句的开销,额外的好处是从程序开发的角度明确知道每个语句是否处于事务中。
在 information_schema 库的 innodb_trx 这个表中可以查询长事务,例如查询持续时间超过 100s 的事务。
mysql> select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>100
###