注意:本系列来源于《Redis实战》一书,侵删
并感谢不洗碗工作室前辈提供的书籍资源(≧∇≦)ノ
本文用到的资源大家可以到我的网站下载:
资源链接
给python初学者:python是通过缩进来判断条件语句的结束条件的。
文章目录
安装redis
pip install redis
在windows使用redis,还需要一个安装程序https://github.com/MicrosoftArchive/redis/releases
来运行第一个程序吧!
import redis
re = redis.Redis(host='127.0.0.1', port=6379,db=0)
re.set('key_name','value_tom')
print(re.get('key_name'))
re.delete('key_name')
print(re.get('key_name'))
输出:
b’value_tom’
None
port在redis-x86安装时可以设定,也可以到安装目录配置文件配置
同时,使用redis-x86客户端也可以增删改:
注意,del命令返回的是删除的值的数量。
Redis数据结构
字符串 列表 集合 散列 有序集合
字符串:
上面的python程序就是一个键为key_name,值为value_tom的字符串
列表:
列表就是链表,在其中,相同元素可以同时出现。
列表命令:
命令 行为
RPUSH 将给定值推入列表右端
LRANGE 获取列表在给定范围上的所有值
LINDEX 获取列表在给定位置上的单个元素
LPOP 从列表左端弹出一个值,并返回被弹出的值
下面在redis-x86中进行进行一个操作
注意 lrange 列表名 0 -1可以取出列表包含的所有元素。
127.0.0.1:6379> rpush list-key item
(integer) 1
127.0.0.1:6379> lpush list-key item2
(integer) 2
127.0.0.1:6379> rpush list-key item
(integer) 3
127.0.0.1:6379> lrange list-key 0 -1
1) "item2"
2) "item"
3) "item"
127.0.0.1:6379> rpush test item
(integer) 1
127.0.0.1:6379> lrange list-key 0 -1
1) "item2"
2) "item"
3) "item"
127.0.0.1:6379> lrange test 0 -1
1) "item"
127.0.0.1:6379> lindex list-key 1
"item"
127.0.0.1:6379> lpop list-key
"item2"
127.0.0.1:6379> lrange list-key 0 -1
1) "item"
2) "item"
除此之外,列表还拥有移除元素、插入元素到列表中间、将列表修剪到指定长度和其它一些命令
集合:
集合和列表的区别就是集合使用散列表来保证自己存储的每一个字符串都是各不相同的
集合命令:
SADD 将给定元素添加到集合(返回布尔值true/false(1/0)
SMEMBERS 返回集合包含的所有元素
SISMEMBER 检查给定元素是否存在于集合中
SREM 移除元素(仍然是返回数量)
127.0.0.1:6379> sadd key item
(integer) 1
127.0.0.1:6379> sadd key item2
(integer) 1
127.0.0.1:6379> sadd key item3
(integer) 1
127.0.0.1:6379> sadd key item
(integer) 0
127.0.0.1:6379> smembers key
1) "item3"
2) "item"
3) "item2"
127.0.0.1:6379> sismember key item4
(integer) 0
127.0.0.1:6379> srem key item item2
(integer) 2
127.0.0.1:6379> smembers key
1) "item3"
127.0.0.1:6379>
集合还支持交集并集差集运算等。
散列
散列同样可以存储多个键值对之间的映射。
命令 行为
HSET 在散列里面关联给定的键值对
HGET 获取指定散列键的值
HGETALL 获取散列包含的所有键值对
HDEL 如果给定键值存在于散列中,那么移除这个键
测试如下:
127.0.0.1:6379> hset hash-key sub-key1 value1
(integer) 1
127.0.0.1:6379> hset hash-key sub-key2 value1
(integer) 1
127.0.0.1:6379> hgetall sub-key1
(empty list or set)
127.0.0.1:6379> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value1"
127.0.0.1:6379> hdel hash-key sub-key1
(integer) 1
127.0.0.1:6379> hdel sub-key2
(error) ERR wrong number of arguments for 'hdel' command
127.0.0.1:6379> hdel hash-key sub-key1
(integer) 0
127.0.0.1:6379> hgetall hash-key
1) "sub-key2"
2) "value1"
127.0.0.1:6379>
可以看出来,hset第一个参数是散列表名,第二、三个参数是键值对
Redis中的散列可以看作是关系数据库中的行
Redis中的有序集合
有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员,每个成员都是各不相同的;有序集合的值被称为分值,分值必须为浮点数。
命令 行为
ZADD 将一个带有给定分值的成员添加到有序集合里面
ZRANGE 根据元素在有序集合中的位置,获取多个元素
ZRANGEBYSCORE 获取在给定分值范围内的所有元素
ZREM 如果给定成员存在于有序集合,那么移除这个成员
测试:
127.0.0.1:6379> zadd zset-key 728 member1
(integer) 1
127.0.0.1:6379> zadd zset-key 982 member0
(integer) 1
127.0.0.1:6379> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
127.0.0.1:6379> zrange zset-key 0 -1
1) "member1"
2) "member0"
127.0.0.1:6379> zrange zset-key 0 800 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
127.0.0.1:6379> zrem zset-key member1
(integer) 1
127.0.0.1:6379> zrange zset-key 0 800 withscores
1) "member0"
2) "982"
到目前为之,我们基本了解了Redis提供的五种结构,下面展示一个使用散列的数据存储能力和有序集合内建的排序能力来解决一个常见问题。
你好Redis
对文章进行投票
ONE_WEEK_IN_SECONDS = 7*86400
VOTE_SCORE = 432
def article_vote(conn,user,article):
cutoff = time.time() - ONE_WEEK_IN_SECONDS # 计算投票截止时间
if conn.zscore('time:',article) < cutoff: # 检查是否还可以对文章进行投票
# zscore 返回有序集 key 中,成员 member 的 score 值
return
article_id = article.partition(':')[-1]
if conn.sadd('voted:'+article_id,user): #如果是第一次投票,那么增加这篇文章的投票数量和评分
conn.zincrby('score:'.article,VOTE_SCORE)
# zincrby为有序集 key 的成员 member 的 score 值加上增量 increment 。
conn.hincrby(article,'votes',1)
发布并获取文章
def post_article(conn ,user,title,link):
#通过对'article:'自增1获取一个新的文章id
article_id = str(conn.incr('article:'))
voted = 'voted:'+article_id
# 将给定元素添加到集合
conn.sadd(voted,user)
# 为给定 key 设置生存时间
conn.expire(voted,ONE_WEEK_IN_SECONDS)
now=time.time()
# 存储文章信息到一个散列中
article = 'article:'+article_id
conn.hmset(article,{
'title':title,
'link':link,
'poster':user,
'time':now,
'votes':1,
})
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:'):
# 文章开始索引和结束索引
start = (page-1)*ARTICLES_PER_PAGE
end= start + ARTICLES_PER_PAGE - 1
# 返回有序集 key 中,指定区间内的成员。
ids = conn.zrevrange(order,start,end)
articles = []
for id in ids:
# 返回哈希表 key 中,所有的域和值。
article_data = conn.hgetall(id)
article_data['id'] = id
articles.append(article_data)
return articles
书中提到,如果对“默认值参数”或者“根据名字(而不是位置)传入参数”感到陌生,可以看看《python语言教程》
但其实在前端语言javascript中就有这种用法。
群组功能
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)
使用redis构建程序
从高层次的角度来看,web应用就是通过http协议对网页浏览器发送的请求进行响应的服务器或者服务
一个web服务器对请求进行响应的典型步骤如下:
(1)服务器对客户端发来的请求进行解析
(2)请求被转发给一个预定义的处理器
(3)处理器可能会从数据库中取出数据
(4)处理器根据取出的数据对模板进行渲染
(5)处理器向客户端返回渲染后的内容作为对请求的响应
通过将传统数据库的一部分数据处理任务以及存储任务交给redis来完成,可以提高网页载入速度,降低资源占用量
登录和cookie缓存
首先是管理登录会话,使用令牌cookie,使用一个散列(Hash)来存储cookie令牌和已登录用户。
检查用户是否登录,可以使用给定的token去查找,返回用户ID,代码如下:
def check_token(conn, token):
return conn.hget('login:', token) #对应redis命令:hget login token
管理用户浏览信息
这里使用了一个update_token函数,个人理解如果是单纯登录,则item参数不存在,更新token,若item存在,则记录用户浏览的时间和item。
使用Zremrangebyrank函数,移除旧记录,保留最新的一部分,代码如下:
def update_token(conn, token, user, item=None):
timestamp = time.time() #返回浮点数,如111.111
conn.hset('login:', token, user) #hset login token user
conn.zadd('recent:', token, timestamp) #zadd recent 111.111 token
if item:
conn.zadd('viewed:' + token, item, timestamp) #zadd viewedtoken 111.111 itemA
conn.zremrangebyrank('viewed:' + token, 0, -26) #zremrangebyrank viewedtoken 0 -26
会话清理
为防止内存过多,需要清理一些旧的会话。在上面代码的第4行,保证了如果用户在进行操作,时间戳将会不断更新。所以清除1000万之后的会话也就有了保证。清理会话的同时,浏览记录等信息也将被清除。代码如下:
QUIT = False
LIMIT = 10000000
def clean_sessions(conn):
while not QUIT:
size = conn.zcard('recent:') #zcard recent 获取长度
if size <= LIMIT:
time.sleep(1)
continue
end_index = min(size - LIMIT, 100)
tokens = conn.zrange('recent:', 0, end_index-1) #zrange recent 0 99 获取token
session_keys = []
for sess in sessions:
session_keys.append('viewed:' + sess)
session_keys.append('cart:' + sess)
conn.delete(*session_keys) #del cart viewed
conn.hdel('login:', *tokens) #hdel login token1 token2
conn.zrem('recent:', *tokens) #zrem recent token1 token2
使用Redis实现购物车
使用散列(Hash)存储每个用户的购物车,进行增删
def add_to_cart(conn, session, item, count): #session为上一步存储的token
if count <= 0:
conn.hrem('cart:' + session, item) #此处书中代码有误,应为hdel,即hdel carttoken1 f1
else:
conn.hset('cart:' + session, item, count) #hset carttoken1 f1 1
网页缓存
网页中许多内容不需要动态生成,对于可以缓存的请求,函数首先从缓存中获取,若不存在,生成内容并放入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) #setex pagekey 300 content
return content
从这里我们可以看出,设置缓存本质上是一个键值对。
这一改动可以将包含大量数据的页面延迟值从
20~50ms降低至查询一次redis所需的时间,查询本地Redis的延迟值通常低于1ms,而查询位于同一个数据中心的Redis
的延迟值通常低于5ms
数据行缓存
假设网站进行促销活动,需要从数据库取出特价商品以及剩余数量,则需要对数据行进行缓存。
使用String缓存一个数据行,列名为其键,内容为其值,使用json格式。
使用两个有序集合记录何时进行更新,第一个有序集合,成员为行ID,值为时间戳,记录何时将其ID缓存到redis中。第二个有序集合成员为行ID,值为一个数字,每隔多久更新一次数据行,若想取消商品,将延迟值删除或者设置小于等于0即可。
redis中没有嵌套结构,如有需要可以通过键名来模拟嵌套。
def schedule_row_cache(conn, row_id, delay):
conn.zadd('delay:', row_id, delay) #数据行的延迟值 zadd delay 5 rowid
conn.zadd('schedule:', row_id, time.time()) #数据行的调度 zadd sche 11.11 rowid
缓存时先取出第一个进行判断是否存在或已经到达时间,然后根据延迟时间判断,延迟时间小于等于0的移除。
若大于,表示商品还需要显示,设置下一次更新时间,并进行更新。
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)
continue
row_id = next[0][0]
delay = conn.zscore('delay:', row_id) #获取延迟值 zscore delay rowid
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())) #更新商品
网页分析
在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)
conn.zremrangebyrank('viewed:' + token, 0, -26) # 移除有序集合中给定的排名区间的所有成员
conn.zincrby('viewed:', item, -1) # 有序集合中对指定成员的分数加上增量 increment,有序集合,能够自动排序
为了让新流行的商品也有机会进入排行,进行一定的修剪,此处是将排名20000之后的商品删除,将剩余的商品的权值降低一半。
def rescale_viewed(conn):
while not QUIT:
conn.zremrangebyrank('viewed:', 20000, -1) #zremrangebyrank viewed 20000 -1
conn.zinterstore('viewed:', {'viewed:': .5}) #zinterstore viewed 1 viewed weight 0.5
time.sleep(300)
判断页面是否需要缓存
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) #获取rank
return rank is not None and rank < 10000 #true为前10000的商品,进行缓存