1.关于事务
理解和学习事务,不能只站在程序猿的角度来理解事务,而是要站在使用者(用户)的角度来理解事务。
比如支付宝转账,A转了B100块前,在程序猿的角度来看,是两条update操作,A -100块,B +100块。但是站在使用者的角度,这就是一条转账逻辑。
事务具有四个属性:
上面四个属性,可以简称为 ACID 。
MYSQL中存在的大量的事务,它们也是需要先被描述,然后再组织的,这样管理起来的。其实就是将一批SQL语句打包成一个事务对象,将这个对象放在事务执行的事务列表里。
2.为什么要存在事务?
后面我们将MYSQL的一行信息称为一条记录。
3.了解事务的提交方式
在MYSQL的两大存储引擎Innodb和MYISAM,只有Innodb支持事务。
事务的提交方式有两种:
1.自动提交。
2.手动提交。
我们可以查看我们的MYSQL的提交方式
mysql> show variables like 'autocommit';
ON表示了它是自动提交的。
我们也可以设置它的提交方式
set autocommit=0;
show variables like 'autocommit';
0是关闭,1是打开。
4.准备工作
我们知道,mysqld是一个网络服务,那么我们可以查看
netstat -nltp
可以看到,端口号为3306就是我们的mysql网络服务。
另外可以查看我们mysql和mysqld文件所在的位置
首先我们先将隔离级别设置成为读未提交
set global transaction isolation level read uncommitted;
但是设置好后,需要登录mysql会话才能生效
此时我们输入
select @@tx_isolation;
此时的隔离级别就是读未提交
然后创建一张测试表
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
5.事务的基本操作
这里我们看到事务是自动提交的。
有两种方式可以开启事务
start transaction;
begin;
推荐 使用begin
设置保存点(比如p1)
savepoint p1;
这是可以方便我们定向回滚的。比如
在插入一条数据后
insert into account values(1,'张三',123.2);
我们看到另一个开启事务的会话可以直接看到更新的结果
然后我们回到插入数据前的保存点p1。
rollback to p1;
数据就消失了
也可以直接
rollback;
直接回到最开始的地方。
如果想结束事务
commit;
事务回滚只能在事务运行期间,如果事务已经提交了,就不能回滚了。
6.事务异常验证与产出的结论
当mysql遇到异常情况(比如强制关闭会话),事务会自动回滚。
但是如果事务已经commit了,那么mysql的数据就不会受到影响,已经持久化了。
另外关于事务的自动提交,如果我们手动开启事务,比如使用begin,那么即便mysql的设置是自动提交,手动开启的事务=必须要手动提交。那么此外,所有的SQL语句都是自动开启的事务,因为又是手动提交,所以每一条SQL都是一个事务,并且执行完就提交了。
7.事务隔离性理论
隔离级别:(由低到高)
比如两个事务同时运行,其中一个事务对表插入了数据,只有提交了,另一个事务才能查看到更新的结果。
还是这两个事务,其中一个事务对表插入了数据,但是就算它提交了,另一个事务也看不到更新的结果,只有当它也结束了才能看得到。
8.事务隔离级别的设置和查看。
1.查看隔离级别:
有三种方式:
-- 查看
mysql> SELECT @@global.tx_isolation; --查看全局隔级别
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT @@session.tx_isolation; --查看会话(当前)全局隔级别
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT @@tx_isolation; --是上一种的简写方式,也是查看当前会话的
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
2.设置隔离级别
中括号表示可以省略,但是这里不建议
-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ
COMMITTED | REPEATABLE READ | SERIALIZABLE}
比如将当前会话的隔离级别改为 读-提交
set session transaction isolation level read committed;
9.事务隔离级别--读未提交
这种隔离级别几乎没有加锁,虽然效率高,但是问题也很多,严重不推荐使用。
读-未提交会产生脏读现象:
即一个事务在执行时,可以看到另一个事务的更新,但是没有提交的数据,就是脏读。
10.事务隔离级别--读提交
在这个隔离级别下,一个事务在执行时,可以看到其他事务更新并且提交后的数据。
这样会带来一个后果:
就是在同一个事务内部,每次查询数据时,根据时间段的不同,可能会查询到不一样的结果,我们把这个现象称为不可重复读。
事务的运行要保证其原子性,不可重复读会破坏原子性。
11.事务隔离级别--可重复读
可重复读解决了两个并发运行的事务,一方提交,不会影响另一个未提交的事务。
也就是说,幻读问题是insert数据操作时伴随出来的问题,属于不可重复度的情况的一种。
12.事务隔离级别--串行化
对所有的操作进行加锁,也就是将所有的事务进行串行化,这样就不会存在安全问题,但是效率很低。
MYSQL默认的隔离级别是可重复读,并且解决了幻读的问题。
13.关于一致性的理解
一致性其实不是技术层面的概念,而是使用层面的概念。
比如有一个事务在运行时,操作到一半时出现了异常,导致事务没有完成,那么那些完成了一半的操作使数据库处于一种不正确的状态,也就是不一致的状态。
所以:
而技术上,通过AID保证C 。
14.MVCC机制
数据库的并发有三种场景:
这里重点介绍读写。
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制 。
理解MVCC需要的三个前提知识:
3个记录隐藏字段:
其实我们在建表的时候,mysql会给我们加上3个隐藏字段。
补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
所以我们删除一条记录其实并不是真的把它删除了,而是改变了它的flag。这也是典型的用空间换时间的一种做法,并且如果出现了误删操作,还可以进行还原。
undo日志(log)
MYSQL将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。
所以可以简单理解为,undolog就是mysql中的一段内存缓冲区。
模拟MVCC的过程:
图中李四是最新的记录。
如果一个事务提交,undo log就清理掉了,自然也就不能回滚了。
一些思考:
所以是隔离级别决定了select是当前读还是快照读。
15.read view理论
read view结构体中重要的字段
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id;
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
// 重点是这四个字段
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的
最大值+1(也没有写错)
creator_trx_id //创建该ReadView的事务ID
read view它生成对象后,只初始化一次。此时里面的字段m_ids表示的是当这个read view起来的时候,此时活跃的事务ID。那么有两种情况,是可以select到的:
1.当前创建该read view时的事务ID等于遍历到的记录的事务ID(DB_TRX_ID),说明是select自己,那么当然可以查看。
2.当初跟我们同时运行的事务,它们的事务ID可能比我们大,但是它们可能比我们先提交了,既然已经提交了,我们也可以查看。
那么,不能查看的也有两种情况:
1.正在运行的事务,我们不能查看,在m_ids中的。
2.当前的事务ID>=low_limit_id。说明是快照之后才提交的事务,也不应该看到。
read view是从版本链中遍历的,也就是undo log中遍历。
我们可以看一下在mysql的源码中,对每次遍历的判断
可见与我们刚刚分析的差不多,如果id小于最低水位,说明这些事务是在这个read view之前就提交了,如果id == 当前事务id,那么也返回true。
并且要注意:这个id是指我们从版本链中遍历到的事务的ID( DB_TRX_ID)。
如果id大于最高水位,说明这个事务是在read view之后创建的,肯定不能查看,所以返回false。
另外,如果m_ids是空的,说明此时就只有一个事务,当然能查看,所以也返回true。
总结:
read view是事务可见性的一个类,但是注意,它不是事务一创建出来就跟着有的(read view对象),而是当这个事务(已经存在)首次进行快照读的时候,mysql才会形成read view对象。
16.read view实验
现在有这样一条记录
然后有四个事务:
根据事务id可以判断它们的创建的先后顺序,纵向代表它们的执行状态,并且可以看到事务4是先提交了的。
事务4的修改操作是:
修改name(张三)为name(李四)
注意:low_limit_id代表的是系统分配的最大的事务id 再 + 1。
那么此时事务2在进行快照读的时候,形成的read view对象的字段为:
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
那么此时的版本链:
因为只有事务4进行了修改并且提交,那么当事务2的read view创建时,读取该记录时,就会拿这个事务的DB_TRX_ID开始进行比较
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
//事务4提交的记录对应的事务ID
DB_TRX_ID=4
//比较步骤
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。
//结论
故,事务4的更改,应该看到。
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
到这里我们也发现,这不就是之前说的RC(读提交)隔离级别吗?
17.RC和RR的本质区别
我们在select语句后面加上 lock in share mode,表示加共享锁的方式读取,是当前读,不加就是快照读。
在RR级别下,(可重复读),有两个事务同时运行
图中我们发现,事务B在事务A提交前就进行了快照读,那么也就形成了read view,在事务A提交后,事务B再使用快照读依旧不能读到更新后的结果。
还有另一种情况:
两种情况的唯一不同就是,后者事务B是在事务A提交后才进行的快照读,此时就能读到更新的结果。这就是RR隔离级别。
结论:
所以read view的形成的时机不同,会影响事务的可见性!!
所以RC与RR的本质区别:
顺便说下读未提交,它直接都不形成read view,所以什么都能看到。