事务机制
事务(Transaction)是数据库区别于文件系统的重要特性之一。事务会把数据库从一种一致状态转换为另一个种一致状态。在数据库提交工作时,可以确保其要么所有修改都已经保存了,要么所有修改都不保存。
InnoDB存储引擎中的事务完全符合ACID的特性。
- 原子性(atomicity)
原子性是指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作执行都成功,才算整个事务成功。如果事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
- 一致性(consistency)
一致性指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- 隔离性(isolation)
一个事务的影响在该事务提交前对其他事务都不可见。
- 持久性(durability)
事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。
事务的实现
隔离性通过Mysql InnoDB锁就可以实现;
原子性、一致性、持久性通过数据库的redo和undo来完成;
要仔细解释事务机制如何实现的,其实在数据库文件部分已经略有提及。
事务控制语句
在MySQL命令行的默认设置下,事务都是自动提交的,即执行SQL语句后就会马上执行COMMIT操作。因此开始一个事务,必须使用BEGIN、START TRANSACTION,或者执行SET AUTOCOMMIT=0,以禁用当前会话的自动提交。
以下为事务控制语句:
- START TRANSACTION | BEGIN:显示地开启一个事务。
- COMMIT:提交你的事务,并使得已对数据库做的所有修改成为永久性的。同时写入undo log,将日志缓冲刷新到redo log
- ROLLBACK:回滚结束事务,并撤销正在进行的所有未提交的修改。同时取出redo log。
- SAVEPOING identifier:SAVERPOINT允许你在事务中创建一个保存点,当没有一个保存点执行这句话时,会抛出异常。(以下语句没用过,只能做知识搬运工咯)
- RELEASE SAVERPOINT identifier:删除一个事务的保存点,当没有一个保存点执行这句话时,会抛出一个异常。
- ROLLBACK TO [SAVEPOINT] identifier:这个语句与SAVEPOINT一起用。可以把事务回归到标记点,而不回滚到此标记点之前的任何工作。
- SET TRANSACTION:这个语句用来设置事务的隔离级别。InnoDB存储引擎提供的事务隔离级别有:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。
隐式提交的SQL语句
以下这些SQL语句会产生一个隐式的提交操作,即执行完这些语句后,会有一个隐式的COMMIT操作。
- DDL语句:ALTER DATABASE...UPGRADE DATA DIRECTORY NAME........
- 用来隐式的修改mysql架构的操作:CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD。
- 管理语句:ANALYZE TABLE、CACHE INDEX、CHECK TABLE、LOAD INDEX INTO CACHE、OPTIMIZE TABLE 、REPAIR TABLE。
对于事务操作的统计
对于数据库的两大性能指标:
QPS:question per second,每秒请求数
TPS:transaction per second,每秒事务处理的能力
计算TPS的方法是(com_commit+com_rollback)/time,用这种方法的前提是,所有的事务必须都是显式提交的。
事务隔离级别
在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。数据库锁,也是为了构建这些隔离级别存在的。SQL标准定义的四个隔离级别为:
- 未提交读(READ UNCOMMITTED):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据;
- 提交读(READ COMMITTED):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读);
- 可重复读(REPEATABLE READ):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读;
- 串行读(SERIALIZABLE):InnoDB存储引擎会对每个SELECT语句后自动加上LOCK IN SHARE MODE,即给每个读取操作加一个共享锁,因此在这个事务隔离级别下,读占用锁了,一致性的非锁定读不再予以支持,一般不再本地事务中使用SERIALIZBLE的隔离级别,SERIALIZABLE的事务隔离级别主要用于InnoDB存储引擎的分布式事务。
隔离级别vs读现象(Isolation Levels vs Read Phenomena)
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
未提交读(Read uncommitted) | |||
已提交读(Read committed) | |||
可重复读(Repeatable read) | |||
可串行化(Serializable) |
隔离级别vs 锁持续时间(Isolation Levels vs Lock Duration)
在基于锁的并发控制中,隔离级别决定了锁的持有时间。""-表示锁会持续到事务提交。 "" –表示锁持续到当前语句执行完毕。如果锁在语句执行完毕就释放则另外一个事务就可以在这个事务提交前修改锁定的数据,从而造成混乱。
隔离级别 | 写操作 | 读操作 | 范围操作 (...where...) |
未提交读(Read uncommitted) | |||
已提交读(Read committed) | |||
可重复读(Repeatable read) | |||
可串行化(Serializable) |
查看当前会话的事务隔离级别命令:
select @@tx_isolation;
查看全局事务隔离级别命令:
select @@global.tx_isolation;
脏读(Dirty reads (Uncommitted Dependency)
当一个事务允许读取另外一个事务修改但未提交的数据时,就可能发生脏读(dirty reads)。
事务2没有提交造成事务1的语句1两次执行得到不同的结果集。在未提交读(READ UNCOMMITTED)隔离级别唯一禁止的是更新混乱,即早期的更新可能出现在后来更新之前的结果集中。
不可重复读(non-repeatable read)
在一次事务中,当一行数据获取两遍得到不同的结果表示发生了“不可重复读(non-repeatable read)”。
在基于锁的并发控制中“不可重复读(non-repeatable read)”现象发生在当执行SELECT 操作时没有获得读锁(read locks)或者SELECT操作执行完后马上释放了读锁; 多版本并发控制中当没有要求一个提交冲突的事务回滚也会发生“不可重复读(non-repeatable read)”现象。
在这个例子中,事务2提交成功,因此他对id为1的行的修改就对其他事务可见了。但是事务1在此前已经从这行读到了另外一个“age”的值。在可序列化(SERIALIZABLE)和可重复读(REPEATABLE READS)的隔离级别,数据库在第二次SELECT请求的时候应该返回事务2更新之前的值。在提交读(READ COMMITTED)和未提交读(READ UNCOMMITTED),返回的是更新之后的值,这个现象就是不可重复读(non-repeatable read)。
有两种策略可以避免不可重复读(non-repeatable read)。一个是要求事务2延迟到事务1提交或者回滚之后再执行。这种方式实现了T1, T2 的串行化调度。串行化调度可以支持可重复读(repeatable reads)。
另一种策略是多版本并发控制。为了得到更好的并发性能,允许事务2先提交。但因为事务1在事务2之前开始,事务1必须在其开始执行时间点的数据库的快照上面操作。当事务1最终提交时候,数据库会检查其结果是否等价于T1, T2串行调度。如果等价,则允许事务1提交,如果不等价,事务1需要回滚并抛出个串行化失败的错误。
使用基于锁的并发控制,在可重复读(REPEATABLE READS)的隔离级别中,ID=1的行会被锁住,在事务1提交或回滚前一直阻塞语句2的执行。在提交读(READ COMMITTED)的级别,语句1第二次执行,age已经被修改了。
在多版本并发控制机制下,可序列化(SERIALIZABLE)级别,两次SELECT语句读到的数据都是事务1开始的快照,因此返回同样的数据。但是,如果事务1试图UPDATE这行数据,事务1会被要求回滚并抛出一个串行化失败的错误。
在提交读(READ COMMITTED)隔离级别,每个语句读到的是语句执行前的快照,因此读到更新前后不同的值。在这种级别不会有串行化的错误(因为这种级别不要求串行化),事务1也不要求重试。
幻读(phantom read)
在事务执行过程中,当两个完全相同的查询语句执行得到不同的结果集。这种现象称为“幻影读(phantom read)”
当事务没有获取范围锁的情况下执行SELECT ... WHERE操作可能会发生“幻影读(phantom read)”。
“幻影读(phantom read)”是不可重复读(Non-repeatable reads)的一种特殊场景:当事务1两次执行SELECT ... WHERE检索一定范围内数据的操作中间,事务2在这个表中创建了(如INSERT)了一行新数据,这条新数据正好满足事务1的“WHERE”子句。
不可重复读和幻读的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。
MySQL系列博客: