Redis作为高性能的键值存储数据库,在现代Web开发中扮演着重要角色。特别是在Node.js应用中,Redis常被用于会话存储、缓存数据和消息队列等场景。本文将详细介绍如何在Node.js项目中集成Redis,并分享我在实际项目中的封装经验和性能优化技巧。
在开始Node.js集成前,确保已正确安装Redis服务。对于开发环境,推荐使用Docker快速启动Redis实例:
bash复制docker run --name some-redis -p 6379:6379 -d redis
如果选择本地安装,各平台安装方式如下:
brew install redissudo apt-get install redis-server安装完成后,可以通过redis-cli ping命令测试服务是否正常运行,正常应返回"PONG"响应。
创建新的Node.js项目并安装Redis客户端库:
bash复制mkdir node-redis-demo && cd node-redis-demo
npm init -y
npm install redis --save
注意:虽然示例中使用cnpm,但在生产环境建议使用npm或yarn,避免潜在的依赖安全问题。如果网络环境受限,可以配置淘宝镜像:
npm config set registry https://registry.npmmirror.com
在实际项目中,我们需要区分开发、测试和生产环境的不同配置。以下是改进后的配置方案:
javascript复制// config/db.js
const env = process.env.NODE_ENV || 'development'
const REDIS_CONFIG = {
development: {
host: '127.0.0.1',
port: 6379,
password: null,
db: 0,
connectTimeout: 5000
},
production: {
host: process.env.REDIS_HOST || 'redis.prod.example.com',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
db: process.env.REDIS_DB || 0,
connectTimeout: 10000
}
}
module.exports = {
redis: REDIS_CONFIG[env]
}
这种配置方式有以下优势:
直接使用createClient创建单个连接在生产环境是不够的,应该使用连接池:
javascript复制const { createPool } = require('redis')
const { redis } = require('../config/db')
const redisPool = createPool({
url: `redis://${redis.host}:${redis.port}`,
password: redis.password,
database: redis.db
})
连接池的关键配置参数:
maxClients: 最大连接数(默认30)minClients: 最小保持连接数(默认5)acquireTimeout: 获取连接超时时间(默认1000ms)idleTimeout: 连接空闲超时(默认30000ms)在原有代码基础上进行增强,增加更多实用方法和错误处理:
javascript复制// db/redis.js
const redis = require('redis')
const { promisify } = require('util')
const { redis: redisConfig } = require('../config/db')
class RedisClient {
constructor() {
this.client = redis.createClient(redisConfig)
this.client.on('error', this.handleError)
// Promisify常用方法
this.getAsync = promisify(this.client.get).bind(this.client)
this.setAsync = promisify(this.client.set).bind(this.client)
this.delAsync = promisify(this.client.del).bind(this.client)
this.expireAsync = promisify(this.client.expire).bind(this.client)
}
handleError(err) {
console.error('Redis Error:', err)
// 这里可以加入错误上报逻辑
}
async set(key, value, ttl = 0) {
try {
const val = typeof value === 'object' ? JSON.stringify(value) : value
await this.setAsync(key, val)
if (ttl > 0) {
await this.expireAsync(key, ttl)
}
return true
} catch (err) {
this.handleError(err)
return false
}
}
async get(key) {
try {
const val = await this.getAsync(key)
if (!val) return null
try {
return JSON.parse(val)
} catch {
return val
}
} catch (err) {
this.handleError(err)
return null
}
}
async delete(key) {
try {
return await this.delAsync(key)
} catch (err) {
this.handleError(err)
return 0
}
}
// 更多方法...
}
module.exports = new RedisClient()
改进点包括:
Redis不仅支持简单的键值存储,还提供丰富的数据结构:
哈希(Hash)操作示例:
javascript复制async setHash(key, field, value) {
const hset = promisify(this.client.hset).bind(this.client)
await hset(key, field, JSON.stringify(value))
}
async getHash(key, field) {
const hget = promisify(this.client.hget).bind(this.client)
const val = await hget(key, field)
return val ? JSON.parse(val) : null
}
有序集合(Sorted Set)示例:
javascript复制async addToLeaderboard(key, score, member) {
const zadd = promisify(this.client.zadd).bind(this.client)
return zadd(key, score, member)
}
async getLeaderboard(key, start = 0, end = -1) {
const zrevrange = promisify(this.client.zrevrange).bind(this.client)
return zrevrange(key, start, end, 'WITHSCORES')
}
生产环境中,需要确保Redis连接的可靠性:
javascript复制class RedisClient {
constructor() {
// ...其他初始化代码
this.client.on('ready', () => {
console.log('Redis连接已就绪')
})
this.client.on('reconnecting', () => {
console.log('Redis正在重连...')
})
this.client.on('end', () => {
console.log('Redis连接已关闭')
})
}
async healthCheck() {
try {
await this.setAsync('__healthcheck', Date.now(), 'EX', 10)
const result = await this.getAsync('__healthcheck')
return result !== null
} catch {
return false
}
}
}
对于需要执行多个命令的场景,使用管道可以显著提高性能:
javascript复制async batchSet(items) {
const pipeline = this.client.pipeline()
items.forEach(({ key, value, ttl }) => {
const val = typeof value === 'object' ? JSON.stringify(value) : value
pipeline.set(key, val)
if (ttl) {
pipeline.expire(key, ttl)
}
})
try {
const results = await promisify(pipeline.exec).bind(pipeline)()
return results.every(([err]) => !err)
} catch (err) {
this.handleError(err)
return false
}
}
合理的缓存策略对系统性能至关重要:
javascript复制async getWithCache(key, fetchFn, ttl = 300) {
const cached = await this.get(key)
if (cached !== null) return cached
// 使用互斥锁防止缓存击穿
const lockKey = `${key}:lock`
const locked = await this.set(lockKey, 1, 'NX', 'EX', 5)
if (!locked) {
await new Promise(resolve => setTimeout(resolve, 100))
return this.getWithCache(key, fetchFn, ttl)
}
try {
const data = await fetchFn()
await this.set(key, data, ttl)
return data
} finally {
await this.delete(lockKey)
}
}
javascript复制async getWithMultiCache(key, memoryCache, fetchFn, ttl = 300) {
// 先检查内存缓存
if (memoryCache.has(key)) {
return memoryCache.get(key)
}
// 然后检查Redis缓存
const cached = await this.get(key)
if (cached !== null) {
memoryCache.set(key, cached)
return cached
}
// 最后从数据源获取
const data = await fetchFn()
await this.set(key, data, ttl)
memoryCache.set(key, data)
return data
}
现象:Redis连接经常超时或断开
解决方案:
ping redis-hosttimeout和tcp-keepalive参数javascript复制const client = redis.createClient({
socket: {
connectTimeout: 10000,
reconnectStrategy: (retries) => Math.min(retries * 100, 5000)
}
})
javascript复制const client = redis.createClient({
retry_strategy: (options) => {
if (options.error.code === 'ECONNREFUSED') {
return new Error('连接被拒绝')
}
if (options.total_retry_time > 1000 * 60) {
return new Error('重试时间超过1分钟')
}
return Math.min(options.attempt * 100, 5000)
}
})
问题:Redis内存占用过高
优化建议:
SCAN代替KEYS命令处理大量键bash复制# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
MEMORY PURGE清理内存当单机Redis性能不足时,可以考虑集群方案:
javascript复制const RedisCluster = require('redis').RedisCluster
const cluster = new RedisCluster({
rootNodes: [
{ host: 'redis-node1', port: 6379 },
{ host: 'redis-node2', port: 6379 },
{ host: 'redis-node3', port: 6379 }
],
defaults: {
enableAutoPipelining: true,
scaleReads: 'slave'
}
})
集群模式下的注意事项:
{}包裹相同部分,如user:{123}:profileKEYS需要遍历所有节点生产环境应该监控以下Redis指标:
可以使用redis-stat或Prometheus+Redis Exporter搭建监控系统。
慢查询日志:
bash复制# redis.conf
slowlog-log-slower-than 10000 # 记录超过10ms的查询
slowlog-max-len 128 # 最多记录128条慢查询
使用redis-benchmark进行压力测试:
bash复制redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
客户端统计信息:
javascript复制const info = promisify(client.info).bind(client)
const stats = await info('stats')
console.log(stats)
RDB快照:
bash复制# redis.conf
save 900 1 # 15分钟内至少1个键变化
save 300 10 # 5分钟内至少10个键变化
save 60 10000 # 1分钟内至少10000个键变化
AOF持久化:
bash复制appendonly yes
appendfsync everysec # 每秒同步一次
混合持久化(RDB+AOF):
bash复制aof-use-rdb-preamble yes
在实际项目中,我通常会结合两种持久化方式,并定期将备份文件上传到云存储服务。对于关键业务数据,还会设置从节点进行实时复制。