导读

如果你在做 Web 应用或者小程序后端,大概率会听到"加一层缓存"的说法。最常见的选择就是 Redis——一个跑在内存里的"超级字典"。本文先带你看清它的五种数据结构(别被名字吓到,都是日常能理解的),然后重点讲缓存系统最容易踩的三个坑:穿透、击穿、雪崩——为什么会出现、怎么解决。最后还会覆盖持久化(RDB vs AOF)、过期策略与内存淘汰,以及用 Python(redis-py)快速上手。全文面向零基础读者,每个术语出现时都会给一句白话解释。


一、Redis 是什么?为什么快?

Redis(REmote DIctionary Server)是一个开源的 key-value 内存数据库。你可以把它理解成一个"超大号的字典"——你告诉它一个关键字(key),它就立刻告诉你对应的值(value)。

# 往 Redis 存一条数据:key = name, value = 笑尘主人
127.0.0.1:6379> SET name "笑尘主人"
OK

# 取出来
127.0.0.1:6379> GET name
"笑尘主人"

为什么比传统数据库快?

对比项MySQL 等传统数据库Redis
数据存储位置硬盘(磁盘 I/O 慢)内存(速度是硬盘的数万倍)
查询方式SQL 语句(需要解析和执行计划)直接 key → value 查找
典型延迟毫秒级 ~ 几十毫秒微秒级 ~ 1 毫秒

一句话:Redis 把最常被访问的数据放在内存里,请求来了不用去硬盘翻找,所以极快。


二、五种数据结构,对应不同场景

Redis 不只是简单的 SET/GET,它提供了丰富的数据结构,每种都适合不同的使用场景。

2.1 String(字符串)—— 最基础的类型

String 是最常用的类型,可以存文本、数字,甚至图片的二进制数据。

# 基本读写
SET user:1001 '{"name":"小明","age":25}'
GET user:1001

# 计数器(Web 访问量统计常用)
INCR page_views              # 每次 +1
INCRBY page_views 100        # 每次 +100

# 设置过期时间(300 秒后自动删除)
SETEX token:abc 300 "session-data-here"

# 批量操作(一次 SET/GET 多个键)
MSET city:beijing "北京" city:shanghai "上海"
MGET city:beijing city:shanghai

常见场景:用户会话存储、计数器、验证码 TTL、配置项缓存。

2.2 Hash(哈希表)—— 像对象一样的存储

Hash 内部是一个 field-value 对集合,非常适合存储"对象"。

# 存入用户信息(类似 JSON 但更高效)
HSET user:1001 name "小明" age 25 email "[email protected]"

# 读取单个字段
HGET user:1001 name          # "小明"

# 读取所有字段
HGETALL user:1001

# 更新单个字段
HINCRBY user:1001 age 1      # age +1

# 判断字段是否存在
HEXISTS user:1001 phone       # 返回 0(不存在)

常见场景:用户资料、商品详情、表单数据。相比 String 存整段 JSON,Hash 只修改某个字段不需要整体读写。

2.3 List(列表)—— 有序的可重复元素

List 按插入顺序排列,允许重复元素,两端都可以进出。

# 右侧推送消息(尾部入队)
RPUSH news_feed "新闻A" "新闻B" "新闻C"

# 左侧弹出(头部出队)—— FIFO 队列
LPOP news_feed               # "新闻A"

# 查看列表长度
LLEN news_feed               # 2(已弹出一个)

# 截取子列表(索引从 0 开始)
LRANGE news_feed 0 -1        # 看全部剩余元素

# 限定列表最大长度(超过自动删最旧的)
LPUSH chat_history "你好" "在吗" "吃了吗"
LTRIM chat_history 0 9       # 只保留最近 10 条

常见场景:最新消息流、任务队列、限流窗口、最近浏览记录。

2.4 Set(集合)—— 不重复的元素

Set 中的元素不允许重复,天然支持并集、交集、差集运算。

# 添加标签(自动去重)
SADD article:1 tags "Redis" "数据库" "缓存" "Redis"   # Redis 只算一次
SCARD article:1         # 3(实际只有 3 个唯一值)

# 判断某元素是否存在
SMEMBERS article:1      # {"Redis","数据库","缓存"}
SISMEMBER article:1 "缓存"    # 1(存在)

# 两个集合的交集(共同关注的话题)
SINTER user:1.tags user:2.tags

# 共同粉丝(两用户的收藏交集)
SINTER user:1.collect user:2.collect

常见场景:标签系统、共同好友/关注、黑名单(成员即黑名单,O(1) 查询)。

2.5 ZSet / Sorted Set(有序集合)—— 带分数的集合

ZSet 和 Set 一样元素不能重复,但每个元素关联一个分数(score),按分数从小到大排序。

# 排行榜:元素 + 分数
ZADD leaderboard 100 "player_a"  85 "player_b"  92 "player_c"

# 获取排名(从高到低:REV 反向;WITHSCORES 显示分数)
ZREVRANGE leaderboard 0 2 WITHSCORES

# 指定分数范围的成员
ZRANGEBYSCORE leaderboard 80 100

# 增加某人分数(累计得分)
ZINCRBY leaderboard 15 "player_a"    # player_a 变成 115 分

常见场景:排行榜、延时队列(用时间戳做分数)、范围查询(年龄在 18~60 岁的用户)。


三、缓存三大问题:穿透、击穿、雪崩

这是生产环境中最容易遇到的三个"灾难性"问题。理解它们之前,先看正常流程是怎样的:

用户请求 → 查缓存(Redis)→ 命中?
  ├─ 是 → 直接返回数据 ✓
  └─ 否 → 查数据库 → 把结果写入缓存 → 返回数据

当这个流程出现异常,就会引发下面三种问题。

3.1 缓存穿透(Cache Penetration)

白话:有人一直查一个根本不存在的数据,缓存里当然没有,每次都打到数据库上。如果量很大,数据库直接被拖垮。

例子:一个恶意用户不断请求 user:999999(这个 ID 永远不存在)。因为没有数据,代码不会往缓存写东西,所以每次查询都不走缓存,直奔数据库

解决方案:

  1. 布隆过滤器(Bloom Filter):在查缓存之前,先用布隆过滤器判断 key 是否可能存在。布隆过滤器是一种概率型数据结构,空间效率极高——如果一个 key 说"不存在",那一定不存在;如果说"可能存在",有大概率是真的存在。
  2. 缓存空值:如果数据库查不到结果,就把一个"空值"(如 "null" 或空字符串)写入缓存,并设很短的过期时间(比如 5 分钟)。这样后续相同请求就直接命中缓存的空值,不会再打到数据库。
# Python 伪代码示例
def get_user(user_id):
    # 1. 先查布隆过滤器
    if not bloom_filter.exists(f"user:{user_id}"):
        return None  # 肯定不存在,直接返回

    # 2. 再查缓存
    data = redis.get(f"user:{user_id}")
    if data is None:
        # 3. 查数据库
        data = db.query_user(user_id)
        # 4. 无论有没有查到,都缓存(空值也缓存 5 分钟)
        ttl = 300 if data else 300
        redis.setex(f"user:{user_id}", ttl, data or "")
    return data

3.2 缓存击穿(Cache Breakdown)

白话:一个非常热门的关键字(比如微博热搜的第一名)突然过期了。那一瞬间,海量请求同时涌向数据库,数据库扛不住。

注意:击穿只影响单个热点 key,而后面要说的"雪崩"是影响一批 key。

例子:某电商大促时,“iPhone 16 价格"这个 key 设置了 30 分钟过期。到期那一刻,成千上万的并发请求同时来查这个 key,发现缓存没了,全冲向数据库。

解决方案:

  1. 互斥锁(Mutex Lock):只有一个线程去查数据库并重建缓存,其他线程等待。
  2. 永不过期(逻辑过期):不设置物理过期时间,而是把过期时间存在值里面。后台异步刷新,读的时候如果发现接近过期就触发刷新但不阻塞读。
# 互斥锁方案
import time
import redis

def get_hot_key(key):
    data = redis.get(key)
    if data:
        return data

    # 缓存未命中,尝试获取分布式锁
    lock_key = f"lock:{key}"
    # SET NX EX 原子操作:不存在则设置,3秒超时
    locked = redis.set(lock_key, "1", nx=True, ex=3)

    if locked:
        try:
            # 拿到锁的线程去 DB 查并重建缓存
            data = db.query(key)
            redis.setex(key, 1800, data)  # 30 分钟
            return data
        finally:
            redis.delete(lock_key)  # 释放锁
    else:
        # 没拿到锁,等 50ms 再试(避免死循环打满 CPU)
        time.sleep(0.05)
        return get_hot_key(key)     # 递归重试

3.3 缓存雪崩(Cache Avalanche)

白话大批量的缓存 Key 在同一时间失效(或者整个 Redis 宕机),导致流量全部涌向数据库,数据库被压垮,整个服务挂了。

例子:全站的商品缓存统一设置的 1 小时过期。到点之后所有商品缓存同时过期,百万级请求涌入数据库,DB 连接池爆满,服务全面瘫痪。

解决方案:

  1. TTL 加随机偏移:不要让所有 key 同一时刻过期。在基础 TTL 上加一段随机数(比如基础 60 分钟 + 随机 0~30 分钟),让过期时间均匀分布。
  2. 多级缓存:本地缓存(如 Python dict / Go sync.Map)+ Redis 两层。第一层挂了还有第二层兜底。
  3. 限流降级:Redis 挂了之后,通过网关层限制进入系统的请求数量,保护数据库。
  4. 高可用架构:Redis 哨兵模式(Sentinel)或集群模式(Cluster),单节点挂了自动故障转移。
import random

def set_with_random_ttl(key, value, base_ttl=3600):
    """
    base_ttl: 基础过期时间(秒)
    加上 0~base_ttl//10 的随机偏移,防止集中过期
    """
    jitter = random.randint(0, base_ttl // 10)
    redis.setex(key, base_ttl + jitter, value)

小结:三兄弟对比

问题影响范围核心原因一句话解决
穿透不存在的 key 被反复查数据不存在但请求量大布隆过滤器 / 缓存空值
击穿单个热点 key 过期瞬间大流量 + key 恰好到期互斥锁 / 永不过期
雪崩大量 key 同时失效或 Redis 宕机TTL 一致 / 实例挂掉随机 TTL / 多级缓存 / 限流

四、过期策略与内存淘汰

4.1 过期策略(Expiration Policy)

Redis 的每个 key 都可以设置过期时间(EXPIRE / PEXPIRE / SETEX / PSETEX)。到期后 Redis 会清理这些 key,清理有三种策略:

策略行为优点缺点
惰性删除(Lazy)只在访问这个 key 时才检查是否过期不浪费 CPU过期 key 可能长期占用内存
定期删除(Periodic)Redis 每隔一段时间抽查一批 key,过期的就删掉平衡 CPU 和内存不能保证所有过期 key 都被清除
主动淘汰(Eviction)当内存满了,按淘汰策略选择要删除的 key保证内存够用可能误删还没过期的有用数据

Redis 实际采用惰性删除 + 定期删除的组合:

  • 惰性删除保证你访问到的 key 一定是新鲜的
  • 定期删除(默认每秒 10 次)清扫大部分过期 key
  • 定期删除也有上限:每次最多扫 25 个 key(hz 参数可调,CONFIG SET hz 20),避免拖慢服务端

4.2 内存淘汰策略(Eviction Policy)

当 Redis 的内存达到上限(maxmemory 配置)且没有 key 过期可删时,会触发淘汰策略。可通过 maxmemory-policy 配置:

策略名含义适用场景
noeviction不淘汰,直接返回错误(默认策略)数据绝对不能丢的场景
allkeys-lru所有 key 中移除最不常用的通用缓存推荐
volatile-lru有过期时间的 key 中移除最不常用的部分关键数据不设过期
allkeys-random随机移除任意 key简单场景,所有 key 重要性差不多
volatile-ttl移除即将过期的 key想让短期数据优先存活
volatile-random随机移除有过期时间的 key

推荐使用 allkeys-lru:LRU = Least Recently Used(最近最少使用),意思是"很久没用过的数据最先被淘汰”。这正好符合缓存的本质——不常用的数据留着也占地方。

# 设置内存上限为 512MB
CONFIG SET maxmemory 512mb

# 设置淘汰策略为 LRU
CONFIG SET maxmemory-policy allkeys-lru

⚠️ 注意:CONFIG SET 是运行时生效,重启后会恢复默认。要永久生效,改配置文件 redis.conf

maxmemory 512mb
maxmemory-policy allkeys-lru

五、持久化:RDB vs AOF

Redis 是内存数据库,断电了数据就没了。持久化的目的就是把内存中的数据保存到硬盘,重启后能恢复。Redis 提供两种持久化方式,可以同时开启。

5.1 RDB(快照持久化)

RDB 就是"拍一张照片"——在某个时间点把内存中的数据全部写到磁盘上一个文件(默认 dump.rdb)。

# 手动触发一次快照
SAVE          # 阻塞式:所有请求停下来等写盘
BGSAVE        # 非阻塞式:后台 fork 进程写盘(推荐)

触发条件(在 redis.conf 中配置)

# 满足任一条件就自动触发 BGSAVE:
save 900 1      # 900 秒内至少 1 个 key 改了
save 300 10     # 300 秒内至少 10 个 key 改了
save 60 10000   # 60 秒内至少 10000 个 key 改了

优点

  • 文件紧凑,适合备份和恢复
  • fork 子进程写盘,不影响主进程服务

缺点

  • 两次快照之间丢失的数据无法恢复(比如 15 分钟才触发的下一次快照)
  • fork 大内存时可能消耗较多资源

5.2 AOF(追加日志持久化)

AOF 记录的是每条写命令(类似 MySQL 的 binlog),追加到一个日志文件(默认 appendonly.aof)。重启时重新执行这些命令恢复数据。

# redis.conf 中开启 AOF
appendonly yes
appendfsync everysec       # 每秒刷盘一次

appendfsync 有三个选项:

选项行为性能安全性
always每条命令立即落盘最安全(零丢失)
everysec(推荐)每秒刷盘一次中等可丢失 1 秒数据
no由操作系统决定何时刷盘最快最不安全

优点

  • 数据更安全(最多丢 1 秒)
  • 文件不可篡改,即使中途崩溃也可以 redis-check-aof 修复

缺点

  • 文件通常比 RDB 大
  • 恢复速度比 RDB 慢(需要回放命令)

5.3 混合持久化(Redis 4.0+)

Redis 4.0 引入了混合持久化:AOF 重写时,把快照用 RDB 格式写,后续的增量命令用 AOF 格式追加。既保证了恢复速度(RDB 部分快速加载),又保证了数据安全(AOF 部分精确回放)。

aof-use-rdb-preamble yes    # 开启混合持久化(推荐)

5.4 对比总结

对比项RDBAOF
原理定时快照逐条记命令日志
文件大小小(压缩快照)大(所有写命令)
恢复速度
数据安全性较差(可能有分钟级差距)好(可选每命令落盘)
对性能影响小(fork 子进程)取决于 flush 策略
推荐用途容灾备份数据保护

生产环境建议:两者都开。RDB 做定期冷备,AOF 做热恢复。如果追求极致安全就用 AOF + always;追求性能就用 AOF everysec + 间隔 RDB。


六、Python(redis-py)与 Node.js 接入示例

6.1 Python 接入(redis-py)

安装:

pip install redis

完整示例(含缓存三兄弟问题的处理思路):

import redis
import json
import time
import random

# 连接 Redis(默认 localhost:6379)
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

# ====== String ======
# 存一个用户信息
r.set("user:1", json.dumps({"name": "小明", "age": 25}))
print(r.get("user:1"))            # {"name": "小明", "age": 25}

# ====== Hash ======
r.hset("product:100", mapping={
    "name": "无线鼠标",
    "price": "99.9",
    "stock": "500"
})
print(r.hget("product:100", "name"))    # 无线鼠标

# ====== 缓存穿透:缓存空值 ======
def get_user_safe(user_id):
    cache_key = f"user:{user_id}"
    data = r.get(cache_key)
    if data is not None:
        return json.loads(data) if data != "" else None

    # 缓存为空,查数据库(模拟)
    result = query_database(user_id)

    if result:
        r.setex(cache_key, 1800, json.dumps(result))  # 正常数据缓存 30 分钟
    else:
        r.setex(cache_key, 300, "")                     # 空值缓存 5 分钟

    return result


def query_database(user_id):
    """模拟数据库查询"""
    users = {1: {"name": "小明", "age": 25}, 2: {"name": "小红", "age": 22}}
    return users.get(user_id)

# ====== 缓存击穿:互斥锁 ======
def get_product_lock(product_id):
    cache_key = f"product:{product_id}"
    data = r.get(cache_key)
    if data:
        return json.loads(data)

    lock_key = f"lock:product:{product_id}"
    # 原子操作:仅当 lock_key 不存在时才设置,3 秒超时
    if r.set(lock_key, "1", nx=True, ex=3):
        try:
            data = query_db_for_product(product_id)
            r.setex(cache_key, 3600, json.dumps(data))  # 缓存 1 小时
            return data
        finally:
            r.delete(lock_key)
    else:
        time.sleep(0.05)
        return get_product_lock(product_id)          # 重试


def query_db_for_product(product_id):
    """模拟数据库查询商品信息"""
    return {"id": product_id, "name": "保温杯", "price": "49.9"}

# ====== 缓存雪崩:随机 TTL ======
def set_with_jitter(key, value, base_ttl=3600):
    """基础 TTL + 随机偏移,防止批量过期"""
    jitter = random.randint(0, base_ttl // 10)
    r.setex(key, base_ttl + jitter, value)
    print(f"{key} 已设置 TTL = {base_ttl + jitter} 秒")

# ====== ZSet 排行榜 ======
r.zadd("game_rank", {"Alice": 1200, "Bob": 980, "Carol": 1050})
top3 = r.zrevrange("game_rank", 0, 2, withscores=True)
for name, score in top3:
    print(f"  {name}: {score} 分")

6.2 Node.js 接入(ioredis)

安装:

npm install ioredis
const Redis = require('ioredis');
const redis = new Redis({ host: '127.0.0.1', port: 6379 });

// String
await redis.set('greeting', 'Hello Redis!');
console.log(await redis.get('greeting'));  // Hello Redis!

// Hash
await redis.hset('config', 'theme', 'dark', 'lang', 'zh');
const theme = await redis.hget('config', 'theme');  // 'dark'

// Set 共同标签
await redis.sadd('tag:a', 'Redis', 'Database', 'Cache');
await redis.sadd('tag:b', 'Redis', 'Performance');
const common = await redis.sinter('tag:a', 'tag:b'); // ['Redis']

// ZSet 排行榜
await redis.zadd('rank', 100, 'peter', 200, 'alice', 150, 'bob');
const leaders = await redis.zrevrange('rank', 0, 2, 'WITHSCORES');
console.log(leaders);  // [ 'alice', '200', 'bob', '150', 'peter', '100' ]

// 设置随机 TTL 防雪崩
async function setWithJitter(key, value, baseTtl = 3600) {
  const jitter = Math.floor(Math.random() * (baseTtl / 10));
  await redis.setex(key, baseTtl + jitter, value);
  console.log(`${key} → TTL ${baseTtl + jitter}s`);
}

redis.quit();

七、安全配置要点

Redis 默认没有密码、绑定本机,在生产环境中必须调整以下配置:

# 1. 绑定 IP(不要绑定 0.0.0.0!)
bind 127.0.0.1 ::1           # 只允许本机访问

# 如果需远程访问,绑内网 IP:
# bind 192.168.1.100         # 假设你的服务器内网地址

# 2. 设置密码
requirepass YourStrongPassword123

# 3. 关闭危险命令(rename-command 重命名或禁用)
# rename-command FLUSHALL ""       # 彻底禁用 FLUSHALL
# rename-command FLUSHDB ""        # 彻底禁用 FLUSHDB
# rename-command CONFIG ""         # 禁用 CONFIG(也可用 rename-command config cfg 改为安全名)

# 4. 以非 root 用户运行
user redis

# 5.  daemonize 设为 no(如果用 systemd 管理,设为 no)
daemonize no

关于 daemonize:Redis 4.0+ 不再推荐使用 daemonize yes 后台运行。正确做法是用 systemdsupervisor 管理进程。

启动验证:

# 本地启动(终端输出日志)
redis-server --daemonize no

# 或用配置文件启动
redis-server /etc/redis/redis.conf

# 测试连接(需要密码时用 -a)
redis-cli -a YourStrongPassword123 ping
# 应返回 PONG

如果你没有自己的服务器、也不想折腾 Redis 的部署和维护,可以考虑用云端 API 的方式跑业务逻辑。像 云间 API 中转站 这种服务支持 OpenAI 兼容和 Anthropic 兼容协议,有 90+ 模型可选择,香港节点国内直连,价格从 0.05 倍起,适合不想自建基础设施的团队。


八、总结

主题核心要点
数据结构String(计数/会话)/ Hash(对象)/ List(队列)/ Set(去重/交集)/ ZSet(排行榜)
缓存穿透不存在的 key 反复查 → 布隆过滤器 + 缓存空值
缓存击穿热点 key 过期一瞬间 → 互斥锁 + 逻辑过期
缓存雪崩大批量 key 同时过期 → 随机 TTL + 限流 + 多级缓存
持久化RDB(快照,恢复快)+ AOF(命令日志,更安全),两者配合最佳
内存淘汰推荐 allkeys-lru,近久不用的数据最先淘汰
安全bind + requirepass + 禁用危险命令 + 非 root 运行

附:一键安装验证脚本

下面的脚本帮你检测网络环境、安装 Redis、安全配置提示、启动服务和写入读取验证。公开源码,欢迎复制给任何 AI 审查。

#!/usr/bin/env bash
set -u
# ============================================================
#  Redis 一键安装与验证脚本(Linux / macOS / WSL)
#  公开源码,欢迎审查 —— 不放心可先复制给 AI 判断
#  功能:安装 Redis → 安全配置提示 → 启动 → redis-cli 验证
#  无 API Key 需求
# ============================================================

GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${GREEN}[INFO]${NC} $1"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
step()  { echo -e "${CYAN}[STEP]${NC} $1"; }

detect_network() {
  step "检测网络环境(国内 / 国外)..."
  if curl -fsI --max-time 5 "https://claude.ai" >/dev/null 2>&1; then
    info "可直连 claude.ai,判定为海外网络"
    echo "overseas"
  else
    warn "无法直连 claude.ai,判定为国内网络环境(将使用镜像源)"
    echo "domestic"
  fi
}

install_redis() {
  local net="$1"
  step "安装 Redis..."

  if command -v redis-server >/dev/null 2>&1 && redis-server --version >/dev/null 2>&1; then
    info "Redis 已安装:"
    redis-server --version | head -1
    return 0
  fi

  if command -v apt-get >/dev/null 2>&1; then
    # Debian / Ubuntu
    step "使用 apt 安装(Debian/Ubuntu)..."
    if [ "$net" = "domestic" ]; then
      warn "国内 apt 源可能较慢,请耐心等待..."
    fi
    sudo apt-get update -qq >/dev/null 2>&1 || { error "apt-get update 失败"; return 1; }
    if sudo apt-get install -y -qq redis-server >/dev/null 2>&1; then
      info "Redis 安装成功"
      return 0
    fi
    warn "官方 apt 源安装失败,回退编译安装..."
  elif command -v yum >/dev/null 2>&1; then
    # CentOS / RHEL / Fedora
    step "使用 yum 安装(CentOS/RHEL)..."
    if sudo yum install -y epel-release >/dev/null 2>&1 || \
       sudo yum install -y redis >/dev/null 2>&1; then
      info "Redis 安装成功(yum)"
      return 0
    fi
    warn "yum 安装失败,回退编译安装..."
  elif command -v brew >/dev/null 2>&1; then
    # macOS
    step "使用 Homebrew 安装(macOS)..."
    if brew install redis >/dev/null 2>&1; then
      info "Redis 安装成功"
      return 0
    fi
    warn "Homebrew 安装失败"
  fi

  # 最终回退:编译安装
  step "回退到编译安装(下载 Redis 源码...)..."
  local VERSION="7.2.4"
  local URL
  if [ "$net" = "domestic" ]; then
    URL="https://npmmirror.com/mirrors/redis/redis-${VERSION}.tar.gz"
  else
    URL="https://download.redis.io/releases/redis-${VERSION}.tar.gz"
  fi

  if ! curl -fsSL --max-time 60 "$URL" -o /tmp/redis.tar.gz 2>/dev/null; then
    warn "主源下载失败,尝试备用源..."
    if [ "$net" = "domestic" ]; then
      URL="https://download.redis.io/releases/redis-${VERSION}.tar.gz"
    else
      URL="https://npmmirror.com/mirrors/redis/redis-${VERSION}.tar.gz"
    fi
    if ! curl -fsSL --max-time 60 "$URL" -o /tmp/redis.tar.gz 2>/dev/null; then
      error "Redis 源码下载失败,请手动访问 https://redis.io/download 下载后编译安装"
      return 1
    fi
  fi

  tar xzf /tmp/redis.tar.gz -C /tmp/ 2>/dev/null
  step "编译 Redis(耗时约 2-5 分钟,请耐心等候)..."
  cd /tmp/redis-${VERSION} 2>/dev/null || { error "解压失败"; return 1; }
  make -j$(nproc) >/dev/null 2>&1 || { error "编译失败,可能需要 gcc 和 make:sudo apt-get install build-essential"; return 1; }
  sudo make install >/dev/null 2>&1 || { error "make install 失败"; return 1; }
  info "Redis 编译安装成功"
  cd - >/dev/null 2>&1
  rm -rf /tmp/redis-${VERSION} /tmp/redis.tar.gz
  return 0
}

configure_and_start() {
  step "检查 Redis 服务状态..."

  if systemctl is-active --quiet redis-server 2>/dev/null || \
     systemctl is-active --quiet redis 2>/dev/null; then
    info "Redis 服务已在运行"
  else
    info "启动 Redis 服务..."
    sudo systemctl start redis-server 2>/dev/null || \
    sudo systemctl start redis 2>/dev/null || {
      warn "systemctl 启动失败,尝试直接启动..."
      sudo redis-server --daemonize yes --protected-mode no
    }
  fi

  sleep 1

  if redis-cli ping 2>/dev/null | grep -q PONG; then
    info "Redis 响应正常(PONG)"
  else
    error "Redis 未响应,请检查日志:journalctl -u redis-server --no-pager -n 20"
    return 1
  fi

  echo ""
  echo "============================================"
  info "安全配置提醒(生产环境必读):"
  echo ""
  echo "  1. 设置密码:redis-cli CONFIG SET requirepass 'YourPassword'"
  echo "  2. 永久写入配置文件 /etc/redis/redis.conf:"
  echo "     requirepass YourPassword"
  echo ""
  echo "  3. 限制监听地址(生产环境):"
  echo "     bind 127.0.0.1  (仅本机访问)"
  echo ""
  echo "  4. 禁用危险命令(编辑 redis.conf):"
  echo "     rename-command FLUSHALL \"\""
  echo "     rename-command FLUSHDB \"\""
  echo ""
  echo "  5. 如果需要远程访问,请确保防火墙只放许可的 IP:"
  echo "     sudo ufw allow from 192.168.1.0/24 to any port 6379"
  echo "     (ufw deny 6379 关闭所有外部访问)"
  echo ""
  echo "  6. daemonize 说明:Redis 4.0+ 推荐用 systemd 管理"
  echo "     而不是 daemonize yes。当前脚本若用 daemonize"
  echo "     是兜底方案。"
  echo "============================================"

  return 0
}

verify() {
  step "写入和读取验证..."

  redis-cli SET verify:test "Redis is working!" >/dev/null 2>&1 || {
    redis-cli -a YourPassword SET verify:test "Redis is working!" >/dev/null 2>&1 || {
      error "写入验证失败(可能设置了密码但未配置,请检查 requirepass)"
      return 1
    }
  }

  local val
  val=$(redis-cli GET verify:test 2>/dev/null) || \
  val=$(redis-cli -a YourPassword GET verify:test 2>/dev/null)

  if [ "$val" = "Redis is working!" ]; then
    info "验证成功!Redis 写入和读取正常"
  else
    warn "验证结果:$val"
  fi

  # 额外测试几种数据结构
  step "测试多种数据结构..."
  redis-cli HSET test:user name Alice age 30 >/dev/null 2>&1 || true
  redis-cli LPUSH test:list a b c >/dev/null 2>&1 || true
  redis-cli SADD test:set red green blue >/dev/null 2>&1 || true
  redis-cli ZADD test:rank 10 alice 20 bob 30 carol >/dev/null 2>&1 || true
  info "Hash、List、Set、ZSet 写入成功"

  local list_count hash_name rank_top
  list_count=$(redis-cli LLEN test:list 2>/dev/null) || true
  hash_name=$(redis-cli HGET test:user name 2>/dev/null) || true
  rank_top=$(redis-cli ZREVRANGE test:rank 0 0 2>/dev/null) || true
  info "List 长度=${list_count:-?} | Hash name=${hash_name:-?} | ZSet Top1=${rank_top:-?}"

  redis-cli DEL verify:test test:user test:list test:set test:rank >/dev/null 2>&1 || true
  info "测试数据已清理"
}

main() {
  echo "============================================"
  echo "  Redis 一键安装与验证脚本"
  echo "  适用于 Linux / macOS / WSL"
  echo "============================================"
  echo ""

  local NET
  NET=$(detect_network)

  if ! install_redis "$NET"; then
    error "安装失败,请按上方提示手动处理。"
    exit 1
  fi

  if ! configure_and_start; then
    error "启动失败,请检查日志。"
    exit 1
  fi

  verify

  echo ""
  echo "============================================"
  info "全部完成!"
  echo ""
  echo "  交互式客户端:  redis-cli"
  echo "  需要密码时:    redis-cli -a YourPassword"
  echo "  停止服务:      sudo systemctl stop redis-server"
  echo "  查看状态:      sudo systemctl status redis-server"
  echo "  配置文件:      /etc/redis/redis.conf"
  echo ""
  echo "  不想自建?试试云端 API(如 https://cloudzone-api.cyou/"
  echo "  支持 OpenAI 兼容 + Anthropic 兼容,90+ 模型,国内直连)"
  echo "============================================"
}

main "$@"
# ============================================================
#  Redis 一键安装与验证脚本(Windows PowerShell)
#  公开源码,欢迎审查 -- 不放心可先复制给 AI 判断
#  说明:Redis 官方不支持 Windows 原生运行,本脚本检测 WSL,
#        并把 Linux 安装脚本送入 WSL 内执行(一站式完成)。
# ============================================================
$ErrorActionPreference = "Stop"

function Write-Info  { Write-Host "[INFO] $args" -ForegroundColor Green }
function Write-Warn  { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Err   { Write-Host "[ERROR] $args" -ForegroundColor Red }
function Write-Step  { Write-Host "[STEP] $args" -ForegroundColor Cyan }

# 0. 临时提高执行策略仅限当前会话(安全说明:-Scope Process 仅本次有效,关窗即恢复)
try {
    Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
} catch {
    Write-Warn "无法修改执行策略,继续运行..."
}

Write-Step "检查 WSL 环境..."
if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
    Write-Err "未找到 wsl 命令。请先安装 WSL:"
    Write-Host "  PowerShell(管理员)执行:wsl --install"
    Write-Host "  重启后安装 Ubuntu 发行版,再运行本脚本。"
    exit 1
}

try {
    $wslHome = (wsl -e sh -c 'echo $HOME' 2>$null).Trim()
} catch {
    $wslHome = "/root"
}
if ([string]::IsNullOrWhiteSpace($wslHome)) { $wslHome = "/root" }
Write-Info "WSL 主目录:$wslHome"

Write-Host ""
Write-Host "此脚本将在 WSL(Linux)中安装 Redis。"
Write-Host "如果你只需连接远程 Redis 服务器(无需本地安装),可以跳过本脚本。"
$continue = Read-Host "继续安装 Redis?[Y/N]"
if ($continue -ne "Y" -and $continue -ne "y") {
    Write-Info "已选择跳过。"
    exit 0
}

Write-Step "将安装脚本传入 WSL 并执行(首次会在 WSL 内安装依赖,耗时较长)..."

$scriptPath = Join-Path $env:TEMP "install-redis-guide-wsl.sh"
$dirOfScript = Split-Path $MyInvocation.MyCommand.Path -Parent

# 优先从同目录获取 .sh 脚本
if (-not (Test-Path $scriptPath)) {
    $shPath = Join-Path $dirOfScript "install-redis-guide.sh"
    if (Test-Path $shPath) {
        Copy-Item $shPath $scriptPath -Force
    } elseif (Test-Path "$PSScriptRoot\install-redis-guide.sh") {
        Copy-Item "$PSScriptRoot\install-redis-guide.sh" $scriptPath -Force
    }
}

if (-not (Test-Path $scriptPath)) {
    Write-Warn "未找到同目录的 install-redis-guide.sh。"
    Write-Host "请将本 .ps1 与 install-redis-guide.sh 放在同一目录后重跑。"
    Write-Host "(或者手动在 WSL 中安装:sudo apt-get install -y redis-server)"
    exit 1
}

$remotePath = "/tmp/install-redis-guide-wsl.sh"
$tempWindows = [System.IO.Path]::GetFullPath($env:TEMP)
$mntPath = "/mnt/c" + $tempWindows -replace '^C:', '' -replace '\\', '/'

try {
    wsl -e cp "$mntPath/install-redis-guide-wsl.sh" $remotePath
    Write-Info "脚本已传入 WSL:$remotePath"
} catch {
    Write-Err "传入 WSL 失败:$_"
    Write-Host "请手动在 WSL 中执行:sudo apt-get install -y redis-server"
    exit 1
}

Write-Host ""
Write-Host "正在 WSL 内执行安装(日志将直接输出)……"
Write-Host "============================================"
wsl -e bash $remotePath
$code = $LASTEXITCODE
Write-Host "============================================"

if ($code -ne 0) {
    Write-Err "WSL 内安装失败(退出码 $code)。请对照上方报错处理,或手动在 WSL 中重试。"
    exit 1
}

Write-Host ""
Write-Host "============================================"
Write-Info "全部完成!接下来在 WSL 终端里:"
Write-Host "    redis-cli                    # 启动交互式客户端"
Write-Host "    redis-cli ping               # 应返回 PONG"
Write-Host "============================================"
Write-Host ""
Write-Host "不方便下载的同学可以直接复制下方完整源码,"
Write-Host "新建文本文档粘贴后改后缀为 .sh 或 .ps1 运行。"
Write-Host "脚本公开源码,欢迎复制给任何 AI 审查。"

也不方便下载的同学可以直接复制下方完整源码,新建文件粘贴后改后缀为 .sh.ps1 运行。脚本公开源码,欢迎复制给任何 AI 审查。

也可从 https://cleanresolver.com/scripts/install-redis-guide.sh.ps1 同名路径)下载。