什么是事务
事务是数据库中的一个重要的概念,它是指数据库操作的一个逻辑单位。事务是使数据库从一种状态变换到另一种状态的过程,要么全部成功,要么全部失败。在事务过程中,其他并发事务是无感知的,事务提交后其效果是永久性的。在MySQL中可以在执行某些操作之前先开启事务,然后进行这一系列操作,这些操作完成后如果发现操作出现异常,可以进行事务的回滚,事务的回滚就是把开启事务之后的所有操作视为无效,把数据还原为事务刚开启还未执行操作的状态。当然如果这些操作没有问题,也可以进行事务提交,那么这些操作对于数据的修改就会变成永久性的。
事务的特点
事务具有四个特性,被称为ACID特性,分别是:
原子性(Atomicity):事务的所有操作要么全部执行成功,要么全部失败回滚。换句话说,事务的执行是一个不可分割的原子操作。
一致性(Consistency):事务执行的结果必须使数据库从一个一致性状态变为另一个一致性状态。简单来说,事务执行前后,数据保持逻辑上一致。
隔离性(Isolation):多个事务并发执行时,每个事务的操作与其他事务的操作相互隔离,不能互相干扰。也就是说,每个事务给出的数据应该是独立的。
持久性(Durability):一旦事务完成提交,其所做的修改将永久保存在数据库中,即使出现系统故障也不会丢失。
这里给出一个简单的例子来说明事务的四个特性:原子性、一致性、隔离性和持久性。
假设有一个银行数据库,有一张账户表account和一张交易记录表trade。事务1从账户A转账100元到账户B,事务2查询账户A的余额。
start transaction; # 开启事务
select * from account where id = 'A'; # 查询A的账户 假设A的余额为1000
select * from account where id = 'B'; # 查询B的账户 假设B的余额为1000
update account set balance = balance - 100 where id = 'A'; # 余额减一百为900
update account set balance = balance + 100 where id = 'B'; #余额加一百为1100
insert into trade values(1, 'A', 'B', 100); #在trade表中增添转账记录
commit;
# 以上操作就体现了原子性:要么全部成功要么全部失败,不会出现只转账不增添记录的情况 或是值减100不加100的情况
# 一致性:转账前后,账户余额的总和应保持逻辑上的一致,也就是说原先AB余额总和为2000,转账后的总和从逻辑上来说应该也是2000,不会出现总和变的情况
# 持久性:事务一旦(commit)提交,数据改变是永久的
start transaction; # 开启一个新的事务
select * from account where id = 'A';
# 隔离性:一个事务无法读取另一个未提交事务的修改,假如上一个事务没有commit,这个操作是无法读取上个事务操作的未提交数据的
从这个例子可以看出:
- 原子性:要么转账并记录成功,要么全部失败,不会只成功一部分。
- 一致性:无论事务是否成功,账户余额总和应保持不变。
- 隔离性:事务2无法读取事务1未提交的修改数据。
- 持久性:事务1提交后,数据改变是永久的。
所以,通过这个简单的例子,我们可以很直观地理解事务的四大特性及其意义。
事务的隔离级别
多个事务并发修改同一个数据导致的问题
当多个事务并发地去修改同一个数据时,就会出现一些问题。典型的就是脏读,幻读和不可重复读,而为了解决这些问题才出现了事务的不同隔离级别。因此我们应该先了解这些问题
- 脏读:一个事务读取了另一个未提交事务的更新数据。
例如:
update user set name = 'b' where id = 1; # 事务1: 把name='a'修改为了'b'
select * from user; # 事务2: 读取name = 'b',但事务1还未提交
rollback; # 事务1: 回滚,user表数据应为name = 'a'
这样事务2读取了事务1未提交的更新数据,这就是脏读。
- 不可重复读:一个事务中多次读取同一数据,得到不同结果。
例如:
select * from user; # 事务1: name = 'a'
update user set name = 'b' where id = 1; # 事务2:提交更新
select * from user; # 事务1:此时name = 'b',与第一次读不同
- 幻读:一个事务查找所有符合某条件的记录,但另一个并发事务却新增了符合该条件的记录。
例如:
select * from user where name = 'a'; # 事务1:查到1条记录
insert into user values(2,'a'); # 事务2:新增一条a记录,提交
select * from user where name = 'a'; # 事务1:此时查到2条,多了一条幻读记录
所以,概括来说:
- 脏读是读取未提交数据,数据不一致。
- 不可重复读是多次读取同数据,值不同,破坏了重复读。
- 幻读是记录集合发生变化,不是所期望的同一记录集,破坏了可序列化。
不可重复读与幻读的区别
到这里你可能会发现不可重复读和幻读不是一个东西吗,不都是多次读取的得到的结果不一样吗?是的,但是又有细微的差别:
不可重复读指在同一个事务中,由于并发操作导致查询结果不一致,即一个事务多次查询同一数据,在这个过程中,另一个事务修改了该数据,导致前一个事务查询时发现数据与前面查询的结果不同。这种情况下,前一个事务可能会做出错误的决策。
而幻读则是指在同一个事务中,由于并发操作导致查询结果的行数不一致,即一个事务先后两次进行相同的查询操作,在这个过程中,另一个事务插入或删除了满足该查询条件的行,导致前一个事务第二次查询时,发现返回的行数与第一次查询结果不同。这种情况下,前一个事务也可能会做出错误的决策。
因此,不可重复读主要涉及到数据的更新,而幻读则主要涉及到数据的插入、删除等操作。
隔离级别
事务的四个隔离级别从低到高分别是:
-
读未提交(Read Uncommitted):允许一个事务读取另一个事务尚未提交的数据。这种隔离级别会引发“脏读”、“不可重复读”和“幻读”。
-
读已提交(Read Committed):只允许一个事务读取另一个事务已经提交的数据。这种隔离级别可以避免“脏读”,但是仍然会引发“不可重复读”和“幻读”。
-
可重复读(Repeatable Read):保证在同一事务内多次读取同一数据时,其结果始终相同。这种隔离级别可以避免“脏读”、“不可重复读”,但是仍然会引发“幻读”。
-
序列化(Serializable):最高的隔离级别,在所有操作之间强制执行串行化顺序,避免了所有的并发问题,但是会降低数据库的并发性能。
它们的特点可以总结为:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | √ | √ | √ |
READ COMMITTED | × | √ | √ |
REPEATABLE READ | × | × | √ |
SERIALIZABLE | × | × | × |
这四个隔离级别从上往下隔离级别依次递增,并发度以此降低。