SQL 注入与 Redis 缓存问题总结

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

一、SQL 注入

1. 定义

  • 攻击者把恶意字符串作为输入,拼接进 SQL 语句中。

  • 数据库按"正常 SQL"执行这些内容,造成绕过登录、泄露数据、删表等后果。

  • 根本原因:用户输入直接参与字符串拼接构造 SQL


2. 漏洞示例

错误写法(C++ + MySQL):

cpp 复制代码
 std::string sql =
     "SELECT id FROM users WHERE name='" + user +
     "' AND password='" + pass + "' LIMIT 1";

如果输入:

cpp 复制代码
 user = Elias
 pass = 1234' OR 1=1 --

拼接后的 SQL:

bash 复制代码
 SELECT id FROM users
 WHERE name='Elias' AND password='1234' OR 1=1 --'
 LIMIT 1;

OR 1=1 恒为真,登录验证被绕过。


3. 常见注入手法

  1. 恒真条件

    bash 复制代码
     ' OR 1=1 --
     ' OR 'a'='a
  2. UNION 联合查询

    bash 复制代码
     ' UNION SELECT id, password FROM users --
  3. 堆叠查询(多语句执行)

    bash 复制代码
     '; DROP TABLE users; --

4. 防御方案(按优先级)

4.1 预处理语句(首选)

思路:SQL 结构固定,参数占位符 ?,参数单独绑定,不参与字符串拼接。

SQL 模板:

bash 复制代码
 SELECT id FROM users WHERE name = ? AND password = ?

伪代码流程:

bash 复制代码
 const char* sql =
     "SELECT id FROM users WHERE name = ? AND password = ?";
 ​
 MYSQL_STMT* stmt = mysql_stmt_init(conn);
 mysql_stmt_prepare(stmt, sql, strlen(sql));
 ​
 // 绑定参数(只示意)
 MYSQL_BIND bind[2] = {};
 // bind[0] 绑定 user,bind[1] 绑定 pass ...
 ​
 mysql_stmt_bind_param(stmt, bind);
 mysql_stmt_execute(stmt);
 mysql_stmt_close(stmt);

特点:参数永远当"值",不会当 SQL 代码执行,从根源上防注入。


4.2 转义用户输入(次优)

用于旧代码过渡,仍然是拼接,但先把危险字符转义掉。

封装:

cpp 复制代码
 std::string escapeForSql(MYSQL* conn, const std::string& s) {
     if (!conn || s.empty()) return s;
 ​
     std::string out;
     out.resize(s.size() * 2 + 1);
 ​
     unsigned long len = mysql_real_escape_string(
         conn,
         &out[0],
         s.c_str(),
         static_cast<unsigned long>(s.size())
     );
     out.resize(len);
     return out;
 }

使用:

cpp 复制代码
 std::string userEsc = escapeForSql(conn, user);
 std::string passEsc = escapeForSql(conn, pass);
 ​
 std::string sql =
     "SELECT id FROM users WHERE name='" + userEsc +
     "' AND password='" + passEsc + "' LIMIT 1";

4.3 输入校验(辅助)

  • 限制长度(如用户名 ≤ 32)。

  • 限制字符集(只允许字母 / 数字 / 少量符号)。

  • 只能减少攻击面,不能单独用来防注入。


5.总结

遇到 "... WHERE name='" + user + "' ..." 这种拼接,就说明写法有问题。 新代码统一用预处理语句,旧代码至少先做转义。


二、Redis 缓存问题总览

Redis 相关常见三个词:

  1. 缓存穿透:请求的 key,在缓存和数据库里都不存在,请求每次直达数据库。

  2. 缓存击穿 :某个热点 key 过期瞬间,大量并发同时访问这个 key,一起打到数据库。

  3. 缓存雪崩:大量 key 在同一时间段集中失效,或者 Redis 整体不可用,导致大量请求直接压到数据库。

下面重点记 缓存击穿缓存雪崩


三、缓存击穿(Hot Key Breakdown)

1. 场景

  • 某个 key 非常热门(例如:热门商品详情、热门直播间信息)。

  • 这个 key 设置了过期时间。

  • 到期瞬间,大量请求同时访问:

    • 缓存中没有

    • 大量请求直接查数据库

    • 数据库短时间内被"打穿"。


2. 解决方案

2.1 互斥锁重建缓存

目标:同一时间只允许一个线程/进程去数据库重建这个 key,其它请求等待或稍后重试。

伪代码:

cpp 复制代码
 val = redis.get(key)
 if (val exists) return val
 ​
 if (!tryLock("lock:" + key)) {
     sleep(10~50ms)
     return redis.get(key)        // 再读一次,读到就直接返回
 }
 ​
 val = db.query(...)
 redis.setex(key, ttl, val)
 unlock("lock:" + key)
 return val

要点:

  • 单机可以用本地互斥锁。

  • 分布式要用 Redis 分布式锁(SET NX EX)保证只有一台机器在重建。

适用:登录信息、用户数据、房间元信息等要求一致性比较高的场景。


2.2 逻辑过期 + 异步刷新

思路:

  • 缓存中保存:{data, logic_expire_time}

  • 请求到来时:

    • 如果 现在时间 < logic_expire_time:直接返回数据。

    • 如果已超过逻辑过期时间:

      • 仍然先返回旧数据给用户。

      • 后台异步启动一个线程从数据库刷新,并更新 logic_expire_time

伪代码:

cpp 复制代码
 cache = redis.get(key)
 ​
 if (cache == null) {
     // 首次加载,按普通查询 + setex 处理
 }
 ​
 if (now < cache.logic_expire_time) {
     return cache.data
 }
 ​
 // 逻辑过期
 if (tryLock("refresh:" + key)) {
     async {
         val = db.query(...)
         redis.set(key, {data: val, logic_expire_time: now + T})
         unlock("refresh:" + key)
     }
 }
 ​
 return cache.data   // 先返回旧值

特点:

  • 用户几乎感受不到阻塞。

  • 适合读多写少,对"绝对实时"要求不高的业务(如群资料、排行榜)。


四、缓存雪崩(Cache Avalanche)

1. 场景

  • 很多 key 在同一时刻或短时间内密集过期,缓存命中率突然大幅下降。

  • 大量请求绕过 Redis,直接压到数据库。

  • 或者 Redis 故障、集群整体不可用,效果类似。


2. 解决方案

2.1 打散过期时间

不要所有 key 都:

cpp 复制代码
 EX 3600

改为:

cpp 复制代码
 EX 3600 + rand(0, 600)

让失效时间分布在一个区间,避免同一时间集中过期。


2.2 热点数据预热

  • 系统启动或大促前,提前把核心数据加载进 Redis。

  • 对核心 key 拉长 TTL,避免在高峰期过期。


2.3 多级缓存

  • 本地缓存(进程内 LRU / map) + Redis。

  • 访问顺序:

    1. 先查本地缓存;

    2. 没有再查 Redis;

    3. 还没有再查数据库。

即使 Redis 挂掉,本地缓存还能挡掉一部分重复请求。


2.4 降级与限流

  • Redis 或数据库压力过高时:

    • 对非核心接口直接返回默认数据 / 静态页。

    • 对热点接口做限流(令牌桶、漏桶、滑动窗口)。

  • 目标:保护数据库,保证核心功能活着。


五、总结

  • SQL 注入:不要拼接 SQL,用预处理语句,旧代码至少先做转义。

  • 缓存击穿:热点 key 过期瞬间被打爆,用互斥锁或逻辑过期 + 异步刷新。

  • 缓存雪崩:一堆 key 同时过期或 Redis 整体挂掉,用随机过期时间、多级缓存、预热、降级限流。

相关推荐
重启的码农8 分钟前
enet源码解析(6)协议处理逻辑 (Protocol Processing)
c++·网络协议
AAA简单玩转程序设计26 分钟前
C++进阶基础:5个让人直呼“专业”的冷门小技巧
c++
hqzing29 分钟前
介绍一个容器化的鸿蒙环境
c++·docker
赖small强1 小时前
【Linux C/C++开发】第25章:元编程技术
linux·c语言·c++·元编程
杜子不疼.1 小时前
【C++】解决哈希冲突的核心方法:开放定址法 & 链地址法
c++·算法·哈希算法
落羽的落羽1 小时前
【Linux系统】解明进程优先级与切换调度O(1)算法
linux·服务器·c++·人工智能·学习·算法·机器学习
零基础的修炼1 小时前
[项目]基于正倒排索引的Boost搜索引擎---编写建立索引的模块Index
c++·搜索引擎
草莓熊Lotso2 小时前
Git 本地操作进阶:版本回退、撤销修改与文件删除全攻略
java·javascript·c++·人工智能·git·python·网络协议
p***43482 小时前
SQL在业务智能中的分析函数
数据库·sql