本文是按照自己的理解進行筆記總結,如有不正確的地方,還望大佬多多指點糾正,勿噴。
課程內(nèi)容:
1、多級緩存架構詳解
2、緩存穿透&緩存擊穿&緩存雪崩詳解
3、熱點緩存key重建優(yōu)化
4、緩存與數(shù)據(jù)庫雙寫不一致終極解決
5、Redis開發(fā)規(guī)范與性能優(yōu)化
1. 多級緩存架構
ngnix到Lua到web層,到redis,最后到數(shù)據(jù)庫這整個過程。
2. 緩存設計
2.1 緩存穿透
緩存穿透是指查詢一個根本不存在的數(shù)據(jù), 緩存層和存儲層都不會命中, 通常出于容錯的考慮, 如果從存儲層查不到數(shù)據(jù)則不寫入緩存層。
也就是每次數(shù)據(jù)過來都要把每層緩存查一遍,查不到,最后查詢到數(shù)據(jù)庫,可能還是查不到,這就是緩存穿透,就在這一個“透”字
緩存穿透將導致不存在的數(shù)據(jù)每次請求都要到存儲層去查詢, 失去了緩存保護后端存儲的意義。
造成緩存穿透的基本原因有兩個:
第一, 自身業(yè)務代碼或者數(shù)據(jù)出現(xiàn)問題。
第二, 一些惡意攻擊、 爬蟲等造成大量空命中。
緩存穿透問題解決方案:
1、緩存空對象
String get(String key) {
// 從緩存中獲取數(shù)據(jù)
String cacheValue = cache.get(key);
// 緩存為空
if (StringUtils.isBlank(cacheValue)) {
// 從存儲中獲取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存儲數(shù)據(jù)為空, 需要設置一個過期時間(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 緩存非空
return cacheValue;
}
}
2、布隆過濾器
對于惡意攻擊,向服務器請求大量不存在的數(shù)據(jù)造成的緩存穿透,還可以用布隆過濾器先做一次過濾,對于不存在的數(shù)據(jù)布隆過濾器一般都能夠過濾掉,不讓請求再往后端發(fā)送。當布隆過濾器說某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在。
布隆過濾器就是一個大型的位數(shù)組和幾個不一樣的無偏 hash 函數(shù)。所謂無偏就是能夠把元素的 hash 值算得比較均勻。
向布隆過濾器中添加 key 時,會使用多個 hash 函數(shù)對 key 進行 hash 算得一個整數(shù)索引值然后對位數(shù)組長度進行取模運算得到一個位置,每個 hash 函數(shù)都會算得一個不同的位置。再把位數(shù)組的這幾個位置都置為 1 就完成了 add 操作。
向布隆過濾器詢問 key 是否存在時,跟 add 一樣,也會把 hash 的幾個位置都算出來,看看位數(shù)組中這幾個位置是否都為 1,只要有一個位為 0,那么說明布隆過濾器中這個key 不存在。如果都是 1,這并不能說明這個 key 就一定存在,只是極有可能存在,因為這些位被置為 1 可能是因為其它的 key 存在所致。如果這個位數(shù)組長度比較大,存在概率就會很大,如果這個位數(shù)組長度比較小,存在概率就會降低。
這種方法適用于數(shù)據(jù)命中不高、 數(shù)據(jù)相對固定、 實時性低(通常是數(shù)據(jù)集較大) 的應用場景, 代碼維護較為復雜, 但是緩存空間占用很少。
可以用redisson實現(xiàn)布隆過濾器,引入依賴:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
package com.redisson;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
//構造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆過濾器:預計元素為100000000L,誤差率為3%,根據(jù)這兩個參數(shù)會計算出底層的bit數(shù)組大小
bloomFilter.tryInit(100000000L,0.03);
//將zhuge插入到布隆過濾器中
bloomFilter.add("zhuge");
//判斷下面號碼是否在布隆過濾器中
System.out.println(bloomFilter.contains("guojia"));//false
System.out.println(bloomFilter.contains("baiqi"));//false
System.out.println(bloomFilter.contains("zhuge"));//true
}
}
使用布隆過濾器需要把所有數(shù)據(jù)提前放入布隆過濾器,并且在增加數(shù)據(jù)時也要往布隆過濾器里放,布隆過濾器緩存過濾偽代碼:
//初始化布隆過濾器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆過濾器:預計元素為100000000L,誤差率為3%
bloomFilter.tryInit(100000000L,0.03);
//把所有數(shù)據(jù)存入布隆過濾器
void init(){
for (String key: keys) {
bloomFilter.put(key);
}
}
String get(String key) {
// 從布隆過濾器這一級緩存判斷下key是否存在
Boolean exist = bloomFilter.contains(key);
if(!exist){
return "";
}
// 從緩存中獲取數(shù)據(jù)
String cacheValue = cache.get(key);
// 緩存為空
if (StringUtils.isBlank(cacheValue)) {
// 從存儲中獲取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存儲數(shù)據(jù)為空, 需要設置一個過期時間(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 緩存非空
return cacheValue;
}
}
注意:布隆過濾器不能刪除數(shù)據(jù),如果要刪除得重新初始化數(shù)據(jù)。
2.2 緩存失效(擊穿)
由于大批量緩存在同一時間失效可能導致大量請求同時穿透緩存直達數(shù)據(jù)庫,可能會造成數(shù)據(jù)庫瞬間壓力過大甚至掛掉,對于這種情況我們在批量增加緩存時最好將這一批數(shù)據(jù)的緩存過期時間設置為一個時間段內(nèi)的不同時間。
示例偽代碼
String get(String key) {
// 從緩存中獲取數(shù)據(jù)
String cacheValue = cache.get(key);
// 緩存為空
if (StringUtils.isBlank(cacheValue)) {
// 從存儲中獲取
String storageValue = storage.get(key);
cache.set(key, storageValue);
//設置一個過期時間(300到600之間的一個隨機數(shù))
int expireTime = new Random().nextInt(300) + 300;
if (storageValue == null) {
cache.expire(key, expireTime);
}
return storageValue;
} else {
// 緩存非空
return cacheValue;
}
}
2.3 緩存雪崩
緩存雪崩指的是緩存層支撐不住或宕掉后, 流量會像奔逃的野牛一樣, 打向后端存儲層。
由于緩存層承載著大量請求, 有效地保護了存儲層, 但是如果緩存層由于某些原因不能提供服務(比如超大并發(fā)過來,緩存層支撐不住,或者由于緩存設計不好,類似大量請求訪問bigkey,導致緩存能支撐的并發(fā)急劇下降), 于是大量請求都會打到存儲層, 存儲層的調(diào)用量會暴增, 造成存儲層也會級聯(lián)宕機的情況。
預防和解決緩存雪崩問題, 可以從以下三個方面進行著手。
- 保證緩存層服務高可用性,比如使用Redis Sentinel或Redis Cluster。
- 依賴隔離組件為后端限流熔斷并降級。比如使用Sentinel或Hystrix限流降級組件。
比如服務降級,我們可以針對不同的數(shù)據(jù)采取不同的處理方式。當業(yè)務應用訪問的是非核心數(shù)據(jù)(例如電商商品屬性,用戶信息等)時,暫時停止從緩存中查詢這些數(shù)據(jù),而是直接返回預定義的默認降級信息、空值或是錯誤提示信息;當業(yè)務應用訪問的是核心數(shù)據(jù)(例如電商商品庫存)時,仍然允許查詢緩存,如果緩存缺失,也可以繼續(xù)通過數(shù)據(jù)庫讀取。 - 提前演練。 在項目上線前, 演練緩存層宕掉后, 應用以及后端的負載情況以及可能出現(xiàn)的問題, 在此基礎上做一些預案設定。
2.4 熱點緩存key重建優(yōu)化
開發(fā)人員使用“緩存+過期時間”的策略既可以加速數(shù)據(jù)讀寫, 又保證數(shù)據(jù)的定期更新, 這種模式基本能夠滿足絕大部分需求。 但是有兩個問題如果同時出現(xiàn), 可能就會對應用造成致命的危害:
- 當前key是一個熱點key(例如一個熱門的娛樂新聞),并發(fā)量非常大。
- 重建緩存不能在短時間完成, 可能是一個復雜計算, 例如復雜的SQL、 多次IO、 多個依賴等。
在緩存失效的瞬間, 有大量線程來重建緩存, 造成后端負載加大, 甚至可能會讓應用崩潰。
要解決這個問題主要就是要避免大量線程同時重建緩存。
我們可以利用互斥鎖來解決,此方法只允許一個線程重建緩存, 其他線程等待重建緩存的線程執(zhí)行完, 重新從緩存獲取數(shù)據(jù)即可。
示例偽代碼:
String get(String key) {
// 從Redis中獲取數(shù)據(jù)
String value = redis.get(key);
// 如果value為空, 則開始重構緩存
if (value == null) {
// 只允許一個線程重建緩存, 使用nx, 并設置過期時間ex
String mutexKey = "mutext:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 從數(shù)據(jù)源獲取數(shù)據(jù)
value = db.get(key);
// 回寫Redis, 并設置過期時間
redis.setex(key, timeout, value);
// 刪除key_mutex
redis.delete(mutexKey);
}// 其他線程休息50毫秒后重試
else {
Thread.sleep(50);
get(key);
}
}
return value;
}
2.5 緩存與數(shù)據(jù)庫雙寫不一致
在大并發(fā)下,同時操作數(shù)據(jù)庫與緩存會存在數(shù)據(jù)不一致性問題
1、雙寫不一致情況
2、讀寫并發(fā)不一致
解決方案:
1、對于并發(fā)幾率很小的數(shù)據(jù)(如個人維度的訂單數(shù)據(jù)、用戶數(shù)據(jù)等),這種幾乎不用考慮這個問題,很少會發(fā)生緩存不一致,可以給緩存數(shù)據(jù)加上過期時間,每隔一段時間觸發(fā)讀的主動更新即可。
2、就算并發(fā)很高,如果業(yè)務上能容忍短時間的緩存數(shù)據(jù)不一致(如商品名稱,商品分類菜單等),緩存加上過期時間依然可以解決大部分業(yè)務對于緩存的要求。
3、如果不能容忍緩存數(shù)據(jù)不一致,可以通過加分布式讀寫鎖保證并發(fā)讀寫或?qū)憣懙臅r候按順序排好隊,讀讀的時候相當于無鎖。
4、也可以用阿里開源的canal通過監(jiān)聽數(shù)據(jù)庫的binlog日志及時的去修改緩存,但是引入了新的中間件,增加了系統(tǒng)的復雜度。
總結:
以上我們針對的都是讀多寫少的情況加入緩存提高性能,如果寫多讀多的情況又不能容忍緩存數(shù)據(jù)不一致,那就沒必要加緩存了,可以直接操作數(shù)據(jù)庫。當然,如果數(shù)據(jù)庫抗不住壓力,還可以把緩存作為數(shù)據(jù)讀寫的主存儲,異步將數(shù)據(jù)同步到數(shù)據(jù)庫,數(shù)據(jù)庫只是作為數(shù)據(jù)的備份。
放入緩存的數(shù)據(jù)應該是對實時性、一致性要求不是很高的數(shù)據(jù)。切記不要為了用緩存,同時又要保證絕對的一致性做大量的過度設計和控制,增加系統(tǒng)復雜性!
3. 開發(fā)規(guī)范與性能優(yōu)化
3.1 鍵值設計
3.1.1 key名設計
- 可讀性和可管理性
以業(yè)務名(或數(shù)據(jù)庫名)為前綴(防止key沖突),用冒號分隔,比如業(yè)務名:表名:id
trade:order:1
- 【建議】:簡潔性
保證語義的前提下,控制key的長度,當key較多時,內(nèi)存占用也不容忽視,例如:
user:{uid}:friends:messages:{mid} 簡化為 u:{uid}:fr:m:{mid}
- 【強制】:不要包含特殊字符
反例:包含空格、換行、單雙引號以及其他轉(zhuǎn)義字符
3.1.2 value設計
- 【強制】:拒絕bigkey(防止網(wǎng)卡流量、慢查詢)
在Redis中,一個字符串最大512MB,一個二級數(shù)據(jù)結構(例如hash、list、set、zset)可以存儲大約40億個(2^32-1)個元素,但實際中如果下面兩種情況,我就會認為它是bigkey。
- 字符串類型:它的big體現(xiàn)在單個value值很大,一般認為超過10KB就是bigkey。
- 非字符串類型:哈希、列表、集合、有序集合,它們的big體現(xiàn)在元素個數(shù)太多。
一般來說,string類型控制在10KB以內(nèi),hash、list、set、zset元素個數(shù)不要超過5000。
反例:一個包含200萬個元素的list。
非字符串的bigkey,不要使用del刪除,使用hscan、sscan、zscan方式漸進式刪除,同時要注意防止bigkey過期時間自動刪除問題(例如一個200萬的zset設置1小時過期,會觸發(fā)del操作,造成阻塞)
bigkey的危害:
-
導致redis阻塞
-
網(wǎng)絡擁塞
bigkey也就意味著每次獲取要產(chǎn)生的網(wǎng)絡流量較大,假設一個bigkey為1MB,客戶端每秒訪問量為1000,那么每秒產(chǎn)生1000MB的流量,對于普通的千兆網(wǎng)卡(按照字節(jié)算是128MB/s)的服務器來說簡直是滅頂之災,而且一般服務器會采用單機多實例的方式來部署,也就是說一個bigkey可能會對其他實例也造成影響,其后果不堪設想。 -
過期刪除
有個bigkey,它安分守己(只執(zhí)行簡單的命令,例如hget、lpop、zscore等),但它設置了過期時間,當它過期后,會被刪除,如果沒有使用Redis 4.0的過期異步刪除(lazyfree-lazy-expire yes),就會存在阻塞Redis的可能性。
bigkey的產(chǎn)生:
一般來說,bigkey的產(chǎn)生都是由于程序設計不當,或者對于數(shù)據(jù)規(guī)模預料不清楚造成的,來看幾個例子:
(1) 社交類:粉絲列表,如果某些明星或者大v不精心設計下,必是bigkey。
(2) 統(tǒng)計類:例如按天存儲某項功能或者網(wǎng)站的用戶集合,除非沒幾個人用,否則必是bigkey。
(3) 緩存類:將數(shù)據(jù)從數(shù)據(jù)庫load出來序列化放到Redis里,這個方式非常常用,但有兩個地方需要注意,第一,是不是有必要把所有字段都緩存;第二,有沒有相關關聯(lián)的數(shù)據(jù),有的同學為了圖方便把相關數(shù)據(jù)都存一個key下,產(chǎn)生bigkey。
如何優(yōu)化bigkey
- 拆
big list: list1、list2、…listN
big hash:可以講數(shù)據(jù)分段存儲,比如一個大的key,假設存了1百萬的用戶數(shù)據(jù),可以拆分成200個key,每個key下面存放5000個用戶數(shù)據(jù) - 如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出來(例如有時候僅僅需要hmget,而不是hgetall),刪除也是一樣,盡量使用優(yōu)雅的方式來處理。
(2)【推薦】:選擇適合的數(shù)據(jù)類型。
例如:實體類型(要合理控制和使用數(shù)據(jù)結構,但也要注意節(jié)省內(nèi)存和性能之間的平衡)
反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football
正例:
hmset user:1 name tom age 19 favor football
3.【推薦】:控制key的生命周期,redis不是垃圾桶。
建議使用expire設置過期時間(條件允許可以打散過期時間,防止集中過期)。
3.2 命令使用
1.【推薦】 O(N)命令關注N的數(shù)量
例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明確N的值。有遍歷的需求可以使用hscan、sscan、zscan代替。
2.【推薦】:禁用命令
禁止線上使用keys、flushall、flushdb等,通過redis的rename機制禁掉命令,或者使用scan的方式漸進式處理。
3.【推薦】合理使用select
redis的多數(shù)據(jù)庫較弱,使用數(shù)字進行區(qū)分,很多客戶端支持較差,同時多業(yè)務用多數(shù)據(jù)庫實際還是單線程處理,會有干擾。
4.【推薦】使用批量操作提高效率
原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素個數(shù)(例如500以內(nèi),實際也和元素字節(jié)數(shù)有關)。
注意兩者不同:
- 原生命令是原子操作,pipeline是非原子操作。
- pipeline可以打包不同的命令,原生命令做不到
- pipeline需要客戶端和服務端同時支持。
5.【建議】Redis事務功能較弱,不建議過多使用,可以用lua替代
3.3 三、客戶端使用
1.【推薦】
避免多個應用使用一個Redis實例
正例:不相干的業(yè)務拆分,公共數(shù)據(jù)做服務化。
2.【推薦】
使用帶有連接池的數(shù)據(jù)庫,可以有效控制連接,同時提高效率,標準使用方式:
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(5);
jedisPoolConfig.setMaxIdle(2);
jedisPoolConfig.setTestOnBorrow(true);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//具體的命令
jedis.executeCommand()
} catch (Exception e) {
logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
//注意這里不是關閉連接,在JedisPool模式下,Jedis會被歸還給資源池。
if (jedis != null)
jedis.close();
}
連接池參數(shù)含義:
序號 | 參數(shù)名 | 含義 | 默認值 | 使用建議 |
---|---|---|---|---|
1 | maxTotal | 資源池中最大連接數(shù) | 8 | 設置建議見下面 |
2 | maxIdle | 資源池允許最大空閑的連接數(shù) | 8 | 設置建議見下面 |
3 | minIdle | 資源池確保最少空閑的連接數(shù) | 0 | 設置建議見下面 |
4 | blockWhenExhausted | 當資源池用盡后,調(diào)用者是否要等待。只有當為true時,下面的maxWaitMillis才會生效 | true | 建議使用默認值 |
5 | maxWaitMillis | 當資源池連接用盡后,調(diào)用者的最大等待時間(單位為毫秒) | -1:表示永不超時 | 不建議使用默認值 |
6 | testOnBorrow | 向資源池借用連接時是否做連接有效性檢測(ping),無效連接會被移除 | false | 業(yè)務量很大時候建議設置為false(多一次ping的開銷)。 |
7 | testOnReturn | 向資源池歸還連接時是否做連接有效性檢測(ping),無效連接會被移除 | false | 業(yè)務量很大時候建議設置為false(多一次ping的開銷)。 |
8 | jmxEnabled | 是否開啟jmx監(jiān)控,可用于監(jiān)控 | true | 建議開啟,但應用本身也要開啟 |
優(yōu)化建議:
- maxTotal:最大連接數(shù),早期的版本叫maxActive
實際上這個是一個很難回答的問題,考慮的因素比較多:
- 業(yè)務希望Redis并發(fā)量
- 客戶端執(zhí)行命令時間
- Redis資源:例如 nodes(例如應用個數(shù)) * maxTotal 是不能超過redis的最大連接數(shù)maxclients。
- 資源開銷:例如雖然希望控制空閑連接(連接池此刻可馬上使用的連接),但是不希望因為連接池的頻繁釋放創(chuàng)建連接造成不必靠開銷。
以一個例子說明,假設:
- 一次命令時間(borrow|return resource + Jedis執(zhí)行命令(含網(wǎng)絡) )的平均耗時約為1ms,一個連接的QPS大約是1000
- 業(yè)務期望的QPS是50000
那么理論上需要的資源池大小是50000 / 1000 = 50個。但事實上這是個理論值,還要考慮到要比理論值預留一些資源,通常來講maxTotal可以比理論值大一些。
但這個值不是越大越好,一方面連接太多占用客戶端和服務端資源,另一方面對于Redis這種高QPS的服務器,一個大命令的阻塞即使設置再大資源池仍然會無濟于事。
- maxIdle和minIdle
maxIdle實際上才是業(yè)務需要的最大連接數(shù),maxTotal是為了給出余量,所以maxIdle不要設置過小,否則會有new Jedis(新連接)開銷。
連接池的最佳性能是maxTotal = maxIdle,這樣就避免連接池伸縮帶來的性能干擾。但是如果并發(fā)量不大或者maxTotal設置過高,會導致不必要的連接資源浪費。一般推薦maxIdle可以設置為按上面的業(yè)務期望QPS計算出來的理論連接數(shù),maxTotal可以再放大一倍。
minIdle(最小空閑連接數(shù)),與其說是最小空閑連接數(shù),不如說是"至少需要保持的空閑連接數(shù)",在使用連接的過程中,如果連接數(shù)超過了minIdle,那么繼續(xù)建立連接,如果超過了maxIdle,當超過的連接執(zhí)行完業(yè)務后會慢慢被移出連接池釋放掉。
如果系統(tǒng)啟動完馬上就會有很多的請求過來,那么可以給redis連接池做預熱,比如快速的創(chuàng)建一些redis連接,執(zhí)行簡單命令,類似ping(),快速的將連接池里的空閑連接提升到minIdle的數(shù)量。
連接池預熱示例代碼:
List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = pool.getResource();
minIdleJedisList.add(jedis);
jedis.ping();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
//注意,這里不能馬上close將連接還回連接池,否則最后連接池里只會建立1個連接。。
//jedis.close();
}
}
//統(tǒng)一將預熱的連接還回連接池
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = minIdleJedisList.get(i);
//將連接歸還回連接池
jedis.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}
總之,要根據(jù)實際系統(tǒng)的QPS和調(diào)用redis客戶端的規(guī)模整體評估每個節(jié)點所使用的連接池大小。
3.【建議】
高并發(fā)下建議客戶端添加熔斷功能(例如sentinel、hystrix)
4.【推薦】
設置合理的密碼,如有必要可以使用SSL加密訪問
5.【建議】
Redis對于過期鍵有三種清除策略:
- 被動刪除:當讀/寫一個已經(jīng)過期的key時,會觸發(fā)惰性刪除策略,直接刪除掉這個過期key
- 主動刪除:由于惰性刪除策略無法保證冷數(shù)據(jù)被及時刪掉,所以Redis會定期(默認每100ms)主動淘汰一批已過期的key,這里的一批只是部分過期key,所以可能會出現(xiàn)部分key已經(jīng)過期但還沒有被清理掉的情況,導致內(nèi)存并沒有被釋放
- 當前已用內(nèi)存超過maxmemory限定時,觸發(fā)主動清理策略
主動清理策略在Redis 4.0 之前一共實現(xiàn)了 6 種內(nèi)存淘汰策略,在 4.0 之后,又增加了 2 種策略,總共8種:
a) 針對設置了過期時間的key做處理:
volatile-ttl:在篩選時,會針對設置了過期時間的鍵值對,根據(jù)過期時間的先后進行刪除,越早過期的越先被刪除。
volatile-random:就像它的名稱一樣,在設置了過期時間的鍵值對中,進行隨機刪除。
volatile-lru:會使用 LRU 算法篩選設置了過期時間的鍵值對刪除。
volatile-lfu:會使用 LFU 算法篩選設置了過期時間的鍵值對刪除。
b) 針對所有的key做處理:
allkeys-random:從所有鍵值對中隨機選擇并刪除數(shù)據(jù)。
allkeys-lru:使用 LRU 算法在所有數(shù)據(jù)中進行篩選刪除。
allkeys-lfu:使用 LFU 算法在所有數(shù)據(jù)中進行篩選刪除。
c) 不處理:
noeviction:不會剔除任何數(shù)據(jù),拒絕所有寫入操作并返回客戶端錯誤信息"(error) OOM command not allowed when used memory",此時Redis只響應讀操作。
LRU 算法(Least Recently Used,最近最少使用)
淘汰很久沒被訪問過的數(shù)據(jù),以最近一次訪問時間作為參考。
LFU 算法(Least Frequently Used,最不經(jīng)常使用)
淘汰最近一段時間被訪問次數(shù)最少的數(shù)據(jù),以次數(shù)作為參考。
當存在熱點數(shù)據(jù)時,LRU的效率很好,但偶發(fā)性的、周期性的批量操作會導致LRU命中率急劇下降,緩存污染情況比較嚴重。這時使用LFU可能更好點。
根據(jù)自身業(yè)務類型,配置好maxmemory-policy(默認是noeviction),推薦使用volatile-lru。如果不設置最大內(nèi)存,當 Redis 內(nèi)存超出物理內(nèi)存限制時,內(nèi)存的數(shù)據(jù)會開始和磁盤產(chǎn)生頻繁的交換 (swap),會讓 Redis 的性能急劇下降。
當Redis運行在主從模式時,只有主結點才會執(zhí)行過期刪除策略,然后把刪除操作”del key”同步到從結點刪除數(shù)據(jù)。
4. 系統(tǒng)內(nèi)核參數(shù)優(yōu)化
vm.swapiness
swap對于操作系統(tǒng)來說比較重要,當物理內(nèi)存不足時,可以將一部分內(nèi)存頁進行swap到硬盤上,以解燃眉之急。但世界上沒有免費午餐,swap空間由硬盤提供,對于需要高并發(fā)、高吞吐的應用來說,磁盤IO通常會成為系統(tǒng)瓶頸。在Linux中,并不是要等到所有物理內(nèi)存都使用完才會使用到swap,系統(tǒng)參數(shù)swppiness會決定操作系統(tǒng)使用swap的傾向程度。swappiness的取值范圍是0~100,swappiness的值越大,說明操作系統(tǒng)可能使用swap的概率越高,swappiness值越低,表示操作系統(tǒng)更加傾向于使用物理內(nèi)存。
swappiness的取值越大,說明操作系統(tǒng)可能使用swap的概率越高,越低則越傾向于使用物理內(nèi)存。
如果linux內(nèi)核版本<3.5,那么swapiness設置為0,這樣系統(tǒng)寧愿swap也不會oom killer(殺掉進程)
如果linux內(nèi)核版本>=3.5,那么swapiness設置為1,這樣系統(tǒng)寧愿swap也不會oom killer
一般需要保證redis不會被kill掉:
cat /proc/version #查看linux內(nèi)核版本
echo 1 > /proc/sys/vm/swappiness
echo vm.swapiness=1 >> /etc/sysctl.conf
PS:OOM killer 機制是指Linux操作系統(tǒng)發(fā)現(xiàn)可用內(nèi)存不足時,強制殺死一些用戶進程(非內(nèi)核進程),來保證系統(tǒng)有足夠的可用內(nèi)存進行分配。
vm.overcommit_memory(默認0)
0:表示內(nèi)核將檢查是否有足夠的可用物理內(nèi)存(實際不一定用滿)供應用進程使用;如果有足夠的可用物理內(nèi)存,內(nèi)存申請允許;否則,內(nèi)存申請失敗,并把錯誤返回給應用進程
1:表示內(nèi)核允許分配所有的物理內(nèi)存,而不管當前的內(nèi)存狀態(tài)如何
如果是0的話,可能導致類似fork等操作執(zhí)行失敗,申請不到足夠的內(nèi)存空間
Redis建議把這個值設置為1,就是為了讓fork操作能夠在低內(nèi)存下也執(zhí)行成功。
cat /proc/sys/vm/overcommit_memory
echo "vm.overcommit_memory=1" >> /etc/sysctl.conf
sysctl vm.overcommit_memory=1
合理設置文件句柄數(shù)
操作系統(tǒng)進程試圖打開一個文件(或者叫句柄),但是現(xiàn)在進程打開的句柄數(shù)已經(jīng)達到了上限,繼續(xù)打開會報錯:“Too many open files”文章來源:http://www.zghlxwxcb.cn/news/detail-505149.html
ulimit -a #查看系統(tǒng)文件句柄數(shù),看open files那項
ulimit -n 65535 #設置系統(tǒng)文件句柄數(shù)
慢查詢?nèi)罩荆簊lowlog文章來源地址http://www.zghlxwxcb.cn/news/detail-505149.html
Redis慢日志命令說明:
config get slow* #查詢有關慢日志的配置信息
config set slowlog-log-slower-than 20000 #設置慢日志使時間閾值,單位微秒,此處為20毫秒,即超過20毫秒的操作都會記錄下來,生產(chǎn)環(huán)境建議設置1000,也就是1ms,這樣理論上redis并發(fā)至少達到1000,如果要求單機并發(fā)達到1萬以上,這個值可以設置為100
config set slowlog-max-len 1024 #設置慢日志記錄保存數(shù)量,如果保存數(shù)量已滿,會刪除最早的記錄,最新的記錄追加進來。記錄慢查詢?nèi)罩緯rRedis會對長命令做截斷操作,并不會占用大量內(nèi)存,建議設置稍大些,防止丟失日志
config rewrite #將服務器當前所使用的配置保存到redis.conf
slowlog len #獲取慢查詢?nèi)罩玖斜淼漠斍伴L度
slowlog get 5 #獲取最新的5條慢查詢?nèi)罩?。慢查詢?nèi)罩居伤膫€屬性組成:標識ID,發(fā)生時間戳,命令耗時,執(zhí)行命令和參數(shù)
slowlog reset #重置慢查詢?nèi)罩?
到了這里,關于6. Redis緩存設計與性能優(yōu)化的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!