引子
MySQL 作为一个数据库,就需要面临多个客户端同时访问同一个数据资源,这个资源在Linux 里就叫做临界资源。如果允许多个客户端同时访问修改同一份数据资源,那就可能会产生数据紊乱等一系列问题。
所以就要让CURD 满足一些属性,来保证上面的问题不会发生:
-
修改过程是原子的。
-
修改过程互不影响。
-
修改后应该永久有效。
-
修改前后应该是确定状态。
事务:事务就是一组DML 语句,这些语句在逻辑上存在相关性,这一组DML 语句要么全部成功,要么全部失败,是一个整体。MySQL 提供一种机制,保证我们达到这样的效果。事务还规定不同客户端看到的数据不同。
事务就是要做的或者所做的事情,主要用于处理操作量大,复杂度高的数据。一条及以上sql 语句操作合起来,就是一个事务。
所有的mysql 操作都被sql 包装成一个事务。mysql 一行信息叫做一行记录。
事务的属性
一个MySQL 数据库,同一时刻可能有大量的请求被包装成事务,在向MySQL 服务器发起事务处理请求。每条事务至少一条SQL,如果不加保护,会出问题。如果事务执行到一半就终止,也会出问题。所以,一个完整的事务绝对不仅仅是sql 集合,还要满足下列四个属性:
-
原子性(Atomicity):一个事务中的所有操作,要么都完成,要么都不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来都没有执行过一样。
-
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏,这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联型以及后续数据库可以自发的完成预定工作。
-
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致的数据不一致。事务隔离分为不同级别:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read) 串行化(serializable)。
-
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失。
事务的本质是为了应用层服务的,而不是伴随着数据库系统天生就有的。因为事务被 MySQL编写者设计出来本质是为了在应用程序访问数据库的时候,简化我们的编程模型,不需要考虑潜在的错误和并发问题。因为事务要么提交要么回滚,我们不会再去考虑网络异常了、服务器宕机使得更改了同一个数据。
-
通过show engines \G 来查看引擎。因为查看InnoDB 的transaction :YES 支持事务。而MyISAM 不支持事务。
-
事务的提交方式:手动提交和自动提交。
可以通过
show variables like 'autocommit'; set autocommit = 0 / 1;
来进行设置自动提交,默认开启。
-
事务的操作:
start transaction; || begin; -- 事务内做操作 savepoint s1; -- 事务内做操作 savepoint s2; -- 事务内做操作 rollback to s2; -- 返回到事务保存点s2 rollback -- 返回到事务起始 commit -- 提交 -- 如果不提交,而是非正常结束,数据不会提交到数据库而是回滚到begin之前。
事务操作的注意事项:
-
如果没有设置操作点也可以混滚,但是只可以回滚到事务的开始,rollback。前提是事务没有提交。
-
如果一个事务被提交了,则不可以被回滚。
-
可以选择回退到哪个保存点。
-
Innodb 支持事务而myisam 不支持。
用mysql 手动启动(使用begin 或者 start transaction + commit )一个事务时,和mysql 中是否事务会自动提交没有关系。
而自动提交是交给谁呢?其实我们之前所有的单条sql 本质在mysql 中全部各自会被以事务的方式进行提交的。如果关闭了自动提交,单敲一条sql 语句,需要commit 提交。否则不正常结束会回滚。
自动提交是给mysql 中的单条sql 设置的,是默认行为。
-
一个事务可以有多条sql 语句构成,也就是任何一个事务都有执行前、执行中、执行后的阶段。而所谓的原子性其实就是让用户层看到执行前执行后,而看不到执行中的状态。执行中如果出现问题,可以随时回滚。所以单个事务对用户的表现出来的特性就是原子性。
但是所有事务的执行过程如果同时发生,也出现了互相影响的情况(比如多事务同时访问一张表)。
为了保证事务执行过程中尽量不受干扰,mysql的重要特性出现:隔离性。对应事务有不同程度的干扰,隔离性也有对应的级别:隔离级别。
隔离级别:
查看隔离级别:
select @@tx_isolation; select @@global.tx_isolation; -- 全局 select @@session.tx_isolation; -- 会话内,重启就会改回global设置会话内/全局隔离级别
set [session | global] transaction isolation level {read uncommitted | readcommitted | repeatable read | serializable}; -- 如果设置session 会立即生效。 -- 如果设置global 需要重启生效。
读未提交【Read Uncommitted】:未commit 就可以看到修改
在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。实际生产中不能使用这种隔离级别的,因为相当于没有任何隔离性,也有很多的并发问题,如脏读(别人没提交的数据你读到了),幻读,不可重复读等。
读提交【Read Committed】:commit 后才可以看到修改。
大多数数据库的默认隔离级别,不是mysql 默认的。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次select,可能得到不同的结果。
不可重复读:一个事务A提交导致另外一个并行运行的事务B也能读到A提交的修改。会产生筛选的结果不同。
可重复读【Repeatable Read】
MYSQL 的默认隔离级别,确保同一个事务在执行中,多次读取操作数据时,会看到同样的数据行。但是存在幻读问题。
幻读:即便是可重复读的隔离级别,可以做到对set、update 进行隔离,也做不到对insert 操作的隔离。因为隔离性的实现用到了对数据的加锁,insert 待插入的数据并不存在,那么一般加锁就无法屏蔽这类问题。大部分内容可重复读。
这是其他数据库,MySQL 解决了RR 级别幻读的缺陷。
串行化【Serializable】
事务的最高隔离级别,通过强制事务的排序,使之不可能互相冲突,从而解决了幻读的问题。在每个读的数据行上加上了共享锁,但是可能会导致超时和锁竞争(隔离级别太极端,实际生产基本不用)。
两方可以同时读取,但是一旦有一方想要修改,就需要等待对方退出或者修改(修改可以结束对方的等待是因为死锁而导致的退出)。
隔离级别的实现基本都是锁,不同级别的锁使用不同,比如表锁、行锁、读锁、写锁。
总结:
隔离级别越严格,安全性越高,但是数据库的并发性能也越低,往往需要在两者之间找一个平衡点。
不可重复读的重点是修改和删除:同样的条件读取过的数据再次读取出来可能发现值不一样了。
幻读的重点在于新增(insert),第一次和第二次读的记录数不同了。
mysql默认隔离级别是 rr 一般不要修改。
隔离性:mysql 的内部机制,让“同时启动”、并发执行的各个事务,看到不同的数据修改(增删改),就叫做隔离性。作为一个事务,可以看到不同可见性的数据的程度的不同,就叫做隔离级别。
为什么要存在隔离级别?
如果单单为了安全,不需要种类繁多的隔离级别,无脑串行化就可以了。
所以不仅仅是考虑安全问题。
安全 + 效率之间找平衡。因为客户有不同的隔离需求,所以就有了不同的隔离级别。
一致性:
事务执行的结果,必须使数据库从一个一致性状态变到另外一个一致性状态。当数据库只包含书屋成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而还未完成的事务对数据库所做的修改一杯写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
其实一致性和用户的业务逻辑强相关,一般MySQL 提供技术支持,但是一致性还要用户业务逻辑做支撑,也就是一致性是由用户和MySQL 共同决定。
其中MySQL 提供原子性、持久性、隔离性构成一致性。
其中用户提供正确的业务逻辑来保持一致性。
技术上通过AID 保证Consistency。
如何更深入的理解隔离性;
数据库有三种并发场景:
读 - 读 不存在线程安全问题。不需要并发控制。
读 - 写 有线程安全问题。可能会造成事务隔离性问题,比如遇到脏读、不可重复读、幻读。
写 - 写 有线程安全问题。可能存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。不存在隔离性问题,都是最新数据。
读 - 写
多版本并发控制(MVCC)
用来解决读写冲突的无锁并发控制。事务在启动时,会被分配一个单项增长的事务ID,为每个修改保存一个版本,版本与事务ID 关联。
3个前提知识:
三个记录隐藏列字段:就是由mysql 自动生成的隐藏列。
DB_TRX_ID : 6 byte,最近修改(修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务ID。
DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上个版本(一般存储在undo log 中)。
DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引。
实际上还有一个删除flag 隐藏字段,即记录被更新或删除,并不代表真的删除,而是删除flag 变了。
undo log:
以修改为例:可以理解成mysql 的一段内存缓冲区,用来保存日志数据。把需要修改的记录复制到undo log中,然后事务在原表中进行修改,回滚指针指向undo log 中复制的记录的信息,事务id改成这个事务的id。
如果有新的修改,原来的undo log 的记录不会删除,而是往下移动,新的备份信息放在undo log 的上面,回滚指针指向新的记录的信息。
这样周而复始,可以形成一个回滚历史版本链。回滚,实际上就是用历史版本覆盖当前数据。
历史版本就叫做快照。
如果以delete 为例,删除并非清空,而是将flag 标识位设计为删除即可。也可以形成版本。
如果是insert ,插入,之前没有数据,那么insert 没有历史版本,但是为了回滚操作,insert的数据也被放入undo log 中,如果当前事务commit,这个undo log 的历史insert 记录可以被清空。一般不会立即清空,因为可能会有快照读的事务,等所有的事务全部都退出了,insert 的历史记录才会被删除。
(undo log 做内存管理是由事务是否仍在进行决定的。)
可以理解成update 和delete 形成版本链,insert 赞数不考虑。
select 读取不会对数据做任何修改,所以为select 维护多版本没有意义。但是select 读取是读最新版还是历史版本?
隔离性的基本原理是不同事务读不同版本的记录。
读取最新的记录:当前读。增删改 都是当前读,select 也可能是当前读。
读取历史版本记录:快照读。快照读只能是select。
多个事务同时删改查时,都是当前读,要加锁。同时有select 过来,如果要读最新版本(当前读),就需要加锁,这就是串行化。如果快照读,不受加锁限制,可以并行执行从而提高效率,这就是MVCC 的意义所在。
是什么决定了select 是当前读还是快照读?
隔离级别。
Read View:
Read View 就是事务进行快照读操作的时候产生的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID,(当每个事务开启时,都会分配一个id,这个id 是递增的,所以最新的事务 id值最大。
Read View 在MySQL 源码中就是一个类,本质是用来进行可见性判断的。即当我们某个事务执行快照读的时候,对该记录创建一个read view 读视图,把它比作条件,用来判断当前事务可以看到哪个版本的数据,即可能是当前最新的数据,也有可能是改行记录的undo log 里面的某个版本的数据。
Read View 中有几个非常重要的字段:
m_ids; 一张列表,维护Read View 生成时刻,系统正活跃的事务ID。
up_limit_id; 记录m_ids 列表中事务id 最小的id。
low_limit_id; 记录read view 生成时刻系统尚未分配的下一个事务id,也就是目前系统已经出现过的事务id 的最大值+1.并非ids 里最大事务Id。
creator_trx_id; 创建该read view 的事务id。
事务按照快照形成的先后顺序可以分为三种状态:
已经提交的事务。creator_trx_id == db_trx_id || db_trx_id < up_limit_id 说明是历史上已经提交的事务,应该能看到。
形成快照后,快照当前能看到的事务(正在操作的事务):有多个事务,对应多个事务ID。所有活跃事务ID 都在m_ids中。活跃的事务id 不一定是连续的,有的事务可能在快照前已经提交。
快照后新来的事务。db_trx_id >= low_limit_id 说明是快照之后才提交事务,不应该被活跃事务ID看到。
事务到来时,形成事务ID,你能看到的mysql 内部的各种数据,就要被确定下来。这个确定下来的各种数据就是快照。
什么时候形成快照?在一个事务要快照读的时候(select 之后)才会形成快照(Read View)然后才会出现隔离性。
RR 级别下形成快照后不会更新,RC 级别下形成快照后会更新。正是因为每次都会更新快照,所以才会产生不可重复读的问题。