HOME 首页
SERVICE 服务产品
XINMEITI 新媒体代运营
CASE 服务案例
NEWS 热点资讯
ABOUT 关于我们
CONTACT 联系我们
创意岭
让品牌有温度、有情感
专注品牌策划15年

    快照读和当前读(快照读和当前读是mvcc技术吗)

    发布时间:2023-04-22 00:46:50     稿源: 创意岭    阅读: 144        

    大家好!今天让创意岭的小编来大家介绍下关于快照读和当前读的问题,以下是小编对此问题的归纳整理,让我们一起来看看吧。

    开始之前先推荐一个非常厉害的Ai人工智能工具,一键生成原创文章、方案、文案、工作计划、工作报告、论文、代码、作文、做题和对话答疑等等

    只需要输入关键词,就能返回你想要的内容,越精准,写出的就越详细,有微信小程序端、在线网页版、PC客户端

    官网:https://ai.de1919.com

    创意岭作为行业内优秀的企业,服务客户遍布全球各地,如需了解SEO相关业务请拨打电话175-8598-2043,或添加微信:1454722008

    本文目录:

    快照读和当前读(快照读和当前读是mvcc技术吗)

    一、MySQL innodb引擎深入讲解

    表空间(ibd文件),一个MySQL实例可以对应多个表空间,用于存储记录,索引等数据。

    段,分为数据段、索引段、回滚段,innodb是索引组织表,数据段就是B+Tree的叶子节点,索引段为非叶子节点,段用来管理多个区。

    区,表空间的单元结构,每个区的大小为1M,默认情况下,innodb存储引擎页大小为16K,即一个区中一共有64个连续的页。

    页,是innodb存储引擎磁盘管理的最小单元,每个页的大小为16K,为了保证页的连续性,innodb存储引擎每次从磁盘申请4~5个区。

    行,innodb存储引擎数据是按行进行存储的。Trx_id 最后一次事务操作的id、roll_pointer滚动指针。

    i nnodb的内存结构 ,由Buffer Pool、Change Buffer和Log Buffer组成。

    Buffer Pool : 缓冲池是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池么有数据,则从磁盘加载并缓存),然后再以一定频率刷新磁盘,从而减少磁盘IO,加快处理速度。

    缓冲池以page页为单位,底层采用链表数据结构管理page,根据状态,将page分为三种类型:

    1、free page 即空闲page,未被使用。

    2、clean page 被使用page,数据没有被修改过。

    3、dirty page 脏页,被使用page,数据被修改过,这个page当中的数据和磁盘当中的数据 不一致。说得简单点就是缓冲池中的数据改了,磁盘中的没改,因为还没刷写到磁盘。

    Change Buffer :更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区Change Buffer中,在未来数据被读取时。再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。

    二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引页,同样,删除和更新可能会影响索引树中不相邻的二级索引页。如果每一次都操作磁盘,会造成大量磁盘IO,有了Change Buffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。

    Adaptive Hash Index: 自适应hash索引,用于优化对Buffer Pool数据的查询,InnoDB存储引擎会监控对表上各索引页的查询,如果观察到hash索引可以提升速度,则建立hash索引,称之为自适应hash索引。无需人工干预,系统根据情况自动完成。

    参数:innodb_adaptive_hash_index

    Log Buffer: 日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log、undo log),默认大小为16M,日志缓冲区的日志会定期刷新到磁盘中,如果需要更新,插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘IO。

    参数: innodb_log_buffer_size 缓冲区大小

    innodb_flush_log_at_trx_commit 日志刷新到磁盘时机

    innodb_flush_log_at_trx_commit=1 表示日志在每次事务提交时写入并刷新到磁盘

    2 表示日志在每次事务提交后写入,并每秒刷新到磁盘一次

    0 表示每秒将日志写入并刷新到磁盘一次。

    InnoDB 的磁盘结构,由系统表空间(ibdata1),独立表空间(*.ibd),通用表空间,撤销表空间(undo tablespaces), 临时表空间(Temporary Tablespaces), 双写缓冲区(Doublewrite Buffer files), 重做日志(Redo Log).

    系统表空间(ibdata1): 系统表空间是更改缓冲区的存储区域,如果表是在系统表空间而不是每个表文件或者通用表空间中创建的,它也可能包含表和索引数据。

    参数为: innodb_data_file_path

    独立表空间(*.ibd): 每个表的文件表空间包含单个innodb表的数据和索引,并存储在文件系 统上的单个数据文件中。 参数: innodb_file_per_table

    通用表空间: 需要通过create tablespace 语法创建,创建表时 可以指定该表空间。

    create tablespace xxx add datafile 'file_name' engine=engine_name

    create table table_name .... tablespace xxx

    撤销表空间(undo tablespaces): MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16K,undo_001,undo_002),用于存储undo log 日志

    临时表空间(Temporary Tablespaces): innodb使用会话临时表空和全局表空间,存储用 户创建的临时表等数据。

    双写缓冲区(Doublewrite Buffer files): innodb引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入缓冲区文件中,便于系统异常时恢复数据。

    重做日志(Redo Log): 是用来实现事务的持久性,该日志文件由两部分组成,重做日志缓冲区(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中,当事务提交之后会把修改信息都会存储到该日志中,用于在刷新脏页到磁盘时,发送错误时,进行数据恢复使用。以循环方式写入重做日志文件,涉及两个文件ib_logfile0,ib_logfile1。

    那内存结构中的数据是如何刷新到磁盘中的? 在MySQL中有4个线程负责刷新日志到磁盘。

    1、Master Thread, mysql核心后台线程,负责调度其它线程,还负责将缓冲池中的数据异 步刷新到磁盘中,保持数据的一致性,还包括脏页的刷新,合并插入缓冲、undo页的回 收。

    2、IO Thread,在innodb存储引擎中大量使用了AIO来处理IO请求,这样可以极大地提高数 据库的性能,而IO Thead主要负责这些IO请求的回调。

    4个读线程 Read thread负责读操作

    4个写线程write thread负责写操作

    1个Log thread线程 负责将日志缓冲区刷新到磁盘

    1个insert buffer线程 负责将写入缓冲区内容刷新到磁盘

    3、Purge Thread,主要用于回收事务已经提交了的undo log,在事务提交之后,undo log 可能不用了,就用它来回收。

    4、Page Cleaner Thread, 协助Master Thread 刷新脏页到磁盘的线程,它可以减轻主线程 的压力,减少阻塞。

    事务就是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失效。

    事务的4大特性分为:

    如何保证事务的4大特性,原子性,一致性和持久性是由innodb存储引擎底层的两份日志来保证的,分别是redo log和undo log。对于隔离性是由锁机制和MVCC(多版本并发控制)来实现的。

    redo log,称为重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。该日志文件由两部分组成: 重做日志缓冲redo log buffer及重做日志文件redo log file,前者是在内存中,后者是在磁盘中,当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发送错误时,进行数据的恢复使用,从而保证事务的持久性。

    具体的操作流程是:

    1、客户端发起事务操作,包含多条DML语句。首先去innodb中的buffer pool中的数据页去查找有没有我们要更新的这些数据,如果没有则通过后台线程从磁盘中加载到buffer pool对应的数据页中,然后就可以在缓冲池中进行数据操作了。

    2、此时缓冲池中的数据页发生了变更,还没刷写到磁盘,这个数据页称为脏页。脏页不是实时刷新到磁盘的,而是根据你配置的刷写策略进行刷写到磁盘的(innodb_flush_log_at_trx_commit,0,1,2三个值)。如果脏页在往磁盘刷新的时候出现了故障,会丢失数据,导致事务的持久性得不到保证。为了避免这种现象,当对缓冲池中的数据进行增删改操作时,会把增删改记录到redo log buffer当中,redo log buffer会把数据页的物理变更持久化到磁盘文件中(ib_logfile0/ib_logfile1)。如果脏页刷新失败,就可以通过这两个日志文件进行恢复。

    undo log,它是用来解决事务的原子性的,也称为回滚日志。用于记录数据被修改前的信息,作用包括:提供回滚和MVCC多版本并发控制。

    undo log和redo log的记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录,当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

    undo log销毁: undo log 在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日子可能用于MVCC。

    undo log存储: undo log 采用段的方式进行管理和记录,存放在前面介绍的rollback segment回滚段中,内部包含1024个undo log segment。

    mvcc(multi-Version Concurrency Control),多版本并发控制,指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能,MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段,undo log日志、readView。

    read committed 每次select 都生成一个快照读

    repeatable read 开启事务后第一个select语句才是快照读的地方

    serializable 快照读会退化为当前读。

    mvcc的实现原理

    DB_TRX_ID: 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID

    DB_ROLL_PTR: 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个 版本

    DB_ROW_ID: 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。

    m_ids当前活跃的事务ID集合

    min_trx_id: 最小活跃事务id

    max_trx_id: 预分配事务ID,当前最大事务id+1,因为事务id是自增的

    creator_trx_id: ReadView创建者的事务ID

    版本链数据访问规则:

    trx_id: 表示当前的事务ID

    1、trx_id == creator_trx_id? 可以访问读版本-->成立的话,说明数据是当前这个事务更改的

    2、trx_id 成立,说明数据已经提交了。

    3、trx_id>max_trx_id?不可用访问读版本-> 成立的话,说明该事务是在ReadView生成后才开启的。

    4、min_trx_id

    二、MySQL之快照读

    快照读 即: snapshot read ,官方叫法是: Consistent Nonlocking Reads ,即: 一致性非锁定读 ,官方的解释是:

    即:

    即 快照读 的问题在于:在同一个事务中,能够读取到之前提交的数据。表现为:

    字面意思:在事务中,为查询创建的快照,并不适用与 DML 语句。

    也就是说:如果事务 A 开始时创建的快照,查询不到数据 col1=1 ,但此时事务 B 刚刚提交 insert col1=1 和 insert col1=1 ,此时如果事务 A 执行, delete col1=1 ,是能将事务 B 生成的数据删除的。

    字面意思:即使事务 A 的快照是在事务 B 提交之前创建的,但事务 A 也只有在事务 A 和事务 B 都提交后,才能看到事务 B 新增的数据。

    三、事务/forupdate会锁表吗

    如果条件中确定使用了索引,则会锁该行,如没有索引或没使用到索引,则会锁表。

    是否使用到索引,利用trace工具判断,这里不做叙述。

    建议用主键做索引验证

    先打开两个连接session

    注:session1此时未提交

    session2修改当前数据被阻塞,因为修改属于特殊读这里会使用当前读,修改阻塞说明session1事务加了锁。但此时不能判断是行锁还是表锁。

    将session1提交后,session2随即成功提交,这里阻塞了20s左右

    session2修改其他数据正常执行,说明锁的是行锁,不是表锁。

    session2查询操作正常,因为普通读时由于mysql的mvcc机制会使用的是快照度,所以不会阻塞。

    mvcc当前读与快照读及其相关原理这里不做叙述

    注:session1此时未提交

    session2修改当前数据被阻塞,因为修改属于特殊读这里会使用当前读,修改阻塞说明session1事务加了锁。但此时不能判断是行锁还是表锁。

    将session1提交后,session2随即成功提交,这里阻塞了20s左右

    session2修改其他数据被阻塞,说明锁的是表锁,不是行锁。

    将session1提交后,session2随即成功提交,这里阻塞了20s左右

    session2查询操作正常,因为普通读时由于mysql的mvcc机制会使用的是快照度,所以不会阻塞。

    mvcc当前读与快照读及其相关原理这里不做叙述

    注:for update只有在begin commit,也就是事务之间才会起作用,如果发现两个session都成功对一条数据加锁成功,注意看下是否有没有开启事务。

    先打开两个连接session

    注:session1此时未提交

    由于session1加了锁,session2查询加锁被阻塞,但此时不能判断是行锁还是表锁。

    将session1提交后,session2随即成功加锁,这里阻塞了20s左右

    session2加锁其他数据正常执行,说明锁的是行锁,不是表锁。

    session2修改当前数据被阻塞

    session2修改其他数据正常执行

    注:session1此时未提交

    由于session1加了锁,session2查询加锁被阻塞,但此时不能判断是行锁还是表锁。

    将session1提交后,session2随即加锁成功,这里阻塞了20s左右

    session2加锁其他数据也被阻塞,说明锁的是表锁,不是行锁。

    将session1提交后,session2随即加锁成功,这里阻塞了20s左右

    session2修改当前数据被阻塞,但此时不能判断是行锁还是表锁。

    将session1提交后,session2随即修改成功,这里阻塞了20s左右

    session2修改其他数据同样被阻塞,说明锁的是表锁,不是行锁。

    将session1提交后,session2随即修改成功,这里阻塞了20s左右

    四、事务的隔离级别 全部都是共享锁吗

    前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在。这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么。 一次封锁or两段锁? 因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。 数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁) 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。 事务 加锁/解锁处理 begin; insert into test ..... 加insert对应的锁 update test set... 加update对应的锁 delete from test .... 加delete对应的锁 commit; 事务提交时,同时释放insert、update、delete对应的锁 这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。 事务中的加锁方式 事务的四种隔离级别 在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。 隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read) 未提交读(Read uncommitted) 可能 可能 可能 已提交读(Read committed) 不可能 可能 可能 可重复读(Repeatable read) 不可能 不可能 可能 可串行化(Serializable ) 不可能 不可能 不可能 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读) 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞 Read Uncommitted这种级别,数据库一般都不会用,而且任何操作都不会加锁,这里就不讨论了。 MySQL中锁的种类 MySQL中锁的种类很多,有常见的表锁和行锁,也有新加入的Metadata Lock等等,表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。 行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。这里主要讨论的也就是行锁。 Read Committed(读取提交内容) 在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。效果如下 MySQL> show create table class_teacher \G\ Table: class_teacher Create Table: CREATE TABLE `class_teacher` ( `id` int(11) NOT NULL AUTO_INCREMENT, `class_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `teacher_id` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_teacher_id` (`teacher_id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 1 row in set (0.02 sec) MySQL> select * from class_teacher; +----+--------------+------------+ id class_name teacher_id +----+--------------+------------+ 1 初三一班 1 3 初二一班 2 4 初二二班 2 +----+--------------+------------+ 由于MySQL的InnoDB默认是使用的RR级别,所以我们先要将该session开启成RC级别,并且设置binlog的模式 SET session transaction isolation level read committed; SET SESSION binlog_format = 'ROW'; (或者是MIXED) 事务A 事务B begin; begin; update class_teacher set class_name='初三二班' where teacher_id=1; update class_teacher set class_name='初三三班' where teacher_id=1; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction commit; 为了防止并发过程中的修改冲突,事务A中MySQL给teacher_id=1的数据行加锁,并一直不commit(释放锁),那么事务B也就一直拿不到该行锁,wait直到超时。 这时我们要注意到,teacher_id是有索引的,如果是没有索引的class_name呢?update class_teacher set teacher_id=3 where class_name = '初三一班'; 那么MySQL会给整张表的所有数据行的加行锁。这里听起来有点不可思议,但是当sql运行的过程中,MySQL并不知道哪些数据行是 class_name = '初三一班'的(没有索引嘛),如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。 但在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是MySQL,为了效率也是会违反规范的。(参见《高性能MySQL》中文第三版p181) 这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。 Repeatable Read(可重读) 这是MySQL中InnoDB默认的隔离级别。我们姑且分“读”和“写”两个模块来讲解。 读 读就是可重读,可重读这个概念是一事务的多个实例在并发读取数据时,会看到同样的数据行,有点抽象,我们来看一下效果。 RC(不可重读)模式下的展现 事务A 事务B begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三二班 1 2 初三一班 1 update class_teacher set class_name='初三三班' where id=1; commit; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三三班 1 2 初三一班 1 读到了事务B修改的数据,和第一次查询的结果不一样,是不可重读的。 commit; 事务B修改id=1的数据提交之后,事务A同样的查询,后一次和前一次的结果不一样,这就是不可重读(重新读取产生的结果不一样)。这就很可能带来一些问题,那么我们来看看在RR级别中MySQL的表现: 事务A 事务B 事务C begin; begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三二班 1 2 初三一班 1 update class_teacher set class_name='初三三班' where id=1; commit; insert into class_teacher values (null,'初三三班',1); commit; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三二班 1 2 初三一班 1 没有读到事务B修改的数据,和第一次sql读取的一样,是可重复读的。 没有读到事务C新添加的数据。 commit; 我们注意到,当teacher_id=1时,事务A先做了一次读取,事务B中间修改了id=1的数据,并commit之后,事务A第二次读到的数据和第一次完全相同。所以说它是可重读的。那么MySQL是怎么做到的呢?这里姑且卖个关子,我们往下看。 不可重复读和幻读的区别 很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。 如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。 所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。 上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。 悲观锁和乐观锁 悲观锁 正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。 乐观锁 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。 MVCC在MySQL的InnoDB中的实现 在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读Repeatable reads事务隔离级别下: SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。 INSERT时,保存当前事务版本号为行的创建版本号 DELETE时,保存当前事务版本号为行的删除版本号 UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行 通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。 我们不管从数据库方面的教课书中学到,还是从网络上看到,大都是上文中事务的四种隔离级别这一模块列出的意思,RR级别是可重复读的,但无法解决幻读,而只有在Serializable级别才能解决幻读。于是我就加了一个事务C来展示效果。在事务C中添加了一条teacher_id=1的数据commit,RR级别中应该会有幻读现象,事务A在查询teacher_id=1的数据时会读到事务C新加的数据。但是测试后发现,在MySQL中是不存在这种情况的,在事务C提交后,事务A还是不会读到这条数据。可见在MySQL的RR级别中,是解决了幻读的读问题的。参见下图 读问题解决了,根据MVCC的定义,并发提交数据时会出现冲突,那么冲突时如何解决呢?我们再来看看InnoDB中RR级别对于写数据的处理。 “读”与“读”的区别 可能有读者会疑惑,事务的隔离级别其实都是对于读数据的定义,但到了这里,就被拆成了读和写两个模块来讲解。这主要是因为MySQL中的读,和事务隔离级别中的读,是不一样的。 我们且看,在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。 对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中: 快照读:就是select select * from table ....; 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。 select * from table where ? lock in share mode; select * from table where ? for update; insert; update ; delete; 事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。 写("当前读") 事务的隔离级别中虽然只定义了读数据的要求,实际上这也可以说是写数据的要求。上文的“读”,实际是讲的快照读;而这里说的“写”就是当前读了。 为了解决当前读中的幻读问题,MySQL事务使用了Next-Key锁。 Next-Key锁 Next-Key锁是行锁和GAP(间隙锁)的合并,行锁上文已经介绍了,接下来说下GAP间隙锁。 行锁可以防止不同事务版本的数据修改提交时造成数据冲突的情况。但如何避免别的事务插入数据就成了问题。我们可以看看RR级别和RC级别的对比 RC级别: 事务A 事务B begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三二班 30 update class_teacher set class_name='初三四班' where teacher_id=30; insert into class_teacher values (null,'初三二班',30); commit; select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三四班 30 10 初三二班 30 RR级别: 事务A 事务B begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三二班 30 update class_teacher set class_name='初三四班' where teacher_id=30; insert into class_teacher values (null,'初三二班',30); waiting.... select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三四班 30 commit; 事务Acommit后,事务B的insert执行。 通过对比我们可以发现,在RC级别中,事务A修改了所有teacher_id=30的数据,但是当事务Binsert进新数据后,事务A发现莫名其妙多了一行teacher_id=30的数据,而且没有被之前的update语句所修改,这就是“当前读”的幻读。 RR级别中,事务A在update后加锁,事务B无法插入新数据,这样事务A在update前后读的数据保持一致,避免了幻读。这个锁,就是Gap锁。 MySQL是这么实现的: 在class_teacher这张表中,teacher_id是个索引,那么它就会维护一套B+树的数据关系,为了简化,我们用链表结构来表达(实际上是个树形结构,但原理相同) 如图所示,InnoDB使用的是聚集索引,teacher_id身为二级索引,就要维护一个索引字段和主键id的树状结构(这里用链表形式表现),并保持顺序排列。 Innodb将这段数据分成几个个区间 (negative infinity, 5], (5,30], (30,positive infinity); update class_teacher set class_name='初三四班' where teacher_id=30; 不仅用行锁,锁住了相应的数据行;同时也在两边的区间,(5,30]和(30,positive infinity),都加入了gap锁。这样事务B就无法在这个两个区间insert进新数据。 受限于这种实现方式,Innodb很多时候会锁住不需要锁的区间。如下所示: 事务A 事务B 事务C begin; begin; begin; select id,class_name,teacher_id from class_teacher; id class_name teacher_id 1 初三一班 5 2 初三二班 30 update class_teacher set class_name='初一一班' where teacher_id=20; insert into class_teacher values (null,'初三五班',10); waiting ..... insert into class_teacher values (null,'初三五班',40); commit; 事务A commit之后,这条语句才插入成功 commit; commit; update的teacher_id=20是在(5,30]区间,即使没有修改任何数据,Innodb也会在这个区间加gap锁,而其它区间不会影响,事务C正常插入。 如果使用的是没有索引的字段,比如update class_teacher set teacher_id=7 where class_name='初三八班(即使没有匹配到任何数据)',那么会给全表加入gap锁。同时,它不能像上文中行锁一样经过MySQL Server过滤自动解除不满足条件的锁,因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其它事务无法插入任何数据。 行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在写数据时的幻读问题。 Serializable 这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。 这里要吐槽一句,不要看到select就说不会加锁了,在Serializable这个级别,还是会加锁的!

    以上就是关于快照读和当前读相关问题的回答。希望能帮到你,如有更多相关问题,您也可以联系我们的客服进行咨询,客服也会为您讲解更多精彩的知识和内容。


    推荐阅读:

    数据库快照作用(数据库快照作用有哪些)

    vm快照删除磁盘过满(vm删除快照是否可以释放空间)

    内存快照与磁盘快照(内存快照与磁盘快照哪个好)

    华为手机调震动微信不震动(华为手机调震动微信不震动了怎么办)

    日本侵华从哪里登陆的(日本侵华从哪里登陆的)