博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
leveldb代码精读 数据库启动和初始化
阅读量:2433 次
发布时间:2019-05-10

本文共 14783 字,大约阅读时间需要 49 分钟。

基本概念
数据库主要包括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/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/26239116/viewspace-1846192/

你可能感兴趣的文章
【Java】【算法】——算法篇
查看>>
【Java】【数据库】知识重点——数据库篇
查看>>
【Java】知识重点——消息队列篇
查看>>
【Java】学习总结 —— HashMap之put()方法实现原理
查看>>
【计算机网络】【TCP】如何讲清楚Tcp的三次握手和四次挥手?
查看>>
【Java】-- Java核心知识点总结
查看>>
【数据库】SQL之重点知识点总结
查看>>
【计算机网络】计算机网络知识总结
查看>>
【Java】【Web】JavaWeb相关知识总结 2018-9-17
查看>>
【数据库】突破单一数据库的性能限制——数据库-分库分表总结 2018-9-20
查看>>
Slurm——作业调度处理
查看>>
Lustre 维护
查看>>
Lustre—磁盘配额测试
查看>>
SSH加密密码中的非对称式密码学
查看>>
Mac Redis安装入门教程
查看>>
python3安装教程配置配置阿里云
查看>>
Mac快捷键和实用技巧
查看>>
Git的多人协作和分支处理测试
查看>>
mysql索引回表
查看>>
iterm2 保存阿里云登陆并防止断开连接
查看>>