leveldb程式碼精讀 資料庫啟動和初始化

liiinuuux發表於2015-11-25


基本概念
資料庫主要包括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

點選(此處)摺疊或開啟

  1. Status DB::Open(const Options& options, const std::string& dbname,
  2.                 DB** dbptr) {
  3.   *dbptr = NULL;


  4.   /*
  5.   DBImpl是leveldb的“主類”,初始化配置資訊
  6.   dbname是資料庫的所在目錄
  7.   */
  8.   DBImpl* impl = new DBImpl(options, dbname);
  9.   impl->mutex_.Lock();


  10.   /*
  11.   開啟資料庫,如果不存在就建立。
  12.   如果存在就檢查manifest檔案,並根據其中的資訊建立一個VersionEdit,用這個VersionEdit還原資料庫的Version。
  13.   */
  14.   VersionEdit edit;
  15.   Status s = impl->Recover(&edit); // Handles create_if_missing, error_if_exists
  16.   if (s.ok()) {
  17.     uint64_t new_log_number = impl->versions_->NewFileNumber();
  18.     WritableFile* lfile;


  19.     /*
  20.     以w選項開啟一個新logfile。
  21.     lfile就是開啟的檔案,型別是PosixWritableFile,封裝了一些IO操作。
  22.     */
  23.     s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),
  24.                                      &lfile);
  25.     if (s.ok()) {
  26.       // 初始化當前logfile檔案號
  27.       edit.SetLogNumber(new_log_number);
  28.       impl->logfile_ = lfile;
  29.       impl->logfile_number_ = new_log_number;
  30.       // log::Writer用來寫日誌,公共函式就一個AddRecord
  31.       impl->log_ = new log::Writer(lfile);


  32.       /*
  33.       1 結合VersionSet,進一步設定VersionEdit,
  34.       2 根據當前logfile號建立一個新version作為current version
  35.       3 建立manifest檔案
  36.       4 建立一個snapshot
  37.       */
  38.       s = impl->versions_->LogAndApply(&edit, &impl->mutex_);
  39.     }
  40.     if (s.ok()) {
  41.       // 根據檔案號等條件,刪除不再需要的檔案。
  42.       impl->DeleteObsoleteFiles();
  43.       // 進行一次壓縮,把重複鍵值的檔案由低階向高階合併。
  44.       impl->MaybeScheduleCompaction();
  45.     }
  46.   }
  47.   impl->mutex_.Unlock();
  48.   if (s.ok()) {
  49.     *dbptr = impl;
  50.   } else {
  51.     delete impl;
  52.   }
  53.   return s;
  54. }


下面是Open函式一上來呼叫的建構函式

點選(此處)摺疊或開啟

  1. DBImpl::DBImpl(const Options& raw_options, const std::string& dbname)
  2.       /*
  3.       leveldb將系統相關的操作封裝到env裡,
  4.       linux版的env是PosixEnv類,在util/env_posix.cc裡
  5.       */
  6.     : env_(raw_options.env),
  7.       // comparator用來比較key的大小,可以自己實現,也可以用leveldb預設的
  8.       internal_comparator_(raw_options.comparator),
  9.       // 定義過濾資料的演算法,比如布隆過濾等
  10.       internal_filter_policy_(raw_options.filter_policy),
  11.       // SanitizeOptions函式用來對引數raw_options的選項進行必要的處理,並封裝成一個處理後的Options
  12.       options_(SanitizeOptions(dbname, &internal_comparator_,
  13.                                &internal_filter_policy_, raw_options)),
  14.       owns_info_log_(options_.info_log != raw_options.info_log),
  15.       owns_cache_(options_.block_cache != raw_options.block_cache),
  16.       // dbname其實是指定資料庫的資料夾,所有資料檔案都會放到這個資料夾下。
  17.       dbname_(dbname),


  18.       // 用於鎖檔案,linux的是PosixFileLock類,裡面有一個檔案描述符和一個字串
  19.       db_lock_(NULL),
  20.       shutting_down_(NULL),


  21.       // 管理後臺執行緒的併發
  22.       bg_cv_(&mutex_),


  23.       /*
  24.       下面兩個memtable,是對SkipList類的封裝。
  25.       關於SkipList, 參考我之前的部落格 http://blog.itpub.net/26239116/viewspace-1839630/
  26.       具體封裝方式是在memtable.h裡定義typedef,並設定為成員變數
  27.       typedef SkipList<const char*, KeyComparator> Table;
  28.       ...
  29.       Table table_;
  30.       */
  31.       mem_(new MemTable(internal_comparator_)),
  32.       imm_(NULL),
  33.       logfile_(NULL),
  34.       logfile_number_(0),
  35.       log_(NULL),
  36.       seed_(0),
  37.       // 檔案IO的工具
  38.       tmp_batch_(new WriteBatch),
  39.       bg_compaction_scheduled_(false),
  40.       manual_compaction_(NULL) {
  41.   mem_->Ref();
  42.   has_imm_.Release_Store(NULL);


  43.   // Reserve ten files or so for other uses and give the rest to TableCache.
  44.   /*
  45.   最大檔案數減去預留的輔助檔案數,需要維護資料的檔案數
  46.   在建立TableCache的時候,這個檔案數對應的是TableCache裡lrucache的數量
  47.   關於lrucache,參考文獻之前的部落格 http://blog.itpub.net/26239116/viewspace-1842049/
  48.   */
  49.   const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles;
  50.   table_cache_ = new TableCache(dbname_, &options_, table_cache_size);


  51.   // 初始化VersionSet,用於存放資料庫裡所有version
  52.   versions_ = new VersionSet(dbname_, &options_, table_cache_,
  53.                              &internal_comparator_);
  54. }





建構函式里調的SanitizeOptions的具體內容。對option做了一下預處理

點選(此處)摺疊或開啟

  1. Options SanitizeOptions(const std::string& dbname,
  2.                         const InternalKeyComparator* icmp,
  3.                         const InternalFilterPolicy* ipolicy,
  4.                         const Options& src) {
  5.   // 複製一份呼叫者提供的Options,做返回值用。
  6.   Options result = src;
  7.   result.comparator = icmp;
  8.   result.filter_policy = (src.filter_policy != NULL) ? ipolicy : NULL;


  9.   // 校驗數值型引數,太大的設定為最大值,太小的設定為最小值
  10.   ClipToRange(&result.max_open_files, 64 + kNumNonTableCacheFiles, 50000);
  11.   ClipToRange(&result.write_buffer_size, 64<<10, 1<<30);
  12.   ClipToRange(&result.block_size, 1<<10, 4<<20);


  13.   // 如果沒有指定日誌,建立一個。
  14.   if (result.info_log == NULL) {
  15.     // Open a log file in the same directory as the db
  16.     src.env->CreateDir(dbname); // In case it does not exist
  17.     /*
  18.     日誌檔案的命名規則是 dbname + "/LOG"
  19.     建立之前需要先原有的重新命名為 dbname + "/LOG.old"
  20.     */
  21.     src.env->RenameFile(InfoLogFileName(dbname), OldInfoLogFileName(dbname));


  22.     /*
  23.     建立一個logger,對於linux平臺,是建立一個PosixLogger
  24.     */
  25.     Status s = src.env->NewLogger(InfoLogFileName(dbname), &result.info_log);
  26.     if (!s.ok()) {
  27.       // No place suitable for logging
  28.       result.info_log = NULL;
  29.     }
  30.   }


  31.   /*
  32.   建立一個lru cache
  33.   關於lru cache,參考我前面的部落格 http://blog.itpub.net/26239116/viewspace-1842049/
  34.   */
  35.   if (result.block_cache == NULL) {
  36.     result.block_cache = NewLRUCache(8 << 20);
  37.   }
  38.   return result;
  39. }



資料庫初始化的主要工作由DBImpl::Recover函式完成

點選(此處)摺疊或開啟

  1. Status DBImpl::Recover(VersionEdit* edit) {
  2.   mutex_.AssertHeld();


  3.   // Ignore error from CreateDir since the creation of the DB is
  4.   // committed only when the descriptor is created, and this directory
  5.   // may already exist from a previous failed creation attempt.
  6.   env_->CreateDir(dbname_);
  7.   assert(db_lock_ == NULL);


  8.   /*
  9.     建立檔案鎖,格式是"dbname_/LOC"
  10.     linux版的env是env_posix.cc
  11.     大致過程是
  12.     1 對檔案資訊進行一系列記錄,如記錄到PosixFileLock類裡。
  13.     2 建立一個“鎖”檔案,寫入flock結構體寫進去。flock定義在fcntl.h裡。
  14.   */
  15.   Status s = env_->LockFile(LockFileName(dbname_), &db_lock_);
  16.   if (!s.ok()) {
  17.     return s;
  18.   }
  19.   
  20.   /*
  21.   判斷current檔案是否存在,格式是“dbname_/CURRENT”
  22.   用來記錄當前的manifest檔名,madifest檔案裡記錄當前資料庫的概要資訊,類似oracle的控制檔案
  23.   */
  24.   if (!env_->FileExists(CurrentFileName(dbname_))) {
  25.     if (options_.create_if_missing) {
  26.       // 初始化一個新的VersionEdit,設定logfile資訊,然後寫入manifest檔案。
  27.       s = NewDB();
  28.       if (!s.ok()) {
  29.         return s;
  30.       }
  31.     } else {
  32.       return Status::InvalidArgument(
  33.           dbname_, "does not exist (create_if_missing is false)");
  34.     }
  35.   } else {
  36.     if (options_.error_if_exists) {
  37.       return Status::InvalidArgument(
  38.           dbname_, "exists (error_if_exists is true)");
  39.     }
  40.   }


  41.   // 如果之前存在這個資料庫,開啟時需要做恢復工作,manifest檔案裡的資訊應用的當前version。
  42.   s = versions_->Recover();
  43.   if (s.ok()) {
  44.     SequenceNumber max_sequence(0);


  45.     // Recover from all newer log files than the ones named in the
  46.     // descriptor (new log files may have been added by the previous
  47.     // incarnation without registering them in the descriptor).
  48.     //
  49.     // Note that PrevLogNumber() is no longer used, but we pay
  50.     // attention to it in case we are recovering a database
  51.     // produced by an older version of leveldb.
  52.     const uint64_t min_log = versions_->LogNumber();
  53.     const uint64_t prev_log = versions_->PrevLogNumber();
  54.     std::vector<std::string> filenames;
  55.     s = env_->GetChildren(dbname_, &filenames);
  56.     if (!s.ok()) {
  57.       return s;
  58.     }
  59.     std::set<uint64_t> expected;


  60.     // 從所有version裡獲得所有檔案號
  61.     versions_->AddLiveFiles(&expected);
  62.     uint64_t number;
  63.     FileType type;
  64.     std::vector<uint64_t> logs;


  65.     /*
  66.     從資料夾內所有檔名中提取檔案型別和檔案號
  67.     從versions_提取的expected中移除這些檔案號
  68.     同時記錄其中有哪些log檔案
  69.     */
  70.     for (size_t i = 0; i < filenames.size(); i++) {
  71.       if (ParseFileName(filenames[i], &number, &type)) {
  72.         expected.erase(number);
  73.         if (type == kLogFile && ((number >= min_log) || (number == prev_log)))
  74.           logs.push_back(number);
  75.       }
  76.     }


  77.     // 如果expected的內容麼有全部清空,說明丟失檔案了。
  78.     if (!expected.empty()) {
  79.       char buf[50];
  80.       snprintf(buf, sizeof(buf), "%d missing files; e.g.",
  81.                static_cast<int>(expected.size()));
  82.       return Status::Corruption(buf, TableFileName(dbname_, *(expected.begin())));
  83.     }


  84.     // 按順序逐個恢復log檔案
  85.     // Recover in the order in which the logs were generated
  86.     std::sort(logs.begin(), logs.end());
  87.     for (size_t i = 0; i < logs.size(); i++) {
  88.       s = RecoverLogFile(logs[i], edit, &max_sequence);


  89.       // The previous incarnation may not have written any MANIFEST
  90.       // records after allocating this log number. So we manually
  91.       // update the file number allocation counter in VersionSet.
  92.       versions_->MarkFileNumberUsed(logs[i]);
  93.     }


  94.     if (s.ok()) {
  95.       if (versions_->LastSequence() < max_sequence) {
  96.         versions_->SetLastSequence(max_sequence);
  97.       }
  98.     }
  99.   }


  100.   return s;
  101. }



由於DBImpl::Recover前面已經判斷過CURRENT檔案是否存在,如果不存在就建立新資料庫了,
因此這裡就是要處理新的或者曾經關閉過的資料庫。

點選(此處)摺疊或開啟

  1. Status VersionSet::Recover() {
  2.   struct LogReporter : public log::Reader::Reporter {
  3.     Status* status;
  4.     virtual void Corruption(size_t bytes, const Status& s) {
  5.       if (this->status->ok()) *this->status = s;
  6.     }
  7.   };


  8.   // Read "CURRENT" file, which contains a pointer to the current manifest file
  9.   std::string current;
  10.   // 從CURRENT檔案把當前的manifest檔名讀到字串裡current裡
  11.   Status s = ReadFileToString(env_, CurrentFileName(dbname_), &current);
  12.   if (!s.ok()) {
  13.     return s;
  14.   }


  15.   /*
  16.   CURRENT檔案應該只有一行,就是當前manifest檔名。
  17.   要求CURRENT檔案必須以換行符結尾
  18.   這樣resize後就只剩檔名了。
  19.   */
  20.   if (current.empty() || current[current.size()-1] != '\n') {
  21.     return Status::Corruption("CURRENT file does not end with newline");
  22.   }
  23.   current.resize(current.size() - 1);
  24.   
  25.   // 開啟manifest檔案
  26.   std::string dscname = dbname_ + "/" + current;
  27.   SequentialFile* file;
  28.   s = env_->NewSequentialFile(dscname, &file);
  29.   if (!s.ok()) {
  30.     return s;
  31.   }
  32.  
  33.   // 初始化logfile資訊,構建一個新的Version作為current version
  34.   bool have_log_number = false;
  35.   bool have_prev_log_number = false;
  36.   bool have_next_file = false;
  37.   bool have_last_sequence = false;
  38.   uint64_t next_file = 0;
  39.   uint64_t last_sequence = 0;
  40.   uint64_t log_number = 0;
  41.   uint64_t prev_log_number = 0;


  42.   /*
  43.   此時的current_,也就是current version是之前在建構函式里初始化的
  44.   AppendVersion(new Version(this));
  45.   在AppendVersion中會把新初始化的Version作為current version
  46.   current_ = v;
  47.   因此到builder這一步,一切都值是初始化,還沒有正式開始真正恢復操作
  48.   */
  49.   Builder builder(this, current_);


  50.   {
  51.     LogReporter reporter;
  52.     reporter.status = &s;
  53.     log::Reader reader(file, &reporter, true/*checksum*/, 0/*initial_offset*/);
  54.     Slice record;
  55.     std::string scratch;
  56.     // 解析manifest的內容,還原出上次關閉的資料庫的logfile等資訊,裝入VersionEdit
  57.     while (reader.ReadRecord(&record, &scratch) && s.ok()) {
  58.       /*
  59.       建立一個edit,要改變Version的狀態,就需要用VersionEdit
  60.       version1 + edit1 = verion2
  61.       */
  62.       VersionEdit edit;


  63.       /*
  64.       leveldb的儲存是按一定格式的,需要decode還原
  65.       將manifest中讀到的資訊decode後放入edit,後面恢復會用到。
  66.       只是概要資訊,比如新檔案,刪除了哪些檔案等。
  67.       */
  68.       s = edit.DecodeFrom(record);
  69.       if (s.ok()) {
  70.         if (edit.has_comparator_ &&
  71.             edit.comparator_ != icmp_.user_comparator()->Name()) {
  72.           s = Status::InvalidArgument(
  73.               edit.comparator_ + " does not match existing comparator ",
  74.               icmp_.user_comparator()->Name());
  75.         }
  76.       }


  77.       if (s.ok()) {
  78.         /*
  79.         將edit的內容封裝到builder裡。
  80.         */
  81.         builder.Apply(&edit);
  82.       }


  83.       if (edit.has_log_number_) {
  84.         log_number = edit.log_number_;
  85.         have_log_number = true;
  86.       }


  87.       if (edit.has_prev_log_number_) {
  88.         prev_log_number = edit.prev_log_number_;
  89.         have_prev_log_number = true;
  90.       }


  91.       if (edit.has_next_file_number_) {
  92.         next_file = edit.next_file_number_;
  93.         have_next_file = true;
  94.       }


  95.       if (edit.has_last_sequence_) {
  96.         last_sequence = edit.last_sequence_;
  97.         have_last_sequence = true;
  98.       }
  99.     }
  100.   }
  101.   delete file;
  102.   file = NULL;


  103.   if (s.ok()) {
  104.     if (!have_next_file) {
  105.       s = Status::Corruption("no meta-nextfile entry in descriptor");
  106.     } else if (!have_log_number) {
  107.       s = Status::Corruption("no meta-lognumber entry in descriptor");
  108.     } else if (!have_last_sequence) {
  109.       s = Status::Corruption("no last-sequence-number entry in descriptor");
  110.     }


  111.     if (!have_prev_log_number) {
  112.       prev_log_number = 0;
  113.     }


  114.     MarkFileNumberUsed(prev_log_number);
  115.     MarkFileNumberUsed(log_number);
  116.   }


  117.   if (s.ok()) {
  118.     // 利用builder的資訊封裝一個新的version,追加到VersionSet裡
  119.     Version* v = new Version(this);


  120.     /*
  121.     builder前面從edit裡讀了manifest檔案,
  122.     SaveTo會將從manifest檔案裡讀到的檔案新增到Version.files_裡
  123.     在開啟資料庫操作的後面步驟裡,會讀取files_裡的檔案資訊,與目錄下的實體檔案進行對照,看檔案全不全。
  124.     這個過程就是version + edit,只不過這個version是新建的空version。
  125.     最終得到的是上次關閉的資料庫version
  126.     */
  127.     builder.SaveTo(v);
  128.     // Install recovered version
  129.     // 根據各level的檔案大小計算一個“得分”,以後影響壓縮行為
  130.     Finalize(v);
  131.     // 將新封裝好的Version放到VersionSet裡,作為current version
  132.     AppendVersion(v);
  133.     manifest_file_number_ = next_file;
  134.     next_file_number_ = next_file + 1;
  135.     last_sequence_ = last_sequence;
  136.     log_number_ = log_number;
  137.     prev_log_number_ = prev_log_number;
  138.   }


  139.   return s;
  140. }


逐個恢復logfile

點選(此處)摺疊或開啟

  1. Status DBImpl::RecoverLogFile(uint64_t log_number,
  2.                               VersionEdit* edit,
  3.                               SequenceNumber* max_sequence) {
  4.   struct LogReporter : public log::Reader::Reporter {
  5.     Env* env;
  6.     Logger* info_log;
  7.     const char* fname;
  8.     Status* status; // NULL if options_.paranoid_checks==false
  9.     virtual void Corruption(size_t bytes, const Status& s) {
  10.       Log(info_log, "%s%s: dropping %d bytes; %s",
  11.           (this->status == NULL ? "(ignoring error) " : ""),
  12.           fname, static_cast<int>(bytes), s.ToString().c_str());
  13.       if (this->status != NULL && this->status->ok()) *this->status = s;
  14.     }
  15.   };


  16.   mutex_.AssertHeld();


  17.   // Open the log file
  18.   std::string fname = LogFileName(dbname_, log_number);
  19.   SequentialFile* file;
  20.   // 只讀開啟logfile
  21.   Status status = env_->NewSequentialFile(fname, &file);
  22.   if (!status.ok()) {
  23.     MaybeIgnoreError(&status);
  24.     return status;
  25.   }


  26.   // Create the log reader.
  27.   LogReporter reporter;
  28.   reporter.env = env_;
  29.   reporter.info_log = options_.info_log;
  30.   reporter.fname = fname.c_str();
  31.   reporter.status = (options_.paranoid_checks ? &status : NULL);
  32.   // We intentionally make log::Reader do checksumming even if
  33.   // paranoid_checks==false so that corruptions cause entire commits
  34.   // to be skipped instead of propagating bad information (like overly
  35.   // large sequence numbers).
  36.   log::Reader reader(file, &reporter, true/*checksum*/,
  37.                      0/*initial_offset*/);
  38.   Log(options_.info_log, "Recovering log #%llu",
  39.       (unsigned long long) log_number);


  40.   // Read all the records and add to a memtable
  41.   std::string scratch;
  42.   Slice record;
  43.   WriteBatch batch;
  44.   MemTable* mem = NULL;
  45.   /*
  46.   逐行讀取logfile
  47.   將得到的資料放入memtable mm裡
  48.   */
  49.   while (reader.ReadRecord(&record, &scratch) &&
  50.          status.ok()) {
  51.     if (record.size() < 12) {
  52.       reporter.Corruption(
  53.           record.size(), Status::Corruption("log record too small"));
  54.       continue;
  55.     }
  56.     WriteBatchInternal::SetContents(&batch, record);


  57.     if (mem == NULL) {
  58.       mem = new MemTable(internal_comparator_);
  59.       mem->Ref();
  60.     }
  61.     status = WriteBatchInternal::InsertInto(&batch, mem);
  62.     MaybeIgnoreError(&status);
  63.     if (!status.ok()) {
  64.       break;
  65.     }
  66.     const SequenceNumber last_seq =
  67.         WriteBatchInternal::Sequence(&batch) +
  68.         WriteBatchInternal::Count(&batch) - 1;
  69.     if (last_seq > *max_sequence) {
  70.       *max_sequence = last_seq;
  71.     }
  72.     
  73.     // 當memtable使用量超過設定的值時,將資料刷到level 0 資料檔案裡。
  74.     // 這樣在恢復過程中不會將記憶體撐爆
  75.     if (mem->ApproximateMemoryUsage() > options_.write_buffer_size) {
  76.       status = WriteLevel0Table(mem, edit, NULL);
  77.       if (!status.ok()) {
  78.         // Reflect errors immediately so that conditions like full
  79.         // file-systems cause the DB::Open() to fail.
  80.         break;
  81.       }
  82.       mem->Unref();
  83.       mem = NULL;
  84.     }
  85.   }


  86.   if (status.ok() && mem != NULL) {
  87.     status = WriteLevel0Table(mem, edit, NULL);
  88.     // Reflect errors immediately so that conditions like full
  89.     // file-systems cause the DB::Open() to fail.
  90.   }


  91.   if (mem != NULL) mem->Unref();
  92.   delete file;
  93.   return status;
  94. }



















來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26239116/viewspace-1846192/,如需轉載,請註明出處,否則將追究法律責任。

相關文章