ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL缓存详解:从Buffer Pool到Redis分层架构与一致性实践

MySQL缓存详解:从Buffer Pool到Redis分层架构与一致性实践 前几天帮一位朋友排查线上MySQL实例现象很典型机器配置不低innodb_buffer_pool_size已经给到24G业务接口却还是经常慢。我翻了一圈状态变量Buffer Pool命中率只有88%慢查询日志里大量全表扫描语句。朋友很困惑“缓存都这么大了怎么还在读磁盘”这个问题并不是个例很多人提到MySQL缓存第一反应就是“把buffer pool调大”但对缓存到底有哪些、怎么工作、为什么失效、怎么配合Redis往往是一笔糊涂账。这篇文章从底层原理讲到上层实战把MySQL内部的各类缓存拆开揉碎再延伸到分布式缓存和缓存一致性方案。内容包括InnoDB Buffer Pool、Query Cache、MyISAM Key Cache、日志缓存、Redis分层缓存、双写一致性问题以及我平时排查缓存问题的具体路径。适合三类人看刚入门想搞懂MySQL内存模型的开发被线上慢查询折腾的DBA以及面试前想系统串一遍MySQL缓存技术点的人。1. MySQL里到底藏着多少种缓存先破除缓存独一份的误解1.1 很多人以为MySQL缓存就是指Buffer Pool实际这是个天大的误解一说MySQL缓存十个里有八个会回答“Buffer Pool”剩下两个也许会提“Query Cache”。这个答案不能算错但远不够完整。MySQL是一个分层的架构Server层和存储引擎层都有自己的缓存组件甚至操作系统还有一层Page Cache在兜底。如果你没有把这层关系理清楚后面调优很容易被表象骗了。在我日常排查里真正会直接影响性能的缓存主要分这几类Server层查询缓存Query Cache、表缓存Table Cache、线程缓存Thread Cache、数据字典缓存Data Dictionary Cache。InnoDB存储引擎层缓冲池Buffer Pool、日志缓冲区Log Buffer、自适应哈希索引AHI、变更缓冲Change Buffer。MyISAM存储引擎层键缓存Key Cache。操作系统的文件系统层Page Cache。其中最关键、最容易被忽略的一点是MySQL高性能的主要来源不是“查询结果缓存”而是存储引擎层对数据页和索引页的内存缓存。日常所说的“读数据走了内存”是指从Buffer Pool里找到了数据页而不是从某个结果集缓存里直接取到了最终结果。这也解释了为什么MySQL 8.0会把Query Cache整个移除——因为从MySQL架构演进的角度看在Server层缓存“最终结果集”是一个高成本低收益的设计。真正高效的缓存位置应该在更靠近磁盘数据访问的那一层也就是Buffer Pool。理解了这一点你再看网上很多“MySQL缓存优化”的文章很多都是在讲表面参数没讲到机制。1.2 各层缓存的协作关系与数据流我用一条最简单的SQL来演示缓存之间的协作比如SELECT * FROM user WHERE id 100;这条查询从客户端发到MySQL后会先走连接器和解析器如果启用了Query Cache并且命中了完全相同的SQL就直接返回结果这一步不碰任何数据页。但在8.0里这条路已经被堵死了。接下来优化器生成执行计划执行器调用InnoDB的接口去读数据。InnoDB读取id100这条记录时不是去磁盘直接找行数据而是先定位到这行数据所在的聚簇索引页。这个页在不在Buffer Pool里如果在直接从内存返回这是逻辑读。如果不在InnoDB才需要从磁盘把页读进Buffer Pool。但是这里还有一个隐藏角色也就是操作系统Page Cache如果这个数据页刚刚被其他查询读过并被OS缓存住了那么InnoDB的“磁盘读取”实际上是从Page Cache里拷进Buffer Pool物理磁盘IO并没有发生。所以你看一个简单的“缓存命中”背后其实有三层Query Cache已废弃、Buffer Pool、OS Page Cache。三者的淘汰策略、并发控制、性能特征完全不同。后面所有章节基本都是在围绕这条数据流里各个关键节点展开。2. Buffer PoolInnoDB缓存的核心命脉参数调优的必争之地2.1 Buffer Pool到底缓存了什么页、索引、数据变更如果非要给MySQL缓存排一个重要性榜单InnoDB的Buffer Pool是毫无疑问的第一名。它是一个内存区域InnoDB把所有数据操作都尽量先放在这个区域里完成而不是直接读写磁盘。和大多数人想的不一样Buffer Pool里不只是数据页还包括数据页聚簇索引页、二级索引页undo页插入缓冲Insert Buffer / Change Buffer自适应哈希索引锁信息数据字典信息InnoDB管理内存的最小单位是“页”默认16KB。一个16KB的页从磁盘读进Buffer Pool后如果被修改了就会变成脏页。脏页不会立即写回磁盘而是由后台线程按一定策略刷盘。好处是多次修改可以合并成一次写入减少随机IO坏处是一旦断电内存里没刷盘的修改就丢了。这时候需要redo log来保证崩溃恢复把已提交的事务重放还原。所以Buffer Pool不仅仅是“读缓存”它在写入路径里同样是核心角色。调优Buffer Pool不只是为了让读命中率更高也是在影响整个数据库的写入吞吐和刷盘压力。很多人只把目光盯着“命中率”忽略了脏页比例和刷盘进程这是一个非常大的盲区。2.2 预读机制与LRU链表为什么缓存命中率会被污染Buffer Pool内部使用LRU最近最少使用算法管理缓存页但InnoDB并不是简单的LRU它把LRU链表分成Young区和Old区默认Old区占37%。新读入的页先放到Old区头部如果这个页在innodb_old_blocks_time默认1000毫秒内再次被访问才会被提升到Young区。这个设计主要就是为了防预读污染和全表扫描污染。预读是InnoDB一个很有意思的优化。当一个查询访问了某个页InnoDB认为你大概率会访问相邻页于是提前把相邻的多个页从磁盘读进Buffer Pool。如果这些页真的被用到预读效果好如果只是表扫描或者一次性把大量页读进来又不再访问这些页就会占据LRU的位置把真正的热点数据挤出去。我处理过最典型的场景是这样线上某个订单表有上亿行因为一个报表查询的SQL漏了过滤条件导致全表扫描。如果没有Old/Young区的保护这一条SQL足以把Buffer Pool里积累了数小时的热点数据全部清洗掉之后核心接口的缓存命中率会从99%瞬间跌到40%以下。而有了Old区设计扫描出来的页在Old区转一圈就被淘汰不会进入Young区热点数据仍然保留。这也是为什么innodb_old_blocks_time这个参数在混合负载环境里非常重要。2.3 Buffer Pool调优参数与实测建议Buffer Pool的核心参数首推innodb_buffer_pool_size。在许多生产环境里我建议把它设置为物理内存的50%到80%。为什么不是越大越好因为MySQL进程内不只有Buffer Pool还需要给连接线程、排序缓冲、join缓冲、临时表、binlog cache等预留内存。如果一台64G的机器把Buffer Pool设为58G一旦并发连接暴涨操作系统内存不足就会触发Swap整个实例性能立刻崩盘。具体参数可以从这几个方向入手参数建议值说明innodb_buffer_pool_size物理内存的50%-80%根据业务类型和内存总量动态评估innodb_buffer_pool_instancesBuffer Pool大于1G时建议设置为8拆分成多个实例降低LRU锁竞争innodb_old_blocks_time1000ms防止预读、扫描页污染热点innodb_old_blocks_pct37Old区占比通常不建议改innodb_buffer_pool_dump_at_shutdownON关闭时记录缓存页位置innodb_buffer_pool_load_at_startupON启动时按记录快速预热还有一个实操细节MySQL 5.7以后Buffer Pool可以动态调整大小不需要重启。但调整过程会触发页的重排伴随着一定的CPU和IO开销建议在业务低峰期执行。比如晚上12点以后执行SET GLOBAL innodb_buffer_pool_size 32G;执行完可以通过INNODB_BUFFER_POOL_RESIZE_STATUS表查看resize进度。不要频繁调整否则Buffer Pool内部页会碎片化锁竞争加剧。3. 查询缓存为什么Oracle、MySQL社区都把它当成反面教材3.1 Query Cache的工作方式与适用场景如果你在网上搜MySQL老版本优化教程经常会看到开启Query Cache的写法query_cache_type1、query_cache_size128M。这套东西看起来非常美好SQL语句缓存下来下一次完全相同的查询直接返回结果不经过存储引擎。但它使用的条件极其苛刻两条SQL必须字节级完全相同大小写、空格、注释不一样都算不同。最致命的是它的维护成本。每个表有更新这个表上所有相关的Query Cache条目都会被清空。注意是“所有”不是“部分”也不是“按行失效”。如果一个热点表每秒更新几十上百次那么这个表的Query Cache基本等于摆设缓存刚建立起来就被清掉还要额外付出清理和加锁的成本。所以Query Cache的适用场景无限趋近于“系统配置表”、“字典表”、“几乎不更新的只读数据”。如果你把这类数据放在应用层的本地缓存里效果比Query Cache好得多。这也是为什么MySQL官方最终在8.0版本直接把Query Cache移除5.7默认也关闭了。3.2 失效机制为什么会成为性能杀手我仔细读过MySQL关于Query Cache的实现这里面的问题比很多人想象的严重。Query Cache不只是“失效粒度太粗”它在高并发下还会带来全局锁竞争。每条SQL执行前Server层都需要检查Query Cache是否存在这会先申请一个全局的query cache mutex。在旧版本MySQL里这个mutex会成为明显的热点。当写入频繁时缓存清理操作还会持有锁去做大范围的内存操作一条UPDATE可能把整个缓存打穿。我自己做过一个印象深刻的实验同一台机器同一个业务开启了128M Query Cache之后QPS不但没有上升反而在写入型业务里下降了约15%。原因很直接缓存命中率极低每次写入都要清理大量无效缓存额外开销比缓存收益大得多。所以后来我遇到有人为了“优化缓存”而开启Query Cache第一反应都是劝他关掉把内存留给Buffer Pool。3.3 从MySQL 8.0移除后的替代方案Query Cache退场之后SQL结果级的缓存应该放在哪里答案是放到应用层比如Redis、Caffeine这类专门的缓存组件。这也是现代互联网架构的共识越靠近用户的地方做结果缓存越靠近数据的地方做页缓存。MySQL自己专心管理数据页和索引页不承担业务结果集缓存的工作。如果你正在使用MyBatis类的ORM框架可以了解下MyBatis二级缓存。它本质上是把查询结果放在应用进程内存通过namespace隔离但仍然需要手动处理缓存失效和一致性。不要把它当成银弹它和MySQL的Buffer Pool是两码事一个缓存的是应用层对象一个缓存的是数据库物理页。很多人把这两个混在一起面试时说MyBatis缓存是MySQL缓存这种理解是错的。4. 别把MyISAM和InnoDB搞混Key Cache里的索引缓存真相4.1 Key Cache只缓存索引数据文件交给系统虽然现在新建表默认都是InnoDB但线上老库里仍然会有不少MyISAM表。MyISAM的缓存机制和InnoDB差异巨大最关键的一点是MyISAM的Key Cache只缓存索引块不缓存数据块。数据文件完全依赖操作系统的文件缓存来加速访问。这带来的后果是什么如果你有一张大表索引完全在内存里但查询时如果只通过索引返回覆盖列速度会很快一旦需要回表去取数据行就必须把数据文件读入OS Page CacheMySQL本身管不到这块缓存有多大、什么时候被淘汰。在内存紧张的情况下数据页很容易被其他进程挤出OS缓存导致查询忽快忽慢性能抖动非常明显。这张对比表可以帮助你快速区分维度MyISAM Key CacheInnoDB Buffer Pool缓存对象索引块数据页、索引页、undo页等是否缓存数据否是管理单位索引块16KB页是否支持事务否是是否支持崩溃恢复较弱强锁粒度表级锁行级锁4.2 key_buffer_size的合理设置如果你实在需要使用MyISAM表key_buffer_size建议先统计所有MyISAM表索引文件的大小然后设置为索引总大小的30%到50%不要盲目给大。给大了并不会缓存到数据只会白白浪费内存。更大的问题是MyISAM的Key Cache是全局共享的所有MyISAM表的索引都在抢占这块内存并没有像InnoDB那样做多实例拆分。对于特别重要的MyISAM索引可以用CACHE INDEX把指定表索引缓存到独立的Key Cache分区里再用LOAD INDEX INTO CACHE预热。这算是一种精细化管理但维护成本不低。实际上我的建议非常明确在今天的硬件条件下没有必要为了“节约内存”去使用MyISAM。InnoDB在性能和可靠性上的优势是压倒性的。4.3 为什么我在生产环境仍然坚持InnoDB面试的时候经常会有朋友被问到“MyISAM和InnoDB的区别”这本身没问题。但如果在真实生产环境里还在新建MyISAM表我只想问一句为什么。InnoDB的Buffer Pool把数据页和索引页统一管理支持行锁、事务、崩溃恢复还能用innodb_buffer_pool_dump和load做内存预热。MyISAM的Key Cache只解决了索引读取问题数据访问始终依赖不可控的OS层。我处理过一个案例某系统有几张历史报表表是MyISAM平时查询有索引数据量不大看起来没问题。但每次服务器重启后这些表的数据页热点全部丢失OS Page Cache又重新慢慢积累。而InnoDB表因为配置了innodb_buffer_pool_load_at_startup启动后能快速恢复热点。两相对比MyISAM的短板就暴露得很明显。所以我的结论很直接除非你有极其特殊的场景否则生产环境不要再用MyISAM把Key Cache的问题留给面试题就足够了。5. MySQL缓存解决不了的事交给Redis之前先学会分层5.1 分层缓存架构L1、L2和DB之间的协作MySQL自己的Buffer Pool再大也只能缓存数据页没办法直接缓存“用户主页”、“商品详情”、“订单列表”这类业务组装结果。一个热点详情页如果每次请求都要到数据库里关联五六张表做聚合计算即使Buffer Pool命中率100%CPU和查询成本也吃不消。这种场景就需要在更上层做结果缓存。业界常见的分层架构长这样应用进程内的本地缓存作为L1Redis作为L2MySQL Buffer Pool和磁盘作为最后的L3。请求进来先查L1本地没有查L2L2没有才落到MySQL。MySQL查回来后再逐层回填。这里面的核心逻辑是越靠近用户的缓存速度越快但容量越小、一致性维护越难越靠近数据库的缓存容量越大、离源头越近但访问延迟相对高。要注意的是L1本地缓存不是免费的。它的命中率高但同一个数据在不同机器上可能不一致而且会占用应用JVM堆内存。对于一致性要求低、并发极高的场景适合用本地缓存对一致性要求较高的数据建议直接用Redis避免多副本同时过期的问题。5.2 Redis缓存设计的关键决策粒度、Key与过期策略Redis和MySQL协作时第一个要决策的是缓存粒度。常见两种行级缓存和结果集缓存。行级缓存的粒度是单条业务数据比如order:12345缺点是每次查询还要在应用层做组装结果集缓存直接缓存整个响应命中率最高但查询条件一多key的组合就会爆炸失效逻辑也复杂。行级缓存是我在大多数业务里的首选项。举个例子一个订单详情页需要展示订单主表、商品、地址三块数据你可以分别缓存order:12345、product:8888、address:9999。订单更新时只需要删除对应的order:12345商品和地址缓存还能继续共享给其他订单不会因一个订单更新导致整个页面缓存失效。Key设计上我会按“业务域:实体类型:主键”的方式来命名比如order:order_no:20240816。不要用可变的查询条件做主key否则无法做有效的失效清理。过期时间上建议所有缓存都设置TTL作为兜底避免缓存永不过期导致数据永远陈旧。对于秒杀、爆款这类热点数据可以用“逻辑过期”手段redis里不设TTL但value带一个过期时间字段业务读取时检查字段异步刷新。这样能避免缓存雪崩但会牺牲短暂的一致性。5.3 缓存穿透、击穿、雪崩的实战防御缓存层面对的三座山每个都得认真处理。缓存穿透请求查询一个根本不存在的数据缓存里查不到每次请求都穿过缓存打到MySQL。如果一个恶意请求不断变换不存在的IDMySQL会被拖垮。解决思路是缓存空值并设置短TTL或者用布隆过滤器在缓存前拦截不存在的ID。缓存击穿某个热点key的TTL刚好过期同一瞬间大量请求同时去数据库重建缓存。如果不做保护数据库压力瞬间拉满。解决办法是互斥锁只允许一个请求去查库并回填缓存其他请求先等待或返回默认值。缓存雪崩大量key同时过期或者Redis实例整体不可用导致流量全部打到MySQL。解决办法是在TTL上增加随机值避免key成群过期同时做好Redis的高可用和降级预案。处理这三类问题的时候我最常用的套路是先把异常流量挡在外面再保证数据库不会被击穿最后才谈缓存命中率。这也是为什么我会在缓存架构里单独引入限流和熔断组件而不是只靠缓存本身。6. MySQL与Redis双写一致性最考验基本功的场景6.1 Cache Aside vs Read Through先更新数据库还是先删除缓存MySQL和Redis之间的一致性是一个被讨论过无数遍的问题但真正能讲清楚的并不多。最常见的模式叫Cache Aside旁路缓存读请求先查Redis没命中就查MySQL把结果写回Redis写请求则先更新MySQL然后把Redis里的旧缓存删除或更新。问题在于为什么不直接更新Redis缓存而是删除缓存因为更新缓存更复杂。假设一个订单的状态从“待支付”变成“已支付”直接SET新值看起来没问题。但如果这个数据的修改涉及多个字段、多条SQL、多个服务你很难保证写数据库和写缓存在同一个事务里。一旦中途失败缓存里可能存了一个中间态数据后续读到的都是脏值。而删除缓存就不一样即使删除失败最多导致一次额外穿透下一轮读请求会把新值重新写入。这是“先删缓存”和“先更新缓存”的本质区别。但更新数据库和删除缓存这两个动作仍然不是一个原子操作。它们之间的顺序很关键。如果先删缓存再更新数据库在数据库更新完成前的窗口期另一个请求发现缓存为空会把旧值读回缓存于是旧数据被“复活”。所以业界的默认方案是先更新数据库再删除缓存。虽然也有竞态但窗口小得多。6.2 延迟双删的边界与坑既然先更新数据库后删缓存还有窗口怎么把这个窗口再压缩很多人会想到延迟双删。它的过程是先删除缓存再更新数据库休眠一定时间后再次删除缓存。目的是把“旧值被读回缓存”这个可能事件中生成的脏缓存再删一次。听起来很美好但坑非常多。第一个坑是延迟时间设多少。时间太短第二次删除发生在并发请求写回旧缓存之前等于白删时间太长中间这段时间缓存一直空白DB压力大。第二个坑是删除缓存失败。第二次删除如果又失败了还是会产生脏数据。第三个坑是它在分布式环境下并没有严格的正确性保证只能减少不一致概率做不到消除。我现在的观点是延迟双删可以作为短期过渡方案但不建议把它当成长期架构。因为它本质上是用“更多的删除操作”去对冲并发竞态是一种概率性的修复手段。如果业务真的要求一致性更好的办法是不要依赖缓存或者说把缓存放在一个业务上可以接受短暂延迟的位置。6.3 基于binlog订阅的最终一致性方案在实际项目里我做缓存一致性最常用的方案其实是利用MySQL的binlog。简单来说应用只写数据库然后通过Canal这样的中间件伪装成MySQL从库去订阅binlog拿到数据变更后再异步更新Redis缓存或删除缓存。好处非常明显业务代码里不需要再调用Redis删除逻辑缓存更新和数据库事务解耦。业务流程大概是这样应用执行UPDATE语句MySQL提交事务并写binlogCanal解析binlog投递给消费者消费者根据主键删除对应的缓存或者把最新数据写入Redis。整个过程对业务代码透明可以把一致性维护收敛到中间件层。这个方案需要注意几点binlog格式必须设置为row模式才能拿到每一行变更前后的完整数据。消费端需要做幂等防止消息重复投递导致重复更新。从数据库变更到缓存更新之间会有毫秒级到秒级的延迟不适合对一致性要求到强一致级别的场景。如果消费端挂了Redis缓存可能长时间保持旧值需要有监控报警和重试机制。最终在我的实践经验里百分之九十以上的业务场景都能接受这种最终一致性。缓存本来就是用来扛读并发的如果你要求缓存和数据库在任何时刻完全一致那缓存存在的意义就减少了一半。真正要紧的是把不一致的窗口控制到业务能够接受的范围内并且保证最终能收敛到正确值。7. 不会看命中率调了半天缓存等于没调7.1 用SHOW GLOBAL STATUS算出真实的Buffer Pool命中率很多开发同学配置了Buffer Pool后从不看监控也不知道自己的内存缓存到底有没有发挥作用。MySQL其实给了非常清晰的状态变量只要你会看。执行以下SQL可以得到Buffer Pool最重要的几个指标SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_read%; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_pages%;重点关注这几个Innodb_buffer_pool_read_requests逻辑读请求次数也就是InnoDB向Buffer Pool发起的读请求总数。Innodb_buffer_pool_reads实际需要从磁盘读取到Buffer Pool的页次数。Innodb_buffer_pool_pages_totalBuffer Pool总页数。Innodb_buffer_pool_pages_free空闲页数。Innodb_buffer_pool_pages_data已使用的数据页总数。命中率计算公式如下命中率 (Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100在实际生产里这个值如果长期高于99%说明绝大多数查询都是走内存的Buffer Pool配置基本健康。如果低于95%你就要小心了。不要第一时间加大内存先问两个问题是不是存在大量全表扫描是不是脏页比例过高很多人一看到命中率低就猛加Buffer Pool结果加了之后命中率还是没变因为问题根本不在容量而在SQL访问模式。7.2 用SHOW ENGINE INNODB STATUS看InnoDB内部信息除了全局状态变量InnoDB还提供了一个更细的诊断入口SHOW ENGINE INNODB STATUS\G输出内容很长我们只需要关注BUFFER POOL AND MEMORY段落。典型内容大致如下---------------------- BUFFER POOL AND MEMORY ---------------------- Total large memory allocated 34445754368 Dictionary memory allocated 1023948 Buffer pool size 2097146 Free buffers 102400 Database pages 1994746 Old database pages 737754 Modified db pages 10921 ... Buffer pool hit rate 996 / 1000, young-making rate 0 / 1000 not ...这里面最重要的是Buffer pool hit rate996/1000代表99.6%。还有young-making rate如果这个数值很高说明频繁有页面从Old区转移到Young区就可能存在大量预读或全表扫描导致的LRU污染。Modified db pages代表脏页数量如果这个比值长期偏高要考虑刷盘能力是否足够。看到这些数字后再结合Innodb_buffer_pool_reads的绝对值去分析。如果逻辑读每秒上百万次而Innodb_buffer_pool_reads偶尔飙一下这种峰值可能来自后台预读或定期任务不一定是业务问题。7.3 常见调优误区与我的排查路径最后再说说我踩过的一些坑以及现在遇到缓存问题时的固定排查顺序。第一个误区是“Buffer Pool越大越好”。内存给得过多会挤占OS Page Cache反而让文件系统层失去缓冲能力尤其对于大文件排序、临时表落盘这类场景反而更慢。第二个误区是“只要开了RedisMySQL缓存调不调无所谓”。Redis这层缓存主要挡住重复的业务查询但一旦缓存穿透或者Redis集群抖动流量瞬间就会到MySQL。如果MySQL Buffer Pool命中率很低数据库会立刻被大查询打满。第三个误区是“命中率低就加内存”。我在系统上线初期见过很多次这种操作SQL走了错误索引或干脆没走索引全表扫描让命中率低得吓人加内存根本解决不了得回到执行计划去优化索引。现在我遇到线上缓存类问题基本的排查路径是这样的先看慢查询日志确认是否有异常的SQL模式。再看Buffer Pool命中率、脏页比例、LRU状态。分析是否存在全表扫描或预读污染用执行计划确认。检查Redis层是否有穿透、击穿、雪崩的风险。最后才去调整Buffer Pool或其他内存参数。这套路径帮我避免了很多“头痛医头”式调优。缓存问题从来不是单一参数问题而是一个从SQL到MySQL再到Redis的全链路协作问题。老实说我见过太多人把调优等同于加内存换SSD。缓存调优从来不是把参数调大就可以关键要知道缓存层下面发生了什么。如果你想动手验证建议先在一个压测环境里把SHOW ENGINE INNODB STATUS里的Buffer pool hit rate、Read views这些关键指标都看一遍再动手改参数。改完之后用同一条慢SQL观察逻辑读和物理读的变化而不是只盯QPS。数据库的缓存世界远比你想象的深弄清楚每个组件的位置和边界才能真正把每一份内存花在刀刃上。
返回列表