数据库的事务
1、数据库事务的基本概念
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
在执行某些SQL语句的时候,会要求该系列操作必须全部执行,而不能仅执行一部分。例如,一个转账操作:
# 从id=1的账户给id=2的账户转账100元
# 第一步:将id=1的A账户余额减去100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
# 第二步:将id=2的B账户余额加上100
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
这两条SQL语句必须全部执行,或者,由于某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。
这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。
2、数据库事务的四个特性(A原子性、C一致性、I 隔离性、D持久性)
数据库事务具有ACID这4个特性:
- A:Atomic:原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
- C:Consistent:一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
- I:Isolation:隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离。但实际上,事务并发时并没有完全隔离,互相会有影响,需要设置隔离级别。例如同时有T1和T2两个并发事务,从T1角度来看,T2要不在T1执行之前就已经结束,要么在T1执行完成后才开始。将多个事务隔离开,每个事务都不能访问到其他事务操作过程中的状态。
- D:Duration:持久性,即事务完成后,对数据库数据的修改被持久化存储。
2.1、隐式事务和显式事务
- 对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
- 要手动把多条SQL语句作为一个事务执行,使用 BEGIN(MySQL中也可以用START TRANSACTION)开启一个事务,使用
COMMIT
提交一个事务,这种事务被称为显式事务。COMMIT
是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果COMMIT
语句执行失败了,整个事务也会失败。多条SQL语句要想作为一个事务执行,就必须使用显式事务。
例如,把上述的转账操作作为一个显式事务:
BEGIN; -- MySQL中也可以用START TRANSACTION UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT;
有些时候,我们希望主动让事务失败,这时,可以用ROLLBACK
回滚事务,整个事务会失败:
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; ROLLBACK;
数据库事务是由数据库系统保证的,我们只需要根据业务逻辑使用它就可以。
(这是数据库层面的操作,Java层面的语法操作开启事务、结束事务等,实际底层最终也是使用了数据库语法)
3、多用户(事务)并发可能发生的问题(脏读、不可重复读、幻读)
对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。在没有数据库隔离性的情况下,多用户并发操作可能会发生以下问题,包括脏读、不可重复读、幻读等。
3.1、脏读(读到已修改但尚未提交的数据)(禁止在生产使用)
脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
例如:当一个事务的操作正在修改数据时,而在该事务还未最终提交的时候,另外一个并发事务来读取了数据,就可能会导致读取到的数据并非是最终持久化之后的数据,因为该事务后面可能会发生回滚,所以读到的这个数据就是脏读的数据。
最典型的例子就是银行转账,从A账户转账100到B账户,脚本命令为:
update account set money = money + 100 where username = 'B'; update account set money = money - 100 where username = 'A';
在这个事务执行过程中,另外一个事务读取结果发现B账户中的钱已经到账,提示B钱已到账,B就进行了下一步的操作。但是假设最终转账事务失败了,导致操作回滚。实际上B并未收到钱,但是进行了下一步的操作,造成了损失,这就是脏读。
3.1.1、脏读演示
我们通过 MySQL 数据库来演示脏读。
首先需要将数据库的自动提交取消掉,然后修改某一条记录:
修改前:
执行语句:
set @@autocommit = 0; UPDATE account set money = 700 WHERE username = 'aa'; SELECT * FROM account;
执行上面语句但未 commit,此时实际上并未持久性地作用到数据库中。
此时我们新起一个连接,将隔离级别置为 Read Uncommitted (因为只有该级别会有脏读,其他隔离级别不会有脏读情况),查询数据:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT * FROM account;
查询结果:
可以看到,查询到了第一个事务已修改但未提交的数据。此时如果第一个事务回滚 roolback,第二个事务又会查到 500 而不是 700 的记录。所以说,产生了脏读。
(注意,在同一事务里不会产生脏读,即在同一事务里即使修改数据未 commit,此时查询也是修改后的数据,脏读等一系列问题只会发生在多事务并发的情况)
视频例子可参考:https://www.liaoxuefeng.com/wiki/1177760294764384/1219071817284064
3.2、不可重复读(虚读,多次读结果不同)(正常现象)
不可重复读发生在一个事务内执行相同的查询两次或两次以上,但是每次都得到不同的数据时,这通常是因为另一个并发事务在两次查询期间进行了更新。不可重复读也可称为虚读。
例如:事务T1读取完某一数据后,事务T2立马修改了这个数据并且提交事务给数据库,事务T1 又有SQL命令再次读取该数据,此时就会发现跟之前的查询结果不一致,这就是不可重复读。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
3.2.1、不可重复读演示
将隔离级别设置为 Read committed 可以避免脏读的情况,但仍然会有不可重复读的情况。
表的初始情况:
事务1:
BEGIN; UPDATE account SET money = money +200 WHERE id = '1' COMMIT;
当执行完 update 后,我们并没有执行 commit,即未提交修改。
事务2如下,我们将事务 2 的隔离级别设置为 READ COMMITTED(可以避免脏读,但是不可避免不可重复读)。
set session transaction isolation level READ COMMITTED; -- 将当前会话隔离级别设置为 READ COMMITTED BEGIN; SELECT * FROM account; SELECT * FROM account; COMMIT;
当我们执行到第一条 select 语句时,查询结果如下:
因为事务 1 未提交,所以查询不到事务1未提交的数据。
此时如果事务1提交了即commit,此时事务2执行到第二条 select 语句,此时查询结果如下:
可以看到查询到的是事务1提交后的数据。由此在一个事务内(即在事务2)内两次查询到的结果并不一致,这也就是我们说的不可重复读,也就是在一个事务内多次查询的结果不一致。
在大部分情况下,不可重复读并不算是问题。但在某些特定情况下,在同一个事务中多次查询结果需要保持一致,所以就需要避免不可重复读的情况。MySQL数据库默认的隔离级别已经可以避免不可重复读的情况,也就是在同一事务内查询同一数据,多次查询得到的结果都是一致的。
视频例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1245266514539200
3.3、幻读(跟不可重复读本质一样,不过是读到新增数据)
幻读是事务非独立执行时发生的一种现象,幻读与不可重复读有点类似。
例如:事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点他们和脏读不同,脏读是读取了未提交的事务),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。不可重复读的重点是修改,同样的条件,你读取过的数据,再次读取出来发现值不一样了。幻读的重点在于新增或者删除,同样的条件,第1次和第2次读出来的记录数不一样。
不可重复读和幻读本质是一样的,只是概念有点区别,一个是跟修改有关,一个是跟增删有关。不可重复读是读到了其他事务修改的数据,而幻读读到的是其他事务新增的数据。
4、数据库提供的四种隔离级别
多个事务之间如果操作同一批数据,会引发一些问题,通过设置不同的隔离级别可以解决这些问题。
当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。
MySQL数据库为我们提供了四种隔离级别:
- Read uncommitted (意译:读未提交):最低级别,任何情况都无法保证。
- Read committed (意译:读已提交,Oracle默认级别):只可避免脏读的发生。
- Repeatable read (意译:可重复读,MySQL默认级别):可避免脏读、不可重复读的发生。
- Serializable (意译:串行化):脏读、不可重复读、幻读均可避免
4 种隔离级别分别对应可能会出现的数据不一致的情况如下:
上面四种隔离级别,从上到下安全级别越来越高,但效率也会越来越慢。
给事务定义了以上隔离级别,则该事务就可以避免一些数据不一致的问题。为什么说定义了这些隔离级别的事务就可以避免一些数据不一致的问题?可参考:https://blog.csdn.net/nevergiveup12345/article/details/24997461
4.1、查询和设置隔离级别
我们可以通过以下语句查询隔离级别:
SELECT @@tx_isolation; -- 查看当前会话的隔离级别 select @@global.tx_isolation; -- 查看全局的隔离级别
查询结果如下:
可以通过命令行设置全局或会话的隔离级别,重启数据库或者退出会话对应的隔离级别将会失效。
设置隔离级别命令格式:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} -- 当未指定设置的是会话还是全局时,默认是设置会话的隔离级别
具体命令:
# 设置全局隔离级别 set global transaction isolation level REPEATABLE READ; set global transaction isolation level READ COMMITTED; set global transaction isolation level READ UNCOMMITTED; set global transaction isolation level SERIALIZABLE; # 设置会话隔离级别 set session transaction isolation level REPEATABLE READ; set session transaction isolation level READ COMMITTED; set session transaction isolation level READ UNCOMMITTED; set session transaction isolation level SERIALIZABLE;
当同时设置了会话和全局的隔离级别时,当前会话的隔离级别的设置是由当前会话所设置的隔离级别决定的。
4.2、Read uncommitted级别(最低级别,无法避免数据不一致)
Read Uncommitted 是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果那另一个事务进行了回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。
详细例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1219071817284064
定义事务为 Read uncommitted 隔离级别:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN; SELECT * FROM students WHERE id = 1; SELECT * FROM students WHERE id = 1; COMMIT;
4.3、Read committed(Oracle默认级别,可避免脏读)
在Read Committed隔离级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
详细例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1245266514539200
定义事务为 Read committed 隔离级别:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; SELECT * FROM students WHERE id = 1; SELECT * FROM students WHERE id = 1; COMMIT;
4.4、Repeatable read(MySQL默认隔离级别,可避免脏读和不可重复读)
Repeatable Read 是 mysql 默认的隔离级别,也就是说在默认情况下,mysql 的同一个事务内不可能会出现脏读和不可重复读的情况,也就是在同一事务内,多次执行同一查询语句,如果不是由本身的会话修改了数据,那么就不可能出现结果不一样的情况,并且不可能会读取到其他事务未提交的数据。
在Repeatable Read隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。
详细例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1245268672511968
定义事务为 Repeatable read 隔离级别:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT * FROM students WHERE id = 99; SELECT * FROM students WHERE id = 99; UPDATE students SET name = 'Alice' WHERE id = 99; SELECT * FROM students WHERE id = 99; COMMIT;
如果没有指定隔离级别,数据库就会使用默认的隔离级别。在MySQL中,如果使用InnoDB,默认的隔离级别是Repeatable Read。
一般情况下,我们不会去修改数据库的隔离级别。
默认隔离级别避免脏读和不可重复读的示例:
我们开启两个会话,并且设置为不自动提交事务(这样才能模拟出在一个事务内的情况,否则事务立马提交就看不出效果了),在 A 会话中修改数据但不提交 commit,可以看到在 A 会话中是可以看到修改后的数据的,但是在 B 会话中是无法看到 A 会话未提交的修改的。
这就是 mysql 默认隔离级别避免了出现脏读的问题。
即使最终 A 会话将修改提交 commit 了,在 B 会话中同一事物内仍然不会看到 A 会话的修改。只有在 B 会话 commit 提交事务了,重新开启一个新事务,在这个新事务里面才可以看到 A 会话的修改。这就是避免了不可重复读的问题。
如下所示:
4.5、Serializable(最高级别,脏读、不可重复读、幻读均可避免)
Serializable 是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行。如果一个事务在操作一个数据表,那么其他事务就无法再操作该数据表(包括查询操作也无法正常执行完成),只有当前面的事务操作完成,后面的事务才能对该表的操作才能正常执行完成。因此在 Serializable 隔离级别下,脏读、不可重复读、幻读都不会出现。
虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。