欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Python Redis应用场景

程序员文章站 2022-07-12 11:12:06
...

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