在 Redis 中,究竟什么是 Key?Redis(或任意其它的 key-value 数据库)的初衷是为每块单独的数据提供一个 Key(或称之为标识符)。Redis 通过数据类型迅速扩充了这一概念,一个 key 可以引用到多块数据,甚至上百万块的数据。随着模块的引入,key 的概念进一步被拉伸,因为单独的一块数据已经可以横跨多个 key(例如 RediSearch)。因此,当有人问 Redis 是否是一个 Key-value 数据库时,我一般都回答“它是一个发源于 key-value 的数据库”,但需要注意的是,现在已经很难把 Redis 仅仅当做一个 key-value 数据库来看待了。
然而,在集群中,Redis 的 Key 依然是非常重要的。在 Redis 集群中,每个节点或分片都只有部分 key 和数据。当然了,如果 Redis 以高可用性方式运行,数据还可能会分布在从节点中。但不管怎么说,一个单独的 key 是不会分布到多个节点中的。
一个集群被分割成 16384 个 slot,这是集群能支持的最大节点数或分片数(如果你需要有更多的节点数或分片数,请与我们联系)。由于大多数集群都是由少量节点组成的,这些 slot 用来对 key 做逻辑划分。在以下 4 节点的简化集群实例中,我们有如下的 slot 分布:
举个例子,如果你知道一个 key 它在 slot 2000 上,那你就知道它在 Node #0 上。同样的,如果你知道一个 key 它在 slot 9000 上,那它就在 Node#02 上。实际上,现实中的集群比这要复杂得多,因为 slot 可能一直在节点间移动和重新平衡,但为了理解事务和 key,这种集群的简化概念理解就够了。
那么 key 和 slot 到底是怎么关联起来的呢?这些 slot 实际上都是散列 slot,每个 key 都先用一个散列函数进行计算(这个散列函数可接受任意长度的字符串),计算完成后得到一个散列数字。散列经常用在密码这个类似但又比这复杂得多的场合,一般来说密码都不是直接存储的,而是存储为密码的某个数学表示。在 Redis 中,你访问请求的 Key,本质上也是访问这个 key 的数学表示,只是在这个场景中,我们使用的是 crc16 散列函数。Crc16 散列函数返回的是一个 14bit 的整数,我们用这个返回的整数除于 16384 并取其余数。这刚好是 Redis 集群能支持的最大分片数,很有趣,不是吗?(译者注:作者可能想表达的意思是,crc16 返回的是一个 14bit 的整数,2 的 14 次方刚好是 16384,集群最大能支持的 slot 个数也是 16384,这两个数字刚好相等,所以有趣。)
这一切如何和 Redis 事务关联起来?
Redis 的事务仅支持单个 Slot,这可确保 Redis 的最大吞吐量。因为不需要节点/分片间的通信,这也避免了很多的故障情况。鉴于此,当你在 Redis 中执行事务的时候,你要确保所有 Key 都在同一个 slot 中。那么,怎么知道事务中一个 key 跟其它 key 是在同一个 slot 中呢(或者在同一个节点/分片中)?
虽然许多 key 可能是属于同一个 slot 的,但从 key 的命名角度来说这也是难于预测的,给 key 命名时每次都去检查下 key 是否同一个 slot 也不现实(Redis 开源版本或企业版本中,都支持 CLUSTER KEYSLOT 命令)。解决这个问题的最佳方式是对 key 进行规划和使用 hash tag 特性。在开源 Redis 中,花括号{}表示 hash tag,这个两个花括号中间的字符才会进行 CRC16 散列计算。让我们看几个例子:
在这些示例中,你可以看到,user-session:1234 和 user-profile:1234 这两个 key 是不允许在同一个事务中的,但 user-profile:{1234} 和 user-session:{1234} 是可以在同一个事务中的。
注意:你可能会想:“太好了,那我将所有 key 都放在同一个 slot 中,那就不用去担心集群的事务了”。很好,你并不孤单,因为我已经不是第一次听到这种不良导向了。Redis 不会阻止你干这种事情或者其它类似的事情,但你将会得到一个不均衡的集群,或者可能更糟糕的是,一个节点爆满了,但其他很多节点都是空的。只在有必要的时候使用 hash tag ,也同时要谨慎的使用它。
Redis 企业版也可以支持这种策略,但还增加了一个特性,使得数据分片更加透明。这个策略不是使用花括号,而是使用正则表达式来定义 key 中的哪部分进行 hash 运算。让我们用正则表达式“/user-.*:(?.+)/ ”来重新审视下上面的那个例子:
这个正则表达式足够灵活,以致于其它“user-”开头的 key 也可以被处理,所以我们也可以使用类似“user-image”或“user-pagecount”的 key。采用这个方案,每个用户的信息都保存在一个单独的 slot 中,可保证一个用户范围内的各种事务都可以被处理。
让我们来进一步扩展下这个样例。假设用户更改了他个人资料的一些信息,我们想要同时更新他的个人资料以及会话信息,并延长会话使会话不被过期。以下是一个典型(简化的)的事务实现版本:
如果你要在 Redis 开源版本中运行这个样例,你必须保证这些 key 都有花括号,否则你会得到一个 CROSSSLOT 错误。这个错误的好处就是 Redis 会立刻通知你无效事务和跨 slot 违规。
请记住,slot 和 key 的问题并不局限于事务,其他场景下它们也是类似的,特别是一个命令需要操作多个 Key 的时候。举个例子:
RPOPLPUSH 是个原子操作,它从一个 list 中取出元素,并将元素放入到另外一个 list 中,这个操作是原子性的。如果这两个 list 是两个不同的 slot 中(就像事务场景那样),你会得到一个 CROSSSLOT 错误。Redis 开源版本是严格限制这点的,任何命令同时操作两个 slot 都是禁止的。
充分利用集群
如果你已经是 Redis 单实例的高级用户,切换到集群后你会感觉到有些奇怪。你已经依赖的部分命令或事务,在集群中可能无法正常工作。如果你真的不走运,你之前设计的 keyspace 可能都会有问题。以下是让你的应用程序与 Redis 集群最佳工作的一些设计提示:
1、考虑 keyspace 设计。key 是否存在一些公共特征可将工作负载智能分散(按用户、按操作,按时间,或者其他)。使用 hashtag 或正则表达式将 key 分散到不同的 slot。
2、避免使用单个 key 来保存全局状态,而且这个操作还是事务性的,否则你很大可能会碰到 CROSSSLOTS 错误。
3、评估你的 MULTI/EXEC 事务。考虑下你是否真的需要事务,或者使用 pipeline 是否就能满足你要求。也不要忘记考虑多 key 的命令,这些多 key 操作的命令是否可以替换为多个命令来操作。
本文转载自公众号中间件小哥(ID:huawei_kevin)。
原文链接:
https://mp.weixin.qq.com/s/KKcd1LmVAvOF51jEx1Yvvw
评论