Skip to content
On this page

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']]
  })

最佳实践

  1. 连接池管理

    javascript
    const pool = new Redis.Cluster(
      [
        // 节点配置
      ],
      {
        maxRedirections: 16,
        retryDelayOnFailover: 100,
      },
    )
  2. 错误处理

    javascript
    redis.on('error', (err) => {
      console.error('Redis 错误:', err)
    })
    
    redis.on('connect', () => {
      console.log('Redis 已连接')
    })
  3. 优雅关闭

    javascript
    process.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()
})

注意事项

  1. 合理配置连接超时和重试策略
  2. 在集群模式下注意键的分布
  3. 使用连接池时注意最大连接数限制
  4. 处理好断线重连机制
  5. 注意内存使用,及时释放不需要的连接
  6. 使用哈希标签确保相关键在同一个集群槽中
  7. 对大型值使用压缩或考虑分片存储
  8. 设置合理的键过期时间,避免内存泄漏
  9. 在生产环境中启用 TLS 加密连接

要保持清醒 永远不抱有意外的幻想 凭空的期待最要命