一、事务
1.1 事务概述
事务:并发连接场景下,用户定义的一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。
MySQL的事务就是将多条SQL语句作为整体进行执行。
1)事务的目的:事务将数据库从一种一致性状态转换为另一种一致性状态;保证系统始终处于一个完整且正确的状态。
2)事务的组成:事务可由一条非常简单的 SQL 语句组成,也可以由一组复杂的SQL 语句组成。
3)事务的特征:在数据库提交事务时,可以确保要么所有修改都已经保存,要么所有修改都不保存。事务是访问并更新数据库各种数据项的一个程序执行单元。在 MySQL innodb 下,单条语句都具备事务。
1.2 事务控制语句
-- 显示开启事务 START TRANSACTION | BEGIN -- 提交事务,并使得已对数据库做的所有修改持久化 COMMIT -- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改 ROLLBACK -- 创建一个保存点,一个事务可以有多个保存点 SAVEPOINT identifier -- 删除一个保存点 RELEASE SAVEPOINT identifier -- 事务回滚到保存点 ROLLBACK TO [SAVEPOINT] identifier
1.3 ACID特性
1)原子性(A)
事务是访问并更新数据库各种数据项的一个程序执行单元,是不可分割的工作单位。因此事务操作要么全部执行,要么全部不执行,不存在中间状态。若事务执行过程发生错误,通过 undolog 来回滚到事务初始状态。undolog 记录的是事务每步具体操作,当回滚时,回放事务具体操作的逆运算。
undo log 存放在共享表空间内,用于存储旧版本的数据。主要有两个作用:
∘ 事务回滚:记录事务 DML 操作步骤,通过逆运算(逻辑取反)实现事务回滚。
∘ MVCC:记录事务 DML 操作提交后产生的行数据版本信息。
2)一致性(C)
事务的前后,所有的数据都保持一个一致的状态,不能违反数据的一致性检测(完整性约束检查);
一致性指事务将数据库从一种一致性状态转变为下一种一致性的状态,在事务执行前后,数据库完整性约束没有被破坏。一个事务单元需要提交之后才会被其他事务可见。一致性由原子性、隔离性以及持久性共同来维护的。
一致性的种类:
∘ 数据一致性(数据库完整性约束 – 五大约束),必须遵守。
∘ 预期一致性(逻辑一致性),可以适当破坏。例:查询是否存在,不存在写入,可能出现数据查询不存在,插入时却存在,此时会报错。
3)隔离性(I)
事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,并发事务之间不会相互影响,防止多个并发事务交叉执行导致数据不一致。
不同程度的隔离级别应对不同的现象,如脏读、不可重复读、幻读。可以通过适度破环一致性,得以提高性能。
通过 MVCC和 锁来实现:
∘ MVCC :多版本并发控制,主要解决数据库中多个事务并发执行时可能出现的读写冲突和数据不一致问题。通过记录和获取行版本,而不是使用锁来限制读操作,从而实现高效并发读性能。
∘ 锁:用来处理并发 DML 操作;数据库中提供粒度锁的策略,针对表(聚集索引 B+ 树)、页(聚集索引 B+ 树叶子节点)、行(叶子节点当中某一段记录行)三种粒度加锁;
4)持久性(D)
事务一旦完成,要将数据所做的变更记录下来,包括数据存储和多副本的网络备份。
事务提交后,事务 DML 操作将会持久化(写入 redolog 磁盘文件 哪一个页 页偏移值 具体数据);即使发生宕机等故障,数据库也能将数据恢复。
redolog 记录的是物理日志。事务提交后,记录事务 DML 操作对应物理页修改的内容,写入 redo log 磁盘文件。发生宕机等故障时,恢复数据库数据。
二、隔离级别
ISO 和 ANIS SQL 标准制定了四种事务隔离级别,目的在于提升数据库并发性能。MySQL innodb默认支持的隔离级别是REPEATABLE READ。
2.1 隔离级别的分类
读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。
2.1.1 读未提交(RU)
最低级别的隔离级别,事务可以读取到其他事务未提交的数据。即事务还未提交,其变更就能被其他事务看到
问题:可能导致脏读、不可重复读和幻读问题。
加锁情况:该级别下读不加锁,写自动加排他锁,写锁在事务提交或回滚后释放锁。
2.1.2 读已提交(RC)
事务只能读取到其他事务已提交的数据。即事务提交后,其变更才能被其他事务看到
问题:避免了脏读问题。但仍然可能出现不可重复读和幻读问题。
加锁情况:该级别后支持 MVCC (多版本并发控制),也就是提供一致性非锁定读(提供了不加锁的读取操作);此时读取操作读取历史快照数据;该隔离级别下读取历史版本的最新数据,所以读取的是已提交的数据。写自动加排他锁
2.1.3 可重复读(RR)
事务开始后,保证在整个事务过程中读取的数据是一致的。即事务执行过程中看到的数据,一直跟与该事务启动时看到的数据是一致的。
问题:避免了脏读和不可重复读问题,但仍然可能出现幻读问题。
加锁情况:该级别下也支持 MVCC,此时读取操作读取事务开始时的版本数据。写自动加排他锁
2.1.4 串行化
最高级别的隔离级别,事务串行执行,确保了最高程度的隔离性。即,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
问题:避免了脏读、不可重复读和幻读问题,但可能导致较高的并发性能开销。
加锁情况:该级别下给读加了共享锁,写自动加排他锁。
2.2 命令
-- 设置隔离级别 SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 或者采用下面的方式设置隔离级别 SET @@tx_isolation = 'REPEATABLE READ'; SET @@global.tx_isolation = 'REPEATABLE READ'; -- 查看全局隔离级别 SELECT @@global.tx_isolation; -- 查看当前会话隔离级别 SELECT @@session.tx_isolation; SELECT @@tx_isolation; -- 手动给读加 S 锁 SELECT ... LOCK IN SHARE MODE; -- 手动给读加 X 锁 SELECT ... FOR UPDATE; -- 查看当前锁信息 SELECT * FROM information_schema.innodb_locks;
2.3 并发读异常
准备工作
DROP TABLE IF EXISTS `account_t`; CREATE TABLE `account_t` ( `id` INT(11) NOT NULL, `name` VARCHAR(255) DEFAULT NULL, `money` INT(11) DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_name` (`name`) )ENGINE = INNODB AUTO_INCREMENT=0 DEFAULT CHARSET = utf8; INSERT INTO `account_t` VALUES (7,'M',1000), (1, 'C', 1000),(2, 'B', 1000),(3, 'A', 1000);
2.3.1 脏读
一个事务读到了另一个事务未提交的修改(读到脏数据)。
不可重复读在 RU 隔离级别存在。在读写分离的场景下,可以将 slave 节点设置为 READ UNCOMMITTED。此时脏读不影响,在 slave 上查询并不需要特别精准的返回值。
例如:session B 读到了 session A 中事务未提交的脏数据。
seq | session A | session B |
1 | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; |
2 | BEGIN; | BEGIN; |
3 | UPDATE account_t SET money = money - 100 WHERE name = ‘A’; | |
4 | UPDATE account_t SET money = money - 100 WHERE name = ‘A’; | |
5 | COMMIT; | COMMIT; |
2.3.2 不可重复读
事务(A) 可以读到另外一个事务(B)中提交的数据;通常发生在一个事务中两次读到的数据是不一样的情况;不可重复读在隔离级别 READ COMMITTED 存在。一般而言,不可重复读的问题是可以接受的,因为读到已经提交的数据,一般不会带来很大的问题.
例如:session B 读到了 session A 中事务提交的修改,造成两次读取同一个数据不一样。
seq | session A | session B |
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
2 | BEGIN; | BEGIN; |
3 | UPDATE account_t SET money = money - 100 WHERE name = ‘A’; | |
4 | SELECT money FROM account_t WHERE name = ‘A’; |
5 | COMMIT; | SELECT money FROM account_t WHERE name = ‘A’; |
6 | COMMI; |
2.3.3 幻读
两次读取同一个范围内的记录得到的结果集不一样。
例如:以name 为唯一键的表,一个事务中查询 select * from table where name = 'tom'; 不存在,接下来 insert into table (name) values ('tom'); 出现错误,此时另外一个事务也执行了 insert 操作;
幻读在隔离级别 REPEATABLE READ 及以下存在;但是可以在 REPEATABLE READ 级别下通过读加锁(使用 next-key locking)解决;
例如:由于 session A 的事务提交了插入操作,导致 session B 两次查询范围的结果不一样。
seq | session A | session B |
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
2 | BEGIN; | BEGIN; |
3 | INSERT INTO account_t(id,name,money) VALUES (4,‘D’,1000); | |
4 | SELECT * FROM account_t WHERE id >= 2; |
5 | COMMIT; | |
6 | SELECT * FROM account_t WHERE id >= 2; |
7 | INSERT INTO account_t(id,name,money) VALUES (4,‘D’,1000); | |
8 | SELECT * FROM account_t WHERE id >= 2 LOCK IN SHARE MODE; | |
9 | COMMI; |
解决:通过读加锁(next-key locking)
seq | session A | session B |
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
2 | BEGIN; | BEGIN; |
3 | SELECT * FROM account_t WHERE id >= 2 LOCK IN SHARE MODE; | |
4 | INSERT INTO account_t(id,name,money) VALUES (4,‘D’,1000); |
5 | INSERT INTO account_t(id,name,money) VALUES (4,‘E’,1000); | |
6 | COMMI; | |
7 | COMMI; |