Redis-Python交互:HyperLogLog、管道、事务、发布和订阅

HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费12KB内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

基数

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

操作

  • pfadd(self, name, *values):添加指定元素到 HyperLogLog 中。
  • pfcount(self, *sources):返回给定 HyperLogLog 的基数估算值
  • pfmerge(self, dest, *sources):将多个 HyperLogLog 合并为一个 HyperLogLog
ip_0101 = []
ip_0102 = []
for i in range(1, 100):
    ip = f'{randint(0,255)}.{randint(0,255)}.{randint(0,255)}.{randint(0,255)}'
    ip_0101.append(ip)
for i in range(1, 40):
    ip = f'{randint(0,255)}.{randint(0,255)}.{randint(0,255)}.{randint(0,255)}'
    ip_0102.append(ip)
r.pfadd('ip:20180101', *ip_0101)
r.pfadd('ip:20180102', *ip_0102)
print(r.pfcount('ip:20180101'))
# 99
print(r.pfcount('ip:20180102'))
# 39
r.pfmerge('ip:201801', 'ip:20180101', 'ip:20180102')
print(r.pfcount('ip:201801'))
# 139

管道

一般情况下,执行一条命令后必须等待结果才能输入下一次命令,管道用于在一次请求中执行多个命令。

"""
pipeline管道用于在一次请求中执行多个命令
"""
from redis import Redis, ConnectionPool
from ..password import redis_passwd
r = Redis(host='localhost', port=6379, password=redis_passwd, db=0)
# ``transaction`` indicates whether all commands should be executed atomically
pipe = r.pipeline(transaction=True)
pipe.set('foo', 'bar')
pipe.set('name', 'jack')
pipe.lpush('l1', 'a')
# the EXECUTE call sends all buffered commands to the server, returning
# a list of responses, one for each command.
pipe.execute()

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis事务的执行并不是原子性的。可以用参数transaction指示是否所有的命令应该以原子方式执行。
所有缓存在管道的命令都会返回一个pipeline对象,所以可以使用链式的调用:

pipe.set('num', 2).rpush('l1', 'b').incr('num').execute()

事务

在使用Python连接时,可以用管道来进行事务。watch命令可以监听一个或多个键,当这些键在事务进行过程中被修改时,整个事务就会被取消,然后抛出一个WatchError

# 使用piple管道实现 事务
with r.pipeline() as pipe:
    while True:
        try:
            # 监听某个键
            pipe.watch('num')
            # 监听之后,管道会立即进入执行状态,直到我们告知要将命令缓存起来
            current_value = pipe.get('num')
            next_value = int(current_value) + 1
            # 管道进行缓存模式,缓存输入的命令,事务开始
            pipe.multi()
            pipe.set('num', next_value)
            pipe.lpop('l1')
            # 执行缓存在pipe的事务
            pipe.execute()

            # 没有WatchError,事务就被原子性地执行了,退出
            break
        except WatchError:
            # 有其他客户端对监听的键进行了设置,事务失败,所有操作都不会执行
            # 事务重试
            continue
        finally:
            pipe.reset()

由于Pipeline必须在WATCH期间绑定到单个连接,因此必须注意确保通过调用reset()方法将连接返回到连接池。 如果Pipeline用作上下文管理器(如上例所示),将自动调用reset()。 当然,可以通过显式调用reset()以手动方式执行此操作。
此外,redis-py提供了一个更简洁的方法将一个可调用对象作为事务来执行:

# 只有一个参数 Pipeline对象
def num_incr_l1_pop(pipe):
    current_value = pipe.get('num')
    next_value = int(current_value) + 1
    pipe.multi()
    pipe.set('num', next_value)
    pipe.lpop('l1')


# 使用 transaction 执行事务
r.transaction(num_incr_l1_pop, 'num')

将要执行的命令放在函数内,输入的参数为管道对象,之后将这个函数传入transaction中,同时可以设置要监听的键。

发布和订阅

redis-py包含一个PubSub对象,使用该对象能订阅频道并接听消息。创建PubSub对象:

>>> r = redis.Redis(...)
>>> p = r.pubsub()

订阅/取消订阅

创建PubSub对象后,可以使用频道的名字或模式(pattern)来进行订阅subscribe/psubscribe

>>> p.subscribe('my-first-channel')
>>>> p.get_message()
{'type': 'subscribe', 'pattern': None, 'channel': b'my-first-channel', 'data': 1}
>>> p.psubscribe('cctv*')
>>> p.get_message()
{'type': 'psubscribe', 'pattern': None, 'channel': b'cctv*', 'data': 2}
>>> 

get_message()通过读取消息可以确认订阅。

  • type: ‘subscribe’、 ‘unsubscribe’、 ‘psubscribe’、 ‘punsubscribe’、‘message’、 'pmessage’之中的一个;
  • channel:订阅/取消订阅的频道,或是发布消息的频道;
  • pattern:与已发布消息的频道匹配的模式。 除了pmessage类型之外,在所有情况下都是None
  • data:消息。订阅或取消订阅时,代表当前连接订阅的频道和模式的数量。使用[p]message 时,此值将是实际发布的消息。

取消订阅unsubscribe/punsubscribe

>>> p.unsubscribe('my-first-channel')
>>> p.punsubscribe('cctv*')
>>> p.get_message()
{'type': 'unsubscribe', 'pattern': None, 'channel': b'my-first-channel', 'data': 1}
>>> p.get_message()
{'type': 'punsubscribe', 'pattern': None, 'channel': b'cctv*', 'data': 0}
>>> 

若不指定取消订阅的频道的名称,就会取消对所有频道的订阅。

发布消息

发布消息时,使用客户端的publish()指定频道和要发送的讯息,会返回此频道的订阅者数量:

>>> r.publish('cctv0', 'first message cctv0')
1
>>> p.get_message()
{'type': 'pmessage', 'pattern': b'cctv*', 'channel': b'cctv0', 'data': b'first message cctv0'}

消息处理

redis-py还允许注册回调函数来处理已发布的消息。 消息处理程序采用单个参数,即消息message,这是一个字典,就像上面的例子一样。 要使用消息处理程序订阅通道或模式,请将通道或模式名称作为关键字参数kwarg传递,其值为回调函数。

>>> def my_handler(message):
...     print(f"my_handler: {message['data']}")
... 
>>> p.subscribe(**{'cctv1': my_handler})
>>> p.get_message()
{'type': 'subscribe', 'pattern': None, 'channel': b'cctv1', 'data': 1}
>>> r.publish('cctv1','Today is Friday')
1
>>> message=p.get_message()
my_handler: b'Today is Friday'
>>> print(message)
None

当使用消息处理程序在通道或模式上读取消息时,将创建消息字典并将其传递给消息处理程序。 在这种情况下,由于消息已经处理,因此从get_message()返回None值。
可以使用ignore_subscribe_messages参数指定隐藏订阅或退订时的消息。

>>> p = r.pubsub(ignore_subscribe_messages=True)
>>> p.subscribe('cctv10')
>>> p.get_message()
>>> r.publish('cctv10', 'this is cctv10')
1
>>> p.get_message()
{'type': 'message', 'pattern': None, 'channel': b'cctv10', 'data': b'this is cctv10'}
>>> p.unsubscribe('cctv10')
>>> p.get_message()
>>> 

阅读消息有三种不同的策略。
上面的示例一直使用pubsub.get_message()。在后台,get_message()使用系统的select模块快速轮询连接的套接字。如果有可供读取的数据,get_message()将读取它,格式化消息并将其返回或传递给消息处理程序。 如果没有要读取的数据,get_message()将立即返回None。这便于将消息集成到事件循环中:

# 在事件循环中使用消息
while True:
    message = p.get_message()
    if message:
        # ...do something
    time.sleep(0.001) # be nice to the system :

旧版本的redis-py只能使用pubsub.listen()读取消息。 listen()是一个阻塞的生成器,直到消息可用。 如果您的应用程序不需要执行任何其他操作,只需接收并处理从redis收到的消息,listen()是一种简单的方法来启动运行。

>>> for message in p.listen():
...     # do something with the message

第三种方法是在单独的线程中运行事件循环。 pubsub.run_in_thread()创建一个新线程并启动事件循环。线程对象返回给run_in_thread()的调用者。调用者可以使用thread.stop()方法来关闭事件循环和线程。在后台,这只是一个围绕get_message()的包装器,它在一个单独的线程中运行,实际上是创建了一个微小的非阻塞事件循环。 run_in_thread()接受一个可选的sleep_time参数。 如果指定,那么事件循环会在每次循环时调用time.sleep(sleep_time)

>>> p.subscribe(**{'my-channel': my_handler})
>>> thread = p.run_in_thread(sleep_time=0.001)
# the event loop is now running in the background processing messages
# when it's time to shut it down...
>>> thread.stop()

注意:由于在单独的线程中运行,因此无法处理未使用已注册的消息处理函数进行自动处理的消息。 因此,如果订阅了没有使用消息处理程序的模式或通道,redis-py会阻止调用run_in_thread()。

关闭链接

PubSub对象记住他们订阅的频道和模式。如果发生网络错误或超时等断开连接,PubSub对象将在重新连接时重新订阅所有先前的通道和模式。客户端断开连接时发布的消息无法传递。完成PubSub对象后,调用其.close()方法关闭连接。

>>> p = r.pubsub()
>>> ...
>>> p.close()

pubsub子命令

redis-py支持PUBSUB的自命令:CHANNELSNUMSUBNUMPAT

>>> r.pubsub_channels()
['foo', 'bar']
>>> r.pubsub_numsub('foo', 'bar')
[('foo', 9001), ('bar', 42)]
>>> r.pubsub_numsub('baz')
[('baz', 0)]
>>> r.pubsub_numpat()
1204
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值