leveldb程式碼精讀 資料庫啟動和初始化
基本概念
資料庫主要包括cache、日誌檔案、資料檔案、CURRENT檔案和manifest檔案幾大塊。
所有檔案都是依照型別+檔案號的命名規則,檔案號非常重要,類似oracle的sequence#。
其中cache是當前寫入的資料快取,寫入資料時
1 將資料寫入日誌檔案
2 將資料放入cache
當達到一定條件,如cache裡的資料夠多時
1 將cache轉成只讀cache,供查詢用。
2 新建一個讀寫cache和一個新日誌檔案。
3 將只讀cache裡的資料寫到資料檔案裡,這種新生成的資料檔案稱為level 0。
由於每次寫檔案後都會啟用新的cache,寫入的key可能和已有資料重複,將來也會落地成檔案。
因此leveldb會對檔案進行“壓縮”,在多個level 0檔案中給key去重,稱為level 1。
隨著資料庫執行,level 1還會進一步和level n+1的檔案合併,以此類推。
每次合併後刪除老檔案。
這樣做的結果就是從level 1開始,每層的檔案不會有重複的key,層與層之間的檔案也不會有重複的key,大大提高從檔案查詢的效率。
由於所有檔案的內容都是排序的,並且記錄了key的範圍,因此這些操作的成本非常可控。
當資料庫重啟時會根據日誌檔案重新構建cache並將cache裡的資料寫到level 0檔案裡。
上述所有資訊稱為資料庫的一個Version。
Version的增量叫VersionEdit,VersionEdit記錄了增加哪些檔案,減少哪些檔案,以及檔案的概要資訊等。
version1 + versionedit1 = version2
資料庫裡可能會儲存多個version資訊,所有version的載體是VersionSet。
這些資訊還記錄在manifest檔案內,相當於oracle的控制檔案。
CURRENT檔案記錄資料庫當前manifest檔案的檔名。
資料庫當機後,例項恢復的流程就是
1 根據檔案命名規則,在資料夾尋找之前的CURRENT
2 尋找之前的manifest
3 定位logfile
4 將logfile資料復原到cache
5 將cache裡的資料寫到level 0資料檔案
下面看程式碼
初始化資料庫包括新建資料庫和開啟已關閉的資料庫兩種情況
功能入口是DB::Open
點選(此處)摺疊或開啟
-
Status DB::Open(const Options& options, const std::string& dbname,
-
DB** dbptr) {
-
*dbptr = NULL;
-
-
-
/*
-
DBImpl是leveldb的“主類”,初始化配置資訊
-
dbname是資料庫的所在目錄
-
*/
-
DBImpl* impl = new DBImpl(options, dbname);
-
impl->mutex_.Lock();
-
-
-
/*
-
開啟資料庫,如果不存在就建立。
-
如果存在就檢查manifest檔案,並根據其中的資訊建立一個VersionEdit,用這個VersionEdit還原資料庫的Version。
-
*/
-
VersionEdit edit;
-
Status s = impl->Recover(&edit); // Handles create_if_missing, error_if_exists
-
if (s.ok()) {
-
uint64_t new_log_number = impl->versions_->NewFileNumber();
-
WritableFile* lfile;
-
-
-
/*
-
以w選項開啟一個新logfile。
-
lfile就是開啟的檔案,型別是PosixWritableFile,封裝了一些IO操作。
-
*/
-
s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),
-
&lfile);
-
if (s.ok()) {
-
// 初始化當前logfile檔案號
-
edit.SetLogNumber(new_log_number);
-
impl->logfile_ = lfile;
-
impl->logfile_number_ = new_log_number;
-
// log::Writer用來寫日誌,公共函式就一個AddRecord
-
impl->log_ = new log::Writer(lfile);
-
-
-
/*
-
1 結合VersionSet,進一步設定VersionEdit,
-
2 根據當前logfile號建立一個新version作為current version
-
3 建立manifest檔案
-
4 建立一個snapshot
-
*/
-
s = impl->versions_->LogAndApply(&edit, &impl->mutex_);
-
}
-
if (s.ok()) {
-
// 根據檔案號等條件,刪除不再需要的檔案。
-
impl->DeleteObsoleteFiles();
-
// 進行一次壓縮,把重複鍵值的檔案由低階向高階合併。
-
impl->MaybeScheduleCompaction();
-
}
-
}
-
impl->mutex_.Unlock();
-
if (s.ok()) {
-
*dbptr = impl;
-
} else {
-
delete impl;
-
}
-
return s;
- }
下面是Open函式一上來呼叫的建構函式
點選(此處)摺疊或開啟
-
DBImpl::DBImpl(const Options& raw_options, const std::string& dbname)
-
/*
-
leveldb將系統相關的操作封裝到env裡,
-
linux版的env是PosixEnv類,在util/env_posix.cc裡
-
*/
-
: env_(raw_options.env),
-
// comparator用來比較key的大小,可以自己實現,也可以用leveldb預設的
-
internal_comparator_(raw_options.comparator),
-
// 定義過濾資料的演算法,比如布隆過濾等
-
internal_filter_policy_(raw_options.filter_policy),
-
// SanitizeOptions函式用來對引數raw_options的選項進行必要的處理,並封裝成一個處理後的Options
-
options_(SanitizeOptions(dbname, &internal_comparator_,
-
&internal_filter_policy_, raw_options)),
-
owns_info_log_(options_.info_log != raw_options.info_log),
-
owns_cache_(options_.block_cache != raw_options.block_cache),
-
// dbname其實是指定資料庫的資料夾,所有資料檔案都會放到這個資料夾下。
-
dbname_(dbname),
-
-
-
// 用於鎖檔案,linux的是PosixFileLock類,裡面有一個檔案描述符和一個字串
-
db_lock_(NULL),
-
shutting_down_(NULL),
-
-
-
// 管理後臺執行緒的併發
-
bg_cv_(&mutex_),
-
-
-
/*
-
下面兩個memtable,是對SkipList類的封裝。
-
關於SkipList, 參考我之前的部落格 http://blog.itpub.net/26239116/viewspace-1839630/
-
具體封裝方式是在memtable.h裡定義typedef,並設定為成員變數
-
typedef SkipList<const char*, KeyComparator> Table;
-
...
-
Table table_;
-
*/
-
mem_(new MemTable(internal_comparator_)),
-
imm_(NULL),
-
logfile_(NULL),
-
logfile_number_(0),
-
log_(NULL),
-
seed_(0),
-
// 檔案IO的工具
-
tmp_batch_(new WriteBatch),
-
bg_compaction_scheduled_(false),
-
manual_compaction_(NULL) {
-
mem_->Ref();
-
has_imm_.Release_Store(NULL);
-
-
-
// Reserve ten files or so for other uses and give the rest to TableCache.
-
/*
-
最大檔案數減去預留的輔助檔案數,需要維護資料的檔案數
-
在建立TableCache的時候,這個檔案數對應的是TableCache裡lrucache的數量
-
關於lrucache,參考文獻之前的部落格 http://blog.itpub.net/26239116/viewspace-1842049/
-
*/
-
const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles;
-
table_cache_ = new TableCache(dbname_, &options_, table_cache_size);
-
-
-
// 初始化VersionSet,用於存放資料庫裡所有version
-
versions_ = new VersionSet(dbname_, &options_, table_cache_,
-
&internal_comparator_);
- }
建構函式里調的SanitizeOptions的具體內容。對option做了一下預處理
點選(此處)摺疊或開啟
-
Options SanitizeOptions(const std::string& dbname,
-
const InternalKeyComparator* icmp,
-
const InternalFilterPolicy* ipolicy,
-
const Options& src) {
-
// 複製一份呼叫者提供的Options,做返回值用。
-
Options result = src;
-
result.comparator = icmp;
-
result.filter_policy = (src.filter_policy != NULL) ? ipolicy : NULL;
-
-
-
// 校驗數值型引數,太大的設定為最大值,太小的設定為最小值
-
ClipToRange(&result.max_open_files, 64 + kNumNonTableCacheFiles, 50000);
-
ClipToRange(&result.write_buffer_size, 64<<10, 1<<30);
-
ClipToRange(&result.block_size, 1<<10, 4<<20);
-
-
-
// 如果沒有指定日誌,建立一個。
-
if (result.info_log == NULL) {
-
// Open a log file in the same directory as the db
-
src.env->CreateDir(dbname); // In case it does not exist
-
/*
-
日誌檔案的命名規則是 dbname + "/LOG",
-
建立之前需要先原有的重新命名為 dbname + "/LOG.old"
-
*/
-
src.env->RenameFile(InfoLogFileName(dbname), OldInfoLogFileName(dbname));
-
-
-
/*
-
建立一個logger,對於linux平臺,是建立一個PosixLogger
-
*/
-
Status s = src.env->NewLogger(InfoLogFileName(dbname), &result.info_log);
-
if (!s.ok()) {
-
// No place suitable for logging
-
result.info_log = NULL;
-
}
-
}
-
-
-
/*
-
建立一個lru cache
-
關於lru cache,參考我前面的部落格 http://blog.itpub.net/26239116/viewspace-1842049/
-
*/
-
if (result.block_cache == NULL) {
-
result.block_cache = NewLRUCache(8 << 20);
-
}
-
return result;
- }
資料庫初始化的主要工作由DBImpl::Recover函式完成
點選(此處)摺疊或開啟
-
Status DBImpl::Recover(VersionEdit* edit) {
-
mutex_.AssertHeld();
-
-
-
// Ignore error from CreateDir since the creation of the DB is
-
// committed only when the descriptor is created, and this directory
-
// may already exist from a previous failed creation attempt.
-
env_->CreateDir(dbname_);
-
assert(db_lock_ == NULL);
-
-
-
/*
-
建立檔案鎖,格式是"dbname_/LOC"
-
linux版的env是env_posix.cc
-
大致過程是
-
1 對檔案資訊進行一系列記錄,如記錄到PosixFileLock類裡。
-
2 建立一個“鎖”檔案,寫入flock結構體寫進去。flock定義在fcntl.h裡。
-
*/
-
Status s = env_->LockFile(LockFileName(dbname_), &db_lock_);
-
if (!s.ok()) {
-
return s;
-
}
-
-
/*
-
判斷current檔案是否存在,格式是“dbname_/CURRENT”
-
用來記錄當前的manifest檔名,madifest檔案裡記錄當前資料庫的概要資訊,類似oracle的控制檔案
-
*/
-
if (!env_->FileExists(CurrentFileName(dbname_))) {
-
if (options_.create_if_missing) {
-
// 初始化一個新的VersionEdit,設定logfile資訊,然後寫入manifest檔案。
-
s = NewDB();
-
if (!s.ok()) {
-
return s;
-
}
-
} else {
-
return Status::InvalidArgument(
-
dbname_, "does not exist (create_if_missing is false)");
-
}
-
} else {
-
if (options_.error_if_exists) {
-
return Status::InvalidArgument(
-
dbname_, "exists (error_if_exists is true)");
-
}
-
}
-
-
-
// 如果之前存在這個資料庫,開啟時需要做恢復工作,manifest檔案裡的資訊應用的當前version。
-
s = versions_->Recover();
-
if (s.ok()) {
-
SequenceNumber max_sequence(0);
-
-
-
// Recover from all newer log files than the ones named in the
-
// descriptor (new log files may have been added by the previous
-
// incarnation without registering them in the descriptor).
-
//
-
// Note that PrevLogNumber() is no longer used, but we pay
-
// attention to it in case we are recovering a database
-
// produced by an older version of leveldb.
-
const uint64_t min_log = versions_->LogNumber();
-
const uint64_t prev_log = versions_->PrevLogNumber();
-
std::vector<std::string> filenames;
-
s = env_->GetChildren(dbname_, &filenames);
-
if (!s.ok()) {
-
return s;
-
}
-
std::set<uint64_t> expected;
-
-
-
// 從所有version裡獲得所有檔案號
-
versions_->AddLiveFiles(&expected);
-
uint64_t number;
-
FileType type;
-
std::vector<uint64_t> logs;
-
-
-
/*
-
從資料夾內所有檔名中提取檔案型別和檔案號
-
從versions_提取的expected中移除這些檔案號
-
同時記錄其中有哪些log檔案
-
*/
-
for (size_t i = 0; i < filenames.size(); i++) {
-
if (ParseFileName(filenames[i], &number, &type)) {
-
expected.erase(number);
-
if (type == kLogFile && ((number >= min_log) || (number == prev_log)))
-
logs.push_back(number);
-
}
-
}
-
-
-
// 如果expected的內容麼有全部清空,說明丟失檔案了。
-
if (!expected.empty()) {
-
char buf[50];
-
snprintf(buf, sizeof(buf), "%d missing files; e.g.",
-
static_cast<int>(expected.size()));
-
return Status::Corruption(buf, TableFileName(dbname_, *(expected.begin())));
-
}
-
-
-
// 按順序逐個恢復log檔案
-
// Recover in the order in which the logs were generated
-
std::sort(logs.begin(), logs.end());
-
for (size_t i = 0; i < logs.size(); i++) {
-
s = RecoverLogFile(logs[i], edit, &max_sequence);
-
-
-
// The previous incarnation may not have written any MANIFEST
-
// records after allocating this log number. So we manually
-
// update the file number allocation counter in VersionSet.
-
versions_->MarkFileNumberUsed(logs[i]);
-
}
-
-
-
if (s.ok()) {
-
if (versions_->LastSequence() < max_sequence) {
-
versions_->SetLastSequence(max_sequence);
-
}
-
}
-
}
-
-
-
return s;
- }
由於DBImpl::Recover前面已經判斷過CURRENT檔案是否存在,如果不存在就建立新資料庫了,
因此這裡就是要處理新的或者曾經關閉過的資料庫。
點選(此處)摺疊或開啟
-
Status VersionSet::Recover() {
-
struct LogReporter : public log::Reader::Reporter {
-
Status* status;
-
virtual void Corruption(size_t bytes, const Status& s) {
-
if (this->status->ok()) *this->status = s;
-
}
-
};
-
-
-
// Read "CURRENT" file, which contains a pointer to the current manifest file
-
std::string current;
-
// 從CURRENT檔案把當前的manifest檔名讀到字串裡current裡
-
Status s = ReadFileToString(env_, CurrentFileName(dbname_), ¤t);
-
if (!s.ok()) {
-
return s;
-
}
-
-
-
/*
-
CURRENT檔案應該只有一行,就是當前manifest檔名。
-
要求CURRENT檔案必須以換行符結尾
-
這樣resize後就只剩檔名了。
-
*/
-
if (current.empty() || current[current.size()-1] != '\n') {
-
return Status::Corruption("CURRENT file does not end with newline");
-
}
-
current.resize(current.size() - 1);
-
-
// 開啟manifest檔案
-
std::string dscname = dbname_ + "/" + current;
-
SequentialFile* file;
-
s = env_->NewSequentialFile(dscname, &file);
-
if (!s.ok()) {
-
return s;
-
}
-
-
// 初始化logfile資訊,構建一個新的Version作為current version
-
bool have_log_number = false;
-
bool have_prev_log_number = false;
-
bool have_next_file = false;
-
bool have_last_sequence = false;
-
uint64_t next_file = 0;
-
uint64_t last_sequence = 0;
-
uint64_t log_number = 0;
-
uint64_t prev_log_number = 0;
-
-
-
/*
-
此時的current_,也就是current version是之前在建構函式里初始化的
-
AppendVersion(new Version(this));
-
在AppendVersion中會把新初始化的Version作為current version
-
current_ = v;
-
因此到builder這一步,一切都值是初始化,還沒有正式開始真正恢復操作
-
*/
-
Builder builder(this, current_);
-
-
-
{
-
LogReporter reporter;
-
reporter.status = &s;
-
log::Reader reader(file, &reporter, true/*checksum*/, 0/*initial_offset*/);
-
Slice record;
-
std::string scratch;
-
// 解析manifest的內容,還原出上次關閉的資料庫的logfile等資訊,裝入VersionEdit
-
while (reader.ReadRecord(&record, &scratch) && s.ok()) {
-
/*
-
建立一個edit,要改變Version的狀態,就需要用VersionEdit
-
version1 + edit1 = verion2
-
*/
-
VersionEdit edit;
-
-
-
/*
-
leveldb的儲存是按一定格式的,需要decode還原
-
將manifest中讀到的資訊decode後放入edit,後面恢復會用到。
-
只是概要資訊,比如新檔案,刪除了哪些檔案等。
-
*/
-
s = edit.DecodeFrom(record);
-
if (s.ok()) {
-
if (edit.has_comparator_ &&
-
edit.comparator_ != icmp_.user_comparator()->Name()) {
-
s = Status::InvalidArgument(
-
edit.comparator_ + " does not match existing comparator ",
-
icmp_.user_comparator()->Name());
-
}
-
}
-
-
-
if (s.ok()) {
-
/*
-
將edit的內容封裝到builder裡。
-
*/
-
builder.Apply(&edit);
-
}
-
-
-
if (edit.has_log_number_) {
-
log_number = edit.log_number_;
-
have_log_number = true;
-
}
-
-
-
if (edit.has_prev_log_number_) {
-
prev_log_number = edit.prev_log_number_;
-
have_prev_log_number = true;
-
}
-
-
-
if (edit.has_next_file_number_) {
-
next_file = edit.next_file_number_;
-
have_next_file = true;
-
}
-
-
-
if (edit.has_last_sequence_) {
-
last_sequence = edit.last_sequence_;
-
have_last_sequence = true;
-
}
-
}
-
}
-
delete file;
-
file = NULL;
-
-
-
if (s.ok()) {
-
if (!have_next_file) {
-
s = Status::Corruption("no meta-nextfile entry in descriptor");
-
} else if (!have_log_number) {
-
s = Status::Corruption("no meta-lognumber entry in descriptor");
-
} else if (!have_last_sequence) {
-
s = Status::Corruption("no last-sequence-number entry in descriptor");
-
}
-
-
-
if (!have_prev_log_number) {
-
prev_log_number = 0;
-
}
-
-
-
MarkFileNumberUsed(prev_log_number);
-
MarkFileNumberUsed(log_number);
-
}
-
-
-
if (s.ok()) {
-
// 利用builder的資訊封裝一個新的version,追加到VersionSet裡
-
Version* v = new Version(this);
-
-
-
/*
-
builder前面從edit裡讀了manifest檔案,
-
SaveTo會將從manifest檔案裡讀到的檔案新增到Version.files_裡
-
在開啟資料庫操作的後面步驟裡,會讀取files_裡的檔案資訊,與目錄下的實體檔案進行對照,看檔案全不全。
-
這個過程就是version + edit,只不過這個version是新建的空version。
-
最終得到的是上次關閉的資料庫version
-
*/
-
builder.SaveTo(v);
-
// Install recovered version
-
// 根據各level的檔案大小計算一個“得分”,以後影響壓縮行為
-
Finalize(v);
-
// 將新封裝好的Version放到VersionSet裡,作為current version
-
AppendVersion(v);
-
manifest_file_number_ = next_file;
-
next_file_number_ = next_file + 1;
-
last_sequence_ = last_sequence;
-
log_number_ = log_number;
-
prev_log_number_ = prev_log_number;
-
}
-
-
-
return s;
- }
逐個恢復logfile
點選(此處)摺疊或開啟
-
Status DBImpl::RecoverLogFile(uint64_t log_number,
-
VersionEdit* edit,
-
SequenceNumber* max_sequence) {
-
struct LogReporter : public log::Reader::Reporter {
-
Env* env;
-
Logger* info_log;
-
const char* fname;
-
Status* status; // NULL if options_.paranoid_checks==false
-
virtual void Corruption(size_t bytes, const Status& s) {
-
Log(info_log, "%s%s: dropping %d bytes; %s",
-
(this->status == NULL ? "(ignoring error) " : ""),
-
fname, static_cast<int>(bytes), s.ToString().c_str());
-
if (this->status != NULL && this->status->ok()) *this->status = s;
-
}
-
};
-
-
-
mutex_.AssertHeld();
-
-
-
// Open the log file
-
std::string fname = LogFileName(dbname_, log_number);
-
SequentialFile* file;
-
// 只讀開啟logfile
-
Status status = env_->NewSequentialFile(fname, &file);
-
if (!status.ok()) {
-
MaybeIgnoreError(&status);
-
return status;
-
}
-
-
-
// Create the log reader.
-
LogReporter reporter;
-
reporter.env = env_;
-
reporter.info_log = options_.info_log;
-
reporter.fname = fname.c_str();
-
reporter.status = (options_.paranoid_checks ? &status : NULL);
-
// We intentionally make log::Reader do checksumming even if
-
// paranoid_checks==false so that corruptions cause entire commits
-
// to be skipped instead of propagating bad information (like overly
-
// large sequence numbers).
-
log::Reader reader(file, &reporter, true/*checksum*/,
-
0/*initial_offset*/);
-
Log(options_.info_log, "Recovering log #%llu",
-
(unsigned long long) log_number);
-
-
-
// Read all the records and add to a memtable
-
std::string scratch;
-
Slice record;
-
WriteBatch batch;
-
MemTable* mem = NULL;
-
/*
-
逐行讀取logfile
-
將得到的資料放入memtable mm裡
-
*/
-
while (reader.ReadRecord(&record, &scratch) &&
-
status.ok()) {
-
if (record.size() < 12) {
-
reporter.Corruption(
-
record.size(), Status::Corruption("log record too small"));
-
continue;
-
}
-
WriteBatchInternal::SetContents(&batch, record);
-
-
-
if (mem == NULL) {
-
mem = new MemTable(internal_comparator_);
-
mem->Ref();
-
}
-
status = WriteBatchInternal::InsertInto(&batch, mem);
-
MaybeIgnoreError(&status);
-
if (!status.ok()) {
-
break;
-
}
-
const SequenceNumber last_seq =
-
WriteBatchInternal::Sequence(&batch) +
-
WriteBatchInternal::Count(&batch) - 1;
-
if (last_seq > *max_sequence) {
-
*max_sequence = last_seq;
-
}
-
-
// 當memtable使用量超過設定的值時,將資料刷到level 0 資料檔案裡。
- // 這樣在恢復過程中不會將記憶體撐爆
-
if (mem->ApproximateMemoryUsage() > options_.write_buffer_size) {
-
status = WriteLevel0Table(mem, edit, NULL);
-
if (!status.ok()) {
-
// Reflect errors immediately so that conditions like full
-
// file-systems cause the DB::Open() to fail.
-
break;
-
}
-
mem->Unref();
-
mem = NULL;
-
}
-
}
-
-
-
if (status.ok() && mem != NULL) {
-
status = WriteLevel0Table(mem, edit, NULL);
-
// Reflect errors immediately so that conditions like full
-
// file-systems cause the DB::Open() to fail.
-
}
-
-
-
if (mem != NULL) mem->Unref();
-
delete file;
-
return status;
- }
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26239116/viewspace-1846192/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- leveldb程式碼精讀 插入操作
- leveldb程式碼精讀 lru cache
- leveldb程式碼精讀 記憶體池Arena記憶體
- leveldb 程式碼閱讀三
- LevelDB C++教程: 如何開啟和關閉資料庫C++資料庫
- Docker容器啟動時初始化Mysql資料庫DockerMySql資料庫
- 啟動和停止資料庫.資料庫
- Fabric 1.0原始碼分析(23)LevelDB(KV資料庫)原始碼資料庫
- 解讀MySQL 8.0資料字典的初始化與啟動MySql
- 3.1.2 啟動時指定資料庫初始化引數資料庫
- 如何解析 Ethereum 資料:讀取 LevelDB 資料
- 啟動oracle資料庫到只讀模式Oracle資料庫模式
- 資料庫啟動和關閉資料庫
- 3.1.2.1 關於資料庫初始化引數檔案和啟動的關係資料庫
- ajax讀取資料庫資料程式碼例項資料庫
- 【指令碼】快速啟動和關閉Windows上的資料庫指令碼Windows資料庫
- 利用dbstart和dbshut指令碼自動啟動和停止資料庫的問題指令碼資料庫
- 【原始碼解讀】asp.net core原始碼啟動流程精細解讀原始碼ASP.NET
- informix 資料庫啟動關閉指令碼ORM資料庫指令碼
- Oracle資料庫的啟動和關閉Oracle資料庫
- ResNet程式碼精讀
- LINUX開機自動啟動ORACLE資料庫和監聽指令碼LinuxOracle資料庫指令碼
- 3.1 啟動資料庫資料庫
- 啟動MySql資料庫MySql資料庫
- 資料庫的啟動資料庫
- 啟動MongoDB資料庫MongoDB資料庫
- LevelDB 程式碼擼起來!
- 延緩Spring Boot啟動時間直到資料庫啟動的方法和原始碼 - MartenSpring Boot資料庫原始碼
- SpringBoot druid配置datasource啟動初始化資料庫連線Spring BootUI資料庫
- 惡意程式造成資料庫啟動報錯資料庫
- ORACLE資料庫的啟動和關閉(轉)Oracle資料庫
- dataguard standby資料庫的關閉和啟動資料庫
- Oracle學習系列—Windows下資料庫程式的啟動和關閉OracleWindows資料庫
- 資料庫系統檔案啟動資料庫資料庫
- 3.1.2.3 通過 SRVCTL 使用非預設初始化引數啟動資料庫資料庫
- 啟動資料庫監聽資料庫
- MySQL資料庫如何啟動?MySql資料庫
- 資料庫啟動過程資料庫