网站设计怎么做有效的,淘宝网站SEO怎么做,wordpress360网站卫士,京东自营入驻费用一览表2022MVCC#xff0c;正是MySQL实现“高并发、低阻塞”的核心技术——它让“读操作不用等写操作#xff0c;写操作也不用等读操作”成为可能#xff0c;其实核心就是“给数据存多个版本#xff0c;不同事务按规则读对应版本”。
一、 MVCC初印象#xff1a;数据库的时光机…MVCC正是MySQL实现“高并发、低阻塞”的核心技术——它让“读操作不用等写操作写操作也不用等读操作”成为可能其实核心就是“给数据存多个版本不同事务按规则读对应版本”。一、 MVCC初印象数据库的时光机1. 为什么需要MVCC想象一下图书馆的场景没有MVCC一本书只能一个人看其他人必须排队等待传统锁机制有MVCC每个人都可以拿到这本书的副本同时阅读不同版本互不干扰MVCC多版本并发控制 就是数据库的时光机技术它维护数据的多个历史版本在不加锁的前提下让读操作不阻塞写操作写操作也不阻塞读操作。2. 核心概念对比当前读 vs 快照读类型工作机制类似场景示例SQL当前读读取最新数据并加锁防止别人修改排队买限量商品买完前别人不能动SELECT … FOR UPDATEUPDATE …DELETE …快照读读取某个时间点的数据版本不加锁查看历史交易记录不影响当前交易普通 SELECT-- 示例银行转账场景 -- 当前读加锁防止并发问题 SELECT balance FROM accounts WHEREid1FOR UPDATE;-- 锁定账户 UPDATE accounts SET balancebalance -100WHEREid1;-- 扣款 -- 快照读不加锁不影响别人操作 SELECT * FROM transaction_history WHEREdate2024-01-01;-- 查看历史记录关键不同隔离级别下的快照读差异快照读的“快照生成时机”会随事务隔离级别的不同而变化这直接影响读取结果的一致性Read Committed读已提交RC每次执行select都会生成一个新的快照读最新的历史版本Repeatable Read可重复读RR一个事务中只有select第一次执行时才生成快照后续所有select都复用这个快照保证多次读结果一致Serializable串行化最严格的隔离级别快照读会退化为当前读加锁阻塞完全放弃并发。重点MySQL默认隔离级别是RR可重复读正是靠MVCC的“快照复用”实现了“可重复读”避免了“不可重复读”问题。1.3 MVCC核心定义多版本并发控制MVCC 全称Multi-Version Concurrency Control多版本并发控制官方定义通过维护数据的多个版本让读写操作不冲突从而实现非阻塞读的并发控制技术。核心要点拆解“多版本”每条数据被修改时都会生成一个新的版本旧版本保留存在Undo Log中“并发控制”通过规则让不同事务读取到自己“有权限”的版本实现读写不阻塞实现载体快照读简单select是MVCC的具体应用场景当前读不依赖MVCC。二、 MVCC三大支柱隐藏字段、Undo Log、ReadViewMVCC不是单一技术而是由“数据记录的隐藏字段”“Undo Log版本链”“ReadView读视图”三个组件协同实现的。就像“时光相册”能正常使用需要“照片的元信息”“相册的排序”“访问相册的权限清单”三者配合。1. 隐藏字段每条记录的身份证InnoDB引擎会给每张表的每条记录自动添加3个隐藏字段不用手动定义数据库底层维护用于标记数据的版本和关联历史版本-- 实际存储结构你看不到但确实存在 CREATE TABLE invisible_fields(idINT PRIMARY KEY, -- 以下是隐藏字段 -- DB_TRX_ID BIGINT, -- 最后修改的事务ID DB_ROLL_PTR POINTER, -- 指向上一个版本的指针 DB_ROW_ID BIGINT, -- 隐藏主键如果没有主键 -- 你的表字段 -- name VARCHAR(50), age INT);字段详解DB_TRX_ID最后修改这条记录的事务ID。事务ID是自增的越大表示事务越新。比如事务10插入一条记录DB_TRX_ID10之后事务20修改了这条记录DB_TRX_ID就更新为20。DB_ROLL_PTR指向这条记录上一个版本的指针类似链表相当于“时光相册”里的“上一张照片”索引。旧版本的数据存在Undo Log中通过这个指针就能串联起所有历史版本形成“版本链”。DB_ROW_ID隐藏主键保证每行记录唯一。如果表没有手动指定主键PRIMARY KEYInnoDB会自动生成这个隐藏字段作为主键自增如果表已有主键这个字段就不会生成。作用是保证每条记录的唯一性和MVCC的版本控制间接相关。可视化理解一条记录的结构就像“照片标签”——数据内容是“照片”DB_TRX_ID、DB_ROLL_PTR是“标签”标注了这张照片的“拍摄者事务ID”和“上一张照片的位置回滚指针”。2. Undo Log(回滚日志)数据的后悔药和时光胶囊Undo Log不仅是回滚工具还是MVCC的版本库// Undo Log的两种角色 class UndoLog{//1. 回滚日志事务失败时撤销操作functionrollback(){// 回滚INSERT删除新记录 // 回滚UPDATE恢复旧值 // 回滚DELETE恢复已删除记录}//2. MVCC版本链存储历史版本functioncreateVersionChain(){// 每次修改都保留旧版本 // 新版本指向旧版本形成链表}}先回顾Undo Log的核心特性Insert操作的Undo Log只在事务回滚时需要事务提交后会立即删除因为插入的记录只有当前事务可见其他事务不会读插入前的“空版本”Update/Delete操作的Undo Log不仅用于回滚还用于MVCC的快照读其他事务可能需要读修改前的旧版本所以事务提交后不会立即删除要等没有事务需要这个版本时才会被清理由purge线程负责。Undo Log版本链的形成过程当不同事务或同一事务多次修改同一条记录时会生成多条Undo Log这些日志通过“回滚指针DB_ROLL_PTR”串联成一条“版本链”。版本链的特点链表的“头部”最新的旧版本离当前时间最近的修改版本链表的“尾部”最早的旧版本第一次修改前的原始版本每条版本都包含自己的DB_TRX_ID修改该版本的事务ID和DB_ROLL_PTR指向上一个版本的指针。示例版本链如何生成假设表user(id, name)id为主键有3个事务依次修改同一条记录id1初始name“张三”事务10TRX_ID10将name改为“李四”生成一条Update Undo Log记录旧值name“张三”DB_TRX_ID10DB_ROLL_PTR指向null此时是第一个版本事务20TRX_ID20将name改为“王五”生成一条Update Undo Log记录旧值name“李四”DB_TRX_ID20DB_ROLL_PTR指向事务10生成的版本事务30TRX_ID30将name改为“赵六”生成一条Update Undo Log记录旧值name“王五”DB_TRX_ID30DB_ROLL_PTR指向事务20生成的版本。最终形成的版本链【当前最新版本name“赵六”TRX_ID30】 ← 【版本2name“王五”TRX_ID20】 ← 【版本1name“李四”TRX_ID10】 ← 【原始版本name“张三”】3. ReadView(读视图)决定你能看到什么有了“版本链”时光相册里的所有照片还需要一个“规则”来判断当前事务能读取版本链中的哪一个版本这个规则就是ReadView读视图。ReadView的核心作用记录并维护“当前系统中活跃的事务ID”未提交的事务作为快照读时判断版本可见性的依据。就像“访问相册的权限清单”规定了哪些“照片版本”可以看。class ReadView{// 四个核心属性 long creator_trx_id;// 创建者的事务ID long[]m_ids;// 活跃事务ID列表未提交的事务 long min_trx_id;// 最小活跃事务ID long max_trx_id;// 最大事务ID 1// 核心方法判断版本是否可见 boolean isVisible(Version version){// 判断逻辑后面详细讲解}}ReadView的4个核心字段每个ReadView都包含4个固定字段用于判断版本可见性m_ids当前活跃的事务ID集合所有未提交的事务ID列表min_trx_id当前活跃事务ID中的最小值最小的“未提交事务ID”max_trx_id预分配的下一个事务ID当前系统中最大的事务ID 1因为事务ID是自增的creator_trx_id创建这个ReadView的事务ID当前执行快照读的事务ID。三、 版本链访问规则判断是否可见快照读时InnoDB会先生成ReadView再从版本链的“头部”最新旧版本开始依次判断每个版本的DB_TRX_ID修改该版本的事务ID是否符合规则。如果符合就读取这个版本如果不符合就通过回滚指针找下一个版本直到找到符合规则的版本或版本链结束返回空。1. 判断版本可见性的四大规则// 简化版可见性判断逻辑 boolean isVersionVisible(Version version, ReadView readView){// 规则1自己修改的自己能看到if(version.trx_idreadView.creator_trx_id){returntrue;}// 规则2版本事务ID最小活跃事务ID → 已提交可见if(version.trx_idreadView.min_trx_id){returntrue;}// 规则3版本事务ID最大事务ID → 将来事务创建的不可见if(version.trx_idreadView.max_trx_id){returnfalse;}// 规则4版本事务ID在活跃事务列表中 → 未提交不可见if(readView.m_ids.contains(version.trx_id)){returnfalse;}// 其他情况事务已提交且不在活跃列表中可见returntrue;}以下是通用的版本可见性判断规则RC和RR隔离级别共用这组规则差异仅在于ReadView的生成时机如果当前版本的DB_TRX_ID creator_trx_id修改这个版本的事务就是当前执行快照读的事务→ 可见自己修改的版本自己当然能看如果当前版本的DB_TRX_ID min_trx_id修改这个版本的事务在当前所有活跃事务之前就已提交→ 可见事务已提交其修改的版本对其他事务可见如果当前版本的DB_TRX_ID max_trx_id修改这个版本的事务是在当前ReadView生成之后才启动的→ 不可见事务还没开始其修改的版本还没生成自然看不到如果当前版本的min_trx_id DB_TRX_ID max_trx_id修改这个版本的事务在当前活跃事务范围内→ 再判断DB_TRX_ID是否在m_ids活跃事务集合中若在不可见事务未提交其修改的版本还不能被其他事务看若不在可见事务已提交其修改的版本对其他事务可见。简化记忆自己改的能看比所有未提交事务都早提交的能看还没开始的事务改的不能看正在运行未提交的事务改的不能看已提交的能看。2. 实战示例银行账户余额变化假设账户初始余额为1000元-- 事务执行顺序 -- 事务100INSERT INTO accounts(id, balance)VALUES(1,1000);-- 事务101UPDATE accounts SET balance800WHEREid1;-- 事务102UPDATE accounts SET balance900WHEREid1;-- 事务103SELECT balance FROM accounts WHEREid1;-- 此时事务101已提交事务102未提交版本链V3:balance900(trx_id102, 未提交)← 指向 V2 ↑ V2:balance800(trx_id101, 已提交)← 指向 V1 ↑ V1:balance1000(trx_id100, 已提交)← 链表尾部DB_TRX_ID102m_ids[102]min_trx_id[102]max_trx_id[104]creator_trx_id创建这个ReadView的事务ID当前执行快照读的事务ID。不同事务的读取结果事务103创建ReadView时活跃事务[102]检查V3trx_id102在活跃列表中 → 不可检查V2trx_id101 min_trx_id → 可见 → 返回800如果是事务102自己查询检查V3trx_id102 creator_trx_id → 可见 → 返回900四、 隔离级别的实现差异1. READ COMMITTED读已提交核心特点每次快照读都创建新的ReadView。这意味着每次快照读都会获取“当前最新的活跃事务状态”只能读到“已提交的最新版本”。示例场景事务ATRX_ID100执行快照读此时系统中有活跃事务BTRX_ID200未提交和已提交事务CTRX_ID150。第一次select生成ReadViewm_ids[200], min_trx_id200, max_trx_id201, creator_trx_id100→ 版本链中事务C150的版本符合规则150 200读取事务C的版本事务B提交TRX_ID200第二次select重新生成ReadViewm_ids[], min_trx_id201, max_trx_id201, creator_trx_id100→ 版本链中事务B200的版本符合规则200 201读取事务B的版本结论两次select读到不同版本不可重复读这就是RC隔离级别的特点。2. REPEATABLE READ可重复读核心特点一个事务中只有第一次执行select快照读时生成ReadView后续所有快照读都复用这个ReadView。这意味着后续即使有其他事务提交当前事务也不会看到其修改的版本从而实现“可重复读”。示例场景用上面的场景事务ATRX_ID100执行快照读系统中有活跃事务BTRX_ID200未提交和已提交事务CTRX_ID150。第一次select生成ReadViewm_ids[200], min_trx_id200, max_trx_id201, creator_trx_id100→读取事务C150的版本事务B提交TRX_ID200第二次select复用第一次的ReadViewm_ids仍为[200]min_trx_id仍为200→ 事务B的DB_TRX_ID200在m_ids中虽然事务B已提交但ReadView没更新仍认为它是活跃的所以事务B的版本不可见继续找下一个版本还是读取事务C150的版本结论两次select读到相同版本可重复读这就是MySQL默认隔离级别的实现原理。关键差异RC和RR的版本访问规则完全相同唯一差异是ReadView的生成时机——RC每次快照读都生成新的RR只生成一次并复用。这也是MVCC实现不同隔离级别一致性的核心。3. 两种隔离级别的对比特性READ COMMITTEDREPEATABLE READReadView创建时机每次快照读都创建第一次快照读创建可见性变化能看到其他事务的已提交修改看不到其他事务的已提交修改幻读问题可能出现幻读通过MVCC避免幻读适用场景数据实时性要求高数据一致性要求高性能影响频繁创建ReadView开销较大ReadView复用开销较小五、 MVCC完整工作流程1. 数据插入流程-- 插入一条新记录 INSERT INTO users(id, name, age)VALUES(1,张三,25);-- MVCC内部执行 --1. 分配事务ID假设trx_id100 --2. 设置隐藏字段 -- DB_TRX_ID100-- DB_ROLL_PTRNULL没有历史版本 --3. 创建Undo Log用于回滚2. 数据更新流程-- 更新记录 UPDATEusersSET age26WHEREid1;-- MVCC内部执行假设事务ID101 --1. 创建新版本Copy-on-Write --2. 设置新版本 -- DB_TRX_ID101-- DB_ROLL_PTR → 指向旧版本 --3. 修改旧版本的DB_ROLL_PTR指向新版本 --4. 创建Undo Log记录旧值3. 数据读取流程-- 读取记录假设在事务102中 SELECT * FROMusersWHEREid1;-- MVCC内部执行 --1. 找到记录的最新版本DB_TRX_ID101 --2. 创建ReadView或在RR中复用已有 --3. 从最新版本开始沿版本链回溯 -- a. 检查版本101是否可见 -- b. 如果不继续检查版本100 -- c. 找到第一个可见的版本返回六、 MVCC与锁的协同工作1. 什么时候用MVCC什么时候用锁-- 场景1只读查询 → 使用MVCC快照读 SELECT * FROM products WHERE category电子产品;-- 不加锁读取历史版本不影响其他事务写操作 -- 场景2读写冲突 → 使用锁当前读 SELECT * FROM orders WHEREid100FOR UPDATE;UPDATE orders SET status已发货WHEREid100;-- 加锁确保数据一致性 -- 场景3混合场景 → MVCC 锁 BEGIN;-- 快照读使用MVCC读取库存 SELECT stock FROM products WHEREid1;-- MVCC -- 当前读修改库存时加锁 UPDATE products SET stockstock -1WHEREid1;-- 加锁 COMMIT;2. MVCC的优势与局限优势高并发读写不冲突大幅提升并发性能无阻塞读读取操作永远不需要等待避免死锁读操作不加锁减少死锁概率实现隔离级别支持RC和RR隔离级别局限存储开销需要额外空间存储多版本版本清理需要定期清理过期版本写冲突写操作仍然需要加锁处理历史数据长期运行的事务可能阻止旧版本清理七、 实战MVCC如何解决并发问题1. 脏读问题解决-- 事务A BEGIN;UPDATE accounts SET balancebalance -100WHEREid1;-- 未提交 -- 事务B使用MVCC BEGIN;-- 脏读如果事务B能读到未提交的修改就是脏读 SELECT balance FROM accounts WHEREid1;-- MVCC会返回之前已提交的版本 -- 结果不会看到事务A未提交的修改2. 不可重复读问题解决-- 事务A BEGIN;SELECT balance FROM accounts WHEREid1;-- 返回1000 -- 事务B提交修改 UPDATE accounts SET balance800WHEREid1;COMMIT;-- 事务A再次查询 SELECT balance FROM accounts WHEREid1;-- 在RR级别下MVCC保证仍返回1000 -- 结果可重复读3. 幻读问题解决-- 事务A BEGIN;SELECT COUNT(*)FROM orders WHERE statuspending;-- 返回5 -- 事务B插入新记录 INSERT INTO orders(status)VALUES(pending);COMMIT;-- 事务A再次查询 SELECT COUNT(*)FROM orders WHERE statuspending;-- 在RR级别下MVCC保证仍返回5 -- 结果没有幻读八、 MVCC性能优化与监控1. 监控MVCC相关指标-- 查看Undo Log使用情况 SHOW ENGINE INNODB STATUS\G -- 查看TRANSACTIONS部分的History list length -- 查看长事务可能阻止版本清理 SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started))60;-- 查看等待清理的版本 SELECT NAME AS table_name, NUM_ROWS, CLUST_INDEX_SIZE, OTHER_INDEX_SIZE FROM information_schema.INNODB_SYS_TABLESTATS;2. 优化建议--1. 控制事务大小及时提交 -- 不好的做法 BEGIN;-- 执行大量操作... -- 长时间不提交占用版本链 COMMIT;-- 好的做法 BEGIN;-- 快速完成操作 COMMIT;--2. 避免长查询 -- 设置查询超时 SET max_execution_time5000;--5秒超时 --3. 定期维护 -- 在业务低峰期执行 OPTIMIZE TABLE large_table;九、 常见问题解答Q1MVCC能完全避免锁吗A不能。MVCC主要优化了读操作避免了读锁。但写操作仍然需要加锁来保证数据一致性尤其是当前读操作SELECT … FOR UPDATEINSERT、UPDATE、DELETE操作唯一约束检查Q2版本链会无限增长吗A不会。InnoDB有purge线程专门清理不再需要的旧版本。清理条件没有活跃事务需要这个版本版本已提交且超过一定时间Undo Log空间需要回收Q3MVCC对存储有什么影响AMVCC会增加存储开销主要体现在额外字段每行数据多3个隐藏字段Undo Log存储历史版本版本链维护多个版本但相比带来的并发性能提升这个开销通常是值得的。Q4为什么RC级别下能看到已提交的修改A因为RC级别每次快照读都创建新的ReadView新的ReadView能看到之前已提交的事务。Q5MVCC和锁哪个性能更好A不同场景适用不同技术高并发读MVCC性能远优于锁-高并发写锁机制更直接高效混合负载MVCC锁的组合最优十、 总结MVCC的本质的是“用空间换时间”通过保留数据的历史版本占用Undo Log空间避免了读写操作的阻塞节省并发等待时间。其核心逻辑可以总结为数据修改时生成新版本记录DB_TRX_ID和DB_ROLL_PTR旧版本存入Undo Log并串联成版本链快照读时生成ReadViewRC每次生成RR只生成一次版本判断从版本链头部开始用ReadView的规则判断版本可见性找到符合规则的版本并读取。MVCC的核心价值高并发实现读写不阻塞大幅提升并发访问性能MySQL能支撑高并发MVCC功不可没一致性配合隔离级别实现不同程度的数据一致性如RR的可重复读高效回滚Undo Log不仅支撑MVCC还支撑事务回滚一举两得。必记的核心要点MVCC只作用于快照读简单select当前读加锁select、DML不依赖MVCC三大核心组件隐藏字段DB_TRX_ID、DB_ROLL_PTR、Undo Log版本链、ReadViewRC和RR的差异ReadView生成时机不同每次vs一次导致是否可重复读Undo Log的作用回滚 存储数据历史版本支撑MVCC。