緩存穿透
高并發下查詢一個值,緩存中沒有,數據庫中也沒有,布隆過濾器
解決方案:
- 如果數據庫中值為空,把空寫入緩存即可。
- 也可以把所有的可能存在的key放入到一個大的Bitmap中,查詢時通過該Bitmap過濾
緩存雪崩
緩存中大量數據同時到期,高并發下,所有請求都走向數據庫
解決方案:
盡量不要把所有緩存都設置在同一時間過期, 通過加鎖或者隊列只允許一個線程查詢數據庫和寫緩存, 其他線程等待.
通過加鎖或者隊列只允許一個線程查詢數據庫和寫緩存,其他線程等待。
熱點緩存(緩存擊穿)
雙重檢測鎖解決熱點緩存問題,需要加volatile防止指令重排
高并發下,一個熱點緩存到期,然后去數據庫中去取,當還沒有放入緩存中時,大量請求過來
解決方案:
- 雙重檢測鎖
Integer count = redis.get("key");
if (count == null) {
synchronized {
count = redis.get("key");
if (count == null) {
count = repo.getCount();
redis.put("key", count);
}
}
}
- 也可以用redis的setnx互斥鎖進行判斷
if (redis.setnx(lockKey, requestId, NX, PX) == 1) {
}
緩存雙寫一致性
解決方案:
延時雙刪策略, 先更新數據庫,再刪緩存
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
// 可以將以下兩步作為異步處理
Thread.sleep(1000);
redis.delKey(key);
}
Redis簡介
Redis是一種用C語言開發的,高性能的,鍵值對key-value形式的noSql數據庫
支持5種string, hash, set, list, 有序集合類型(sorted set, 簡稱zset)等數據類型
劣勢就是存儲的數據缺少結構化
應用場景:
- 內存數據庫(登錄信息,購物車信息,用戶瀏覽記錄)
- 緩存信息
- 解決分布式架構中的session分離問題
redis常用命令
- redis-server
- redis-client
- 性能測試工具redis-benchmarkredis-benchmark -q(Quiet. Just show query/sec values) -n(default 100000 requests)-h <hostname> Server hostname (default 127.0.0.1) -p <port> Server port (default 6379) -s <socket> Server socket (overrides host and port) -a <password> Password for Redis Auth -c <clients> Number of parallel connections (default 50) -n <requests> Total number of requests (default 100000) -d <size> Data size of SET/GET value in bytes (default 2) -dbnum <db> SELECT the specified db number (default 0) -k <boolean> 1=keep alive 0=reconnect (default 1) -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD Using this option the benchmark will expand the string rand_int inside an argument with a 12 digits number in the specified range from 0 to keyspacelen-1. The substitution changes every time a command is executed. Default tests use this to hit random keys in the specified range. -P <numreq> Pipeline <numreq> requests. Default 1 (no pipeline). -q Quiet. Just show query/sec values --csv Output in CSV format -l Loop. Run the tests forever -t <tests> Only run the comma separated list of tests. The test names are the same as the ones produced as output. -I Idle mode. Just open N idle connections and wait.
- redis-check-aofaof文件檢查的工具
- redis-check-dumprdb文件進行檢查的工具
- redis-sentinel啟動哨兵監控服務
redis數據類型及常用操作
- string set key value, get key, getset key value, incr key(必須為整數), incrby key increment, decr key, decrby incrementsetnx key value, Append key value, strlen key, mset key1 value2 key2 value2..., mget key1, key2 ...
- hash散列類型,如(people --> name --> "chris")字段的名只能用stringhset key field value, hget key field, hmset ..., hsetnx key field value(同hset,但是如果field存在,則不執行任何操作),hmget 批量取, hdel key, hincrby key field increment, hexists key field, hkeys key, hvals key, hlen key, hgetall key
- list類型(鏈表實現的)lpush/rpush, lrange, lpop/rpop, llen, lrem key count value當count>0時,從左邊開始刪,刪除在count范圍內,值為value的元素當count<0時,從右邊開始刪當count=0時,刪除所有值為value的元素lindex, lset key index value, ltrip key start stop, linsert key before|after "specified value" value, rpoplpush,
- set類型 不重復且沒有順序(指放入和取出的順序不一致)sadd,srem key value, smembers key, sismember key value, sdiff A B(A - B), sinter A B(A ∩ B), sunion A B(A ∪ B),scard key(獲取元素個數),spop(從集合中隨機選擇一個元素彈出)
- zset類型(為每個元素都關聯一個分數) 有序集合和list對比相同點:兩者都有序,兩者都可以獲得某一范圍內的元素區別:列表訪問兩邊數據很快,訪問中間數據很慢。有序集合都很快有序列表可以調整元素位置,通過分數實現;有序集合耗內存zadd key score member, zrange/zrevrange key start stop [withscores],zscore key,zrem, zrangebyscore key min max, zincrby key increment member, zcard key(當前集合中元素數量)zcount key min max(指定分數范圍內元素的個數), zremrangebyrank key start stop, zrank/zrevrank key member
- 通用命令keys, del, exists, expire key, ttl key(剩余生存時間), persist key(清除生存時間), pexpire key milliseconds(生存時間設置單位為毫秒), rname oldkey newkey, type key,
redis事務介紹(指一組命令的集合)
redis使用multi, exec, discard, watch, unwatch實現事務
redis不支持事務回滾
執行multi后,Redis會將命令逐個放入隊列中,然后用exce執行這個隊列中的命令
而watch是在multi之前,watch某個屬性,表示我這個multi塊中可能要修改該屬性,如果multi塊中的命令在未執行前有客戶端修改了該請求,那么該multi塊中的命令就會執行失敗。
redis持久化(指的是持久化到磁盤)
redis持久化的方式有兩種,RDB和AOF
RDB(redis默認方式)
rdb是使用快照(snapshotting)的方式進行持久化的
觸發快照的時機
- 符合自定義的快照規則
- 執行save或者bgsave命令注: save命令是阻塞的,執行bgsave時會fork出一個進程進行保存,非阻塞的
- 執行flushall命令注:線上一般要禁止掉flushall(刪除所有數據庫的所有 key),flushdb(刪除當前數據庫的所有key), keys *等命令在redis配置文件中添加:rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command KEYS ""
- 執行主從復制操作
redis獲取所有數據庫:
config get databases(默認有16個數據庫,index從0開始)
select 0選擇數據庫
快照規則(或的關系)
save 900 1 “**15分鐘內有1次修改就進行快照”**
save 300 10 “**5分鐘內有10次修改就進行快照”**
save 60 10000 “**1分鐘內有10000次修改就進行快照”**
dir ./ 指定快照地址(rdb文件地址)
dbfilename dump.rdb
快照過程
- Redis調用系統fork函數復制出一份當前進程的副本(子進程)
- 子進程開始將內存中的數據寫入到硬盤中的臨時文件
- 用臨時文件替代舊的rdb文件(經過壓縮的二進制文件)
優缺點
- 缺點: 一旦Redis異常退出,就將丟失最后一次快照后更改的所有數據
- 優點: rdb可以最大化Redis的性能
AOF
AOF: 每執行一條更改,Redis就會將該命令寫入AOF文件. 實際上是先寫入到硬盤緩存,然后通過硬盤緩存刷新機制保存到文件。
appendfsync always
appendfsync everysec(默認)
appendfsync no(由系統進行sync)
默認關閉,打開是appendonly yes
在數據量比較大的時候,頻繁的寫入和修改,aof文件會變得非常臃腫,所以我們可以設置重寫規則:
- auto-aof-rewrite-min-size:64m
- auto-aof-rewrite-percentage:100
RDB 和 AOF比較
RDB持久化是指在指定的時間間隔內將內存中的數據集快照寫入磁盤,實際操作過程是fork一個子進程,先將數據集寫入臨時文件,寫入成功后,再替換之前的文件,用二進制壓縮存儲。
AOF持久化以日志的形式記錄服務器所處理的每一個寫、刪除操作,查詢操作不會記錄,以文本的方式記錄,可以打開文件看到詳細的操作記錄。
數據庫備份和災難恢復
定時生成RDB 快照(snapshot)非常便于進行數據庫備份, 并且 RDB 恢復數據集的速度也要比 AOF 恢復的速度要快。
Redis 支持同時開啟 RDB 和 AOF,系統重啟后,Redis 會優先使用 AOF 來恢復數據,這樣丟失的數據會最少。
RDB 和 AOF ,我應該用哪一個
如果你非常關心你的數據,但仍然可以承受數分鐘以內的數據丟失, 那么你可以只使用 RDB 持久。
AOF 將 Redis 執行的每一條命令追加到磁盤中,處理巨大的寫入會降低 Redis 的性能,不知道你是否可以接受。
redis主從復制
- 只需要在從服務器的配置文件中添加:slaveof 192.168.1.123 6379
- 主從復制保證了即使有服務器宕機,也能保證對外提供服務。
- 當進行主從復制時,不會阻塞。
- 一個從服務器也可能是另一臺服務器的主
原理:
分為全量同步和增量同步
- 全量同步是當第一次從服務器連接上主服務器時進行的同步,在全量同步期間,主服務器還會有新的寫操作過來,這時候主服務器會把這些操作放入到緩沖區。master創建快照并發送給slave(將此期間的寫入放入緩沖區)master向slave同步緩沖區的寫操作命令同步增量階段
- 增量同步是全量同步之后的一個正常操作的過程master每執行一個寫操作,都會將該命令發送到slave
redis哨兵機制
- redis主從復制的缺點是當有Redis主服務器進行宕機時,不能進行動態的選舉。需要使用Sentinel機制完成動態選舉。
- 因此Sentinel進程的作用:監控master的狀態(實際上也可以監控slave),在master宕機之后完成動態的選舉。
- 如果有master或者slave宕機,可以通過腳本向管理員發送通知(短信或郵件)。即Monitoring 和 Notification.
- sentinel動態選舉過程(Automatic failover):檢測到master出現異常將其中一個slave復制為新的master當有slave請求master時返回新的master地址注: master和slave的redis.conf,和sentinel.conf都會發生變化,
- sentinel故障分析過程sentinel會以每秒1次的頻率發送ping命令到Master, Slave 和 其他Sentinel若回復ping命令超時(sentinel.conf文件中指定的down-after-milliseconds),則該實例會被標記為SDOWN(主管下線)如果有足夠數量(sentinel.conf中指定的)的Sentinel都將該實例標記為SDOWN,則該實例變為ODOWN
- 監控的主機名稱為master,地址和IP,當有2個quorum認為mymaster失聯時,則標記為ODOWN sentinel monitor mymaster 127.0.0.1 6379 2注意:雖然沒有寫監控slave,但是slave是被自動檢測的雖然指定了ODOWN的數量,但是還是需要大多數的Sentinel同意來開啟故障轉移
sentinel一些配置
- port 26379(default)
- dir /tmp(工作目錄)
- 當實例開啟了requirepass foobared,需要在sentinel.conf中添加如下配置sentinel auth-pass <master-name> <password>sentinel down-after-milliseconds <master-name> <milliseconds>sentinel parallel-syncs <master-name> <numreplicas> 當master發生故障時,最多有幾個slave同時對master進行更新
- sentinel failover-timeout mymaster 180000(這個超時時間有4種用途)所有slave對新的master進行更新時所需的最大時間,如果超過這個時間,則parallel-syncs無效,變為一次只能有一個更新同一個Sentinel對同一個master兩次failover之間的間隔時間取消一個正在failover的實例所允許的最大時間(取消的前提是配置文件還未發生變化)slave從一個錯誤的master同步數據到糾正為從正確的master同步數據所需要的最大時間
- 腳本腳本返回1,則會重試,默認重試10次腳本返回值 > 2,不重試腳本執行中中斷,則和返回1效果一樣當一個腳本執行超過60秒,則會被一個SIGKILL信號終止,然后重試
- 通知型腳本sentinel notification-script mymaster /var/redis/notify.sh當系統有sdown或者ODOWN時會向管理員發送短信或郵件,該通知接收兩個參數,事件類型和事件描述注:如果配置了該腳本,那么該腳本必須存在且是可執行的,否則無法啟動Sentinel
- 客戶端重新配置主節點參數腳本sentinel client-reconfig-script <master-name> <script-path>當master發生改變,執行該腳本通知客戶端主機的新地這些參數將會被傳遞到該腳本:<master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>state 一直是 failoverrole 是 observer或者leaderfrom-:老的master的IP和端口號,to-:新的master的IP和端口號
Redis 集群
redis 集群保證了高可用
- Redis集群特點集群中的各個實例(節點)彼此互聯,通過ping-pong機制節點失效判斷(fail): 需要集群中所有的master投票, 經過半數以上的節點檢測失效時才生效
- 客戶端與Redis節點是直連,不需要經過任何代理
- Redis-cluster把所有物理節點映射到[0-16383]slot上,cluster負責維護node -- slot -- value注:redis集群內置了16384個slot,當客戶端保存一個key-value時,redis先對key使用crc16算法算出一個結果,然后把結果對16384取余,Redis會把16384個slot均等的分配到各個節點上。每個節點都包含了一個各個node的信息
- 集群失效判斷如果集群任意master掛掉,且該master沒有slave時。集群掛掉。因為16384個hash槽不完整集群超過半數的master掛掉,不管是否有slave。
- 注: 為什么是16384個槽?(自我描述: redis對一個key進行crc16算法, 產生一個16位(bit)的hash值, 那么該算法可以產生65536個值, 但為什么不是65536個槽, 而是16384個槽呢? 原因有幾點: 1. 與Redis的心跳機制有關, redis兩個節點在發生心跳的時候, 消息頭中包含如myslots[CLUSTER_SLOTS/8], 所以如果發送65536個這樣的信息, 就需要65536 * 8 * 1024 = 8K, 太大, 浪費帶寬; 2. 實際16384個槽已經足夠用, 因為當redis的節點超過1000時, 整個集群的效率會非常低, 會造成網絡擁堵. 因此作者建議不要超過1000個節點)
客戶端連接集群
- ./redis-cli -h 127.0.0.1 -p 7001 -c
- 添加新的節點:./redis-trib.rb add-node 127.0.0.1:7007 127.0.0.1:7001./redis-trib.rb reshard 127.0.0.1:7001(連接上任一節點即可)./redis-trib.rb add-node --slave --master-id 主節點id 新節點的IP和端口 舊節點ip和端口(集群中任一節點都可以)
redis實現分布式鎖
- 單應用一般用synchronize,ReentrantLock實現鎖
- 分布式分布式鎖注意事項:互斥性:即在任一時刻只有一個客戶端能持有鎖同一性:加鎖和解鎖必須是同一客戶端可重入性:即使一個客戶端沒有主動解鎖(崩潰等),也能保證后續其他客戶端能加鎖(超時時間)
- 基于數據庫的樂觀鎖實現分布式鎖
- zookeeper臨時節點的分布式鎖
- 基于Redis的分布式鎖使用set key value [ex seconds] [px milliseconds] [NX|XX]ex和px都表示過期時間,單位不一樣NX是在不存在時設置,XX是在存在時設置public static boolean getLock(String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
return "OK".equals(result);
}釋放鎖public static void releaseLock(String requestId, String lockKey) {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
redis 過期策略
- 定期刪除+ 惰性刪除 + 內存淘汰機制定期刪除: Redis默認是每隔100ms就隨機抽取一些設置了過期時間的key. 假如redis中有100萬個key, 都設置了過期時間,那么肯定不會每隔100毫秒就遍歷100萬個key然后刪除過期了的key. 當get某個key的時候, redis會檢測該key有沒有過期, 如果過期,就刪除, 然后返回空.這就是惰性刪除. 但是內存中如果有10萬個key沒有被訪問到, 不可能讓他們長期在內存中消耗內存, 這時候就需要走內存淘汰機制內存淘汰機制: noeviction:當內存不足以容納新寫入數據時,新寫入操作會報錯,這個一般沒人用吧allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key(這個是最常用的)allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個key,這個一般沒人用吧volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key(這個一般不太合適)volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個keyvolatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除
redis cluster對mget的操作
Redis cluster不支持mget操作. 最初是facebook, 2010年使用memcache作緩存, 共有3000個節點. 發現節點太多, 連接頻率下降. 繼續增加節點, 并沒有改善, 是因為IO的成本已經超過數據傳輸.
所以redis cluster也因此不支持mget操作.redis引入cluster模式后, 是將數據hash到16384個slot上, 每個node負責一部分slot.
mget優化方案:
- n個key, 傳統IO, 分別獲取, 時間復雜度為O(n)
- n個key, 通過Redis的hash算法可以得出各個key所對應的節點, 這樣時間復雜度就位O(node.size())
- 在B方案的基礎之上并發處理
redis的redlock
- redlock的前提是有N個redis的master, 這些節點之間沒有主從復制, 或者其他集群協調機制.
- client從N個節點嘗試獲取鎖, 只要有N/2 + 1個節點獲取成功, 那么便獲取成功; 如果最終獲取失敗, 客戶端應該在所有的節點上進行解鎖.
- redlock的出發點是為了解決Redis集群環境下, 出現的分布式鎖的問題(client1獲取鎖, master 宕機, slave變成master, client2獲取到鎖). 但是redlock的出現并沒有解決這樣的問題.
Martin和Redis作者antirez之間的爭辯:
martin挑了兩個缺點:
1. 對于提升效率的場景, redlock太重
2. 對于正確性要求極高的場景, redlock并不能保證正確性;
問題: 在client1獲取鎖之后, 由于某種原因發生系統停頓, 鎖過期, 然后client1執行操作; client2這時候也會拿到鎖, 就會出現問題)
問題: A, B, C, D, E 5個redis節點,如果C的時間走得快, client1拿到鎖(A, B, C), C節點先過期, client2又拿到了(C, D, E)這樣就出問題了;
所以Redis從根本上來說是AP, 而分布式鎖是要求CP的.
redis各種數據類型的數據結構
Redis的底層數據結構
- 簡單動態字符串sds(Simple Dynamic String)
- 雙端鏈表(LinkedList)
- 字典(Map)
- 跳躍表(SkipList)
redis各種數據類型使用的數據結構
- String, SDS(simple dynamic string) 簡單動態字符串, 包含len(字符串長度), free(空閑的字節數量), buf(字節數組,存儲數據)
- List, 使用壓縮列表(數據集比較少的時候, 列表中單個數據小于64字節或者列表中數據個數少于512個)和雙向循環鏈表, 包含pre, next, value
- hash, 使用壓縮列表(鍵和值的大小小于64字節, 列表中鍵值對個數小于512個)和散列表
- Set, 有序數組(個數不超過512)和散列表
- Zset, 壓縮列表(數據小于64字節或者個數小于128個)和跳躍表
用ziplist代替key-value減少80%內存占用的案例
背景: 因業務原因, 需要大量存儲key-value數據, key和value都為string, 如果存儲1千萬條數據,占用了redis共計1.17G的內存. 當數據量變成1個億時,實測大約占用8個G. 但是修改為key(int), value 為ziplist時, 內存占用為123M, 減少了85%.
步驟:
- 要將1千萬個鍵值對, 放到N個bucket中, 但是為了防止ziplist變為hashtable, 每個bucket不能超過512個鍵值對, 1千萬 / 512 = 19531. 將所有key hash到所有bucket中, 但由于hash函數的不確定性, 可能出現不均等分配, 可以分配25000個bucket, 或者30000個bucket.
- 選用hash算法, 決定將key放到哪個bucket. 這里我們采用高效而且均衡的知名算法crc32. 通過獲取原有md5(key)的crc32后, 再對bucket的數量進行取余.
- 第2步確定了外層的key, 內部的field我們選用bkdr哈希算法. public static int BKDRHash(String str) {
int seed = 131;
int hash = 0;
for (int i = 0; i < str.length; i++) {
hash = (hash * seed) + str.charAt(i);
}
return (hash & 0X7FFFFFFF);
} - 測試裝入1000萬條數據, 內存降低了85%; 查詢測試, 查100萬條數據, 對比查詢速度: key-value耗時:10653、10790、11318、9900、11270、11029毫秒 hash-field耗時:12042、11349、11126、11355、11168毫秒
Redis高延遲原因
redis命令執行過程
藍色的表示可能發生高延遲的地方
redis提供的慢查詢統計功能: slowlog get {n}, 默認返回執行超過10ms(可配置)的命令.
- slowlog-log-slower-than, 配置超過幾毫秒的數據被記錄到慢查詢隊列中
- 慢查詢隊列的最大長度: slowlog-max-len
slowlog get會返回值如下:
> slowlog get
1) (integer) 26 # 在慢日志中的序列號
2) (integer) 1450253133 # 該記錄執行的系統時間
3) (integer) 43097 # 該記錄執行所消耗的時間
4) "flushdb" # 執行的操作
redis高延遲原因
- 不合理的命令或者數據結構避免使用hgetall操作, redis提供發現各種數據結構中大對象的工具, redis-cli-h {ip} -p {port} bigkeys
- 持久化阻塞開啟了持久化操作的redis, 當執行fork和AOF時會引起阻塞fork阻塞fork 操作發生在 RDB 和 AOF 重寫時, Redis 主線程調用 fork 操作產生共享內存的子進程, 由子進程完成對應的持久化工作. 如果 fork 操作本身耗時過長, 必然會導致主線程的阻塞。AOF阻塞當我們開啟AOF持久化功能時,文件刷盤的方式一般采用每秒一次, 后臺線程每秒對AOF文件做 fsync 操作. 當硬盤壓力過大時, fsync 操作需要等待,直到寫入完成.內存交換內存交換(swap)對于 Redis 來說是非常致命的, Redis 保證高性能的一個重要前提是所有的數據在內存中. 如果操作系統把 Redis 使用的部分內存換出到硬盤, 由于內存與硬盤讀寫速度差幾個數量級, 會導致發生交換后的 Redis 性能急劇下降. 識別 Redis 內存交換的檢查方法如下:>redis-cli -p 6383 info server | grep process_id # 查詢 redis 進程號
>cat /proc/4476/smaps | grep Swap # 查詢內存交換大小
Swap: 0 kB
Swap: 4 kB
Swap: 0 kB
Swap: 0 kB如果交換量都是0KB或者個別的是4KB, 則是正常現象, 說明Redis進程內存沒有被交換有很多方法可以避免內存交換的發生:保證機器充足的可用內存確保所有Redis實例設置最大可用內存(maxmemory), 防止極端情況下Redis內存不可控的增長降低系統使用swap優先級, 如 echo10>/proc/sys/vm/swappiness