超硬核!1.6W 字 Redis 面試知識點(diǎn)總結(jié),建議收藏!
作者:業(yè)余草
來源:https://www.xttblog.com/?p=4842
今天,我不自量力的面試了某大廠的 Java 開發(fā)崗位,迎面走來一位風(fēng)塵仆仆的中年男子,手里拿著屏幕還亮著的 Mac。他沖著我禮貌的笑了笑,然后說了句“不好意思,讓你久等了”,然后示意我坐下,說:“我們開始吧,看了你的簡歷,覺得你對 Redis 應(yīng)該掌握的不錯,我們今天就來討論下 Redis……”。我想:“來就來,兵來將擋水來土掩”。
Redis 是什么
面試官:你先來說下 Redis 是什么吧!
我:(這不就是總結(jié)下 Redis 的定義和特點(diǎn)嘛)Redis 是 C 語言開發(fā)的一個(gè)開源的(遵從 BSD 協(xié)議)高性能鍵值對(key-value)的內(nèi)存數(shù)據(jù)庫,可以用作數(shù)據(jù)庫、緩存、消息中間件等。
它是一種 NoSQL(not-only sql,泛指非關(guān)系型數(shù)據(jù)庫)的數(shù)據(jù)庫。
我頓了一下,接著說,Redis 作為一個(gè)內(nèi)存數(shù)據(jù)庫:
性能優(yōu)秀,數(shù)據(jù)在內(nèi)存中,讀寫速度非常快,支持并發(fā) 10W QPS。
單進(jìn)程單線程,是線程安全的,采用 IO 多路復(fù)用機(jī)制。
豐富的數(shù)據(jù)類型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
支持?jǐn)?shù)據(jù)持久化。
可以將內(nèi)存中數(shù)據(jù)保存在磁盤中,重啟時(shí)加載。
主從復(fù)制,哨兵,高可用。
可以用作分布式鎖。
可以作為消息中間件使用,支持發(fā)布訂閱。
五種數(shù)據(jù)類型
說著,我拿著筆給面試官畫了一張圖:

數(shù)據(jù)類型應(yīng)用場景總結(jié):

Redis 緩存
直接通過 RedisTemplate 來使用,使用 Spring Cache 集成 Redis pom.xml 中加入以下依賴:
<dependencies>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-data-redisartifactId>
????dependency>
????<dependency>
????????<groupId>org.apache.commonsgroupId>
????????<artifactId>commons-pool2artifactId>
????dependency>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-webartifactId>
????dependency>
????<dependency>
????????<groupId>org.springframework.sessiongroupId>
????????<artifactId>spring-session-data-redisartifactId>
????dependency>
????<dependency>
????????<groupId>org.projectlombokgroupId>
????????<artifactId>lombokartifactId>
????????<optional>trueoptional>
????dependency>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-testartifactId>
????????<scope>testscope>
????dependency>
dependencies>
配置文件 application.yml 的配置:
server:
??port:?8082
??servlet:
????session:
??????timeout:?30ms
spring:
??cache:
????type:?redis
??redis:
????host:?127.0.0.1
????port:?6379
????password:
????#?redis默認(rèn)情況下有16個(gè)分片,這里配置具體使用的分片,默認(rèn)為0
????database:?0
????lettuce:
??????pool:
????????#?連接池最大連接數(shù)(使用負(fù)數(shù)表示沒有限制),默認(rèn)8
????????max-active:?100
創(chuàng)建實(shí)體類 User.java:
public?class?User?implements?Serializable{
????private?static?final?long?serialVersionUID?=?662692455422902539L;
????private?Integer?id;
????private?String?name;
????private?Integer?age;
????public?User()?{
????}
????public?User(Integer?id,?String?name,?Integer?age)?{
????????this.id?=?id;
????????this.name?=?name;
????????this.age?=?age;
????}
????public?Integer?getId()?{
????????return?id;
????}
????public?void?setId(Integer?id)?{
????????this.id?=?id;
????}
????public?String?getName()?{
????????return?name;
????}
????public?void?setName(String?name)?{
????????this.name?=?name;
????}
????public?Integer?getAge()?{
????????return?age;
????}
????public?void?setAge(Integer?age)?{
????????this.age?=?age;
????}
????@Override
????public?String?toString()?{
????????return?"User{"?+
????????????????"id="?+?id?+
????????????????",?name='"?+?name?+?'\''?+
????????????????",?age="?+?age?+
????????????????'}';
????}
}
RedisTemplate 的使用方式
添加配置類 RedisCacheConfig.java:
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public?class?RedisCacheConfig?{
????@Bean
????public?RedisTemplate?redisCacheTemplate(LettuceConnectionFactory?connectionFactory)?{
????????RedisTemplate?template?=?new?RedisTemplate<>();
????????template.setKeySerializer(new?StringRedisSerializer());
????????template.setValueSerializer(new?GenericJackson2JsonRedisSerializer());
????????template.setConnectionFactory(connectionFactory);
????????return?template;
????}
}
測試類:
@RestController
@RequestMapping("/user")
public?class?UserController?{
????public?static?Logger?logger?=?LogManager.getLogger(UserController.class);
????@Autowired
????private?StringRedisTemplate?stringRedisTemplate;
????@Autowired
????private?RedisTemplate?redisCacheTemplate;
????@RequestMapping("/test")
????public?void?test()?{
????????redisCacheTemplate.opsForValue().set("userkey",?new?User(1,?"張三",?25));
????????User?user?=?(User)?redisCacheTemplate.opsForValue().get("userkey");
????????logger.info("當(dāng)前獲取對象:{}",?user.toString());
????}
然后在瀏覽器訪問,觀察后臺日志 http://localhost:8082/user/test

使用 Spring Cache 集成 Redis
public?interface?UserService?{
????User?save(User?user);
????void?delete(int?id);
????User?get(Integer?id);
}
接口實(shí)現(xiàn)類 UserServiceImpl.java:
@Service
public?class?UserServiceImpl?implements?UserService{
????public?static?Logger?logger?=?LogManager.getLogger(UserServiceImpl.class);
????private?static?Map?userMap?=?new?HashMap<>();
????static?{
????????userMap.put(1,?new?User(1,?"肖戰(zhàn)",?25));
????????userMap.put(2,?new?User(2,?"王一博",?26));
????????userMap.put(3,?new?User(3,?"楊紫",?24));
????}
????@CachePut(value?="user",?key?=?"#user.id")
????@Override
????public?User?save(User?user)?{
????????userMap.put(user.getId(),?user);
????????logger.info("進(jìn)入save方法,當(dāng)前存儲對象:{}",?user.toString());
????????return?user;
????}
????@CacheEvict(value="user",?key?=?"#id")
????@Override
????public?void?delete(int?id)?{
????????userMap.remove(id);
????????logger.info("進(jìn)入delete方法,刪除成功");
????}
????@Cacheable(value?=?"user",?key?=?"#id")
????@Override
????public?User?get(Integer?id)?{
????????logger.info("進(jìn)入get方法,當(dāng)前獲取對象:{}",?userMap.get(id)==null?null:userMap.get(id).toString());
????????return?userMap.get(id);
????}
}
@Cachable
@CachePut
@CacheEvict
測試類:UserController
@RestController
@RequestMapping("/user")
public?class?UserController?{
????public?static?Logger?logger?=?LogManager.getLogger(UserController.class);
????@Autowired
????private?StringRedisTemplate?stringRedisTemplate;
????@Autowired
????private?RedisTemplate?redisCacheTemplate;
????@Autowired
????private?UserService?userService;
????@RequestMapping("/test")
????public?void?test()?{
????????redisCacheTemplate.opsForValue().set("userkey",?new?User(1,?"張三",?25));
????????User?user?=?(User)?redisCacheTemplate.opsForValue().get("userkey");
????????logger.info("當(dāng)前獲取對象:{}",?user.toString());
????}
????@RequestMapping("/add")
????public?void?add()?{
????????User?user?=?userService.save(new?User(4,?"李現(xiàn)",?30));
????????logger.info("添加的用戶信息:{}",user.toString());
????}
????@RequestMapping("/delete")
????public?void?delete()?{
????????userService.delete(4);
????}
????@RequestMapping("/get/{id}")
????public?void?get(@PathVariable("id")?String?idStr)?throws?Exception{
????????if?(StringUtils.isBlank(idStr))?{
????????????throw?new?Exception("id為空");
????????}
????????Integer?id?=?Integer.parseInt(idStr);
????????User?user?=?userService.get(id);
????????logger.info("獲取的用戶信息:{}",user.toString());
????}
}
用緩存要注意,啟動類要加上一個(gè)注解開啟緩存:
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
@EnableCaching
public?class?Application?{
????public?static?void?main(String[]?args)?{
????????SpringApplication.run(Application.class,?args);
????}
}
①先調(diào)用添加接口:http://localhost:8082/user/add

②再調(diào)用查詢接口,查詢 id=4 的用戶信息:

可以看出,這里已經(jīng)從緩存中獲取數(shù)據(jù)了,因?yàn)樯弦徊?add 方法已經(jīng)把 id=4 的用戶數(shù)據(jù)放入了 Redis 緩存 3、調(diào)用刪除方法,刪除 id=4 的用戶信息,同時(shí)清除緩存:

④再次調(diào)用查詢接口,查詢 id=4 的用戶信息:

緩存注解
Key:緩存的 Key,可以為空,如果指定要按照 SPEL 表達(dá)式編寫,如果不指定,則按照方法的所有參數(shù)進(jìn)行組合。
Value:緩存的名稱,必須指定至少一個(gè)(如 @Cacheable (value='user')或者 @Cacheable(value={'user1','user2'}))
Condition:緩存的條件,可以為空,使用 SPEL 編寫,返回 true 或者 false,只有為 true 才進(jìn)行緩存。
Key:同上。
Value:同上。
Condition:同上。
allEntries:是否清空所有緩存內(nèi)容,缺省為 false,如果指定為 true,則方法調(diào)用后將立即清空所有緩存。
beforeInvocation:是否在方法執(zhí)行前就清空,缺省為 false,如果指定為 true,則在方法還沒有執(zhí)行的時(shí)候就清空緩存。缺省情況下,如果方法執(zhí)行拋出異常,則不會清空緩存。
緩存問題
我:處理緩存雪崩簡單,在批量往 Redis 存數(shù)據(jù)的時(shí)候,把每個(gè) Key 的失效時(shí)間都加個(gè)隨機(jī)值就好了,這樣可以保證數(shù)據(jù)不會再同一時(shí)間大面積失效。
setRedis(key,?value,?time+Math.random()*10000);
緩存擊穿的話,設(shè)置熱點(diǎn)數(shù)據(jù)永不過期,或者加上互斥鎖就搞定了。作為暖男,代碼給你準(zhǔn)備好了,拿走不謝。
public?static?String?getData(String?key)?throws?InterruptedException?{
????????//從Redis查詢數(shù)據(jù)
????????String?result?=?getDataByKV(key);
????????//參數(shù)校驗(yàn)
????????if?(StringUtils.isBlank(result))?{
????????????try?{
????????????????//獲得鎖
????????????????if?(reenLock.tryLock())?{
????????????????????//去數(shù)據(jù)庫查詢
????????????????????result?=?getDataByDB(key);
????????????????????//校驗(yàn)
????????????????????if?(StringUtils.isNotBlank(result))?{
????????????????????????//插進(jìn)緩存
????????????????????????setDataToKV(key,?result);
????????????????????}
????????????????}?else?{
????????????????????//睡一會再拿
????????????????????Thread.sleep(100L);
????????????????????result?=?getData(key);
????????????????}
????????????}?finally?{
????????????????//釋放鎖
????????????????reenLock.unlock();
????????????}
????????}
????????return?result;
????}
Redis 為何這么快
Redis 完全基于內(nèi)存,絕大部分請求是純粹的內(nèi)存操作,非常迅速,數(shù)據(jù)存在內(nèi)存中,類似于 HashMap,HashMap 的優(yōu)勢就是查找和操作的時(shí)間復(fù)雜度是 O(1)。
數(shù)據(jù)結(jié)構(gòu)簡單,對數(shù)據(jù)操作也簡單。
采用單線程,避免了不必要的上下文切換和競爭條件,不存在多線程導(dǎo)致的 CPU 切換,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有死鎖問題導(dǎo)致的性能消耗。
使用多路復(fù)用 IO 模型,非阻塞 IO。
Redis 和 Memcached 的區(qū)別
存儲方式上:Memcache 會把數(shù)據(jù)全部存在內(nèi)存之中,斷電后會掛掉,數(shù)據(jù)不能超過內(nèi)存大小。Redis 有部分?jǐn)?shù)據(jù)存在硬盤上,這樣能保證數(shù)據(jù)的持久性。
數(shù)據(jù)支持類型上:Memcache 對數(shù)據(jù)類型的支持簡單,只支持簡單的 key-value,,而 Redis 支持五種數(shù)據(jù)類型。
使用底層模型不同:它們之間底層實(shí)現(xiàn)方式以及與客戶端之間通信的應(yīng)用協(xié)議不一樣。Redis 直接自己構(gòu)建了 VM 機(jī)制,因?yàn)橐话愕南到y(tǒng)調(diào)用系統(tǒng)函數(shù)的話,會浪費(fèi)一定的時(shí)間去移動和請求。
Value 的大小:Redis 可以達(dá)到 1GB,而 Memcache 只有 1MB。
淘汰策略
我:Redis 有六種淘汰策略,如下圖:

持久化
RDB:快照形式是直接把內(nèi)存中的數(shù)據(jù)保存到一個(gè) dump 的文件中,定時(shí)保存,保存策略。
AOF:把所有的對 Redis 的服務(wù)器進(jìn)行修改的命令都存到一個(gè)文件里,命令的集合。Redis 默認(rèn)是快照 RDB 的持久化方式。
我:(說就一起說下吧)使用 AOF 做持久化,每一個(gè)寫命令都通過 write 函數(shù)追加到 appendonly.aof 中,配置方式如下:
appendfsync?yes???
appendfsync?always?????#每次有數(shù)據(jù)修改發(fā)生時(shí)都會寫入AOF文件。
appendfsync?everysec???#每秒鐘同步一次,該策略為AOF的缺省策略。
主從復(fù)制
從節(jié)點(diǎn)執(zhí)行 slaveof[masterIP][masterPort],保存主節(jié)點(diǎn)信息。
從節(jié)點(diǎn)中的定時(shí)任務(wù)發(fā)現(xiàn)主節(jié)點(diǎn)信息,建立和主節(jié)點(diǎn)的 Socket 連接。
從節(jié)點(diǎn)發(fā)送 Ping 信號,主節(jié)點(diǎn)返回 Pong,兩邊能互相通信。
連接建立后,主節(jié)點(diǎn)將所有數(shù)據(jù)發(fā)送給從節(jié)點(diǎn)(數(shù)據(jù)同步)。
主節(jié)點(diǎn)把當(dāng)前的數(shù)據(jù)同步給從節(jié)點(diǎn)后,便完成了復(fù)制的建立過程。接下來,主節(jié)點(diǎn)就會持續(xù)的把寫命令發(fā)送給從節(jié)點(diǎn),保證主從數(shù)據(jù)一致性。
runId:每個(gè) Redis 節(jié)點(diǎn)啟動都會生成唯一的 uuid,每次 Redis 重啟后,runId 都會發(fā)生變化。
offset:主節(jié)點(diǎn)和從節(jié)點(diǎn)都各自維護(hù)自己的主從復(fù)制偏移量 offset,當(dāng)主節(jié)點(diǎn)有寫入命令時(shí),offset=offset+命令的字節(jié)長度。
從節(jié)點(diǎn)在收到主節(jié)點(diǎn)發(fā)送的命令后,也會增加自己的 offset,并把自己的 offset 發(fā)送給主節(jié)點(diǎn)。
這樣,主節(jié)點(diǎn)同時(shí)保存自己的 offset 和從節(jié)點(diǎn)的 offset,通過對比 offset 來判斷主從節(jié)點(diǎn)數(shù)據(jù)是否一致。
repl_backlog_size:保存在主節(jié)點(diǎn)上的一個(gè)固定長度的先進(jìn)先出隊(duì)列,默認(rèn)大小是 1MB。
主節(jié)點(diǎn)發(fā)送數(shù)據(jù)給從節(jié)點(diǎn)過程中,主節(jié)點(diǎn)還會進(jìn)行一些寫操作,這時(shí)候的數(shù)據(jù)存儲在復(fù)制緩沖區(qū)中。
從節(jié)點(diǎn)同步主節(jié)點(diǎn)數(shù)據(jù)完成后,主節(jié)點(diǎn)將緩沖區(qū)的數(shù)據(jù)繼續(xù)發(fā)送給從節(jié)點(diǎn),用于部分復(fù)制。

FULLRESYNC:第一次連接,進(jìn)行全量復(fù)制
CONTINUE:進(jìn)行部分復(fù)制
ERR:不支持 psync 命令,進(jìn)行全量復(fù)制
我:可以!

從節(jié)點(diǎn)發(fā)送 psync ? -1 命令(因?yàn)榈谝淮伟l(fā)送,不知道主節(jié)點(diǎn)的 runId,所以為?,因?yàn)槭堑谝淮螐?fù)制,所以 offset=-1)。
主節(jié)點(diǎn)發(fā)現(xiàn)從節(jié)點(diǎn)是第一次復(fù)制,返回 FULLRESYNC {runId} {offset},runId 是主節(jié)點(diǎn)的 runId,offset 是主節(jié)點(diǎn)目前的 offset。
從節(jié)點(diǎn)接收主節(jié)點(diǎn)信息后,保存到 info 中。
主節(jié)點(diǎn)在發(fā)送 FULLRESYNC 后,啟動 bgsave 命令,生成 RDB 文件(數(shù)據(jù)持久化)。
主節(jié)點(diǎn)發(fā)送 RDB 文件給從節(jié)點(diǎn)。到從節(jié)點(diǎn)加載數(shù)據(jù)完成這段期間主節(jié)點(diǎn)的寫命令放入緩沖區(qū)。
從節(jié)點(diǎn)清理自己的數(shù)據(jù)庫數(shù)據(jù)。
從節(jié)點(diǎn)加載 RDB 文件,將數(shù)據(jù)保存到自己的數(shù)據(jù)庫中。如果從節(jié)點(diǎn)開啟了 AOF,從節(jié)點(diǎn)會異步重寫 AOF 文件。
哨兵
一旦主節(jié)點(diǎn)宕機(jī),從節(jié)點(diǎn)晉升為主節(jié)點(diǎn),同時(shí)需要修改應(yīng)用方的主節(jié)點(diǎn)地址,還需要命令所有從節(jié)點(diǎn)去復(fù)制新的主節(jié)點(diǎn),整個(gè)過程需要人工干預(yù)。
主節(jié)點(diǎn)的寫能力受到單機(jī)的限制。
主節(jié)點(diǎn)的存儲能力受到單機(jī)的限制。
原生復(fù)制的弊端在早期的版本中也會比較突出,比如:Redis 復(fù)制中斷后,從節(jié)點(diǎn)會發(fā)起 psync。
此時(shí)如果同步不成功,則會進(jìn)行全量同步,主庫執(zhí)行全量備份的同時(shí),可能會造成毫秒或秒級的卡頓。
面試官:那么問題又來了。那你說下哨兵有哪些功能?

監(jiān)控:不斷檢查主服務(wù)器和從服務(wù)器是否正常運(yùn)行。
通知:當(dāng)被監(jiān)控的某個(gè) Redis 服務(wù)器出現(xiàn)問題,Sentinel 通過 API 腳本向管理員或者其他應(yīng)用程序發(fā)出通知。
自動故障轉(zhuǎn)移:當(dāng)主節(jié)點(diǎn)不能正常工作時(shí),Sentinel 會開始一次自動的故障轉(zhuǎn)移操作,它會將與失效主節(jié)點(diǎn)是主從關(guān)系的其中一個(gè)從節(jié)點(diǎn)升級為新的主節(jié)點(diǎn),并且將其他的從節(jié)點(diǎn)指向新的主節(jié)點(diǎn),這樣人工干預(yù)就可以免了。
配置提供者:在 Redis Sentinel 模式下,客戶端應(yīng)用在初始化時(shí)連接的是 Sentinel 節(jié)點(diǎn)集合,從中獲取主節(jié)點(diǎn)的信息。
我:話不多說,直接上圖:

①每個(gè) Sentinel 節(jié)點(diǎn)都需要定期執(zhí)行以下任務(wù):每個(gè) Sentinel 以每秒一次的頻率,向它所知的主服務(wù)器、從服務(wù)器以及其他的 Sentinel 實(shí)例發(fā)送一個(gè) PING 命令。(如上圖)

②如果一個(gè)實(shí)例距離最后一次有效回復(fù) PING 命令的時(shí)間超過 down-after-milliseconds 所指定的值,那么這個(gè)實(shí)例會被 Sentinel 標(biāo)記為主觀下線。(如上圖)

③如果一個(gè)主服務(wù)器被標(biāo)記為主觀下線,那么正在監(jiān)視這個(gè)服務(wù)器的所有 Sentinel 節(jié)點(diǎn),要以每秒一次的頻率確認(rèn)主服務(wù)器的確進(jìn)入了主觀下線狀態(tài)。

④如果一個(gè)主服務(wù)器被標(biāo)記為主觀下線,并且有足夠數(shù)量的 Sentinel(至少要達(dá)到配置文件指定的數(shù)量)在指定的時(shí)間范圍內(nèi)同意這一判斷,那么這個(gè)主服務(wù)器被標(biāo)記為客觀下線。

當(dāng)一個(gè)主服務(wù)器被標(biāo)記為客觀下線時(shí),Sentinel 向下線主服務(wù)器的所有從服務(wù)器發(fā)送 INFO 命令的頻率,會從 10 秒一次改為每秒一次。

⑥Sentinel 和其他 Sentinel 協(xié)商客觀下線的主節(jié)點(diǎn)的狀態(tài),如果處于 SDOWN 狀態(tài),則投票自動選出新的主節(jié)點(diǎn),將剩余從節(jié)點(diǎn)指向新的主節(jié)點(diǎn)進(jìn)行數(shù)據(jù)復(fù)制。

總結(jié)
推薦閱讀
