《今天面試了嗎》-Redis
今天,我不自量力的面試了某大廠的java開發崗位,迎面走來一位風塵僕僕的中年男子,手裡拿著螢幕還亮著的mac,他衝著我禮貌的笑了笑,然後說了句“不好意思,讓你久等了”,然後示意我坐下,說:“我們開始吧。看了你的簡歷,覺得你對redis應該掌握的不錯,我們今天就來討論下redis......”。我想:“來就來,兵來將擋水來土掩”。
Redis是什麼
-
面試官:你先來說下redis是什麼吧
-
我:(這不就是總結下redis的定義和特點嘛)Redis是C語言開發的一個開源的(遵從BSD協議)高效能鍵值對(key-value)的記憶體資料庫,可以用作資料庫、快取、訊息中介軟體等。它是一種NoSQL(not-only sql,泛指非關係型資料庫)的資料庫。
-
我頓了一下,接著說:Redis作為一個記憶體資料庫。1、效能優秀,資料在記憶體中,讀寫速度非常快,支援併發10W QPS;2、單程式單執行緒,是執行緒安全的,採用IO多路複用機制;3、豐富的資料型別,支援字串(strings)、雜湊(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等;4、支援資料持久化。可以將記憶體中資料儲存在磁碟中,重啟時載入;5、主從複製,哨兵,高可用;6、可以用作分散式鎖;7、可以作為訊息中介軟體使用,支援釋出訂閱
五種資料型別
-
面試官:總結的不錯,看來是早有準備啊。剛來聽你提到redis支援五種資料型別,那你能簡單說下這五種資料型別嗎?
-
我:當然可以,但是在說之前,我覺得有必要先來瞭解下Redis內部記憶體管理是如何描述這5種資料型別的。說著,我拿著筆給面試官畫了一張圖:
-
我:首先redis內部使用一個redisObject物件來表示所有的key和value,redisObject最主要的資訊如上圖所示:type表示一個value物件具體是何種資料型別,encoding是不同資料型別在redis內部的儲存方式。比如:type=string表示value儲存的是一個普通字串,那麼encoding可以是raw或者int。
-
我頓了一下,接著說:下面我簡單說下5種資料型別:
1、string是redis最基本的型別,可以理解成與memcached一模一樣的型別,一個key對應一個value。value不僅是string,也可以是數字。string型別是二進位制安全的,意思是redis的string型別可以包含任何資料,比如jpg圖片或者序列化的物件。string型別的值最大能儲存512M。
2、Hash是一個鍵值(key-value)的集合。redis的hash是一個string的key和value的對映表,Hash特別適合儲存物件。常用命令:hget,hset,hgetall等。
3、list列表是簡單的字串列表,按照插入順序排序。可以新增一個元素到列表的頭部(左邊)或者尾部(右邊) 常用命令:lpush、rpush、lpop、rpop、lrange(獲取列表片段)等。
應用場景:list應用場景非常多,也是Redis最重要的資料結構之一,比如twitter的關注列表,粉絲列表都可以用list結構來實現。
資料結構:list就是連結串列,可以用來當訊息佇列用。redis提供了List的push和pop操作,還提供了操作某一段的api,可以直接查詢或者刪除某一段的元素。
實現方式:redis list的是實現是一個雙向連結串列,既可以支援反向查詢和遍歷,更方便操作,不過帶來了額外的記憶體開銷。
4、set是string型別的無序集合。集合是通過hashtable實現的。set中的元素是沒有順序的,而且是沒有重複的。
常用命令:sdd、spop、smembers、sunion等。
應用場景:redis set對外提供的功能和list一樣是一個列表,特殊之處在於set是自動去重的,而且set提供了判斷某個成員是否在一個set集合中。
5、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,使用跳躍表的結構可以獲得比較高的查詢效率,並且在實現上比較簡單。 -
我:我之前總結了一張圖,關於資料型別的應用場景,如果您感興趣,可以去我的掘金看。。
資料型別應用場景總結
型別 | 簡介 | 特性 | 場景 |
---|---|---|---|
string(字串) | 二進位制安全 | 可以包含任何資料,比如jpg圖片或者序列化物件 | --- |
Hash(字典) | 鍵值對集合,即程式語言中的map型別 | 適合儲存物件,並且可以像資料庫中的update一個屬性一樣只修改某一項屬性值 | 儲存、讀取、修改使用者屬性 |
List(列表) | 連結串列(雙向連結串列) | 增刪快,提供了操作某一元素的api | 最新訊息排行;訊息佇列 |
set(集合) | hash表實現,元素不重複 | 新增、刪除、查詢的複雜度都是O(1),提供了求交集、並集、差集的操作 | 共同好友;利用唯一性,統計訪問網站的所有Ip |
sorted set(有序集合) | 將set中的元素增加一個權重引數score,元素按score有序排列 | 資料插入集合時,已經進行了天然排序 | 排行榜;帶權重的訊息佇列 |
-
面試官:想不到你平時也下了不少工夫,那redis快取你一定用過的吧
-
我:用過的。。
-
面試官:那你跟我說下你是怎麼用的?
-
我是結合spring boot使用的。一般有兩種方式,一種是直接通過RedisTemplate來使用,另一種是使用spring cache整合Redis(也就是註解的方式)。具體的程式碼我就不說了,在我的掘金中有一個demo(見下)。
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);
}
}
複製程式碼
1、先呼叫新增介面:http://localhost:8082/user/add
2、再呼叫查詢介面,查詢id=4的使用者資訊: 可以看出,這裡已經從快取中獲取資料了,因為上一步add方法已經把id=4的使用者資料放入了redis快取 3、呼叫刪除方法,刪除id=4的使用者資訊,同時清除快取 4、再次呼叫查詢介面,查詢id=4的使用者資訊: 沒有了快取,所以進入了get方法,從userMap中獲取。快取註解
1、@Cacheable 根據方法的請求引數對其結果進行快取
- key:快取的key,可以為空,如果指定要按照SPEL表示式編寫,如果不指定,則按照方法的所有引數進行組合。
- value:快取的名稱,必須指定至少一個(如 @Cacheable (value='user')或者@Cacheable(value={'user1','user2'}))
- condition:快取的條件,可以為空,使用SPEL編寫,返回true或者false,只有為true才進行快取。
2、@CachePut
根據方法的請求引數對其結果進行快取,和@Cacheable不同的是,它每次都會觸發真實方法的呼叫。引數描述見上。
3、@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呢
-
我:1、儲存方式上:memcache會把資料全部存在記憶體之中,斷電後會掛掉,資料不能超過記憶體大小。redis有部分資料存在硬碟上,這樣能保證資料的永續性。2、資料支援型別上:memcache對資料型別的支援簡單,只支援簡單的key-value,,而redis支援五種資料型別。3、使用底層模型不同:它們之間底層實現方式以及與客戶端之間通訊的應用協議不一樣。redis直接自己構建了VM機制,因為一般的系統呼叫系統函式的話,會浪費一定的時間去移動和請求。4、value的大小:redis可以達到1GB,而memcache只有1MB。
淘汰策略
-
面試官:那你說說你知道的redis的淘汰策略有哪些?
-
我:Redis有六種淘汰策略
策略 | 描述 |
---|---|
volatile-lru | 從已設定過期時間的KV集中優先對最近最少使用(less recently used)的資料淘汰 |
volitile-ttl | 從已設定過期時間的KV集中優先對剩餘時間短(time to live)的資料淘汰 |
volitile-random | 從已設定過期時間的KV集中隨機選擇資料淘汰 |
allkeys-lru | 從所有KV集中優先對最近最少使用(less recently used)的資料淘汰 |
allKeys-random | 從所有KV集中隨機選擇資料淘汰 |
noeviction | 不淘汰策略,若超過最大記憶體,返回錯誤資訊 |
補充一下:Redis4.0加入了LFU(least frequency use)淘汰策略,包括volatile-lfu和allkeys-lfu,通過統計訪問頻率,將訪問頻率最少,即最不經常使用的KV淘汰。
持久化
-
面試官:你對redis的持久化機制瞭解嗎?能講一下嗎?
-
我:redis為了保證效率,資料快取在了記憶體中,但是會週期性的把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案中,以保證資料的持久化。Redis的持久化策略有兩種: 1、RDB:快照形式是直接把記憶體中的資料儲存到一個dump的檔案中,定時儲存,儲存策略。 2、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可用性。從節點僅提供讀操作,主節點提供寫操作。對於讀多寫少的狀況,可給主節點配置多個從節點,從而提高響應效率。
-
我頓了一下,接著說:關於複製過程,是這樣的: 1、從節點執行slaveof[masterIP][masterPort],儲存主節點資訊
2、從節點中的定時任務發現主節點資訊,建立和主節點的socket連線
3、從節點傳送Ping訊號,主節點返回Pong,兩邊能互相通訊
4、連線建立後,主節點將所有資料傳送給從節點(資料同步)
5、主節點把當前的資料同步給從節點後,便完成了複製的建立過程。接下來,主節點就會持續的把寫命令傳送給從節點,保證主從資料一致性。 -
面試官:那你能詳細說下資料同步的過程嗎?
-
(我心想:這也問的太細了吧)我:可以。redis2.8之前使用sync[runId][offset]同步命令,redis2.8之後使用psync[runId][offset]命令。兩者不同在於,sync命令僅支援全量複製過程,psync支援全量和部分複製。介紹同步之前,先介紹幾個概念:
runId:每個redis節點啟動都會生成唯一的uuid,每次redis重啟後,runId都會發生變化。
offset:主節點和從節點都各自維護自己的主從複製偏移量offset,當主節點有寫入命令時,offset=offset+命令的位元組長度。從節點在收到主節點傳送的命令後,也會增加自己的offset,並把自己的offset傳送給主節點。這樣,主節點同時儲存自己的offset和從節點的offset,通過對比offset來判斷主從節點資料是否一致。
repl_backlog_size:儲存在主節點上的一個固定長度的先進先出佇列,預設大小是1MB。
(1)主節點傳送資料給從節點過程中,主節點還會進行一些寫操作,這時候的資料儲存在複製緩衝區中。從節點同步主節點資料完成後,主節點將緩衝區的資料繼續傳送給從節點,用於部分複製。
(2)主節點響應寫命令時,不但會把命名傳送給從節點,還會寫入複製積壓緩衝區,用於複製命令丟失的資料補救。
從節點傳送psync[runId][offset]命令,主節點有三種響應:
(1)FULLRESYNC:第一次連線,進行全量複製
(2)CONTINUE:進行部分複製
(3)ERR:不支援psync命令,進行全量複製
-
面試官:很好,那你能具體說下全量複製和部分複製的過程嗎?
-
我:可以
上面是全量複製的流程。主要有以下幾步:
1、從節點傳送psync ? -1命令(因為第一次傳送,不知道主節點的runId,所以為?,因為是第一次複製,所以offset=-1)。
2、主節點發現從節點是第一次複製,返回FULLRESYNC {runId} {offset},runId是主節點的runId,offset是主節點目前的offset。
3、從節點接收主節點資訊後,儲存到info中。
4、主節點在傳送FULLRESYNC後,啟動bgsave命令,生成RDB檔案(資料持久化)。
5、主節點傳送RDB檔案給從節點。到從節點載入資料完成這段期間主節點的寫命令放入緩衝區。
6、從節點清理自己的資料庫資料。
7、從節點載入RDB檔案,將資料儲存到自己的資料庫中。
8、如果從節點開啟了AOF,從節點會非同步重寫AOF檔案。
關於部分複製有以下幾點說明:
1、部分複製主要是Redis針對全量複製的過高開銷做出的一種優化措施,使用psync[runId][offset]命令實現。當從節點正在複製主節點時,如果出現網路閃斷或者命令丟失等異常情況時,從節點會向主節點要求補發丟失的命令資料,主節點的複製積壓緩衝區將這部分資料直接傳送給從節點,這樣就可以保持主從節點複製的一致性。補發的這部分資料一般遠遠小於全量資料。
2、主從連線中斷期間主節點依然響應命令,但因複製連線中斷命令無法傳送給從節點,不過主節點內的複製積壓緩衝區依然可以儲存最近一段時間的寫命令資料。
3、當主從連線恢復後,由於從節點之前儲存了自身已複製的偏移量和主節點的執行ID。因此會把它們當做psync引數傳送給主節點,要求進行部分複製。
4、主節點接收到psync命令後首先核對引數runId是否與自身一致,如果一致,說明之前複製的是當前主節點;之後根據引數offset在複製積壓緩衝區中查詢,如果offset之後的資料存在,則對從節點傳送+COUTINUE命令,表示可以進行部分複製。因為緩衝區大小固定,若發生緩衝溢位,則進行全量複製。
5、主節點根據偏移量把複製積壓緩衝區裡的資料傳送給從節點,保證主從複製進入正常狀態。
哨兵
-
面試官:那主從複製會存在哪些問題呢?
-
我:主從複製會存在以下問題:
1、一旦主節點宕機,從節點晉升為主節點,同時需要修改應用方的主節點地址,還需要命令所有從節點去複製新的主節點,整個過程需要人工幹預。
2、主節點的寫能力受到單機的限制。
3、主節點的儲存能力受到單機的限制。 4、原生複製的弊端在早期的版本中也會比較突出,比如:redis複製中斷後,從節點會發起psync。此時如果同步不成功,則會進行全量同步,主庫執行全量備份的同時,可能會造成毫秒或秒級的卡頓。 -
面試官:那比較主流的解決方案是什麼呢?
-
我:當然是哨兵啊。
-
面試官:那麼問題又來了。那你說下哨兵有哪些功能?
-
我:如圖,是Redis Sentinel(哨兵)的架構圖。Redis Sentinel(哨兵)主要功能包括主節點存活檢測、主從執行情況檢測、自動故障轉移、主從切換。Redis Sentinel最小配置是一主一從。Redis的Sentinel系統可以用來管理多個Redis伺服器,該系統可以執行以下四個任務:
1、監控:不斷檢查主伺服器和從伺服器是否正常執行。
2、通知:當被監控的某個redis伺服器出現問題,Sentinel通過API指令碼向管理員或者其他應用程式發出通知。
3、自動故障轉移:當主節點不能正常工作時,Sentinel會開始一次自動的故障轉移操作,它會將與失效主節點是主從關係的其中一個從節點升級為新的主節點,並且將其他的從節點指向新的主節點,這樣人工幹預就可以免了。
4、配置提供者:在Redis Sentinel模式下,客戶端應用在初始化時連線的是Sentinel節點集合,從中獲取主節點的資訊。 -
面試官:那你能說下哨兵的工作原理嗎?
-
我:話不多說,直接上圖:
2、如果一個例項距離最後一次有效回覆PING命令的時間超過down-after-milliseconds所指定的值,那麼這個例項會被Sentinel標記為主觀下線。(如上圖)
3、如果一個主伺服器被標記為主觀下線,那麼正在監視這個伺服器的所有Sentinel節點,要以每秒一次的頻率確認主伺服器的確進入了主觀下線狀態。 4、如果一個主伺服器被標記為主觀下線,並且有足夠數量的Sentinel(至少要達到配置檔案指定的數量)在指定的時間範圍內同意這一判斷,那麼這個主伺服器被標記為客觀下線。 5、一般情況下,每個Sentinel會以每10秒一次的頻率向它已知的所有主伺服器和從伺服器傳送INFO命令,當一個主伺服器被標記為客觀下線時,Sentinel向下線主伺服器的所有從伺服器傳送INFO命令的頻率,會從10秒一次改為每秒一次。 6、Sentinel和其他Sentinel協商客觀下線的主節點的狀態,如果處於SDOWN狀態,則投票自動選出新的主節點,將剩餘從節點指向新的主節點進行資料複製。 7、當沒有足夠數量的Sentinel同意主伺服器下線時,主伺服器的客觀下線狀態就會被移除。當主伺服器重新向Sentinel的PING命令返回有效回覆時,主伺服器的主觀下線狀態就會被移除。-
面試官:不錯,面試前沒少下工夫啊,今天Redis這關你過了,明天找個時間我們再聊聊其他的。(露出欣慰的微笑)
-
我:沒問題。
總結
本文在一次面試的過程中講述了Redis是什麼,Redis的特點和功能,Redis快取的使用,Redis為什麼能這麼快,Redis快取的淘汰策略,持久化的兩種方式,Redis高可用部分的主從複製和哨兵的基本原理。只要功夫深,鐵杵磨成針,平時準備好,面試不用慌。雖然面試不一定是這樣問的,但萬變不離其“宗”。(筆者覺得這種問答形式的部落格很不錯,可讀性強而且讀後記的比較深刻)