所有内容均为测试可用,真实
当前位置:绿茶加糖-郭保升 > 数据库资料 > 正文

一次讲透事务、锁和性能的秘密!

正儿八经说 InnoDB ,肯定晦涩难懂,所以下面按照我们老师上课语气,瞎掰 InnoDB,应该好懂一些。MySQL 5.5 版本后,InnoDB 作为 MySQL 默认存储引擎,真的是个厉害角色,是企业级应用的扛把子,搞事务(ACID)、玩高并发、支持行锁,都得靠它。它里边设计得挺巧妙,塞满了各种东西:内存缓冲、日志系统(redo/undo)、多版本控制(MVCC)…… 我们下面就掰开了、揉碎了仔细聊聊它,主要从它怎么实现这些牛X特性、以及我们平时怎么用效果最好入手,仅供参考。

一、事务支持(ACID):不只是说说而已

为啥大家都用 InnoDB?就是它那实打实搞定事务这四大金刚(ACID),即:InnoDB 是 MySQL 中几乎唯一完全支持 ACID(原子性、一致性、隔离性、持久性)的存储引擎。支撑这套东西的,主要有三剑客:事务日志(Redo/Undo) 和 MVCC,即:其事务机制依赖三大核心组件:事务日志、锁机制、MVCC(多版本并发控制)。

1、原子性(Atomicity):整活儿必须完整!事务要么全执行,要么全回滚
这意思很简单:一个事务,要么全做,要么全别做,不能做一半扔那儿。

关键在 Undo Log(回滚日志)。比方你更新一个字段(UPDATE),Undo Log 就悄悄记下“这家伙原来值是啥”(相当于留了个反操作的说明书)。删除(DELETE)?它就记“被删的那行数据长啥样”。
万一事务半路翻车了(如:服务器崩了),InnoDB 就翻出 Undo Log,照着说明书把修改都“撤”回去,直接回档到事务开始之前。
Undo Log 还有个副业:给 MVCC 提供历史版本数据,比如读“老黄历”数据的时候。

2、一致性(Consistency):数据讲规矩,事务执行前后数据状态符合业务规则
目标是事务干完活,数据还能符合咱定的那些规则(如:账户总额不变)。
靠 约束 (外键、唯一索引这些) 和 隔离 来保证。
约束:比如你定了外键,订单表里的用户ID,必须在用户表里找得到,否则就不让你插,强制保证关系没错。隔离性呢,就是通过锁和 MVCC 避免并发事务的相互干扰,防止别的事务捣乱(脏写破坏规则)。
拿转账来说,扣钱和加钱必须绑死一起成功或一起失败,不能只扣不加(那不乱套了)。

3、隔离性(Isolation):别互相打扰!并发事务相互独立,避免干扰
几个事务同时跑,得让它们感觉不到别人存在(或者控制在可接受的程度)。
MySQL 有四种隔离级别(用 transaction_isolation 设定):

读未提交 (Read Uncommitted):最野,能看到别人没提交的数据(脏读)。基本上没谁用,太容易出事了。
读已提交 (Read Committed, RC):成熟点,只读别人提交后的数据,解决了脏读。但有个问题:同一个事务里,两次读可能结果不一样(不可重复读)。
可重复读 (Repeatable Read, RR):InnoDB 默认!保证你事务里看到的数据,跟事务开始那会儿一样,解决了不可重复读。还顺便用间隙锁幻读也给按住了(你查的时候没这条,结果别人插了一条进来吓你一跳)。
串行化 (Serializable):最狠但最慢。直接让事务排队,一个接一个执行,完全没并发问题了,但性能也差到感人。
隔离性的实现靠 (行锁、间隙锁) 和 MVCC。注意:在 RR 下,普通的 SELECT 是快照读(读历史版本,不锁数据),而 SELECT ... FOR UPDATE 那种就叫当前读了(要锁行)。

4、持久性(Durability):干了就得认账!事务提交后数据永久保存
提交成功的数据,必须保证丢不了,即使数据库崩了重启。
依赖 Redo Log(重做日志)。这家伙不记具体数据行改成了啥样,记的是物理操作:如:“在某个页的第 XX 字节位置写入了 ZZZ”。而且人家用的是 WAL 机制(Write-Ahead Logging):数据页还没往磁盘里写呢,Redo Log 必须先写进去。
你事务一提交,InnoDB 先把对应的 Redo Log 刷到硬盘里(确保落盘 fsync)。这样即使突然崩了,重启的时候也能靠 Redo Log “重放”操作,把丢失的数据恢复回来。
还有个帮手 双写缓冲 (Doublewrite Buffer)。防一种恶心的情况“部分写失败”:比如一个数据页很大(16K),写到一半突然断电了,磁盘上的页就废了。咋办?写之前,先把这页完整地复制一份到一个连续的磁盘区域(双写缓冲),然后再覆盖目标页。万一覆盖时断了电,目标页坏了?没关系,恢复的时候用双写缓冲里的完整副本 + Redo Log,就能把这页完美复原了。相当于多留了个安全备份。

二、行级锁:挤破头时的VIP通道,高并发的核心保障

为啥 InnoDB 扛得住多人同时操作?秘密武器就是行级锁!只锁你想改的那一行,不像表锁那样一棍子打死一桌人,大大减少了打架冲突。

1、锁的品种可不少:
共享锁 (S锁):我想读这行,给你也加个读锁吧?放心,能同时读,大家一起读(读共享)。用 SELECT ... LOCK IN SHARE MODE; 主动加。
排他锁 (X锁):我要改这行,谁都别动!等我改完(写独占)。隐式情况:你执行 UPDATE/DELETE/INSERT 时自动加;显式用法:SELECT ... FOR UPDATE;
意向锁 (IS/IX):表级的“打招呼”。我(事务)打算给表里某些行加 S锁/X锁了,通知一下,防止表锁和行锁打架。比如你要加个行S锁,得先给表挂个 IS 锁;要加行X锁,得先挂个 IX 锁。
间隙锁 (Gap Locks):锁的不是实际数据行,是行与行之间的空档(如:索引值10和20之间的那片虚无)。为啥锁这玩意儿?主要防幻读(在 RR级别下)。比如:SELECT ... WHERE id BETWEEN 10 AND 20 FOR UPDATE,它就锁住 10到20 的空隙,不让别人往里插新行(ID=15 这种)。
临键锁 (Next-Key Locks):InnoDB 对付幻读的常规武器。它 = 行锁 + 其前面的间隙锁。例如:索引有值10, 20, 30。它锁的范围是 (-∞, 10](10, 20](20, 30](30, +∞)。理解成“锁上当前行,并锁上它前面的那条缝”。
插入意向锁 (Insert Intention Locks):一种特殊的间隙锁。当你想往某个间隙里插数据时,就挂上这个锁。但注意哦,几个人都往同一个间隙插(比如都往10和20之间插),只要插入位置不一样(一个插15,一个插16),这个锁是兼容的,不会互相卡死。

2、锁靠索引罩着:锁的实现依赖索引
行锁得通过索引记录来锁。没索引?那抱歉,只能退化成表锁了!如:UPDATE ... WHERE age=20,要是 age 没建索引,InnoDB 得扫全表,然后...就悲催地把整个表都锁上。这是大坑!一定注意。
用辅助索引(二级索引)时,除了锁辅助索引记录,还得把对应的聚簇索引(主键)记录也锁了,不然数据一致性怎么保证?

3、死锁咋办?
俩人(事务)互相等对方松手?这就死锁了。InnoDB 内置检测器(innodb_lock_wait_timeout 设个超时时间,默认50秒),发现死锁后会自动踢掉一个(通常是那个做活少、持有锁也少的冤种事务),释放资源,让另一个活下来。
咱写代码时预防死锁小技巧:操作多个表多个对象时,按固定顺序(比如都先 A 后 B)。别一会儿先 A 后 B,一会儿又先 B 后 A,容易打架。再一个就是事务尽量短小精悍,别磨磨唧蹭占着锁不撒手。

三、外键:数据关系的铁律,数据完整性的强制保障

这个 MySQL 引擎里也就 InnoDB 玩得转正牌外键(FOREIGN KEY。强关联的表就得靠它保证数据不乱套。专家说:说句人话就是:InnoDB 是 MySQL 中唯一支持外键(FOREIGN KEY)的存储引擎,通过外键约束保证关联表之间的数据一致性。

1、干嘛使?外键的核心作用
管着子表的写操作:子表的外键值,爹(父)表(主表)的主键里必须有。如:orders 表的 user_id,必须在 users 表的 id 里找得到。
支持级联:主表动一下,子表可以跟着动。ON DELETE CASCADE 意思是主表删了某用户,用户的订单也跟着自动清掉;ON UPDATE CASCADE 就是主键变了,子表外键也跟着变。

2、怎么干?外键的实现机制
外键依赖父表的主键或者唯一索引上(确保关联值唯一)。好消息是,建完外键,子表的那个外键列会自动建索引(方便关联查询)。
检查时机默认在事务提交时foreign_key_checks=1)。有时嫌慢(比如你要导入超大批量数据),可以暂时关掉检查 SET foreign_key_checks=0,导完再开回来。记住!导完一定得开回来,而且关的时候别做破坏关系的事,不然开了检查就报错。

3、啥时候用?啥时候别用?
非常适合强绑定数据:订单 vs 用户,学生选课 vs 课程表。有它把关,数据关系贼牢靠。
缺点:写数据得多一步检查,需检查父表数据(看看爹在不在),开销大点。在搞分库分表这种高大上架构时,外键就成鸡肋了(跨分片检查太麻烦),这时不如把关联逻辑放在应用代码里搞定。

四、聚簇索引:数据和钥匙放一起

InnoDB 里存储数据跟别家有点不一样,它用的是**聚簇索引 (Clustered Index)**。这设计让数据物理存储跟索引绑一块了,是性能关键点。

1、啥特点?
索引即数据!聚簇索引的叶子节点(B+树最底层),直接存着完整的一行数据记录(row),而不是指向数据的地址指针。
主键是老大,默认基于主键构建:定义了主键?那主键索引就是聚簇索引。没主键咋办?找第一个非空唯一索引顶上。再不行?InnoDB 给你造个隐藏的 ROW_ID 当聚簇索引(尽量自己定义主键)。

2、辅助索引和回表查询的爱恨情仇:
辅助索引(如:普通索引、联合索引)的叶子节点存的是啥?是主键值!不是完整数据。
所以,用辅助索引查数据:先找到主键值 -> 再用这个主键值跑回聚簇索引查完整数据行。这叫 回表查询(Bookmark Lookup)
优化诀窍:覆盖索引!如果我要查的字段,比如:SELECT id, name,正好都在我建的 (age,name) 这个联合索引里(叶子节点直接有),那就不用再去回表查聚簇索引了,省一步,性能嗖嗖的。

3、优点和槽点:
优:按主键查,快如闪电(一步到位)。范围查询(按主键范围查一堆)效率高,因为主键临近的行很可能就存在一起(连续磁盘页)。
坑:插入顺序影响性能。用自增主键(顺序增长),新行都往最后插,很舒服。用 UUID 这种乱序的?乱插容易导致页分裂(空间不够了挪数据),慢得很。还有就是辅助索引查最终数据得回表,多绕一下。

五、MVCC:读不挡写,即读写不阻塞的并发魔法

MVCC(多版本并发控制) 就是 InnoDB 能让你边读边写的秘密武器。允许读写操作并发执行,大幅提升读性能。没有它,高并发查询下锁打架太严重了。

1、魔法核心:MVCC 的核心原理
每一行数据背后,可能藏了好几个历史版本
每个版本标上号(DB_TRX_ID 谁改的,DB_ROLL_PTR 指向上个版本在哪)。DB_ROLL_PTR 指向 Undo Log,形成了一个版本链表。
查询的时候,不看最新数据(可能有别人在改),而是根据当前事务的 ID 和版本规则,挑一个合适的历史快照来看,完全不用上锁!

2、谁可见?规则咋定?版本号与可见性判断
DB_TRX_ID 告诉你这版本是哪个事务最后改的。记录最后修改该行的事务 ID(自增唯一)。
DB_ROLL_PTR:指向 undo log 中的上一版本数据(形成版本链)。
(以 RR 级别为例)事务读数据时:只能看到 DB_TRX_ID < 我事务ID 且已提交的版本,或者我自己改的版本。未提交的别人(未提交事务的版本对其他事务不可见。)?拜拜您嘞,看不见!

3、快照读 vs 当前读:
快照读:就是普通 SELECT。走 MVCC 路线,读历史快照,不加锁,快乐。
当前读SELECT ... FOR UPDATEUPDATEDELETEINSERT 这些。它们读最新提交的版本,并且会加锁,确保别人不能在我操作时乱改。

4、赢在哪?MVCC 的优势
读写互不阻塞(读写并发)!这是最牛的。读不会等着写操作释放锁,写操作也不用怕被读锁卡住(具体锁类型决定冲突)。避免了读锁的开销,适合读多写少的场景,特别是那种读远远多于写的业务场景,性能提升飞起。

六、缓冲池(Buffer Pool):内存里的高速路,内存中的性能加速器

数据库慢主要就慢在读写硬盘。InnoDB 的 **缓冲池 (Buffer Pool)**,就是个把热门数据偷偷存进内存的大缓存池子,让你大部分操作在内存里就搞定了。

1、这池子长啥样?缓冲池的结构
大小很重要!由 innodb_buffer_pool_size 参数管着(一般设服务器内存一半到七八成,给操作系统和其它应用留点饭)。里面放的是一个个页(页大小通常默认16KB,跟磁盘数据页对齐)。
缓存啥?数据页、索引页、Undo 页这些有用的页都会被塞进来。

2、咋管理?缓冲池的管理机制
淘汰机制:LRU (最近最少使用) 算法。内存是有限的,缓存啥不能装就淘汰谁?优先踢掉长时间没人碰的页。但简单 LRU 有问题,比如来个全表扫描,一下把缓存全冲了(全是只读一次的数据)。InnoDB 搞了个优化:把 LRU 分成两段,一个放年轻活跃页(young区),一个放老年页(old区)。新来的页先在老年区待着,等命中了次数多了才“升迁”到年轻区,避免了偶发大查询污染热点数据。
预读机制:聪明的猜测。觉得你接下来可能要查的数据,先偷偷提前加载进缓冲池。比如你连续查了几个数据页(顺序读),InnoDB觉得你要线性读下去?那就提前读后面几个页进来(线性预读)。或者基于索引范围(随机预读)。
脏页刷新:缓冲池中被修改的数据页(脏页)通过后台线程异步刷新到磁盘(innodb_flush_log_at_trx_commit控制刷新策略),避免阻塞用户线程。就是你在内存里改的数据页(缓存池里变脏了),不会一改就写硬盘。后台有线程专门悄悄地把它们 异步 刷新到磁盘里(别担心数据丢,有 Redo Log 兜着呢!)。刷新策略由 innodb_flush_log_at_trx_commit 等参数控制,在安全性和性能之间找平衡。

3、重要指标:命中率!
想知道缓存池给不给力?重点看 缓存命中率(计算公式大概:Innodb_buffer_pool_read_requests / Innodb_buffer_pool_reads)。这个值越高越好,99%+那是相当理想了,说明你几乎都是在内存里操作。
性能优化:没啥魔法,就是给够内存!根据你数据库的真实大小和热点数据量,设定个足够大的 innodb_buffer_pool_size。小了,命中率低,硬盘就遭罪了。

七、崩溃恢复:挂了也得爬起来,日志系统的双重保障

不怕一万就怕万一,数据库服务突然崩了?InnoDB 靠日志系统保证重启后不乱套:已提交的不丢,未提交的打回原形,即:InnoDB 通过 redo log 和 undo log 实现崩溃后的一致性恢复,确保已提交事务不丢失,未提交事务完全回滚。

1、Redo Log(重做日志) - 保证提交的不丢:
它负责记录:事务对数据页做的具体物理修改(动作)。专门用来重放已提交的事务。
物理结构是环形文件(ib_logfile0ib_logfile1),写满第一个接着写第二个,覆盖第一个。大小由 innodb_log_file_size(单个大小)和 innodb_log_files_in_group(个数)决定。太大恢复慢,太小要频繁刷盘?建议设512MB~2GB之间平衡。
写入流程:操作来了,先写到 Redo Log Buffer (内存)。提交事务时,关键参数 innodb_flush_log_at_trx_commit 决定何时刷盘:
=1:最稳!每次提交都强制刷盘。提交成功=数据丢不了。
=0:最浪!日志每秒刷一次。提交完立马崩?可能最多丢1秒数据。
=2:折中。日志立刻写进操作系统缓存,但让操作系统决定啥时真正刷盘。操作系统健壮时还行,系统崩了也可能丢点数据。

2、Undo Log(回滚日志) - 保证未提交的滚回去:
记录逻辑反向操作(比如回滚时把值改回去),用于事务回滚和给 MVCC 提供历史版本。
存放位置:要么在共享表空间 ibdata 里,要么在独立表空间里(由 innodb_file_per_table 选项控制)。
事务提交后,Undo Log 不会立马删!它可能还要给其他事务做 MVCC 历史快照用。没用的旧版本会由Purge线程异步清理掉。

3、重启恢复流程:
(1)Phase 1 - Redo Playback:数据库启动,先拿出 Redo Log,把里面所有标记为“已提交”的操作重做一遍(应用修改到数据页),确保提交的数据不丢失。
(2)Phase 2 - Undo Rollback:再把 Undo Log 里属于“未提交”事务的操作(反向逻辑)执行一遍(相当于撤销未提交事务的动作),回滚到事务开始之前的状态。最后数据就是既持久又一致的了。

八、用在哪最爽?怎么用才爽?适用场景与最佳实践

简单说,大部分需要点严肃性的业务场景,用 InnoDB 准没错。特别是这些:
高并发读写:电商抢购、社交点赞评论之类。行锁+MVCC有效缓解卡顿。
强事务依赖:银行转账、订单支付。ACID 特性核心保障。
复杂查询:各种业务报表、多条件筛选。聚簇索引+缓冲池优化查询。

最佳实践 (划重点!):

(1)主键好好搞
优先用自增整数(BIGINT) 做主键。顺序插入对聚簇索引友好(减少页分裂),存储空间还小(辅助索引要存主键值,大了浪费)。
别用 UUID 之类的随机长串做主键!容易页分裂,降低插入性能。

(2)索引不是越多越好,要巧用
为常出现 WHEREJOINORDER BY 的列建索引。
避免无索引写操作UPDATE/DELETE 没索引列条件?锁警告!)。
善用覆盖索引:想办法让索引直接包含查询需要的所有字段(SELECT a, b FROM table WHERE c = ? 就建 (c, a, b)),消除回表。
合理使用联合索引:注意最左匹配原则。

(3)别让事务变“长工”
避免长事务!事务时间长,锁就长时间占着(尤其还可能锁元数据MDL),会卡住后面等着的人(DDL、备份之类)。尽快提交!
隔离级别别乱设:绝大部分场景,RR (默认) 或 RC 足够了。RC 在特定读场景下有时比 RR 锁冲突少(因为只锁已提交行)。Serializable?真用到的请举手... 估计极少。

(4)内存要给足
innodb_buffer_pool_size 是 MySQL 性能的关键调优点之一!要设得足够大,把常访问的数据和索引都装进去。服务器内存80%给缓冲池?也不是不行,留点给 OS 和 App。

(5)日志配置看需求
强一致性场景(如:金融):innodb_flush_log_at_trx_commit=1,配合 sync_binlog=1。虽然每次提交刷盘性能慢点,但保证零丢失。
能容忍丢一点数据(如:用户行为日志等):考虑 =0 或 =2 提升性能。
innodb_log_file_size:前面说了,1~2个G一般适合现代系统,太大恢复慢,太小导致频繁 checkpoint。

总结:InnoDB 这引擎能打,真的不是白给的。它把 Redo/Undo 日志、行级锁、MVCC、聚簇索引、缓冲池这些法宝揉在一起,巧妙地同时满足了事务安全高并发性能这俩“既要……又要……”的需求。理解它的内部机制(这些日志咋工作的?锁怎么加的?索引怎么存的?MVCC怎么判断版本的?),对于我们调优数据库(让 SQL 飞起来)、排查那些莫名其妙的慢、锁、崩等问题,绝对是必备基本功。用好了它,我们的后端服务稳定性可靠性都能往上窜一截!

版权保护: 本文由 绿茶加糖-郭保升 原创,转载请保留链接: https://www.guobaosheng.com/shujuku/409.html