Appearance
ioredis
简介
ioredis 是一个功能强大的 Node.js Redis 客户端,提供了高性能、完整的 Redis 命令支持、集群和哨兵模式等特性。
主要特性
- 完整支持 Redis 命令集
- 支持 Redis 集群和哨兵模式
- 内置 Promise 支持
- 支持 Lua 脚本
- 支持管道和事务操作
- 自动重连和错误处理
- 高性能设计
- TypeScript 类型定义
安装
bash
npm install ioredis基本使用
1. 创建连接
javascript
const Redis = require('ioredis')
// 基本连接
const redis = new Redis({
port: 6379,
host: 'localhost',
password: 'auth',
db: 0,
})
// URL 方式连接
const redis = new Redis('redis://:auth@localhost:6379/0')2. 基本操作
javascript
// 设置值
await redis.set('key', 'value')
// 获取值
const value = await redis.get('key')
// 设置过期时间
await redis.set('key', 'value', 'EX', 60) // 60秒后过期
// 删除键
await redis.del('key')高级特性
1. 集群模式
javascript
const Redis = require('ioredis')
const cluster = new Redis.Cluster([
{
port: 6380,
host: '127.0.0.1',
},
{
port: 6381,
host: '127.0.0.1',
},
])2. 哨兵模式
javascript
const redis = new Redis({
sentinels: [
{ host: 'localhost', port: 26379 },
{ host: 'localhost', port: 26380 },
],
name: 'mymaster',
})3. 管道操作
javascript
const pipeline = redis.pipeline()
pipeline.set('foo', 'bar')
pipeline.del('cc')
pipeline.exec((err, results) => {
// 处理结果
})4. 事务支持
javascript
redis
.multi()
.set('foo', 'bar')
.get('foo')
.exec((err, results) => {
// results === [[null, 'OK'], [null, 'bar']]
})最佳实践
连接池管理
javascriptconst pool = new Redis.Cluster( [ // 节点配置 ], { maxRedirections: 16, retryDelayOnFailover: 100, }, )错误处理
javascriptredis.on('error', (err) => { console.error('Redis 错误:', err) }) redis.on('connect', () => { console.log('Redis 已连接') })优雅关闭
javascriptprocess.on('SIGTERM', () => { redis.disconnect() })
应用场景
1. 缓存系统
javascript
const Redis = require('ioredis')
const redis = new Redis()
// 缓存中间件
async function cacheMiddleware(req, res, next) {
const cacheKey = `api:${req.originalUrl}`
try {
// 尝试从缓存获取数据
const cachedData = await redis.get(cacheKey)
if (cachedData) {
console.log(`从缓存返回: ${cacheKey}`)
return res.json(JSON.parse(cachedData))
}
// 保存原始 res.json 方法
const originalJson = res.json
// 重写 res.json 方法以缓存结果
res.json = function (data) {
// 缓存数据,设置 5 分钟过期
redis.set(cacheKey, JSON.stringify(data), 'EX', 300)
return originalJson.call(this, data)
}
next()
} catch (err) {
console.error('缓存错误:', err)
next()
}
}
// 使用中间件
app.get('/api/products', cacheMiddleware, async (req, res) => {
// 数据库查询等耗时操作
const products = await db.products.findAll()
res.json(products)
})2. 分布式锁
javascript
const Redis = require('ioredis')
const { v4: uuidv4 } = require('uuid')
const redis = new Redis()
// 获取分布式锁
async function acquireLock(lockName, ttl = 30000) {
const lockValue = uuidv4()
const acquired = await redis.set(lockName, lockValue, 'PX', ttl, 'NX')
if (acquired === 'OK') {
return {
lockValue,
release: async () => {
// 使用 Lua 脚本确保只释放自己的锁
const script = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
`
return redis.eval(script, 1, lockName, lockValue)
},
}
}
return null
}
// 使用分布式锁
async function processPayment(orderId) {
const lockName = `order:lock:${orderId}`
const lock = await acquireLock(lockName)
if (!lock) {
console.log(`订单 ${orderId} 正在被处理,请稍后再试`)
return false
}
try {
// 执行需要加锁的操作
await processOrderPayment(orderId)
console.log(`订单 ${orderId} 支付处理完成`)
return true
} catch (err) {
console.error(`处理订单 ${orderId} 时出错:`, err)
return false
} finally {
// 释放锁
await lock.release()
}
}常见问题与解决方案
1. 连接断开问题
问题: Redis 连接意外断开导致应用错误。
解决方案:
javascript
const redis = new Redis({
host: 'localhost',
port: 6379,
retryStrategy(times) {
const delay = Math.min(times * 50, 2000)
return delay
},
maxRetriesPerRequest: 3,
})
// 监听错误和重连事件
redis.on('error', (err) => {
console.error('Redis 错误:', err)
})
redis.on('close', () => {
console.log('Redis 连接已关闭,尝试重连...')
})
redis.on('reconnecting', (ms) => {
console.log(`Redis 将在 ${ms}ms 后重连`)
})
redis.on('connect', () => {
console.log('Redis 已连接')
})2. 集群键分布问题
问题: 在 Redis 集群中,相关数据可能分布在不同节点上,影响事务和多键操作。
解决方案:
javascript
// 使用哈希标签确保相关键在同一个槽中
async function storeRelatedData(userId, data) {
const cluster = new Redis.Cluster([
/* 集群配置 */
])
// 使用 {userId} 作为哈希标签,确保所有键都路由到同一个节点
const userKey = `user:{${userId}}`
const profileKey = `profile:{${userId}}`
const prefsKey = `preferences:{${userId}}`
// 使用管道批量操作
const pipeline = cluster.pipeline()
pipeline.set(userKey, JSON.stringify(data.user))
pipeline.set(profileKey, JSON.stringify(data.profile))
pipeline.set(prefsKey, JSON.stringify(data.preferences))
return pipeline.exec()
}3. 内存占用过高
问题: 长时间运行的应用中,Redis 客户端可能占用过多内存。
解决方案:
javascript
// 创建连接池管理器
class RedisManager {
constructor(options = {}) {
this.options = options
this.clients = new Map()
this.maxIdleTime = options.maxIdleTime || 30000 // 30秒
// 定期清理空闲连接
setInterval(() => this.cleanupIdleConnections(), this.maxIdleTime)
}
getClient(name = 'default') {
if (!this.clients.has(name)) {
const client = new Redis(this.options)
this.clients.set(name, {
client,
lastUsed: Date.now(),
})
} else {
// 更新最后使用时间
const entry = this.clients.get(name)
entry.lastUsed = Date.now()
}
return this.clients.get(name).client
}
cleanupIdleConnections() {
const now = Date.now()
for (const [name, entry] of this.clients.entries()) {
if (now - entry.lastUsed > this.maxIdleTime) {
console.log(`关闭空闲连接: ${name}`)
entry.client.disconnect()
this.clients.delete(name)
}
}
}
closeAll() {
for (const [name, entry] of this.clients.entries()) {
entry.client.disconnect()
}
this.clients.clear()
}
}
// 使用连接管理器
const redisManager = new RedisManager({
host: 'localhost',
port: 6379,
})
// 获取连接
const redis = redisManager.getClient()
// 应用关闭时清理所有连接
process.on('SIGTERM', () => {
redisManager.closeAll()
})注意事项
- 合理配置连接超时和重试策略
- 在集群模式下注意键的分布
- 使用连接池时注意最大连接数限制
- 处理好断线重连机制
- 注意内存使用,及时释放不需要的连接
- 使用哈希标签确保相关键在同一个集群槽中
- 对大型值使用压缩或考虑分片存储
- 设置合理的键过期时间,避免内存泄漏
- 在生产环境中启用 TLS 加密连接