第一部分 入门
1章 初识Redis
- Redis的5种数据结构
- Redis和其它数据库的对比
1.1 Redis简介
- Redis和其它数据库的对比
名称 | 类型 | 数据存储 | 查询操作 | 额外功能 |
---|---|---|---|---|
Redis | 内存存储;非关系型数据库 | 字符串, 列表, 集合, 散列, 有序集合 | 数据结构各自有查询命令 | 发布订阅, 主从复制, 持久化, 脚本 |
memcached | 内存存储;键值缓存 | 键值对 | 增删改查 | 多线程 |
MySQL | 关系型数据库 | 库, 表, 行; 视图处理 | 增删改查; 存储过程, 函数 | 支持ACID; 主从复制, 主主复制 |
PostgreSQL | 关系型数据库 | 库, 表, 行; 视图处理; 可定制类型 | 增删改查; 内置函数; 存储过程 | 支持ACID; 主从复制, 多主复制 |
MongoDB | 非关系文档存储 | 库, 表, 文档 | 增删改查; 条件查询 | 支持map-reduce操作; 主从复制; 分片; 空间索引 |
- Redis附加特性
- 两种持久化方式:时间点转储,命令追加;
- 主从复制以扩展读性能;
- 使用Redis的理由
- 操作数据比memcached方便
- 随机写的操作快,关系型数据库随机写流程多且慢
1.2 Redis数据结构
字符串
- 对整个字符串或字符串一部分执行操作;对数值执行自增自减操作。
- 常用命令
- GET,获取值;
- SET,设置值;
- DEL,删除值。
列表
- 有序存储多个字符串。
- 常用命令
- LPUSH/RPUSH,值推到列表左端/右端;
- LRANGE,获取范围内的所有值;
- LINDEX,获取索引指定的单个元素;
- LPOP/RPOP,列表左端/右端弹出值并返回;
集合
- 无序存储多个字符串。通过散列表保证无重复值。
- 常用命令
- SADD, 添加元素;
- SMEMBERS,获取集合的所有元素;
- SISMEMBER,判断元素是否在集合;
- SREM,从集合移除元素;
散列表
- 存储多个键值对。
- 常用命令
-
HSET,在散列中关联键值对;
-
HGET,指定散列键,获取值;
-
HGETALL,获取散列中的全部键值对;
-
HDEL,移除散列中的键;
-
有序集合
- 类似散列的键值对结构,存储成员对应的分值。能按成员访问,也能按分值访问。
- 常用命令
- ZADD,将分值和成员添加到有序集合内;
- ZRANGE,从有序集合获取多个元素;
- ZRANGEBYSCORE,获取给定分值范围内的元素;
- ZREM,从有序集合移除成员;
1.3 Redis实现文章投票网站
要求
- 文章发布加投票;
- 投票高的文章(高于200)能在网站多放至少一天;(文章评分 = 发布时间+投票数*432)
- 文章发布一周后就不能再投票了;
信息存储
- article:文章id 散列存储文章信息。title,标题;time,发布时间;votes,投票数。
- 两个有序集合,time: 按发布时间排序文章,score: 按评分排序文章。成员均为 article:文章id。
- voted:文章id 集合存已投票用户;
对文章投票
ONE_WEEK_IN_SECONDS = 7 * 86400
VOTE_SCORE = 432
def article_vote(conn, userid, article):
# 文章的投票截止时间
cutoff = time.time() - ONE_WEEK_IN_SECONDS
# 检查文章投票是否截止
if conn.zscore('time:', article) < cutoff:
return
article_id = article.partition(':', -1)
if conn.sadd(f'voted:{article_id}', userid):
conn.zincrby('score:', article, VOTE_SCORE)
conn.hincrby(article, 'votes', 1)
发布文章
def post_article(conn, userid, title):
# 自增生成文章id
article_id = str(conn.incr('article:'))
# 建立投票用户集合, 过期时间一周
voted = f'voted:{article_id}'
conn.sadd(voted, userid)
conn.expire(voted, ONE_WEEK_IN_SECONDS)
# 保存文章信息
now = time.time()
article = f"article:{article_id}"
conn.hset(article, {
"title": title,
"votes": 1,
"time": now,
})
# 文章添加到有序集合中
conn.zadd("score:", article, now + VOTE_SCORE)
conn.zadd("time:", article, now)
return article_id
获取文章
ARTICLES_PER_PAGE = 25
def get_articles(conn, page, order="score:"):
# 获取文章的开始和结束索引
strat = (page - 1) + ARTICLES_PER_PAGE
end = start + ARTICLES_PER_PAGE - 1
ids = conn.zrevrange(order, start, end)
articles = []
for aid in ids:
article_data = conn.hgetall(aid)
article_data['aid'] = aid
articles.append(article_data)
return articles
文章分组
def add_remove_groups(conn, article_id, to_add=[], to_remove=[]):
article = 'article:' + article_id
for group in to_add:
conn.sadd('group:' + group, article)
for group in to_remove:
conn.srem('group:' + group, article)
文章排序
ZINTERSTORE命令可以接受多个集合和多个有序集合作为输入,找出同时存在于集合和有序集合的成员并以不同方式合并成员的分值。
def get_group_articles(conn, group, page, order='score:'):
# 每个分组的每种排序都会有一个数据结构
key = order + group
if not conn.exists(key):
conn.zinterstore(key,
['group:' + group, order],
aggregate = 'max',
)
# 60秒后自动删除这个有序集合
conn.expire(key, 60)
return get_articles(conn, page, key)
2章 使用Redis构建Web应用
- 如何用Redis提升应用程序性能
将传统数据库的一部分数据处理任务和存储任务交给Redis来完成,可以提升网页的载入速度,降低资源占用。
2.1 登录和cookie缓存
cookie
cookie由少量数据组成,会存在浏览器中,每次请求时把这些数据传给服务。登录的信息有两种方式存在cookie中:签名(signed)和令牌(token)。
原理 | 优点 | 缺点 | |
---|---|---|---|
签名cookie | 存储用户的相关信息和一个签名 | 验证信息都存在cookie里面,对这些信息进行签名很容易 | 正确地处理签名很难,很容易忘记对数据进行签名,或者忘记验证数据签名,从而造成安全漏洞 |
令牌cookie | 存储一串随机字节作为令牌,服务器根据令牌去数据库查其拥有者。令牌会随时间更新。 | 添加信息非常容易,cookie体积小。速度慢的客户端可以更快地发送请求 | 需要在服务器存储更多信息 |
令牌更新
使用散列存储登陆cookie令牌和已登录用户之间的映射,基于这个映射对令牌进行检查。
每次用户浏览页面时,都会对用户存在散列的信息更新。
def update_token(conn, token, user, item=None):
tmp = time.time()
# 令牌与已登陆用户映射
conn.hset('login:', token, user)
# 令牌最后一次出现时间
conn.zadd('recent:', token, tmp)
if item:
# 记录用户浏览过的页面
conn.zadd('viewed:' + token, item, tmp)
# 裁剪, 仅保留最近浏览的25个页面
con.zremrangebyrank('viewed:' + token, 0, -26)
会话清理
存储会话的内存会随时间的推移不断增加,需要定期清理。
每次循环检查有序集合的大小,若超过限制,就删除100个最旧的令牌,并把对应用户的登录信息从散列移除,他们的浏览记录清空。若低于限制,程序会先休眠。
QUIT = False
LIMIT = 10000000
def clean_sessions(conn):
while not QUIT:
# 目前已有令牌的数目
size = conn.zcard('recent:')
if size <= LIMIT:
time.sleep(1)
continue
# 获取将移除的令牌id
end_index = min(size - LIMIT, 100)
tokens = conn.zrange('recent:', 0, end_index-1)
# 移除令牌相关的信息
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、请求的发送和处理速度可能会降低。
商品数量变化时更新购物车。
def add_to_cart(conn, session, item, count):
if count < 0:
conn.hrem('cart:' + session, item)
else:
conn.hset('cart:' + session, item, count)
会话清理函数同时修改,加上对购物车的清理:
def clean_full_sessions(conn):
while not QUIT:
size = conn.zcard('recent:')
if size <= LIMIT:
time.sleep(1)
continue
# 获取将移除的令牌id
end_index = min(size - LIMIT, 100)
tokens = conn.zrange('recent:', 0, end_index-1)
session_keys = []
for token in tokens:
session_keys.append('viewed:' + token)
session_keys.append('cart:' + token)
conn.delete(*session_keys)
conn.hdel('login:', *tokens)
conn.zrem('recent:', *tokens)
购物车存在Redis中,可以进行统计: 查看过该商品的有xx%用户最终购买、购买了该商品的用户也购买了另一些商品。
2.3 网页缓存
网站的大部分页面很少会发生变化,这些页面的内容不需要动态生成。
Python的应用框架提供了在处理请求之前或者之后添加层的能力,这些层即中间件或插件。Redis缓存函数就处在这样的层。
def cache_request(conn, request, callback):
if not can_cache(conn, request):
return 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 数据行缓存
假设网站每天推出一些数量有限的特价商品,网站不能对促销页面缓存,因为会导致用户看到错误的商品余量。但是每次载入页面都查数据库则会给数据库很大压力。
需要一个持续运行的守护进程函数,将指定数据行缓存到Redis里面,随时更新。
Redis不直接存储嵌套结构。使用时能用键名模拟嵌套或者直接把数据存储到JSON序列中。
数据行编码为JSON格式存在Redis字符串中。另外需要一个调度有序集合(行id 时间戳)和延时有序集合(行id 延迟值)。根据缓存时间和延迟值自动缓存数据,并且能控制缓存的更新频率。
# 调度函数 控制那些行需要缓存
def schedule_row_cache(conn, row_id, delay):
# 设置延迟值
conn.zadd("delay:", row_id, delay)
# 设置调度
conn.zadd("schedule:", row_id, time.time())
# 缓存函数
def cache_rows(conn):
while not QUIT:
# 取下一条数据行以及该行的时间戳
next = conn.zrange('schedule:', 0, 0, withscores=True)
now = time.time()
# 没有需要缓存的行,程序休眠50ms
if not next or next[0][1] > now:
time.sleep(0.05)
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)
continue
row = Inventory.get(row_id)
# 更新调度时间
conn.zadd('schedule:', row_id, now + delay)
# 设置缓存
conn.set('inv:' + row_id, json.dumps(row.to_dict()))
2.5 网页分析
网站可以记录用户在各个网页的访问量,然后只缓存访问量大的一部分网页。
能在记录浏览历史的同时记录网页的浏览次数。根据浏览次数排序网页,浏览最多的网页会排在前面。
def update_token(conn, token, user, item=None):
...
if item:
...
conn.zincrby('viewed:', item, -1)
除了缓存浏览次数最多的商品外,新加的商品应该也有排到前面的机会。需要定期修剪排行榜并调整分值。
网站根据这个有序集合的浏览次数排序进行缓存。
# 调整排行榜
def rescale_viewed(conn):
while not QUIT:
# 删除排在2万后面的商品
conn.zremrangebyrank('viewed:', 0, -20001)
# 浏览次数降为原来的一半
conn.zinterstore('viewed:', {'viewed:', 0.5})
time.sleep(300)
# 判断页面是否需要缓存
def can_cache(conn, request):
item_id = extract_item_id(request)
# 判断是否为商品页、是否为可缓存页
if not item_id or is_dynamic(request):
return False
# 商品的浏览次数排名
rank = conn.zrank('viewed:', item_id)
# 判断是否需要缓存
return rank is not None and rank < 10000