搞懂這些Redis知識點,吊打面試官!

2020-12-14 51CTO

「今天,我不自量力的面試了某大廠的 Java 開發崗位,迎面走來一位風塵僕僕的中年男子,手裡拿著屏幕還亮著的 Mac。

圖片來自 Pexels

他衝著我禮貌的笑了笑,然後說了句「不好意思,讓你久等了」,然後示意我坐下,說:「我們開始吧,看了你的簡歷,覺得你對 Redis 應該掌握的不錯,我們今天就來討論下 Redis……」。我想:「來就來,兵來將擋水來土掩」。

Redis 是什麼

面試官:你先來說下 Redis 是什麼吧!

我:(這不就是總結下 Redis 的定義和特點嘛)Redis 是 C 語言開發的一個開源的(遵從 BSD 協議)高性能鍵值對(key-value)的內存資料庫,可以用作資料庫、緩存、消息中間件等。

它是一種 NoSQL(not-only sql,泛指非關係型資料庫)的資料庫。

我頓了一下,接著說,Redis 作為一個內存資料庫:

性能優秀,數據在內存中,讀寫速度非常快,支持並發 10W QPS。單進程單線程,是線程安全的,採用 IO 多路復用機制。豐富的數據類型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。支持數據持久化。可以將內存中數據保存在磁碟中,重啟時加載。主從複製,哨兵,高可用。可以用作分布式鎖。可以作為消息中間件使用,支持發布訂閱。

五種數據類型

面試官:總結的不錯,看來是早有準備啊。剛來聽你提到 Redis 支持五種數據類型,那你能簡單說下這五種數據類型嗎?

我:當然可以,但是在說之前,我覺得有必要先來了解下 Redis 內部內存管理是如何描述這 5 種數據類型的。

說著,我拿著筆給面試官畫了一張圖:

我:首先 Redis 內部使用一個 redisObject 對象來表示所有的 key 和 value。

redisObject 最主要的信息如上圖所示:type 表示一個 value 對象具體是何種數據類型,encoding 是不同數據類型在 Redis 內部的存儲方式。

比如:type=string 表示 value 存儲的是一個普通字符串,那麼 encoding 可以是 raw 或者 int。

我頓了一下,接著說,下面我簡單說下 5 種數據類型:

①String 是 Redis 最基本的類型,可以理解成與 Memcached一模一樣的類型,一個 Key 對應一個 Value。Value 不僅是 String,也可以是數字。

String 類型是二進位安全的,意思是 Redis 的 String 類型可以包含任何數據,比如 jpg 圖片或者序列化的對象。String 類型的值最大能存儲 512M。

②Hash是一個鍵值(key-value)的集合。Redis 的 Hash 是一個 String 的 Key 和 Value 的映射表,Hash 特別適合存儲對象。常用命令:hget,hset,hgetall 等。

③List 列表是簡單的字符串列表,按照插入順序排序。可以添加一個元素到列表的頭部(左邊)或者尾部(右邊) 常用命令:lpush、rpush、lpop、rpop、lrange(獲取列表片段)等。

應用場景:List 應用場景非常多,也是 Redis 最重要的數據結構之一,比如 Twitter 的關注列表,粉絲列表都可以用 List 結構來實現。

數據結構:List 就是鍊表,可以用來當消息隊列用。Redis 提供了 List 的 Push 和 Pop 操作,還提供了操作某一段的 API,可以直接查詢或者刪除某一段的元素。

實現方式:Redis List 的是實現是一個雙向鍊表,既可以支持反向查找和遍歷,更方便操作,不過帶來了額外的內存開銷。

④Set 是 String 類型的無序集合。集合是通過 hashtable 實現的。Set 中的元素是沒有順序的,而且是沒有重複的。常用命令:sdd、spop、smembers、sunion 等。

應用場景:Redis Set 對外提供的功能和 List 一樣是一個列表,特殊之處在於 Set 是自動去重的,而且 Set 提供了判斷某個成員是否在一個 Set 集合中。

⑤Zset 和 Set 一樣是 String 類型元素的集合,且不允許重複的元素。常用命令:zadd、zrange、zrem、zcard 等。

使用場景:Sorted Set 可以通過用戶額外提供一個優先級(score)的參數來為成員排序,並且是插入有序的,即自動排序。

當你需要一個有序的並且不重複的集合列表,那麼可以選擇 Sorted Set 結構。

和 Set 相比,Sorted Set關聯了一個 Double 類型權重的參數 Score,使得集合中的元素能夠按照 Score 進行有序排列,Redis 正是通過分數來為集合中的成員進行從小到大的排序。

實現方式:Redis Sorted Set 的內部使用 HashMap 和跳躍表(skipList)來保證數據的存儲和有序,HashMap 裡放的是成員到 Score 的映射。

而跳躍表裡存放的是所有的成員,排序依據是 HashMap 裡存的 Score,使用跳躍表的結構可以獲得比較高的查找效率,並且在實現上比較簡單。

數據類型應用場景總結:

面試官:想不到你平時也下了不少工夫,那 Redis 緩存你一定用過的吧?

我:用過的。

面試官:那你跟我說下你是怎麼用的?

我是結合 Spring Boot 使用的。一般有兩種方式,一種是直接通過 RedisTemplate 來使用,另一種是使用 Spring Cache 集成 Redis(也就是註解的方式)。

Redis 緩存

直接通過 RedisTemplate 來使用,使用 Spring Cache 集成 Redis pom.xml 中加入以下依賴:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

spring-boot-starter-data-redis:在 Spring Boot 2.x 以後底層不再使用 Jedis,而是換成了 Lettuce。

commons-pool2:用作 Redis 連接池,如不引入啟動會報錯。

spring-session-data-redis:Spring Session 引入,用作共享 Session。

配置文件 application.yml 的配置:

server: port: 8082 servlet: session: timeout: 30msspring:cache:type: redis redis: host: 127.0.0.1 port: 6379password:# redis默認情況下有16個分片,這裡配置具體使用的分片,默認為0database: 0 lettuce: pool:# 連接池最大連接數(使用負數表示沒有限制),默認8max-active: 100

創建實體類 User.java:

publicclassUserimplementsSerializable{privatestaticfinallong serialVersionUID = 662692455422902539L;private Integer id;private String name;private Integer age;publicUser(){ }publicUser(Integer id, String name, Integer age){this.id = id;this.name = name;this.age = age; }public Integer getId(){return id; }publicvoidsetId(Integer id){this.id = id; }public String getName(){return name; }publicvoidsetName(String name){this.name = name; }public Integer getAge(){return age; }publicvoidsetAge(Integer age){this.age = age; }@Overridepublic String toString(){return"User{" +"id=" + id +", name='" + name + '\'' +", age=" + age +'}'; }}

RedisTemplate 的使用方式

默認情況下的模板只能支持 RedisTemplate<String, String>,也就是只能存入字符串,所以自定義模板很有必要。

添加配置類 RedisCacheConfig.java:

@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)publicclassRedisCacheConfig { @Beanpublic RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory connectionFactory) { RedisTemplate<String, Serializable> template = new RedisTemplate<>();template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(new GenericJackson2JsonRedisSerializer());template.setConnectionFactory(connectionFactory);returntemplate; }}

測試類:

@RestController@RequestMapping("/user")publicclassUserController{public static Logger logger = LogManager.getLogger(UserController.class);@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate<String, Serializable> redisCacheTemplate;@RequestMapping("/test")public void test() { redisCacheTemplate.opsForValue().set("userkey", new User(1, "張三", 25)); User user = (User) redisCacheTemplate.opsForValue().get("userkey"); logger.info("當前獲取對象:{}", user.toString()); }

然後在瀏覽器訪問,觀察後臺日誌 http://localhost:8082/user/test

使用 Spring Cache 集成 Redis

Spring Cache 具備很好的靈活性,不僅能夠使用 SPEL(spring expression language)來定義緩存的 Key 和各種 Condition,還提供了開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存如 EhCache、Redis、Guava 的集成。

定義接口 UserService.java:

publicinterfaceUserService {User save(User user);voiddelete(int id);User get(Integer id);}

接口實現類 UserServiceImpl.java:

@ServicepublicclassUserServiceImplimplementsUserService{publicstatic Logger logger = LogManager.getLogger(UserServiceImpl.class);privatestatic Map<Integer, User> userMap = new HashMap<>();static { userMap.put(1, new User(1, "肖戰", 25)); userMap.put(2, new User(2, "王一博", 26)); userMap.put(3, new User(3, "楊紫", 24)); }@CachePut(value ="user", key = "#user.id")@Overridepublic User save(User user){ userMap.put(user.getId(), user); logger.info("進入save方法,當前存儲對象:{}", user.toString());return user; }@CacheEvict(value="user", key = "#id")@Overridepublicvoiddelete(int id){ userMap.remove(id); logger.info("進入delete方法,刪除成功"); }@Cacheable(value = "user", key = "#id")@Overridepublic User get(Integer id){ logger.info("進入get方法,當前獲取對象:{}", userMap.get(id)==null?null:userMap.get(id).toString());return userMap.get(id); }}

為了方便演示資料庫的操作,這裡直接定義了一個 Map<Integer,User> userMap。

這裡的核心是三個註解:

@Cachable@CachePut@CacheEvict

測試類:UserController

@RestController@RequestMapping("/user")publicclassUserController{publicstatic Logger logger = LogManager.getLogger(UserController.class);@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate<String, Serializable> redisCacheTemplate;@Autowiredprivate UserService userService;@RequestMapping("/test")publicvoidtest(){ redisCacheTemplate.opsForValue().set("userkey", new User(1, "張三", 25)); User user = (User) redisCacheTemplate.opsForValue().get("userkey"); logger.info("當前獲取對象:{}", user.toString()); }@RequestMapping("/add")publicvoidadd(){ User user = userService.save(new User(4, "李現", 30)); logger.info("添加的用戶信息:{}",user.toString()); }@RequestMapping("/delete")publicvoiddelete(){ userService.delete(4); }@RequestMapping("/get/{id}")publicvoidget(@PathVariable("id") String idStr) throws Exception{if (StringUtils.isBlank(idStr)) {thrownew Exception("id為空"); } Integer id = Integer.parseInt(idStr); User user = userService.get(id); logger.info("獲取的用戶信息:{}",user.toString()); }}

用緩存要注意,啟動類要加上一個註解開啟緩存:

@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)@EnableCachingpublicclassApplication{publicstaticvoidmain(String[] args){ SpringApplication.run(Application.class, args); }}

①先調用添加接口:http://localhost:8082/user/add

②再調用查詢接口,查詢 id=4 的用戶信息:

可以看出,這裡已經從緩存中獲取數據了,因為上一步 add 方法已經把 id=4 的用戶數據放入了 Redis 緩存 3、調用刪除方法,刪除 id=4 的用戶信息,同時清除緩存:

④再次調用查詢接口,查詢 id=4 的用戶信息:

沒有了緩存,所以進入了 get 方法,從 userMap 中獲取。

緩存註解

①@Cacheable

根據方法的請求參數對其結果進行緩存:

Key:緩存的 Key,可以為空,如果指定要按照 SPEL 表達式編寫,如果不指定,則按照方法的所有參數進行組合。Value:緩存的名稱,必須指定至少一個(如 @Cacheable (value='user')或者 @Cacheable(value={'user1','user2'}))Condition:緩存的條件,可以為空,使用 SPEL 編寫,返回 true 或者 false,只有為 true 才進行緩存。

②@CachePut

根據方法的請求參數對其結果進行緩存,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用。參數描述見上。

③@CacheEvict

根據條件對緩存進行清空:

Key:同上。Value:同上。Condition:同上。allEntries:是否清空所有緩存內容,預設為 false,如果指定為 true,則方法調用後將立即清空所有緩存。beforeInvocation:是否在方法執行前就清空,預設為 false,如果指定為 true,則在方法還沒有執行的時候就清空緩存。預設情況下,如果方法執行拋出異常,則不會清空緩存。

緩存問題

面試官:看了一下你的 Demo,簡單易懂。那你在實際項目中使用緩存有遇到什麼問題或者會遇到什麼問題你知道嗎?

我:緩存和資料庫數據一致性問題:分布式環境下非常容易出現緩存和資料庫間數據一致性問題,針對這一點,如果項目對緩存的要求是強一致性的,那麼就不要使用緩存。

我們只能採取合適的策略來降低緩存和資料庫間數據不一致的概率,而無法保證兩者間的強一致性。

合適的策略包括合適的緩存更新策略,更新資料庫後及時更新緩存、緩存失敗時增加重試機制。

面試官:Redis 雪崩了解嗎?

我:我了解的,目前電商首頁以及熱點數據都會去做緩存,一般緩存都是定時任務去刷新,或者查不到之後去更新緩存的,定時任務刷新就有一個問題。

舉個慄子:如果首頁所有 Key 的失效時間都是 12 小時,中午 12 點刷新的,我零點有個大促活動大量用戶湧入,假設每秒 6000 個請求,本來緩存可以抗住每秒 5000 個請求,但是緩存中所有 Key 都失效了。

此時 6000 個/秒的請求全部落在了資料庫上,資料庫必然扛不住,真實情況可能 DBA 都沒反應過來直接掛了。

此時,如果沒什麼特別的方案來處理,DBA 很著急,重啟資料庫,但是資料庫立馬又被新流量給打死了。這就是我理解的緩存雪崩。

我心想:同一時間大面積失效,瞬間 Redis 跟沒有一樣,那這個數量級別的請求直接打到資料庫幾乎是災難性的。

你想想如果掛的是一個用戶服務的庫,那其他依賴他的庫所有接口幾乎都會報錯。

如果沒做熔斷等策略基本上就是瞬間掛一片的節奏,你怎麼重啟用戶都會把你打掛,等你重啟好的時候,用戶早睡覺去了,臨睡之前,罵罵咧咧「什麼垃圾產品」。

面試官摸摸了自己的頭髮:嗯,還不錯,那這種情況你都是怎麼應對的?

我:處理緩存雪崩簡單,在批量往 Redis 存數據的時候,把每個 Key 的失效時間都加個隨機值就好了,這樣可以保證數據不會再同一時間大面積失效。

setRedis(key, value, time+Math.random()*10000);

如果 Redis 是集群部署,將熱點數據均勻分布在不同的 Redis 庫中也能避免全部失效。

或者設置熱點數據永不過期,有更新操作就更新緩存就好了(比如運維更新了首頁商品,那你刷下緩存就好了,不要設置過期時間),電商首頁的數據也可以用這個操作,保險。

面試官:那你了解緩存穿透和擊穿麼,可以說說他們跟雪崩的區別嗎?

我:嗯,了解,先說下緩存穿透吧,緩存穿透是指緩存和資料庫中都沒有的數據,而用戶(黑客)不斷發起請求。

舉個慄子:我們資料庫的 id 都是從 1 自增的,如果發起 id=-1 的數據或者 id 特別大不存在的數據,這樣的不斷攻擊導致資料庫壓力很大,嚴重會擊垮資料庫。

我又接著說:至於緩存擊穿嘛,這個跟緩存雪崩有點像,但是又有一點不一樣,緩存雪崩是因為大面積的緩存失效,打崩了 DB。

而緩存擊穿不同的是緩存擊穿是指一個 Key 非常熱點,在不停地扛著大量的請求,大並發集中對這一個點進行訪問,當這個 Key 在失效的瞬間,持續的大並發直接落到了資料庫上,就在這個 Key 的點上擊穿了緩存。

面試官露出欣慰的眼光:那他們分別怎麼解決?

我:緩存穿透我會在接口層增加校驗,比如用戶鑑權,參數做校驗,不合法的校驗直接 return,比如 id 做基礎校驗,id<=0 直接攔截。

面試官:那你還有別的方法嗎?

我:我記得 Redis 裡還有一個高級用法布隆過濾器(Bloom Filter)這個也能很好的預防緩存穿透的發生。

它的原理也很簡單,就是利用高效的數據結構和算法快速判斷出你這個 Key 是否在資料庫中存在,不存在你 return 就好了,存在你就去查 DB 刷新 KV 再 return。

緩存擊穿的話,設置熱點數據永不過期,或者加上互斥鎖就搞定了。作為暖男,代碼給你準備好了,拿走不謝。

publicstatic String getData(String key)throws InterruptedException {//從Redis查詢數據 String result = getDataByKV(key);//參數校驗if (StringUtils.isBlank(result)) {try {//獲得鎖if (reenLock.tryLock()) {//去資料庫查詢 result = getDataByDB(key);//校驗if (StringUtils.isNotBlank(result)) {//插進緩存 setDataToKV(key, result); } } else {//睡一會再拿 Thread.sleep(100L); result = getData(key); } } finally {//釋放鎖 reenLock.unlock(); } }return result; }

面試官:嗯嗯,還不錯。

Redis 為何這麼快

面試官:Redis 作為緩存大家都在用,那 Redis 一定很快咯?

我:當然了,官方提供的數據可以達到 100000+ 的 QPS(每秒內的查詢次數),這個數據不比 Memcached 差!

面試官:Redis 這麼快,它的「多線程模型」你了解嗎?(露出邪魅一笑)

我:您是想問 Redis 這麼快,為什麼還是單線程的吧。Redis 確實是單進程單線程的模型,因為 Redis 完全是基於內存的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機器內存的大小或者網絡帶寬。

既然單線程容易實現,而且 CPU 不會成為瓶頸,那就順理成章的採用單線程的方案了(畢竟採用多線程會有很多麻煩)。

面試官:嗯,是的。那你能說說 Redis 是單線程的,為什麼還能這麼快嗎?

我:可以這麼說吧,總結一下有如下四點:

Redis 完全基於內存,絕大部分請求是純粹的內存操作,非常迅速,數據存在內存中,類似於 HashMap,HashMap 的優勢就是查找和操作的時間複雜度是 O(1)。數據結構簡單,對數據操作也簡單。採用單線程,避免了不必要的上下文切換和競爭條件,不存在多線程導致的 CPU 切換,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有死鎖問題導致的性能消耗。使用多路復用 IO 模型,非阻塞 IO。

Redis 和 Memcached 的區別

面試官:嗯嗯,說的很詳細。那你為什麼選擇 Redis 的緩存方案而不用 Memcached 呢?

我:原因有如下四點:

存儲方式上:Memcache 會把數據全部存在內存之中,斷電後會掛掉,數據不能超過內存大小。Redis 有部分數據存在硬碟上,這樣能保證數據的持久性。數據支持類型上:Memcache 對數據類型的支持簡單,只支持簡單的 key-value,,而 Redis 支持五種數據類型。使用底層模型不同:它們之間底層實現方式以及與客戶端之間通信的應用協議不一樣。Redis 直接自己構建了 VM 機制,因為一般的系統調用系統函數的話,會浪費一定的時間去移動和請求。Value 的大小:Redis 可以達到 1GB,而 Memcache 只有 1MB。

淘汰策略

面試官:那你說說你知道的 Redis 的淘汰策略有哪些?

我:Redis 有六種淘汰策略,如下圖:

補充一下:Redis 4.0 加入了 LFU(least frequency use)淘汰策略,包括 volatile-lfu 和 allkeys-lfu,通過統計訪問頻率,將訪問頻率最少,即最不經常使用的 KV 淘汰。

持久化

面試官:你對 Redis 的持久化機制了解嗎?能講一下嗎?

我:Redis 為了保證效率,數據緩存在了內存中,但是會周期性的把更新的數據寫入磁碟或者把修改操作寫入追加的記錄文件中,以保證數據的持久化。

Redis 的持久化策略有兩種:

RDB:快照形式是直接把內存中的數據保存到一個 dump 的文件中,定時保存,保存策略。AOF:把所有的對 Redis 的伺服器進行修改的命令都存到一個文件裡,命令的集合。Redis 默認是快照 RDB 的持久化方式。

當 Redis 重啟的時候,它會優先使用 AOF 文件來還原數據集,因為 AOF 文件保存的數據集通常比 RDB 文件所保存的數據集更完整。你甚至可以關閉持久化功能,讓數據只在伺服器運行時存。

面試官:那你再說下 RDB 是怎麼工作的?

我:默認 Redis 是會以快照"RDB"的形式將數據持久化到磁碟的一個二進位文件 dump.rdb。

工作原理簡單說一下:當 Redis 需要做持久化時,Redis 會 fork 一個子進程,子進程將數據寫到磁碟上一個臨時 RDB 文件中。

當子進程完成寫臨時文件後,將原來的 RDB 替換掉,這樣的好處是可以 copy-on-write。

我:RDB 的優點是:這種文件非常適合用於備份:比如,你可以在最近的 24 小時內,每小時備份一次,並且在每個月的每一天也備份一個 RDB 文件。

這樣的話,即使遇上問題,也可以隨時將數據集還原到不同的版本。RDB 非常適合災難恢復。

RDB 的缺點是:如果你需要儘量避免在伺服器故障時丟失數據,那麼RDB不合適你。

面試官:那你要不再說下 AOF?

我:(說就一起說下吧)使用 AOF 做持久化,每一個寫命令都通過 write 函數追加到 appendonly.aof 中,配置方式如下:

appendfsyncyesappendfsync always #每次有數據修改發生時都會寫入AOF文件。appendfsync everysec #每秒鐘同步一次,該策略為AOF的預設策略。

AOF 可以做到全程持久化,只需要在配置中開啟 appendonly yes。這樣 Redis 每執行一個修改數據的命令,都會把它添加到 AOF 文件中,當 Redis 重啟時,將會讀取 AOF 文件進行重放,恢復到 Redis 關閉前的最後時刻。

我頓了一下,繼續說:使用 AOF 的優點是會讓 Redis 變得非常耐久。可以設置不同的 Fsync 策略,AOF的默認策略是每秒鐘 Fsync 一次,在這種配置下,就算發生故障停機,也最多丟失一秒鐘的數據。

缺點是對於相同的數據集來說,AOF 的文件體積通常要大於 RDB 文件的體積。根據所使用的 Fsync 策略,AOF 的速度可能會慢於 RDB。

面試官又問:你說了這麼多,那我該用哪一個呢?

我:如果你非常關心你的數據,但仍然可以承受數分鐘內的數據丟失,那麼可以額只使用 RDB 持久。

AOF 將 Redis 執行的每一條命令追加到磁碟中,處理巨大的寫入會降低Redis的性能,不知道你是否可以接受。

資料庫備份和災難恢復:定時生成 RDB 快照非常便於進行資料庫備份,並且 RDB 恢復數據集的速度也要比 AOF 恢復的速度快。

當然了,Redis 支持同時開啟 RDB 和 AOF,系統重啟後,Redis 會優先使用 AOF 來恢復數據,這樣丟失的數據會最少。

主從複製

面試官:Redis 單節點存在單點故障問題,為了解決單點問題,一般都需要對 Redis 配置從節點,然後使用哨兵來監聽主節點的存活狀態,如果主節點掛掉,從節點能繼續提供緩存功能,你能說說 Redis 主從複製的過程和原理嗎?

我有點懵,這個說來就話長了。但幸好提前準備了:主從配置結合哨兵模式能解決單點故障問題,提高 Redis 可用性。

從節點僅提供讀操作,主節點提供寫操作。對於讀多寫少的狀況,可給主節點配置多個從節點,從而提高響應效率。

我頓了一下,接著說:關於複製過程,是這樣的:

從節點執行 slaveof[masterIP][masterPort],保存主節點信息。從節點中的定時任務發現主節點信息,建立和主節點的 Socket 連接。從節點發送 Ping 信號,主節點返回 Pong,兩邊能互相通信。連接建立後,主節點將所有數據發送給從節點(數據同步)。主節點把當前的數據同步給從節點後,便完成了複製的建立過程。接下來,主節點就會持續的把寫命令發送給從節點,保證主從數據一致性。

面試官:那你能詳細說下數據同步的過程嗎?

(我心想:這也問的太細了吧)我:可以。Redis 2.8 之前使用 sync[runId][offset] 同步命令,Redis 2.8 之後使用 psync[runId][offset] 命令。

兩者不同在於,Sync 命令僅支持全量複製過程,Psync 支持全量和部分複製。

介紹同步之前,先介紹幾個概念:

runId:每個 Redis 節點啟動都會生成唯一的 uuid,每次 Redis 重啟後,runId 都會發生變化。offset:主節點和從節點都各自維護自己的主從複製偏移量 offset,當主節點有寫入命令時,offset=offset+命令的字節長度。從節點在收到主節點發送的命令後,也會增加自己的 offset,並把自己的 offset 發送給主節點。這樣,主節點同時保存自己的 offset 和從節點的 offset,通過對比 offset 來判斷主從節點數據是否一致。repl_backlog_size:保存在主節點上的一個固定長度的先進先出隊列,默認大小是 1MB。

主節點發送數據給從節點過程中,主節點還會進行一些寫操作,這時候的數據存儲在複製緩衝區中。

從節點同步主節點數據完成後,主節點將緩衝區的數據繼續發送給從節點,用於部分複製。

主節點響應寫命令時,不但會把命名發送給從節點,還會寫入複製積壓緩衝區,用於複製命令丟失的數據補救。

上面是 Psync 的執行流程,從節點發送 psync[runId][offset] 命令,主節點有三種響應:

FULLRESYNC:第一次連接,進行全量複製CONTINUE:進行部分複製ERR:不支持 psync 命令,進行全量複製

面試官:很好,那你能具體說下全量複製和部分複製的過程嗎?

我:可以!

上面是全量複製的流程。主要有以下幾步:

從節點發送 psync ? -1 命令(因為第一次發送,不知道主節點的 runId,所以為?,因為是第一次複製,所以 offset=-1)。主節點發現從節點是第一次複製,返回 FULLRESYNC {runId} {offset},runId 是主節點的 runId,offset 是主節點目前的 offset。從節點接收主節點信息後,保存到 info 中。主節點在發送 FULLRESYNC 後,啟動 bgsave 命令,生成 RDB 文件(數據持久化)。主節點發送 RDB 文件給從節點。到從節點加載數據完成這段期間主節點的寫命令放入緩衝區。從節點清理自己的資料庫數據。從節點加載 RDB 文件,將數據保存到自己的資料庫中。如果從節點開啟了 AOF,從節點會異步重寫 AOF 文件。

關於部分複製有以下幾點說明:

①部分複製主要是 Redis 針對全量複製的過高開銷做出的一種優化措施,使用 psync[runId][offset] 命令實現。

當從節點正在複製主節點時,如果出現網絡閃斷或者命令丟失等異常情況時,從節點會向主節點要求補發丟失的命令數據,主節點的複製積壓緩衝區將這部分數據直接發送給從節點。

這樣就可以保持主從節點複製的一致性。補發的這部分數據一般遠遠小於全量數據。

②主從連接中斷期間主節點依然響應命令,但因複製連接中斷命令無法發送給從節點,不過主節點內的複製積壓緩衝區依然可以保存最近一段時間的寫命令數據。

③當主從連接恢復後,由於從節點之前保存了自身已複製的偏移量和主節點的運行 ID。因此會把它們當做 psync 參數發送給主節點,要求進行部分複製。

④主節點接收到 psync 命令後首先核對參數 runId 是否與自身一致,如果一致,說明之前複製的是當前主節點。

之後根據參數 offset 在複製積壓緩衝區中查找,如果 offset 之後的數據存在,則對從節點發送+COUTINUE 命令,表示可以進行部分複製。因為緩衝區大小固定,若發生緩衝溢出,則進行全量複製。

⑤主節點根據偏移量把複製積壓緩衝區裡的數據發送給從節點,保證主從複製進入正常狀態。

哨兵

面試官:那主從複製會存在哪些問題呢?

我:主從複製會存在以下問題:

一旦主節點宕機,從節點晉升為主節點,同時需要修改應用方的主節點地址,還需要命令所有從節點去複製新的主節點,整個過程需要人工幹預。主節點的寫能力受到單機的限制。主節點的存儲能力受到單機的限制。原生複製的弊端在早期的版本中也會比較突出,比如:Redis 複製中斷後,從節點會發起 psync。此時如果同步不成功,則會進行全量同步,主庫執行全量備份的同時,可能會造成毫秒或秒級的卡頓。

面試官:那比較主流的解決方案是什麼呢?

我:當然是哨兵啊。

面試官:那麼問題又來了。那你說下哨兵有哪些功能?

我:如圖,是 Redis Sentinel(哨兵)的架構圖。Redis Sentinel(哨兵)主要功能包括主節點存活檢測、主從運行情況檢測、自動故障轉移、主從切換。

Redis Sentinel 最小配置是一主一從。Redis 的 Sentinel 系統可以用來管理多個 Redis 伺服器。

該系統可以執行以下四個任務:

監控:不斷檢查主伺服器和從伺服器是否正常運行。通知:當被監控的某個 Redis 伺服器出現問題,Sentinel 通過 API 腳本向管理員或者其他應用程式發出通知。自動故障轉移:當主節點不能正常工作時,Sentinel 會開始一次自動的故障轉移操作,它會將與失效主節點是主從關係的其中一個從節點升級為新的主節點,並且將其他的從節點指向新的主節點,這樣人工幹預就可以免了。配置提供者:在 Redis Sentinel 模式下,客戶端應用在初始化時連接的是 Sentinel 節點集合,從中獲取主節點的信息。

面試官:那你能說下哨兵的工作原理嗎?

我:話不多說,直接上圖:

①每個 Sentinel 節點都需要定期執行以下任務:每個 Sentinel 以每秒一次的頻率,向它所知的主伺服器、從伺服器以及其他的 Sentinel 實例發送一個 PING 命令。(如上圖)

②如果一個實例距離最後一次有效回復 PING 命令的時間超過 down-after-milliseconds 所指定的值,那麼這個實例會被 Sentinel 標記為主觀下線。(如上圖)

③如果一個主伺服器被標記為主觀下線,那么正在監視這個伺服器的所有 Sentinel 節點,要以每秒一次的頻率確認主伺服器的確進入了主觀下線狀態。

④如果一個主伺服器被標記為主觀下線,並且有足夠數量的 Sentinel(至少要達到配置文件指定的數量)在指定的時間範圍內同意這一判斷,那麼這個主伺服器被標記為客觀下線。

⑤一般情況下,每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有主伺服器和從伺服器發送 INFO 命令。

當一個主伺服器被標記為客觀下線時,Sentinel 向下線主伺服器的所有從伺服器發送 INFO 命令的頻率,會從 10 秒一次改為每秒一次。

⑥Sentinel 和其他 Sentinel 協商客觀下線的主節點的狀態,如果處於 SDOWN 狀態,則投票自動選出新的主節點,將剩餘從節點指向新的主節點進行數據複製。

⑦當沒有足夠數量的 Sentinel 同意主伺服器下線時,主伺服器的客觀下線狀態就會被移除。

當主伺服器重新向 Sentinel 的 PING 命令返回有效回復時,主伺服器的主觀下線狀態就會被移除。

面試官:不錯,面試前沒少下工夫啊,今天 Redis 這關你過了,明天找個時間我們再聊聊其他的。(露出欣慰的微笑)

我:沒問題。

總結

本文在一次面試的過程中講述了 Redis 是什麼,Redis 的特點和功能,Redis 緩存的使用,Redis 為什麼能這麼快,Redis 緩存的淘汰策略,持久化的兩種方式,Redis 高可用部分的主從複製和哨兵的基本原理。

相關焦點

  • Redis面試:八問字典內部構造與rehash,給我整懵了
    在字典中,一個 鍵 (key)可以和一個值(value)進行關聯,這些關聯的 鍵 和值就稱之為鍵值對。抽象數據結構,啥意思?就是可以需要實際的數據結構是實現這個功能。抽象,意味著它這是實現功能的標準,凡是能夠完成這些功能的都可以是其實現。
  • 等差數列知識點及類型題總結,數學最重要的知識點之一!需搞懂
    小學數學到底學什麼,這是很多人都沒有搞懂的一個問題,當人們度過了長久的學習階段,在來回顧自己的整個學習生涯的時候,會發現自己對小學階段的生活是一片茫然,完全不知道小學到底學了些什麼知識。是什麼讓我們在六年的時間裡,現在還包括幼兒園三年的時間,九年的時間來學習,有些人還學的生活不能自理?
  • 工作五年,一年內我靠這系列java面試寶典從13K到大廠30K
    並發篇並發編程可以說是Java編程的靈魂,可以讓系統性能更高,用戶響應速度更快,讓我們的程序模塊化和異步化,且內部機制與實現原理也是一線網際網路面試官比較青睞的。知識點:Java內存模型AQS、CAS鎖並發工具類……面試題:(部分)
  • 迷茫期後面試阿里奮發圖強8個月,如願拿到offer,定級阿里P7
    》這些書也只是看了一部分,並沒有全部看完,有時會經常帶著問題就進入了夢鄉,所以這段時期我的進展並沒有太大明顯。分享下這段時期面試總結的一些經驗1、面試一定要帶簡歷,這是態度問題,不然容易第一印象就不好。2、簡歷上最核心的2點,1是自己掌握的技術棧說明,讓面試官清楚知道你掌握技術棧;2是項目中使用了哪些技術棧,體現自己對這些技術棧的實際項目的運用情況。
  • 985碩,秋招面試30家企業,怒斬阿里、字節、美團offer
    ,跟那些項目大佬真的沒的比,沒有實習就意味著只能做網上的開源項目,面試官一看就知道項目是怎麼來的,很尷尬,不過既然這樣,那就盡力把項目搞熟,爭取每個細節都不放過,這樣面試官問起來也會對答如流,證明自己親手做過,能做的就是不能讓面試官在項目這裡減分吧,加分肯定是夠嗆!
  • 面試官:木字多一筆,是啥字?小夥機智作答,面試官紛紛鼓掌
    但實際上,現在的學歷早就不是面試官衡量這個人能力的唯一標準了。面試官會專門設計一些話題來考驗你的綜合素質,以及情商和思維變通能力。小王是一名剛剛畢業的大學生,小王的學歷雖然不是很高,只是一般的本科,但是腦子很機靈,是一個聰明人。在參加一家公司的面試的時候,經過幾輪的專業知識測試和面試,進入最後一關的只有4個人。
  • 憑藉清華掃地僧的路線指引,從Java基礎到算法,吊打阿里面試官!
    本文素材來自於一位鐵粉經驗分享上周收到字節跳動的面試邀請,真的又驚又喜。驚得是害怕面試時會被秒殺;喜得是有大廠邀請,是自己能力的一種體現。於是抱著試試看的輕鬆心態,居然一不小心就反殺了面試官(嘻嘻,有點嘚瑟啊)。
  • 年初離職,學習半年源碼,終於拿到了螞蟻Offer,分享面試過程
    面試官隨便針對一個知識點深入考察一下,就回答不出來,就這樣,還怎麼能通過面試?不過,最近收到了小夥伴的捷報,已拿到阿里的offer,公司足夠大,base還可以,雖然是個P6,但還是隱隱感覺到他很滿意。其實,我還是有點疑惑,他之前的基礎很一般,咋就突然拿到了阿里的offer。
  • Redis-Window Server快速搭建Redis Cluster集群
    /releases選擇redis 3.0+版本配置redis在redis安裝目錄中找到redis.windows.conf,修改配置文件中配置,修改選項如下:cluster-enabled yescluster-node-timeout
  • 面試官:你還有什麼要問我的嗎?這樣回答讓你脫穎而出!
    當我們在平時參加面試的時候,在面試過程當中,面試官總會對我們說一句,你還有什麼問題要問呢?其實就是這麼一個簡單的問題,面試官就可以問出很多的東西,同時這個問題也暗藏很多的玄機。面試的過程,也是展示自己能力的過程。要回答這個問題,首先要搞懂面試官為什麼會反問面試者。只有真正懂得面試官的意思,才能得提出好的問題。其實,在面試的過程中,當面試官在問你還有什麼問題要問的時候,其實這個時候他就已經把詢問問題的主動權交到你的手裡了。
  • 女面試官:「禾」字加一筆,是什麼字?男子機智回答,被錄取
    女面試官:「禾」字加一筆,是什麼字?男子機智回答,被錄取在這個信息膨脹的社會,時代瞬息萬變,要想在職場上玩得風生水起,僅僅靠專業是知識是完全不夠的。面試圖片來源網絡/與本文無關這是一家公司正在應聘高級營銷專員,面試官是位看起來很乾練的女性,在對所有求職者進行了營銷方面的專業考核後,篩選了比較優秀的3位求職者,讓3人做了自我介紹後,發現表達能力均沒有問題。
  • Redis詳解:sets數據類型及操作
    通過這些操作可以很容易的實現sns中的好友推薦和blog的tag功能。  (integer) 1  redis 127.0.0.1:6379> sadd myset "world"  (integer) 0  redis 127.0.0.1:6379> smembers myset  1) "world"  2) "hello"  redis 127.0.0.1
  • redis集群架構的演進之路
    一、redis的主從架構一開始我們的業務量不大時,一個redis節點就能滿足我們的業務需求,當我們的業務量不斷上漲,單臺redis節點已經不能滿足我們的業務需求時,這個時候redis的主從結構就出現了。
  • 面試官:「午」字加一筆是什麼字?本科生回答牛字,被淘汰
    很多公司的人事部,他們為了能夠找到真正有能力的人,就在面試的時候,會考驗求職者的能力。他們會出一些題,這些題看似簡單,但是答案並非那麼簡單。小莉她是一位本科生,畢業後還想找到一份好的工作,畢竟,一份工作就決定了自己的未來。
  • 分布式鎖解決方案-Redis
    行數據一直存在,無法再次成功插入數據,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;3、沒有鎖失效機制,因為有可能出現成功插入數據後,伺服器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的數據
  • 希賽網:教資面試說錯知識點、緊張忘詞怎麼辦?這些方法Get一下
    俗話說:「不怕一萬就怕萬一」,不論在面試前準備的如何充分,在正式面試時總會出現各式各樣的問題,比如說錯知識點、緊張忘詞、忘記板書等。那麼在面試過程中出現「意外」該怎麼解決呢?
  • 聊聊python 數據處理全家桶(Redis篇)
    yum install redis然後,通過 vim 命令修改 Redis 配置文件,打開遠程連接,設置連接密碼配置文件目錄:/etc/redis.confbind 更改為 0.0.0.0,容許外網訪問
  • Redis緩存與NodeJS的初學教程
    有時候,查詢需要一些操作,比如從資料庫中檢索數據、執行計算、從其他服務中檢索額外數據等等,這些操作會降低我們的性能。這正是緩存的優勢所在,因為我們可以一次性處理數據,將其存儲在緩存中,然後稍後直接從緩存中檢索數據,而無需執行所有那些昂貴的操作。然後,我們會定期更新緩存,以便用戶可以看到更新的信息。
  • 快速製作Redis Docker鏡像
    鏡像可以通過編寫DockerFile來鏡像redis編譯並打包成新的鏡像。From alpine:3.12.1  ADD https://mirrors.huaweicloud.com/redis/redis-6.0.9.tar.gz /