LearnProj
本文作為數(shù)工底層的項目CfengDB開始篇章,介紹開發(fā)緣由和實現(xiàn)思路
cfeng之前對數(shù)據(jù)庫研究不深入,之前只是能夠做到基本的SQL查詢和基本的慢SQL優(yōu)化,之前拿到數(shù)據(jù)庫系統(tǒng)工程師證書還是只在業(yè)務上對于DB系統(tǒng)使用更深入,但是cfeng基于work的理解,當作為一個優(yōu)秀的產(chǎn)品使用者之后,再來作為產(chǎn)品的開發(fā)者,二者是互相促進的, 現(xiàn)在信息時代的發(fā)展大數(shù)據(jù)量變得非常普遍,了解DB的設計開發(fā)對于我們的code是非常有幫助的,從SQLboy變成engineer
MySQL系統(tǒng)結構
工作中最常使用的DB系統(tǒng)應該就是MySQL了,當然隨著信創(chuàng)生態(tài),一些國產(chǎn)數(shù)據(jù)庫也廣泛普及,如何讓我們的SQL更高效,查詢更迅速,一致性問題等都是需要考慮的問題,MySQL最常見的幾個概念應該就是索引,鎖 + 事務MVCC了, 本文就不探究這些業(yè)務層面的實現(xiàn)了,這里主要介紹一下MySQL的架構。
這是網(wǎng)絡上一個比較簡化的結構圖, 整體SQL的執(zhí)行來說就4個部分:
- 連接器: 管理相關MySQL客戶端與服務端的連接同時會進行旋前的驗證
- 分析器: 詞法分析和語法分析,也就是MySQL需要讀懂整條SQL的意思
- 優(yōu)化器: 客戶端傳送的SQL可能非常臃腫繁雜,server端需要進行簡單的優(yōu)化
- 執(zhí)行器: MySQL讀懂SQL含義并優(yōu)化之后,開始執(zhí)行,底層的存儲引擎是插件式的,可供選擇
一條SQL執(zhí)行流程
上面的各個模塊都是比較簡化版的,這里再實例詳述一遍。
后臺應用和MySQL之間都維護了連接池,用于管理所有的連接,通過Connect就可以將待執(zhí)行的SQL傳送給MySQL服務端執(zhí)行。
SQL語句到達MySQL服務端之后
- server的線程會將接收到的語句交給SQL接口(組件),該interface專門由于執(zhí)行SQL語句
- SQL接口將語句傳遞給SQL解析器parser,進行詞法語法分析,理解SQL語句的意思(從哪張表,什么過濾條件,提取哪些字段…)
- 之后將語句傳送給查詢優(yōu)化器,選擇最優(yōu)的路徑,達到最佳的性能(比如select x from student where id =1, 可以先查找所有的x再過濾,或者先過濾再取x)這類的路徑選擇就是優(yōu)化器的工作
- 選擇最佳路徑后,再傳遞給執(zhí)行器將SQL語句按邏輯執(zhí)行(比如調(diào)用存儲引擎的接口,獲取user表的數(shù)據(jù),判斷,繼續(xù)…)
- 調(diào)用存儲引擎,執(zhí)行,執(zhí)行器會調(diào)用存儲引擎完成 SQL的操作,存儲引擎按照一定的步驟查詢內(nèi)存緩存數(shù)據(jù),更新磁盤數(shù)據(jù)。
為了提高效率,MySQL中也存在緩存,如果SQL命中緩存,就不會再走流程,節(jié)約時間
cfengDB整體結構
就模塊劃分來說,MySQL是十分優(yōu)秀的,cfengDB最多只能說是幫助us更加理解數(shù)據(jù)庫這種產(chǎn)品的一種最簡單的設計而已。
因為cfengDB需要完成的最基礎的任務就是識別并正確執(zhí)行SQL,因此1.0.0就只針對這一過程進行結構設計(其余的向連接池化,用戶管理…都后續(xù)再設計)
cfengDB整體上也是分為前端和后端,前后端通過網(wǎng)絡Socket同學,前端的職責就是讀取用戶的SQL并發(fā)送給后端server指向,輸出返回的結果,后臺和MySQL流程一樣需要識別合法的SQL并執(zhí)行
為了整體的可擴展性和實現(xiàn)的難度,采用分層和分治的思想進行模塊劃分,整體上劃分為事務管理模塊Transaction Manager, 數(shù)據(jù)管理模塊Data Manager, 版本鎖管理Version Manager,索引管理Index Manager,表管理Table Manager
最終需要實現(xiàn)的就是表管理Table Manager,通過表管理模塊就可以提供server的相關服務,模塊分層和依賴關系如下:
- 事務管理TM: 維護TID文件來維護事務的狀態(tài),提供接口供其他的模塊來查詢事務(參考的MySQL的MVCC)
- 數(shù)據(jù)管理DM: 直接管理數(shù)據(jù)庫DB文件和日志文件,需要分頁管理DB文件并且需要緩存,同時需要管理日志文件以供故障回復,抽象DB文件類提供上層使用
- 鎖管理VM: 并發(fā)管理模塊,基于2PL協(xié)議實現(xiàn)調(diào)度可串行化,利用MVCC實現(xiàn)隔離級別
- 索引管理IM: 基于B+樹實現(xiàn)索引
- 表管理TBM: 整體上的表和字段管理,完成SQL解析和執(zhí)行
事務管理TM模塊
從模塊層級來看,TM作為底層是最基礎的部分,所以先從最基本的模塊開始講解:
TM主要是通過維護TID來維護事務的狀態(tài),提供接口供其他的模塊查詢狀態(tài)
TID文件規(guī)則定義
cfengDB中每一個事務都會有一個TID進行表示,事務ID從1開始自增,不重復; 特殊規(guī)定 ----- TID為0表示無事務noneTransaction,代表事務不申請事務直接執(zhí)行,該類型事務狀態(tài)永遠都是committed
TM中維護一個TID格式文件,記錄Transaction的狀態(tài),其中cfengDB中規(guī)定事務的狀態(tài)為三種:
-
0 running
: 正在執(zhí)行,還沒有結束 -
1 commited
: 已提交 -
3 rollback
: 撤銷回滾
TID中,每一個事務有1字節(jié)空間保存狀態(tài),同時,TID頭部還有8字節(jié)的數(shù)字,記錄TID文件管理的事務的個數(shù),那么事務tid在文件中的狀態(tài)就存儲在8 +(tid-1) 字節(jié)位置,【從0開始計數(shù),并且TID為0代表none】
文件讀寫 – NIO
IO種類很多,有磁盤IO和網(wǎng)絡IO, BIO、NIO、IO多路復用、AIO的概念大多用于網(wǎng)絡IO — 用戶程序從網(wǎng)絡中獲取數(shù)據(jù)socket
- 用戶進程系統(tǒng)調(diào)用進入內(nèi)核態(tài)
- OS等待遠程客戶端發(fā)送數(shù)據(jù)(TCP建立連接),OS從網(wǎng)卡設備獲取數(shù)據(jù),從Socket協(xié)議棧拷貝到內(nèi)核緩沖區(qū)
- 把內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到用戶緩沖區(qū)
- 用戶進程獲取到數(shù)據(jù),繼續(xù)執(zhí)行
NIO相比BIO,AIO的區(qū)別就是:
步驟1,用戶進程要進行IO了,是阻塞掛起,還是非阻塞
步驟3: 內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到用戶緩沖區(qū),用戶進程是阻塞還是非阻塞
BIO是同步阻塞,數(shù)據(jù)的讀寫都會阻塞在一個線程中
NIO是同步非阻塞,通過Selector監(jiān)聽不同的channel上的數(shù)據(jù)變化,當channel數(shù)據(jù)發(fā)生變化時,通知該線程進行讀寫操作,然后線程就會自己進行讀寫
AIO是異步,接收到客戶端管道后,交給底層處理IO通信,自己本身做其余的事情
(同步和異步的意思是是否需要自己處理,而阻塞和非阻塞就是點餐后是否需要一直等著飯做好)
【步驟一只是通知(點餐),3就是真正開始讀寫】,1,3都阻塞,就是BIO,1不阻塞,3阻塞,就是NIO; 1,3都不阻塞,就是AIO
- BIO:阻塞IO,包括常見的ServerSocket/Socket, accept(); //連接阻塞; InputStream/OutputStream; IO讀寫阻塞
- NIO: new IO,noBlock; Selector復路器,緩沖區(qū)Buffer,ServerSocketChannel; IO未就緒的時候都是非阻塞的; 通過Linux提供的epoll + Selector + Channel實現(xiàn)多路復用【一個線程處理N個連接】
NIO核心組件:
- Channel: 通道,NIO數(shù)據(jù)源頭/目的地,緩沖區(qū)Buffer的唯一接口: 雙向讀寫【可讀出和寫入】、數(shù)據(jù)in/out總是先到緩沖區(qū); {文件IO: FileChannel 從文件中讀取數(shù)據(jù), DataChannel: UDP讀寫網(wǎng)絡數(shù)據(jù),SocketChannel: TCP讀寫 ServerSocketChannel:服務端監(jiān)聽新TCP連接,每進入一個創(chuàng)建SocketChannel}
- Buffer: 緩沖區(qū), NIO數(shù)據(jù)讀寫的中轉(zhuǎn)地【一塊連續(xù)內(nèi)存塊】,Buffer和傳入的數(shù)組共享相同的數(shù)據(jù)存儲區(qū)域; 可以簡單理解兩指針指向相同的塊,作為數(shù)據(jù)緩存, 適用于除了boolean之外所有基本類型, 比如ByteBuffer、IntBuffer… allocate()動態(tài)分配yi
文件讀寫采用的是NIO方式,(NIO非阻塞,支持基于通道的IO操作,區(qū)別傳統(tǒng)的Input/OutputStream),使用FileChannel,創(chuàng)建TxManager后,需要對TID文件校驗,保證TID文件合法
校驗方式: 文件頭的8byte數(shù)字推斷文件的理論長度,如果不同就認為不合法,不合法的,直接panic強制停機【對于無法回復的錯誤只能先粗暴停機】
//事務tid在tid文件中的位置
LEN_TID_HEADER_LEN + (tid - 1)* TID_FIELD_SIZE [頭長度 + (序號 -1) * 每個tid長度]
所有的文件操作,執(zhí)行后立刻刷入文件,防止崩潰后丟失, 使用NIO的fileChannel的force方法,和BIO的flush類似
【為了方便,java8開始支持在interface中定義靜態(tài)方法體,static的方法: 調(diào)用時直接采用接口名稱調(diào)用即可】
create()和open() 創(chuàng)建TID文件,從TID文件創(chuàng)建TransactionManager,創(chuàng)建XID文件時需要寫一個空的TID的header,設置tidCounter為0(數(shù)量為0),否則后續(xù)會不合法
RandomAccessFile、FileChannel、ByteBuffer
IO的底層涉及的概念: 緩沖區(qū)操作
, 內(nèi)核空間與用戶空間
, 虛擬內(nèi)存
,分頁技術
磁盤操作屬于內(nèi)存管理,是OS系統(tǒng)級功能,需要在內(nèi)核態(tài)執(zhí)行,所以讀取的數(shù)據(jù)是到內(nèi)核緩沖區(qū),之后再拷貝到用戶態(tài)緩沖區(qū)
java中的IO常見操作read()和write()完成的作用: 數(shù)據(jù)在內(nèi)核緩沖區(qū)和用戶緩沖區(qū)進行交換, 傳統(tǒng)IO是面向流(按字節(jié)讀取),而NIO則是面向Buffer的
在java的內(nèi)存結構中, 直接內(nèi)存不受JVM管理, 而堆heap是受JVM用戶進程管理; NIO中的Selector可以監(jiān)聽channel,【channel是數(shù)據(jù)源的抽象】,只有當某個channel準備好之后,線程才會阻塞去取數(shù)據(jù)操作,而不需要一直等待【BIO從最開始準備數(shù)據(jù)就開始阻塞】
- RandomAccessFile允許隨機讀寫文件,也就是可以按照設定的位置開始讀取(部分讀?。?; 訪問模式包括: r: 只讀; rw: 讀寫; rws:讀寫,每次文件內(nèi)容和元數(shù)據(jù)修改都同步磁盤; rwd: 讀寫,每次文件內(nèi)容同步磁盤
- FileChannel: 通道, 通過文件位置指定開始讀寫, instance可以通過RandomAccessFile.getChannel()獲得, 讀寫buffer后會自動向后移動pos
- ByteBuffer: 緩沖區(qū),對byte數(shù)組的一種封裝, position表示當前的小標,limit結束標記,capacity就是底層的byte數(shù)組的容量, 可以通過wrap或者allocate進行內(nèi)存的分配,通過getXXX方法可以進行類型轉(zhuǎn)換; 每次進行write的時候可以進行force來避免數(shù)據(jù)丟失
在這里為了方便進行各種類型的轉(zhuǎn)換, 實現(xiàn)了一個byte[] 和 類型的轉(zhuǎn)換工具類:
public static byte[] long2Byte(long value) {
return ByteBuffer.allocate(Long.SIZE/Byte.SIZE).putLong(value).array();
}
public static long parseLong(byte[] buf) {
ByteBuffer buffer = ByteBuffer.wrap(buf,0,8);
return buffer.getLong();
}
接口實現(xiàn)
首先TM中都是對于事務的操作,NO Transaction使用TID為0, TID的狀態(tài)變化和事務的新增都是通過操作TID文件實現(xiàn)的, 所以TM的實現(xiàn)就是對于TID文件的創(chuàng)建和修改
//事務管理大多是對tid文件的管理,所以需要nio的RandomAccessFile和相關的channel
private RandomAccessFile randomAccessFile;
private FileChannel fileChannel;
private long tidCounter; //tid計數(shù)器
private Lock counterLock; //使用Lock鎖保證線程安全
再進行TM初始化的時候就會進行TIdCounter的檢查,檢查的方式就是先檢查頭的長度,之后再整體計算文件長度
文件合法檢測
getTidPosition就可以獲取tid對應事務的狀態(tài)字節(jié)開始的位置, 而實際的長度就需要再加上一個TID狀態(tài)的長度 再和 fileLen相比即可
fileLen = randomAccessFile.length(); //文件實際長度
if(fileLen < TransactionConstant.LEN_TID_HEADER_LEN)
this.tidCounter = ByteBufferParser.parseLong(buffer.array()); //tid就是頭8個字節(jié)表示的數(shù)據(jù)
long end = getTidPosition(this.tidCounter) + TransactionConstant.TID_FIELD_SIZE; //getTidPosition相當于取得的是tid該狀態(tài)byte開始的位置,結束位置需要
if(end != fileLen)
private long getTidPosition(long tid) {
return TransactionConstant.LEN_TID_HEADER_LEN + (tid - 1) * TransactionConstant.TID_FIELD_SIZE;
}
而修改事務TID的狀態(tài)的方法也很簡單, 直接獲取到TID的開始位置,之后將待寫入的byte進行wrap為Buffer,寫入再force即可
long offset = getTidPosition(tid);
byte[] tmp = new byte[TransactionConstant.TID_FIELD_SIZE];
tmp[0] = status; //修改狀態(tài)為status
ByteBuffer buffer = ByteBuffer.wrap(tmp);
try {
fileChannel.position(offset);
fileChannel.write(buffer);
} catch (IOException e) {
FaultHandler.forcedStop(e);
}
//每次寫buffer時候強制刷新一下,避免丟失
try {
fileChannel.force(false);
而檢查狀態(tài)就是讀取對應位置的Buffer通過buffer.array()獲取之后再進行比較即可
對于定義的相關的接口對于事務的相關操作,就是修改TID文件中的TID的狀態(tài); eg:
begin()
開始一個新的事務,所以首先tid ++,之后將該新事務的狀態(tài)設置為運行RUNNING, 再將頭部tidCounter數(shù)量加一寫入頭部
因為需要修改類屬性,在并發(fā)狀態(tài)下存在安全問題,所以使用ReentrantLock進行加鎖修改
tidCounter ++;
//修改數(shù)量
ByteBuffer buffer = ByteBuffer.wrap(ByteBufferParser.long2Byte(tidCounter));
try {
fileChannel.position(0);
fileChannel.write(buffer);
} catch (IOException e) {
FaultHandler.forcedStop(e);
}
//每次寫buffer時候強制刷新一下,避免丟失
try {
fileChannel.force(false);
this.counterLock.lock();
try {
long tid = tidCounter + 1; //當前事務
updateStatus(tid, TransactionConstant.FIELD_TRAN_RUNNING); //事務激活
incrTidCounter(); //頭部size增加
return tid;
} finally {
this.counterLock.unlock();
}
commit(tid)
提交事務,有了之前的操作,這里就很好實現(xiàn) — 直接修改事務TID文件中tid的狀態(tài)為commited即可
rollback(tid)
和commit同理, 直接修改TID的狀態(tài)(文件中)
isXXXX(tid)
狀態(tài)檢查,直接buffer讀取對應tid位置的事務的狀態(tài)再進行比較即可
tid文件創(chuàng)建
tid的文件創(chuàng)建可以直接利用File即可,因為TM的類屬性有RandomAccessFile和FileChannel,所以再初始化一下文章來源:http://www.zghlxwxcb.cn/news/detail-571490.html
//創(chuàng)建文件
File file = new File(path + TransactionConstant.TID_SUFFIX);
try {
if(!file.createNewFile()) {
//文件創(chuàng)建失敗已存在,直接粗暴處理,因為不能進行后續(xù)操作了
FaultHandler.forcedStop(new DatabaseException(ErrorCode.FILE_EXISTS));
}
}catch (Exception e) {
FaultHandler.forcedStop(e);
}
//查看文件是否可讀寫,直接調(diào)用File的接口
if(!file.canRead() || !file.canWrite()) {
FaultHandler.forcedStop(new DatabaseException(ErrorCode.FILE_CANNOT_READ_OR_WRITE));
}
//使用NIO進行文件讀寫
FileChannel fc = null;
RandomAccessFile raf = null; //與簡單IO不同,可以調(diào)轉(zhuǎn)到文件任何位置進行IO,訪問文件部分內(nèi)容
try {
raf = new RandomAccessFile(file,"rw"); //將file轉(zhuǎn)為隨機讀寫File,文件權限為rw
fc = raf.getChannel(); //利用randomAccessFile創(chuàng)建channel通道
} catch (FileNotFoundException e) {
FaultHandler.forcedStop(e);
}
//利用buffer和channel進行文件讀寫
//寫空的文件頭,將byte[]包裝為buffer
ByteBuffer buf = ByteBuffer.wrap(new byte[TransactionConstant.LEN_TID_HEADER_LEN]);
try {
fc.position(0); //RandomAccessFile定位到0處開始
fc.write(buf); //緩沖區(qū)寫入
} catch (IOException e) {
FaultHandler.forcedStop(e);
}
return new TransactionManagerImpl(raf, fc);
}
整個TM的實現(xiàn)都是依靠的tid文件的操作,主要是定義好規(guī)則,實現(xiàn)不難,這里的兩重點技術: 可重入鎖保證線程安全 + NIO方式進行文件操作提升性能??文章來源地址http://www.zghlxwxcb.cn/news/detail-571490.html
到了這里,關于【cfengDB】自己實現(xiàn)數(shù)據(jù)庫第0節(jié) ---整體介紹及事務管理層實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!