Redis 是什么

Redis 是内存型键值数据库,读写速度在微秒级,是后端缓存的绝对主力。核心价值:把高频读的数据放内存,减轻数据库压力。

一、五大数据类型

1. String(字符串)

最基础的类型,存 JSON 序列化结果、计数器、验证码:

1
2
3
4
5
6
SET user:1 '{"name":"张三","age":25}'
GET user:1

# 计数器(原子自增,秒杀/点赞)
INCR visit:count
INCRBY article:100:like 2

注意:INCR 是原子操作,并发下不会出错,这是 String 做计数器的原因。

2. Hash(哈希)

适合存对象,字段可单独操作:

1
2
3
4
HSET user:100 name 张三 age 25
HGET user:100 name
HGETALL user:100
HINCRBY user:100 age 1

场景:用户信息、购物车(HSET cart:1001 sku_1 2)。

3. List(列表)

双向链表,适合消息队列和时间线:

1
2
3
4
5
6
# 右侧入队,左侧出队 = 先进先出队列
LPUSH queue:task task1 task2
RPOP queue:task

# 分页:取最新 N 条(比如最新评论)
LRANGE feed:news 0 9

4. Set(集合)

自动去重,适合标签、关注关系:

1
2
3
4
5
6
7
SADD user:1:tags 前端 后端
SISMEMBER user:1:tags 前端 # 是否包含
SINTER user:1:tags user:2:tags # 共同标签(交集)

# 抽奖去重
SADD lottery 1001 1002 1003
SPOP lottery

5. ZSet(有序集合)

带分数(score)排序,适合排行榜:

1
2
3
4
5
6
7
ZADD rank:game 9500 player_a
ZADD rank:game 8800 player_b
ZINCRBY rank:game 500 player_a # 加分自动重排

ZREVRANGE rank:game 0 9 # 前十名
ZSCORE rank:game player_a # 查分数
ZRANK rank:game player_a # 查名次

二、过期时间:缓存的命根子

1
2
3
SET session:token abc123 EX 3600    # 1 小时过期
EXPIRE cache:key 60 # 已有 key 设过期
TTL cache:key # 查剩余秒数(-1 永不过期,-2 不存在)

所有缓存都必须设过期时间,否则一旦数据变了,用户永远读到旧值。

三、Node.js 接入(ioredis)

1
npm install ioredis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Redis = require('ioredis')

const redis = new Redis({
host: '127.0.0.1',
port: 6379,
password: process.env.REDIS_PASSWORD,
// 连接池 + 重试
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 200, 2000)
})

// 基本的缓存读写
async function getUser(id) {
const cached = await redis.get(`user:${id}`)
if (cached) return JSON.parse(cached) // 缓存命中

const user = await db.query('SELECT * FROM users WHERE id = ?', [id])
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 600) // 10 分钟
return user
}

四、三个经典缓存问题

1. 缓存穿透:查不存在的数据

请求一个永远查不到的 key(如恶意遍历 id),每次都打到数据库。

解决:缓存空值 + 布隆过滤器

1
2
3
4
5
6
7
8
const cached = await redis.get(`user:${id}`)
if (cached !== null) {
// 缓存了空值('null')也直接返回,不查库
return cached === 'NULL' ? null : JSON.parse(cached)
}
const user = await db.query(...)
// 查不到也缓存空值,短过期防止穿透
await redis.set(`user:${id}`, user ? JSON.stringify(user) : 'NULL', 'EX', 300)

2. 缓存击穿:热点 key 过期

某个超高并发的 key 过期的瞬间,大量请求同时打数据库。

解决:互斥锁(只让一个请求去重建缓存)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function getHotData() {
const cached = await redis.get('hot:data')
if (cached) return JSON.parse(cached)

// 抢锁:拿不到锁说明别人正在重建,短暂等待后重试
const lock = await redis.set('lock:hot:data', '1', 'EX', 10, 'NX')
if (lock) {
try {
const data = await db.query(...)
await redis.set('hot:data', JSON.stringify(data), 'EX', 600)
return data
} finally {
await redis.del('lock:hot:data')
}
} else {
await sleep(50)
return getHotData() // 重试读缓存
}
}

3. 缓存雪崩:大量 key 同时过期

很多 key 同一时刻过期,数据库瞬时压力爆表。

解决:过期时间加随机偏移

1
2
// 过期时间 = 基准 + 随机 0~60 秒
await redis.set(key, value, 'EX', 600 + Math.floor(Math.random() * 60))

五、缓存一致性问题(必知)

缓存和数据库双写,怎么保证一致?业界共识:先更新数据库,再删缓存(Cache Aside):

1
2
3
// 更新
await db.query('UPDATE users SET name=? WHERE id=?', [name, id])
await redis.del(`user:${id}`) // 删缓存,下次读取时重建

不要先删缓存再更新库:中间会有并发读到旧数据写回,缓存永远脏。

六、常见坑

  1. Redis 当数据库用:内存贵、有淘汰策略(LRU),只放高频热数据;
  2. 没设过期时间:缓存永不失效,数据更新后一直脏;
  3. 大 key 问题:单个 value 几 MB 会导致 Redis 阻塞,大数据要拆分;
  4. 连接数打满:每个请求都 new Redis 实例会耗尽连接,用连接池(ioredis 默认复用);
  5. 生产环境 Redis 没密码/没绑定内网:默认可被外网扫描爆破,务必配置。

总结

Redis 的掌握路径:五大类型记场景(String 缓存/计数、Hash 对象、List 队列、Set 去重、ZSet 排行榜)→ 过期时间必须设 → 用 ioredis 接入 → 解决穿透/击穿/雪崩 → 记住”先更库再删缓存”。这套是后端高频缓存的标准答案。