总结我的小项目里现在用到的Redis

一、在 NebulaChat 里,Redis 到底在干什么

项目地址:https://github.com/elaysia-feng/NebulaChat.git

当前和计划中的用途:

  1. 短信验证码缓存

    • key: sms:<phone>

    • value: 6 位数字验证码,字符串

    • TTL: 60 秒

    • 用途: 注册、短信登录、找回密码

  2. 用户信息缓存

    • key 示例

      • user:phone:<phone>{"id":1,"username":"Elias","phone":"123..."}

      • user:name:<username>{"id":1,"username":"Elias"}

    • 用途

      • 登录时快速根据用户名 / 手机号查到用户 id

      • 减少 MySQL SELECT 压力

统一思想:

  • Redis 只是加速访问的「缓存层」

  • 真正的权威数据存储在 MySQL

  • 写操作必须以 MySQL 为准,Redis 可以删、可以重新写

二、最核心的缓存模式:旁路缓存 Cache-Aside

1. 读缓存的流程(以手机号登录为例)

函数: AuthService::loginByPhone(phone, userId, usernameOut)

伪代码:

cpp 复制代码
bool loginByPhone(const std::string& phone, int& userId, std::string& usernameOut) {
    std::string key = "user:phone:" + phone;

    // 1. 先查 Redis
    auto redisConn = RedisPool::Instance().getConnection();
    if (redisConn) {
        std::string cached;
        if (redisConn->get(key, cached)) {
            if (cached == "null") {
                // 说明 DB 里也没有这个 phone
                return false;
            }

            json j = json::parse(cached);
            userId      = j.value("id", 0);
            usernameOut = j.value("username", "");
            if (userId > 0) return true;
        }
    }

    // 2. Redis 未命中 → 查 MySQL
    auto conn = DBPool::Instance().getConnection();
    MYSQL_RES* res = conn->query(
        "SELECT id, username FROM users WHERE phone = '" + phone + "' LIMIT 1");

    MYSQL_ROW row = mysql_fetch_row(res);
    if (!row) {
        mysql_free_result(res);
        // 3. DB 中也没有 → 写一个短期空缓存,防止穿透
        if (redisConn) redisConn->setEX(key, "null", 30);
        return false;
    }

    userId      = std::stoi(row[0]);
    usernameOut = row[1] ? row[1] : "";
    mysql_free_result(res);

    // 4. 将查询结果写入 Redis,设置 TTL
    if (redisConn) {
        json j;
        j["id"]       = userId;
        j["username"] = usernameOut;
        j["phone"]    = phone;

        int baseTTL   = 3600;
        int randDelta = RandInt(0, 600);       // 随机打散
        int ttl       = baseTTL + randDelta;

        redisConn->setEX(key, j.dump(), ttl);
    }

    return true;
}

要点:

  • 先查 Redis,命中直接返回

  • 未命中再查 DB

  • 查到后写入 Redis 并设置过期时间

  • 查不到时写空值 "null",防止同一个垃圾 key 反复打 DB

这就是「读的时候走缓存」的标准写法。

2. 写操作的流程(注册 / 改名 / 改密码)

写操作的关键原则:

  1. 必须先更新数据库,保证权威数据正确

  2. 缓存不要直接改,最简单可靠的姿势是: 删缓存

  3. 下次有读请求时,走上面的流程自动重建缓存

以修改用户名为例:

cpp 复制代码
bool AuthService::updateUsername(int userId,
                                 const std::string& newName,
                                 std::string&       oldNameOut,
                                 std::string&       phoneOut) {
    auto conn = DBPool::Instance().getConnection();

    // 1. 查出旧的 username 和 phone (删除缓存要用)
    MYSQL_RES* res = conn->query(
        "SELECT username, phone FROM users WHERE id = " + std::to_string(userId) + " LIMIT 1");
    MYSQL_ROW row = mysql_fetch_row(res);
    if (!row) { mysql_free_result(res); return false; }

    oldNameOut = row[0] ? row[0] : "";
    phoneOut   = row[1] ? row[1] : "";
    mysql_free_result(res);

    // 2. 更新数据库
    std::string sql =
        "UPDATE users SET username = '" + newName + "' WHERE id = " + std::to_string(userId);
    if (!conn->update(sql)) return false;

    // 3. 删除相关缓存
    auto redisConn = RedisPool::Instance().getConnection();
    if (redisConn) {
        if (!oldNameOut.empty())
            redisConn->del("user:name:" + oldNameOut);
        if (!phoneOut.empty())
            redisConn->del("user:phone:" + phoneOut);
        redisConn->del("user:id:" + std::to_string(userId));
    }

    return true;
}

结论:

  • 读: 查缓存, 缺了再查库, 查完再写缓存

  • 写: 改库, 然后删缓存, 让下次读去重建

这是「缓存更新策略」里最推荐、也是很容易做到的一种。

三、验证码缓存和业务数据缓存的区别

  1. 验证码

    • 特点: 强时效、只用一次

    • 流程:

      • 发送验证码: 生成 code, setEX sms:phone code 60

      • 校验验证码: GET sms:phone 比对, 成功后 DEL sms:phone

    • 不存在「一致性问题」: 它本来就不是数据库里的持久数据

  2. 用户信息缓存

    • 特点: 相对稳定, 多次使用

    • DB 中有持久存储, Redis 中只是一层加速

    • 有读写顺序、空值缓存、TTL、雪崩等一堆需要考虑的问题

记忆点:

验证码缓存 = 轻量、一次性

用户缓存 = 正儿八经的业务缓存,需要考虑各种边界问题

四、三个核心问题: 穿透、击穿、雪崩

1. 缓存穿透

定义:

  • 请求的 key 在 Redis 里没有,数据库里也没有

  • Redis 每次 miss,所有请求都直接打数据库

  • 恶意用户可以利用这个特点构造大量不存在的 key

在 NebulaChat 里的例子:

  • 用各种乱七八糟的手机号不停请求登录

  • 用不存在的用户名反复尝试登录

解决方案:

  1. 参数校验

    • 手机号格式不合法的直接拒绝,不去访问 Redis 和 DB

    • 用户名长度、字符合法性等也可以加

  2. 缓存空值

    • 某个手机号 / 用户名在 DB 中查不到时,

      写入 Redis: setEX key "null" 短 TTL

    • 之后同样的请求会直接命中 "null",不打 DB

伪代码片段:

cpp 复制代码
if (!row) { // DB 查不到
    mysql_free_result(res);
    if (redisConn) {
        int ttl = 30 + RandInt(0, 30); // 30~60 秒
        redisConn->setEX(key, "null", ttl);
    }
    return false;
}

2. 缓存击穿

定义:

  • 某个「热点 key」在某一时刻刚好过期

  • 结果大量请求同时到来,全部 miss

  • 一下子全打到数据库

在 NebulaChat 里的例子:

  • 某个热门用户, 很多连接频繁查询他的资料

  • user:phone:xxx 缓存 TTL 都是 3600 秒

  • 到第 3600 秒过期时, 突然很多请求并发到达 → 同时查 DB

解决方案:

  1. TTL 加随机值

    • 写缓存时, TTL 不要统一相同

    • 比如:

      cpp 复制代码
      int base = 3600;
      int ttl  = base + RandInt(0, 600); // 1 小时到 1 小时 10 分钟
    • 把不同 key 的过期时间打散,减少同一时刻过期的概率

  2. 互斥锁重建缓存(进阶)

    • 针对某个 key 在某个时刻只允许一个线程去查 DB、重建缓存

    • 其他线程要么等待,要么先返回旧值

    • 这个可以以后再做,不急着在 NebulaChat 里实现

  3. 逻辑过期(进阶)

    • 缓存里不仅存数据,还存一个逻辑过期时间

    • 读取时即使发现逻辑上过期,也可以先返回旧数据,再由后台线程异步重建

    • 对实时性要求不高的场景适用

3. 缓存雪崩

定义:

  • 大量 key 在同一时间集中失效

  • 或者 Redis 整体不可用

  • 导致所有请求都访问数据库,可能把 DB 打崩

在 NebulaChat 里的例子:

  • 所有 user:* 缓存 TTL 都是 3600

  • 项目启动一小时后,很多用户的缓存同时失效

  • 或者 Redis 整体挂掉,所有登录请求都绕过 Redis 打 DB

解决方案(分层次):

  1. TTL 随机化

    • 把不同 key 的过期时间打散,是最简单有效的第一步
  2. 多级缓存

    • 在进程内部加一层本地缓存,例如:

      • 局部 unordered_map<std::string, User>

      • 保存最近 N 个手机号对应的用户信息

      • 读取顺序变为: 本地缓存 → Redis → MySQL

    • 即使 Redis 出问题,本地缓存还能挡掉一部分请求

    简单本地缓存示例:

    cpp 复制代码
    struct UserCacheVal {
        int id;
        std::string username;
        std::chrono::steady_clock::time_point expire;
    };
    
    class LocalUserCacheByPhone {
    public:
        bool get(const std::string& phone, int& id, std::string& name);
        void put(const std::string& phone, int id, const std::string& name);
        void erase(const std::string& phone);
    private:
        std::unordered_map<std::string, UserCacheVal> map_;
        std::mutex mu_;
    };

    然后在 loginByPhone 里顺序是:

    • g_localUserCacheByPhone.get(...)

    • 再查 Redis

    • 最后查 MySQL

  3. 降级和限流(进阶)

    • 按 RedisPool 提供一个 IsDown() 标记

    • 当发现 Redis 长时间不可用时:

      核心接口(登录)仍然允许访问 DB,非核心接口(例如未来的某些列表查询)直接返回「系统繁忙」

    • 为 DB 查询加一个简单的 QPS 限流器,防止被压死

相关推荐
xlq223222 小时前
15.list(上)
数据结构·c++·list
a123560mh2 小时前
国产信创操作系统银河麒麟常见软件适配(MongoDB、 Redis、Nginx、Tomcat)
linux·redis·nginx·mongodb·tomcat·kylin
云帆小二2 小时前
从开发语言出发如何选择学习考试系统
开发语言·学习
BullSmall3 小时前
《道德经》第六十三章
学习
一个处女座的程序猿O(∩_∩)O3 小时前
Spring Boot、Redis、RabbitMQ 在项目中的核心作用详解
spring boot·redis·java-rabbitmq
AA陈超3 小时前
使用UnrealEngine引擎,实现鼠标点击移动
c++·笔记·学习·ue5·虚幻引擎
BullSmall3 小时前
《道德经》第六十二章
学习
No0d1es4 小时前
电子学会青少年软件编程(C/C++)六级等级考试真题试卷(2025年9月)
c语言·c++·算法·青少年编程·图形化编程·六级
⑩-4 小时前
缓存穿透,击穿,雪崩
java·redis