Redis高并發(fā)緩存設(shè)計(jì)問(wèn)題與性能優(yōu)化
1、緩存設(shè)計(jì)典型問(wèn)題
1.1、緩存穿透
緩存穿透是指查詢一個(gè)根本不存在的數(shù)據(jù),緩存層和存儲(chǔ)層都不會(huì)命中,通常出于容錯(cuò)的考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存層。
緩存穿透將導(dǎo)致不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢,失去了緩存保護(hù)后端存儲(chǔ)的意義。造成緩存穿透的基本原因有兩個(gè):
第一,自身業(yè)務(wù)代碼或者數(shù)據(jù)出現(xiàn)問(wèn)題。
第二,一些惡意攻擊、爬蟲(chóng)等造成大量空命中。
緩存穿透問(wèn)題解決方案:
public String get (String key){ JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPoolConfig.setMaxWaitMillis(5); //timeout,這里既是連接超時(shí)又是讀寫超時(shí),從Jedis2.8 開(kāi)始有區(qū)分 connectionTimeout 和soTimeout 的構(gòu)造函數(shù) JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 3000, "redis"); Jedis cache =jedisPool.getResource(); // 從緩存中獲取數(shù)據(jù) String cacheValue = cache.get(key); // 緩存為空,從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) if (StrUtil.isEmpty(cacheValue)){ // 從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) String storageValue = "data from db"; cache.set(key, storageValue); // 如果存儲(chǔ)數(shù)據(jù)為空,需要設(shè)置一個(gè)過(guò)去時(shí)間(300秒) if (StrUtil.isEmpty(storageValue)){ cache.expire(key,60 * 5); } return cacheValue; }else { // 緩存不為空,直接返回 return cacheValue; } }
1.2、布隆過(guò)濾器
對(duì)于惡意攻擊,向服務(wù)器請(qǐng)求大量不存在的數(shù)據(jù)造成的緩存穿透,還可以用布隆過(guò)濾器先做一次過(guò)濾,對(duì)于不存在的數(shù)據(jù)布隆過(guò)濾器一般都能夠過(guò)濾掉,不讓請(qǐng)求再往后端發(fā)送。當(dāng)布隆過(guò)濾器說(shuō)某個(gè)值存在時(shí),這個(gè)值可能不存在;當(dāng)它說(shuō)不存在時(shí),那就肯定不存在。
布隆過(guò)濾器就是一個(gè)大型的位數(shù)組和幾個(gè)不一樣的無(wú)偏hash函數(shù)。所謂無(wú)偏就是能夠把元素的hash值算得比較均勻。
向布隆過(guò)濾器中添加key時(shí),會(huì)使用多個(gè)hash函數(shù)對(duì)key進(jìn)行hash算得一個(gè)整數(shù)索引值然后對(duì)位數(shù)組長(zhǎng)度進(jìn)行取模運(yùn)算得到一個(gè)位置,每個(gè)hash函數(shù)都會(huì)算得一個(gè)不同的位置。再把位數(shù)組的這幾個(gè)位置都置為1就完成了add操作。
向布隆過(guò)濾器詢問(wèn)key是否存在時(shí),跟add一樣,也會(huì)把hash的幾個(gè)位置都算出來(lái),看看位數(shù)組中這幾個(gè)位置是否都為1,只要有一個(gè)位為0,那么說(shuō)明布隆過(guò)濾器中這個(gè)key不存在。如果都是1,這并不能說(shuō)明這個(gè)key就一定存在,只是極有可能存在,因?yàn)檫@些位被置為1可能是因?yàn)槠渌膋ey存在所致。如果這個(gè)位數(shù)組比較稀疏,這個(gè)概率就會(huì)很大,如果這個(gè)位數(shù)組比較擁擠,這個(gè)概率就會(huì)降低。
這種方法適用于數(shù)據(jù)命中不高、數(shù)據(jù)相對(duì)固定、實(shí)時(shí)性低(通常是數(shù)據(jù)集較大)的應(yīng)用場(chǎng)景,代碼維護(hù)較為復(fù)雜,但是緩存空間占用很少。
可以用redisson實(shí)現(xiàn)布隆過(guò)濾器,引入依賴:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.5</version> </dependency>
示例偽代碼:
import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.redisson.config.Config; /** * @Author hw * @Date 2024/10/8 10:55 * @PackageName:com.redxun.eip.controller.jobController * @ClassName: RedissonBloomFilter * @Description: redisson實(shí)現(xiàn)布隆過(guò)濾器 * @Version 1.0 */ public class RedissonBloomFilter { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 構(gòu)造Redisson RedissonClient redisson = Redisson.create(config); RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList"); //初始化布隆過(guò)濾器:預(yù)計(jì)元素為100000000L,誤差率為3%,根據(jù)這兩個(gè)參數(shù)會(huì)計(jì)算出底層bit數(shù)組大小 bloomFilter.tryInit(100000000L, 0.03); // 將zhuge插入布隆過(guò)濾器中 bloomFilter.add("zhuge"); // 判斷下面號(hào)碼是否存在于布隆過(guò)濾器中 System.out.println(bloomFilter.contains("guojia"));// false System.out.println(bloomFilter.contains("baiqi")); // false System.out.println(bloomFilter.contains("zhuge")); // true } }
使用布隆過(guò)濾器需要把所有數(shù)據(jù)提前放入布隆過(guò)濾器,并且在增加數(shù)據(jù)時(shí)也要往布隆過(guò)濾器里放,布隆過(guò)濾器緩存過(guò)濾偽代碼:
import cn.hutool.core.util.StrUtil; import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.util.ArrayList; import java.util.List; /** * @Author hw * @Date 2024/10/8 10:55 * @PackageName:com.redxun.eip.controller.jobController * @ClassName: RedissonBloomFilter * @Description: redisson實(shí)現(xiàn)布隆過(guò)濾器 * @Version 1.0 */ public class RedissonBloomFilter { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 構(gòu)造Redisson RedissonClient redisson = Redisson.create(config); RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList"); //初始化布隆過(guò)濾器:預(yù)計(jì)元素為100000000L,誤差率為3%,根據(jù)這兩個(gè)參數(shù)會(huì)計(jì)算出底層bit數(shù)組大小 bloomFilter.tryInit(100000000L, 0.03); // 將zhuge插入布隆過(guò)濾器中 bloomFilter.add("zhuge"); // 判斷下面號(hào)碼是否存在于布隆過(guò)濾器中 System.out.println(bloomFilter.contains("guojia"));// false System.out.println(bloomFilter.contains("baiqi")); // false System.out.println(bloomFilter.contains("zhuge")); // true } /** * 添加所有key數(shù)據(jù)到布隆過(guò)濾器中 */ public void pushBloomFilter() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 構(gòu)造Redisson RedissonClient redisson = Redisson.create(config); RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList"); //初始化布隆過(guò)濾器:預(yù)計(jì)元素為100000000L,誤差率為3%,根據(jù)這兩個(gè)參數(shù)會(huì)計(jì)算出底層bit數(shù)組大小 bloomFilter.tryInit(100000000L, 0.03); List<String> keys = new ArrayList<>(); keys.add("zhuge"); keys.add("guojia"); keys.add("baiqi"); // 把所有數(shù)據(jù)存入布隆過(guò)濾器 for (String key: keys) { bloomFilter.add(key); } } /** * 從布隆過(guò)濾器中過(guò)濾并獲取數(shù)據(jù) * @param key * @return */ public String get (String key){ Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 構(gòu)造Redisson RedissonClient redisson = Redisson.create(config); RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList"); // 從布隆過(guò)濾器這一級(jí)緩存判斷下key是否存在 boolean exists = bloomFilter.contains(key); if (!exists){ return ""; } JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPoolConfig.setMaxWaitMillis(5); //timeout,這里既是連接超時(shí)又是讀寫超時(shí),從Jedis2.8 開(kāi)始有區(qū)分 connectionTimeout 和soTimeout 的構(gòu)造函數(shù) JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 3000, "redis"); Jedis cache =jedisPool.getResource(); // 從緩存中獲取數(shù)據(jù) String cacheValue = cache.get(key); // 緩存為空,從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) if (StrUtil.isEmpty(cacheValue)){ // 從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) String storageValue = "data from db"; cache.set(key, storageValue); // 如果存儲(chǔ)數(shù)據(jù)為空,需要設(shè)置一個(gè)過(guò)去時(shí)間(300秒) if (StrUtil.isEmpty(storageValue)){ cache.expire(key,60 * 5); } return cacheValue; }else { // 緩存不為空,直接返回 return cacheValue; } } }
注意:布隆過(guò)濾器不能刪除數(shù)據(jù),如果要?jiǎng)h除得重新初始化數(shù)據(jù)。
1.3、緩存失效(擊穿)
由于大批量緩存在同一時(shí)間失效可能導(dǎo)致大量請(qǐng)求同時(shí)穿透緩存直達(dá)數(shù)據(jù)庫(kù),可能會(huì)造成數(shù)據(jù)庫(kù)瞬間壓力過(guò)大甚至掛掉,對(duì)于這種情況我們?cè)谂吭黾泳彺鏁r(shí)最好將這一批數(shù)據(jù)的緩存過(guò)期時(shí)間設(shè)置為一個(gè)時(shí)間段內(nèi)的不同時(shí)間。
示例偽代碼:
public String get (String key){ // 連接redis JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPoolConfig.setMaxWaitMillis(5); //timeout,這里既是連接超時(shí)又是讀寫超時(shí),從Jedis2.8 開(kāi)始有區(qū)分 connectionTimeout 和soTimeout 的構(gòu)造函數(shù) JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 3000, "redis"); Jedis cache =jedisPool.getResource(); // 從緩存中獲取數(shù)據(jù) String cacheValue = cache.get(key); // 緩存為空,從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) if (StrUtil.isEmpty(cacheValue)){ // 從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) String storageValue = "data from db"; cache.set(key, storageValue); // 設(shè)置一個(gè)過(guò)期時(shí)間(300到600之間的一個(gè)隨機(jī)數(shù)) int expireTime = new Random().nextInt(300) + 300; if (StrUtil.isEmpty(storageValue)){ cache.expire(key,expireTime); } return cacheValue; }else { // 緩存不為空,直接返回 return cacheValue; } }
1.4、緩存雪崩
緩存雪崩指的是緩存層支撐不住或宕掉后,流量會(huì)像奔逃的野牛一樣,打向后端存儲(chǔ)層。
由于緩存層承載著大量請(qǐng)求,有效地保護(hù)了存儲(chǔ)層,但是如果緩存層由于某些原因不能提供服務(wù)(比如超大并發(fā)過(guò)來(lái),緩存層支撐不住,或者由于緩存設(shè)計(jì)不好,類似大量請(qǐng)求訪問(wèn)bigkey,導(dǎo)致緩存能支撐的并發(fā)急劇下降),于是大量請(qǐng)求都會(huì)打到存儲(chǔ)層,存儲(chǔ)層的調(diào)用量會(huì)暴增,造成存儲(chǔ)層也會(huì)級(jí)聯(lián)宕機(jī)的情況。
預(yù)防和解決緩存雪崩問(wèn)題,可以從以下三個(gè)方面進(jìn)行著手。
1)保證緩存層服務(wù)高可用性,比如使用 RedisSentinel 或 RedisCluster。
2)依賴隔離組件為后端限流熔斷并降級(jí)。比如使用 Sentinel 或 Hystrix 限流降級(jí)組件。
比如服務(wù)降級(jí),我們可以針對(duì)不同的數(shù)據(jù)采取不同的處理方式。當(dāng)業(yè)務(wù)應(yīng)用訪問(wèn)的是非核心數(shù)據(jù)(例如電商商品屬性,用戶信息等)時(shí),暫時(shí)停止從緩存中查詢這些數(shù)據(jù),而是直接返回預(yù)定義的默認(rèn)降級(jí)信息、空值或是錯(cuò)誤提示信息;當(dāng)業(yè)務(wù)應(yīng)用訪問(wèn)的是核心數(shù)據(jù)(例如電商商品庫(kù)存)時(shí),仍然允許查詢緩存,如果緩存缺失,也可以繼續(xù)通過(guò)數(shù)據(jù)庫(kù)讀取。
3)提前演練。在項(xiàng)目上線前,演練緩存層宕掉后,應(yīng)用以及后端的負(fù)載情況以及可能出現(xiàn)的問(wèn)題,在此基礎(chǔ)上做一些預(yù)案設(shè)定。
1.5、熱點(diǎn)緩存key重建優(yōu)化
開(kāi)發(fā)人員使用“緩存+過(guò)期時(shí)間”的策略既可以加速數(shù)據(jù)讀寫,又保證數(shù)據(jù)的定期更新,這種模式基本能夠滿足絕大部分需求。但是有兩個(gè)問(wèn)題如果同時(shí)出現(xiàn),可能就會(huì)對(duì)應(yīng)用造成致命的危害:
- 當(dāng)前key是一個(gè)熱點(diǎn)key(例如一個(gè)熱門的娛樂(lè)新聞),并發(fā)量非常大。
- 重建緩存不能在短時(shí)間完成,可能是一個(gè)復(fù)雜計(jì)算,例如復(fù)雜的SQL、多次IO、多個(gè)依賴等。
在緩存失效的瞬間,有大量線程來(lái)重建緩存,造成后端負(fù)載加大,甚至可能會(huì)讓應(yīng)用崩潰。
要解決這個(gè)問(wèn)題主要就是要避免大量線程同時(shí)重建緩存。
我們可以利用互斥鎖來(lái)解決,此方法只允許一個(gè)線程重建緩存,其他線程等待重建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)即可。
示例偽代碼:
public String get (String key) throws InterruptedException { // 連接redis JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPoolConfig.setMaxWaitMillis(5); //timeout,這里既是連接超時(shí)又是讀寫超時(shí),從Jedis2.8 開(kāi)始有區(qū)分 connectionTimeout 和soTimeout 的構(gòu)造函數(shù) JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 3000, "redis"); Jedis cache = jedisPool.getResource(); // 從緩存中獲取數(shù)據(jù) String value = cache.get(key); // 緩存為空,從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) if (StrUtil.isEmpty(value)){ // 只允許一個(gè)線程重建緩存,使用 nx, 并設(shè)置過(guò)期時(shí)間 ex String mutexKey = "mutex:key:" + key; if (cache.set(mutexKey, "1","ex 180","nx")) { // 從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù) value = "dbValue"; // 回寫Redis, 并設(shè)置過(guò)期時(shí)間 cache.set(key, value); cache.expire(key, 60*5); // 刪除 key_mutex cache.del(mutexKey); }else { //其他線程等待 50 毫秒后重試 Thread.sleep(50); get(key); } } return value; }
1.6、緩存與數(shù)據(jù)庫(kù)雙寫不一致
在大并發(fā)下,同時(shí)操作數(shù)據(jù)庫(kù)與緩存會(huì)存在數(shù)據(jù)不一致性問(wèn)題
1.6.1、雙寫不一致情況
1.6.2、讀寫并發(fā)不一致
解決方案:
1、對(duì)于并發(fā)幾率很小的數(shù)據(jù)(如個(gè)人維度的訂單數(shù)據(jù)、用戶數(shù)據(jù)等),這種幾乎不用考慮這個(gè)問(wèn)題,很少會(huì)發(fā)生緩存不一致,可以給緩存數(shù)據(jù)加上過(guò)期時(shí)間,每隔一段時(shí)間觸發(fā)讀的主動(dòng)更新即可。
2、就算并發(fā)很高,如果業(yè)務(wù)上能容忍短時(shí)間的緩存數(shù)據(jù)不一致(如商品名稱,商品分類菜單等),緩存加上過(guò)期時(shí)間依然可以解決大部分業(yè)務(wù)對(duì)于緩存的要求。
3、如果不能容忍緩存數(shù)據(jù)不一致,可以通過(guò)加讀寫鎖保證并發(fā)讀寫或?qū)憣懙臅r(shí)候按順序排好隊(duì),讀讀的時(shí)候相當(dāng)于無(wú)鎖。
4、也可以用阿里開(kāi)源的canal通過(guò)監(jiān)聽(tīng)數(shù)據(jù)庫(kù)的binlog日志及時(shí)的去修改緩存,但是引入了新的中間件,增加了系統(tǒng)的復(fù)雜度。
總結(jié):
以上我們針對(duì)的都是讀多寫少的情況加入緩存提高性能,如果寫多讀多的情況又不能容忍緩存數(shù)據(jù)不一致,那就沒(méi)必要加緩存了,可以直接操作數(shù)據(jù)庫(kù)。當(dāng)然,如果數(shù)據(jù)庫(kù)抗不住壓力,還可以把緩存作為數(shù)據(jù)讀寫的主存儲(chǔ),異步將數(shù)據(jù)同步到數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)只是作為數(shù)據(jù)的備份。
放入緩存的數(shù)據(jù)應(yīng)該是對(duì)實(shí)時(shí)性、一致性要求不是很高的數(shù)據(jù)。切記不要為了用緩存,同時(shí)又要保證絕對(duì)的一致性做大量的過(guò)度設(shè)計(jì)和控制,增加系統(tǒng)復(fù)雜性!
1.7、開(kāi)發(fā)規(guī)范與性能優(yōu)化
1.7.1、鍵值設(shè)計(jì)
1.key名設(shè)計(jì)
(1)【建議】:可讀性和可管理性
以業(yè)務(wù)名(或數(shù)據(jù)庫(kù)名)為前綴(防止key沖突),用冒號(hào)分隔,比如業(yè)務(wù)名:表名:id
rade:order:1
(2)【建議】:簡(jiǎn)潔性
保證語(yǔ)義的前提下,控制key的長(zhǎng)度,當(dāng)key較多時(shí),內(nèi)存占用也不容忽視,例如:
user:{uid}:friends:messages:{mid}簡(jiǎn)化為u:{uid}:fr:m:{mid}
(3)【強(qiáng)制】:不要包含特殊字符
反例:包含空格、換行、單雙引號(hào)以及其他轉(zhuǎn)義字符
2.value設(shè)計(jì)
(1)【強(qiáng)制】:拒絕 bigkey (防止網(wǎng)卡流量、慢查詢)
在Redis中,一個(gè)字符串最大512MB,一個(gè)二級(jí)數(shù)據(jù)結(jié)構(gòu)(例如hash、list、set、zset)可以存儲(chǔ)大約40億個(gè)(2^32-1)個(gè)元素,但實(shí)際中如果下面兩種情況,我就會(huì)認(rèn)為它是bigkey。
1.字符串類型:它的big體現(xiàn)在單個(gè)value值很大,一般認(rèn)為超過(guò)10KB就是bigkey。
2.非字符串類型:哈希、列表、集合、有序集合,它們的big體現(xiàn)在元素個(gè)數(shù)太多。
一般來(lái)說(shuō),string類型控制在10KB以內(nèi),hash、list、set、zset元素個(gè)數(shù)不要超過(guò)5000。
反例:一個(gè)包含200萬(wàn)個(gè)元素的list。
非字符串的bigkey,不要使用del刪除,使用hscan、sscan、zscan方式漸進(jìn)式刪除,同時(shí)要注意防止bigkey過(guò)期時(shí)間自動(dòng)刪除問(wèn)題(例如一個(gè)200萬(wàn)的zset設(shè)置1小時(shí)過(guò)期,會(huì)觸發(fā)del操作,造成阻塞)
bigkey的危害:
1.導(dǎo)致redis阻塞
2.網(wǎng)絡(luò)擁塞
bigkey也就意味著每次獲取要產(chǎn)生的網(wǎng)絡(luò)流量較大,假設(shè)一個(gè)bigkey為1MB,客戶端每秒訪問(wèn)量為1000,那么每秒產(chǎn)生1000MB的流量,對(duì)于普通的千兆網(wǎng)卡(按照字節(jié)算是128MB/s)的服務(wù)器來(lái)說(shuō)簡(jiǎn)直是滅頂之災(zāi),而且一般服務(wù)器會(huì)采用單機(jī)多實(shí)例的方式來(lái)部署,也就是說(shuō)一個(gè)bigkey可能會(huì)對(duì)其他實(shí)例也造成影響,其后果不堪設(shè)想。
3.過(guò)期刪除
有個(gè)bigkey,它安分守己(只執(zhí)行簡(jiǎn)單的命令,例如hget、lpop、zscore等),但它設(shè)置了過(guò)期時(shí)間,當(dāng)它過(guò)期后,會(huì)被刪除,如果沒(méi)有使用Redis4.0的過(guò)期異步刪除(lazyfree-lazyexpireyes),就會(huì)存在阻塞Redis的可能性。
bigkey的產(chǎn)生:
一般來(lái)說(shuō),bigkey的產(chǎn)生都是由于程序設(shè)計(jì)不當(dāng),或者對(duì)于數(shù)據(jù)規(guī)模預(yù)料不清楚造成的,來(lái)看幾個(gè)例子:
(1)社交類:粉絲列表,如果某些明星或者大v不精心設(shè)計(jì)下,必是bigkey。
(2)統(tǒng)計(jì)類:例如按天存儲(chǔ)某項(xiàng)功能或者網(wǎng)站的用戶集合,除非沒(méi)幾個(gè)人用,否則必是bigkey。
(3)緩存類:將數(shù)據(jù)從數(shù)據(jù)庫(kù)load出來(lái)序列化放到Redis里,這個(gè)方式非常常用,但有兩個(gè)地方需要注意,第一,是不是有必要把所有字段都緩存;第二,有沒(méi)有相關(guān)關(guān)聯(lián)的數(shù)據(jù),有的同學(xué)為了圖方便把相關(guān)數(shù)據(jù)都存一個(gè)key下,產(chǎn)生bigkey。
如何優(yōu)化bigkey
1.拆
biglist:list1、list2、...listN
bighash:可以講數(shù)據(jù)分段存儲(chǔ),比如一個(gè)大的key,假設(shè)存了1百萬(wàn)的用戶數(shù)據(jù),可以拆分成200個(gè)key,每個(gè)key下面存放5000個(gè)用戶數(shù)據(jù)
2.如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出來(lái)(例如有時(shí)候僅僅需要hmget,而不是hgetall),刪除也是一樣,盡量使用優(yōu)雅的方式來(lái)處理。
(2)【推薦】:選擇適合的數(shù)據(jù)類型。
例如:實(shí)體類型(要合理控制和使用數(shù)據(jù)結(jié)構(gòu),但也要注意節(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設(shè)置過(guò)期時(shí)間(條件允許可以打散過(guò)期時(shí)間,防止集中過(guò)期)。
1.7.2、命令使用
1.【推薦】O(N)命令關(guān)注N的數(shù)量
例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明確N的值。有遍歷的需求可以使用hscan、sscan、zscan代替。
2.【推薦】:禁用命令
禁止線上使用keys、flushall、flushdb等,通過(guò)redis的rename機(jī)制禁掉命令,或者使用scan的方式漸進(jìn)式處理。
3.【推薦】合理使用select
redis的多數(shù)據(jù)庫(kù)較弱,使用數(shù)字進(jìn)行區(qū)分,很多客戶端支持較差,同時(shí)多業(yè)務(wù)用多數(shù)據(jù)庫(kù)實(shí)際還是單線程處理,會(huì)有干擾。
4.【推薦】使用批量操作提高效率
原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素個(gè)數(shù)(例如500以內(nèi),實(shí)際也和元素字節(jié)數(shù)有關(guān))。注意兩者不同:
1.原生命令是原子操作,pipeline 是非原子操作。
2.pipeline 可以打包不同的命令,原生命令做不到
3.pipeline 需要客戶端和服務(wù)端同時(shí)支持。
5.【建議】Redis事務(wù)功能較弱,不建議過(guò)多使用,可以用lua替代
到此這篇關(guān)于Redis高并發(fā)緩存設(shè)計(jì)問(wèn)題與性能優(yōu)化的文章就介紹到這了,更多相關(guān)Redis高并發(fā)緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Deepin UOS編譯安裝Redis的實(shí)現(xiàn)步驟
本文主要介紹了Deepin UOS編譯安裝Redis的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01詳解Redis SCAN命令實(shí)現(xiàn)有限保證的原理
這篇文章主要介紹了Redis SCAN命令實(shí)現(xiàn)有限保證的原理,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-07-07redis查詢keys報(bào)錯(cuò)的實(shí)現(xiàn)
在Redis中使用KEYS命令來(lái)查詢所有符合特定模式的鍵名是一個(gè)常見(jiàn)需求,本文主要介紹了redis查詢keys報(bào)錯(cuò)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2025-04-04Mac中Redis服務(wù)啟動(dòng)時(shí)錯(cuò)誤信息:NOAUTH Authentication required
這篇文章主要介紹了Mac中使用Redis服務(wù)啟動(dòng)時(shí)錯(cuò)誤信息:"NOAUTH Authentication required"問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08Redis中LRU算法和LFU算法的區(qū)別小結(jié)
在Redis中,LRU算法和LFU算法是兩種常用的緩存淘汰算法,它們可以幫助我們優(yōu)化緩存性能,本文主要介紹了Redis中LRU算法和LFU算法的區(qū)別,感興趣的可以了解一下2023-12-12redis實(shí)現(xiàn)動(dòng)態(tài)字符串SDS
簡(jiǎn)單動(dòng)態(tài)字符串是Redis的基本數(shù)據(jù)結(jié)構(gòu)之一,用于存儲(chǔ)字符串和整型數(shù)據(jù),本文主要介紹了redis實(shí)現(xiàn)動(dòng)態(tài)字符串SDS,具有一定的參考價(jià)值,感興趣的可以了解一下2024-04-04Redis面試必備之緩存設(shè)計(jì)規(guī)范與性能優(yōu)化詳解
你是否在使用Redis時(shí),不清楚Redis應(yīng)該遵循的設(shè)計(jì)規(guī)范而苦惱,你是否在Redis出現(xiàn)性能問(wèn)題時(shí),不知道該如何優(yōu)化而發(fā)愁,快跟隨小編一起學(xué)習(xí)起來(lái)吧2024-03-03