基础概念
事务:
事务是一系列的数据库操作,是数据库应用的基本单位。MySQL 事务主要用于处理操作量大,复杂度高的数据。
MySQL默认事务隔离级别:
- 原子性:事务是一个完整的操作,不可被分割
- 一致性:事务完成时,数据必须处于一致状态
- 隔离性:对数据进行修改的所有并发事务是彼此隔离的
- 持久性:事务完成时,对数据库的修改被永久保持
并发事务数据问题:
更新丢失:两个事务同时更新一行数据,出现更新被覆盖。
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题,最后的更新覆盖了由其他事务所做的更新。
例如,两个编辑人员制作了同一文档的电子副本,每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。
最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改,如果在前一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。
脏读:一个事务读取到另一个事务未提交的结果。
一个事务正在对一条记录做修改,在这个事务完成并提交前, 这条记录的数据就处于不一致状态;
这时, 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系,这种现象被形象地叫做脏读。
不可重复读(虚读):事务开启后的,两次查询中,一个事务读取到另一个事务更新(修改)的数据
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读” 。
幻读:事务开启后的,两次查询中。一个事务读取到另一个事务提交(新增或删除)的数据。
事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。
事务隔离级别:
- 读未提交(Read uncommited):一个事务还没提交时,它做的变更就能被别的事务看到。可脏读,不可更新丢失。
- 读已提交(read commited):一个事务提交之后,它做的变更才会被其他事务看到。允许不可重复读,不允许脏读(Oracle数据库默认隔离级别)
- 可重复读(repetable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的,不再允许修改操作。禁止不可重复读和脏读。(MySQL数据库默认隔离级别)
- 序列化(serializable):事务串行化顺序执行,避免脏读、不可重复读、幻读
并发事务可能会造成的问题及解决方案:
并发事务可能造成:脏读、不可重复读和幻读等问题 ,这些问题其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决,解决方案如下:
1、加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
2、提供数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取,从用户的角度来看,好象是数据库可以提供同一数据的多个版本。
MVCC
什么是MVCC?
MVCC 全称是多版本并发控制系统,InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决幻读问题。
在MySQL中,每一条记录在更新时都会同时记录一条回滚操作,记录上的最新值,通过回滚可以得到前一个状态的值。假设一个值从1按顺序改为2,3,4.在回滚日志中会有如下记录:
当前值为4,但查询该记录时,不同时刻的事务会有不同的read-view。在视图A,B,C里分别记录1/2/4,同一个记录在系统中可以存在多个版本,就是数据库的多版本并发(MVCC)。对于read-viewA,要得到1,就必须将当前值依次执行图中所有回滚操作。当系统里没有比这个回滚日志更早的read-view时会删除该回滚日志。
MVCC工作原理:
在高性能MySQL中。InnoDB 的 MVCC 是通过在每行记录后面保存两个隐藏的列来实现,这两个列一个保存了行的创建时间,一个保存行的过期时间(删除时间)。当然存储的并不是真实的时间而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动新增,事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行记录的版本号进行比较。(高性能MySQL)
严格来讲InnoDB会给数据库中的每一行增加三个字段,分别是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID。另外每条记录的头信息(record header)里都有一个专门的bit(deleted_flag)来表示当前记录是否已经被删除
DB_TRX_ID:6byte, 创建这条记录/最后一次更新这条记录的事务ID DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里) DB_ROW_ID:6byte,隐含的自增ID,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
REPEATABLE READ(可重读)隔离级别下 MVCC 如何工作?(高性能MySQL)
SELECT:InnoDB 会根据以下条件检查每一行记录:第一,InnoDB 只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么是在开始事务之前已经存在要么是事务自身插入或者修改过的。第二,行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除。
INSERT:InnoDB 为新插入的每一行保存当前系统版本号作为行版本号。
DELETE:InnoDB 为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE:InnoDB 为插入的一行新纪录保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识保存这两个版本号,使大多数操作都不用加锁。它不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。
详细的UPDATE语句效果:
- 老记录被复制到rollback segement中形成undo log(回滚日志),DB_TRX_ID和DB_ROLL_PTR不动
- 新记录的DB_TRX_ID = 当前事务ID,DB_ROLL_PTR指向老记录形成的undo log
- 这样就可以通过DR_ROLL_PTR找到这条记录的历史版本。如果对同一行记录执行连续的update操作,新记录与undo log会组成一个链表,遍历这个链表可以看到这条记录的变迁)
Read View
MySQL一致性视图:
InnDB中在实现MVCC时用到的一致性视图(consistent read view)。用于支持读已提交和可重复读隔离级别的实现。没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。
在可重复读隔离级别下,事务在启动时基于整库产生快照。在InnDB里每个事务都有一个唯一事务ID(transcation id),它是事务开始时向InnDB事务系统申请的,按申请顺序严格递增。而每行数据都是有多个版本的,每次事务更新数据时,会生成一个新的数据版本,并且把事务id赋值给这个数据版本的事务id,记为row_trx_id。同时旧的数据版本会保留,并在新的数据版本中,能够有信息可以直接拿到它。
Read View组成:
InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前启动了但未提交的所有事务ID。数组里事务ID最小值为低水位,当前系统里已经创建过的事务ID的最大值加1为高水位。这个视图数组和高水位,组成了当前事务的一致性视图。
Read View中主要包含4个重要的内容:m_ids、min_trx_id、max_trx_id、creator_trx_id
m_ids(未提交事务集合):表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
min_trx_id(低水位):表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
max_trx_id(高水位):表示生成ReadView时系统中应该分配给下一个事务的id值。
creator_trx_id(当前事务):表示生成该ReadView的事务的事务id。
数据版本的可见性规则:基于数据的row_trx_id和一致性视图对比结果得到。
1、如果被访问版本的row_trx_id属性值与当前事务相同,表明当前事务在访问它自己修改过的记录,该版本可以被当前事务访问。(黄色) 2、如果被访问版本的row_trx_id属性值小于低水位,表明生成该版本的事务在当前事务生成ReadView前已经提交,该版本可以被当前事务访问。(绿色) 3、如果被访问版本的row_trx_id属性值大于高水位值,表明生成该版本的事务在当前事务生成ReadView后才开启,该版本不可以被当前事务访问。(红色) 4、如果被访问版本的row_trx_id属性值高水位与低水位之间,那就判断row_trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。(黄色)
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录
底层实现
MySQL事务的实现原理:
事务的实现是基于数据库的存储引擎,不同的存储引擎对事务的支持程度不一样。MySQL 中支持事务的存储引擎有InnoDB 和 NDB。 InnoDB 是高版本 MySQL 的默认的存储引擎,因此就以 InnoDB 的事务实现为例,InnoDB 是通过多版本并发控制(MVCC,Multiversion Concurrency Control )解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。因此 InnoDB 的 RR 隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能。事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现。
MVCC实现RR与RC的底层原理:
读未提交是直接返回记录上的最新值,没有视图概念,而串行化是直接用加锁的方式来避免并行访问。
对于可重复读:Read View在事务启动时创建,整个事务期间所有的SELECT都使用这个试图。
对于读已提交:Read View在SQL语句开始执行时创建,每次读取,都会创建一个新的read view。这样就能读取到其他事务已经COMMIT的内容。
为何不建议使用长事务:
长事务意味着系统里会存在很老的事务视图。由于这些事务随时可能访问数据库里的任何数据,所以这个事务提交之前,数据库里它可能用到的回滚记录都必须保留,会占用大量的存储空间。
其他
InnoDB默认的事务隔离级别,如何修改?
InnoDB默认事务隔离级别是RR,通过set 作用域 transaction isolation level 事务隔离级别来修改事务的隔离级别,如下:
MySQL> set global transaction isolation level read committed; // 设置全局事务隔离级别为 read committed MySQL> set session transaction isolation level read committed; // 设置当前会话事务隔离级别为 read committed
事务的启动方式:
显示启动:begin或start transaction。提交时commit,回滚是rollback。
设置手动提交事务:set autocommit = 0,该命令会将这个线程的自动提交关掉。意味着如果只执行一个select,这个事务就启动了并且不会自动提交,该事务持续存在直到主动执行commit或rollback,或者断开连接。
操作演示:
begin; insert person(uname,age) values(‘laowang‘,18); rollback; commit;
使用begin或start transaction命令,事务在执行第一个操作语句时才真正启动。如果要马上启动事务可以使用命令:start transaction with consistent snapshot。
begin或start transaction命令:一致性视图是在执行第一个快照语句时创建的。
start transaction with consistent snapshot。:一致性视图是在执行命令语句时创建的。
文章内容参考:
高性能MySQL第三版
极客时间:MySQL45讲
扛得住的MySQL数据库架构:https://coding.imooc.com/class/chapter/49.html#Anchor