你不了解 Redis
我在之前的文章中提到过,Redis 不仅仅是一个内存缓存。
大多数人甚至不会将 Redis 作为主数据库。但实际上,在很多非缓存相关的任务中,Redis 都是理想的选择。
在本文中,我将演示如何构建一个功能齐全的问答论坛,用于提出问题和对最有趣的问题进行投票。Redis将用作主要数据库。
我将使用 Gatsby(React)、Netlify 无服务器函数和Upstash 无服务器 Redis。
目前为止,Upstash 的表现一直不错,所以我决定在一个更正式的项目中尝试使用它。我非常喜欢无服务器架构,它让我的工作变得更简单。
对于大多数任务来说,无服务器架构都是不错的选择,但您需要了解所用技术的优缺点。我建议您深入了解无服务器架构,以便充分发挥其优势。
问答版块功能
您可能知道,我运营着一份面向招聘人员的技术简报,在简明易懂的语言中解释复杂的技术。我有一个想法,可以利用问答平台收集招聘人员的问题,并让他们投票选出最满意的问题。
所有问题最终都会在我的简报中得到解答,但是点赞最多的问题会优先解答。
任何人都可以给问题点赞,无需注册。
问题将列在三个标签页中:
- 活跃 - 按投票数排序并可供投票的问题。
- 最新问题按日期排序(最新问题在前)。
- 已回答——仅回答有答案的问题。
点赞功能将是最常用的功能之一,Redis 为其提供了相应的数据类型和优化的命令。
有序集合非常适合这项任务,因为它的所有成员都会按分数自动排序。
分数是与投票关联的数值。使用ZINCRBY命令可以非常轻松地增加分数(增加一票)。
我们还将利用评分来处理未经审核的问题,将其分数设定为0。所有已批准的问题的分数均为1+。
它允许我们通过简单地使用ZRANGEBYSCOREmin命令并指定max参数来获取所有未经审核的问题0。
要获取所有按分数排序(最高分排在最前面)的已批准问题,我们可以使用ZREVRANGEBYSCORE命令,并将 score 参数设置min为1。
只需使用几条 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 事务。
我们将在适用情况下采用这种方法。例如,HMSET它ZADD也将在事务中执行(参见下面的源代码)。
ZADD命令需要一个键,其模式如下:questions:{boardID}
所有问题都映射到一个看板boardID。目前,这是一个硬编码值,因为我只需要一个看板。将来,我可能会添加更多看板,例如,分别用于前端、后端、QA 等。提前搭建好所需的结构是件好事。
终点:POST /api/create_question
以下是create_question无服务器函数的源代码。
批准问题
议题在开放投票前需要获得批准。批准议题意味着:
- 使用HINCRBY命令将哈希表中的分数值从更新为
0。1 - 使用ZADD命令
questions:{boardID}将排序集中的分数值从更新为。01 - 使用相同的命令,将问题添加
ID到questions:{boardID}:time已排序的集合中,并将时间戳作为分数,以获取按日期排序的问题(最近的问题)ZADD。
ID我们可以通过HGET命令查找问题来获取时间戳。
一旦我们有了结果,就可以在一个事务中执行剩余的三条命令。这将确保哈希值和排序后的集合中的分数完全相同。
要获取所有未批准的问题,可以使用ZRANGEBYSCORE命令,并将min和max值设置为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命令获取。已回答的题目将按分数降序排列。
然后我们就可以:
url使用命令更新哈希表中的属性HSET;questions:{boardID}:answered使用以下方法将哈希值添加到已排序的集合中ZADD:questions:{boardID}运行命令,从排序后的数据集中移除该问题ZREM。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 的表情符号的文章。
关注我们,获取更多内容。
文章来源:https://dev.to/sandorturanszky/you-don-t-know-redis-3onh