数据库的事务

什么是事务

事务的典型场景

月末了,快活不下去了,急需要老父亲给我们生活费
假如老父亲给我门转账转了1500元
那么这个过程当中就涉及到两个对数据库的操作
老父亲的账户余额减1500
我们自己的账户余额加1500

这两个操作是同时成功同时失败的,只要老父亲账户余额减少,我们的账户余额就增加。

单独一个操作的成功或失败都是不合理的,比如说老父亲的账户余额减少了,而我们的账户余额却没有增加。这中情况老父亲就会以为你在骗他钱。假如说老父亲的账户余额没有减少而我们的账户余额还是增加了,当然这种情况最好,但是也是不合理的(我觉得特别合理)。

定义

数据库事务是访问并可能更新数据库中各种数据项的一个程序执行单元,由一个有限的数据库操作序列构成。

目的

  1. 为数据库操作提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法
  2. 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此操作相互干扰

成功情况下: 能将数据从一种状态变为另外一种状态,并且能够持久化

异常情况下:

  1. 能将数据恢复到正常状态
  2. 要能保证一致性,包含数据的一致性和约束的一致性

并发情况下: 并发的操作之间不能产生相互影响

事务的使用

事务的开启

首先,当我们在执行单条增删改语句时,会自动开启事务并且会自动提交,所以最终写入了磁盘

或者手动开启事务,两种方式:

  1. begin
  2. start transaction

事务的结束

首先,如果客户端断开连接了,自然就会结束事务

或者手动结束事务:

  1. 回滚事务,使用 rollback
  2. 提交事务,使用 commit

理论有效,实践开始

首先创建一张表,并插入数据

create table bank(	
	name varchar(20),
	money decimal(5,1)
);

 insert into bank values
 	('dad',5000),
 	('son',500); 

查询该表就可以看到数据

select * from bank;

// 数据如下
+------+------+
| name |money |
+------+------+
|  dad | 5000 |
+------+------+
|  son |  500 |
+------+------+

那么让我们开启事务并进行转账

//开启事务
begin;
// 成功会提示
// Query OK, 0 rows affected (0.00 sec) 

update bank set money=money-1500 where name='dad';
//成功会提示
//Query OK, 1 row affected (0.00 sec) 
//Rows matched: 1 Changed: 1 Warnings: 0 

// 此时打开另一个终端查询表中数据是没有变化的,因为还没有提交

update bank set money=money+1500 where name='son';

// 提交之后在另一个终端就可以看到此时数据才发生变化
// 或者此处用 rollback 就可以看到数据不会变化
commit;

事务的特性

原子性: 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行

一致性: 事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束

隔离性: 多个事务并发执行时,一个事务的执行不应影响其他事务的执行

持久性: 一个事务一旦提交,他对数据库的修改应该永久保存在数据库中

事务的并发异常

回滚丢失/第一类更新丢失

此种更新丢失的是因为回滚的原因,所以也叫回滚丢失

事务A事务B
查询数据值得到初始值10查询数据得到初始值10
更新为10+1=11更新为10+2=12
提交回滚失败
数据回到初始状态10,导致事务A的更新丢失了,由于回滚导致丢失所以叫做回滚丢失

覆盖丢失/第二类更新丢失

此种更新丢失是因为更新被其他事务给覆盖了,也可以叫覆盖丢失

事务A事务B
查询数据得到初始值10查询数据得到初始值10
更新10+1=11更新10+2=12
提交提交
最终结果变为12,由于事务B是从10开始计算的,并不知道数据已经发生了变化,所以导致事务A的更新结果被覆盖了,所以叫覆盖更新

脏读

此种异常是因为一个事务读取了,另一个事务修改了但是未提交的数据

事务A事务B
查询数据得到初始值10查询数据得到初始值10
更新为10+1=11再次读取数值的到11
更新数据11+1=12
回滚提交
最终数据变为12,由于读取到的数据11被回滚了,结果是错误的,读取到了脏数据

不可重复读

一个事务对同一行数据执行了两次或更多次查询,但是却得到了不同的结果

事务A事务B
查询数据得到初始值10查询数据得到初始值10
更新为10+1=11再次读取数据得到11
在一个事务内两次对同一条数据查询,得到的结果是不同的,因此叫做不可重复读

幻读

幻读和不可重复读有点像,只是针对的不是数据的值而是数据的数量

事务A事务B
查询数据有11条查询数据有11条
更新全部数据为1插入一条数据
查询结果有1条没被更新,比原来多了一条
仿佛全表更新语句不正常,自己出现了幻觉,因此叫幻读

事务隔离级别

读未提交: 该隔离级别指即使一个事务更新语句没有提交,但是别的事务可以读到这个改变

读已提交: 指一个事务只能看到其他事务的已经提交的更新,看不到未提交的更新,消除了脏读和第一类丢失更新,这是大多数数据库的默认隔离级别,如Oracle SqlServer

可重复读: 该隔离级别指一个事务中进行两次或多次同样的对于数据内容的查询,得到的结果是一样的,但不保证对于数据条数的查询是一样的,只要存在读,该行数据就禁止写,消除了不可重复读和第二类更新丢失,这是MySql数据库的默认隔离级别

串行化: 事务执行的时候不允许别的事务并发执行,完全串行化的读,只要存在读就禁止写,但可以同时读,消除了幻读。这是事务隔离级别的最高级别。虽然最安全最省心,但是效率太低,一般不会用

各种隔离级别能解决哪些并发异常

隔离级别 \ 并发异常回滚丢失脏读不可重复读覆盖丢失幻读
读未提交×××××
读已提交×××
可重复读×
串行化

mysql为什么选择 可重复读 为默认隔离级别

首先说一些 mysql InnoDB对隔离界别的支持

事务隔离级别脏读不可重复读幻读
读未提交(Read Uncommitted)可能可能可能
读已提交(Read Committed不可能可能可能
可重复读(Repeatable Read)不可能不可能对InnoDB不可能
串行化(Serializable)不可能不可能不可能

InnoDB 支持的四个隔离级别和 SQL92 定义的完全一致,隔离级别越高事务的并发度就越低。唯一的区别就在于,InnoDB在 RR 的级别就解决了幻读的问题。也就是说,不需要使用串行化的隔离级别就可以解决所有问题,既保证了数据的一致性,又支持较高的并发度。这个就是 InnoDB 默认使用 RR 作为事务隔离级别的原因。

mysql是如何实现事务的提交、回滚,怎样保证数据的一致性?

首先说如何实现事务的提交和回滚
数据库为了保证事务的原子性和持久性,引入了 redo log和 undo log

redo log和 undo log包括两部分:一个是内存中的日志缓冲(redo log buffer / undo log buffer),另一个是磁盘上的日志文件(redo log file / undo log file)。mysql每执行一条语句,先将记录写入redo log buffer / undo log buffer,后续某个时间点再一次性将多个操作记录写到redo log file / undo log file。

在计算机操作系统中,用户空间(user space)下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间(kernel space)缓冲区(OS Buffer)。因此,redo log buffer / undo log buffer写入redo log file / undo log file实际上是先写入OS Buffer,然后再通过系统调用fsync()将其刷到redo log file / undo log file中,过程如下
在这里插入图片描述

redo log

redo log叫做重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。

SQL操作数据库之前,会先记录重做日志,为了保证效率会先写到日志缓冲区中(redo log Buffer),再通过缓冲区写到磁盘文件中进行持久化,既然有缓冲区说明数据不是实时写到redo log file中的,那么假如redo log写到缓冲区后,此时服务器断电了,那redo log岂不是会丢失?

在MySQL中可以自已控制log buffer刷新到log file中的频率,通过innodb_flush_log_at_trx_commit参数可以设置事务提交时log buffer如何保存到log file中,innodb_flush_log_at_trx_commit参数有3个值(0、1、2),表示三种不同的方式

  • 为0表示事务提交时不会将log buffer写入到os buffer中,而是每秒写入os buffer然后调用fsync()方法将日志写入log file,这种方式在MySQL系统崩溃时会丢失大约1秒钟的数据

  • 为1表示事务每次提交都会将log buffer写入到os buffer,并调用操作系统的fsync()方法将日志写入log file,这种方式的好处是就算MySQL崩溃也不会丢数据,redo log file保存了所有已提交事务的日志,MySQL重新启动后会通过redo log file进行恢复。但这种方式每次提交事务都会写入磁盘,IO性能较差

  • 为2表示事务每次提交仅将log buffer写入到os buffer中,然后每秒调用fsync()方法将日志写入log file,这种方式在MySQL崩溃时也会丢失大约1秒钟的数据

undo log

undo log是回滚日志,用来回滚行记录到某个版本,undo log一般是逻辑日志,根据行的数据变化进行记录,可以简单的理解为:当insert一条记录时,undo log会记录一条对应的delete语句;当update一条语句时,undo log记录的是一条与之操作相反的语句

undo log跟redo log一样也是在SQL操作数据之前记录的,也就是SQL操作先记录日志,再进行操作数据

当事务需要回滚时,可以从undo log中找到相应的内容进行回滚操作,回滚后数据恢复到操作之前的状态

保证数据的一致性

如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?

总体上来说,我们有两大类的方案。

LBCC

既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制 Lock Based Concurrency Control(LBCC)。

如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。

MVCC

如果要让一个事务前后两次读取的数据保持一致,那么我们可以再修改数据之前给它建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control(MVCC)。

MVCC只在 读已提交和可重复读两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,因为 读未提交总是读取最新的数据行,而不是符合当前事务版本的数据行。而串行化则会对所有读取的行都加锁。

聚簇索引记录中有两个必要的隐藏列:
trx_id:用来存储每次对某条聚簇索引记录进行修改的时候的事务id

roll_pointer:每次对哪条聚簇索引记录有修改的时候,都会把老版本写入 undo 日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息(注意插入操作的 undo操作日志没有这个属性,因为它没有老版本)

MVCC 的原则:
一个事务能看到的数据版本:

  1. 第一次查询之前已经提交的事务的修改
  2. 本事务的修改

一个事务不能看到的数据版本:

  1. 在本事务第一次查询之后创建的事务(事务 ID 比我的事务 ID 大)
  2. 活跃的(未提交的)事务的修改

MVCC 的效果:我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。而在我这个事务之后新增的数据,我是查不到的。

所以我们才把这个叫做快照,不管别的事务做任何增删改查的操作,它只能看到第一次查询时看到的数据版本。

问题:这个快照是怎么实现的呢?会不会占用额外的存储空间?
下面我们来分析一下 MVCC 的原理。首先,InnoDB的事务都是有编号的,而且会不断递增。

InnoDB 为每行记录都实现了两个隐藏字段:

DB_TRX_ID(6字节):事务ID,数据是在哪个事务插入或者修改为新数据的,就记录为当前事务 ID。

DB_ROLL_PTR(7字节):回滚指针(我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID,没有修改或者删除的时候是空,注意插入操作的 undo操作日志没有这个属性,因为它没有老版本)


拿个栗子来说,现在有一张表

idnameDB_TRX_IDDB_ROLL_PTR

第一个事务,初始化数据

begin;
insert into mvcctest values(1,'qingshan');
insert into mvcctest values(2,'huihui');
commit;

此时的数据,创建版本是当前事务ID(假设事务编号是1),删除版本为空

idname创建版本删除版本
1唐三藏1undefined
2孙悟空1undefined

第二个事务,执行第一次查询,读取到两条原始数据,这个时候事务ID是2

begin;
select * from mvcctest;  // 第一次查询

第三个事务,插入数据

begin;
insert into mvcctest values(3,'猪八戒');
commit;

此时的数据,多了一条猪八戒,他的创建版本号是当前事务id(3)

idname创建版本删除版本
1唐三藏1undefined
2孙悟空1undefined
3猪八戒3undefined

第二个事务,执行第二次查询

select * from mvcctest;  // 第二次查询

MVCC的查询规则:只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事务 ID 的行(或未删除)。

也就是不能查到在我的事务开始之后插入的数据,猪八戒 的创建 ID 大于 2,所以还是只能查到两条数据。

第四个事务,删除数据,删除 id=2,孙悟空这条记录

begin;
delete from mvcctest where id=2;
commit;

此时的数据,孙悟空 的删除版本被记录为当前事务 ID (4),其他数据不变:

idname创建版本删除版本
1唐三藏1undefined
2孙悟空14
3猪八戒3undefined

在第二个事务中,执行第 3 次查询:

select * from mvcctest;  //第三次查询

查找规则:只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事务 ID 的行(或未删除)。

也就是,在我事务开始之后删除的数据,所以 孙悟空 依然可以查出来。所以还是这两条数据。

第五个事务,执行更新操作,这个事务 ID 是5:

begin;
update mvcctest set name='杨过' where id=1;
commit;

此时的数据,更新数据的时候,旧数据的删除版本被记录为当前事务 ID (5)(undo),产生了一条新数据,创建 ID 为当前事务 ID 5:

idname创建版本删除版本
1唐三藏15
2孙悟空14
3猪八戒3undefined
1杨过5undefined

第二个事务,执行第 4 次查询:

select * from mvcctest;  //第四次查询

查找规则:只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事务 ID 的行(或未删除)。

因为更新后的数据 杨过 创建版本大于 2,代表是在事务之后增加的,查不出来。

而旧数据 孙悟空 的删除版本大于 2,代表是在事务之后删除的,可以查出来。


通过以上演示我们能看到,通过版本号的控制,无论其他事务是插入、修改、删除,第一个事务查询到的数据都没有变化。这个就是 MVCC 的效果。

回想一下MVCC规则:
一个事务能看到的数据版本:

  1. 第一次查询之前已经提交的事务的修改
  2. 本事务的修改

一个事务不能看见的数据版本:

  1. 在本事务第一次查询之后创建的事务(事务 ID 比我的事务 ID 大)
  2. 活跃的(未提交的)事务的修改

所以,我们必须要有一个数据结构,把本事务 ID、活跃事务 ID、当前系统最大事务 ID 存起来,这样才能实现判断。这个数据结果就叫 Read View(可见性视图),每个事务都维护一个自己的 Read View。

m_ids: 表示在生成 ReadView 时,当前系统中活跃的读写事务的事务 id 列表。

min_trx_id: 表示在生成 ReadView 时,当前系统中活跃的读写事务中最小的事务 id,也就是 m_ids 中的最小值。

max_trx_id: 表示生成 ReadView 时,系统中应该分配给下一个事务的 id 值。

creator_trx_id: 表示生成该 ReadView 的事务的事务 id。

有了这个数据结构之后,事务判断可见性的规则是这样的:

  1. 从数据的最早版本开始(undo log)。
  2. 数据版本的 trx_id = creator_trx_id,本事务修改,可以访问。
  3. 数据版本的 trx_id < min_trx_id(未提交事务的最小 ID),说明这个版本在生成 ReadView 已经提交,可以访问。
  4. 数据版本的 trx_id > max_trx_id(下一个事务 ID),这个版本是生成 ReadView 之后才开启的事务建立的,不能访问。
  5. 数据版本的 trx_id 在 min_trx_id 和 max_trx_id 之间,看看是否在 m_ids 中。如果在,不可以。如果不在,可以。
  6. 如果当前版本不可见,就找 undo log 链中的下一个版本。

注意:
RR 中 Read View 是事务第一次查询的时候建立的。RC 的 Read View 是事务每次查询的时候建立的。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值