【備戰秋招】高質量 Java知識點整理3:Spring、MySQL

Spring 10

P1:Spring 框架

Spring 是分層的企業級應用輕量級開源框架,以 IoC 和 AOP爲內核。Spring 可以降低企業級應用開發的複雜性,對此主要採取了四個關鍵策略:基於 POJO 的輕量級和最小侵入性編程、通過依賴注入和麪向接口實現松耦合、基於切面和慣性進行聲明式編程、通過切面和模板減少樣板式代碼。

好處

降低代碼耦合度、簡化開發。通過 Spring 提供的 IoC 容器可以將對象間的依賴關係交由 Spring 進行控制,避免硬編碼所造成的過度程序耦合。用戶也不必再爲單例模式類、屬性文件解析等這些底層的需求編寫代碼,可以更專注於上層的應用。

AOP 編程以及聲明式事務的支持。通過 Spring 的 AOP 功能可以方便進行面向切面的編程,通過聲明式事務可以靈活進行事務管理,提高開發效率和質量。

方便程序的測試和集成各種框架。可以用非容器依賴的編程方式進行幾乎所有的測試工作,可以降低各種框架的使用難度,提供了對 Mybatis 和 Hibernate 等框架的直接支持。

降低了 JavaEE API 的使用難度。Spring 對 JDBC、JavaMail、遠程調用等 API 進行了封裝,使這些 API 的使用難度大幅降低。

核心容器

核心容器由 spring-beans、spring-core、spring-context 和 spring-expression 四個模塊組成。

spring-beans 和 spring-core 模塊是 Spring 的核心模塊,包括了控制反轉和依賴注入。BeanFactory 使用控制反轉對應用程序的配置和依賴性規範與實際的應用代碼進行分離,BeanFactory 實例化後並不會自動實例化 Bean,只有當 Bean 被使用時纔會對其進行實例化與依賴關係的裝配。

spring-context 模塊構架於核心模塊之上,擴展了 BeanFactory,爲它添加了 Bean 的生命週期控制、框架事件體系及資源透明化加載等功能。ApplicationConext 是該模塊的核心接口,它是 BeanFactory 的子接口,它實例化後會自動對所有單例 Bean 進行實例化與依賴關係的裝配,使之處於待用狀態。

spring-expression 是 EL 語言的擴展模塊,可以查詢、管理運行中的對象,同時也可以方便地調用對象方法,以及操作數組、集合等。


P2:IoC 控制反轉

IoC 即控制反轉,是一種給予應用程序中目標組件更多控制的設計範式,簡單來說就是把原來代碼裏需要實現的對象創建、依賴反轉給容器來幫忙實現,需要創建一個容器並且需要一種描述來讓容器知道要創建的對象之間的關係,在 Spring 框架中管理對象及其依賴關係是通過 Spring 的 IoC 容器實現的,IoC 的作用是降低代碼耦合度。

IoC 的實現方式有依賴注入和依賴查找,由於依賴查找使用的很少,因此 IoC 也叫做依賴注入。依賴注入指對象被動地接受依賴類而不用自己主動去找,對象不是從容器中查找它依賴的類,而是在容器實例化對象時主動將它依賴的類注入給它。假設一個 Car 類需要一個 Engine 的對象,那麼一般需要需要手動 new 一個 Engine,利用 IoC 就只需要定義一個私有的 Engine 類型的成員變量,容器會在運行時自動創建一個 Engine 的實例對象並將引用自動注入給成員變量。

基於 XML 的容器初始化

當創建一個 ClassPathXmlApplicationContext 時,構造器做了兩件事:首先調用父容器的構造器爲容器設置好 Bean 資源加載器,然後調用父類的 setConfigLocations 方法設置 Bean 配置信息的定位路徑。

ClassPathXmlApplicationContext 通過調用父類 AbstractApplicationContext 的 refresh 方法啓動整個 IoC 容器對 Bean 定義的載入過程,refresh 是一個模板方法,規定了 IoC 容器的啓動流程。refresh 方法的主要作用是:在創建 IoC 容器之前如果已有容器存在,需要把已有的容器銷燬和關閉,以保證在 refresh 方法之後使用的是新創建的 IoC 容器。

容器創建後通過 loadBeanDefinitions 方法加載 Bean 配置資源,該方法會做兩件事:首先調用資源加載器的方法獲取要加載的資源,其次真正執行加載功能,由子類 XmlBeanDefinitionReader 實現。在加載資源時,首先會解析配置文件路徑,讀取配置文件的內容,然後通過 XML 解析器將 Bean 配置信息轉換成文檔對象,之後再按照 Spring Bean 的定義規則對文檔對象進行解析。

Spring IoC 容器中註冊解析的 Bean 信息存放在一個 HashMap 集合中,key 是 String 字符串,值是 BeanDefinition,在註冊過程中需要使用 synchronized 同步塊保證線程安全。當 Bean 配置信息中配置的 Bean 被解析後且被註冊到 IoC 容器中,初始化就算真正完成了,Bean 定義信息已經可以使用,並且可以被檢索。Spring IoC 容器的作用就是對這些註冊的 Bean 定義信息進行處理和維護,註冊的 Bean 定義信息是控制反轉和依賴注入的基礎。

基於註解的容器初始化

Spring 對註解的處理分爲兩種方式:① 直接將註解 Bean 註冊到容器中,可以在初始化容器時註冊,也可以在容器創建之後手動註冊,然後刷新容器使其對註冊的註解 Bean 進行處理。② 通過掃描指定的包及其子包的所有類處理,在初始化註解容器時指定要自動掃描的路徑。


P3:DI 依賴注入

可注入的數據類型

基本數據類型和 String、集合類型、Bean 類型。

實現方式

構造器注入:IoC Service Provider 會檢查被注入對象的構造器,取得它所需要的依賴對象列表,進而爲其注入相應的對象。這種方法的優點是在對象構造完成後就處於就緒狀態,可以馬上使用。缺點是當依賴對象較多時,構造器的參數列表會比較長,構造器無法被繼承,無法設置默認值。對於非必需的依賴處理可能需要引入多個構造器,參數數量的變動可能會造成維護的困難。

setter 方法注入:當前對象只需要爲其依賴對象對應的屬性添加 setter 方法,就可以通過 setter 方法將依賴對象注入到被依賴對象中。setter 方法注入在描述性上要比構造器注入強,並且可以被繼承,允許設置默認值。缺點是無法在對象構造完成後馬上進入就緒狀態。

接口注入:必須實現某個接口,這個接口提供一個方法來爲其注入依賴對象。使用較少,因爲它強制要求被注入對象實現不必要的接口,侵入性強。

相關注解

@Autowired:自動按類型注入,如果有多個匹配則按照指定 Bean 的 id 查找,查找不到會報錯。

@Qualifier:在自動按照類型注入的基礎上再按照 Bean 的 id 注入,給變量注入時必須搭配 @Autowired,給方法注入時可單獨使用。

@Resource :直接按照 Bean 的 id 注入,只能注入 Bean 類型。

@Value :用於注入基本數據類型和 String 類型。

依賴注入的過程

getBean 方法是獲取 Bean 實例的方法,該方法會調用 doGetBean 方法,doGetBean 真正實現向 IoC 容器獲取 Bean 的功能,也是觸發依賴注入的地方。如果 Bean 定義爲單例模式,容器在創建之前先從緩存中查找以確保整個容器中只存在一個實例對象。如果 Bean 定義爲原型模式,則容器每次都會創建一個新的實例。

具體創建 Bean 實例對象的過程由 ObjectFactory 的 createBean 方法完成,該方法主要通過 createBeanInstance 方法生成 Bean 包含的 Java 對象實例和 populateBean 方法對 Bean 屬性的依賴注入進行處理。

在 createBeanInstance 方法中根據指定的初始化策略,通過簡單工廠、工廠方法或容器的自動裝配特性生成 Java 實例對象,對工廠方法和自動裝配特性的 Bean,調用相應的工廠方法或參數匹配的構造器即可完成實例化對象的工作,但最常用的默認無參構造器需要使用 JDK 的反射或 CGLib 來進行初始化。

在 populateBean 方法中,注入過程主要分爲兩種情況:① 屬性值類型不需要強制轉換時,不需要解析屬性值,直接進行依賴注入。② 屬性值類型需要強制轉換時,首先需要解析屬性值,然後對解析後的屬性值進行依賴注入。依賴注入的過程就是將 Bean 對象實例設置到它所依賴的 Bean 對象屬性上,真正的依賴注入是通過 setPropertyValues 方法實現的,該方法使用了委派模式。

BeanWrapperImpl 類負責對容器完成初始化的 Bean 實例對象進行屬性的依賴注入,對於非集合類型的屬性,大量使用 JDK 的反射機制,通過屬性的 getter 方法獲取指定屬性注入前的值,同時調用屬性的 setter 方法爲屬性設置注入後的值。對於集合類型的屬性,將屬性值解析爲目標類型的集合後直接賦值給屬性。

當 Spring IoC 容器對 Bean 定義資源的定位、載入、解析和依賴注入全部完成後,就不再需要我們手動創建所需的對象,Spring IoC 容器會自動爲我們創建對象並且注入好相關依賴。


P4:Bean 對象

生命週期

在 IoC 容器的初始化過程中會對 Bean 定義完成資源定位,加載讀取配置並解析,最後將解析的 Bean 信息放在一個 HashMap 集合中。當 IoC 容器初始化完成後,會進行對 Bean 實例的創建和依賴注入過程,注入對象依賴的各種屬性值,在初始化時可以指定自定義的初始化方法。經過這一系列初始化操作後 Bean 達到可用狀態,接下來就可以使用 Bean 了,當使用完成後會調用 destroy 方法進行銷燬,此時也可以指定自定義的銷燬方法,最終 Bean 被銷燬且從容器中移除。

指定 Bean 初始化和銷燬的方法:

XML 方式通過配置 bean 標籤中的 init-Method 和 destory-Method 指定自定義初始化和銷燬方法。

註解方式通過 @PreConstruct@PostConstruct 註解指定自定義初始化和銷燬方法。

作用範圍

通過 scope 屬性指定 bean 的作用範圍,包括:① singleton:單例模式,是默認作用域,不管收到多少 Bean 請求每個容器中只有一個唯一的 Bean 實例。② prototype:原型模式,和 singleton 相反,每次 Bean 請求都會創建一個新的實例。③ request:每次 HTTP 請求都會創建一個新的 Bean 並把它放到 request 域中,在請求完成後 Bean 會失效並被垃圾收集器回收。④ session:和 request 類似,確保每個 session 中有一個 Bean 實例,session 過期後 bean 會隨之失效。⑤ global session:當應用部署在 Portlet 容器中時,如果想讓所有 Portlet 共用全局存儲變量,那麼這個變量需要存儲在 global session 中。

創建方式

XML

通過默認無參構造器,只需要指明 bean 標籤中的 id 和 class 屬性,如果沒有無參構造器會報錯。

使用靜態工廠方法,通過 bean 標籤中的 class 屬性指明靜態工廠,factory-method 屬性指明靜態工廠方法。

使用實例工廠方法,通過 bean 標籤中的 factory-bean 屬性指明實例工廠,factory-method 屬性指明實例工廠方法。

註解

@Component 把當前類對象存入 Spring 容器中,相當於在 xml 中配置一個 bean 標籤。value 屬性指定 bean 的 id,默認使用當前類的首字母小寫的類名。

@Controller@Service@Repository 三個註解都是 @Component 的衍生註解,作用及屬性都是一模一樣的。只是提供了更加明確語義,@Controller 用於表現層,@Service用於業務層,@Repository用於持久層。如果註解中有且只有一個 value 屬性要賦值時可以省略 value。

如果想將第三方的類變成組件又沒有源代碼,也就沒辦法使用 @Component 進行自動配置,這種時候就要使用 @Bean 註解。被 @Bean 註解的方法返回值是一個對象,將會實例化,配置和初始化一個新對象並返回,這個對象由 Spring 的 IoC 容器管理。name 屬性用於給當前 @Bean 註解方法創建的對象指定一個名稱,即 bean 的 id。當使用註解配置方法時,如果方法有參數,Spring 會去容器查找是否有可用 bean對象,查找方式和 @Autowired 一樣。

@Configuration 用於指定當前類是一個 spring 配置類,當創建容器時會從該類上加載註解,value 屬性用於指定配置類的字節碼。

@ComponentScan 用於指定 Spring 在初始化容器時要掃描的包。basePackages 屬性用於指定要掃描的包。

@PropertySource 用於加載 .properties 文件中的配置。value 屬性用於指定文件位置,如果是在類路徑下需要加上 classpath。

@Import 用於導入其他配置類,在引入其他配置類時可以不用再寫 @Configuration 註解。有 @Import 的是父配置類,引入的是子配置類。value 屬性用於指定其他配置類的字節碼。

BeanFactory、FactoryBean 和 ApplicationContext 的區別

BeanFactory 是一個 Bean 工廠,實現了工廠模式,是 Spring IoC 容器最頂級的接口,可以理解爲含有 Bean 集合的工廠類,它的作用是管理 Bean,包括實例化、定位、配置應用程序中的對象及建立這些對象之間的依賴。BeanFactory 實例化後並不會自動實例化 Bean,只有當 Bean 被使用時纔會對其進行實例化與依賴關係的裝配,屬於延遲加載,適合多例模式。

FactoryBean 是一個工廠 Bean,作用是生產其他 Bean 實例,可以通過實現該接口,提供一個工廠方法來自定義實例化 Bean 的邏輯。

ApplicationConext 是 BeanFactory 的子接口,擴展了 BeanFactory 的功能,提供了支持國際化的文本消息,統一的資源文件讀取方式,事件傳播以及應用層的特別配置等。容器會在初始化時對配置的 Bean 進行預實例化,Bean 的依賴注入在容器初始化時就已經完成,屬於立即加載,適合單例模式,一般推薦使用 ApplicationContext。


P5:AOP 面向切面編程

概念和原理

AOP 即面向切面編程,簡單地說就是將代碼中重複的部分抽取出來,在需要執行的時候使用動態代理的技術,在不修改源碼的基礎上對方法進行增強。優點是可以減少代碼的冗餘,提高開發效率,維護方便。

Spring 會根據類是否實現了接口來判斷動態代理的方式,如果實現了接口會使用 JDK 的動態代理,核心是 InvocationHandler 接口和 Proxy 類,如果沒有實現接口會使用 CGLib 動態代理,CGLib 是在運行時動態生成某個類的子類,如果某一個類被標記爲 final,是不能使用 CGLib 動態代理的。

JDK 動態代理主要通過重組字節碼實現,首先獲得被代理對象的引用和所有接口,生成新的類必須實現被代理類的所有接口,動態生成Java 代碼後編譯新生成的 .class 文件並重新加載到 JVM 運行。JDK 代理直接寫 Class 字節碼,CGLib是採用ASM框架寫字節碼,生成代理類的效率低。但是CGLib調用方法的效率高,因爲 JDK 使用反射調用方法,CGLib 使用 FastClass 機制爲代理類和被代理類各生成一個類,這個類會爲代理類或被代理類的方法生成一個 index,這個 index 可以作爲參數直接定位要調用的方法。

常用場景包括權限認證、自動緩存、錯誤處理、日誌、調試和事務等。

相關注解

@Aspect:聲明被註解的類是一個切面 Bean。

@Before:前置通知,指在某個連接點之前執行的通知。

@After:後置通知,指某個連接點退出時執行的通知(不論正常返回還是異常退出)。

@AfterReturning:返回後通知,指某連接點正常完成之後執行的通知,返回值使用returning屬性接收。

@AfterThrowing:異常通知,指方法拋出異常導致退出時執行的通知,和@AfterReturning只會有一個執行,異常使用throwing屬性接收。

相關術語

Aspect:切面,一個關注點的模塊化,這個關注點可能會橫切多個對象。

Joinpoint:連接點,程序執行過程中的某一行爲,即業務層中的所有方法。。

Advice:通知,指切面對於某個連接點所產生的動作,包括前置通知、後置通知、返回後通知、異常通知和環繞通知。

Pointcut:切入點,指被攔截的連接點,切入點一定是連接點,但連接點不一定是切入點。

Proxy:代理,Spring AOP 中有 JDK 動態代理和 CGLib 代理,目標對象實現了接口時採用 JDK 動態代理,反之採用 CGLib 代理。

Target:代理的目標對象,指一個或多個切面所通知的對象。

Weaving :織入,指把增強應用到目標對象來創建代理對象的過程。

AOP 的過程

Spring AOP 是由 BeanPostProcessor 後置處理器開始的,這個後置處理器是一個監聽器,可以監聽容器觸發的 Bean 生命週期事件,向容器註冊後置處理器以後,容器中管理的 Bean 就具備了接收 IoC 容器回調事件的能力。BeanPostProcessor 的調用發生在 Spring IoC 容器完成 Bean 實例對象的創建和屬性的依賴注入之後,爲 Bean 對象添加後置處理器的入口是 initializeBean 方法。

Spring 中 JDK 動態代理生通過 JdkDynamicAopProxy 調用 Proxy 的 newInstance 方法來生成代理類,JdkDynamicAopProxy 也實現了 InvocationHandler 接口,invoke 方法的具體邏輯是先獲取應用到此方法上的攔截器鏈,如果有攔截器則創建 MethodInvocation 並調用其 proceed 方法,否則直接反射調用目標方法。因此 Spring AOP 對目標對象的增強是通過攔截器實現的。


P6:Spring MVC 核心組件

DispatcherServlet:SpringMVC 中的前端控制器,是整個流程控制的核心,負責接收請求並轉發給對應的處理組件。

Handler:處理器,完成具體業務邏輯,相當於 Servlet 或 Action。

HandlerMapping:完成URL 到 Controller映射的組件,DispatcherServlet 接收到請求之後,通過 HandlerMapping 將不同的請求映射到不同的 Handler。

HandlerInterceptor:處理器攔截器,是一個接口,如果需要完成一些攔截處理,可以實現該接口。

HandlerExecutionChain:處理器執行鏈,包括兩部分內容:Handler 和 HandlerInterceptor。

HandlerAdapter:處理器適配器,Handler執行業務方法前需要進行一系列操作,包括表單數據驗證、數據類型轉換、將表單數據封裝到JavaBean等,這些操作都由 HandlerAdapter 完成。DispatcherServlet 通過 HandlerAdapter 來執行不同的 Handler。

ModelAndView:裝載了模型數據和視圖信息,作爲 Handler 的處理結果返回給 DispatcherServlet。

ViewResolver:視圖解析器,DispatcherServlet 通過它將邏輯視圖解析爲物理視圖,最終將渲染的結果響應給客戶端。


P7:Spring MVC 處理流程

Web 容器啓動時會通知 Spring 初始化容器,加載 Bean 的定義信息並初始化所有單例 Bean,然後遍歷容器中的 Bean,獲取每一個 Controller 中的所有方法訪問的 URL,將 URL 和對應的 Controller 保存到一個 Map 集合中。

所有的請求會轉發給 DispatcherServlet 前端處理器處理,DispatcherServlet 會請求 HandlerMapping 找出容器中被 @Controler 註解修飾的 Bean 以及被 @RequestMapping 修飾的方法和類,生成 Handler 和 HandlerInterceptor 並以一個 HandlerExcutionChain 處理器執行鏈的形式返回。

之後 DispatcherServlet 使用 Handler 找到對應的 HandlerApapter,通過 HandlerApapter 調用 Handler 的方法,將請求參數綁定到方法的形參上,執行方法處理請求並得到 ModelAndView。

最後 DispatcherServlet 根據使用 ViewResolver 試圖解析器對得到的 ModelAndView 邏輯視圖進行解析得到 View 物理視圖,然後對視圖渲染,將數據填充到視圖中並返回給客戶端。

註解

@Controller:在類定義處添加,將類交給IoC容器管理。

@RequtestMapping:將URL請求和業務方法映射起來,在類和方法定義上都可以添加該註解。value 屬性指定URL請求的實際地址,是默認值。method 屬性限制請求的方法類型,包括GET、POST、PUT、DELETE等。如果沒有使用指定的請求方法請求URL,會報405 Method Not Allowed 錯誤。params 屬性限制必須提供的參數,如果沒有會報錯。

@RequestParam:如果 Controller 方法的形參和 URL 參數名一致可以不添加註解,如果不一致可以使用該註解綁定。value 屬性表示HTTP請求中的參數名。required 屬性設置參數是否必要,默認false。defaultValue 屬性指定沒有給參數賦值時的默認值。

@PathVariable:Spring MVC 也支持 RESTful 風格的 URL,通過 @PathVariable 完成請求參數與形參的綁定。


P8:Spring Data JPA 框架

Spring Data JPA 是 Spring 基於 ORM 框架、JPA 規範的基礎上封裝的一套 JPA 應用框架,可使開發者用極簡的代碼實現對數據庫的訪問和操作。它提供了包括增刪改查等在內的常用功能,且易於擴展,可以極大提高開發效率。

ORM 即 Object-Relational Mapping ,表示對象關係映射,映射的不只是對象的值還有對象之間的關係,通過 ORM 就可以把對象映射到關係型數據庫中。操作實體類就相當於操作數據庫表,可以不再重點關注 SQL 語句。

使用時只需要持久層接口繼承 JpaRepository 即可,泛型參數列表中第一個參數是實體類類型,第二個參數是主鍵類型。運行時通過 JdkDynamicAopProxyinvoke 方法創建了一個動態代理對象 SimpleJpaRepositorySimpleJpaRepository 中封裝了 JPA 的操作,通過 hibernate(封裝了JDBC)完成數據庫操作。

註解

@Entity:表明當前類是一個實體類。

@Table :關聯實體類和數據庫表。

@Column :關聯實體類屬性和數據庫表中字段。

@Id :聲明當前屬性爲數據庫表主鍵對應的屬性。

@GeneratedValue: 配置主鍵生成策略。

@OneToMany :配置一對多關係,mappedBy 屬性值爲主表實體類在從表實體類中對應的屬性名。

@ManyToOne :配置多對一關係,targetEntity 屬性值爲主表對應實體類的字節碼。

@JoinColumn:配置外鍵關係,name 屬性值爲外鍵名稱,referencedColumnName 屬性值爲主表主鍵名稱。

對象導航查詢

通過 get 方法查詢一個對象的同時,通過此對象可以查詢它的關聯對象。

對象導航查詢一到多默認使用延遲加載的形式, 關聯對象是集合,因此使用立即加載可能浪費資源。

對象導航查詢多到一默認使用立即加載的形式, 關聯對象是一個對象,因此使用立即加載。

如果要改變加載方式,在實體類註解配置加上 fetch 屬性即可,LAZY 表示延遲加載,EAGER 表示立即加載。


P9:Mybatis 框架

Mybatis 是一個實現了數據持久化的 ORM 框架,簡單理解就是對 JDBC 進行了封裝。

優點

相比 JDBC 減少了大量代碼量,減少冗餘代碼。

使用靈活,SQL 語句寫在 XML 裏,從程序代碼中徹底分離,降低了耦合度,便於管理。

提供 XML 標籤,支持編寫動態 SQL 語句。

提供映射標籤,支持對象與數據庫的 ORM 字段映射關係。

缺點

SQL 語句編寫工作量較大,尤其是字段和關聯表多時。

SQL 語句依賴於數據庫,導致數據庫移植性差,不能隨意更換數據庫。

映射文件標籤

selectinsertupdatedelete 標籤分別對應查詢、添加、更新、刪除操作。

parameterType 屬性表示參數的數據類型,包括基本數據類型和對應的包裝類型、String 和 Java Bean 類型,當有多個參數時可以使用 #{argn} 的形式表示第 n 個參數。除了基本數據類型都要以全限定類名的形式指定參數類型。

resultType 表示返回的結果類型,包括基本數據類型和對應的包裝類型、String 和 Java Bean 類型。還可以使用把返回結果封裝爲複雜類型的 resultMap

緩存

使用緩存可以減少程序和數據庫交互的次數,從而提高程序的運行效率。第一次查詢後會自動將結果保存到緩存中,下一次查詢時直接從緩存中返回結果無需再次查詢數據庫。

  • 一級緩存

    SqlSession 級別,默認開啓且不能關閉。

    操作數據庫時需要創建 SqlSession 對象,在對象中有一個 HashMap 用於存儲緩存數據,不同 SqlSession 之間緩存數據區域互不影響。

    一級緩存的作用域是 SqlSession 範圍的,在同一個 SqlSession 中執行兩次相同的 SQL 語句時,第一次執行完畢會將結果保存在緩存中,第二次查詢直接從緩存中獲取。

    如果 SqlSession 執行了 DML 操作(insert、update、delete),Mybatis 必須將緩存清空以保證數據的有效性。

  • 二級緩存

    Mapper 級別,默認關閉。

    使用二級緩存時多個 SqlSession 使用同一個 Mapper 的 SQL 語句操作數據庫,得到的數據會存在二級緩存區,同樣使用 HashMap 進行數據存儲,相比於一級緩存,二級緩存範圍更大,多個 SqlSession 可以共用二級緩存,作用域是 Mapper 的同一個 namespace,不同 SqlSession 兩次執行相同的 namespace 下的 SQL 語句,參數也相等,則第一次執行成功後會將數據保存在二級緩存中,第二次可直接從二級緩存中取出數據。

    要使用二級緩存,先在在全局配置文件中配置:

    <!-- 開啓二級緩存 -->
    <setting name="cacheEnabled" value="true"/>
    

    再在對應的映射文件中配置一個 cache 標籤即可。

    <cache/>
    

P10:Spring Cloud 框架

單體應用存在的問題

隨着業務發展,開發越來越複雜。

修改、新增某個功能,需要對整個系統進行測試、重新部署。

一個模塊出現問題,可能導致整個系統崩潰。

多個開發團隊同時對數據進行管理,容易產生安全漏洞。

各個模塊使用同一種技術開發,各個模塊很難根據實際情況選擇更合適的技術框架,侷限性很大。

分佈式和集羣的區別

集羣:一臺服務器無法負荷高併發的數據訪問量,就設置多臺服務器一起分擔壓力,是在物理層面解決問題。

分佈式:將一個複雜的問題拆分成若干簡單的小問題,將一個大型的項目架構拆分成若干個微服務來協同完成,在軟件設計層面解決問題。

微服務的優點

各個服務的開發、測試、部署都相互獨立,用戶服務可以拆分爲獨立服務,如果用戶量很大,可以很容易對其實現負載。

當新需求出現時,使用微服務不再需要考慮各方面的問題,例如兼容性、影響度等。

使用微服務拆分項目後,各個服務之間消除了很多限制,只需要保證對外提供的接口正常可用,而不限制語言和框架等選擇。

服務治理 Eureka

服務治理的核心由三部分組成:服務提供者服務消費者註冊中心

服務註冊:在分佈式系統架構中,每個微服務在啓動時,將自己的信息存儲在註冊中心。

服務發現:服務消費者從註冊中心獲取服務提供者的網絡信息,通過該信息調用服務。

Spring Cloud 的服務治理使用 Eureka 實現,Eureka 是 Netflix 開源的基於 REST 的服務治理解決方案,Spring Cloud 集成了 Eureka,提供服務註冊和服務發現的功能,可以和基於 Spring Boot 搭建的微服務應用輕鬆完成整合,將 Eureka 二次封裝爲 Spring Cloud Eureka。Eureka Server 是註冊中心,所有要進行註冊的微服務通過 Eureka Client 連接到 Eureka Server 完成註冊。

服務網關 Zuul

Spring Cloud 集成了 Zuul 組件,實現服務網關。Zuul 是 Netflix 提供的一個開源的 API 網關服務器,是客戶端和網站後端所有請求的中間層,對外開放一個 API,將所有請求導入統一的入口,屏蔽了服務端的具體實現邏輯,可以實現方向代理功能,在網關內部實現動態路由、身份認證、IP過濾、數據監控等。

負載均衡 Ribbon

Spring Cloud Ribbon 是一個負載均衡的解決方案,Ribbon 是 Netflix 發佈的均衡負載器,Spring Cloud Ribbon是基於 Netflix Ribbon 實現的,是一個用於對 HTTP 請求進行控制的負載均衡客戶端。

在註冊中心對 Ribbon 進行註冊之後,Ribbon 就可以基於某種負載均衡算法(輪循、隨機、加權輪詢、加權隨機等)自動幫助服務消費者調用接口,開發者也可以根據具體需求自定義 Ribbon 負載均衡算法。實際開發中 Spring Clooud Ribbon 需要結合 Spring Cloud Eureka 使用,Eureka 提供所有可以調用的服務提供者列表,Ribbon 基於特定的負載均衡算法從這些服務提供者中選擇要調用的具體實例。

聲明式接口調用 Feign

Feign 與 Ribbon 一樣也是 Netflix 提供的,Feign 是一個聲明式、模板化的 Web Service 客戶端,簡化了開發者編寫 Web 服務客戶端的操作,開發者可以通過簡單的接口和註解來調用 HTTP API,Spring Cloud Feign 整合了 Ribbon 和 Hystrix,具有可插拔、基於註解、負載均衡、服務熔斷等一系列功能。

相比於 Ribbon + RestTemplate 的方式,Feign 可以大大簡化代碼開發,支持多種註解,包括 Feign 註解、JAX-RS 註解、Spring MVC 註解等。RestTemplate 是 Spring 框架提供的基於 REST 的服務組件,底層是對 HTTP 請求及響應進行了封裝,提供了很多訪問 REST 服務的方法,可以簡化代碼開發。

服務熔斷 Hystrix

熔斷器的作用是在不改變各個微服務調用關係的前提下,針對錯誤情況進行預先處理。

設計原則:服務隔離機制、服務降級機制、熔斷機制、提供實時監控和報警功能和提供實時配置修改功能

Hystrix 數據監控需要結合 Spring Boot Actuator 使用,Actuator 提供了對服務的數據監控、數據統計,可以通過 hystirx-stream 節點獲取監控的請求數據,同時提供了可視化監控界面。

服務配置 Config

Spring Cloud Config 通過服務端可以爲多個客戶端提供配置服務,既可以將配置文件存儲在本地,也可以將配置文件存儲在遠程的 Git 倉庫,創建 Config Server,通過它管理所有的配置文件。

服務跟蹤 Zipkin

Spring Cloud Zipkin 是一個可以採集並跟蹤分佈式系統中請求數據的組件,讓開發者更直觀地監控到請求在各個微服務耗費的時間,Zipkin 包括兩部分 Zipkin Server 和 Zipkin Client。


MySQL 15

P1:邏輯架構

第一層是服務器層,主要提供連接處理、授權認證、安全等功能,該層的服務不是 MySQL 獨有的,大多數基於網絡的 C/S 服務都有類似架構。

第二層實現了 MySQL 核心服務功能,包括查詢解析、分析、優化、緩存以及日期、時間等所有內置函數,所有跨存儲引擎的功能都在這一層實現,例如存儲過程、觸發器、視圖等。

第三層是存儲引擎層,存儲引擎負責 MySQL 中數據的存儲和提取。服務器通過API 與存儲引擎通信,這些接口屏蔽了不同存儲引擎的差異,使得差異對上層查詢過程透明。除了會解析外鍵定義的 InnoDB 外,存儲引擎不會解析SQL,不同存儲引擎之間也不會相互通信,只是簡單響應上層服務器請求。


P2:鎖

當有多個查詢需要在同一時刻修改數據時就會產生併發控制的問題,MySQL 在兩個層面進行併發控制:服務器層與存儲引擎層。

讀寫鎖

在處理併發讀或寫時,可以通過實現一個由兩種類型組成的鎖系統來解決問題。這兩種類型的鎖通常被稱爲共享鎖和排它鎖,也叫讀鎖和寫鎖。讀鎖是共享的,或者說相互不阻塞的,多個客戶在同一時刻可以同時讀取同一個資源而不相互干擾。寫鎖則是排他的,也就是說一個寫鎖會阻塞其他的寫鎖和讀鎖,只有如此才能確保在給定時間內只有一個用戶能執行寫入並防止其他用戶讀取正在寫入的同一資源。在實際的數據庫系統中,每時每刻都在發生鎖定,當某個用戶在修改某一部分數據時,MySQL 會通過鎖定防止其他用戶讀取同一數據。寫鎖比讀鎖有更高的優先級,一個寫鎖請求可能會被插入到讀鎖隊列的前面,但是讀鎖不能插入到寫鎖前面。

鎖策略

一種提高共享資源併發性的方法就是讓鎖定對象更有選擇性,儘量只鎖定需要修改的部分數據而不是所有資源,更理想的方式是隻對會修改的數據進行精確鎖定。任何時刻在給定的資源上,鎖定的數據量越少,系統的併發程度就越高,只要不發生衝突即可。

鎖策略就是在鎖的開銷和數據安全性之間尋求平衡,這種平衡也會影響性能。大多數商業數據庫系統沒有提供更多選擇,一般都是在表上加行鎖,而 MySQL 提供了多種選擇,每種MySQL存儲引擎都可以實現自己的鎖策略和鎖粒度。MySQL最重要的兩種鎖策略是:

  • 表鎖是MySQL中最基本的鎖策略,並且是開銷最小的策略。表鎖會鎖定整張表,一個用戶在對錶進行寫操作前需要先獲得寫鎖,這會阻塞其他用戶對該表的所有讀寫操作。只有沒有寫鎖時,其他讀取的用戶才能獲取讀鎖,讀鎖之間不相互阻塞。

  • 行鎖可以最大程度地支持併發處理,同時也帶來了最大的鎖開銷。InnoDB 和 XtraDB 以及一些其他存儲引擎實現了行鎖。行鎖只在存儲引擎層實現,而服務器層沒有實現。

死鎖

死鎖是指兩個或者多個事務在同一資源上相互佔用並請求鎖定對方佔用的資源,從而導致惡性循環的現象。當多個事務試圖以不同順序鎖定資源時就可能會產生死鎖,多個事務同時鎖定同一個資源時也會產生死鎖。

爲了解決死鎖問題,數據庫系統實現了各種死鎖檢測和死鎖超時機制。越複雜的系統,例如InnoDB 存儲引擎,越能檢測到死鎖的循環依賴,並立即返回一個錯誤。這種解決方式很有效,否則死鎖會導致出現非常慢的查詢。還有一種解決方法,就是當查詢的時間達到鎖等待超時的設定後放棄鎖請求,這種方式通常來說不太好。InnoDB 目前處理死鎖的方法是將持有最少行級排它鎖的事務進行回滾。

鎖的行爲與順序是和存儲引擎相關的,以同樣的順序執行語句,有些存儲引擎會產生死鎖有些則不會。死鎖的產生有雙重原因:有些是真正的數據衝突,這種情況很難避免,有些則完全是由於存儲引擎的實現方式導致的。

死鎖發生之後,只有部分或者完全回滾其中一個事務,才能打破死鎖。對於事務型系統這是無法避免的,所以應用程序在設計時必須考慮如何處理死鎖。大多數情況下只需要重新執行因死鎖回滾的事務即可。


P3:事務

事務就是一組原子性的 SQL 查詢,或者說一個獨立的工作單元。如果數據庫引擎能夠成功地對數據庫應用該組查詢的全部語句,那麼就執行該組查詢。如果其中有任何一條語句因爲崩潰或其他原因無法執行,那麼所有的語句都不會執行。也就是說事務內的語句要麼全部執行成功,要麼全部執行失敗。

ACID 特性

一個運行良好的事務處理系統必須具備 ACID 特性,實現了 ACID 的數據庫需要更強的CPU處理能力、更大的內存和磁盤空間。

  • 原子性 atomicity

    一個事務在邏輯上是必須不可分割的最小工作單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗回滾,對於一個事務來說不可能只執行其中的一部分。

  • 一致性 consistency

    數據庫總是從一個一致性的狀態轉換到另一個一致性的狀態。

  • 隔離性 isolation

    針對併發事務而言,隔離性就是要隔離併發運行的多個事務之間的相互影響,一般來說一個事務所做的修改在最終提交以前,對其他事務是不可見的。

  • 持久性 durability

    一旦事務提交成功,其修改就會永久保存到數據庫中,此時即使系統崩潰,修改的數據也不會丟失。

隔離級別

在 SQL 標準中定義了四種隔離級別,每一種隔離級別都規定了一個事務中所做的修改,哪些在事務內和事務間是可見的,哪些是不可見的。較低級別的隔離通常可以執行更高的併發,系統的開銷也更低。

  • 未提交讀 READ UNCOMMITTED

    在該級別事務中的修改即使沒有被提交,對其他事務也是可見的。事務可以讀取其他事務修改完但未提交的數據,這種問題稱爲髒讀。這個級別還會導致不可重複讀和幻讀,從性能上說也沒有比其他級別好很多,因此很少使用。

  • 提交讀 READ COMMITTED

    大多數數據庫系統默認的隔離級別就是提交讀,但 MySQL 不是。提交讀滿足了隔離性的簡單定義:一個事務開始時只能"看見"已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前的任何修改對其他事務都是不可見的。這個級別有時也叫不可重複讀,因爲兩次執行同樣的查詢可能會得到不同結果。提交讀存在不可重複讀和幻讀的問題。

  • 可重複讀 REPEATABLE READ(MySQL默認的隔離級別)

    可重複讀解決了不可重複讀的問題,該級別保證了在同一個事務中多次讀取同樣的記錄結果是一致的。但可重複讀隔離級別還是無法解決幻讀的問題,所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,會產生幻行。InnoDB 存儲引擎通過多版本併發控制MVCC 解決幻讀的問題。

  • 可串行化 SERIALIZABLE

    該級別是最高的隔離級別,通過強制事務串行執行,避免了幻讀的問題。可串行化會在讀取的每一行數據上都加鎖,可能導致大量的超時和鎖爭用的問題。實際應用中很少用到這個隔離級別,只有非常需要確保數據一致性且可以接受沒有併發的情況下才考慮該級別。

MySQL 中的事務

MySQL 提供了兩種事務型的存儲引擎:InnoDB 和 NDB Cluster。

MySQL 事務默認採用自動提交模式,如果不是顯式地開始一個事務,則每個查詢都將被當作一個事務執行提交操作。在當前連接中,可以通過設置 AUTOCOMMIT 變量來啓用或禁用自動提交模式。

1 或 ON 表示啓用,0 或 OFF表示禁用,當禁用自動提交時,所有的查詢都是在一個事務中,直到顯式地執行 COMMIT 或 ROLLBACK 後該事務纔會結束,同時又開始了一個新事務。修改 AUTOCOMMIT 對非事務型表,例如 MyISAM 或內存表不會有任何影響,對這類表來說沒有 COMMIT 或 ROLLBACK 的概念,也可以理解爲一直處於啓用自動提交的模式

有一些命令在執行之前會強制執行提交當前的活動事務,例如ALTER TABLELOCK TABLES等。

MySQL能夠識別所有的 4個 ANSI 隔離級別,InnoDB 引擎也支持所有隔離級別。


P4:MVCC 多版本併發控制

可以認爲 MVCC 是行級鎖的一個變種,但它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現機制有所不同,但大都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。

MVCC 的實現,是通過保存數據在某個時間點的快照來實現的。也就是說不管需要執行多長時間,每個事務看到的數據都是一致的。根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的數據可能是不一樣的。

不同的存儲引擎的 MVCC 實現是不同的,典型的有樂觀併發控制和悲觀併發控制。

InnoDB 的 MVCC 實現

InnoDB 的MVCC 通過在每行記錄後面保存兩個隱藏的列來實現,這兩個列一個保存了行的創建時間,一個保存行的過期時間間。不過存儲的不是實際的時間值而是系統版本號,每開始一個新的事務系統版本號都會自動遞增,事務開始時刻的系統版本號會作爲事務的版本號,用來和查詢到的每行記錄的版本號進行比較。

REPEATABLE READ 級別下 MVCC 的具體實現

SELECT:InnoDB 會根據以下兩個條件檢查每行記錄:

  • 只查找版本早於當前事務版本的數據行,可以確保事務讀取的行要麼是事務開始前已經存在的,要麼是事物自身插入或修改過的。

  • 行的刪除版本要麼未定義,要麼大於當前事務版本號,可以確保事務讀取到的行在事務開始前未被刪除。

INSERT :爲新插入的每一行保存當前系統版本號作爲行版本號。

DELETE:爲刪除的每一行保存當前系統版本號作爲行刪除標識。

UPDATE:爲插入的每一行新記錄保存當前系統版本號作爲行版本號,同時保存當前系統版本號到原來的行作爲行刪除標識。

保存這兩個額外系統版本號使大多數讀操作都可以不用加鎖。這樣設計使讀數據操作簡單且高效,並且能保證只會讀取到符合標準的行。不足之處是每行記錄都需要額外存儲空間,需要做更多行檢查工作以及一些額外維護工作。

MVCC 只能在 READ COMMITTEDREPEATABLE READ 兩個隔離級別下工作,因爲 READ UNCOMMITTED 總是讀取最新的數據行,而不是符合當前事務版本的數據行,而 SERIALIZABLE 則會對所有讀取的行都加鎖。


P5:InnoDB 存儲引擎

InnoDB 是 MySQL 的默認事務型引擎,它被設計用來處理大量的短期事務。InnoDB 的性能和自動崩潰恢復特性,使得它在非事務型存儲需求中也很流行,除非有特別原因否則應該優先考慮 InnoDB 引擎。

InnoDB 的數據存儲在表空間中,表空間由一系列數據文件組成。MySQL4.1 後 InnoDB 可以將每個表的數據和索引放在單獨的文件中。

InnoDB 採用 MVCC 來支持高併發,並且實現了四個標準的隔離級別。其默認級別是 REPEATABLE READ,並且通過間隙鎖策略防止幻讀,間隙鎖使 InnoDB 不僅僅鎖定查詢涉及的行,還會對索引中的間隙進行鎖定防止幻行的插入。

InnoDB 表是基於聚簇索引建立的,InnoDB 的索引結構和其他存儲引擎有很大不同,聚簇索引對主鍵查詢有很高的性能,不過它的二級索引中必須包含主鍵列,所以如果主鍵很大的話其他所有索引都會很大,因此如果表上索引較多的話主鍵應當儘可能小。

InnoDB 的存儲格式是平臺獨立的,可以將數據和索引文件從一個平臺複製到另一個平臺。

InnoDB 內部做了很多優化,包括從磁盤讀取數據時採用的可預測性預讀,能夠自動在內存中創建加速讀操作的自適應哈希索引,以及能夠加速插入操作的插入緩衝區等。

選擇合適的存儲引擎

MySQL5.5 將 InnoDB 作爲默認存儲引擎,除非需要用到某些 InnoDB 不具備的特性,並且沒有其他方法可以代替,否則都應該優先選用InnoDB。

如果應用需要事務支持,那麼 InnoDB 是目前最穩定並且經過驗證的選擇。如果不需要事務並且主要是 SELECT 和 INSERT 操作,那麼MyISAM 是不錯的選擇。相對而言,MyISAM 崩潰後發生損壞的概率要比 InnoDB 大很多而且恢復速度也要慢,因此即使不需要事務支持,也可以選擇InnoDB。

如果可以定期地關閉服務器來執行備份,那麼備份的因素可以忽略。反之如果需要在線熱備份,那麼 InnoDB 就是基本的要求。


P6:MyISAM 存儲引擎

在 MySQL5.1及之前,MyISAM 是默認的存儲引擎,MyISAM 提供了大量的特性,包括全文索引、壓縮、空間函數等,但不支持事務和行鎖,最大的缺陷就是崩潰後無法安全恢復。對於只讀的數據或者表比較小、可以忍受修復操作的情況仍然可以使用 MyISAM。

MyISAM 將表存儲在數據文件和索引文件中,分別以 .MYD.MYI 作爲擴展名。MyISAM 表可以包含動態或者靜態行,MySQL 會根據表的定義決定行格式。MyISAM 表可以存儲的行記錄數一般受限於可用磁盤空間或者操作系統中單個文件的最大尺寸。

MyISAM 對整張表進行加鎖,讀取時會對需要讀到的所有表加共享鎖,寫入時則對錶加排它鎖。但是在表有讀取查詢的同時,也支持併發往表中插入新的記錄。

對於MyISAM 表,MySQL 可以手動或自動執行檢查和修復操作,這裏的修復和事務恢復以及崩潰恢復的概念不同。執行表的修復可能導致一些數據丟失,而且修復操作很慢。

對於 MyISAM 表,即使是 BLOB 和 TEXT 等長字段,也可以基於其前 500 個字符創建索引。MyISAM 也支持全文索引,這是一種基於分詞創建的索引,可以支持複雜的查詢。

創建 MyISAM 表時如果指定了 DELAY_KEY_WRITE 選項,在每次修改執行完成時不會立刻將修改的索引數據寫入磁盤,而是會寫到內存中的鍵緩衝區,只有在清理緩衝區或關閉表的時候纔會將對應的索引庫寫入磁盤。這種方式可以極大提升寫性能,但在數據庫或主機崩潰時會造成索引損壞,需要執行修復。延遲更新索引鍵的特性可以在全局設置也可以單個表設置。

MyISAM 設計簡單,數據以緊密格式存儲,所以在某些場景下性能很好。MyISAM 最典型的性能問題還是表鎖問題,如果所有的查詢長期處於 Locked 狀態,那麼原因毫無疑問就是表鎖。

P7:Memory 存儲引擎

如果需要快速訪問數據,並且這些數據不會被修改,重啓以後丟失也沒有關係,那麼使用 Memory 表是非常有用的。Memory 表至少要比 MyISAM 錶快一個數量級,因爲所有的數據都保存在內存中,不需要進行磁盤 IO,Memory 表的結構在重啓以後還會保留,但數據會丟失。

Memory 表適合的場景:查找或者映射表、緩存週期性聚合數據的結果、保存數據分析中產生的中間數據。

Memory 表支持哈希索引,因此查找速度極快。雖然速度很快但還是無法取代傳統的基於磁盤的表,Memory 表使用表級鎖,因此併發寫入的性能較低。它不支持 BLOB 和 TEXT 類型的列,並且每行的長度是固定的,所以即使指定了 VARCHAR 列,實際存儲時也會轉換成CHAR,這可能導致部分內存的浪費。

如果 MySQL 在執行查詢的過程中需要使用臨時表來保持中間結果,內部使用的臨時表就是 Memory 表。如果中間結果太大超出了Memory 表的限制,或者含有 BLOB 或 TEXT 字段,臨時表會轉換成 MyISAM 表。


P8:數據類型

整數類型

如果存儲整數可以使用這幾種整數類型:TINYINT、SMALLINT、MEDIUMINT、INT,BIGINT,它們分別使用8、16、24、32、64 位存儲空間。

整數類型有可選的 UNSIGNED 屬性,表示不允許負值,可以使整數的上限提高一倍。有符號和無符號類型使用相同的存儲空間並具有相同的性能,可以根據實際情況選擇合適的類型。

MySQL 可以爲整數類型指定寬度,例如 INT(11),這對大多數應用沒有意義,不會限制值的範圍,只是規定了 MySQL 的交互工具顯示字符的個數,對於存儲和計算來說 INT(1) 和 INT(11) 是相同的。

實數類型

實數是帶有小數部分的數字,但它們不只是爲了存儲小數,也可以使用 DECIMAL 存儲比 BIGINT 還大的整數。MySQL既支持精確類型,也支持不精確類型。

FLOAT 和 DOUBLE 支持使用標準的浮點運算進行近似運算,DECIMAL 用於存儲精確的小數。

浮點類型在存儲同樣範圍的值時,通常比 DECIMAL 使用更少的空間。FLOAT 使用 4 字節存儲,DOUBLE 佔用8字節,MySQL 內部使用DOUBLE 作爲內部浮點計算的類型。

因爲需要額外空間和計算開銷,所以應當儘量只在對小數進行精確計算時才使用 DECIMAL。在數據量較大時可以考慮 BIGINT 代替DECIMAL,將需要存儲的貨幣單位根據小數的位數乘以相應的倍數即可。假設要存儲的數據精確到萬分之一分,則可以把所有金額乘以一百萬將結果存儲在 BIGINT 中,這樣可以同時避免浮點存儲計算不精確和 DECIMAL 精確計算代價高的問題。

VARCHAR

VARCHAR 用於存儲可變字符串,是最常見的字符串數據類型。它比定長字符串更節省空間,因爲它僅使用必要的空間。VARCHAR 需要 1或 2 個額外字節記錄字符串長度,如果列的最大長度不大於 255 字節則只需要1 字節。VARCHAR 不會刪除末尾空格。

VARCHAR 節省了存儲空間,但由於行是變長的,在 UPDATE 時可能使行變得比原來更長,這就導致需要做額外的工作。如果一個行佔用的空間增長並且頁內沒有更多的空間可以存儲,這種情況下不同存儲引擎處理不同,InnoDB 會分裂頁而 MyISAM 會將行拆分成不同片。

適用場景:字符串列的最大長度比平均長度大很多、列的更新很少、使用了 UTF8 這種複雜字符集,每個字符都使用不同的字節數存儲。

InnoDB 可以把過長的 VARCHAR 存儲爲 BLOB。

CHAR

CHAR 是定長的,根據定義的字符串長度分配足夠的空間。CHAR 會刪除末尾空格。

CHAR 適合存儲很短的字符串,或所有值都接近同一個長度,例如存儲密碼的 MD5 值。對於經常變更的數據,CHAR 也比 VARCHAR更好,因爲定長的 CHAR 不容易產生碎片。對於非常短的列,CHAR 在存儲空間上也更有效率,例如用 CHAR 來存儲只有 Y 和 N 的值只需要一個字節,但是 VARCHAR 需要兩個字節,因爲還有一個記錄長度的額外字節。

BLOB 和 TEXT 類型

BLOB 和TEXT 都是爲了存儲大數據而設計的字符串數據類型,分別採用二進制和字符串方式存儲。MySQL會把每個 BLOB 和 TEXT 值當作一個獨立的對象處理,存儲引擎在存儲時通常會做特殊處理。當值太大時,InnoDB 會使用專門的外部存儲區來進行存儲。BLOB 和TEXT 僅有的不同是 BLOB 存儲的是二進制數據,沒有排序規則或字符集,而 TEXT 有字符集和排序規則。

MySQL 對 BLOB 和TEXT 列進行排序與其他類型不同:它只對每個列最前 max_sort_length 字節而不是整個字符串做排序,如果只需要排序前面一小部分字符,則可以減小 max_sort_length 的配置。MySQL 不能將 BLOB 和 TEXT 列全部長度的字符串進行索引,也不能使用這些索引消除排序。

DATETIME

這個類型能保存大範圍的值,從 1001 年到 9999 年,精度爲秒。它把日期和時間封裝到了一個整數中,與時區無關,使用 8 字節的存儲空間。

TIMESTAMP

它和 UNIX 時間戳相同。TIMESTAMP 只使用 4 字節的存儲空間,因此它的範圍比DATETIME 小得多,只能表示1970年到2038年,並且依賴於時區。通常應該選擇 TIMESTAMP,因爲它比 DATETIME 空間效率更高。


P9:索引的分類

索引在也叫做鍵,是存儲引擎用於快速找到記錄的一種數據結構。索引對於良好的性能很關鍵,尤其是當表中數據量越來越大時,索引對性能的影響愈發重要。在數據量較小且負載較低時,不恰當的索引對性能的影響可能還不明顯,但數據量逐漸增大時,性能會急劇下降。

索引大大減少了服務器需要掃描的數據量、可以幫助服務器避免排序和臨時表、可以將隨機 IO 變成順序 IO。但索引並不總是最好的工具,對於非常小的表,大部分情況下會採用全表掃描。對於中到大型的表,索引就非常有效。但對於特大型的表,建立和使用索引的代價也隨之增長,這種情況下應該使用分區技術。

在MySQL中,首先在索引中找到對應的值,然後根據匹配的索引記錄找到對應的數據行。索引可以包括一個或多個列的值,如果索引包含多個列,那麼列的順序也十分重要,因爲 MySQL 只能高效地使用索引的最左前綴列。

B-Tree 索引

大多數 MySQL 引擎都支持這種索引,使用術語 B-Tree 是因爲 MySQL 在 CREATE TABLE 和其他語句中也使用該關鍵字。不過底層的存儲引擎可能使用不同的存儲結構,例如 NDB 集羣實際使用 T-Tree,而 InnoDB 則使用 B+Tree。

存儲引擎以不同方式使用 B-Tree 索引,性能也不同。例如 MyISAM 使用前綴壓縮技術使得索引更小,但 InnoDB 則按照原數據格式進行存儲。再例如 MyISAM 索引通過數據的物理位置引用被索引的行,而 InnoDB 則根據主鍵引用被索引的行。

B-Tree 通常意味着所有的值都是按順序存儲的,並且每個葉子頁到根的距離相同。B-Tree 索引能夠加快訪問數據的速度,因爲存儲引擎不再需要進行全表掃描來獲取需要的數據,取而代之的是從索引的根節點開始進行搜索。根節點的槽中存放了指向子節點的指針,存儲引擎根據這些指針向下層查找。通過比較節點頁的值和要查找的值可以找到合適的指針進入下層子節點,這些指針實際上定義了子節點頁中值的上限和下限。最終存儲引擎要麼找到對應的值,要麼該記錄不存在。葉子節點的指針指向的是被索引的數據,而不是其他的節點頁。

B-Tree索引適用於全鍵值、鍵值範圍或鍵前綴查找,其中鍵前綴查找只適用於最左前綴查找。索引對如下類型的查詢有效:

  • 全值匹配:全值匹配指的是和索引中的所有列進行匹配。
  • 匹配最左前綴:只使用索引的第一列。
  • 匹配列前綴:只匹配某一列的值的開頭部分。
  • 匹配範圍值:查找某兩個值之間的範圍。
  • 精確匹配某一列並範圍匹配另一列:有一列全匹配而另一列範圍匹配。
  • 只訪問索引的查詢:B-Tree 通常可以支持只訪問索引的查詢,即查詢只需要訪問索引而無需訪問數據行。

因爲索引樹中的節點有序,所以除了按值查找之外索引還可以用於查詢中的 ORDER BY 操作。一般如果 B-Tree 可以按照某種方式查找到值,那麼也可以按照這種方式排序。

B-Tree索引的限制:

  • 如果不是按照索引的最左列開始查找,則無法使用索引。
  • 不能跳過索引中的列,例如索引爲 (id,name,sex),不能只使用 id 和 sex 而跳過 name。
  • 如果查詢中有某個列的範圍查詢,則其右邊的所有列都無法使用索引。

哈希索引

哈希索引基於哈希表實現,只有精確匹配索引所有列的查詢纔有效。對於每一行數據,存儲引擎都會對所有的索引列計算一個哈希碼,哈希碼是一個較小的值,並且不同鍵值的行計算出的哈希碼也不一樣。哈希索引將所有的哈希碼存儲在索引中,同時在哈希表中保存指向每個數據行的指針。

只有 Memory 引擎顯式支持哈希索引,這也是 Memory 引擎的默認索引類型。

因爲索引自身只需存儲對應的哈希值,所以索引的結構十分緊湊,這讓哈希索引的速度非常快,但它也有一些限制:

  • 哈希索引只包含哈希值和行指針而不存儲字段值,所以不能使用索引中的值來避免讀取行。
  • 哈希索引數據並不是按照索引值順序存儲的,因此無法用於排序。
  • 哈希索引不支持部分索引列匹配查找,因爲哈希索引始終是使用索引列的全部內容來計算哈希值的。例如在數據列(a,b)上建立哈希索引,如果查詢的列只有a就無法使用該索引。
  • 哈希索引只支持等值比較查詢,不支持任何範圍查詢。
  • 訪問哈希索引的數據非常快,除非有很多哈希衝突。當出現哈希衝突時,存儲引擎必須遍歷鏈表中所有的行指針,逐行進行比較直到找到所有符合條件的行。
  • 如果哈希衝突很高的話,索引維護的代價也會很高。

自適應哈希索引是 InnoDB 引擎的一個特殊功能,當它注意到某些索引值被使用的非常頻繁時,會在內存中基於 B-Tree 索引之上再創鍵一個哈希索引,這樣就讓 B-Tree 索引也具有哈希索引的一些優點,比如快速哈希查找。這是一個完全自動的內部行爲,用戶無法控制或配置,但如果有必要可以關閉該功能。

如果存儲引擎不支持哈希索引,可以創建自定義哈希索引,在 B-Tree基礎 上創建一個僞哈希索引,它使用哈希值而不是鍵本身進行索引查找,需要在查詢的 WHERE 子句中手動指定哈希函數。當數據表非常大時,CRC32 會出現大量的哈希衝突,可以考慮自己實現 64 位哈希函數,或者使用 MD5 函數返回值的一部分作爲自定義哈希函數。

空間索引

MyISAM 表支持空間索引,可以用作地理數據存儲。和 B-Tree 索引不同,這類索引無需前綴查詢。空間索引會從所有維度來索引數據,查詢時可以有效地使用任意維度來組合查詢。必須使用 MySQL 的 GIS 即地理信息系統的相關函數來維護數據,但 MySQL 對 GIS 的支持並不完善,因此大部分人都不會使用這個特性。

全文索引

通過數值比較、範圍過濾等就可以完成絕大多數需要的查詢,但如果希望通過關鍵字的匹配進行查詢過濾,那麼就需要基於相似度的查詢,而不是精確的數值比較,全文索引就是爲這種場景設計的。全文索引有自己獨特的語法,沒有索引也可以工作,如果有索引效率會更高。

全文索引可以支持各種字符內容的搜索,包括 CHAR、VARCHAR 和 TEXT 類型,也支持自然語言搜索和布爾搜索。在 MySQL 中全文索引有很多限制,例如表鎖對性能的影響、數據文件的崩潰恢復等,這使得 MyISAM 的全文索引對很多應用場景並不合適。MyISAM 的全文索引作用對象是一個"全文集合",可能是某個數據表的一列,也可能是多個列。具體的對某一條記錄,MySQL 會將需要索引的列全部拼接成一個字符串然後進行索引。

MyISAM 的全文索引是一種特殊的 B-Tree 索引,一共有兩層。第一層是所有關鍵字,然後對於每一個關鍵字的第二層,包含的是一組相關的"文檔指針"。全文索引不會索引文檔對象中的所有詞語,它會根據規則過濾掉一些詞語,例如停用詞列表中的詞都不會被索引。

聚簇索引

聚簇索引並不是一種單獨的索引類型,而是一種數據存儲方式。InnoDB 的聚簇索引實際上在同一個結構中保存了 B-Tree 索引和數據行。當表有聚餐索引時,它的行數據實際上存放在索引的葉子頁中,因爲無法同時把數據行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。

優點:① 可以把相關數據保存在一起,例如實現電子郵箱時可以根據用戶 ID 聚集數據,這樣只需要從磁盤讀取少數數據頁就能獲取某個用戶的全部郵件,如果沒有使用聚簇索引,每封郵件可能都導致一次磁盤 IO。② 數據訪問更快,聚簇索引將索引和數據保存在同一個 B-Tree 中,因此獲取數據比非聚簇索引要更快。③ 使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值。

缺點:① 聚簇索引最大限度提高了 IO 密集型應用的性能,如果數據全部在內存中將會失去優勢。② 插入速度驗證依賴於插入順序,按照主鍵的順序插入是加載數據到 InnoDB 引擎最快的方式。③ 更新聚簇索引列的代價很高,因爲會強制每個被更新的行移動到新位置。④ 基於聚簇索引的表插入新行或主鍵被更新導致行移動時,可能導致頁分裂,表會佔用更多磁盤空間。④ 當行稀疏或由於頁分裂導致數據存儲不連續時,全表掃描可能很慢。

覆蓋索引

覆蓋索引指一個索引包含或覆蓋了所有需要查詢的字段的值,不再需要根據索引回表查詢數據。覆蓋索引必須要存儲索引列的值,因此 MySQL 只能使用 B-Tree 索引做覆蓋索引。

優點:① 索引條目通常遠小於數據行大小,可以極大減少數據訪問量。② 因爲索引按照列值順序存儲,所以對於 IO 密集型防僞查詢迴避隨機從磁盤讀取每一行數據的 IO 少得多。③ 由於 InnoDB 使用聚簇索引,覆蓋索引對 InnoDB 很有幫助。InnoDB 的二級索引在葉子節點保存了行的主鍵值,如果二級主鍵能覆蓋查詢那麼可以避免對主鍵索引的二次查詢。


P10:索引使用原則

建立索引

對查詢頻次較高,且數據量比較大的表建立索引。索引字段的選擇,最佳候選列應當從 WHERE 子句的條件中提取,如果 WHERE 子句中的組合比較多,那麼應當挑選最常用、過濾效果最好的列的組合。

使用前綴索引

索引列開始的部分字符,索引創建後也是使用硬盤來存儲的,因此短索引可以提升索引訪問的 IO 效率。對於 BLOB、TEXT 或很長的 VARCHAR 列必須使用前綴索引,MySQL 不允許索引這些列的完整長度。前綴索引是一種能使索引更小更快的有效方法,但缺點是 MySQL 無法使用前綴索引做 ORDER BY 和 GROUP BY,也無法使用前綴索引做覆蓋掃描。

選擇合適的索引順序

當不需要考慮排序和分組時,將選擇性最高的列放在前面。索引的選擇性是指不重複的索引值和數據表的記錄總數之比,索引的選擇性越高則查詢效率越高,唯一索引的選擇性是 1,因此也可以使用唯一索引提升查詢效率。

刪除無用索引

MySQL 允許在相同列上創建多個索引,重複的索引需要單獨維護,並且優化器在優化查詢時也需要逐個考慮,這會影響性能。重複索引是指在相同的列上按照相同的順序創建的相同類型的索引,應該避免創建重複索引。如果創建了索引 (A,B) 再創建索引 (A) 就是冗餘索引,因爲這只是前一個索引的前綴索引,對於 B-Tree 索引來說是冗餘的。解決重複索引和冗餘索引的方法就是刪除這些索引。除了重複索引和冗餘索引,可能還會有一些服務器永遠不用的索引,也應該考慮刪除。

減少碎片

B-Tree 索引可能會碎片化,碎片化的索引可能會以很差或無序的方式存儲在磁盤上,這會降低查詢的效率。表的數據存儲也可能碎片化,包括行碎片、行間碎片、剩餘空間碎片,對於 MyISAM 這三類碎片化都有可能發生,對於 InnoDB 不會出現短小的行碎片,它會移動短小的行重寫到一個片段中。可以通過執行 OPTIMIZE TABLE 或者導出再導入的方式重新整理數據,對於 MyISAM 可以通過排序重建索引消除碎片。InnoDB 可以通過先刪除再重新創建索引的方式消除索引碎片。

索引失效情況

如果索引列出現了隱式類型轉換,則 MySQL 不會使用索引。常見的情況是在 SQL 的 WHERE 條件中字段類型爲字符串,其值爲數值,如果沒有加引號那麼 MySQL 不會使用索引。

如果 WHERE 條件中含有 OR,除非 OR 前使用了索引列而 OR 之後是非索引列,索引會失效。

MySQL 不能在索引中執行 LIKE 操作,這是底層存儲引擎 API 的限制,最左匹配的 LIKE 比較會被轉換爲簡單的比較操作,但如果是以通配符開頭的 LIKE 查詢,存儲引擎就無法做筆記。這種情況下 MySQL 服務器只能提取數據行的值而不是索引值來做比較。

如果查詢中的列不是獨立的,則 MySQL 不會使用索引。獨立的列是指索引列不能是表達式的一部分,也不能是函數的參數。

對於多個範圍條件查詢,MySQL 無法使用第一個範圍列後面的其他索引列,對於多個等值查詢則沒有這種限制。

如果 MySQL 判斷全表掃描比使用索引查詢更快,則不會使用索引。


P11:優化數據類型

更小的通常更好

一般情況下儘量使用可以正確存儲數據的最小數據類型,更小的數據類型通常也更快,因爲它們佔用更少的磁盤、內存和 CPU 緩存。

儘可能簡單

簡單數據類型的操作通常需要更少的 CPU 週期,例如整數比字符操作代價更低,因爲字符集和校對規則使字符相比整形更復雜。應該使用 MySQL 的內建類型 date、time 和 datetime 而不是字符串來存儲日期和時間,另一點是應該使用整形存儲 IP 地址。

儘量避免 NULL

通常情況下最好指定列爲 NOT NULL,除非需要存儲 NULL值。因爲如果查詢中包含可爲 NULL 的列對 MySQL 來說更難優化,可爲 NULL 的列使索引、索引統計和值比較都更復雜,並且會使用更多存儲空間。當可爲 NULL 的列被索引時,每個索引記錄需要一個額外字節,在MyISAM 中還可能導致固定大小的索引變成可變大小的索引。

通常把可爲 NULL 的列設置爲 NOT NULL 帶來的性能提升較小,因此調優時沒必要首先查找並修改這種情況。但如果計劃在列上建索引,就應該儘量避免設計成可爲 NULL 的列。

在爲列選擇數據類型時,第一步需要確定合適的大類型:數字、字符串、時間等。下一步是選擇具體類型,很多 MySQL 數據類型可以存儲相同類型的數據,只是存儲的長度和範圍不一樣,允許的精度不同或需要的物理空間不同。


P12:優化查詢概述

優化數據訪問

如果把查詢看作一個任務,那麼它由一系列子任務組成,每個子任務都會消耗一定時間。如果要優化查詢,要麼消除一些子任務,要麼減少子任務的執行次數。查詢性能低下最基本的原因是訪問的數據太多,大部分性能低下的查詢都可以通過減少訪問的數據量進行優化。可以通過以下兩個步驟分析。

是否向數據庫請求了不需要的數據:有些查詢會請求超過實際需要的數據,然後這些多餘的數據會被應用程序丟棄,這會給 MySQL 服務器造成額外負擔並增加網絡開銷,另外也會消耗應用服務器的 CPU 和內存資源。例如多表關聯時返回全部列,取出全部列會讓優化器無法完成索引覆蓋掃描這類優化,還會爲服務器帶來額外的 IO、內存和 CPU 的消耗,因此使用 SELECT * 時需要仔細考慮是否真的需要返回全部列。再例如總是重複查詢相同的數據,比較好的解決方案是初次查詢時將數據緩存起來,需要的時候從緩存中取出。

MySQL 是否在掃描額外的記錄:在確定查詢只返回需要的數據後,應該看看查詢爲了返回結果是否掃描了過多的數據,最簡單的三個衡量指標時響應時間、掃描的行數和返回的行數。如果發現查詢需要掃描大量數據但只返回少數的行,可以使用以下手動優化:① 使用覆蓋索引掃描,把所有需要用的列都放到索引中,這樣存儲引擎無需回表查詢對應行就可以返回結果。② 改變庫表結構。 ③ 重寫這個複雜的查詢,讓 MySQL 優化器能夠以更優化的方式執行這個查詢。

重構查詢方式

在優化有問題的查詢時,目標應該是找到一個更優的方法獲取實際需要的結果,而不一定總是需要從 MySQL 獲取一模一樣的結果集。

切分查詢:有時候對於一個大查詢可以將其切分成小查詢,每個查詢功能完全一樣,只完成一小部分,每次只返回一小部分查詢結果。例如刪除舊數據,定期清除大量數據時,如果用一個大的語句一次性完成的話可能需要一次鎖住很多數據、佔滿整個事務日誌、耗盡系統資源、阻塞很多小的但重要的查詢。將一個大的 DELETE 語句切分成多個較小的查詢可以儘可能小地影響 MySQL 的性能,同時還可以減少MySQL 複製的延遲。

分解關聯查詢:很多高性能應用都會對關聯查詢進行分解,可以對每一個表進行單表查詢,然後將結果在應用程序中進行關聯。分解關聯查詢可以讓緩存的效率更高、減少鎖的競爭、提升查詢效率、還可以減少冗餘記錄的查詢。


P13:查詢執行流程

簡單來說分爲五步:① 客戶端發送一條查詢給服務器。② 服務器先檢查查詢緩存,如果命中了緩存則立刻返回存儲在緩存中的結果,否則進入下一階段。③ 服務器端進行 SQL 解析、預處理,再由優化器生成對應的執行計劃。④ MySQL 根據優化器生成的執行計劃,調用存儲引擎的 API 來執行查詢。⑤ 將結果返回給客戶端。

查詢緩存

在解析一個查詢語句之前,如果查詢緩存是打開的,那麼 MySQL 會優先檢查這個查詢是否命中查詢緩存中的數據。這個檢查是通過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即使只有一個字節不同,也不會匹配緩存結果,這種情況下會進行下一個階段的處理。如果當前的查詢恰好命中了查詢緩存,那麼在返回查詢結果之前 MySQL 會檢查一次用戶權限。如果權限沒有問題,MySQL 會跳過其他階段,直接從緩衝中拿到結果並返回給客戶端,這種情況下查詢不會被解析,不用生成執行計劃,不會被執行。

查詢優化處理

該階段包括多個子階段:解析 SQL、預處理、優化 SQL 執行計劃。首先 MySQL 通過關鍵字將 SQL 語句進行解析,並生成一顆對應的解析樹,MySQL 解析器將使用 MySQL 語法規則驗證和解析查詢。例如它將驗證是否使用了錯誤的關鍵字,或者使用關鍵字的順序是否正確等。預處理器則根據一些 MySQL 規則進一步檢查解析樹是否合法,例如檢查數據表和數據列是否存在,還會解析名字和別名看它們是否有歧義。下一步預處理器會驗證權限,這一步通常很快,除非服務器上有非常多的權限配置。

語法樹被認爲合法後,查詢優化器將其轉成執行計劃。一條查詢可以有多種查詢方式,最後都返回相同的結果,優化器的作用就是找到這其中最好的執行計劃。MySQL 使用基於成本的優化器,它將嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個。優化策略可以簡單分爲兩種,一種是靜態優化,可以直接對解析樹分析並完成優化,不依賴於特別的數值,可以認爲是一種編譯時優化。另一種是動態優化,和查詢的上下文有關,每次查詢時都需要重新評估。

MySQL 可以處理的優化類型包括:重新定義表的關聯順序、將外連接轉化成內連接、使用等價變換規則、優化 COUNT() 和 MIN() 以及 MAX() 函數、預估並轉爲常數表達式、覆蓋索引掃描、子查詢優化等。

查詢執行引擎

在解析和優化階段,MySQL 將生成查詢對應的執行計劃,MySQL 的查詢執行引擎則根據這個計劃來完成整個查詢。執行計劃是一個數據結構,而不是其他關係型數據庫那樣會生成對應的字節碼。查詢執行階段並不複雜,MySQL 只是簡單的根據執行計劃給出的指令逐步執行,再根據執行計劃執行的過程中,有大量操作需要通過調用存儲引擎實現的接口來完成。

返回結果給客戶端

查詢執行的最後一個階段是將結果返回給客戶端,即使查詢不需要返回結果集,MySQL 仍然會返回這個查詢的一些信息,如該查詢影響到的行數。如果查詢可以被緩存,那麼 MySQL 會在這個階段將結果存放到查詢緩衝中。MySQL 將結果集返回客戶端是一個增量、逐步返回的過程,這樣做的好處是服務器無需存儲太多的結果,減少內存消耗,也可以讓客戶端第一時間獲得響應結果。結果集中的每一行給都會以一個滿足 MySQL 客戶端/服務器通信協議的包發送,再通過 TCP 協議進行傳輸,在 TCP 傳輸過程中可能對包進行緩存然後批量傳輸。


P14:優化 SQL

優化 COUNT 查詢

COUNT 是一個特殊的函數,它可以統計某個列值的數量,在統計列值時要求列值是非空的,不會統計 NULL 值。如果在 COUNT 中指定了列或列的表達式,則統計的就是這個表達式有值的結果數,而不是 NULL。

COUNT 的另一個作用是統計結果集的行數,當 MySQL 確定括號內的表達式不可能爲 NULL 時,實際上就是在統計行數。當使用 COUNT(*) 時,* 不會擴展成所有列,它會忽略所有的列而直接統計所有的行數。

某些業務場景並不要求完全精確的 COUNT 值,此時可以使用近似值來代替,EXPLAIN 出來的優化器估算的行數就是一個不錯的近似值,因爲執行 EXPLAIN 並不需要真正地執行查詢。

通常來說 COUNT 都需要掃描大量的行才能獲取精確的結果,因此很難優化。在 MySQL 層還能做的就只有覆蓋掃描了,如果還不夠就需要修改應用的架構,可以增加彙總表或者外部緩存系統。

優化關聯查詢

確保 ON 或 USING 子句中的列上有索引,在創建索引時就要考慮到關聯的順序。

確保任何 GROUP BY 和 ORDER BY 的表達式只涉及到一個表中的列,這樣 MySQL 纔有可能使用索引來優化這個過程。

在 MySQL 5.5 及以下版本儘量避免子查詢,可以用關聯查詢代替,因爲執行器會先執行外部的 SQL 再執行內部的 SQL。

優化 GROUP BY

如果沒有通過 ORDER BY 子句顯式指定要排序的列,當查詢使用 GROUP BY 子句的時候,結果集會自動按照分組的字段進行排序,如果不關心結果集的順序,可以使用 ORDER BY NULL 禁止排序。

優化 LIMIT 分頁

在偏移量非常大的時候,需要查詢很多條數據再捨棄,這樣的代價非常高。要優化這種查詢,要麼是在頁面中限制分頁的數量,要麼是優化大偏移量的性能。最簡單的辦法是儘可能地使用覆蓋索引掃描,而不是查詢所有的列,然後根據需要做一次關聯操作再返回所需的列。

還有一種方法是從上一次取數據的位置開始掃描,這樣就可以避免使用 OFFSET。其他優化方法還包括使用預先計算的彙總表,或者關聯到一個冗餘表,冗餘表只包含主鍵列和需要做排序的數據列。

優化 UNION 查詢

MySQL 通過創建並填充臨時表的方式來執行 UNION 查詢,除非確實需要服務器消除重複的行,否則一定要使用 UNION ALL,如果沒有 ALL 關鍵字,MySQL 會給臨時表加上 DISTINCT 選項,這會導致對整個臨時表的數據做唯一性檢查,這樣做的代價非常高。

使用用戶自定義變量

在查詢中混合使用過程化和關係化邏輯的時候,自定義變量可能會非常有用。用戶自定義變量是一個用來存儲內容的臨時容器,在連接 MySQL 的整個過程中都存在,可以在任何可以使用表達式的地方使用自定義變量。例如可以使用變量來避免重複查詢剛剛更新過的數據、統計更新和插入的數量等。

優化 INSERT

需要對一張表插入很多行數據時,應該儘量使用一次性插入多個值的 INSERT 語句,這種方式將縮減客戶端與數據庫之間的連接、關閉等消耗,效率比多條插入單個值的 INSERT 語句高。也可以關閉事務的自動提交,在插入完數據後提交。當插入的數據是按主鍵的順序插入時,效率更高。


P15:複製

複製解決的基本問題是讓一臺服務器的數據與其他服務器保持同步,一臺主庫的數據可以同步到多臺備庫上,備庫本身也可以被配置成另外一臺服務器的主庫。主庫和備庫之間可以有多種不同的組合方式。

MySQL 支持兩種複製方式:基於行的複製和基於語句的複製,基於語句的複製也稱爲邏輯複製,從 MySQL 3.23 版本就已存在,基於行的複製方式在 5.1 版本才被加進來。這兩種方式都是通過在主庫上記錄二進制日誌、在備庫重放日誌的方式來實現異步的數據複製。因此同一時刻備庫的數據可能與主庫存在不一致,並且無法包裝主備之間的延遲。

MySQL 複製大部分是向後兼容的,新版本的服務器可以作爲老版本服務器的備庫,但是老版本不能作爲新版本服務器的備庫,因爲它可能無法解析新版本所用的新特性或語法,另外所使用的二進制文件格式也可能不同。

複製解決的問題:數據分佈、負載均衡、備份、高可用性和故障切換、MySQL 升級測試。

複製步驟

概述:① 在主庫上把數據更改記錄到二進制日誌中。② 備庫將主庫的日誌複製到自己的中繼日誌中。 ③ 備庫讀取中繼日誌中的事件,將其重放到備庫數據之上。

第一步是在主庫上記錄二進制日誌,每次準備提交事務完成數據更新前,主庫將數據更新的事件記錄到二進制日誌中。MySQL 會按事務提交的順序而非每條語句的執行順序來記錄二進制日誌,在記錄二進制日誌後,主庫會告訴存儲引擎可以提交事務了。

下一步,備庫將主庫的二進制日誌複製到其本地的中繼日誌中。備庫首先會啓動一個工作的 IO 線程,IO 線程跟主庫建立一個普通的客戶端連接,然後在主庫上啓動一個特殊的二進制轉儲線程,這個線程會讀取主庫上二進制日誌中的事件。它不會對事件進行輪詢。如果該線程追趕上了主庫將進入睡眠狀態,直到主庫發送信號量通知其有新的事件產生時纔會被喚醒,備庫 IO 線程會將接收到的事件記錄到中繼日誌中。

備庫的 SQL 線程執行最後一步,該線程從中繼日誌中讀取事件並在備庫執行,從而實現備庫數據的更新。當 SQL 線程追趕上 IO 線程時,中繼日誌通常已經在系統緩存中,所以中繼日誌的開銷很低。SQL 線程執行的時間也可以通過配置選項來決定是否寫入其自己的二進制日誌中。

這種複製架構實現了獲取事件和重放事件的解耦,允許這兩個過程異步進行,也就是說 IO 線程能夠獨立於 SQL 線程工作。但這種架構也限制了複製的過程,在主庫上併發允許的查詢在備庫只能串行化執行,因爲只有一個 SQL 線程來重放中繼日誌中的事件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章