Redis缓存回滚踩坑实录:一个配置失误,让我在凌晨3点丢了3000条数据

凌晨三点,手机像发了疯一样震动。告警信息只有一句话:"MySQL连接池耗尽,API成功率跌至12%"。哆嗦着打开监控,数据库QPS从800直接冲上2万,所有请求都在穿透打到MySQL。问题出在Redis------它刚被自动重启,但缓存全空了,3000多条商品热数据全部从内存消失,RDB文件没加载上来。运维前天改过一个配置,把RDB目录指向了一个不存在的路径,重启后Redis安安静静地启动了零数据的实例。我们自信满满的回滚机制,就这样华丽地"成功回滚到了空白"。

从那以后,我给自己立了一条规矩:任何涉及持久化、缓存恢复的变更,必须有一条自动化测试链路,能像黑客一样无情地注入故障,然后用事实告诉我"数据到底丢了没有"。这就是我折腾出 Playwright + Python + Docker 这套自动化验证方案的起因。今天把完整的实现和血泪教训写出来,希望能帮同行少掉一层皮。

问题拆解:为什么你的缓存回滚测试形同虚设

很多团队对"Redis持久化恢复"的测试仅停留在:停掉Redis → 重启Redis → 看看keys还在不在。这种测试根本测不出生产的问题,因为:

  1. 持久化文件的状态是不可信的 。RDB可能因为磁盘满而损坏、AOF可能被bgrewrite截断、配置改错导致读不到文件------这些都是高频故障,但手工测试几乎不会覆盖。
  2. 业务逻辑对"空缓存"的反应没有被验证 。Redis恢复失败 → 缓存为空 → Go/Python服务有没有兜底从MySQL加载?加载过程中会不会因为并发穿透打崩数据库?这些需要端到端的流程测试,而不是简单的GET一个key。
  3. 回滚成功与否没有和前端表现挂钩。用户看到的是商品详情报错还是旧数据?白屏吗?接口返回什么状态码?只有把整个链路跑通才算验证到位。

所以我们需要的不是几行单元测试,而是一套能模拟真实用户操作 → 制造持久化故障 → 检查业务层恢复效果的自动化方案。

方案设计:为什么是Playwright,而不是JMeter或单测?

我们评估了三条路:

  • JMeter/Locust压测:能模拟大量请求,但做不了"点击按钮→填写表单→提交"这样的用户行为,也无法校验页面元素的值(比如商品名称是否正确恢复)。
  • 后端单元测试+Mock Redis:只能测逻辑分支,测不到真正的RDB文件系统、进程重启、IO负载这些内核级行为。
  • Playwright + Docker + pytest :Playwright控制浏览器模拟完整用户流程,pytest的fixture机制让我们可以随意停止/销毁Redis容器、删RDB文件、改配置,Docker保证每次测试环境干净。最大优势:故障注入和业务验证在同一个测试用例里闭环

最终架构很简单:docker-compose拉起一个最小业务环境(Flask Web + Redis + MySQL),Playwright通过浏览器界面创建数据,pytest执行Redis"破坏",然后再次用Playwright读取页面数据并对比MySQL,判定回滚是否成功。

核心实现:从零搭建故障注入测试框架

下面这段docker-compose.yml是整个测试的基准环境。注意Redis的volume配置------这是第一个踩坑点,必须把RDB目录显式挂载出来,否则你删的文件根本不是它启动时加载的那个。

yaml 复制代码
# docker-compose.yml
version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      REDIS_HOST: redis
      DB_HOST: mysql
    depends_on:
      - redis
      - mysql

  redis:
    image: redis:7-alpine
    # 必须显式挂载 /data,否则RDB文件会写在容器层,重启后丢失路径关系
    volumes:
      - redis_data:/data
    command: redis-server --save 60 1 --loglevel warning

  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: testpass
      MYSQL_DATABASE: shop
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  redis_data:
  mysql_data:

接下来是pytest测试用例的整体结构,这段代码解决"怎么在一个测试函数里把用户操作、Redis破坏、恢复验证串起来"。我们使用async函数和Playwright的async API。

python 复制代码
# test_cache_rollback.py
import asyncio
import time
import pytest
import docker
import redis
import pymysql
from playwright.async_api import async_playwright

BASE_URL = "http://localhost:8000"
REDIS_HOST = "localhost"
REDIS_PORT = 6379

@pytest.fixture(scope="module")
def docker_client():
    return docker.from_env()

@pytest.fixture(scope="module")
def redis_conn():
    r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
    yield r
    r.close()

@pytest.fixture(scope="module")
def mysql_conn():
    conn = pymysql.connect(host='localhost', user='root', password='testpass', database='shop')
    yield conn
    conn.close()

async def test_rollback_after_rdb_loss(docker_client, redis_conn, mysql_conn):
    # 1. 用户通过前端创建商品,缓存写入Redis
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto(f"{BASE_URL}/products/new")
        await page.fill('input[name="name"]', '限量球鞋')
        await page.fill('input[name="price"]', '999')
        await page.click('button[type="submit"]')
        # 等待创建成功
        await page.wait_for_selector("text=商品创建成功")
        product_id = await page.get_attribute('#product-id', 'value')

        # 2. 验证Redis缓存存在
        cache_key = f"product:{product_id}"
        assert redis_conn.exists(cache_key), "初始缓存未写入"

        # 3. 注入故障:停掉Redis,删除RDB文件,模拟持久化丢失
        redis_container = docker_client.containers.get("redis")
        redis_container.stop()
        # 删除宿主机上传的dump.rdb,这要求volume正确挂载(前面docker-compose已保证)
        import os
        rdb_path = os.path.join(os.getcwd(), "redis_data", "dump.rdb")
        if os.path.exists(rdb_path):
            os.remove(rdb_path)
        # 重启Redis
        redis_container.start()
        # 关键:等待Redis加载完毕(持久化文件为空,会启动一个空实例)
        time.sleep(2)

        # 4. 刷新页面,验证业务层是否从MySQL回源并重建缓存
        await page.goto(f"{BASE_URL}/products/{product_id}")
        product_name = await page.text_content('.product-name')
        assert product_name == '限量球鞋', "页面未正确显示商品,可能回源失败"

        # 5. 再次检查Redis缓存是否被重建
        assert redis_conn.exists(cache_key), "缓存未重建,回滚机制有bug"

        # 6. 最后和MySQL数据进行终极对比
        with mysql_conn.cursor() as cursor:
            cursor.execute("SELECT name FROM products WHERE id=%s", (product_id,))
            result = cursor.fetchone()
            assert result and result[0] == '限量球鞋', "数据库数据不一致"

        await browser.close()

这段测试覆盖了最致盲的故障:Redis完全丢失RDB后重启。但现实世界远没有这么友善,踩的坑都在下面。

踩坑记录:官方文档没告诉你的事

坑1:Docker volume不挂载,删文件删了个寂寞

现象:我测试时手动docker exec进容器删掉/data/dump.rdb,重启后Redis竟然还能恢复出数据。

原因:我偷懒没在docker-compose.yml里写volumes,Redis的RDB实际落在了容器可写层,容器停止后那一层并未被提交,重启后是全新的容器,自然看不到原来的RDB文件,所以我的"删除"操作根本没影响到新实例。

解决:必须挂载具名volume到/data,确保RDB文件在宿主机上持久化。测试时删除宿主机路径下的文件才能真正模拟RDB丢失。

坑2:Redis重启并不是原子性的"立即可用"

现象:测试中stop()、删文件、start()之后立刻发请求,经常得到空缓存,导致断言失败,但等几秒再查又好了。

原因:Redis启动后有一个短暂的加载过程,哪怕没有RDB文件,它也会初始化内部数据结构。start()方法返回仅代表容器进程起来了,不代表Redis服务已经accept连接。

解决:采用轮询等待redis_conn.ping()成功,或检查INFO persistenceloading字段为0。生产代码同样应该对恢复完成事件敏感,不能上来就无脑写缓存。

效果验证

同样的测试人力,手动测试一个月只能覆盖重启后keys是否存在,而且不可能去改容器卷。自动化后,每提交一次代码,以下5种故障场景全自动跑完:

测试场景 手动测试 自动化后
正常重启恢复 1次/月 每次PR
RDB文件丢失 0 每次PR
AOF截断修复 0 每次PR
磁盘满写入失败 0 每次PR
配置dir错误 0 每次PR

上线前最后一次自动化跑出RDB文件丢失场景失败,追查发现是回源逻辑中缺少分布式锁,并发回源时击穿了数据库。修完当天部署,此后零数据丢失故障。

可直接用的代码/工具

把下面的 fixture 粘贴到你的 conftest.py,直接复用Redis故障注入能力:

python 复制代码
@pytest.fixture
def redis_crash_injection(docker_client):
    def _inject(volume_path: str):
        container = docker_client.containers.get("redis")
        container.stop()
        # 删除本地rdb文件
        import os
        rdb = os.path.join(volume_path, "dump.rdb")
        if os.path.exists(rdb):
            os.remove(rdb)
        container.start()
        import time; time.sleep(2)  # 简单等加载
    return _inject

测试里调用 redis_crash_injection("./redis_data") 即可。


#Python #Redis #Playwright #自动化测试 #后端踩坑

关于作者

我是宝哥,一个在深夜和数据库搏斗的后端/架构实战派,坚信"不经过故障注入的容灾方案都是定时炸弹"。

GitHub: github.com/baofugege

如果这篇文章让你少掉了几根头发,欢迎 Sponsor 请我喝杯咖啡。

提供 Python 后端性能优化 / 工具定制 / 技术咨询服务,联系 Telegram @baofugege

相关推荐
kyriewen2 小时前
别再这样写条件渲染了——你的React组件里藏着这5种定时炸弹
前端·javascript·react.js
IT_陈寒2 小时前
Redis缓存击穿把我坑惨了,原来这样设过期时间才靠谱
前端·人工智能·后端
用户938515635073 小时前
从"坐电梯"到"前端路由"——深入理解 Hash 路由原理
前端·typescript·全栈
梦想CAD控件3 小时前
网页端CAD的图形选择、编辑与夹点操作教程
前端·javascript·node.js
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十二):管理员权限检查中间件,AI 随意放行预闯大祸
前端·后端·go
月月大王的3D日记3 小时前
Three.js 入门系列(8):六种光源全解析 —— 关灯了,开光!
前端·javascript
属于自己的天空4 小时前
每天省去重复 Prompt:我用 Skills 把 CRUD 生成标准化了
前端·后端
卡卡罗特AI4 小时前
GitHub怎么用?零基础,保姆级使用教程-上!建议收藏
前端·后端·github