美文网首页
第二章 使用Redis构建Web应用 Redis In Acti

第二章 使用Redis构建Web应用 Redis In Acti

作者: 好好学习Sun | 来源:发表于2018-07-19 17:42 被阅读16次

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

    (1) 服务器对客户端发来的请求 (request)进行解析 。

    (2) 请求被转发给一个预定义的处理器(handler)。

    (3) 处理器可能会从数据库中取出数据。

    (4) 处理器根据取出的数据对模板(template)进行渲染(render)。

    (5) 处理器向客户端返回渲染后的内容作为对请求的响应(response)。

            请求被认为是无状态的(stateless), 也就是说,服务器本身不会记录与过往请求有关的任何信息, 这使得失效(fail)的服务器可以很容易地被替换掉。 有不少书籍专门介绍了如何优化响应过程的各个步骤,本书要做的事情也和它们类似,不同之处在于 本书讲解的是如何使用更快的Redis查询来代替传统的关系数据库查询,以及如何使用redis来完成一些使用关系数据库没办法高效完成的任务。

            通过将传统数据库的一部分数据处理任务以及存出任务转交给Redis来完成,可以提升网页的载入速度,并降低资源的占用量。

            第一个要解决的问题是使用Redis来管理用户登录会话。

    2.1 登录和cookie缓存

            对于用来登录的cookie,有两种常见的方法可以使用这个签名来验证浏览器发送的信息是否未经改动。

            我们选择使用令牌cookie来引用关系数据库表中负责存储用户登录信息的条目 (entry)。除了用户登录信息之外, 还可以将用户的访问时长和已浏览商品的数掀等信息存储到数据库里面,这样便于将来通过分析这些信息来学习如何更好地向用户推销商品。

            一般来说 用户在决定购买某个或某些商品之前,通常 都会先浏览多个不同的商品,而记录用户浏览过的所有商品以及用户最后一次访问页面的时间等信息,通常会导致大量的数据库写入。从长远来看,用户的这些浏览数据的确非常有用,但问题在于,即使经过优化,大多数关系数据库在每台数据库服务器上面每秒也只能插入、更新或者删除200􀀏2000个数据库行。尽管批 批插入、批址更新和批扯删除等操作可以以更快的速度执行,但因为客户端每次浏览网页都只更新少数几个行,所以高速的批址插入在这里并不适用。

            因为 Fake Web Retailer 目前一天的负载最相对比较大——平均情况下每秒大约1200次写入, 高峰时期每秒接近6000次写入,所以它必须部署1 0台关系数据库服务器才能应对高峰时期的负载批。而 我们要做的就是使用 Redis 重新实现登录 cookie 功能,取代目前由关系数据库实现的登 录 cookie 功能。

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

            对令牌进行检查 并不困难,因为大部分复杂的工作都是在更新令牌时完成的:用户每次浏览页面的时候,程序都会对用户存储在登录散列里面的信息进行更新,并将用户的令牌和 当前时间戳添加到记录最近登录用户的有序集合里面;如果用户正在浏览的是一个商品页面,那么程序还会将这个商品添加到记录这个用户最近浏览过的商品的有序集合里面,并在被记 录商品的数量超过25个时,对这个有序集合进行修剪。

            通过update_token()函数,我们可以记录用户最后一次浏览商品的时间以及用户最近浏览了哪些商品。在一台最近几年生产的服务器上面,使用update_token ()函数每秒至少可以记录20 000件商品,这比Fake Web Retailer高峰时期所需的6000次写入要高3倍有余。不仅如此,通过后面介绍的一些方法,我们还可以进一步优化update_七oken()函数的运行速度。但即使是现在这个版本的update_七oken()函数,比起原来的关系数据库,性能也已经提升了10-100倍。

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

            Redis 的过期数据处理 随着对 Redis 的了解逐渐加深, 读者应该会慢慢发现本书展示的一些解决方案有时候并不是问题的唯一解决办法。 比如对于这个登录 cookie 例子来说,我们可以直接将登录用户和令牌的信息存储到字符串键值对里面, 然后使用 Redis 的 EXPIRE 命令, 为这个字符串和记录用户商品浏览记录的有序集合设置过期时间, 让 Redis 在一段时间之后自动删除它们, 这样就不 需要再使用有序集合来记录最近出现的令牉了. 但是这样一来, 我们就没有办法将会话的数贷限制 在1000万之内了, 并且在将来有需要的时候, 我们也没办法在会话过期之后对被废弃的购物车进 行分析了

    2.2 使用 Redis 实现购物车

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

            因为我们在前而已经使用Redis实现了会话cookie和记录用户最近浏览过的商品这两个特性,所以我们决定将购物车的信息也存储到Redis里而,并且使用与用户会话coo压e相同的cookieID来引用购物车。

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

    我们现在将会话和购物车都存储到了Redis里面, 这种做法除了可以减少请求的体积之外 ,还使得我们可以根据用户浏览过的商品、用户放入购物车的商品以及用户最终购买的商品进行统计计算, 并构建起很多大型网络零售商都在提供的 ”在查看过这件商品的用户当中, 有汗lo的用户最终购买了这件商品” “购买了这件商品的用户也购买了某某其他商品” 等功能, 这些功能可 以帮助用户查找其他相关的商品, 并最终提升网站的销售业绩。

            通过将会话cookie和购物车cookie存储在Reclis里面,我们得到了进行数据分析所需的两个重要的数据来源, 接下来的一节将展示如何使用缓存来减少数据库和Web前端的负载。

    2.3 网页缓存

            通过对浏览数据进行分析,FakeWeb Retailer发现自己所处理的95%的Web页面每天最多只会改变一次,这些页面的内容实际上并不需要动态地生成,而我们的工作就是想办法不再生成这些页面。减少网站在动态生成内容上面所花的时间, 可以降低网站处理相同负载所需的服务器数掀, 并让网站的速度变得更快。(研究表明, 减少用户等待页面载入的时间, 可以增加用户使用网站的欲望,并改善用户对网站的印象。)

            所有标准的Python应用框架都提供了在处理请求之前或者之后添加层(layer) 的能力, 这些层通常被称为中间件(皿ddleware)或者插件(plugin)。我们将创建一个这样的层来调用Redis 缓存函数:对于一个不能被缓存的请求, 函数将直接生成并返回页面;而对于可以被缓存的请求, 函数首先会尝试从缓存里面取出并返回被缓存的页面 , 如果缓存页面不存在,那么函数会生成页 面并将其缓存在Redis里面5分钟, 最后再将页面返回给函数调用者。

            对于那些需要访问数据库的页面来说,这个缓存函数对于减少页面载入时间和降低数据库负载的作用会更加显著。

            在这一节中,我们学习了如何使用Redis来减少载入不常改变页面所需的时间,那么对于那 些经常发生变化的页面,我们是否也能够使用Redis来减少它们的载入时间呢?答案是肯定的,接下来的一节将介绍实现这一目标的具体做法。

    2.4 数据行缓存

            为了展示数据行缓存的作用,我们假设Fake Web Retailer为了清空旧库存和吸引客户消费, 决定开始新一轮的促销活动:这个活动每天都会推出一些特价商品供用户抢购,所有特价商品的数量都是限定的,卖完即止。在这种情况下,网站是不能对整个促销页面进行缓存的,因为这可能会导致用户看到错误的特价商品剩余数呈,但是每次载入页面都从数据库里面取出特价商品的剩余数量的话, 又会给数据库带来巨大的压力, 并导致我们需要花费额外的成本为了应对促销活动带来的大批负来扩展数据库载。

            为了应对促销活动带来的大量负载,我们需要对数据行进行缓存, 具体的做法是:编写一个持续运行的守护进程函数, 让这个函数将指定的数据行缓存到Redis里面 ,并不定期地对这些缓存进行更新。 缓存函数会将数据行编码(encode)为JSON字典并存储在Redis的字符串里面,其中, 数据列(column)的名字会被映射为JSON字典的键, 而数据行的值则会被映射为 JSON字典的值。

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

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

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

            通过组合使用调度函数和持续运行缓存函数, 我们实现了一种重复进行调度的自动缓存机制, 并且可以随心所欲地控制数据行缓存的更新频率:如果数据行记录的是特价促销商品的剩余数盘, 并且参与促销活动的用户非常多的话,那么我们最好每隔几秒更新一次数据行缓存。

    2.5 网页分析

            网站可以从用户的访问、交互和购买行为中收集到有价值的信息。2.3节中冒然缓存所有商品页面将需要大量内存。决定只对浏览量前1W的页面进行缓存。通过使用前面介绍的几个函数, FakeWeb Retailer现在可以统计商品被浏览的次数,并以此来缓存用户最经常浏览的10 000 个商品页面。如果我们想以最少的代价来存储更多页面,那么可以考虑先对页面进行压缩, 然后再缓存到Redis里面;或者使用Edge Side Includes技术移除页面中的部分内容;又或者对模板进行提前优化(pre-optimize), 移除所有非必要的空格字符。这些技术能够减少内存消耗并增加Redis能够缓存的页面数扯,为访问量不断增长的网站带来额外的性能提升。

    2.6 小结

            本章介绍了几种用于降低Fake Web Retailer的数据库负载和Web服务器负载的方法,这些例子里面介绍的都是真实的Web应用程序当今正在使用的思路和方法。

            本章希望向读者传达这样一个概念:在为应用程序创建新构件时,不要害怕回过头去重构已有的构件。

    相关文章

      网友评论

          本文标题:第二章 使用Redis构建Web应用 Redis In Acti

          本文链接:https://www.haomeiwen.com/subject/ygbtmftx.html