Redis 模式示例

通过构建一个 Twitter 克隆学习几种 Redis 模式

本文介绍了如何使用 PHP 和 Redis(作为唯一的数据库)构建一个非常简单的 [Twitter 克隆的设计和实现](https://github.com/antirez/retwis)。编程社区传统上认为键值存储是一种特殊用途的数据库,不能作为关系数据库的直接替代品用于开发 Web 应用程序。本文将试图展示,在键值层之上构建的 Redis 数据结构是实现多种应用程序的有效数据模型。

注意:本文的最初版本写于 2009 年 Redis 发布时。当时并不完全清楚 Redis 数据模型是否适合编写完整的应用程序。现在,经过 5 年的发展,有许多应用程序使用 Redis 作为主要存储。因此,本文今天的目标是作为 Redis 新手的入门教程。您将学习如何使用 Redis 设计简单的数据布局,以及如何应用不同的数据结构。

我们的 Twitter 克隆名为 [Retwis](https://github.com/antirez/retwis),结构简单,性能非常好,只需少量努力即可分布在任意数量的 Web 服务器和 Redis 服务器上。[查看 Retwis 源代码](https://github.com/antirez/retwis)。

我在示例中使用了 PHP,因为它的通用可读性。使用 Ruby、Python、Erlang 等语言也可以获得相同(或更好)的结果。存在一些克隆版本(但并非所有克隆版本都使用与本教程当前版本相同的数据布局,因此请坚持使用官方 PHP 实现,以便更好地跟随本文)。

  • [Retwis-RB](https://github.com/danlucraft/retwis-rb) 是 Daniel Lucraft 编写的 Retwis 的 Ruby 和 Sinatra 移植版。
  • [Retwis-J](https://docs.springframework.org.cn/spring-data/data-keyvalue/examples/retwisj/current/) 是使用 Spring Data Framework 编写的 Retwis 的 Java 移植版,由 [Costin Leau](http://twitter.com/costinl) 编写。其源代码可在 [GitHub](https://github.com/SpringSource/spring-data-keyvalue-examples) 上找到,并且在 [springsource.org](http://j.mp/eo6z6I) 上提供了全面的文档。

什么是键值存储?

键值存储的本质是能够将一些数据(称为**值**)存储在键中。只有知道存储该值的特定键,才能稍后检索该值。没有直接通过值搜索键的方法。从某种意义上说,它就像一个非常大的哈希/字典,但它是持久化的,即当您的应用程序结束时,数据不会丢失。因此,例如,我可以使用 [`SET`](/docs/latest/commands/set/) 命令将值 *bar* 存储在键 *foo* 中

SET foo bar

Redis 会永久存储数据,因此如果我稍后询问“*键 foo 中存储的值是什么?*”,Redis 将回复 *bar*

GET foo => bar

键值存储提供的其他常见操作包括 [`DEL`](/docs/latest/commands/del/),用于删除给定的键及其关联的值;SET-if-not-exists(在 Redis 中称为 [`SETNX`](/docs/latest/commands/setnx/)),仅在键尚不存在时为其赋值;以及 [`INCR`](/docs/latest/commands/incr/),用于原子地递增存储在给定键中的数值

SET foo 10
INCR foo => 11
INCR foo => 12
INCR foo => 13

原子操作

[`INCR`](/docs/latest/commands/incr/) 有一些特别之处。您可能会想,既然我们可以通过编写一些代码来实现递增操作,Redis 为什么还要提供这样的操作呢?毕竟,这就像这样简单:

x = GET foo
x = x + 1
SET foo x

问题在于,这种方式的递增操作只有在同一时间只有一个客户端操作键 *foo* 时才能正常工作。看看如果两个客户端同时访问这个键会发生什么:

x = GET foo (yields 10)
y = GET foo (yields 10)
x = x + 1 (x is now 11)
y = y + 1 (y is now 11)
SET foo x (foo is now 11)
SET foo y (foo is now 11)

出错了!我们递增了两次,但键中的值不是从 10 变为 12,而是 11。这是因为通过 GET / increment / SET 完成的递增*不是原子操作*。相比之下,Redis、Memcached 等提供的 INCR 是原子实现,服务器会在完成递增所需的时间内负责保护键,以防止同时访问。

Redis 与其他键值存储的不同之处在于,它提供了类似于 INCR 的其他操作,可用于解决复杂问题。这就是为什么您可以使用 Redis 编写整个 Web 应用程序,而无需使用像 SQL 数据库这样的其他数据库,并且不会让您抓狂。

超越键值存储:列表(Lists)

在本节中,我们将了解构建 Twitter 克隆所需的 Redis 特性。首先要知道的是,Redis 的值不仅仅可以是字符串。Redis 支持 Lists、Sets、Hashes、Sorted Sets、Bitmaps 和 HyperLogLog 等类型作为值,并且提供了对它们进行操作的原子操作,因此即使有多个访问同一键,我们也能够安全地进行操作。让我们从 Lists 开始:

LPUSH mylist a (now mylist holds 'a')
LPUSH mylist b (now mylist holds 'b','a')
LPUSH mylist c (now mylist holds 'c','b','a')

[`LPUSH`](/docs/latest/commands/lpush/) 意思是 *Left Push*,即将一个元素添加到存储在 *mylist* 中的列表的左侧(或头部)。如果键 *mylist* 不存在,则在 PUSH 操作之前会自动创建一个空列表。您可以想象,还有一个 [`RPUSH`](/docs/latest/commands/rpush/) 操作,它将元素添加到列表的右侧(尾部)。这对于我们的 Twitter 克隆非常有用。例如,用户更新可以添加到存储在 username:updates 中的列表中。

当然,还有从 Lists 获取数据的操作。例如,LRANGE 返回列表中的一个范围,或者整个列表。

LRANGE mylist 0 1 => c,b

LRANGE 使用从零开始的索引——也就是说第一个元素是 0,第二个是 1,依此类推。命令参数是 LRANGE key first-index last-index。*last-index* 参数可以是负数,具有特殊含义:-1 是列表中的最后一个元素,-2 是倒数第二个,依此类推。因此,要获取整个列表,请使用:

LRANGE mylist 0 -1 => c,b,a

其他重要操作包括 LLEN,返回列表中的元素数量;以及 LTRIM,它类似于 LRANGE,但不是返回指定范围,而是**修剪**列表,所以它类似于 *从 mylist 获取范围,将此范围设置为新值*,但它是原子执行的。

Set 数据类型

目前,本教程中我们不直接使用 Set 类型,但由于我们使用了 Sorted Sets(它是 Sets 的更强大的版本),最好先介绍 Sets(Sets 本身是非常有用的数据结构),然后再介绍 Sorted Sets。

除了 Lists,还有更多数据类型。Redis 还支持 Sets,它是无序的元素集合。可以添加、删除和测试成员是否存在,以及执行不同 Sets 之间的交集操作。当然,也可以获取 Set 中的元素。一些例子会更清楚地说明。请记住,[`SADD`](/docs/latest/commands/sadd/) 是*添加到 Set* 的操作,[`SREM`](/docs/latest/commands/srem/) 是*从 Set 中移除*的操作,[`SISMEMBER`](/docs/latest/commands/sismember/) 是*测试成员是否存在*的操作,而 [`SINTER`](/docs/latest/commands/sinter/) 是*执行交集*的操作。其他操作包括 [`SCARD`](/docs/latest/commands/scard/) 获取 Set 的基数(元素数量),以及 [`SMEMBERS`](/docs/latest/commands/smembers/) 返回 Set 的所有成员。

SADD myset a
SADD myset b
SADD myset foo
SADD myset bar
SCARD myset => 4
SMEMBERS myset => bar,a,foo,b

注意,[`SMEMBERS`](/docs/latest/commands/smembers/) 不会按我们添加元素的顺序返回,因为 Sets 是*无序*的元素集合。当您想要按顺序存储时,最好使用 Lists。还有一些针对 Sets 的操作:

SADD mynewset b
SADD mynewset foo
SADD mynewset hello
SINTER myset mynewset => foo,b

[`SINTER`](/docs/latest/commands/sinter/) 可以返回 Sets 之间的交集,并且不限于两个 Sets。您可以请求 4 个、5 个或 10000 个 Sets 的交集。最后,让我们看看 [`SISMEMBER`](/docs/latest/commands/sismember/) 如何工作:

SISMEMBER myset foo => 1
SISMEMBER myset notamember => 0

Sorted Set 数据类型

Sorted Sets 类似于 Sets:元素集合。但是,在 Sorted Sets 中,每个元素都关联着一个浮点值,称为**元素分数**。由于分数的存在,Sorted Set 中的元素是有序的,因为我们总是可以通过分数比较两个元素(如果分数相同,则按字符串比较两个元素)。

与 Sets 类似,Sorted Sets 中不能添加重复的元素,每个元素都是唯一的。但是,可以更新元素的分数。

Sorted Set 命令都以 Z 开头。以下是 Sorted Sets 的用法示例:

ZADD zset 10 a
ZADD zset 5 b
ZADD zset 12.55 c
ZRANGE zset 0 -1 => b,a,c

在上面的示例中,我们使用 [`ZADD`](/docs/latest/commands/zadd/) 添加了一些元素,然后使用 [`ZRANGE`](/docs/latest/commands/zrange/) 检索了这些元素。正如您所见,元素按照分数顺序返回。为了检查给定元素是否存在,并在存在时检索其分数,我们使用 [`ZSCORE`](/docs/latest/commands/zscore/) 命令:

ZSCORE zset a => 10
ZSCORE zset non_existing_element => NULL

Sorted Sets 是一种非常强大的数据结构,您可以通过分数范围、按字典序、逆序等方式查询元素。要了解更多信息,[请查阅 Redis 官方命令文档中的 Sorted Set 部分](/docs/latest/commands/#sorted_set)。

Hash 数据类型

这是我们在程序中使用的最后一种数据结构,它非常容易理解,因为几乎每种编程语言都有与之对应的结构:哈希。Redis Hashes 基本类似于 Ruby 或 Python 的哈希,是一个关联着字段和值的集合:

HMSET myuser name Salvatore surname Sanfilippo country Italy
HGET myuser surname => Sanfilippo

[`HMSET`](/docs/latest/commands/hmset/) 可用于设置哈希中的字段,稍后可以使用 [`HGET`](/docs/latest/commands/hget/) 进行检索。可以使用 [`HEXISTS`](/docs/latest/commands/hexists/) 检查字段是否存在,或者使用 [`HINCRBY`](/docs/latest/commands/hincrby/) 递增哈希字段等。

Hashes 是表示**对象**的理想数据结构。例如,我们在 Twitter 克隆中使用 Hashes 来表示 Users 和 Updates。

好了,我们刚刚介绍了 Redis 主要数据结构的基础知识,现在可以开始编码了!

先决条件

如果您还没有下载 [Retwis 源代码](https://github.com/antirez/retwis),请立即获取。它包含一些 PHP 文件,以及我们在本示例中使用的 PHP 客户端库 [Predis](https://github.com/nrk/predis) 的副本。

您可能还需要一个可用的 Redis 服务器。只需获取源代码,使用 make 构建,使用 ./redis-server 运行,就可以开始了。为了在您的计算机上使用或运行 Retwis,根本不需要任何配置。

数据布局

使用关系数据库时,必须设计数据库 schema,以便了解数据库将包含的表、索引等。Redis 中没有表,那么我们需要设计什么?我们需要确定表示对象需要哪些键,以及这些键需要持有哪种类型的值。

让我们从用户开始。当然,我们需要表示用户,包括他们的用户名、用户 ID、密码、关注给定用户的用户集合、给定用户关注的用户集合等等。第一个问题是,我们应该如何标识一个用户?就像在关系数据库中一样,一个好的解决方案是用不同的数字标识不同的用户,这样我们就可以为每个用户关联一个唯一的 ID。使用我们的原子 [`INCR`](/docs/latest/commands/incr/) 操作可以非常简单地创建唯一的 ID。当我们创建一个新用户时,假设用户名为“antirez”,我们可以这样做:

INCR next_user_id => 1000
HMSET user:1000 username antirez password p1pp0

注意:在实际应用程序中,您应该使用哈希处理过的密码,为了简单起见,我们在此存储明文密码。

我们使用 next_user_id 键,以便为每个新用户始终获取一个唯一的 ID。然后,我们使用这个唯一的 ID 来命名持有用户数据 Hash 的键。**这在键值存储中是一种常见的设计模式!** 请记住这一点。除了已经定义的字段之外,我们还需要一些其他信息来完整定义一个 User。例如,有时能够通过用户名获取用户 ID 可能很有用,因此每次添加用户时,我们还会填充 users 键(它是一个 Hash),以用户名为字段,用户 ID 为值。

HSET users antirez 1000

这起初可能看起来很奇怪,但请记住,我们只能以直接的方式访问数据,没有二级索引。无法告诉 Redis 返回持有特定值的键。这也是**我们的优势**。这种新范式迫使我们组织数据,使得所有内容都可以通过**主键**访问,用关系数据库术语来说就是这样。

关注者(Followers)、正在关注(Following)和更新

我们的系统还有另一个核心需求。一个用户可能有关注他们的用户,我们称之为关注者(followers)。一个用户可能关注其他用户,我们称之为正在关注(following)。我们有一种完美的数据结构来处理这种情况。那就是... Sets。Set 元素的唯一性,以及我们可以在常数时间内测试成员是否存在的特性,是两个有趣的特点。但是,如果我们还需要记住某个用户开始关注另一个用户的时间呢?在我们这个简单的 Twitter 克隆的增强版本中,这可能会有用,因此我们不使用简单的 Set,而是使用 Sorted Set,将正在关注或关注者的用户 ID 作为元素,将用户之间建立关系时的 Unix 时间作为分数(score)。

所以我们来定义我们的键:

followers:1000 => Sorted Set of uids of all the followers users
following:1000 => Sorted Set of uids of all the following users

我们可以使用以下命令添加新的关注者:

ZADD followers:1000 1401267618 1234 => Add user 1234 with time 1401267618

我们还需要一个重要的地方来添加要在用户主页上显示的更新。稍后我们需要按时间顺序访问这些数据,从最新的更新到最旧的更新,因此最适合的数据结构类型是 List。基本上,每一个新的更新都会被 [`LPUSH`](/docs/latest/commands/lpush/) 到用户更新键中,并且借助 [`LRANGE`](/docs/latest/commands/lrange/),我们可以实现分页等功能。请注意,我们交替使用*更新(updates)*和*帖子(posts)*这两个词,因为更新在某种意义上就是“小帖子”。

posts:1000 => a List of post ids - every new post is LPUSHed here.

这个列表基本上就是用户的时间线。我们将把用户自己的帖子的 ID 推送进去,以及所有被关注用户创建的帖子的 ID。基本上,我们将实现一个写扇出(write fanout)机制。

认证

好的,除了认证之外,我们基本上已经涵盖了关于用户的所有内容。我们将以一种简单但健壮的方式处理认证:我们不想使用 PHP session,因为我们的系统必须易于分布在不同的 Web 服务器之间,所以我们将整个状态保存在 Redis 数据库中。我们只需要一个随机的**不可猜测的**字符串作为已认证用户的 cookie,以及一个包含持有该字符串的客户端的用户 ID 的键。

为了使这项工作可靠地进行,我们需要两件事。首先:当前的认证**密钥**(随机的不可猜测的字符串)应该是 User 对象的一部分,所以当用户创建时,我们也在其 Hash 中设置一个 auth 字段

HSET user:1000 auth fea5e81ac8ca77622bed1c2132a021f9

此外,我们需要一种将认证密钥映射到用户 ID 的方式,因此我们还需要一个 auths 键,它的值是一个 Hash 类型,将认证密钥映射到用户 ID。

HSET auths fea5e81ac8ca77622bed1c2132a021f9 1000

为了认证一个用户,我们将执行以下简单步骤(请参阅 Retwis 源代码中的 login.php 文件):

  • 通过登录表单获取用户名和密码。
  • 检查 users Hash 中 username 字段是否实际存在。
  • 如果存在,我们就得到了用户 ID(例如 1000)。
  • 检查 user:1000 的密码是否匹配,如果不匹配,返回错误消息。
  • 好的,认证成功!将“fea5e81ac8ca77622bed1c2132a021f9”(user:1000 的 auth 字段的值)设置为“auth”cookie。

这是实际的代码:

include("retwis.php");

# Form sanity checks
if (!gt("username") || !gt("password"))
    goback("You need to enter both username and password to login.");

# The form is ok, check if the username is available
$username = gt("username");
$password = gt("password");
$r = redisLink();
$userid = $r->hget("users",$username);
if (!$userid)
    goback("Wrong username or password");
$realpassword = $r->hget("user:$userid","password");
if ($realpassword != $password)
    goback("Wrong username or password");

# Username / password OK, set the cookie and redirect to index.php
$authsecret = $r->hget("user:$userid","auth");
setcookie("auth",$authsecret,time()+3600*24*365);
header("Location: index.php");

这发生在用户每次登录时,但我们还需要一个 isLoggedIn 函数来检查给定用户是否已认证。这些是 isLoggedIn 函数执行的逻辑步骤:

  • 从用户获取“auth”cookie。如果不存在 cookie,用户当然没有登录。我们将 cookie 的值称为 <authcookie>
  • 检查 auths Hash 中 <authcookie> 字段是否存在,以及其值(用户 ID)是什么(示例中是 1000)。
  • 为了使系统更健壮,还要验证 user:1000 的 auth 字段是否也匹配。
  • 好的,用户已认证,我们将一些信息加载到了 $User 全局变量中。

代码可能比描述更简单:

function isLoggedIn() {
    global $User, $_COOKIE;

    if (isset($User)) return true;

    if (isset($_COOKIE['auth'])) {
        $r = redisLink();
        $authcookie = $_COOKIE['auth'];
        if ($userid = $r->hget("auths",$authcookie)) {
            if ($r->hget("user:$userid","auth") != $authcookie) return false;
            loadUserInfo($userid);
            return true;
        }
    }
    return false;
}

function loadUserInfo($userid) {
    global $User;

    $r = redisLink();
    $User['id'] = $userid;
    $User['username'] = $r->hget("user:$userid","username");
    return true;
}

对于我们的应用程序来说,将 loadUserInfo 作为单独的函数是多余的,但在复杂的应用程序中这是一个不错的方法。所有认证流程中唯一缺少的是注销。注销时我们做什么?很简单,我们只需更改 user:1000 的 auth 字段中的随机字符串,从 auths Hash 中删除旧的认证密钥,并添加新的认证密钥。

**重要提示:** 注销过程解释了为什么我们不仅在 auths Hash 中查找认证密钥后就认证用户,还要对照 user:1000 的 auth 字段进行二次检查。真正的认证字符串是后者,而 auths Hash 只是一个认证字段,它甚至可能是易失的,或者如果程序存在 bug 或脚本被中断,我们甚至可能在 auths 键中出现指向同一个用户 ID 的多个条目。以下是注销代码(logout.php):

include("retwis.php");

if (!isLoggedIn()) {
    header("Location: index.php");
    exit;
}

$r = redisLink();
$newauthsecret = getrand();
$userid = $User['id'];
$oldauthsecret = $r->hget("user:$userid","auth");

$r->hset("user:$userid","auth",$newauthsecret);
$r->hset("auths",$newauthsecret,$userid);
$r->hdel("auths",$oldauthsecret);

header("Location: index.php");

这正是我们所描述的,应该很容易理解。

更新

更新(也称为帖子)甚至更简单。为了在数据库中创建一个新帖子,我们这样做:

INCR next_post_id => 10343
HMSET post:10343 user_id $owner_id time $time body "I'm having fun with Retwis"

正如您所见,每个帖子都由一个包含三个字段的 Hash 表示。拥有帖子的用户 ID、帖子发布的时间,最后是帖子的正文,即实际的状态消息。

创建帖子并获得帖子 ID 后,我们需要将 ID [`LPUSH`](/docs/latest/commands/lpush/) 到所有关注帖子作者的用户的时间线中,当然也包括作者本人的帖子列表中(每个人实际上都在关注自己)。以下是展示如何执行此操作的 post.php 文件:

include("retwis.php");

if (!isLoggedIn() || !gt("status")) {
    header("Location:index.php");
    exit;
}

$r = redisLink();
$postid = $r->incr("next_post_id");
$status = str_replace("\n"," ",gt("status"));
$r->hmset("post:$postid","user_id",$User['id'],"time",time(),"body",$status);
$followers = $r->zrange("followers:".$User['id'],0,-1);
$followers[] = $User['id']; /* Add the post to our own posts too */

foreach($followers as $fid) {
    $r->lpush("posts:$fid",$postid);
}
# Push the post on the timeline, and trim the timeline to the
# newest 1000 elements.
$r->lpush("timeline",$postid);
$r->ltrim("timeline",0,1000);

header("Location: index.php");

函数的核心是 foreach 循环。我们使用 [`ZRANGE`](/docs/latest/commands/zrange/) 获取当前用户的所有关注者,然后循环将帖子 [`LPUSH`](/docs/latest/commands/lpush/) 到每个关注者的时间线 List 中。

注意,我们还维护了一个所有帖子的全局时间线,这样在 Retwis 主页上,我们可以轻松显示每个人的更新。这只需要将帖子 [`LPUSH`](/docs/latest/commands/lpush/) 到 timeline List 中。说实话,您是否开始觉得使用 SQL 的 ORDER BY 按时间顺序对添加的内容进行排序有点奇怪?我认为是这样。

上面的代码有一个有趣的地方:我们在对全局时间线执行 [`LPUSH`](/docs/latest/commands/lpush/) 操作后,使用了一个名为 [`LTRIM`](/docs/latest/commands/ltrim/) 的新命令。这是为了将列表修剪到只剩 1000 个元素。全局时间线实际上只用于在主页显示少量帖子,没有必要保留所有帖子的完整历史记录。

基本上,[`LTRIM`](/docs/latest/commands/ltrim/) + [`LPUSH`](/docs/latest/commands/lpush/) 是一种在 Redis 中创建**有上限集合(capped collection)**的方式。

更新分页

现在应该很清楚我们如何使用 [`LRANGE`](/docs/latest/commands/lrange/) 来获取帖子范围,并在屏幕上渲染这些帖子了。代码很简单:

function showPost($id) {
    $r = redisLink();
    $post = $r->hgetall("post:$id");
    if (empty($post)) return false;

    $userid = $post['user_id'];
    $username = $r->hget("user:$userid","username");
    $elapsed = strElapsed($post['time']);
    $userlink = "<a class=\"username\" href=\"profile.php?u=".urlencode($username)."\">".utf8entities($username)."</a>";

    echo('<div class="post">'.$userlink.' '.utf8entities($post['body'])."<br>");
    echo('<i>posted '.$elapsed.' ago via web</i></div>');
    return true;
}

function showUserPosts($userid,$start,$count) {
    $r = redisLink();
    $key = ($userid == -1) ? "timeline" : "posts:$userid";
    $posts = $r->lrange($key,$start,$start+$count);
    $c = 0;
    foreach($posts as $p) {
        if (showPost($p)) $c++;
        if ($c == $count) break;
    }
    return count($posts) == $count+1;
}

showPost 将简单地将帖子转换为 HTML 并打印出来,而 showUserPosts 获取一个帖子范围,然后将其传递给 showPosts

注意:如果帖子列表非常大,并且我们想要访问列表中间的元素,[`LRANGE`](/docs/latest/commands/lrange/) 的效率不是很高,因为 Redis Lists 的底层是链表。如果系统需要对数百万个条目进行深度分页,最好改用 Sorted Sets。

关注用户

这并不难,但我们还没有检查如何创建正在关注/关注者关系。如果用户 ID 1000 (antirez) 想要关注用户 ID 5000 (pippo),我们需要创建正在关注关系和关注者关系。我们只需要执行 [`ZADD`](/docs/latest/commands/zadd/) 调用:

    ZADD following:1000 5000
    ZADD followers:5000 1000

请注意这里再次出现的相同模式。理论上,在关系数据库中,正在关注和关注者的列表将包含在一个具有 following_idfollower_id 等字段的单一表中。您可以使用 SQL 查询提取每个用户的关注者或正在关注的用户。然而,使用键值数据库则有所不同,因为我们需要同时设置 1000 正在关注 50005000 被 1000 关注 这两种关系。这是需要付出的代价,但另一方面,访问数据更简单、速度极快。将这些关系存储为独立的集合,我们可以做一些有趣的事情。例如,使用 [`ZINTERSTORE`](/docs/latest/commands/zinterstore/),我们可以获得两个不同用户的 following 的交集,这样我们就可以为我们的 Twitter 克隆添加一个功能,当您访问其他人的个人资料时,能够非常快速地告诉您“您和 Alice 有 34 个共同关注者”,等等。

您可以在 follow.php 文件中找到设置或删除正在关注/关注者关系的代码。

使其可横向扩展

亲爱的读者,如果您读到了这里,您已经是英雄了。谢谢。在讨论横向扩展之前,值得先检查单服务器性能。Retwis**极快**,没有任何缓存。在一个非常慢且负载高的服务器上,使用 100 个并行客户端发起 100000 个请求进行的 Apache Benchmark 测量显示平均页面加载时间为 5 毫秒。这意味着您仅用一台 Linux 服务器每天就可以服务数百万用户,而这台服务器还是极其慢的... 想象一下在更现代的硬件上会得到什么样的结果。

然而,您不可能永远只使用一台服务器,那么如何扩展键值存储呢?

Retwis 不执行任何多键操作,因此使其可扩展很简单:您可以使用客户端分片,或者像 Twemproxy 这样的分片代理,或者即将推出的 Redis Cluster。

要了解更多关于这些主题的信息,请阅读[我们关于分片的文档](/docs/latest/operate/oss_and_stack/management/scaling/)。然而,这里要强调的一点是,在键值存储中,如果您仔细设计,数据集会被分割成**许多独立的微小键**。与使用语义更复杂的数据库系统相比,将这些键分布到多个节点更直接且可预测。

给此页面评分
回到顶部 ↑