尧图精选

数据库进阶实战:索引优化、事务隔离与并发控制

🕒 发布时间:2026/9/26 20:09:34 📁 来源:尧图网络
1. 第四部分到底在补什么学习笔记写到第68天数据库系列进入了第四部分。说句实话前面几部分学完的时候我心里是有底气的建表、约束、主外键、三大范式、单表和多表查询、增删改查这些基础语法和概念都过了一遍随便给张表写个查询语句基本不在话下。可真到了自己动手做点小项目、翻看别人的工程代码时才发现“会写SQL”和“能处理数据库问题”之间隔着一整片海。这片海就是第四部分要补的内容。我把这一阶段的学习目标拆成了三块每一块都对应实际开发中的一类痛点性能相关索引到底怎么工作、为什么建了索引查询还是慢、SQL优化从哪下手。解决的问题是“数据量一大系统就跑不动”。并发相关事务隔离级别、MVCC、锁机制、死锁以及多个用户同时读写同一批数据时怎么保证不出乱子。工程化相关连接池的作用与配置、生产环境常见数据库产品的差异、数据同步的基本思路解决的是“代码写完要上线了数据库这边怎么保障”。之所以放到第四部分才集中讲这些是因为前三部分的内容本质上是“知识”靠记忆和练习就能掌握而这一部分的内容是“能力”需要把零散知识点串成体系才能理解。举个例子事务的四种隔离级别背下来只要十分钟但如果你不知道MySQL默认是可重复读、Oracle默认是读已提交不知道这个默认值会怎么影响同一套业务代码在两个库上的表现那背下来的东西只是面试用的纸面答案。所以这阶段我要求自己从“我懂了”切换成“我能解决”每学一个点都追问一句这个知识点在真实项目里什么时候会来坑我这种心态上的转变比多记几个函数重要得多。数据库不是孤立的技术它连着应用代码、连着操作系统、连着业务架构。第四部分让我开始用“系统”的视角去看数据库很多以前觉得抽象的概念也一点点落地了。2. 索引与SQL优化先搞懂索引为什么不总是有用2.1 索引的本质以及最容易被忽略的代价几乎所有教程都会告诉你“索引能加速查询”但很少告诉你它加速的原理和代价。我是在自己动手测索引效果时才真正想明白的索引的本质是一种用空间换时间的有序数据结构在MySQL InnoDB里最常见的实现是B树。它把字段值按顺序组织起来查询时可以像查字典一样快速定位不需要从头到尾扫一遍整张表。但“有序”这两个字背后藏着成本。每次往表里插入一行、修改一行、删除一行数据库不仅要更新数据本身还要同步维护索引树的结构。字段上有几个索引写操作就要额外付出几份维护代价。这就是索引典型的“读快写慢”权衡——读得爽是靠写的时候忍着的。我这次实操时踩过一个实打实的坑。一张订单表每天业务高峰期要插入上百万行某天突然开始频繁出现写入卡顿。我排查了一大圈最后发现表上堆了八个索引其中有三个是之前某个临时报表任务加的任务结束后就没再用过。索引维护成本在写入量小的时候完全感觉不到一旦写入量大起来多余的索引就是拖垮写入性能的隐形杀手。删掉那三个废索引之后写入速度当场恢复。这个案例让我养成了一个习惯建索引之前先问三个问题——这个字段会被高频查询吗查询结果集大吗表的写入频率高吗三个答案共同决定索引该不该建、怎么建。2.2 用EXPLAIN揪出慢查询的三大关键点遇到慢查询我现在的第一反应不是对着SQL猜而是先让数据库把执行计划交出来。MySQL里就是EXPLAINOracle里有对应的执行计划视图。执行计划就像导航软件给的路线图你是绕远路走了全表扫描还是上了高速走了索引一眼就能看出来。我重点看三个信息type列从好到差大致是const、ref、range、index、ALL。看到ALL就说明全表扫描这是优化首要目标。key列实际命中的索引。有时候你明明建了索引但这列显示的是NULL说明索引压根没被用上这时候就要检查SQL写法了。rows列预估扫描的行数。这个数字和真实响应时间强相关数字越大越危险。最常见的索引失效写法我自己就中过好几次招在索引列上套函数。比如给create_time建了索引然后写WHERE DATE(create_time) 2024-01-01数据库为了判断每一行是否满足条件得先把所有行的create_time都算一遍DATE函数索引树的有序性直接失效只能全表扫。正确做法是改成范围条件WHERE create_time 2024-01-01 AND create_time 2024-01-02。改完之后执行计划里的type从ALL直接变成range数据量一大差距就是几十倍。还有一个特别值得记住的规则复合索引的最左前缀原则。如果建了(a, b)复合索引查询条件里只有a、或者同时有a和b才能走索引单独用b是不行的。这决定了建复合索引时字段顺序怎么排——把等值查询的字段放前面把范围查询的字段放后面。2.3 增删改查的代价并不对称第四部分我专门把增删改查重新过了一遍但这次的角度不是语法而是“代价”。语法层面谁都能写真正拉开差距的是对操作代价的理解。这四种操作的代价逻辑完全不同SELECT看着简单但关联查询、子查询、排序、分组都可能带来全表扫描或临时文件排序。联合索引、覆盖索引就是为SELECT服务的。INSERT代价主要来自索引维护。索引越多插入越慢。批量插入比逐条插入快得多因为可以减少日志刷盘次数和索引树重建次数。UPDATE本质上是“先查后改”要先把目标行定位出来再修改并更新索引。更新频率高的字段如果都建了索引代价会成倍增加。DELETE代价最容易被低估。删除一行不只是删数据还要处理索引同步、外键约束、触发器甚至记录binlog。更重要的是锁和undo log的问题。举一个实操例子清理一张千万级表的过期数据如果一次性DELETE几十万行这个事务会持有大量行锁、产生巨大的undo日志、拉长主从复制延迟甚至可能把从库拖垮。正确做法是分批删除每批删一万行commit一次循环执行。我实测过同样的清理任务分批执行不仅对在线业务的影响小很多整体耗时反而更短。这就是理解代价模型带来的实际收益。3. 事务、锁与并发控制多用户同时操作时的秩序3.1 把ACID和底层机制串起来以前学事务的ACID特性感觉就是四个字的缩写这次我把它们和底层机制串在一起才真正明白了这套体系。原子性靠undo log实现——事务执行过程中如果出错就靠undo log把数据回滚到执行前的状态持久性靠redo log实现——提交时把变更刷盘即使数据库崩溃也能重放恢复隔离性靠锁和MVCC多版本并发控制实现——让多个事务各看各的互不干扰一致性则是前面三个特性共同作用的结果。隔离级别和锁、MVCC的关系是这一部分的核心。四个隔离级别读未提交、读已提交、可重复读、串行化本质上是对“一个事务能看到另一个事务的什么状态”的不同约束。MVCC用快照实现了读写不互斥读事务读的是历史快照写事务在最新版本上操作两者不打架。这也是为什么数据库能同时扛住大量读写请求。这里有个非常实际的点不同数据库的默认隔离级别不一样。MySQL默认是REPEATABLE READ可重复读Oracle默认是READ COMMITTED读已提交。在MySQL的可重复读下一个事务内多次SELECT看到的是同一份快照结果一致在Oracle的读已提交下每次SELECT都是新快照可能看到其他事务刚提交的数据。同样的代码在两套数据库上跑出来的结果可能不一样。这个差异我在一个迁移项目里真实遇到过排查了很久才把问题定位到隔离级别上。3.2 死锁互相等待的僵局死锁是数据库并发里最经典的问题。通俗讲就是两个事务各自握着一把锁同时又都在等对方手上的锁谁也不肯先松手最后僵住。经典场景是两个事务按不同顺序更新多张表事务A先更新订单表再更新支付表事务B先更新支付表再更新订单表两者同时执行时就可能在某个中间时刻互相卡住。处理死锁有个反直觉的点死锁没法完全避免只能尽量减少并做好兜底。InnoDB有死锁检测机制检测到死锁后会立刻回滚其中一个事务让另一个继续执行然后抛出类似“Deadlock found when trying to get lock”的异常。所以从应用层角度看遇到死锁异常要有重试机制比如捕获异常后重试两三次。但比死锁更常见的是锁等待超时。死锁是两个人互相等锁等待是一个人等着另一个人释放。实际项目里经常是一个大事务或者慢查询长时间持着锁不放后面排队的写操作全部超时。这次我排查过一个线上问题一个统计任务在一个事务里处理了大量数据又不及时提交导致一张核心业务表的行锁被长时间占住高峰期积压了一堆UPDATE超时报错。解决办法就是拆事务、缩时间、及时提交。把事务拆小不只是为了减少回滚范围更是为了缩短锁的持有时间。3.3 间隙锁、MVCC与乐观锁悲观锁概念落到真实场景如果要给“数据库面试题”划个重点MVCC、间隙锁、next-key lock、乐观锁悲观锁绝对都在里面。但我的体会是面试官想听的不只是定义而是你是否有真实场景的理解。拿间隙锁来说MySQL在可重复读级别下范围查询会锁住命中的行以及行与行之间的“间隙”防止其他事务在这个间隙里插入新数据从而避免幻读。这个机制保证了数据一致性但也带来副作用一个范围很大的查询条件可能锁住一大片区间导致并发插入被阻塞。理解了这一点你就会明白为什么线上经常建议把大范围查询改成更精确的条件或者尽量走唯一索引。乐观锁和悲观锁也是同理。乐观锁通常用version字段实现UPDATE的时候带上WHERE version 期望值如果影响行数为0说明版本已经变了需要重试。它适合并发冲突少的场景读多写多但冲突少时效率高。悲观锁用SELECT ... FOR UPDATE直接锁住行适合冲突频率高的场景但锁持有期间其他事务都得等。代码里怎么选取决于业务对数据一致性的要求和对吞吐量的期望。面试时如果能讲清楚自己的项目里为什么选乐观锁、引入后出过什么问题比单纯背概念强得多。4. 连接池、数据同步与多数据库实操4.1 连接池为什么是标配参数应该怎么调数据库连接不是免费的。每次从应用服务器到数据库建立一条新连接都要经过TCP握手、身份认证、资源初始化这一整套流程开销比执行一条普通SQL还大。所以工程上几乎从来不会让每个请求都新建连接而是用一个连接池维护一批已经建好的连接谁要用就借出去用完还回来。主流连接池里Java生态用得最多的是HikariCP和Druid。核心参数就这么几个initialSize / minimumIdle池里最少维持多少条空闲连接。maxActive / maximumPoolSize池子最多能同时给出多少条连接。maxWait / connectionTimeout连接耗尽时请求最多等多久。连接有效性检测比如Druid的testWhileIdle、testOnBorrow。配置不当引发的经典事故就是连接池耗尽。maxActive设太小并发一上来连接不够请求全在排队等待接口响应时间暴涨设太大也不行数据库服务端连接数有上限连接池把数据库连满其他服务就再也连不上了。还有一个隐蔽坑数据库重启之后连接池里缓存的旧连接其实已经失效如果不做有效性检测应用会反复拿到坏连接然后报错。这个问题的典型报错是“Connection is not available, request timed out”排查方向基本就是连接池。我自己的建议是连接池参数一定要压测之后再定别照抄网上的默认值。不同业务的QPS、SQL耗时不一样最优参数差异很大。重点观察两个指标——连接池活跃连接数的水位、等待获取连接的时间这两个指标一旦异常就该调参数了。4.2 数据同步的场景主从复制、异构迁移与“先写库还是先写MQ”项目一旦跑起来数据同步就是个绕不开的话题。最基础的主从复制用数据库自带的方案就行——MySQL的binlog复制、PostgreSQL的流复制成熟稳定。但在异构场景下就没这么简单了比如要把MySQL的数据实时同步到数仓或者Elasticsearch就得借助专门的工具比如解析binlog的Canal、做离线批量同步的DataX。这类工具选型时重点看三件事支持的数据源种类、同步延迟水平、对源库性能的影响。数据同步背后其实藏着一个更大的一致性问题先写数据库还是先写消息队列MQ。这是分布式系统里的经典问题。先写库再发MQMQ发送失败就必须有补偿机制先发MQ再写库消费方可能读到旧数据。没有完美方案只有基于业务的取舍。我在笔记里把这个问题专门记了一页因为它让我意识到数据库的问题边界已经延伸到架构层面了——你做的每一个顺序选择都在影响数据的最终一致性。另外如果你的系统需要做数据库变更审计类似audit4j这类框架也会出现在技术选型里它可以把数据变更记录统一收起来做追踪。这些工具和框架看着五花八门但核心思路是一致的数据是有生命周期的从产生、变更、同步到归档每个环节都要有对应方案。4.3 主流数据库的“方言”差异与迁移避坑这段时间我把MySQL、Oracle、SQLite、达梦、人大金仓都过了过手最强烈的感受是SQL标准是理想方言才是现实。先说最常见的差异分页MySQL写LIMIT offset, countOracle得用ROWNUM或者FETCH FIRST。自增主键MySQL有AUTO_INCREMENTOracle用序列SEQUENCE插入时要显式取nextval。日期函数日期格式化、日期加减各有一套写法迁移时最容易漏改。NULL排序默认排序顺序在不同数据库里不一样不显式写ORDER BY很容易踩坑。国产数据库这两年用得越来越多达梦、人大金仓在兼容Oracle语法上做得不错迁移相对平滑但细节坑还是不少。比如字段注释的修改语法、表结构DDL的差异、驱动包的版本匹配。我试过用Navicat连接达梦版本不对就提示连不上换了匹配版本的驱动才正常。人大金仓可以用Docker快速跑起来做验证省去本地安装的一堆麻烦对学习来说很实用。SQLite则是另一种风格的代表它是单文件数据库一个.db文件就能跑Linux下尤其方便适合嵌入式设备、本地缓存、小工具开发。但它的并发写能力弱不适合服务端多用户高并发写入。理解每种数据库的定位比盲目迷信某个产品重要得多——选工具得看场景。5. 问题排查实录这一周踩过的坑和速查表5.1 一次真实的死锁排查过程这次实操里我完整经历了一次死锁排查。两个事务要同时更新订单表和支付表但因为代码里更新表的顺序不一致高并发下就撞上了。排查时我用的命令是SHOW ENGINE INNODB STATUS里面会展示最近一次死锁的两个事务各自持有和等待的锁看过之后定位非常清晰。最终的修复分了三层应用层统一所有事务对多张表的加锁顺序这是最彻底的办法。比如约定多表操作一律按表名字典序加锁。SQL层缩小事务里锁的范围能用主键定位的就别用大范围条件。兜底层应用捕获死锁异常做有限次重试。排查这类问题还有个经验别只盯着死锁本身先看事务里到底执行了多少条SQL。很多时候死锁只是表象真正的问题是大事务——事务里塞了太多操作、执行时间太长和其他事务的交集自然就多了。把事务拆小之后死锁频率会肉眼可见地下降。5.2 一张很快能上手的问题速查表这个阶段我整理了一张速查表都是自己或同事实际踩过的坑列出来给大家参考现象常见原因排查方向查询突然变慢索引失效、统计信息过期用EXPLAIN看执行计划必要时ANALYZE TABLE写入极慢索引过多、单事务过大查索引使用率拆分批量操作应用报连接超时连接池耗尽、连接失效看活跃连接数检查池参数多个更新一直卡住长事务持锁、锁等待查INNODB_TRX定位并处理长事务主从数据不一致大事务导致从库延迟拆事务查看主从复制状态唯一索引建不上表里已有重复数据先清理重复行再建唯一约束多说一句“唯一索引建不上”的问题。很多人给已有数据的表加唯一约束时会遇到报错原因就是表里已经存在重复数据。这时候不是直接强加约束而是先把重复数据查出来去重。一条简单的SQL就能找出重复组SELECT 字段, COUNT() FROM 表 GROUP BY 字段 HAVING COUNT() 1。先把重复数据处理干净再建索引就顺了。5.3 环境与驱动层的两件小事除了业务层面的问题环境层面的坑也值得记录。比如在Windows上装数据库客户端工具时经常会碰到“请先安装Access数据库64位系统驱动程序”这类报错本质上是ODBC驱动位数和应用位数不匹配——64位的应用必须装64位驱动32位的应用要装32位驱动混着来就一定报错。这种问题虽小但排查起来特别容易绕弯先确认位数基本就能定位。还有一次我想从IDE导出数据库脚本试了好几种方式都不顺手。后来发现用IDE自带的数据库工具面板右键选择导出可以生成包含表结构和数据的完整脚本比手动拼SQL可靠得多。这些环境工具类的经验课本上不会写但实际干活时能省下大量时间。另外如果你刚入门想找个练习库经典的Northwind北风数据库就很合适表多、关系复杂、练习题也多适合把增删改查和查询优化都练一遍。学习数据库光看不够必须上手遇到几个问题才算真正有手感。第四部分学到这里最大的收获不是多背了几个概念而是看数据库的视角变了。以前写SQL只关心“能不能跑出结果”现在会下意识想“这条SQL走了什么执行计划”“这个事务会持锁多久”“这个查询会不会拖垮连接池”。这种思维方式上的变化是数据库从“会写”走向“能用”的分水岭。最后再说一个我给自己留的扩展方向关系型数据库这些年演进得很成熟但向量数据库这类新形态已经在很多场景里落地了等我把知识图谱这块补完打算专门花时间研究一下向量检索和关系型模型结合的路子。数据库的学习没有终点第68天只是又一个开始。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →