來自:掘金庐冯,作者:堅持就是勝利
“
今天孽亲,我不自量力的面試了某大廠的 Java 開發(fā)崗位,迎面走來一位風塵仆仆的中年男子展父,手里拿著屏幕還亮著的 Mac返劲。
他沖著我禮貌的笑了笑,然后說了句“不好意思栖茉,讓你久等了”篮绿,然后示意我坐下,說:“我們開始吧吕漂,看了你的簡歷亲配,覺得你對 Redis 應該掌握的不錯,我們今天就來討論下 Redis……”。我想:“來就來吼虎,兵來將擋水來土掩”犬钢。
Redis 是什么
面試官:你先來說下 Redis 是什么吧!
我:(這不就是總結下 Redis 的定義和特點嘛)Redis 是 C 語言開發(fā)的一個開源的(遵從 BSD 協(xié)議)高性能鍵值對(key-value)的內存數據庫思灰,可以用作數據庫玷犹、緩存、消息中間件等官辈。
它是一種 NoSQL(not-only sql,泛指非關系型數據庫)的數據庫遍坟。
我頓了一下拳亿,接著說,Redis 作為一個內存數據庫:
性能優(yōu)秀愿伴,數據在內存中肺魁,讀寫速度非常快隔节,支持并發(fā) 10W QPS鹅经。
單進程單線程,是線程安全的怎诫,采用 IO 多路復用機制瘾晃。
豐富的數據類型,支持字符串(strings)幻妓、散列(hashes)蹦误、列表(lists)、集合(sets)肉津、有序集合(sorted sets)等强胰。
-
支持數據持久化。
可以將內存中數據保存在磁盤中妹沙,重啟時加載偶洋。
主從復制,哨兵距糖,高可用玄窝。
可以用作分布式鎖。
可以作為消息中間件使用悍引,支持發(fā)布訂閱哆料。
五種數據類型
面試官:總結的不錯,看來是早有準備啊吗铐。剛來聽你提到 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 可以通過用戶額外提供一個優(yōu)先級(score)的參數來為成員排序欺矫,并且是插入有序的纱新,即自動排序。當你需要一個有序的并且不重復的集合列表穆趴,那么可以選擇 Sorted Set 結構脸爱。和 Set 相比,Sorted Set關聯(lián)了一個 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
創(chuàng)建實體類 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蜻牢,還提供了開箱即用的緩存臨時存儲方案烤咧,也支持和主流的專業(yè)緩存如 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, "肖戰(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("進入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", 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 緩存 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 不同的是,它每次都會觸發(fā)真實方法的調用涡贱。參數描述見上咏删。③@CacheEvict根據條件對緩存進行清空:
Key:同上。
Value:同上盼产。
Condition:同上饵婆。
allEntries:是否清空所有緩存內容勺馆,缺省為 false戏售,如果指定為 true,則方法調用后將立即清空所有緩存草穆。
beforeInvocation:是否在方法執(zhí)行前就清空灌灾,缺省為 false,如果指定為 true悲柱,則在方法還沒有執(zhí)行的時候就清空緩存锋喜。缺省情況下,如果方法執(zhí)行拋出異常豌鸡,則不會清空緩存嘿般。
緩存問題
面試官:看了一下你的 Demo,簡單易懂涯冠。那你在實際項目中使用緩存有遇到什么問題或者會遇到什么問題你知道嗎炉奴?我:緩存和數據庫數據一致性問題:分布式環(huán)境下非常容易出現緩存和數據庫間數據一致性問題,針對這一點蛇更,如果項目對緩存的要求是強一致性的瞻赶,那么就不要使用緩存赛糟。我們只能采取合適的策略來降低緩存和數據庫間數據不一致的概率,而無法保證兩者間的強一致性砸逊。合適的策略包括合適的緩存更新策略璧南,更新數據庫后及時更新緩存、緩存失敗時增加重試機制师逸。面試官:Redis 雪崩了解嗎司倚?我:我了解的,目前電商首頁以及熱點數據都會去做緩存篓像,一般緩存都是定時任務去刷新对湃,或者查不到之后去更新緩存的,定時任務刷新就有一個問題遗淳。舉個栗子:如果首頁所有 Key 的失效時間都是 12 小時拍柒,中午 12 點刷新的,我零點有個大促活動大量用戶涌入屈暗,假設每秒 6000 個請求拆讯,本來緩存可以抗住每秒 5000 個請求,但是緩存中所有 Key 都失效了养叛。此時 6000 個/秒的請求全部落在了數據庫上种呐,數據庫必然扛不住,真實情況可能 DBA 都沒反應過來直接掛了弃甥。此時爽室,如果沒什么特別的方案來處理,DBA 很著急淆攻,重啟數據庫阔墩,但是數據庫立馬又被新流量給打死了。這就是我理解的緩存雪崩瓶珊。我心想:同一時間大面積失效啸箫,瞬間 Redis 跟沒有一樣,那這個數量級別的請求直接打到數據庫幾乎是災難性的伞芹。你想想如果掛的是一個用戶服務的庫忘苛,那其他依賴他的庫所有接口幾乎都會報錯。如果沒做熔斷等策略基本上就是瞬間掛一片的節(jié)奏唱较,你怎么重啟用戶都會把你打掛扎唾,等你重啟好的時候,用戶早睡覺去了南缓,臨睡之前胸遇,罵罵咧咧“什么垃圾產品”。面試官摸摸了自己的頭發(fā):嗯西乖,還不錯狐榔,那這種情況你都是怎么應對的坛增?
我:處理緩存雪崩簡單,在批量往 Redis 存數據的時候薄腻,把每個 Key 的失效時間都加個隨機值就好了收捣,這樣可以保證數據不會再同一時間大面積失效。
setRedis(key, value, time+Math.random()*10000);
如果 Redis 是集群部署庵楷,將熱點數據均勻分布在不同的 Redis 庫中也能避免全部失效罢艾。或者設置熱點數據永不過期尽纽,有更新操作就更新緩存就好了(比如運維更新了首頁商品咐蚯,那你刷下緩存就好了,不要設置過期時間)弄贿,電商首頁的數據也可以用這個操作春锋,保險。面試官:那你了解緩存穿透和擊穿么差凹,可以說說他們跟雪崩的區(qū)別嗎期奔?我:嗯,了解危尿,先說下緩存穿透吧甸各,緩存穿透是指緩存和數據庫中都沒有的數據畏吓,而用戶(黑客)不斷發(fā)起請求。舉個栗子:我們數據庫的 id 都是從 1 自增的筹误,如果發(fā)起 id=-1 的數據或者 id 特別大不存在的數據禀酱,這樣的不斷攻擊導致數據庫壓力很大误褪,嚴重會擊垮數據庫脆贵。我又接著說:至于緩存擊穿嘛忆植,這個跟緩存雪崩有點像,但是又有一點不一樣船逮,緩存雪崩是因為大面積的緩存失效顾腊,打崩了 DB粤铭。而緩存擊穿不同的是緩存擊穿是指一個 Key 非常熱點挖胃,在不停地扛著大量的請求,大并發(fā)集中對這一個點進行訪問梆惯,當這個 Key 在失效的瞬間酱鸭,持續(xù)的大并發(fā)直接落到了數據庫上,就在這個 Key 的點上擊穿了緩存垛吗。面試官露出欣慰的眼光:那他們分別怎么解決凹髓?我:緩存穿透我會在接口層增加校驗,比如用戶鑒權怯屉,參數做校驗蔚舀,不合法的校驗直接 return饵沧,比如 id 做基礎校驗,id<=0 直接攔截赌躺。面試官:那你還有別的方法嗎狼牺?我:我記得 Redis 里還有一個高級用法布隆過濾器(Bloom Filter)這個也能很好的預防緩存穿透的發(fā)生。它的原理也很簡單礼患,就是利用高效的數據結構和算法快速判斷出你這個 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 的優(yōu)勢就是查找和操作的時間復雜度是 O(1)沟娱。
數據結構簡單,對數據操作也簡單腕柜。
采用單線程济似,避免了不必要的上下文切換和競爭條件,不存在多線程導致的 CPU 切換盏缤,不用去考慮各種鎖的問題砰蠢,不存在加鎖釋放鎖操作,沒有死鎖問題導致的性能消耗唉铜。
使用多路復用 IO 模型台舱,非阻塞 IO。
Redis 和 Memcached 的區(qū)別
面試官:嗯嗯潭流,說的很詳細竞惋。那你為什么選擇 Redis 的緩存方案而不用 Memcached 呢?我:原因有如下四點:
存儲方式上:Memcache 會把數據全部存在內存之中灰嫉,斷電后會掛掉拆宛,數據不能超過內存大小。Redis 有部分數據存在硬盤上讼撒,這樣能保證數據的持久性浑厚。
數據支持類型上:Memcache 對數據類型的支持簡單,只支持簡單的 key-value根盒,钳幅,而 Redis 支持五種數據類型。
使用底層模型不同:它們之間底層實現方式以及與客戶端之間通信的應用協(xié)議不一樣炎滞。Redis 直接自己構建了 VM 機制敢艰,因為一般的系統(tǒng)調用系統(tǒng)函數的話,會浪費一定的時間去移動和請求册赛。
Value 的大心频肌:Redis 可以達到 1GB,而 Memcache 只有 1MB击奶。
淘汰策略
面試官:那你說說你知道的 Redis 的淘汰策略有哪些辈双?
我:Redis 有六種淘汰策略,如下圖:
補充一下:Redis 4.0 加入了 LFU(least frequency use)淘汰策略柜砾,包括 volatile-lfu 和 allkeys-lfu,通過統(tǒng)計訪問頻率换衬,將訪問頻率最少痰驱,即最不經常使用的 KV 淘汰证芭。
持久化
面試官:你對 Redis 的持久化機制了解嗎?能講一下嗎担映?我:Redis 為了保證效率废士,數據緩存在了內存中,但是會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件中蝇完,以保證數據的持久化官硝。Redis 的持久化策略有兩種:
RDB:快照形式是直接把內存中的數據保存到一個 dump 的文件中,定時保存短蜕,保存策略氢架。
AOF:把所有的對 Redis 的服務器進行修改的命令都存到一個文件里,命令的集合朋魔。Redis 默認是快照 RDB 的持久化方式岖研。
當 Redis 重啟的時候,它會優(yōu)先使用 AOF 文件來還原數據集警检,因為 AOF 文件保存的數據集通常比 RDB 文件所保存的數據集更完整孙援。你甚至可以關閉持久化功能,讓數據只在服務器運行時存扇雕。面試官:那你再說下 RDB 是怎么工作的拓售?我:默認 Redis 是會以快照"RDB"的形式將數據持久化到磁盤的一個二進制文件 dump.rdb。工作原理簡單說一下:當 Redis 需要做持久化時镶奉,Redis 會 fork 一個子進程邻辉,子進程將數據寫到磁盤上一個臨時 RDB 文件中。當子進程完成寫臨時文件后腮鞍,將原來的 RDB 替換掉值骇,這樣的好處是可以 copy-on-write。我:RDB 的優(yōu)點是:這種文件非常適合用于備份:比如移国,你可以在最近的 24 小時內吱瘩,每小時備份一次,并且在每個月的每一天也備份一個 RDB 文件迹缀。這樣的話使碾,即使遇上問題,也可以隨時將數據集還原到不同的版本祝懂。RDB 非常適合災難恢復票摇。RDB 的缺點是:如果你需要盡量避免在服務器故障時丟失數據,那么RDB不合適你砚蓬。面試官:那你要不再說下 AOF矢门?
我:(說就一起說下吧)使用 AOF 做持久化,每一個寫命令都通過 write 函數追加到 appendonly.aof 中,配置方式如下:
appendfsync yes
appendfsync always #每次有數據修改發(fā)生時都會寫入AOF文件祟剔。
appendfsync everysec #每秒鐘同步一次隔躲,該策略為AOF的缺省策略。
AOF 可以做到全程持久化物延,只需要在配置中開啟 appendonly yes宣旱。這樣 Redis 每執(zhí)行一個修改數據的命令,都會把它添加到 AOF 文件中叛薯,當 Redis 重啟時浑吟,將會讀取 AOF 文件進行重放,恢復到 Redis 關閉前的最后時刻耗溜。我頓了一下组力,繼續(xù)說:使用 AOF 的優(yōu)點是會讓 Redis 變得非常耐久∏况可以設置不同的 Fsync 策略忿项,AOF的默認策略是每秒鐘 Fsync 一次,在這種配置下城舞,就算發(fā)生故障停機轩触,也最多丟失一秒鐘的數據。缺點是對于相同的數據集來說家夺,AOF 的文件體積通常要大于 RDB 文件的體積脱柱。根據所使用的 Fsync 策略,AOF 的速度可能會慢于 RDB拉馋。面試官又問:你說了這么多榨为,那我該用哪一個呢?我:如果你非常關心你的數據煌茴,但仍然可以承受數分鐘內的數據丟失随闺,那么可以額只使用 RDB 持久。AOF 將 Redis 執(zhí)行的每一條命令追加到磁盤中蔓腐,處理巨大的寫入會降低Redis的性能矩乐,不知道你是否可以接受。數據庫備份和災難恢復:定時生成 RDB 快照非常便于進行數據庫備份回论,并且 RDB 恢復數據集的速度也要比 AOF 恢復的速度快散罕。當然了,Redis 支持同時開啟 RDB 和 AOF傀蓉,系統(tǒng)重啟后欧漱,Redis 會優(yōu)先使用 AOF 來恢復數據,這樣丟失的數據會最少葬燎。
主從復制
面試官:Redis 單節(jié)點存在單點故障問題误甚,為了解決單點問題缚甩,一般都需要對 Redis 配置從節(jié)點,然后使用哨兵來監(jiān)聽主節(jié)點的存活狀態(tài)靶草,如果主節(jié)點掛掉蹄胰,從節(jié)點能繼續(xù)提供緩存功能岳遥,你能說說 Redis 主從復制的過程和原理嗎奕翔?我有點懵,這個說來就話長了浩蓉。但幸好提前準備了:主從配置結合哨兵模式能解決單點故障問題派继,提高 Redis 可用性。從節(jié)點僅提供讀操作捻艳,主節(jié)點提供寫操作驾窟。對于讀多寫少的狀況,可給主節(jié)點配置多個從節(jié)點认轨,從而提高響應效率绅络。我頓了一下,接著說:關于復制過程嘁字,是這樣的:
從節(jié)點執(zhí)行 slaveof[masterIP][masterPort]恩急,保存主節(jié)點信息。
從節(jié)點中的定時任務發(fā)現主節(jié)點信息纪蜒,建立和主節(jié)點的 Socket 連接衷恭。
從節(jié)點發(fā)送 Ping 信號,主節(jié)點返回 Pong纯续,兩邊能互相通信随珠。
連接建立后,主節(jié)點將所有數據發(fā)送給從節(jié)點(數據同步)猬错。
主節(jié)點把當前的數據同步給從節(jié)點后窗看,便完成了復制的建立過程。接下來倦炒,主節(jié)點就會持續(xù)的把寫命令發(fā)送給從節(jié)點显沈,保證主從數據一致性。
面試官:那你能詳細說下數據同步的過程嗎析校?(我心想:這也問的太細了吧)我:可以构罗。Redis 2.8 之前使用 sync[runId][offset] 同步命令,Redis 2.8 之后使用 psync[runId][offset] 命令智玻。兩者不同在于遂唧,Sync 命令僅支持全量復制過程,Psync 支持全量和部分復制吊奢。介紹同步之前盖彭,先介紹幾個概念:
runId:每個 Redis 節(jié)點啟動都會生成唯一的 uuid纹烹,每次 Redis 重啟后,runId 都會發(fā)生變化召边。
-
offset:主節(jié)點和從節(jié)點都各自維護自己的主從復制偏移量 offset铺呵,當主節(jié)點有寫入命令時,offset=offset+命令的字節(jié)長度隧熙。
從節(jié)點在收到主節(jié)點發(fā)送的命令后片挂,也會增加自己的 offset,并把自己的 offset 發(fā)送給主節(jié)點贞盯。
這樣音念,主節(jié)點同時保存自己的 offset 和從節(jié)點的 offset,通過對比 offset 來判斷主從節(jié)點數據是否一致躏敢。
repl_backlog_size:保存在主節(jié)點上的一個固定長度的先進先出隊列闷愤,默認大小是 1MB。
主節(jié)點發(fā)送數據給從節(jié)點過程中件余,主節(jié)點還會進行一些寫操作讥脐,這時候的數據存儲在復制緩沖區(qū)中。
從節(jié)點同步主節(jié)點數據完成后啼器,主節(jié)點將緩沖區(qū)的數據繼續(xù)發(fā)送給從節(jié)點旬渠,用于部分復制。
主節(jié)點響應寫命令時镀首,不但會把命名發(fā)送給從節(jié)點坟漱,還會寫入復制積壓緩沖區(qū),用于復制命令丟失的數據補救更哄。
上面是 Psync 的執(zhí)行流程芋齿,從節(jié)點發(fā)送 psync[runId][offset] 命令,主節(jié)點有三種響應:
FULLRESYNC:第一次連接成翩,進行全量復制
CONTINUE:進行部分復制
ERR:不支持 psync 命令觅捆,進行全量復制
面試官:很好,那你能具體說下全量復制和部分復制的過程嗎麻敌?
我:可以栅炒!
上面是全量復制的流程。主要有以下幾步:
從節(jié)點發(fā)送 psync ? -1 命令(因為第一次發(fā)送术羔,不知道主節(jié)點的 runId赢赊,所以為?,因為是第一次復制级历,所以 offset=-1)释移。
主節(jié)點發(fā)現從節(jié)點是第一次復制,返回 FULLRESYNC {runId} {offset}寥殖,runId 是主節(jié)點的 runId玩讳,offset 是主節(jié)點目前的 offset涩蜘。
從節(jié)點接收主節(jié)點信息后,保存到 info 中熏纯。
主節(jié)點在發(fā)送 FULLRESYNC 后同诫,啟動 bgsave 命令,生成 RDB 文件(數據持久化)樟澜。
主節(jié)點發(fā)送 RDB 文件給從節(jié)點误窖。到從節(jié)點加載數據完成這段期間主節(jié)點的寫命令放入緩沖區(qū)。
從節(jié)點清理自己的數據庫數據往扔。
從節(jié)點加載 RDB 文件贩猎,將數據保存到自己的數據庫中熊户。如果從節(jié)點開啟了 AOF萍膛,從節(jié)點會異步重寫 AOF 文件。
關于部分復制有以下幾點說明:①部分復制主要是 Redis 針對全量復制的過高開銷做出的一種優(yōu)化措施嚷堡,使用 psync[runId][offset] 命令實現蝗罗。當從節(jié)點正在復制主節(jié)點時,如果出現網絡閃斷或者命令丟失等異常情況時蝌戒,從節(jié)點會向主節(jié)點要求補發(fā)丟失的命令數據串塑,主節(jié)點的復制積壓緩沖區(qū)將這部分數據直接發(fā)送給從節(jié)點。這樣就可以保持主從節(jié)點復制的一致性北苟。補發(fā)的這部分數據一般遠遠小于全量數據桩匪。②主從連接中斷期間主節(jié)點依然響應命令,但因復制連接中斷命令無法發(fā)送給從節(jié)點友鼻,不過主節(jié)點內的復制積壓緩沖區(qū)依然可以保存最近一段時間的寫命令數據傻昙。③當主從連接恢復后,由于從節(jié)點之前保存了自身已復制的偏移量和主節(jié)點的運行 ID彩扔。因此會把它們當做 psync 參數發(fā)送給主節(jié)點妆档,要求進行部分復制。④主節(jié)點接收到 psync 命令后首先核對參數 runId 是否與自身一致虫碉,如果一致贾惦,說明之前復制的是當前主節(jié)點。之后根據參數 offset 在復制積壓緩沖區(qū)中查找敦捧,如果 offset 之后的數據存在须板,則對從節(jié)點發(fā)送+COUTINUE 命令,表示可以進行部分復制兢卵。因為緩沖區(qū)大小固定习瑰,若發(fā)生緩沖溢出,則進行全量復制济蝉。⑤主節(jié)點根據偏移量把復制積壓緩沖區(qū)里的數據發(fā)送給從節(jié)點杰刽,保證主從復制進入正常狀態(tài)菠发。
哨兵
面試官:那主從復制會存在哪些問題呢?我:主從復制會存在以下問題:
一旦主節(jié)點宕機贺嫂,從節(jié)點晉升為主節(jié)點滓鸠,同時需要修改應用方的主節(jié)點地址,還需要命令所有從節(jié)點去復制新的主節(jié)點第喳,整個過程需要人工干預糜俗。
主節(jié)點的寫能力受到單機的限制。
主節(jié)點的存儲能力受到單機的限制曲饱。
-
原生復制的弊端在早期的版本中也會比較突出悠抹,比如:Redis 復制中斷后,從節(jié)點會發(fā)起 psync扩淀。
此時如果同步不成功楔敌,則會進行全量同步,主庫執(zhí)行全量備份的同時驻谆,可能會造成毫秒或秒級的卡頓卵凑。
面試官:那比較主流的解決方案是什么呢?我:當然是哨兵啊胜臊。
面試官:那么問題又來了勺卢。那你說下哨兵有哪些功能?
我:如圖象对,是 Redis Sentinel(哨兵)的架構圖黑忱。Redis Sentinel(哨兵)主要功能包括主節(jié)點存活檢測、主從運行情況檢測勒魔、自動故障轉移甫煞、主從切換。Redis Sentinel 最小配置是一主一從沥邻。Redis 的 Sentinel 系統(tǒng)可以用來管理多個 Redis 服務器危虱。該系統(tǒng)可以執(zhí)行以下四個任務:
監(jiān)控:不斷檢查主服務器和從服務器是否正常運行。
通知:當被監(jiān)控的某個 Redis 服務器出現問題唐全,Sentinel 通過 API 腳本向管理員或者其他應用程序發(fā)出通知埃跷。
自動故障轉移:當主節(jié)點不能正常工作時,Sentinel 會開始一次自動的故障轉移操作邮利,它會將與失效主節(jié)點是主從關系的其中一個從節(jié)點升級為新的主節(jié)點弥雹,并且將其他的從節(jié)點指向新的主節(jié)點,這樣人工干預就可以免了延届。
配置提供者:在 Redis Sentinel 模式下剪勿,客戶端應用在初始化時連接的是 Sentinel 節(jié)點集合,從中獲取主節(jié)點的信息方庭。
面試官:那你能說下哨兵的工作原理嗎厕吉?
我:話不多說酱固,直接上圖:
①每個 Sentinel 節(jié)點都需要定期執(zhí)行以下任務:每個 Sentinel 以每秒一次的頻率,向它所知的主服務器头朱、從服務器以及其他的 Sentinel 實例發(fā)送一個 PING 命令运悲。(如上圖)
②如果一個實例距離最后一次有效回復 PING 命令的時間超過 down-after-milliseconds 所指定的值,那么這個實例會被 Sentinel 標記為主觀下線项钮。(如上圖)
③如果一個主服務器被標記為主觀下線班眯,那么正在監(jiān)視這個服務器的所有 Sentinel 節(jié)點,要以每秒一次的頻率確認主服務器的確進入了主觀下線狀態(tài)烁巫。
④如果一個主服務器被標記為主觀下線署隘,并且有足夠數量的 Sentinel(至少要達到配置文件指定的數量)在指定的時間范圍內同意這一判斷,那么這個主服務器被標記為客觀下線亚隙。
⑤一般情況下磁餐,每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有主服務器和從服務器發(fā)送 INFO 命令。
當一個主服務器被標記為客觀下線時恃鞋,Sentinel 向下線主服務器的所有從服務器發(fā)送 INFO 命令的頻率崖媚,會從 10 秒一次改為每秒一次。
⑥Sentinel 和其他 Sentinel 協(xié)商客觀下線的主節(jié)點的狀態(tài)恤浪,如果處于 SDOWN 狀態(tài),則投票自動選出新的主節(jié)點肴楷,將剩余從節(jié)點指向新的主節(jié)點進行數據復制水由。
⑦當沒有足夠數量的 Sentinel 同意主服務器下線時,主服務器的客觀下線狀態(tài)就會被移除赛蔫。當主服務器重新向 Sentinel 的 PING 命令返回有效回復時砂客,主服務器的主觀下線狀態(tài)就會被移除。面試官:不錯呵恢,面試前沒少下工夫啊鞠值,今天 Redis 這關你過了,明天找個時間我們再聊聊其他的渗钉。(露出欣慰的微笑)我:沒問題彤恶。
總結
本文在一次面試的過程中講述了 Redis 是什么,Redis 的特點和功能鳄橘,Redis 緩存的使用声离,Redis 為什么能這么快,Redis 緩存的淘汰策略瘫怜,持久化的兩種方式术徊,Redis 高可用部分的主從復制和哨兵的基本原理。只要功夫深鲸湃,鐵杵磨成針赠涮,平時準備好子寓,面試不用慌。雖然面試不一定是這樣問的笋除,但萬變不離其“宗”别瞭。