本文将对Mysql事务进行总结。
1 事务
-
数据库的事务(Transaction)是一种机制、一个操作序列,是访问和更新数据库的程序执行单元,包含了一组数据库操作命令。
事务把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库命令要么都执行,要么都不执行,因此事务是一个不可分割的工作逻辑单元。
在数据库系统上执行并发操作时,事务是作为最小的控制单元来使用的,特别适用于多用户同时操作的数据库系统。
2 事务的特性
-
事务具有四个特性:原子性( Atomic )、一致性( Consistency )、隔离性( Isolation )、持久性( Durability ), 简称为ACID特性。
-
原子性
事务的原子性指的是构成事务的所有操作要么全部执行成功,要么全部执行失败,不会出现部分执行成功,部分执行失败的情况。
例如在转账业务中,张三向李四转账 100 元,于是张三的账户余额减少 100 元,李四的账户余额增加 100 元。在开启事务的情况下,这两个操作要么全部执行成功,要么全部执行失败,不可能出现只将张三的账户余额减少 100 元的操作,也不可能出现只将李四的账户余额增加 100 元的操作。
-
一致性
事务的一致性指的是事务在执行前和执行后,数据库中已存在的约束不会被打破。
比如余额必须大于等于 0 就是一个约束,而张三余额只有 90 元,这个时候如果转账 100 元给李四,那么之后它的余额就变成了 -10,此时就破坏了数据库的约束。所以数据库认为这个事务是不合法的,因此执行失败。
-
隔离性:
事务的隔离性指的是并发执行的两个事务之间互不干扰,也就是说,一个事务在执行过程中不会影响其它事务运行。
-
持久性:
事务的持久性指的是事务提交完成后,对数据的更改操作会被持久化到数据库中,并且不会被回滚。
例如张三向李四转账,在同一事务中执行扣减张三账户余额和增加李四账户余额操作。事务提交完成后,这种对数据的修改操作就会被持久化到数据库中,且不会被回滚,因为已经被提交了,而回滚是在事务执行之后、事务提交之前发生的。
所以数据库的事务在实现时,会将一次事务中包含的所有操作全部封装成一个不可分割的执行单元,这个单元中的所有操作必须全部执行成功,事务才算成功。只要其中任意一个操作执行失败,整个事务就会执行回滚操作,即自动回滚(当然也可以手动回滚)。但执行成功之后,就无法再回滚了,因为事务已经结束了。
-
事务,能保证AID,即原子性,隔离性,持久性。但是一致性无法通过事务来保证,一致性依赖于应用层,开发者。
3 事务的使用
-
通过下列语句可以开启一个事务:
-- 开启事务 -- BEGIN 也可以写成 START TRANSACTION BEGIN -- 执行一系列操作,这些操作是一个整体 INSERT INTO table_name (id, name) VALUES (1, 'satori'); UPDATE table_name set name = 'koishi' WHERE id = 1 ; DELETE FROM table_name WHERE id = 2 ; -- 执行 commit 表示提交事务 -- 执行 rollback 表示回滚事务 COMMIT / ROLLBACK
-
savepoint 是在数据库事务处理中实现“子事务”(subtransaction),也称为嵌套事务的方法。事务可以回滚到 savepoint 而不影响 savepoint 创建前的变化, 不需要放弃整个事务。
ROLLBACK 回滚的用法可以设置保留点 SAVEPOINT,执行多条操作时,回滚到想要的那条语句之前。
使用 SAVEPOINT
SAVEPOINT savepoint_name; --声明一个 savepoint ROLLBACK TO savepoint_name; --回滚到savepoint
删除 SAVEPOINT
保留点在事务处理完成(执行一条 ROLLBACK 或 COMMIT)后自动释放。
MySQL5 以来,可以用:
RELEASE SAVEPOINT savepoint_name; --删除指定保留点
4 事务并发带来的问题
-
并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多用户。但是同时会带来以下几种问题: 更新丢失 / 脏写、脏读、 不可重复读、 幻读。
-
更新丢失(Lost Update) / 脏写
两个或者多个事务同时选择同一行数据,都基于最初选定的值更新该行,由于每个事务都不知道其它事务的存在,就会发生更新丢失的问题。最后提交的更新覆盖了之前其它事务所做的更新。
如上图所示,t1时刻,事务T1和事务T2同时读取了记录Data=10 , 并分别保存了Data副本。在t2时刻,T2在10的基础上加5,并写回Data,此时Data = 15; 在 t3 时刻,T1由将
a = 20
写入Data,此时Data = 20。此时T1的写入操作覆盖了T2的更新,导致T2的更新丢失。更新丢失(脏写)本质上是写操作的冲突,解决办法是让每个事务按照串行的方式执行,按照一定的顺序依次进行写操作。
-
脏读(Dirty Reads)
一个事务正在对一条记录进行修改,这个事务完成并提交前,这条记录的数据就处于不一致的状态。此时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并依据此做了进一步的处理,就会产生对未提交的数据的依赖关系。这种现象就叫做“脏读”。
总结:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
脏读本质上是读写操作的冲突,解决办法是先写后读,也就是写完之后再读。
-
不可重复读(Non-Repeatable Reads)
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”,重复读到的是不同的数据。
总结:事务A读取到了事务B已经提交的修改数据,不符合隔离性。
不可重复读本质上也是读写操作的冲突,解决办法是先读后写,也就是读完之后再写。
-
幻读(Phantom Reads)
一个事务按照相同的查询条件重新读取数据,发现读到了其它事务插入的满足当前查询条件的新数据,这种现象叫作幻读。即一个事务两次读取一个范围的数据记录,两次读取到的结果不同。
举个例子:比如事务A第一次查询到表Student中有a、b、c三条数据,然后事务B向里面添加一条d数据,事务A再按照原来的查询条件查询发现查询出四条数据,这让事务A产生幻想,之前明明就只有三条数据,为什么现在却有四条数据了呢?
总结:事务A读取到了事务B提交的新增数据,不符合隔离性。
-
不可重复读和幻读区别
-
不可重复读的重点在于更新和删除操作,而幻读的重点在于插入操作;
-
使用锁机制实现事务隔离级别(一会说)时,在可重复读隔离级别中,SQL 语句第一次读取到数据后,会将相应的数据加锁,使得其他事务无法修改和删除这些数据,此时可以实现可重复读。但这种方法无法对新插入的数据加锁,如果事务 A 读取了数据,或者修改和删除了数据,此时事务 B 还可以进行插入操作,导致事务 A 莫名其妙地多了一条之前没有的数据,这就是幻读;
-
幻读无法通过行级锁来避免,需要使用串行化的事务隔离级别,但是这种事务隔离级别会极大降低数据库的并发能力;
-
从本质上讲,不可重复读和幻读最大的区别在于如何通过锁机制解决问题;
-
-
解决方案
解决更新丢失主要有以下两个方式:
-
使用事务+锁定读,也就是for update
-
不使用事务,用CAS自旋来操作
脏读、不可重复读和幻读其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
-
5 事务隔离级别
-
MySQL 中的 InnoDB 储存引擎提供 SQL 标准所描述的 4 种事务隔离级别,分别为:读未提交(Read Uncommitted) 、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
- 在讨论隔离级别之前,让我们先了解数据库的锁。
-
读(共享)锁:如果T1在一行上拿到读锁,T2仍然可以读该行。这意味着T1和T2都可以在同一行上读(共享锁)。而且,由于T1持有读锁,并且”读不阻塞写”,T2仍然可以通过获取写锁来更新该行。
-
写(独占)锁:如果T1持有一行的写锁,则T2不能读或写该行。(写锁阻塞读锁)。 这意味着如果在一行上设置了写锁,则没有其他事务可以读/写该行。
-
-
由不同的锁,数据库理论提出了三级封锁协议,内容如下:
-
一级封锁协议:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据。
-
二级封锁协议:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。 二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。
-
三级封锁协议 :一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。 三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。
-
-
读未提交隔离级别
在Read Uncommitted策略下,数据库遵循一级封锁协议,可以读到别的事务未提交的数据,也就是事务A可以读取事务B未提交的数据。
这提供了0%的隔离,因为它也允许读取未提交的数据。 在这样的隔离级别上,所有上述并发问题都存在。
-
读提交隔离级别
在Read committed策略下,数据库遵循二级封锁协议,只可以读到别的事务已提交的数据,也就是事务1只可以读取到事务2已经提交了的数据,那么在2未提交之前的数据是读取不到的,也就不可能产生脏读,但是因为事务2已提交的数据是可以读取到的,所以可能会导致不可重复读和幻读。
在MySql的InnoDB下,虽然这种操作(指读取未提交的数据)不被允许,但MySQL不会阻塞住数据的查询操作,而是会查询出数据被修改之前的备份,返回给客户端。MySQL的这种机制称为MVCC(多版本并发控制),就是说数据库在事务并发的过程中对数据维护多个版本,使得不同的事务对不同的数据版本进行读写。这样的机制反映在应用中就是,在任何时候对数据库查询总是可以得到数据库中最近提交的数据。未被提交的脏数据被隔离起来,无法被查询到,即防止脏读发生。
示例:
当T1读取数据Data,它拿到了共享/读锁。然后T1写/更新Data,并拿到写锁。现在当另一个事务试图读取已经在写锁状态的值就不允许的,要等到写锁释放或T1事务完成。
因此一旦T1提交或回滚,写锁释放T2将不受阻塞,并读取Data,这是正确的值不是脏读。类似地,T2不能更新Data,直到T1已经提交/回滚,所以它也可以防止脏写。
-
可重复读取隔离级别
Repeat Read又比Read Committed更加严格一点,但仍然是在二级封锁协议的范畴,只是读取过程受到更多MVCC的影响。事务可以重复读取数据,在事务期间,每次读取的数据都是一样的,也就是事务1开启事务后,读取了某一个表的5条数据,不管事务2怎么对这5条数据修改操作,事务1每次查询都是5条一摸一样的数据,所以是可重复读的,因此不可能导致脏读,不可重复读,但是还是可能导致幻读的。
这在读提交之上增加了另一个隔离层,以进一步防止可重复读问题。这是通过读锁可以阻塞写锁原则实现的,这与一般的读锁行为相违背。
在这一级别下,有赖于MVCC,同一个事务中的查询只能查到版本号不高于当前事务版本的数据,即事务只能看到该事务开始前或者被该事物影响的数据。反过来说,这一级别下,不允许事务读取在该事务开始后新提交的数据。即防止了不可重复读的发生。
示例:
T1事务开始T1读取Data = 10【第一次读,拿到读锁】,T2事务开始读取 Data = 10【可以读,由于读锁共享的】,T2更新Data = 5【阻塞等待锁】,T1读取Data = 10【第二次读取结果相同】
请注意:T1读取->拿到读锁, T2读取->允许,由于读=共享锁。多读是允许的。T2写->不允许,由于行被锁定,T2将处X-WAIT等待, 状态X-WAIT意思是等待写锁。因此T2不能获取锁来更新这一行,因此当T1再次读取同一行时,它仍然会得到相同的结果。 一旦T1完成,锁就被释放,然后T2就可以获得这个锁来更新。
-
串行读取隔离级别
Serializable遵循三级封锁协议。mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
示例:
当T1查询一个范围或记录时,它会获得一种不同类型的锁,这表明它属于这个范围。 这个锁被称为范围锁(范围S-S是它的状态),而不是S代表读锁,X代表写锁。 所以当T1查询一个范围时,所有的行都是范围锁定的。 如果T2尝试插入新行,这可能会影响到这个范围,那么T2将被阻塞,直到T1完成并释放范围锁。 但是,T2可以读取这些行,因为范围锁允许共享读,但阻止某些写操作。
T1查询:select * from Tbl where X>100 → 【结果100行】(范围S-S锁定保持100行), T2事务开始T2插入一行数据:X=150【阻塞】。 T1查询:select * from Tbl where X>100 → 【结果还是100行】。
-
不同隔离级别可以解决的问题
不同隔离级别可以解决的问题如下表所示:
-
MySql隔离级别使用
MySQL 默认的隔离级别为可重复读,当然我们也可以手动指定隔离级别,通过在 my.cnf 或者 my.ini 文件中的 mysqld 节点下面配置如下选项。
-- 读未提交 transaction-isolation = READ-UNCOMMITTED -- 读已提交 transaction-isolation = READ-COMMITTED -- 可重复读 transaction-isolation = REPEATABLE-READ -- 串行化 transaction-isolation = SERIALIZABLE
也可以使用 SET TRANSACTION 命令改变单个或者所有新连接的事务隔离级别,基本语法如下所示。
-- 读未提交 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL READ UNCOMMITTED -- 读已提交 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL READ COMMITTED -- 可重复读 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL REPEATABLE READ -- 串行化 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL SERIALIZABLE
如果使用 SET TRANSACTION 命令来设置事务隔离级别,需要注意以下几点。
-
不带 SESSION 或 GLOBAL 关键字设置事务隔离级别,指的是为下一个(还未开始的)事务设置隔离级别;
-
使用 GLOBAL 关键字指的是对全局设置事务隔离级别,也就是设置后的事务隔离级别对所有新产生的数据库连接生效;
-
用 SESSION 关键字指的是对当前的数据库连接设置事务隔离级别,此时的事务隔离级别只对当前连接的后续事务生效;
-
任何客户端都能自由改变当前会话的事务隔离级别,可以在事务中间改变,也可以改变下一个事务的隔离级别;
使用如下命令可以查询全局级别和会话级别的事务隔离级别。
SELECT @@global.tx_isolation; SELECT @@session.tx_isolation; SELECT @@tx_isolation;
-