Python Redis应用场景
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)结构中的:BITCOUNT
,GETBIT
,BITOP
命令
对本月每天的用户登录情况进行统计,会针对每天生成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有序集合的:ZADD
,ZREVRANGE
,ZCOUNT
,ZREVRANGEBYSCORE
命令
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
上一篇: 常用线程池及适用场景
下一篇: 路由Routers