導讀
篇幅較長,乾貨十足,閱讀需要花點時間,全部手打出來的字,難免出現錯別字,敬請諒解。珍惜原創,轉載請註明出處,謝謝~!
學習之前,先附上一張知識腦圖,百度上找噠~~~
NoSql介紹與Redis介紹
什麼是Redis?
Redis是用C語言開發的一個開源的高性能鍵值對(key-value)內存數據庫。
它提供五種數據類型來存儲值:字符串類型、散列類型、列表類型、集合類型、有序類型。
它是一種NoSql數據庫。
什麼是NoSql?
- NoSql,即Not-Only Sql(不僅僅是SQL),泛指非關係型的數據庫。
- 什麼是關係型數據庫?數據結構是一種有行有列的數據庫。
- NoSql數據庫是爲了解決高併發、高可用、高可擴展、大數據存儲問題而產生的數據庫解決方案。
- NoSql可以作爲關係型數據庫的良好補充,但是不能替代關係型數據庫。
NoSql數據庫分類
鍵值(key-value)存儲數據庫
- 相關產品:Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley Db等
- 典型應用:內存緩存,主要用於處理大量數據的高訪問負載
- 數據模型:一系列鍵值對
- 優勢:快速查詢
- 劣勢:存儲的數據缺少結構化
列存儲數據庫
- 相關產品:Cassandra、Hbase、Riak
- 典型應用:分佈式的文件系統
- 數據模型:以列簇式存儲,將同一列數據存在一起
- 優勢:查找速度快,可擴展性強,更容易進行分佈式擴展
- 劣勢:功能相對侷限
文檔型數據庫
- 相關產品:CouchDB、MongoDB
- 典型應用:web應用(與key-value類似,value是結構化的)
- 數據模型:一系列鍵值對
- 優勢:數據結構要求不嚴格
- 劣勢
圖形(Graph)數據庫
- 相關數據庫:Neo4J、InfoGrid、Infinite、Graph
- 典型應用:社交網絡
- 數據模型:圖結構
- 優勢:利用圖結構先關算法
- 劣勢:需要對整個圖做計算才能得出結果,不容易做分佈式的集羣方案。
Redis歷史發展
2008年,意大利的一家創業公司Merzia推出了一款給予MySql的網站實時統計系統LLOOGG,然而沒過多久該公司的創始人Salvatore Sanfilippo便對MySql的性能感到失望,於是他決定親力爲LLOOGG量身定做一個數據庫,並於2009年開發完成,這個數據庫就是Redis。
不過Salvatore Sanfilippo並不滿足只將Redis用於LLOOGG這一款產品,而是希望更多的人使用它,於是在同一年Salvatore Sanfilippo將Redis開源發佈。
並開始和Redis的另一名主要的代碼貢獻者Pieter Noordhuis一起繼續着Redis的開發,直到今天。
Salvatore Sanfilippo自己也沒有想到,短短的幾年時間,Redis就擁有了龐大的用戶羣體。Hacker News在2012年發佈一份數據庫的使用請款調查,結果顯示有近12%的公司在使用Redis。國內如新浪微博、街旁網、知乎網、國外如GitHub、Stack、Overflow、Flickr等都是Redis的用戶。
VmWare公司從2010年開始贊助Redis的開發,Salvatore Sanfilippo和Pieter Noordhuis也分別在3月和5月加入VMware,全職開發Redis。
Redis的應用場景
- 內存數據庫(登錄信息、購物車信息、用戶瀏覽記錄等)
- 緩存服務器(商品數據、廣告數據等等)(最多使用)
- 解決分佈式集羣架構中的Session分離問題(Session共享)
- 任務隊列。(秒殺、搶購、12306等等)
- 支持發佈訂閱的消息模式
- 應用排行榜
- 網站訪問統計
- 數據過期處理(可以精確到毫秒)
Redis安裝及配置
- 官網地址:https://redis.io/
- 中文官網地址:http://www.redis.cn
- 下載地址:http://download.redis.io/releases/
Linux環境下安裝Redis
注:將下載後的Redis拖進Linux需要安裝下,VMware Tools,參考鏈接
將下載後的Redis拖進linux
安裝C語言需要的GCC環境
yum install gcc-c++
解壓Redis源碼壓縮包
tar -zxf redis-4.0.11.tar.gz
編譯Redis源碼
make
安裝Redis
make install PREFIX=/user/local/redis
格式:make install PREFIX=安裝目錄
Redis啓動
redis設置密碼
更改redis.conf配置
重啓服務
前端啓動
- 啓動命令:redis-server,直接運行bin/redis-server將以前端模式啓動。
關閉服務
ctrl+c
啓動缺點:客戶端窗口關閉,則redis-server程序結束,不推薦使用
後端啓動(守護進程啓動)
拷貝redis
cp redis.conf /usr/local/redis/bin
格式:cp 拷貝文件夾 拷貝路徑
修改redis.conf,將daemonize由no改爲yes
vim redis.conf
執行命令
./redis-server redis.conf
格式:啓動服務 指定配置文件
關閉服務(粗暴方式)
kill -9 42126
格式:kill -9 進程號
正常關閉
./redis-cli shutdown
修改redis配置文件(解決IP綁定問題)
# bind 127.0.0.1 綁定的IP才能fangwenredis服務器,註釋掉該配置 protected-mode yes 是否開啓保護模式,由yes改爲no
其他命令說明
redis-server :啓動redis服務 redis-cli :進入redis命令客戶端 redis-benchmark: 性能測試的工具 redis-check-aof : aof文件進行檢查的工具 redis-check-dump : rdb文件進行檢查的工具 redis-sentinel : 啓動哨兵監控服務
Redis客戶端
自帶命令行客戶端
語法
./redis-cli -h 127.0.0.1 -p 6379
修改redis.conf配置文件(解決ip綁定問題)
#bind 127.0.0.1 綁定的ip才能訪問redis服務器,註釋掉該配置 protected-mode yes 是否開啓保護模式,由yes改爲no
參數說明
- -h:redis服務器的ip地址
- -p:redis實例的端口號
默認方式
如果不制定主機和端口號也可以
./redis-cli 默認的主機地址是:127.0.0.1 默認的端口號是:6379
Redis數據類型
官網命令大全網址
http://www.redis.cn/commands.html
- String(字符類型)
- Hash(散列類型)
- List(列表類型)
- Set(集合類型)
- SortedSet(有序集合類型,簡稱zset)
注:命令不區分大小寫,而key是區分大小寫的。
String類型
賦值
語法:SET key value
取值
語法:GET key
取值並賦值
語法:GETSET key value
演示
數值增減
前提條件:
- 當value爲整數數據時,才能使用以下命令操作數值的增減。
- 數值增減都是原子操作。
遞增數字
語法:INCR key
增加指定的整數
語法:INCRBY key increment
遞減數值
語法:DECR key
減少指定的整數
語法:DECRBY key decrement
僅當不存在時賦值
注:該命令可以實現分佈式鎖的功能,後續講解!!!!
語法:setnx key value
向尾部追加值
注:APPEND命令,向鍵值的末尾追加value。如果鍵不存在則該鍵的值設置爲value,即相當於set key value。返回值是追加後字符串的總長度。
獲取字符串長度
注:strlen命令,返回鍵值的長度,如果鍵不存在則返回0
語法:STRLEN key
同時設置/獲取多個鍵值
語法:
- MSET key value [key value ....]
- MGET key [key ....]
應用場景之自增主鍵
需求:商品編號、訂單號採用INCR命令生成。
設計:key明明要有一定的設計
實現:定義商品編號key:items:id
Hash類型
Hash叫散列類型,它提供了字段和字段值的映射。字段值只能是字符串類型,不支持散列類型、集合類型等其他類型。
賦值
HSET命令不區分插入和更新操作,當執行插入操作時HSET命令返回1,當執行更新操作時返回0。
一次只能設置一個字段值
語法:HSET key field value
一次設置多個字段值
語法:HMSET key field value [field value ...]
當字段不存在時
類似HSET,區別在於如何字段存在,該命令不執行任何操作
語法:HSETNX key field value
取值
一次只能獲取一個字段值
語法:HGET key field
一次可以獲取多個字段值
語法:HMGET key field [field ....]
獲取所有字段值
語法:HGETALL key
刪除字段
可以刪除一個或多個字段,返回值是被刪除的字段個數
語法:HDEL key field [field ...]
增加數字
語法:HINCRBY key field increment
判斷字段是否存在
語法:HEXISTS key field
只獲取字段名或字段值
語法:
- HKEYS key
- HVALS key
獲取字段數量
語法:HLEN key
獲取所有字段
作用:獲取hash的所有信息,包括key和value
語法:hgetall key
應用之存儲商品信息
注意事項:存在哪些對象數據,特別是對象屬性經常發生增刪改操作的數據。
商品信息字段
【商品id,商品名稱,商品描述,商品庫存,商品好評】
定義商品信息的key
商品id爲1001的信息在Redis中的key爲:[items.1001]
示例
List類型
ArrayList使用數組方式存儲數據,所以根據索引查詢數據速度快,而新增或者刪除元素時需要涉及到位移操作,所以比較慢。
LinkedList使用雙向鏈表方式存儲數據,每個元素都記錄前後元素的指針,所以插入、刪除數據時只是更改前後元素的指針即可,速度非常快。然後通過下標查詢元素時需要從頭開始索引,所以比較慢,但是如果查詢前幾個元素或後幾個元素速度比較快。
List介紹
Redis的列表類型(list)可以存儲一個有序的字符串列表,常用的操作是向列表兩端添加元素,或者獲取列表的某一個片段。
列表類型內部是使用雙向鏈表(double linked list)實現的,所以向列表兩端添加元素的時間複雜度爲0/1,獲取越接近兩端的元素速度就越快。意味着即使是一個有幾千萬個元素的列表,獲取頭部或尾部的10條記錄也是極快的。
向列表兩端添加元素
向列表左邊添加元素
語法:LPUSH key value [value ...]
向列表右邊添加元素
語法:RPUSH key value [value ....]
查看列表
語法:LRANGE key start stop
LRANGE命令是列表類型最常用的命令之一,獲取列表中的某一片段,將返回start、stop之間的所有元素(包括兩端的元素),索引從0開始。索引可以是負數,“-1”代表最後一邊的一個元素
從列表兩端彈出元素
LPOP命令從列表左邊彈出一個元素,會分兩步完成:
- 將列表左邊的元素從列表中移除
- 返回被移除的元素值
語法:
- LPOP key
- RPOP key
獲取列表中元素的個數
語法:LLEN key
刪除列表中指定個數的值
LREM命令會刪除列表中前count個數爲value的元素,返回實際刪除的元素個數。根據count值不同,該命令的執行方式會有所不同。
語法:LREM key count value
- 當count>0時,LREM會從列表左邊開始刪除
- 當count<0時,LREM會從列表右邊開始刪除
- 當count=0時,LREM會刪除所有值爲value的元素
獲取/設置指定索引的元素值
獲取指定索引的元素值
語法:LINDEX key index
設置指定索引的元素值
語法:LSET key index value
向列表中插入元素
該命令首先會在列表中從左到右查詢值爲pivot的元素,然後根據第二個參數是BEFORE還是AFTER來決定將value插入到該元素的前面還是後面。
語法:LINSERT key BEFORE|AFTER pivot value
將元素從一個列表轉移到另一個列表中
語法:RPOPLPUSH source destination
應用之商品評論列表
需求1:用戶針對某一商品發佈評論,一個商品會被不同的用戶進行評論,存儲商品評論時,要按時間順序排序。
需要2:用戶在前端頁面查詢該商品的評論,需要按照時間順序降序排序。
思路:
使用list存儲商品評論信息,key是該商品的id,value是商品評論信息商品編號爲1001的商品評論key【items:comment:1001】
Set類型
set類型即集合類型,其中的數據時不重複且沒有順序。
集合類型和列表類型的對比:
集合類型的常用操作是向集合中加入或刪除元素、判斷某個元素是否存在等,由於集合類型的Redis內部是使用值爲空散列標實現,所有這些操作的時間複雜度都爲0/1。
Redis還提供了多個集合之間的交集、並集、差集的運算。
添加/刪除元素
語法:SADD key member [member ...]
語法:SREM key member [member ...]
獲取集合中的所有元素
語法:SMEMBERS key
判斷元素是否在集合中
語法:SISMEMBER key member
集合運算命令
集合的差集運算 A-B
屬於A並且不屬於B的元素構成的集合
語法:SDIFF key [key ...]
集合的交集運算 A∩B
屬於A且屬於B的元素構成的集合。
語法:SINTER key [key ...]
集合的並集運算 A ∪ B
屬於A或者屬於B的元素構成的集合
語法:SUNION key [key ...]
獲取集合中的元素個數
語法:SCARD key
從集合中彈出一個元素
注意:集合是無序的,所有spop命令會從集合中隨機選擇一個元素彈出
語法:SPOP key
SortedSet類型zset
在集合類型的基礎上,有序集合爲集合中的每個元素都關聯一個分數,這使得我們不僅可以完成插入、刪除和判斷元素是否存在集合中,還能夠獲得最高或最低的前N個元素、獲取指定分數範圍內的元素等與分蘇有關的操作。
在某些方面有序集合和列表類型有些相似。
- 二者都是有序的。
- 二者都可以獲得某一範圍的元素
但是二者有着很大的區別:
- 列表類型是通過鏈表實現的,後去靠近兩端的數據速度極快,而當元素增多後,訪問中間數據的速度會變慢。
- 有序集合類型使用散列實現,所有即使讀取位於中間部分的數據也很快。
- 列表中不能簡單的調整某個元素的位置,但是有序集合可以(通過更改分數實現)。
- 有序集合要比列表類型更耗內存。
添加元素
向有序集合中加入一個元素和該元素的分數,如果該元素已經存在則會用新的分數替換原有的分數。返回值是新加入到集合中的元素個數,不不含之前已經存在的元素。
語法:ZADD key score member [score member ...]
獲取排名在某個範圍的元素列表
按照元素分數從小到大的順序返回索引從start到stop之間的所有元素(包含兩端的元素)
語法:ZRANGE key start stop [WITHSCORES]
如果需要獲取元素的分數的可以在命令尾部加上WITHSCORES參數
獲取元素的分數
語法:ZSCORE key member
刪除元素
移除有序集key中的一個或多個成員,不存在的成員將被忽略。
當key存在但不是有序集類型時,返回錯誤。
語法:ZREM key member [member ...]
獲取指定分數範圍的元素
語法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
增加某個元素的分數
返回值是更改後的分數
語法:ZINCRBY key increment member
獲取集合中元素的數量
語法:ZCARD key
獲得指定分數範圍內的元素個數
語法:ZCOUNT key min max
按照排名範圍刪除元素
語法:ZREMRANGEBYRANK key start stop
按照分數範圍刪除元素
語法:ZREMRANGEBYSCORE key min max
獲取元素的排名
從小到大
語法:ZRANK key member
從大到小
語法:ZREVRANK key member
應用之商品銷售排行榜
需求:根據商品銷售對商品進行排序顯示
思路:定義商品銷售排行榜(sorted set集合),key爲items:sellsort,分數爲商品小數量。
寫入商品銷售量:
>商品編號1001的銷量是9,商品編號1002的銷量是10
>商品編號1001銷量家1
>商品銷量前10名
通用命令
keys
語法:keys pattern
del
語法:DEL key
exists
作用:確認一個key是否存在
語法:exists key
expire
Redis在實際使用過程中更多的用作緩存,然後緩存的數據一般都是需要設置生存時間的,即:到期後數據銷燬。
EXPIRE key seconds 設置key的生存時間(單位:秒)key在多少秒後會自動刪除
TTL key 查看key生於的生存時間
PERSIST key 清除生存時間
PEXPIRE key milliseconds 生存時間設置單位爲:毫秒
例子:
192.168.101.3:7002> set test 1 設置test的值爲1
OK
192.168.101.3:7002> get test 獲取test的值
"1"
192.168.101.3:7002> EXPIRE test 5 設置test的生存時間爲5秒
(integer) 1
192.168.101.3:7002> TTL test 查看test的生於生成時間還有1秒刪除
(integer) 1
192.168.101.3:7002> TTL test
(integer) -2
192.168.101.3:7002> get test 獲取test的值,已經刪除
(nil)
rename
作用:重命名key
語法:rename oldkey newkey
type
作用:顯示指定key的數據類型
語法:type key
Redis事務
事務介紹
- Redis的事務是通過MULTI,EXEC,DISCARD和WATCH這四個命令來完成。
- Redis的單個命令都是原子性的,所以這裏確保事務性的對象是命令集合。
- Redis將命令集合序列化並確保處於一事務的命令集合連續且不被打斷的執行。
- Redis不支持回滾的操作。
相關命令
-
MULTI
注:用於標記事務塊的開始。
Redis會將後續的命令逐個放入隊列中,然後使用EXEC命令原子化地執行這個命令序列。
語法:MULTI
-
EXEC
在一個事務中執行所有先前放入隊列的命令,然後恢復正常的連接狀態。
語法:EXEC
-
DISCARD
清楚所有先前在一個事務中放入隊列的命令,然後恢復正常的連接狀態。
語法:DISCARD
-
WATCH
當某個事務需要按條件執行時,就要使用這個命令將給定的鍵設置爲受監控的狀態。
語法:WATCH key [key ....]
注:該命令可以實現redis的樂觀鎖
-
UNWATCH
清除所有先前爲一個事務監控的鍵。
語法:UNWATCH
事務失敗處理
- Redis語法錯誤(編譯器錯誤)
- Redis類型錯誤(運行期錯誤)
爲什麼redis不支持事務回滾?
- 大多數事務失敗是因爲語法錯誤或者類型錯誤,這兩種錯誤,再開發階段都是可以避免的
- Redis爲了性能方面就忽略了事務回滾
Redis實現分佈式鎖
鎖的處理
單應用中使用鎖:單線程多線程
synchronize、Lock
分佈式應用中使用鎖:多進程
分佈式鎖的實現方式
- 數據庫的樂觀鎖
- 給予zookeeper的分佈式鎖
- 給予redis的分佈式鎖
分佈式鎖的注意事項
- 互斥性:在任意時刻,只有一個客戶端能持有鎖
- 同一性:加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
- 避免死鎖:即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
實現分佈式鎖
獲取鎖
方式一(使用set命令實現)
方式二(使用setnx命令實現)
package com.cyb.redis.utils; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class jedisUtils { private static String ip = "192.168.31.200"; private static int port = 6379; private static JedisPool pool; static { pool = new JedisPool(ip, port); } public static Jedis getJedis() { return pool.getResource(); } public static boolean getLock(String lockKey, String requestId, int timeout) { //獲取jedis對象,負責和遠程redis服務器進行連接 Jedis je=getJedis(); //參數3:NX和XX //參數4:EX和PX String result = je.set(lockKey, requestId, "NX", "EX", timeout); if (result=="ok") { return true; } return false; } public static synchronized boolean getLock2(String lockKey, String requestId, int timeout) { //獲取jedis對象,負責和遠程redis服務器進行連接 Jedis je=getJedis(); //參數3:NX和XX //參數4:EX和PX Long result = je.setnx(lockKey, requestId); if (result==1) { je.expire(lockKey, timeout); //設置有效期 return true; } return false; } }
釋放鎖
package com.cyb.redis.utils; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class jedisUtils { private static String ip = "192.168.31.200"; private static int port = 6379; private static JedisPool pool; static { pool = new JedisPool(ip, port); } public static Jedis getJedis() { return pool.getResource(); } /** * 釋放分佈式鎖 * @param lockKey * @param requestId */ public static void releaseLock(String lockKey, String requestId) { Jedis je=getJedis(); if (requestId.equals(je.get(lockKey))) { je.del(lockKey); } } }
Redis持久化方案
導讀
Redis是一個內存數據庫,爲了保證數據的持久性,它提供了兩種持久化方案。
- RDB方式(默認)
- AOF方式
RDB方式
RDB是Redis默認採用的持久化方式。
RDB方式是通過快照(snapshotting)完成的,當符合一定條件時Redis會自動將內存中的數據進行快照並持久化到硬盤。
RDB觸發條件
- 符合自定義配置的快照規則
- 執行save或者bgsave命令
- 執行flushall命令
- 執行主從複製操作
在redis.conf中設置自定義快照規則
1、RDB持久化條件
格式:save <seconds> <changes>
示例:
save 900 1:表示15分鐘(900秒)內至少1個鍵更改則進行快照。
save 300 10:表示5分鐘(300秒)內至少10個鍵被更改則進行快照。
save 60 10000:表示1分鐘內至少10000個鍵被更改則進行快照。
2、配置dir指定rdb快照文件的位置
# Note that you must specify a directory here, not a file name.
dir ./
3、配置dbfilename指定rdb快照文件的名稱
# The filename where to dump the DB
dbfilename dump.rdb
說明
- Redis啓動後會讀取RDB快照文件,將數據從硬盤載入到內存
- 根據數據量大小與結構和服務器性能不同,這個時間也不同。通常將記錄1千萬個字符串類型鍵,大小爲1GB的快照文件載入到內存中需要花費20-30秒鐘。
快照的實現原理
快照過程
- redis使用fork函數複製一份當前進程的副本(子進程)
- 父進程繼續接受並處理客戶端發來的命令,而子進程開始將內存中的數據寫入到硬盤中的臨時文件。
- 當子進程寫入完所有數據後會用該臨時文件替換舊的RDB文件,至此,一次快照操作完成。
注意
- redis在進行快照的過程中不會修改RDB文件,只有快照結束後纔會將舊的文件替換成新的,也就是說任何時候RDB文件都是完整的。
- 這就使得我們可以通過定時備份RDB文件來實現redis數據庫的備份,RDB文件是經過壓縮的二進制文件,佔用的空間會小於內存中的數據,更加利於傳輸。
RDB優缺點
缺點
使用RDB方式實現持久化,一旦redis異常退出,就會丟失最後一次快照以後更改的所有數據。這個時候我們就需要根據具體的應用場景,通過組合設置自動快照條件的方式將可能發生的數據損失控制在能夠接受範圍。如果數據相對來說比較重要,希望將損失降到最小,則可以使用AOF方式進行持久化
優點
RDB可以最大化redis的性能:父進程在保存RDB文件時唯一要做的就是fork出一個字進程,然後這個子進程就會處理接下來的所有保存工作,父進程無需執行任何磁盤I/O操作。同時這個也是一個缺點,如果數據集比較大的時候,fork可能比較耗時,造成服務器在一段時間內停止處理客戶端的請求。
AOF方式
介紹
默認情況下Redis沒有開啓AOF(append only file)方式的持久化
開啓AOF持久化後每執行一條會更改Redis中的數據命令,Redis就會將該命令寫入硬盤中的AOF文件,這一過程顯示會降低Redis的性能,但大部分下這個影響是能夠接受的,另外使用較快的硬盤可以提高AOF的性能。
配置redis.conf
設置appendonly參數爲yes
appendonly yes
AOF文件的保存位置和RDB文件的位置相同,都是通過dir參數設置的
dir ./
默認的文件名是appendonly.aof,可以通過appendfilename參數修改
appendfilename appendonly.aof
AOF重寫原理(優化AOF文件)
- Redis可以在AOF文件體積變得過大時,自動地後臺對AOF進行重寫
- 重寫後的新AOF文件包含了恢復當前數據集所需的最小命令集合。
- 整個重寫操作是絕對安全的,因爲Redis在創建新的AOF文件的過程中,會繼續將命令追加到現有的AOF文件裏面,即使重寫過程中發生停機,現有的AOF文件也不會丟失。而一旦新AOF文件創建完畢,Redis就會從舊AOF文件切換到新AOF文件,並開始對新AOF文件進行追加操作。
- AOF文件有序地保存了對數據庫執行的所有寫入操作,這些寫入操作以Redis協議的格式保存,因此AOF文件的內容非常容易被人讀懂,對文件進行分析(parse)也很輕鬆。
參數說明
- #auto-aof-rewrite-percentage 100:表示當前aof文件大小超過上次aof文件大小的百分之多少的時候會進行重寫。如果之前沒有重寫過,以啓動時aof文件大小爲基準。
- #auto-aof-rewrite-min-size 64mb:表示限制允許重寫最小aof文件大小,也就是文件大小小於64mb的時候,不需要進行優化
同步磁盤數據
Redis每次更改數據的時候,aof機制都會將命令記錄到aof文件,但是實際上由於操作系統的緩存機制,數據並沒有實時寫入到硬盤,而是進入硬盤緩存。再通過硬盤緩存機制去刷新到保存文件中。
參數說明
- appendfsync always:每次執行寫入都會進行同步,這個是最安全但是效率比較低
- appendfsync everysec:每一秒執行
- appendfsync no:不主動進行同步操作,由於操作系統去執行,這個是最快但是最不安全的方式
AOF文件損壞以後如何修復
服務器可能在程序正在對AOF文件進行寫入時停機,如果停機造成AOF文件出錯(corrupt),那麼Redis在重啓時會拒絕載入這個AOF文件,從而確保數據的一致性不會被破壞。
當發生這種情況時,可以以以下方式來修復出錯的AOF文件:
1、爲現有的AOF文件創建一個備份。
2、使用Redis附帶的redis-check-aof程序,對原來的AOF文件進行修復。
3、重啓Redis服務器,等待服務器字啊如修復後的AOF文件,並進行數據恢復。
如何選擇RDB和AOF
- 一般來說,如果對數據的安全性要求非常高的話,應該同時使用兩種持久化功能。
- 如果可以承受數分鐘以內的數據丟失,那麼可以只使用RDB持久化。
- 有很多用戶都只使用AOF持久化,但並不推薦這種方式:因爲定時生成RDB快照(snapshot)非常便於進行數據庫備份,並且RDB恢復數據集的速度也要比AOF恢復的速度要快。
- 兩種持久化策略可以同時使用,也可以使用其中一種。如果同時使用的話,那麼Redis啓動時,會優先使用AOF文件來還原數據。
Redis的主從複製
什麼是主從複製
持久性保證了即使redis服務重啓也不會丟失數據,因爲redis服務重啓後將硬盤上持久化的數據恢復到內存中,但是當redis服務器的硬盤損壞了可能導致數據丟失,不過通過redis的主從複製機制舊可以避免這種單點故障,如下圖:
說明:
- 主redis中的數據有兩個副本(replication)即從redis1和從redis2,即使一臺redis服務器宕機其他兩臺redis服務也可以繼續提供服務。
- 主redis中的數據和從redis上的數據保持實時同步,當主redis寫入數據時通過主從複製機制會複製到兩個從redis服務上。
- 只有一個主redis,可以有多個從redis。
- 主從複製不會阻塞master,在同步數據時,master可以繼續處理client請求
- 一個redis可以即是主從,如下圖:
主從配置
主redis配置
無需特殊配置
從redis配置
修改從服務器上的redis.conf文件
# slaveof <masterip> <masterport> slaveof 192.168.31.200 6379
上邊的配置說明當前【從服務器】對應的【主服務器】的ip是192.168.31.200,端口是6379.
實現原理
- slave第一次或者重連到master發送一個SYNC的命令。
- master收到SYNC的時候,會做兩件事
- 執行bgsave(rdb的快照文件)
- master會把新收到的修改命令存入到緩衝區
缺點:沒有辦法對master進行動態選舉
Redis Sentinel哨兵機制
簡介
Sentinel(哨兵)進程是用於監控redis集羣中Master主服務器工作的狀態,在Master主服務器發生故障的時候,可以實現Master和Slave服務器的切換,保證系統的高可用,其已經被集成在redis2.6+的版本中,Redis的哨兵模式到2.8版本之後就穩定了下來。
哨兵進程的作用
- 監控(Monitoring):哨兵(Sentinel)會不斷地檢查你的Master和Slave是否運作正常。
- 提醒(Notification):當被監控的某個Redis節點出現問題時,哨兵(Sentinel)可以通過API向管理員或者其他應用程序發送通知。
- 自動故障遷移(Automatic failover):當一個Master不能正常工作時,哨兵(Sentinel)會開始一次自動故障遷移操作。
- 它會將失效Master的其中一個Slave升級爲新的Master,並讓失效Master的其他Slave改爲複製新的Master;
- 當客戶端視圖連接失效的Master時,集羣也會向客戶端返回新Master的地址,使得集羣可以使用現在的Master替換失效的Master。
- Master和Slave服務器切換後,Master的redis.conf、Slave的redis.conf和sentinel.conf的配置文件的內容都會發生相應的改變,即Master主服務器的redis.conf配置文件中會多一行Slave的配置,sentinel.conf的監控目標會隨之調換。
哨兵進程的工作方式
- 每個Sentinel(哨兵)進程以每秒鐘一次的頻率向整個集羣中的Master主服務器,Slave從服務器以及其他Sentinel(哨兵)進程發送一個PING命令。
- 如果一個實例(instance)距離最後一次有效回覆PING命令的時間超過down-after-milliseconds選項所指定的值,則這個實例會被Sentinel(哨兵)進程標記爲主觀下線(SDOWN)。
- 如果一個Master主服務器被標記爲主觀下線(SDOWN),則正在監視這個Master主服務器的所有Sentinel(哨兵)進程要以每秒一次的頻率確認Master主服務器確實進入了主觀下線狀態。
- 當有足夠數量的Sentinel(哨兵)進程(大於等於配置文件指定的值)在指定的時間範圍內確認Master主服務器進入了主觀下線狀態(SDOWN),則Master主服務器會被標記爲客觀下線(ODOWN)。
- 在一般情況下,每個Sentinel(哨兵)進程會以每10秒一次的頻率向集羣中的所有Master主服務器、Slave從服務器發送INFO命令。
- 當Master主服務器被Sentinel(哨兵)進程標記爲客觀下線(ODOWN)時,Sentinel(哨兵)進程向下線的Master主服務器的所有Slave從服務器發送INFO命令的頻率會從10秒一次改爲每秒一次。
- 若沒有足夠數量的Sentinel(哨兵)進程同意Master主服務器下線,Master主服務器的客觀下線狀態就會被移除。若Master主服務器重新向Sentinel(哨兵)進程發送PING命令返回有效回覆,Master主服務器的主觀下線狀態就會被移除。
實現
修改從機的sentinel.conf
sentinel monitor mymaster 192.168.127.129 6379 1
啓動哨兵服務器
./redis-sentinel sentinel.conf
Redis Cluster集羣
redis-cluster架構圖
架構細節
- 所有的redis節點彼此互聯(PING-PING機制),內部使用二進制協議優化傳輸速度和帶寬。
- 節點的fail是通過集羣中超過半數的節點檢測失效時才生效。
- 客戶端與redis節點直連,不需要中間proxy層,客戶端不需要連接集羣所有節點,連接集羣中任何一個可用節點即可。
- redis-cluster把所有的物理節點映射到[0-16383]slot上,cluster負責維護node<->slot<->value
Redis集羣中內置了16384個哈希槽,當需要在Redis集羣中放置一個key-value時,redis先對key使用crc16算法算出一個結果,然後把結果對16384求餘數,這樣每個key都會對應一個編號在0-16384之間的哈希槽,redis會根據節點數量大致均等的將哈希槽映射到不同節點。
redis-cluster投票:容錯
- 集羣中所有master參與投票,如果半數以上master節點與其中一個master節點通信超過(cluster-node-timeout),認爲該master節點掛掉。
- 什麼時候整個集羣不可用(cluster_state:fail)?
- 如果集羣任意master掛掉,且當前master沒有slave,則集羣進入fail狀態。也可以理解成集羣的[0-16384]slot映射不完全時進入fail狀態。
- 如果集羣超過半數以上master掛掉,無論是否有slave,集羣進入fail狀態。
安裝Ruby環境
導讀
redis集羣需要使用集羣管理腳本redis-trib.rb,它的執行相應依賴ruby環境。
安裝
安裝ruby
yum install ruby
yum install rubygems
將redis-3.2.9.gen拖近Linux系統
安裝ruby和redis的接口程序redis-3.2.9.gem
gem install redis-3.2.9.gem
複製redis-3.2.9/src/redis-trib.rb 文件到/usr/local/redis目錄
cp redis-3.2.9/src/redis-trib.rb /usr/local/redis/ -r
安裝Redis集羣(RedisCluster)
Redis集羣最少需要三臺主服務器,三臺從服務器,端口號分別爲7001~7006。
創建7001實例,並編輯redis.conf文件,修改port爲7001。
修改redis.conf配置文件,打開Cluster-enable yes
重複以上2個步驟,完成7002~7006實例的創建,注意端口修改
啓動所有的實例
創建Redis集羣
./redis-trib.rb create --replicas 1 192.168.242.129:7001 192.168.242.129:7002 192.168.242.129:7003 192.168.242.129:7004 192.168.242.129:7005 192.168.242.129:7006
>>> Creating cluster
Connecting to node 192.168.242.129:7001: OK
Connecting to node 192.168.242.129:7002: OK
Connecting to node 192.168.242.129:7003: OK
Connecting to node 192.168.242.129:7004: OK
Connecting to node 192.168.242.129:7005: OK
Connecting to node 192.168.242.129:7006: OK
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.242.129:7001
192.168.242.129:7002
192.168.242.129:7003
Adding replica 192.168.242.129:7004 to 192.168.242.129:7001
Adding replica 192.168.242.129:7005 to 192.168.242.129:7002
Adding replica 192.168.242.129:7006 to 192.168.242.129:7003
M: d8f6a0e3192c905f0aad411946f3ef9305350420 192.168.242.129:7001
slots:0-5460 (5461 slots) master
M: 7a12bc730ddc939c84a156f276c446c28acf798c 192.168.242.129:7002
slots:5461-10922 (5462 slots) master
M: 93f73d2424a796657948c660928b71edd3db881f 192.168.242.129:7003
slots:10923-16383 (5461 slots) master
S: f79802d3da6b58ef6f9f30c903db7b2f79664e61 192.168.242.129:7004
replicates d8f6a0e3192c905f0aad411946f3ef9305350420
S: 0bc78702413eb88eb6d7982833a6e040c6af05be 192.168.242.129:7005
replicates 7a12bc730ddc939c84a156f276c446c28acf798c
S: 4170a68ba6b7757e914056e2857bb84c5e10950e 192.168.242.129:7006
replicates 93f73d2424a796657948c660928b71edd3db881f
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join....
>>> Performing Cluster Check (using node 192.168.242.129:7001)
M: d8f6a0e3192c905f0aad411946f3ef9305350420 192.168.242.129:7001
slots:0-5460 (5461 slots) master
M: 7a12bc730ddc939c84a156f276c446c28acf798c 192.168.242.129:7002
slots:5461-10922 (5462 slots) master
M: 93f73d2424a796657948c660928b71edd3db881f 192.168.242.129:7003
slots:10923-16383 (5461 slots) master
M: f79802d3da6b58ef6f9f30c903db7b2f79664e61 192.168.242.129:7004
slots: (0 slots) master
replicates d8f6a0e3192c905f0aad411946f3ef9305350420
M: 0bc78702413eb88eb6d7982833a6e040c6af05be 192.168.242.129:7005
slots: (0 slots) master
replicates 7a12bc730ddc939c84a156f276c446c28acf798c
M: 4170a68ba6b7757e914056e2857bb84c5e10950e 192.168.242.129:7006
slots: (0 slots) master
replicates 93f73d2424a796657948c660928b71edd3db881f
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
[root@localhost-0723 redis]#
命令客戶端連接集羣
命令:
./redis-cli -h 127.0.0.1 -p 7001 -c
注:-c表示是以redis集羣方式進行連接
./redis-cli -p 7006 -c
127.0.0.1:7006> set key1 123
-> Redirected to slot [9189] located at 127.0.0.1:7002
OK
127.0.0.1:7002>
查看集羣的命令
查看集羣狀態
127.0.0.1:7003> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_sent:926
cluster_stats_messages_received:926
查看集羣中的節點
127.0.0.1:7003> cluster nodes
7a12bc730ddc939c84a156f276c446c28acf798c 127.0.0.1:7002 master - 0 1443601739754 2 connected 5461-10922
93f73d2424a796657948c660928b71edd3db881f 127.0.0.1:7003 myself,master - 0 0 3 connected 10923-16383
d8f6a0e3192c905f0aad411946f3ef9305350420 127.0.0.1:7001 master - 0 1443601741267 1 connected 0-5460
4170a68ba6b7757e914056e2857bb84c5e10950e 127.0.0.1:7006 slave 93f73d2424a796657948c660928b71edd3db881f 0 1443601739250 6 connected
f79802d3da6b58ef6f9f30c903db7b2f79664e61 127.0.0.1:7004 slave d8f6a0e3192c905f0aad411946f3ef9305350420 0 1443601742277 4 connected
0bc78702413eb88eb6d7982833a6e040c6af05be 127.0.0.1:7005 slave 7a12bc730ddc939c84a156f276c446c28acf798c 0 1443601740259 5 connected
127.0.0.1:7003>
維護節點
集羣創建完成後可以繼續向集羣中添加節點。
添加主節點
添加7007節點作爲新節點
命令:./redis-trib.rb add-node 127.0.0.1:7007 127.0.0.1:7001
查看集羣節點發現7007已加到集羣中
hash槽重新分配
添加完主節點需要對主節點進行hash槽分配,這樣該主節纔可以存儲數據。
查看集羣中槽佔用情況
redis集羣有16384個槽,集羣中的每個節點分配自己槽,通過查看集羣節點可以看到槽佔用情況。
給剛添加的7007節點分配槽
第一步:連上集羣(連接集羣中任意一個可用節點都行)
./redis-trib.rb reshard 192.168.101.3:7001
第二步:輸入要分配的槽數量
輸入500,表示要分配500個槽
第三步:輸入接收槽的節點id
輸入:15b809eadae88955e36bcdbb8144f61bbbaf38fb
ps:這裏準備給7007分配槽,通過cluster node查看7007節點id爲:
15b809eadae88955e36bcdbb8144f61bbbaf38fb
第四步:輸入源節點id
輸入:all
第五步:輸入yes開始移動槽到目標節點id
輸入:yes
添加從節點
添加7008從節點,將7008作爲7007的從節點
命令:
./redis-trib.rb add-node --slave --master-id 主節點id 新節點的ip和端口 舊節點ip和端口
執行如下命令:
./redis-trib.rb add-node --slave --master-id cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 192.168.101.3:7008 192.168.101.3:7001
cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 是7007結點的id,可通過cluster nodes查看。
nodes查看
注意:如果原來該節點在集羣中的配置信息已經生成到cluster-config-file指定的配置文件中(如果cluster-config-file沒有指定則默認爲nodes.conf),這時可能會報錯
[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0
解決辦法是刪除生成的配置文件nodes.conf,刪除後再執行./redis-trib.rb add-node指令
查看集羣中的節點,剛添加7008爲7007的從節點
刪除節點
命令:
./redis-trib.rb del-node 127.0.0.1:7005 4b45eb75c8b428fbd77ab979b85080146a9bc017
刪除已經佔用hash槽的節點會失敗,報錯如下
[ERR] Node 127.0.0.1:7005 is not empty! Reshard data away and try again.
需要將該節點佔用的hash槽分配出去
Jedis連接集羣
創建JedisCluster類連接Redis集羣
@Test public void testJedisCluster() throws Exception { //創建一連接,JedisCluster對象,在系統中是單例存在 Set<HostAndPort> nodes = new HashSet<>(); nodes.add(new HostAndPort("192.168.242.129", 7001)); nodes.add(new HostAndPort("192.168.242.129", 7002)); nodes.add(new HostAndPort("192.168.242.129", 7003)); nodes.add(new HostAndPort("192.168.242.129", 7004)); nodes.add(new HostAndPort("192.168.242.129", 7005)); nodes.add(new HostAndPort("192.168.242.129", 7006)); JedisCluster cluster = new JedisCluster(nodes); //執行JedisCluster對象中的方法,方法和redis一一對應。 cluster.set("cluster-test", "my jedis cluster test"); String result = cluster.get("cluster-test"); System.out.println(result); //程序結束時需要關閉JedisCluster對象 cluster.close(); }
使用Spring
配置applicationContext.xml
<!-- 連接池配置 --> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <!-- 最大連接數 --> <property name="maxTotal" value="30" /> <!-- 最大空閒連接數 --> <property name="maxIdle" value="10" /> <!-- 每次釋放連接的最大數目 --> <property name="numTestsPerEvictionRun" value="1024" /> <!-- 釋放連接的掃描間隔(毫秒) --> <property name="timeBetweenEvictionRunsMillis" value="30000" /> <!-- 連接最小空閒時間 --> <property name="minEvictableIdleTimeMillis" value="1800000" /> <!-- 連接空閒多久後釋放, 當空閒時間>該值 且 空閒連接>最大空閒連接數 時直接釋放 --> <property name="softMinEvictableIdleTimeMillis" value="10000" /> <!-- 獲取連接時的最大等待毫秒數,小於零:阻塞不確定的時間,默認-1 --> <property name="maxWaitMillis" value="1500" /> <!-- 在獲取連接的時候檢查有效性, 默認false --> <property name="testOnBorrow" value="true" /> <!-- 在空閒時檢查有效性, 默認false --> <property name="testWhileIdle" value="true" /> <!-- 連接耗盡時是否阻塞, false報異常,ture阻塞直到超時, 默認true --> <property name="blockWhenExhausted" value="false" /> </bean> <!-- redis集羣 --> <bean id="jedisCluster" class="redis.clients.jedis.JedisCluster"> <constructor-arg index="0"> <set> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.101.3"></constructor-arg> <constructor-arg index="1" value="7001"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.101.3"></constructor-arg> <constructor-arg index="1" value="7002"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.101.3"></constructor-arg> <constructor-arg index="1" value="7003"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.101.3"></constructor-arg> <constructor-arg index="1" value="7004"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.101.3"></constructor-arg> <constructor-arg index="1" value="7005"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.101.3"></constructor-arg> <constructor-arg index="1" value="7006"></constructor-arg> </bean> </set> </constructor-arg> <constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg> </bean>
測試代碼
private ApplicationContext applicationContext; @Before public void init() { applicationContext = new ClassPathXmlApplicationContext( "classpath:applicationContext.xml"); } // redis集羣 @Test public void testJedisCluster() { JedisCluster jedisCluster = (JedisCluster) applicationContext .getBean("jedisCluster"); jedisCluster.set("name", "zhangsan"); String value = jedisCluster.get("name"); System.out.println(value); }
緩存穿透、緩存擊穿、緩存雪崩
緩存數據步驟
- 查詢緩存,如果沒有數據,則查詢數據庫
- 查詢數據庫,如果數據不爲空,將結果寫入緩存
緩存穿透
什麼叫緩存穿透?
一般的緩存系統,都是按照key去緩存查詢,如果不存在對應的value,就應該去後端系統查詢。如果key對應的value是一定不存在的,並且對key併發請求量很大,就會對後端系統造成很大的壓力。這就叫做緩存穿透。
如何解決?
- 對查詢結果爲空的情況也進行緩存,緩存時間設置短一點,或者該key對應的數據insert了之後清楚緩存。
- 對一定不存在的key進行過濾。可以把所有的可能存在的key放到一個大的Bitmap中,查詢時通過該Bitmap過濾。(布隆表達式)
緩存雪崩
什麼叫緩存雪崩?
當緩存服務器重啓或者大量緩存集合中某一個時間段失效,這樣在失效的時候,也會給後端系統帶來很大壓力。
如何解決?
- 在緩存失效後,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
- 不同的key,設置不同的過期時間,讓緩存失效的時間點儘量均勻。
- 做二級緩存,A1爲原始緩存,A3爲拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置爲短期,A2設置爲長期。
緩存擊穿
什麼叫緩存擊穿?
對於一些設置了過期時間的key,如果這些key可能會在某些時間點被超高併發地訪問,是一種非常“熱點”的數據。這個時候,需要考慮一個問題:“緩存”被擊穿的問題,這個和緩存雪崩的區別在於這裏針對某一key緩存,前者則是很多key。
緩存在某個時間點過期的時候,恰好在這個時間點對這個key有大量的併發請求過來,這些請求發現緩存過期一般都會從後端DB加載數據並回設到緩存,這個時候大併發的請求可能會瞬間把後端DB壓垮。
如何解決?
使用redis的setnx互斥鎖先進行判斷,這樣其他線程就處於等待狀態,保證不會有大併發操作去操作數據庫。
if(redis.setnx()==1){
//查數據庫
//加入線程
}
緩存淘汰策略
- 當 Redis 內存超出物理內存限制時,內存的數據會開始和磁盤產生頻繁的交換 (swap)。交換會讓 Redis 的性能急劇下降,對於訪問量比較頻繁的 Redis 來說,這樣龜速的存取效率基本上等於不可用。
- 在生產環境中我們是不允許 Redis 出現交換行爲的,爲了限制最大使用內存,Redis 提供了配置參數 maxmemory 來限制內存超出期望大小。
- 當實際內存超出 maxmemory 時,Redis 提供了幾種可選策略 (maxmemory-policy) 來讓用戶自己決定該如何騰出新的空間以繼續提供讀寫服務。
策略
noeviction:當內存不足以容納新寫入數據時,新寫入操作會報錯。
allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key。
allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個key。
volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key。
volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key。
volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除。
使用
修改redis.conf的maxmemory
,設置最大使用內存:
maxmemory 1024000
修改redis.conf的maxmemory-policy
,設置redis緩存淘汰機制:
maxmemory-policy noeviction