集群#

redis-py 现在支持集群模式,并提供了一个用于 Redis 集群 的客户端。

集群客户端基于 Grokzen 的 redis-py-cluster,添加了错误修复,现在取代了该库。对这些更改的支持要感谢他的贡献。

要了解有关 Redis 集群的更多信息,请参阅 Redis 集群规范

创建集群 | 指定目标节点 | 多键命令 | 已知 PubSub 限制

创建集群#

将 redis-py 连接到 Redis 集群实例至少需要一个节点用于集群发现。有多种方法可以创建集群实例

  • 使用 'host' 和 'port' 参数

>>> from redis.cluster import RedisCluster as Redis
>>> rc = Redis(host='localhost', port=6379)
>>> print(rc.get_nodes())
    [[host=127.0.0.1,port=6379,name=127.0.0.1:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>], [host=127.0.0.1,port=6378,name=127.0.0.1:6378,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6378,db=0>>>], [host=127.0.0.1,port=6377,name=127.0.0.1:6377,server_type=replica,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6377,db=0>>>]]
  • 使用 Redis URL 规范

>>> from redis.cluster import RedisCluster as Redis
>>> rc = Redis.from_url("redis://localhost:6379/0")
  • 直接通过 ClusterNode 类

>>> from redis.cluster import RedisCluster as Redis
>>> from redis.cluster import ClusterNode
>>> nodes = [ClusterNode('localhost', 6379), ClusterNode('localhost', 6378)]
>>> rc = Redis(startup_nodes=nodes)

当创建 RedisCluster 实例时,它首先尝试建立与提供的启动节点之一的连接。如果所有启动节点都无法访问,则会抛出 'RedisClusterException'。在与集群节点之一建立连接后,RedisCluster 实例将使用 3 个缓存进行初始化:一个槽位缓存,它将 16384 个槽位中的每一个映射到处理它们的节点;一个节点缓存,它包含所有集群节点的 ClusterNode 对象(名称、主机、端口、Redis 连接);以及一个命令缓存,它包含使用 Redis 'COMMAND' 输出检索到的所有服务器支持的命令。有关更多信息,请参阅下面的“RedisCluster 特定选项”。

RedisCluster 实例可以直接用于执行 Redis 命令。当通过集群实例执行命令时,目标节点将被内部确定。当使用基于键的命令时,目标节点将是持有该键槽位的节点。集群管理命令和其他非基于键的命令有一个名为 'target_nodes' 的参数,您可以在其中指定要执行命令的节点。在没有 target_nodes 的情况下,命令将在默认集群节点上执行。作为集群实例初始化的一部分,集群的默认节点将从集群的主节点中随机选择,并在重新初始化时更新。使用 r.get_default_node(),您可以获取集群的默认节点,或者可以使用 'set_default_node' 方法更改它。

'target_nodes' 参数将在下一节“指定目标节点”中解释。

>>> # target-nodes: the node that holds 'foo1's key slot
>>> rc.set('foo1', 'bar1')
>>> # target-nodes: the node that holds 'foo2's key slot
>>> rc.set('foo2', 'bar2')
>>> # target-nodes: the node that holds 'foo1's key slot
>>> print(rc.get('foo1'))
b'bar'
>>> # target-node: default-node
>>> print(rc.keys())
[b'foo1']
>>> # target-node: default-node
>>> rc.ping()

指定目标节点#

如上所述,所有非基于键的 RedisCluster 命令都接受 kwarg 参数 'target_nodes',它指定应在哪个节点上执行命令。最佳实践是使用 RedisCluster 类的节点标志指定目标节点:PRIMARIES、REPLICAS、ALL_NODES、RANDOM。当节点标志与命令一起传递时,它将在内部解析为相关的节点。如果集群的节点拓扑在命令执行期间发生变化,客户端将能够使用新的拓扑再次解析节点标志,并尝试重新执行命令。

>>> from redis.cluster import RedisCluster as Redis
>>> # run cluster-meet command on all of the cluster's nodes
>>> rc.cluster_meet('127.0.0.1', 6379, target_nodes=Redis.ALL_NODES)
>>> # ping all replicas
>>> rc.ping(target_nodes=Redis.REPLICAS)
>>> # ping a random node
>>> rc.ping(target_nodes=Redis.RANDOM)
>>> # get the keys from all cluster nodes
>>> rc.keys(target_nodes=Redis.ALL_NODES)
[b'foo1', b'foo2']
>>> # execute bgsave in all primaries
>>> rc.bgsave(Redis.PRIMARIES)

如果您想在节点标志未解决的特定节点/节点组上执行命令,也可以直接传递 ClusterNodes。但是,如果命令执行由于集群拓扑更改而失败,则不会尝试重试,因为传递的目标节点可能不再有效,并且将返回相关的集群或连接错误。

>>> node = rc.get_node('localhost', 6379)
>>> # Get the keys only for that specific node
>>> rc.keys(target_nodes=node)
>>> # get Redis info from a subset of primaries
>>> subset_primaries = [node for node in rc.get_primaries() if node.port > 6378]
>>> rc.info(target_nodes=subset_primaries)

此外,RedisCluster 实例可以查询特定节点的 Redis 实例,并在该节点上直接执行命令。但是,Redis 客户端不处理集群故障和重试。

>>> cluster_node = rc.get_node(host='localhost', port=6379)
>>> print(cluster_node)
[host=127.0.0.1,port=6379,name=127.0.0.1:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>]
>>> r = cluster_node.redis_connection
>>> r.client_list()
[{'id': '276', 'addr': '127.0.0.1:64108', 'fd': '16', 'name': '', 'age': '0', 'idle': '0', 'flags': 'N', 'db': '0', 'sub': '0', 'psub': '0', 'multi': '-1', 'qbuf': '26', 'qbuf-free': '32742', 'argv-mem': '10', 'obl': '0', 'oll': '0', 'omem': '0', 'tot-mem': '54298', 'events': 'r', 'cmd': 'client', 'user': 'default'}]
>>> # Get the keys only for that specific node
>>> r.keys()
[b'foo1']

多键命令#

Redis 在集群模式下支持多键命令,例如 Set 类型并集或交集、mset 和 mget,只要所有键都散列到同一个槽位。通过使用 RedisCluster 客户端,您可以使用已知函数(例如 mget、mset)执行原子多键操作。但是,您必须确保所有键都映射到同一个槽位,否则将抛出 RedisClusterException。Redis 集群实现了一个称为哈希标签的概念,可用于强制将某些键存储在同一个哈希槽位中,请参阅 键哈希标签。您也可以对某些多键操作使用非原子操作,并传递未映射到同一个槽位的键。然后,客户端将把键映射到相关的槽位,并将命令发送到槽位的节点所有者。非原子操作根据键的哈希值对键进行批处理,然后将每个批次分别发送到槽位的拥有者。

# Atomic operations can be used when all keys are mapped to the same slot
>>> rc.mset({'{foo}1': 'bar1', '{foo}2': 'bar2'})
>>> rc.mget('{foo}1', '{foo}2')
[b'bar1', b'bar2']
# Non-atomic multi-key operations splits the keys into different slots
>>> rc.mset_nonatomic({'foo': 'value1', 'bar': 'value2', 'zzz': 'value3')
>>> rc.mget_nonatomic('foo', 'bar', 'zzz')
[b'value1', b'value2', b'value3']

集群 PubSub

当创建 ClusterPubSub 实例时,如果没有指定节点,将在第一个命令执行时透明地选择一个节点用于 pubsub 连接。该节点将通过以下方式确定:1. 对请求中的频道名称进行散列以查找其键槽位 2. 选择处理键槽位的节点:如果 read_from_replicas 设置为 true,则可以选择副本。

已知的 PubSub 限制#

由于键槽位,模式订阅和发布目前无法正常工作。如果我们对像 fo* 这样的模式进行散列,我们将收到该字符串的键槽位,但基于此模式的频道名称有无限种可能性 - 事先无法得知。此功能未被禁用,但目前不建议使用这些命令。有关更多信息,请参阅 redis-py-cluster 文档

>>> p1 = rc.pubsub()
# p1 connection will be set to the node that holds 'foo' keyslot
>>> p1.subscribe('foo')
# p2 connection will be set to node 'localhost:6379'
>>> p2 = rc.pubsub(rc.get_node('localhost', 6379))

只读模式

默认情况下,Redis 集群在访问副本节点时始终返回 MOVE 重定向响应。您可以克服此限制并通过触发 READONLY 模式来扩展读取命令。

要启用 READONLY 模式,请将 read_from_replicas=True 传递给 RedisCluster 构造函数。当设置为 true 时,读取命令将在主节点及其副本之间以循环方式分配。

可以通过调用 `readonly()` 方法并将 `target_nodes` 设置为 `'replicas'` 在运行时设置只读模式,可以通过调用 `readwrite()` 方法恢复读写访问权限。

>>> from cluster import RedisCluster as Redis
# Use 'debug' log level to print the node that the command is executed on
>>> rc_readonly = Redis(startup_nodes=startup_nodes,
...                     read_from_replicas=True)
>>> rc_readonly.set('{foo}1', 'bar1')
>>> for i in range(0, 4):
...     # Assigns read command to the slot's hosts in a Round-Robin manner
...     rc_readonly.get('{foo}1')
# set command would be directed only to the slot's primary node
>>> rc_readonly.set('{foo}2', 'bar2')
# reset READONLY flag
>>> rc_readonly.readwrite(target_nodes='replicas')
# now the get command would be directed only to the slot's primary node
>>> rc_readonly.get('{foo}1')