SISMEMBER:检查给定元素是否存在于集合

通过使用 SISMEMBER 命令,用户可以检查给定的元素是否存在于集合当中:

SISMEMBER set element

SISMEMBER 命令返回 1 表示给定的元素存在于集合当中;返回 0 则表示给定元素不存在于集合当中。

举个例子,对于以下这个 databases 集合来说:

redis> SMEMBERS databases
1) "Redis"
2) "MySQL"
3) "MongoDB"
4) "PostgreSQL"

使用 SISMEMBER 命令去检测已经存在于集合中的 "Redis" 元素、"MongoDB"元素以及 "MySQL" 元素都将得到肯定的回答:

redis> SISMEMBER databases "Redis"
(integer) 1
redis> SISMEMBER databases "MongoDB"
(integer) 1
redis> SISMEMBER databases "MySQL"
(integer) 1

而使用 SISMEMBER 命令去检测不存在于集合当中的 "Oracle" 元素、"Neo4j"元素以及 "Memcached" 元素则会得到否定的回答:

redis> SISMEMBER databases "Oracle"
(integer) 0
redis> SISMEMBER databases "Neo4j"
(integer) 0
redis> SISMEMBER databases "Memcached"
(integer) 0

其他信息

  • 复杂度:O(1)。

  • 版本要求:SISMEMBER 命令从 Redis 1.0.0 版本开始可用。

示例:唯一计数器

在前面对字符串键以及散列键进行介绍的时候,曾经展示过如何使用这两种键去实现计数器程序。我们当时实现的计数器都非常简单:每当某个动作被执行时,程序就可以调用计数器的加法操作或者减法操作,对动作的执行次数进行记录。

以上这种简单的计数行为在大部分情况下都是有用的,但是在某些情况下,我们需要一种要求更为严格的计数器,这种计数器只会对特定的动作或者对象进行一次计数而不是多次计数。

举个例子,一个网站的受欢迎程度通常可以用浏览量和用户数量这两个指标进行描述:

  • 浏览量记录的是网站页面被用户访问的总次数,网站的每个用户都可以重复地对同一个页面进行多次访问,而这些访问会被浏览量计数器一个不漏地记下来。

  • 用户数量记录的是访问网站的 IP 地址数量,即使同一个 IP 地址多次访问相同的页面,用户数量计数器也只会对这个IP地址进行一次计数。

对于网站的浏览量,我们可以继续使用字符串键或者散列键实现的计数器进行计数,但如果我们想要记录网站的用户数量,就需要构建一个新的计数器,这个计数器对于每个特定的 IP 地址只会进行一次计数,我们把这种对每个对象只进行一次计数的计数器称为唯一计数器(unique counter)。

代码清单5-1展示了一个使用集合实现的唯一计数器,这个计数器通过把被计数的对象添加到集合来保证每个对象只会被计数一次,然后通过获取集合的大小来判断计数器目前总共对多少个对象进行了计数。

代码清单5-1 使用集合实现唯一计数器:/set/unique_counter.py
class UniqueCounter:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def count_in(self, item):
        """
        尝试将给定元素计入到计数器当中:
        如果给定元素之前没有被计数过,那么方法返回 True 表示此次计数有效;
        如果给定元素之前已经被计数过,那么方法返回 False 表示此次计数无效。
        """
        return self.client.sadd(self.key, item) == 1

    def get_result(self):
        """
        返回计数器的值。
        """
        return self.client.scard(self.key)

以下代码展示了如何使用唯一计数器去计算网站的用户数量:

>>> from redis import Redis
>>> from unique_counter import UniqueCounter
>>> client = Redis(decode_responses=True)
>>> counter = UniqueCounter(client, 'ip counter')
>>> counter.count_in('8.8.8.8') # 将一些IP地址添加到计数器当中
True
>>> counter.count_in('9.9.9.9')
True
>>> counter.count_in('10.10.10.10')
True
>>> counter.get_result() # 获取计数结果
3
>>> counter.count_in('8.8.8.8') # 添加一个已存在的IP地址
False
>>> counter.get_result() # 计数结果没有发生变化
3

示例:打标签

为了对网站上的内容进行分类标识,很多网站都提供了打标签(tagging)功能。比如论坛可能会允许用户为帖子添加标签,这些标签既可以对帖子进行归类,又可以让其他用户快速地了解到帖子要讲述的内容。再比如,一个图书分类网站可能会允许用户为自己收藏的每一本书添加标签,使得用户可以快速地找到被添加了某个标签的所有图书,并且网站还可以根据用户的这些标签进行数据分析,从而帮助用户找到他们可能感兴趣的图书,除此之外,购物网站也可以为自己的商品加上标签,比如“新上架”、“热销中”、“原装进口” 等,方便顾客了解每件商品的不同特点和属性。类似的例子还有很多。

代码清单5-2展示了一个使用集合实现的打标签程序,通过这个程序,我们可以为不同的对象添加任意多个标签:同一个对象的所有标签都会被放到同一个集合里面,集合里的每一个元素就是一个标签。

代码清单5-2 使用集合实现的打标签程序:/set/tagging.py

def make_tag_key(item):
    return item + "::tags"

class Tagging:

    def __init__(self, client, item):
        self.client = client
        self.key = make_tag_key(item)

    def add(self, *tags):
        """
        为对象添加一个或多个标签。
        """
        self.client.sadd(self.key, *tags)

    def remove(self, *tags):
        """
        移除对象的一个或多个标签。
        """
        self.client.srem(self.key, *tags)

    def is_included(self, tag):
        """
        检查对象是否带有给定的标签,
        是的话返回 True ,不是的话返回 False 。
        """
        return self.client.sismember(self.key, tag)

    def get_all_tags(self):
        """
        返回对象带有的所有标签。
        """
        return self.client.smembers(self.key)

    def count(self):
        """
        返回对象带有的标签数量。
        """
        return self.client.scard(self.key)

以下代码展示了如何使用这个打标签程序为《The C Programming Language》一书添加标签:

>>> from redis import Redis
>>> from tagging import Tagging
>>> client = Redis(decode_responses=True)
>>> book_tags = Tagging(client, "The C Programming Language")
>>> book_tags.add('c') # 添加标签
>>> book_tags.add('programming')
>>> book_tags.add('programming language')
>>> book_tags.get_all_tags() # 查看所有标签
set(['c', 'programming', 'programming language'])
>>> book_tags.count() # 查看标签的数量
3

作为例子,图5-7 展示了一些使用打标签程序创建出的集合数据结构。

image 2025 01 03 18 01 08 512
Figure 1. 图5-7 使用打标签程序创建出的集合

示例:点赞

为了让用户表达自己对某一项内容的喜欢和赞赏之情,很多网站都提供了点赞(like)功能,通过这一功能,用户可以给自己喜欢的内容点赞,也可以查看给相同内容点赞的其他用户,还可以查看给相同内容点赞的用户数量,诸如此类。

除了点赞之外,很多网站还有诸如 “+1”、“顶”、“喜欢” 等功能,这些功能的名字虽然各有不同,但它们在本质上和点赞功能是一样的。

代码清单5-3展示了一个使用集合实现的点赞程序,这个程序使用集合来存储对内容进行了点赞的用户,从而确保每个用户只能对同一内容点赞一次,并通过使用不同的集合命令来实现查看点赞数量、查看所有点赞用户以及取消点赞等功能。

代码清单5-3 使用集合实现的点赞程序:/set/like.py
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) == 1

    def undo(self, user):
        """
        取消用户的点赞。
        """
        self.client.srem(self.key, user)

    def is_liked(self, user):
        """
        检查用户是否已经点过赞。
        是的话返回 True ,否则的话返回 False 。
        """
        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)

以下代码展示了如何使用点赞程序去记录一篇帖子的点赞信息:

>>> from redis import Redis
>>> from like import Like
>>> client = Redis(decode_responses=True)
>>> like_topic = Like(client, 'topic::10086::like')
>>> like_topic.cast('peter') # 用户对帖子进行点赞
True
>>> like_topic.cast('john')
True
>>> like_topic.cast('mary')
True
>>> like_topic.get_all_liked_users() # 获取所有为帖子点过赞的用户
set(['john', 'peter', 'mary'])
>>> like_topic.count() # 获取为帖子点过赞的用户数量
3
>>> like_topic.is_liked('peter') # peter为帖子点过赞了
True
>>> like_topic.is_liked('dan') # dan还没有为帖子点过赞
False

示例:投票

问答网站、文章推荐网站、论坛这类注重内容质量的网站上通常都会提供投票功能,用户可以通过投票来支持一项内容或者反对一项内容:

  • 一项内容获得的支持票数越多,就会被网站安排到越明显的位置,使得网站的用户可以更快速地浏览到高质量的内容。

  • 与此相反,一项内容获得的反对票数越多,它就会被网站安排到越不明显的位置,甚至被当作广告或者无用内容隐藏起来,使得用户可以忽略这些低质量的内容。

根据网站性质的不同,不同的网站可能会为投票功能设置不同的名称,比如有些网站可能会把 “支持” 和 “反对” 叫作 “推荐” 和 “不推荐”,而有些网站可能会使用 “喜欢” 和 “不喜欢” 来表示 “支持” 和 “反对”,诸如此类,但这些网站的投票功能在本质上都是一样的。

作为示例,图5-8展示了 StackOverflow 问答网站的一个截图,这个网站允许用户对问题及其答案进行投票,从而帮助用户发现高质量的问题和答案。

image 2025 01 03 18 04 43 464
Figure 2. 图5-8 StackOverflow网站的投票示例,图中所示的问题获得了10个推荐

代码清单5-4 展示了一个使用集合实现的投票程序:对于每一项需要投票的内容,这个程序都会使用两个集合来分别存储投支持票的用户以及投反对票的用户,然后通过对这两个集合执行命令来实现投票、取消投票、统计投票数量、获取已投票用户名单等功能。

代码清单5-4 使用集合实现的投票程序,用户可以选择支持或者反对一项内容:/set/vote.py
def vote_up_key(vote_target):
    return vote_target + "::vote_up"

def vote_down_key(vote_target):
    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):
        """
        检查用户是否已经投过票(可以是赞成票也可以是反对票),
        是的话返回 True ,否则返回 False 。
        """
        return self.client.sismember(self.vote_up_set, user) or \
               self.client.sismember(self.vote_down_set, user)

    def vote_up(self, user):
        """
        让用户投赞成票,并在投票成功时返回 True ;
        如果用户已经投过票,那么返回 False 表示此次投票无效。
        """
        if self.is_voted(user):
            return False

        self.client.sadd(self.vote_up_set, user)
        return True

    def vote_down(self, user):
        """
        让用户投反对票,并在投票成功时返回 True ;
        如果用户已经投过票,那么返回 False 表示此次投票无效。
        """
        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)

以下代码展示了如何使用这个投票程序去记录一个问题的投票信息:

>>> from redis import Redis
>>> from vote import Vote
>>> client = Redis(decode_responses=True)
>>> question_vote = Vote(client, 'question::10086') # 记录问题的投票信息
>>> question_vote.vote_up('peter') # 投支持票
True
>>> question_vote.vote_up('jack')
True
>>> question_vote.vote_up('tom')
True
>>> question_vote.vote_down('mary') # 投反对票
True
>>> question_vote.vote_up_count() # 统计支持票数量
3
>>> question_vote.vote_down_count() # 统计反对票数量
1
>>> question_vote.get_all_vote_up_users() # 获取所有投支持票的用户
{'jack', 'peter', 'tom'}
>>> question_vote.get_all_vote_down_users() # 获取所有投反对票的用户
{'mary'}

图5-9展示了这段代码创建出的两个集合,以及这两个集合包含的元素。

image 2025 01 03 18 10 19 061
Figure 3. 图5-9 投票程序创建出的两个集合

示例:社交关系

微博、Twitter 以及类似的社交网站都允许用户通过加关注或者加好友的方式,构建一种社交关系。这些网站上的每个用户都可以关注其他用户,也可以被其他用户关注。通过正在关注名单(following list),用户可以查看自己正在关注的用户及其人数;通过关注者名单(follower list),用户可以查看有哪些人正在关注自己,以及有多少人正在关注自己。

代码清单5-5展示了一个使用集合来记录社交关系的方法:

  • 程序为每个用户维护两个集合,一个集合存储用户的正在关注名单,而另一个集合则存储用户的关注者名单。

  • 当一个用户(关注者)关注另一个用户(被关注者)的时候,程序会将被关注者添加到关注者的正在关注名单中,并将关注者添加到被关注者的关注者名单里面。

  • 当关注者取消对被关注者的关注时,程序会将被关注者从关注者的正在关注名单中移除,并将关注者从被关注者的关注者名单中移除。

代码清单5-5 使用集合实现社交关系:/set/relationship.py
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):
        """
        关注目标用户。
        """
        # 把 target 添加到当前用户的正在关注集合里面
        user_following_set = following_key(self.user)
        self.client.sadd(user_following_set, target)
        # 把当前用户添加到 target 的关注者集合里面
        target_follower_set = follower_key(target)
        self.client.sadd(target_follower_set, self.user)

    def unfollow(self, target):
        """
        取消对目标用户的关注。
        """
        # 从当前用户的正在关注集合中移除 target
        user_following_set = following_key(self.user)
        self.client.srem(user_following_set, target)
        # 从 target 的关注者集合中移除当前用户
        target_follower_set = follower_key(target)
        self.client.srem(target_follower_set, self.user)

    def is_following(self, target):
        """
        检查当前用户是否正在关注目标用户,
        是的话返回 True ,否则返回 False 。
        """
        # 如果 target 存在于当前用户的正在关注集合中
        # 那么说明当前用户正在关注 target
        user_following_set = following_key(self.user)
        return self.client.sismember(user_following_set, target)

    def get_all_following(self):
        """
        返回当前用户正在关注的所有人。
        """
        user_following_set = following_key(self.user)
        return self.client.smembers(user_following_set)

    def get_all_follower(self):
        """
        返回当前用户的所有关注者。
        """
        user_follower_set = follower_key(self.user)
        return self.client.smembers(user_follower_set)

    def count_following(self):
        """
        返回当前用户正在关注的人数。
        """
        user_following_set = following_key(self.user)
        return self.client.scard(user_following_set)

    def count_follower(self):
        """
        返回当前用户的关注者人数。
        """
        user_follower_set = follower_key(self.user)
        return self.client.scard(user_follower_set)

以下代码展示了社交关系程序的基本使用方法:

>>> from redis import Redis
>>> from relationship import Relationship
>>> client = Redis(decode_responses=True)
>>> peter = Relationship(client, 'peter') # 这个对象记录的是peter的社交关系
>>> peter.follow('jack') # 关注一些人
>>> peter.follow('tom')
>>> peter.follow('mary')
>>> peter.get_all_following() # 获取目前正在关注的所有人
set(['mary', 'jack', 'tom'])
>>> peter.count_following() # 统计目前正在关注的人数
3
>>> jack = Relationship(client, 'jack') # 这个对象记录的是jack的社交关系
>>> jack.get_all_follower() # peter前面关注了jack,所以他是jack的关注者
set(['peter'])
>>> jack.count_follower() # jack目前只有一个关注者
1

图5-10展示了以上代码创建的各个集合。

image 2025 01 03 18 12 38 979
Figure 4. 图5-10 社交关系集合示例