发布于 2026-01-06 1 阅读
0

你不了解 Redis

你不了解 Redis

我在之前的文章中提到过,Redis 不仅仅是一个内存缓存。

大多数人甚至不会将 Redis 作为主数据库。但实际上,在很多非缓存相关的任务中,Redis 都是理想的选择。

在本文中,我将演示如何构建一个功能齐全的问答论坛,用于提出问题和对最有趣的问题进行投票。Redis将用作主要数据库

我将使用 Gatsby(React)、Netlify 无服务器函数和Upstash 无服务器 Redis

目前为止,Upstash 的表现一直不错,所以我决定在一个更正式的项目中尝试使用它。我非常喜欢无服务器架构,它让我的工作变得更简单。

对于大多数任务来说,无服务器架构都是不错的选择,但您需要了解所用技术的优缺点。我建议您深入了解无服务器架构,以便充分发挥其优势。

问答版块功能

您可能知道,我运营着一份面向招聘人员的技术简报,在简明易懂的语言中解释复杂的技术。我有一个想法,可以利用问答平台收集招聘人员的问题,并让他们投票选出最满意的问题。

所有问题最终都会在我的简报中得到解答,但是点赞最多的问题会优先解答。

任何人都可以给问题点赞,无需注册。

问题将列在三个标签页中:

  • 活跃 - 按投票数排序并可供投票的问题。
  • 最新问题按日期排序(最新问题在前)。
  • 已回答——仅回答有答案的问题。

点赞功能将是最常用的功能之一,Redis 为其提供了相应的数据类型和优化的命令。

有序集合非常适合这项任务,因为它的所有成员都会按分数自动排序。

分数是与投票关联的数值。使用ZINCRBY命令可以非常轻松地增加分数(增加一票)。

我们还将利用评分来处理未经审核的问题,将其分数设定为0。所有已批准的问题的分数均为1+

它允许我们通过简单地使用ZRANGEBYSCOREmin命令并指定max参数来获取所有未经审核的问题0

要获取所有按分数排序(最高分排在最前面)的已批准问题,我们可以使用ZREVRANGEBYSCORE命令,并将 score 参数设置min1

只需使用几条 Redis 命令,我们就能轻松解决一些逻辑性问题,这真是太棒了。降低复杂度是一大优势

我们还会使用有序集来按日期对问题进行排序,或者筛选出有答案的问题。稍后我会详细解释。

使用哈希也很容易实现不常用的操作,例如创建、更新和删除问题

实施细节

最有趣的部分永远是实际的实现。我使用了无服务器函数和ioredis库,我会附上源代码链接来解释它的工作原理。

本文主要介绍面向客户端的功能。虽然我会解释一些与管理相关的功能,但最终的源代码中不会包含后端接口。您需要使用 Postman 或类似的工具来调用与管理相关的端点。

我们来看看 API 端点及其功能。

添加问题

用户可以创建问题。所有问题都需要审核才能显示。

问题本身就是一个对象,而 Redis 哈希表是表示对象的完美数据类型。

这是问题的结构:
{"datetime":"1633992009", "question":"What are Frontend technologies?", "author":"Alex", "email":"alex@email.com", “score:” “0”, “url”: “www.answer.com” }

我们将使用HMSET命令将问题存储在哈希表中,该命令接受一个键和多个键值对。

关键架构是使用uuid库生成的问题 ID 的question:{ID}位置ID

这是一个新问题,目前还没有答案。我们暂时跳过这个属性,但之后可以使用HSETurl命令轻松添加

新创建问题的默认分数是00。根据我们的设计,这意味着该问题需要审核,并且不会被列出,因为我们只获取分数从 0 开始的问题1

由于我们将分数存储在哈希表中,因此每当分数发生变化时,我们都需要更新哈希表。我们可以使用HINCRBY命令轻松地递增哈希表中的值。

如您所见,使用 Redis 哈希为我们解决的问题远不止存储数据。

既然我们知道了如何存储问题,我们还需要跟踪问题以便以后能够获取它们。

为此,我们使用ZADDID命令将问题的 ID 添加到按分数排序的集合中。排序后的集合允许我们按分数获取问题 ID。0

如您所见,我们设置分数的方式与上面哈希表中属性0的设置方式相同score。之所以在哈希表中重复设置分数,是因为在显示最新问题或已有答案的问题时需要用到它。

例如,最近的问题存储在一个单独的已排序集合中,时间戳作为分数,因此除非在哈希表中重复出现,否则无法获得原始分数值。

由于我们将分数存储在两个地方,因此需要确保哈希表和有序集合中的值都得到更新。我们使用MULTI命令来执行命令,要么所有命令都成功执行,要么全部回滚。更多详情请参阅Redis 事务。

我们将在适用情况下采用这种方法。例如,HMSETZADD也将在事务中执行(参见下面的源代码)。

ZADD命令需要一个键,其模式如下:questions:{boardID}

所有问题都映射到一个看板boardID。目前,这是一个硬编码值,因为我只需要一个看板。将来,我可能会添加更多看板,例如,分别用于前端、后端、QA 等。提前搭建好所需的结构是件好事。

终点:
POST /api/create_question

以下是create_question无服务器函数的源代码

批准问题

议题在开放投票前需要获得批准。批准议题意味着:

  1. 使用HINCRBY命令将哈希表中的分数值从更新为01
  2. 使用ZADD命令questions:{boardID}将排序集中的分数值从更新为01
  3. 使用相同的命令,将问题添加IDquestions:{boardID}:time已排序的集合中,并将时间戳作为分数,以获取按日期排序的问题(最近的问题)ZADD

ID我们可以通过HGET命令查找问题来获取时间戳

一旦我们有了结果,就可以在一个事务中执行剩余的三条命令。这将确保哈希值和排序后的集合中的分数完全相同。

要获取所有未批准的问题,可以使用ZRANGEBYSCORE命令,并将minmax值设置为0

ZRANGEBYSCORE返回按分数从低到高排序的元素ZREVRANGEBYSCORE,而返回按分数从高到低排序的元素。我们将使用后者来获取按投票数排序的问题。

用于获取所有未批准问题的端点:
GET /api/questions_unapproved

问题审批端点:
PUT: /api/question_approve

以下是questions_unapproved无服务器函数的源代码。大部分代码与其他GET端点类似,我将在下一节中进行解释。

以下是question_approve无服务器函数的源代码

获取已批准的问题

要获取所有已批准的问题,我们使用ZREVRANGEBYSCORE命令并将参数设置min为,1以便跳过所有未批准的问题。

因此,我们只能得到一个包含 ID 的列表。我们需要遍历这些 ID,使用HGETALL命令获取问题详情。

根据获取的问题数量,这种方法可能会变得非常耗费资源,并阻塞 Node 中的事件循环(我使用的是 Node.js)。有几种方法可以缓解这个潜在问题。

例如,我们可以使用ZREVRANGEBYSCORE可选LIMIT参数来仅获取元素范围。但是,如果偏移量很大,则时间复杂度可能会达到 O(N)

或者我们可以使用 Lua 脚本来扩展 Redis,通过添加自定义命令,根据 ID 从存储的集合中获取问题详细信息,而无需我们在应用程序层手动执行此操作。

我认为在这种情况下这样做会造成额外的开销。此外,使用 Lua 脚本时必须格外小心,因为它们会阻塞 Redis,而且如果不降低性能,就无法用它们执行耗时的任务。虽然这种方法可能更简洁,但我们仍然会使用它LIMIT来避免处理大量数据。

在最终实施之前,务必权衡利弊。只要你了解潜在问题并评估了应对方法,就万无一失了。

就我而言,我知道我需要相当长的时间才能积累足够的问题来解决这个问题。无需过早优化

终点:
GET /api/questions

以下是问题无服务器函数的源代码

投票选出问题

对问题进行点赞的过程包括两个重要步骤,这两个步骤都需要作为一个交易来执行。

但是,在修改分数之前,我们需要检查这个问题是否没有答案(url属性)。换句话说,我们不允许任何人为已经回答过的问题投票。

此类问题的投票按钮已被禁用。但我们不信任互联网上的任何人,因此会使用ZSCOREID命令在服务器上检查给定的元素是否存在于已排序的集合中。如果存在,则不进行任何操作。questions:{boardID}:answered

我们使用HINCRBY命令将哈希中的分数增加1,使用ZINCRBY命令将排序集中的分数增加1

终点:
PATCH /api/question_upvote

以下是question_upvote无服务器函数的源代码

获取最近已批准的问题

这与我们获取所有已批准问题的方式非常相似,唯一的区别在于我们读取的是另一个已排序的集合,其键模式为[键模式] questions:{boardID}:time。由于我们使用时间戳作为分数,因此该ZREVRANGEBYSCORE命令返回的 ID 将按降序排列。

终点:
PATCH /api/questions_recent

以下是questions_recent无服务器函数的源代码

用答案更新问题

使用命令更新哈希表或添加新属性非常简单HSET。但是,当我们添加答案时,我们会将问题从questions:{boardID}已排序的集合移动到questions:{boardID}:answered保留分数的集合中。

为此,我们需要知道题目的分数,可以使用ZSCORE命令获取。已回答的题目将按分数降序排列。

然后我们就可以:

  1. url使用命令更新哈希表中的属性HSET
  2. questions:{boardID}:answered使用以下方法将哈希值添加到已排序的集合中ZADD
  3. questions:{boardID}运行命令,从排序后的数据集中移除该问题ZREM
  4. questions:{boardID}:time运行命令,从排序后的数据集中移除该问题ZREM

这四条命令都在同一个事务中执行。

终点:
PATCH /api/question_add_answer

以下是question_add_answer无服务器函数的源代码

获取问题及答案

同样,这个过程与获取所有已批准的问题类似。这次是从questions:{boardID}:answered已排序的集合中获取问题。

终点:
PATCH /api/questions_unswered

以下是questions_unswered无服务器函数的源代码


完整源代码我的网站上
有可运行的演示。

结论

Redis 的应用场景远不止缓存。我只是展示了 Redis 众多应用场景中的一种,你可以考虑用它来代替直接使用 SQL 数据库。

当然,如果您已经在使用数据库,再添加一个数据库可能会增加额外的开销。

Redis 速度非常快,而且可扩展性很好。大多数商业项目在其技术栈中都包含 Redis,并且通常将其用作辅助数据库,而不仅仅是内存缓存。

我强烈建议学习Redis 数据模式和最佳实践,以便了解它的强大之处,并从中长期受益。

如果你还没看过的话,可以去看看我之前那篇用 Serverless Redis创建类似 LinkedIn 的表情符号的文章。

这是《你可能不了解 Redis(第二部分)》

关注我们,获取更多内容。

文章来源:https://dev.to/sandorturanszky/you-don-t-know-redis-3onh