事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全部成功,要么全部失败。而ACID 是数据库管理系统为了保证事务的正确性而提出来的一个理论,ACID 包含四个约束:原子性(atomicity)、一致性(consistency)、隔离性(isolation)与持久性(durability)。
一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某个环节结束。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。也就是说事务中的所有操作是一个不可分割的整体,要么全部成功,要么全部失败。
一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。如A给B转账,不论转账的事务操作是否成功,其两者的存款总额不变。
数据库允许多个并发事务同时对数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
下面就以如下的例子说明不同的隔离级别的可见性
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
但是在可重复读场景下,如果是当前读情况又不一样,见如下表。
也就是说更新数据总是当前读,且更新之后的的一致性读总能读到最新的值。但是如果把上面示例中的update T set c=c+1 where c=1改成update T set c=2,虽然最终MySQL的返回结果也是修改的行数为0,但是随后的select c from T,得到的值确是2。为此更新后的一致性读,是否能读到最新的值,还得看MySQL的server层是否会真正的调用InnoDB的接口更新行。为此使用CAS锁重试时,一定要重新开启事务
其实除了更新数据的场景,如下的场景也是当前读
mysql> select c from T lock in share mode;
mysql> select c from T for update;
总计来说,可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
在INNODB中,每次开启一个事务的时候,就会在如下的时间点创建一个一致性视图:
在执行第一个快照读语句时创建的;
在执行 start transaction with consistent snapshot 时创建的
一致性视图是通过多版本并发控制(MVCC)实现的,那么什么是多版本并发控制(MVCC)呢?其类似于如下的描述:
每次更新操作的时候,都会同时记录一条回滚日志(undo_ og),这样通过从最新值+回滚操作就可以得到历史的版本,这就是MySQL中多版本并发控制(MVCC)的实现方式。假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录:
那么这些回滚日志什么时候可以删除呢?他们在不需要用到的时候才会删除,也就是说没有更早的事务存在的时候才删除。
为此我们需要避免长事务,因为长事务会导致回滚日志长时间占用存储空间,包括内存与硬盘。除了回滚日志,长事务还占用锁资源。
1、显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
2、set autocommit=0,这个命令会将事务的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
3、autocommit=1时,单条语句就是一个事务,执行的时候自动开启,结束的时候自动提交。
MySQL可以通过如下的语句监控长事务
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
考虑如下的语句
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
Q2的当期读可以看到事务2新插入的语句,这就是幻读。也就是说幻读是指在当前读下读到了其他事务新插入的已提交的值。
那么MySQL在什么场景下没有幻读问题呢?MySQL在可重复度与串行化下解决了幻读问题,我会在后面的文章中说明MySQL是如何在可重复读级别下解决幻读问题的。
1、语义被破坏。Q1时声明了要在d=5的所有行加行锁,不允许其他事务修改。但是在Q2语句与Q3语句却拿到了不同得值,说明语义被修改了。
2、数据一致性问题,由于事务2的2条更新语句先提交,故他们会先写到binlog里面,如果把事务1改成如下的
那么binlog的日志的语句顺序将是:
update t set d=5 where id =0;
Insert into t value(1,1,5)
Begin
update t set d=6 where d=5 ;
commit
那么备机语句同步后备机会把
(5,5,5)被修改为(5,5,6)
(0,0,0)被修改为(0,0,6)
(1,1,5)被修改为(1,1,6)
显然与主库不一致,而且主库如果故障后从binlog恢复数据,也会与原先的数据不一致。
这也是为什么在读提交的级别下,需要把binlog格式改为row的原因。
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。