據(jù)庫與數(shù)據(jù)存儲:Redis)
1. 引言在當(dāng)今互聯(lián)網(wǎng)高并發(fā)場景下數(shù)據(jù)庫的性能瓶頸往往成為系統(tǒng)擴(kuò)展的最大障礙。Redis 作為一款高性能的內(nèi)存數(shù)據(jù)庫憑借其豐富的數(shù)據(jù)結(jié)構(gòu)、極快的讀寫速度和靈活的持久化機(jī)制已經(jīng)成為 Java 后端開發(fā)中不可或缺的組件。本文將系統(tǒng)性地介紹 Redis 的核心知識體系包括五種基本數(shù)據(jù)結(jié)構(gòu)、持久化機(jī)制、集群部署模式以及面試中高頻出現(xiàn)的緩存穿透、緩存雪崩和緩存擊穿三大經(jīng)典問題。無論你是準(zhǔn)備面試還是實(shí)際項(xiàng)目開發(fā)這篇文章都能為你提供完整的知識框架。2. Redis 概述2.1 什么是 RedisRedisRemote Dictionary Server是一個(gè)開源的、基于內(nèi)存的鍵值對存儲系統(tǒng)由 Salvatore Sanfilippo 于 2009 年開發(fā)。它支持多種數(shù)據(jù)結(jié)構(gòu)如字符串、哈希、列表、集合和有序集合并提供了豐富的操作命令。2.2 Redis 的核心特性高性能基于內(nèi)存操作單線程模型避免了并發(fā)競爭讀寫速度可達(dá) 10 萬次/秒以上豐富的數(shù)據(jù)結(jié)構(gòu)支持 String、Hash、List、Set、ZSet 五種基本類型及 Bitmap、HyperLogLog、Geo 等高級類型持久化機(jī)制支持 RDB 快照和 AOF 日志兩種持久化方式高可用支持主從復(fù)制、哨兵模式和集群模式原子性操作所有命令都是原子操作支持事務(wù)和 Lua 腳本2.3 Redis 的應(yīng)用場景緩存系統(tǒng)減輕數(shù)據(jù)庫壓力提升系統(tǒng)響應(yīng)速度分布式鎖基于 SETNX 命令實(shí)現(xiàn)分布式鎖排行榜利用 ZSet 實(shí)現(xiàn)實(shí)時(shí)排行榜計(jì)數(shù)器利用 INCR/DECR 實(shí)現(xiàn)訪問計(jì)數(shù)、庫存扣減消息隊(duì)列利用 List 的 LPUSH/BRPOP 實(shí)現(xiàn)簡單的消息隊(duì)列會話管理存儲用戶登錄狀態(tài)和 Session 信息3. Redis 數(shù)據(jù)結(jié)構(gòu)詳解3.1 String字符串String 是 Redis 最基本的數(shù)據(jù)類型可以存儲字符串、整數(shù)或浮點(diǎn)數(shù)最大容量為 512MB。常用命令SET key value# 設(shè)置鍵值GET key# 獲取值INCR key# 值加1DECR key# 值減1SETEX key seconds value# 設(shè)置帶過期時(shí)間的鍵值MSET key1 value1 key2 value2# 批量設(shè)置Java 使用示例// 使用 Spring Data RedisAutowiredprivateStringRedisTemplatestringRedisTemplate;// 設(shè)置緩存stringRedisTemplate.opsForValue().set(user:1001,張三,30,TimeUnit.MINUTES);// 獲取緩存StringuserstringRedisTemplate.opsForValue().get(user:1001);// 計(jì)數(shù)器操作LongcountstringRedisTemplate.opsForValue().increment(page:view:count);應(yīng)用場景緩存對象、計(jì)數(shù)器、分布式鎖、Session 管理。3.2 Hash哈希Hash 是一個(gè)鍵值對集合適合存儲對象類型的數(shù)據(jù)可以單獨(dú)修改某個(gè)字段而不影響其他字段。常用命令HSET key field value# 設(shè)置字段值HGET key field# 獲取字段值HGETALL key# 獲取所有字段和值HDEL key field# 刪除字段HINCRBY key field increment# 字段值增加Java 使用示例AutowiredprivateRedisTemplateString,ObjectredisTemplate;// 存儲用戶對象HashOperationsString,String,ObjecthashOpsredisTemplate.opsForHash();MapString,ObjectuserMapnewHashMap();userMap.put(name,李四);userMap.put(age,25);userMap.put(email,lisiexample.com);hashOps.putAll(user:1002,userMap);// 獲取單個(gè)字段ObjectnamehashOps.get(user:1002,name);// 獲取整個(gè)對象MapString,ObjectuserhashOps.entries(user:1002);應(yīng)用場景存儲對象信息、購物車、用戶資料。3.3 List列表List 是一個(gè)雙向鏈表支持從兩端插入和彈出元素適合實(shí)現(xiàn)隊(duì)列和棧。常用命令LPUSH key value# 從左側(cè)插入RPUSH key value# 從右側(cè)插入LPOP key# 從左側(cè)彈出RPOP key# 從右側(cè)彈出LRANGE key start stop# 獲取范圍內(nèi)的元素LLEN key# 獲取列表長度Java 使用示例// 實(shí)現(xiàn)消息隊(duì)列ListOperationsString,StringlistOpsredisTemplate.opsForList();// 生產(chǎn)者發(fā)送消息listOps.rightPush(message:queue,消息1);listOps.rightPush(message:queue,消息2);// 消費(fèi)者接收消息阻塞式StringmessagelistOps.leftPop(message:queue,10,TimeUnit.SECONDS);應(yīng)用場景消息隊(duì)列、最新消息列表、時(shí)間線。3.4 Set集合Set 是無序的字符串集合元素唯一支持集合運(yùn)算交集、并集、差集。常用命令SADD key member# 添加元素SREM key member# 刪除元素SMEMBERS key# 獲取所有元素SISMEMBER key member# 判斷元素是否存在SINTER key1 key2# 求交集SUNION key1 key2# 求并集Java 使用示例SetOperationsString,StringsetOpsredisTemplate.opsForSet();// 添加標(biāo)簽setOps.add(user:1001:tags,Java,Redis,Spring);// 判斷是否包含某元素BooleanhasTagsetOps.isMember(user:1001:tags,Java);// 求共同關(guān)注SetStringcommonsetOps.intersect(user:1001:follows,user:1002:follows);應(yīng)用場景標(biāo)簽系統(tǒng)、好友關(guān)系、去重操作。3.5 ZSet有序集合ZSet 在 Set 的基礎(chǔ)上增加了分?jǐn)?shù)score屬性元素按分?jǐn)?shù)排序適合實(shí)現(xiàn)排行榜。常用命令ZADD key score member# 添加元素及分?jǐn)?shù)ZRANGE key start stop# 按分?jǐn)?shù)升序獲取ZREVRANGE key start stop# 按分?jǐn)?shù)降序獲取ZSCORE key member# 獲取元素分?jǐn)?shù)ZINCRBY key increment member# 增加元素分?jǐn)?shù)ZRANK key member# 獲取元素排名Java 使用示例ZSetOperationsString,StringzSetOpsredisTemplate.opsForZSet();// 添加排行榜數(shù)據(jù)zSetOps.add(ranking:game,玩家A,100);zSetOps.add(ranking:game,玩家B,85);zSetOps.add(ranking:game,玩家C,95);// 獲取前三名SetStringtop3zSetOps.reverseRange(ranking:game,0,2);// 增加分?jǐn)?shù)zSetOps.incrementScore(ranking:game,玩家B,20);應(yīng)用場景排行榜、延時(shí)隊(duì)列、優(yōu)先級隊(duì)列。4. Redis 持久化機(jī)制4.1 RDBRedis DataBase持久化RDB 是 Redis 默認(rèn)的持久化方式通過創(chuàng)建快照Snapshot將內(nèi)存中的數(shù)據(jù)保存到磁盤。工作原理在指定的時(shí)間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤默認(rèn)文件名為dump.rdb通過 fork 子進(jìn)程完成快照生成不影響主進(jìn)程服務(wù)觸發(fā)條件# 配置文件 redis.conf 中的默認(rèn)配置save9001# 900秒內(nèi)至少1次修改save30010# 300秒內(nèi)至少10次修改save6010000# 60秒內(nèi)至少10000次修改優(yōu)點(diǎn)文件緊湊適合備份和災(zāi)難恢復(fù)恢復(fù)速度快對性能影響小缺點(diǎn)可能丟失最后一次快照后的數(shù)據(jù)fork 子進(jìn)程時(shí)可能短暫阻塞4.2 AOFAppend Only File持久化AOF 通過記錄每次寫操作命令來持久化數(shù)據(jù)以追加的方式寫入日志文件。工作原理將每次寫命令追加到appendonly.aof文件支持三種同步策略always、everysec、no支持 AOF 重寫機(jī)制壓縮文件大小配置方式# 開啟 AOFappendonlyyes# 同步策略appendfsync always# 每次寫入都同步最安全但性能最差appendfsync everysec# 每秒同步一次推薦appendfsync no# 由操作系統(tǒng)決定同步時(shí)機(jī)優(yōu)點(diǎn)數(shù)據(jù)安全性高最多丟失 1 秒數(shù)據(jù)日志文件可讀便于排查問題缺點(diǎn)文件體積較大恢復(fù)速度較慢相同數(shù)據(jù)集下 AOF 文件比 RDB 大4.3 混合持久化Redis 4.0 引入了混合持久化機(jī)制結(jié)合 RDB 和 AOF 的優(yōu)點(diǎn)使用 RDB 作為基礎(chǔ)快照在快照之間使用 AOF 記錄增量操作兼顧恢復(fù)速度和數(shù)據(jù)安全性# 開啟混合持久化aof-use-rdb-preambleyes5. Redis 集群模式5.1 主從復(fù)制模式主從復(fù)制是最基礎(chǔ)的集群模式通過將主節(jié)點(diǎn)的數(shù)據(jù)同步到從節(jié)點(diǎn)實(shí)現(xiàn)讀寫分離和數(shù)據(jù)備份。架構(gòu)特點(diǎn)一個(gè)主節(jié)點(diǎn)Master負(fù)責(zé)寫操作多個(gè)從節(jié)點(diǎn)Slave負(fù)責(zé)讀操作從節(jié)點(diǎn)自動(dòng)同步主節(jié)點(diǎn)數(shù)據(jù)配置方式# 在從節(jié)點(diǎn)配置文件中指定主節(jié)點(diǎn)replicaof192.168.1.1006379# 或使用命令動(dòng)態(tài)設(shè)置SLAVEOF192.168.1.1006379復(fù)制原理從節(jié)點(diǎn)發(fā)送 SYNC 命令主節(jié)點(diǎn)執(zhí)行 BGSAVE 生成 RDB 快照主節(jié)點(diǎn)將快照發(fā)送給從節(jié)點(diǎn)從節(jié)點(diǎn)加載快照主節(jié)點(diǎn)將增量命令發(fā)送給從節(jié)點(diǎn)優(yōu)點(diǎn)實(shí)現(xiàn)讀寫分離提升讀性能數(shù)據(jù)冗余備份配置簡單缺點(diǎn)主節(jié)點(diǎn)故障時(shí)無法自動(dòng)切換從節(jié)點(diǎn)過多時(shí)同步壓力大5.2 哨兵模式Sentinel哨兵模式在主從復(fù)制基礎(chǔ)上增加了自動(dòng)故障轉(zhuǎn)移能力實(shí)現(xiàn)高可用。架構(gòu)特點(diǎn)哨兵進(jìn)程監(jiān)控主從節(jié)點(diǎn)的運(yùn)行狀態(tài)主節(jié)點(diǎn)故障時(shí)自動(dòng)選舉新的主節(jié)點(diǎn)客戶端通過哨兵獲取當(dāng)前主節(jié)點(diǎn)地址哨兵配置# sentinel.confsentinel monitor mymaster192.168.1.10063792sentinel down-after-milliseconds mymaster5000sentinel failover-timeout mymaster15000故障轉(zhuǎn)移流程主節(jié)點(diǎn)故障哨兵檢測到主節(jié)點(diǎn)下線投票選舉新主節(jié)點(diǎn)從節(jié)點(diǎn)升級為主節(jié)點(diǎn)其他從節(jié)點(diǎn)重新同步客戶端連接新主節(jié)點(diǎn)優(yōu)點(diǎn)自動(dòng)故障轉(zhuǎn)移高可用無需人工干預(yù)支持監(jiān)控和通知缺點(diǎn)只有一個(gè)主節(jié)點(diǎn)寫性能受限配置相對復(fù)雜5.3 Cluster 集群模式Cluster 模式是 Redis 3.0 引入的分布式解決方案支持?jǐn)?shù)據(jù)分片和水平擴(kuò)展。架構(gòu)特點(diǎn)數(shù)據(jù)自動(dòng)分片到 16384 個(gè)哈希槽每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分哈希槽支持多主多從無中心化架構(gòu)支持在線擴(kuò)容和縮容哈希槽分配# 集群節(jié)點(diǎn)分配哈希槽redis-cli-p7001cluster addslots0-5460 redis-cli-p7002cluster addslots5461-10922 redis-cli-p7003cluster addslots10923-16383集群配置示例# 創(chuàng)建集群至少3個(gè)主節(jié)點(diǎn)redis-cli--clustercreate192.168.1.101:7001192.168.1.102:7002\192.168.1.103:7003 --cluster-replicas1數(shù)據(jù)分片原理客戶端請求計(jì)算哈希槽CRC16(key) % 16384定位節(jié)點(diǎn)執(zhí)行操作優(yōu)點(diǎn)支持水平擴(kuò)展突破單機(jī)內(nèi)存限制高可用支持自動(dòng)故障轉(zhuǎn)移無中心化避免單點(diǎn)瓶頸缺點(diǎn)不支持多鍵操作跨槽位客戶端需要支持集群協(xié)議數(shù)據(jù)遷移期間可能影響性能6. 緩存穿透、緩存雪崩與緩存擊穿6.1 緩存穿透問題描述查詢一個(gè)不存在的數(shù)據(jù)緩存和數(shù)據(jù)庫中都沒有導(dǎo)致每次請求都直接打到數(shù)據(jù)庫。產(chǎn)生原因惡意攻擊構(gòu)造大量不存在的 key業(yè)務(wù)邏輯缺陷查詢了不存在的 ID解決方案方案一緩存空值publicObjectgetData(Stringkey){// 先從緩存查詢ObjectvalueredisTemplate.opsForValue().get(key);if(value!null){returnvalue;}// 緩存未命中查詢數(shù)據(jù)庫ObjectdbValuequeryFromDatabase(key);if(dbValuenull){// 緩存空值設(shè)置較短的過期時(shí)間redisTemplate.opsForValue().set(key,,60,TimeUnit.SECONDS);returnnull;}// 緩存真實(shí)數(shù)據(jù)redisTemplate.opsForValue().set(key,dbValue,30,TimeUnit.MINUTES);returndbValue;}方案二布隆過濾器// 使用 Guava 布隆過濾器BloomFilterStringbloomFilterBloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000000,// 預(yù)計(jì)元素?cái)?shù)量0.01// 誤判率);// 初始化時(shí)加載所有存在的 keybloomFilter.put(user:1001);bloomFilter.put(user:1002);publicObjectgetData(Stringkey){// 布隆過濾器判斷if(!bloomFilter.mightContain(key)){returnnull;// 一定不存在直接返回}// 可能存在繼續(xù)查詢緩存和數(shù)據(jù)庫// ...}6.2 緩存雪崩問題描述大量緩存同時(shí)過期或 Redis 服務(wù)宕機(jī)導(dǎo)致所有請求直接打到數(shù)據(jù)庫造成數(shù)據(jù)庫壓力驟增甚至崩潰。產(chǎn)生原因緩存設(shè)置了相同的過期時(shí)間Redis 服務(wù)宕機(jī)緩存服務(wù)器重啟解決方案方案一過期時(shí)間加隨機(jī)值// 設(shè)置過期時(shí)間時(shí)加入隨機(jī)值避免同時(shí)過期intbaseExpire30*60;// 基礎(chǔ)過期時(shí)間30分鐘intrandomExpirenewRandom().nextInt(300);// 隨機(jī)0-300秒redisTemplate.opsForValue().set(key,value,baseExpirerandomExpire,TimeUnit.SECONDS);方案二多級緩存// 一級緩存本地緩存CaffeineCacheString,ObjectlocalCacheCaffeine.newBuilder().expireAfterWrite(5,TimeUnit.MINUTES).maximumSize(10000).build();// 二級緩存RedispublicObjectgetData(Stringkey){// 先查本地緩存ObjectlocalValuelocalCache.getIfPresent(key);if(localValue!null){returnlocalValue;}// 再查 RedisObjectredisValueredisTemplate.opsForValue().get(key);if(redisValue!null){localCache.put(key,redisValue);returnredisValue;}// 最后查數(shù)據(jù)庫ObjectdbValuequeryFromDatabase(key);if(dbValue!null){redisTemplate.opsForValue().set(key,dbValue,30,TimeUnit.MINUTES);localCache.put(key,dbValue);}returndbValue;}方案三Redis 高可用使用哨兵模式或集群模式配置主從復(fù)制實(shí)現(xiàn)故障自動(dòng)轉(zhuǎn)移使用持久化機(jī)制重啟后快速恢復(fù)6.3 緩存擊穿問題描述某個(gè)熱點(diǎn) key 在緩存過期的瞬間大量并發(fā)請求同時(shí)訪問該 key導(dǎo)致所有請求都打到數(shù)據(jù)庫。產(chǎn)生原因熱點(diǎn)數(shù)據(jù)過期高并發(fā)訪問同一數(shù)據(jù)解決方案方案一互斥鎖publicObjectgetData(Stringkey){// 先從緩存查詢ObjectvalueredisTemplate.opsForValue().get(key);if(value!null){returnvalue;}