RSS
菜单
全部文章快讯开发科技深度热点

第 71 章 开发规范类踩坑

内容摘要

开发规范看起来是"软要求",但每一条规范背后都是一次真实的线上事故。本章汇总日常 CRUD、事务使用、ORM 写法、DDL 与权限管控中最容易踩的 11 个规范类坑,覆盖 SELECT *、隐式类型转换、N+1、大事务、大表 DDL 等高频问题。所有 SQL 示例基于 MySQL 8.0 语法。读完你能在 Code Review 阶段就拦住那些"看起来能跑、上线就炸"的写法。

📖 本篇简介:开发规范看起来是"软要求",但每一条规范背后都是一次真实的线上事故。本章汇总日常 CRUD、事务使用、ORM 写法、DDL 与权限管控中最容易踩的 11 个规范类坑,覆盖 SELECT *、隐式类型转换、N+1、大事务、大表 DDL 等高频问题。所有 SQL 示例基于 MySQL 8.0 语法。读完你能在 Code Review 阶段就拦住那些"看起来能跑、上线就炸"的写法。

本章适合已经写完业务代码、准备上线前做自我检查的工程师,也适合 Tech Lead 拿来做 Code Review Checklist。建议配合第 44、45、46、47、53、54、55 章一起阅读,那里有更细的索引与事务专题。

目录

  • 坑 1:随手写 SELECT 拉出整张大表
  • 坑 2:事务里夹带 RPC 与外部 HTTP 调用
  • 坑 3:把事务开成"长事务"不拆分
  • 坑 4:WHERE 字段隐式类型转换导致索引失效
  • 坑 5:LIKE 左前缀模糊查询把索引废掉
  • 坑 6:批量 UPDATE 或 DELETE 不带 LIMIT
  • 坑 7:ORM 循环查库制造 N+1
  • 坑 8:连接用完不归还,连接池被占满
  • 坑 9:大表 DDL 不带 ALGORITHM 与 LOCK
  • 坑 10:开发账号直连生产库执行写操作
  • 坑 11:字段名见名不知意,上线三个月没人敢改
  • 常见注意事项:规范不是凭空定的
  • 工具链:把规范焊死在流程里
  • 一句话总结
  • 速查表:开发规范红线一览
  • 自测:你能在 Code Review 拦住它们吗

坑 1:随手写 SELECT 拉出整张大表

场景 / 错误示范:

-- 后台管理系统:运营要"看一下最近的订单"
SELECT * FROM orders
WHERE user_id = 10086;
-- 错误:SELECT * 把订单表所有列(含 2KB 的收货地址快照、JSON 备注)全拉回应用
-- 没有 LIMIT,订单量大时一次返回几十万行

为什么是坑:SELECT * 会把聚簇索引里所有列、甚至溢出页的 TEXT/BLOB 全部读出来,回表次数与 IO 被放大数倍;没有 LIMIT 时,网络包与应用内存都会被瞬间打爆。慢查询日志里 90% 的大 SQL,第一行都是 SELECT *。更隐蔽的是,将来加列后老 SQL 结果集会悄悄膨胀,性能问题会在无人改动的情况下突然爆发。

正确做法:

-- 正确:只取需要的列,强制分页
SELECT order_id, status, amount, created_at
FROM orders
WHERE user_id = 10086
ORDER BY created_at DESC
LIMIT 20;                 -- 后台列表一律分页,单页不超过 100 行

加分点:团队规范里应明令禁止线上代码出现 SELECT *;如果是联表查询,列名必须写 t1.col, t2.col,避免将来加列后结果集悄悄膨胀。

坑 2:事务里夹带 RPC 与外部 HTTP 调用

场景 / 错误示范:

-- 一段典型的"下单"伪代码(Python + pymysql 风格,本示例基于 MySQL 8.0)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
-- 下面这步是外部支付网关 RPC,平均耗时 800ms,超时还要重试
call_payment_gateway(1, 100);
INSERT INTO order_log(user_id, amount) VALUES (1, 100);
COMMIT;

为什么是坑:事务期间持有的行锁、undo log、MVCC 版本链都会一直挂着,外部 RPC 一旦超时变慢,整个事务被拖到几秒甚至几十秒,阻塞其他请求。高并发下锁等待堆积,连接池被打满,整站雪崩。

正确做法:

-- 正确:事务内只做"短、快、纯数据库"的操作
-- 1) 事务内扣库存、写订单主表
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
INSERT INTO order_main(user_id, amount, status) VALUES (1, 100, 'PROCESSING');
COMMIT;
-- 2) 事务提交后,再调用外部支付网关
-- 3) 支付结果通过回调/消息队列异步落库,更新订单状态

加分点:事务内严禁出现 HTTP/RPC/发 MQ/写 Redis 之外的任何远程调用;事务目标时长应控制在 200ms 以内,超过就要考虑拆分。

坑 3:把事务开成"长事务"不拆分

场景 / 错误示范:

-- 运营一次性把 10 万条老订单从'PAID'改成'ARCHIVED'
BEGIN;
UPDATE orders SET status = 'ARCHIVED'
WHERE status = 'PAID' AND created_at < '2024-01-01';
-- 这条 UPDATE 跑了 40 秒,期间持锁 10 万行
-- 期间 undo log 暴涨,purge 线程无法清理旧版本,从库延迟跟着飙升
COMMIT;

为什么是坑:长事务持锁时间长、阻塞面广,还会让 purge 线程无法回收旧版本,undo log 表空间只增不减,最终拖慢全库。从库回放时同样会卡,主从延迟从毫秒涨到几分钟。

正确做法:

-- 正确:分批小事务,每批 500~1000 行,批与批之间 sleep 一下
-- Python 伪代码(本示例基于 MySQL 8.0)
for batch in range(200):
    cur.execute("""
        UPDATE orders SET status='ARCHIVED'
        WHERE status='PAID' AND created_at < '2024-01-01'
        LIMIT 500
    """)
    conn.commit()
    time.sleep(0.2)   -- 给主从复制和 purge 留喘息时间

加分点:线上应开启 innodb_print_all_deadlocks 与 information_schema.innodb_trx 监控,事务时长超过 5 秒就告警。

坑 4:WHERE 字段隐式类型转换导致索引失效

场景 / 错误示范:

-- user 表 phone 字段是 VARCHAR(32),建了索引
CREATE INDEX idx_phone ON user(phone);

-- 错误写法:应用层把 phone 当数字传进来
SELECT * FROM user WHERE phone = 13800001111;
-- MySQL 被迫把 phone 列隐式转成 DOUBLE 再比较:WHERE CAST(phone AS DOUBLE) = 13800001111
-- 函数包裹列 = 索引失效,全表扫描

为什么是坑:当列类型与传入参数类型不一致时,MySQL 会按"数值优先"规则对列做 CAST,相当于在索引列上套了函数,B+ 树索引直接废掉。VARCHAR 与 BIGINT 混用在订单号、手机号上最常见,数据量一大就是慢查询。

正确做法:

-- 正确:传入参数类型与列严格一致,手机号用字符串
SELECT * FROM user WHERE phone = '13800001111';

-- 建表时就把这类字段统一成 VARCHAR
-- phone VARCHAR(32) NOT NULL,
-- order_no VARCHAR(32) NOT NULL,

加分点:Code Review 时凡是 WHERE 字符串列 = 数字 的写法一律打回;ORM 框架也要检查参数绑定类型,避免 Jackson 把字符串反序列化成 Long。

坑 5:LIKE 左前缀模糊查询把索引废掉

场景 / 错误示范:

-- 商品搜索:用户输入关键词"手机",希望搜商品标题
SELECT * FROM product
WHERE title LIKE '%手机%';
-- 错误:前后都带 %,B+ 树索引无从定位起点,退化为全表扫描

为什么是坑:B+ 树索引按前缀有序,LIKE '手机%' 还能走索引区间扫描,LIKE '%手机%' 必须逐行比较,千万级表就是慢查询。商品、文章、日志搜索一旦这么写,数据库很快被打挂。

正确做法:

-- 正确 1:业务上能改前缀就改前缀
SELECT * FROM product WHERE title LIKE '手机%';

-- 正确 2:真正的全文搜索交给 ES / OpenSearch,MySQL 只做主键回查
-- 搜索请求先打 ES,命中 product_id 列表后再回 MySQL 取详情
SELECT product_id, title, price FROM product
WHERE product_id IN (101, 102, 103);

加分点:MySQL 8.0 虽然支持全文索引 FULLTEXT 与 ngram 分词,但中文模糊搜索的召回与排序远不如专用搜索引擎,生产环境不要硬扛。如果只是后台管理系统偶尔按手机号、订单号前缀查一查,正常 B+ 树索引就够了;一旦面对用户输入的关键词模糊搜索,毫不犹豫上 ES。

坑 6:批量 UPDATE 或 DELETE 不带 LIMIT

场景 / 错误示范:

-- 数据清理脚本:删除 90 天前的浏览记录
DELETE FROM browse_log
WHERE updated_at < '2024-07-01';
-- 错误:一次删了 800 万行,产生巨大 undo/redo,主从延迟 30 分钟
-- 期间该表上的所有写入全部被阻塞

为什么是坑:批量 DELETE/UPDATE 是一个大事务,锁行数多、binlog 事件大、主从回放慢;更可怕的是一旦 WHERE 条件写错(比如少写一个 AND),就是全表锁、全表改、全表删,连回滚都来不及。

正确做法:

-- 正确:小批量循环,每批带 LIMIT,控制单次影响行数
-- 本示例基于 MySQL 8.0
DELETE FROM browse_log
WHERE updated_at < '2024-07-01'
LIMIT 1000;
-- 应用层反复执行直到 affected_rows = 0,每批之间 sleep 200ms

加分点:线上执行任何不带 LIMIT 的写操作前,先用 SELECT COUNT(*) 评估影响行数,超过 1 万行必须走分批脚本,禁止直接在客户端跑。

坑 7:ORM 循环查库制造 N+1

场景 / 错误示范:

# Python + SQLAlchemy 伪代码,本示例基于 MySQL 8.0
users = session.query(User).limit(100).all()
for u in users:
    # 每个用户都再发一次 SQL 查订单
    orders = session.query(Order).filter_by(user_id=u.id).all()
    print(u.name, len(orders))
# 实际发出 1 + 100 = 101 次 SQL
# 列表分页 100 条,接口 QPS 100 时,数据库瞬间承受 1 万次 QPS

为什么是坑:N+1 是 ORM 时代最经典的性能杀手,单次接口看似没问题,一上量就把数据库连接池打满。慢查询日志里同一形态的 SQL 重复出现几百次,CPU 都耗在网络往返上。

正确做法:

# 正确 1:用 JOIN 一次取齐
users = session.query(User, Order).join(Order, Order.user_id == User.id).limit(100).all()

# 正确 2:用 IN 批量取
user_ids = [u.id for u in users]
orders = session.query(Order).filter(Order.user_id.in_(user_ids)).all()
# 在内存里按 user_id 分组拼装
from collections import defaultdict
by_user = defaultdict(list)
for o in orders:
    by_user[o.user_id].append(o)

加分点:Code Review 看到 for 循环里出现数据库查询,一律要求改成批量;ORM 开启 echo=True 打印 SQL,压测前先数一下 SQL 条数。

坑 8:连接用完不归还,连接池被占满

场景 / 错误示范:

# Python 伪代码(pymysql),本示例基于 MySQL 8.0
def query_user(uid):
    conn = pymysql.connect(host='127.0.0.1', user='r', password='x', db='app')
    cur = conn.cursor()
    cur.execute('SELECT * FROM user WHERE id = %s', (uid,))
    row = cur.fetchone()
    # 错误:忘记 conn.close(),也没写 finally
    return row
# 高并发下,连接不断泄漏,MySQL 端 Threads_connected 飙升到 max_connections 后,新连接全部报错

为什么是坑:MySQL 每个连接对应一个线程,连接数有上限(默认 151)。连接泄漏后,新请求拿不到连接,接口全部报 "Too many connections",整个服务对外不可用。

正确做法:

# 正确:用上下文管理器 / 连接池,保证连接一定归还
from contextlib import closing

def query_user(uid):
    # 假设 pool 是预先建好的连接池(如 DBUtils、sqlalchemy.pool)
    conn = pool.get_conn()
    try:
        with closing(conn.cursor()) as cur:
            cur.execute('SELECT * FROM user WHERE id = %s', (uid,))
            return cur.fetchone()
    finally:
        pool.put_conn(conn)   # 无论异常与否都归还

加分点:线上监控 Threads_connected 与连接池等待队列长度;开发规范里禁止业务代码自己 pymysql.connect,必须走统一封装的数据源。

坑 9:大表 DDL 不带 ALGORITHM 与 LOCK

场景 / 错误示范:

-- 一张 5000 万行的订单表,运营要求加一列 remark
ALTER TABLE orders ADD COLUMN remark VARCHAR(255);
-- 错误:MySQL 默认行为可能 COPY 整张表,期间锁表 30 分钟
-- 业务写入全部 hang 住,告警刷屏

为什么是坑:早期 MySQL 的 ALTER TABLE 是 COPY 方式,整表锁读写。8.0 虽然默认走 INSTANT/INPLACE,但加列类型、加全文索引、改列类型仍可能触发 COPY 或 metadata lock。不显式指定 ALGORITHM 和 LOCK,DBA 无法预判风险。

正确做法:

-- 正确:显式声明算法与锁级别,让 MySQL 校验能否满足,不满足直接报错而不是悄悄锁表
ALTER TABLE orders
  ADD COLUMN remark VARCHAR(255) NULL,
  ALGORITHM=INPLACE,    -- 不复制整表;若实际必须 COPY 则直接报错,提前暴露
  LOCK=NONE;            -- 允许并发读写;若实际需要 DDL,则直接报错

-- 大表加索引同理
ALTER TABLE orders ADD INDEX idx_user_status(user_id, status),
  ALGORITHM=INPLACE, LOCK=NONE;

加分点:超过千万行的表,DDL 一律走 pt-online-schema-change 或 gh-ost 这类在线 DDL 工具,业务低峰执行,并且先在从库上观察一遍。

坑 10:开发账号直连生产库执行写操作

场景 / 错误示范:

-- 开发小哥本地 navicat 里,图省事选了"生产"这个连接
-- 本来只想查一下 user_id=1 的信息,手滑把 SELECT 写成了 DELETE
DELETE FROM user WHERE id = 1;
-- 又手抖没加 WHERE 校验,回车瞬间整表没了

为什么是坑:生产库写权限一旦放到个人手里,任何一次手抖、任何一次脚本 bug 都是不可逆事故。权限应该按"最小可用"原则收敛:开发只读、测试在测试库、生产 DDL 必须走工单 + DBA 执行。

正确做法:

-- 正确 1:生产账号分角色
-- app_rw      应用账号,只允许业务库的 DML
-- ro_readonly 只读账号,开发/DBA 排查用,带 SESSION 级 sql_log_bin=0 权限
-- dba_admin   DDL/高权账号,禁止远程登录,只能跳板机 + 工单

-- 正确 2:强制 SQL 审核平台拦截高危语句
-- DELETE/UPDATE 不带 WHERE、DROP/TRUNCATE、ALTER 大表 -> 直接拒绝

加分点:生产库开启 binlog_rows_query_log_events 与审计日志,谁在什么时间执行了什么 SQL 全部可追溯;开发环境一律用脱敏后的从库快照。

坑 11:字段名见名不知意,上线三个月没人敢改

场景 / 错误示范:

-- 一张订单表,字段名看得人一头雾水
CREATE TABLE t1 (
  id BIGINT PRIMARY KEY,
  a1 VARCHAR(64),    -- 这是啥?订单号?
  a2 INT,            -- 状态?类型?
  a3 DECIMAL(12,2),  -- 金额?运费?
  a4 DATETIME        -- 创建时间?支付时间?
);
-- 半年后接手的开发不敢加索引、不敢改字段,只能不停 SELECT * 盲猜

为什么是坑:命名规范是协作成本的第一道防线。a1/a2/foo/bar 这种名字在 Code Review 阶段看不出问题,三个月后新人接手就是灾难:没人知道字段含义、没人敢动表结构、bug 定位要花几倍时间。

正确做法:

-- 正确:字段名见名知意,统一前缀与命名风格
CREATE TABLE trade_order (
  order_id        BIGINT       PRIMARY KEY,
  order_no        VARCHAR(32)  NOT NULL UNIQUE COMMENT '业务订单号',
  order_status    TINYINT      NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成',
  total_amount    DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '订单总金额,单位元',
  created_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 每张表必须有 created_at / updated_at,每个字段必须 COMMENT

加分点:团队应有统一的命名规范文档:表名用业务下划线小写、布尔字段用 is_/has_、状态字段用 _status、时间字段统一 _at;建表语句评审时把 COMMENT 是否齐全作为硬指标。

常见注意事项:规范不是凭空定的

| 注意点 | 说明 |

|---|---|

| 规范要可执行 | 每条规范必须能翻译成 Code Review 时可勾选的检查项,不能只写"注意性能"这种空话 |

| 规范要有出处 | 每条规范背后至少能对应一次真实事故或一次慢查询案例,新成员培训时讲事故比讲条文有效 |

| 规范要配套工具 | 比如 SQL 审核平台、慢查询日志告警、ORM SQL 打印,把规范"写进工具"而不是"写进 PPT" |

| 规范要随版本演进 | MySQL 8.0 的 INSTANT 加列、直方图、CUBE 等新特性出现后,老规范要及时更新,不能刻舟求剑 |

| 规范要区分场景 | 后台运营脚本与核心交易链路容忍度完全不同,不要一刀切;但"红线"类规范必须一刀切 |

| 规范要有人背 | 每条规范要有 owner,通常是 DBA 或技术委员会,新规范提案要走评审 |

| 规范要有版本号 | 规范文档本身要版本化,重大变更要发通知并组织培训 |

| 规范要考核 | 上线前 SQL 审核拦截率、慢查询占比等指标纳入团队绩效,不然规范就是一纸空文 |

落地建议:每个季度把线上慢查询 TOP20、死锁 TOP10、主从延迟 TOP5 拉出来复盘,看看是哪条规范没被执行,再决定是补规范、补工具还是补培训。

工具链:把规范"焊死"在流程里

规范只写在 Wiki 上是没用的,必须落到工具链上才能强制执行。下面这套组合是中大型团队的常见做法,本示例基于 MySQL 8.0。

-- 1) SQL 审核平台拦截:上线前把 SQL 提交到审核平台,自动扫描
--    规则示例(用正则/抽象语法树实现):
--    - SELECT 是否带 *
--    - DELETE/UPDATE 是否带 WHERE
--    - WHERE 列类型与参数是否一致
--    - 是否出现左前缀 LIKE
--    - 是否在大表上直接 DDL

-- 2) 开启慢查询日志,把超过 200ms 的 SQL 全部采集
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.2;      -- 200ms 即记录
SET GLOBAL log_queries_not_using_indexes = ON;

-- 3) 定期巡检没用过的索引
SELECT OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE INDEX_NAME IS NOT NULL
  AND COUNT_STAR = 0
  AND OBJECT_SCHEMA NOT IN ('mysql','sys','performance_schema')
ORDER BY SUM_TIMER_WAIT DESC;
# 4) 应用层统一封装数据源,禁止业务代码直连
# Python 伪代码:所有 DAO 必须走 BaseDAO,连接从连接池取
# 压测时打印每条 SQL 的耗时,P99 > 50ms 就告警

把这四件事做扎实,规范就不再靠"人自觉",而是靠系统兜底。

下面再给一个最小可落地的巡检 SQL,DBA 可以每周跑一次,把命中结果发给对应业务负责人:

-- 巡检 1:找出最耗时的 TOP10 SQL 形态(基于 performance_schema)
-- 本示例基于 MySQL 8.0
SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1e12 AS total_sec,
       AVG_TIMER_WAIT/1e9 AS avg_ms
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME NOT IN ('mysql','sys')
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

-- 巡检 2:找出从未被使用过的冗余索引
SELECT t.TABLE_SCHEMA, t.TABLE_NAME, t.INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage t
WHERE t.INDEX_NAME IS NOT NULL
  AND t.INDEX_NAME <> 'PRIMARY'
  AND t.COUNT_STAR = 0
  AND t.TABLE_SCHEMA NOT IN ('mysql','sys')
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME;

跑出来的结果不要急着 DROP,先观察一个月,确认业务确实没用到再删。

一句话总结

开发规范类踩坑,本质上是"把个人习惯升级成团队约束"。一个人写代码图省事,后果由自己承担;一个团队没有规范,后果由全公司承担。本章列的 11 个坑,没有任何一条是"技术做不到",全都是"图省事"。记住三句话:线上慢查询大多是 SELECT *、没 LIMIT、没索引;线上事故大多是长事务、批量写、权限过大;线上维护成本爆炸大多是命名随意、缺注释、缺审计字段。把这三句话贴在工位上,能躲开本章 80% 的坑。

速查表:开发规范红线一览

下面这张表是本章 11 个坑的浓缩版,建议直接打印贴在工位上。每一行都对应一次真实事故,不是凭空想象的"最佳实践"。

| 类别 | 红线规范 | 违反后果 |

|---|---|---|

| 查询 | 禁止 SELECT *,必须显式列名 | 回表放大、结果集膨胀 |

| 查询 | 必须 LIMIT,单页 ≤100 行 | 大结果集打满内存 |

| 查询 | 禁止左前缀 LIKE '%xx%' | 索引失效全表扫 |

| 事务 | 事务内禁止 RPC/HTTP/MQ | 长事务锁等待雪崩 |

| 事务 | 单事务目标 <200ms,大改分批 | undo 暴涨、主从延迟 |

| 写入 | UPDATE/DELETE 必须带 WHERE 与 LIMIT | 误删全表、大事务 |

| 类型 | WHERE 列与参数类型严格一致 | 隐式转换索引失效 |

| ORM | 禁止循环里查库,必须 IN/JOIN | N+1 打满连接 |

| DDL | 大表 ALTER 必带 ALGORITHM/LOCK | 锁表事故 |

| 权限 | 生产写权限收敛到应用账号 | 误操作不可挽回 |

| 连接 | 禁止业务裸建连接,走连接池 | 连接泄漏 Too many connections |

| 命名 | 字段必加 COMMENT、见名知意 | 维护成本爆炸 |

| 字符集 | 库/表/连接统一 utf8mb4 | 乱码、emoji 截断 |

| 时间 | 统一 DATETIME 或 TIMESTAMP,注明时区 | 跨时区业务时间错乱 |

| 排序 | ORDER BY 字段必须有索引或走覆盖索引 | filesort 大结果集落盘 |

自测:你能在 Code Review 拦住它们吗

题 1:为什么 WHERE phone = 13800001111(phone 是 VARCHAR)会让索引失效?

<details>

<summary>参考答案</summary>

MySQL 会把字符串列 CAST 成 DOUBLE 再与数字比较,相当于在索引列上套了函数 CAST(phone AS DOUBLE) = 13800001111,B+ 树索引无法用于函数结果,退化为全表扫描。应传入字符串 '13800001111'。

</details>

题 2:事务里调用外部支付网关为什么危险?

<details>

<summary>参考答案</summary>

事务期间持有的行锁、undo log 版本、MVCC 读视图都会一直挂着。外部 RPC 一旦变慢或超时,事务被拖到几秒甚至几十秒,阻塞其他业务线程,高并发下连接池打满、锁等待雪崩。正确做法是事务只做纯数据库短操作,外部调用放到事务提交之后,结果通过回调异步落库。

</details>

题 3:批量 DELETE 为什么必须分批 + LIMIT?

<details>

<summary>参考答案</summary>

一次删几百万行是一个大事务,持锁时间长、binlog 事件巨大、主从回放慢,期间该表写入全部阻塞;一旦 WHERE 写错就是全表事故。分批 DELETE ... LIMIT 1000 循环执行,每批之间 sleep,把大事务拆成小事务,控制锁范围与主从延迟。

</details>

题 4:ORM 的 N+1 问题怎么识别与修复?

<details>

<summary>参考答案</summary>

识别:开启 ORM 的 SQL 日志,压测时发现同形态 SQL 重复出现几百次。修复:把循环里的单条查询改成 IN (id 列表) 一次批量取,或用 JOIN 一次取齐,再在内存中按外键分组拼装。

</details>

题 5:大表 ALTER TABLE 为什么要写 ALGORITHM=INPLACE, LOCK=NONE?

<details>

<summary>参考答案</summary>

显式指定后,MySQL 会校验本次 DDL 是否真的支持在线变更;如果实际必须 COPY 或必须锁表,语句会直接报错而不是悄悄跑几十分钟锁死业务。这是把"静默风险"变成"显式失败"的工程手段,避免大表 DDL 锁表事故。

</details>

题 6:线上一条慢查询把主库 CPU 打满,你的第一反应是什么?

<details>

<summary>参考答案</summary>

第一步不要急着 kill,先用 SHOW PROCESSLIST 与 information_schema.innodb_trx 定位是哪条 SQL、来自哪个应用账号、已经跑了多久;第二步评估影响面,是否需要临时 kill 这条会话止损;第三步拉慢日志看执行计划,确认是索引失效还是数据量突增;第四步补索引或改写 SQL,并把这条 SQL 加进 SQL 审核平台规则,防止再次出现。

</details>

题 7:为什么生产库不允许开发直连?

<details>

<summary>参考答案</summary>

个人账号一旦有生产写权限,任何手抖、任何脚本 bug 都可能造成不可逆数据事故。正确做法是按角色分权:应用账号跑业务 DML、只读账号给开发排查、DDL 高权账号只允许跳板机 + 工单执行;同时开启 binlog 审计,所有操作可追溯。

</details>

下一篇预告:第 72 章 业务建模踩坑

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 71 章 开发规范类踩坑》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。