事务

前置知识

并发事务处理带来的问题

  • 脏读(Dirty reads):一个事务处理一条数据,在这个事务还没有 commit 的时候,另一个事务来读取这个记录,并且用这个记录做一些工作。
  • 不可重复读(Non-repeatable Reads):一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了。举个通俗点的例子:事务A第一次查询得到一行记录 row1,事务 B 提交修改后,事务 A 第二次查询得到 row1,但列内容发生了变化(相当于读到了 B 提交修改后的内容)。
  • 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为 “幻读” 。

关于脏读你可能会想,我在 commit 的时候再落盘不就可以了吗(其实我就是这么想的(•̀ᴗ•́)و ̑̑)?

这个是不可以的,上一篇的图里有:prepare(redo log 落盘) -> binlog 落盘 -> commit。因为‘从’数据库是根据 binlog 来进行落盘的,若 binlog 落盘 -> prepare(redo log 落盘)-> commit,如果在 binlog 和 prepare 中间崩了,但是 redo log 没有这个记录,重启时自然不会去查 binlog 日志,导致‘主从’库不一致(当然我感觉也可以用 InnoDB 写一些日志 -> binlog 落盘 -> redolog 落盘 -> commit,我理解这么做是没有问题的,而且不会出现脏读,但是会强制多落一次盘,这么实现是不合理的,因为 InnoDB 实在引擎层,但是 binlog 在 server 层,且只有数据真正写入之后,binlog 才会落盘)。

这里有一篇大佬写的二阶段提交的文章(超详细),感兴趣的可以看一看:https://blog.csdn.net/staforn/article/details/80423137

数据库的事务隔离级别

  • 未提交读(Read Uncommitted):能够读取到其他事务还没 commit 的数据,可能出现脏读问题。
  • 已提交读(Read Committed):只能读到已经 commit 的数据,但是可能出现不可重复读的问题。
  • 可重复读(Repeated Read):一个事务中,同一个读操作在任意时刻中读到的结果都是一样的,可能出现幻读问题。
  • 串行化(Serializable):故名思意,串行执行可能会导致冲突的事务。这个事务会阻碍下个事务的执行。

放一张图来更直观的表示不同隔离级别可能出现的现象

img

查看数据库的隔离级别

1
show variables like '%isolation';

image-20200618044747078

可以看到我的电脑是可重复读,这里解释一下为什么有的机器的 Variable_name 是 transaction_isolation,因为 transaction_isolation 是在 my.cnf 中指定的,而 tx_isolation 在程序中指定的。可以参考这里:https://bugs.mysql.com/bug.php?id=59744

MVCC

MVCC 全称是多版本并发控制,它保留每一次的修改,并用版本号来追踪这些修改。

注意:未提交读和串行化没有 MVCC。

undo log

先说一下上一章没介绍到的 undo log:提供回滚和多个行版本控制(MVCC)。

undo log 删除时机:这个事务的 id 不用了,undo log 就会被删除。

实现原理

在 InnoDB 中,聚簇索引记录中有两个必要的隐藏列:trx_id & roll_pointer,trx_id 指的是这次事务的 id,roll_pointer 指向上次记录的 roll_pointer(undo log 中)。

注意插入操作的 undo 日志没有这个属性。

字段 1 字段 2 trx_id roll_pointer
1 12 50 null
2 12 60 指向上面的地址

现在来看看 readview,如果当前列表里的事务 id 是 [55-100]。

如果事务 id 是 50 之前的,那么这个记录找不到,但是会出现幻读问题(对 id 为 50 之前的不清楚,如有大佬了解,不吝赐教)。

如果事务 id 是 52 ,那么读到的是 id 为 50 的。

如果事务 id 是 55-100 之间的,那么需要判断这个事务是否被 commit,如果被 commit,读到的就是 60 这个版本,否则为 50 的版本。

如果事务 id > 100,那说明这个版本是在 ReadView 生成之后才发生的,所以不能被访问(不理解为什么会发生这种情况)。

这里说一下已提交读可重复读:已提交读是在每次执行 sql 的时候,将 id 置为现在的值,可重复读是在事务开始时,将事务置为现在的值,所有本事务中的操作都会用这个 id。

若某个时间点内某个小于 id 的所有事务执行完毕(说的有点绕,就是说,不会在有这个 id 和之前的 id),那么 undo log 就会删除这个日志。

快照读和当前读

  • 快照读:如果没有 crud 操作,都是读之前的快照,不会产生幻读。
  • 当前读:如果有 crud 操作后,此事务就要看别的东西(比如别的事务删除了这一行,但是此事务却修改这一行),并且会把自己的 id 记录到操作中,会产生幻读(解决办法使用 Next-Key)。

关于快照读和当前读可以参考这一篇文章:https://blog.csdn.net/sanyuesan0000/article/details/90235335#%E4%B8%89%E3%80%81%E5%B9%BB%E8%AF%BB

长事务

最后再说一下,不要使用长事务。但是为了测试可以启动set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

1
在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。

引用过的 blog

https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html#auto_id_12

https://www.cnblogs.com/csic716/p/12521481.html

https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc

https://blog.csdn.net/sanyuesan0000/article/details/90235335#%E4%B8%89%E3%80%81%E5%B9%BB%E8%AF%BB