首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一到饭点扫码买单就转圈、每单多等3秒:门店收银接口卡顿的六个排查步骤

一到饭点扫码买单就转圈、每单多等3秒:门店收银接口卡顿的六个排查步骤

原创
作者头像
数字化落地笔记
发布于 2026-10-08 10:27:06
发布于 2026-10-08 10:27:06
250
举报

导读

门店收银平时正常,一到饭点扫码买单就转圈,每单平均多等两三秒,顾客排起队。先给结论:瓶颈几乎都在数据库,是几条没走索引的慢查询,在高峰期把连接池占满了。这次我们把结账接口 P95 从 6 秒压到 400 毫秒,平均响应回到 300 毫秒以内。下面是六个排查步骤。

一、先说背景,交代下环境

客户在商场里开了三家直营餐饮小店,饭点每个柜台每小时要走 150 到 200 笔,扫码自助点餐和柜台收银都有。

日常的点单、收银和会员台账,跑在唯顿收银系统(线下门店智慧收银系统)上;结账时的会员积分和券核销这一段,是我们自己另外接的逻辑。

先把这段边界交代清楚,是因为这次卡顿最后就定位在我们自己写的那段核销查询上,跟日常的收银台账没有关系。

二、第一步:先量化,别凭感觉说“卡”

不要一上来就加机器、改代码,先把“慢在哪”测出来。

我们在结账链路上加了埋点,把“提交订单”这个接口拆成几段计时:参数校验、库存扣减、券核销、积分、写流水。一拉数据就很清楚——接口自身逻辑耗时不大,大头都耗在数据库等待上,且饭点明显恶化。

同时看数据库侧的慢查询日志和连接数。两条曲线一对:慢查询一增多,活跃连接数就跟着往上顶,后面排队的请求只能干等。这一步把问题从“系统卡”收敛成了“某些 SQL 在高峰期拖慢了数据库”。

开启并查看慢查询日志(MySQL 为例):

代码语言:bash
复制
# 临时开启慢查询记录,阈值设为 0.5 秒
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;

# 再用 EXPLAIN 看清单里耗时最高的那条语句

三、第二步:用 EXPLAIN 抓住全表扫描

从慢查询日志里挑出耗时最高、出现最频繁的几条,逐一用 EXPLAIN 看执行计划。

我们当时最可疑的是券核销里的一条查询:结账时要查这张券的历史核销记录,结果一次结账就要执行,单次耗时 1.2 秒。

代码语言:sql
复制
-- 原始写法:按会员手机号查券,手机号字段是字符串,
-- 但传入时被当成了数字,发生隐式类型转换
SELECT *
FROM coupon_record
WHERE phone = 13800000000
  AND status = 1;

EXPLAIN SELECT * FROM coupon_record WHERE phone = 13800000000 AND status = 1;
-- type = ALL   表示全表扫描
-- rows = 很大   预计扫描行数
-- key = NULL    没有用到任何索引

type=ALL、key=NULL、rows 很大,就是典型的全表扫描。数据量小的时候无感,饭点并发一上来,几十条这样的查询同时扫表,数据库 CPU 直接被打满。

四、第三步:确认索引为什么没生效

很多时候索引明明加了,却没被用上。常见有这么几种:

  • 隐式类型转换:字段是字符串,传入却没加引号,索引失效(就是上面这条);
  • 列上用了函数或运算:WHERE DATE(create_time)=...、WHERE amount+0>10,索引用不上;
  • 没满足最左前缀:联合索引 (phone,status),查询却只按 status 过滤;
  • 左模糊:LIKE '%123' 无法走普通索引。

把参数类型改对、把函数挪到等号右侧(或新增生成列),再按查询条件补齐联合索引:

代码语言:sql
复制
-- 1) 传入值加引号,消除隐式转换
-- 2) 建立联合索引,并尽量用覆盖索引避免回表
ALTER TABLE coupon_record
  ADD INDEX idx_phone_status (phone, status);

-- 如果只需要 id 和券号,可直接让索引覆盖查询列,省去回表
SELECT id, coupon_no
FROM coupon_record
WHERE phone = '13800000000'
  AND status = 1;

改完再 EXPLAIN,应能看到 type=ref、key=idx_phone_status、rows 明显变小、Extra 出现 Using index。这一条改完,单次查询就从 1.2 秒降到了几毫秒。

五、第四步:检查连接池和锁

慢查询本身慢,还会连锁拖垮连接池,这是高峰期雪崩的关键。

连接被慢查询长期占用。 结账请求每个都要拿数据库连接,慢查询一条占住连接几百毫秒到几秒,饭点并发一来,连接池很快被占满,新请求排队等待连接,表现就是“转圈”。

大事务和行锁等待。 扣库存、核销券这类操作要加行锁,如果事务里还夹了远程调用、写了一堆无关数据,事务时间被拉长,锁也就一直不释放,其他结账请求被堵在锁等待上。

合理设置连接池、把事务边界收紧:

代码语言:java
复制
// HikariCP:按数据库规格和并发设置连接数,不是越大越好
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(20);          // 结合数据库 max_connections 评估
config.setConnectionTimeout(1000);     // 拿不到连接快速失败,别无限堆积
config.addDataSourceProperty("slow_query_timeout", "1000");

// 事务方法里只放必须原子提交的数据库操作,
// 远程调用、消息通知、日志上报都挪到事务外

原则是:事务里只做“要么全成、要么全回滚”的数据库操作,其余一律移出去,让连接和锁尽快释放。

六、第五步:找高峰期的放大因素

单点修好后,再排查几类会在高峰放大问题的写法。

  • N+1 查询:订单明细逐条去查商品名、券信息,一笔结账打出几十条 SQL。改成按 ID 集合一次性批量查询;
  • 热点数据没缓存:门店信息、商品、会员等级每次结账都查库,可对这类低频变更数据加缓存;
  • 报表和交易共用库:月底对账、营业报表这类大查询和在线结账跑在同一个库上,高峰互相抢资源。报表应走只读库或异步离线统计。

我们把 N+1 改成批量、报表迁到只读库后,高峰期单笔结账的 SQL 数量从十几条降到个位数,数据库压力进一步下降。

七、踩坑记录

坑1:只盯接口耗时,不抓慢查询。 看到接口慢就想加机器、加缓存,结果根因是全表扫描。务必先开慢查询日志、用 EXPLAIN 定位到具体 SQL。

坑2:索引加了却不生效。 隐式类型转换、列上用函数、不满足最左前缀,都会让索引形同虚设。改完一定要用 EXPLAIN 复核执行计划。

坑3:忽略回表开销。 SELECT * 即使走了索引还要回表取整行,高并发下开销不小;只需要少量列时用覆盖索引。

坑4:事务里夹远程调用。 事务时间被网络抖动拉长,连接和行锁被长时间占住,极易在高峰引发连锁等待。事务边界要尽量短。

坑5:报表和在线交易共用一个库。 月底对账大查询一跑,前台收银就跟着卡。读写分离、报表异步化,把在线交易和分析负载隔开。

结语

门店收银高峰期卡顿,排查路径其实很固定:先量化耗时、再抓慢查询、确认索引生效、检查连接池与锁、最后清理 N+1 和报表争抢。这套顺序走下来,瓶颈基本都能定位到具体的几条 SQL。

这次整改后,三家店在随后的饭点高峰平稳运行,结账 P95 稳定在 400 毫秒上下,排队明显减少。收银是门店生意的“最后一米”,卡顿顾客感知最直接,建议把慢查询监控和结账耗时告警长期开着,别等顾客排起队才回头排查。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 导读
  • 一、先说背景,交代下环境
  • 二、第一步:先量化,别凭感觉说“卡”
  • 三、第二步:用 EXPLAIN 抓住全表扫描
  • 四、第三步:确认索引为什么没生效
  • 五、第四步:检查连接池和锁
  • 六、第五步:找高峰期的放大因素
  • 七、踩坑记录
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档