事务
是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
事务简介
- 数据库的事务(Transaction)是一种机制、一个操作序列,包含了一组数据库操作命令。
- 事务把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库命令要么同时成功,要么同时失败。
- 事务是一个不可分割的工作逻辑单元。
我们可以举一个例子更好的理解事务:
如下图有一张表
张三和李四账户中各有100块钱,现李四需要转换500块钱给张三,具体的转账操作为
- 第一步:查询李四账户余额
- 第二步:从李四账户金额 -500
- 第三步:给张三账户金额 +500
现在假设在转账过程中第二步完成后出现了异常第三步没有执行,就会造成李四账户金额少了500,而张三金额并没有多500;这样的系统是有问题的。如果解决呢?使用事务可以解决上述问题
从上图可以看到在转账前开启事务,如果出现了异常回滚事务,三步正常执行就提交事务,这样就可以完美解决问题。
开启事务其实就是打一个标记,告诉数据库后边的执行都是一些临时的更改。只有当遇到提交事务的时候,才会成为一个持久化更改。而回滚事务就是撤销那些临时的操作回到开启事务之前的状态。
相应语法:
事务操作
我们将上述问题用事务去解决
-
不加事务演示问题
-- 转账操作 -- 1. 查询李四账户金额是否大于500 -- 2. 李四账户 -500 UPDATE account set money = money - 500 where name = '李四'; 出现异常了... -- 此处不是注释,在整体执行时会出问题,后面的sql则不执行 -- 3. 张三账户 +500 UPDATE account set money = money + 500 where name = '张三';
整体执行结果肯定会出问题,我们查询账户表中数据,发现李四账户少了500。
-
添加事务sql如下:
-- 开启事务 BEGIN; -- 转账操作 -- 1. 查询李四账户金额是否大于500 -- 2. 李四账户 -500 UPDATE account set money = money - 500 where name = '李四'; 出现异常了... -- 此处不是注释,在整体执行时会出问题,后面的sql则不执行 -- 3. 张三账户 +500 UPDATE account set money = money + 500 where name = '张三'; -- 提交事务 COMMIT; -- 回滚事务 ROLLBACK;
上面sql中的执行成功进选择执行提交事务,而出现问题则执行回滚事务的语句。以后我们肯定不可能这样操作,而是在java中进行操作,在java中可以抓取异常,没出现异常提交事务,出现异常回滚事务。
事务四大特征(ACID)
-
原子性(Atomicity)
: 事务是不可分割的最小操作单位,要么同时成功,要么同时失败 -
一致性(Consistency)
: 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; -
隔离性(Isolation)
:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。 -
持久性(Durability)
:事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
理论依据:
《Designing Data-Intensive Application(数据密集型应用系统设计)》 的作者在他的这本书中如是说:
Atomicity, isolation, and durability are properties of the database, whereas consis‐ tency (in the ACID sense) is a property of the application. The application may rely on the database’s atomicity and isolation properties in order to achieve consistency, but it’s not up to the database alone.
翻译过来的意思是:原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。
隔离性的说明:
隔离性越强,操作越不可见,性能越低
隔离性约弱,操作越可见,性能越高
一般我们不会去改变事务的隔离性。
默认:当我们在MySQL中回滚或者提交之后,别人才能看到我们对应的修改
说明:
默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MySQL会立即隐式的提交事务。
可以通过下面语句查询默认提交方式:SELECT @@autocommit;
查询到的结果是1 则表示自动提交,结果是0表示手动提交。当然也可以通过下面语句修改提交方式
set @@autocommit = 0;
并发事务
问题 | 描述 |
---|---|
脏读 | 一个事务读到另一个事务还没提交的数据 |
不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同 |
幻读 | 一个事务按照条件查询数据时,没有对应的数据行,但是再插入数据时,又发现这行数据已经存在 |
我们来详细解释一下这几个概念:
脏读
一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。
例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并为提交到数据库, A 的值还是 20。
不可重复读
事务A先去执行一个select语句,在执行第二步操作的时候,B事务并发的进行了一个update操作,并且成功提交。而这时当事务A再次去查询id为1的数据的时候会发现数据不一样。这种现象称之为不可重复读。
幻读
事务A先开始查询id为1的数据,发现没有;这时候有一个并发事务B往数据库里面添加了一条id为1的数据,并且成功提交,也就是说此时数据库中已经有一条id为1的数据了,当事务A再去执行insert的时候因为主键冲突肯定不会成功的。此时事务A再去查询数据库仍然查不到id为1的数据(假设已经解决了重复读的问题),但是前面的插入操作确实失败了啊,好像出现了幻觉一般,这种情况就叫做幻读。
幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
那么不可重复读和幻读有什么区别?
- 不可重复读的重点是内容修改或者记录减少,比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增,比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
幻读其实可以看作是不可重复读的一种特殊情况,单独区分幻读的原因主要是解决幻读和不可重复读的方案不一样。
举个例子:
- 执行 delete 和 update 操作的时候,可以直接对记录加锁,保证事务安全。
- 而执行 insert 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 insert 操作的时候需要依赖临键锁 Next-Key Lock(Record Lock+Gap Lock) 进行加锁来保证不出现幻读。
事务隔离级别
事务的隔离级别就是用来解决并发事务中出现的问题
SQL 标准定义了四个隔离级别:
READ-UNCOMMITTED(读取未提交)
: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。READ-COMMITTED(读取已提交)
: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。REPEATABLE-READ(可重复读)
: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。SERIALIZABLE(可串行化)
: 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted | √ | √ | √ |
Read committed | × | √ | √ |
Repeatable Read(默认) | × | × | √ |
Serializable | × | × | × |
- Serializable 性能最低;Read uncommitted 性能最高,数据安全性最差
MySQL的默认隔离级别是Repeatable Read,而Oracle的是Read committed
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
从上面对 SQL 标准定义了四个隔离级别的介绍可以看出,标准的 SQL 隔离级别定义里,REPEATABLE-READ(可重复读)是不可以防止幻读的。但是InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况:
- 快照读 :由 MVCC 机制来保证不出现幻读。
- 当前读 : 使用 Next-Key Lock (临键锁)进行加锁来保证不出现幻读,Next-Key Lock 是记录锁(Record Lock)和间隙锁(Gap Lock)的结合,记录锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED ,但是你要知道的是 InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失。
看到这可能会有一个疑问:什么是快照读?什么是当前读?
在数据库中,事务可以以快照读(Snapshot Read)
或当前读(Current Read)
的方式读取数据。这两种读方式对应的隔离级别不同,有各自的优点和缺点。
快照读:
- 快照读对应的隔离级别是可重复读(REPEATABLE READ)。
- 事务在开始时保存数据的一个快照(版本),并在事务期间读取这个快照中的数据。
- 这意味着快照读可以隔离其他事务的修改,一个事务在读取这个snapshot中的数据时,不会看到其他未提交事务的修改。
- 快照读可以避免读取过程中数据被其他事务修改的问题,保证事务读到一致的数据。
当前读:
- 当前读对应的隔离级别是读未提交(READ UNCOMMITTED)。
- 事务直接在数据上读取最新的数据版本(包括未提交的修改)。
- 这意味着当前读无法隔离其他事务的修改,可能会读取到已提交或未提交的最新数据版本。
- 当前读可以读取最新的修改版本,但可能会导致读取到未提交的数据或遇到脏读问题。
总结如下:
快照读 | 当前读 | |
---|---|---|
隔离级别 | 可重复读 | 读未提交 |
是否可以隔离其他事务未提交修改 | 是 | 否 |
是否可以避免脏读 | 是 | 否 |
是否可以读取最新数据 | 否 | 是 |
应用场景:
- 快照读适用于要求一致读取且不需要最新数据的场景,如报表统计。
- 当前读适用于要求读取实时数据且一致性要求不高的场景,如监控应用。
查看事务隔离级别:
SELECT @@TRANSACTION_ISOLATION
设置事务隔离级别:
SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE };
SESSION 是会话级别,表示只针对当前会话有效,GLOBAL 表示对所有会话有效
注意:事务隔离级别越高,数据越安全,但是性能越低。
我们对这四种事务隔离级别进行一个演示:
脏读(读未提交)
避免脏读(读已提交)
不可重复读
还是刚才上面的读已提交的图,虽然避免了读未提交,但是却出现了,一个事务还没有结束,就发生了 不可重复读问题。
可重复读
幻读
SQL 脚本 1 在第一次查询工资为 500 的记录时只有一条,SQL 脚本 2 插入了一条工资为 500 的记录,提交之后;SQL 脚本 1 在同一个事务中再次使用当前读查询发现出现了两条工资为 500 的记录这种就是幻读。
解决幻读的方法解决幻读的方式有很多,但是它们的核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据了。解决幻读的方式主要有以下几种:
- 将事务隔离级别调整为 SERIALIZABLE 。
- 在可重复读的事务级别下,给事务操作的这张表添加表锁。
- 在可重复读的事务级别下,给事务操作的这张表添加 Next-key Lock(Record Lock+Gap Lock)(这也就是InnoDB的解决方式)