事务是对数据访问更新操作用户定义的一组操作序列,不可分割,这些操作要么全做要么全不做,为了服务器保证数据完整性
InnoDB由于支持行锁而支持事务操作
MySQL事务主要用于处理操作量大,复杂度高的数据
事务是最小的操作序列单元,事务包含的操作全部成功或者全部失败,失败会回滚到事务开始前的状态
数据库从一个一致性状态变到另一个一致性状态
事务操作结束后数据相关的所有规则都必须保持一直,包括内部结构(索引等)
一系列操作后,所有的操作和更新全部提交成功,数据库只包含全部成功后的数据就是数据的一致性
由于系统异常或数据库系统出现故障导致只有部分数据更新成功,但是这不是我们需要的最终数据,这就是数据的不一致
原子性,隔离性,持久性共同保证了一致性
事务互相隔离互不干扰
(事务内部操作的数据对其它事务是隔离的,在一个事务执行完之前不会被其他事务影响和操作)
InnoDB引擎使用redo log 保证事务的持久性 redo log 包含两部分,重做日志缓冲redo log buffer,和重做日志文件 redo log file,前者是缓存,易丢失,后者是持久(WAL机制)化文件
一个成功的事务将会永久的改变系统的状态.所有导致状态变换的记录都在一个持久的日志文件中保存,所以就算突然宕机,所有未完成已提交的事务会重演,(提交后不会导致数据丢失)
保证了高可靠性,而不是高可用性
事务回滚需要手动rollback,而不是commit自动判断回滚
事务提交后数据应该被永久的保存下来,出现宕机等故障后可以恢复数据
InnoDB由于支持了事务,上锁时不会对表进行上锁(其他会话可并发操作同一张表),为了保证数据的一致性 会出现以下问题
两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
又称无效数据读出,读取到了一个事务修改但未提交的数据.
由于这个数据未提交,被错误的读取后进行其他操作,可能会导致数据的不一致性
表现:一个事务(同一个read view)在前后两次查询同一范围的时候,后一次查询看到了前一次查询没有看到的行。(这一行是其他事务在本事务执行间隙提交的,是已提交的数据)
幻读产生的原因
行锁只能锁住行,即使把所有的行记录都上锁,也阻止不了新插入的记录。
如何解决幻读
指的是一个事务范围内,两次读取同一行,却读取到了不同的数据
危害:
程序里面一堆 if 怎样再怎样,每次读的东西都不一样
不可重复读的重点在于对数据的修改,是读取到的某行记录与之前不一样,属于行级别问题,解决只需要行级别上锁即可
幻读的重点在于数据的增删,是读取到的行数与之前不一样,属于表级别问题,解决需要表级别的上锁
脏读首先是读取到了未提交的事务,而其他两者都是读取到已提交的事务
脏读和不可重复读的行级别的错误,而幻读是表级别的错误
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
Read uncommitted(读未提交) | √ | √ | √ |
Read committed(读已提交) | × | √ | √ |
Repeatable read(可重复读) | × | × | √ |
Serializable(串行化) | × | × | × |
能够读取未提交的数据
允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
只能读取已提交的数据(由于不可重复读和幻读都是已提交的错误,所以不能避免)
允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
为了实现可重复读,MySQL 采用了 MVVC (多版本并发控制) 的方式。
可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。
禁止不可重复读取和脏读取,但是有时可能出现幻读数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
mysql中默认REPEATABLE_READ(RR)级别,可重复读
Oracle中默认READ_COMMITTED级别,读已提交
默认情况下,可重复读依然不能解决幻读问题(已提交的增删数据被读取)
而MySQL为了在RR级别上解决幻读问题引入的间隙锁
行锁就是直接加在索引记录上的锁,锁住的是key
当使用范围条件而不是等值条件请求检索并请求锁后,InnoDB会给符合条件的所有有记录的索引项加锁;
对于不符合条件并且不存在的记录就称之为间隙.
InnoDB也会对这个间隙加锁,即为间隙锁,确保索引间记录的间隙不变
间隙锁的唯一作用就是防止其他事务的插入操作,以此防止幻读
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制,是一种并发控制的方法
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
updata,insert,delete,select for update 等语句都是当前读操作,当前读时,保证的是每次读到的都是最新版本的数据,同时在读取时还要保证其他事务不能并发修改数据(进行加锁)。
普通的select语句就是快照读,就是在读取时不加锁,也不阻塞其他线程的修改操作,出现快照读是为了性能考虑,快照读的实现是基于MVCC
照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读
说白了MVCC就是为了实现读(select)-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。
所以MVCC可以为数据库解决以下问题:
在并发读写数据库时,可以做到在读(select)操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
同时还可以解决脏读,幻读,不可重复读等事务隔离问题(使用MVCC实现了可重复读隔离级别),但不能解决更新丢失问题(脏写问题需要加锁解决)
MVCC + 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
MVCC的实现主要是依赖了记录中的以下内容
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,
表中没有主键时生成隐式主键DB_ROW_ID
DB_TRX_ID
最近修改事务ID:记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR
回滚指针,指向这条记录的上一个版本(配合undo log日志)
undo log日志主要分为两种:
insert undo log:
代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log:
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读(select,当读的过程中有写的十事务开始和提交,会造成读数据的脏读、不可重复读、幻读等)时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。
MVCC依赖的实际上是update undo log日志
purge线程:
从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。
当执行快照读SQL(普通查询SQL)会生成一个一致性视图read-view,一个快照查询事务下只会使用一个快照(第一次查询操作生成的那一个快照),每次查询都是使用这个快照进行查询,这样就保证了即使并发读写,也能到可重复读的并发级别(一个事务下读取的数据是相同的,即使数据进行了修改)
每次update执行,实际上是将新的记录插入表,将旧的记录写入undo日志中,同时将回滚指针指向日志中的列
多次操作后就会形成类似下图的一个记录链
下图四个字段分别是id,name,DB_TRX_ID(事务ID),DB_ROLL_PTR(回滚指针)
说人话就是:
1.事务的第一次查询会生成一个read-view视图,里面包含了这个查询执行时其他线程还未提交的事务id数组
和以创建的最大事务id
readview:100,200 (视图创建时还未提交的事务id数组),300(已经创建的最大事务id)
2.当前事务每次查询都使用这个视图进行查询匹配,按照上述规则对版本记录链进行匹配,最终就会匹配到
同一个数据
3.由于视图中的事务id数组是在第一次查询时就确定好了的(在第一次查询时100,200号事务都没有提交),所以就算之后他们提交了,当前事务还是按照原视图进行过滤,对于当前事务而言,之后提交的修改依然是不可见的