Redis-python

Redis

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) , 有序集合(sorted sets)和流Stream(Redis5.0)等类型。

安装和设置

使用安装命令:
sudo add-apt-repository ppa:chris-lea/redis-server:可以更新到最新的5.0版本
sudo apt-get update
sudo apt-get install redis-server
使用redis-cli 进入命令行模式:

ulysses@ulysses:~$ redis-cli
127.0.0.1:6379> 

Redis 成功安装了,但是现在 Red is 还是无法远程连接 的,依然需要修改配置文件,
配置文件的路径为/etc/redis/redis.conf.或者使用redis 127.0.0.1:6379> CONFIG GET CONFIG_SETTING_NAME的形式获取或者设置配置项。
首先注释bind 127.0.0.1 ::1 ,再取消requirepass foobared的注释,自行设置密码,最后重启redis服务。使用密码登录redis:

ulysses@ulysses:~$ redis-cli -a 'password'
127.0.0.1:6379> PING
PONG

Redis Desktop Manager是一个开源的redis数据库可视化工具,下载地址: https://github.com/uglide/RedisDesktopManager 。可以使用snap安装:sudo snap install redis-desktop-manager

redis-py的安装

使用 redis-py 库来与redis进行交互:pip install redis
pythob操作redis:

# -*- coding: utf-8 -*-
# @Date    : 2018-11-13 17:28:59
# @Author  : Your Name ([email protected])
# @Link    : http://example.org
from redis import StrictRedis

redis = StrictRedis(host='localhost', port=6379, db=0, password='password')
redis.set('name', 'Bob')
print(redis.get('name'))

运行结果:

b'Bob'
[Finished in 0.1s]

在Redis Desktop Manager中查看结果:
在这里插入图片描述

Python和Redis交互

在redis-py 3.0之后就不在支持使用传统的‘Redis’客户端类了。StrictRedis现在只是Redis的一个别名,现在这个连接更加python化。例如,使用redis的字符串操作setbitgetbit来统计用户活跃天数:

'''
用Redis统计用户上线次数
理解:
A用户 100010001000001  //活跃了4天
B用户 111111111111111  //每日必到
'''
import redis
from ..password import redis_passwd

# 连接Redis,选择 db0
r = redis.Redis(host='localhost', port=6379, password=redis_passwd, db=0)

# A用户,一年中,每3天上线一次
for i in range(0, 365, 3):
    r.setbit('usera', i, 1)

# B用户 每10天上线一次

for i in range(0, 365, 10):
    r.setbit('userb', i, 1)

# 用户列表
# "Returns a list of keys matching ``pattern``"
userList = r.keys('user*')
print(userList)

Au = []
Nau = []

# 判断是否为活跃用户,(用户,登录天数)
for u in userList:
    logincount = r.bitcount(u)
    if logincount > 100:
        Au.append((u, logincount))
    else:
        Nau.append((u, logincount))

for u in Au:
    print(f'用户{u[0]}: 活跃用户, 共登录{u[1]}天')

for u in Nau:
    print(f'用户{u[0]}: 非活跃用户, 共登录{u[1]}天')

编码

PubSub对象遵循与其创建的客户端实例相同的编码方式。在发送到Redis之前,将使用客户端上指定的字符集对任何使用unicode编码的通道或模式进行编码。如果客户端的decode_responses标志设置为False(默认值),则消息字典中的channelpatterndata值将是字节字符串(Python2的str,Python3的bytes)。如果客户端的decode_responses为True,则它们值将使用客户端的字符集自动解码为unicode字符串。
默认,bytes类型:

>>> import redis
>>> r = redis.Redis(host='localhost', port=6379, password='***')
>>> r.set('str', 'time')
True
>>> ret = r.get('name')
>>> print(ret, type(ret))
b'Redis' <class 'bytes'>
>>> 

修改为str

>>> import redis
>>> r = redis.Redis(host='localhost', port=6379, password='***', decode_responses=True)
>>> r.set('str', 'time')
True
>>> ret = r.get('name')
>>> print(ret, type(ret))
Redis <class 'str'>

默认redis入库编码是utf-8,如果要修改的话,需要指明 charset和 decode_responsers 为True。
使用GBK编码:

>>> r2 = redis.Redis(host='localhost', port=6379, password='***', charset='GBK' ,decode_responses=True)
>>> r2.set('greet', '你好')
True
>>> r2.get('greet')
'你好'

连接池

redis使用connection pool来管理对一个redis server 的所有连接,避免每次建立、释放连接的开销。默认,每个Redis实例都会维护一个自己的连接池。可以直接建立一个连接池,然后作为参数创建Redis实例,这样就可以实现多个Redis实例共享一个连接池。

"""
连接池
"""
import redis
from ..password import redis_passwd
pool = redis.ConnectionPool(host='localhost', port=6379, db=0, password=redis_passwd)
r = redis.Redis(connection_pool=pool)
r.set('name', 'Redis')
print(r.get('name'))

# 输出结果
b'Redis'

连接方式

ConnectionPools管理一组Connection实例。redis-py有两种类型的连接。默认值连接方式是基于TCP套接字的常规连接。 UnixDomainSocketConnection允许在与服务器相同的设备上运行的客户端通过unix域套接字进行连接。 要使用UnixDomainSocketConnection连接,只需将unix_socket_path参数传递给unix域套接字文件,该参数是一个字符串。 此外,请确保在redis.conf文件中定义了unixsocket参数,默认注释掉了。

# Unix socket.
#
# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
# unixsocket /var/run/redis/redis-server.sock
# unixsocketperm 700
>>> r = redis.Redis(unix_socket_path='/tmp/redis.sock')

操作

key操作

  • delete(self, *names):删除键
  • dump(self, name):序列化给定 key ,并返回被序列化的值
  • exists(self, *names):检查给定 key 是否存在。
  • expire(self, name, time):为给定 key 设置过期时间,以秒计,time为整数Python timedelta 对象。
  • expireat(self, name, when):EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)或python datetime对象
  • pexpire(self, name, time):类似expire, 时间以毫秒计
  • pexpireat(self, name, when):类似expireat, 时间以毫秒计
  • keys(self, pattern=’*’):查找所有符合给定模式( pattern)的 key
  • move(self, name, db):将当前数据库的 key 移动到给定的数据库 db 当中
  • persist(self, name):移除 key 的过期时间,key 将持久保持。
  • pttl(self, name):以毫秒为单位返回 key 的剩余的过期时间
  • ttl(self, name):以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)
  • randomkey(self):从当前数据库中随机返回一个 key
  • rename(self, src, dst):修改 key 的名称
  • renamenx(self, src, dst):当dst不存在时,可以使用它作为src的名字
  • type(self, name):返回 key 所储存的值的类型

服务器操作

  • bgrewriteaof(self):异步执行一个 AOF(AppendOnly File) 文件重写操作
  • bgsave(self):在后台异步保存当前数据库的数据到磁盘
  • client_kill(self, address):关闭客户端连接
  • client_list(self, _type=None):获取连接到服务器的客户端连接列表
  • client_getname(self):获取连接的名称
  • client_id(self):获取当前连接的id
  • client_setname(self, name):设置当前连接的名称
  • client_pause(self, timeout):在指定时间内终止运行来自客户端的命令(毫秒)
  • client_unblock(self, client_id, error=False):解除指定连接id的客户端阻塞
  • config_get, config_set,config_rewrite:读写redis.conf配置文件
  • config_resetstat(self):重置 INFO 命令中的某些统计数据
  • dbsize(self):返回当前数据库的 key 的数量
  • debug_object(self, key):获取 key 的调试信息
  • echo(self, value):打印字符串
  • flushall(self, asynchronous=False):清空所有数据库
  • flushdb(self, asynchronous=False):清空当前数据库
  • info(self, section=None):获取 Redis 服务器的各种信息和统计数值
  • lastsave(self):返回最近一次 Redis 成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示
  • save(self):同步保存数据到硬盘
  • ping(self):查看服务是否运行
  • migrate(self, host, port, keys, destination_db, timeout,
    copy=False, replace=False, auth=None): 数据库迁移
  • shutdown(self, save=False, nosave=False):异步保存数据到硬盘,并关闭服务器
  • slaveof(self, host=None, port=None):将当前服务器转变为指定服务器的从属服务器(slave server)
  • slowlog_get(self, num=None):管理 redis 的慢日志
  • wait(self, num_replicas, timeout):Redis同步复制

String操作

redis中的String在在内存中按照name:value来存储的。

  • set(self, name, value, ex=None, px=None, nx=False, xx=False):
    get(self, name):获取key的值
    getset(self, name, value):将给定 key 的值设为 value ,并返回 key 的旧值(old value)
# 参数:
# ex,过期时间(秒)
# px,过期时间(毫秒)
# nx,如果设置为True,则只有name不存在时,当前set操作才执行
# xx,如果设置为True,则只有name存在时,岗前set操作才执行
r.set('name', 'redis', ex=60, nx=True)
print(r.get('name'))
# 结果:
# b'redis'
ret = r.getset('name', 'new value')
print(ret)
# b'redis'
print(r.get('name'))
# b'new value'
  • setnx(self, name, value)设置值,只有name不存在时,执行设置操作(添加):
r.setnx('num', 123)
print(r.get('num'))
# 结果:
# b'123'
  • setex(self, name, time, value)设置name的value和过期时间(秒或timedelta对象):
    psetex(name,time_ms,value) 过期时间为毫秒或timedelta对象
# time,过期时间(数字秒 或 timedelta对象)
r.setex('ex', 20, 'guoqi')
print(r.get('ex'))
r.psetex('pex', 20000, 'weimiao')
print(r.get('ex'))
# 结果:
# b'guoqi'
# b'guoqi'
# 20s后 ex、pex消失
  • mset(self, mapping):批量设置值 ,redis-py3.0后参数改为mapping对象,不在使用(*args, **kwargs)了
    msetnx(self, mapping):如果所有的key都未设置就会进行设置
tom = {'name': 'Tom', 'age': 20, 'id': 1}
r.mset(tom)
print(f"name:{r.get('name')}\n'age':{r.get('age')}\n'id':{r.get('id')}")
# 结果
# name:b'Tom'
# 'age':b'20'
# 'id':b'1'

  • getrange(self, key, start, end):获取字符串的子序列,包括起始、结束
    setrange(self, name, offset, value):用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始
r.set('num', '0123456')
ret = r.getrange('num', 1, 4)
print(ret)
# b'1234'
r.setrange('num', 4, 'adcde')
print(r.get('num'))
# b'0123adcde'
  • setbit(self, name, offset, value):对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)
    getbit(self, name, offset):对 key 所储存的字符串值,获取指定偏移量上的位(bit)
r.set('bit', '012345')
# 012345 ascii = 30 31 32 33 34 35
# = 00110000 00110001 00110010 ...
# 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)
r.setbit('bit', 1, 1)  # 00110000 -> 01110000=70=p
print(r.get('bit'))
# b'p12345'
print(r.getbit('bit', 2))
# 1
  • bitcount(key,start=None,end=None):获取name对应的值的二进制表示中 1 的个数,区间[start, end]
r.set('bit', '1')  # 1 = 31 = 00110001
print(r.bitcount('bit', 0, -1))
# 3
  • bitop(operation,dest,*keys):获取多个值,并将值做位运算,将最后的结果保存至新的name对应的值:
# bit 操作 与 或 亦或 非
# dest, 新的Redis的name
# *keys,要查找的Redis的name
r.set('a', 0x11)  # 17 = 31 37 = 00110001 00110111
r.set('b', 0x22)  # 34 = 33 34 = 00110011 00110100
r.bitop('AND', 'yu', 'a', 'b') # 00110001 00110100 = 31 34 = 14
r.bitop('OR', 'huo', 'a', 'b') # 00110011 00110111 = 33 37 = 37
r.bitop('XOR', 'yihuo', 'a', 'b') # 00000010 00000011 = 02 03
r.bitop('NOT', 'fei', 'a') # 11001110 11001000 = ce c8
print(r.get('yu'))
# b'14'
print(r.get('huo'))
# b'37'
print(r.get('yihuo'))
# b'\x02\x03'
print(r.get('fei'))
# b'\xce\xc8'
  • strlen(self, name): 返回 key 所储存的字符串值的长度
r.set('len', '012345678')
ret = r.strlen('len')
print(ret)
# 9
  • incr(self, name, amount=1):将 key 中储存的数字值增一。
    incrby(self, name, amount=1):将 key 所储存的值加上给定的增量值,默认1
    incrbyfloat(self, name, amount=1.0):将 key 所储存的值加上给定的浮点增量值, 默认1.0
r.set('num', 1)
r.incr('num')
print(r.get('num'))
# b'2'
r.incrby('num', 3)
print(r.get('num'))
# b'5'
r.incrbyfloat('num', 2.1)
print(r.get('num'))
# b'7.1'
  • decr(self, name, amount=1):将 key 中储存的数字值减一
    decrby(self, name, amount=1):key 所储存的值减去给定的减量值
    key和amount都要为整数
# 减
r.set('num', 10)
r.decr('num')
print(r.get('num'))
# b'9'
r.decrby('num', 4)
print(r.get('num'))
# b'5'
  • append(self, key, value): 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。
# 追加
r.set('code', 'abcdefg')
r.append('code', 'hijk')
print(r.get('code'))
b'abcdefghijk'

Hash操作

Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。

  • hset(self, name, key, value):将哈希表 name 中的字段 key 的值设为 value ;
    hsetnx(self, name, key, value):只有在字段key不存在时,设置哈希表字段的值;
    hmset(self, name, mapping):同时将多个key-value对设置到表name,格式为映射
  • hget(self, name, key):获取存储在哈希表中指定字段的值
    hgetall(self, name):获取在哈希表中指定 name的所有字段和值
    hmget(self, name, keys, *args):获取所有给定字段的值
r.hset('t1', 'name', 'jack')
print(r.hget('t1', 'name'))
# b'jack'
r.hsetnx('t1', 'name', 'ulysses')
print(r.hget('t1', 'name'))
# b'jack'
d1 = {'name': 'Jack', 'id': 1, 'location': 'US'}
r.hmset('t1', d1)
print(r.hgetall('t1'))
{b'name': b'Jack', b'id': b'1', b'location': b'US'}
keys = ['name', 'location']
print(r.hmget('t1', keys, 'id'))
[b'Jack', b'US', b'1']
  • hvals(self, name):获取哈希表中所有值
    hkeys(self, name):获取所有哈希表中的字段
    hlen(self, name):获取哈希表中字段的数量
d2 = {'date': str(datetime.now().date()), 'name': 'Akali', 'age': '16', 'id': 2}
r.hmset('t2', d2)
print(r.hlen('t2'))
# 4
print(r.hkeys('t2'))
# [b'date', b'name', b'age', b'id']
print(r.hvals('t2'))
# [b'2018-11-25', b'Akali', b'16', b'2']
  • hexists(self, name, key):查看哈希表 key 中,指定的字段是否存在
    hdel(self, name, *keys):删除一个或多个哈希表字段
print(r.hgetall('t1'))
# {b'name': b'Jack', b'id': b'1', b'location': b'US'}
if r.hexists('t1', 'name'):
    r.hdel('t1', 'name')
print(r.hgetall('t1'))
# {b'id': b'1', b'location': b'US'}
  • hincrby(self, name, key, amount=1):为哈希表 key 中的指定字段的整数值加上增量
    hincrbyfloat(self, name, key, amount=1.0):为哈希表 key 中的指定字段的浮点数值加上增量
print(r.hget('t2', 'id'))
# b'2'
if r.hexists('t2', 'id'):
    r.hincrby('t2', 'id', 25)
    print(r.hget('t2', 'id'))
    # b'27'
    r.hincrbyfloat('t2', 'id', 3.14)
    print(r.hget('t2', 'id'))
    # b'30.14'

列表操作

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

  • lpop(self, name):移出并获取列表的第一个元素
    lpush(self, name, *values):将一个或多个值插入到列表头部
    lpushx(self, name, value):将一个值插入到已存在的列表头部
    lrange(self, name, start, end):获取列表指定范围内的元素
    rpop, rpush和rpushx操作列表最后一个元素
l1 = [0, 1, 2, 3, 4, 5, 6, 7]
r.lpush('l1', *l1)
print(r.lrange('l1', 0, -1))
# [b'7', b'6', b'5', b'4', b'3', b'2', b'1', b'0']
r.lpush('l1', 'a')
r.lpushx('l1', 'b')
ret = r.lpop('l1')
print(ret)
# b'b'
print(r.lrange('l1', 2, 5))
# [b'6', b'5', b'4', b'3']
print(r.lrange('l1', 10, 15))
# []
print(r.lrange('l1', 4, 1))
# []
  • lindex(self, name, index):通过索引获取列表中的元素
  • linsert(self, name, where, refvalue, value):在列表的元素前或者后插入元素
  • llen(self, name):获取列表长度
if r.exists('l2'):
    r.delete('l2')
l2 = ['a', 'b', 'c', 'd', 'e']
r.rpush('l2', *l2)
print(r.lrange('l2', 0, -1))
# [b'a', b'b', b'c', b'd', b'e']
r.linsert('l2', 'before', 'b', '0')
r.linsert('l2', 'after', 'd', '1')
print(r.lrange('l2', 0, -1))
# [b'a', b'0', b'b', b'c', b'd', b'1', b'e']
print(r.llen('l2'))
# 7
print(r.lindex('l2', 1))
# b'0'
print(r.lindex('l2', 4))
# b'd'

  • lset(self, name, index, value):通过索引设置列表元素的值
    lrem(self, name, count, value):移除列表元素
    ltrim(self, name, start, end):对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除
if r.exists('l3'):
    r.delete('l3')
l3 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
r.rpush('l3', *l3)
r.lset('l3', 1, 1)
r.lset('l3', 3, 1)
print(r.lrange('l3', 0, -1))
# [b'a', b'1', b'c', b'1', b'e', b'f', b'g', b'h']
# count > 0: Remove elements equal to value moving from head to tail.
# count < 0: Remove elements equal to value moving from tail to head.
# count = 0: Remove all elements equal to value.
r.lrem('l3', 2, 1)
print(r.lrange('l3', 0, -1))
# [b'a', b'c', b'e', b'f', b'g', b'h']
r.ltrim('l3', 1, 4)
print(r.lrange('l3', 0, -1))
# [b'c', b'e', b'f', b'g']
  • rpoplpush(self, src, dst):移除列表的最后一个元素,并将该元素添加到另一个列表的头部并返回:
if r.exists('l4'):
    r.delete('l4')
if r.exists('l5'):
    r.delete('l5')
l4 = ['US', 'CN', 'JP', 'FR', 'UK']
l5 = ['baidu', 'google', 'facebook', 'twitter']
r.rpush('l4', *l4)
r.rpush('l5', *l5)
r.rpoplpush('l4', 'l5')
print(r.lrange('l4', 0, -1))
# [b'US', b'CN', b'JP', b'FR']
print(r.lrange('l5', 0, -1))
# [b'UK', b'baidu', b'google', b'facebook', b'twitter']
  • blpop(self, keys, timeout=0):移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
    brpop(self, keys, timeout=0):移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
    brpoplpush(self, src, dst, timeout=0):从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止

先运行①

ret1 = r.blpop('l6', timeout=10)
ret2 = r.brpop('l7', timeout=10)
print(ret1)
print(ret2)

再运行②

if r.exists('l6'):
    r.delete('l6')
if r.exists('l7'):
    r.delete('l7')
l6 = [1, 2, 3, 4, 5, 6]
l7 = ['a', 'b', 'c', 'd']
# ret1 = r.blpop('l6', timeout=10)
# ret2 = r.brpop('l7', timeout=10)
r.rpush('l6', *l6)
r.rpush('l7', *l7)
print(r.lrange('l6', 0, -1))
print(r.lrange('l7', 0, -1))

①的结果:

(b'l6', b'1')
(b'l7', b'd')

②的结果:

[b'2', b'3', b'4', b'5', b'6']
[b'a', b'b', b'c']

集合

Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

  • sadd(self, name, *values):向集合添加一个或多个成员
    scard(self, name):获取集合的成员数
    sismember(self, name, value):判断 value 元素是否是集合 name 的成员
    smembers(self, name):返回集合中的所有成员
    srem(self, name, *values):移除集合中一个或多个成员
if r.exists('s1'):
    r.delete('s1')
r.sadd('s1', *s1)
print(r.scard('s1'))
# 10
if r.sismember('s1', 'US'):
    r.srem('s1', 'US')
print(r.smembers('s1'))
# {b'3', b'Tom', b'2', b'b', b'a', b'Jack', b'1', b'CN', b'z'}
  • spop(self, name, count=None):移除并返回集合中的一个随机元素
    srandmember(self, name, number=None):返回集合中一个或多个随机数,不会移除元素
s2 = ['baidu', 'google', 'sina', 'sohu', 'twitter']
if r.exists('s2'):
    r.delete('s2')
r.sadd('s2', *s2)
ret = r.spop('s2')
print(ret)
# b'sohu'
print(r.smembers('s2'))
# {b'google', b'sina', b'twitter', b'baidu'}
ret = r.srandmember('s2', 3)
print(ret)
# [b'twitter', b'baidu', b'google']
print(r.smembers('s2'))
# {b'google', b'sina', b'twitter', b'baidu'}
  • smove(self, src, dst, value):将value 元素从src移动到dst
    sdiff(self, keys, *args):返回给定所有集合的差集
    sdiffstore(self, dest, keys, *args):返回给定所有集合的差集并存储在 dest 中
    sinter(self, keys, *args)::返会给定集合的交集
    sinterstore(self, dest, keys, *args):返回给定所有集合的交集并存储在dest
    sunion(self, keys, *args):返回所有给定集合的并集
    sunionstore(self, dest, keys, *args):所有给定集合的并集存储在 dest集合中
s3 = ['baidu', 'google', 'sina', 'sohu', 'twitter', 1, 2, 3, 'a', 'b']
s4 = ['a', 'b', 'c', 1, 2, 'google']
if r.exists('s3'):
    r.delete('s3')
if r.exists('s4'):
    r.delete('s4')
r.sadd('s3', *s3)
r.sadd('s4', *s4)

diff = r.sdiff('s3', 's4')
r.sdiffstore('s_diff', 's3', 's4')
print(diff)
print(r.smembers('s_diff'))
# {b'3', b'sina', b'sohu', b'baidu', b'twitter'}

inter = r.sinter('s3', 's4')
r.sinterstore('s_inter', 's3', 's4')
print(inter)
print(r.smembers('s_inter'))
# {b'google', b'b', b'2', b'a', b'1'}

union = r.sunion('s3', 's4')
r.sunionstore('s_union', 's3', 's4')
print(union)
print(r.smembers('s_union'))
# {b'3', b'google', b'sina', b'sohu', b'baidu', b'c', b'b', b'twitter', b'2', b'a', b'1'}

r.smove('s3', 's4', 'sina')
print(r.smembers('s3'))
# {b'3', b'google', b'sohu', b'baidu', b'b', b'twitter', b'2', b'a', b'1'}
print(r.smembers('s4'))
# {b'google', b'sina', b'c', b'b', b'2', b'a', b'1'}

有序集合sorted set

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。

  • zadd(self, name, mapping, nx=False, xx=False, ch=False, incr=False):向有序集合添加一个或多个成员,或者更新已存在成员的分数
    1. nx: 仅创建新元素而不更新已存在元素的分数。
    2. xx: 只更新已存在元素的分数,不添加新元素
    3. ch:将返回值修改为更改的元素数。更改的元素包括添加的新元素和分数更改的元素。
    4. incr: 将ZADD变为类似ZINCRBY
  • zcard(self, name):获取有序集合的成员数
  • zcount(self, name, min, max):计算在有序集合中指定区间分数的成员数
  • zscore(self, name, value):返回有序集中,成员的分数值
  • zrange(self, name, start, end, desc=False, withscores=False,
    score_cast_func=float):通过索引区间返回有序集合成指定区间内的成员:
    1. desc:反序
    2. withscores:返回(value, score)对
    3. score_cast_func:对返回的score进行处理的函数
  • zrevrange(self, name, start, end, withscores=False,
    score_cast_func=float):返回有序集中指定区间内的成员,通过索引,分数从高到底
if r.exists('zs1'):
    r.delete('zs1')
zs = {'us': 1, 'cn': 2, 'jp': 2.5, 'kr': 4, 'eu': 3.5}
r.zadd('zs1', zs)
print(r.zcard('zs1'))
# 5
print(r.zcount('zs1', 2, 4))
# 4
print(r.zscore('zs1', 'jp'))
# 2.5
print(r.zrange('zs1', 0, -1, withscores=True))
# [(b'us', 1.0), (b'cn', 2.0), (b'jp', 2.5), (b'eu', 3.5), (b'kr', 4.0)]
print(r.zrevrange('zs1', 0, -1))
# [b'kr', b'eu', b'jp', b'cn', b'us']
  • zincrby(self, name, amount, value):有序集合中对指定成员value的分数加上增量 amout
  • zlexcount(self, name, min, max):在有序集合中计算指定字典区间内成员数量
  • zrangebylex(self, name, min, max, start=None, num=None):通过字典区间返回有序集合的成员,如果指定start和num就会返回一个切片
  • zrevrangebylex(self, name, max, min, start=None, num=None):返回有序集中指定区间内的成员,通过索引,分数从高到底
  • zrangebyscore(self, name, min, max, start=None, num=None,withscores=False, score_cast_func=float):通过分数返回有序集合指定区间内的成员
  • zrevrangebyscore(self, name, max, min, start=None, num=None,withscores=False, score_cast_func=float):返回有序集中指定分数区间内的成员,分数从高到低排序
if r.exists('zs2'):
    r.delete('zs2')
zs2 = {'a': 1, 'b': 2, 'c': 2.5, 'd': 2.2, 'e': 3.5, 'f': 0.5}
r.zadd('zs2', zs2)
r.zincrby('zs2', 2, 'c')
print(r.zscore('zs2', 'c'))
# 4.5
print(r.zlexcount(name="zs2", min="-", max="+"))
# 6
print(r.zrangebylex(name='zs2', min="-", max="+"))
# [b'f', b'a', b'b', b'd', b'e', b'c']
print(r.zrevrangebylex('zs2', "[f", "[a"))
# [b'c', b'e', b'd', b'b', b'a', b'f']
print(r.zrangebyscore('zs2', 0, 4))
# [b'f', b'a', b'b', b'd', b'e']
print(r.zrevrangebyscore('zs2', 5, 1, withscores=True))
# [(b'c', 4.5), (b'e', 3.5), (b'd', 2.2), (b'b', 2.0), (b'a', 1.0)]
  • zrank(self, name, value):返回有序集合中指定成员的索引
  • zrevrank(self, name, value):返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
  • zrem(self, name, *values):移除有序集合中的一个或多个成员
  • zremrangebylex(self, name, min, max):移除有序集合中给定的字典区间的所有成员
  • zremrangebyrank(self, name, min, max):移除有序集合中给定的排名区间的所有成员
  • zremrangebyscore(self, name, min, max):移除有序集合中给定的分数区间的所有成员
if r.exists('zs2'):
    r.delete('zs2')
zs3 = {'a': 1, 'b': 2, 'c': 2.5, 'd': 3.2, 'e': 3.5, 'f': 3.55, 'g': 4, 'h': 4.1,
       'i': 4.3, 'j': 4.5, 'k': 5, 'l': 5.3, 'm': 5.6, 'n': 5.8, 'o': 6}
r.zadd('zs3', zs3)
print(r.zrank('zs3', 'f'))
# 5
print(r.zrevrank('zs3', 'f'))
# 9
r.zrem('zs3', 'a')
print(r.zrange('zs3', 0, -1, withscores=True))
# [(b'b', 2.0), (b'c', 2.5), (b'd', 3.2), (b'e', 3.5), (b'f', 3.55), (b'g', 4.0), (b'h', 4.1), (b'i', 4.3), (b'j', 4.5), (b'k', 5.0), (b'l', 5.3), (b'm', 5.6), (b'n', 5.8), (b'o', 6.0)]
r.zremrangebylex('zs3', '[b', '[d')
print(r.zrange('zs3', 0, -1, withscores=True))
# [(b'e', 3.5), (b'f', 3.55), (b'g', 4.0), (b'h', 4.1), (b'i', 4.3), (b'j', 4.5), (b'k', 5.0), (b'l', 5.3), (b'm', 5.6), (b'n', 5.8), (b'o', 6.0)]
r.zremrangebyrank('zs3', '2', '4')
print(r.zrange('zs3', 0, -1, withscores=True))
# [(b'e', 3.5), (b'f', 3.55), (b'j', 4.5), (b'k', 5.0), (b'l', 5.3), (b'm', 5.6), (b'n', 5.8), (b'o', 6.0)]
r.zremrangebyscore('zs3', 3, 5)
print(r.zrange('zs3', 0, -1, withscores=True))
# [(b'l', 5.3), (b'm', 5.6), (b'n', 5.8), (b'o', 6.0)]
  • zinterstore(self, dest, keys, aggregate=None):计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合,默认分数为相加后的值
  • zunionstore(self, dest, keys, aggregate=None):计算给定的一个或多个有序集的并集,并存储在新的有序集合中,默认分数为相加后的值
if r.exists('zs4'):
    r.delete('zs4')
if r.exists('zs5'):
    r.delete('zs5')
zs4 = {'baidu': 1, 'google': 2, 'sina': 4, 'twitter': 3, 'facebook': 3.54}
zs5 = {'taobao': 1, 'youtube': 2, 'twitch': 4, 'google': 5, 'sina': 5.5}
r.zadd('zs4', zs4)
r.zadd('zs5', zs5)
r.zinterstore('zs_inter', ('zs4', 'zs5'))
r.zunionstore('zs_union', ('zs4', 'zs5'))
print(r.zrange('zs_inter', 0, -1, withscores=True))
# [(b'google', 7.0), (b'sina', 9.5)]
print(r.zrange('zs_union', 0, -1, withscores=True))
# [(b'baidu', 1.0), (b'taobao', 1.0), (b'youtube', 2.0), (b'twitter', 3.0), (b'facebook', 3.54), (b'twitch', 4.0), (b'google', 7.0), (b'sina', 9.5)]

redis-5.0版本新内容

  • zpopmax(self, name, count=None):移除集合中分数最大的值,数量为count(默认1个)
  • zpopmin(self, name, count=None):移除集合中分数最小的值,数量为count(默认1个)
  • bzpopmax(self, keys, timeout=0):与zpopmax类似,阻塞操作
  • bzpopmin(self, keys, timeout=0):与zpopmin类似,阻塞操作
if r.exists('zs6'):
    r.delete('zs6')
zs6 = {'a': 1, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5}
r.zadd('zs6', zs6)
print(r.zpopmax('zs6', 2))
# [(b'f', 5.0), (b'e', 4.0)]
print(r.zpopmin('zs6', 2))
# [(b'a', 1.0), (b'b', 1.0)]
print(r.zrange('zs6', 0, -1))
# [b'c', b'd']

*SCAN操作

Redis 2.8中引入的* SCAN命令使用起来很麻烦。 虽然完全支持这些命令,但redis-py还公开了以下返回Python迭代器的方法:scan_iter,hscan_iter,sscan_iter和zscan_iter。

  • scan:
for key, value in (('A', '1'), ('B', '2'), ('C', '3')):
    r.set(key, value)
for key in r.scan_iter():
    print(key, r.get(key))

# b'C' b'3'
# b'B' b'2'
# b'A' b'1
  • sscan:
s1 = [1, 2, 3, 4, 'white', 'black']
s2 = ['a', 'b', 'c', 'Jack']
r.sadd('s1', *s1)
r.sadd('s2', *s2)
for member in r.sscan_iter('s1'):
    print(member)
print('---------')
for member in r.sscan_iter('s2'):
    print(member)
# b'2'
# b'3'
# b'black'
# b'4'
# b'white'
# b'1'
# ---------
# b'Jack'
# b'b'
# b'a'
# b'c'
  • hsacn:
d1 = {'name': 'Jack', 'id': 1, 'location': 'US'}
r.hmset('h1', d1)
for field in r.hscan_iter('h1'):
    print(field)
# (b'name', b'Jack')
# (b'id', b'1')
# (b'location', b'US')
  • zscan:
zs = {'a1': 1, 'a2': 2, 'a3': 2.5, 'b1': 4, 'b2': 3.5}
r.zadd('zs1', zs)
for field in r.zscan_iter('zs1', match='a*'):
    print(field)
(b'a1', 1.0)
(b'a2', 2.0)
(b'a3', 2.5)

流Stream操作

redis5.0 新特性,官方介绍:https://redis.io/topics/streams-intro
参考:https://blog.csdn.net/shellquery/article/details/80562422

增删改查:

  • xadd(self, name, fields, id=’*’, maxlen=None, approximate=True):追加消息
    • name:流的名字;
    • fields:追加的消息key-value,字典表形式;
    • id:*表示由服务器自动生成id,也可以自己生成,但后面加入的消息的ID要大于前面的消息ID;
    • maxlen:截断超出此大小的旧有的stream成员;
    • approximate:实际流长度可能略大于maxlen,
  • xdel(self, name, *ids):删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度
  • xlen(self, name):Stream内消息的长度
  • xrange(self, name, min=’-’, max=’+’, count=None):获取消息列表,会自动过滤已经删除的消息
if r.exists('stream_1'):
    r.delete('stream_1')
message1 = {'name': 'Flask', 'price': 10}
message1_id = r.xadd('stream_1', message1)
message2 = {'name': 'Django', 'price': 15}
message2_id = r.xadd('stream_1', message2)
print(f"message1_id:{message1_id}\nmessage2_id: {message2_id}")
# message1_id:b'1543370219288-0'
# message2_id: b'1543370219288-1'
print(f"stream len:{r.xlen('stream_1')}")
# 2
print(f"stream messages:{r.xrange('stream_1')}")
#stream messages:[(b'1543370219288-0', {b'name': b'Flask', b'price': b'10'}), (b'1543370219288-1', {b'name': b'Django', b'price': b'15'})]
r.xdel('stream_1', message1_id)
print(f"stream messages:{r.xrange('stream_1')}")
# stream messages:[(b'1543370219288-1', {b'name': b'Django', b'price': b'15'})]

独立消费

可以在不定义消费组的情况下进行Stream消息的独立消费,当Stream没有新消息时,甚至可以阻塞等待。

  • xread(self, streams, count=None, block=None): 字典表形式{流的名称:消息id},其中id时已被读取的最后一条消息的id。
    • streams: 要进行读取的流;
    • count:读取的数据数量;
    • block:阻塞的时间,毫秒;
if r.exists('stream_2'):
    r.delete('stream_2')
countrys = ['CN', 'US', 'UK', 'EU', 'JP']
i = 0
ret = []
for country in countrys:
    ret.append(r.xadd('stream_2', {'country': country, 'id': i}))
print(r.xread({'stream_2': ret[1], 'stream_1': 0}, 2))
# [['stream_2', [(b'1543373133225-2', {b'country': b'UK', b'id': b'0'}), (b'1543373133226-0', {b'country': b'EU', b'id': b'0'})]], ['stream_1', [(b'1543373133224-1', {b'name': b'Django', b'price': b'15'})]]]

# 0-0 从头开始
print(r.xread({'stream_2': '0-0'}))
# [['stream_2', [(b'1543373133225-0', {b'country': b'CN', b'id': b'0'}), (b'1543373133225-1', {b'country': b'US', b'id': b'0'}), (b'1543373133225-2', {b'country': b'UK', b'id': b'0'}), (b'1543373133226-0', {b'country': b'EU', b'id': b'0'}), (b'1543373133226-1', {b'country': b'JP', b'id': b'0'})]]]

阻塞模式

# 从尾端读取数据, 阻塞时间60s
print(r.xread({'stream_2': '$'}, block=1000 * 60))

等待60s后返回

[]

在阻塞的过程中,另一界面

r.xadd('stream_2', {'name': 'jack', 'age': 11})

读取到stream_2尾部的消息:

[['stream_2', [(b'1543373840860-0', {b'name': b'jack', b'age': b'11'})]]]

创建消费组

消费组消费模型:

  • xgroup_create(self, name, groupname, id=’$’, mkstream=False):在stream上创建新的消费组;
    • name:stream的名字;
    • groupname:消费组名称;
    • id:起始消息ID,用来初始化last_delivered_id变量,默认$从尾部开始消费,只接受新消息,当前Stream消息会全部忽略;
  • xgroup_destroy(self, name, groupname):删除消费组;
  • xinfo_groups(self, name):stream的消费组信息;
  • xinfo_stream(self, name):流的信息;
if r.exists('stream_3'):
    r.delete('stream_3')
r.xadd('stream_3', {'id': 0})
r.xadd('stream_3', {'id': 1})
# 从头部开始消费
r.xgroup_create('stream_3', 'group_2', id=0)
# 从尾部开始消费
r.xgroup_create('stream_3', 'group_1', id="$")
# 流的信息
print(r.xinfo_stream('stream_3'))
#{'length': 2, 'radix-tree-keys': 1, 'radix-tree-nodes': 2, 'groups': 2, 'last-generated-id': b'1543375608869-1', 'first-entry': (b'1543375608869-0', {b'id': b'0'}), 'last-entry': (b'1543375608869-1', {b'id': b'1'})}
# 消费组信息
print(r.xinfo_groups('stream_3'))
# pending 待处理消息
# [{'name': b'group_1', 'consumers': 0, 'pending': 0, 'last-delivered-id': b'1543375608869-1'}, {'name': b'group_2', 'consumers': 0, 'pending': 0, 'last-delivered-id': b'0-0'}]
# 移除消费组
r.xgroup_destroy('stream_3', 'group_2')
print(r.xinfo_groups('stream_3'))
# [{'name': b'group_1', 'consumers': 0, 'pending': 0, 'last-delivered-id': b'1543375608869-1'}]

消费

Stream提供了xreadgroup指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息ID。它同xread一样,也可以阻塞等待新消息。读到新消息后,对应的消息ID就会进入消费者的PEL(正在处理的消息)结构里,客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕,该消息ID就会从PEL中移除。如果消费者收到了消息处理完了但是没有回复ack,就会导致PEL列表不断增长,如果有很多消费组的话,那么这个PEL占用的内存就会放大。

  • xgroup_delconsumer(self, name, groupname, consumername):从消费组移除特定消费者
  • xinfo_consumers(self, name, groupname):消费者信息;
  • xreadgroup(self, groupname, consumername, streams, count=None, block=None):消费组的组内消费
  • xack(self, name, groupname, *ids): 正确处理消息后的响应
  • xpending(self, name, groupname):消费组内待处理的消息的信息
if r.exists('stream_4'):
    r.delete('stream_4')
r1 = r.xadd('stream_4', {'name': 'jack'})
r2 = r.xadd('stream_4', {'name': 'Tom'})
r3 = r.xadd('stream_4', {'name': 'Will'})
r.xgroup_create('stream_4', 'group_1', id=0)
# >号表示从当前消费组的last_delivered_id后面开始读
# 每当消费者读取一条消息,last_delivered_id变量就会前进
ret = r.xreadgroup('group_1', 'consumer_1', {'stream_4': ">"}, count=1)
print(ret)
# [['stream_4', [(b'1543455084777-0', {b'name': b'jack'})]]]
r.xreadgroup('group_1', 'consumer_1', {'stream_4': ">"}, count=2)
print(r.xinfo_consumers('stream_4', 'group_1'))
# idle空闲了多长时间ms没有读取消息了
# [{'name': b'consumer_1', 'pending': 3, 'idle': 1}]
print(r.xpending('stream_4', 'group_1'))
# {'pending': 3, 'min': b'1543455084777-0', 'max': b'1543455084777-2', 'consumers': [{'name': b'consumer_1', 'pending': 3}]}
# ack 2条消息
r.xack('stream_4', 'group_1', *[r1, r2])
print(r.xinfo_consumers('stream_4', 'group_1'))
# [{'name': b'consumer_1', 'pending': 1, 'idle': 1}] pending减少了

控制消息的长度

xadd 的maxlen参数可以将消息控制在一定长度,但要设置approximate=False
xtrim(self, name, maxlen, approximate=True):同样可以将stream内的消息修剪为指定长度

if r.exists('stream_5'):
    r.delete('stream_5')
for i in range(0, 10):
    r.xadd('stream_5', {'id': i})
print(r.xlen('stream_5'))
# 10
r.xadd('stream_5', {'id': 10}, maxlen=5, approximate=False)
print(r.xlen('stream_5'))
# 5
r.xtrim('stream_5', maxlen=3, approximate=False)
print(r.xlen('stream_5'))
# 3
print(r.xrange('stream_5'))
# [(b'1543455600180-4', {b'id': b'8'}), (b'1543455600181-0', {b'id': b'9'}), (b'1543455600181-1', {b'id': b'10'})]

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

持久化

Redis 提供了多种不同级别的持久化方式:

  • RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。
  • AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。
  • Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。
  • 你甚至可以关闭持久化功能,让数据只在服务器运行时存在。

RDB 持久化和 AOF 持久化之间的异同,查看 http://doc.redisfans.com/topic/persistence.html

RDB快照

在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb(var/lib/redis) 的二进制文件中。
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。
你也可以通过调用 SAVE 或者 BGSAVE , 手动让 Redis 进行数据集保存操作。
比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:
save 60 1000
在redis.conf中的默认设置

# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

这种持久化方式被称为快照(snapshot)。

RDB快照运作

当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:

  1. Redis 调用 fork() ,同时拥有父进程和子进程。
  2. 子进程将数据集写入到一个临时 RDB 文件中。
  3. 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。

AOF (append-only file)

快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。
尽管对于某些程序来说, 数据的耐久性并不是最重要的考虑因素, 但是对于那些追求完全耐久能力(full durability)的程序来说, 快照功能就不太适用了。
你可以通过修改配置文件来打开 AOF 功能:
appendonly yes
从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件(默认appendonly.aof)的末尾。
这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。

AOF重写

因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。
举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。
然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。
执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。

AOF耐久

你可以配置 Redis 多久才将数据 fsync 到磁盘一次。

有三个选项:

  • 每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。
  • 每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
  • 从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择。
    推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
    redis.conf的默认设置
# If unsure, use "everysec".

# appendfsync always
appendfsync everysec
# appendfsync no

从RDB切换到AOF

  1. 为最新的 dump.rdb 文件创建一个备份。
  2. 将备份放到一个安全的地方。
  3. 执行以下两条命令:
redis-cli> CONFIG SET appendonly yes

redis-cli> CONFIG SET save "" 
  1. 确保命令执行之后,数据库的键的数量没有改变。
  2. 确保写命令会被正确地追加到 AOF 文件的末尾。

执行CONFIG SET save "" 会关闭 RDB 功能。 这一步是可选的,如果你愿意的话,也可以同时使用 RDB 和 AOF 这两种持久化功能。当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。

备份Redis数据

Redis 对于数据备份是非常友好的, 因为你可以在服务器运行的时候对 RDB 文件进行复制: RDB 文件一旦被创建, 就不会进行任何修改。 当服务器要创建一个新的 RDB 文件时, 它先将文件的内容保存在一个临时文件里面, 当临时文件写入完毕时, 程序才使用 rename(2) 原子地用临时文件替换原来的 RDB 文件。
建议:

  • 创建一个定期任务(cron job), 每小时将一个 RDB 文件备份到一个文件夹, 并且每天将一个 RDB 文件备份到另一个文件夹。
  • 确保快照的备份都带有相应的日期和时间信息, 每次执行定期任务脚本时, 使用 find 命令来删除过期的快照: 比如说, 你可以保留最近 48 小时内的每小时快照, 还可以保留最近一两个月的每日快照。
  • 至少每天一次, 将 RDB 备份到你的数据中心之外, 或者至少是备份到你运行 Redis 服务器的物理机器之外。

猜你喜欢

转载自blog.csdn.net/qq_19268039/article/details/84474220