作为一名Java老司机,应该清楚,数据库事务这个知识点在面试中基本上必问,接下来就带你彻底搞懂他
创作不易,你的关注分享就是博主更新的最大动力, 每周持续更新
微信搜索【企鹅君】关注还能领取学习资料喔,第一时间阅读(比博客早两到三篇)
求关注❤️ 求点赞❤️ 求分享❤️ 对博主真的非常重要
企鹅君原创|GitHub开源项目github.com/JavaDance 欢迎Star和完善
提到事务,你肯定不陌生,和数据库打交道的时候,我们总是会用到事务。最经典的例子就是转账,你要给朋友小王转 100 块钱,而此时你的银行卡只有 100 块钱。
转账过程具体到程序里会有一系列的操作,比如查询余额、做加减法、更新余额等,这些操作必须保证是一体的,不然等程序查完之后,还没做减法之前,你这 100 块钱,完全可以借着这个时间差再查一次,然后再给另外一个朋友转账,如果银行这么整,不就乱了么?这时就要用到“事务”这个概念了。
简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。
只有使用了 Innodb 数据库引擎的数据库或表才支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
事务提交操作
mysql> start transaction;#手动开启事务
mysql> insert into t_user(name) values('pp');
mysql> commit;#commit之后即可改变底层数据库数据, pp成功插入
mysql> select * from t_user;
+----+------+
| id | name |
+----+------+
| 1 | jay |
| 2 | man |
| 3 | pp |
+----+------+
3 rows in set (0.00 sec)
事务回滚操作
mysql> insert into t_user(name) values('yy');
mysql> rollback; #事务回滚后,上面的操作也不去执行了,yy没有成功插入
mysql> select * from t_user;
+----+------+
| id | name |
+----+------+
| 1 | jay |
| 2 | man |
| 3 | pp |
+----+------+
3 rows in set (0.00 sec)
##事务四大特性
一般来说,事务(transaction)是必须满足4个条件(ACID)
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
事务的目的是保障数据的一致性
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。
也就是说 A、I、D 是手段,C 是目的
原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。(比如:A向B转账,不可能A扣了钱,B却没有收到)
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
时间顺序 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询账户余额为2000元 | |
4 | 取款1000元,余额被更改为1000元(未提交) | |
5 | 查询账户余额为1000元(读到了事务B还未提交的脏数据) | |
6 | 事务回滚,取款操作发生未知错误,余额变更为2000元 | |
7 | 转入2000元,余额被更改为3000元(脏读1000+2000) | |
8 | 提交事务 | |
备注 | 按照正常逻辑此时账户应该为4000元 |
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)
例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
时间顺序 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 第一次查询,小明的年龄为20岁 | |
3 | 开始事务 | |
4 | 其他操作 | |
5 | 更改小明的年龄为30岁 | |
6 | 提交事务 | |
7 | 第二次查询,小明的年龄为30岁 | |
备注 | 按照正确逻辑,事务A前后两次读取到的数据应该一致 |
事务在插入已经检查过不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测获取到的数据如同鬼影一般。
时间顺序 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 第一次查询,数据总量为100条 | |
3 | 开始事务 | |
4 | 其他操作 | |
5 | 新增100条数据 | |
6 | 提交事务 | |
7 | 第二次查询,数据总量为200条 | |
备注 | 按照正确逻辑,事务A前后两次读取到的数据总量应该一致,第二次没有插入过查出来却多了100条记录,像见鬼了一样, 产生了幻读问题 |
(1)不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2)幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改
(1)读未提交:read uncommitted
一个事务还没提交时,它做的变更就能被别的事务看到。
最低的隔离级别,脏读、不可重复读或幻读都有可能发生,数据库隔离级别一般都高于该级别
(2)读已提交:read committed
一个事务提交之后,它做的变更才会被其他事务看到。
可以阻止“脏读”, 但是幻读或不可重复读仍有可能发生。
(3)可重复读:repeatable read
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
可以阻止“脏读”和“不可重复读”,但幻读仍有可能发生。
InnoDB引擎默认隔离级别
(4)串行化:serializable
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
读未提交 Read uncommitted | 可能 | 可能 | 可能 |
读已提交 Read committed | 不可能 | 可能 | 可能 |
可重复读 Repeatable read | 不可能 | 不可能 | 可能 |
串行化 Serializable | 不可能 | 不可能 | 不可能 |
事务的原子性是通过undolog来实现的
事务的持久性性是通过redolog来实现的
事务的隔离性是通过(读写锁+MVCC)来实现的
事务的终极大 boss 一致性是通过原子性,持久性,隔离性来实现的!!!
原子性,持久性,隔离性的目的也是为了保障数据的一致性!
总之,ACID只是个概念,事务最终目的是要保障数据的可靠性,一致性。
下面我首先讲实现事务功能的三个技术,分别是日志文件(redo log 和 undo log),锁技术以及MVCC,然后再讲事务的实现原理,包括原子性是怎么实现的,隔离型是怎么实现的等等。最后在做一个总结,希望大家能够耐心看完
redo log叫做重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。
– 银行卡账户表 bank –
id | name | balance |
---|---|---|
1 | zhangsan | 1000 |
--理财账户表 finance –
id | name | amount |
---|---|---|
1 | zhangsan | 0 |
start transaction;
select balance from bank where name="zhangsan";
// 生成 重做日志 balance=600
update bank set balance = balance - 400;
// 生成 重做日志 amount=400
update finance set amount = amount + 400;
mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Boffer Pool(缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步。
那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及执行上面图中红色的操作。这样会导致丢部分已提交事务的修改信息!
所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。
undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
还用上面那两张表
每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。
undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。
当有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时必须有一种措施来进行并发控制。不然很有可能会造成不一致。
读写锁
解决上述问题很简单,只需用两种锁的组合来对读写请求进行控制即可,这两种锁被称为:
共享锁(shared lock),又叫做"读锁"
读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。
排他锁(exclusive lock),又叫做"写锁"
写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。
读锁 | 写锁 | |
---|---|---|
读锁 | 可并行 | 不可并行 |
写锁 | 不可并行 | 不可并行 |
总结:通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行
MVCC(Multiversion Concurrency Control),多版本并发控制。它和undo log中的版本链息息相关,MVVC通过数据行的多个版本来实现数据库的并发控制。
简单的说就是当前事务查询另一个事务正在更改的行(如果此时读取就会发生脏读),不用加锁等待,而是读取该数据的历史版本,降低响应时间。
MVCC的实现依赖于Undo日志和Read View。下面我们先来详细介绍一下这两个机制。
undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
Undo存放在数据库内部的一个特殊段(segment)中,这个段称为Undo段(undo segment)。Undo段位于系统表空间内,也可以设置为Undo表空间。
Undo日志保存了记录修改前的数据,并且用两个隐藏字段trx_id和roll_pointer把这些Undo日志串联起来形成一个历史记录版本链(参考图1)。
有了undo log就可以读取到记录的历史版本,那么在什么情况下,读取哪个版本的记录呢?这就用到了Read View,它帮我们解决了行的可见性问题。
Read View就是当某个事务在使用MVVC机制进行快照读操作时产生的读视图。该视图是数据库当前所有活跃事务id(还未提交的事务)组成的列表的一个快照。
四种隔离级别里,读未提交和串行化是不会使用MVVC的,因为读未提交直接读取某个数据的最新数据即可,串行化是通过加锁来读的。
读已提交和可重复读都必须保证读到的数据都是其他事务提交了的,所以,其他事务修改了数据但是还未提交,我们不能够访问该数据,但可以通过MVVC机制读取该记录的历史版本,核心问题就是需要判断版本链中的哪条历史版本是当前事务可见的,这也是ReadView要解决的问题。
Read View包含4个比较重要的内容:
只有事务对表中的记录做修改时才会为事务分配事务id,否则一个事务中只有读操作,该事务的id默认为0。
注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1, 2,5这三个事务,之后id为5的事务提交了。那么一个新的读事务在生成ReadView时, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是6。
版本链
当某个事务有了Read View,访问某条记录时,需要按照下面的步骤判断该记录的哪个版本可见:
了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:
在隔离级别为读已提交时,一个事务中的每一次SELECT查询都会重新获取一次Read View,而可重复读是第一SELECT操作才会生成Read View,之后的查询操作复用这一个。
导致这两种的差距是因为:可重复读要保证一个事务中相同的SELECT读取的内容是相同的。
COMMITTED隔离级别下
现在有两个事务id分别为10、20的事务在执行:
-- id为10的事务
begin;
update t set name='李四' where id=1;
update t set name='王五' where id=1;
-- id为20的事务
更新其他行的数据
此刻,表中id为1的记录得到的版本链表如下所示:
此时新来一个事务执行如下操作:
begin;
select * from t where id=1;
-- 事务10、20未提交
查询到的结果为张三。
具体的过程如下:
接下来,再将id为10的事务进行commit提交。然后id为20的事务来更新记录:
begin;
-- id为20的事务
update t set name='赵六' where id=1;
update t set name='钱七' where id=1;
此时版本链更新为:
再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个id 为1的记录,得到的结果为name=王五的那条记录。执行过程如下:
注意:READ COMMITTED,每次读取数据前都生成一个新的ReadView。
假如此时id为10的事务和id为20的事务正在修改,都未提交,修改内容和前面的一样,但是还未提交,此时当前事务做一个查询。
步骤为:
此时,id为10的记录提交事务。
当前事务又需要select id为1的记录,步骤为:
注意:REPEATABLE READ,每次读取都复用第一次生成的Read View
假设现在有一条数据,id为1
当前活跃的事务有10和20。
此时当前事务启动了,执行如下SQL语句:
begin;
select * from student where id>=1;
在开始前生成Read View,内容如下:creator_trx_id=0,trx_ids= [10,20] , up_limit_id=10, low_limit_id=21。
由于id大于等于1的数据只有一个,且该数据的trx_id为8,小于up_limit_id,所以可以读取到。
在这之后id为10的事务新增了一行数据,增加了id为2的数据,且提交了。
此时当前线程继续查找id>=1的数据,因为是可重复读,复用刚刚的Read View。
得到两行数据,但是因为id为2的数据trx_id为10,该值在Read View的trx_ids中存在,所以该记录对当前事务不可见,所以最后查询到的数据只有一条记录。
如果当前事务再插入id为2的数据就插不进去,所以说MVVC只解决了一半的幻读问题。
参考文章:
《Mysql 实战45讲》
Mysql事务
MySQL MVVC多版本并发控制的实现详解_Mysql
创作不易,你的关注分享就是博主更新的最大动力, 每周持续更新
微信搜索【 企鹅君 】第一时间阅读(比博客早一到两篇), 关注还能领取资料
求关注❤️ 求点赞❤️ 求分享❤️ 对博主真的非常重要
企鹅君原创|GitHub开源项目github.com/JavaDance 欢迎Star和完善
本文由mdnice多平台发布