Redis 大键值和大 List 上的零碎 bug 细节

Redis 是一个高性能的键值存储系统,常用于缓存、队列、计数器等场景。在前端开发中,我们经常会使用 Redis 作为数据存储和缓存的方案。然而,在使用 Redis 的过程中,我们也会遇到一些零碎的 bug 细节,特别是在处理大键值和大 List 的时候。本文将介绍这些细节,并给出相应的解决方案。

Redis 大键值的问题

Redis 对键值的大小有一定的限制,如果键值过大,会影响 Redis 的性能和稳定性。具体来说,Redis 对键值的限制如下:

  • 每个键的最大长度为 512MB。
  • 每个值的最大长度为 512MB。
  • 如果值的长度超过了一定的阈值,Redis 会将它存储在独立的数据结构中,而不是直接存储在键值对中。这个阈值可以通过 hash-max-ziplist-valuelist-max-ziplist-value 配置项来调整,默认值分别为 64 和 512。

如果我们在使用 Redis 的时候遇到了大键值的问题,可以考虑以下几种解决方案:

1. 分割键值

将大键值分割成多个小键值存储,可以有效地避免单个键值过大的问题。例如,将一个大字符串分割成多个小字符串存储,每个小字符串的键名可以使用相同的前缀,不同的后缀来区分。

示例代码:

// 将一个大字符串分割成多个小字符串存储
async function setLargeString(key, value, chunkSize = 1024 * 1024) {
  const chunks = [];
  for (let i = 0; i < value.length; i += chunkSize) {
    const chunkKey = `${key}:${i / chunkSize}`;
    const chunkValue = value.slice(i, i + chunkSize);
    chunks.push([chunkKey, chunkValue]);
  }
  await redis.mset(chunks.flat());
}

async function getLargeString(key) {
  const keys = await redis.keys(`${key}:*`);
  const chunks = await redis.mget(keys);
  return chunks.join('');
}

2. 使用 Redis Hash

如果我们需要存储的键值是一个复杂的对象,可以使用 Redis Hash 来存储。Redis Hash 可以将一个对象分解成多个键值对存储,每个键值对的键名可以使用相同的前缀,不同的后缀来区分。

示例代码:

// 使用 Redis Hash 存储一个复杂对象
async function setLargeObject(key, obj, chunkSize = 1024 * 1024) {
  const chunks = [];
  for (const [k, v] of Object.entries(obj)) {
    const chunkKey = `${key}:${k}`;
    const chunkValue = JSON.stringify(v);
    chunks.push([chunkKey, chunkValue]);
  }
  await redis.hmset(key, chunks.flat());
}

async function getLargeObject(key) {
  const chunks = await redis.hgetall(key);
  const obj = {};
  for (const [k, v] of Object.entries(chunks)) {
    obj[k] = JSON.parse(v);
  }
  return obj;
}

Redis 大 List 的问题

Redis 的 List 是一个双向链表,可以在列表的两端插入和删除元素,支持类似栈和队列的操作。然而,在处理大 List 的时候,我们也会遇到一些问题。

1. 遍历大 List 的性能问题

如果我们需要遍历一个大 List,例如计算 List 中所有元素的和,使用 lrange 命令逐个获取元素的值会非常慢。这是因为 lrange 命令需要进行网络传输和解析等操作,而且每次只能获取一个元素的值。

解决这个问题的方法是使用 lrange 命令一次性获取 List 的所有元素,然后在本地进行计算。这样可以避免网络传输和解析等开销,提高遍历 List 的性能。

示例代码:

// 计算 List 中所有元素的和
async function sumList(listKey) {
  const values = await redis.lrange(listKey, 0, -1);
  return values.reduce((acc, cur) => acc + parseInt(cur), 0);
}

2. List 长度过大的问题

如果一个 List 的长度非常大,例如超过了 100 万个元素,会影响 Redis 的性能和稳定性。具体来说,会出现以下问题:

  • 内存使用过高,导致 Redis 进程被操作系统 kill。
  • List 遍历和操作的性能下降,导致 Redis 响应变慢。

如果我们需要处理一个大 List,可以考虑以下几种解决方案:

1. 分割 List

将一个大 List 分割成多个小 List 存储,可以有效地避免单个 List 过大的问题。例如,将一个包含 100 万个元素的 List 分割成 100 个包含 1 万个元素的 List 存储,每个小 List 的键名可以使用相同的前缀,不同的后缀来区分。

示例代码:

// 将一个大 List 分割成多个小 List 存储
async function setLargeList(listKey, values, chunkSize = 10000) {
  const chunks = [];
  for (let i = 0; i < values.length; i += chunkSize) {
    const chunkKey = `${listKey}:${i / chunkSize}`;
    const chunkValues = values.slice(i, i + chunkSize);
    chunks.push([chunkKey, ...chunkValues]);
  }
  await redis.mset(chunks.flat());
}

async function getLargeList(listKey) {
  const keys = await redis.keys(`${listKey}:*`);
  const chunks = await redis.mget(keys);
  return chunks.flat().slice(1);
}

2. 使用 Redis Stream

Redis Stream 是一个高性能、高可靠性的消息队列系统,支持类似 List 的操作,但是比 List 更加灵活和安全。如果我们需要处理大量的消息,可以考虑使用 Redis Stream 来代替 List。

示例代码:

// 使用 Redis Stream 存储和读取消息
async function setLargeStream(streamKey, values) {
  const producer = redis.xack(streamKey);
  for (const value of values) {
    await producer.add({ value });
  }
}

async function getLargeStream(streamKey) {
  const consumer = redis.xread({ streams: [streamKey, '0'] });
  const values = [];
  for await (const { messages } of consumer) {
    for (const { value } of messages) {
      values.push(value);
    }
  }
  return values;
}

总结

在使用 Redis 的过程中,我们需要注意大键值和大 List 的问题,以避免影响 Redis 的性能和稳定性。对于大键值,可以使用分割键值或者 Redis Hash 来存储;对于大 List,可以使用一次性获取或者分割 List 来遍历,或者使用 Redis Stream 来代替 List。这些解决方案可以提高 Redis 的性能和稳定性,同时也可以提高我们的开发效率。

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/658a7dc3eb4cecbf2dfaf314


纠错
反馈