1、数据库事务的基本概念
在执行某些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、数据库事务的四个特性(原子性、一致性、隔离性、持久性)
数据库事务具有ACID这4个特性:
- A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
- C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
- I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;例如同时有T1和T2两个并发事务,从T1角度来看,T2要不在T1执行之前就已经结束,要么在T1执行完成后才开始。将多个事务隔离开,每个事务都不能访问到其他事务操作过程中的状态。
- D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。
对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
要手动把多条SQL语句作为一个事务执行,使用BEGIN
开启一个事务,使用COMMIT
提交一个事务,这种事务被称为显式事务。COMMIT
是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果COMMIT
语句执行失败了,整个事务也会失败。多条SQL语句要想作为一个事务执行,就必须使用显式事务。
例如,把上述的转账操作作为一个显式事务:
BEGIN;
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;
数据库事务是由数据库系统保证的,我们只需要根据业务逻辑使用它就可以。
2.1、多用户并发可能发生的问题(脏读、不可重复读、幻读)
对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。在没有数据库隔离性的情况下,多用户并发操作可能会发生以下问题,包括脏读、不可重复读、幻读等。
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并未收到钱,但是进行了下一步的操作,造成了损失,这就是脏读。
详细例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1219071817284064
2)不可重复读
不可重复读是指对于数据库中的某个数据,一个事务在执行过程中多次查询返回了不同的查询结果,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1正在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据时就得到了不同的结果,由此就发生了不可重复读。
不可重复读同脏读的区别在于,脏读是一个事务读取了另一未完成的事务执行过程中的数据,而不可重复读是一个事务执行过程中,另一事务提交并修改了当前事务正在读取的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
详细例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1245266514539200
3)幻读
幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
3、数据库提供的四种隔离级别
当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。
MySQL数据库为我们提供了四种隔离级别:
- Read uncommitted (读未提交):最低级别,任何情况都无法保证。
- Read committed (读已提交):可避免脏读的发生。
- Repeatable read (可重复读):可避免脏读、不可重复读的发生。
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
4 种隔离级别分别对应可能会出现的数据不一致的情况如下:
给事务定义了以上隔离级别,则该事务就可以避免一些数据不一致的问题。为什么说定义了这些隔离级别的事务就可以避免一些数据不一致的问题?可参考:https://blog.csdn.net/nevergiveup12345/article/details/24997461
3.1、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;
3.2、Read committed隔离级别 (可避免脏读)
在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;
3.3、Repeatable read隔离级别 (可避免脏读和不可重复读,默认隔离级别)
在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。
3.4、Serializable(可避免脏读、不可重复读、幻读)
Serializable是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。