共计 2603 个字符,预计需要花费 7 分钟才能阅读完成。
日志系统是保证数据库管理系统正确执行事务的基本机制。根据作用的不同,日志系统分为 undo 和 redo 两种,本文对 undo 类型日志的原理进行简单模拟说明。
1 UNDO 日志要求
- 日志记录了数据修改之前的旧值;
- 数据刷盘之前,把日志刷盘;(一致性)
- 数据刷盘之后,把日志 COMMIT 刷盘。(持久性)
2 UNDO 日志缺陷
UNDO 日志提供了足够的信息可以保证事务的一致性和持久性。但是,为了保持一致性,采取的是被动保守的策略,即:用旧值覆盖不能确保成功的事务。 未成功的事务不能重新执行,只能恢复到事务之前的一致状态。
2 模拟代码
只是模拟了数据写入的过程,没有模拟数据恢复过程,待以后有时间补充。
/* UNDO 类型日志基本流程模拟 */ | |
/* 日志文件, 基本格式: | |
* T1_START | |
* A=100 | |
* B=100 | |
* T1_COMMIT | |
*/ | |
/* 数据文件, 基本格式: 每行一个键值对,长度固定为 1024,右侧用空格填充。* A=100 (padding) | |
* B=100 (padding) | |
*/ | |
/* 键值对 */ | |
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=100 | |
B=100 |
上述代码执行后:
数据文件内容如下:
A=50 | |
B=100 |
日志文件内容如下:
T1 START | |
T1:A=100 | |
T1:B=100 |
根据日志文件,由于没有找到 T1 COMMIT,所以断定事务 T1 未能成功,数据可能处于不一致状态,需要数据恢复。进而根据日志文件,可以得到事务 T1 执行之前的数据旧值 A =100,B=100,恢复也就很容易了,只要把 A,B 的值都更新为其对应的旧值就可以了。
恢复之后的数据文件:
A=100 | |
B=100 |
更多 Oracle 相关信息见 Oracle 专题页面 http://www.linuxidc.com/topicnews.aspx?tid=12
本文永久更新链接地址 :http://www.linuxidc.com/Linux/2018-01/150490.htm
正文完
星哥玩云-微信公众号
