ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MyDAC v5.10 源文件编译安装与MySQL访问调优实战

MyDAC v5.10 源文件编译安装与MySQL访问调优实战

简介:MySQL Data Access Components(MyDAC)v5.10 源文件包面向使用 Delphi 7 至 2007 与 C++Builder 的数据库开发者,提供直连 MySQL 服务端的原生组件方案,不依赖第三方库或中间件,覆盖数据读写、事务、批处理、存储过程与异步调用等场景,适合需要深度定制的中高级开发者。整包共 1403 个文件,压缩后约 1.39MB,以 360 个 pas 源文件、146 个 dpk 组件包、107 个 dfm 窗体、142 个 res 资源及 81 个 bdsproj、24 个 dproj 工程文件为主,另含 cpp、dpr、bpk 等 C++Builder 与旧版工程文件和多套 Make.bat 构建脚本,构成可直接编译的源码树;包内附 HLP 帮助、History、FAQ 与许可协议,便于查阅 API 与版本变更。已有 169 人学习下载,读者可据此编译调试并改造组件源码,按需裁剪驱动层、扩展连接与事务逻辑,也可借工程组织与批处理脚本了解 Delphi 组件库的构建流程。

1. MyDAC v5.10 源文件到底解决什么问题

Delphi 老项目要连 MySQL,装上组件包、拖一个 TMyConnection 上去,多数人跑到能查出数据就停了。真正回头去找 MyDAC v5.10 源文件的,往往是另一类麻烦:编译出来的 exe 换台机器就起不来,字符集怎么调都出乱码,某个查询慢到离谱却看不到组件内部到底发了什么 SQL,手里只有编好的 bpl 和 dcu,这些问题只能靠猜。源码版的价值不在功能多,而在于能重编译、能单步、能改。下面按能不能用、怎么装、参数怎么设、坑在哪的顺序拆开讲。

2. 把 MyDAC v5.10 源文件编进 IDE:包结构、搜索路径与编译顺序

拿到一份 Delphi 组件源码,最常见的失败不是代码有错,而是编译顺序和搜索路径没理清:先编了设计期包,它依赖的运行期包还没生成;或者运行期包编到一半,编译器去 System 目录里找了一份旧的同名单元。理顺这三件事,后面全是机械操作。

2.1 先分清源码包里的三层单元

一套完整的 MySQL 数据访问组件源码,通常分三层组织。最底下是协议与类型层,负责跟 MySQL 服务端握手、打包解包、字段类型映射,这一层跟 VCL 无关,理论上可以拿去控制台程序里跑;中间是数据集层,把协议层的结果包装成 TDataSet 的形态,提供 TMyQuery、TMyTable、TMyStoredProc 这些能拖到窗体上的组件;最上面是设计期层,只包含 Register 过程和属性编辑器,编译产物是 dcl 开头的设计期包。

三层对应三种编译产物:协议层和数据集层合成运行期包(运行期 .bpl + 一堆 .dcu),设计期包单独一个,只被 IDE 加载。搞混的代价很直接——你把设计期包装进 IDE,编译项目时却提示找不到 DBAccess 之类的单元,因为运行期包的 dcu 输出目录没加进 Library Path。

我一般会先在源码根目录下按 Core、Datasets、Design 建三个子目录,把对应的 .pas 归位,再建一个 Lib\Win32\Release 作为统一输出目录。目录结构定死之后,后面命令行编译、IDE 编译、同事接手,用的都是同一套路径,不会出现“我这能编你那不能编”的玄学。单元文件的实际命名以包内为准,认的是层次而不是具体名字。

顺手在包里翻一下有没有 .inc 条件编译文件。这类组件通常用一组开关控制功能裁剪,比如是否启用 SSL、是否启用连接池、是否带本地化资源。编之前先确认开关组合,编完再改开关等于白编一遍。

2.2 用命令行批处理重编译运行期包

IDE 里点 Install 编译一次就够用了,但源码版的意义在于“改完能立刻重编”,手工点太慢。写成批处理,改一行源码跑一次脚本,十秒钟出新包。

:: build_runtime.bat —— 放在源码根目录,先编运行期包 set DCC="C:\Program Files\Delphi\bin\dcc32.exe" set SRC=.\Source set OUT=.\Lib\Win32\Release :: -B 全量重编 -Q 静默 -$D- 关调试信息 -$L- 关运行时范围检查 :: -U 单元搜索路径 -NU/-NB/-NH 分别指定 dcu、bpl、hpp 的输出位置 %DCC% -B -Q -$D- -$L- ^ -U"%SRC%\Core;%SRC%\Datasets" ^ -NU"%OUT%" -NB"%OUT%" -NH"%OUT%" ^ "%SRC%\Packages\mydacXX.dpk" if errorlevel 1 ( echo [FAIL] 运行期包编译失败,先看上面第一条 Error exit /b 1 ) echo [OK] 运行期包已输出到 %OUT%

这段脚本的关键在三个参数。-U是单元搜索路径,只放 Core 和 Datasets,故意不放 Design,这样一旦数据集层误引用了设计期单元,编译当场报错,不会拖到安装阶段才暴露。-NU指定 dcu 输出目录,必须和后面 IDE 里配的 Library Path 完全一致,路径差一个反斜杠都可能让 IDE 去别处找旧 dcu。-B是全量重编,源码版每次改完都建议全量,增量编译在改了 .inc 开关的时候会给你留下一个半新半旧的包,症状是运行期报“找不到入口点”,很难查。

包里那个mydacXX.dpk的 XX 是 IDE 版本代号,换成你自己环境对应的那个,包文件名以源码包里的实际文件为准。如果源码包同时给了 .dpk 和 .dproj,命令行走 dcc32 更稳,msbuild 那套依赖 MSBuild 的 Delphi 目标文件,老版本 IDE 上经常因为框架版本对不上直接罢工。

编译完立刻做一次验证,别急着装设计期包:

:: 检查输出目录里 dcu 数量是否和源码 .pas 数量对得上(排除设计期单元) dir /b "%SRC%\Core\*.pas" "%SRC%\Datasets\*.pas" | find /c ".pas" dir /b "%OUT%\*.dcu" | find /c ".dcu"

两个数字差太多,说明有条件编译分支被跳过了,或者某个单元因为缺依赖根本没参与编译。这一步花三十秒,能省掉后面半小时的“组件装上了但一拖就报错”。

2.3 安装设计期包与组件面板注册

运行期包编好、Library Path 配好之后,设计期包才轮到上场。IDE 里走 Component → Install Packages → Add,选中输出目录里的 dcl 开头的 .bpl。装完组件面板应该多出一个分类页,里面是 TMyConnection、TMyQuery 这一组。

装不上时按这个顺序查。第一,设计期包和运行期包必须同一套编译配置,一个是 Release 一个带调试信息,加载时报“Cannot load package”或者“单元版本不匹配”,这类报错信息很含糊,实际原因就是 .dcu 的编译指纹对不上。第二,Library Path 里旧版本的路径要删干净,同一台机器装过别的版本组件时,IDE 会优先命中先出现的那条路径。第三,包里如果带了资源文件(.res),资源编译产物也要跟着拷到输出目录,缺了它设计期包能加载但组件图标全是空白。

装好之后别急着写业务,先拖一个 TMyConnection 到空白窗体,把 Server、Port、Username、Password、Database 填上,Connected 设 True。这一步能连上,说明协议层、字符集、网络都通了,后面出问题一定出在数据集层或者你的 SQL 上,排查范围直接砍一半。

3. TMyConnection 的连接参数:直连协议、字符集与连接池怎么设

连接串是这类组件里最容易被复制粘贴毁掉的部分。网上抄来的一段初始化代码,字段名对得上、能跑通,但池大小、超时、字符集全是默认值,一到并发或者跨机房就原形毕露。这一章把几个真正影响稳定性的参数挑出来讲。

3.1 直连模式与客户端库模式的选择边界

这类组件一般给两种连接方式。一种是直连,组件自己实现 MySQL 的通信协议,不需要任何外部动态库,exe 拷过去就能跑;另一种是走官方客户端库,需要目标机器上有对应的 dll,好处是协议兼容性完全跟着官方走,遇到新版本服务端的认证插件、新的加密方式时不容易卡住。

选哪边,看两个条件。客户端是绿色部署、机器不受你控制(比如装在客户内网的一堆终端上),直连模式省掉“少一个 dll 就启动失败”的售后电话,这是压倒性优势。反过来,服务端版本比较新、开了比较激进的认证方式,或者要用到一些只有客户端库才完整支持的扩展能力,那就老老实实走客户端库模式,把 dll 一起打进安装包。

对比项直连模式客户端库模式
外部依赖无需要对应平台的客户端动态库
部署体积小多出几 MB 的库文件
新认证方式兼容取决于组件版本,可能要等更新跟着官方库走
排错难度组件内部,有源码可单步多一层黑匣子
适合场景绿色部署、内网批量终端服务端版本新、功能依赖完整协议

这段判断做完,再往下配参数才有意义。用直连模式时,字符集、超时、包大小这些全靠组件自己的 Option 控制;用客户端库模式时,一部分行为会继承 dll 那边的默认值,两边都调一遍容易出现“改了这个覆盖那个”的情况,建议只在一处集中设置。

3.2 一份可以直接抄的连接初始化代码

下面这段是常规写法,重点是每个属性后面都写清了为什么这么设,照抄之后按你的环境改值即可。

procedure TDM.InitConnection; begin FConn := TMyConnection.Create(nil); try FConn.Server := '10.0.0.21'; // 建议走内网地址,公网域名解析抖动会直接体现为连接超时 FConn.Port := 3306; FConn.Username := 'app_rw'; // 读写账号,报表类只读功能另开只读账号 FConn.Password := GetPwdFromCfg; // 不要硬编码,从配置文件或凭据服务取 FConn.Database := 'erp_main'; FConn.LoginPrompt := False; // 服务端程序必须关,否则弹登录框卡死后台线程 // 协议与传输:属性挂在 Connection 下还是 Options 下随版本略有差异, // 以 IDE 对象检视器实际显示为准,名字对不上就按同名项设置 FConn.Options.Protocol := mpTCP; FConn.Options.Compress := True; // 跨机房或广域网必开,内网同交换机可关掉省 CPU FConn.Options.UseUnicode := True; // 与 utf8mb4 配套,关掉必出乱码 FConn.OnConnect := DoAfterConnect; // 连接建立后的会话级设置统一挂这里 FConn.Pooling := True; // 内置连接池,服务端程序默认开 FConn.MaxPoolSize := 40; // 起步值,按并发线程数 × 1.2 估 FConn.ConnectionTimeout := 15; // 秒,太短会在网络抖动时雪崩重连 FConn.Connect; except on E: Exception do begin LogError('MySQL 连接失败: ' + E.Message); FreeAndNil(FConn); raise; end; end; end; procedure TDM.DoAfterConnect(Sender: TObject); begin // 会话级设置:字符集、时区、SQL 模式,一次设完,不依赖服务端默认值 (Sender as TMyConnection).ExecSQL('SET NAMES utf8mb4'); (Sender as TMyConnection).ExecSQL('SET time_zone = ''+08:00'''); (Sender as TMyConnection).ExecSQL('SET SESSION sql_mode = ''STRICT_TRANS_TABLES'''); end;

逻辑上就三块。第一块是基础寻址,把 Server、Port、账号、库名固定下来,LoginPrompt一定要关,服务端或后台线程里弹出一个登录对话框,表现就是进程卡死、日志一片空白。第二块是传输层,压缩开关按网络距离决定,Unicode 开关必须和库的字符集配套。第三块是会话级设置,放在 OnConnect 里最省心——连接池复用连接时,会话状态是跟着物理连接走的,只要第一次建连设好了,后续取出来的连接就都带着这套设置,不会出现“第一个请求正常,第二个请求乱码”这种只有池化场景才有的怪现象。

字符集这块多说一句。不同版本里设置字符集的入口可能是连接属性,也可能是 Options 下的子项,名字对不上很正常。稳妥做法是两者都用:属性有的就设属性,同时在 OnConnect 里补一条SET NAMES,两条路都走一遍不会冲突,还能兜住版本差异。

3.3 连接池的四个必调参数

连接池默认值是按“能跑”设计的,不是按“能扛”设计的。四个参数建议在项目初期就定下来,后面改要动回归测试。

参数常见默认建议区间调错的后果
Pooling关闭服务端程序开不开池,每个请求建连一次,握手开销压垮 QPS
MaxPoolSize100 左右20~50设太大,数据库端连接数打满,反而互相等待
ConnectionTimeout0 或较长10~15 秒设太长,故障时线程全堵在等连接;设太短,抖动即报错
Compress关闭广域网开、同机房关同机房开压缩,CPU 白白多烧一截

MaxPoolSize 的估算方法很朴素:把应用的并发处理线程数数出来,乘 1.2 到 1.5,再和数据库的 max_connections 除一下,取小的那个。见过太多项目直接写 200,结果数据库端连接数被打满,DBA 那边看到的是一堆 Sleep 状态的连接,应用这边却在报“too many connections”,两边互相甩锅。宁可把池设小、把超时设短,让压力早点以报错的形式暴露出来,也别让它悄悄堆积成一个看不出来的雪崩。

4. TMyQuery 取数与写库:FetchAll、UniDirectional 与批量装载

连接通了之后,性能问题九成出在数据集组件上。同样是SELECT * FROM t_order,开不开 FetchAll、是不是单向游标、关闭前有没有取完,内存占用能差一个数量级,出事的方式也完全不一样。

4.1 大结果集为什么先调 FetchAll

默认情况下,很多数据集组件会把整个结果集一次性拉到客户端,好处是断开连接后还能翻记录,坏处是碰上一张几百万行的表,进程内存瞬间起飞。关掉 FetchAll 之后行为反过来:查询只把游标开到服务端,记录按需一条条取,内存平稳,但连接被这个游标一直占着,而且必须把记录取完或者显式关闭,才能拿这条连接干别的事。

// 场景:导出/统计类查询,结果集可能几十万行,只要顺序读一遍 qry.Close; qry.Connection := DM.Conn; qry.FetchAll := False; // 服务端游标,按需取 qry.UniDirectional := True; // 单向只读,省掉双向缓冲,速度明显更快 qry.SQL.Text := 'SELECT id, order_no, amount, created_at ' + 'FROM t_order WHERE created_at >= :d1 AND created_at < :d2 ' + 'ORDER BY id'; qry.ParamByName('d1').AsDateTime := FStartDate; qry.ParamByName('d2').AsDateTime := FEndDate; qry.Open; try while not qry.Eof do begin WriteRowToCsv(qry); // 边取边处理,内存只跟单条记录有关 qry.Next; end; finally qry.Close; // 单向游标必须显式关闭,否则连接不归还 end;

三个参数的配合关系要记牢。FetchAll := False换来低内存,代价是连接占用时间长,所以这类查询一定要单独走一条连接,别和界面上的其他查询共用;UniDirectional := True只对顺序读有效,一旦你要RecNo、要往回翻、要排序后再定位,就不能开,开了在运行期直接抛异常;ORDER BY加在 SQL 里而不是客户端排序,配合单向游标才有意义,客户端排序等于把整个结果集又拉回内存,前面的优化白做。

判断标准很简单:这个结果集要不要在界面上反复浏览?要,就保持 FetchAll 为真,但必须加分页或者强制加 WHERE 条件;不要,只是导出、对账、跑批,那就关掉,用单向游标顺序扫。这两条路没有中间态,硬要混着用就是给自己埋雷。

qry.Close放在 finally 里不是形式主义。单向游标没关,连接池里那条连接就一直占着,高压下几次异常就能把池耗空,症状是过一会儿所有查询都开始超时,重启进程立刻恢复——这种“重启治百病”的问题,十次有八次是游标或连接没释放。

4.2 批量导入用装载组件,不要循环 ExecSQL

往 MySQL 灌十万行数据,最差的做法是写个 for 循环,每行拼一条 INSERT 然后 ExecSQL。这样一次一个往返,十万次网络交互,跑一晚上都算快的。更隐蔽的坑是:如果 autocommit 是开的,每行还各自带一次提交,磁盘刷十万次,机器直接趴窝。

正确的做法是用组件自带的批量装载器,它把多行数据打包成一条多值 INSERT(或者走 LOAD DATA 语义)发过去,往返次数从十万降到几百。

procedure TDM.BulkInsertOrders(AData: TDataSet); var Loader: TMyLoader; begin Loader := TMyLoader.Create(nil); try Loader.Connection := FConn; Loader.TableName := 't_order_stage'; // 中间表,先落库再在服务端做清洗 // 只导指定列,顺序必须和数据集字段一一对应; // 不给自增主键、不给有默认值的列,少给比多给安全 Loader.Columns.Clear; Loader.Columns.Add('order_no'); Loader.Columns.Add('customer_id'); Loader.Columns.Add('amount'); Loader.Columns.Add('created_at'); FConn.StartTransaction; try Loader.LoadFromDataset(AData); // 全量灌入,内部按批拆分 FConn.Commit; except FConn.Rollback; raise; end; finally Loader.Free; end; end;

调用方式和属性名在不同版本间有些出入,认准两件事就够了:一是必须显式指定列清单,靠位置匹配,字段顺序错了不会报错,只会把 A 列写进 B 列,这种错误在数据校验环节才会暴露,代价极大;二是整批必须包在一个事务里,要么全进要么全不进,否则中途一条脏数据失败,前面已经写进去的几万行全成了需要人工清理的垃圾。

如果因为版本原因没有装载组件,退而求其次的方案是:把 autocommit 关掉,把 INSERT 拼成每批 500 到 1000 条的多值语句,手动控制提交频率。这个土办法比循环 ExecSQL 快两个数量级,缺点是出错时定位不到具体哪一行,所以生产环境里只当降级预案用。

4.3 事务和 CachedUpdates 的取舍

事务这块没什么可犹豫的,凡是跨多表的写操作,一律显式StartTransaction/Commit/Rollback,别指望自动提交。真正需要想清楚的是要不要开 CachedUpdates。

开了之后,所有的增删改先记在客户端的变更日志里,不发给服务端,直到你调 ApplyUpdates。适合的场景是主从表一起编辑——主表新增一行要拿到自增主键才能填子表的外键,缓存在客户端可以先把关系理顺再一次性提交。不适合的场景是高频单行写入,缓存一层只是徒增一次内存拷贝和一次状态维护。

用 CachedUpdates 时有个必须注意的点:变更日志不会自己清。ApplyUpdates 之后如果不调 CommitUpdates,日志一直在涨,界面上改了十次保存十次,第九次的数据还在日志里排着队,最终表现就是“我明明只改了一行,数据库里却更新了十几条记录”。相关的事件回调(更新前、更新后、更新出错)该挂就挂,出错时至少能在日志里看到是哪张表、哪条记录出的问题,而不是一个光秃秃的异常。

qry.CachedUpdates := True; qry.UpdateMode := upWhereKeyOnly; // 只按主键定位,避免 Where 全字段导致更新不到 // ... 界面上做增删改 ... qry.ApplyUpdates; // 可能抛异常,外面套 try qry.CommitUpdates; // 必须调,否则变更日志不清,下次 Apply 会重复提交

UpdateMode这个细节容易忽略。默认用全字段做 WHERE 条件时,只要有一个字段被别的会话改过,你的更新就影响 0 行,而且不报错,静默失败。改成只按主键定位,冲突变成后写覆盖,至少行为是确定的、可预期的。

5. 上线前必查:MyDAC 源文件版五个高频翻车点排查

前四章把能跑通的路径走完了,这一章全是踩坑记录。每条按现象、原因、解决写,遇到对得上的直接照做。

5.1 编码与时区类的两个坑

现象:中文全变问号或者一堆乱码,英文数字正常。库里的表已经是 utf8mb4,客户端代码也没做转换。

原因:连接建立时的会话字符集没设对,三次握手完成后服务端用的是它自己的默认字符集,跟你程序里的字符串编码对不上。只看表的字符集是不够的,连接字符集、表字符集、字段字符集三者一致才行。

解决:在 OnConnect 里统一执行一次SET NAMES utf8mb4,同时把连接组件的 Unicode 相关开关打开。设完用一条最简单的语句验证:SELECT '中文测试',返回正常再往下走。这个验证要进冒烟测试清单,因为不同机器上组件命中不同的连接路径,表现可能不一致。

现象:所有时间字段取出来都比实际早或晚 8 小时,日期类型(DATE)却完全正常。

原因:时间戳类型的转换是在会话时区下做的,程序里的 TDateTime 按本地时间理解,服务端按自己的时区返回,中间差了一个偏移。日期类型不带时区信息,所以不受影响,这个对比正好能帮你确认问题定位准确。

解决:连接建立后固定会话时区,SET time_zone = '+08:00',让服务端和客户端约定用同一个偏移。更彻底的做法是时间戳统一用 UTC 存、显示时再转本地时区,但改造量大的老项目,先把会话时区钉死就能消掉绝大部分问题。

5.2 编译与部署类的两个坑

现象:IDE 里一切正常,打包发给测试同事,双击就报“找不到 XXX.bpl”或者启动即闪退,事件查看器里只有一条无用的模块加载失败。

原因:项目用了运行期包但没把 .bpl 随 exe 一起发布,或者装了源码版之后仍然是动态链接运行期包,而目标机器上压根没装这套组件。

解决:打开项目选项,把“使用运行期包”这一项根据场景定死——要绿色部署就改成把运行期库一起静态链接进 exe,代价是体积涨几 MB;要走动态包,就必须把对应的 .bpl 打进安装目录并确认能被搜索到。两种模式别混着来,开发机上一半静态一半动态,本机照样能跑,一换机器立刻崩。

现象:换了个 IDE 版本重编源码包,报一堆“单元版本不匹配”或者“找不到入口点”,删了重装也没用。

原因:机器上同时存在多个版本的组件 dcu,Library Path 里旧的路径排在前面,编译器命中了旧 dcu,链接时又跟新 bpl 对不上。

解决:把所有旧版本的搜索路径从 IDE 全局配置和项目配置里逐条删干净,dcu 输出目录也清空重编。判定是否彻底的办法:全量重编,看编译日志里每个单元的实际加载路径,只要出现不在你指定的输出目录下的路径,就还没清干净。

5.3 连接与游标类的一个坑

现象:系统跑十几分钟后开始大面积超时,重启应用立刻恢复,数据库端监控看起来连接数不高。

原因:连接池被耗尽但不报警。常见诱因有两个:单向游标没关闭,物理连接一直被游标占着不归还;或者某个异常分支里 Create 了数据集没 Free,泄漏的是数据集对象连带它持有的连接引用。

解决:把所有数据集操作套进 try/finally,Close 和 Free 都写进去。再在连接池上加监控,定期把当前占用数、空闲数、等待数打到日志里,出问题时看一眼曲线就知道是泄漏还是真的并发太高。这个日志平时没人看,出事那十分钟能救命。

6. 改源码之前先做基线:用监控组件抓真实瓶颈

源码在手,最大的诱惑是先去改代码。我吃过这个亏:花了两天把协议层的包大小调优了一遍,性能只涨了 3%,回头一查,真实瓶颈是某条查询没走索引,全表扫描。所以动源码之前,先做一次能重复的基线测量。

基线要靠观测而不是靠猜。组件通常自带监控工具或事件,把它们挂上,记录每条 SQL 的文本、执行耗时、返回行数,把慢查询按耗时排序,前十条看完再决定要不要动代码。

观测指标采集方式正常表现异常信号
单条 SQL 耗时监控组件记录与数据库端慢日志量级一致客户端耗时远大于服务端,说明在等网络或等连接
每秒新建连接数连接池计数峰值后迅速回落持续高位,说明池没命中,每次都新建
结果集行数监控记录与业务预期一致预期几行却返回几万行,多半是 WHERE 条件丢了
内存占用曲线进程监控平稳锯齿单调上涨,典型是游标或数据集没释放

这套东西搭起来不超过一天,收益是把“我觉得慢”变成“这条 SQL 慢了 400 毫秒”。有了基线,改源码才有对照:改完重跑同一批查询,耗时、连接数、内存三条曲线一起看,任何一条变差都说明改动有问题,哪怕总体变快了也要查清楚是哪一块被牺牲掉了。

真要动源码,从最小的切入点开始。第一优先级是加日志,在协议层的收发位置打点,把完整的 SQL 文本和参数值落盘——这一步不改变任何行为,只是把黑匣子打开,而且后面的所有优化都要靠它验证。第二优先级才是改算法或改缓冲策略,改完必须有基线数据撑着,别凭感觉。

验证方法说具体一点:准备一份固定数据集和固定查询清单,跑三轮取中位数,每轮之间重启进程避免池状态干扰。改前改后用的是同一份脚本、同一台机器、同一个数据库实例,任何一项变了结论就不成立。这套流程听起来繁琐,但它能挡掉至少一半的“优化完反而更慢”。

最后一条是血泪经验:别在同一个迭代里既升级组件版本又改业务 SQL。两者叠加之后,性能变好你不知道是谁的功劳,变差也定位不到原因,只能整批回滚,白干一轮。分两次上线,中间留一天观察,后悔药才有得吃。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进