首先 什么是事务?
MySQL中的事务是由一组SQL语句组成的逻辑处理单元,这个工作单元中的所有操作,要么都成功,要么都失败。
数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作。这样就会带来一些问题 (如 脏读、脏写 下面我们详细介绍)。
为了解决多事务的并发问题,数据库设计了
- 事务隔离机制、
- 锁机制、
- MVCC多版本并发控制隔离机制
等来解决多事务并发问题。
在事务这个逻辑工作单元中,所有的操作看做是一个整体。要么整体都成功 要么都失败。
那么 事务用于做什么?
- 保证业务的完整性
- 保证数据的正确性
事务拥有四大特性:
- 原子性
- 一致性
- 隔离性
- 持久性
下面我们详细介绍一下 事务的特性。
1.原子性
事务是一个原子操作单元,其对数据的修改,要么全都执行、要么全都不执行(在操作层面,所有操作 要么全部成功 要么全部失败)
通过undolog(回滚日志)实现事务的原子性
2.一致性
- 数据层面:在事务开始和完成时,数据都必须保持一致状态
- 引擎层面:undolog(回滚日志)、redolog(重做日志)
- 服务层面:binlog(归档日志)。
3.隔离性
数据库系统提供了一定的隔离机制(mysql默认的隔离级别是 repeatable-read 可重复读),保证事务在不受外部并发操作影响的“独立”环境执行(事务处理过程中的中间状态外部是不可见的)
事务隔离级别有哪些呢?
read-uncommitted 读未提交
read-committed 读已提交
repeatable-read 可重复读
serilizable 可串行化
从名字可以看出 分出了严谨的级别,数据库的事务隔离 越严格,并发副作用就越小。但是付出的代价 就越大。
比如串行化 无论进行增删改查什么操作,都会对数据进行加锁,在这期间,其他事务是无法对这些记录进行操作的,如果想操作 ,只能等待 前面事务提交后 再执行。
# 查看数据库当前事务隔离级别
select @@tx_isolation;
# 设置事务隔离级别 (后面跟上对应的隔离级别)
set tx_isolation = 'repeatable-read';
# 设置全局事务隔离界别 (上面单独的设置 是针对与当前会话窗口)
set global tx_isolation = 'repeatable-read';
那么使用低隔离限制的事务隔离级别会有什么问题呢?
(1)、脏读(一个事务读取了其他事务未提交的数据)
当事务隔离级别设置为 read-uncommitted 时,会出现脏读的情况。
#会话窗口A
set tx_isolation = 'read-uncommitted'; #设置事务隔离级别为读未提交
select @@tx_isolation; #查看事务隔离级别是否设置成功
begin; #开启事务
#假设往 user 表里插入一条数据
insert into user values (1,'华强','34岁')
#插入完后 查询user表 ,目前只有一个 华强用户
select * from user;
#当会话B 执行完插入后 ,再进行查询。
select * from user;
#可以发现 数据库里有会话B 插入 但是还未提交的数据 这就是脏读现象
commit; #关闭事务
#会话窗口B
set tx_isolation = 'read-uncommitted'; #设置事务隔离级别为读未提交
select @@tx_isolation; #查看事务隔离级别是否设置成功
begin; #开启事务
#假设往 user 表里插入一条数据
insert into user values (4,'沙僧','539岁')
(2)、不可重复读(一个事务对同样查询条件的数据进行多次查询时,得到的结果不一致)
#会话窗口A
set tx_isolation = 'read-committed'; #设置事务隔离级别为读未提交
select @@tx_isolation; #查看事务隔离级别是否设置成功
#假设往 user 表里插入一条数据
insert into user values (1,'华强','34岁')
begin; #开启事务
#执行查询语句,目前应可以看到 1条 为华强的数据
select * from user;
#等会话B 的update 执行完后再进行 select 查询,查询时会发现 华强 变成了 沙僧
select * from user;
#这就是不可重读,即为 相同的查询语句 在隔离的事务里 不同的时刻中 查询出来的结果不一致
#这不符合事务的隔离性
#会话窗口B
#当会话A执行完 第一个select 后,进行update 操作
update user set username = '沙僧' where id = 1
(3)、幻读(一个读取到的数据可以是表中不存在的数据)
#会话窗口A
set tx_isolation = 'repeatable-read'; #设置事务隔离级别为 可重复读
select @@tx_isolation; #查看事务隔离级别是否设置成功
#先往 user 表里插入三条数据
insert into user values (1,'华强','34岁')
insert into user values (2,'沙僧','413岁')
insert into user values (3,'三藏','3岁')
begin; #开启事务
#现在查询的时候 可以看到 3条数据
select * from user;
#当窗口B 执行完后 再进行查询,就只可以看到两条了
select * from user;
#这叫幻读 就是 事务A读取到了 事务B删除了的数据,不符合隔离性
#会话窗口B
set tx_isolation = 'repeatable-read'; #设置事务隔离级别为 可重复读
select @@tx_isolation; #查看事务隔离级别是否设置成功
begin; #开启事务
#删除一条数据
delect from user where id = 3;
事务的隔离性 底层是如何实现的呢?
使用 读写锁 和 MVCC(多版本并发控制)
3.1、读写锁
锁的分类:
性能上: 乐观锁(每次对数据进行操作时,认为没有其他事务在对该数据进行操作,仅在提交前进行对比,之前数据被修改的话 则重新操作)
悲观锁(不管何时 认为都有别的事务和我抢,而直接加锁)
操作类型上:读锁:针对同一份数据,多个读操作可以同时进行 不会互相影响。
写锁:在“写”操作没有完成前,阻塞别的事务读 写
简单来说就是 读锁会堵塞写,不限制读。 写锁会把读写 都限制。
数据操作粒度上:
全局锁:对整个库的表 都加锁。(通常是备份整个库的时候使用,限制其 插入 更新 删除操作)
#加全局锁
flush tables with read lock;
#解全局锁
unlock tables;
表锁:每次操作直接锁住整张表,开销小 加锁快 不会出现死锁。包括表读锁、表写锁和元数据锁。 锁定粒度大,发生锁冲突的概率最高,并发度最低。
#读锁 所有线程可以读,当前线程写会出错,其他线程写会阻塞
lock table xxx read;
#写锁 当前线程 可以读写,其他线程读写 会阻塞
lock table xxx write;
#解锁
unlock tables;
行锁:针对表中某一行进行锁定。开销大,加锁慢;会出现死锁。锁定粒度最小,发生锁冲突的概率最低,并发度 最高。
#共享锁 - 又称s锁 - 允许当前事务读取一行,阻止其他事务获取相同数据集的排它锁
select * from regions where id = 13 lock in share mode
#排它锁 - 又称x锁 - 允许当前事务更新数据,阻止其他事务获取相同数据集的共享锁和排它锁
select * from regions where id = 13 for update
3.2、一致性快照读(MVCC-多版本并发控制)
MVCC多版本并发控制,可通过这种方式在保证其性能的基础上 实现事务的隔离级别,例如,MySQL中的 RC(read-committed)、RR(repeatable-read)底层就是通过 MVCC实现的。
那么MVCC底层逻辑是如何实现的呢?
MVCC实现原理 主要依赖于记录中的 undolog(回滚日志)、readview、三个隐藏字段来实现的.
三个隐藏字段有哪些?
- DB_TRX_ID :记录创建这条记录或 最后一次修改记录的事务id。
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undolog实现数据的回滚。
- DB_ROW_ID:隐藏的主键,如果数据表中 没有主键、也没有非空且唯一的字段,那么innodb 会自动生成一个row_id
如何理解MVCC中的 版本链?
当我们开启了一个事务,并且这个事务要对数据进行更新,此时会产生一条undolog日志,多个事务同事操作 这一条记录的时候,那就会产生多个版本的undolog日志,这些日志会通过 DB_ROLL_PTR回滚指针,来构成一个链表,这个链表就称之为 版本链。
ReadView?
ReadView提供了某一时刻事务系统的一个快照读,主要用来做“可见性”判断。这个ReadView中也保存了对事务不可见的一些其他事务的ID。
ReadView的应用场景:
对于Read-Committed 和 repeatable-read 隔离级别,都要读取已经提交的事务数据,也就是说 如果版本链中的事务没有提交,该版本的记录是不能被读取的,那 哪些版本的事务是可以被读取的呢? 此时就引入了ReadView。
MySQL中 RC、RR隔离级别 底层都是通过MVCC实现的。
ReadView中包含如下属性:
- m_ids:截止到当前事务id,之前 所有活跃中的 事务id。(即没有commit的事务id)
- min_trx_id:记录活跃事务id中(m_ids)的最小值。
- max_trx_id:当前事务id 的下一个值。
- creator_trx_id:保存创建ReadView的当前事务id。
ReadView会根据这四个属性,再结合undolog日志版本链,实现mvcc机制。
事务隔离(RC-ReadCommitted RR-Repeatable Read)是何时创建ReadView视图的?
- RC Read-Committed 隔离级别,是在每次select的时候 创建ReadView(每次查询都创建新的ReadView 会出现 幻读 和 重复读的情况)
- RR Repeatable-Read 隔离级别,是在第一次select的时候 创建的ReadView(之后的每次select都读到第一次select 的 值, 这样不会出现幻读的情况)
4.持久性
事务完成之后,他对于数据的修改是永久性的,即使出现系统故障也能够保存下来。
通过redolog (重做日志) 实现
总结:事务的底层逻辑就是上方说明这些了,事务底层通过 锁 和 MVCC 来实现。
欢迎纠错指正,谢谢。