在 MySQL 中,事务支持是在引擎层实现的。MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
1.1 四大特性
原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如 A 向 B 转账,不可能 A 扣了钱,B 却没收到。 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如 A 正在从一张银行卡中取钱,在 A 取钱的过程结束前,B 不能向这张卡转账。 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。 1.2 隔离级别
SQL 事务的四大特性中原子性、一致性、持久性都比较好理解。但事务的隔离级别确实比较难的,今天主要聊聊 MySQL 事务的隔离性。
脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据 不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致。 幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。 SQL 不同的事务隔离级别能解决的并发问题也不一样,如下表所示:只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。
CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `age` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
假设现在,我要同时启动两个食物,一个事务 A 查询 id = 2 的学生的 age,一个事务 B 更新 id = 2 的学生的 age。流程如下,在四种隔离级别下的 X1、X2、X3 的值分别是怎样的呢?
读未提交:X1 的值是 23,因为事务 B 虽然没提交但它的更改已被 A 看到。(如果 B 后面又回滚了 X1 的值就是脏的)。X2、X3 的值也是 23,这无可厚非。 读已提交:X1 的值是 22,因为 B 虽然改了,但 A 看不到。(如果 B 后面回滚了,X1 的值不变,解决了脏读),X2、X3 的值是 23,没毛病,B 提交了,A 才能看到。 可重复读:X1、X2 都是 22,A 开启的时刻值是 22,那么在 A 的整个过程中,它的值都是 22。(不管 B 在这期间怎么修改,只要 A 还没提交,都是看不见的,解决了不可重复读),而 X3 的值是 23,因为 A 提交了,能看到 B 修改的值了。 串行化:B 在执行更改期间会被锁住,直至 A 提交。B 才能继续执行。(A 在读期间,B 不能写。得保证此时数据是最新的。解决了幻读)所以 X1、X2 都是 22,而最后的 X3 在 B 提交之后执行,它的值就是 23。 那为什么会出现这样的结果呢?事务隔离级别到底是怎么实现的呢?
2.3.2.2 select 当前读 除了更新语句,查询语句如果加锁也是当前读。如果把事务 A 的查询语句 select age from t where id = 2 改一下,加上锁(lock in mode 或者 for update),也都可以得到当前版本 4 返回的 age = 24
下面就是加了锁的 select 语句:
select age from t where id = 2 lock in mode; select age from t where id = 2 for update; 2.3.2.3 事务 C 不马上提交 假设事务 C 不马上提交,但是 age = 23 版本已生成。事务 B 的更新将会怎么走呢?
事务 C 还没提交,写锁还没释放,但是事务 B 的更新必须要当前读且必须加锁。所以事务 B 就阻塞了,必须等到事务 C 提交,释放锁才能继续当前的读。
注意:在上图的表格中用于启动事务的是 start transaction with consistent snapshot 命令,它会创建一个持续整个事务的视图。所以,在 RC 级别下,这命令其实不起作用。等效于普通的 start transaction(在执行 sql 语句之前才算是启动了事务)。所以,事务 B 的更新其实是在事务 C 之后的,它还没真正启动事务,而 C 已提交。
现在假设:
事务 A 开始前,只有一个活跃的事务,ID = 2, 已提交的事务也就是插入数据的事务 ID = 1 事务 A、B、C 的事务 ID 分别是 3、4、5 在这种隔离级别下,他们创建视图的时刻如下:
根据上图得,事务 A 的视图数组是[2,3,4],但它的高水位是 6或者更大(已创建事务 ID + 1);事务 B 的视图数组是 [2,4];事务 C 的视图数组是 [2,5]。分析一波: