redis實戰之事務與持久化
1. 事務描述
(1)什麼是事務
事務,就是把一堆事情綁在一起,按順序的執行,都成功了才算完成,否則恢復之前的樣子
事務必須服從ACID原則,ACID原則分別是原子性(atomicity)、一致性(consistency)、隔離性(isolation)和永續性(durability)
原子性:操作這些指令時,要麼全部執行成功,要麼全部不執行。只要其中一個指令執行失敗,所有的指令都執行失敗,資料進行回滾,回到執行指令前的資料狀態
一致性:事務的執行使資料從一個狀態轉換為另一個狀態,但是對於整個資料的完整性保持穩定
隔離性:在該事務執行的過程中,無論發生的任何資料的改變都應該只存在於該事務之中,對外界不存在影響,只有在事務確認提交之後們才會顯示該事務對資料的改變,其他事務才能獲取到這些改變後的資料
永續性:當事務正確完成後,它對於資料的改變是永久性的
(2)併發事務導致的常見錯誤
第一類丟失更新:撤銷一個事務時,把其他事務已提交的更新資料覆蓋
髒讀:一個事務讀取到另一個事務未提交的更新資料
幻讀也叫虛讀:一個事務執行兩次查詢,第二次結果集包含第一次中沒有或某些行已經被刪除的資料,造成兩次結果不一致,只是另一個事務在這兩次查詢中間插入或刪除了資料造成的
不可重複讀:一個事務兩次讀取同一行的資料,結果得到不同狀態的結果,中間正好另一個事務更新了該資料,兩次結果相異,不可被信任
第二類丟失更新:是不可重複讀的特殊情況。如果兩個事物都讀取同一行,然後兩個都進行寫操作,並提交,第一個事物所做的改變就會丟失
2. redis事務處理
redis事務通過MULTI、WATCH、UNWAYCH、EXEC、DISCARD五個命令實現
MUTIL命令:用於開啟一個事務,客戶端可以繼續向伺服器傳送任意多條命令,這些命令不會立即被執行,而是被放到一個佇列中,當EXEC命令被呼叫時,所有佇列中的命令才會被執行,它總是返回OK
WATCH命令:對鍵進行監視,直到使用者執行EXEC命令的這段時間裡面,如果其他客戶端搶先對任何被監視的鍵進行了替換、更新或刪除等操作,那麼當用戶艙室執行EXEC命令的時候,事務將失敗並返回一個錯誤(之後使用者可以選擇重試或者放棄事務)
UNWATCH命令:在WATCH命令執行之後,EXEC命令執行之前對連結進行重置
EXEC命令:執行事務命令
DISCARD命令:客戶端可以取消WATCH命令名清空所有已入隊命令
3. redis事務示例
下面將用商品交易示例說明事務處理過程
(1)將商品投放到市場
a. 使用雜湊(users:)來管理市場中的所有使用者資訊,包括使用者名稱、使用者擁有的錢
b. 使用集合(inventory:)來管理每個使用者的所有商品資訊,包括商品名名
inventory:1對應使用者User:1的報告
c. 使用有序集合(market:)來管理投放到市場中的商品
流程:檢查inventory:1包裹中是否含有ItemL商品----->將inventory:1包裹中的ItemL商品新增到交易市場----->刪除inventory:1包裹中的ItemL商品
def list_item(conn,userid,goodsname,price): #使用有序集合(market:)來管理投放到市場中的商品 #商品的key為userid:goodsid inventory = 'inventory:%s' %(userid) user = 'User:%s' %(userid) goodsitem = '%s:%s' %(userid,goodsname) end = time.time() + 5 pipe = conn.pipeline() while time.time() < end: try: pipe.watch(inventory) #監視包裹發生的變化 if not pipe.sismember(inventory,goodsname):#檢查使用者是否仍然持有將要被放入市場的商品 pipe.unwatch() return None pipe.multi() pipe.zadd('market:',{goodsitem:price}) #將商品投放到市場 pipe.srem(inventory, goodsname) #從使用者包裹中刪除該商品 pipe.execute() print('商場中的商品:',conn.zrange('market:', 0, -1,withscores=True)) print('商品投放市場後用戶{}的商品:{}'.format(userid, conn.smembers(inventory))) return True except redis.exceptions.WatchError as e: print(e) return False
測試:
(2)交易:User:2購買交易市場中的中的ItemL商品
思想:使用watch對市場以及買家的個人資訊進行監視,然後獲取買家擁有的錢數以及商品市場的售價,並檢查買家是否有足夠的錢來購買商品,如果買家沒有足夠的錢,那麼程式會取消事務,如果買家錢足夠,那麼程式首先會將買家支付的錢轉移給賣家,然後將售出的商品移除商品交易市場。
def purchase_item(conn, buyerid, goodsname, sellerid): buyer = 'User:{}'.format(buyerid) #買家key seller = 'User:{}'.format(sellerid) #賣家key buy_inventory = 'inventory:{}'.format(buyerid)#買家包裹key sell_inverntory = 'inventory:{}'.format(sellerid)#賣家包裹key goodsitem = '{}:{}'.format(sellerid,goodsname)#市場中的商品key end = time.time() + 5 pipe = conn.pipeline() while time.time() < end: try: pipe.watch('market:',buyer)#對商品買賣市場以及買家的個人資訊進行監視 funds = int(pipe.hget(buyer,'funds'))#得到買家擁有的錢 print('買家手上的錢:', funds) price = pipe.zscore('market:', goodsitem)#得到商品的價格 print('price:',price) if funds < price: pipe.unwatch() return None pipe.multi() pipe.hincrby(buyer, 'funds', int(-price))#買家的錢減少 pipe.hincrby(seller, 'funds', int(price))#賣家的錢增加 pipe.sadd(buy_inventory,goodsname) pipe.zrem('market:', goodsitem) pipe.execute() print('交易市場中的商品:', conn.zrange('market:', 0, -1, withscores=True)) print('買家現在手中的錢:',conn.hget(buyer, 'funds')) print('買家包裹內容:', conn.smembers(buy_inventory)) print('賣家現在手中的錢:',conn.hget(seller, 'funds')) return True except redis.exceptions.WatchError as e: print(e) return False
測試:
if __name__ == '__main__': conn = redis.Redis() #create_users(conn) #create_user_inventory(conn) list_item(conn,1,'ItemL',28) purchase_item(conn, 2, 'ItemL', 1)
4. redis持久化
redis提供了兩種不同的持久化方式來將資料從記憶體儲存到硬盤裡面,一種是RDB持久化,原理是將redis記憶體中的資料庫記錄定時dump到磁碟上的RDB持久化,另外一種是AOF(append only file)持久化,原理是被執行的寫命令複製到磁盤裡。
(1)RDB持久化配置
# Save the DB on disk: #設定sedis進行資料庫映象的頻率。 #900秒(15分鐘)內至少1個key值改變(則進行資料庫儲存--持久化)。 #300秒(5分鐘)內至少10個key值改變(則進行資料庫儲存--持久化)。 #60秒(1分鐘)內至少10000個key值改變(則進行資料庫儲存--持久化)。 save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes # 在進行映象備份時,是否進行壓縮。yes:壓縮,但是需要一些cpu的消耗。no:不壓縮,需要更多的磁碟空間。 rdbcompression yes # 一個CRC64的校驗就被放在了檔案末尾,當儲存或者載入rbd檔案的時候會有一個10%左右的效能下降,為了達到效能的最大化,你可以關掉這個配置項。 rdbchecksum yes # 快照的檔名 dbfilename dump.rdb # 存放快照的目錄 dir ./
(2)建立快照的方法
a. 客戶端可以通過想redis傳送besave命令來建立一個快照,對於支援bgsave命令的平臺來說(基本上所有平臺都支援,除了windows平臺),redis會呼叫fork來建立一個子程序,然後子程序負責將快照寫入磁碟,而父程序繼續處理命令請求
b. 客戶端還可以向redis傳送save命令來建立一個快照,接到save命令的redis伺服器在快照建立完畢之前不再響應任何其他命令,save命令不常用,我們通常只會在沒有足夠記憶體去支援bgsave的情況下,又或者即使等待持久化操作執行完畢也無所謂的情況下,才會使用這個命令
c. 當redis通過shutdown命令接收到關閉伺服器請求時,或者接收到標準term命令時,會執行一個save命令,阻塞所有客戶端,不再執行客戶端傳送的任何命令,並在save命令執行完畢之後關閉伺服器
d. 當一個redis伺服器連結另一個redis伺服器,並向對方傳送sync同步命令來開始一次複製操作時,如果主伺服器目前沒有執行bgsave操作,或者主伺服器並非剛剛執行完bgsave操作,那麼主伺服器就會執行bgsave
(3)AOF持久化配置
# 是否開啟AOF,預設關閉(no) appendonly yes # 指定 AOF 檔名 appendfilename appendonly.aof # Redis支援三種不同的刷寫模式: # appendfsync always #每次收到寫命令就立即強制寫入磁碟,是最有保證的完全的持久化,但速度也是最慢的,一般不推薦使用。 appendfsync everysec #每秒鐘強制寫入磁碟一次,在效能和持久化方面做了很好的折中,是受推薦的方式。 # appendfsync no#完全依賴OS的寫入,一般為30秒左右一次,效能最好但是持久化最沒有保證,不被推薦。 #在日誌重寫時,不進行命令追加操作,而只是將其放在緩衝區裡,避免與命令的追加造成DISK IO上的衝突。 #設定為yes表示rewrite期間對新寫操作不fsync,暫時存在記憶體中,等rewrite完成後再寫入,預設為no,建議yes no-appendfsync-on-rewrite yes #當前AOF檔案大小是上次日誌重寫得到AOF檔案大小的二倍時,自動啟動新的日誌重寫過程。 auto-aof-rewrite-percentage 100 #當前AOF檔案啟動新的日誌重寫過程的最小值,避免剛剛啟動Reids時由於檔案尺寸較小導致頻繁的重寫。 auto-aof-rewrite-min-size 64mb
(4)AOF重寫原理
AOF 重寫和 RDB 建立快照一樣,都巧妙地利用了寫時複製機制:
a. Redis 執行 fork() ,現在同時擁有父程序和子程序
b. 子程序開始將新 AOF 檔案的內容寫入到臨時檔案
c . 對於所有新執行的寫入命令,父程序一邊將它們累積到一個記憶體快取中,一邊將這些改動追加到現有 AOF 檔案的末尾,這樣樣即使在重寫的中途發生停機,現有的 AOF 檔案也還是安全的
d. 當子程序完成重寫工作時,它給父程序傳送一個訊號,父程序在接收到訊號之後,將記憶體快取中的所有資料追加到新 AOF 檔案的末尾
注意:在將記憶體快取中的資料追加到新AOF檔案末尾和rename時,主程序是阻塞的