架構師訓練營第 1 期 - 第 09 周總結
數據庫的基本原理
數據庫架構
連接器
數據庫連接也是一個網路通信
為每個連接請求分配一塊專用的內存
用於會話上下文管理
應用程序啟動時,通常會初始化建立一些數據庫連接放在連接池裡
TCP 的長連接
需要花費一定的時間建立對數據庫的連接,有一定的內存消耗
當處理外部請求執行 SQL 做作時,就不需要花費時間建立連接
語法分析器
對 SQL 命令進行分析,並產生抽象語法樹
example:
select s_grade from staff where s_city not in
(select p_city from proj where s_empname = p_pname)
如果 SQL 命令不符合語法,則回報 ERROR
example:
mysql> explain select * from users whee id = 1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'id = 1' at line 1
語意分析與優化器
將各種複雜嵌套的 SQL 進行語意等價轉換
得到有限幾種關係代數計算結構
選擇
投影
交
積
連接
利用語法的特徵、執行規則及索引等信息進一步進行優化
合併
前置計算
example:
原始: select f.id from orders f where f.user_id = (select id from users);
轉化: select f.id from orders f join users u on f.user_id = u.id;
為數據庫最核心、最高科技,代碼量最多的部分
執行計畫
由優化器產生,並交於執行引擎執行,產生結果
example
Key: 索引類型
NULL 無索引
Rows: 需要處理的列數
Possible_keys: 潛在可以利用的索引
JDBC 兩種編程方式
即時編譯
SQL 命令當成字符串輸入
預編譯
將要以佔位符構建 SQL 命令,先進行命令的預處理
預編譯比即時編譯好
預先提交帶佔位符的 SQL 到數據庫進行預處理
提前生成執行計畫
給定佔位符參數、真正執行 SQL 的時候,執行引擎可以直接執行,效率更好
防止 SQL 注入攻擊 (SQL Injection),安全性更好
語法樹、優化、執行計畫已經生成好
真實值僅當成參數傳入,並取代佔位符
SQL 注入 example
一般: select * from users where username = 'Frank’;
注入: Frank';drop table users;--
select * from users where username = 'Frank';drop table users;--’;
若即時編譯,此時才解析 SQL 命令字串,會有三個 SQL 命令
select
drop
-- (註解)
結果 users 表被刪除
預編譯
select * from users where username = ?;
有佔位符 '?'
不會重新編譯語法樹及執行計畫
Frank';drop table users;-- 被當成參數傳入
當成一般字串處理
儲存方式
B+ Tree
多層的檢索樹
增進查找性能
要查找的數據存在葉節點
所有葉節點連接成一個鏈表
聚簇索引 (Clustered Index)
數據庫紀錄和索引存儲在一起
索引字段為主鍵
葉節點存放索引字段外,也存放相對應的紀錄列
MySQL 數據庫的主鍵就是聚簇索引
主鍵 ID 和所在的紀錄列,存儲在一個 B+ 樹的葉節點中
主鍵 1、2 ..........
與主鍵相應的紀錄列 r1、r2 .....
非聚簇索引 (Non-clustered Index)
以某非主鍵欄位構建的 B+ Tree
葉節點為主鍵值 (即聚簇索引值)
回表
通過非聚簇索引找到主鍵索引
再通過主鍵索引找到列紀錄
此過程稱之
性能優化
添加必要的索引,優化 SQL 查詢性能
沒有索引,需全表掃描,檢索所有的列紀錄
沒有索引
列紀錄越多,性能越差
對數據庫的負載壓力大
優化 SQL 主要的手段,就是添加必要的索引
頻繁的更新,刪除也需要索引
合理使用索引
不要盲目添加索引,尤其在生產環境中
添加索引的 alter 操作會消耗較長時間 (分鐘級)
alter 操作期間,所有增刪改操作全部阻塞
對應用而言,因為連接不能釋放,事實上連查詢也被阻塞
刪除不用的索引,並免不必要的增刪開銷
當新增、刪除時,所有索引樹必須更新
使用更小的數據類型創建索引
減少空間開銷
可以讀到更多的數據,性能也會提升
int: 4 bytes, bigint: 8 bytes
Timestamp: 4 bytes, Datetime: 8 bytes
數據庫事務 (Transaction)
ACID 特性
原子性 (Atomicity)
事務要麼全部完成,要麼全不取消
如果事務崩潰或者 SQL 執行失敗,狀態回到事務之前 (事務回滾)
隔離性 (Isolation)
如果兩個事務 T1 和 T2 同時運行,事務 T1 和 T2 最終的結果是相同的,不管哪一個先結束
主要靠 "鎖" 來實現
持久性 (Durability)
一旦事務提交,不管發生甚麼 (崩潰或者出錯),數據要保存在數據庫中
一致性 (Consistency)
只有合法的數據(依照關係約束和函數約束)才能寫入數據庫中
事務日誌
記錄更新前的數據紀錄 (UNDO)
記錄更新後的數據記錄 (REDO)
全部記錄成功,事務正常結束
若過程中記錄更新失敗,整個事務回滾
利用 UNDO log 回滾
若過程中更新數據庫成功,但數據庫並沒有成功回寫磁碟,或者 SQL 執行失敗
可利用 REDO log 更新
由事務決定調用 REDO 或 UNDO
UNDO : 回復原始狀態
REDO : 繼續完成事務
example
LSN : 按時間順序分配的唯一事務記錄日誌序列號
TransID: 產生操作的事物 ID
PageID: 被修改的數據在磁盤上的位置
PrevLSN: 同一個事務產生的上一條日誌記錄的指針
UNDO: 取消本次操作的方法,按照此方法回滾
REDO: 重複本次操作的方法
JVM 虛擬機架構原理
JVM 組成架構
類加載器
啟動 Java 進程
java org.apache.catalina.startup.Bootstrap "$@" start
通過類加載器,加載啟動類
org.apache.catalina.startup.Bootstrap
Java 內執行的主程序
一定要有 main(...) 函數
運行時期的數據區
加載進來的類,放於 "方法區" 內
啟動一個主線程,執行類中的 main(...) 函數
分配 "Java 棧"
分配 "程序計數寄存器"
存儲當前線程執行的指令
每一線程獨享 "棧" 及 "計數寄存器"
若 new 一個類,則會從 "堆" 中創建此類的物件實例
但引用的變量存儲在每個線程的 "棧" 中
執行引擎
執行計數寄存器所指的 java 字節碼(byte code)指令
通過解譯或編譯成本地操作系統的本地指令
Java 虛擬機
有自己的 "計數寄存器"
有自己的內存管理
有自己的執行引擎
可以執行字節碼指令
如同 CPU 一般
有完整的計算機體系
Java 字節碼文件
Java 可以跨平台、跨操作系統、跨不同的硬件平台原因
有通用的字節碼
有執行引擎可以把字節碼轉換成本地的執行指令
Java 有 200 個左右的指令,可存儲於一個字節 (byte) 中
一個指令後面帶有不同的長度的操作數
文件 format
Magic code "cafebabe"
reference
Java 字節碼文件編譯過程
與 SQL 語法到執行計畫類似
Java 字節碼執行流程
方法調用超過閥值,才會啟動 "JIT" 動態編譯器,編譯成本地指令
編譯可以執行最佳化
可增加執行效能
類加載器的雙親委託模型
類加載器為分層加載
累加載器高層 -> 低層
Bootstrap Class Loader
加載 Java 自身的核心類,運行環境的類
jre/lib/rt.jar
Platform Class Loader (Extension Class Loader)
加載 ext/ 下的 jar 包
Application Class Loader
加載指定的 CLASSPATH 路徑下的類
User defined Class Loader
必須繼承 Application Class Loader
低層的加載器,不能覆蓋更高層次類加載器已經加載的類
只有較高層的加載器確認過沒有加載過,才允許低層的加載器去加載
自行定義 class loader 原因
隔離加載
在同一個 JVM 中的不同組件,想要加載同一個類的不同版本
如 Tomcat
可以啟動多個 war 包
實現多個 web 應用
多個 web 應用中,可能有同樣的 class,不同的實現
為每一個 war 包,分配一個 class loader
擴展加載源
從網路、數據庫等處加載類
字節碼加密
加載自訂譯的加密字節碼
在自定義的 class loader 中解密
JVM 的 "堆" 和 "棧"
堆
每一個 JVM 實例唯一對應一個 "堆"
運行中所創建的所有類實例或數組均放於 "堆" 中
由應用中的所有線程共享
棧
每一個新創件的線程都分配一個 "棧"
每一個 Java 程序的運行是通過對 "棧" 的操作完成
每個類實例的引用在 "棧" 中分配,引用指向 "堆" 中的實例
線程棧
方法內定義的基本類型變量,會被每個運行這個方法的線程放入自己的棧中
線程的棧彼此隔離
這類基本類型變量一定是線程安全的
JVM "方法區" 和 "程序計數器"
方法區
存放從磁盤加載進來的字節碼
程序計數器
JVM 在 "堆" 中創建啟動類的實例
JVM 進入啟動類的 main 方法時,創建一個應用程序主線程
main 方法理的代碼被主線程執行
"棧" 裡存放方法運行期的局部變量
程序計數器存放當前線程執行到哪一行字節指令
Java 線程工作內存和 "volatile"
工作內存 == CPU cache
Java 內存模型規定
多線程的情況下,線程操作主內存變量
需要通過線程獨有的工作內存的副本來進行
副本拷貝自主內存變量
一個共享變量(類成員變數、類靜態成員變數)照內存模型,在工作內存中產生髒數據或不可見
每個線程工作內存不同
更新共享變量不會立刻反應至其他工作內存
volatile 修飾
修飾共享變數
保證不同線程對這個變數進行操作時的可見性
一個線程修改了各個變數的值,新值對其他線程來說立即可見
Java 運行環境
JVM 垃圾回收性能分析
垃圾回收
將 JVM 堆中已經不再被使用的對象清理掉,釋放內存資源
通過一種可達性分析算法進行垃圾對象識別
垃圾對象 - 沒有被引用的對象
標示過程
從線程棧幀中的局部變量,或方法區中的靜態變量出發
這些稱為 "根"
將這些變量所引用的對象進行標記
若引用的對象內部引用了其他對象,繼續進行標記
被標記的對象都是被使用的對象
沒有被標記的對象就是可回收的垃圾對象
回收方法
清理
將垃圾對象佔據的內存清理
JVM 將所占內存空間
標示為空閒
記錄在一個空閒列表
下次創建新對象時,從列表中找一個適合大小的空閒空間分配給新對象
會產生碎片的空閒空間
回收幾次後,碎片會越來越多
壓縮
從堆空間頭部開始
將存活的對象拷貝放在一段連續的內存空間中
其餘空閒空間也是連續的空閒空間
複製
將堆空間分成兩部分
只在其中一部分創建對象
當這一部分空間用完時,將標記可用對象複製到另外一個空間中
回收策略 - 分代回收
Java 對象大部分生存時間很短暫
將生存時間很短的對象,創建在較小的區域
回收範圍小
可快速回收
分成新生代、老年代
新生代
Eden 區
初始對象創建區
From 區
若 Eden 區滿了進行垃圾回收
將可用對象複製至此區
To 區
若 From 滿了,連同 Eden 區與 From 區進行垃圾回收
將可用對象複製至此區
在 From 與 To 區交叉進行回收與複製
老年代
在新生代交叉回收與複製後,會產生一些生命周期很長的對象
將這些對象複製至此區
Young GC
對新生代進行回收
Full GC
全量垃圾回收
新生代、老年代一起回收
回收器算法
串行回收器
所有應用程序線程停止
對應用線程影響很大
stop-the-world 時間也算進響應時間內
啟動一個垃圾回收線程
標記對象
回收
早期 Java 運行在單核 CPU 使用
並行回收器
所有應用程序線程停止
對應用線程影響很大
stop-the-world 時間也算進響應時間內
針對多核 CPU ,啟動多個垃圾回收線程
並發回收器 (CMS)
初始化標記
所有應用線程停下
啟動標記線程
並發標記
標記線程與應用線程一起執行
重標記
所有應用線程停下
啟動重新標記線程
因為前一個並發標記時,應用程序可能改變物件引用狀態
並發清理
清理線程與應用線程一起執行
stop-the-world 時間比較短,對應用線程影響比較小
G1 回收器
將內存空間分成更小的區域
默認為 2000 個區域
區域越小,垃圾對象標記、清理也越快
每個區域也區分不同的分代角色,進行分代回收
利用 -XX:MaxGCPauseMillis 進行 stop-the-world 暫停時間控管
期望 GC 暫停時間的最大值
G1 回收器根據這個時間,動態調整回收策略
Java 啟動參數
標準參數
所有 JVM 實現都必須時間這些參數的功能
保證向後兼容
Example
運行模式 : -server, -client
類加載路徑 : -cp, -classpath
運行調試: --verbose
系統變量: --D
非標準參數
默認 JVM 實現這些參數
不保證所有 JVM 實現都實現
不保證向後兼容
Example
-Xms : 初始堆大小
-Xmx : 最大堆大小
-Xmn : 新生代大小
-Xss : 線成堆棧大小
非 Stable 參數
此類參數各 JVM 實現會有所不同
將來可能隨時取消
Example
-XX:-UseConcMarkSweepGC
啟用 CMS 垃圾回收
JVM 性能診斷工具
基本工具
jps
查看 Host 上,所有 Java 進程 pid (jvmid)
找出 JVM 進程 ID,進一步使用其他工具來監控、分析
常用參數
-l : 輸出 Java 應用程序 main class 的完整包
-q : 僅顯示 pid,不顯示其他訊息
-m : 輸出傳遞給 main 方法的參數
-v : 輸出傳遞給 JVM 的參數
診對 JVM 相關問題時查看
jstat
Java Virtual Machine Statistics Monitoring Tool
JDK 自帶輕量級工具
對 Java 應用程序的資源和性能進行實時的命令行監控
包括對 Heap Size、垃圾回收狀況
語法
jstat [options] vmid [interval] [count]
options : 一般使用 -gcutil 查看 gc 情況
vmid : JVM 進程 ID
interval : 間隔時間,單位 ms
count : 打印次數,若缺省則打印無數次
欄位
S0 : Heap 上的 Survivor space 0 區域已使用空間百分比
S1 : Heap 上的 Survivor space 1 區域已使用空間百分比
E : Heap 上的 Eden space 區域已使用空間百分比
O : Heap 上的 Old space 區域已使用空間百分比
YGC : 從應用程序啟動到採樣時,發生 Young GC 的次數
YGCT : 從應用程序啟動到採樣時,Young GC 使用的時間 (s)
FGC : 從應用程序啟動到採樣時,發生 Full GC 的次數
FGCT : 從應用程序啟動到採樣時,Full GC 使用的時間 (s)
GCT : 從應用程序啟動到採樣時,用於 GC 的總時間 (s)
jmap
輸出所有內存對象的工具
找出內存洩漏
可將 JVM 中的 heap,以二進制輸出成文本
使用方法
jmap -histo pid > a.log
保存到文本
可使用文本比對工具,找出 GC 回收了哪些對象
jmap -dump:format=b, file=f1 PID
將 PID 進程的內存 heap 輸出至 f1 文件中
jstack
查看 JVM 內線程堆棧信息
集成工具 (可視化工具)
JConsole
JVisualVM
Java 代碼優化技巧及原理
合理並謹慎使用多線程
使用場景 : I/O 阻塞、多 CPU 並發,多用戶並發
會有資源爭用與同步問題
啟動線程數 = [ 任務執行時間 / (任務執行時間 - IO 等待時間) ] * CPU 內核數
最佳啟動線程數與 CPU 內核數成正比和 IO 阻塞時間成反比
如果都是 CPU 型計算任務,線程數最多不超過 CPU 內核數
如果需要等待 I/O 操作,可多啟動線程
提高任務並發數
提高系統吞吐能力
改善系統效能
競態條件與臨界區
多線程訪問相同資源會有問題
多線程競爭同一資源時
競態條件 : 對資源訪問順序敏感
當前後順序不一致或混亂時,會對結果造成不一致
臨界區 : 導致競態條件發生的代碼區
在臨界區使用適當的同步,就可以避免競態條件
Java 線程安全
線程安全代碼 : 允許被多個線程安全執行的代碼
方法局部變量
局部變量不會被多個線程共享
局部變量存於線程棧中
線程不共享線程棧
基礎類型的局部變量是線程安全的
方法局部對象引用
方法中創建的對象不傳遞給其他方法或設定至全局變量,則線程安全
此方法引用完後,沒有其他引用,被垃圾回收
對象成員變量
對象成員存於堆中
若多線程同時更新同一對象的同一成員,則線程不安全
Java ThreadLocal
線程共享 : 可能線程不安全
線程私有 : 一定安全
ThreadLocal : 既私有又共享
在堆中,大家共享
每個對象可以訪問到私有的 ThreadLocal 變量,不會產生線程安全問題
使用
創建一個 ThreadLocal 變量 (X 類靜態成員變量)
public static ThreadLocal myThreadLocal = new ThreadLocal()
存儲此對象的值 (A 類 a 方法)
X.myThreadLocal.set("A Thread Local Value")
讀取一個 ThreadLocal 對象的值 (B 類 b 方法)
String threadLocalValue = (String)X.myThreadLocal.get()
每個線程 get 出來的值,就是自己 set 進去的
set 時
會根據當前是哪一個 thread
取出這個 thread 中的 ThreadLocalMap()
再把值存入
整個 map 對象線程共享,裡面的 每個 Key-Value 線程私有
Java 內存洩漏
無引用的對象由垃圾回收器回收
Java 內存洩漏是邏輯上的洩漏,不是物理上的洩漏
一個對象邏輯上不再使用,卻能存在在內存中,仍在系統某處被引用
是由開發人員的錯誤引起的
如果程序保留對永遠不再使用對象的引用,這些對象將會占用並耗盡內存
沒有被垃圾回收
Example
長生命週期的對象
引用可能一直存在使用的框架中,沒有被釋放
靜態容器
不斷的往容器增加對象,卻從不清除
緩存
自身不對對象進行釋放管理
合理使用線程池和對象池
復用線程或對象
比較耗資源、耗時比較長的對象
避免在程序生命週期中,創建和刪除大量對象
注意
池管理算法
紀錄對象空閒及使用狀況
對象內容清除
如 ThreadLocal 的清空
使用合適的 JDK 容器類
順序表、鏈表、Hash 等
了解 LinkList 和 ArrayList 的區別及適用場景
了解 HashMap 的算法及應用場景
不同場景使用不同的容器類
使用 concurrent 包
如使用線程安全的 ConcurrentHashMap
HashMap 需應用程序自行控制同步 (鎖)
ConcurrentHashMap 自帶分段加鎖,效能較高
縮短對象生命週期,加速垃圾回收
減少對象駐留內存的時間
在使用時創建對象,用完後立即釋放
避免讓對象進入老年代
可手動釋放,將變量設 null 值,便釋放
對象創建的步驟
靜態代碼段 -> 靜態成員變量 -> 父類構造函數 -> 子類構造函數
使用 I/O buffer 及 NIO
延遲寫與提前讀策略
應用程序不會等待寫入及讀取
提升 I/O 操作性能
異步無阻塞 I/O
優先使用組合代替繼承
減少對象耦合
更符合面向對象的設計模式
避免太深的繼承層次帶來的對象創建性能損失
太多祖先類構造函數的呼叫,影響性能
合理使用單例模式
類等於全局靜態對象
做到無狀態的單例對象
沒有成員變量或成員變量也是無狀態
不用考慮線程安全問題
保證線程安全
系統性能優化案例: 秒殺系統
技術挑戰
瞬間高並發
帶寬耗盡
服務器崩潰,猶如 DDOS 攻擊
確保使用戶遵循秒殺規則
第一類秒殺器: 秒殺前不斷刷新頁面
第二類秒殺器: 跳過秒殺頁面,直接進入下單頁面
核心架構方案思路
與原系統隔離
避免修改影響原系統
部屬一個新的秒殺服務器集群
避免對原系統重構,增加新功能開發難度及時間
根據新的秒殺系統需求,進行最適化設計
簡單化設計
避免複雜的業務流程
用戶端流程簡化
砍掉不重要的分枝流程
如開通支付接口
延遲部分流程,分散並發
如訂單完成之後的支付流程
使用原有支付及訂單管理系統
經過層層篩減,秒殺成功後對支付及訂單管理系統的並發量不大
避免重新驗證金流正確性
降低高並發影響
利用緩存 (CDN,反向代理),降低對系統的負載
將動態頁面轉成靜態頁面
也可減少數據庫存取
規範商品圖片大小
降低帶寬需求
精簡優化頁面
減少 HTTP 請求數
精簡 CSS,JS
圖片合併
HTML 內容壓縮
設置閥門,控制訪問流量,降低後端系統負擔
防止秒殺器干擾
閥門可有效防止第一類秒殺器
最後一刻動態生成 URL,可防止第二類秒殺器
降低運營複雜性
依秒殺商品列表,定時自動產生靜態頁面
版权声明: 本文为 InfoQ 作者【Panda】的原创文章。
原文链接:【http://xie.infoq.cn/article/a9fcb149fefdcbc63cfd5f00c】。未经作者许可,禁止转载。
评论