什么是数据库事务
数据库事务是指一组数据库操作,这些操作必须被视为一个不可分割的单元,要么全部执行成功,要么全部失败回滚。事务通常由多个SQL语句组成,这些语句可以读取、插入、更新或删除数据库中的数据。事务具有ACID属性:
1. 原子性(Atomicity):事务的所有操作被视为单个原子操作,要么全部执行成功,要么全部执行失败回滚。
2. 一致性(Consistency):事务执行的结果必须使数据库从一个一致性状态转换到另一个一致性状态,其中包括所有数据完整性和约束性规则的应用。
3. 隔离性(Isolation):一个事务的执行不能被其他并发执行的事务干扰,每个事务应该感觉自己在独立地执行。
4. 持久性(Durability):一旦事务提交,其结果应该持久保存在数据库中,即使系统故障也应该如此。
通过实现事务,数据库系统可以确保数据的完整性和一致性,以及并发访问时的正确性。如果一个事务中的任何一个操作失败,整个事务将被回滚到最初的状态,这确保了数据库的一致性。
Mysql如何保证原子性
undo log名为回滚日志,是实现原子性的关键。InnoDB把这些为了回滚而记录的这些东西称之为undo log。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log。undo log主要分为3种:
• Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
• Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
• Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
• 删除操作都只是设置一下老记录的DELETED_BIT,并不真正将过时的记录删除。
• 为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
举个栗子:
sql | undo log |
insert | delete |
delete | insert |
update T set v=3 where v=1 | update T set v=1 where v=3 |
Mysql如何保证持久性
通过Innodb架构解析我们了解到InnoDB 为了提升读写效率,引入了Buffer Pool(缓存池):
- • 当数据库读取数据时,会首先从缓存池中读取
- • 往数据库写入数据时,会先写入缓存池
- • 缓存池中更新的数据会定期刷新到磁盘中
如果MySQL宕机,缓存池中更新的数据还没有刷回到磁盘中,就会导致数据丢失。于是,redo log被引入进来解决这个问题。
图片
1. 先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝。
2. 生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值。
3. 当事务commit时,将redo log buffer中的内容刷新到 redolog file,对 redo log file采用追加写的方式。
4. 定期将内存中修改的数据刷新到磁盘中。
redo与undo在一次事务操作中是如何交互的?假设有A、B两个数据,值分别为1、2,开启事务分别对其进行修改A → 3,B → 4,在提交,过程如下:
事务 | redo&undo logo |
begin; | 开启事务 |
记录A->3到redo log buffer | |
update T set A=3 where A=1; | A修改为3 |
记录A=1到undo log | |
记录B->4到redo log buffer | |
update T set B=4 where B=2; | B修改为4 |
记录B=2到undo log | |
记录A->3到redo log记录B->4到redo log | |
commit; | 事务提交 |
MySQL怎么保证隔离性
事务在并发情形下会互相干扰到的操作大体可以分为两类,与之相对应地,MySQL采用了两种方式来实现它们的隔离:
1. 一个事务的写操作对另一个事务的写操作的影响:锁机制保证隔离性
2. 一个事务的写操作对另一个事务的读操作的影响:MVCC保证隔离性
加锁:读取数据之前,对其加锁,阻止其他事务对数据进行修改
MVCC:不加任何锁,采用多版本并发控制实现,把数据库的行锁和行的多个版本结合起来,可以实现非锁定读,从而提高数据库的并发性能。
事务隔离级别
当数据库上有多个事务同时执行的时候,会带来以下问题:
问题 | 描述 | 举例 |
脏读 | 一个事务读到了另一个事务未提交修改的数据。 | 事务A开始一个更新操作,但是还没有提交,这时事务B读取了这个未提交的数据,就会产生脏读。 |
幻读 | 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据。 | 事务A进行一个范围查询,此时事务B插入了一些符合该范围查询条件的新数据,当事务A再次进行相同的范围查询时,会发现多了一些之前没有的行,就产生了幻读。 |
不可重复读 | 在一个事务中,多次查询的数据不一致。 | 事务A读取了一行数据,然后事务B对这一行数据进行了更新,并且提交了,当事务A再次读取这一行数据时,会发现数据已经发生了变化,就产生了不可重复读。 |
为了避免这些问题的出现,数据库引入了隔离级别的概念,通过对不同隔离级别的设置,可以控制事务之间的隔离程度,从而避免并发问题的产生。不同的隔离级别有不同的特点和使用场景,需要根据实际情况进行选择。
以下是四个标准的事务隔离级别:
隔离级别 | 含义 | 脏读 | 不可重复读 | 幻读 |
读未提交,Read Uncommitted | 事务中的修改,即使没有提交,对其他事务都是可见的 | Y | Y | Y |
读已提交,Read Committed | 事务从开始到提交之前,所做的修改对其他事务都不可见 | N | Y | Y |
可重复读,Repeatable read | 同一事务中多次读取同样的记录结果是一致的 | N | N | Y |
可序列化,Serializable | 在读取的每一行数据上加锁,强制事务串行执行 | N | N | N |
脏读的解决
Innodb是通过在每行数据中增加一个隐藏的事务ID来实现mvcc,当一个事物开始时他会获取一个唯一的事务ID,该事务ID用来标记事务做的修改。当事务读取一行数据时,innodb会检查该行数据事务ID是否小于当前事务ID,如果是说明该行数据是未提交的数据,innodb会阻止该事务读取该行数据,从而避免了脏读的问题。
不可重复读的解决
innodb通过mvcc解决不可重复读的问题,在RR数据库隔离级别下,当我们使用快照进行数据读取的时候,只会在第一次读取的时候生成一个ReadView,后续所有快照读都是使用同一个快照,所以就不会发生不可重复读的问题了。
可重复读模式下举个栗子:事务隔离级别为RR:
图片
创建个测试表,并插入一条数据(1,1,1)
create table table1(
id int(11) not null,
a varchar(50) default null,
b varchar(50) default null,
primary key(id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
分别开启两个事务测试:
事务1 | 事务2 |
启动事务,查询如下: | 启动事务,查询如下: |
~ | 将a改为2,可以查到 |
查询a的结果还是1 | |
~ | 提交事务 |
再次查询a的结果还是1 | |
提交事务,再次查询a的结果变为2了 |
幻读的解决
innodb的mvcc和间隙锁在一定程度上避免了幻读的发生,但是没有办法完全避免,当一个事务读的时候会导致幻读的发生。
幻读的case:
- • 创建一个用户表
create table user(
id int not null,
name varchar(50),
age int,
primary key(id)
);
- • 插入几条数据
insert into user values(1,'张三',10),(2,'李四',20),(3,'王二',30);
- • 分别开启两个事务测试:
事务1 | 事务2 |
begin;select * from user where age >10 and age<40; | |
begin;insert into user value(4,'麻子',25); commit; | |
select * from user where age >10 and age<40; | |
update user set name='呵呵' where age=25;select * from user where age >10 and age<40; |
MVCC实现
每条记录在更新的时候都会同时记录一条回滚操作。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。
MySQL中每条记录,除了我们自定义的字段之外,还有数据库隐藏定义的三个字段:
字段 | 描述 |
DB_TRX_ID | 6字节,最近修改事务id,记录创建这套记录后者最后一次修改该记录的事务id |
DB_ROLL_PTR | 7字节,回滚指针,指向这条记录的上一个版本,用于配合undolog |
DB_ROW_ID | 6字节,隐藏的主键,如果数据表没有主键,那么innodb会生成一个6字节的row_id |
在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
InnoDB 并不会真正地去开辟空间存储多个版本的行记录,只是借助 undo log 记录每次写操作的反向操作。所以B+ 索引树上对应的记录只会有一个最新版本,InnoDB 可以根据 undo log 得到数据的历史版本,从而实现多版本控制。
Read View
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,即可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本
假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
图片
当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。
那么RC、RR级别下的InnoDB快照读有什么不同?
在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。