导读
如果你在做 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 永远不存在)。因为没有数据,代码不会往缓存写东西,所以每次查询都不走缓存,直奔数据库。
解决方案:
- 布隆过滤器(Bloom Filter):在查缓存之前,先用布隆过滤器判断 key 是否可能存在。布隆过滤器是一种概率型数据结构,空间效率极高——如果一个 key 说"不存在",那一定不存在;如果说"可能存在",有大概率是真的存在。
- 缓存空值:如果数据库查不到结果,就把一个"空值"(如
"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,发现缓存没了,全冲向数据库。
解决方案:
- 互斥锁(Mutex Lock):只有一个线程去查数据库并重建缓存,其他线程等待。
- 永不过期(逻辑过期):不设置物理过期时间,而是把过期时间存在值里面。后台异步刷新,读的时候如果发现接近过期就触发刷新但不阻塞读。
# 互斥锁方案
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 连接池爆满,服务全面瘫痪。
解决方案:
- TTL 加随机偏移:不要让所有 key 同一时刻过期。在基础 TTL 上加一段随机数(比如基础 60 分钟 + 随机 0~30 分钟),让过期时间均匀分布。
- 多级缓存:本地缓存(如 Python dict / Go sync.Map)+ Redis 两层。第一层挂了还有第二层兜底。
- 限流降级:Redis 挂了之后,通过网关层限制进入系统的请求数量,保护数据库。
- 高可用架构: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 对比总结
| 对比项 | RDB | AOF |
|---|---|---|
| 原理 | 定时快照 | 逐条记命令日志 |
| 文件大小 | 小(压缩快照) | 大(所有写命令) |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 较差(可能有分钟级差距) | 好(可选每命令落盘) |
| 对性能影响 | 小(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后台运行。正确做法是用 systemd 或 supervisor 管理进程。
启动验证:
# 本地启动(终端输出日志)
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 同名路径)下载。
请完成验证后查看评论区