Redis缓存踩坑记录

Redis缓存踩坑记录

记录Redis在生产环境部署和使用中遇到的问题和解决方案。

基础配置与连接

1.1 Redis安装配置

CentOS 7安装:

1
2
3
4
5
6
7
8
9
10
11
# 安装Redis
yum install -y redis

# 启动服务
systemctl start redis

# 设置开机自启
systemctl enable redis

# 查看状态
systemctl status redis

Windows环境配置(开发环境):

1
2
3
4
5
6
7
8
9
10
11
# 1. 下载Redis for Windows
# 2. 修改配置文件 redis.windows.conf

# 注释掉bind 127.0.0.1以允许远程访问
# bind 127.0.0.1

# 关闭保护模式
protected-mode no

# 3. 重启Redis服务
redis-server.exe redis.windows.conf

1.2 配置文件详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# /etc/redis.conf 核心配置

# 绑定地址
bind 0.0.0.0

# 端口
port 6379

# 保护模式
protected-mode yes

# 密码认证
requirepass your_strong_password

# 持久化配置
save 900 1 # 900秒内至少1次修改则触发RDB
save 300 10 # 300秒内至少10次修改则触发RDB
save 60 10000 # 60秒内至少10000次修改则触发RDB

# AOF持久化
appendonly yes
appendfsync everysec

# 内存限制
maxmemory 256mb
maxmemory-policy allkeys-lru

# 日志级别
loglevel notice

1.3 连接认证

命令行连接:

1
2
3
4
5
6
7
8
9
10
# 方式1:命令行直接认证
redis-cli -h 127.0.0.1 -p 6379 -a your_password

# 方式2:连接后认证
redis-cli -h 127.0.0.1 -p 6379
auth your_password

# 方式3:远程连接
redis-cli -h xxx.xxx.xxx.xxx -p 6379
auth your_password

二、Node.js Redis客户端

2.1 ioredis客户端

ioredis是Node.js中功能最完备的Redis客户端。

安装:

1
npm install ioredis

基础配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const Redis = require('ioredis');

// 基础连接
const redis = new Redis({
port: 6379,
host: '127.0.0.1',
password: 'your_password',
db: 0,
// 连接池配置
retryStrategy(times) {
const delay = Math.min(times * 50, 2000);
return delay;
},
// 重连配置
maxRetriesPerRequest: 3,
enableReadyCheck: true
});

// 事件监听
redis.on('connect', () => {
console.log('Redis连接成功');
});

redis.on('error', (err) => {
console.error('Redis错误:', err);
});

redis.on('reconnecting', () => {
console.log('Redis重新连接中...');
});

基础使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 设置键值
await redis.set('key', 'value');

// 获取值
const value = await redis.get('key');
console.log(value); // 'value'

// 设置过期时间(秒)
await redis.setex('session', 3600, 'user_data');

// 使用Promise
redis.get('key').then(result => {
console.log(result);
});

2.2 连接错误处理

常见问题:NOAUTH Authentication required

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 错误配置
const redis = new Redis({
port: 6379,
host: 'xxx.xxx.xxx.xxx',
pass: 'wrong_key_name' // 错误:应该是password
});

// 正确配置
const redis = new Redis({
port: 6379,
host: 'xxx.xxx.xxx.xxx',
password: 'your_password' // 正确
});

// 或使用auth方法
const redis = new Redis({
port: 6379,
host: 'xxx.xxx.xxx.xxx'
});
redis.auth('your_password');

2.3 集群配置

1
2
3
4
5
6
7
8
9
10
11
12
// Redis集群配置
const redis = new Redis.Cluster([
{ port: 7000, host: '192.168.1.1' },
{ port: 7001, host: '192.168.1.1' },
{ port: 7002, host: '192.168.1.2' }
], {
redisOptions: {
password: 'cluster_password'
},
slotsRefreshTimeout: 2000,
slotsRefreshInterval: 5000
});

三、缓存设计模式

3.1 四种经典缓存模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────┐
│ 缓存设计模式 │
├─────────────────────────────────────────────────────────┤
│ 1. Cache Aside (旁路缓存) │
│ 应用同时操作缓存和数据库 │
├─────────────────────────────────────────────────────────┤
│ 2. Read Through (读穿透) │
│ 缓存服务负责从数据库加载数据 │
├─────────────────────────────────────────────────────────┤
│ 3. Write Through (写穿透) │
│ 数据先写缓存,再同步写数据库 │
├─────────────────────────────────────────────────────────┤
│ 4. Write Behind (异步写) │
│ 数据先写缓存,异步批量写数据库 │
└─────────────────────────────────────────────────────────┘

3.2 Cache Aside模式详解

这是最常用的缓存模式,应用程序直接管理缓存和数据库。

读取流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class CacheAsideService {
constructor(redis, db) {
this.redis = redis;
this.db = db;
}

/**
* 读取数据
*/
async get(key) {
// 1. 先读缓存
let data = await this.redis.get(key);

if (data) {
console.log('缓存命中');
return JSON.parse(data);
}

// 2. 缓存未命中,读数据库
console.log('缓存未命中,查询数据库');
data = await this.db.query(key);

if (data) {
// 3. 写入缓存
await this.redis.setex(key, 3600, JSON.stringify(data));
}

return data;
}

/**
* 更新数据
*/
async update(key, data) {
// 1. 先更新数据库
await this.db.update(key, data);

// 2. 删除缓存(不是更新缓存!)
await this.redis.del(key);

// 说明:先更新数据库,再删除缓存,避免并发问题
}

/**
* 删除数据
*/
async delete(key) {
// 1. 删除数据库
await this.db.delete(key);

// 2. 删除缓存
await this.redis.del(key);
}
}

为什么更新时删除缓存而不是更新缓存?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 场景:两个并发写操作
// 时间线:
// T1: 写请求A更新数据库为 value_A
// T2: 写请求B更新数据库为 value_B
// T3: 写请求B更新缓存为 value_B
// T4: 写请求A更新缓存为 value_A

// 结果:数据库是value_B,缓存是value_A(不一致!)

// 解决方案:删除缓存
// T1: 写请求A更新数据库
// T2: 写请求A删除缓存
// T3: 写请求B更新数据库
// T4: 写请求B删除缓存
// T5: 读请求查询缓存未命中,从数据库读取最新值

3.3 Read Through模式

缓存服务负责从数据库加载数据,对应用透明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ReadThroughCache {
constructor(redis, db, options = {}) {
this.redis = redis;
this.db = db;
this.defaultTTL = options.defaultTTL || 3600;
}

/**
* 读取数据(缓存自动处理加载)
*/
async get(key, loader) {
const data = await this.redis.get(key);

if (data) {
return JSON.parse(data);
}

// 缓存未命中,调用加载器
if (loader) {
const dbData = await loader();
if (dbData) {
await this.redis.setex(
key,
this.defaultTTL,
JSON.stringify(dbData)
);
}
return dbData;
}

return null;
}
}

// 使用示例
const cache = new ReadThroughCache(redis, db);

const user = await cache.get('user:1001', async () => {
return await db.users.findById(1001);
});

3.4 Write Through模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class WriteThroughCache {
constructor(redis, db) {
this.redis = redis;
this.db = db;
}

/**
* 写入数据(同步写缓存和数据库)
*/
async set(key, data, ttl = 3600) {
// 开启事务
const pipeline = this.redis.pipeline();

// 写缓存
pipeline.setex(key, ttl, JSON.stringify(data));

// 执行
await pipeline.exec();

// 同步写数据库
await this.db.update(key, data);

return true;
}
}

3.5 Write Behind模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class WriteBehindCache {
constructor(redis, db, options = {}) {
this.redis = redis;
this.db = db;
this.writeQueue = [];
this.batchSize = options.batchSize || 100;
this.flushInterval = options.flushInterval || 5000;

// 启动定时刷盘
this.startFlushTimer();
}

/**
* 写入数据(只写缓存,异步写数据库)
*/
async set(key, data, ttl = 3600) {
// 写入缓存
await this.redis.setex(key, ttl, JSON.stringify(data));

// 加入写队列
this.writeQueue.push({ key, data, timestamp: Date.now() });

// 队列满则立即刷盘
if (this.writeQueue.length >= this.batchSize) {
await this.flush();
}
}

/**
* 批量写入数据库
*/
async flush() {
if (this.writeQueue.length === 0) return;

const batch = this.writeQueue.splice(0, this.batchSize);

try {
// 批量写入数据库
await this.db.batchUpdate(batch);
console.log(`批量写入 ${batch.length} 条数据`);
} catch (err) {
// 写入失败,重新放回队列
this.writeQueue.unshift(...batch);
console.error('批量写入失败:', err);
}
}

/**
* 启动定时刷盘
*/
startFlushTimer() {
setInterval(() => {
this.flush();
}, this.flushInterval);
}
}

四、数据缓存层架构设计

4.1 三层架构模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────┐
│ Application Layer │
│ (应用层) │
└─────────────┬───────────────────────────┘

┌─────────────▼───────────────────────────┐
│ Cache Layer │
│ (Redis缓存层) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Hot Data │ │ Warm Data │ │
│ │ (热数据) │ │ (温数据) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────┬───────────────────────────┘

┌─────────────▼───────────────────────────┐
│ Database Layer │
│ (持久化层) │
│ MySQL / MongoDB / PostgreSQL │
└─────────────────────────────────────────┘

4.2 缓存数据结构设计

用户信息缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 用户基本信息
redis.setex('user:1001:profile', 3600, JSON.stringify({
userId: 1001,
nickname: 'Player001',
avatar: 'avatar_001.png',
level: 10,
exp: 1500
}));

// 用户游戏数据
redis.hmset('user:1001:game', {
coins: 10000,
gems: 100,
level: 10,
lastLogin: Date.now()
});

// 设置过期时间
redis.expire('user:1001:game', 86400);

排行榜缓存:

1
2
3
4
5
6
7
8
9
10
11
12
// 使用Sorted Set实现排行榜
// 添加分数
redis.zadd('leaderboard:weekly', score, 'user:1001');

// 获取前100名
const top100 = await redis.zrevrange('leaderboard:weekly', 0, 99, 'WITHSCORES');

// 获取用户排名
const rank = await redis.zrevrank('leaderboard:weekly', 'user:1001');

// 获取用户分数
const score = await redis.zscore('leaderboard:weekly', 'user:1001');

4.3 缓存预热与更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class CacheWarmer {
constructor(redis, db) {
this.redis = redis;
this.db = db;
}

/**
* 预热热门数据
*/
async warmHotData() {
// 获取活跃用户列表
const activeUsers = await this.db.getActiveUsers(1000);

for (const user of activeUsers) {
const cacheKey = `user:${user.id}:profile`;
await this.redis.setex(cacheKey, 3600, JSON.stringify(user));
}

console.log(`预热完成: ${activeUsers.length} 个用户`);
}

/**
* 定时刷新缓存
*/
scheduleRefresh(key, loader, interval = 300000) {
const refresh = async () => {
const data = await loader();
await this.redis.setex(key, 3600, JSON.stringify(data));
};

// 立即执行一次
refresh();

// 定时刷新
setInterval(refresh, interval);
}
}

五、缓存性能优化

5.1 连接池配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const Redis = require('ioredis');

const redis = new Redis({
// 连接池大小
connectionName: 'my-app',

// 重试策略
retryStrategy(times) {
const delay = Math.min(times * 50, 2000);
return delay;
},

// 连接超时
connectTimeout: 10000,

// 命令超时
commandTimeout: 5000,

// 最大重试次数
maxRetriesPerRequest: 3,

// 启用离线队列
enableOfflineQueue: true,

// 启用就绪检查
enableReadyCheck: true
});

5.2 Pipeline批量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 不使用Pipeline - 多次网络往返
for (let i = 0; i < 1000; i++) {
await redis.set(`key:${i}`, `value:${i}`);
}
// 耗时: 1000次RTT

// 使用Pipeline - 一次网络往返
const pipeline = redis.pipeline();
for (let i = 0; i < 1000; i++) {
pipeline.set(`key:${i}`, `value:${i}`);
}
await pipeline.exec();
// 耗时: 1次RTT

// Pipeline读取示例
const readPipeline = redis.pipeline();
readPipeline.get('key:1');
readPipeline.get('key:2');
readPipeline.get('key:3');
const results = await readPipeline.exec();
// results: [[null, 'value:1'], [null, 'value:2'], [null, 'value:3']]

5.3 缓存穿透防护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class CacheProtection {
constructor(redis) {
this.redis = redis;
}

/**
* 布隆过滤器防止缓存穿透
*/
async getWithBloomFilter(key, loader) {
// 1. 检查布隆过滤器
const mightExist = await this.redis.getbit('bloom:filter', this.hash(key));

if (!mightExist) {
console.log('布隆过滤器判定不存在');
return null;
}

// 2. 查询缓存
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}

// 3. 查询数据库
const data = await loader();

if (data) {
await this.redis.setex(key, 3600, JSON.stringify(data));
} else {
// 缓存空值,防止穿透
await this.redis.setex(key, 60, 'null');
}

return data;
}

hash(key) {
// 简化示例,实际使用多个hash函数
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = ((hash << 5) - hash) + key.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash) % 1000000;
}
}

5.4 缓存雪崩防护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class CacheAvalancheProtection {
constructor(redis) {
this.redis = redis;
}

/**
* 随机过期时间防止雪崩
*/
async setWithRandomTTL(key, data, baseTTL = 3600) {
// 基础TTL + 随机偏移(0-300秒)
const randomOffset = Math.floor(Math.random() * 300);
const ttl = baseTTL + randomOffset;

await this.redis.setex(key, ttl, JSON.stringify(data));
}

/**
* 互斥锁防止缓存击穿
*/
async getWithMutex(key, loader) {
// 1. 查询缓存
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}

// 2. 尝试获取互斥锁
const lockKey = `lock:${key}`;
const lock = await this.redis.set(lockKey, '1', 'EX', 10, 'NX');

if (!lock) {
// 获取锁失败,等待后重试
await this.sleep(100);
return this.getWithMutex(key, loader);
}

try {
// 双重检查
const cached2 = await this.redis.get(key);
if (cached2) {
return JSON.parse(cached2);
}

// 查询数据库
const data = await loader();
if (data) {
await this.setWithRandomTTL(key, data);
}

return data;
} finally {
// 释放锁
await this.redis.del(lockKey);
}
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

六、高可用架构

6.1 主从复制配置

1
2
3
4
5
6
7
8
9
10
# Redis主节点配置 (redis-master.conf)
port 6379
daemonize yes
pidfile /var/run/redis_6379.pid
logfile /var/log/redis/redis-master.log
dir /var/lib/redis/6379

# 开启持久化
appendonly yes
appendfsync everysec
1
2
3
4
5
6
7
8
9
10
11
12
13
# Redis从节点配置 (redis-slave.conf)
port 6380
daemonize yes
pidfile /var/run/redis_6380.pid
logfile /var/log/redis/redis-slave.log
dir /var/lib/redis/6380

# 配置主节点
replicaof 127.0.0.1 6379
masterauth your_password

# 从节点只读
replica-read-only yes

6.2 Sentinel高可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# sentinel.conf
port 26379
daemonize yes
pidfile /var/run/redis-sentinel.pid
logfile /var/log/redis/sentinel.log
dir /tmp

# 监控主节点
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel auth-pass mymaster your_password

# 故障判定时间
sentinel down-after-milliseconds mymaster 5000

# 故障转移超时
sentinel failover-timeout mymaster 60000

# 并行同步数量
sentinel parallel-syncs mymaster 1

Node.js Sentinel连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Redis = require('ioredis');

const redis = new Redis({
sentinels: [
{ host: '192.168.1.1', port: 26379 },
{ host: '192.168.1.2', port: 26379 },
{ host: '192.168.1.3', port: 26379 }
],
name: 'mymaster',
password: 'your_password',
sentinelPassword: 'sentinel_password',
role: 'master'
});

监控与运维

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class RedisMonitor {
constructor(redis) {
this.redis = redis;
}

async getStats() {
const info = await this.redis.info();
const stats = {};

info.split('\r\n').forEach(line => {
if (line.includes(':')) {
const [key, value] = line.split(':');
stats[key] = value;
}
});

return {
usedMemory: parseInt(stats.used_memory),
usedMemoryHuman: stats.used_memory_human,
connectedClients: parseInt(stats.connected_clients),
blockedClients: parseInt(stats.blocked_clients),
keyspaceHits: parseInt(stats.keyspace_hits),
keyspaceMisses: parseInt(stats.keyspace_misses),
hitRate: this.calculateHitRate(stats),
commandsProcessed: parseInt(stats.total_commands_processed),
opsPerSec: parseInt(stats.instantaneous_ops_per_sec)
};
}

calculateHitRate(stats) {
const hits = parseInt(stats.keyspace_hits) || 0;
const misses = parseInt(stats.keyspace_misses) || 0;
const total = hits + misses;
return total > 0 ? (hits / total * 100).toFixed(2) + '%' : 'N/A';
}
}

慢查询分析

1
2
3
4
redis-cli CONFIG SET slowlog-log-slower-than 10000
redis-cli CONFIG SET slowlog-max-len 128
redis-cli SLOWLOG GET 10
redis-cli SLOWLOG RESET

内存分析

1
2
3
redis-cli INFO memory
redis-cli --bigkeys
redis-cli --memkeys-samples 1000

小结

这篇文章记录了Redis在生产环境部署和使用中的踩坑经验,包括基础配置、Node.js客户端使用、缓存设计模式、性能优化、高可用架构等方面。希望对你有帮助。


有问题欢迎交流。