<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>性能优化 on XEDCZQ的博客</title><link>https://xedczq.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</link><description>Recent content in 性能优化 on XEDCZQ的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 26 Jun 2026 21:16:00 +0800</lastBuildDate><atom:link href="https://xedczq.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/index.xml" rel="self" type="application/rss+xml"/><item><title>Java 与 MySQL 查询优化复盘：从循环查库到批量查询、联合索引与短缓存</title><link>https://xedczq.cn/post/java_mysql_queryoptimization/</link><pubDate>Fri, 26 Jun 2026 21:16:00 +0800</pubDate><guid>https://xedczq.cn/post/java_mysql_queryoptimization/</guid><description>&lt;p&gt;这篇笔记根据项目的历史测试记录、开发日志、索引迁移脚本和 Java 源码整理，记录我围绕会场列表、直播列表和首页查询开展的一次性能优化。&lt;/p&gt;
&lt;p&gt;主要改动集中在四个方面：减少数据库查询次数、改善索引访问路径、缩小返回数据量、减少重复查询。过程中还处理了数据库网络链路和历史数据结构问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文中的性能指标来自历史测试记录，整理本文时没有重新压测，也没有重新核对生产环境的索引状态。&lt;/strong&gt; 不同测试的环境、并发方式和统计口径不同，需要分别理解。&lt;/p&gt;
&lt;h2 id="1-先建立基线区分接口耗时和网络耗时"&gt;&lt;a href="#1-%e5%85%88%e5%bb%ba%e7%ab%8b%e5%9f%ba%e7%ba%bf%e5%8c%ba%e5%88%86%e6%8e%a5%e5%8f%a3%e8%80%97%e6%97%b6%e5%92%8c%e7%bd%91%e7%bb%9c%e8%80%97%e6%97%b6" class="header-anchor"&gt;&lt;/a&gt;1. 先建立基线，区分接口耗时和网络耗时
&lt;/h2&gt;&lt;p&gt;最初我先确认服务能够启动、接口正常返回，再对列表建立性能基线。测试入口为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;GET /api/Index/mettingnew?page=1&amp;amp;pagesize=20
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;mettingnew&lt;/code&gt; 在记录中被称为“会议列表”，实际返回的是带有所属会议信息的会场列表。&lt;/p&gt;
&lt;p&gt;第一轮使用 ApacheBench，配置为 200 次请求、20 并发、开启 Keep-Alive，结果如下：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;指标&lt;/th&gt;
 &lt;th&gt;基线结果&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;P95&lt;/td&gt;
 &lt;td&gt;1385 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;吞吐量&lt;/td&gt;
 &lt;td&gt;25.59 请求/秒&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;单请求响应体&lt;/td&gt;
 &lt;td&gt;约 33.9 KB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;成功请求&lt;/td&gt;
 &lt;td&gt;200 / 200&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;当时本地 Java 连接远程 MySQL，所以测试结果包含数据库网络往返耗时。后续分析必须把“SQL 执行慢”和“访问数据库的链路慢”分开。&lt;/p&gt;
&lt;p&gt;开发日志中还记录过一次独立的代理问题：JVM 将腾讯云 MySQL 连接经本机代理转发，单条 JDBC &lt;code&gt;SELECT 1&lt;/code&gt; 约耗时 305 ms，直连约 20 ms。我在启动脚本中为数据库域名配置代理绕过，避免每次查询承担额外代理延迟。&lt;/p&gt;
&lt;p&gt;这属于连接链路优化，不能把它算成索引或 SQL 执行计划优化的效果。&lt;/p&gt;
&lt;h2 id="2-结合执行计划与调用链定位问题"&gt;&lt;a href="#2-%e7%bb%93%e5%90%88%e6%89%a7%e8%a1%8c%e8%ae%a1%e5%88%92%e4%b8%8e%e8%b0%83%e7%94%a8%e9%93%be%e5%ae%9a%e4%bd%8d%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;2. 结合执行计划与调用链定位问题
&lt;/h2&gt;&lt;p&gt;我对实际查询执行 &lt;code&gt;SHOW INDEX&lt;/code&gt; 和 &lt;code&gt;EXPLAIN&lt;/code&gt;，重点关注使用的索引、访问类型、预计扫描行数和排序方式。&lt;/p&gt;
&lt;p&gt;当时发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会场表只有主键，列表查询出现 &lt;code&gt;type=ALL&lt;/code&gt;，预计扫描约 579 行。&lt;/li&gt;
&lt;li&gt;会场查询出现 &lt;code&gt;Using where; Using filesort&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;会议表约 161 行、专家表约 3920 行的相关查询也存在全表扫描和额外排序。&lt;/li&gt;
&lt;li&gt;会场列表读取当前页后，还会逐条查询所属会议。&lt;/li&gt;
&lt;li&gt;登录后的直播列表，还会逐条查询用户是否订阅。&lt;/li&gt;
&lt;li&gt;列表读取了大量仅详情页需要的富文本字段。&lt;/li&gt;
&lt;li&gt;专家日程在排序比较器中执行数据库查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些问题分成两个层面：单条 SQL 的扫描、排序成本，以及一次接口请求中执行过多 SQL 的网络往返成本。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Using filesort&lt;/code&gt; 表示没有直接通过索引顺序完成排序，并不代表一定发生磁盘排序。后续优化也不能概括成“所有 filesort 都已消除”。&lt;/p&gt;
&lt;h2 id="3-将循环关联查询改为按页批量读取"&gt;&lt;a href="#3-%e5%b0%86%e5%be%aa%e7%8e%af%e5%85%b3%e8%81%94%e6%9f%a5%e8%af%a2%e6%94%b9%e4%b8%ba%e6%8c%89%e9%a1%b5%e6%89%b9%e9%87%8f%e8%af%bb%e5%8f%96" class="header-anchor"&gt;&lt;/a&gt;3. 将循环关联查询改为按页批量读取
&lt;/h2&gt;&lt;p&gt;原来的匿名会场列表流程是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;查询总数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 查询当前页 N 个会场
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 遍历 N 个会场，分别查询所属会议
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;查询次数为 &lt;code&gt;1 次 COUNT + 1 次列表 + N 次会议查询 = 2+N&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;登录态直播列表还要逐条读取订阅状态，因此达到 &lt;code&gt;2+2N&lt;/code&gt;。一页 20 条记录时，两个场景分别可能执行 22 次和 42 次 SQL。&lt;/p&gt;
&lt;p&gt;问题的根源是循环查库，不能仅根据实体之间是一对多还是多对多关系判断。&lt;/p&gt;
&lt;p&gt;我把流程改为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询当前页会场。&lt;/li&gt;
&lt;li&gt;收集并去重当前页的 &lt;code&gt;mettingid&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;使用一次参数化 &lt;code&gt;IN (...)&lt;/code&gt; 查询读取所属会议。&lt;/li&gt;
&lt;li&gt;登录时，再收集当前页会场 ID，一次读取该用户的订阅状态。&lt;/li&gt;
&lt;li&gt;将会议组织为 &lt;code&gt;Map&lt;/code&gt;、订阅会场组织为 &lt;code&gt;Set&lt;/code&gt;，在内存中回填结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;场景&lt;/th&gt;
 &lt;th&gt;优化前&lt;/th&gt;
 &lt;th&gt;优化后&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;匿名会场或直播列表&lt;/td&gt;
 &lt;td&gt;2+N 次&lt;/td&gt;
 &lt;td&gt;通常 3 次&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;登录态直播列表&lt;/td&gt;
 &lt;td&gt;2+2N 次&lt;/td&gt;
 &lt;td&gt;通常 4 次&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;一页 20 条的匿名列表&lt;/td&gt;
 &lt;td&gt;22 次&lt;/td&gt;
 &lt;td&gt;3 次&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;一页 20 条的登录态列表&lt;/td&gt;
 &lt;td&gt;42 次&lt;/td&gt;
 &lt;td&gt;4 次&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以上只计算列表业务链路，登录身份解析等公共查询另计。&lt;/p&gt;
&lt;p&gt;这项改动减少了 SQL 执行次数、网络往返和连接占用，数据库位于远程时尤其有价值。但它不表示整体计算量变成常数，返回和组装 N 条记录仍然需要随 N 增长。&lt;/p&gt;
&lt;h2 id="4-将批量读取推广到日程专家和消息列表"&gt;&lt;a href="#4-%e5%b0%86%e6%89%b9%e9%87%8f%e8%af%bb%e5%8f%96%e6%8e%a8%e5%b9%bf%e5%88%b0%e6%97%a5%e7%a8%8b%e4%b8%93%e5%ae%b6%e5%92%8c%e6%b6%88%e6%81%af%e5%88%97%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;4. 将批量读取推广到日程、专家和消息列表
&lt;/h2&gt;&lt;h3 id="专家会议日程"&gt;&lt;a href="#%e4%b8%93%e5%ae%b6%e4%bc%9a%e8%ae%ae%e6%97%a5%e7%a8%8b" class="header-anchor"&gt;&lt;/a&gt;专家会议日程
&lt;/h3&gt;&lt;p&gt;原先排序比较器会查询会场日期。排序会反复调用比较器，数据库访问次数也随之增加。&lt;/p&gt;
&lt;p&gt;改造后先收集会场 ID，一次读取会场名称和日期，再完全在内存中排序。比较器只比较已准备好的数据。&lt;/p&gt;
&lt;h3 id="会场详情日程"&gt;&lt;a href="#%e4%bc%9a%e5%9c%ba%e8%af%a6%e6%83%85%e6%97%a5%e7%a8%8b" class="header-anchor"&gt;&lt;/a&gt;会场详情日程
&lt;/h3&gt;&lt;p&gt;原先按环节逐个读取日程，改为按照会场和环节 ID 集合一次读取，再按 &lt;code&gt;huanjieid&lt;/code&gt; 分组，回填到对应环节。&lt;/p&gt;
&lt;h3 id="专家信息补全"&gt;&lt;a href="#%e4%b8%93%e5%ae%b6%e4%bf%a1%e6%81%af%e8%a1%a5%e5%85%a8" class="header-anchor"&gt;&lt;/a&gt;专家信息补全
&lt;/h3&gt;&lt;p&gt;医院、科室名称和会员订阅状态等关联信息也通过批量查询组织后回填，避免列表逐条补全。&lt;/p&gt;
&lt;h3 id="会员消息列表"&gt;&lt;a href="#%e4%bc%9a%e5%91%98%e6%b6%88%e6%81%af%e5%88%97%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;会员消息列表
&lt;/h3&gt;&lt;p&gt;邀请函封面的获取由相关子查询改成 &lt;code&gt;LEFT JOIN my_content_mettinghall&lt;/code&gt;，统一读取消息和关联封面。&lt;/p&gt;
&lt;p&gt;连接查询是否获得更优执行计划仍取决于索引与数据分布。这些改动没有各自独立的性能百分比，不能为每一项分配一个未经测试的提升数字。&lt;/p&gt;
&lt;h2 id="5-精简列表字段减少数据搬运"&gt;&lt;a href="#5-%e7%b2%be%e7%ae%80%e5%88%97%e8%a1%a8%e5%ad%97%e6%ae%b5%e5%87%8f%e5%b0%91%e6%95%b0%e6%8d%ae%e6%90%ac%e8%bf%90" class="header-anchor"&gt;&lt;/a&gt;5. 精简列表字段，减少数据搬运
&lt;/h2&gt;&lt;p&gt;列表和详情承担不同职责。我将部分列表查询收缩到展示所需的字段，把富文本 &lt;code&gt;content&lt;/code&gt; 留给详情接口。&lt;/p&gt;
&lt;p&gt;实际改动包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;批量所属会议查询仅保留 &lt;code&gt;id&lt;/code&gt;、&lt;code&gt;title&lt;/code&gt;、&lt;code&gt;indexpic&lt;/code&gt;、&lt;code&gt;starttime&lt;/code&gt;、&lt;code&gt;endtime&lt;/code&gt;、&lt;code&gt;address&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;首页、资讯列表和部分专家列表移除不需要的富文本。&lt;/li&gt;
&lt;li&gt;直播列表返回 &lt;code&gt;content: &amp;quot;&amp;quot;&lt;/code&gt;，减少读取的同时保持旧接口字段兼容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;收益覆盖数据库结果传输、JDBC 对象构建、JSON 序列化和 HTTP 响应体积。&lt;/p&gt;
&lt;p&gt;一次本地对比中，单请求响应体从约 &lt;strong&gt;33,877 bytes 降至 27,493 bytes，减少 18.84%&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里有一个实现边界：当时普通会场列表仍读取会场自身的 &lt;code&gt;h.content&lt;/code&gt;。精简已落实到所属会议批量查询、直播列表和其他指定入口，不能写成“全部列表都已移除富文本”。&lt;/p&gt;
&lt;h2 id="6-按筛选和排序条件设计联合索引"&gt;&lt;a href="#6-%e6%8c%89%e7%ad%9b%e9%80%89%e5%92%8c%e6%8e%92%e5%ba%8f%e6%9d%a1%e4%bb%b6%e8%ae%be%e8%ae%a1%e8%81%94%e5%90%88%e7%b4%a2%e5%bc%95" class="header-anchor"&gt;&lt;/a&gt;6. 按筛选和排序条件设计联合索引
&lt;/h2&gt;&lt;p&gt;索引设计围绕实际查询展开，而不是为每个字段分别创建一个索引。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;查询场景&lt;/th&gt;
 &lt;th&gt;对应索引列&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;普通会议列表&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;(status, starttime, id)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;普通会场列表&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;(status, sort, starttime, mettingid)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;直播会场列表&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;(status, starttime, id, mettingid)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;首页推荐会议&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;(status, isresume, starttime, id)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;用户直播订阅查询&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;(memberid, hallid)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;专家科室筛选&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;(status, keshi, isresume, sort, id)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;专家展示排序治理后&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;(status, isresume, sort, en, id)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;删除会议前的依赖检查&lt;/td&gt;
 &lt;td&gt;会场表和模板表的 &lt;code&gt;(mettingid)&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;首页分类、资讯、课程和章节也准备了与状态、推荐标记和排序字段相匹配的索引。&lt;/p&gt;
&lt;p&gt;基本思路是利用 B+Tree 最左前缀，在条件匹配时缩小候选记录，再尽可能利用索引完成范围读取与排序。一个索引无法覆盖所有筛选组合，所以普通会场列表和直播时间列表使用了不同索引。&lt;/p&gt;
&lt;h3 id="mysql-57-和历史表结构的限制"&gt;&lt;a href="#mysql-57-%e5%92%8c%e5%8e%86%e5%8f%b2%e8%a1%a8%e7%bb%93%e6%9e%84%e7%9a%84%e9%99%90%e5%88%b6" class="header-anchor"&gt;&lt;/a&gt;MySQL 5.7 和历史表结构的限制
&lt;/h3&gt;&lt;p&gt;实施中遇到了以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 5.7 无法利用混合方向索引完整匹配部分 &lt;code&gt;ASC/DESC&lt;/code&gt; 混合排序，不能承诺彻底消除 &lt;code&gt;filesort&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;历史 &lt;code&gt;my_staff.en&lt;/code&gt; 为 &lt;code&gt;utf8mb4 varchar(255)&lt;/code&gt;，加入联合索引时遇到旧库 767-byte 索引长度限制。&lt;/li&gt;
&lt;li&gt;部分历史表带有零日期默认值，迁移需要在会话内兼容处理 SQL Mode，并在结束后恢复。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LIKE '%关键词%'&lt;/code&gt; 无法依靠普通 B+Tree 做有效的前缀定位，因此没有盲目为医院、科室名称增加索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;历史记录中，直播查询在隔离库已验证使用范围扫描，会议删除依赖检查已验证使用 &lt;code&gt;ref&lt;/code&gt; 索引访问。迁移脚本存在，只能证明已准备相应变更，不能直接证明所有索引都已经发布到生产。&lt;/p&gt;
&lt;h2 id="7-治理专家关联数据和拼音字段"&gt;&lt;a href="#7-%e6%b2%bb%e7%90%86%e4%b8%93%e5%ae%b6%e5%85%b3%e8%81%94%e6%95%b0%e6%8d%ae%e5%92%8c%e6%8b%bc%e9%9f%b3%e5%ad%97%e6%ae%b5" class="header-anchor"&gt;&lt;/a&gt;7. 治理专家关联数据和拼音字段
&lt;/h2&gt;&lt;h3 id="将字符串关系整理为可索引的关系表"&gt;&lt;a href="#%e5%b0%86%e5%ad%97%e7%ac%a6%e4%b8%b2%e5%85%b3%e7%b3%bb%e6%95%b4%e7%90%86%e4%b8%ba%e5%8f%af%e7%b4%a2%e5%bc%95%e7%9a%84%e5%85%b3%e7%b3%bb%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;将字符串关系整理为可索引的关系表
&lt;/h3&gt;&lt;p&gt;旧系统把专家 ID 存在会场、环节和日程的 &lt;code&gt;staffs&lt;/code&gt; 字符串中。我增加了 &lt;code&gt;my_meeting_hall_staff&lt;/code&gt; 关系表，使用结构化字段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;meeting_id、hall_id、staff_id
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并建立对应约束和索引：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;索引或约束&lt;/th&gt;
 &lt;th&gt;用途&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;(hall_id, staff_id)&lt;/code&gt; 唯一约束&lt;/td&gt;
 &lt;td&gt;防止重复关系&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;(staff_id, hall_id)&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;从专家查会场&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;(meeting_id, hall_id)&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;从会议查关联关系&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;为了兼容旧 PHP，原有 &lt;code&gt;staffs&lt;/code&gt; 字段继续保留，通过事务同步新关系表。历史不存在的专家 ID 进入异常表，回填后再校验缺失关系和多余关系。&lt;/p&gt;
&lt;p&gt;这样部分专家关联查询可以通过结构化关系和索引完成。具体环节、日程仍保留旧字段，用于保存绑定位置与顺序，所以不能说整个系统已经完全消除字符串关联查询。&lt;/p&gt;
&lt;h3 id="收缩姓名拼音首字母字段"&gt;&lt;a href="#%e6%94%b6%e7%bc%a9%e5%a7%93%e5%90%8d%e6%8b%bc%e9%9f%b3%e9%a6%96%e5%ad%97%e6%af%8d%e5%ad%97%e6%ae%b5" class="header-anchor"&gt;&lt;/a&gt;收缩姓名拼音首字母字段
&lt;/h3&gt;&lt;p&gt;专家姓名拼音首字母统一由后端生成，历史上回填了 4155 条专家记录，并将 &lt;code&gt;en&lt;/code&gt; 从 &lt;code&gt;utf8mb4 varchar(255)&lt;/code&gt; 收缩为 &lt;code&gt;CHAR(1) ASCII&lt;/code&gt;，随后建立相应联合索引。&lt;/p&gt;
&lt;p&gt;这改善了数据一致性、字段紧凑性和索引可建立性，但没有单独测得的接口提升百分比。&lt;/p&gt;
&lt;h2 id="8-给匿名首页增加短缓存并约束分页开销"&gt;&lt;a href="#8-%e7%bb%99%e5%8c%bf%e5%90%8d%e9%a6%96%e9%a1%b5%e5%a2%9e%e5%8a%a0%e7%9f%ad%e7%bc%93%e5%ad%98%e5%b9%b6%e7%ba%a6%e6%9d%9f%e5%88%86%e9%a1%b5%e5%bc%80%e9%94%80" class="header-anchor"&gt;&lt;/a&gt;8. 给匿名首页增加短缓存，并约束分页开销
&lt;/h2&gt;&lt;p&gt;首页需要读取会议、专家、资讯、课程等多张表。我将匿名首页公共结果缓存在 &lt;strong&gt;Java 进程内，过期时间为 10 秒&lt;/strong&gt;，减少短时间内的重复读取。&lt;/p&gt;
&lt;p&gt;实现包含过期时间和锁内二次检查，以减少同一实例缓存过期时的重复构建。登录用户继续实时读取，不共享会员和收藏状态。&lt;/p&gt;
&lt;p&gt;缓存的边界包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容更新存在约 10 秒的缓存窗口。&lt;/li&gt;
&lt;li&gt;每个 Java 实例维护自己的缓存。&lt;/li&gt;
&lt;li&gt;缓存未命中仍然需要查询数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里使用的是进程内缓存，不能在复盘时写成 Redis 首页缓存。&lt;/p&gt;
&lt;p&gt;另外，会员中心通用分页限制了最大 &lt;code&gt;pageSize=100&lt;/code&gt;，并使用 &lt;code&gt;long&lt;/code&gt; 计算 offset，减少超大单页请求和整数溢出的风险。这属于分页防护，现有记录没有证明已经完成深分页的游标化改造。&lt;/p&gt;
&lt;h2 id="9-用业务回归和分口径测试验证结果"&gt;&lt;a href="#9-%e7%94%a8%e4%b8%9a%e5%8a%a1%e5%9b%9e%e5%bd%92%e5%92%8c%e5%88%86%e5%8f%a3%e5%be%84%e6%b5%8b%e8%af%95%e9%aa%8c%e8%af%81%e7%bb%93%e6%9e%9c" class="header-anchor"&gt;&lt;/a&gt;9. 用业务回归和分口径测试验证结果
&lt;/h2&gt;&lt;p&gt;验证不仅检查编译，还覆盖列表总数、分页条数、会场字段、登录订阅状态和旧接口字段兼容。&lt;/p&gt;
&lt;p&gt;例如结束回放场景，优化前后均返回 483 条总记录、每页 12 条，确认响应变快没有以丢失数据为代价。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;测试口径&lt;/th&gt;
 &lt;th&gt;指标&lt;/th&gt;
 &lt;th&gt;结果&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;PHP 与 Java 同机等价功能对照，10 并发、3 轮&lt;/td&gt;
 &lt;td&gt;列表 P95&lt;/td&gt;
 &lt;td&gt;1170 → 529 ms，降低 54.79%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;同一组新旧系统对照&lt;/td&gt;
 &lt;td&gt;吞吐量&lt;/td&gt;
 &lt;td&gt;7.13 → 29.33 RPS，约为原来的 4.1 倍&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;同一组新旧系统对照&lt;/td&gt;
 &lt;td&gt;P99&lt;/td&gt;
 &lt;td&gt;6938 → 964 ms，降低 86.11%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Java 查询优化后的本地测试，20 并发、3 轮&lt;/td&gt;
 &lt;td&gt;列表 P95&lt;/td&gt;
 &lt;td&gt;三轮中位数 218 ms，600 次请求均成功&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;本地列表响应体对比&lt;/td&gt;
 &lt;td&gt;响应体积&lt;/td&gt;
 &lt;td&gt;减少 18.84%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Java 优化前后双实例，预热后各串行请求 30 次&lt;/td&gt;
 &lt;td&gt;即将开始直播 P95&lt;/td&gt;
 &lt;td&gt;162.4 → 57.1 ms，降低 64.8%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;同一组直播专项测试&lt;/td&gt;
 &lt;td&gt;结束回放 P95&lt;/td&gt;
 &lt;td&gt;217.7 → 54.4 ms，降低 75.0%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;同一组首页专项测试，匿名缓存命中&lt;/td&gt;
 &lt;td&gt;首页 P95&lt;/td&gt;
 &lt;td&gt;145.6 → 3.5 ms，降低 97.6%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="这些结果能说明什么"&gt;&lt;a href="#%e8%bf%99%e4%ba%9b%e7%bb%93%e6%9e%9c%e8%83%bd%e8%af%b4%e6%98%8e%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;这些结果能说明什么
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;系统迁移对照不能全部归因于某个索引。&lt;/strong&gt; &lt;code&gt;1170 → 529 ms&lt;/code&gt; 包含运行框架、查询实现和返回数据等差异，两边总记录数也有小幅差异。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地基线与优化后测试不是严格控制变量实验。&lt;/strong&gt; 优化前只有一轮，优化后取三轮中位数，不能直接把 &lt;code&gt;1385 → 218 ms&lt;/code&gt; 包装成严格的改善率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直播专项属于串行联调测试。&lt;/strong&gt; 不能将其描述为高并发生产压测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首页 97.6% 是缓存命中收益。&lt;/strong&gt; 首次未命中的记录为 297.5 ms。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求没有失败不代表真实业务负载性能达标。&lt;/strong&gt; 另一次隔离库 2200 请求没有 HTTP 失败，但业务数据为空，不能推断生产数据量下的性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺少监控对照的指标不作定量结论。&lt;/strong&gt; CPU、内存、锁等待、磁盘 I/O 和慢查询数量没有完整前后对照，不能声称降低了具体比例。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;测试还发现手机号、短信记录查询缺少合适索引。这些属于后续建议，现有记录不足以证明相关索引优化和复测已经完成。&lt;/p&gt;
&lt;h2 id="10-面试时如何讲清楚这段经历"&gt;&lt;a href="#10-%e9%9d%a2%e8%af%95%e6%97%b6%e5%a6%82%e4%bd%95%e8%ae%b2%e6%b8%85%e6%a5%9a%e8%bf%99%e6%ae%b5%e7%bb%8f%e5%8e%86" class="header-anchor"&gt;&lt;/a&gt;10. 面试时如何讲清楚这段经历
&lt;/h2&gt;&lt;p&gt;我先通过接口基线测试区分应用、数据库与网络耗时，再结合实际 SQL、&lt;code&gt;SHOW INDEX&lt;/code&gt; 和 &lt;code&gt;EXPLAIN&lt;/code&gt;，定位全表扫描、额外排序及循环关联查询。&lt;/p&gt;
&lt;p&gt;针对会场和直播列表，我将逐条查询会议、订阅状态改为按页收集 ID、批量 &lt;code&gt;IN&lt;/code&gt; 查询并在内存回填，使匿名列表查询从 &lt;code&gt;2+N&lt;/code&gt; 收敛到通常 3 次，登录态列表从 &lt;code&gt;2+2N&lt;/code&gt; 收敛到通常 4 次。同时精简富文本字段，按筛选和排序条件设计联合索引，并处理 MySQL 5.7 的索引长度、混合排序和历史零日期兼容问题。&lt;/p&gt;
&lt;p&gt;随后通过数据一致性回归、执行计划复核和优化前后测试验证效果。在预热后串行请求的直播专项测试中，P95 降低 64.8%～75.0%；本地列表响应体减少 18.84%。匿名首页使用 10 秒进程内缓存，缓存命中场景 P95 降低 97.6%。这些结果分别对应各自的测试条件，不作为高并发生产环境的统一性能承诺。&lt;/p&gt;</description></item></channel></rss>