Redis 的集和 set 键允许用户将任意多个不同的元素存储到集和中, 既可以是文本数据, 也可以是二进制数据. 其与列表有以下两个明显的区别:
- 列表可以存储重复元素, 而集和只存储非重复元素
- 列表以有序方式存储元素, 而集和则以无序方式存储元素
下面介绍结合键的各个命令
Set 集和
SADD: 将元素添加到集和
PLAINTEXTSADD set element [element ...]
返回成功添加的新元素数量作为返回值, 由于集和不存储相同元素, 所以会自动忽略重复的元素
SREM: 从集和中移出元素
PLAINTEXTSREM set element [element ...]
返回被移除的元素数量, 同样的, 不存在的元素会被忽略
SMOVE: 将元素从一个集和移动到另一个集和
PLAINTEXTSMOVE source target element
移动操作成功时返回1, 若不存在于源集和, 返回0.
如果 source 的元素不存在, 则返回0表示失败.
如果 target 的元素已存在, 则会覆盖该元素. 从结果来看, 并不会导致 target 中元素变化, 但是会导致 source 中的该元素消失.SMEMBERS: 获取集和包含的所有元素
PLAINTEXTSMEMBERS set
由于集和是无序的, 且 SMEMBERS 命令不会进行任何排序操作, 所以根据元素添加的顺序不同, 含相同元素的集和执行该命令结果可能不同.
SCARD: 获取集和包含的元素数量
PLAINTEXTSCARD set
SISMEMBER: 检查给定元素是否存在于集和
PLAINTEXTSISMEMBER set element
返回1表示给定的元素存在于集和中, 返回0表示不存在于集和中.
示例: 唯一计数器
例如, 一个网站想要统计浏览量和用户量
- 流览量可以使用是网页被用户访问的次数, 一个用户可以多次访问. 这种类型的数量使用字符串键或者散列键都可以实现
- 用户数量是访问网站的 IP 地址数量, 这时候就需要构建一个更加严格的计数器, 对每个 IP 地址进行一次次数, 这种计数器就是唯一计数器(unique counter)
from redis import Redis
class UniqueCounter:
def __init__(self, client, key):
self.client = client
self.key = key
def count_in(self, item):
return self.client.sadd(self.key, item)
def get_result(self):
return self.client.scard(self.key)
client = Redis(decode_responses=True)
counter = UniqueCounter(client, "ip counter")
print("Add ip", counter.count_in("8.8.8.8"))
print("Add ip", counter.count_in("9.9.9.9"))
print("Add ip", counter.count_in("10.10.10.10"))
print("Numbers of IP:", counter.get_result())
示例: 点赞
点赞功能可以使用集和来实现, 保证了每个用户对同一个内容只能点1次赞
from redis import Redis
class Like:
def __init__(self, client, key):
self.client = client
self.key = key
def cast(self, user):
"""执行点赞 True/False"""
return self.client.sadd(self.key, user)
def undo(self, user):
"""取消点赞"""
self.client.srem(self.key, user)
def is_liked(self, user):
"""是否已点赞"""
return self.client.sismember(self.key, user)
def get_all_liked_users(self):
"""所有点赞用户"""
return self.client.smembers(self.key)
def count(self):
"""点赞人数"""
return self.client.scard(self.key)
client = Redis(decode_responses=True)
like_topic = Like(client, "topic::10086::like")
print("Peter like:", like_topic.cast("peter"))
print("Mary like:", like_topic.cast("mary"))
print("Liked Users:", like_topic.get_all_liked_users())
print("How many likes:", like_topic.count())
print("Peter liked:", like_topic.is_liked("peter"))
print("Dan liked:", like_topic.is_liked("dan"))
示例: 投票
问答网站、文章推荐网、论坛这类注重内容质量的网站上通常会提供投票功能, 用户可以通过投票来支持一项内容或者反对一项内容:
- 支持票越多的文章, 会被网站安排到更显眼的位置, 使得网站的用户快速流览高质量内容.
- 反对票越多的文章, 则会被放到更不明显的位置, 甚至被当作广告隐藏起来, 使得用户可以忽略这些低质量内容.
例如 Stackoverflow 上面会对回答的答案进行投票, 帮助用户发现高质量的问题和答案.
from redis import Redis
def vote_up_key(vote_target):
"""赞成 vote_target 用户集和 key"""
return vote_target + "::vote_up"
def vote_down_key(vote_target):
"""反对 vote_target 用户集和 key"""
return vote_target + "::vote_down"
class Vote:
def __init__(self, client, vote_target):
self.client = client
self.vote_up_set = vote_up_key(vote_target)
self.vote_down_set = vote_down_key(vote_target)
def is_voted(self, user):
"""检查用户是否已投过票"""
return self.client.sismember(self.vote_up_set, user) or self.client.sismember(self.vote_down_set, user)
def vote_up(self, user):
"""user 投赞成票"""
if self.is_voted(user):
return False
self.client.sadd(self.vote_up_set, user)
return True
def vote_down(self, user):
"""user 投反对票"""
if self.is_voted(user):
return False
self.client.sadd(self.vote_down_set, user)
return True
def undo(self, user):
"""取消用户投票"""
self.client.srem(self.vote_up_set, user)
self.client.srem(self.vote_down_set, user)
def vote_up_count(self):
"""赞成票的数量"""
return self.client.scard(self.vote_up_set)
def get_all_vote_up_users(self):
"""所有投赞成票的用户"""
return self.client.smembers(self.vote_up_set)
def vote_down_count(self):
"""反对票的数量"""
return self.client.scard(self.vote_down_set)
def get_all_vote_down_users(self):
"""所有投反对票的用户"""
return self.client.smembers(self.vote_down_set)
client = Redis(decode_responses=True) # 是否将字节数据自动解码额日字符串
question_vote = Vote(client, "question::10")
print("Peter 投支持票:", question_vote.vote_up("peter"))
print("Jack 投支持票:", question_vote.vote_up("jack"))
print("Tom 投支持票:", question_vote.vote_up("tom"))
print("Mary 投反对票:", question_vote.vote_down("mary"))
print("支持票数量:", question_vote.vote_up_count())
print("反对票数量:", question_vote.vote_down_count())
print("支持票用户:", question_vote.get_all_vote_up_users())
print("反对票用户:", question_vote.get_all_vote_down_users())
# 取消用户投票(为了多次运行代码)
question_vote.undo("peter")
question_vote.undo("jack")
question_vote.undo("tom")
question_vote.undo("mary")
示例: 社交关系
Twitter 这类社交软件都可以通过关注或者加好友的方式, 构成一种社交关系. 这些网站上的用户都可以关注其他用户, 也可以被其他用户关注. 通过正在关注名单(following list), 用户可以查看自己正在关注的用户及其人数; 通过关注者名单(follower list), 用户可以查看有哪些人正在关注自己.
下面使用集和来维护这种关系:
- 程序为每个用户维护两个集和: 一个集和存储用户的正在关注名单, 另一个集和存储用户的关注者名单.
- 当 A 关注 B 的时候, 将 A 加入自己的 following list, 并加入 B 的follower list.
- 当 A 取消对 B 的关注的时候, 将 A 从自己的 following list 移出, 并将 A 从 B 的 follower list 移除.
def following_key(user):
return user + "::following"
def follower_key(user):
return user + "::follower"
class Relationship:
def __init__(self, client, user):
self.client = client
self.user = user
def follow(self, target):
"""关注目标用户"""
user_following_set = following_key(self.user)
self.client.sadd(user_following_set, target)
target_follower_set = follower_key(target)
self.client.sadd(target_follower_set, self.user)
def unfollow(self, target):
"""取消关注目标用户"""
user_following_set = following_key(self.user)
self.client.srem(user_following_set, target)
target_follower_set = follower_key(target)
self.client.srem(target_follower_set, self.user)
def is_following(self, target):
"""是否关注了目标用户"""
user_following_set = following_key(self.user)
return self.client.sismember(user_following_set, target)
def get_all_following(self):
"""所有user关注的用户"""
user_following_set = following_key(self.user)
return self.client.smembers(user_following_set)
def get_all_follower(self):
"""所有关注user的用户"""
user_follower_set = follower_key(self.user)
return self.client.smembers(user_follower_set)
def count_following(self):
"""user关注的用户数量"""
user_following_set = following_key(self.user)
return self.client.scard(user_following_set)
def count_follower(self):
"""关注user的用户数量"""
user_follower_set = follower_key(self.user)
return self.client.scard(user_follower_set)
SRANDMEMBER: 随机获取集和中的元素
PLAINTEXTSRANDMEMBER set [count]
该命令接受一个可选的 count 参数, 用于指定用户想要获取的元素数量. 默认只返回一个元素.
如果 count 为正数, 将返回 count 个不重复的元素. 当 count 值大于集的元素数量, 将返回集和所有元素.
如果 count 为负数, 则随机返回 abs(count) 个元素, 并且允许出现重复值.SPOP: 随机地从集和中移出指定数量的元素
PLAINTEXTSPOP key [count]
该命令会返回被移除的元素值作为命令的返回值.
count 参数不同于 SRANDMEMBER 命令的参数, 其值只能为正数
示例: 抽奖
为了推销产品并回馈消费者, 商家经常举办一些抽奖活动, 消费者可以抽奖获取礼品. 下面代码展示了使用集和实现的抽象程序, 这个成会把所有参与抽奖的玩家都添加到一个集和中, 然后通过 SRANDMEMBER 命令随机地选出获奖者.
class Lottery:
def __init__(self, client, key):
self.client = client
self.key = key
def add_player(self, user):
"""添加用户到抽奖名单中"""
self.client.sadd(self.key, user)
def get_all_players(self):
"""返回参加抽奖活动的所有用户"""
return self.client.smembers(self.key)
def player_count(self):
"""返回抽奖用户数量"""
return self.client.scard(self.key)
def draw(self, number):
"""抽取指定数量的获奖者"""
return self.client.srandmember(self.key, number)
考虑到完整的抽奖者名单可能会有用, 所以这个抽奖程序使用了随机获取元素的 SRANDMEMBER 命令, 而不是随机移除元素的 SPOP 命令. 如果不需要保留完整的名单, 也可以使用 SPOP 命令实现抽奖程序.
SINTER、SINTERSTORE: 对集和执行交集计算
PLAINTEXTSINTER set [set ...]
该命令计算用户给定的所有集和的交集, 返回交集的所有元素.
此外, 还有 SINTERSTORE 命令, 将集和的交集计算结果存储到指定的键里面.PLAINTEXTSINTERSTORE destination_key set [set ...]
如果给定的键已存在, 则 SINTERSTORE 命令结果会覆盖原来的集和键
SUNION、SUNIONSTORE: 对集和执行并集计算
PLAINTEXTSUNION set [set ...]
并集计算类似上面的交集计算
SDIFF、SDIFFSTORE: 对集和执行差集计算
PLAINTEXTSDIFF set [set ...]
SDIFF 命令会安装用户给定集和的顺序, 从左到右依次对给定的集和执行差集计算.
因为对集合执行交集、并集、差集等集合计算需要耗费大量的资源, 所以用户应该尽量使用SINTERSTORE等命令来存储并重用计算结果, 而不要每次都重复进行计算. 此外, 当集合计算涉及的元素数量非常大时, Redis服务器在进行计算时可能会被阻塞. 这时, 可以考虑使用Redis的复制功能, 通过从服务器来执行集合计算任务, 从而确保主服务器可以继续处理其他客户端发送的命令请求.
- 共同关注与推荐关注
前面使用集和实现了社交网站好友关系的存储, 即关注和被关注列表. 除此之外, 社交网站还通常会提供一些额外功能, 例如共同关注, 推荐关注等.
要实现共同关注功能, 程序需要计算出两个用户正在关注集和之间的交集.
推荐关注可以从用户关注集和中, 随机选出指定数量的用户作为种子用户, 然后对这些用户的正在管组集和执行并集计算, 最后从这个并集中随机选出一些推荐关注的对象.
示例: 使用反向索引构建商品筛选器
在访问购物类网站的时候, 通常可以通过一些标签来筛选产品. 这时候, 对每个产品可以建立一个集和, 对每个标签也都建立一个集和, 这样就得到了一份物品到关键字, 以及关键字到物品的映射关系.
def make_item_key(item):
return "InvertedIndex::" + item + "::keyword"
def make_keyword_key(keyword):
return "InvertedIndex::" + keyword + "::item"
class InvertedIndex:
def __init__(self, client):
self.client = client
def add_index(self, item, *keywords):
"""为物品添加关键字"""
# 将给定物品添加到
item_key = make_item_key(item)
result = self.client.sadd(item_key, *keywords)
# 遍历关键字集和, 将该物品添加进去
for keyword in keywords:
keyword_key = make_keyword_key(keyword)
self.client.sadd(keyword_key, item)
# 返回添加关键字数量作为结果
return result
def remove_index(self, item, *keywords):
"""移除物品的关键字"""
item_key = make_item_key(item)
result = self.client.srem(item_key, *keywords)
for keyword in keywords:
keyword_key = make_keyword_key(keyword)
self.client.srem(keyword_key, item)
return result
def get_keywords(self, item):
"""获取物品所有的关键字"""
return self.client.smembers(make_item_key(item))
def get_items(self, *keywords):
"""根据给定的关键字获取物品"""
# 根据给定的关键字计算出与之对应的集合 key
keyword_key_list = map(make_keyword_key, keywords)
# 将这些集和 key 做并集
return self.client.sinter(*keyword_key_list)