Python Redis应用场景

作者: 吾星喵 | 来源:发表于2018-08-07 15:29 被阅读15次

    Python Redis应用场景

    页面点击数

    《Redis Cookbook》对这个经典场景进行详细描述。假定我们对一系列页面需要记录点击次数。例如论坛的每个帖子都要记录点击次数,而点击次数比回帖的次数的多得多。如果使用关系数据库来存储点击,可能存在大量的行级锁争用。

    行级锁,一般是指排它锁,即被锁定行不可进行修改,删除,只可以被其他会话select。行级锁之前需要先加表结构共享锁。

    所以,点击数的增加使用redis的INCR命令最好不过了。

    当redis服务器启动时,可以从关系数据库读入点击数的初始值(id为66这个页面被访问了258次)

    >>> import redis
    >>> pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
    >>> r = redis.Redis(connection_pool=pool)
    >>> r.set('visit:66:totals', 258)
    True
    >>>
    >>> # 每当一个月面点击,则使用INCR增加点击数即可
    >>> r.incr('visit:66:totals')
    259
    >>> r.incr('visit:66:totals')
    260
    >>>
    >>>  # 页面载入的时候则可直接获取这个值
    >>> r.get('visit:66:totals')
    '260'
    

    关注人数

    >>> r.set('blog:5:follow', 3)
    True
    >>>  # 当有用户关注时,增加关注量
    >>> r.incr('blog:5:follow')
    4
    >>> r.incr('blog:5:follow')
    5
    >>> r.incr('blog:5:follow')
    6
    >>>  # 当有用户取消关注时,减少关注量
    >>> r.decr('blog:5:follow')
    5
    >>> r.incr('blog:5:follow')
    6
    >>> r.get('blog:5:follow')
    '6'
    

    社交圈子数据--集合

    在社交网站中,每一个圈子(circle)都有自己的用户群。通过圈子可以找到有共同特征(比如某一体育活动、游戏、电影等爱好者)的人。当一个用户加入一个或几个圈子后,系统可以向这个用户推荐圈子中的人。

    我们定义这样两个圈子,并加入一些圈子成员。

    >>> r.sadd('circle:game:lol', 'user:aaa', 'user:bbb')
    2
    >>> r.sadd('circle:game:lol', 'user:ccc')
    1
    >>>
    >>> r.sadd('circle:sport:run', 'user:aaa', 'user:leo')
    2
    >>>  # 获取某一圈子的所有成员
    >>> r.smembers('circle:game:lol')
    {'user:bbb', 'user:aaa', 'user:ccc'}
    >>>  # 交集,获取几个圈子的共同成员
    >>> r.sinter('circle:game:lol', 'circle:sport:run')
    {'user:aaa'}
    >>>  # 并集,获取几个圈子的所有不重复的成员
    >>> r.sunion('circle:game:lol', 'circle:sport:run')
    {'user:bbb', 'user:leo', 'user:aaa', 'user:ccc'}
    

    实时在线用户统计

    当我们需要在页面上显示当前的在线用户时,就可以使用Redis来完成了。首先获得当前时间(以Unix timestamps方式)除以60,可以基于这个值创建一个key。然后添加用户到这个集合中。当超过你设定的最大的超时时间,则将这个集合设为过期;而当需要查询当前在线用户的时候,则将最后N分钟的集合交集在一起即可。由于redis连接对象是线程安全的,所以可以直接使用一个全局变量来表示。

    import time
    from redis import Redis
    import datetime
    
    
    ONLINE_LAST_MINUTES = 5  # 标记在线结束时间
    redis = Redis(decode_responses=True)  # 改参数让redis存储不会转换为字节类型
    
    
    def mark_online(user_id):
        """
        将一个用户标记为online
        :param user_id:
        :return:
        """
        now_time = datetime.datetime.now()
        now_stamp = int(time.mktime(now_time.timetuple()))  # 当前时间的时间戳
        last_time = now_time + datetime.timedelta(minutes=ONLINE_LAST_MINUTES)
        last_stamp = int(time.mktime(last_time.timetuple()))  # 过期时间的时间戳
    
        now = int(time.time())
        expires = now + ONLINE_LAST_MINUTES * 60 + 10  # 过期的Unix时间戳
        all_user_key = 'online-users/{}'.format(now // 60)  # 集合名,包含分钟信息
        user_key = 'user-activity/%s' % user_id
    
        p = redis.pipeline()
        p.sadd(all_user_key, user_id)  # 将用户id插入包含分钟信息的集合中
        p.set(user_key, now)  # 记录用户的标记时间
        p.expireat(all_user_key, expires)  # 设定集合的过期时间为Unix的时间戳
        p.expireat(user_key, expires)
        p.execute()
    
    
    def get_user_last_activity(user_id):
        """
        获取用户的最后活跃时间
        :param user_id:
        :return:
        """
        last_active = redis.get('user-activity/{}'.format(user_id))  # 如果获取不到,则返回None
        if last_active is None:
            return None
        # return datetime.datetime.utcfromtimestamp(int(last_active))  # 返回utc时间,比实际时间晚8小时
        return datetime.datetime.fromtimestamp(int(last_active))  # 返回实际时间
    
    
    def get_online_users():
        """
        获取当前online用户的列表
        :return:
        """
        current = int(time.time()) // 60
        minutes = range(ONLINE_LAST_MINUTES)
        return redis.sunion(['online-users/{}'.format(current - x) for x in minutes])
    
    
    if __name__ == '__main__':
        mark_online(user_id=1)
        mark_online(user_id=5)
        mark_online(user_id=29)
        mark_online(user_id=18)
        mark_online(user_id=99)
        time.sleep(5)
        mark_online(user_id=1)
        print('user1最后活跃时间:', get_user_last_activity(user_id=1))
        print('所有在线用户:', get_online_users())
    

    得到结果

    user1最后活跃时间: 2018-08-07 11:53:09
    所有在线用户: {'5', '18', '1', '99', '29'}
    

    实时用户登录状态统计

    Redis的位图提供了二进制操作,非常适合存储布尔类型的值,常见场景就是记录用户登陆状态。

    该场景用二进制的方式表示用户是否登录,比如说有10个用户,则0000000000表示无人登录,0010010001表示第3个、第6个、第10个用户登录过,即是活跃的。

    用到Redis字符串(String)结构中的:BITCOUNTGETBITBITOP命令

    对本月每天的用户登录情况进行统计,会针对每天生成key,例如今天的:account:active:2018:08:07,也会生成月的key:account:active:2018:08和年的key:key:account:active:2018

    每个key中的字符串长度就是人数(可能有的key的str没有那么长,那是因为最后一个bit没有set成1,不过没有就相当于是0)

    import redis
    import random
    from datetime import datetime
    import time
    
    
    r = redis.StrictRedis(host='localhost', port=6379, db=0)
    ACCOUNT_ACTIVE_KEY = 'account:active'
    
    # r.flushall()  # 为不影响测试,清空数据库,或者根据名字进行删除
    print(r.keys(ACCOUNT_ACTIVE_KEY + '*'))  # 找到以ACCOUNT_ACTIVE_KEY开头的名字
    # [b'account:active:2018', b'account:active:2018:8:7', b'account:active:2018:8']
    r.delete(*(r.keys(ACCOUNT_ACTIVE_KEY + '*')))  # 进行删除
    print(r.keys(ACCOUNT_ACTIVE_KEY + '*'))
    # []
    
    # now = datetime.utcnow()
    now = datetime.now()
    
    
    def record_active(account_id, t=None):
        """
        第一次t自己生成,后面t接收传入的年月日
        :param account_id: 用户id,在redis二进制位表示索引值(索引从0开始的)
        :param t: 传入的时间对象
        :return:
        """
        if not t:
            t = now
    
        # Redis事务开始
        p = r.pipeline()
        key = ACCOUNT_ACTIVE_KEY
        # 组合了年月日三种键值,同时将三个键值对应字符串的account_id位置为1
        # 符合逻辑:该人在这一天登陆,肯定也在当前月登陆,也在当年登陆
    
        # for arg in ('year', 'month', 'day'):
        #     key = '{}:{}'.format(key, getattr(t, arg))  # getattr() 函数用于返回一个对象属性值。datetime.now().year->2018、datetime.now().day->7
        #     p.setbit(key, account_id, 1)  # 设置二进制的值,name, offset, value分列式redis的name,位的索引,值为1或0
        # 以上方法修正
        key_year = '{}:{}'.format(key, t.year)  # account:active:2018
        p.setbit(key_year, account_id, 1)
        key_year_month = '{}:{}:{}'.format(key, t.year, t.month)  # account:active:2018:8
        p.setbit(key_year_month, account_id, 1)
        key_year_month_day = '{}:{}:{}:{}'.format(key, t.year, t.month, t.day)  # account:active:2018:8:7
        p.setbit(key_year_month_day, account_id, 1)
    
        # Redis事务提交,真正执行
        p.execute()
    
    
    def gen_records(max_days, population, k):
        """
        循环每天的情况,从1---max_days天
        :param max_days:
        :param population: 根据给的人数,随机生成用户id
        :param k: 随机数量,也就是从 0---population人中,随机选择k个人表示当天登陆过,然后将redis名为 account:active:日期 的值对应的id位更改为1
        :return:
        """
        for day in range(1, max_days):
            time_ = datetime(now.year, now.month, now.day)
            # 每天随机生成k个数字,表示k个人活跃
            accounts = random.sample(range(population), k)
            # 将这k个人对应在当天的字符串中修改,对应位置的bit置为1,表明这个天他有登陆过
            for account_id in accounts:
                record_active(account_id, time_)
    
    
    def calc_memory():
        """
        查看记录100万数据中随机选择10万活跃用户时的内存占用
        :return:
        """
        r.flushall()
        print('执行前的内存占用:{}'.format(r.info()['used_memory_human']))
    
        start = time.time()
        # 100万中选择10万,20天
        gen_records(21, 1000000, 100000)
        print('花费时间:', time.time()-start)
    
        print('执行后的内存占用:{}'.format(r.info()['used_memory_human']))
    
    
    gen_records(29, 10000, 2000)
    # 这个月总的活跃用户数,直接查询记录月的key:bitcount "account:active:2018:8"
    print(r.bitcount('{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month)))
    # 今天的活跃用户数:bitcount "account:active:2018:8:7"
    print(r.bitcount('{}:{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month, now.day)))
    
    # 随机找一个account_id为1200的用户,查看他是否登陆过:getbit "account:active:2018:8" 1200
    account_id = 1200
    print(r.getbit('{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month), account_id))  # 如果结果为1,则表明该用户当月登陆过,如果为0,表明未登录过
    
    # 获取当月1号和2号的建
    keys = ['{}:{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month, day) for day in range(1, 3)]
    print(keys)  # ['account:active:2018:8:1', 'account:active:2018:8:2']
    
    # 获取1号和2号的活跃的用户总数---做OR位运算
    r.bitop('or', 'destkey:or', *keys)
    print(r.bitcount('destkey:or'))
    
    # 获取在1号和2号都活跃的用户数---做AND位运算
    r.bitop('and', 'destkey:and', *keys)
    print(r.bitcount('destkey:and'))
    

    排行榜

    该场景用于游戏或者需要分数排名的地方,主要利用Redis的有序集合(SortedSet)其中:score值递减(从大到小)的次序排列。

    用到Redis有序集合的:ZADDZREVRANGEZCOUNTZREVRANGEBYSCORE命令

    import redis
    import random
    import string
    
    
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    SS_KEY = 'student.score'
    
    
    for _ in range(1000):
        """
        随机生成1000个用户,每个用户具有得分和用户名字,插入Redis的有序集合中
        """
        score = round((random.random() * 100), 2)  # 随机从0--1中取一个数*100,最终保留两位小数,用户数据初始化
        user_id = ''.join(random.sample(string.ascii_letters, 6))  # 取6位英文字母随机组成用户名字
        r.zadd(SS_KEY, user_id, score)
    
    
    user_id, score = r.zrevrange(SS_KEY, 0, -1, withscores=True)[random.randint(0, 1000)]
    
    print('随机获取的用户分数为:', user_id, score)
    
    # 获取分为在0--100的个数,也就是所有人的数量
    student_count = r.zcount(SS_KEY, 0, 100)
    print('所有人的数量:', student_count)
    
    # 获取分数在0--score段的人数,也就是这个用户分数超了多少人
    current_count = r.zcount(SS_KEY, 0, score)
    print('这个人的分数超过人数:', current_count - 1)  # 除去分数为score的本身
    
    # 显示分数前10名
    print('显示分数前10名')
    print('{}: {}'.format('学生', '分数'))
    for user_id, score in r.zrevrangebyscore(SS_KEY, 100, 0, start=0, num=10, withscores=True):
        print('{}: {}'.format(user_id, score))
    
    # 删除排行字段
    r.delete(SS_KEY)
    

    结果

    随机获取的用户分数为: XiJnaK 59.53
    所有人的数量: 1000
    这个人的分数超过人数: 579
    显示分数前10名
    学生: 分数
    FbICcf: 99.96
    ZmSkso: 99.72
    xTJrkY: 99.67
    gwhVcU: 99.52
    qWKyLr: 99.5
    kWlQab: 99.37
    KAjioC: 99.37
    pMXcuv: 99.36
    ALEMoI: 99.32
    pIikgb: 98.59
    

    相关文章

      网友评论

        本文标题:Python Redis应用场景

        本文链接:https://www.haomeiwen.com/subject/sqxsyftx.html