1-Python - redis-py模块

about

python3.10 + redis软件3.2.100 + redis-py4.6.0
django-redis
GitHub官档:https://github.com/jazzband/django-redis
pypi官网:https://pypi.org/project/django-redis/
中文官网:https://django-redis-chs.readthedocs.io/zh_CN/latest/

Python中想要操作Redis软件,可以通过redis-py模块来完成。

下载:

pip install redis
# pip install redis==4.6.0

快速上手

"""
pip install redis
"""
import redis

# 创建连接对象,后续操作都是通过连接对象来操作,方式1
conn1 = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# 创建连接对象,后续操作都是通过连接对象来操作,方式2
conn2 = redis.Redis.from_url("redis://:1234@127.0.0.1:6379/0")
conn1.set('k1', 'v1')
print(conn1.get('k1'))

conn2.set('k2', 'v2')
print(conn2.get('k2'))

decode_responses

指定该参数就可以不用每次都手动decode了。

import redis

# 创建连接对象,后续操作都是通过连接对象来操作
conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8')


conn.set('k1', 'v1')
print(conn.get('k1'), type(conn.get('k1')))  # b'v1' <class 'bytes'>

conn2 = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True)

conn2.set('k2', 'v2')
print(conn2.get('k2'), type(conn2.get('k2')))  # v2 <class 'str'>

连接池

参考:

https://www.cnblogs.com/jiangxiaobo/p/12856575.html

https://www.ycpai.cn/python/VxSw8IKS.html

通常情况下,当我们操作redis时,会经过创建并建立连接对象,执行redis操作,操作完成之后,释放连接。

一般情况下, 这是没问题的, 但当并发量比较高的时候, 频繁的连接创建和释放对性能会有较高的影响。

所以,并发场景较高的时候,我们会采用连接池。

其原理就是通过预先创建多个连接,当进行redis操作时,直接获取已经创建的连接进行操作, 而且操作完成后,不会释放,用于后续的其他redis操作,这样就达到了避免频繁的redis连接创建和释放的目的,从而提高性能了。

配置简单,用法也不变:

import redis

pool = redis.ConnectionPool(
    host="127.0.0.1",
    port=6379,
    password="1234",
    db=0,
    max_connections=100,
    decode_responses=True  # 如果使用这个参数,那么必须在连接池中声明
)
conn = redis.Redis(connection_pool=pool)
conn.set('k1', 'v1')
print(conn.get('k1'))

简单说下连接池的原理,在ConnectionPool类中,主要维护了:

  • _available_connections可用连接列表。
  • _in_use_connections正在使用的连接集合。

在执行操作时,其获取连接时执行get_connection方法,该方法中,获取连接的过程是:

  • 从可用连接列表中尝试获取连接,如果没有获取到,则生成一个连接对象,同时将该对象添加到可用连接列表中。
  • 将获取到的,或者新生成的连接对象添加到正在使用的连接集合中。

那么操作执行完,连接对象又是如何处理的呢?在release方法中,将连接从_in_use_connections正在使用的连接集合中拿出来,放回到可用的连接列表中,这样后续连接就可以复用这个连接了。

通用操作

import time
import redis

# 创建连接对象,后续操作都是通过连接对象来操作,方式1
conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)



# ------------------ 通用操作 ------------------
# print(conn.keys("*"))  # 获取当前数据库下所有的key,尽量避免在生产中使用

# 返回k1的value类型
# print(conn.type('k1'))  # string

# 判断 key 是否存在
# print(conn.exists('k1'))  # 1
# print(conn.exists('k8888'))  # 0

# 重命名
# conn.rename('k1', 'k11')
# print(conn.get('k11'))

# 删除key,key不存在返回0,key存在,且删除成功返回1
# print(conn.delete('k2'))

# 设置过期时间
# conn.set('k1', 'v1')  # 起始是没有过期时间的
# time.sleep(5)
# conn.expire('k1', 10)  # 给它设置上过期时间
# conn.persist('k3')    # 取消过期时间

# 返回key的剩余过期时间
# conn.set('k1', 'v1')
# print(conn.ttl('k1'))  # -1 表示没有设置生存时间
# conn.set('k1', 'v1', ex=10)
# print(conn.ttl('k1'))  # 10
# time.sleep(2)
# print(conn.ttl('k1'))  # 8

# 返回数据库中key的数量,生产可用
# print(conn.dbsize())

# 清空数据库的所有key,生产禁用
# print(conn.flushall())  # True
# print(conn.dbsize())    # 0

str

基础操作

import time
import redis

# 创建连接对象,后续操作都是通过连接对象来操作,方式1
conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)



# ------------------ 字符串操作 ------------------

# 设置一个值
# conn.set('k1', 'v1')  # 没有过期时间限制
# conn.set('k1', 'v1', ex=10)  # ex: 过期时间:秒

# 设置一个值,带过期时间
# conn.setex('k2', 10, 'v1')  # 过期时间:秒

# 先get,不存在返回None,存在返回上一次的值,然后再设置一个新值
# print(conn.getset('k1', 'v11'))
# print(conn.get('k1'))

# 如果key不存在,就设置一个key:value,如果key存在则什么都不做
# conn.setnx('k3', 'v3')
# print(conn.get('k3'))

# 批量设置值和取值
# d = {'k1': "v1", "k2": "v2"}
# conn.mset(mapping=d)
# conn.set('k3', 'v3')
# print(conn.mget('k1', 'k2'))  # 批量获取
# print(conn.mget(d.keys()))  # 批量获取
# print(conn.keys("*"))  # 获取当前数据库下所有的key

# 如果key存在,返回值的长度,否则返回0
# print(conn.strlen('k3'))

# 查
# conn.set('k1', 'v1')  # 没有过期时间限制
# conn.set('k2', 'v2')  # 没有过期时间限制
# print(conn.get('k1'))  # v1
# print(conn.keys('*'))
# 删
# conn.set('k1', 'v1')  # 没有过期时间限制
# print(conn.get('k1'))  # v1
# conn.delete('k1')
# print(conn.get('k1'))  # None

# 修改,起始就是从新赋值就完了
# conn.set('k1', 'v1')
# print(conn.get('k1'))  # v1
# conn.set('k1', 'v111')
# print(conn.get('k1'))  # v111

计数功能

import time
import redis

# 创建连接对象,后续操作都是通过连接对象来操作,方式1
conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ 字符串计数功能 ------------------
# 每次调用incr命令,num值自加一
# conn.set('num', 1)
# print(conn.get('num'))
# for i in range(5):
#     print(conn.incr('num'))

# 一次性累加指定数
# conn.set('num', 1)
# print(conn.incrby('num', 1000))  # 1001
# print(conn.get('num'))  # 1001

# decr 使num自减一
# conn.set('num', 10)
# for i in range(5):
#     print(conn.decr('num'))
# print(conn.get('num'))  # 5

# 一次性累减指定数
conn.set('num', 10)
print(conn.decrby('num', 8))  # 2
print(conn.get('num'))  # 2

hash

类似python的字典,但是成员只能是string,专门用于结构化的数据信息。

import time
import redis

conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ hash ------------------
data = {"id": 1, "user": "zhangkai", 'age': 18}

# conn.hmset('user1', mapping=data)
# conn.hset(name='user2', key="name", value='zhangkai')  # 这种写法没问题
# conn.hset(name='user3', mapping=data)  # 这种写法,有问题
# print(conn.hget('user3', 'name'))
"""
关于conn.hset(name='user3', mapping=data):
    hmset:
        兼容redis低版本(redis3.2.100)和高版本(redis5.0.7及以上)    
        但高版本不推荐使用这个方法了,虽然能用,但是运行有warning
        高版本的redis推荐使用hset来代替hmset
        至于低版本的redis使用hmset有warning的话,请忽略
    hset
        低版本的redis(redis3.2.100)用这个报错:redis.exceptions.ResponseError: wrong number of arguments for 'hset' command
        但高版本的redis就不报错
    最终:如果你用的redis版本较低如Redis3,请用hmset,如果redis版本较高Redis5,请用hset
"""

# 获取指定字段的值
# print(conn.hget('user1', 'id'))
# 获取多个字典的值
# print(conn.hmget('user1', "id", "user"))
# 返回所有字段和值
# print(conn.hgetall('user1'))

# # 返回key中所有的字段
# print(conn.hkeys('user1'))
# # 返回key中所有的字段值
# print(conn.hvals('user1'))
# # 判断指定字段是否存在,存在返回1,否则返回0
# print(conn.hexists('user1', 'id'))
# # 返回key中所有字段的数量
# print(conn.hlen('user1'))
# # 为整型字段的值加固定数字
# print(conn.hincrby('user1', "age", 10))
# # 为指定字段重新赋值
# print(conn.hset('user1', "age", 12))
# print(conn.hget('user1', 'age'))

# 当key不存在,创建该key,并且设置上指定的字段和值
# print(conn.hgetall('user5'))
# print(conn.hsetnx('user5', 'addr2', "beijing2"))
# print(conn.hgetall('user5'))

# # 当key存在,想要设置的字段和值存在,不生效
# print(conn.hgetall('user1'))
# print(conn.hsetnx('user1', 'addr', "beijing"))
# print(conn.hgetall('user1'))
# # 当key存在,指想要设置的字段值如果不存在则设置上该字段和值
# # print(conn.hsetnx('user4', 'age', 30))
# print(conn.hgetall('user1'))
# print(conn.hsetnx('user1', 'addr2', "beijing2"))
# print(conn.hgetall('user1'))

为hash的key设置过期时间

注意,这里演示的是为整个hash的key设置过期时间,而不能为其中某个字段设置过期时间:

import time
import redis

conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ hash ------------------
data = {"id": 1, "user": "zhangkai", 'age': 18}
conn.hmset('user1', mapping=data)
print(conn.hgetall('user1'))
conn.expire('user1', 8)  # 过期时间: 秒
time.sleep(10)
print(conn.hgetall('user1'))

list

import time
import redis

conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ list ------------------
# l1 = ['a', 'b', 'c', 'd']
# # conn.lpush('l1', 'a', 'b', 'c')
# # 批量插入列表
# conn.lpush('l1', *l1)
# # 如果 key 存在则插入,不存在则什么也不做
# conn.lpushx('l1', 'd')
# conn.lpushx('l2', 'w')
#
# # 从列表的左侧找到元素a,即不往后找了,然后在元素 a 前面/后面插入一个元素
# conn.linsert('l1', 'before', 'c', 'x')
# conn.linsert('l1', 'after', 'c', 'z')

# 在列表尾部追加 元素 1   注意,插入的整型元素也会转变为str类型
# print(conn.lrange('l1', 0, -1))
# # conn.rpush('l1', 1)
# # 在列表尾部先追加 2 再追加 3
# conn.rpush('l1', 2, 3)
# print(conn.lrange('l1', 0, -1))
# 如果列表 l1 存在就将元素4追加到列表尾部,如果列表不存在则什么也不做
# conn.rpushx('l1', 4)

# 根据索引下标返回元素
# print(conn.lindex('l1', 1))
# 从索引0开始取到-1,也就是从头取到尾,获取列表中的所有元素
# print(conn.lrange('l1', 0, -1))
# 取索引 0 1 2 三个索引对应的元素
# print(conn.lrange('l1', 0, 2))
# 取索引下标为 2 对应的元素
# print(conn.lrange('l1', 2, 2))
# 如果索引不在合法范围内,则取空
# print(conn.lrange('l1', 8, 9))
# 返回列表的长度
# print(conn.llen('l1'))

# 抛出列表头部/尾部元素
# print(conn.lpop('l1'))
# print(conn.rpop('l1'))

# 将列表 l1 尾部的元素抛出并且添加到列表 l2 中,l2不存在则自动创建
# conn.rpoplpush('l1', 'l2')

# 从左到右,删除指定个数的元素 a1 ,本示例中,若 a1 有 1 个,就删一个,若 a1  有 3 个或者更多,也就删除 2 个
# l3 = ['a1', 'a2', 'a1', 'a3', 'a1', 'a4', 'a5']
# conn.lpush('l3', *l3)
# print(conn.lrem('l3', 2, 'a1'))
# print(conn.lrange('l3', 0, -1))
# 保留索引2-4的元素,其余删除
# conn.ltrim('l3', 2, 4)
# print(conn.lrange('l3', 0, -1))


# 将列表索引为0的元素修改为指定值,如果索引位置不存在则报错
# l1 = ['a', 'b', 'c', 'd']
# conn.lpush('l1', *l1)
# print(conn.lrange('l1', 0, -1))
# conn.lset('l1', 0, 'w')
# print(conn.lrange('l1', 0, -1))


# 删除列表
# conn.delete('l1')

set

import redis

conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ set ------------------
# 增
# 声明集合s1,并添加多个元素,也可以是一个元素,也可以批量添加多个元素
# conn.sadd('s1', 'a')
# 对于整型同样转为str, 且元素不能重复,且是无序的
# conn.sadd('s1', 'a', 'b', 'c', 1, 2, 3, 1)

# 查
# conn.sadd('s1', *{'a', 'b', 'c', 1, 2, 3, 1})
# 返回 s1 中所有元素
# print(conn.smembers('s1'))
# 返回 s1 中元素的个数
# print(conn.scard('s1'))
# 随机返回集合中的元素
# print(conn.srandmember('s1'))
# 判断元素 a 是否存在
# print(conn.sismember('s1', 'a'))

# 删除
# conn.sadd('s1', *{'a', 'b', 'c', 1, 2, 3, 1})
# 随机删除一个元素,并将这个元素返回
# print(conn.spop('s1'))
# 删除整个集合
# print(conn.delete('s1'))


# 移动
# 将元素 a 从s1移动到s2中,如果s2不存在,就先创建再移动
# conn.sadd('s1', *{'a', 'b', 'c', 1, 2, 3, 1})
# print(conn.smove('s1', 's2', 'w'))  # s1中没有该元素则啥也不做,并返回False
# print(conn.smove('s1', 's2', 'a'))
# print(conn.smembers('s1'))
# print(conn.smembers('s2'))

交集、并集、差集:

import redis

conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ set ------------------
# 准备数据
python = {"小a", '小b', '葫芦娃', '小c', '小麻雀'}
linux = {'小c', '小麻雀', '小d', '小e', '小东北'}
conn.sadd('python', *python)
conn.sadd('linux', *linux)

# 求交集,找出即学习了Python又学习了Linux的同学
# print(conn.sinter('python', 'linux'))

# 求并集,找出学习两门课程的所有人, python + linux
# print(conn.sunion('python', 'linux'))

# 求差集,找出只学习了Python(或者Linux)课程的人
# print(conn.sdiff('python', 'linux'))  # 只学习Python课程的人
# print(conn.sdiff('linux', 'python'))  # 只学习Linux课程的人

zset

import redis

conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ zset ------------------
# 增
score = {
    "zhangkai": 108,
    "likai": 99,
    "wangkai": 100,
    "zhaokai": 102,
}
conn.zadd('score', mapping=score)

# 查, withscores是可选参数,表示同时返回所有成员和分数
# print(conn.zrange('score', 0, -1))
# print(conn.zrange('score', 0, -1, withscores=True))
# 升序/降序排序返回成员和得分
# print(conn.zrange('score', 0, -1, withscores=True))
# print(conn.zrevrange('score', 0, -1, withscores=True))

# 返回成员在有序集合中的索引下标,如果成员不存在返回None
# print(conn.zrank('score', 'zhangkai'))
# print(conn.zrank('score', 'w'))  # None

# 返回成员的总数量
# print(conn.zcard('score'))

# 升序返回score中所有成员和分数
# print(conn.zrangebyscore('score', '-inf', '+inf', withscores=True))
# # 加减不能调换位置
# print(conn.zrangebyscore('score', '+inf', '-inf', withscores=True))

# 升序返回score中分数在 100 <= 分数 <= 200 这个范围内的成员/分数
# print(conn.zrangebyscore('score', 100, 200))
# print(conn.zrangebyscore('score', 100, 200, withscores=True))
#
# # 返回score中分数在 100 <= 分数 <= 200 这个范围内的成员的数量
# print(conn.zcount('score', 100, 200))

# 返回score中所有成员/分数,并且以索引从大到小排序
# print(conn.zrevrange('score', 0, -1, withscores=True))

# 返回降序集合score中 0 <= index <= 2 索引范围内的成员
print(conn.zrevrange('score', 0, 2))


# 返回score中分数范围在 120 <= 的 <= 100 范围内的成员,并以大到小降序排序
# 注意,源码中的min和max位置和其他的方法不一样,其他的都是min,max, 这个方法是max,min
# 所以,按下面这么写,你要把大的值放前面
# print(conn.zrevrangebyscore('score', 120, 100, withscores=True))
# # 或者这样写,指名道姓的传值也行
# print(conn.zrevrangebyscore('score', min=100, max=120, withscores=True))

# 改
# 为指定成员重新赋值
print(conn.zrevrange('score', 0, -1, withscores=True))
print(conn.zadd('score',  mapping={"zhangkai": 110}))  # 修改其得分,可以同时修改多个
print(conn.zadd('score',  mapping={"zhangkai": 120, 'sunkai': 12}))  # 修改其得分,可以同时修改多个,也可以添加新的成员
print(conn.zrevrange('score', 0, -1, withscores=True))



# 为指定成员增加指定分数,并返回增加后的分数
# print(conn.zscore('score', 'zhangkai'))
# print(conn.zincrby('score',  5, 'zhangkai'))
# print(conn.zscore('score', 'zhangkai'))

# 删除
# 删除分数在指定范围(包含)内的成员,并返回删除成员的数量
# print(conn.zrange('score', 0, -1, withscores=True))
# print(conn.zremrangebyscore('score', 90, 100))
# print(conn.zrange('score', 0, -1, withscores=True))


# 删除score中指定索引范围内 0 <= index <= 2 的成员,并返回删除成员的数量
# print(conn.zrange('score', 0, -1, withscores=True))
# print(conn.zremrangebyrank('score', 0, 2))
# print(conn.zrange('score', 0, -1, withscores=True))

# 删除score中一个或者多个成员,返回实际删除的个数
# print(conn.zrange('score', 0, -1, withscores=True))
# print(conn.zrem('score', *{"zhangkai", 'likai'}))
# print(conn.zrange('score', 0, -1, withscores=True))

# 删除key
# conn.delete('score')

音乐排行榜示例

import random
import redis

conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)
#
# # ------------------ zset ------------------
topn = {
    '以父之名': 0,
    '蓝莲花': 0,
    '时光': 0,
    '稻香': 0,
}
conn.zadd('topn', mapping=topn)

# 一段时间过后,各个歌曲的播放量就不一样了

for key in topn:
    score = random.randrange(100, 200)
    conn.zincrby('topn', score, key)  # 为指定的成员增加指定的分数

# 返回播放量前三名的歌名和播放量
print(conn.zrevrange('topn', 0, 2, withscores=True))
# print(conn.zrevrange('topn', 0, -1, withscores=True))

队列操作

消息队列

消息队列是一种消息传递模式,它将消息从发送者传递到一个或多个接收者,而且不需要彼此之间显式的连接。这样做的好处是解耦发送者和接收者。

常见的消息队列软件:

  • Redis
  • kafka
  • rabbitmq

Redis消息队列的使用

redis的消息队列使用list类型实现,支持添加、移除队列元素,队列结构为先进先出。

简单示例:


import redis

# 创建连接对象,后续操作都是通过连接对象来操作
conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8')

conn.lpush('my_queue', 'root')
conn.lpush('my_queue', 'admin')

# 取值
q1 = conn.brpop('my_queue', timeout=5)
print(q1)  # (b'my_queue', b'root')
print(q1[-1])  # b'root'
print(q1[-1].decode('utf8'))  # root

q2 = conn.brpop('my_queue', timeout=5)
print(q2)  # (b'my_queue', b'admin')

# 由于队列中没有元素了,那么就会等5秒,然后不等了,返回None
q3 = conn.brpop('my_queue', timeout=5)
print(q3)  # None

再来一个示例:

import time
import random
import redis
from concurrent.futures import ThreadPoolExecutor


pool = redis.ConnectionPool(
    host="127.0.0.1",
    port=6379,
    password="1234",
    db=0,
    max_connections=2,
    decode_responses=True  # 这个参数必须在连接池中声明
)
conn = redis.Redis(connection_pool=pool)

def producer():
    """ 生产者 """
    count = 0
    while True:
        conn.lpush('my_queue', count)
        time.sleep(random.randrange(1, 10))
        count += 1

def customer1():
    while True:
        res = conn.rpop('my_queue')
        if res:
            print('\n消费者rpop,消费了: ', res)
        print('\r消费者rpop啥都没拿到,默认返回{} {}'.format(res, '.' * random.randrange(1, 6)),   end='')
        """
        通过上面的打印效果,可以发现,conn.rpop的执行效果是一直取值
        取不到值,返回None之后马不停蹄的接着取值,这个在生产中尽量不用,损耗性能
        """
def customer2():
    while True:
        res = conn.brpop('my_queue', timeout=5)
        print('消费者brpop,消费了: ', res)
        """
        brpop可以设置阻塞时间,比如示例中的5秒
        执行逻辑是这样的:
            取值,取到了就消费掉,也就是赋值给res了,我们可以进行后续的处理
            如果没有取到值,就阻塞5秒,这5秒内拿到值了,就消费掉,然后进行下一轮的阻塞取值
            如果阻塞的5秒内,也没有取到值,就返回None,然后进行下一轮的阻塞取值
        实际开发中,这个用的多一些
        """

if __name__ == '__main__':
    with ThreadPoolExecutor(2) as t:
        # for i in [producer, customer1]:
        for i in [producer, customer2]:
            t.submit(i)

管道

参考:

https://blog.csdn.net/weixin_44799217/article/details/128572007

https://www.cnblogs.com/kangoroo/p/7647052.html

默认执行一次redis操作,都会创建(连接池申请连接)和断开(归还连接池)一次连接操作,如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,从而提高性能。

管道技术

管道技术(Pipeline) 是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。

管道技术解决了多个命令集中请求时造成网络资源浪费的问题,加快了 Redis 的响应速度,让 Redis 拥有更高的运行速度。但要注意的一点是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。

注意事项:

发送的命令数量不会被限制,但输入缓存区也就是命令的最大存储体积为 1GB,当发送的命令超过此限制时,命令不会被执行,并且会被 Redis 服务器端断开此链接;
如果管道的数据过多可能会导致客户端的等待时间过长,导致网络阻塞;
部分客户端自己本身也有缓存区大小的设置,如果管道命令没有没执行或者是执行不完整,可以排查此情况或较少管道内的命令重新尝试执行。

import redis

pool = redis.ConnectionPool(
    host="127.0.0.1",
    port=6379,
    password="1234",
    db=0,
    max_connections=2,
    decode_responses=True  # 这个参数必须在连接池中声明
)
conn = redis.Redis(connection_pool=pool)
# ------------------ pipeline基础写法 ------------------
# # 再执行多个命令之前,要创建好管道
# pipe = conn.pipeline()
# # 写法1
# pipe.set('k1', 'v1')
# pipe.set('k2', 'v2')
#
# # 写法2
# pipe.set('k3', 'v3').set('k4', 'v4')
#
# # 批量执行多个命令,但注意,返回的是这几个命令的执行情况
# res = pipe.execute()
# print(res)  # [True, True, True, True]
#
# pipe = conn.pipeline()
# pipe.get('k1')
# pipe.get('k2')
# pipe.get('k3').get('k4')
# res = pipe.execute()
# print(res)  # ['v1', 'v2', 'v3', 'v4']

# ------------------ with管理pipeline ------------------
with conn.pipeline() as pipe:
    pipe.get('k1')
    pipe.get('k2')
    pipe.get('k3').get('k4')
    res = pipe.execute()
    print(res)  # ['v1', 'v2', 'v3', 'v4']

发布订阅

import time
import datetime
import random
import redis
from concurrent.futures import ThreadPoolExecutor


class Product(object):
    def __init__(self):
        # 这个示例中,多个订阅者,应该是多个独立的连接对象,所以不能共用一个连接池,一个订阅者一个脚本文件就没问题
        # self.conn = redis.Redis(connection_pool=pool)
        self.conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', decode_responses=True)
        """
        # 向指定频道发布消息
        self.conn.publish(channel, message)

        # 下面三行一起用
        self.pub = self.conn.pubsub()  # 打开收音机
        self.pub.subscribe(channel)   # 以列表的形式指定一个或者多个频道
            channel: 以列表的形式指定一个或者多个频道['fm97.5', 'fm80.5', 'fm77.5']
                     如果该频道有消息,则都会接收到
        res = self.pub.listen()  # 准备接收消息
        for msg in res:  # 从监听的这些频道中,拿到最新的消息
            print(res)

        # 取消订阅指定频道
        self.pub.unsubscribe(channel)
            channel: 取消订阅一个或者多个频道['fm97.5', 'fm80.5', 'fm77.5']
        """

    def add_channel_msg(self):
        """ 模拟发布消息:往不同的频道中添加消息 """
        # 频道列表
        self.pub_list = ['fm97.5', 'fm80.5', 'fm77.5']
        while True:
            channel = random.choice(self.pub_list)  # 随机选出来一个频道
            msg = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')  # 处理消息
            print(f"{channel},发布消息{msg}")
            self.conn.publish(channel, msg)  # 往指定频道发布消息
            time.sleep(random.randrange(1, 3))

    def subscribe(self, channel):
        """
        不同的消费者,通过传递不同的频道号,订阅该频道的消息
        channel: ['fm97.5', 'fm77.5']
        """
        # 1. 打开收音机
        self.pub = self.conn.pubsub()
        # 2. 调频
        self.pub.subscribe(channel)
        # 3. 监听我订阅的频道(可以是多个)
        return self.pub.listen()  # 准备接收消息,后续就可以循环接受来自该频道发布的消息了


    def get(self, user, channel):
        # 持续接受来自指定(多个)频道的消息
        for msg in self.subscribe(channel):  # channel = ['fm97.5', 'fm77.5']
            print(f"用户{user}接受来自{msg['channel']}的消息: {msg}")
            time.sleep(1)


if __name__ == '__main__':

    with ThreadPoolExecutor(4) as t:
        # 整个连接对象,专门负责发布消息
        t.submit(Product().add_channel_msg)
        user_dict = {
            "zhangkai1": {'get': Product().get, 'channel': ['fm97.5', 'fm77.5']},
            "zhangkai2": {'get': Product().get, 'channel': ['fm80.5']},
            "zhangkai3": {'get': Product().get, 'channel': ['fm80.5']},
        }
        # 模拟三个订阅者,订阅指定频道的消息
        for k, v in user_dict.items():
            t.submit(v['get'], k, v['channel'])

Redis发布订阅优点:

Redis的发布订阅支持多个生产者/消费者,同时生产消费消息,优点就是非常简洁,因为它的实现原理就是单纯地为生产者、消费者建立「数据转发通道」,把符合规则的数据,从一端转发到另一端。
缺点:

发布订阅模式是"发后既忘"的工作模式,如果订阅者离线,那么重连之后不能消费之前的历史消息。
无法持久化保存消息,如果 Redis 服务器宕机或重启,那么所有的消息将会丢失。
积压的消息不能太多,否则也会丢数据【可以通过client-output-buffer-limit这个参数配置】。
小结:对于特别简单的发布订阅场景,可以考虑Redis的发布订阅功能,但是复杂的场景,还是选择专业的消息队列软件实现吧。

连接Redis哨兵

参考:

from redis.sentinel import Sentinel

if __name__ == '__main__':
    # 哨兵监听的别名,这个就是你redis配置中的名字
    server_name = "myredis"

    # 设置哨兵组的IP和PORT
    sentinel_list = [
        ("192.168.10.150", 26379),
        ("192.168.10.151", 26379),
        ("192.168.10.152", 26379)
    ]
    # 初始化哨兵对象,并传递哨兵组的IP和端口信息
    sentinel = Sentinel(sentinel_list)

    # print(sentinel.discover_slaves(server_name))
    # print(sentinel.discover_master(server_name))

    # 从哨兵监视中获取master主库 ('192.168.10.150', 6379)
    master_client = sentinel.master_for(server_name, decode_responses=True)

    # 从哨兵监视中获取slave从库 [('192.168.10.151', 6379), ('192.168.10.152', 6379)]
    slave_client = sentinel.slave_for(server_name, decode_responses=True)

    # 主库中设置值
    master_client.set("user", "zhangkai")

    # 从库中获取值
    print(slave_client.get("user"))

来封装下:

from redis.sentinel import Sentinel

class PyConnectionSentinel(object):
    """ Python连接Redis哨兵 """

    def __init__(self, server_name, sentinel_list, decode_responses=True):
        # 从库对象
        self.replica = None
        # 主库对象
        self.master = None
        # 哨兵对象
        self.sentinel_obj = None
        # 哨兵监听的别名,这个就是你redis配置中的名字
        self.server_name = server_name
        self.sentinel_list = sentinel_list
        # 根据需要决定是否自动对响应结果进行decode操作,这里默认是True
        self.decode_responses = decode_responses
        self.init_sentinel()
        self.get_master_client()
        self.get_replica_client()

    def init_sentinel(self):
        """ 初始化哨兵对象,并传递哨兵组的IP和端口信息 """
        self.sentinel_obj = Sentinel(self.sentinel_list)
        print("discover_master-->", self.sentinel_obj.discover_master(self.server_name))
        print("discover_slaves-->", self.sentinel_obj.discover_slaves(self.server_name))
        """
        discover_master--> ('192.168.10.150', 6379)
		discover_slaves--> [('192.168.10.151', 6379), ('192.168.10.152', 6379)]
        """

    def get_master_client(self):
        """ 从哨兵监视中获取master主库 """
        self.master = self.sentinel_obj.master_for(
            self.server_name,
            decode_responses=self.decode_responses,
            # 指Redis发出命令接收响应的时间不能超过此参数设置时间. 如果超过了此时间, 将会抛出异常redis.exceptions.TimeoutError: Timeout reading from socket, 即读取响应超时。
            # 建议设置这个时间,防止程序读取redis数据超时导致服务卡住,同时增加对这个的异常处理。
            # socket_timeout=0.5,

            # 指Redis建立连接超时时间. 当设置此参数时, 如果在此时间内没有建立连接, 将会抛出异常redis.exceptions.TimeoutError: Timeout connecting to server。
            # socket_connect_timeout不设置时,这个值等于socket_timeout。
            # 可以只设置socket_timeout
            # socket_connect_timeout=0.5

            # password='12345'

            # Boolean类型,建议设置为True,当设置False时, 一个命令超时后, 将会直接抛出timeout异常。
            # 当设置为True时, 命令超时后,将会重试一次, 重试成功则正常返回; 失败则抛出timeout异常。
            retry_on_timeout=True
        )

    def get_replica_client(self):
        """ 从哨兵监视中获取slave主库 """
        self.replica = self.sentinel_obj.master_for(self.server_name, decode_responses=self.decode_responses)


if __name__ == '__main__':
    # 哨兵监听的别名,这个就是你redis配置中的名字
    server_name = "myredis"

    # 设置哨兵组的IP和PORT
    sentinel_list = [
        ("192.168.10.150", 26379),
        ("192.168.10.151", 26379),
        ("192.168.10.152", 26379)
    ]
    # 关于哨兵的所有操作都可以通过这个类封装
    obj = PyConnectionSentinel(
        server_name=server_name,
        sentinel_list=sentinel_list
    )
    # 通过master可以进行读写操作
    obj.master.set('k1', 'v1111')
    print(obj.master.get("k1"))  # v1111
    # 你也可以通过从节点进行读操作,从而达到主从分离的目的
    print(obj.replica.get("k1"))  # v1111

连接集群

参考:https://www.cnblogs.com/juelian/p/17560257.html

from redis import RedisCluster
from redis.cluster import ClusterNode

if __name__ == '__main__':


    # 集群所有节点的列表
    nodes = [
        ClusterNode("192.168.10.150", port=6380), ClusterNode("192.168.10.150", port=6381),

        ClusterNode("192.168.10.151", port=6380), ClusterNode("192.168.10.151", port=6381),

        ClusterNode("192.168.10.152", port=6380), ClusterNode("192.168.10.152", port=6381),
    ]

    # 可以给定全部集群中的机器IP信息
    cluster = RedisCluster(startup_nodes=nodes, decode_responses=True)

    # 也可以随便指定一个节点(不管主从都可以,它会自动定位)
    # cluster = RedisCluster(host="192.168.10.150", port=6379, decode_responses=True)
    cluster.set("user", "zhangkai")
    print(cluster.get("user"))

常见错误

redis.exceptions.ResponseError: wrong number of arguments for 'hset' command

python3.10 + redis3.0 + redis模块4.5.4

import redis

# 创建连接对象,后续操作都是通过连接对象来操作,方式1
conn = redis.Redis(host='127.0.0.1', port=6379, password='1234', encoding='utf-8', decode_responses=True, db=0)

# ------------------ hash ------------------
data = {"id": 1, "user": "zhangkai"}
# conn.hmset('user1', mapping=data)  # 不推荐使用了,请用hget代替
conn.hset('user2', mapping=data)  #

执行conn.hset报错:

Traceback (most recent call last):
  File "D:\mydata\tmp\test.py", line 10, in <module>
    conn.hset('user2', mapping=data)  # 不推荐使用了,请用hget代替
  File "C:\software\miniconda3\lib\site-packages\redis\commands\core.py", line 4932, in hset
    return self.execute_command("HSET", name, *items)
  File "C:\software\miniconda3\lib\site-packages\redis\client.py", line 1258, in execute_command
    return conn.retry.call_with_retry(
  File "C:\software\miniconda3\lib\site-packages\redis\retry.py", line 46, in call_with_retry
    return do()
  File "C:\software\miniconda3\lib\site-packages\redis\client.py", line 1259, in <lambda>
    lambda: self._send_command_parse_response(
  File "C:\software\miniconda3\lib\site-packages\redis\client.py", line 1235, in _send_command_parse_response
    return self.parse_response(conn, command_name, **options)
  File "C:\software\miniconda3\lib\site-packages\redis\client.py", line 1275, in parse_response
    response = connection.read_response()
  File "C:\software\miniconda3\lib\site-packages\redis\connection.py", line 882, in read_response
    raise response
redis.exceptions.ResponseError: wrong number of arguments for 'hset' command

Process finished with exit code 1
posted @ 2023-07-28 15:33  听雨危楼  阅读(662)  评论(0编辑  收藏  举报