注意:本系列來源於《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的商品,進行緩存