1 事务基本概念
事务将数据库从一种一致性状态转换为另一种一致性状态。它是访问并更新数据库各种数据项的一个程序执行单元,可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。
简而言之,在数据库提交事务时,可以确保要么所有修改都已经保存,要么所有修改都不保存。
在 MySQL InnoDB 下,每一条语句默认都是事务。可以通过set autocommit = 0;
设置当前会话手动提交。
ACID特性
原子性(A)
事务操作要么都做(提交),要么都不做(回滚)。
事务的回滚的通过undolog来实现的。undolog记录的是事务每步具体操作,当回滚时,执行事务具体操作的逆运算。
一致性(C)
在事务执行前后,数据库的完整性约束没有被破坏。一致性由原子性、隔离性以及持久性共同来维护的。
例如:一个表的姓名是唯一键,如果一个事务对姓名进行修改,但是在事务提交或事务回滚后,表中的姓名变得不唯一了,这样就破坏了一致性
隔离性(I)
对于同时执行的多个不同事务,其操作对象互相隔离,也就是说一个事务提交前对其他事务都不可见。这主要是通过 MVCC(多版本并发控制)和 锁来实现的。
锁用来处理并发 DML 操作。而 MVCC 实现一致性非锁定读,通过记录和获取行版本,而不是使用锁来限制读操作,从而实现高效并发读性能。
持久性(D)
事务提交后,事务DML操作将会持久化,即使发生宕机等故障,数据库也能将数据恢复。这是通过写入redolog磁盘文件来实现的,大致内容包括操作了数据的哪一个页、页偏移值以及具体数据等。
事务并发异常
在我们未对事务做优化前,事务的并发操作可能存在如下问题。
1)脏读
事务(A)可以读到另外一个事务(B)中未提交的数据,则称事务A读到了脏数据。
2)不可重复读
一个事务还未提交前,两次读同一个数据得到的结果却不一样的情况。出现这种情况说明有另一个事务提交了修改。一般而言,不可重复读的问题是可以接受的,因为读到已经提交的数据,一般不会带来很大的问题。
3)幻读
往往发生在事务中需要先读数据进行条件判断然后再进行写操作的情况。读数据后,判断条件成立,然后在执行写操作前,数据已被其他事务修改,之前读操作得到的条件已经不满足,导致后续写操作失败或错误。也就是说读了个”寂寞“。
例如:对于一个以name
为唯一索引的表,在一个事务中经查询得知不存在name=aaa
的行记录,于是正准备执行插入name=aaa
的行记录,此时另外一个事务执行了相同的插入操作,这将导致本事务的此次插入操作失败。
幻读也可以通过手动给读操作加锁来解决,但具体还需要看隔离级别。
隔离级别
引入隔离级别的目的就是为了解决上面提到的事务并发异常。
SQL标准制定了四种事务隔离级别的标准,各数据库厂商在正确性和性能之间做了妥协,并没有严格遵循这些标准。
1)READ UNCOMMITTED
所谓读未提交,即可以读到其他事务未提交的操作。该级别下读操作不加锁,写操作加排他锁,在事务提交或回滚后释放锁;
2)READ COMMITTED
读已提交,即只能读到已提交的数据,但可能出现”不可重复读“。从该级别开始支持 MVCC ,也就是提供一致性非锁定读。此时读取操作实际读取的是历史快照数据的最新版本,如果出现更新的版本,则读到的就是更新的版本了。
很多厂商(如Oracle、SQL Server)默认隔离级别就是READ COMMITTED
。
3)REPEATABLE READ
可重复读,可以避免”不可重复读“的问题,即事务提交前读同一个数据得到的结果总是相同的。此时执行读操作实际读到的是该事务开始时的快照版本(不是最新版本),因为事务开始时刻的那个快照版本肯定不会变,所以每次读的结果都是一样。该级别下也支持 MVCC,但如果不主动加锁仍可能出现幻读。
4)SERIALIZABLE
可串行化,即所有事务都是串行化的执行,因此是最严苛的隔离级别。可以解决上面的所有的并发异常。
不同隔离级别对比
MySQL InnoDB默认支持的隔离级别是REPEATABLE READ
。
事务控制语句
设置事务隔离级别的命令为:
-- 设置隔离级别
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ;
开始和提交事务等:
-- 显示开启事务
START TRANSACTION
-- 提交事务,并使得已对数据库做的所有修改持久化
COMMIT
-- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改
ROLLBACK
-- 创建一个保存点,一个事务可以有多个保存点
SAVEPOINT identifier
-- 删除一个保存点
RELEASE SAVEPOINT identifier
-- 事务回滚到保存点
ROLLBACK TO [SAVEPOINT] identifier
示例:
DROP TABLE IF EXISTS `lock_test`;
CREATE TABLE `lock_test` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`cid` INT,
`num` INT, -- 用于测试修改
KEY(`cid`)
) ENGINE=InnoDB; -- 建表
INSERT INTO `lock_test` (`cid`, `num`) VALUES (3, 1), (5, 2), (9, 3), (11, 4), (15,