事务特性(ACID)
- 原子性 Atomicity。每个事务中的操作,要么都成功,要么都失败
- 一致性 Consistency。事务执行前后,数据库中的数据应该保持一致
- 隔离性 Isolation。事务之间应该是隔离的,事务之间互不影响、干扰
- 持久性 Durability。事务一旦提交,便会将修改持久化到数据库
数据库事务隔离级别
- 读未提交:read uncommitted
- 读已提交:read committed
- 可重复读:repeatable read
- 串行化:serializable
事务带来的问题
脏读:读取了其他事务未提交的数据
举例:当前事务隔离级别read uncommitted
,开启两个事务A、B。
ID为1的ACCOUNT为100.
//事务A
start transaction;
update salary SET ACCOUNT = 300 WHERE ID = 1;
SELECT SLEEP(10);
ROLLBACK;
SELECT * FROM salary;
//事务B
start transaction;
SELECT * FROM salary;
COMMIT;
事务B读取的ACCOUNT为300,实际这是个A事务产生的脏数据。
由此可见,read uncommitted
会出现脏读。
不可重复读:同一事务内多次读取同一数据出现不一致
举例:当前事务隔离级别read committed
,开启两个事务A、B。
//事务A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;
//事务B
start transaction;
SELECT * FROM salary;
update salary SET ACCOUNT = 300 WHERE ID = 1;
COMMIT;
事务A两次查询的结果不一致,第一次ACCOUNT为100,第二次为300.
那read committed
是否会出现脏读的情况呢,执行前面示例中的语句,会发现事务B读取的还是100,没有读到A产生的脏数据。
综上,read committed
可以解决脏读,但是不能解决不可重复读。
幻读:像是读取了不存在的数据
举例:当前事务隔离级别repeatable read
,开启两个事务A、B
//事务A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;
//事务B
start transaction;
DELETE FROM salary WHERE ID = 1;
COMMIT;
事务A两次查询的结果一致,都有ID为1的数据,说明repeatable read
解决了不可重复读。但是实际上事务B中把ID为1的数据已经删除并提交了,事务A第二次查询仍然能看到这个实际不存在的数据。
如果此时事务A在开启事务后,并没有立即查询,即事务B的操作在事务A的第一次查询前完成
//事务A
start transaction;
SELECT SLEEP(10);
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;
此时两次查询的结果和事务B执行后的结果一致,没有出现幻读。repeatable read采用快照读解决不可重复读的问题,即事务中第一次查询时读取一个快照,而不是开启事务就读取快照,所以看到两次执行结果会有不同。
如果两个事务同时插入一条ID为2的数据
//事务A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
INSERT INTO salary (ID,NAME,ACCOUNT) VALUES (2, 'lisi',200);
COMMIT;
//事务B
start transaction;
INSERT INTO salary (ID,NAME,ACCOUNT) VALUES (2, 'lisi',200);
COMMIT;
事务B插入成功,事务A查询的时候没有ID为2的数据,插入数据的时候提示重复主键。此时就是产生了幻读。
不可重复读VS幻读
-
不可重复读:针对已存在数据的多次读取结果不一致,update产生的影响
-
幻读:针对数据量的前后不一致,有新增和删除数据,insert和delete产生的影响
锁
Record Lock
作用于索引上的锁,而不是行记录本身。
Gap Lock
作用于索引之间的锁,不锁索引本身。
Next-Key Lock
repeatable read默认采用的锁。Record Lock+record之前的Gap lock。
locking read:当前读
select xxx for update
select xxx lock in share mode
locking read、update、delete操作时
- 当检索条件为非唯一索引时,需要获取next-key锁或gap锁
- 当检索条件为主键或唯一索引,获取record锁
- 当检索条件没有任何索引,全表扫描
关于next-key、gap的详细分析见理解innodb的锁、mysql锁详解
读锁只能和读锁并行,读锁和写锁、写锁和写锁之间不能并行。比如两个请求可以同时读数据,但是不能一个读一个写或者两个都写。
MVCC
MVCC(Multi-Version Concurrency Control),多版本并发控制。通过版本控制消除一些不必要的加锁操作,实现不加锁也可以读写并行,提供了并发能力。
InnoDB的MVCC是通过在每行记录的后面保存两个隐藏的列来实现的,一个保存了行创建时间,一个保存了过期时间。其中保存的并不是实际时间,而是系统版本号。每开始一个新事务,系统版本号递增。
MVCC只在read committed和repeatable read下工作。read uncommitted总是读取最新行,serializable则是对所有读取行加锁。
repeatable read下的MVCC:
- SELECT
1、InnoDB只查找版本早于当前事务版本的数据行(行的系统版本号小于等于当前事务的系统版本号)。确保当前事务读取的行都是在事务开始前已提交的,或者是事务本身修改的。
2、行的删除版本号要么未定义,要么大于当前事务的版本号。确保事务读取的数据都是在事务开始时存在的数据。
以上两个条件均满足,才能作为查询结果返回。
- INSERT
InnoDB新插入一行时,将当前系统版本号作为行版本号,删除版本号此时为未定义。
- DELETE
InnoDB删除一行时,将当前系统版本号作为该行的删除版本号。
- UPDATE
InnoDB插入一行数据,保存当前系统版本号作为行版本号,同时将当前系统版本号保存到原来行的删除版本号。
ReadView
所有开启的事务都会被维护在一个事务链表中,当事务结束后,从事务链表中删除。
ReadView就是当开启事务时,对当前事务链表的一个快照。其中包含了4类信息:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
- creator_trx_id:表示生成该ReadView的事务的事务id。
比如事务链中有1、2、3,3提交了,那么此时一个新事务生成ReadView,m_ids就是1、2,min_trx_id是1,max_trx_id是4,creator_trx_id是4.
根据ReadView就可以判断当前事务可以看到哪些数据:
- 如果某行的版本号小于min_trx_id,表示ReadView创建时,该行已经被提交,可以被访问。
- 如果某行的版本号大于max_trx_id,表示ReadView创建时,修改该行数据的事务还未创建,不可被访问。
- 如果某行的版本号大于min_trx_id并小于max_trx_id,则看m_ids是否存在该版本号,如果存在(如上面的事务2),表示ReadView创建时,事务还未提交,不可访问;如果不存在(如上面的事务3),表示ReadView创建时,事务已提交,可以被访问。
- 如果某行的版本号等于creator_trx_id,表示是当前事务自己修改的数据,可以被访问。
read committed在每次读取数据时生成ReadView
。这也就是read committed不可重复读的原因,
repeatable read在开启事务后的第一次读取时生成ReadView
。所以上面的例子中开启事务后立即读和不立即读结果会不一样,就是因为两次生成的ReadView不一样。由于只生成一次ReadView,所以可以实现重复读。
Redo log和Undo log
事务隔离性由锁来实现,原子性和、一致性和持久性由redo log和undo log保证。
Redo log
redo log记录的是物理上的数据页变化,而不是逻辑上的操作。
Redo log又可以分为两部分:内存中的日志缓冲(redo log buffer)和磁盘中的重做日志(redo log file)。
关系如图
- 从磁盘中读取数据到内存拷贝
- 修改数据的内存拷贝
- 内存中记录redo log
- 提交事务
- 将内存中的redo log持久化,写入到磁盘中
- 后台线程同步,非实时的
内存中的修改是先修改数据,再生成redo log,而持久化时相反,先持久化redo log,再持久化数据。为了性能,不可能每次数据修改完成后都实时持久化数据,所以采用了异步的方式。那么当这个同步的过程中如果宕机了,由于先持久化了redo log,就可以根据redo log恢复数据。
Undo log
undo log记录逻辑上的修改,比如一个insert操作生成一个delete的undo log,便于进行回滚数据,所以在修改数据前就要生成undo log。
redo log和undo log的生成过程:
假设有A、B两个数据,值分别为1,2.
- 事务开始
- 记录A=1到undo log
- 修改A=3
- 记录A=3到 redo log
- 记录B=2到 undo log
- 修改B=4
- 记录B=4到redo log
- 将redo log写入磁盘
- 事务提交
注意:undo log不是redo log的逆向,redo log注重数据上的修改,undo log注重逻辑上的修改。
参考
- 《高性能Mysql》
- MySQL事务
- 浅析MySQL事务中的redo与undo
- mysql版本链和readView原理