初識 undo類型日誌系統
日誌系統是保證資料庫管理系統正確執行事務的基本機制。根據作用的不同,日誌系統分為undo和redo兩種,本文對undo類型日誌的原理進行簡單類比說明。
1 UNDO日誌要求
- 日誌記錄了資料修改之前的舊值;
- 資料刷盤之前,把日誌刷盤;(一致性)
- 資料刷盤之後,把日誌COMMIT刷盤。(持久性)
2 UNDO日誌缺陷
UNDO日誌提供了足夠的資訊可以保證事務的一致性和持久性。但是,為了保持一致性,採取的是被動保守的策略,即:用舊值覆蓋不能確保成功的事務。未成功的事務不能重新執行,只能恢複到事務之前的一致狀態。
2 類比代碼
只是類比了資料寫入的過程,沒有類比資料恢複過程,待以後有時間補充。
/* UNDO 類型日誌基本流程類比 */#include <stdio.h>#include <string.h>#include <stdlib.h>#include <err.h>/* 記錄檔,基本格式: * T1_START * A=100 * B=100 * T1_COMMIT */#define LOG_FILE "test.log"/* 資料檔案,基本格式: 每行一個索引值對,長度固定為1024,右側用空格填充。 * A=100 (padding) * B=100 (padding) */#define DATA_FILE "test.data"#define LINE_MAX 1024/* 索引值對 */typedef struct KV KV;struct KV{ char* key; char* value;};/* 從硬碟讀取名為k的資料 */static int fread_kv(char* k, char* v, FILE* fp){ rewind(fp); char line[LINE_MAX+1]={0}; int lineNo = 0; while(fread(line, 1, LINE_MAX, fp)==LINE_MAX){ int i=0; while(k[i] && line[i]!='='){ if(k[i] != line[i]){ break; } i++; } if(line[i] == '='){ strcpy(v, &line[i+1]); return lineNo; 1,1 Top } lineNo ++; } return -1;}/* 把資料刷入硬碟 */static void fwrite_kv( char* k, char* v, FILE* fp){ int lineNo = -1; char newLine[LINE_MAX]; sprintf(newLine, "%s=%s", k, v); int offset = 0; if( (lineNo=fread_kv(k,v,fp))<0) /* insert */ { offset = fseek(fp, 0, SEEK_END); }else{ /* update */ offset = fseek(fp, LINE_MAX * lineNo, 0); } fprintf(fp, "%-1023s\n", newLine); fflush(fp);}int main(int argc, char** argv){ FILE* fpLog = fopen(LOG_FILE, "r+"); FILE* fpData = fopen(DATA_FILE, "r+"); if(!(fpLog && fpData)){ perror(NULL); } /* 開始一個事務 */ char log[1024] = "T1 START\n"; /* 在記憶體中執行事務操作 */ char v[LINE_MAX]; fread_kv("A", v, fpData); int A = atoi(v); fread_kv("B", v, fpData); int B = atoi(v); char logA[1024]; sprintf(logA, "T1:A=%d\n", A); /*在日誌中記錄舊值*/ strcat(log, logA); A -= 50; char logB[1024]; sprintf(logB, "T1:B=%d\n", B); /*在日誌中記錄舊值*/ strcat(log, logB); B += 50; /************** 如果此時發生故障,日誌和資料均尚未寫出到硬碟上, 事務丟失,但保持資料庫一致性.*************/ /* 資料刷盤之前,先把日誌刷入硬碟 */ fputs(log, fpLog); fflush(fpLog); /************* 如果此時發生故障,日誌舊值已經被寫出到硬碟上,資料尚未寫入,恢複時需要把舊值恢複.(隨然資料未刷盤,但並不可知)********/ /* 把資料新值刷入硬碟 */ sprintf(v, "%d", A); fwrite_kv("A", v, fpData); /************* 如果此時發生故障,日誌舊值已經被寫出到硬碟上,資料尚未寫入,恢複時需要把舊值恢複.********/ abort(); /* 類比故障 */ sprintf(v, "%d", B); fwrite_kv("B", v, fpData); /************* 如果此時發生故障,日誌舊值已經被寫出到硬碟上,資料已經寫入硬碟,但是COMMIT日誌未寫出,也需要恢複舊值.(雖然資料已完整刷盤,但並不可知)********/ /* 資料刷盤之後,把提交日誌刷入硬碟*/ fputs("T1 COMMIT\n", fpLog); fflush(fpLog); /************* 如果此時發生故障,日誌COMMIT已刷盤,能夠確保資料也已經刷盤成功。無需恢複********/ fclose(fpLog); fclose(fpData); return 0;}
代碼執行之前:
資料檔案內容如下:
A=100B=100
上述代碼執行後:
資料檔案內容如下:
A=50B=100
記錄檔內容如下:
T1 STARTT1:A=100T1:B=100
根據記錄檔,由於沒有找到T1 COMMIT,所以斷定事務T1未能成功,資料可能處於不一致狀態,需要資料恢複。進而根據記錄檔,可以得到事務T1執行之前的資料舊值A=100,B=100,恢複也就很容易了,只要把A,B的值都更新為其對應的舊值就可以了。
恢複之後的資料檔案:
A=100B=100