Skip to content

Redis与缓存Part1

BX

本文记录了 Redis 的核心操作与实战经验,特别是针对缓存场景下的常见问题及其解决方案。

一、基本操作

1. 五大基本命令

Redis 的基础交互非常直观,以下是 Python (redis-py) 的操作演示:

import redis

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)

# 写入与获取
r.set("target", "127.0.0.1")
ip = r.get("target")
print(ip)  # b'127.0.0.1'

# 删除
count = r.delete("target")
print(count) # 1

2. 生存时间 (TTL)

控制数据的生命周期是缓存系统的核心能力:

import redis
import time

r = redis.Redis(host='localhost', port=6379, db=0)

r.set("test", "test") # 永不过期
r.set("target", "bx33661", ex=5) # 5秒后过期

print(r.ttl("target")) # 输出 5
time.sleep(3)
print(r.ttl("target")) # 输出 2
time.sleep(3)
print(r.ttl("target")) # 输出 -2 (已过期)
print(r.ttl("test"))   # 输出 -1 (永不过期)

TTL 返回值的含义:

3. 批量操作

为了减少网络开销,可以使用批量命令:

r.mset({
    "user:1:flag": "flag{hello}",
    "user:2:flag": "flag{world}",
    "user:3:flag": "flag{redis}"
})

flags = r.mget(["user:1:flag", "user:2:flag", "user:3:flag"])
print(flags)

4. 管道 (Pipeline)

为什么要用管道?

这能带来极大的性能提升,特别是在批量写入场景。

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 使用 Pipeline
with r.pipeline() as pipe:
    for i in range(1000):
        pipe.set(f"task:{i}", "waiting")
        pipe.expire(f"task:{i}", 60)
    
    # 执行打包好的命令
    results = pipe.execute()

print(f"成功执行了 {len(results)} 个命令")

原理图示:

Python 本地内存缓冲:
┌───────────────────────────┐
│ set task:0                │
│ expire task:0             │
│ ...                       │
│ set task:999              │
└───────────────────────────┘

              │ execute() (一次网络发送)

Redis Server

二、数据类型与场景

Redis 不仅仅是 KV 存储,它提供了丰富的数据结构。

场景推荐类型理由
Token / 验证码String简单,原生支持过期
用户信息 / 对象Hash修改字段方便,比 String 存 JSON 更省内存
消息队列 / 历史记录List双向链表,支持阻塞读取 (BLPOP)
标签 / 黑名单Set自动去重,集合运算(交并补)快
排行榜 / 延时队列ZSet自动按 Score 排序,支持范围查询

1. Hash (哈希)

适合存储对象,例如游戏中的玩家属性:

# 设置玩家属性
r.hset("user:1002", mapping={
    "name": "bx",
    "age": 22,
    "score": 500
})

# 增加分数 (直接操作字段,无需取回整个对象)
r.hincrby("user:1002", "score", 100)

score = r.hget("user:1002", 'score')
print(score) # b'600'

常用命令速查:

命令功能典型场景
HSET / HMGET设置/获取字段对象存取
HINCRBY字段自增计数器、点赞数
HEXISTS判断字段是否存在属性检查
HGETALL获取所有字段读取完整对象 (注意大 Key 风险)

2. List (列表)

Redis 的 List 是一个双向链表

3. ZSet (有序集合)

ZSet 是 Redis 最具特色的数据结构。它类似于 Set(元素去重),但每个元素关联一个 Double 类型的 Score

场景:CTF 竞赛排行榜

# 添加战队分数
r.zadd("ctf_rank", {"Team_A": 100, "Team_B": 250, "Team_C": 50})

# 获取第一名 (按分数从高到低, 0-0 即取第一个)
top1 = r.zrevrange("ctf_rank", 0, 0, withscores=True)
print(f"当前第一名: {top1}") # [('Team_B', 250.0)]

# Team_C 解出一题,加 200 分
r.zincrby("ctf_rank", 200, "Team_C")

# 查看最新全榜
print(f"最新排名: {r.zrevrange('ctf_rank', 0, -1)}")

三、缓存系统的经典问题 (“缓存三兄弟”)

做缓存设计时,必须考虑这三个极端场景。

1. 缓存穿透 (Cache Penetration)

现象:请求绕过缓存,直接查询数据库。但数据库里也没有这个数据。 后果:缓存完全失效,高并发下数据库被不存在的查询打垮。

场景模拟:

  1. 攻击者请求 id = -1id = 99999999 (不存在的 ID)。
  2. Redis 查不到 -> 去查 DB。
  3. DB 也查不到 -> 不写入 Redis(通常逻辑是查到了才写缓存)。
  4. 循环上述过程,数据库压力爆满。

解决方案:

A. 缓存空对象 (Cache Null)

当 DB 查不到时,也在 Redis 里存一个特殊值(如 NULLNOT_FOUND),并设置较短的过期时间。

# 伪代码
val = redis.get(key)
if not val:
    val = db.query(key)
    if not val:
        # 防穿透:写入空值,过期时间设短一点
        redis.set(key, "NULL", ex=60) 
    else:
        redis.set(key, val, ex=3600)

B. 布隆过滤器 (Bloom Filter)

布隆过滤器是一种空间效率极高的数据结构,用于判断**“某样东西一定不存在(或者可能存在)”**。

它不存具体数据,只存“指纹”(Hash 位)。

原理举例(签到表): 有一张长纸条(Bit Array),初始全为 0。

  1. 存储 "Alice":用 3 个哈希函数算出位置 1, 4, 7,把这 3 个格子打钩(置 1)。
  2. 查询 "Bob":算出位置 2, 5, 8。一看这 3 个格子不全为 1,直接断定 Bob 绝对不在
  3. 查询 "Eve":算出位置 1, 4, 7。一看全是 1(Alice 打的钩)。过滤器会说 “Eve 可能在”(实际是误判)。

特点:

2. 缓存击穿 (Cache Breakdown)

现象:一个超级热点 Key(如秒杀商品、突发新闻)突然过期。 后果:那一瞬间,成千上万的并发请求同时发现缓存失效,同时涌向数据库,瞬间击垮数据库。

解决方案:

3. 缓存雪崩 (Cache Avalanche)

现象大量 Key 在同一时间集中过期,或者 Redis 服务宕机。 后果:整个缓存层失效,流量如同雪崩一样全部砸向数据库。

解决方案:

编辑这篇文章

评论区

使用 GitHub Discussions 驱动,欢迎留言交流。

上一篇
Redis 与 Lua 脚本(Part 2)
下一篇
HnuCTF2026竞赛平台搭建和运维记录