Redis实战5 - 构建简单的社交网站
构建简单的社交网站
用户和状态
用户信息
使用hash存储
def create_user(conn, login, name):
llogin = login.lower()
# 锁住小写用户名,防止多用户同时申请一个名字
lock = acquire_lock_with_timeout(conn, 'user:' + llogin, 1) #A
if not lock: #B
return None #B
# users: 哈希结构用户存储用户名和用户ID间映射,已存在则不能分配
if conn.hget('users:', llogin): #C
release_lock(conn, 'user:' + llogin, lock) #C
return None #C
# 通过计数器生成独一无二ID
id = conn.incr('user:id:') #D
pipeline = conn.pipeline(True)
pipeline.hset('users:', llogin, id) # 将小写用户名映射到ID
pipeline.hmset('user:%s'%id, { #F
'login': login, #F
'id': id, #F
'name': name, #F
'followers': 0, #F
'following': 0, #F
'posts': 0, #F
'signup': time.time(), #F
})
pipeline.execute()
release_lock(conn, 'user:' + llogin, lock) # 释放之前的锁
return id #H
状态消息
将用户所说的话记录到状态消息里面,也用hash。
def create_status(conn, uid, message, **data):
pipeline = conn.pipeline(True)
pipeline.hget('user:%s'%uid, 'login') # 根据用户ID获取用户名
pipeline.incr('status:id:') # 为消息创建唯一id
login, id = pipeline.execute()
if not login: # 发布消息前验证账号是否存在
return None #C
data.update({
'message': message, #D
'posted': time.time(), #D
'id': id, #D
'uid': uid, #D
'login': login, #D
})
pipeline.hmset('status:%s'%id, data) #D
pipeline.hincrby('user:%s'%uid, 'posts')# 更新用户已发送状态消息数量
pipeline.execute()
return id # 返回新创建的状态消息ID
主页时间线
用户以及用户正在关注的人所发布的状态消息组成。使用zset,成员为消息ID,分值为发布时间戳。
# 三个可选参数:哪条时间线、获取多少页、每页多少条状态信息
def get_status_messages(conn, uid, timeline='home:', page=1, count=30):#A
# 获取时间线上最小状态消息ID
statuses = conn.zrevrange( #B
'%s%s'%(timeline, uid), (page-1)*count, page*count-1) #B
pipeline = conn.pipeline(True)
# 获取状态消息本身
for id in statuses: #C
pipeline.hgetall('status:%s'%id) #C
# 使用过滤器移除那些被删除了的状态消息
return filter(None, pipeline.execute()) #D
另一个重要时间线是个人时间线,区别是只展示自己发布的,只要将timeline参数设置为profile:
关注者列表和正在关注列表
用zset,成员为yonghuID,分值为关注的时间戳。
关注和取消关注时,修改关注hash、被关注hash、用户信息hash中关注数被关注数、个人时间线。
HOME_TIMELINE_SIZE = 1000
def follow_user(conn, uid, other_uid): # uid 关注 other_uid
fkey1 = 'following:%s'%uid #A
fkey2 = 'followers:%s'%other_uid #A
if conn.zscore(fkey1, other_uid): #B
return None #B
now = time.time()
pipeline = conn.pipeline(True)
pipeline.zadd(fkey1, other_uid, now) #C
pipeline.zadd(fkey2, uid, now) #C
pipeline.zrevrange('profile:%s'%other_uid, 0, HOME_TIMELINE_SIZE-1, withscores=True) # 从被关注者的个人时间线里去最新状态消息
following, followers, status_and_score = pipeline.execute()[-3:]
# 更新他们信息hash
pipeline.hincrby('user:%s'%uid, 'following', int(following)) #F
pipeline.hincrby('user:%s'%other_uid, 'followers', int(followers)) #F
# 更新用户主页时间线
if status_and_score:
pipeline.zadd('home:%s'%uid, **dict(status_and_score)) #G
pipeline.zremrangebyrank('home:%s'%uid, 0, -HOME_TIMELINE_SIZE-1)#G
pipeline.execute()
return True #H
def unfollow_user(conn, uid, other_uid):
fkey1 = 'following:%s'%uid #A
fkey2 = 'followers:%s'%other_uid #A
if not conn.zscore(fkey1, other_uid): #B
return None #B
# 从正在关注和被关注中移除双方ID
pipeline = conn.pipeline(True)
pipeline.zrem(fkey1, other_uid) #C
pipeline.zrem(fkey2, uid) #C
# 获取被取消关注用户最近发布状态消息
pipeline.zrevrange('profile:%s'%other_uid, #E
0, HOME_TIMELINE_SIZE-1) #E
following, followers, statuses = pipeline.execute()[-3:]
pipeline.hincrby('user:%s'%uid, 'following', -int(following)) #F
pipeline.hincrby('user:%s'%other_uid, 'followers', -int(followers)) #F
if statuses:
# 移除用户主页时间线中相应消息
pipeline.zrem('home:%s'%uid, *statuses) #G
pipeline.execute()
return True #H
# 用户取消关注后,主页时间线上消息减少,此函数将其填满
def refill_timeline(conn, incoming, timeline, start=0):
if not start and conn.zcard(timeline) >= 750: #A
return #A
users = conn.zrangebyscore(incoming, start, 'inf', #B
start=0, num=REFILL_USERS_STEP, withscores=True) #B
pipeline = conn.pipeline(False)
for uid, start in users:
pipeline.zrevrange('profile:%s'%uid, #C
0, HOME_TIMELINE_SIZE-1, withscores=True) #C
messages = []
for results in pipeline.execute():
messages.extend(results) #D
messages.sort(key=lambda x:-x[1]) #E
del messages[HOME_TIMELINE_SIZE:] #E
pipeline = conn.pipeline(True)
if messages:
pipeline.zadd(timeline, **dict(messages)) #F
pipeline.zremrangebyrank( #G
timeline, 0, -HOME_TIMELINE_SIZE-1) #G
pipeline.execute()
if len(users) >= REFILL_USERS_STEP:
execute_later(conn, 'default', 'refill_timeline', #H
[conn, incoming, timeline, start]) #H
状态消息的发布与删除
消息发布后要更新要添加到用户个人时间线和关注者主页时间线。当关注者非常多时,更新所有关注者主页会很慢。为了让发布操作可以尽快地返回,程序需要做两件事情。首先,在发布状态消息的时候,程序会将状态消息的ID添加到前1000个关注者的主页时间线里面。根据Twitter的一项统计表明,关注者数量在1000人以上的用户只有10万~25万,而这10万~25万用户只占了活跃用户数量的0.1%,这意味着99.9%的消息发布人在这一阶段就可以完成自己的发布操作,而剩下的0.1%则需要接着执行下一个步骤。其次,对于那些关注者数量超过1000人的用户来说,程序会使用类似于6.4节中介绍的系统来开始一项延迟任务。代码清单8-6展示了程序是如何将状态更新推送给各个关注者的。
def post_status(conn, uid, message, **data):
id = create_status(conn, uid, message, **data) # 创建新的状态消息
if not id: #B
return None #B
posted = conn.hget('status:%s'%id, 'posted') # 获取发布时间
if not posted: #D
return None #D
post = {str(id): float(posted)}
conn.zadd('profile:%s'%uid, **post) # 添加到个人时间线
syndicate_status(conn, uid, post) # 推送给关注者
return id
POSTS_PER_PASS = 1000 # 每次最多发给1000给关注者
def syndicate_status(conn, uid, post, start=0):
# 获取上传被更新的最后一个关注者为启动,获取接下来1000个关注者
followers = conn.zrangebyscore('followers:%s'%uid, start, 'inf',
start=0, num=POSTS_PER_PASS, withscores=True) #B
pipeline = conn.pipeline(False)
for follower, start in followers: # 遍历时更新start,可用于下一次syndicate_status()调用
# 修改主页时间线
pipeline.zadd('home:%s'%follower, **post) #C
pipeline.zremrangebyrank( #C
'home:%s'%follower, 0, -HOME_TIMELINE_SIZE-1)#C
pipeline.execute()
# 超过1000人在延迟任务中继续操作
if len(followers) >= POSTS_PER_PASS: #D
execute_later(conn, 'default', 'syndicate_status', #D
[conn, uid, post, start]) #D
def delete_status(conn, uid, status_id):
key = 'status:%s'%status_id
lock = acquire_lock_with_timeout(conn, key, 1) # 防止多个程序同时删除
if not lock: #B
return None #B
# 权限检测
if conn.hget(key, 'uid') != str(uid): #C
release_lock(conn, key, lock) #C
return None #C
pipeline = conn.pipeline(True)
pipeline.delete(key) # 删除指定状态消息
pipeline.zrem('profile:%s'%uid, status_id) # 从用户个人时间线中删除
pipeline.zrem('home:%s'%uid, status_id) # 从用户主页时间线中删除消息id
pipeline.hincrby('user:%s'%uid, 'posts', -1) # 减少已发布消息数量
pipeline.execute()
release_lock(conn, key, lock)
return True
# 清理在关注者的主页时间线中被删除的消息ID
def clean_timelines(conn, uid, status_id, start=0, on_lists=False):
key = 'followers:%s'%uid #A
base = 'home:%s' #A
if on_lists: #A
key = 'list:out:%s'%uid #A
base = 'list:statuses:%s' #A
followers = conn.zrangebyscore(key, start, 'inf', #B
start=0, num=POSTS_PER_PASS, withscores=True) #B
pipeline = conn.pipeline(False)
for follower, start in followers: #C
pipeline.zrem(base%follower, status_id) #C
pipeline.execute()
if len(followers) >= POSTS_PER_PASS: #D
execute_later(conn, 'default', 'clean_timelines' , #D
[conn, uid, status_id, start, on_lists]) #D
elif not on_lists:
execute_later(conn, 'default', 'clean_timelines', #E
[conn, uid, status_id, 0, True]) #E