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

 

posted @ 2022-11-29 11:30  某某人8265  阅读(49)  评论(0编辑  收藏  举报