RabbitMQ基本概念

RabbitMQ , 是一个使用 erlang 编写的 AMQP (高级消息队列协议) 的服务实现. 简单来说, 就是一个功能强大的消息队列服务.

通常我们谈到队列服务, 会有三个概念, 发消息者 , 队列 , 收消息者 . ( 消息 本来也应该算是一个独立的概念, 但是简单处理之下, 它可能并没有太多的内涵)

流程上是, 发消息者 把消息放到 队列 中去, 然后 收消息者 从 队列 中取出消息.

RabbitMQ 在这个基本概念之上, 多做了一层抽象, 在 发消息者 和 队列 之间, 加入了 交换器(Exchange) . 这样 发消息者 和 队列 就没有直接联系, 转而变成 发消息者 把消息给 交换器 , 交换器根据调度策略再把消息再给 队列 .

当然, 多一层抽象会增加复杂度, 但是同时, 功能上也更灵活. 事实上, 很多时候面对具体场景时, 在这种"四段式"的结构下, 你可选择的方案不止一种的. 不过也不必过于担心, 在一些自我规定的"原则"之下, "正确"的方案也不会那么纠结.

总结一下 4 + 1 个概念, 或者说, 五种角色:

Producing , 生产者, 产生消息的角色.
Exchange , 交换器, 在得到生产者的消息后, 把消息扔到队列的角色.
Queue , 队列, 消息暂时呆的地方.
Consuming , 消费者, 把消息从队列中取出的角色.
消息 Message , RabbitMQ 中的消息有自己的一系列属性, 某些属性对信息流有直接影响.

在使用过程中, 我们通常还会关注如下的机制:

持久化 , 服务重启时, 是否能恢复队列中的数据.
调度策略 , 交换器如何把消息给到哪些队列, 是每个队列给一条, 或者把一条消息给多个队列.
分配策略 , 队列面对消费者时, 如何把消息吐出去, 来一个消费者就把消息全给它, 还是只给一条.
状态反馈 , 当消息从某一个队列中被提出后, 这个消息的生命周期就此结束, 还是说需要一个具体的信号以明确标识消息已被正确处理.

上面这些内容, 初看之下好像情况有些复杂了, 不过在具体使用过程中, 这些东西都是很自然地需要考虑的. 当一套服务跑起来之后, 这些细枝末节自然消失在无形之中.

3. 基本形式

当服务启在 5672 端口之后, 我们就可以开始使用 RabbitMQ 了.

根据前面的内容, 我们需要站在两个角度(消息的提供方, 和消息的使用方), 去分别考虑五种角色的情况. 当然, 在使用时其实只是两个角度, 每边四种角色的情况. 因为消息的提供方不关心使用方, 反之, 消息的使用方也不关心消息的提供方. 这种关系上的无依赖本身是"队列服务"的一个最大使用意义所在, 用于业务间的分离(不管是分了好, 还是必须分).

我们先看如何产生消息, 即把消息放到队列当中, 等待下一步的处理.

(之后的代码, 使用 Python , 相应的 AMQP 协议实现的模块是 pika )

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='hello')
channel.queue_bind(exchange='first', queue='hello')
channel.basic_publish(exchange='first', routing_key='', body='Hello World!')

上面代码的细节先不用管它, 但是直观看到做的事有:

获取连接.
    从连接上获取一个 channel , 类似于数据库访问在连接上获取一个 cursor .
    声明一个 exchange . (只会创建一次)
    声明一个 queue . (只会创建一次)
    把 queue 绑定到 exchange 上.
    向指定的 exchange 发送一条消息.

消息发出之后, 可以使用 rabbitmqctl 这个工具查看服务的一些当前状态, 比如队列情况:

$ ./rabbitmqctl list_queues
Listing queues ...
hello    3
...done.

然后是另一边, 从队列取出消息:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')

def callback(ch, method, properties, body):
    print body

channel.basic_consume(callback, queue='hello', no_ack=True)
channel.start_consuming()

前面部分和之前的一样, 获取连接, 拿到 channel . 这里声明 queue 是在做重复的事(之前 Producing的代码已经做过声明了). 但是 Producing 和 Consuming 的代码你并不知道哪一个会先执行, 所以为了确保需要的 queue 是存在的, 使用时总先声明一下是好的方式.

接下来就是定义了一个异步回调, 标明在获取到消息之后要执行的处理函数.

最后, 开始接收服务器的消息.

和前面一样, 看一下代码做的事:

获取连接.
    从连接上拿到 channel .
    声明需要的 queue .
    定义一个从指定 queue 获取消息的回调处理.
    开始接收消息.

两边的代码都完成了, 可以先把取出消息的代码跑起来, 然后再重复运行产生消息的代码, 就能看到效果.

这里我们可以先反思一下我们的思维. 从流程上来说, 之前我们是先考虑如何产生消息, 然后是如何获取消息. 我们按这个顺序来编写了两段代码. 但是我们在使用时, 顺序反过来是一种更直观的方式. 即先是有服务跑起来, 守着等消息. 然后才是不定时有消息产生出来. 这一前一后在思维上是有一些微妙的不同的. 如果从 C/S 结构上来看, Consuming 的角色更像是 Server , 而 Producing 的角色更像是Client .

为什么在这里讲这个呢, 是因为稍后会依次介绍整个流程中的细节, 比如 exchange 的调度策略, 多个Producing , 多个 Consuming , 多个 Queue 的情况下, 我们如何去实现期望的行为. 当系统中的元素与角色有些多的时候, 我们需要一个比较明确的思维方式来保持自己的清醒.

基本的流程就是前面的两段代码. 接下来会依次介绍提到过的, 我们关心的几个机制.

持久化
    调度策略
    分配策略
    状态反馈

然后, 会有几个实例分析, 用以演示一些典型的使用模式.

4. 持久化

考虑这样的场景, 当消息被暂存到队列后, 在没有被提取的情况下, RabbitMQ 服务停掉了怎么办.

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='hello')
channel.queue_bind(exchange='first', queue='hello')
channel.basic_publish(exchange='first', routing_key='', body='Hello World!')

上面的代码, 我们创建了一条内容为 Hello World! 的消息, 通过命令行工具:

$  ./rabbitmqctl list_queues
Listing queues ...
hello    1
...done.

$ ./rabbitmqctl list_exchanges
Listing exchanges ...
    direct
amq.direct    direct
amq.fanout    fanout
amq.headers    headers
amq.match    headers
amq.rabbitmq.log    topic
amq.rabbitmq.trace    topic
amq.topic    topic
first    fanout
...done.

可以查到, 在名为 hello 的队列中, 有 1 条消息. 有一个类型为 fanout , 名为 first 的交换器.

此时通过 Ctrl-C 或 ./rabbitmqctl stop 把 RabbitMQ 服务停掉, 再重启. 交换器, 队列, 消息都是不会恢复的.

所以, 默认情况下, 消息, 队列, 交换器 都不具有持久化的性质. 如果我们需要持久化功能, 那么在声明的时候就需要配置好.

交换器和队列的持久化性质, 在声明时通过一个 durable 参数即可实现:

channel.exchange_declare(exchange='first', type='fanout', durable=True)
channel.queue_declare(queue='hello', durable=True)

这样, 在服务重启之后, first 和 hello 都会恢复. 但是 hello 中的消息不会, 还需要额外配置. 这是 消息 的属性的相关内容:

channel.basic_publish(exchange='first',
                      routing_key='',
                      body='Hello World!',
                      properties=pika.BasicProperties(
                         delivery_mode = 2,
                      ))

通过 properties , 把此条消息, 仅仅是此条消息配置成需要持久化的. 这样, 在服务重启之后, 队列中的这种消息就可以恢复.

这里注意一下, 消息的持久化并不是一个很强的约束, 涉及数据落地的时机, 及系统层面的 fsync 等问题, 不要认为消息完全不会丢. 如果要尽可能高地提高消息的持久化的有效性, 还需要配置其它的一些机制, 比如后面会谈到的 状态反馈 中的 confirm mode.

交换器 , 队列, 消息 这三者的持久化问题都介绍过了. 前两者是一经声明, 则其性质无法再被更改, 即你不能先声明一个非持久化的队列, 再声明一个持久化的同名队列, 企图修改它, 这是不行的. 你重复声明时, 相关参数需要一致. 当然, 你可以删除它们再重新声明:

channel.queue_delete(queue='hello')
channel.exchange_delete(exchange='first')

5. 调度策略

我们考虑交换器 Exchange 和队列 Queue 的关系. Exchange 在得到消息后会依据规则把消息投到一个或多个队列当中.

在调度策略方面, 有两个需要了解的地方, 一是交换器的类型(前面我们用的是 fanout), 二是交换器和队列的绑定关系. 在绑定了的前提下, 我们再谈不同类型的交换器的规则. 绑定动作本身也会影响交换器的行为.

交换器的类型, 内置的有四种, 分别是:

fanout
    direct
    topic
    headers

下面一一介绍.

5.1. fanout

故名思义, fanout 类型的交换器, 其行为是把消息转发给所有绑定的队列上, 就是一个"广播"行为.

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='A')
channel.queue_declare(queue='B')
channel.queue_declare(queue='C')

channel.queue_bind(exchange='first', queue='A')
channel.queue_bind(exchange='first', queue='B')

channel.basic_publish(exchange='first',
                      routing_key='',
                      body='Hello World!')

运行 N 次, 通过 rabbitmqctl 可以看到 A 和 B 中就有 N 条消息, 而 C 中没有消息. 因为只有 A和 B 是绑定到了 first 上的.

5.2. direct

direct 类型的行为是"先匹配, 再投送". 即在绑定时设定一个 routing_key , 消息的 routing_key 匹配时, 才会被交换器投送到绑定的队列中去.

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='direct')
channel.queue_declare(queue='A')
channel.queue_declare(queue='B')

channel.queue_bind(exchange='first', queue='A', routing_key='a')
channel.queue_bind(exchange='first', queue='B', routing_key='b')

channel.basic_publish(exchange='first',
                      routing_key='a',
                      body='Hello World!')

A 和 B 虽然都绑定在了类型为 direct 的 first 上, 但是绑定时的 routing_key 不同.

当一个 routing_key 为 a 的消息出来时, 只会被 first 投送到 A 里.

5.3. topic

topic 和 direct 类似, 只是匹配上支持了"模式", 在"点分"的 routing_key 形式中, 可以使用两个通配符:

* 表示一个词.
    # 表示零个或多个词.

代码:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='topic')
channel.queue_declare(queue='A')
channel.queue_declare(queue='B')

channel.queue_bind(exchange='first', queue='A', routing_key='a.*.*')
channel.queue_bind(exchange='first', queue='B', routing_key='a.#')

channel.basic_publish(exchange='first',
                      routing_key='a',
                      body='Hello World!')

channel.basic_publish(exchange='first',
                      routing_key='a.b.c',
                      body='Hello World!')

在发出的两条消息当中, a 只会被 a.# 匹配到. 而 a.b.c 会被两个都匹配到.

所以, 最终的结果会是 A 中有一条消息, B 中有两条消息.

5.4. headers

headers 也是根据规则匹配, 相较于 direct 和 topic 固定地使用 routing_key , headers 则是一个自定义匹配规则的类型.

在队列与交换器绑定时, 会设定一组键值对规则, 消息中也包括一组键值对( headers 属性), 当这些键值对有一对, 或全部匹配时, 消息被投送到对应队列.

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='headers')
channel.queue_declare(queue='A')
channel.queue_declare(queue='B')

channel.queue_bind(exchange='first', queue='A', arguments={'a': '1'})
channel.queue_bind(exchange='first', queue='B', arguments={'b': '2', 'c': 3, 'x-match': 'all'})

channel.basic_publish(exchange='first',
                      routing_key='',
                      properties=pika.BasicProperties(
                          headers = {'a': '2'},
                      ),
                      body='Hello World!')

channel.basic_publish(exchange='first',
                      routing_key='',
                      properties=pika.BasicProperties(
                          headers = {'a': '1', 'b': '2'},
                      ),
                      body='Hello World!')

绑定时, 通过 arguments 参数设定匹配规则, x-match 是一个特殊的规则, 表示需要全部匹配上, 还是只匹配一条:

all , 全部匹配.
    any , 只匹配一个.

消息的 headers 属性会用于规则的匹配.

上面的代码中, 第一条消息不会匹配任何规则. 第二条消息, 匹配到 A , 但是不会匹配到 B (虽然有一条 b:2 ).

最终的结果是, A 中有一条消息, B 中没有消息.

6. 分配策略

调度策略是影响 Exchange 是不是要把消息给 Queue , 而分配策略影响队列如何把消息给Consuming .

考虑这样的场景: 队列中有多条消息, 每一个消费者取出消息后, 都要花 10 秒来处理它, 处理完一条消息之后才可能再取出一条继续处理. 刚开始只有一个消费者, 过了 2 秒后来了第二个消费者, 此时, 这两个消费者获取消息的行为是一个什么状态?

我们的需求可能是, 当一个消费者来时, 只给它一条消息, 等它再"请求"时, 再给. 或者也可能是, 当有消费者时, 就把目前有的消息全给它(因为不知道是否还有其它的消费者, 所以既然来了一个就让它尽量多处理一些消息).

先产生一些等待处理的消息:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='A')

channel.queue_bind(exchange='first', queue='A')

for i in range(10):
    channel.basic_publish(exchange='first',
                          routing_key='',
                          body=str(i))

然后是消费者的实现:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='A')

def callback(ch, method, properties, body):
    import time
    time.sleep(10)
    print body

channel.basic_consume(callback, queue='A', no_ack=True)
channel.start_consuming()

上面的代码, 是假设处理一条消息需要 10 秒的时间. 但是事实上, 你只要一执行代码, 马上再使用rabbitmqctl 查看队列状态时, 会发现队列已经空了. 因为在关闭 ack 的情况下, Queue 的行为是, 一旦有消费者请求, 那么当前队列中的消息它都会一次性吐很多出去.

ack 机制在后面 状态反馈 会介绍到, 简单来说是一种确认消息被正确处理的机制.

如果我们想一次只吐一条消息, 当其它消费者连上来时, 还可以并行处理, 简单地把 ack 打开就可以了(默认就是打开的).

再考虑一下细节. 当有多个消费者连上时, 它是从队列一次取一条消息, 还是一次取多条消息(这样至少可以改善性能). 这可以通过配置 channel 的 qos 相关参数实现:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='A')
channel.basic_qos(prefetch_count=2)

def callback(ch, method, properties, body):
    import time
    time.sleep(10)
    print body
    ch.basic_ack(delivery_tag = method.delivery_tag)

channel.basic_consume(callback, queue='A', no_ack=False)
channel.start_consuming()

通过配置 prefetch_count 参数, 来设置一次从队列中取多少条消息. 要看到效果, 至少需要启 2 个消费者.

之前是 10 个数字按顺序入了队列, channel 的配置是一次取 2 个, 那么启 2 个消费者的话, 过 10 秒, 在两个消费者的输出中分别能看到 0 , 2 . 这时把两个消费者都 Ctrl-C , 通过 rabbitmqctl 能看到 A 队列中还有 8 条消息.

7. 状态反馈

状态反馈 的功能目的是为了确认行为的结果. 比如, 当你向 Exchange 提交一个消息时, 这个消息是否提交成功, 是否送达到了队列中. 当你从队列中提取消息之后, RabbitMQ 的 Server 如何处理, 因为在提取消息之后, Consuming 可能判断消息有问题, 可能在处理的过程中出现了异常.

在一些关键的节点上, 要保证消息的正确处理, 安全处理, 是需要很多细节上的控制的. AMQP 协议本身也为此作了相关设计, 甚至是事务机制. 事实上在 AMQP 中要确保消息的业务可靠性只能使用事务, 不过在 RabbitMQ 中有一些相应的简便的扩展机制来达到同样目的.

7.1. 信息发布的确认

回看一下之前的一段代码:

channel.basic_publish(exchange='first', routing_key='', body='Hello World!')

这段代码要做的事, 是把一条消息发给名为 first 的交换器. 这个过程中可能出现意外:

exchange 的名字写错了.
    exchange 得到消息后, 发现没有对应的 queue 可以投送.
    投送到 queue 后当前没有消费者来提取它.

上面的三种情况, 第一种, 会直接引发一个调用错误. 第三种, 通常不是问题, 反正消息会在 queue 中暂存. 但是第二种情况很多时候是需要避免的, 否则消息就丢失了, 更严重的是 Producing 对此浑然不知.

在这个地方, 我们就需要确认消息发出之后, 是否成功地被投送到 queue 中去了(或者知道它不能被投送到任何 queue 中去).

要确认这些状态信息, 首先需要把 channel 设置到 confirm mode , 也称之为 Publisher Acknowledgements 机制 (和消息的 ack 机制区分开). 它的目的就是为了确认 Producing 发出的信息的状态.

打开 confirm mode 的方法是:

channel.confirm_delivery()

之后的 publish 行为就可以收到服务器的反馈. 比如在 basic_publish 函数中, 通过mandatory=True 参数来确认发出的消息是否有 queue 接收, 并且所有 queue 都成功接收.

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='A')

#channel.queue_bind(exchange='first', queue='A')

channel.confirm_delivery()

r = channel.basic_publish(exchange='first',
                          routing_key='',
                          body='Hello',
                          mandatory=True,
                         )
print r

上面的代码中, 因为名为 first 的 Exchange 没有绑定任何的 queue , 在 mandatory 参数的作用下, basic_publish 会返回 False .

对于持久化性质, confirm mode 的确认结果是表示, 一条 persisting 的消息, 投送给一个 durable 的队列成功, 并且数据已经成功写到磁盘. 当然, 因为系统缓存的问题, 为确保数据成功落地, 得到确认信息有时可能需要长达几百毫秒的时间, 应用对此应该有所准备, 而不至于在性能上受此影响.

7.2. 消息提取的确认

在未关闭消息的 ack 机制的情况下, 当消息被 Consuming 从队列中提取后, 在未明确获取确认信息之前, 队列中的消息是不会被删除的. 这样, 流程上就变成, 当消息被提取之后, 队列中的这条消息处于"等待确认"的状态. 如果 Consuming 反馈"成功"给队列, 则消息可以安全地被删除了. 如果反馈"拒绝"给队列, 则消息可能还需要再次被其它 Consuming 提取.

看下面的例子, 我们先创建顺序的 10 个数字为内容的 10 条消息:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='A')

channel.queue_bind(exchange='first', queue='A')

for i in range(10):
    channel.basic_publish(exchange='first', routing_key='', body=str(i))

提取消息的逻辑:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
r = channel.basic_get(queue='A', no_ack=False) #0
print r[-1], r[0].delivery_tag

上面的代码会提取第一条消息, 但是并没有向 Queue 反馈此消息是否被正确处理, 所以这条消息在队列中仍然存在, 直到 Connection 被释放后, 被提取过但是未被确认的消息的状态被重置, 它就可以被重新提取.

要确认消息, 或者拒绝消息, 使用对应的 basic_ack 和 baskc_reject 方法:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
r = channel.basic_get(queue='A', no_ack=False) #0
print r[-1], r[0].delivery_tag
#channel.basic_ack(delivery_tag=r[0].delivery_tag)
channel.basic_reject(delivery_tag=r[0].delivery_tag)

AMQP 协议中, 只提供了 reject 方法, 它只能处理一条消息. 因为 Consuming 是可以一次性提取多条消息的, 所以 RabbitMQ 为此做了扩展, 提供了 basic_nack 方法, 它和 basic_reject 的唯一区别就是支持一次性拒绝多条消息.

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
r = channel.basic_get(queue='A', no_ack=False) #0
r = channel.basic_get(queue='A', no_ack=False) #1
r = channel.basic_get(queue='A', no_ack=False) #2
channel.basic_nack(delivery_tag=r[0].delivery_tag, multiple=True)

delivery_tag 是在 channel 中的一个消息计数, 每次消息提取行为都对应一个数字. nack 的multiple 机制会自动把不大于指定 delivery_tag 的消息提取都 reject 掉.

在 reject 和 nack 中还有一个 requeue 参数, 表示被拒绝的消息是否可以被重新分配. 默认是True . 如果消息被 reject 之后, 不希望再被其它的 Consuming 得到, 可以把 requeue 参数设置成False :

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
r = channel.basic_get(queue='A', no_ack=False) #0
channel.basic_nack(delivery_tag=r[0].delivery_tag, multiple=False, requeue=False)

basic_consume 和 basic_get 都是从指定 queue 中提取消息, 前者是一个更高层的方法, 还支持qos 等.

8. 示例: 多消费者, 并行处理

这可能是最常遇到的一种场景了. 消息产生之后堆到队列里, 有多个消费者的 worker 来共同处理这些消息, 以并行的方式提高处理效率.

这种场景在 Exchange 的类似选择上, 不管是 fanout 或者是 direct 都可以实现. 稍有不同在于,fanout 类型的话, 你在一个 exchange 上就不要乱绑定队列. direct 类型的话, 则是需要每条消息自己处理好 routing_key .

这里以 fanout 类型先创建一些消息到 A 这个队列中:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='A')

channel.queue_bind(exchange='first', queue='A')

for i in range(10):
    channel.basic_publish(exchange='first', routing_key='', body=str(i))

消费者的实现没什么特别的:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='A')
channel.basic_qos(prefetch_count=1)

def callback(ch, method, properties, body):
    print body
    import time
    time.sleep(4)
    ch.basic_ack(delivery_tag = method.delivery_tag)

channel.basic_consume(callback, queue='A')
channel.start_consuming()

多个消费者直接跑就行了. A 中的消息会被多个 Consuming 提取处理.

9. 示例: 一条消息多种处理, 临时队列

fanout 典型的广播模式就是我们这里考虑的场景, 其它的还有像"发布/订阅"的模式也是这种. 就是一条消息, 最终会有多个消费者得到它(前面说的多消费者并行处理的场景, 是一条消息, 只会给到一个消费者).

实现上, 自然可以是当一个消费者被创建之后, 同时也创建一个自己的 queue , 然后绑定到指定的exchange 上. 每个 Consuming 有自己的 queue , 那么于其自己做一套命名方法, 不如就忽略 queue的名字, 让系统处理, 这就是 临时队列 .

在声明队列时, 不指定名字:

r = channel.queue_declare()

系统会创建一个队列, 并且随机给一个类似于 amq.gen-a_rJcuQ1mJigV9xp5G_uZQ 这样的名字. 从返回的 r 中可以得到这个信息. 接下来, 就可以把这个队列绑定到 exchange .

但是还有一个问题, Consuming 自己创建了一个 queue , 那么在 Consuming 断掉连接之后, 这个queue 也是应该被销毁的. 自己在 on_close 之类的事件回调中处理不是不过以, 不过 RabbitMQ 有提供现成的机制, 声明 queue 时使用 exclusive=True 即可:

r = channel.queue_declare(exclusive=True)

这样当连接断掉后, 声明的 queue 会被自动删除(相应的 bind 关系也会取消).

一个即插即用的 Consuming 就是:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='first', type='fanout')
r = channel.queue_declare(exclusive=True)
channel.queue_bind(exchange='first', queue=r.method.queue)
channel.basic_qos(prefetch_count=1)

def callback(ch, method, properties, body):
    print body
    import time
    time.sleep(4)
    ch.basic_ack(delivery_tag = method.delivery_tag)

channel.basic_consume(callback, queue=r.method.queue)
channel.start_consuming()

特别之处只是动态创建 queue .

这种情况下的 Producing , 就只关注 Exchange , 不关心 queue 了:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='fanout')

for i in range(10):
    channel.basic_publish(exchange='first', routing_key='', body=str(i))

10. 示例: 发布订阅, 多种形式的实现

"发布/订阅"的模式, 在前面已经提过. 简单地使用临时队列就可以实现.

不过在这里, 我们多思考一点. "发布"的形式倒是单一, 就是把消息提交到 exchange . 但是 "订阅" 的行为, 就可以有多种解释了.

最简单的创建一个临时队列, bind 到了 exchange 上, 就算是"订阅了这个 exchange ".

其它, 还可以针对 direct 或 topic 类型的 exchange , 创建临时队列之后, bind 到 exchange 上时指定routing_key , 这可以说是"订阅了这个 exchange 中的某些 消息 ". headers 类型的 exchange 同理.

换个角度来看这个问题, 是选择使用 exchange 来分割消息, 还是使用 routing_key 来分割消息. 前者在 Producing 阶段会比较麻烦, 因为你需要往多个 exchange 提交消息. 而后者在 Producing 和Consuming 阶段都要多做一些事, Producing 阶段需要正确设定消息的 routing_key , 在 Consuming阶段 bind 时也需要正确设置 routing_key . 更进一步说, 我们在提交消息时, 是愿意选择 exchange , 还是更愿意考虑给一个合适的 routing_key 呢?

当然, exchange 和 routing_key 肯定不是矛盾的. 它们是两个层面的抽象, 彼此应该独立. 在具体业务中使用时, 也应该对应到合适的业务抽象层中去. 就消息而言, 如果是一个有多项目的大系统共用一个RabbitMQ 服务, 那在 exchange 这层可能就是"项目"的分割. 而如果这种环境下你把 exchange 搞成"业务"的分割, 情况就复杂了, 我认为这是错误的设计. "面向数据而不是面向业务"的原则, 在这里同样适用.

11. 示例: 远程调用, 信息流方向与角色转换

队列系统有一个很本质的东西, 就是信息流的方向是单一的. 信息总是被放进 queue 后, 再被取出.

考虑远程调用的模型, "调用"本身是一个"请求/响应"的过程, 这是两个方向的信息流. 对应到队列中, 两个方向, 则至少需要两个队列. 想明白了这点, 我们要做的事就清楚了:

+------------------+   
      +---------->|     queue A      |------------+
      |           +------------------+            |
      |                                           v
  +-------+                                   +-------+
  |       |                                   |       |
  +-------+                                   +-------+
      ^                                           |
      |           +------------------+            |
      +-----------|     queue B      |<-----------+
                  +------------------+

上图是我们要实现的信息流, 左侧是调用方, 要做的事是把参数写到 queue A , 然后从 queue B 中取出结果. 右侧是计算方, 要做的事是从 queue A 中取出参数, 运算后把结果写到 queue B 中.

计算方的代码:

# -*- coding: utf-8 -*-

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.exchange_declare(exchange='rpc_r', type='fanout')
ch.queue_declare(queue='rpc_r')
ch.queue_declare(queue='rpc_p')
ch.queue_bind(exchange='rpc_r', queue='rpc_r')

def callback(channel, method, properties, body):
    print body
    s = sum(int(x) for x in body.split(','))
    channel.basic_publish(exchange='rpc_r', routing_key='', body=str(s))
    channel.basic_ack(delivery_tag = method.delivery_tag)

ch.basic_consume(callback, queue='rpc_p')
ch.start_consuming()

逻辑是从 rpc_p 中取出数据, 计算后把结果写到 rpc_r 中.

调用方代码:

# -*- coding: utf-8 -*-

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.exchange_declare(exchange='rpc_p', type='fanout')
ch.queue_declare(queue='rpc_p')
ch.queue_declare(queue='rpc_r')
ch.queue_bind(exchange='rpc_p', queue='rpc_p')

def callback(channel, method, properties, body):
    print body
    channel.basic_ack(delivery_tag = method.delivery_tag)

ch.basic_consume(callback, queue='rpc_r')
ch.basic_publish(exchange='rpc_p', routing_key='',  body='1,2,3,4')
ch.start_consuming()

逻辑是把参数写到 rpc_p 中, 然后等着从 rpc_r 中读出结果.

两段代码, 实现了最简单的功能. 但是这样有一个很明显的问题, 调用和输出之间, 是没有任何联系的, 即函数调用的输入和输出之间无法对应起来. 当有多个调用方时, 就乱套了. 所以我们需要改进一下, 让输入和输出能一一对应上.

输入部分不用改, 还是使用 rpc_p 保存参数. 输出部分, 我们把一个确定的 queue 换成每次生成的临时队列, 并且把其对应的 exchange 改成 headers 类型, 目的是通过 headers 参数实现, 从 rpc_p 取出一组参数之后, 往 rpc_r 写回的东西只路由到特定的一个临时队列去.

计算方代码:

# -*- coding: utf-8 -*-

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.exchange_declare(exchange='rpc_r', type='headers')
ch.queue_declare(queue='rpc_p')

def callback(channel, method, properties, body):
    print body
    s = sum(int(x) for x in body.split(','))
    q = properties.headers['q']
    channel.basic_publish(exchange='rpc_r', routing_key='', body=str(s),
                              properties=pika.BasicProperties(
                                  headers = {'q': q},
                              )
                         )
    channel.basic_ack(delivery_tag = method.delivery_tag)

ch.basic_consume(callback, queue='rpc_p')
ch.start_consuming()

计算之后, 往 rpc_r 中写回数据时, 数据带上特殊的头, 而特殊的头的值则是原始消息中自带的, 指明了这组参数对应的临时队列.

调用方的代码改成:

# -*- coding: utf-8 -*-

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.exchange_declare(exchange='rpc_p', type='fanout')
ch.exchange_declare(exchange='rpc_r', type='headers')
ch.queue_declare(queue='rpc_p')
ch.queue_bind(exchange='rpc_p', queue='rpc_p')

def callback(channel, method, properties, body):
    print body
    channel.basic_ack(delivery_tag = method.delivery_tag)

rq = ch.queue_declare(exclusive=True)
qname = rq.method.queue
ch.queue_bind(exchange='rpc_r', queue=qname, arguments={'q': qname})
ch.basic_consume(callback, queue=qname)
ch.basic_publish(exchange='rpc_p', routing_key='',  body='1,2,3',
                     properties=pika.BasicProperties(
                          headers = {'q': qname},
                     )
                 )
ch.start_consuming()

调用前生成一个用于保存结果的临时队列, 把这个队列绑定到 rpc_r 这个交换器上, 并且规定了一个路由规则.

然后把参数写到 rpc_p 时, 同时也在数据中写入了结果队列的路由规则.

这是一个很常用的"先挖坑, 再填坑"的方式.

同理, 换成 direct 类型的 exchange , 以 routing_key 保存路由规则也能实现类似的效果.

道理就是上面讲的, 不过做同样的事, RabbitMQ , 或者说 AMQP 中有一些事先定义的参数可以直接拿来用. 比如消息中的 reply_to 参数来标明此消息处理后的结果往哪个队列中送, correlation_id 来标明"请求"与"响应"的对应关系.

计算方实现:

# -*- coding: utf-8 -*-

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.exchange_declare(exchange='rpc_r', type='direct')
ch.queue_declare(queue='rpc_p')

def callback(channel, method, properties, body):
    print body
    s = sum(int(x) for x in body.split(','))
    channel.basic_publish(exchange='rpc_r', routing_key=properties.reply_to, body=str(s),
                          properties=pika.BasicProperties(correlation_id=properties.correlation_id))
    channel.basic_ack(delivery_tag = method.delivery_tag)

ch.basic_consume(callback, queue='rpc_p')
ch.start_consuming()

调用方实现:

# -*- coding: utf-8 -*-

import pika
import uuid

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.exchange_declare(exchange='rpc_p', type='fanout')
ch.exchange_declare(exchange='rpc_r', type='direct')
ch.queue_declare(queue='rpc_p')
ch.queue_bind(exchange='rpc_p', queue='rpc_p')

def callback(channel, method, properties, body):
    print body, properties.correlation_id
    channel.basic_ack(delivery_tag = method.delivery_tag)

rq = ch.queue_declare(exclusive=True)
qname = rq.method.queue
ch.queue_bind(exchange='rpc_r', queue=qname, routing_key=qname)
ch.basic_consume(callback, queue=qname)
ch.basic_publish(exchange='rpc_p', routing_key=qname,  body='1,2,3',
                 properties=pika.BasicProperties(
                     reply_to = qname,
                     correlation_id = uuid.uuid4().hex
                 ))
ch.start_consuming()

12. 消息的BasicProperties

在 AMQP 协议中, 为消息预定了 14 个属性, 有些在前面我们已经用到过了, 有些则本来就很少用到:

content_type 标明消息的类型.
    content_encoding 标明消息的编码.
    headers 可扩展的信息对.
    delivery_mode 为 2 时表示该消息需要被持久化支持.
    priority 该消息的权重.
    correlation_id 用于"请求"与"响应"之间的匹配.
    reply_to "响应"的目标队列.
    expiration 有效期.
    message_id 消息的ID.
    timestamp 一个时间戳.
    type 消息的类型.
    user_id 用户的ID.
    app_id 应用的ID.
    cluster_id 服务集群ID.

13. pika在Tornado中的使用

前面的介绍中, 我们都使用的是 BlockingConnection , 是一个同步阻塞的交互模型. pika 的代码本身在组织时, 其实是以异步的方式来架构的, BlockingConnection 不过是一个特殊的封装而已.

pika 提供了多种异步调度机制的适配实现, 对应 Tornado 的是 TornadoConnection . 同时, 在channel 的 API 上, 本身就有 callback 的实现, 所以, 把之前的代码改成异步的形式, 多是在获取Connection 和 channel 这两步上的异步形式调整.

13.1. Producing

先看同步的代码:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='first', type='fanout')
channel.queue_declare(queue='A')
channel.queue_bind(exchange='first', queue='A')
channel.confirm_delivery()

r = channel.basic_publish(exchange='first', routing_key='', body='hello', mandatory=True)
print r

它做的事有:

获取连接.
    从连接上获取 channel .
    声明 exchange .
    声明 queue .
    绑定 exchange 和 queue .
    打开 confirm mode .
    发出消息.
    确认结果.

除了前面两步: "获取连接" 和 "从连接上获取 channel " , 其它的操作都是在 channel 上完成的. 对应到 Tornado 上来做, 就是:

# -*- coding: utf-8 -*-

import pika
import tornado.gen
import tornado.ioloop

def on_confirm(method):
    print method

def on_publish((ch, method, prop, body)):
    print method.reply_code

@tornado.gen.engine
def on_channel(channel):
    method = yield tornado.gen.Task(channel.exchange_declare, exchange='first', type='fanout')
    method = yield tornado.gen.Task(channel.queue_declare, queue='A')
    #method = yield tornado.gen.Task(channel.queue_bind, exchange='first', queue='A')
    channel.confirm_delivery(callback=on_confirm, nowait=True)
    channel.add_on_return_callback(on_publish)
    channel.basic_publish(exchange='first', routing_key='', body='good', mandatory=True)
    #channel.basic_publish(exchange='first', routing_key='', body='good')
    #channel.close()

def on_connect(conn):
    conn.channel(on_open_callback=on_channel)

def publish():
    pika.TornadoConnection(pika.ConnectionParameters('localhost'),
                           on_open_callback=on_connect)

if __name__ == '__main__':
    publish()
    tornado.ioloop.IOLoop().current().start()

代码好像会复杂一些, 不过继续封装一下用起来也可以很方便.

前面提到过, 好消息是 channel 的所有 API 都带有 callback 实现, 说的是 channel 有所有 API 几乎都有一个 callback 参数. 坏消息是, callback 参数全是第一个参数. 所以, 如果要直接使用tornado.gen.engine 的话, 后面的参数需要全带上参数名以 keyword 形式传递.

从上面异步结构的代码中, 更容易把 RabbitMQ 自己扩展实现的 confirm mode 说清楚了.

channel.confirm_delivery(callback=on_confirm, nowait=True)

是打开 confirm mode 功能, 当向服务器 publish 一条消息, 服务器确认消息之后, 就会回调这里指定的函数. 回调的内容要么是 ACK , 要么是 NACK . 按官方文档的说法, 出现 NACK 的情况只可能是服务内部出现了错误. 而正确的回调函数被执行时, 意即服务器确认了消息内容, 消息已经被所有对应的队列接收, 如果是需要持久化支持的内容, 则相关数据已经写到磁盘.

后面的:

channel.add_on_return_callback(on_publish)

是对应 mandatory=True 的回调的. 即当 publish 出去的消息无法被投递到任何队列时, 服务会回调这里的函数.

上面的 confirm 和 on_return 是两套东西. 示例代码执行时, mandatory=True 生效的情况下, 你会看到如下输入:

312
<METHOD(['channel_number=1', 'frame_type=1', "method=<Basic.Ack(['delivery_tag=1', 'multiple=False'])>"])>

先执行的是 on_return 的回调, 再是 confirm 的回调.

还有一点, 从:

channel.add_on_return_callback(on_publish)

这里也可以看出, pika 的 API 组织上, channel 中有很多的回调函数是单独定义的(可能有 AMQP 协议有关).

add_callback(callback, replies, one_shot=True)
    在当前 channel 上注册指定类型的事件回调.

add_on_cancel_callback(callback)
    使用 basic_cancel 回调的函数.

add_on_close_callback(callback)
    channel 关闭时回调的函数.

add_on_return_callback(callback)
    basic_publish 中的消息被拒绝时的回调函数.

13.2. Consuming

Consuming 部分在异步环境上比 Producing 部分还简单一点. 最大的不同, 可能在于异步环境下需要自己控制对消息的提取(提取之后再次监听, 而不像同步环境下一个 while).

先看同步的代码:

# -*- coding: utf-8 -*-

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='first', type='fanout')
r = channel.queue_declare(exclusive=True)
channel.queue_bind(exchange='first', queue=r.method.queue)
channel.basic_qos(prefetch_count=1)

def callback(ch, method, properties, body):
    print body
    import time
    time.sleep(4)
    ch.basic_ack(delivery_tag = method.delivery_tag)

channel.basic_consume(callback, queue=r.method.queue)
channel.start_consuming()

异步代码:

# -*- coding: utf-8 -*-

import pika
import tornado.gen
import tornado.ioloop
import functools

def on_msg(qname, ch, method, prop, body):
    print qname, body
    ch.basic_ack(delivery_tag = method.delivery_tag)
    ch.basic_consume(functools.partial(on_msg, qname), queue=qname)

@tornado.gen.engine
def on_channel(channel):
    method = yield tornado.gen.Task(channel.exchange_declare, exchange='first', type='fanout')
    method = yield tornado.gen.Task(channel.queue_declare)
    qname = method.method.queue
    method = yield tornado.gen.Task(channel.queue_bind, exchange='first', queue=qname)
    method = yield tornado.gen.Task(channel.basic_qos, prefetch_count=1)
    channel.basic_consume(functools.partial(on_msg, qname), queue=qname)

def on_connect(conn):
    conn.channel(on_open_callback=on_channel)

def consume():
    pika.TornadoConnection(pika.ConnectionParameters('localhost'),
                           on_open_callback=on_connect)

if __name__ == '__main__':
    consume()
    tornado.ioloop.IOLoop().current().start()


redis连接

redis-py提供两个类Redis和StrictRedis用于实现Redis的命令,StrictRedis用于实现大部分官方的命令,

并使用官方的语法和命令,Redis是StrictRedis的子类,用于向后兼容旧版本的redis-py。

import redis 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

r = redis.Redis(host='192.168.19.130', port=6379) host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r.set('foo', 'Bar') key是"foo" value是"bar" 将键值对存入redis缓存

print r.get('foo') Bar 取出键foo对应的值

2、连接池
redis-py使用connection pool来管理对一个redis server的所有连接,避免每次建立、释放连接的开销。

默认,每个Redis实例都会维护一个自己的连接池。

可以直接建立一个连接池,然后作为参数Redis,这样就可以实现多个Redis实例共享一个连接池

import redis 通过python操作redis缓存

pool = redis.ConnectionPool(host='192.168.19.130', port=6379) host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r = redis.Redis(connection_pool=pool)

r.set('foo', 'Bar') key是"foo" value是"bar" 将键值对存入redis缓存

print r.get('foo') Bar 取出键foo对应的值

3、redis基本命令_string

set(name, value, ex=None, px=None, nx=False, xx=False)

在Redis中设置值,默认,不存在则创建,存在则修改
参数:
ex,过期时间(秒)
px,过期时间(毫秒)
nx,如果设置为True,则只有name不存在时,当前set操作才执行
xx,如果设置为True,则只有name存在时,当前set操作才执行

import redis 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='192.168.19.130', port=6379)

host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r = redis.Redis(connection_pool=pool) 创建实例

1 ex,过期时间(秒) 这里过期时间是3秒,3秒后p,键foo的值就变成None
r.set('foo', 'Bar',ex=3) key是"foo" value是"bar" 将键值对存入redis缓存
print r.get('foo') Bar 取出键foo对应的值

2 px,过期时间(豪秒) 这里过期时间是3豪秒,3毫秒后,键foo的值就变成None
r.set('foo', 'Bar',px=3) key是"foo" value是"bar" 将键值对存入redis缓存
print r.get('foo') Bar 取出键foo对应的值

3 nx,如果设置为True,则只有name不存在时,当前set操作才执行 (新建)
print(r.set('foo', 'Bar',nx=True)) None--已经存在
如果键foo已经存在,那么输出是True;如果键foo不存在,输出是None

4 xx,如果设置为True,则只有name存在时,当前set操作才执行 (修改)
print(r.set('foo1', 'Bar',xx=True)) True--已经存在
如果键foo已经存在,那么输出是None;如果键foo不存在,输出是True

5 setnx(name, value)
设置值,只有name不存在时,执行设置操作(添加)
print(r.setnx("foo2","bar")) False--已经存在的话,无法执行

6 setex(name, value, time)
设置值
参数:
time,过期时间(数字秒 或 timedelta对象)
r.setex("foo","bar",5)
print r.get('foo') 5秒后,取值就从bar变成None

7 psetex(name, time_ms, value)
设置值
参数:
time_ms,过期时间(数字毫秒 或 timedelta对象)
r.psetex("foo",5000,"bar")
print r.get('foo') 5000毫秒后,取值就从bar变成None

8 mset(*args, **kwargs)
批量设置值
如:
mset(k1='v1', k2='v2')

mget({'k1': 'v1', 'k2': 'v2'})
r.mset(k1="v1",k2="v2") 这里k1 和k2 不能带引号 一次设置对个键值对
print r.mget("k1","k2") ['v1', 'v2'] 一次取出多个键对应的值
print r.mget("k1") ['v1']

9 mget(keys, *args)
批量获取
如:
mget('ylr', 'wupeiqi')

r.mget(['ylr', 'wupeiqi'])
print r.mget("foo","foo1","foo2","k1","k2") [None, 'Bar', 'bar', 'v1', 'v2']
将目前redis缓存中的键对应的值批量取出来

10 getset(name, value)
设置新值并获取原来的值
print(r.getset("foo1","bar_NEW")) Bar
设置的新值是"bar_NEW" 设置前的值是Bar

11 getrange(key, start, end)
获取子序列(根据字节获取,非字符)
参数:
name,Redis 的 name
start,起始位置(字节)
end,结束位置(字节)
如: "武沛齐" ,0-3表示 "武"
r.set("foo1","武沛齐") 汉字
print(r.getrange("foo1",0,2)) 取索引号是0-2 前3位的字节 武 切片操作 (一个汉字3个字节 1个字母一个字节 每个字节8bit)
print(r.getrange("foo1",0,-1)) 取所有的字节 武沛齐 切片操作

r.set("foo1","bar_new") 字母
print(r.getrange("foo1",0,2)) 取索引号是0-2 前3位的字节 bar 切片操作 (一个汉字3个字节 1个字母一个字节 每个字节8bit)
print(r.getrange("foo1",0,-1)) 取所有的字节 bar_new 切片操作

12 setrange(name, offset, value)
修改字符串内容,从指定字符串索引开始向后替换(新值太长时,则向后添加)
参数:
offset,字符串的索引,字节(一个汉字三个字节)
value,要设置的值
r.setrange("foo1",1,"aaa")
print(r.get("foo1")) baaanew 原始值是bar_new 从索引号是1开始替换成aaa 变成 baaanew
bar_new

13 setbit(name, offset, value)
对name对应值的二进制表示的位进行操作
参数:
name,redis的name
offset,位的索引(将值变换成二进制后再进行索引)
value,值只能是 1 或 0

注:如果在Redis中有一个对应: n1 = "foo",
那么字符串foo的二进制表示为:01100110 01101111 01101111
所以,如果执行 setbit('n1', 7, 1),则就会将第7位设置为1,
那么最终二进制则变成 01100111 01101111 01101111,即:"goo"

扩展,转换二进制表示:
source = "陈思维"
source = "foo"
for i in source:
num = ord(i)
print bin(num).replace('b','')
特别的,如果source是汉字 "陈思维"怎么办?
答:对于utf-8,每一个汉字占 3 个字节,那么 "陈思维" 则有 9个字节
对于汉字,for循环时候会按照 字节 迭代,那么在迭代时,将每一个字节转换 十进制数,然后再将十进制数转换成二进制
11100110 10101101 10100110 11100110 10110010 10011011 11101001 10111101 10010000
-------------------------- ----------------------------- -----------------------------
陈思维

13 应用场景 :统计uv
注:如果在Redis中有一个对应: n1 = "foo",
那么字符串foo的二进制表示为:01100110 01101111 01101111
所以,如果执行 setbit('n1', 7, 1),则就会将第7位设置为1,
那么最终二进制则变成 01100111 01101111 01101111,即:"goo"
r.set("foo","foo1") foo1的二进制表示为:01100110 01101111 01101111 00110001
这里f对应的ascii值是102 折算二进制是 01100110 (64+32+4+2)
这里o对应的ascii值是111 折算二进制是 01101111 (64+32+8+4+2+1)
这里数字1对应的ascii值是49 折算二进制是 00110001 (32+16+1)
r.setbit("foo",7,1) 将第7位设置为1,
print(r.get("foo")) goo1
那么最终二进制则变成 01100111 01101111 01101111 00000001
print(ord("f")) 102 将字符f的ascii对应的值打印出来
print(ord("o")) 111 将字符o的ascii对应的值打印出来
print(chr(103)) g 将ascii数字103对应的字符打印出来
print(ord("1")) 49 将数字1的ascii对应的值打印出来

扩展,转换二进制表示:
source = "陈思维"
source = "foo1"
for i in source:
num = ord(i)
print(num) 打印每个字母字符或者汉字字符对应的ascii码值 f-102-0b100111-01100111
print(bin(num)) 打印每个10进制ascii码值转换成二进制的值 0b1100110(0b表示二进制)
print bin(num).replace('b','') 将二进制0b1100110替换成01100110

特别的,如果source是汉字 "陈思维"怎么办?
答:对于utf-8,每一个汉字占 3 个字节,那么 "陈思维" 则有 9个字节
对于汉字,for循环时候会按照 字节 迭代,那么在迭代时,将每一个字节转换 十进制数,然后再将十进制数转换成二进制
11100110 10101101 10100110 11100110 10110010 10011011 11101001 10111101 10010000

14 getbit(name, offset)
获取name对应的值的二进制表示中的某位的值 (0或1)
print(r.getbit("foo1",0)) 0 foo1对应的二进制 4个字节 32位 第0位是0还是1

15 bitcount(key, start=None, end=None)
获取name对应的值的二进制表示中 1 的个数
参数:
key,Redis的name
start 字节起始位置
end,字节结束位置
print(r.get("foo")) goo1 01100111
print(r.bitcount("foo",0,1)) 11 表示前2个字节中,1出现的个数

16 bitop(operation, dest, *keys)
获取多个值,并将值做位运算,将最后的结果保存至新的name对应的值

参数:
operation,AND(并) 、 OR(或) 、 NOT(非) 、 XOR(异或)
dest, 新的Redis的name
*keys,要查找的Redis的name

如:
bitop("AND", 'new_name', 'n1', 'n2', 'n3')
获取Redis中n1,n2,n3对应的值,然后讲所有的值做位运算(求并集),然后将结果保存 new_name 对应的值中
r.set("foo","1") 0110001
r.set("foo1","2") 0110010
print(r.mget("foo","foo1")) ['goo1', 'baaanew']
print(r.bitop("AND","new","foo","foo1")) "new" 0 0110000
print(r.mget("foo","foo1","new"))

source = "12"
for i in source:
num = ord(i)
print(num) 打印每个字母字符或者汉字字符对应的ascii码值 f-102-0b100111-01100111
print(bin(num)) 打印每个10进制ascii码值转换成二进制的值 0b1100110(0b表示二进制)
print bin(num).replace('b','') 将二进制0b1100110替换成01100110

17 strlen(name)
返回name对应值的字节长度(一个汉字3个字节)
print(r.strlen("foo")) 4 'goo1'的长度是4

18 incr(self, name, amount=1)
自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
参数:
name,Redis的name
amount,自增数(必须是整数)
注:同incrby
r.set("foo",123)
print r.mget("foo","foo1","foo2","k1","k2") ['123', '2', 'bar', 'v1', 'v2']
r.incr("foo",amount=1)
print r.mget("foo","foo1","foo2","k1","k2") ['124', '2', 'bar', 'v1', 'v2']

19 incrbyfloat(self, name, amount=1.0)
自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
参数:
name,Redis的name
amount,自增数(浮点型)
r.set("foo1","123.0")
print r.mget("foo","foo1","foo2","k1","k2") ['124', '123.0', 'bar', 'v1', 'v2']
r.incrbyfloat("foo1",amount=2.0)
r.incrbyfloat("foo3",amount=3.0)
print r.mget("foo","foo1","foo2","foo3","k1","k2") ['124', '125', 'bar', '-3', 'v1', 'v2']

20 decr(self, name, amount=1)
自减 name对应的值,当name不存在时,则创建name=amount,否则,则自减。
参数:
name,Redis的name
amount,自减数(整数)
r.decr("foo4",amount=3) 递减3
r.decr("foo1",amount=1) 递减1
print r.mget("foo","foo1","foo2","foo3","foo4","k1","k2")
['goo1', '121', 'bar', '15', '-18', 'v1', 'v2']

21 append(key, value)
在redis name对应的值后面追加内容
参数:
key, redis的name
value, 要追加的字符串
r.append("foo","abc") 在foo对应的值goo1后面追加字符串abc
print r.mget("foo","foo1","foo2","foo3","foo4","k1","k2")
['goo1abc', '121', 'bar', '15', '-18', 'v1', 'v2']

4 redis基本命令_hash

import redis 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='192.168.19.130', port=6379)

host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r = redis.Redis(connection_pool=pool) 创建实例

1 单个增加--修改(单个取出)--没有就新增,有的话就修改
hset(name, key, value)
name对应的hash中设置一个键值对(不存在,则创建;否则,修改)
参数:
name,redis的name
key,name对应的hash中的key
value,name对应的hash中的value
注:
hsetnx(name, key, value),当name对应的hash中不存在当前key时则创建(相当于添加)
r.hset("foo_hash1","k1","v1")
print(r.mget("foo","foo1","foo2","foo3","foo4","k1","k2"))
['goo1abcabc', '121', 'bar', '15', '-18', 'v1', 'v2'] 取字符串
print(r.hget("foo_hash1","k1")) v1 单个取hash的key
print(r.hmget("foo_hash1","k1")) ['v1'] 批量取hash的key

r.hsetnx("foo_hash1","k2","v2") 只能新建
print(r.hget("foo_hash1","k2")) v2
print(r.hmget("foo_hash1","k2")) ['v2']

2 批量增加(取出)
hmset(name, mapping)
在name对应的hash中批量设置键值对
参数:
name,redis的name
mapping,字典,如:{'k1':'v1', 'k2': 'v2'}
如:
r.hmset('xx', {'k1':'v1', 'k2': 'v2'})
r.hmset("foo_hash2",{"k2":"v2","k3":"v3"})
print(r.hget("foo_hash2","k2")) v2
单个取出"foo_hash2"的key-k2对应的value
print(r.hmget("foo_hash2","k2","k3")) ['v2', 'v3']
批量取出"foo_hash2"的key-k2 k3对应的value --方式1
print(r.hmget("foo_hash2",["k2","k3"])) ['v2', 'v3']
批量取出"foo_hash2"的key-k2 k3对应的value --方式2

hget(name,key)
在name对应的hash中获取根据key获取value
hmget(name, keys, *args)
在name对应的hash中获取多个key的值
参数:
name,reids对应的name
keys,要获取key集合,如:['k1', 'k2', 'k3']
*args,要获取的key,如:k1,k2,k3
如:
r.hmget('xx', ['k1', 'k2'])

print r.hmget('xx', 'k1', 'k2')

3 取出所有的键值对
hgetall(name)
获取name对应hash的所有键值
print(r.hgetall("foo_hash1"))
{'k2': 'v2', 'k1': 'v1'}

4 得到所有键值对的格式 hash长度
hlen(name)
获取name对应的hash中键值对的个数
print(r.hlen("foo_hash1")) 2

5 得到所有的keys(类似字典的取所有keys)
hkeys(name)
获取name对应的hash中所有的key的值
print(r.hkeys("foo_hash1")) ['k1', 'k2'] 取出所有的keys

6 得到所有的value(类似字典的取所有value)
hvals(name)
获取name对应的hash中所有的value的值
print(r.hvals("foo_hash1")) ['v1', 'v2'] 取出所有的values

7 判断成员是否存在(类似字典的in)
hexists(name, key)
检查name对应的hash是否存在当前传入的key
print(r.hexists("foo_hash1","k3")) False 不存在
print(r.hexists("foo_hash1","k1")) True 存在

8 删除键值对
hdel(name,*keys)
将name对应的hash中指定key的键值对删除
print(r.hgetall("foo_hash1")) {'k2': 'v2', 'k1': 'v1'}
r.hset("foo_hash1","k2","v3") 修改已有的key k2
r.hset("foo_hash1","k1","v1") 新增键值对 k1
r.hdel("foo_hash1","k1") 删除一个键值对
print(r.hgetall("foo_hash1")) {'k2': 'v3'}

9 自增自减整数(将key对应的value--整数 自增1或者2,或者别的整数 负数就是自减)
hincrby(name, key, amount=1)
自增name对应的hash中的指定key的值,不存在则创建key=amount
参数:
name,redis中的name
key, hash对应的key
amount,自增数(整数)
r.hset("foo_hash1","k3",123)
r.hincrby("foo_hash1","k3",amount=-1)
print(r.hgetall("foo_hash1")) {'k3': '122', 'k2': 'v3', 'k1': 'v1'}
r.hincrby("foo_hash1","k4",amount=1) 不存在的话,value默认就是1
print(r.hgetall("foo_hash1")) {'k3': '122', 'k2': 'v3', 'k1': 'v1', 'k4': '4'}

10 自增自减浮点数(将key对应的value--浮点数 自增1.0或者2.0,或者别的浮点数 负数就是自减)
hincrbyfloat(name, key, amount=1.0)
自增name对应的hash中的指定key的值,不存在则创建key=amount
参数:
name,redis中的name
key, hash对应的key
amount,自增数(浮点数)
自增name对应的hash中的指定key的值,不存在则创建key=amount
r.hset("foo_hash1","k5","1.0")
r.hincrbyfloat("foo_hash1","k5",amount=-1.0) 已经存在,递减-1.0
print(r.hgetall("foo_hash1"))
r.hincrbyfloat("foo_hash1","k6",amount=-1.0) 不存在,value初始值是-1.0 每次递减1.0
print(r.hgetall("foo_hash1")) {'k3': '122', 'k2': 'v3', 'k1': 'v1', 'k6': '-6', 'k5': '0', 'k4': '4'}

11 取值查看--分片读取
hscan(name, cursor=0, match=None, count=None)
增量式迭代获取,对于数据大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而放置内存被撑爆
参数:
name,redis的name
cursor,游标(基于游标分批取获取数据)
match,匹配指定key,默认None 表示所有的key
count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
如:
第一次:cursor1, data1 = r.hscan('xx', cursor=0, match=None, count=None)
第二次:cursor2, data1 = r.hscan('xx', cursor=cursor1, match=None, count=None)
...
直到返回值cursor的值为0时,表示数据已经通过分片获取完毕
print(r.hscan("foo_hash1"))
(0L, {'k3': '122', 'k2': 'v3', 'k1': 'v1', 'k6': '-6', 'k5': '0', 'k4': '4'})

12 hscan_iter(name, match=None, count=None)
利用yield封装hscan创建生成器,实现分批去redis中获取数据
参数:
match,匹配指定key,默认None 表示所有的key
count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
如:
for item in r.hscan_iter('xx'):
print item
print(r.hscan_iter("foo_hash1")) <generator object hscan_iter at 0x027B2C88> 生成器内存地址
for item in r.hscan_iter('foo_hash1'):
print item
('k3', '122')
('k2', 'v3')
('k1', 'v1')
('k6', '-6')
('k5', '0')
('k4', '4')

5 redis基本命令_list

import redis 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='192.168.19.130', port=6379)

host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r = redis.Redis(connection_pool=pool) 创建实例

1 增加(类似于list的append,只是这里是从左边新增加)--没有就新建
lpush(name,values)
在name对应的list中添加元素,每个新的元素都添加到列表的最左边
如:
r.lpush('oo', 11,22,33)
保存顺序为: 33,22,11
扩展:
rpush(name, values) 表示从右向左操作
r.lpush("foo_list1",11,22) 从列表的左边,先添加11,后添加22
print(r.lrange("foo_list1",0,20))
['22', '11', '22', '11', '22', '11', '22', '11', '22', '11', '22', '11', '22', '11', '22', '11', '22', '11']
切片取出值,范围是索引号0-20
print(r.llen("foo_list1")) 18 长度是18

2 增加(从右边增加)--没有就新建
r.rpush("foo_list1",2,3,4) 在列表的右边,依次添加2,3,4
print(r.lrange("foo_list1",0,-1))
['22', '11', '22', '11', '22', '11', '22', '11', '22', '11', '22',
'11', '22', '11', '22', '11', '22', '11', '2', '3', '4']
切片取出值,范围是索引号0-最后一个元素
print(r.llen("foo_list1")) 21 列表长度是21

3 往已经有的name的列表的左边添加元素,没有的话无法创建
lpushx(name,value)
在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边
更多:
rpushx(name, value) 表示从右向左操作
r.lpushx("foo_list2",1) 这里"foo_list2"不存在
print(r.lrange("foo_list2",0,-1)) []
print(r.llen("foo_list2")) 0

r.lpushx("foo_list1",1) 这里"foo_list1"之前已经存在,往列表最左边添加一个元素,一次只能添加一个
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
['1', '22', '11', '22', '11', '22', '11', '22', '11', '22',
'11', '22', '11', '22', '11', '22', '11', '22', '11', '2', '3', '4']
print(r.llen("foo_list1")) 22 列表长度是22

4 往已经有的name的列表的右边添加元素,没有的话无法创建
r.rpushx("foo_list1",1) 这里"foo_list1"之前已经存在,往列表最右边添加一个元素,一次只能添加一个
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
['1', '22', '11', '22', '11', '22', '11', '22', '11', '22',
'11', '22', '11', '22', '11', '22', '11', '22', '11', '2', '3', '4','1']
print(r.llen("foo_list1")) 23 列表长度是23

5 新增(固定索引号位置插入元素)
linsert(name, where, refvalue, value))
在name对应的列表的某一个值前或后插入一个新值
参数:
name,redis的name
where,BEFORE或AFTER
refvalue,标杆值,即:在它前后插入数据
value,要插入的数据
r.linsert("foo_list1","before","22","33") 往列表中左边第一个出现的元素"22"前插入元素"33"
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
['1', '33', '22', '11', '22', '11', '22', '11', '22',
'11', '22', '11', '22', '11', '22', '11', '22', '11', '22', '11', '2', '3', '4', '1']
print(r.llen("foo_list1")) 24 列表长度是24

6 修改(指定索引号进行修改)
r.lset(name, index, value)
对name对应的list中的某一个索引位置重新赋值
参数:
name,redis的name
index,list的索引位置
value,要设置的值
r.lset("foo_list1",4,44) 把索引号是4的元素修改成44
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
print(r.llen("foo_list1")) 24 列表长度是24

7 删除(指定值进行删除)
r.lrem(name, value, num)
在name对应的list中删除指定的值
参数:
name,redis的name
value,要删除的值
num, num=0,删除列表中所有的指定值;
num=2,从前到后,删除2个; num=1,从前到后,删除左边第1个
num=-2,从后向前,删除2个
r.lrem("foo_list1","2",1) 将列表中左边第一次出现的"2"删除
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
['1', '33', '22', '11', '44', '11', '22', '11', '22', '11', '22', '11',
'22', '11', '22', '11', '22', '11', '22', '11', '3', '4', '1']
print(r.llen("foo_list1")) 23 列表长度是23

r.lrem("foo_list1","11",0) 将列表中所有的"11"删除
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
['1', '33', '22', '44', '22', '22', '22', '22', '22', '22', '22', '3', '4', '1']
print(r.llen("foo_list1")) 14 列表长度是14

8 删除并返回
lpop(name)
在name对应的列表的左侧获取第一个元素并在列表中移除,返回值则是第一个元素
更多:
rpop(name) 表示从右向左操作
print(r.lpop("foo_list1")) 删除最左边的22,并且返回删除的值22
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
['44', '22', '22', '22', '22', '22', '22', '22', '3', '4', '1']
print(r.llen("foo_list1")) 11 列表长度是11

print(r.rpop("foo_list1")) 删除最右边的1,并且返回删除的值1
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素
['44', '22', '22', '22', '22', '22', '22', '22', '3', '4']
print(r.llen("foo_list1")) 10 列表长度是10

9 删除索引之外的值
ltrim(name, start, end)
在name对应的列表中移除没有在start-end索引之间的值
参数:
name,redis的name
start,索引的起始位置
end,索引结束位置
r.ltrim("foo_list1",0,8) 删除索引号是0-8之外的元素,值保留索引号是0-8的元素
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)
['44', '22', '22', '22', '22', '22', '22', '22', '3']

10 取值(根据索引号取值)
lindex(name, index)
在name对应的列表中根据索引获取列表元素
print(r.lindex("foo_list1",0)) 44 取出索引号是0的值
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)
['44', '22', '22', '22', '22', '22', '22', '22', '3', '4']

11 移动 元素从一个列表移动到另外一个列表
rpoplpush(src, dst)
从一个列表取出最右边的元素,同时将其添加至另一个列表的最左边
参数:
src,要取数据的列表的name
dst,要添加数据的列表的name
r.rpoplpush("foo_list1","foo_list2")
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)
['44', '22', '22', '22', '22', '22', '22']
print(r.lrange("foo_list2",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)
['22', '3']

12 移动 元素从一个列表移动到另外一个列表 可以设置超时
brpoplpush(src, dst, timeout=0)
从一个列表的右侧移除一个元素并将其添加到另一个列表的左侧
参数:
src,取出并要移除元素的列表对应的name
dst,要插入元素的列表对应的name
timeout,当src对应的列表中没有数据时,阻塞等待其有数据的超时时间(秒),0 表示永远阻塞
r.brpoplpush("foo_list2","foo_list1",timeout=2)
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)
['22', '3', '44', '22', '22', '22', '22', '22', '22']
print(r.lrange("foo_list2",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)
[]

13 一次移除多个列表
blpop(keys, timeout)
将多个列表排列,按照从左到右去pop对应列表的元素
参数:
keys,redis的name的集合
timeout,超时时间,当元素所有列表的元素获取完之后,阻塞等待列表内有数据的时间(秒), 0 表示永远阻塞
更多:
r.brpop(keys, timeout),从右向左获取数据
r.blpop("foo_list1",timeout=2)
print(r.lrange("foo_list1",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)
['22', '3', '44', '22', '22', '22', '22', '22', '22']
print(r.lrange("foo_list2",0,-1)) 切片取出值,范围是索引号0-最后一个元素(这里是包含最后一个元素的,是左闭右闭)

14 自定义增量迭代
由于redis类库中没有提供对列表元素的增量迭代,如果想要循环name对应的列表的所有元素,那么就需要:
1、获取name对应的所有列表
2、循环列表
但是,如果列表非常大,那么就有可能在第一步时就将程序的内容撑爆,所有有必要自定义一个增量迭代的功能:
def list_iter(name):
"""
自定义redis列表增量迭代
:param name: redis中的name,即:迭代name对应的列表
:return: yield 返回 列表元素
"""
list_count = r.llen(name)
for index in xrange(list_count):
yield r.lindex(name, index)

使用
for item in list_iter('foo_list1'): ['3', '44', '22', '22', '22'] 遍历这个列表
print item

6 redis基本命令_set

import redis 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='192.168.19.130', port=6379)

host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r = redis.Redis(connection_pool=pool) 创建实例

Set操作,Set集合就是不允许重复的列表

1 新增
sadd(name,values)
name对应的集合中添加元素
r.sadd("foo_set1",33,44,55,66) 往集合中添加一个元素 11
print(r.smembers("foo_set1")) set(['11']) 获取集合中所有的成员
print(r.scard("foo_set1")) 1 集合的长度是1

r.sadd("foo_set2",66,77) 往集合中添加2个元素 22,33
print(r.smembers("foo_set2")) set(['22',"33"]) 获取集合中所有的成员
print(r.scard("foo_set2")) 2 集合的长度是2

2 获取元素个数 类似于len
scard(name)
获取name对应的集合中元素个数

3 获取集合中所有的成员
smembers(name)
获取name对应的集合的所有成员

3-1 获取集合中所有的成员--元组形式
sscan(name, cursor=0, match=None, count=None)
print(r.sscan("foo_set1")) (0L, ['11', '22', '33', '55'])

3-2 获取集合中所有的成员--迭代器的方式
sscan_iter(name, match=None, count=None)
同字符串的操作,用于增量迭代分批获取元素,避免内存消耗太大
for i in r.sscan_iter("foo_set1"):
print(i)

4 差集
sdiff(keys, *args)
在第一个name对应的集合中且不在其他name对应的集合的元素集合
print(r.sdiff("foo_set1","foo_set2")) set(['11']) 在集合foo_set1但是不在集合foo_set2中
print(r.smembers("foo_set1")) set(['22',"11"]) 获取集合中所有的成员
print(r.smembers("foo_set2")) set(['22',"33"]) 获取集合中所有的成员

5 差集--差集存在一个新的集合中
sdiffstore(dest, keys, *args)
获取第一个name对应的集合中且不在其他name对应的集合,再将其新加入到dest对应的集合中
r.sdiffstore("foo_set3","foo_set1","foo_set2")
print(r.smembers("foo_set1")) set(['22',"11"]) 获取集合1中所有的成员
print(r.smembers("foo_set2")) set(['22',"33"]) 获取集合2中所有的成员
print(r.smembers("foo_set3")) set(['11']) 获取集合3中所有的成员

6 交集
sinter(keys, *args)
获取多一个name对应集合的交集
print(r.sinter("foo_set1","foo_set2")) set(['22']) 取2个集合的交集
print(r.smembers("foo_set1")) set(['22',"11"]) 获取集合1中所有的成员
print(r.smembers("foo_set2")) set(['22',"33"]) 获取集合2中所有的成员

7 交集--交集存在一个新的集合中
sinterstore(dest, keys, *args)
获取多一个name对应集合的并集,再将其加入到dest对应的集合中
r.sinterstore("foo_set3","foo_set1","foo_set2")
print(r.smembers("foo_set1")) set(['22',"11"]) 获取集合1中所有的成员
print(r.smembers("foo_set2")) set(['22',"33"]) 获取集合2中所有的成员
print(r.smembers("foo_set3")) set(['22']) 获取集合3中所有的成员

7-1 并集
sunion(keys, *args)
获取多个name对应的集合的并集
print(r.sunion("foo_set1","foo_set2")) set(['11', '22', '33', '77', '55', '66'])
print(r.smembers("foo_set1")) set(['11', '33', '22', '55']) 获取集合1中所有的成员
print(r.smembers("foo_set2")) set(['33', '77', '66', '22']) 获取集合2中所有的成员

7-2 并集--并集存在一个新的集合
sunionstore(dest,keys, *args)
获取多一个name对应的集合的并集,并将结果保存到dest对应的集合中
r.sunionstore("foo_bingji","foo_set1","foo_set2")
print(r.smembers("foo_set1")) set(['11', '33', '22', '55']) 获取集合1中所有的成员
print(r.smembers("foo_set2")) set(['33', '77', '66', '22']) 获取集合2中所有的成员
print(r.smembers("foo_bingji")) set(['11', '22', '33', '77', '55', '66'])

8 判断是否是集合的成员 类似in
sismember(name, value)
检查value是否是name对应的集合的成员
print(r.sismember("foo_set1",11)) True 11是集合的成员
print(r.sismember("foo_set1","11")) True
print(r.sismember("foo_set1",23)) False 23不是集合的成员

9 移动
smove(src, dst, value)
将某个成员从一个集合中移动到另外一个集合
r.smove("foo_set1","foo_set4",11)
print(r.smembers("foo_set1")) set(['22',"11"]) 获取集合1中所有的成员
print(r.smembers("foo_set4")) set(['22',"33"]) 获取集合4中所有的成员

10 删除--随机删除并且返回被删除值
spop(name)
从集合移除一个成员,并将其返回,说明一下,集合是无序的,所有是随机删除的
print(r.smembers("foo_set1")) set(['11', '22', '33', '44', '55', '66']) 获取集合1中所有的成员
print(r.spop("foo_set1")) 44 (这个删除的值是随机删除的,集合是无序的)
print(r.smembers("foo_set1")) set(['11', '33', '66', '22', '55']) 获取集合1中所有的成员

11 删除--指定值删除
srem(name, values)
在name对应的集合中删除某些值
print(r.smembers("foo_set1")) set(['11', '33', '66', '22', '55'])
r.srem("foo_set1",66) 从集合中删除指定值 66
print(r.smembers("foo_set1")) set(['11', '33', '22', '55'])

12 随机获取多个集合的元素
srandmember(name, numbers)
从name对应的集合中随机获取 numbers 个元素
print(r.srandmember("foo_set1",3)) ['33', '55', '66'] 随机获取3个元素
print(r.smembers("foo_set1")) set(['11', '33', '66', '22', '55'])

07 redis基本命令_有序set

import redis 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='192.168.19.130', port=6379)
host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r = redis.Redis(connection_pool=pool) 创建实例

Set操作,Set集合就是不允许重复的列表,本身是无序的
有序集合,在集合的基础上,为每元素排序;元素的排序需要根据另外一个值来进行比较,
所以,对于有序集合,每一个元素有两个值,即:值和分数,分数专门用来做排序。

1 新增
zadd(name, *args, **kwargs)
在name对应的有序集合中添加元素
如:
zadd('zz', 'n1', 1, 'n2', 2)

zadd('zz', n1=11, n2=22)
r.zadd("foo_zset1",n3=11,n4=22)
r.zadd("foo_zset2",n3=11,n4=23)
print(r.zcard("foo_zset1")) 2 长度是2 2个元素
print(r.zrange("foo_zset1",0,-1)) ['n1', 'n2'] 获取有序集合中所有元素
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n1', 11.0), ('n2', 22.0)] 获取有序集合中所有元素和分数

2 获取有序集合元素个数 类似于len
zcard(name)
获取name对应的有序集合元素的数量

3 获取有序集合的所有元素
r.zrange( name, start, end, desc=False, withscores=False, score_cast_func=float)
按照索引范围获取name对应的有序集合的元素
参数:
name,redis的name
start,有序集合索引起始位置(非分数)
end,有序集合索引结束位置(非分数)
desc,排序规则,默认按照分数从小到大排序
withscores,是否获取元素的分数,默认只获取元素的值
score_cast_func,对分数进行数据转换的函数
更多:
从大到小排序
zrevrange(name, start, end, withscores=False, score_cast_func=float)
按照分数范围获取name对应的有序集合的元素
zrangebyscore(name, min, max, start=None, num=None, withscores=False, score_cast_func=float)
从大到小排序
zrevrangebyscore(name, max, min, start=None, num=None, withscores=False, score_cast_func=float)

3-1 从大到小排序
zrevrange(name, start, end, withscores=False, score_cast_func=float)
print(r.zrevrange("foo_zset1",0,-1)) ['n2', 'n1'] 只获取元素,不显示分数
print(r.zrevrange("foo_zset1",0,-1,withscores=True)) [('n2', 22.0), ('n1', 11.0)]
获取有序集合中所有元素和分数,安装分数倒序

3-2 按照分数范围获取name对应的有序集合的元素
zrangebyscore(name, min, max, start=None, num=None, withscores=False, score_cast_func=float)
print(r.zrangebyscore("foo_zset1",15,25)) ['n2']
print(r.zrangebyscore("foo_zset1",12,22, withscores=True)) [('n2', 22.0)]
在分数是12-22之间(左闭右闭),取出符合条件的元素

3-3 从大到小排序
zrevrangebyscore(name, max, min, start=None, num=None, withscores=False, score_cast_func=float)
print(r.zrevrangebyscore("foo_zset1",22,11,withscores=True)) [('n2', 22.0), ('n1', 11.0)]
在分数是22-11之间(左闭右闭),取出符合条件的元素 按照分数倒序

3-4 获取所有元素--默认按照分数顺序排序
zscan(name, cursor=0, match=None, count=None, score_cast_func=float)
print(r.zscan("foo_zset1")) (0L, [('n3', 11.0), ('n4', 22.0), ('n2', 30.0)])

3-5 获取所有元素--迭代器
zscan_iter(name, match=None, count=None,score_cast_func=float)
for i in r.zscan_iter("foo_zset1"): 遍历迭代器
print(i)
('n3', 11.0)
('n4', 22.0)
('n2', 30.0)

4 zcount(name, min, max)
获取name对应的有序集合中分数 在 [min,max] 之间的个数
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n1', 11.0), ('n2', 22.0)]
print(r.zcount("foo_zset1",11,22)) 2

5 自增
zincrby(name, value, amount)
自增name对应的有序集合的 name 对应的分数
r.zincrby("foo_zset1","n2",amount=2) 每次将n2的分数自增2
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n1', 11.0), ('n2', 30.0)]

6 获取值的索引号
zrank(name, value)
获取某个值在 name对应的有序集合中的排行(从 0 开始)
更多:
zrevrank(name, value),从大到小排序
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n1', 11.0), ('n2', 30.0)]
print(r.zrank("foo_zset1","n1")) 0 n1的索引号是0 这里按照分数顺序(从小到大)
print(r.zrank("foo_zset1","n2")) 1 n2的索引号是1

print(r.zrevrank("foo_zset1","n1")) 1 n1的索引号是1 这里安照分数倒序(从大到小)

7 删除--指定值删除
zrem(name, values)
删除name对应的有序集合中值是values的成员
如:zrem('zz', ['s1', 's2'])
print(r.zrange("foo_zset1",0,-1,withscores=True))
r.zrem("foo_zset2","n3") 删除有序集合中的元素n1 删除单个
print(r.zrange("foo_zset1",0,-1,withscores=True))

8 删除--根据排行范围删除,按照索引号来删除
zremrangebyrank(name, min, max)
根据排行范围删除
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n3', 11.0), ('n4', 22.0), ('n2', 30.0)]
r.zremrangebyrank("foo_zset1",0,1) 删除有序集合中的索引号是0,1的元素
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n2', 30.0)]

9 删除--根据分数范围删除
zremrangebyscore(name, min, max)
根据分数范围删除
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n3', 11.0), ('n4', 22.0), ('n2', 30.0)]
r.zremrangebyscore("foo_zset1",11,22) 删除有序集合中的分数是11-22的元素
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n2', 30.0)]

10 获取值对应的分数
zscore(name, value)
获取name对应有序集合中 value 对应的分数
print(r.zrange("foo_zset1",0,-1,withscores=True)) [('n3', 11.0), ('n4', 22.0), ('n2', 30.0)]
print(r.zscore("foo_zset1","n3")) 11.0 获取元素n3对应的分数11.0

import redis 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='192.168.19.130', port=6379)
host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379

r = redis.Redis(connection_pool=pool) 创建实例

08 其他常用操作

1 删除
delete(*names)
根据删除redis中的任意数据类型(string、hash、list、set、有序set)

1-1删除string
r.set('foo', 'Bar')
print(r.strlen("foo")) 3 3ge 字节
print(r.getrange("foo",0,-1)) Bar
r.delete("foo") 删除字符串类型的foo
print(r.get("foo")) None
print(r.getrange("foo",0,-1))
print(r.strlen("foo")) 0 0个字节

1-2 删除hash
r.hset("foo_hash4","k1","v1")
print(r.hscan("foo_hash4")) (0L, {'k1': 'v1'})
r.delete("foo_hash4") 删除hash类型的键值对
print(r.hscan("foo_hash4")) (0L, {})

2 检查名字是否存在
exists(name)
检测redis的name是否存在
print(r.exists("foo_hash4")) True 存在就是True
print(r.exists("foo_hash5")) False 不存在就是False

2-1
r.lpush("foo_list5",11,22)
print(r.lrange("foo_list5",0,-1)) ['22', '11', '22', '11']
print(r.exists("foo_list5")) True 存在就是True
print(r.exists("foo_list6")) False 不存在就是False

3 模糊匹配
keys(pattern='*')
根据模型获取redis的name
更多:
KEYS * 匹配数据库中所有 key 。
KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
KEYS h*llo 匹配 hllo 和 heeeeello 等。
KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo
print(r.keys("foo*"))
['foo_hash1', 'foo_bingji', 'foo_list1', 'foo_list2', 'foo3', 'foo_set2', 'foo_hash4', 'foo_zset2',
'foo2', 'foo4', 'foo_set1', 'foo_zset1', 'foo_hash2', 'foo1', 'foo_list5', 'foo_set3']

4 设置超时时间
expire(name ,time)
为某个redis的某个name设置超时时间
r.lpush("foo_list5",11,22)
r.expire("foo_list5",time=10)
print(r.lrange("foo_list5",0,-1))

5 重命名
rename(src, dst)
对redis的name重命名为
r.rename("foo_list6","foo_list5")
print(r.lrange("foo_list5",0,-1)) ['22', '11']
print(r.lrange("foo_list6",0,-1)) []

6 随机获取name
randomkey()
随机获取一个redis的name(不删除)
print(r.keys("foo*"))
['foo_set1', 'foo3', 'foo_set2', 'foo_zset2', 'foo4', 'foo_zset1', 'foo_list5', 'foo2',
'foo_hash2', 'foo1', 'foo_set3', 'foo_hash1', 'foo_hash4', 'foo_list2', 'foo_bingji']
print(r.randomkey()) foo_hash2 随机获取一个name

7 获取类型
type(name)
获取name对应值的类型
print(r.type("foo_hash2")) hash
print(r.type("foo_set1")) set
print(r.type("foo3")) string

8 查看所有元素
scan(cursor=0, match=None, count=None)
print(r.hscan("foo_hash2")) (0L, {'k3': 'v3', 'k2': 'v2'})
print(r.sscan("foo_set3")) (0L, ['22'])
print(r.zscan("foo_zset2")) (0L, [('n4', 23.0)])
print(r.getrange("foo1",0,-1)) 121 --字符串
print(r.lrange("foo_list5",0,-1)) ['22', '11'] --列表

9 查看所有元素--迭代器
scan_iter(match=None, count=None)
for i in r.hscan_iter("foo_hash2"):--遍历
print(i)
('k3', 'v3')
('k2', 'v2')

for i in r.sscan_iter("foo_set3"):
print(i) 22

for i in r.zscan_iter("foo_zset2"):
print(i) ('n4', 23.0)

__author__ = 'Administrator'
-*- coding:utf-8 -*-

管道
redis-py默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池)一次连接操作,
如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,并且默认情况下一次pipline 是原子性操作。

import redis

pool = redis.ConnectionPool(host='192.168.19.130', port=6379)

r = redis.Redis(connection_pool=pool)

pipe = r.pipeline(transaction=False)
pipe = r.pipeline(transaction=True)

r.set('name', 'jack')
r.set('role', 'sb')

pipe.execute()

print(r.get("name")) jack
print(r.get("role")) sb

python之路十一的更多相关文章

  1. python之路(十一)-socke开发

    socket简介 socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. so ...

  2. Python之路【第十一篇】:CSS --暂无内容-待更新

    Python之路[第十一篇]:CSS --暂无内容-待更新

  3. 【我的python之路】

    目录 我的python之路[第一章]字符编码集,数据类型 我的python之路[第二章]循环-内置方法-数据类型 我的python之路[第三章]函数 我的python之路[第四章]装饰器.生成器.迭代 ...

  4. 孤荷凌寒自学python第八十一天学习爬取图片1

    孤荷凌寒自学python第八十一天学习爬取图片1 (完整学习过程屏幕记录视频地址在文末) 通过前面十天的学习,我已经基本了解了通过requests模块来与网站服务器进行交互的方法,也知道了Beauti ...

  5. 孤荷凌寒自学python第七十一天开始写Python的第一个爬虫

    孤荷凌寒自学python第七十一天开始写Python的第一个爬虫 (完整学习过程屏幕记录视频地址在文末) 在了解了requests模块和BeautifulSoup模块后,今天开始真正写一个自己的爬虫代 ...

  6. 孤荷凌寒自学python第六十一天在Fedora28版的linux系统上找搭建本地Mongodb数据服务

    孤荷凌寒自学python第六十一天在Fedora28版的linux系统上找搭建本地Mongodb数据服务 (完整学习过程屏幕记录视频地址在文末) 今天是学习mongoDB数据库的第七天.成功在本地搭建 ...

  7. 孤荷凌寒自学python第五十一天初次尝试使用python连接Firebase数据库

    孤荷凌寒自学python第五十一天初次尝试使用python连接Firebase数据库 (完整学习过程屏幕记录视频地址在文末) 今天继续研究Firebase数据库,利用google免费提供的这个数据库服 ...

  8. 孤荷凌寒自学python第三十一天python的datetime.timedelta模块

     孤荷凌寒自学python第三十一天python的datetime.timedelta模块 (完整学习过程屏幕记录视频地址在文末,手写笔记在文末) datetime.timedelta模块是一个表示 ...

  9. Python 之路

    Python之路[第一篇]:Python简介和入门 Python之路[第二篇]:Python基础(一) Python之路[第三篇]:Python基础(二) Python之路[第四篇]:模块 Pytho ...

随机推荐

  1. file-loader及url-loader的使用

    file-loader主要用来处理图片,其实也可以在js和html及其他文件上,但很少那么使用,比如: require("file-loader?name=js/[hash].script. ...

  2. time.c 的Java实现(从timestamp计算年月日时分秒等数值)

    time.c的Java实现 public class GMT { public static final int EPOCH_YEAR = 1970; public static final int[ ...

  3. 潭州学院-JavaVIP的Javascript的高级进阶-KeKe老师

    潭州学院-JavaVIP的Javascript的高级进阶-KeKe老师 讲的不错,可以学习 下面是教程的目录截图: 下载地址:http://www.fu83.cn/thread-283-1-1.htm ...

  4. bzoj4726【POI2017】Sabota?

    首先可以推出来如果i没有带头叛变,那么i的父亲也一定不会带头叛变,证明显然 所以最劣情况初始的叛徒肯定是叶子,并且带头叛变的人一定是从某个叶子往上走一条链 f[i]表示i不带头叛变的话最小的x 那么我 ...

  5. IBatis 批量插入数据之SqlBulkCopy

    public void AddLetters(IList<int> customerIds, string title, string content, LetterEnum.Letter ...

  6. Unicode, UTF, ASCII, ANSI format differences

    Going down your list: "Unicode" isn't an encoding, although unfortunately, a lot of docume ...

  7. JAVA教师:给JAVA初学者的忠告

    我带过不少JAVA,C++班的课,来学习的同学很多都是初学者,一部分是急着找工作的,一部分是很感兴趣的.他们都想在短短一两个星期内掌握Java,这是不切实际的.而且这样做很容易让自己心浮气燥,难以静下 ...

  8. Oracle数据库开发

    Oracle数据库开发之PL/SQL基础实战视频课程 1 PL/SQL 简介 2 入门实例(一) 3 入门实例(二) 4 PL/SQL 变量和常量 5 PL/SQL数据类型(一) 6 PL/SQL数据 ...

  9. 在eclipse中遇到cannot open output file xxx.exe: Permission denied 的解决办法

    该问题出现的原因主要原因是,编译后运行的程序未能正确关闭,解决方法:删除debug目录即可 同理在vc6.0遇到同样问题时,删除debug目录,或者重启vc6.0即可

  10. 第二轮冲刺-Runner站立会议05

    今天:将baseadapter的原理弄清楚了 明天:解决适配问题 困难:程序会停止运行