【资料图】
事务隔离级别遗留问题:
在读已提交的级别下,事务B可以读到事务A持有写锁的的记录,且读到的是未更新前的,为何写读没有冲突?
可重复读级别,事务B可以更新事务A理论上应该已经获取读锁的记录,且更新后,事务A依然可以读到数据,为何读-写-读没有冲突?
- 在可重复读级别,幻读没有产生
其中,前两个问题就是因为mvcc机制(读锁的一种优化机制),通过不加读锁,避免读写冲突,进而提高了性能。
为什么要有MVCC机制?
在读已提交的级别下,由于是给读加锁来保证读已提交, 如果事务A持有写锁,为了保证读已提交,事务B必须等待事务A提交之后才可以读;其他的读事务也是这样的情况,效率太低
在可重复读级别,为了保证可重复读,如果事务A持有读锁,为了第二次读到的一样,其他所有写事务必须等待读完才可以,同样效率低
那么很自然的想到,无论读事务是先产生还是后产生,如果这个时候还存在写事务没有执行,或者需要执行;那么就应该让读事务读到目前最新的值,且写事务可以更新;只不过读事务在写事务提交更新后,依据隔离级别是否可见最新更新即可。这就是MVCC机制的核心能力,将读锁干掉。
MVCC机制核心组件
MVCC机制由版本链、undolog、readview三大核心构成版本链
猜测很多人第一次看到MVCC的版本都是和我一样在各种各样的博客文章上,或者可能是在一些课程专栏或者《高性能mysql》这本书的mvcc部分看到的,那么在你的理解中,版本的底层是什么样子呢?innodb引擎数据库中的每一条记录上,我们都可以认为上面有3个隐藏字段,分别是DB_ROW_ID(不在此次讨论范围),DB_TRX_ID和DB_ROLL_PTR,如下图一样在我的理解中,DB_TRX_ID就是插入或者更新时,当前事务的trx_id,由全局事务管理器分配的递增的一个id;DB_ROLL_PTR存储的undolog中当前记录上一个版本的指针,先姑且记住这是一个指针。当插入一条记录时在这条记录的DB_TRX_ID填入当前事务的id,由于没有历史版本,所以DB_ROLL_PTR为空当更新一条记录时由于这个时候存在历史版本,所以需要将老版本的数据写到undolog里,然后构建指针,将DB_TRX_ID更新为当前事务的id,将DB_ROLL_PTR更新为刚才构建的指针,以及更新需要更新的字段。当删除一条记录时(这个不太确定,主观猜测)猜测是将老记录写到undolog,然后构建指针,新记录DB_TRX_ID更新为当前事务的id,将DB_ROLL_PTR更新为刚才构建的指针,但是没有需要更新的字段。而且mysql不会立即删除,记录上有一个info_bits字段,会标记上删除标识(REC_INFO_DELETED_FLAG),后续由purge线程(不了解,姑且认为是个scheduleTask吧)删除这样,当多次更新之后,新记录存储的永远都是最新操作的事务id,并通过指针指向了老版本,老版本还指向了更老的版本...等等,最终构成了一个版本链Readview
理论:
在周志明老师的凤凰架构(或者极客时间的‘周志明的软件架构课’)中对mvcc简单介绍到隔离级别是可重复读:总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务 ID 最大)的。隔离级别是读已提交:总是取最新的版本即可,即最近被 Commit 的那个版本的数据记录。在mysql官网中是这么描述的
If the transaction isolation level is翻译:隔离级别是可重复读:在同一个事务中,一致性读总是去读在该事务第一次读取时生成的快照。隔离级别是读已提交:事务中的每次读取都取自己新生成的快照。相比之下,周老师形容的更贴近隔离级别的概念上,官方的描述则是底层的具体实现逻辑。两者结合一下就是可重复读:通过在每个事物只读取第一次select时生成的快照和undolog比较,根据一个可见性规则判断,是否可以读当前版本的记录,可以就返回,不行就继续比较再上一个版本,直到最老的版本;读已提交:除了每次读取都会使用最新的快照,后面的都和可重复读的逻辑一样。为什么我这里说的是可见性规则呢?是因为周老师描述里“总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录”很容易错误的理解为当前版本记录里的trx_id<=快照创建时的事务id(create_trx_id)就都可见,真正的判断逻辑并不只是一个create_trx_id就能搞定的。但这里先不展开讲,自己想一下为什么不行,下面的图可能会给你一点灵感,接下来我们先去读一下“可见性规则”的底层源码。REPEATABLE READ
(the default level), all consistent reads within the same transaction read the snapshot established by the first such read in that transaction. You can get a fresher snapshot for your queries by committing the current transaction and after that issuing new queries.WithREAD COMMITTED
isolation level, each consistent read within a transaction sets and reads its own fresh snapshot.
可见性规则底层实现
ReadView类
storage/innobase/include/read0types.h:47//ReadView类class ReadView {...private: /** trx id of creating transaction, set to TRX_ID_MAX for free views. */ //创建快照的时候,快照对应的事务id,只有含有写操作的才会分配真正的事务id trx_id_t m_creator_trx_id;/** Set of RW transactions that was active when this snapshot was taken */ //活跃的读写事务id列表,从trx_sys->rw_trx_ids抄过来的 ids_t m_ids;/** The read should not see any transaction with trx id >= this value. In other words, this is the "high water mark". */ //赋值是即将分配的下一个事务id,所以大于等于这个id的记录对当前事务来说都是不可见的 trx_id_t m_low_limit_id;/** The read should see all trx ids which are strictly smaller (<) than this value. In other words, this is the low water mark". */ //m_ids不为空就是ids.get(0),为空则是m_low_limit_id,所以小于这个事务id的就代表着快照建立的时候 //已经不是活跃事务了,即已经提交了,所以一定可以看到这些事务的改动记录 trx_id_t m_up_limit_id;.... }
初始化赋值的时候
//read0read.cc//row_search_mvcc -> trx_assign_read_view -> MVCC::view_open ->void ReadView::prepare(trx_id_t id) { ut_ad(trx_sys_mutex_own()); m_creator_trx_id = id; m_low_limit_no = trx_get_serialisation_min_trx_no(); m_low_limit_id = trx_sys_get_next_trx_id_or_no(); ut_a(m_low_limit_no <= m_low_limit_id); if (!trx_sys->rw_trx_ids.empty()) { copy_trx_ids(trx_sys->rw_trx_ids); } else { m_ids.clear(); }/* The first active transaction has the smallest id. */ m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id; ut_a(m_up_limit_id <= m_low_limit_id); ut_d(m_view_low_limit_no = m_low_limit_no); m_closed = false;}
判断某个版本的记录是否可见?
//read0types.h bool changes_visible(trx_id_t id, const table_name_t &name) const { ut_ad(id > 0);//如果当前版本记录上的事务id(DB_TRX_ID)小于低水位或者等于当前事务,//那么要么就是自己更改的,要么就是历史上已经提交了的,所以可以读到 if (id < m_up_limit_id || id == m_creator_trx_id) { return (true); } check_trx_id_sanity(id, name);//如果当前版本记录上的事务id(DB_TRX_ID)大于高水位,那么就是在当前快照生成后生成的事务,一律看不到 if (id >= m_low_limit_id) { return (false); //这一步我没有理解, } else if (m_ids.empty()) { return (true); } const ids_t::value_type *p = m_ids.data();//二分查找,如果活跃的事务里面没有,那么就返回true//这里我是这么理解的,[低水位,高水位]包含活水和死水,即活跃的事务和已经提交的事务//假如存在事务1是活跃的,事物2是已提交的,事务3是活跃的,我们在事务4的时候开启快照,很明显我们只能读到事务2或者事务4的变更//假如正在判断的是事务2,因为已经经过了上面的校验,//所以我们知道当前版本记录的事务m_low_limit_id(高水位)>id>=m_up_limit_id(低水位),且不是当前事务;//所以就需要判断事务只要不是活跃的,那么就一定是已经提交的事务,那么就可读 return (!std::binary_search(p, p + m_ids.size(), id));}在了解可见性规则之后我们知道,快照建立的时候会初始化几个属性,在查询的时候会通过changes_visible方法来判断是否可见,而调用这个方法的上层就是下面这两段逻辑,和我先前描述的类似,判断是否可以读当前版本的记录,可以就返回,不行就继续比较再上一个版本,直到最老的版本
//row0sel.cc#row_search_mvccif (srv_force_recovery < 5 && !lock_clust_rec_cons_read_sees(rec, index, offsets, trx_get_read_view(trx))) { rec_t *old_vers; /* The following call returns "offsets" associated with "old_vers" */ err = row_sel_build_prev_vers_for_mysql( trx->read_view, clust_index, prebuilt, rec, &offsets, &heap, &old_vers, need_vrow ? &vrow : nullptr, &mtr, prebuilt->get_lob_undo()); if (err != DB_SUCCESS) { goto lock_wait_or_error; } if (old_vers == nullptr) { /* The row did not exist yet in the read view */ goto next_rec; } rec = old_vers; prev_rec = rec; ut_d(prev_rec_debug = row_search_debug_copy_rec_order_prefix( pcur, index, prev_rec, &prev_rec_debug_n_fields, &prev_rec_debug_buf, &prev_rec_debug_buf_size));}//lock0lock.cc#lock_clust_rec_cons_read_seesbool lock_clust_rec_cons_read_sees( const rec_t *rec, /*!< in: user record which should be read or passed over by a read cursor */ dict_index_t *index, /*!< in: clustered index */ const ulint *offsets, /*!< in: rec_get_offsets(rec, index) */ ReadView *view) /*!< in: consistent read view */{ ut_ad(index->is_clustered()); ut_ad(page_rec_is_user_rec(rec)); ut_ad(rec_offs_validate(rec, index, offsets)); /* Temp-tables are not shared across connections and multiple transactions from different connections cannot simultaneously operate on same temp-table and so read of temp-table is always consistent read. */ if (srv_read_only_mode || index->table->is_temporary()) { ut_ad(view == nullptr || index->table->is_temporary()); return (true); } /* NOTE that we call this function while holding the search system latch. */ trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets); return (view->changes_visible(trx_id, index->table->name));}
事务的trx_id
在我还没开始看mysql源码,只是跟着博客学习写用例测试的时候,我发现,开启事务进行了第一次查询之后,确实有生成事务id,但后面我执行了一条更新语句之后,原来的事务id变了;就像下面这个图一样,最开始只有查询的时候是比较长的这个id,但执行了一条update语句后,事务id变成了一个短的。这个时候我就产生了很多疑问?同一个事务里,事务id怎么还能变呢?变的话changes_visible里面的比较怎么算?搜索了一下之后了解到,只读事务是不会生成事务id的,是假的!于是我又疑惑,那这个假id怎么参与changes_visible呢?也就是这个时候,我才下定决心去看源码,也借此理解了高低水位的设计,并认识到自己之前的理解是错误的。先上结论只读事务不会分配真正的事务id,他的值是0;只读事务参与change_visable的时候,create_trx_id也确实是0,是通过m_up_limit_id(低水位)来判断是否可见的,只有在变成读写事务时,create_trx_id才会起效并应用;因为值是0,所以在通过下面sql查询的时候,那串id只是展示的时候特殊处理的select * from information_schema.INNODB_TRX;
//trx0trx.cc#trx_start_low //这里可以看到只有读写事务才真正分配了id else {trx->id = 0;if (!trx_is_autocommit_non_locking(trx)) { /* If this is a read-only transaction that is writing to a temporary table then it needs a transaction id to write to the temporary table. */ if (read_write) { trx_sys_mutex_enter(); ut_ad(!srv_read_only_mode); trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed); trx->id = trx_sys_allocate_trx_id(); trx_sys->rw_trx_ids.push_back(trx->id); trx_sys_mutex_exit(); trx_sys_rw_trx_add(trx); } else { trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed); } } else { ut_ad(!read_write); trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed); }}//trx0trx.ic//这里是在展示的时候对只读事务的id做了处理/** Retreieves the transaction ID.In a given point in time it is guaranteed that IDs of the runningtransactions are unique. The values returned by this function for readonlytransactions may be reused, so a subsequent RO transaction may get the same IDas a RO transaction that existed in the past. The values returned by thisfunction should be used for printing purposes only.@param[in] trx transaction whose id to retrieve@return transaction id */static inline trx_id_t trx_get_id_for_print(const trx_t *trx) { /* Readonly and transactions whose intentions are unknown (whether they will eventually do a WRITE) don"t have trx_t::id assigned (it is 0 for those transactions). Transaction IDs in information_schema.innodb_trx.trx_id, performance_schema.data_locks.engine_transaction_id, performance_schema.data_lock_waits.requesting_engine_transaction_id, performance_schema.data_lock_waits.blocking_engine_transaction_id should match because those tables could be used in an SQL JOIN on those columns. Also trx_t::id is printed by SHOW ENGINE INNODB STATUS, and in logs, so we must have the same value printed everywhere consistently. */ /* DATA_TRX_ID_LEN is the storage size in bytes. */ static const trx_id_t max_trx_id = (1ULL << (DATA_TRX_ID_LEN * CHAR_BIT)) - 1; ut_ad(trx->id <= max_trx_id); /* on some 32bit architectures casting trx_t* (4 bytes) directly to trx_id_t (8 bytes unsigned) does sign extension and the resulting value has highest 32 bits set to 1, so the number is unnecessarily huge. Also there is no guarantee that we will obtain the same integer each time. Casting to uintptr_t first, and then extending to 64 bits keeps the highest bits clean. */ return (trx->id != 0 ? trx->id : trx_id_t{reinterpret_cast(trx)} | (max_trx_id + 1));}
生成快照时机(不太确定)
可重复读:只生成一次,后面继续使用ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */{ ut_ad(trx_can_be_handled_by_current_thread_or_is_hp_victim(trx)); ut_ad(trx->state.load(std::memory_order_relaxed) == TRX_STATE_ACTIVE); if (srv_read_only_mode) { ut_ad(trx->read_view == nullptr); return (nullptr); } else if (!MVCC::is_view_active(trx->read_view)) { trx_sys->mvcc->view_open(trx->read_view, trx); } return (trx->read_view);}读已提交:用完就关,所以每次再获取就得新开,但是这里的关有两个地方调
ha_innodb.cc#store_lock 和ha_innodb.cc#external_lockif (lock_type != TL_IGNORE && trx->n_mysql_tables_in_use == 0) { trx->isolation_level = innobase_trx_map_isolation_level(thd_get_trx_isolation(thd)); if (trx->isolation_level <= TRX_ISO_READ_COMMITTED && MVCC::is_view_active(trx->read_view)) { /* At low transaction isolation levels we let each consistent read set its own snapshot */ mutex_enter(&trx_sys->mutex); trx_sys->mvcc->view_close(trx->read_view, true); mutex_exit(&trx_sys->mutex); }}
在学习了解MVCC机制中遇到的问题:
- 为什么更新操作必须使用当前读?
- 只读事务突然更新的话,因为更新必须使用当前读,那是否需要重新生成事务id?
- 只读事务分配的事务id是什么东西?如何参与运作?
- readview的范围
- 知道了mvcc底层是undolog和readview后,怎么理解“版本”这个概念
- 在只读视图能查到其他事务已经删除并且提交的记录吗?
怎么解决的幻读?
在只读事务下,如上文所说的事务1读不到事务2的更新是因为事务2的版本号要大于当前快照的高水位,那对于新增的记录来说,其版本号也是同样的道理,因此事务1读不到比当前快照里的高水位高的,也就避免了幻读这种情况。参考资料:
MySQL 8.0 MVCC 源码解析 - 掘金https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.htmlMySQL事务ID的分配时机_mysql事务id什么时候分配_哲学长的博客-CSDN博客MYSQL innodb中的只读事物以及事物id的分配方式_ITPUB博客Mysql如何实现隔离级别 - 可重复读和读提交 源码分析_mysql 可重复度源码_择维士的博客-CSDN博客关键词:
(责任编辑:黄俊飞)推荐内容
- MySql的MVCC机制
- 世界新动态:樗里子临终遗言_樗里子
- 薄铁皮焊接方法(薄束结节)
- 天天视讯!阳台种植芹菜和香菜要注意事项
- 部编版九年级上册语文教学计划_语文版九
- 江西万年:教书育人路上 一朵洁白无暇的
- 【女王霸气】斗破苍穹动画中的美杜莎cosp
- 神武方寸加点攻略(神武方寸怎么加点简介
- 河南明日调整油价:92号汽油不超过7.49元/升
- 公司问答丨东方明珠:盖娅互娱海外上市工
- 东阿阿胶: 监事会决议公告 环球速看料
- 岚图汽车获得六家银行150亿元授信 与
- 安全风险是指什么 安全风险是指什么的概率
- 贵州茅台董事长丁雄军:“i茅台”累计交
- 新闻观察:中国新能源汽车表现亮眼-世界
- 昆明长水机场航站区派出所“四项措施”优
- 家常鲁味菜(关于家常鲁味菜介绍)
- 【量子科技概念股研发强度远超A股平均水
- 创世纪吸引人的2021 GV80豪华SUV的详细
- fc2id 全球热点评
- 2023湖北襄阳市枣阳市招聘事业单位工作人
- 全球滚动:黑龙江省绥芬河市发布雷电黄色
- 光迅科技:6月12日融券卖出金额462.59万
- 方硕:再次入选国家队是对自己的认可 会
- 环球消息!催奶的正确手法_正确催奶手法
- 环球今日报丨罗德里当选欧冠赛季最佳球员
- 全球观点:追的五笔怎么打法_追的五笔怎
- 不等边角钢理论重量计算_不等边角钢理论
- 【独家焦点】“无理由退货”莫滥用
- 多家银行清理长期不动存折账户,未来存折
- 世界观热点:博格华纳:进军充电桩将完成
- 天天快资讯丨有深度有品味(有深度有品位
- 爱情重点石梅歌词_爱情重点石梅歌词是什么
- 《明日之后》大神攻略从零到富打造财富帝
- 华为手机定位找回遗失手机_魅族20infinit
- 最新:客车龙头股一览表(客车的四大龙头
- 【短讯】通用股份:海外需求即使衰退影响
- hand是什么意思(arm是什么意思)|天天热门
- 环球即时:韦德布什:将AMD目标价从95美
- 环球头条:微波炉烤花生 微波炉烤花生米
- 伟明环保向159名激励对象授予1043万股限
- 上海嘉定电子社保卡怎么领取?
- 观非遗文化 购非遗好物 非遗购物节在峨
- 把设备开上山 泥巴当护手霜 一位80后女
- 2023中国·银川第七届黄河文化旅游节6月2
- 热消息:实用的美好的晚安心语
- 卡拉ok歌曲哪里有免费下载的_高清卡拉ok
- 澳门举办欢乐跑喜迎杭州亚运会
- pop服务器地址怎么填outlook pop服务器
- 既是参展者 也是记录者 深圳商报/读创
- 开发商打折促销?注意!“低首付”买房暗
- 小社区,大治理 每日热讯
- 迪亚斯告别米兰:亲爱的米兰要说再见了,
- 孟子叶个人资料_孟子资料
- 环球关注:遵守公司规章制度总结怎么写(
- 保卫萝卜4周赛6.10攻略 2023年6月10日
- play a role in是什么意思(play a role)
- 环球视讯!battleinfo使用教程_battleinfo
- 厦门推出今年第二批商住用地 计划本月底
- 【世界聚看点】中央气象台6月10日10时发
- 2023湖北襄阳市枣阳市招聘事业单位工作人
- 全球滚动:黑龙江省绥芬河市发布雷电黄色
- 光迅科技:6月12日融券卖出金额462.59万
- 方硕:再次入选国家队是对自己的认可 会
- 环球消息!催奶的正确手法_正确催奶手法
- 环球今日报丨罗德里当选欧冠赛季最佳球员
- 全球观点:追的五笔怎么打法_追的五笔怎
- 不等边角钢理论重量计算_不等边角钢理论
- 【独家焦点】“无理由退货”莫滥用
- 多家银行清理长期不动存折账户,未来存折
- 世界观热点:博格华纳:进军充电桩将完成
- 天天快资讯丨有深度有品味(有深度有品位
- 爱情重点石梅歌词_爱情重点石梅歌词是什么
- 《明日之后》大神攻略从零到富打造财富帝
- 华为手机定位找回遗失手机_魅族20infinit
- 最新:客车龙头股一览表(客车的四大龙头
- 【短讯】通用股份:海外需求即使衰退影响
- hand是什么意思(arm是什么意思)|天天热门
- 环球即时:韦德布什:将AMD目标价从95美
- 环球头条:微波炉烤花生 微波炉烤花生米
- 伟明环保向159名激励对象授予1043万股限
- 上海嘉定电子社保卡怎么领取?
- 观非遗文化 购非遗好物 非遗购物节在峨
- 把设备开上山 泥巴当护手霜 一位80后女
- 2023中国·银川第七届黄河文化旅游节6月2
- 热消息:实用的美好的晚安心语
- 卡拉ok歌曲哪里有免费下载的_高清卡拉ok
- 澳门举办欢乐跑喜迎杭州亚运会
- pop服务器地址怎么填outlook pop服务器
- 既是参展者 也是记录者 深圳商报/读创
- 开发商打折促销?注意!“低首付”买房暗
- 小社区,大治理 每日热讯
- 迪亚斯告别米兰:亲爱的米兰要说再见了,
- 孟子叶个人资料_孟子资料
- 环球关注:遵守公司规章制度总结怎么写(
- 保卫萝卜4周赛6.10攻略 2023年6月10日
- play a role in是什么意思(play a role)
- 环球视讯!battleinfo使用教程_battleinfo
- 厦门推出今年第二批商住用地 计划本月底
- 【世界聚看点】中央气象台6月10日10时发
- 高质量发展调研行 | 在智慧港口见证大
- 摩根:只有不败阿森纳是不朽的,三冠王太
- 每日热点:厦门有什么好吃的(厦门有什么
- 影子形成的原因动画(影子形成的原因)|
- 关于铁路计次票、定期票 如何购买使用
- 世界微动态丨1000万元家电补贴!先报名再
- 首届上海国际碳中和博览会来了!第一财经
- 极目时评丨环卫母亲与高考儿子相拥而泣刷
- 焦点热讯:高血脂吃什么药最有效不伤肝_高
- 当前播报:报考这所学院,让你“玩转”智
- 一个容积为2.5升的塑料壶用它装酒精最多
- 95开箱网好不好 十大最受欢迎的csgo箱子
- 世界观速讯丨文学类书籍排行榜_文学类书籍
- RK3588开发板(armsom-w3)之PWM实操
- 这个全国区县级首创活动在东安湖体育公园
- confidence可数吗_confidence_微资讯
- 环球百事通!火车计次票、定期票 如何购
- 全球即时:男孩手指中“套” 消防帮助脱困
- 天天动态:厦门有什么好吃的(厦门有什么
- 乌海京东到家代运营公司_提供专业的代运