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: 30ms
spring:
cache:
type: redis
redis:
host: 127.0.0.1
port: 6379
password:
# redis預設情況下有16個分片,這裡配置具體使用的分片,預設為0
database: 0
lettuce:
pool:
# 連線池最大連線數(使用負數表示沒有限制),預設8
max-active: 100
建立實體類 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 的使用方式
預設情況下的模板只能支援 RedisTemplate<String,String>,也就是隻能存入字串,所以自定義模板很有必要。
新增配置類 RedisCacheConfig.java:
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheConfig {
@Bean
public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> 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<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:
public interface UserService {
User save(User user);
void delete(int id);
User get(Integer id);
}
介面實現類 UserServiceImpl.java:
@Service
public class UserServiceImpl implements UserService{
public static Logger logger = LogManager.getLogger(UserServiceImpl.class);
private static Map<Integer, User> userMap = new HashMap<>();
static {
userMap.put(1, "肖戰", 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("進入save方法,當前儲存物件:{}", user.toString());
return user;
}
@CacheEvict(value="user", key = "#id")
@Override
public void delete(int id) {
userMap.remove(id);
logger.info("進入delete方法,刪除成功");
}
@Cacheable(value = "user", key = "#id")
@Override
public 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")
public class UserController {
public static Logger logger = LogManager.getLogger(UserController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate<String, Serializable> redisCacheTemplate;
@Autowired
private UserService userService;
@RequestMapping("/test")
public void test() {
redisCacheTemplate.opsForValue().set("userkey", user.toString());
}
@RequestMapping("/add")
public void add() {
User user = userService.save(new User(4, "李現", 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());
}
}
用快取要注意,啟動類要加上一個註解開啟快取:
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
@EnableCaching
public class Application {
public static void main(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。
快取擊穿的話,設定熱點資料永不過期,或者加上互斥鎖就搞定了。作為暖男,程式碼給你準備好了,拿走不謝。
public static 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 中,配置方式如下:
appendfsync yes
appendfsync 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 高可用部分的主從複製和哨兵的基本原理。
只要功夫深,鐵杵磨成針,平時準備好,面試不用慌。雖然面試不一定是這樣問的,但萬變不離其“宗”。
【面試題專欄】
2020面試還搞不懂MyBatis?看看這27道面試題!(含答案和思維導圖)
Spring Cloud+Spring Boot高頻面試題解析
18道kafka高頻面試題哪些你還不會?(含答案和思維導圖)
2019年12道RabbitMQ高頻面試題你都會了嗎?(含答案解析)
2019年Dubbo你掌握的如何?快看看這30道高頻面試題!
2019年228道Java中高階面試題(8),哪些你還不會?