高性能 MySQL 笔记之一 事务


最近在抽时间看《高性能 MySQL》这本书,顺便做下笔记。


Mysql 事务介绍

事务:一组原子性sql操作。
特点:要么全执行,要么全部执行失败。
一个良好的事务所要具备四个标准的特性:
ACID 特性:
原子性(atomicity): 一个事物必须是不可分割的最小单元, 对于一个事务来说,要么全部执行成功,要么全部执行失败,不可能只执行其中一部分。这就是事务的原子性。
一致性(consistency):事务总是从一个一致的状态转到另一个一致的状态,不可能只改变其中一部分,也还是必须全部一起改变状态。
隔离性(isolation):通常来说, 一个事务在执行时对其他事务是不可见的,也就是说是一个独立的存在。具体特殊情况特殊分析,具体参考其隔离级别。
持久性(durability):一旦事务提交,数据将会永久保存到数据库中。

ACID 说起来容易,但是真的支持这一特性,数据库需要很复杂的实现,相比于不支持 ACID的数据库要耗费更多的资源。如果是不需要事务类的应用场景,选用不支持事务的数据库可以获得更高的性能。

隔离级别

四种隔离级别:

READ UNCOMMITED 未提交读

事务中的修改,在没有提交之前可以被读,这样容易造成脏读,简单来说,脏读就是,在一个事务请求数据库,但是还没提交的时候,另一个请求用了此数据,这样就造成了脏读。通常,未提交读会造成很多问题,而且并不会提高太多性能,所以一般不推荐使用此隔离级别。

READ COMMITTED 提交读

MySQL 是开启自动提交的,如果要测试可以关闭自动提交,手动 commit。
show variables like ‘autocommit’
set autocommit = 0

一个事务在提交之前对其它事务不可见,也就是说一个事务执行时候只对已经提交的数据进行操作。这一级别也叫做 不重复读,也就是说:两次执行同样的操作,读取的数据可能是不同的。

举例: A 客户端操作

1
select value from table where id = 1

得到结果 1
这时我们 A 客户端并未提交

这时候如果我们用 B 客户端 修改 id=1的值并且提交:

1
2
update table set value=10 where id = 1
commit;

这时候再用 A 客户端 取 id =1 的值 得到的会是10。
这就是不可重复读带来的问题,两次同样的 sql 读取结果可能会出现不一致的情况。

REPEATABLE READ 可重复读

可重复读避免了不可重复读的问题,也避免了脏读问题,但是同时也带来了新的问题:幻读。
幻读:幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行。


mysql默认的隔离级别就是 REPEATABLE READ 可重复读

SERIALIZABLE 可串行化

SERIALIABLE是最高的隔离级别,通过强制事务串执行,避免了幻读问题。简单来说,SERIALIABLE 会在每一行数据上加锁,所以可能导致大量的超时和锁征用问题,实际中很少用到此级别,只有对数据一致性要求极高的情况下才会使用。

隔离级别示意图

隔离级别 脏读可能性 不可重复读可能性 幻读可能性 加锁读
READ UNCOMMITTED ✔
READ COMMITTED
REPEATABLE READ ✘
SERIALIZALBE

死锁

死锁是指两个或多个事务在同一资源上互相占用,并请求锁定对方所占用的资源,从而导致恶性循环的情况。死锁发生时候要完全或者部分回滚其中一个事务才能打破死锁。
死锁就是两个人迎面过独木桥,只有其中一个人返回才能打破僵局。

关于锁,在 mysql 中有基本的共享锁(读锁)和排它锁(写锁),当然你也可以手动加锁,但是并不推荐,除非你关闭了自动提交。如果不关闭自动提交加锁会导致事务和锁造成@#¥%……&,会发生啥我也不知道,总之不可预知,不要尝试。

事务日志

事务日志可以帮助提高事务的效率,使用事务日志,存储引擎在修改表的数据时候只需要修改其内存拷贝,再把修改记录持久到硬盘日志中,不用每次将数据直接持久化到磁盘,事务日志采用追加的方式,顺序 I/O,所以效率很高。事务日志可以在后台慢慢刷回磁盘中,所以采用事务日志效率相对较高。目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志,修改数据需要写两次磁盘。
如果数据的修改已经持久化到硬盘,但是还没写到磁盘。若中途发生意外重启,恢复后硬盘数据会恢复,具体恢复机制视搜索引擎而定。

MVCC 多版本并发控制

我们看到了简单的行级锁存在诸多限制,大多数的搜索引擎实现的都不是简单的行级锁,基于提高并发性能考虑,他们一般都实现了MVCC 即多版本并发控制。因为 MVCC 并没有一个标准,所以不同的数据库的实现方式也不同。
MVCC 是通过保存某一个时刻的快照来实现的,也就是说,不同时间点执行的同样操作,结果可能是不同的。
以 InnoDB 为例,InnoDB 的 MVCC 通过保存后面两个隐藏列:开始时间(开始版本号),结束时间(结束版本号)。但是并不是存的真的时间,而是自增的版本号。

SELECT
a.查找版本早于当前事务版本号数据行,这样可以保证数据是之前插入过的或者是事务本身插入的。
b.行的删除版本号要么晚于当前版本号,要么未定义,这样可以确保读取到的数据在事务开始之前未被删除。
INSERT
为新插入的每一行保存当前系统号作为行版本号。
DELETE
为删除的每一行保存当前系统版本号作为删除标志。
UPDATE
保存当前系统版本号为版本号,同时保存当前系统版本号到原来的行作为删除标志。

这两个版本号,可以使大多数操作不用加锁,操作简单,提高了性能,不足之处是每行记录都需要额外的空间,需要更多的行检查工作。
MVCC 只在REPEATABLE 和 READ COMMITTED 两个隔离级别下工作。其他两个隔离级别和 MVCC 都不兼容。因为 READ UNCOMMITTED 总是读取到最新的数据行而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。