米哈游提前批,開始了!
共 19741字,需瀏覽 40分鐘
·
2024-07-11 16:56
圖解學習網(wǎng)站:https://xiaolincoding.com
大家好,我是小林。
最近這一兩周看到不少互聯(lián)網(wǎng)公司都已經(jīng)開始秋招提前批了,比如百度、Oppo、網(wǎng)易雷火、大疆、米哈游等等。
如果想投遞提前批的同學,直接去公司官方對應的校招招聘這一欄進行投遞就行了,現(xiàn)在還是在網(wǎng)申階段,等 8 月份開始才會慢慢約面。
昨天看到米哈游剛開了提前批公告,看到說明提前批和正式批只能投一次,但是有的公司是可以投 2 次的,所以大家投遞的時候,多注意一下這些事情。
那么,今天就來分享之前一位同學的米哈游Java 校招的面經(jīng),給大家提前感受一下秋招面試,大家看看難度如何?
考察的知識點,我?guī)痛蠹伊_列一下:
-
Java 基礎、集合、spirng -
MySQL索引、事務、索引失效、最左匹配原則 -
Redis 應用場景、持久化
Java
深拷貝和淺拷貝的區(qū)別?
-
淺拷貝是指只復制對象本身和其內(nèi)部的值類型字段,但不會復制對象內(nèi)部的引用類型字段。換句話說,淺拷貝只是創(chuàng)建一個新的對象,然后將原對象的字段值復制到新對象中,但如果原對象內(nèi)部有引用類型的字段,只是將引用復制到新對象中,兩個對象指向的是同一個引用對象。 -
深拷貝是指在復制對象的同時,將對象內(nèi)部的所有引用類型字段的內(nèi)容也復制一份,而不是共享引用。換句話說,深拷貝會遞歸復制對象內(nèi)部所有引用類型的字段,生成一個全新的對象以及其內(nèi)部的所有對象。
實現(xiàn)深拷貝的三種方法是什么?
在 Java 中,實現(xiàn)對象深拷貝的方法有以下幾種主要方式:
實現(xiàn) Cloneable 接口并重寫 clone() 方法
這種方法要求對象及其所有引用類型字段都實現(xiàn) Cloneable 接口,并且重寫 clone() 方法。在 clone() 方法中,通過遞歸克隆引用類型字段來實現(xiàn)深拷貝。
class MyClass implements Cloneable {
private String field1;
private NestedClass nestedObject;
@Override
protected Object clone() throws CloneNotSupportedException {
MyClass cloned = (MyClass) super.clone();
cloned.nestedObject = (NestedClass) nestedObject.clone(); // 深拷貝內(nèi)部的引用對象
return cloned;
}
}
class NestedClass implements Cloneable {
private int nestedField;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
使用序列化和反序列化
通過將對象序列化為字節(jié)流,再從字節(jié)流反序列化為對象來實現(xiàn)深拷貝。要求對象及其所有引用類型字段都實現(xiàn) Serializable 接口。
import java.io.*;
class MyClass implements Serializable {
private String field1;
private NestedClass nestedObject;
public MyClass deepCopy() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
oos.flush();
oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (MyClass) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
class NestedClass implements Serializable {
private int nestedField;
}
手動遞歸復制
針對特定對象結構,手動遞歸復制對象及其引用類型字段。適用于對象結構復雜度不高的情況。
class MyClass {
private String field1;
private NestedClass nestedObject;
public MyClass deepCopy() {
MyClass copy = new MyClass();
copy.setField1(this.field1);
copy.setNestedObject(this.nestedObject.deepCopy());
return copy;
}
}
class NestedClass {
private int nestedField;
public NestedClass deepCopy() {
NestedClass copy = new NestedClass();
copy.setNestedField(this.nestedField);
return copy;
}
}
介紹一下Java異常
Java異常類層次結構圖:
Java的異常體系主要基于兩大類:Throwable類及其子類。Throwable有兩個重要的子類:Error和Exception,它們分別代表了不同類型的異常情況。
-
Error(錯誤):表示運行時環(huán)境的錯誤。錯誤是程序無法處理的嚴重問題,如系統(tǒng)崩潰、虛擬機錯誤、動態(tài)鏈接失敗等。通常,程序不應該嘗試捕獲這類錯誤。例如,OutOfMemoryError、StackOverflowError等。 -
Exception(異常):表示程序本身可以處理的異常條件。異常分為兩大類: -
非運行時異常:這類異常在編譯時期就必須被捕獲或者聲明拋出。它們通常是外部錯誤,如文件不存在(FileNotFoundException)、類未找到(ClassNotFoundException)等。非運行時異常強制程序員處理這些可能出現(xiàn)的問題,增強了程序的健壯性。 -
運行時異常:這類異常包括運行時異常(RuntimeException)和錯誤(Error)。運行時異常由程序錯誤導致,如空指針訪問(NullPointerException)、數(shù)組越界(ArrayIndexOutOfBoundsException)等。運行時異常是不需要在編譯時強制捕獲或聲明的。
hashmap的put過程介紹一下
HashMap HashMap的put()方法用于向HashMap中添加鍵值對,當調(diào)用HashMap的put()方法時,會按照以下詳細流程執(zhí)行(JDK8 1.8版本):
第一步:根據(jù)要添加的鍵的哈希碼計算在數(shù)組中的位置(索引)。
第二步:檢查該位置是否為空(即沒有鍵值對存在)
-
如果為空,則直接在該位置創(chuàng)建一個新的Entry對象來存儲鍵值對。將要添加的鍵值對作為該Entry的鍵和值,并保存在數(shù)組的對應位置。將HashMap的修改次數(shù)(modCount)加1,以便在進行迭代時發(fā)現(xiàn)并發(fā)修改。
第三步:如果該位置已經(jīng)存在其他鍵值對,檢查該位置的第一個鍵值對的哈希碼和鍵是否與要添加的鍵值對相同?
-
如果相同,則表示找到了相同的鍵,直接將新的值替換舊的值,完成更新操作。
第四步:如果第一個鍵值對的哈希碼和鍵不相同,則需要遍歷鏈表或紅黑樹來查找是否有相同的鍵:
如果鍵值對集合是鏈表結構,從鏈表的頭部開始逐個比較鍵的哈希碼和equals()方法,直到找到相同的鍵或達到鏈表末尾。
-
如果找到了相同的鍵,則使用新的值取代舊的值,即更新鍵對應的值。 -
如果沒有找到相同的鍵,則將新的鍵值對添加到鏈表的頭部。
如果鍵值對集合是紅黑樹結構,在紅黑樹中使用哈希碼和equals()方法進行查找。根據(jù)鍵的哈希碼,定位到紅黑樹中的某個節(jié)點,然后逐個比較鍵,直到找到相同的鍵或達到紅黑樹末尾。
-
如果找到了相同的鍵,則使用新的值取代舊的值,即更新鍵對應的值。 -
如果沒有找到相同的鍵,則將新的鍵值對添加到紅黑樹中。
第五步:檢查鏈表長度是否達到閾值(默認為8):
-
如果鏈表長度超過閾值,且HashMap的數(shù)組長度大于等于64,則會將鏈表轉換為紅黑樹,以提高查詢效率。
第六步:檢查負載因子是否超過閾值(默認為0.75):
-
如果鍵值對的數(shù)量(size)與數(shù)組的長度的比值大于閾值,則需要進行擴容操作。
第七步:擴容操作:
-
創(chuàng)建一個新的兩倍大小的數(shù)組。 -
將舊數(shù)組中的鍵值對重新計算哈希碼并分配到新數(shù)組中的位置。 -
更新HashMap的數(shù)組引用和閾值參數(shù)。
第八步:完成添加操作。
此外,HashMap是非線程安全的,如果在多線程環(huán)境下使用,需要采取額外的同步措施或使用線程安全的ConcurrentHashMap。
hashmap key可以為null嗎?
可以為 null。
-
hashMap中使用hash()方法來計算key的哈希值,當key為空時,直接另key的哈希值為0,不走key.hashCode()方法;
-
hashMap雖然支持key和value為null,但是null作為key只能有一個,null作為value可以有多個; -
因為hashMap中,如果key值一樣,那么會覆蓋相同key值的value為最新,所以key為null只能有一個。
ConcurrentHashMap 和 hashmap 區(qū)別是什么?
HashMap 不是線程安全的,ConcurrentHashMap是線程安全的。HashMap 底層實現(xiàn)
-
在 JDK 1.7 版本之前, HashMap 數(shù)據(jù)結構是數(shù)組和鏈表,HashMap通過哈希算法將元素的鍵(Key)映射到數(shù)組中的槽位(Bucket)。如果多個鍵映射到同一個槽位,它們會以鏈表的形式存儲在同一個槽位上,因為鏈表的查詢時間是O(n),所以沖突很嚴重,一個索引上的鏈表非常長,效率就很低了。
-
所以在 JDK 1.8 版本的時候做了優(yōu)化,當一個鏈表的長度超過8的時候就轉換數(shù)據(jù)結構,不再使用鏈表存儲,而是使用紅黑樹,查找時使用紅黑樹,時間復雜度O(log n),可以提高查詢性能,但是在數(shù)量較少時,即數(shù)量小于6時,會將紅黑樹轉換回鏈表。
ConcurrentHashMap 底層實現(xiàn)
-
在 JDK 1.7 中它使用的是數(shù)組加鏈表的形式實現(xiàn)的,而數(shù)組又分為:大數(shù)組 Segment 和小數(shù)組 HashEntry。Segment 是一種可重入鎖(ReentrantLock),在 ConcurrentHashMap 里扮演鎖的角色;HashEntry 則用于存儲鍵值對數(shù)據(jù)。一個 ConcurrentHashMap 里包含一個 Segment 數(shù)組,一個 Segment 里包含一個 HashEntry 數(shù)組,每個 HashEntry 是一個鏈表結構的元素。簡單理解就是,ConcurrentHashMap 是一個 Segment 數(shù)組,Segment 通過繼承 ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每個 Segment 是線程安全的,也就實現(xiàn)了全局的線程安全。
-
JDK 1.8 也引入了紅黑樹,優(yōu)化了之前的固定鏈表,那么當數(shù)據(jù)量比較大的時候,查詢性能也得到了很大的提升,從之前的 O(n) 優(yōu)化到了 O(logn) 的時間復雜度。ConcurrentHashMap 主要通過 volatile + CAS 或者 synchronized 來實現(xiàn)的線程安全的,ConcurrentHashMap通過對頭結點加鎖來保證線程安全的,鎖的粒度相比 Segment 來說更小了,發(fā)生沖突和加鎖的頻率降低了,并發(fā)操作的性能就提高了。
spring是如何解決循環(huán)依賴的?
循環(huán)依賴指的是兩個類中的屬性相互依賴對方:例如 A 類中有 B 屬性,B 類中有 A屬性,從而形成了一個依賴閉環(huán),如下圖。
循環(huán)依賴問題在Spring中主要有三種情況:
-
第一種:通過構造方法進行依賴注入時產(chǎn)生的循環(huán)依賴問題。 -
第二種:通過setter方法進行依賴注入且是在多例(原型)模式下產(chǎn)生的循環(huán)依賴問題。 -
第三種:通過setter方法進行依賴注入且是在單例模式下產(chǎn)生的循環(huán)依賴問題。
只有【第三種方式】的循環(huán)依賴問題被 Spring 解決了,其他兩種方式在遇到循環(huán)依賴問題時,Spring都會產(chǎn)生異常。
Spring 解決單例模式下的setter循環(huán)依賴問題的主要方式是通過三級緩存解決循環(huán)依賴。三級緩存指的是 Spring 在創(chuàng)建 Bean 的過程中,通過三級緩存來緩存正在創(chuàng)建的 Bean,以及已經(jīng)創(chuàng)建完成的 Bean 實例。具體步驟如下:
-
實例化 Bean:Spring 在實例化 Bean 時,會先創(chuàng)建一個空的 Bean 對象,并將其放入一級緩存中。 -
屬性賦值:Spring 開始對 Bean 進行屬性賦值,如果發(fā)現(xiàn)循環(huán)依賴,會將當前 Bean 對象提前暴露給后續(xù)需要依賴的 Bean(通過提前暴露的方式解決循環(huán)依賴)。 -
初始化 Bean:完成屬性賦值后,Spring 將 Bean 進行初始化,并將其放入二級緩存中。 -
注入依賴:Spring 繼續(xù)對 Bean 進行依賴注入,如果發(fā)現(xiàn)循環(huán)依賴,會從二級緩存中獲取已經(jīng)完成初始化的 Bean 實例。
通過三級緩存的機制,Spring 能夠在處理循環(huán)依賴時,確保及時暴露正在創(chuàng)建的 Bean 對象,并能夠正確地注入已經(jīng)初始化的 Bean 實例,從而解決循環(huán)依賴問題,保證應用程序的正常運行。
spring 常用注解有什么?
@Autowired 注解
@Autowired:主要用于自動裝配bean。當Spring容器中存在與要注入的屬性類型匹配的bean時,它會自動將bean注入到屬性中。就跟我們new 對象一樣。用法很簡單,如下示例代碼:
@Component
public class MyService {
}
@Component
public class MyController {
@Autowired
private MyService myService;
}
在上面的示例代碼中,MyController類中的myService屬性被@Autowired注解標記,Spring會自動將MyService類型的bean注入到myService屬性中。
@Component
這個注解用于標記一個類作為Spring的bean。當一個類被@Component注解標記時,Spring會將其實例化為一個bean,并將其添加到Spring容器中。在上面講解@Autowired的時候也看到了,示例代碼:
@Component
public class MyComponent {
}
在上面的示例代碼中,MyComponent類被@Component注解標記,Spring會將其實例化為一個bean,并將其添加到Spring容器中。
@Configuration
@Configuration,注解用于標記一個類作為Spring的配置類。配置類可以包含@Bean注解的方法,用于定義和配置bean,作為全局配置。示例代碼:
@Configuration
public class MyConfiguration {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@Bean
@Bean注解用于標記一個方法作為Spring的bean工廠方法。當一個方法被@Bean注解標記時,Spring會將該方法的返回值作為一個bean,并將其添加到Spring容器中,如果自定義配置,經(jīng)常用到這個注解。
@Configuration
public class MyConfiguration {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@Service
@Service,這個注解用于標記一個類作為服務層的組件。它是@Component注解的特例,用于標記服務層的bean,一般標記在業(yè)務service的實現(xiàn)類。
@Service
public class MyServiceImpl {
}
@Repository
@Repository注解用于標記一個類作為數(shù)據(jù)訪問層的組件。它也是@Component注解的特例,用于標記數(shù)據(jù)訪問層的bean。這個注解很容易被忽略,導致數(shù)據(jù)庫無法訪問。
@Repository
public class MyRepository {
}
在上面的示例代碼中,MyRepository類被@Repository注解標記,Spring會將其實例化為一個bean,并將其添加到Spring容器中。
@Controller
@Controller注解用于標記一個類作為控制層的組件。它也是@Component注解的特例,用于標記控制層的bean。這是MVC結構的另一個部分,加在控制層
@Controller
public class MyController {
}
在上面的示例代碼中,MyController類被@Controller注解標記,Spring會將其實例化為一個bean,并將其添加到Spring容器中。
MySQL
索引是什么?有什么好處?
索引類似于書籍的目錄,可以減少掃描的數(shù)據(jù)量,提高查詢效率。
-
如果查詢的時候,沒有用到索引就會全表掃描,這時候查詢的時間復雜度是On -
如果用到了索引,那么查詢的時候,可以基于二分查找算法,通過索引快速定位到目標數(shù)據(jù), mysql 索引的數(shù)據(jù)結構一般是 b+樹,其搜索復雜度為O(logdN),其中 d 表示節(jié)點允許的最大子節(jié)點個數(shù)為 d 個。
事務是什么?怎么實現(xiàn)的?
實現(xiàn)事務必須要遵守 4 個特性,分別如下:
-
原子性(Atomicity):一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環(huán)節(jié),而且事務在執(zhí)行過程中發(fā)生錯誤,會被回滾到事務開始前的狀態(tài),就像這個事務從來沒有執(zhí)行過一樣,就好比買一件商品,購買成功時,則給商家付了錢,商品到手;購買失敗時,則商品在商家手中,消費者的錢也沒花出去。 -
一致性(Consistency):是指事務操作前和操作后,數(shù)據(jù)滿足完整性約束,數(shù)據(jù)庫保持一致性狀態(tài)。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉賬 200 元,分為兩個步驟,從 A 的賬戶扣除 200 元和對 B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會出現(xiàn)用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。 -
隔離性(Isolation):數(shù)據(jù)庫允許多個并發(fā)事務同時對其數(shù)據(jù)進行讀寫和修改的能力,隔離性可以防止多個事務并發(fā)執(zhí)行時由于交叉執(zhí)行而導致數(shù)據(jù)的不一致,因為多個事務同時使用相同的數(shù)據(jù)時,不會相互干擾,每個事務都有一個完整的數(shù)據(jù)空間,對其他并發(fā)事務是隔離的。也就是說,消費者購買商品這個事務,是不影響其他消費者購買的。 -
持久性(Durability):事務處理結束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。
InnoDB 引擎通過什么技術來保證事務的這四個特性的呢?
-
持久性是通過 redo log (重做日志)來保證的; -
原子性是通過 undo log(回滾日志) 來保證的; -
隔離性是通過 MVCC(多版本并發(fā)控制) 或鎖機制來保證的; -
一致性則是通過持久性+原子性+隔離性來保證;
索引失效有哪些?
6 種會發(fā)生索引失效的情況:
-
當我們使用左或者左右模糊匹配的時候,也就是 like %xx 或者 like %xx%這兩種方式都會造成索引失效; -
當我們在查詢條件中對索引列使用函數(shù),就會導致索引失效。 -
當我們在查詢條件中對索引列進行表達式計算,也是無法走索引的。 -
MySQL 在遇到字符串和數(shù)字比較的時候,會自動把字符串轉為數(shù)字,然后再進行比較。如果字符串是索引列,而條件語句中的輸入?yún)?shù)是數(shù)字的話,那么索引列會發(fā)生隱式類型轉換,由于隱式類型轉換是通過 CAST 函數(shù)實現(xiàn)的,等同于對索引列使用了函數(shù),所以就會導致索引失效。 -
聯(lián)合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優(yōu)先的方式進行索引的匹配,否則就會導致索引失效。 -
在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 后的條件列不是索引列,那么索引會失效。
聚簇索引和非聚簇索引區(qū)別是什么?
在 MySQL 的 InnoDB 引擎中,每個索引都會對應一顆 B+ 樹,而聚簇索引和非聚簇索引最大的區(qū)別在于葉子節(jié)點存儲的數(shù)據(jù)不同,聚簇索引葉子節(jié)點存儲的是行數(shù)據(jù),因此通過聚簇索引可以直接找到真正的行數(shù)據(jù);而非聚簇索引葉子節(jié)點存儲的是主鍵id,所以使用非聚簇索引還需要回表查詢。
因此聚簇索引和非聚簇索引的區(qū)別主要有以下幾個:
-
聚簇索引葉子節(jié)點存儲的是行數(shù)據(jù);而非聚簇索引葉子節(jié)點存儲的是聚簇索引(通常是主鍵 ID)。 -
聚簇索引查詢效率更高,而非聚簇索引需要進行回表查詢,因此性能不如聚簇索引。 -
聚簇索引一般為主鍵索引,而主鍵一個表中只能有一個,因此聚簇索引一個表中也只能有一個,而非聚簇索引則沒有數(shù)量上的限制。
最左匹配原則是什么?
通過將多個字段組合成一個索引,該索引就被稱為聯(lián)合索引。
比如,將商品表中的 product_no 和 name 字段組合成聯(lián)合索引(product_no, name),創(chuàng)建聯(lián)合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
聯(lián)合索引(product_no, name) 的 B+Tree 示意圖如下。
可以看到,聯(lián)合索引的非葉子節(jié)點用兩個字段的值作為 B+Tree 的 key 值。當在聯(lián)合索引查詢數(shù)據(jù)時,先按 product_no 字段比較,在 product_no 相同的情況下再按 name 字段比較。
也就是說,聯(lián)合索引查詢的 B+Tree 是先按 product_no 進行排序,然后再 product_no 相同的情況再按 name 字段排序。
因此,使用聯(lián)合索引時,存在最左匹配原則,也就是按照最左優(yōu)先的方式進行索引的匹配。在使用聯(lián)合索引進行查詢的時候,如果不遵循「最左匹配原則」,聯(lián)合索引會失效,這樣就無法利用到索引快速查詢的特性了。
比如,如果創(chuàng)建了一個 (a, b, c) 聯(lián)合索引,如果查詢條件是以下這幾種,就可以匹配上聯(lián)合索引:
-
where a=1; -
where a=1 and b=2 and c=3; -
where a=1 and b=2;
需要注意的是,因為有查詢優(yōu)化器,所以 a 字段在 where 子句的順序并不重要。但是,如果查詢條件是以下這幾種,因為不符合最左匹配原則,所以就無法匹配上聯(lián)合索引,聯(lián)合索引就會失效:
-
where b=2; -
where c=3; -
where b=2 and c=3;
上面這些查詢條件之所以會失效,是因為(a, b, c) 聯(lián)合索引,是先按 a 排序,在 a 相同的情況再按 b 排序,在 b 相同的情況再按 c 排序。
所以,b 和 c 是全局無序,局部相對有序的,這樣在沒有遵循最左匹配原則的情況下,是無法利用到索引的。
我這里舉聯(lián)合索引(a,b)的例子,該聯(lián)合索引的 B+ Tree 如下。
可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是無序的(12,7,8,2,3,8,10,5,2)。因此,直接執(zhí)行where b = 2這種查詢條件沒有辦法利用聯(lián)合索引的,利用索引的前提是索引里的 key 是有序的。
只有在 a 相同的情況才,b 才是有序的,比如 a 等于 2 的時候,b 的值為(7,8),這時就是有序的,這個有序狀態(tài)是局部的,因此,執(zhí)行where a = 2 and b = 7是 a 和 b 字段能用到聯(lián)合索引的,也就是聯(lián)合索引生效了。
聯(lián)合索引有一些特殊情況,并不是查詢過程使用了聯(lián)合索引查詢,就代表聯(lián)合索引中的所有字段都用到了聯(lián)合索引進行索引查詢**,也就是可能存在部分字段用到聯(lián)合索引的 B+Tree,部分字段沒有用到聯(lián)合索引的 B+Tree 的情況。
這種特殊情況就發(fā)生在范圍查詢。聯(lián)合索引的最左匹配原則會一直向右匹配直到遇到「范圍查詢」就會停止匹配。也就是范圍查詢的字段可以用到聯(lián)合索引,但是在范圍查詢字段的后面的字段無法用到聯(lián)合索引。
Redis
redis應用場景是什么?
-
緩存: Redis最常見的用途就是作為緩存系統(tǒng)。通過將熱門數(shù)據(jù)存儲在內(nèi)存中,可以極大地提高訪問速度,減輕數(shù)據(jù)庫負載,這對于需要快速響應時間的應用程序非常重要。 -
排行榜: Redis的有序集合結構非常適合用于實現(xiàn)排行榜和排名系統(tǒng),可以方便地進行數(shù)據(jù)排序和排名。 -
分布式鎖: Redis的特性可以用來實現(xiàn)分布式鎖,確保多個進程或服務之間的數(shù)據(jù)操作的原子性和一致性。 -
計數(shù)器 由于Redis的原子操作和高性能,它非常適合用于實現(xiàn)計數(shù)器和統(tǒng)計數(shù)據(jù)的存儲,如網(wǎng)站訪問量統(tǒng)計、點贊數(shù)統(tǒng)計等。 -
消息隊列: Redis的發(fā)布訂閱功能使其成為一個輕量級的消息隊列,它可以用來實現(xiàn)發(fā)布和訂閱模式,以便實時處理消息。
使用時注意什么問題?
如果使用 redis 作為緩存的話,要注意mysql 和 redis 雙寫一致性的問題。
緩存是通過犧牲強一致性來提高性能的。這是由CAP理論決定的。緩存系統(tǒng)適用的場景就是非強一致性的場景,它屬于CAP中的AP。所以,如果需要數(shù)據(jù)庫和緩存數(shù)據(jù)保持強一致,就不適合使用緩存。
所以使用緩存提升性能,就是會有數(shù)據(jù)更新的延遲。這需要我們在設計時結合業(yè)務仔細思考是否適合用緩存。然后緩存一定要設置過期時間,這個時間太短、或者太長都不好:
-
太短的話請求可能會比較多的落到數(shù)據(jù)庫上,這也意味著失去了緩存的優(yōu)勢。 -
太長的話緩存中的臟數(shù)據(jù)會使系統(tǒng)長時間處于一個延遲的狀態(tài),而且系統(tǒng)中長時間沒有人訪問的數(shù)據(jù)一直存在內(nèi)存中不過期,浪費內(nèi)存。
但是,通過一些方案優(yōu)化處理,是可以最終一致性的。
針對刪除緩存異常的情況,可以使用 2 個方案避免:
-
刪除緩存重試策略(消息隊列) -
訂閱 binlog,再刪除緩存(Canal+消息隊列)
消息隊列方案
我們可以引入消息隊列,將第二個操作(刪除緩存)要操作的數(shù)據(jù)加入到消息隊列,由消費者來操作數(shù)據(jù)。
-
如果應用刪除緩存失敗,可以從消息隊列中重新讀取數(shù)據(jù),然后再次刪除緩存,這個就是重試機制。當然,如果重試超過的一定次數(shù),還是沒有成功,我們就需要向業(yè)務層發(fā)送報錯信息了。 -
如果刪除緩存成功,就要把數(shù)據(jù)從消息隊列中移除,避免重復操作,否則就繼續(xù)重試。
舉個例子,來說明重試機制的過程。
重試刪除緩存機制還可以,就是會造成好多業(yè)務代碼入侵。
訂閱 MySQL binlog,再操作緩存
「先更新數(shù)據(jù)庫,再刪緩存」的策略的第一步是更新數(shù)據(jù)庫,那么更新數(shù)據(jù)庫成功,就會產(chǎn)生一條變更日志,記錄在 binlog 里。
于是我們就可以通過訂閱 binlog 日志,拿到具體要操作的數(shù)據(jù),然后再執(zhí)行緩存刪除,阿里巴巴開源的 Canal 中間件就是基于這個實現(xiàn)的。
Canal 模擬 MySQL 主從復制的交互協(xié)議,把自己偽裝成一個 MySQL 的從節(jié)點,向 MySQL 主節(jié)點發(fā)送 dump 請求,MySQL 收到請求后,就會開始推送 Binlog 給 Canal,Canal 解析 Binlog 字節(jié)流之后,轉換為便于讀取的結構化數(shù)據(jù),供下游程序訂閱使用。
下圖是 Canal 的工作原理:
將binlog日志采集發(fā)送到MQ隊列里面,然后編寫一個簡單的緩存刪除消息者訂閱binlog日志,根據(jù)更新log刪除緩存,并且通過ACK機制確認處理這條更新log,保證數(shù)據(jù)緩存一致性
持久化方式有哪些?各有什么區(qū)別
Redis 的讀寫操作都是在內(nèi)存中,所以 Redis 性能才會高,但是當 Redis 重啟后,內(nèi)存中的數(shù)據(jù)就會丟失,那為了保證內(nèi)存中的數(shù)據(jù)不會丟失,Redis 實現(xiàn)了數(shù)據(jù)持久化的機制,這個機制會把數(shù)據(jù)存儲到磁盤,這樣在 Redis 重啟就能夠從磁盤中恢復原有的數(shù)據(jù)。Redis 共有三種數(shù)據(jù)持久化的方式:
-
AOF 日志:每執(zhí)行一條寫操作命令,就把該命令以追加的方式寫入到一個文件里; -
RDB 快照:將某一時刻的內(nèi)存數(shù)據(jù),以二進制的方式寫入磁盤; -
混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的優(yōu)點;
AOF 日志是如何實現(xiàn)的?
Redis 在執(zhí)行完一條寫操作命令后,就會把該命令以追加的方式寫入到一個文件里,然后 Redis 重啟時,會讀取該文件記錄的命令,然后逐一執(zhí)行命令的方式來進行數(shù)據(jù)恢復。
我這里以「_set name xiaolin_」命令作為例子,Redis 執(zhí)行了這條命令后,記錄在 AOF 日志里的內(nèi)容如下圖:
Redis 提供了 3 種寫回硬盤的策略, 在 Redis.conf 配置文件中的 appendfsync 配置項可以有以下 3 種參數(shù)可填:
-
Always,這個單詞的意思是「總是」,所以它的意思是每次寫操作命令執(zhí)行完后,同步將 AOF 日志數(shù)據(jù)寫回硬盤; -
Everysec,這個單詞的意思是「每秒」,所以它的意思是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),然后每隔一秒將緩沖區(qū)里的內(nèi)容寫回到硬盤; -
No,意味著不由 Redis 控制寫回硬盤的時機,轉交給操作系統(tǒng)控制寫回的時機,也就是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),再由操作系統(tǒng)決定何時將緩沖區(qū)內(nèi)容寫回硬盤。
我也把這 3 個寫回策略的優(yōu)缺點總結成了一張表格:
RDB 快照是如何實現(xiàn)的呢?
因為 AOF 日志記錄的是操作命令,不是實際的數(shù)據(jù),所以用 AOF 方法做故障恢復時,需要全量把日志都執(zhí)行一遍,一旦 AOF 日志非常多,勢必會造成 Redis 的恢復操作緩慢。
為了解決這個問題,Redis 增加了 RDB 快照。所謂的快照,就是記錄某一個瞬間東西,比如當我們給風景拍照時,那一個瞬間的畫面和信息就記錄到了一張照片。所以,RDB 快照就是記錄某一個瞬間的內(nèi)存數(shù)據(jù),記錄的是實際數(shù)據(jù),而 AOF 文件記錄的是命令操作的日志,而不是實際的數(shù)據(jù)。
因此在 Redis 恢復數(shù)據(jù)時, RDB 恢復數(shù)據(jù)的效率會比 AOF 高些,因為直接將 RDB 文件讀入內(nèi)存就可以,不需要像 AOF 那樣還需要額外執(zhí)行操作命令的步驟才能恢復數(shù)據(jù)。Redis 提供了兩個命令來生成 RDB 文件,分別是 save 和 bgsave,他們的區(qū)別就在于是否在「主線程」里執(zhí)行:
-
執(zhí)行了 save 命令,就會在主線程生成 RDB 文件,由于和執(zhí)行操作命令在同一個線程,所以如果寫入 RDB 文件的時間太長,會阻塞主線程; -
執(zhí)行了 bgsave 命令,會創(chuàng)建一個子進程來生成 RDB 文件,這樣可以避免主線程的阻塞;
混合持久化
在 Redis 4.0 提出了混合持久化。如果想要開啟混合持久化功能,可以在 Redis 配置文件將下面這個配置項設置成 yes:
aof-use-rdb-preamble yes
混合持久化是工作在 AOF 日志重寫過程。
當開啟了混合持久化時,在 AOF 重寫日志時,fork 出來的重寫子進程會先將與主線程共享的內(nèi)存數(shù)據(jù)以 RDB 方式寫入到 AOF 文件,然后主線程處理的操作命令會被記錄在重寫緩沖區(qū)里,重寫緩沖區(qū)里的增量命令會以 AOF 方式寫入到 AOF 文件,寫入完成后通知主進程將新的含有 RDB 格式和 AOF 格式的 AOF 文件替換舊的的 AOF 文件。
也就是說,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量數(shù)據(jù),后半部分是 AOF 格式的增量數(shù)據(jù)。
這樣的好處在于,重啟 Redis 加載數(shù)據(jù)的時候,由于前半部分是 RDB 內(nèi)容,這樣加載的時候速度會很快。
加載完 RDB 的內(nèi)容后,才會加載后半部分的 AOF 內(nèi)容,這里的內(nèi)容是 Redis 后臺子進程重寫 AOF 期間,主線程處理的操作命令,可以使得數(shù)據(jù)更少的丟失。
其他
-
如何看待米哈游公司? -
如何看待toB和toC? -
如果你有好幾個offer,你會看重公司什么?(內(nèi)心??) -
你的職業(yè)規(guī)劃是什么? -
無算法
推薦閱讀:
第一次面字節(jié),我賊緊張!