flaky 测试治理:用工程化方式根治“上次过这次不过“

系列专栏:测试工程师每日一博 · Day 10

日期:2026-07-16

关键词:flaky 测试、测试稳定性、flaky 检测、@Retry、quarantine、Awaitility、SLA

阅读对象:被"CI 莫名其妙挂了,重跑又过了"折磨过无数次的工程师


开篇:flaky 是测试套件的慢性病

Day 7 聊 CI 时,我用四个字给 flaky 定了个性------"fix or remove,没有第三条路"。但那只是政策,没说具体怎么做。Day 10 来兑现承诺。

先讲一个场景:周一早上 9 点,你打开电脑,CI 说"订单创建测试失败"。你点进去,发现 79 个测试里有 1 个红色,失败原因 Expected: <true> but was: <false>。你 rerun 一次,绿了。你过上一个快乐但充满罪恶感的周一。

这不是"Bug 修了",这是 flaky 测试 ------同一个 commit 在同一个环境下跑,有时过有时不过 。它和功能性 Bug 是两种东西:功能性 Bug 是代码错了,要改代码;flaky 是测试本身有非确定性,要么改测试(去掉非确定性),要么改成其它的验证方式。

flaky 是测试套件的慢性病 ------不会立刻致命,但长期不治会把整套测试打回原形。flaky 一旦超过 1%,团队的信任就会塌掉:CI 挂了?哦又是 flaky,直接 rerun 吧。三个月后,CI 失败没有人当回事,这就是测试工程的死亡。

今天这一篇就讲:怎么从 0 检测 flaky、怎么定位根因、@Retry 是解药还是毒药、怎么用工程化方式让 flaky 自动上报、怎么定 SLA 治理。所有方法论基于 Google 那篇 Flaky Tests at Google (Luo et al.,2019)和 Where Do Google's Flaky Tests Come From? (Memon et al.,2020)这两篇工业级研究,我用电商场景跑通一遍。


一、flaky 的五大根因:别只盯一个

很多人排查 flaky 只会"改成 Thread.sleep(2000) 试试",但根因其实有五大类。下表是去掉具体数字后的工业分布(来自 Google 论文 + 我多年观察整理):

# 根因类别 典型表现 占比
1 时间 / 日期依赖 LocalDateTime.now() 进入测试 ~25%
2 并发 / 等待 Thread.sleep(100) 等异步完成 ~24%
3 测试间相互污染 static 字段没清理、数据库没人回滚 ~16%
4 外部环境依赖 网络、文件系统、随机数、时区、host ~21%
5 资源耗尽 / 平台差异 端口冲突、内存、CI 容器架构不同 ~14%

记住这张表。收到 flaky 报告的第一反应不是看错误堆栈,而是先问"这测试踩了上面哪一类坑"。错误堆栈只告诉你"哪里炸了",分类告诉你"为什么会时灵时不灵"。

下面挑前四类(占大头的前 85%)逐个深挖。


二、根因 1:时间依赖------"我用 now(),我认罪"

最常见的 flaky:测试里写了 LocalDateTime.now()

反例:优惠券有效期校验

java 复制代码
// src/main/java/com/example/coupon/Coupon.java
public class Coupon {
    private final String code;
    private final LocalDate validFrom;
    private final LocalDate validTo;

    public boolean isValid() {
        LocalDate today = LocalDate.now();  // ← 罪魁祸首
        return !today.isBefore(validFrom) && !today.isAfter(validTo);
    }
}

// 测试
@Test
@DisplayName("测试优惠券在有效期内")
void shouldReturnTrue_whenWithinRange() {
    Coupon coupon = new Coupon("SAVE10",
            LocalDate.of(2026, 1, 1),
            LocalDate.of(2026, 12, 31));
    assertTrue(coupon.isValid());  // 2026 年内永远过;2027 年 1 月 1 日突然红
}

这个测试什么时候挂? 2026 年 12 月 31 日 23:59 还绿,2027 年 1 月 1 日 00:00 忽然 CI 全红。谁也不会提前想到------直到年关那天事故。

修复:注入 Clock,不要调 now()

java 复制代码
// src/main/java/com/example/coupon/Coupon.java
import java.time.Clock;
import java.time.LocalDate;

public class Coupon {
    private final String code;
    private final LocalDate validFrom;
    private final LocalDate validTo;
    private final Clock clock;  // ← 依赖注入

    public Coupon(String code, LocalDate validFrom, LocalDate validTo, Clock clock) {
        this.code = code;
        this.validFrom = validFrom;
        this.validTo = validTo;
        this.clock = clock;
    }

    public boolean isValid() {
        LocalDate today = LocalDate.now(clock);  // ← 用注入的 Clock
        return !today.isBefore(validFrom) && !today.isAfter(validTo);
    }
}

// 测试:用固定 Clock
@Test
@DisplayName("有效期内的优惠券应有效")
void shouldReturnTrue_whenWithinRange() {
    // 固定时间 2026-06-15
    Clock fixed = Clock.fixed(
        LocalDate.of(2026, 6, 15).atStartOfDay(java.time.ZoneOffset.UTC).toInstant(),
        java.time.ZoneOffset.UTC);

    Coupon coupon = new Coupon("SAVE10",
            LocalDate.of(2026, 1, 1),
            LocalDate.of(2026, 12, 31),
            fixed);
    assertTrue(coupon.isValid());
}

@Test
@DisplayName("过期优惠券应无效")
void shouldReturnFalse_whenExpired() {
    Clock fixed = Clock.fixed(
        LocalDate.of(2027, 1, 1).atStartOfDay(java.time.ZoneOffset.UTC).toInstant(),
        java.time.ZoneOffset.UTC);

    Coupon coupon = new Coupon("SAVE10",
            LocalDate.of(2026, 1, 1),
            LocalDate.of(2026, 12, 31),
            fixed);
    assertFalse(coupon.isValid());
}

关键纪律 :生产代码里永远不直接调 LocalDateTime.now() / Instant.now() ,而是注入 Clock,测试可固定、生产传真实 Clock。这是 flaky 治理的第一原则。

Spring 项目里更好做------Clock 注册成 @Bean,生产用 Clock.systemDefaultZone(),测试 @TestConfiguration 替换成 Clock.fixed(...)


三、根因 2:并发等待------Thread.sleep 是 flaky 的干细胞

第二常见:测异步代码时,用 Thread.sleep 等结果。

反例:异步订单通知

java 复制代码
// 消息发到 MQ,下游消费后落一条 audit 记录
@Test
@DisplayName("下单后应异步写出 audit 记录")
void shouldWriteAuditAfterOrderCreated() throws Exception {
    Order order = orderService.create(buildOrderRequest());

    // ❌ 等 2 秒,期望下游处理完
    Thread.sleep(2000);

    AuditLog log = auditRepository.findByOrderId(order.getId());
    assertNotNull(log);
}

这个测试什么时候挂? 在你机器上 2 秒够,CI 上某次 MQ 消费者积压要 2.5 秒,挂了。Thread.sleep 永远没法稳定地"等够":等待短时易超时(慢机器),等待长时浪费时间(快机器还多等)。

修复:用 Awaitility,断言会"等"

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>4.2.2</version>
    <scope>test</scope>
</dependency>
java 复制代码
import static org.awaitility.Awaitility.await;
import static java.time.Duration.ofSeconds;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@Test
@DisplayName("下单后 5 秒内应写出 audit 记录")
void shouldWriteAuditAfterOrderCreated() {
    Order order = orderService.create(buildOrderRequest());

    await()
        .atMost(ofSeconds(5))         // 最多等 5 秒
        .pollInterval(ofMillis(200))  // 每 200ms 检查一次
        .untilAsserted(() -> {
            // 用 assertion 而不是 if/check,失败会自动重试
            AuditLog log = auditRepository.findByOrderId(order.getId());
            assertNotNull(log, "audit 记录应在 5 秒内出现");
        });
}

Awaitility 干了三件事:

  1. 轮询重试------每 200ms 跑一次断言,直到通过或超时;
  2. 断言失败时继续等------不是"等 2 秒再判一次",是"持续地在 5 秒窗口内尝试";
  3. 失败信息可读------超时时报告"5 秒内 audit 记录一直为 null",而不是干巴巴的 NPE。

纪律 :测试里能用 Thread.sleep(x) 的地方,99% 都可以换成 Awaitility。剩下 1% 是真实硬件依赖(读传感器那种),不在我们聊范围。


四、根因 3:测试间相互污染------"上一个测试给我留了垃圾"

第三类最隐蔽:测试单独跑过、一起跑挂。有一次我排查这个排查了两天,因为单测每个都过,但 mvn test 全跑就红两个。

反例:static 状态没清理

java 复制代码
public class DiscountContext {
    private static DiscountRule currentRule;  // ← static 可变状态

    public static void setRule(DiscountRule rule) {
        currentRule = rule;
    }

    public static BigDecimal apply(BigDecimal price) {
        return currentRule.apply(price);
    }
}

// 测试 A
@Test
void shouldApply10PercentDiscount() {
    DiscountContext.setRule(new PercentDiscountRule(BigDecimal.valueOf(0.1)));
    assertEquals(new BigDecimal("90.00"), DiscountContext.apply(new BigDecimal("100.00")));
}

// 测试 B(跑在 A 之后)
@Test
void shouldApplyNoDiscountByDefault() {
    // ❌ 这里期望"默认无折扣",但如果 A 先跑了,currentRule 还在
    assertEquals(new BigDecimal("100.00"), DiscountContext.apply(new BigDecimal("100.00")));
}

问题 :currentRule 是 static,A 测试改了它,B 测试假设"默认值"------所以 B 单独跑过(刚启动 static 是 null 抛 NPE),A 跑过之后 B 也过(currentRule 被改成 10% 折扣......等等,这次反而挂了)。

实际中最常见的污染是这两种:

  1. 数据库没回滚------A 测试插了一条数据,B 测试假设表是空的;
  2. Spring 上下文 static 字段 ------@SpringBootTest 启的 bean 持有状态,A 改了之后 B 看到的是脏值。

修复:@BeforeEach 清理 + 事务回滚

java 复制代码
// 方案 A:用 @BeforeEach 显式 reset
@BeforeEach
void resetState() {
    DiscountContext.setRule(DiscountRules.noDiscount());
}

// 方案 B(集成测试):让 Spring 自动回滚事务
@SpringBootTest
@Transactional  // ← 测试结束自动 rollback,数据库恢复干净
class OrderRepositoryIT {
    @Autowired
    private OrderRepository repo;

    @Test
    void shouldFindSavedOrder() {
        repo.save(buildOrder());
        // 测试结束,@Transactional 自动 rollback
    }
}

纪律 1 :测试不该依赖"前一个测试留了什么"------每个测试都从已知初始态开始。这是测试可隔离性的底线。

纪律 2 :static 可变状态在测试代码里出现就要警觉 。如果是别人写的生产代码 static,测试里用 @BeforeEach 强制重置。


五、根因 4:外部环境依赖------网络、文件、随机数

第四类:测试依赖"测试代码外面的东西"。

反例 1:测试调真实第三方

java 复制代码
// ❌ 测试里直接调真实的快递 API
@Test
void shouldReturnShippingFee() {
    ExpressClient client = new ExpressClient("https://api.express.com");  // ← 真实 API
    BigDecimal fee = client.calculateFee("杭州", "北京", 1.5);
    assertEquals(new BigDecimal("12.00"), fee);
}

这种测试什么时候挂?(a)公司网络抽风时;(b)快递公司 API 变价时;©CI 在境外网络访问国内 API 超时时。任何外部依赖都可以让 flaky 凭空产生

修复:用 WireMock 桩掉(参考 Day 5),或用 Pact 测真的契约(参考 Day 9)。基础设施代码绝对不能在测试里调真实第三方。

反例 2:依赖文件系统绝对路径

java 复制代码
// ❌ 假设 /tmp/orders.json 一定可写
@Test
void shouldExportOrders() throws IOException {
    File f = new File("/tmp/orders.json");
    service.exportTo(f);
    assertTrue(f.length() > 0);
}

CI 容器上 /tmp 可能不一样、可能不可写、可能上次跑的垃圾还在。修复 :用 JUnit 5 的 @TempDir:

java 复制代码
@Test
void shouldExportOrders(@TempDir Path tempDir) throws IOException {
    File f = tempDir.resolve("orders.json").toFile();
    service.exportTo(f);
    assertTrue(f.length() > 0);
}
// 测试结束 JUnit 自动删这个临时目录,绝对隔离

反例 3:Math.random() / UUID.randomUUID()

java 复制代码
// ❌ 测试里用 random 数据,断言时只检查"非空"
@Test
void shouldGenerateUniqueOrderNumber() {
    String orderNo = orderService.generateOrderNo();
    assertNotNull(orderNo);
    // 这测试永远过,但根本没验证"唯一"------真到生产偶发撞号,测试从来没报
}

更糟的:

java 复制代码
// ❌❌ 测试本身依赖随机值
@Test
void shouldHandleRandomPrice() {
    BigDecimal price = new BigDecimal(Math.random() * 100);
    BigDecimal result = service.applyDiscount(price);
    assertTrue(result.compareTo(price) <= 0);  // 接近 trivial,几乎肯定过
}

修复 :测试要么用固定值 (边界值显式列),要么用确定性伪随机(seeded random)。

java 复制代码
Random seeded = new Random(42);  // 固定 seed
BigDecimal price = new BigDecimal(seeded.nextDouble() * 100);
// 每次跑结果一致,flaky 根除

六、根因 5:资源耗尽 / 平台差异------CI 上特有的怪事

第五类留给那些"我本机好好的"诡异 case。典型症状:

  • 端口冲突 :Testcontainers 启容器时本机端口可能被占用 → 让框架使用随机端口(端口设为 0,由操作系统分配空闲端口);
  • CI 容器架构不同:Apple M 系列 ARM64 vs CI x86,镜像坑(参考 Day 4);
  • 内存不足:并行跑 10 个 SpringBootTest,OOM 不是功能 bug;
  • 文件名大小写敏感 :Mac 文件系统不区分大小写,Linux 区分,Icon.png vs icon.png 在 CI 上挂;

这类问题排查技巧:写好 CI 失败时上传完整日志 + 容器状态 + 内存 dump 的 artifact(Day 7 的 nightly job 已示范),让"我本机好的"这种话不再有市场。


七、@Retry 是解药还是毒药?这是 flaky 治理最大的争论

JUnit 5 有个 @RetryingTest,Maven Surefire 有 rerunFailingTestsCount,作用相同------失败时自动重试 N 次。

java 复制代码
@Test
@RetryingTest(3)  // 失败时再跑 2 次,只要有一次过就算过
void flakyTest() {
    // ...
}

直觉 :flaky 重试一下就好了。

现实 :@Retry 是把地毯下面的灰尘扫得更深------测试本身的非确定性没消除,只是被掩盖 。加 @Retry 后,这条测试有 30% 概率过,你重试 3 次,变成 1 - 0.7³ = 65.7% 概率过------但还有 34.3% 概率会最终挂。你以为是"治好了",其实是把"3 倍时间 + 还有概率挂"加到了 CI 里。

Google 那篇论文里的态度叫"no retry on failure "------失败就是失败,不重试。这种硬态度的好处是:flaky 立刻可见,不会躲在 retry 数字背后。

@Retry 真的完全不能用?不是。它适合两种情况:

  1. 已知非确定性但短期无法修 ------加 @Retry 是临时缓解,不是修复,必须搭配 quarantine(下节讲);
  2. 真实外部偶发不稳定 (比如真网络包丢失)------@Retry 帮助"测试本身的逻辑无关的偶发噪音"被过滤掉,且失败信息必须保留日志。

判断标准 :@Retry 用之前,问自己一句------"如果这条测试从来没挂过,@Retry 还有必要吗?"。答案应该是"没必要"。如果"必要",说明你把 retry 当成了功能,这就是误用。


八、quarantine:像处理传染病一样处理 flaky

quarantine(隔离区)是 flaky 治理的核心工程化动作。流程是:

复制代码
flaky 检测 → 自动移动到隔离区 → 标记 @Tag("flaky") → CI 跳过 → 7 天修复期
                                                                    ↓
                                                            修好→回归主套件
                                                            未修好→删除测试

步骤 1:检测

检测 flaky 有两种方式:

(a) rerun 矩阵:同一次 commit 在 CI 上重跑 N 次,统计稳定性。简单但烧 CI 资源。

(b) 时间序列:把历史 CI 跑的结果接入数据系统,统计"这条测试最近 100 次跑里失败多少次"。Google 用的是这种。低成本但需要 CI 数据基础设施。

一个最朴素的实现------给 Surefire 加 flaky 检测(基于重跑):

xml 复制代码
<!-- pom.xml -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <!-- 失败时重跑 1 次,如果重跑过了,标记为 flaky -->
        <rerunFailingTestsCount>1</rerunFailingTestsCount>
    </configuration>
</plugin>

Surefire 报告里会出现 <flakyFailure> 标签,你可以解析这个标签做后续动作。

步骤 2:隔离

JUnit 5 的 @Tag 是天然的隔离机制。

java 复制代码
@Test
@Tag("flaky")  // ← 加标签
@DisplayName("暂存:[flaky] 偶发失败,正在排查")
void temporarilyFlaky() {
    // 已知 flaky,等待修复
}

CI 里跳过 flaky 标签:

xml 复制代码
<!-- pom.xml -->
<configuration>
    <excludedGroups>flaky</excludedGroups>
</configuration>

或者 GitHub Actions 里:

yaml 复制代码
- name: 跑测试(跳过 flaky)
  run: mvn -B test -Dgroups='!flaky'

- name: 单独跑 flaky(只统计,不阻塞)
  continue-on-error: true
  run: mvn -B test -Dgroups='flaky'

关键设计 :flaky 不阻塞 PR merge ,但 CI 单独跑一次,把通过率记录下来。这样既不阻塞业务,又让 flaky 持续可见

步骤 3:SLA 治理

光隔离还不够,得有"修复时限"。我推荐的 SLA:

flaky 严重度 通过率 修复 SLA 失败后果
🔴 高 < 70% 3 天 自动转 Issue,P0 标签,owner 必须响应
🟡 中 70%~90% 7 天 自动转 Issue,P1 标签
🟢 低 > 90% 14 天 进入 watch list

关键 :超 SLA 自动删除测试。这听起来无情,但很有必要------flaky 测试不删等于说"我们的测试套件不需要稳定",长期下来整个 CI 都没人信。删除测试不等于"代码没保护",如果代码重要,owner 会重新写一个稳定的测试;如果代码不重要,删了也无所谓。

步骤 4:修复回归

修好后,移除 @Tag("flaky"),测试回到主套件,owner 必须在 PR 里说明根因和修复方式------这样 flaky 知识沉淀下来,下次踩同样坑的人能搜到。


九、flaky 数据上报:让治理不再是感觉

最后一块拼图------怎么持续看到 flaky 的整体面

GitHub Actions 有个简单做法:每个 PR 末尾解析 Surefire 报告,把 flaky 数量上报到 Slack / Dashboard:

yaml 复制代码
# .github/workflows/pr.yml
- name: 解析 flaky 报告
  if: always()
  run: |
    FLAKY_COUNT=$(find target/surefire-reports -name '*.xml' \
      -exec grep -l '<flakyFailure>' {} \; | wc -l)
    echo "本 PR 检测到 $FLAKY_COUNT 条 flaky 测试"
    echo "FLAKY_COUNT=$FLAKY_COUNT" >> $GITHUB_ENV

- name: flaky 超阈值告警
  if: env.FLAKY_COUNT > 5
  uses: slackapi/slack-github-action@v1
  with:
    slack-message: "⚠️ 本仓库 flaky 测试已达 ${{ env.FLAKY_COUNT }} 条,注意治理"

进阶版是把 CI 结果接入 Datadog / Prometheus,做时序图。关键指标:

  • flaky rate(flaky 测试数 / 总测试数)------趋势看治理效果;
  • 单条 flaky 存活天数------谁超 SLA 了;
  • flaky 测试 owner 分布------发现"某个团队持续产生 flaky",说明他们的测试工程文化需要支援。

数据可视化让 flaky 治理从"我听说有几个 flaky"变成"全仓库 47 条 flaky,3 条超 SLA,本月净减少 12 条"。没有数据的治理,永远停留在感觉


十、和系列前文的回扣

把 Day 10 摆回系列地图:

  • Day 1(单元测试金字塔) :flaky 大多发生在金字塔任何一个层,但单元层最不该 flaky(纯内存);
  • Day 2(Mockito) :Mockito 严格模式 (@MockitoSettings(strictness = STRICT_STUBS))能抓到"测试之间偷偷改 mock"导致的非确定性;
  • Day 3(覆盖率) :flaky 测试经常覆盖率很高但价值很低------它在测,但测的东西是随机的;
  • Day 4(Testcontainers) :用真实容器替代 wire mock,本身就是消除一类 flaky------但要注意容器启动时的 flaky(端口、镜像拉取);
  • Day 5(WireMock) :WireMock 的固定 delay 也是 flaky 高发地 ------Day 5 提到的 withFixedDelay,生产不抖测试时也用这个值,这种"假同步"就是 flaky;
  • Day 6(性能测试):性能测试本身没必要 flaky-free,SLO 应该用统计区间表示,单次失败不算 bug;
  • Day 7(CI 分层):Day 7 给出了"flaky 检测 → 隔离 → fix or remove"政策框架,Day 10 把它具象化成可执行工程;
  • Day 8(TDD) :TDD 出来的测试天然反 flaky------因为 Red-Green 循环要求"每次同样输入同样输出",flaky 测试在 TDD 循环里立刻被发现;
  • Day 9(契约测试) :Pact 验证失败常被误判为 flaky(其实是契约真不兼容)------必须严格区分"flaky(测试本身非确定性)"和"契约失败(两边约定不一致)",后者不能 retry。

十一、思考题

  1. 你团队 CI 上"上次过这次不过"的报警,平均每个月出现几次?有没有把它定量上报,而不是凭感觉?
  2. 挑一条你们仓库的 flaky 测试,对照五大根因分类,它属于哪一类?对应的修复方式是什么?
  3. 你团队有没有 @Retry / rerunFailingTestsCount?如果有,他们是在"治理"还是"掩盖"?
  4. 你敢不敢在团队里推"flaky 7 天不修就删"的政策?阻力会来自哪里?(提示:测试作者怕代码没保护;但没保护的代码其实本就没保护)
  5. flaky rate 应不应该作为 KPI?如果定为 KPI,会出什么怪事?(参考 Day 3 覆盖率 KPI 的反模式)

TL;DR

  • flaky 五大根因:时间依赖、并发等待、测试间污染、外部环境依赖、资源耗尽------收到报告先归类,再看堆栈;
  • 生产代码不调 now(),注入 Clock ;测试代码不用 Thread.sleep,换 Awaitility ;静态状态在 @BeforeEach 显式重置;
  • @Retry 是缓解不是修复------除非短期 quarantine,否则不要长期依赖重试;
  • quarantine 流程:检测 → @Tag("flaky") 隔离 → CI 跳过但记录 → SLA 计时 → 超时删除;
  • flaky 治理三根支柱:检测(automated)、隔离(quarantine)、上报(dashboard)------三件事都做才叫工程化;
  • flaky rate 不该当 KPI(参考 Day 3 覆盖率陷阱),但应作为趋势指标和工作清单;
  • 删一条 flaky 测试不是损失,是"承认这条测试本来也没在保护代码"------代码真重要的话,会用稳定的方式重写。

明天 Day 11:《测试左移:需求评审阶段如何把缺陷消灭在编码前》 ------

flaky 是测试在执行层的问题,Day 11 要跳到上一层------**测试能不能更早介入?**需求评审阶段,QA 还没动手写测试,但已经能通过"逆向需求""场景拆解""缺陷预测",把 60% 的潜在 bug 挡在编码之前。如果这片对你有帮助,欢迎点赞收藏 ⭐。下篇见。

相关推荐
Z5998178411 小时前
c#软件开发学习笔记--事务、索引
笔记·学习·c#
十月的皮皮1 小时前
C语言学习笔记20260716-编译与链接
c语言·笔记·学习
私人珍藏库11 小时前
[Android] 会计快题库 -财会职称考试刷题学习
android·人工智能·学习·app·软件·多功能
从零开始的代码生活_17 小时前
C++ list 原理与实践:双向链表、迭代器与简化实现
开发语言·c++·后端·学习·算法·链表·list
二炮手亮子17 小时前
记录AI学习之路Day13:小模型(AI生成)
学习
程序员小八77718 小时前
从 0 学习 MySQL 索引——7 大核心精讲
android·学习·mysql
渣渣灰飞19 小时前
MySQL 系统学习 第六阶段:Redis + 缓存 + 高并发设计 第六章:Redis 实现登录 Token
学习·mysql·缓存
渣渣灰飞19 小时前
MySQL 系统学习 第六阶段:Redis + 缓存 + 高并发设计 第五章:缓存穿透、缓存击穿、缓存雪崩
学习·mysql·缓存
念何架构之路19 小时前
Docker 容器状态机 学习
java·学习·docker