在看完第一章以后,第二章主要是作为一个实用指南,告诉你可以用Redis做什么事情。在本章的代码中,将会出现较多第一章中没出现过的Redis命令,不用太过在意细节,我在代码中加了充足的注释,理解大概的功能即可。
一个小tip:在Redis中的命令中,第一个字母代表对应的数据结构,比如:hget - hash get; zset - zhash set。 h,s,z,l分别对应hash,set,zset和list。如果不以这几个开头,则是更高一层的操作。
从高层次的角度看,Web应用就是用过HTTP协议对网页浏览器发送的Request进行响应的服务器。一个Web服务器响应的典型步骤如下:
本章的所有内容都围绕着Fake Web Retailer这个虚构的大型网络电商,我们可以将传统数据库的一部分数据处理任务以及存储任务转交给Redis来完成,可以提升网页的载入速度,并降低资源的占用量。
cookie可以用来存储用户登录的信息、访问时长和已浏览商品的数量等。但是,存储这些数据将会带来大量的数据库IO:平均每台关系型数据库服务器,经过优化后每秒也只能插入、更新或者删除200~2000个数据库行。但是Fake Web Retailer目前一天的负载量比较大——平均情况下每秒大约1200次写入,高峰时期美妙大约6000次写入,所以必须部署10台关系型数据库服务器才能应对高峰时期的负载量。我们可以用Redis重新实现登录cookie功能。
我们用一个hash来存储登录cookie令牌与已登录用户间的映射,要检查一个用户是否已经登录,就根据用户token来查找对应的用户,并在用户已经登录的情况下,返回用户ID:
def check_token(conn, token):
return conn.hget('login:', 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)
# 对每个用户,只保留最新的25条最近浏览
conn.zremrangebyrank('viewed:' + token, 0, -26)
通过使用update_token()函数,一台最近几年生产的服务器上面,每秒至少可以记录20000件商品,比高峰时期所需的6000次读写要高3倍还多,通过后面介绍的方法,我们还可以对他进一步进行优化,即使是现在这个版本,也比原来的关系型数据库性能提升了100倍。
因为存储session所需的内存会随着时间不断的增加,所以我们需要一个定期清理函数来限制session的上限:
QUIT = False
#Session上限为一千万
LIMIT = 10000000
def clean_sessions(conn):
#一直循环清理session
while not QUIT:
#获得当前一共有多少session在内存中
size = conn.zcard('recent:')
#未超过10,000,000,休眠一秒,之后继续检查
if size <= LIMIT:
time.sleedp(1)
continue
#找到最多100条最旧的session
end_index = min(size - LIMIT, 100)
sessions = conn.zrange('recent:', 0, end_index-1)
#存储要被删除的key
session_keys = []
for sess in sessions:
#用户的浏览记录
session_keys.append('viewed:' + sess)
#清除数据
conn.delete(*session_keys)
conn.hdel('login:', *sessions)
conn.zrem('recent:', *sessions)
假设网站每天有500W的访问量,那么两天令牌就会到达1KW的上限。一天有24×3600 = 86400秒,所以每秒网站平均产生5000000/86400 < 58个新对话。也就是说在达到1KW以后,每秒至少需要清理58个token才能防止内存消耗殆尽的情况产生。但是实际上,我们的clean_session()在通过网络来运行时,每秒能够清理10K多个token,所以因为旧token过多耗光内存的情况不会出现。
每个用户的购物车都是一个散列,这个散列存储了商品ID与商品订购数量之间的映射:
def add_to_cart(conn, session, item, count):
#数目小于等于0, 则将这件商品从购物车中删除
if count <= 0:
conn.hrem('cart:' + session, item)
#更新用户对应的购物车中商品的数目
else:
conn.hset('cart:' + session, item, count)
同时更新我们的清理函数,将用户的购物车一并删除:
def clean_full_sessions(conn):
#一直循环清理session
while not QUIT:
#获得当前一共有多少session在内存中
size = conn.zcard('recent:')
#未超过10,000,000,休眠一秒,之后继续检查
if size <= LIMIT:
time.sleedp(1)
continue
#找到最多100条最旧的session
end_index = min(size - LIMIT, 100)
sessions = conn.zrange('recent:', 0, end_index-1)
#存储要被删除的key
session_keys = []
for sess in sessions:
#用户的浏览记录
session_keys.append('viewed:' + sess)
#新添加:用户的购物车
session_keys.append('cart:' + sess)
#清除数据
conn.delete(*session_keys)
conn.hdel('login:', *sessions)
conn.zrem('recent:', *sessions)
网页中很多内容并不会发生大的变化,我们可以缓存这些静态网页,加快网页加载速度,也能提升用户使用的感受。
def cache_request(conn, request, callback):
#如果网页不能缓存,也就是动态网页,则通过回调函数动态生成网页
if not can_cache(conn, request):
return callback(request)
#将request通过hash函数转换为简单的字符串键,方便之后的查找
page_key = 'cache:' + hash_request(request)
#查找缓存页面
content = conn.get(page_key)
#查找的页面不在缓存中
if not content:
#生成页面
content = callback(request)
#将页面放入缓存中,并保存5分钟
conn.setex(page_key, content, 300)
return content
假设Fake Web Retailer在做一个促销活动,所有商品的数目都是限定的。这种情况下,不能对整个商品页面进行缓存,这样有可能让用户看到错误的剩余数量,每次载入页面都从数据库提取剩余数量的话,会对数据库带来巨大的压力。我们可以对数据行进行缓存。
具体做法是:编写一个持续运行的守护进程函数,让这个函数将指定的数据行缓存到Redis里面,并不定期的对这些缓存进行更新。缓存函数会将数据行编码为JSON字典并存储在Redis的字符串里面,其中column的名字会被映射为JSON字典的键,而数据行的值会被映射为JSON字典的值。
我们先写一个调度函数:
def schedule_row_cache(conn, row_id, delay):
#设置数据行的延迟值
conn.zadd('delay:', row_id, delay)
#立即对需要缓存的数据行进行调度
conn.zadd('scheduled:', row_id, time.time() )
之后是守护进程函数:
def cache_rows(conn):
while not QUIT:
#从schedule中读取出第一行需要缓存的数据
next = conn.zrange('schedule:', 0, 0, withscores=True)
now = time.time()
#如果schedule中没有要缓存的数据,或者数据应该在之后缓存,休眠50ms后重试
if not next or next[0][1] > now:
time.sleep(.05)
continue
#获取数据库行的id
row_id = next[0][0]
#提前获取下一次调度的延迟时间
delay = conn.zscore('delay:', row_id)
#延迟值<=0, 将这个行从所有缓存中移除
if delay <= 0:
conn.zrem('delay:', row_id)
conn.zrem('schedule:', row_id)
conn.delete('inv:'+ row_id)
continue
#从数据库中读取这一行
row = Inventory.get(row_id)
#更新调度时间
conn.zadd('schedule:', row_id, now + delay)
#设置缓存的内容
conn.set('inv:'+ row_id, json.dumps(row.to_dict()))
通过组合使用调度函数和持续运行的守护进程函数,我们实现了一种重复进行调度的自动缓存机制,并且可以随心所欲的控制数据行缓存的更新频率:如果参与活动的用户非常多,我们最好几秒更新一次缓存,如果商品数据并不经常改变,或者缺货是可以接受的,我们可以一分钟更新一次缓存。
通过缓存我们极大的提升了网站的响应速度,但是Fake Web Retailer总共有100000件商品。全部缓存将会耗光内存,于是我们决定只缓存10000件最经常被浏览的商品。
修改之前的update_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)
# 对每个用户,只保留最新的25条最近浏览
conn.zremrangebyrank('viewed:' + token, 0, -26)
# 新添加:采用负数表示页面浏览次数,浏览次数越高的页面,其索引值越小,排名也就越靠前
conn.zincrby('viewed:', item, -1)
为了浏览次数排行榜能够保持最新,我们需要定期修剪有序集合的长度并且调整已有元素的分值,使得新流行的商品也可以在排行榜里占据一席之地:
def rescale_viewed(conn):
while not QUIT:
#删除所有排名在20000名之后的商品
conn.zremrangebyrank('viewed:', 0, -20001)
#将浏览次数将为之前的一半
conn.zinterstore('viewed:', {'viewed:' : .5})
#5分钟后再次进行这个操作
time.sleep(300)
之后我们实现在缓存页面时用到的函数can_cache():
def can_cache(conn, request):
#从request中获取商品id
item_id = extract_item_id(request)
#动态页面不应被缓存
if not item_id or is_dynamic(request):
return False
#获取商品的排名
rank = conn.zrank('viewed:', item_id)
#只在前10000名才应该被缓存
return rank is not None and rank < 10000
本章例子中介绍的都是真实的Web应用程序当今正在使用的思路和方法。
在为应用程序创建新构件的时候,不要害怕去重构已有的构件,就像购物车cookie的例子和基于登录会话cookie实现网页分析的的例子一样,已有的构件有时候需要细微的修改才能真正满足需求。