Redis缓存踩坑记录
记录Redis在生产环境部署和使用中遇到的问题和解决方案。
基础配置与连接
1.1 Redis安装配置
CentOS 7安装:
1 2 3 4 5 6 7 8 9 10 11
| 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
|
protected-mode no
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
| redis-cli -h 127.0.0.1 -p 6379 -a your_password
redis-cli -h 127.0.0.1 -p 6379 auth your_password
redis-cli -h xxx.xxx.xxx.xxx -p 6379 auth your_password
|
二、Node.js Redis客户端
2.1 ioredis客户端
ioredis是Node.js中功能最完备的Redis客户端。
安装:
基础配置:
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);
await redis.setex('session', 3600, 'user_data');
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' });
const redis = new Redis({ port: 6379, host: 'xxx.xxx.xxx.xxx', password: 'your_password' });
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
| 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) { let data = await this.redis.get(key);
if (data) { console.log('缓存命中'); return JSON.parse(data); }
console.log('缓存未命中,查询数据库'); data = await this.db.query(key);
if (data) { await this.redis.setex(key, 3600, JSON.stringify(data)); }
return data; }
async update(key, data) { await this.db.update(key, data);
await this.redis.del(key);
}
async delete(key) { await this.db.delete(key);
await this.redis.del(key); } }
|
为什么更新时删除缓存而不是更新缓存?
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
|
redis.zadd('leaderboard:weekly', score, 'user:1001');
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
| for (let i = 0; i < 1000; i++) { await redis.set(`key:${i}`, `value:${i}`); }
const pipeline = redis.pipeline(); for (let i = 0; i < 1000; i++) { pipeline.set(`key:${i}`, `value:${i}`); } await pipeline.exec();
const readPipeline = redis.pipeline(); readPipeline.get('key:1'); readPipeline.get('key:2'); readPipeline.get('key:3'); const results = await readPipeline.exec();
|
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) { const mightExist = await this.redis.getbit('bloom:filter', this.hash(key));
if (!mightExist) { console.log('布隆过滤器判定不存在'); return null; }
const cached = await this.redis.get(key); if (cached) { return JSON.parse(cached); }
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) { 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) { const randomOffset = Math.floor(Math.random() * 300); const ttl = baseTTL + randomOffset;
await this.redis.setex(key, ttl, JSON.stringify(data)); }
async getWithMutex(key, loader) { const cached = await this.redis.get(key); if (cached) { return JSON.parse(cached); }
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客户端使用、缓存设计模式、性能优化、高可用架构等方面。希望对你有帮助。
有问题欢迎交流。