《Redis IN ACTION》-【第二篇】使用Redis构建Web应用

本篇将通过几个实例,对一些典型的Web应用进行介绍。

从高层次的角度来看,Web应用就是通过HTTP协议对网页浏览器发送的请求进行响应的服务器或者服务(service)。一个Web服务器对请求进行响应对典型步骤如下:

  1. 服务器对客户端发来的请求(request)进行解析。
  2. 请求被转发给一个预定义的处理器(handler)。
  3. 处理器可能会从数据库中取出数据。
  4. 处理器根据取出的数据对模板(template)进行渲染(render)。
  5. 处理器想客户端返回渲染后对内容作为对请求对响应(response)。

以上列举对5个步骤从高层次的角度展示了典型Web服务器的运作方式,这种情况下的Web请求被认为是无状态的(statelesse),也就是说,服务器本身不会记录与过往请求有关的任何信息,这使得失效(fail)的服务器可以很容易地被替换掉。

我们解决的第一个问题就是使用Redis来管理用户登录会话(session)。

2.1 登录和cookie缓存

每当我们登录互联网服务(比如银行账号或者电子邮件)的时候,这些服务都会使用cookie来记录我们都身份。cookie由少量数据组成,网站会要求我们的浏览器存储这些数据并在每次服务发送请求时将这些数据传回给服务。对于用来登录的cookie,由两种常用的方法可以将登录信息存储在cookie里面:一种是签名(signed)cookie,另一种是令牌(token)cookie。

签名cookie通常会存储用户名,可能还有用户ID、用户最后一次成功登录的时间,以及网站觉得有用的其他任何信息。除了用户的相关信息之外,签名cookie还包含一个签名,服务器可以使用这个签名来验证浏览器发送的信息是否未经改动(比如将cookie中的登录用户名改成另一个用户)。

令牌cookie会在cookie里面存储一串随机字节作为令牌,服务器可以根据令牌在数据库中查找令牌的拥有者。随着时间的推移,旧令牌会被新令牌取代。

cookie类型优点缺点
签名cookie验证cookie所需的一切信息都存储在cookie里面。cookie可以包含额外的信息(additional information),并且对这些信息进行签名也很容易正确处理签名很难。很容易忘记对数据进行签名,或者忘记验证数据对签名,从而造成安全漏洞。
令牌cookie添加信息非常容易。cookie的体积非常小,因为移动终端和速度较慢的客户端可以更快地发送请求需要在服务器中存储更多信息。如果使用的是关系数据库,那么载入和存储cookie的代价可能会很高

###代码清单1 check_token()函数

首先,我们将使用一个散列来存储登录cookie令牌与已登录用户之间的映射。要检查一个用户是否已经登录,需要根据给定的令牌来查找与之对应的用户,并在用户已经登录的情况下,返回该用户的ID。

def check_token(conn, token):
    return conn.hget('login:', token)

###代码清单2 update_token()函数

用户每次浏览页面的时候,程序会对用户存储在登录散列里面的信息进行更新,并将用户的令牌和当前时间戳添加到记录最近登录用户的有序集合里面;如果用户正在浏览的是一个商品页面,那么程序还会将这个商品添加到记录这个用户最近浏览过的商品的有序集合里面,并在被记录商品的数量超过25个使,对这个有序集合进行修建。

def update_token(conn, token, user, item=None):
    timestamp = time.time()    #获取当前时间戳    
    conn.hser('login:', token, user) #维持令牌与已登录用户之间的映射
    conn.zadd('recent:', token, timestamp) #记录令牌最后一次出现的时间
    if item:
        conn.zadd('viewed:' + token, item, timestamp) #记录用户浏览过的商品
        conn.zremrangebyrank('viewed:' +token, 0, -26) #移除旧的记录,只保留用户最近浏览过的25个商品

###代码清单3 clean_sessions()函数

因为存储绘画数据所需的内存会随着时间推移而不断增加,所以我们需要定期清理旧的会话数据。为了限制会话数据的数量,我们决定只保存最新的1000万个会话。清理旧会话的程序由一个循环构成,这个循化每次执行的时候,都会检查存储最近登录令牌的有序集合的大小,如果有序集合的大小超过了限制,那么程序就会从有序集合里面移除最多100个最旧的令牌,并从记录用户登录信息的散列里面,移除被删除令牌对应的用户的信息,并对存储了这些用户最近浏览商品记录的有序集合进行清理。与此相反,如果令牌的数量未超过限制,那么程序会先休眠1秒,之后再重新进行检查。

QUIT = False
LIMIT = 10000000

def clean_sessions(conn):
    while not QUIT:
        size = conn.zcard('recent:') #找出目前已有令牌的数量
        if size <= LIMIT: #令牌数量未超过限制,休眠并在之后重新检查
            time.sleep(1)
            continue

        end_index = min(size - LIMIT, 100)
        tokens = conn.zrange('recent:', 0, end_index - 1) #获取需要移除的令牌ID

        session_keys = []
        for token in tokens:
            session_keys.append('viewed:' + token) #为那些将要被删除的令牌构建令牌
        '''
            上面构建令牌的三行程序可以替换成下面这条语句
            session_keys = ['viewed:' + token for token in tokens]
        '''

        conn.delete(*session_keys)
        conn.hdel('login:', *tokens)
        conn.zrem('recent:', *tokens) #移除最旧的那些令牌

2.2 使用Redis实现购物车

使用cookie实现购物车——也就是将整个购物车存储到cookie里面,这种做法到一大优点是无须对数据库进行写入就可以实现购物车功能,而缺点则是程序需要重新解析和验证(validate)cookie,确保cookie的格式正确,并且包含的商品都是可购买的商品。cookie购物车还有一个缺点:因为浏览器每次发送请求都会连cookie一起发送,所以如果购物车cookie的体积比较大,那么请求发送和处理的速度可能会有所降低。

###代码清单4 add_to_cart()函数

购物车的定义非常简单:每个用户的购物车都是一个散列,整个散列存储了商品ID与商品订购数量之间的映射。对商品数量进行验证的工作由web应用程序负责,我们要做的则是在商品的订购数量出现变化时,对购物车进行更新:如果用户订购某件商品的数量大于0,那么程序会将这件商品的ID以及用户订购该商品的数量添加到散列里面,如果用户订购某件商品以及存在与散列里面,那么新的订购数量会覆盖已有的订购数量;相反地,如果用户订购某件商品的数量不大于0,那么程序将从散列里面移除该条目。

def add_to_card(conn, session, item, count):	
    if count <= 0:
        conn.hrem('cart:' + session, item) #从购物车里面移除指定的商品
    else:
        conn.hset('cart:' + session, item, count) # 将指定的商品添加到购物车

###代码清单5 clean_full_session()函数

对前面会话清理函数进行更新,让它在清理旧会话的同时,将旧会话对应用户的购物车也一并删除。

def clean_full_sessions(conn):
    while not QUIT:
        size = conn.zcard('recent:')
        if size <= LIMIT:
            time .sleep(1)
            continue

        end_index = min(size - LIMIT, 100)
        sessions = con.zrange('recent:', 0, end_index - 1)
        
        session_keys = []
        for sess in sessions:
            session_keys.append('viewed:' + sess)
            session_keys.append('card:' + sess)

        conn.delete(*session_keys)
        conn.hdel('login:', *sessions)
        conn.zrem('recent:', *sessions)

2.3 网页缓存

所以标准的Python应用框架都提供了在处理请求之前或者之后添加层(layer)的能力,这些层通常被称为中间件(middleware)或者插件(plugin)。

###代码清单6 cahe_request()函数

对于一个不能被缓存的请求,函数将直接生产并返回页面;而对于可以被缓存的请求,函数首先会尝试从缓存里面取出并返回被缓存的页面,如果缓存页面不存在,那么函数会生成页面并将其缓存在Redis里面5分钟,最会再将页面返回给函数调用者。

def cache_request(conn, request, callback):
    if not can_cache(conn, request): # 对于不能被缓存的请求,直接调用回调函数
        callback(request)
    
    page_key = 'cache:' + hash_request(request) #将请求转换成一个简单的字符串键,方便之后进行查找
    content = conn.get(page_key) #尝试查找被缓存的页面

    if not content:
        content = callback(request) #如果页面还没有被缓存,那么生成页面
        conn.setex(page_key, content, 300) #将新生成的页面放到缓存里面
    
    return content

2.4 数据行缓存

程序可以通过缓存页面载入时所需的数据库行来减少载入页面所需的时间。

程序使用了两个有序集合来记录应该在何时对缓存进行更新:第一个有序集合为调度(schedule)有序集合,它的成员为数据行的行ID,而分值则是一个时间戳,整个时间戳记录了应该在何时将制定的数据行缓存到Redis里面;第二个有序集合为延时(delay)有序集合,它的成员也是数据行的行ID,而分值则记录了指定数据行的缓存需要每隔多少秒更新一次。

###代码清代7 schedule_row_cache()函数

为了让缓存函数定期地缓存数据行,程序首先需要将行ID和给定的延迟值添加到延迟有序集合里面,然后再将行ID和当前时间到时间戳添加到调度有序集合里面。实际执行缓存操作的函数需要用到数据行的延迟值,如果某个数据行的延迟址不存在,那么程序将取消这个数据行的调度,如果我们想要移除某个数据行已有的缓存,并且让缓存函数不再缓存那个数据行,那么只需要把那个数据行的延迟值设置为小于或等于0就可以了。

def schedule_row_cache(conn, row_id, delay):
    conn.zadd('delay:', row_id, delay) #先设置数据行的
    conn.zadd('schedule:', row_id, time.time()) #立即对需要缓存的数据行进行调度

###代码清单8 守护进程函数cache_rows()

负责缓存数据行的函数会尝试读取调度有序集合的第一个元素以及该元素的分支,如果调度有序集合没有包含任何元素,或者分值存储的时间戳所指定的时间稍微来临,那么函数会先休眠50毫秒,然后再重新进行检查。当缓存函数发现一个需要立即进行更新的数据行时,缓存函数会检查这个数据行的延迟值:如果数据行的延迟值小于或者等于0,那么缓存函数会从延迟有序集合和调度有序集合里面移除这个数据行的ID,并从缓存里面删除这个数据行已有的缓存,然后再重新进行检查;对于延迟值大于0的数据行来说,缓存函数会从数据库里面取出这些行,将它们编码为JSON格式并存储到Redis里面,然后更新这些行的调度时间。

def cache_rows(conn):
    while not QUIT:
        next = conn.zrange('schedule:', 0, 0, withscores=True)
        now = time.time()
        
        if not next or next[0][1] > now:
            time.sleep(.05) #暂时没有行需要被缓存,休眠50毫秒后重试
            continue

        row_id = next[0][0]
        delay = conn.zscore('delay:', row_id) #提前获取下一次调度的延迟时间

        if delay <= 0 #不必再缓存这个行,将它从缓存中移除
            conn.zrem('delay:', row_id)
            conn.zrem('schedule:', row_id)
            conn.delete('inv':, row_id)

        row = Inventory.get(row_id) #读取数据行
        conn.zadd('schedule:', row_id, now + delay)
        conn.set('inv' + row_id, json.dumps(row.to_dict() #更新调度时间并设置缓存值

2.5 网页分析

###代码清单9 修改后的updat_token()函数

解决计算用户经常浏览的商品。

def update_token(conn, token, user, item=None)
    timestamp = time.time()
    conn.hset('login:', token, user)
    conn.zadd('recent:', token, timestamp)

    if item:
        conn.zadd('viewed:' + token, item, timestamp)
        conn.zremrangebyrank('viewed:' + token, 0, -26)
        conn.zincrby('viewed:', item, -1)

###代码清单10 守护进程rescale_viewed()

为了让商品浏览次数排行榜能够保持最新,需要定期修剪有序集合的长度并调整已有元素的分值,从而使得新流行的商品可以再排行榜里面占一席之地。调整元素分值的动作则可以通过ZINTERSTORE命令来完成。ZINTERSTORE命令可以组合一个或多个有序集合,并将有序集合包含的每个分值都乘以一个给定的数值。每隔5分钟,删除20000后面的商品,并将剩余的商品浏览次数减半。

def rescale_viewed(conn):
    while not QUIT:
        conn.zremrangebyrank('viewed:', 0, -20001) #删除所有排名在20000名之后的商品
        conn.zinterstore('viewed:', {'viewed:', .5})
        time.sleep(300)

###代码清单11 can_cache()函数

页面是否需要缓存函数。

def can_cache(conn, request):
    item_id = extract_item_id(request) #尝试从页面里面取出商品ID

    if not item_id or is_dynamic(request): #检查这个页面能否被缓存以及这个页面是否为商品页面
        return False

    rank = conn.zrank('viewed:', item_id) #取得商品的浏览次数排名

    return rank is not None and rank < 10000 # 根据商品的浏览次数排名来排名是否需要缓存这个页面

转载于:https://my.oschina.net/tucci/blog/919452

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值