事务
事务的概念是:逻辑上的一组操作,这组操作在执行后,要不全部成功,要不全部不成功。
在MySQL数据库中,只有InnoDB引擎支持事务,且只有DML语言才有事务。
事务特性
事务要保证操作的一系列单元要不全部成功,要不全部不成功,且多个事务并行的时候,需要保证幂等性,不能存在不确定的结果。所以一个事务需要一定特性,即ACID。
原子性(Atomicity,A)
事务中的操作要不全执行,要不全部不执行,即回滚部分操作。
一致性(Consistency,C)
几个并行执行的事务,其执行结果必是按某一顺序串行执行的结果一致。
隔离性(Isolation,I)
事务的执行不受其他事务的干扰。
永久性(Durability,D)
事务一旦提交,对数据库的改变应该是永久的。
MySQL数据库隔离级别
在MySQL中,难以避免的会出现多个事务并行执行的情况,因此可能会带来,脏读,不可重复读,幻读等问题,MySQL为了解决这些问题,使用了隔离级别,即:根据不同的隔离级别,将不同的事务隔离开。
不同的隔离级别解决了问题,同时使用到的优化手段也会带来一定的性能损耗,隔离级别根据性能损耗升序排列为读未提交,读已提交,可重复读,序列化读。
读未提交(Read Uncommited)
一个事务能读取到其他事务未提交的修改变更。
实现原理:直接返回的是记录的最新值。
读已提交(Read Commited)
一个事务能读取到其他事物提交后的修改变更。
实现原理:是在每个sql开始执行的时候创建一个视图。
可重复读(Repeatable Read)
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。
实现原理:是在事务启动的时候,第一条select语句执行的时候创建一个视图(需要注意的是,rr隔离级别下,uodate/delete等语句是当前读,即读最新的数据),整个事务期间都用这个视图。
序列化读(Serializable Read)
对同一行记录,写会加写锁,读会加读锁。当出现读写锁的冲突时,后访问的事务需要等前一个事务执行完成才能继续执行,即不同的事务严格的顺序序列化操作。
实现原理:使用加锁的方式避免并行访问。
视图
在MySQL中,视图分为两类,普通视图和一致性视图。
普通视图
普通视图是查询语句定义的虚拟表,在调用的时候执行查询语句并声明结果
一致性视图
用于MVCC的实现上,即在事务启动的时候,对整个数据库拍了个视图快照,即为一致性视图。是数据库RR隔离级别上的重要实现原理。
MySQL中创建一致性视图的时机为事务开始的时候,即为第一条select语句执行的时候,所以如果只有start traction;是不会创建一致性视图的。
MVCC(多版本并发控制)
MVCC为多版本并发控制,即数据库中不同事务对于同一行数据的更新,都会记录日志,包含对应的事务id等信息,以便于不同事务在生成该事务一致性视图的时候能快速回溯找到本事务可见的数据,这也是数据库的undo log。
如果数据库高并发访问的时候,势必会生成大量的回滚日志,mysql会在系统中没有比这个回滚日志更早的视图的时候进行删除,mysql会创建一个purge线程,定时将版本号最低的数据进行删除。
因此也不建议使用长事务,因为可能会存在很老的事务视图,占用太多的存储空间。
实现原理
多版本
不同的事务在对一条数据进行更新的时候,会记录哪些事务操作了这条数据,应该每次更新都生成了一个对应的版本,所以每条数据在数据库中都是存在多个版本的。一条数据在数据库中逻辑版本如下表所示:
版本1 | 版本2 | 版本3 |
---|---|---|
row_data:1, trx_id:10 | row_data:2, trx_id: 11 | row_data:3, trx_id: 12 |
同时在语句更新的时候会生成对应的undo log,即有反向箭头表明,事务12可以在事务11执行之后执行,可以回溯到事务11,事务11同理。
当然,这些不同的版本不是物理上真是存在的,不然数据库的数据存储量得翻N番了。
并发控制
以上的是对应着多个事务在对同一条数据进行更行的时候的记录,即在执行update语句的时候的记录,但是事务可能不只是包含一条语句,即在不同的隔离级别下,未commit的事务,不应该被其他事物所感知。
当一个事务在创建的瞬间,会保存当前所有未提交的事务,存入活跃数组,在计算当前视图数据的时候,需要根据隔离级别,判断该版本对应的事务id是否在活跃数组中,如果在,则继续回溯,最终找到那个版本的数据,根据undo log恢复出来,生成一致性视图。
根据上面的多版本+并发控制,InnoDB实现了秒级创建快照的能力。
MVCC实战
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
第一种事务执行情况
事务 1 | 事务 2 | 事务 3 |
---|---|---|
start transaction with consistent | ||
start transaction with consistent | ||
update t set k=k+1 where id = 1 | ||
select k from t where id = 1; | ||
select k from t where id =1; commit; | ||
commit; |
根据前面的mvcc原理,根据事务在数据库中真正执行的语句,事务1的事务id为1,1的活跃事务数组为[1],事物2的活跃事务数组为[1,2],事务3的活跃事务数组为[1,2,3],根据上述原理进行回溯,事务3对应的k为2,此时轮到事务2执行,根据mvcc回溯,trx_id:3不属于当前事务能看到的,进行回溯trx_id:2能看到,根据undo log找到此时的k为1,事务1同理。
所以上述事务执行情况为,事务1的k为1,事务2的k为1。
第二种事务执行情况
事务 1 | 事务 2 | 事务 3 |
---|---|---|
start transaction with consistent | ||
start transaction with consistent | ||
update t set k=k+1 where id = 1 | ||
update t set k=k+1 where id = 1; select k from t where id = 1; | ||
select k from t where id =1; commit; | ||
commit; |
该情况和上述情况不同的是,多了一条update语句,如果按照上述分析,事务2在更新的时候,读到的k应该是1,最终事务2读取的k是2,并且提交了,事务2的结果应该是最终结果,但是这种情况却违背了事务的ACID中的一致性(C),丢失了事务3的提交。MySQL为了解决这类问题,当每一行记录在更新的时候,都是使用的当前读。
事务在更新数据的时候,都是先读当前的值后在写的。
所以上述执行情况,事务2在生成一致性视图后,由于是update语句,回去读取一遍k当前数据库内的数据,所以最终事务2读取的的k值为3。
第三种事务执行情况
事务 1 | 事务 2 | 事务 3 |
---|---|---|
start transaction with consistent | ||
start transaction with consistent | ||
start transaction with consistent; update t set k=k+1 where id = 1 | ||
update t set k=k+1 where id = 1; select k from t where id = 1; | ||
commit; | ||
select k from t where id =1; commit; | ||
commit; |
MySQL不丢失任务事务的更新结果,但是事务的情况更新为上述情况怎么解呢。
数据库读锁(S锁,共享锁):一个事务加了S锁后,可以读取,但不能修改,其他事务职能在加S锁,不能加写锁,直至之前事务分别解锁;
写锁(X锁,拍他锁):事务添加了X锁,当前事务可以读也可以写,但是其他事物不能添加任何锁,必须等加了X锁的事务释放锁。
mysql在事务3的update语句执行的时候,加了写锁,根据两阶段锁协议,其他事务必须等待锁的释放才能加锁。
两阶段锁协议,分为加锁阶段和解锁阶段,所有的 lock 操作都在 unlock 操作之后。
读提交/可重复读
读提交是可以读到其他事务提交了的结果,可重复读是当前事务内,读取的时候都是同一份(排除当前读的情况),两者的区别为在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。