1、什么是事务?
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。
2、事务的四大特性
原子性(Atomicity)
也就是我们刚才说的不可再分,也就意味着我们对数据库的一系列的操作,要么都是成功,要么都是失败,不可能出现部分成功或者部分失败的情况,以刚才提到的转账的场景为例,一个账户的余额减少,对应一个账户的增加,这两个一定是同时成功或者同时失败的。全部成功比较简单,问题是如果前面一个操作已经成功了,后面的操作失败了,怎么让它全部失败呢?这个时候我们必须要回滚。
原子性,在 InnoDB 里面是通过 undo log 来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用 undo log 来实现回滚操作。
一致性(consistent)
指的是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。比如主键必须是唯一的,字段长度符合要求。
除了数据库自身的完整性约束,还有一个是用户自定义的完整性。
举例:
1.比如说转账的这个场景,A 账户余额减少 1000,B 账户余额只增加了 500,这个时候因为两个操作都成功了,按照我们对原子性的定义,它是满足原子性的, 但是它没有满足一致性,因为它导致了会计科目的不平衡。
2.还有一种情况,A 账户余额为 0,如果这个时候转账成功了,A 账户的余额会变成-1000,虽然它满足了原子性的,但是我们知道,借记卡的余额是不能够小于 0 的,所以也违反了一致性。用户自定义的完整性通常要在代码中控制。
隔离性(isolation)
有了事务的定义以后,在数据库里面会有很多的事务同时去操作我们的同一张表或者同一行数据,必然会产生一些并发或者干扰的操作,对隔离性就是这些很多个的事务,对表或者 行的并发操作,应该是透明的,互相不干扰的。通过这种方式,我们最终也是保证业务数据的一致性。
隔离性是通过MVCC以及锁来进行实现的,后续会对MVCC做详细的讲解
持久性(Durable)
我们对数据库的任意的操作,增删改,只要事务提交成功,那么结果就是永久性的,不可能因为我们重启了数据库的服务器,它又恢复到原来的状态了。
持久性怎么实现呢?数据库崩溃恢复(crash-safe)是通过什么实现的?持久性是通过 redo log 来实现的,我们操作数据的时候,会先写到内存的 buffer pool 里面,同时记录 redo log,如果在刷盘之前出现异常,在重启后就可以读取 redo log的内容,写入到磁盘,保证数据的持久性。
总结:原子性,隔离性,持久性,最后都是为了实现一致性
3、事务并发会带来什么问题?
当很多事务并发的去操作数据库的表或者行的时候,会出现脏读,不可重复读,幻读等问题。
(1)脏读
当一个事务正在访问数据并且对数据进行了修改,但这种修改还没有提交到数据库中时,另一个事务也访问了这个数据并使用了它。这种情况下,第二个事务读取的数据是第一个事务尚未提交的修改,因此可能是不准确或不一致的。脏读通常发生在事务隔离级别较低的情况下,例如读未提交(Read Uncommitted)级别。
(2)不可重复读
在一个事务内,多次读取同一数据。在这个事务还没有结束时,另一个事务也访问了该同一数据并对其进行了修改。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,第一个事务两次读到的数据可能是不一样的。这种情况通常发生在读已提交(Read Committed)或可重复读(Repeatable Read)的事务隔离级别下。
(3)幻读
当事务不是独立执行时发生的一种现象,通常发生在可重复读(Repeatable Read)的事务隔离级别下。例如,第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。在第二个事务中,由于第一个事务的修改,导致第二个事务在读取数据时出现了额外的行或缺失的行,就像产生了幻觉一样。
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB ; INSERT into user VALUES (1,'1',20),(5,'5',20),(15,'15',30),(20,'20',30);
假设有如下业务场景:
时间 | 事务1 | 事务2 |
begin; | ||
T1 | select * from user where age = 20;2个结果 | |
T2 | insert into user values(25,'25',20);commit; | |
T3 | select * from user where age =20;2个结果 | |
T4 | update user set name='00' where age =20;此时看到影响的行数为3 | |
T5 | select * from user where age =20;三个结果 |
执行流程如下:
1、T1时刻读取年龄为20 的数据,事务1拿到了2条记录
2、T2时刻另一个事务插入一条新的记录,年龄也是20
3、T3时刻,事务1再次读取年龄为20的数据,发现还是2条记录,事务2插入的数据并没有影响到事务1的事务读取
4、T4时刻,事务1修改年龄为20的数据,发现结果变成了三条,修改了三条数据
5、T5时刻,事务1再次读取年龄为20的数据,发现结果有三条,第三条数据就是事务2插入的数据,此时就产生了幻读情况
此时大家需要思考一个问题,在当下场景里,为什么没有解决幻读问题?
其实通过前面的分析,大家应该知道了快照读和当前读,一般情况下select * from ....where ...是快照读,不会加锁,而 for update,lock in share mode,update,delete都属于当前读,如果事务中都是用快照读,那么不会产生幻读的问题,但是快照读和当前读一起使用的时候就会产生幻读。
如果都是当前读的话,如何解决幻读问题呢?
truncate table user; INSERT into user VALUES (1,'1',20),(5,'5',20),(15,'15',30),(20,'20',30);
时间 | 事务1 | 事务2 |
begin; | ||
T1 | select * from user where age =20 for update; | |
T2 | insert into user values(25,'25',20);此时会阻塞等待锁 | |
T3 | select * from user where age =20 for update; |
此时,可以看到事务2被阻塞了,需要等待事务1提交事务之后才能完成,其实本质上来说采用的是间隙锁的机制解决幻读问题。
发表评论