關系型數據庫 VS 非關系型數據庫(NoSQL)
關系型數據庫
我們過去使用的 mysql、Oracle 都屬於關系型數據庫。關系型數據庫的特點是數據表之間可以存在聯系,表內每列數據也存在關聯,同時支持事務、復雜的鎖機制,這樣可以支持復雜操作,在查詢時也可以很快得到與之相關聯的數據,但同時這些也成為限制數據庫速度的因素,在存儲大數據的表中進行查詢修改、拓展表時會格外消耗時間。在過去受硬件水平的限制,系統架構往往比較簡單,並發量也比較小,但隨着硬件水平的提高,系統變得越拉越龐大,需要存儲的數據量也越來越大,很多業務都需要在海量數據中快速查找到所需要的數據,此時靠關系型數據庫已經無法滿足了,所以提出了非關系型數據庫的概念。
非關系型數據庫
非關系型數據庫中的數據間沒有關聯關系,數據結構簡單,在數據的增刪改查時速度都比較快,缺點是不能支持復雜的操作,比如事務的ACID、復雜的鎖機制,也因為數據間沒有關聯關系所以在查詢符合條件的數據會很快。所以我們通常只使用 NoSQL 來存儲一些簡單、不需要復雜操作的數據,如某個用戶的粉絲數,某個博客的點贊數、字數等。
四種聚合模型
總結
傳統的關系型數據庫因為內部功能多,數據間存在關聯,導致在數據量過大時操作起來效率比較低。非關系型數據庫則與之相反從而得到了很好的性能,在日益要求性能的今天起到了很好的作用,但是因為其不能實現復雜功能,所以對於一些需要復雜操作(讀寫鎖、事務、多表聯查等)還是使用關系型數據庫,而對於一些簡單數據,不需要太復雜操作的可以使用非關系型數據庫。
Redis
redis 是一個單線程(底層使用IO多路復用模型)分布式數據庫,也是一個典型的 NoSQL,它的執行效率非常高,其原因主要有以下幾點:
1、是非關系型數據庫,數據結構簡單,且沒有復雜的關聯關系。
2、單線程操作,避免了多線程之間切換和競爭,並通過IO多路復用模型來避免傳統 BIO 的低效執行。
3、數據存儲在內存,讀取時直接從內存中讀取。
基礎知識
1、在安裝后相應的執行命令和配置文件默認在 /usr/local/bin/ 目錄下
2、redis 默認有 16個數據庫,0-15,默認是0號數據庫,可以通過 " select 數據庫號" 來切換數據庫。數據庫個數可以在 redis.conf 中配置。
3、redis 是統一密碼管理,默認情況下沒有開啟密碼,可以在配置文件 redis.conf 中配置開啟
4、默認端口是 6379。
5、啟動服務器:redis-server 配置文件全路徑。配置文件可以是自定義的配置文件。啟動客戶端:redis -cli -p 6379。
五大基本數據類型及常用方法
String
最基本的數據類型,雖然為 String 類型,但是其 value 可以為 string 也可以為 int 類型。其可以用於實現計數器,也可以用於進行 json 格式的對象存儲。
常用方法:
set / get / del / append / strlen : 設值 / 獲值 / 刪值 / 末尾添加值 / 獲取長度
Incr / decr / incrby key n / decrby key n: 自增 / 自減 / 增加 n / 減去 n
getrange n1 n2 / setrange n1 n2 val: 截取下標n1,n2之間的值(從0開始,兩邊都是閉區間) / 設置下標n1,n2區間的值
setex key time val / setnx key val: 設值並指定過期時間(單位為秒) / 在 val 不存在或者已過期時設值
mset / mget / msetnx: 批量(進行設置 / 獲值 / 非空設值)
List
底層是鏈表結構,方法名開頭的 l 表示 left,r 表示 right。可以用於實現消息隊列、文章列表。
常用方法:
lpush / rpush / lrange n1 n2: 左添 / 右添 / 從左開始截取下標n1,n2之間的內容(0開始,兩邊都是閉區間)
lpop / rpop: 類似於消息隊列和棧的出棧操作,分別是 (左出棧 / 右出棧)
lindex: 從左邊計算獲取指定下標的值
lrem key n value: 對 key 對應的 list 數據從左邊開始刪除 n 個 value
ltrim key n1 n2: 獲取 key 對應的 list 值,截取 n1 到 n2 之間的值再賦值覆蓋當前的 list 值。
rpoplpush list1 list2: 將list1中的右邊尾部數據移到 list2 的左邊頭部
lset key index value: 左邊開始修改指定索引上的值
linsert key before/after val1 val2: 從左開始,獲取第一個 val1,在其 (左 / 右) 插入 val2
Set
和 java 中的 Set 集合一樣,唯一無需的結構。可以用來存儲好友,然后計算共同好友;抽獎,隨機pop出棧元素。
常用方法:
sadd / smembers / sismember key val: 添加(可以批量添加) / 顯示所有值 / 查看是否存在val
scard key: 獲取集合中元素的個數
srem key val: 刪除某個元素
scrandmember key n: 隨機出 n 個元素(不會從集合中移除改元素)
spop key: 隨機出棧一個元素(會從集合中移除該元素)
smove key1 key2 key1中的某個值val: 將 key1 對應集合中的某個值val 移入 key2 對應的集合中
sdiff key1 key2...: 獲取存在與於key1 對應集合中但不存在后面所有key對應集合中的元素
sinter key1 key2...: 獲取存在於 key1 對應的集合中且存在於后面所有key對應集合中的元素
sunion key1 key2...: 獲取存在於 key1 對應的集合中或者存在於后面所有key對應集合中的元素
Hash
類似於 java 中的 Map 結構,可以用於存儲對象。
常用方法:
hset / hget / hmset / hmget / hgettall key/ hdel key key(hash): 設值 / 獲值 / 批量設值 / 批量獲取 / 獲取 key 對應所有的鍵值對數據 / 刪除 key 對應 hash 結構中的 key(hash) 對應的值。
hlen key : 獲取元素個數
hexists key key(hash): 查看 key 對應 hash 結構的 key(hash) 對應的值是否存在
hkeys key / hvals key: 獲取 key 對應 hash 結構所有(key 值 / val 值)
hincrby key key(hash) val / hincrbyfloat key key(hash) val: 對 key 對應 hash 結構中的 key(hash) 對應的值添加(整數 / 小數) val
hsetnx key key(hash) val: 不存在時賦值
ZSet
每個數據關聯一個分數,排序時會按分數升序排列,相當與一個有序 Set。可以用於實現各種排行榜。
常用方法:
zadd key score val / zrange key n1 n2 withscores: 添加 / 獲取所有值
zrangebysorce key score1 score2: 查找 score1 與 score2 之間的數據
zrem key val: 刪除某個值
zcard key / zcount key score1 score2 / zrank key value / zscore key value: 獲取數據數 / 統計在 score1 與 score2 之間元素的個數 / 獲取指定數據所在下標 / 獲取指定數據的分數
zrevrant key value: 逆序獲取指定值的下標
zrevrangebyscore key score1 score2: 逆序獲取 score1 與 score2 之間的數據
Key 及其他操作方法
keys *: 獲取所有的key
exists key: 查看是否存在key,返回1是存在,0不存在
expire key 時間: 為 key 設置過期時間,單位是秒
ttl key: 查看 key 還有多久過期,-1表示永不過期,-2表示已過期。
type key: 查看 key 是什么類型
Dbsize: 查看當前數據的 key 數量
Flushdb: 清除當前庫中的所有數據
Flushall: 清除所有庫中的所有數據
配置文件 redis.conf(Linux)
下面列出的各個配置可能存在多出,因為參考了多個版本的配置文件,同時可能存在遺漏,請見諒。修改配置文件的原則是不要動默認的配置文件,應該將默認配置文件復制一份到指定目錄,然后去處理,防止修改錯誤無法恢復。
Units:配置大小單位,可以用於自定義一些度量單位,底層單位只支持 bytes,大小寫不敏感。
includes:可以來引入其他配置文件
general:
Daemonize: 是否以守護進程的方式運行,默認為no,也就是服務器窗口關閉后就會關閉服務器,如果需要后台運行可以設置為 yes。
protected-mode: 保護模式是否開啟,默認是 yes。關閉其他任何ip 地址都可以來訪問連接,關閉后必須通過 bind 來配置相應的 ip 后,其才能連接。
Pidfile: 如果 redis 以守護進程的方式運行時,系統就會將這個守護進程的 id 記錄下來,記錄的位置就是通過 Pidfile 來配置,默認是 /var/run/redis.pid
Port: 端口端口號
Tcp-backlog: 設置 tcp 的連接隊列長度,其值 = 未完成三次握手隊列 + 已完成三次握手隊列。默認是 511,如果並發量比較大時可以設置為 2048。
timeout: 設置最大空閑時間,也就是多久沒有操作服務器就會自動斷開。默認是0,也就是永不斷開。
Bind: 設置允許訪問的 ip 地址,在 protected-mode 為 yes 時使用。
tcp-keepalive: 服務器會檢測客戶端是否還在使用,如果一段時間內客戶端沒有操作,那么 redis 服務器就會釋放這條連接,tcp-keepalive 就是設置客戶端的最大空閑時間的,默認是0,也就是永不斷開,而官方推薦是 60,也就是超過60秒沒操作服務器就會回收這一條的連接。
loglevel: 日志級別。從高到低分別為 warning、notice、verbose、debug。默認是 notice。
logfile: 日志文件存放位置。
Syslog-enable: 是否把輸出日志保存到日志文件中。默認關閉
Syslog-ident: 設置日志中的日志標識。
Syslog-facility: 指定輸出 syslog 的設備,可以為 user 或 local0-local7。
Databases: 設置數據庫個數。默認16
always-show-logo: 是否總是顯示 logo,默認 yes。
REPLICATION(主從復制相關):
一般配置主從復制時,需要自己手動通過 salveof 命令來配置從機,而如果各機器事先就決定好角色,可以直接在配置文件中配置來避免手動配置。
masterip: 主機的 ip 地址
masterport: 主機的端口號
masterpassword: 主機的密碼
SNAPAHOTTING快照(RDB相關):
save : 設置RDB自動備份的時間間隔,save 配置的格式是 "save 時間間隔 次數",默認配置是
save 900 1
save 300 10
save 60 10000
從第一行開始作用分別是在 900s 內執行了一次寫操作就會觸發一次備份;300s 內執行了 10次寫操作就會觸發一次備份;60s 內執行了10000次寫操作就會觸發一次備份。
一般來說使用默認配置就可以,如果想要禁用自動備份可以將其刪除或者改成 save ""。
Stop-writes-on-bgsave-error: 通過bgsave備份出錯時,主線程是否繼續工作。
rdbcompression: 是否壓縮 rdb文件,需要消耗一些 cpu 資源。壓縮會使文件占用空間減小。
rdbchecknum: 存儲快照后是否進行數據校驗。如果想要提高執行效率可以關閉。
dbfilename: 備份的文件名。默認是 dump.rdb。
dir: RDB、AOF 備份文件的存儲地址。在服務器啟動時恢復數據也會在 dir 配置的目錄中讀取對應的備份文件。默認在執行目錄下。可以通過 "config get dir" 獲取 dir 。
APPEND ONLY MODE追加(AOF相關)
appendonly: 是否啟用AOF。
appendfilename: 備份文件的文件名,默認為 appendonly.aof。
appendfsync: 存儲策略。共有以下三種。
always:同步持久化,每次寫操作后都會立刻記錄到磁盤,性能較差但是數據完整性最好。
everysec:異步操作,每一秒執行一次記錄,可能會有少量數據丟失。
no:不進行記錄。
No-appendfsync-on-rewrite: rewrite 時是否執行存儲策略(進行存儲)。一般使用默認 no 即可,保證數據安全性。
Auto-aof-rewrite-min-Size: 設置 rewrite 觸發的最小基准值。
Auto-aof-rewrite-percentage: 設置 rewrite 觸發的超出百分比基准值。
Security(安全權限相關)
默認情況下,安全權限是關閉的,在客戶端連接時不需要輸入密碼,執行操作也不需要密碼,但是如果開啟了安全檢測,那么所有客戶端在執行命令時都需要先執行 "auth 密碼" 來驗證身份。
可以在配置文件中配置 "requirepass 密碼" 來配置密碼,也可以在命令行執行 "config set requirepass 密碼" 來配置,查看密碼可以使用 "config get requirepass"。如果想要關閉可以直接在命令行執行 "config set requirepass "" " 。
limit
maxclients: 最大連接的客戶端數,默認無限制,下同。
maxmemory: redis 服務器占用的最大內存。默認無限制,推薦是最大內存的四分之三。單位 byte 。 1024 * 1024 * 100 byte = 100MB。可以通過 info memory 來查看 redis 內存使用情況
maxmemory-policy: redis 內存淘汰策略。類似與線程池的拒絕策略,就是當存儲新數據時內存空間不足執行的操作。策略主要有以下六種。
1)volatile-lru:使用 LRU 算法移除 key,只對設置了過期時間的鍵
2)allkeys-lru(常用):使用 LRU 算法移除 key。
3)volatile-random:在過期集合中移除隨機的 key,只對設置了過期時間的鍵
4)allkeys-random:移除隨機的 key。
5))volatile-ttl:移除那些 TTL 最小的 key,即那些最先要過期的 key
6)noeviction(默認):不進行移除。針對寫操作,只是返回錯誤信息。
7)allkeys-lfu:使用 LFU 算法移除 key
8)volatile-lfu:使用 LFU 算法移除 key,只對設置了過期時間的鍵
LRU 算法就是被使用的數據會被移到頭部,未使用的就會慢慢向尾部靠近,在移除時從尾部移除。
LFU 算法是按頻率排列,頻率低的移到尾部,頻率高的移到頭部。
maxmemory-samples: 設置樣本數。在清除時,因為LRU算法和最小TTL算法都並非是精確的算法,而是估算值,所以我們可以設置一個具體的大小,redis會抽出這個數量的數據並根據算法進行清除。
redis 刪除策略
定時刪除:在 key 到達過期時間后,就會立刻刪除。優點是可以最大程度利用內存,缺點是每次刪除都需要調用CPU,在過期數據多時CPU調用次數過於頻繁,影響CPU執行業務代碼效率。
惰性刪除:在 key 過期后,不會刪除,完全依靠配置文件 maxmemory-plicy 配置的內存淘汰策略來在內存不足時刪除。優點是對 CPU 友好,缺點是對內存不友好,可能會浪費大量的內存。
定期刪除:是前兩種的折中方案,可以通過添加 " hz 10 " (默認配置也是10)來選擇一秒鍾執行定期刪除的次數。定期刪除會進行隨機抽樣,刪除抽樣中已過期的 key ,同時會消耗規定峰值以下的 CPU,不影響業務代碼的效率。但是這樣還是會遺漏一部分已過期的 key (一直未被隨機抽樣到),那么就需要依靠前面的內存淘汰策略了。
RDB
默認的持久化方式。其本質就是保存當前時刻的數據快照。默認生成文件名是 dump.rdb。
默認會在一段時間內自動保存數據,規則就是上面配置文件 RDB 部分 save 的配置。需要注意的是,RDB 的持久化方式分為 save 與 bgsave。
save 是中斷當前進程,然后進行持久化操作,等到持久化完成后再繼續執行其他操作;
bgsave 是 fork 一個子進程,fork 出來的子進程會擁有當前進程所有的內存數據,然后子進程單獨進行持久化操作,不會阻塞當前進程執行。
RDB 持久化觸發時機
1、來自配置文件中配置的自動備份,也就是上面說得 save。其實現方式是 bgsave。
2、執行命令 save 或 bgsave。執行 save 就是中斷狀態來實現的;而 bgsave 則是 fork 子進程來實現,不會中斷當前進程執行。
3、執行flushdb、flushall、shutdown 命令后在命令生效前也會先備份一次。其實現方式是 save。
RDB 文件恢復
默認情況下只需要將備份文件放在啟動目錄下然后在啟動目錄下啟動服務器即可。
啟動目錄指的是啟動 redis-server 命令的目錄,在備份時會自動備份到該目錄下,比如在 /temp/ 啟動,那么默認會讀取該目錄下的 dump.rdb 文件,備份也會在該目錄,如果下次啟動在 /myredis/ 下,那么也會讀取 /myredis/下的 dump.rdb 文件,備份數據也是會在該目錄下備份。這個目錄也可以自定義,配置參數是 redis.conf 中 dir 參數。
如何關閉 RDB 的自動備份
可以在配置文件中將 save 改成 save "",也可以直接執行 " redis-cli config set save "" "
優勢
1、執行效率高,適用於大規模數據的備份恢復。自動備份不會影響主線程工作。
2、備份的文件占用空間小。其備份的是數據快照,相對於 AOF 來說文件大小要小一些。
劣勢
1、可能會造成部分數據丟失。因為是自動備份,所以如果修改的數據量不足以觸發自動備份,同時發生斷電等異常導致 redis 不能正常關閉,所以也沒有觸發關閉的備份,那么在上一次備份到異常宕機過程中發生的寫操作就會丟失。
2、自動備份通過 fork 進程來執行備份操作,而 fork 進程會將當前進程的內存數據完整的復制一份,所以這個過程占用的空間是原來的 2 倍,可能會導致內存不足。
AOF
AOF 是在 RDB 的補充備份方式,其本質是保存執行的每一條寫操作(包括flushdb、flushall),所以其產生的備份文件是可以直接閱讀的。默認備份文件名是 appendonly.aof,保存位置和 RDB 備份文件一樣。因為其保存的是每一條寫操作,所以會比較占用 CPU,同時生成的備份文件也比較占空間,所以默認是關閉的。使用時需要在配置文件中將其打開。
AOF 持久化規則
AOF 備份也是采用自動備份,但是備份的頻率會比 RDB 要高,其備份方式分為三種:
1、always:同步持久化,每次寫操作后都會立刻記錄到磁盤,性能較差但是數據完整性最好。
2、everysec(默認):異步操作,每一秒執行一次記錄,可能會有少量數據丟失,但是性能更好。
3、no:不進行記錄。
除此之外,redis 為了防止隨着寫操作越來越多,AOF 的備份文件越來越大,設置了 rewrite 機制。
Rewrite 機制類似於 RDB 的 bgsave,同樣在后台開啟一個子進程,其內存數據與當前進程的數據一致,然后將這些數據生成對應的寫操作,然后將這些寫操作依次一個臨時文件,等到全部寫入完畢,再將這個臨時文件覆蓋掉默認備份文件 appendonly.aof,在覆蓋過程中 AOF 自動備份會被阻塞。因為 Rewrite 需要消耗額外的 CPU,同時在寫入原文件時還會造成阻塞,所以應該避免執行 Rewrite。
Rewrite觸發機制:
1、通過配置文件中配置的規則默認觸發。
1)Auto-aof-rewrite-min-Size:觸發重寫最小的基准值。默認是 64M。
2)Auto-aof-rewrite-percentage:觸發重寫的超出百分比。默認是 100%。
如果配置按照上面默認配置,那么觸發 AOF 自動配置需要當前 AOF 文件超過 64M,同時文件大小達到了上一次 Rewrite 后文件大小的兩倍。如果沒有 Rewrite 過那么會在達到 64M 后觸發第一次。
2、手動通過 " bgrewriteaof " 來觸發重寫。
AOF 文件恢復
和 RDB 備份文件恢復一樣,在默認情況下將備份文件放在啟動目錄,然后啟動服務器即可。
文件修復:因為 AOF 文件是可修改的,如果內部有一些異常操作,那么在下次啟動時就會報錯,此時可以通過 redis 提供的修復工具來修復備份文件。執行命令 " redis-check-aof --fix 文件名" 來修復。
優勢
總體上來說,要比 RDB 備份方式數據完整性要更好,在數據完整性要求高的場景下可以使用 AOF。
而因為 AOF 有兩種不同的備份規則,所以在數據完整性最優先、性能可以不考慮的場景可以使用 always 方式;在數據完整性要求比較高,但是也允許少量的數據丟失,但是要求性能也不會差,那么可以選擇 everysec。
劣勢
1、因為保存的是每一步操作,所以執行效率低。
2、雖然引入 rewrite 來避免備份文件過大,但是 rewrite 造成的 CPU 資源消耗加上原本備份的 CPU 資源消耗會比只使用 RDB 要多得多,所以如果不是對數據完整性有特別高的要求建議只使用 RDB。
兩種持久化總結
1、默認情況下,redis 只使用 RDB 持久化。因為 AOF 會消耗過多的 CPU,同時執行效率低。
2、如果開啟了 AOF 持久化,那么在恢復數據時優先使用 AOF 配置文件來恢復,因為 AOF 保存的數據更完整。
3、如果 redis 只用於做緩存,那么可以直接禁用 RDB 和 AOF 的自動持久化。
4、RDB 持久化一般用作數據的定期備份,如果對數據完整性要求沒有那么高,那么可以只使用 RDB,同時在配置文件中只保存 "save 900 1" 這條規則,以此來減少不必要的 CPU 消耗。如果需要使用 AOF ,應該調大 Auto-aof-rewrite-min-Size 來避免頻繁的 Rewrite。
Redis 事務
mysql 中的事務擁有 ACID 特性,即原子性、一致性、可見性、持久性。那么 Redis 的事務呢? Redis 的事務擁有隔離性,但是不包證原子性。並且其沒有隔離級別的概念,也就是說它不像 mysql 中執行了操作,但是因為隔離級別的影響而導致 " 操作未執行 " 的假象。
基本操作
開啟事務:multi;
提交事務:exec;
放棄事務:discard;
事務執行的流程是 " 開啟事務--->操作入隊---->提交事務執行所有操作",下面的截圖就是典型的事務執行過程
如果在提交之前想中斷此次事務,可以通過 "discard" 來取消當前事務。
特點
1、隔離性:事務的執行不會被其他客戶端的操作打斷。
2、不保證原子性:如果事務中的某一條操作執行失敗,那么其不會影響該事務中的其他操作。
3、在事務提交時,如果某個操作有異常(操作本身的格式有問題,在入隊時就報錯,也就是編譯異常),那么這個事務在提交后不會生效,內部的所有操作都不會生效。如下圖。
4、在事務提交時,如果某個操作在入隊時沒有異常,在提交時發生異常,那么這個操作不會影響其他操作的執行。
除此之外,Redis 還可以通過 "watch key" 來對指定的數據設置樂觀鎖,此后如果其他會話對該數據操作后,當前會話執行的事務就會被中斷。
Redis 的發布訂閱
雖然這模塊功能一般是由消息隊列來實現,但是如果是簡單的 "發布-訂閱" 操作通過 redis 也是可以實現的。
相關操作
1、訂閱一個或多個:SUBSCRIBE 訂閱名1 訂閱名2 ...
2、消息發布: PUBLISH 訂閱名 消息內容
實現如下:
3、訂閱通配符: PUBLISH 通配符*
實現如下:
主從復制
在操作量較大時,一台 redis 服務器往往不能滿足需求,所以需要搭建 redis 服務器集群,而實現的方式一般是搭建 "主從復制" 的服務器模式,其本質就是主機來處理寫操作,從機處理讀操作。
配置
首先,要知道的是每台服務器啟動后,默認就是 master,也就是主。所以我們只需要去配置 salve(從機),配置方式就是在需要成為從機的客戶端上執行 " slaveof 主庫ip 主庫端口 ",在執行后,可以通過 " info replication"來查看當前服務器的狀態。
在完成配置后,在主機上進行寫操作后從機上就可以進行對應的讀操作。
同步原理
在從機與主機完成交互關系后,主機就會收到從機發送的一條 sync 命令,這條命令會使 master 啟動后台的存盤進程,等到進程存盤完成后,就會將整個數據文件發送給 slave,slave 接收到數據然后加載到內存,這種所有數據全部復制給 slave 叫做 "全量復制"。而后續 master 進行寫操作,相關的寫操作會依次復制傳給 slave,這種附加的復制叫做 "增量復制"。
注意細節
配置上:
在進行主從復制時,需要將配置文件拷貝服務器台數的數量,然后需要修改相應配置文件,打開守護模式,修改其端口號、pid 文件名字、Log 文件名、RDB 備份文件名,如果使用了 AOF 還需要修改 AOF 備份文件名。然后再啟動服務器並指定相應的配置文件。如果一開始就確定主從機關系也可以通過配置 masterip、masterport 來避免手動配置。
執行時:
1、默認情況下 master 用於處理寫操作, slave 用於處理讀操作。
2、主機宕機后,從機會原地待命(還是不能執行寫操作可以執行讀操作), 等待 master 重新連接后又恢復正常。
3、當一台 slave 成為另外一台 slave 的 " master "后,其身份還是 slave,不能執行寫操作。
4、從機斷開后需要重新通過 " slaveof " 來連接成為從機,但如果配置進配置文件則不需要。
5、使用 " salveof no one " 可以讓當前從機退出關聯,重新成為 "master" 狀態。
不足
在 master 向 slave 復制數據時會有一定的延遲,這種在數據量小的情況下不會有明顯感覺,但是在操作數多的情況下,或者在 slave 服務器較多時,在 slave 讀取的數據就不能保證是最新的值了。
哨兵模式
對於上面的配置方式是有明顯缺陷的,如果 master 出現故障宕機,那么此系統就會無法正常工作,如果是 "一主二從" ,那么我們完全可以在 master 宕機后從兩台 slave 中選擇一台成為新的 master,以此來保證系統的正常執行。" 哨兵" 就是干這件事的,通過它可以在 master 宕機后自動從剩下的 slave 中選擇一台成為新的 master。
配置
1、創建文件 sentinel.conf 。在文件中編寫哨兵 "監視" 的服務器信息:" sentinel monitor 數據庫名字(自定義) 數據庫所在ip 端口號 1 " 。 結尾的 1 表示投票數,也就是在 master 宕機后,哨兵會對剩下的 slave 進行投票,得票數多的成為下一個 master。而總票數就是 1。為了不讓每台服務器得到的票數相同就設為 1。一個哨兵可以監視多個 master,也就是一個文件中可以配置多個。
2、另開一個窗口執行 " Redis-sentinel sentinel.conf所在目錄/sentinel.conf " 。
注意細節
1、在主機宕機從機成為新的 master后,前主機重新連接,那么其會被哨兵分配成 slave 執行讀操作。
2、當前說的是配置一個哨兵,如果這個哨兵宕機,那么就存在着隱患,所以一般在項目中會搭建哨兵集群,來避免哨兵的宕機,同時哨兵搭建配置參數並沒有這么少,這里展現的只是核心配置,如有其他要求需要另行配置。
客戶端
redis 的Java客戶端主要有 jedis、lettuce、Redisson。
1、在 SpringBoot 里封裝的 Redis 依賴使用的是 lettuce,其優點是底層使用的是 netty,執行效率非常高,同時是線程安全的,支持絕大多數的 redis 功能。
2、jedis 是老牌的 Java 客戶端,但是隨着硬件的升級,其效率變得越來越低,同時也是線程不安全的。其優點或許只是能全面支持 Redis 的操作特性了吧。
3、 Redisson 則是在封裝了 Redis 許多高級功能的基礎上效率也比較高,使用起來也比較方便。但是其對字符串操作支持比較差。
高並發下的問題與解決
緩存穿透
查詢一個不存在的值,導致緩存未生效。如查詢的結果值是null,那么就去數據庫進行查詢,而如果數據庫查詢的結果就是null,那么就造成下次查詢還是會走數據庫,即使結果就是null。也就是故意惡意去查詢不存在的值讓數據庫承受高並發的查詢最終造成性能上的影響。
解決:將null值進行緩存,並加入短暫的過期時間。過期后再去查詢數據庫。
緩存雪崩
在增加緩存時設置了同一個過期時間,使得這些數據在某一時刻同時大面積失效,而在這時刻過來的所有請求全部需要去數據庫進行查詢,造成數據庫壓力過大。
解決:設置一個隨機的時間,避免在同一時刻大面積過期。
緩存擊穿
指的是某個熱點數據的key失效后收到大量的請求,那么這些請求都會去查詢數據庫造成數據庫壓力過大。
解決:加鎖,先由一個人去查,然后添加緩存,添加完成后解鎖,其他人再從緩存中查詢。
讀問題(本地鎖與分布式鎖)
redis 緩存在高並發下的讀取會有三個問題,緩存穿透(查詢不存在的值都會走數據庫)、緩存雪崩(同一時刻大量的緩存同時過期導致數據庫壓力過大)、緩存擊穿(熱點數據在過期失效時大量請求進來訪問數據庫)。前兩個可以通過緩存空數據、設置不同的過期時間來解決。第三個就需要添加鎖來解決。
如果加的是本地鎖,且應用是單體應用,那么因為Spring默認是單例的,所以是可行的,但是如果是分布式集群項目,那么每個集群的 bean 都是不同的,所以集群之前是鎖不住的
分布式鎖
分布式鎖應該滿足下面的條件:
在緩存查詢前通過對特定 key 中嘗試插入一個期限時間(防止加鎖后的線程發生異常造成死鎖)的數據來模擬加鎖,加鎖后其他線程嘗試設置數據失敗進入自旋。在業務執行完成后將數據添加到緩存,然后再進行解鎖。加鎖並設置過期時間、校驗是否是當前鎖(每個線程設置的value都不同,在刪除前檢查是否是之前加鎖時設置的值,防止在業務執行時當前設置的數據已經到期而其他線程新設置了鎖,導致當前線程將其他線程加的鎖刪除了)並解鎖都是原子操作。
Redisson鎖
特點:
1、默認會設置一個30s過期時間的鎖,所以即使斷電沒有釋放也不會死鎖
2、當業務達到三分之一的看門狗時間(30/3=10s),會觸發看門狗機制,在過期時間到達時會續期30s。
3、如果在加鎖時自定義了過期時間,那么在到達過期時間后鎖也會過期。(在大業務場景(執行超過30s)效率高一些)
配置:
@Configuration
public class RedissonConfig { @Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.56.10:6379"); RedissonClient redisson = Redisson.create(config); return redisson; } }
使用
讀寫鎖
信號量
CountDownLatch
寫問題
如果一個線程修改了緩存中的某個原數據,那么緩存中的數據就不是最新數據了,造成寫問題。
解決:
1、對於一致性要求不高的,可以等待緩存數據過期后,自動更新最新數據。
2、對於一致性要求高的。
1)使用 Redisson 的讀寫鎖,限制讀寫同時執行。缺點是在寫多的場景下執行效率非常低。
2)雙寫模式。在寫操作完成也更新緩存數據。這個效率不低,但是可能還是會造成數據不一致的問題。在緩存數據過期后就會恢復正常。
進一步優化就是在寫操作和更新緩存整個操作加鎖,保證只能有一個線程在執行寫操作。
3)失效模式。在更新后刪除緩存數據。這個和雙寫模式一樣,也可能會存在數據不一致的問題。並且在緩存數據被刪除或過期后會達成最終一致性。
4)使用 Canal 中間件(阿里開源的中間件),通過監控mysql的binlog日志,在數據修改后會自動更新對應的緩存數據。
總結:
在要求數據一致性的場景下,也就是解決寫問題,通常的方法就是加鎖。而加鎖所帶來的就是性能上的下降。所以兩者之間的取舍就需要結合業務場景來看。
SpringCache
SpringCache 是 Spring 對各種緩存的封裝,在引入 Spring-boot-starter-cache 后就可以使用,通過注解直接實現緩存的獲取、刪除、添加。
特點:
1、划分區域。SpringCache 可以將緩存區划分為多個區域,每個區域用於存放某一類數據。刪除時也可以直接刪除某區域的數據。如@Cacheable({"category"}),category 就是指定的區域。{}可以指定存入多個分區。
2、自定義緩存類型,存活時間、是否存儲null、緩存名前綴等屬性:
3、可以自定義Redis的配置類,改變Redis key、value的序列化器,解決存儲數據的亂碼問題。但是需要對配置文件中的配置進行另外賦值(也可以只對CacheManager進行修改,修改其關聯的組件。CacheManager 是RedisCacheConfiguration 內的一個組件)
@EnableConfigurationProperties(CacheProperties.class) // 將讀取配置緩存的類CacheProperties加入Spring容器(因為默認沒用加到容器中,所以這里需要加入容器)
@Configuration
@EnableCaching // 開啟緩存配置
public class MyCacheConfig { @Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){ // CacheProperties 會自動從容器中獲取到 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 設置 key 序列化(默認String) config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 設置 value 序列化(默認JDK) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())); // 默認在自定義 RedisConfiguration 后會不使用配置文件中配置,所以我們需要額外設置配置文件中的配置,也就是讀取配置文件中的配置賦值到配置對象中 CacheProperties.Redis redisProperties = cacheProperties.getRedis(); //將配置文件中所有的配置都生效 if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); // 存活時間 } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); // 緩存 key 前綴 } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); // 是否緩存空值 } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); // 是否啟用 key 前綴 } return config; } }
@EnableConfigurationProperties(CacheProperties.class),因為默認的CacheProperties類未加入到容器,所以這里必須指定加入容器才能在方法里直接獲取。
4、緩存key在未指定key時是”(分區名或前綴名)::SimpleKey[]”,如果指定了key,那么就是”(分區名或前綴名)_key”。(新版的key好像不顯示(分區名::),但是如果指定前綴名格式還是一樣。)
5、緩存未指定時間則默認是永不過期
6、注解使用
@Cacheable(value={“category”},key=”#root.method.name”)
先執行方法前先查詢是否包含緩存,分區是 category,key 是方法名,也就是尋找
“category_方法名” 為key的 緩存
@CacheEvict(value=”category”,key=” ’getCataJson ’ ”) (相當於上面一致性實現的失效模式)
刪除category_getCataJson 為 key 的緩存
@Caching():執行多個緩存操作
@CacheEvict(value=”category”, allEntries = true)
刪除 category分區的所有數據,也就是刪除 category_ 開頭的 key 所有緩存
@CachePut 同理,是將返回值來更新到指定的緩存上,如果返回值為null,執行的就是刪除,不為空執行的就是修改更新。 (相當於上面一致性實現的雙寫模式)
SpringCache 與 Spring 封裝的Redis依賴的區別
在引入Spring 對 redis 封裝的依賴spring-boot-starter-cache就可以使用 redis了。傳統的Redis只需要配置其端口,路徑就可以了,然后使用封裝好的 StringRedisTemplate來操作數據庫。
而如果需要使用SpringCache(注解),那么還需要配置類型(至少要有一個類型)以及其他配置,然后加入容器才能使用。SpringCache 相比較於 普通的 Redis 開發更方便簡潔,但是只適用於常規的使用。
SpringBoot 中的使用
在 springboot2.0 之前,底層默認使用的是 jedis,而 2.0 以后變成了 lettuce,這是因為 jedis 采用的是直連,當多個線程操作時,是不安全的。此時可以通過 jedis 的連接池來避免線程不安全,但是在執行時還是會比較慢。而 lettuce 底層使用的是 netty,實例可以在多個線程中共享,不會發生線程不安全的情況。
亂碼問題
在使用 redisTemplate 將數據存入 redis 后往往會發現在 redis 客戶端中讀取會亂碼,這是為什么?
redis 內部維護的 redisTemplate 底層使用的是 JDK 序列化器,在 redis 中以二進制形式保存,所以我們在客戶端直接讀取的是二進制數據,相關源碼可以看下面代碼
// RedisAutoConfiguration.class @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } // RedisTemplate.class public void afterPropertiesSet() { super.afterPropertiesSet(); boolean defaultUsed = false; if (this.defaultSerializer == null) { this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader()); } if (this.enableDefaultSerializer) { if (this.keySerializer == null) { this.keySerializer = this.defaultSerializer; defaultUsed = true; } if (this.valueSerializer == null) { this.valueSerializer = this.defaultSerializer; defaultUsed = true; } if (this.hashKeySerializer == null) { this.hashKeySerializer = this.defaultSerializer; defaultUsed = true; } if (this.hashValueSerializer == null) { this.hashValueSerializer = this.defaultSerializer; defaultUsed = true; } } ... }
關於 redis 的序列化器有以下幾種:
我們着重看一下常用的幾種:
1、JdkSerializationRedisSerializer。RedisTemplate默認的序列化器,存儲的對象必須實現Serializable接口,不需要指定對象類型信息,在redis中以二進制格式來保存,不可讀,且轉化后的二進制數據往往比json數據要大。
2、StringRedisSerializer。已String類型進行保存,不需要指定,不需要實現Serializable接口
3、Jackson2JsonRedisSerializer。需要指定序列化對象的類型,不需要實現Serializable接口
4、GenericJackson2JsonRedisSerializer。不需要指定序列化對象類型。不需要實現Serializable接口,與 Jackson2JsonRedisSerializer 區別是其保存的對象數據雖然也是 Json 格式的,但是會顯示存儲對象的類型,以及元素對象所在的類路徑。具體可以百度。
redisTemplate 默認 key 與 value 使用的都是 JDK 序列化器,我們可以自定義一個 redisTemplate 組件來覆蓋默認的,key 可以使用 String 序列化器,value 使用 Jackson2 序列化器。代碼如下:
@Configuration public class RedisConfig { @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
同時如果要存儲的數據就是字符串類型的,那么也可以直接使用 redisTemplate,其 key 與 value 都是使用 String 序列化器。
常用方法
redisTemplate.opsForValue();//操作字符串 redisTemplate.opsForHash();//操作hash redisTemplate.opsForList();//操作list redisTemplate.opsForSet();//操作set redisTemplate.opsForZSet();//操作有序set redistempalate.boundValueOps; redistempalate.boundSetOps; redistempalate.boundListOps; redistempalate.boundHashOps; redistempalate.boundZSetOps; //兩者區別:ops就相當於創建一個operator,前者是通過一個operator來執行各個數據類型的操作,后者是選中數據類型再為這個類型來創建一個operator, //也就是前者是一個operator執行多種數據,后者是一個operator操作一種數據 // 通過 connection 對象來執行數據庫相關的操作 RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.flushDb(); connection.flushAll();