Redis 的集和 set 键允许用户将任意多个不同的元素存储到集和中, 既可以是文本数据, 也可以是二进制数据. 其与列表有以下两个明显的区别:

  • 列表可以存储重复元素, 而集和只存储非重复元素
  • 列表以有序方式存储元素, 而集和则以无序方式存储元素

下面介绍结合键的各个命令

Set 集和

  • SADD: 将元素添加到集和

    SADD set element [element ...]
    

    返回成功添加的新元素数量作为返回值, 由于集和不存储相同元素, 所以会自动忽略重复的元素

  • SREM: 从集和中移出元素

    SREM set element [element ...]
    

    返回被移除的元素数量, 同样的, 不存在的元素会被忽略

  • SMOVE: 将元素从一个集和移动到另一个集和

    SMOVE source target element
    

    移动操作成功时返回1, 若不存在于源集和, 返回0.
    如果 source 的元素不存在, 则返回0表示失败.
    如果 target 的元素已存在, 则会覆盖该元素. 从结果来看, 并不会导致 target 中元素变化, 但是会导致 source 中的该元素消失.

  • SMEMBERS: 获取集和包含的所有元素

    SMEMBERS set
    

    由于集和是无序的, 且 SMEMBERS 命令不会进行任何排序操作, 所以根据元素添加的顺序不同, 含相同元素的集和执行该命令结果可能不同.

  • SCARD: 获取集和包含的元素数量

    SCARD set
    
  • SISMEMBER: 检查给定元素是否存在于集和

    SISMEMBER 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: 随机获取集和中的元素

    SRANDMEMBER set [count]
    

    该命令接受一个可选的 count 参数, 用于指定用户想要获取的元素数量. 默认只返回一个元素.
    如果 count 为正数, 将返回 count 个不重复的元素. 当 count 值大于集的元素数量, 将返回集和所有元素.
    如果 count 为负数, 则随机返回 abs(count) 个元素, 并且允许出现重复值.

  • SPOP: 随机地从集和中移出指定数量的元素

    SPOP 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: 对集和执行交集计算

    SINTER set [set ...]
    

    该命令计算用户给定的所有集和的交集, 返回交集的所有元素.
    此外, 还有 SINTERSTORE 命令, 将集和的交集计算结果存储到指定的键里面.

    SINTERSTORE destination_key set [set ...]
    

    如果给定的键已存在, 则 SINTERSTORE 命令结果会覆盖原来的集和键

  • SUNION、SUNIONSTORE: 对集和执行并集计算

    SUNION set [set ...]
    

    并集计算类似上面的交集计算

  • SDIFF、SDIFFSTORE: 对集和执行差集计算

    SDIFF 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)