文档目录

7.2 配套代码:减少工作的五种模式

对应小节:7.2 减少工作 每种模式给「反例 → 正例 → 验证方法」,以及一份可自动扫描的审查清单。

一、❶ 批处理:N 次变 1 次

// ❌ 反例:N 次往返
suspend fun loadOrdersBad(ids: List<Long>): List<Order> =
    ids.map { repo.findById(it) ?: error("not found") }      // N 次查询

// ✅ 正例 1:批量查询(= any)
suspend fun loadOrdersGood(ids: List<Long>): List<Order> = withContext(Dispatchers.IO) {
    ds.connection.use { c ->
        c.prepareStatement("select id, user_id, amount from orders where id = any(?)").use { ps ->
            ps.setArray(1, c.createArrayOf("bigint", ids.toTypedArray()))
            ps.executeQuery().use { rs ->
                buildList { while (rs.next()) add(rs.toOrder()) }
            }
        }
    }
}

// ✅ 正例 2:分批(避免单次 IN 列表过长,也便于控制内存)
suspend fun loadOrdersBatched(ids: List<Long>, batchSize: Int = 500): List<Order> =
    ids.chunked(batchSize).flatMap { chunk -> loadOrdersGood(chunk) }

// ✅ 正例 3:并行分批(IO 密集,且下游能承受)
suspend fun loadOrdersParallel(ids: List<Long>, batchSize: Int = 500): List<Order> =
    coroutineScope {
        ids.chunked(batchSize)
            .map { chunk -> async(Dispatchers.IO) { loadOrdersGood(chunk) } }
            .awaitAll()
            .flatten()
    }

验证方法:

// 统计"每个请求执行了多少次查询"
val before = queryCounter.get()
loadOrdersBad(ids)
val badQueries = queryCounter.get() - before      // ≈ ids.size

queryCounter.reset()
loadOrdersGood(ids)
val goodQueries = queryCounter.get()              // = 1

二、❷ 消除 N+1

// ❌ 反例:典型的 N+1(循环里查关联对象)
suspend fun listOrdersBad(userId: Long): List<OrderView> {
    val orders = orderRepo.findByUserId(userId)          // 1 次
    return orders.map { order ->
        val user = userRepo.findById(order.userId)       // N 次!
        OrderView(order.id, user.name, order.amount)
    }
}

// ✅ 正例 1:批量预取 + 内存组装
suspend fun listOrdersGood(userId: Long): List<OrderView> {
    val orders = orderRepo.findByUserId(userId)                  // 1 次
    val userIds = orders.map { it.userId }.distinct()
    val users = userRepo.findByIds(userIds).associateBy { it.id } // 1 次
    return orders.map { OrderView(it.id, users[it.userId]?.name ?: "?", it.amount) }
}

// ✅ 正例 2:JOIN(同一个库时最省)
suspend fun listOrdersJoin(userId: Long): List<OrderView> = withContext(Dispatchers.IO) {
    ds.connection.use { c ->
        c.prepareStatement("""
            select o.id, o.amount, u.name
            from orders o join users u on u.id = o.user_id
            where o.user_id = ?
        """.trimIndent()).use { ps ->
            ps.setLong(1, userId)
            ps.executeQuery().use { rs ->
                buildList { while (rs.next()) add(OrderView(rs.getLong(1), rs.getString(3), rs.getLong(2))) }
            }
        }
    }
}

验证:pg_stat_statements 里那条子查询的 calls 应该从「与 QPS 成正比」变成「与 QPS 同阶」。

三、❸ 提前剪枝

// ❌ 反例 1:先算再判断
suspend fun reportBad(user: User, data: Data): Report {
    val full = generateFullReport(data)          // 很贵(800ms)
    return if (user.isAdmin) full else full.summary()
}

// ✅ 正例 1:先判断
suspend fun reportGood(user: User, data: Data): Report =
    if (user.isAdmin) generateFullReport(data)
    else generateSummary(data)                    // 便宜(50ms)

// ❌ 反例 2:先加载数据再鉴权(安全问题 + 性能问题)
suspend fun getOrderBad(userId: Long, orderId: Long): Order {
    val order = orderRepo.findById(orderId) ?: throw NotFound()    // 先加载(贵)
    if (order.userId != userId) throw Forbidden()                  // 后鉴权
    return order
}

// ✅ 正例 2:先鉴权(既安全又快)
suspend fun getOrderGood(userId: Long, orderId: Long): Order {
    // 轻量查询:只取 owner,或者直接按 (id, userId) 联合查询
    val order = orderRepo.findByIdAndUserId(orderId, userId)
        ?: throw NotFoundOrForbidden()            // 不泄露"存在但无权限"
    return order                                  // 只有鉴权通过才加载完整对象
}

// ❌ 反例 3:空集合也去查询
suspend fun loadBad(ids: List<Long>) = repo.findByIds(ids)        // ids 为空时白跑一次

// ✅ 正例 3:先判断
suspend fun loadGood(ids: List<Long>) =
    if (ids.isEmpty()) emptyMap() else repo.findByIds(ids)

验证方法:

// 用一个"昂贵的下游"测量:无权限的请求是否还调用它
val called = AtomicBoolean(false)
val mockRepo = object : OrderRepo {
    override suspend fun findById(id: Long): Order {
        called.set(true)          // 标记"昂贵调用发生了"
        return heavyLoad(id)
    }
}

getOrderGood(nonOwnerId, orderId)
assert(!called.get()) { "鉴权失败时不应加载数据" }   // ✅ 应该通过

四、❹ 避免重复序列化

// ❌ 反例 1:对象 → JSON → Map → JSON
fun processBad(order: Order, mapper: ObjectMapper): String {
    val json = mapper.writeValueAsString(order)                 // 序列化 1
    val map = mapper.readValue(json, Map::class.java)           // 反序列化
    map.remove("secret")
    return mapper.writeValueAsString(map)                       // 序列化 2
}

// ✅ 正例 1:直接在强类型对象上处理
fun processGood(order: Order, mapper: ObjectMapper): String =
    mapper.writeValueAsString(order.copy(secret = null))        // 序列化 1

// ❌ 反例 2:同一份数据序列化多次(比如打日志 + 返回)
fun handleBad(order: Order): Response {
    val json = mapper.writeValueAsString(order)
    log.info("response: {}", json)                              // 序列化 1(且打日志)
    return Response(json)                                       // 复用
}

// ✅ 正例 2:日志用参数化(不拼接),或者只在需要时序列化
fun handleGood(order: Order): Response {
    if (log.isDebugEnabled) log.debug("order id={}", order.id)  // 不序列化
    return Response(mapper.writeValueAsString(order))           // 序列化 1(只此一次)
}

// ❌ 反例 3:服务间透传时反复转换
fun forwardBad(payload: ByteArray): ByteArray {
    val obj = mapper.readValue(payload)                          // 反序列化
    obj.metadata = enrich(obj)                                   // 处理
    return mapper.writeValueAsBytes(obj)                         // 再序列化
}

// ✅ 正例 3:如果只是透传,用二进制流(或明确只解析需要的字段)

五、❺ 减少系统调用

// ❌ 反例 1:热路径同步日志
get("/orders/{id}") {
    log.info("handling order id={}", call.parameters["id"])     // 每次请求都写磁盘
    // ...
}

// ✅ 正例 1:异步 appender(配置层面)+ 降低级别
// logback.xml: <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
//                  <neverBlock>true</neverBlock>

// ❌ 反例 2:逐条 flush
fun writeBad(records: List<Record>) {
    records.forEach { r ->
        file.write(r.toString())
        file.flush()                                             // 每次都 flush!
    }
}

// ✅ 正例 2:批量写 + 一次 flush
fun writeGood(records: List<Record>) {
    val sb = StringBuilder()
    records.forEach { sb.append(it).append('\n') }
    file.write(sb.toString())
    file.flush()                                                 // 只 flush 一次
}

// ❌ 反例 3:每个请求建连
suspend fun queryBad(sql: String) = DriverManager.getConnection(URL).use { /* ... */ }

// ✅ 正例 3:用连接池(第 7.6 节)
suspend fun queryGood(sql: String) = withContext(Dispatchers.IO) {
    dataSource.connection.use { /* ... */ }
}

六、审查清单脚本

# tools/review-reduce-work.py <SRC_DIR>
"""
扫描代码,检出「可能有减少工作空间」的模式。

注意:这是启发式扫描,会误报——它的价值是提醒你去检查。
"""
import pathlib
import re
import sys

PATTERNS = [
    ("❶ N+1 嫌疑:循环里的单条查询", r"\.(map|forEach)\s*\{[^}]*\.(findById|queryOne|selectOne|get)\(",
     "改成批量查询(where id = any(?))"),
    ("❶ 循环里的数据库调用", r"for\s*\([^)]*\)\s*\{[^}]*\.(query|execute|find)",
     "考虑批处理"),
    ("❸ 空集合检查缺失", r"fun\s+\w+\(ids:\s*List<[^>]+>\)[^{]*\{\s*\n\s*(?!.*isEmpty)[^\n]*",
     "先判断 ids.isEmpty() 再查询"),
    ("❹ 重复序列化嫌疑", r"writeValueAsString\([^)]*\)[\s\S]{0,200}?readValue",
     "避免 JSON → Map → JSON 的来回转换"),
    ("❺ 热路径日志", r"(get|post)\s*\(\s*\"[^\"]+\"\s*\)\s*\{[\s\S]{0,200}?log\.(info|debug)",
     "降低级别或用异步 appender"),
    ("❺ 逐条 flush", r"\.(write|append)\([^)]*\)[\s\S]{0,50}?\.flush\(\)",
     "改成批量写 + 一次 flush"),
    ("正则重复编译", r"Regex\s*\(\s*\"\"\"",
     "检查是否应该提到顶层常量"),
    ("重复查询同一数据", r"(\w+Repo|\w+Repository)\.(findById|get)\([^)]*\)[\s\S]{0,500}?\2\.\1",
     "同一请求内重复查询 → 缓存到局部变量"),
]


def scan_file(path):
    try:
        text = path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return []
    hits = []
    for label, pattern, advice in PATTERNS:
        for m in re.finditer(pattern, text, re.MULTILINE):
            # 找行号
            line_no = text[:m.start()].count("\n") + 1
            hits.append((label, line_no, advice))
    return hits


def main(src_dir):
    root = pathlib.Path(src_dir)
    files = list(root.rglob("*.kt")) + list(root.rglob("*.java"))
    total = 0

    print("═" * 84)
    print(f"「减少工作」审查:{src_dir}")
    print("═" * 84)
    print()

    for f in files:
        hits = scan_file(f)
        if hits:
            total += len(hits)
            print(f"📄 {f.relative_to(root)}")
            for label, line_no, advice in hits:
                print(f"   L{line_no:<5} {label}")
                print(f"          → {advice}")
            print()

    print("═" * 84)
    if total == 0:
        print("✅ 未检测到明显模式(但这不代表没有问题——启发式扫描有盲区)")
    else:
        print(f"⚠️  检出 {total} 处待检查点")
        print()
        print("提醒:")
        print("  ① 这些是「嫌疑」不是「问题」——需要人工确认")
        print("  ② 优先检查 N+1 与循环里的数据库调用(收益最大)")
        print("  ③ 用 queryCounter 之类的指标验证,而不是靠看代码")
    print("═" * 84)


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "src/main/kotlin")

七、动手改造

改动 观察什么
对 Lab 6 的 /slow-db 用 review-reduce-work.py 扫描 能检出 N+1 嫌疑
用 queryCounter 量化优化前后的查询次数 得到最直接的证据
给 /slow-cpu 的 Regex 做「提到顶层」的改动 用第 6.7 节的 RegexDemo 验证收益
把「先鉴权再加载」应用到你的真实接口 既提升性能又加固安全
在真实项目上跑审查脚本 看有多少误报(理解启发式扫描的局限)

八、这段代码的局限

  • review-reduce-work.py 是正则扫描:会大量误报(比如 map { ... findById(...) } 可能是在批量处理,也可能真的有 N+1);也会漏报(跨文件、跨方法的模式看不出来)。
  • 收益需要用指标验证:脚本只能提示"去看看",真正的判断要靠 queryCounter、pg_stat_statements、火焰图这些工具。
  • 批处理有前提:where id = any(?) 在 ID 数量很大时可能变慢(IN 列表过长);需要分批(chunked)。
  • 「先鉴权」要注意错误语义:要避免"404 vs 403"泄露资源是否存在(示例里用了 NotFoundOrForbidden)。