文中的 TPS、延迟和 SQL 耗时均来我的次实际测试
做订单系统时,有个场景挺常见的。
一边是用户不断下单,系统要查商品、扣库存、写订单;另一边是运营盯着报表,隔一会儿就要看小时销售额、热销商品和区域业绩。两边都着急,而且往往读写的是同一批数据。
我想验证的核心问题是:报表开始运行后,订单事务会不会明显变慢?
这次没有从大段概念讲起,而是在 DM9 上构造一套订单数据,分别测试订单任务、报表任务以及两者同时运行的情况。TPS 下降多少、报表耗时增加多少,都以程序日志和三轮测试结果为准。
我主要想弄清四件事:单跑基线是多少;混跑之后差多少;这个变化是不是连续三轮都能看到;最后,加个索引到底是真优化,还是看起来像优化。说白了,能用数字回答的,就尽量不靠感觉。
两边访问的是同一套订单数据,CPU、内存和磁盘自然也得一起分。
先用订单业务解释一下。
TP 做的是一笔一笔的小事务。比如创建订单:选两件商品,查价格,扣库存,写一条订单,再写两条明细,没问题就提交,有异常就回滚。每一笔不算复杂,但是量大、频率高。所以我更关心 TPS、平均延迟、P95 和失败数。
AP 则是在已经产生的订单里找答案。比如某个小时卖了多少钱,哪些商品卖得最好,各地区订单量和客单价怎么样。单条查询次数没那么多,但它会扫更多数据,还要做连接、分组和排序。我看的主要是每条查询耗时。
这里有个测试前提:TP 和 AP 必须碰同一套表。如果订单写 A 库,报表查 B 库,两边井水不犯河水,那就测不出混合负载的影响。所以这次两类任务都直接访问 ORDERS 和 ORDER_ITEM。这样结果可能没那么漂亮,但更接近我真正想看的问题。
性能数据必须结合运行环境理解,下面是本次测试使用的软硬件配置。
我拿到的是 DM9 x86 CentOS 7 安装包,但这次实际跑测试的环境是 Ubuntu 24.04 容器。核心程序能启动,数据库能建,JDBC 也能正常连接。不过这只能算一次隔离环境里的本地验证,不能据此说它就是正式兼容方案。真要上生产,安装包还是要和目标操作系统对应。
| 项目 | 本次实测环境 |
|---|---|
| DM9 版本 | DM Database Server 64 V9;03151060506-20260417-322930-20218;DB Version 0x7000D |
| JDBC 驱动 | DmJdbcDriver11 9.1.0.26 - Production |
| 操作系统 | Ubuntu 24.04.3 LTS,Linux 6.18.35 |
| CPU | 9 vCPU,Intel Xeon Platinum 8573C,KVM |
| 内存 | 15 GiB,无 Swap |
| 磁盘 | 32 GiB 虚拟磁盘,系统标记 rotational=1 |
| 部署方式 | 单实例、容器化 KVM 本地环境 |
| 压测工具 | Java 17.0.20,自编 JDBC 并发程序 |
| 数据库关键参数 | 端口 5236;页大小 16K;BUFFER 2048 MB;WORKER_THREADS 16;TASK_THREADS 16;其余保持初始化值 |
这里还有两个不太标准的地方,我也不藏着。
第一,容器里不能正常切换 UID 和修改文件属主,所以这次进程是用 root 跑的。生产环境肯定不建议这样做,按安装手册创建 dmdba 专用用户才是正常做法。
第二,不同命令会话之间是隔离的,我只能把数据库服务和压测程序放到同一个执行会话里。所以下面的数据,只代表我这次这台机器和这套运行方式。
机器不算大,所以这里更适合看变化趋势,不适合拿去做产品横向排名。
模型我没有做得很大,一共四张表:APP_USER、PRODUCT、ORDERS 和 ORDER_ITEM。用户表里放地区,商品表里放价格和库存,订单表保存时间、状态和总金额,明细表记录每件商品。
这几个字段都是后面真要用的,不是为了把表做得像业务系统。ORDER_TIME 决定报表查多长时间,STATUS 用来筛选已支付订单,TOTAL_AMOUNT 和 REGION 分别用来算销售额和区域汇总。下面贴的是实际执行过的核心建表 SQL,外键也确实建了。
CREATE TABLE ORDERS (
ID BIGINT PRIMARY KEY, USER_ID BIGINT NOT NULL,
ORDER_TIME TIMESTAMP NOT NULL, STATUS VARCHAR(16) NOT NULL,
TOTAL_AMOUNT DECIMAL(14,2) NOT NULL, REGION VARCHAR(16) NOT NULL,
CONSTRAINT FK_ORDER_USER FOREIGN KEY(USER_ID) REFERENCES APP_USER(ID)
);
CREATE TABLE ORDER_ITEM (
ID BIGINT PRIMARY KEY, ORDER_ID BIGINT NOT NULL, PRODUCT_ID BIGINT NOT NULL,
QUANTITY INT NOT NULL, UNIT_PRICE DECIMAL(12,2) NOT NULL,
LINE_AMOUNT DECIMAL(14,2) NOT NULL,
CONSTRAINT FK_ITEM_ORDER FOREIGN KEY(ORDER_ID) REFERENCES ORDERS(ID),
CONSTRAINT FK_ITEM_PRODUCT FOREIGN KEY(PRODUCT_ID) REFERENCES PRODUCT(ID)
);
CREATE INDEX IDX_ITEM_ORDER ON ORDER_ITEM(ORDER_ID);
四张表既支撑短事务写入,也为三类分析查询提供来源。
造数据时我固定了随机种子 20260903,不然每次重跑,数据分布都变,前后对比就不太好解释。
最后生成了 5 万用户、1 万商品、30 万订单和 90 万订单明细。基础订单覆盖 180 天,最晚到 2026-09-01 23:59:59,每笔订单固定 3 条明细。PAID 状态占得比较多,是故意的。要是已支付订单只占很小一部分,报表很容易因为过滤掉大量数据而显得特别快,那就没什么意思了。
主键使用递增长整型,主要是方便测试后按 ID 找到新增数据。ORDER_ITEM 还单独给 ORDER_ID 建了索引,因为删订单之前,要先把对应明细删掉。如果这里每次都扫全表,轮次之间的清理反而会成为另一个负载。
初始化时,用户和商品每 2000 行提交一次,订单每 500 笔连同 1500 条明细提交一次。整套数据写入并收集统计信息,用了 28.908 秒。写完以后我没有马上压测,而是逐表跑 COUNT(*)。结果正好是 50000、10000、300000、900000。数量对上以后,才开始下一步。
压测前逐表核对数据量,避免初始化偏差影响后续结果。
TP 这部分,我按一次真实下单来设计。
每个工作线程使用一个 JDBC 连接,并关闭自动提交。一次事务随机选两个不同商品,先按商品 ID 从小到大排序,再查价格、扣库存、写订单和明细,最后提交。如果中间有一步失败,就整笔回滚。
为什么先排序?因为多个线程同时更新两个商品时,如果锁定顺序一会儿先 A 后 B,一会儿先 B 后 A,更容易互相等。统一顺序不能保证完全没有等待,但至少能减少这种人为制造的交叉锁序。下面这段就是实际逻辑,省略的只是参数绑定和计数器。
c.setAutoCommit(false);
try {
BigDecimal p1 = queryPrice(priceQ, product1);
BigDecimal p2 = queryPrice(priceQ, product2);
if (updateStock(stockU, product1) != 1 ||
updateStock(stockU, product2) != 1) throw new SQLException();
insertOrder(orderI, orderId, userId, p1.add(p2));
insertItem(itemI, itemId1, orderId, product1, p1);
insertItem(itemI, itemId2, orderId, product2, p2);
c.commit();
} catch (Exception e) {
c.rollback();
}
TP 一共开 8 个线程,也就是 8 个并发连接。线程先用 CountDownLatch 等齐,然后一起开始。每组先预热 10 秒,清掉预热数据,再正式跑 60 秒。整个流程重复 3 次,最后取中位数。
我没有再套一个连接池框架。线程启动时建好连接,正式计时期间一直复用。因为这次要比较的是 TP、AP 和混跑,不是比较连接池性能。每轮结束后,新增订单会删除,库存恢复成 100 万,避免越往后表越大。
TP_ONLY 三轮 TPS 分别是 5215.530、5185.830、4515.208。第三轮明显低一些,所以不能只拿第一轮最好看的数字。我最终用中位数:TPS 5185.830,平均延迟 1.541 ms,P95 3.880 ms,三轮业务失败数都是 0。
第三轮确实掉了一截,所以这里不挑最好成绩,只看中位数。
AP 我开了 2 个线程。每个线程按 Q1、Q2、Q3 的顺序循环执行。计时从提交 SQL 开始,一直到 JDBC 把结果集读完,不是只记数据库返回第一行的时间。
三条 SQL 的时间窗口不一样,这是我故意保留的。真实报表一般也不会整整齐齐地只查一个范围。
-- Q1:2026-08-01 起,按小时统计订单量与销售额
SELECT TO_CHAR(ORDER_TIME,'YYYY-MM-DD HH24') HOUR_BUCKET,
COUNT(*) ORDER_COUNT, SUM(TOTAL_AMOUNT) SALES
FROM ORDERS
WHERE STATUS='PAID' AND ORDER_TIME>=?
GROUP BY TO_CHAR(ORDER_TIME,'YYYY-MM-DD HH24')
ORDER BY HOUR_BUCKET;
-- Q2:2026-08-25 起,统计销量最高的 20 个商品
SELECT * FROM (
SELECT I.PRODUCT_ID, SUM(I.QUANTITY) QTY,
SUM(I.LINE_AMOUNT) REVENUE
FROM ORDER_ITEM I JOIN ORDERS O ON O.ID=I.ORDER_ID
WHERE O.STATUS='PAID' AND O.ORDER_TIME>=?
GROUP BY I.PRODUCT_ID ORDER BY QTY DESC
) WHERE ROWNUM<=20;
-- Q3:2026-06-01 起,按地区统计订单与客单价
SELECT REGION, COUNT(*) ORDER_COUNT, SUM(TOTAL_AMOUNT) SALES,
AVG(TOTAL_AMOUNT) AVG_TICKET
FROM ORDERS
WHERE STATUS='PAID' AND ORDER_TIME>=?
GROUP BY REGION ORDER BY SALES DESC;
Q1 查一个月的小时趋势;Q2 虽然只查 2026-08-25 之后的数据,但要连接订单和明细,再按商品聚合、排序并取前 20;Q3 查的时间最长,不过最终只有 8 个地区分组。实际跑下来,最慢的是 Q2,这也不意外,连接、聚合、排序基本都占了。
AP_ONLY 三轮总体平均耗时是 55.251、64.165、52.521 ms,中位数 55.251 ms。拆开看,Q1 是 38.231 ms,Q2 是 85.075 ms,Q3 是 42.465 ms。
这里也能看到,查询范围长不等于一定最慢。Q3 查得更久,但分组少;Q2 时间范围短一些,却要穿过明细表。因此后面看执行计划时,我没有只盯着总代价,还会看它是不是回表、聚合前经过多少数据。
Q2 查的天数不算最长,但连接、聚合、排序一个不少,最后它最慢。
三组测试使用同一个数据库实例和同一份基础数据,以减少环境差异对结果的干扰。
为了让三组结果能放到一起比,我尽量不动其他条件:同一个 DM9 实例,同一份 30 万订单基线,同样的线程代码;每组预热 10 秒,正式运行 60 秒,连续跑 3 次,最后取中位数。有 TP 的场景,每轮结束后都会把新增数据清掉并恢复库存。
CPU 是每秒读取一次 /proc/stat。每轮先取采样峰值,三轮再取中位数。所以表里的 CPU 不是 60 秒平均值,这个口径要先说清楚。
| 参数 | TP_ONLY | AP_ONLY | MIXED / 优化后 |
|---|---|---|---|
| TP 线程 | 8 | 0 | 8 |
| AP 线程 | 0 | 2 | 2 |
| 预热 / 正式运行 | 10 s / 60 s | 10 s / 60 s | 10 s / 60 s |
| 重复次数 | 3 | 3 | 3 |
| 汇总方法 | 中位数 | 中位数 | 中位数 |
把 TP 和 AP 同时运行以后,程序没有出现业务错误,订单持续提交,三条报表也在循环执行。CPU 峰值中位数从 68.791% 升至 83.462%,说明资源竞争已经出现,具体影响仍需结合 TPS 和查询耗时判断。
表面上两边都在正常跑,真正的影响要看 TPS 和延迟。
| 测试场景 | TP TPS | TP平均延迟 | TP P95延迟 | AP平均耗时 | CPU峰值中位数 | 失败数 |
|---|---|---|---|---|---|---|
| TP单独运行 | 5185.830 | 1.541 ms | 3.880 ms | — | 68.791% | 0 |
| AP单独运行 | — | — | — | 55.251 ms | 45.333% | 0 |
| TP+AP混合运行 | 3872.585 | 2.065 ms | 4.893 ms | 127.605 ms | 83.462% | 0 |
| 覆盖索引后混合运行 | 3803.020 | 2.102 ms | 5.027 ms | 90.696 ms | 85.028% | 0 |
先看 TP。TPS 从 5185.830 掉到 3872.585,少了 25.32%。平均延迟从 1.541 ms 增加到 2.065 ms,P95 从 3.880 ms 增加到 4.893 ms。换成绝对值,就是每秒少完成 1313.245 笔事务,P95 多了 1.013 ms。
这不是某一轮突然抖了一下。MIXED 三轮 TPS 分别是 4166.097、3872.585、3696.472,全部低于 TP_ONLY 三轮里最低的 4515.208。
AP 受到的影响更明显。总体平均耗时从 55.251 ms 涨到 127.605 ms,增加 130.96%。MIXED 三轮是 127.605、124.801、149.762 ms,也全部高于 AP_ONLY 最慢一轮的 64.165 ms。Q2 混跑后到了 209.975 ms,还是三条 SQL 里最慢的。
CPU 峰值中位数同时多了 14.671 个百分点,这和吞吐下降、查询变慢是对得上的。不过我这次没有采集 DM9 等待事件,也没有单独记录磁盘延迟,所以不能拍脑袋说“一定就是 CPU”或者“一定就是 I/O”。能确定的是,两类负载放在一起以后,双方都受到了影响。
中途还踩了一个挺真实的坑。
第一次跑到 MIXED 第三轮以后,业务事务本身没失败,出错的反而是清理程序。它想在一个事务里删掉 253670 笔新增订单和 507340 条明细,DM9 返回了 “Out of undo segment space”。
这不是下单报错,而是我写的测试清理逻辑太粗暴。我把它改成每 10000 个订单 ID 提交一批,再跑一次,5.979 秒就清完了。为了不让这次异常影响后面的数据,原第三轮没有放进汇总,而是在基线恢复以后重新跑了一轮。这个坑也提醒我:压测脚本不只是“把压力打上去”,数据恢复逻辑同样要认真写。
三轮方向都一样,所以这次的下降不是只靠一轮数据得出的。
看到混跑变慢以后,我最开始的想法也很直接:Q1 经常按状态和订单时间筛选,那就先给这两列加联合索引。没有先动内存参数,也没有一上来就改并发数。
CREATE INDEX IDX_ORDERS_AP ON ORDERS(STATUS, ORDER_TIME);
CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'ORDERS');
CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'ORDER_ITEM');
普通索引的结果和我原先的预期不一致。加完以后,TPS 是 3886.169,只比原混跑多 0.35%;P95 变成 5.196 ms;AP 平均耗时不但没降,反而从 127.605 ms 涨到了 213.111 ms,慢了 67.01%。
我又看了一遍执行计划。没加 AP 索引时,Q1 计划代价是 40,主要走 CSCN2 后过滤;加普通索引后,代价变成 38,也确实出现了 SSEK2 IDX_ORDERS_AP,但后面还跟着 BLKUP2。
问题就在这里。PAID 占比不低,Q1 除了状态和时间,还要读取 TOTAL_AMOUNT 并按小时分组。索引命中以后,大量记录还是得回表。普通索引三轮 AP 平均耗时分别是 216.956、213.111、211.491 ms,三轮都比原 MIXED 慢,不像是偶发抖动。
所以,加了索引不等于完成优化。这个失败结果我觉得反而挺有价值,就保留下来了。
第二次我把 Q1、Q3 需要的列一起放进索引,做成覆盖索引:
CREATE INDEX IDX_ORDERS_AP_COVER
ON ORDERS(STATUS, ORDER_TIME, ID, TOTAL_AMOUNT, REGION);
重新收集 ORDERS、ORDER_ITEM 的统计信息后,Q1 计划代价降到了 5,计划里只剩 SSEK2 IDX_ORDERS_AP_COVER,BLKUP2 不见了。
这次复测终于有变化。AP 平均耗时从 127.605 ms 降到 90.696 ms,下降 28.92%;Q1 从 89.946 降到 40.543 ms;Q3 从 82.938 降到 46.822 ms;Q2 也从 209.975 降到 184.523 ms。三轮 AP 平均耗时分别是 81.783、90.696、103.971 ms,每一轮都比原 MIXED 对应范围低。
覆盖索引也带来了额外成本。TP TPS 变成 3803.020,比原 MIXED 又低了 1.80%;P95 增加 2.74%,CPU 峰值中位数升到 85.028%。原因也不难理解:报表少回表了,但每写一笔订单,数据库还要维护这条更宽的索引。
所以我更愿意把它叫作一次取舍:牺牲一点 TP 吞吐,换来更明显的 AP 改善。至于 BUFFER、线程数这些参数,我这次没有实际调过,就不把它们包装成优化成果了。
覆盖索引对报表有效,但写订单也多了一份索引维护成本。
综合三组基准数据和两轮索引调整,可以得到以下结论。
在 9 vCPU、15 GiB 内存和 30 万基础订单的环境里,DM9 能同时跑 8 个订单线程和 2 个报表线程,纳入统计的业务失败数是 0。但“能一起跑”不等于“互不影响”。混跑后,TP TPS 下降 25.32%,AP 平均耗时增加 130.96%。
第一次加普通联合索引没有解决问题,反而让 AP 更慢;换成覆盖索引后,AP 平均耗时下降 28.92%,代价是 TP 吞吐又损失大约 1.80%。这个结果不算完美,但我觉得比写一句“加索引后性能大幅提升”更真实。
当然,这组数字不能拿去代表 DM9 的生产性能上限。机器是虚拟环境,测试只有 10 个业务连接,每轮也只跑了 60 秒。下载包对应 CentOS 7,实际运行环境却是 Ubuntu 24.04;能启动并完成测试,只能当作本地实验记录,不能当成正式兼容承诺。
如果数据变成数亿行、连接变成几百个,锁竞争、日志、缓存和执行计划很可能都会变。真要做上线前预演,我还会补下面几项:
文章
阅读量
获赞
