今天,我蚍蜉撼樹的面試了某大廠的 Java 開發崗位,迎面走來一位風塵僕僕的中年男子,手裏拿着屏幕還亮着的 Mac。他衝着我禮貌的笑了笑,而後說了句「很差意思,讓你久等了」,而後示意我坐下,說:「咱們開始吧,看了你的簡歷,以爲你對 Redis 應該掌握的不錯,咱們今天就來討論下 Redis……」。我想:「來就來,兵來將擋水來土掩」。java
面試官:你先來講下 Redis 是什麼吧!web
我:(這不就是總結下 Redis 的定義和特色嘛)Redis 是 C 語言開發的一個開源的(聽從 BSD 協議)高性能鍵值對(key-value)的內存數據庫,能夠用做數據庫、緩存、消息中間件等。面試
它是一種 NoSQL(not-only sql,泛指非關係型數據庫)的數據庫。redis
我頓了一下,接着說,Redis 做爲一個內存數據庫:算法
性能優秀,數據在內存中,讀寫速度很是快,支持併發 10W QPS。spring
單進程單線程,是線程安全的,採用 IO 多路複用機制。sql
豐富的數據類型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。數據庫
支持數據持久化。express
能夠將內存中數據保存在磁盤中,重啓時加載。apache
主從複製,哨兵,高可用。
能夠用做分佈式鎖。
能夠做爲消息中間件使用,支持發佈訂閱。
面試官:總結的不錯,看來是早有準備啊。剛來聽你提到 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(也就是註解的方式)。
直接經過 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<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 具有很好的靈活性,不只可以使用 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, new User(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。
這裏的核心是三個註解:
測試類: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", new User(1, "張三", 25));
User user = (User) redisCacheTemplate.opsForValue().get("userkey");
logger.info("當前獲取對象:{}", 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 緩存
③調用刪除方法,刪除 id=4 的用戶信息,同時清除緩存:
④再次調用查詢接口,查詢 id=4 的用戶信息:
沒有了緩存,因此進入了 get 方法,從 userMap 中獲取。
①@Cacheable根據方法的請求參數對其結果進行緩存:
②@CachePut
根據方法的請求參數對其結果進行緩存,和 @Cacheable 不一樣的是,它每次都會觸發真實方法的調用。參數描述見上。
③@CacheEvict 根據條件對緩存進行清空:
面試官:看了一下你的 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 必定很快咯?
我:固然了,官方提供的數據能夠達到 100000+ 的 QPS(每秒內的查詢次數),這個數據不比 Memcached 差!
面試官:Redis 這麼快,它的「多線程模型」你瞭解嗎?(露出邪魅一笑)
我:您是想問 Redis 這麼快,爲何仍是單線程的吧。Redis 確實是單進程單線程的模型,由於 Redis 徹底是基於內存的操做,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有多是機器內存的大小或者網絡帶寬。
既然單線程容易實現,並且 CPU 不會成爲瓶頸,那就瓜熟蒂落的採用單線程的方案了(畢竟採用多線程會有不少麻煩)。
面試官:嗯,是的。那你能說說 Redis 是單線程的,爲何還能這麼快嗎?
我:能夠這麼說吧,總結一下有以下四點:
Redis 徹底基於內存,絕大部分請求是純粹的內存操做,很是迅速,數據存在內存中,相似於 HashMap,HashMap 的優點就是查找和操做的時間複雜度是 O(1)。
數據結構簡單,對數據操做也簡單。
採用單線程,避免了沒必要要的上下文切換和競爭條件,不存在多線程致使的 CPU 切換,不用去考慮各類鎖的問題,不存在加鎖釋放鎖操做,沒有死鎖問題致使的性能消耗。
使用多路複用 IO 模型,非阻塞 IO。
面試官:嗯嗯,說的很詳細。那你爲何選擇 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 可用性。
從節點僅提供讀操做,主節點提供寫操做。對於讀多寫少的情況,可給主節點配置多個從節點,從而提升響應效率。
我頓了一下,接着說:關於複製過程,是這樣的:
面試官:那你能詳細說下數據同步的過程嗎?
(我心想:這也問的太細了吧)我:能夠。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] 命令,主節點有三種響應:
面試官:很好,那你能具體說下全量複製和部分複製的過程嗎?
我:能夠!
上面是全量複製的流程。主要有如下幾步:
從節點發送 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 高可用部分的主從複製和哨兵的基本原理。
只要功夫深,鐵杵磨成針,平時準備好,面試不用慌。雖然面試不必定是這樣問的,但萬變不離其「宗」。
轉載自微信公衆號
歡迎你們關注個人公衆號【風平浪靜如碼】,海量Java相關文章,學習資料都會在裏面更新,整理的資料也會放在裏面。
以爲寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!