【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过下方给出的链接进行查看~
按照惯例——首先要做几点说明:
1、什么是事务?
答
在计算机术语中,事务(Transaction)指的是访问并可能更新数据库中各种数据项的一个程序执行单元。
我们为什么需要事务?
事务是为了解决数据安全操作提出的解决方案,事务的控制实际上就是控制数据的安全访问与隔离。
举一个简单的例子:
银行转账,A 账户将自己的 1000 元转账给 B ,那么业务实现的逻辑首先是将 A 的余额减少 1000,然后往 B 的余额里增加 1000,假如这个过程中出现意外,导致过程中断,A 已经扣款成功,B 还没来得及增加,就会导致 B 损失了1000 元。所以我们必须做出控制,要求 A 账户转帐业务撤销,这才能保证业务的正确性,完成这个操作就需要事务,将 A 账户资金减少和 B 账户资金增加放到同一个事务里,要么全部执行成功,要么全部失败,这样就保证了数据的安全性。
事务有四大特性(ACID):
原子性是指事务是应用中不可再分的最小执行体,即事务包含的一系列操作要么全部成功,要么全部失败并回滚(rollback),绝对不存在部分成功或者部分失败的情况。
一致性指的是:保证事务遵循对数据的完整性约束。完整性约束在数据库层面上包含主键约束,唯一索引约束,外键约束等,在业务逻辑上是我们通过程序人为规定的。事务的一致性保证了事务会从一个正确有效的状态转移到另一个正确有效的状态。而正是这些约束规定了一个状态是正确有效的还是错误的~
隔离性是最重要的一点,当有多个用户以并发的方式操作数据库时,事务是彼此隔离的,不会被其他的事务干扰或影响。
永久性是指一个事务一旦提交,那么对数据库中数据的改变就是永久的,即便数据库发生了故障也不会丢失事务提交的数据。
接下来,我们来看一下事务的隔离级别。
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read),不可重复读(non-repeatable read),幻读(phantom read)这些问题,为了解决这些问题,就有了 MySQL 隔离级别的概念。在了解 MySQL 的隔离级别之前,我们先来了解一下什么是脏读,幻读与不可重复读。
1. 脏读
如上图所示,脏读是指当事务 A 正在访问数据,并且对数据进行了修改,而这个修改还没有提交到数据库中,此时另一个事务 B 也访问到了这个数据,然后使用了这个数据,结果事务 A 发生回滚,那么事务 B 读到的就是一个“脏数据”。
2. 不可重复读
不可重复读是指在一个事务内 ,多次读同一数据。例如:事务 B 读取某一数据,在事务 B 还没有结束时,另外一个事务 A 也访问了该同一数据,并且修改了这一数据。那么,在事务 B 的两次读数据之间,由于事务 A 对数据的修改,导致事务 B 两次读到的的数据可能是不一样的,这就是不可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。
3. 幻读
例如上图所表示的示例中,事务 B 读到 id 小于 10 的记录有 3 条,事务 A 又插入了一条记录,那么事务 B 再次执行相同的查询时,就会发现多了一行。简而言之,幻读指的是一个事务在前后两次查询同一个范围的数据时,后一次查询看到了前一次查询没有的行。
事务具有隔离性,不同的隔离级别对以上三种问题的隔离能力不同,事务的隔离级别越高,就越安全,与此同时,效率也会越低。
事务具有以下四种隔离级别:
读未提交这种隔离级别指的是,一个事务还没有提交时,它所做的变更可以被其他事务“看到”。读提交是指,一个事务提交之后,它所做的变更才会被其他事务看到。可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。串行化则是最高的一个隔离级别,串行化是对于同一行记录加锁,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成释放锁后,才能继续执行。
不同的隔离级别对脏读,不可重复读,幻读的预防能力如下表所示(这是 ANSI SQL STANDARD 给出的对于各种隔离级别下允许出现并发事务问题的规定,对于在 RR 级别下通过 Next-Key 锁是否可以真正地防止幻读,笔者会认真研究后给大家一个交代,并在后续对这部分内容进行补充):
来看一个示例:
create table T(c int) engine=InnoDB;
insert into T(c) values(1);
我们有数据表 T ,T 只有一列;并且,T 表中有我们插入的一条数据。
大家不妨停下来思考一下,在不同的隔离级别下,图中 V1,V2,V3 的值分别是什么?
如果事务的隔离级别为 RU,V1 的值 就是 2。此时虽然事务 B 还没有提交,但是结果已经被 A 看到了。这就导致了脏读,因此,V2,V3 的值也都是 2。
如果隔离级别是 RC,脏读就可以避免了,V1 的值是 1;但是在事务 B 提交之后,事务 A 查询得到的值 V2 就是 2,这就导致了原始读取不可重复,即:不可重复读。V3 的值也是 2。
如果隔离级别为 RR,则 V1,V2 都是 1,V3 是 2。因为隔离级别设置为可重复读,就要求事务在执行期间看到的数据前后必须是一致的,所以 V2 查询的值为 1。
如果隔离级别是“串行化”,则事务 B 在执行 “将 1 改成 2” 的时候,会被锁住。直到事务 A 提交之后,事务 B 才可以继续执行。所以 V1,V2 的值是 1,V3 的值是 2。
那么在 MySQL 客户端中要如何设置事务的隔离级别呢?
设置事务隔离级别可以使用语句:
set session transaction isolation level read committed; # read uncommitted,repeatable read,serializable
InnoDB 存储引擎默认的事务隔离级别为 RR(Repeatable Read)。
总结
本题涵盖的面试问题包括:
这些问题并不难,都是标准的“八股文”。只需要在理解的基础上熟记即可~
2、Repeatable Read 级别下,事务的可重复读能力是如何实现的?
答
其实这句话要说出来还得有个前提,那就是 RR 实现的可重复读的能力是基于“快照读”方式下的。
是不是有点一头雾水?少年莫慌,我们先来认识一下当前读(Current Read)与快照读(Snapshot Read)分别是啥子东西?
当前读包括:select ... lock in share mode
(s 锁);select ... for update
,update
,delete
,insert
(x 锁)。以上操作都是当前读。为啥叫当前读?因为这些操作读取的行都是“最新版本”的数据,并且还会加锁,来保证其他并发事务对其无法造成修改。
那什么是快照读呢?
我们知道,“当前读”是通过悲观锁实现的,加锁便会影响性能。InnoDB 存储引擎为了提高并发的效率,使用了 MVCC(Multi-Version Concurrency Control) 机制,在事务并发时,通过读取视图(read view),以不加锁的方式来实现一致性读(Consistent Nonlocking Read)。
简而言之,不加锁的 select
操作就是快照读了,你也可以叫它非阻塞读。首先,快照读的前提是你将隔离级别设置为 RC 或者 RR。因为在 RU 的隔离级别下,事务读取的数据总是最新的,而在 Serializable 这个隔离级别下,即使是普通的 select
语句也会加 s 锁,也就不存在快照读这一码事。
说了这么多,我们不妨先来看一个示例,回顾一下什么是不可重复读。
现在我们有一张 test 表:
create table test
(
id int(11) not null auto_increment,
name varchar(20) default null,
balance int(11) default '0',
primary key (id)
) engine = InnoDB
charset = utf8;
test 表中的三个字段分别为 id,姓名以及账户余额,且表中有插入的一条数据:
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jack | 1000 |
+----+------+---------+
目前我开启了两个 MySQL 客户端窗口,并将这两个 Session 的事务隔离级别设置为 RC:
set session transaction isolation level read committed;
接下来我在这两个 Session 中执行了如下操作:
Session1
start transaction;
# T1
select * from test where id = 1;
# T3
select * from test where id = 1;
# T4
commit;
Session2
start transaction;
# T2
update test set balance = 500 where id = 1;
commit;
Session1 ,T1 时刻查询的结果为:
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jack | 1000 |
+----+------+---------+
T3 时刻查询的结果为:
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jack | 500 |
+----+------+---------+
我们看到,在 Session1 的事务还未提交时,Session2 的事务对数据做了更新并提交,Session1 事务读取到了 Session2 事务对数据的变更,这就是不可重复读。
接下来,我们将 Session1 和 Session2 都设置为 RR 这个隔离级别:
set session transaction isolation level repeatable read ;
在这两个 Session 中执行如下的 sql 操作:
Session1
start transaction;
# T1
select * from test where id = 1;
# T3
select * from test where id = 1;
# T4
commit;
Session2
start transaction;
# T2
update test set balance = 1000 where id = 1;
commit;
Session1 ,T1 时刻查询的结果为:
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jack | 500 |
+----+------+---------+
T3 时刻查询的结果为:
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jack | 500 |
+----+------+---------+
这时,我们发现,在 RR 隔离级别下实现了可重复读,即在事务未提交时,前后读取数据保持一致。不过,大家可以想一下,如果在 RC,RR 这两个隔离级别,我将 T3 时刻的语句改变为:
select * from test where id = 1 lock in share mode;
在 Session1 的 T3 时刻会出现什么结果?
答案为:
RC 隔离级别下,T3 时刻查询的结果为:
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jack | 500 |
+----+------+---------+
RR 隔离级别下,T3 时刻查询的结果为:
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jack | 1000 |
+----+------+---------+
原因非常简单,我在上文中已经强调说明了,当前读方式读到的数据是最新版本的。那么,当我们使用 select ... lock in share mode
读取到的数据一定是更新过的。所以,我才会说,RR 实现的可重复读的能力是基于“快照读”方式下的。那么,究竟为什么在 RC,RR 这两种隔离级别下,T3 时刻快照读的结果会不同?它们实现快照读的机制是什么? RR 级别下的事务为什么可以实现可重复读?
再跟你回答并解释这一篮子问题前,咱们先来谈一个东西—— MVCC。
MVCC(Multi-Version Concurrency Control)即:多版本并发控制。啥意思呢?你可以理解为不同的事务对一条记录执行快照读时,MVCC 决定了返回给你哪个版本的数据,并且 MVCC 控制管理着这些版本快照,决定啥时候保留它们,啥时候删除它们。咋实现的呢?主要通过以下几个东西:
DB_TRX_ID
,DB_ROLL_PTR
,DB_ROW_ID
三个字段先来说一下这三个字段分别是啥含义。我们知道数据表的每一行记录除了我们自定义的 field 外,还有一些隐式的字段。与 MVCC 相关的主要就是 DB_TRX_ID
,DB_ROLL_PTR
,DB_ROW_ID
。DB_TRX_ID
表示最近修改事务的 ID,它用于记录最后一次修改该记录的事务 ID;DB_ROLL_PTR
为回滚指针,配合 undo log 指向该记录的上一个版本,从而形成 undo log 链;DB_ROW_ID
是隐含的主键 ID,如果我们创建的数据表没有设置主键,那么 InnoDB 引擎便偷偷使用 DB_ROW_ID
作为主键。
undo log 即回滚日志。我们都知道 redo log(不知道的童鞋可以看下我的系列文章),当我们对数据进行修改时,有一个重要的原则叫作 WAL(写前日志),这里面除了会记录 redo log 日志也会记录对应的 undo log 日志,如果因为某些原因导致事务失败回滚,就可以通过 undo log 恢复到数据更新前的状态。undo log 除了在事务回滚中起着重要的作用之外,也是用于 MVCC 的版本控制链 。
假设我们的数据表中有这样的一行数据:
事务 1 将该行 Field2 字段的值改为 24,整个流程大概是这个样子的:首先使用排它锁锁定这一行;然后将该行拷贝到 undo log 日志中;拷贝完成后,修改字段值,更新 DB_TRX_ID
,并将该行 DB_ROLL_PTR
指针指向 undo log 副本记录的 DB_ROW_ID
;最后事务提交,释放锁:
事务 2 将 Field3 字段的值改为 45,整个流程示意图如下:
从上面的示意图,我们可以看出不同的事务在对一条记录进行修改时,undo log 会形成一条版本线性表,而这个版本线性表就是 MVCC 的版本控制链。
那什么是 read-view 呢?简单地理解,read-view 是当前所有活跃事务的一个集合,大概长这样:
read-view 的功能就是决定在不同事务的隔离级别下,数据的可见性。read-view 中有两个字段: m_up_limit-id
表示“低水位”,m_low_limit-id
表示”高水位“。如果当前读取出来的数据行上的 DB_TRX_ID
小于“低水位”时,那就表示这条记录最后的修改在 read-view 创建之前,即:是一个已提交的事务,所以这条记录就是可见的;如果当前读取出来的数据行上的 DB_TRX_ID
高于“高水位”时,那就表示这条记录最后的修改在 read-view 创建之后,即:是一个还未开始的事务,所以这条记录不可见。那么如果说,读取出来的数据行上的 DB_TRX_ID
在“低水位”和“高水位”之间,那 read-view 就会判断 DB_TRX_ID
是否在当前 read-view 的未提交事务集合中,如果不存在那就是可见的;如果存在,那就通过 DB_ROLL_PTR
回滚指针去 undo log 链中继续取出历史版本的 DB_TRX_ID
再进行比较... ...
讲完了 MVCC 之后,接下来我就可以告诉你本题的答案了。
在 RC 隔离级别下,事务每次执行快照读 select 时,都会重新在当前系统中构建一个新的 read-view。一个事务在还未提交的情况下,两次查询之间如果有别的事务提交了,那么两次查询使用的 read-view 不一样,就会导致两次 select 查到的结果不一样。
而在 RR 隔离级别下,事务首次执行快照读 select 时,会在当前系统中生成一个 read-view。之后这个事务的其他 select 查询都将共用这个视图!所以,在 RR 这个隔离级别下,事务具有可重复读的能力。
总结
又是絮絮叨叨了很多内容。我来给大家总结一下吧~
本题涵盖的面试问题包括:
相信你可以轻松作答哦~
感谢阅读,由于篇幅有限以上面经资料博主已经整理打包好了,这些知识点的导图和问题的答案详解的PDF文档都可以免费分享给大家,点赞收藏文章后,私信【资料】免费领取!