參考:
圖靈課堂
快取穿透之布隆過濾器
布隆過濾器,底層是一個大的bitmap陣列,值是0或者1,經過多個hash函式進行計算後,針對布隆過濾器的長度取模,針對取模得到的值的位置賦值為1,因為hash函式存在計算衝突,所以會有一定的誤差,但是這個是可以接受的。同時布隆過濾器初始化的時候要指定儲存的元素大概個數,然後指定誤差率,這樣可以根據這兩個引數進行初始化布隆過濾器的長度;誤差率當然是越小越好,但是越小會導致陣列長度增加,並且hash函式增加,每次運算的效率就會下降,這個是要綜合考慮的。並且布隆過濾器是不能刪除無效資料的,這個是要注意的。
布穀鳥過濾器據說是可以進行資料的刪除,但是生產使用並不多。
<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%,根據這兩個引數會計算出底層的bit陣列大小
bloomFilter.tryInit(100000000L,0.03);
//將zhuge插入到布隆過濾器中
bloomFilter.add("hh");
//判斷下面號碼是否在布隆過濾器中
System.out.println(bloomFilter.contains("dd"));//false
System.out.println(bloomFilter.contains("gg"));//false
System.out.println(bloomFilter.contains("hh"));//true
}
}
//初始化布隆過濾器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆過濾器:預計元素為100000000L,誤差率為3%
bloomFilter.tryInit(100000000L,0.03);
//把所有資料存入布隆過濾器
void init(){
for (String key: keys) {
bloomFilter.put(key);
}
}
String get(String key) {
// 從布隆過濾器這一級快取判斷下key是否存在
Boolean exist = bloomFilter.contains(key);
if(!exist){
return "";
}
// 從快取中獲取資料
String cacheValue = cache.get(key);
// 快取為空
if (StringUtils.isBlank(cacheValue)) {
// 從儲存中獲取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果儲存資料為空, 需要設定一個過期時間(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 快取非空
return cacheValue;
}
}
快取雪崩
4)快取過期時間不在同一時間過期,在基礎上加上一個隨機值。
熱點快取key重建最佳化
熱點快取key失效就類似於快取擊穿。
- 當前key是一個熱點key(例如一個熱門的娛樂新聞),併發量非常大。
- 重建快取不能在短時間完成, 可能是一個複雜計算, 例如複雜的SQL、 多次IO、 多個依賴等。
String get(String key) {
// 從Redis中獲取資料
String value = redis.get(key);
// 如果value為空, 則開始重構快取
if (value == null) {
// 只允許一個執行緒重建快取, 使用nx, 並設定過期時間ex
String mutexKey = "mutext:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 從資料來源獲取資料
value = db.get(key);
// 回寫Redis, 並設定過期時間
redis.setex(key, timeout, value);
// 刪除key_mutex
redis.delete(mutexKey);
}// 其他執行緒休息50毫秒後重試
else {
Thread.sleep(50);
get(key);
}
}
return value;
}
快取與資料庫雙寫不一致
開發規範與效能最佳化
一、鍵值設計
1. key名設計
- (1)【建議】: 可讀性和可管理性
- (2)【建議】:簡潔性
- (3)【強制】:不要包含特殊字元
2. value設計
- (1)【強制】:拒絕bigkey(防止網路卡流量、慢查詢)
- 字串型別:它的big體現在單個value值很大,一般認為超過10KB就是bigkey。
- 非字串型別:雜湊、列表、集合、有序集合,它們的big體現在元素個數太多。
bigkey的危害:
bigkey的產生:
- (2)【推薦】:選擇適合的資料型別。
二、命令使用
1.【推薦】 O(N)命令關注N的數量
2.【推薦】:禁用命令
3.【推薦】合理使用select
4.【推薦】使用批次操作提高效率
5.【建議】
三、客戶端使用
1.【推薦】
2.【推薦】
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();
}
連線池引數含義:
序號
|
引數名 | 含義 | 預設值 | 使用建議 |
1 |
maxTotal
|
資源池中最大連線數
|
8 | 如下 |
2 |
maxIdle
|
資源池允許最大空閒
的連線數
|
8 | 如下 |
3 |
minIdle
|
資源池確保最少空閒
的連線數
|
0 | 如下 |
4 | blockWhenExhausted |
當資源池用盡後,呼叫者是否要等待。只有當為true時,下面
的maxWaitMillis才會生效
|
true | 建議使用預設值 |
5 |
maxWaitMillis
|
當資源池連線用盡後,呼叫者的最大等待時間(單位為毫秒)
|
-1:表示永不超時
|
不建議使用預設值 |
6 |
testOnBorrow
|
向資源池借用連線時是否做連線有效性檢測(ping),無效連線會被移除
|
false |
業務量很大時候建議設定為false(多一次ping的開銷)。
|
7 |
testOnReturn
|
向資源池歸還連線時是否做連線有效性檢測(ping),無效連線會被移除
|
false |
業務量很大時候建議設定為false(多一次ping的開銷)。
|
8 |
jmxEnabled
|
是否開啟jmx監控,可用於監控
|
true |
建議開啟,但應用本身也要開啟
|
最佳化建議:
1)maxTotal:最大連線數,早期的版本叫maxActive 實際上這個是一個很難回答的問題,考慮的因素比較多:
業務希望Redis併發量
客戶端執行命令時間
Redis資源:例如 nodes(例如應用個數) * maxTotal 是不能超過redis的最大連線數 maxclients。
資源開銷:例如雖然希望控制空閒連線(連線池此刻可馬上使用的連線),但是不希望因 為連線池的頻繁釋放建立連線造成不必靠開銷。
以一個例子說明。
假設: 一次命令時間(borrow|return resource + Jedis執行命令(含網路) )的平均耗時約為 1ms,一個連線的QPS大約是1000
業務期望的QPS是50000
那麼理論上需要的資源池大小是50000 / 1000 = 50個。
但事實上這是個理論值,還要考慮到要 比理論值預留一些資源,通常來講maxTotal可以比理論值大一些。
但這個值不是越大越好,一方面連線太多佔用客戶端和服務端資源,另一方面對於Redis這種高 QPS的伺服器,一個大命令的阻塞即使設定再大資源池仍然會無濟於事。
2)maxIdle和minIdle
maxIdle實際上才是業務需要的最大連線數,maxTotal是為了給出餘量,所以maxIdle不要設定 過小,否則會有new Jedis(新連線)開銷。
連線池的最佳效能是maxTotal = maxIdle,這樣就避免連線池伸縮帶來的效能干擾。但是如果 併發量不大或者maxTotal設定過高,會導致不必要的連線資源浪費。一般推薦maxIdle可以設定 為按上面的業務期望QPS計算出來的理論連線數,maxTotal可以再放大一倍。
minIdle(最小空閒連線數),與其說是最小空閒連線數,不如說是"至少需要保持的空閒連線 數",在使用連線的過程中,如果連線數超過了minIdle,那麼繼續建立連線,如果超過了 maxIdle,當超過的連線執行完業務後會慢慢被移出連線池釋放掉。
如果系統啟動完馬上就會有很多的請求過來,那麼可以給redis連線池做預熱,比如快速的建立一 些redis連線,執行簡單命令,類似ping(),快速的將連線池裡的空閒連線提升到minIdle的數量。、
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個連線。。
// 這裡是因為每次初始化一個連線之後,如果close,就會將這個連線放入到連線池,然後下次迴圈去連線池那連線還是這一個連線,因為這個for迴圈是序列的。
//jedis.close();
}
}
//統一將預熱的連線還回連線池
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 {
}
}
3.【建議】
高併發下建議客戶端新增熔斷功能(例如sentinel、hystrix)
4.【推薦】
設定合理的密碼,如有必要可以使用SSL加密訪問
5.【建議】
Redis對於過期鍵有三種清除策略:
被動刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key
主動刪除:由於惰性刪除策略無法保證冷資料被及時刪掉,所以Redis會定期(預設每100ms)主動淘汰一批已過期的key,這裡的一批只是部分過期key,所以可能會出現部分key已經過期但還沒有被清理掉的情況,導致記憶體並沒有被釋放
當前已用記憶體超過maxmemory限定時,觸發主動清理策略
主動清理策略在Redis 4.0 之前一共實現了 6 種記憶體淘汰策略,在 4.0 之後,又增加了 2 種策略,總共8種:
a) 針對設定了過期時間的key做處理:
volatile-ttl:在篩選時,會針對設定了過期時間的鍵值對,根據過期時間的先後進行刪除,越早過期的越先被刪除。
volatile-random:就像它的名稱一樣,在設定了過期時間的鍵值對中,進行隨機刪除。
volatile-lru:會使用 LRU 演算法篩選設定了過期時間的鍵值對刪除。
volatile-lfu:會使用 LFU 演算法篩選設定了過期時間的鍵值對刪除。
b) 針對所有的key做處理:
allkeys-random:從所有鍵值對中隨機選擇並刪除資料。
allkeys-lru:使用 LRU 演算法在所有資料中進行篩選刪除。
allkeys-lfu:使用 LFU 演算法在所有資料中進行篩選刪除。
c) 不處理:
noeviction:不會剔除任何資料,拒絕所有寫入操作並返回客戶端錯誤資訊"(error) OOM command not allowed when used memory",此時Redis只響應讀操作。
LRU 演算法(Least Recently Used,最近最少使用) 淘汰很久沒被訪問過的資料,以最近一次訪問時間作為參考。
LFU 演算法(Least Frequently Used,最不經常使用) 淘汰最近一段時間被訪問次數最少的資料,以次數作為參考。
當存在熱點資料時,LRU的效率很好,但偶發性的、週期性的批次操作會導致LRU命中率急劇下降,快取汙染情況比較嚴重。這時使用LFU可能更好點。 根據自身業務型別,配置好maxmemory-policy(預設是noeviction),推薦使用volatile-lru。如果不設定最大記憶體,當 Redis 記憶體超出實體記憶體限制時,記憶體的資料會開始和磁碟產生頻繁的交換 (swap),會讓 Redis 的效能急劇下降。 當Redis執行在主從模式時,只有主結點才會執行過期刪除策略,然後把刪除操作”del key”同步到從結點刪除資料。