大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
依赖缓存
这一课你将能做到
- 说明为什么每次 CI 重下 Maven/npm 依赖又慢又费分钟数,以及缓存命中/未命中分别意味着什么
- 用
actions/setup-java的cache: maven与actions/setup-node的cache: npm为本站 monorepo 两侧加速 - 读懂
hashFiles('…/pom.xml'|lock)作 cache key 的失效逻辑,并知道哪些目录不该缓存
为什么要学
干净 runner 上没有你本机积攒的 ~/.m2 仓库或 npm 缓存。没有缓存时,每次 npm ci、每次 Maven 解析都像「第一次下载依赖宇宙」,日志刷屏、分钟数飙升。
缓存不是魔法:key 对了才命中;依赖变了应主动失效,否则会用到旧依赖,出现「本地更新了、CI 却像没更新」的诡异现象。
本课学会官方推荐的 setup-* 内置缓存,并理解底层 actions/cache 的 key 思维——后者在缓存任何非依赖目录(如构建工具链)时都要用。
先建立图像
第一次跑 CI
下载依赖 → 按key写入缓存后端
构建…
第二次跑(pom / lock 未变)
同一 key 命中 → 恢复缓存 → 下载很少 → 构建更快
pom / package-lock 变了
key 变了 → 未命中 → 重新下载 → 写入新 key
人话:缓存像快递柜格子。锁文件没变就开同一格取包裹;锁文件变了就换新格子,避免拿到过期货。
| 工具 | 常见缓存什么 | 推荐入口 |
|---|---|---|
| Maven | ~/.m2/repository | setup-java → cache: maven |
| npm | npm 全局缓存目录 | setup-node → cache: npm + lock 路径 |
| 通用 | 任意 path | actions/cache@v4 |
跟着做
步骤 1:先看「无缓存」基线(可选但推荐)
临时用最简 Node job(无 cache)跑一次,记下 npm ci 耗时。有基线才知道后面省了多少——否则容易主观感觉「好像快了」。
步骤 2:为 platform 打开 npm 缓存
jobs:
platform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: platform
steps:
- uses: actions/checkout@v4
- name: 安装 Node 并缓存 npm
uses: actions/setup-node@v4
with:
node-version: "20"
# 让 setup-node 按 lockfile 管理缓存
cache: npm
cache-dependency-path: platform/package-lock.json
- run: npm ci
第二次跑:Cache 相关 step 应出现 cache hit,npm ci 时间明显缩短。
步骤 3:为 backend 打开 Maven 缓存
jobs:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- name: 安装 JDK 并缓存 Maven 仓库
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- run: |
chmod +x mvnw
./mvnw -B verify
cache: maven 内部按 pom 哈希计算 key 并缓存 ~/.m2/repository。应看到依赖下载显著减少。
步骤 4:读懂手写 actions/cache(知其所以然)
- name: 手写 Maven 缓存示例
uses: actions/cache@v4
with:
path: ~/.m2/repository
# pom 变 → key 变 → 自动换新缓存
key: maven-${{ runner.os }}-${{ hashFiles('backend/pom.xml') }}
restore-keys: |
maven-${{ runner.os }}-
三个字段的含义:
path:要缓存/恢复的目录key:精确匹配才命中的身份证restore-keys:前缀回退——精确 key 未命中时,尝试同前缀较旧缓存,仍可能加速大部分下载
日常优先用 setup-* 内置;手写用于特殊目录(如 Gradle、自定义工具链)。
步骤 5:故意让缓存失效
改 platform/package-lock.json(加一个依赖)后推送:应看到 miss 新 key,跑完生成新缓存。笔记写:「锁文件是缓存的身份证。」
步骤 6:列出「不要缓存」清单
- 不要把
backend/target/、platform/.next/当依赖缓存——体积大、易脏、难保证正确性;跨 job 传递用 artifact - 不要缓存含密钥的目录或文件
- 不要假设命中率 100%:配额、驱逐、分支作用域都会导致 miss,workflow 必须在 miss 时能完整安装
讲清楚要点
- 因为 runner 是干净的,所以缓存换的是「重复下载时间」,不是「跳过测试」——测试照样要跑。
- 因为依赖图由锁文件/pom 决定,所以 cache key 应包含它们的哈希——依赖变则自动失效。
- 因为 setup-node/setup-java 已封装常见路径,所以优先用内置
cache:——少踩 path 写错的坑。 - 因为 monorepo 有多个 lockfile,所以
cache-dependency-path必须指到正确子路径——根目录乱指会错缓存或永远 miss。 - 因为缓存可能被驱逐(10GB 上限、7 天未用回收),所以 miss 时完整安装必须仍能成功——缓存是加速项,不是正确性来源。
- 因为构建产物易脏,所以
target/、.next/用 artifact 而非长期 cache——缓存面向可重算的输入,artifact 面向确定的结果。
你可能会懵的地方
-
「命中了为什么有时仍然有点慢?」 恢复缓存要下载解压,测试本身也要时间;加速的是依赖下载段,不是把
next build变成零。 -
「缓存了还要
npm ci吗?」 要。常见做法是缓存 npm 的缓存目录,npm ci再据此快速安装并保证与 lock 一致。直接缓存整个node_modules更易脏(原生模块、平台差异),不建议入门用。 -
「restore-keys 会不会恢复到错误依赖?」 可能恢复「较旧但不精确」的缓存,再由包管理器补齐增量。精确 key 命中最理想,restore-keys 是兜底提速。
-
「PR 分支上生成的缓存,别的分支能用吗?」 缓存有分支作用域:默认分支创建的缓存所有分支可用;feature 分支创建的缓存只对该分支及其派生分支可见。所以「在 PR 里第一次跑永远 miss」可能是分支隔离所致,不是配置错。
-
「私有包注册表怎么办?」 缓存只存已下载内容,不替代认证;仍需 secret 登录 registry 后再安装。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 每次 CI 无缓存重下全部依赖 | 慢、费分钟 | setup-* 开 cache |
key 写死成 maven-v1 永不变 | 依赖升级 CI 仍用旧缓存 | key 含 hashFiles(pom/lock) |
缓存整个 target/ 当加速 | 脏产物、难复现 | 需要跨 job 用 upload-artifact |
cache-dependency-path 指到仓库根 | 永远 miss 或错命中 | 指 platform/package-lock.json |
| 依赖缓存保正确性(删了 ci 只靠缓存) | 环境一变就崩 | miss 时必须能完整安装 |
卡点急救
- 缓存从不命中:按序查——key 是否包含
hashFiles()且路径写对(monorepo 注意子目录前缀);是否在无缓存可见的分支作用域里;GitHub Actions 页 run 详情的 Post job 段(保存缓存在 job 结束后执行)是否报错。 - 缓存命中率突然归零:可能是旧缓存被驱逐(总上限 10GB、7 天未访问回收)或锁文件大幅变更换了 key。属预期行为,确认全量安装能成功即可。
- 「明明没改依赖,为什么这次慢了」:大概率是 miss 后全量重下(缓存被驱逐或首次在该分支跑)。偶发一次无碍;若频繁发生,检查 key 是否把易变内容(如
${{ github.run_id }})误写进去了。
面试常问
-
CI 里依赖缓存的 key 应该怎么设计? 核心原则:key 反映「缓存内容的输入」。依赖缓存用包管理器锁文件(或 pom)的哈希做 key,如
maven-${{ runner.os }}-${{ hashFiles('backend/pom.xml') }},并配restore-keys前缀兜底;操作系统维度也要进 key,因为原生二进制跨平台不通用。避免把 run 编号这类每次必变的值写进 key——那等于永远不缓存。 -
缓存会带来什么正确性风险?如何规避? 风险是「拿到过期缓存还以为依赖是新的」。规避:① key 绑定锁文件哈希,依赖变更自动失效;② 缓存只覆盖可重算的输入(依赖包),安装步骤照跑(如
npm ci)以对齐 lock;③ 把缓存当纯加速——miss 时流程必须完整可用;④ 构建产物不用缓存传递,用 artifact。 -
npm ci 和缓存怎么配合最稳? 缓存 npm 全局缓存目录(setup-node 的
cache: npm自动做),安装仍用npm ci——它严格按package-lock.json安装,缓存命中时只是下载更快,版本精确性不受影响。而缓存node_modules会让安装结果依赖上次环境,容易引入难以复现的漂移。
自测三问
- 为什么说缓存未命中时 workflow 仍必须能成功?
setup-node的cache: npm通常还要配什么字段?monorepo 里写什么值?- 举两样不该当作依赖缓存的目录,正确的替代手段是什么?
小结一句话
用锁文件/pom 的哈希做 key,优先 setup- 内置缓存;缓存加速下载,不替代安装与测试。*
延伸
- Caching dependencies to speed up workflows
- actions/cache
- 下一课:构建 Spring Boot——把这套骨架落到本站的 Maven + Spring Boot backend 上。