LevelDB二级索引实现 姚凯文(kevinyao0901) 姜嘉祺
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

826 lines
28 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
3 weeks ago
3 weeks ago
3 weeks ago
3 weeks ago
3 weeks ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. # 实验报告:在 LevelDB 中构建二级索引的设计与实现
  2. ---
  3. ## 目录
  4. - [一,实验目的](#一实验目的)
  5. - [二,项目背景概述](#二项目背景概述)
  6. - [1. **背景与需求**](#1-背景与需求)
  7. - [2. **设计目标**](#2-设计目标)
  8. - [三,`LevelDB`二级索引设计思路](#三leveldb二级索引设计思路)
  9. - [1. **设计结构**](#1-设计结构)
  10. - [1.1 **核心组件**](#11-核心组件)
  11. - [1.2 **数据结构**](#12-数据结构)
  12. - [1.3 **字段管理**](#13-字段管理)
  13. - [1.4 **数据结构关系图**](#14-数据结构关系图)
  14. - [2. **计划实现细节**](#2-计划实现细节)
  15. - [3. **动态索引管理**](#3-动态索引管理)
  16. - [4. **事务与回滚机制**](#4-事务与回滚机制)
  17. - [5. **设计的优势**](#5-设计的优势)
  18. - [6. **未来优化方向**](#6-未来优化方向)
  19. - [四,具体实现](#四具体实现)
  20. - [1. **DBImpl 类的设计**](#1-dbimpl-类的设计)
  21. - [2. **二级索引的创建**](#2-二级索引的创建)
  22. - [3. **二级索引的查询**](#3-二级索引的查询)
  23. - [4. **二级索引的删除**](#4-二级索引的删除)
  24. - [5. **`Put` 和 `Delete` 方法的内容**](#5-put-和-delete-方法的内容)
  25. - [6. 数据插入与删除的原子性实现](#6-数据插入与删除的原子性实现)
  26. - [7.持久化与恢复机制](#7-持久化与恢复机制)
  27. - [五,性能测试](#五性能测试)
  28. - [1. 测试流程](#1-测试流程)
  29. - [2. 结果分析](#2-结果分析)
  30. - [六,问题与解决方案](#六问题与解决方案)
  31. - [七,总结](#七总结)
  32. ---
  33. ## 一,实验目的
  34. 在 LevelDB 的基础上设计和实现一个支持二级索引的功能,优化特定字段的查询效率。通过此功能,用户能够根据字段值高效地检索对应的数据记录,而不需要遍历整个数据库。
  35. <br>
  36. ---
  37. ## 二,项目背景概述
  38. #### 1. **背景与需求**
  39. `LevelDB` 是一个高性能、轻量级的键值存储引擎,但其查询能力仅限于主键。在许多应用场景中,需要支持基于非主键字段的高效查询(例如按用户 ID 或类别查询数据)。因此,设计并实现二级索引系统,为 LevelDB 增强多字段查询能力,成为一个核心需求。
  40. ### **二级索引的概念**
  41. 二级索引是一种额外的数据结构,用于加速某些特定字段的查询。在 LevelDB 中,键值对的存储是以 `key:value` 的形式。通过创建二级索引,我们将目标字段的值与原始 `key` 建立映射关系,存储在独立的索引数据库中,从而支持基于字段值的快速查询。
  42. 例如,原始数据如下:
  43. ```
  44. k_1 : name:Customer#000000001|address:IVhzIApeRb|phone:25-989-741-2988
  45. k_2 : name:Customer#000000002|address:XSTf4,NCwDVaW|phone:23-768-687-3665
  46. k_3 : name:Customer#000000001|address:MG9kdTD2WBHm|phone:11-719-748-3364
  47. ```
  48. 为字段 `name` 创建索引后,索引数据库中的条目如下:
  49. ```
  50. name:Customer#000000001-k_1 : k_1
  51. name:Customer#000000001-k_3 : k_3
  52. name:Customer#000000002-k_2 : k_2
  53. ```
  54. ---
  55. #### 2. **设计目标**
  56. - **高效性**:二级索引查询性能接近主键查询。
  57. - **一致性**:保证主数据库与二级索引的一致性,支持事务和回滚机制。
  58. - **灵活性**:允许用户指定需要创建索引的字段,支持动态创建和删除索引。
  59. - **易用性**:通过统一接口隐藏索引管理的复杂性,保持与原始 LevelDB 类似的用户体验。
  60. <br>
  61. ---
  62. ## 三,`LevelDB`二级索引设计思路
  63. #### 1. **设计结构**
  64. `LevelDB` 的基础上扩展,补充并实现以下组件:
  65. ##### 1.1 **核心组件**
  66. 1. **主数据库(DBImpl)**
  67. 存储用户原始数据的键值对,提供 `Put`、`Delete` 和 `Get` 方法。
  68. 2. **二级索引数据库(indexDb_)**
  69. 专门存储索引数据,键为 `fieldName:fieldValue`,值为主数据库中对应的主键。
  70. ##### 1.2 **数据结构**
  71. 1. **主数据库键格式**
  72. 使用字符串表示,例如:`userID:123|name:JohnDoe`,包含多个字段。
  73. 2. **索引键格式**
  74. 例如:`userID:123`,方便通过字段值快速查询。
  75. 3. **映射关系**
  76. 二级索引数据库的值存储主数据库的主键,用于指向完整数据记录。
  77. ##### 1.3 **字段管理**
  78. - `fieldWithIndex_`:一个集合,用于管理需要创建索引的字段,支持动态增删。
  79. #### 1.4 **数据结构关系图**
  80. 以下是主数据库和二级索引数据库的逻辑关系示意图:
  81. ```
  82. lessCopy code 主数据库 (DBImpl)
  83. +-------------------------------------------------------+
  84. | key | value |
  85. +-------+-----------------------------------------------+
  86. | k_1 | name:Customer#000000001|address:IVhzIApeRb|.. |
  87. | k_2 | name:Customer#000000002|address:XSTf4,NCwDVaW |
  88. +-------+-----------------------------------------------+
  89. 二级索引数据库 (indexDb_)
  90. +----------------------------------------+-------------+
  91. | indexKey | indexValue |
  92. +----------------------------------------+-------------+
  93. | name:Customer#000000001-k_1 | k_1 |
  94. | name:Customer#000000001-k_3 | k_3 |
  95. | name:Customer#000000002-k_2 | k_2 |
  96. +----------------------------------------+-------------+
  97. 数据关联关系
  98. 主数据库 <-------------> 二级索引数据库
  99. (key) 映射到字段值 (fieldName:fieldValue)
  100. ```
  101. ---
  102. ### 2. **计划实现细节**
  103. #### 2.1 数据插入流程 (`Put`)
  104. 1. 用户调用 `Put` 将数据插入到主数据库。
  105. 2. 从用户数据中解析需要创建索引的字段及其值。
  106. 3. 构造二级索引的键值对,并插入到二级索引数据库中。
  107. 4. 如果任意数据库的写入失败,通过事务回滚保证一致性。
  108. **关键点**:
  109. - 需要先提交主数据库事务,再提交二级索引数据库事务。
  110. - 索引更新时要考虑覆盖旧索引的场景。
  111. #### 2.2 数据删除流程 (`Delete`)
  112. 1. 用户调用 `Delete` 从主数据库删除数据。
  113. 2. 在删除前,读取原始数据以提取相关字段的索引键。
  114. 3. 删除主数据库中的数据。
  115. 4. 删除对应的二级索引键。
  116. 5. 如果任意数据库的删除失败,通过事务回滚恢复数据。
  117. **关键点**:
  118. - 删除前必须读取原始数据以提取相关索引信息。
  119. - 回滚时需恢复原始主数据库记录。
  120. #### 3.3 查询流程
  121. 1. 用户指定查询条件(字段名和字段值)。
  122. 2. 从二级索引数据库中获取与查询条件匹配的主键。
  123. 3. 使用主键从主数据库获取完整记录。
  124. ---
  125. ### 3. **动态索引管理**
  126. #### 3.1 动态创建索引
  127. - 提供接口 `CreateIndex(fieldName)`,用于动态为字段创建索引:
  128. - 遍历主数据库的所有记录。
  129. - 根据指定字段生成索引键值对并插入到二级索引数据库。
  130. - 将字段名添加到 `fieldWithIndex_` 集合。
  131. #### 3.2 动态删除索引
  132. - 提供接口 `DeleteIndex(fieldName)`,用于动态删除字段索引:
  133. - 遍历二级索引数据库,删除与该字段相关的索引键。
  134. -`fieldWithIndex_` 集合中移除字段名。
  135. ---
  136. ### 4. **事务与回滚机制**
  137. #### 4.1 事务设计
  138. - 使用 `WriteBatch` 封装多个操作(如 `Put``Delete`)。
  139. - 在主数据库和二级索引数据库上分别维护独立事务。
  140. #### 4.2 回滚机制
  141. - 在主数据库操作失败时直接返回错误,不影响索引。
  142. - 在二级索引操作失败时,回滚主数据库的写入或删除操作:
  143. -`Put` 操作,删除已插入的数据。
  144. -`Delete` 操作,恢复已删除的数据。
  145. ---
  146. ### 5. **设计的优势**
  147. 1. **数据一致性强**:通过事务和回滚机制,确保主数据库和二级索引数据库始终保持一致。
  148. 2. **查询高效**:支持基于字段的快速查询,二级索引性能接近主键查询。
  149. 3. **易于扩展**:动态索引创建和删除机制使得系统适应性更强。
  150. 4. **兼容性好**:用户接口保持与原始 LevelDB 类似,降低学习成本。
  151. ---
  152. ### 6. **未来优化方向**
  153. 1. **多字段联合索引**:支持对多个字段的联合索引,提高复杂查询的效率。
  154. 2. **异步索引更新**:通过异步任务队列优化索引构建和更新的性能。
  155. 3. **空间优化**:采用压缩技术减少二级索引数据库的存储占用。
  156. 4. **并发支持**:优化写锁机制以提高高并发场景下的性能。
  157. ---
  158. 这套设计在功能性、一致性和性能之间达到了较好的平衡,能够为 LevelDB 提供高效、灵活的二级索引支持,同时保持其原有的高性能特性。
  159. <br>
  160. ------
  161. ## 四,具体实现
  162. 为了便于审阅和维护,在项目中对代码的所有修改均使用统一的注释格式进行标记。具体而言,所有修改的代码块均以 `//ToDo` 开始,并以 `//ToDo end` 结束。通过这种方式,审阅者可以快速定位和识别修改内容,与原始代码进行对比。
  163. ### 1. **DBImpl 类的设计**
  164. 在 LevelDB 的核心类 `DBImpl` 中,增加了对二级索引的支持,包括:
  165. - **索引字段管理**:使用成员变量 `fieldWithIndex_` 保存所有已经创建索引的字段名。
  166. - **索引数据库**:使用成员变量 `indexDb_` 管理二级索引数据库。
  167. ```cpp
  168. class DBImpl : public DB {
  169. private:
  170. std::vector<std::string> fieldWithIndex_; // 已创建索引的字段列表
  171. leveldb::DB* indexDb_; // 存储二级索引的数据库
  172. };
  173. ```
  174. ### 2. **二级索引的创建**
  175. `DBImpl` 中实现 `CreateIndexOnField` 方法,用于对指定字段创建二级索引:
  176. - 遍历主数据库中的所有数据记录。
  177. - 解析目标字段的值。
  178. - 在索引数据库中写入二级索引条目,键为 `"fieldName:field_value-key"`,值为原始数据的键。
  179. 示例:
  180. ![error](Report/png/indexDb.png)
  181. #### 核心代码:
  182. ```cpp
  183. Status DBImpl::CreateIndexOnField(const std::string& fieldName) {
  184. // 检查字段是否已创建索引
  185. for (const auto& field : fieldWithIndex_) {
  186. if (field == fieldName) {
  187. return Status::InvalidArgument("Index already exists for this field");
  188. }
  189. }
  190. // 添加到已创建索引的字段列表
  191. fieldWithIndex_.push_back(fieldName);
  192. // 遍历主数据库,解析字段值并写入索引数据库
  193. leveldb::ReadOptions read_options;
  194. leveldb::Iterator* it = this->NewIterator(read_options);
  195. for (it->SeekToFirst(); it->Valid(); it->Next()) {
  196. std::string key = it->key().ToString();
  197. std::string value = it->value().ToString();
  198. // 提取字段值
  199. // ...
  200. // 在索引数据库中创建条目
  201. // ...
  202. }
  203. delete it;
  204. return Status::OK();
  205. }
  206. ```
  207. ---
  208. ### 3. **二级索引的查询**
  209. 在查询二级索引时,基于二级索引,通过范围查询从 LevelDB 数据库中检索与指定字段名 (`fieldName`) 相关联的所有值。。
  210. #### 核心代码:
  211. ```cpp
  212. // 查询通过字段名索引的所有值
  213. std::vector<std::string> DBImpl::QueryByIndex(const std::string& fieldName) {
  214. function QueryByIndex(fieldName):
  215. results = [] // 用于存储查询结果
  216. // 初始化读取选项和迭代器
  217. create ReadOptions read_options
  218. create Iterator it from indexDb_ using read_options
  219. // 遍历所有键值对,从 fieldName 开始
  220. for it.Seek(fieldName) to it.Valid():
  221. key = current key from it
  222. value = current value from it
  223. // 如果键匹配并且值非空,将其加入结果列表
  224. if key equals fieldName and value is not empty:
  225. add value to results
  226. // 检查迭代器的状态是否正常
  227. if iterator status is not OK:
  228. log error with status message
  229. return results
  230. }
  231. ```
  232. 1. **输入与输出**:
  233. - 输入:`fieldName`(目标字段名)。
  234. - 输出:`results`(包含所有匹配值的列表)。
  235. 2. **逻辑流程**:
  236. - 使用 `ReadOptions` 初始化读取配置,并创建一个迭代器。
  237. - 调用 `Seek(fieldName)` 将迭代器定位到目标字段的起始位置。
  238. - 遍历满足条件的键值对:
  239. - 如果键等于目标字段名,并且值非空,将值添加到结果列表。
  240. - 在遍历结束后,检查迭代器的状态以捕捉可能的错误。
  241. 3. **优势**:
  242. - 使用迭代器实现范围查询(`Seek` 方法快速定位)。
  243. - 避免全表扫描,提高查询效率。
  244. - 针对多值字段支持查询返回多个结果。
  245. 4. **错误处理**:
  246. - 如果迭代过程中出现错误,记录错误信息,便于调试。
  247. 此方法通过范围查询机制提升了效率,同时确保了结果的准确性。
  248. ---
  249. ### 4. **二级索引的删除**
  250. `DBImpl` 中实现 `DeleteIndex` 方法,通过目标字段名移除对应的所有索引条目:
  251. -`fieldWithIndex_` 中移除字段。
  252. - 遍历索引数据库,删除所有以 `fieldName:` 开头的条目。
  253. #### 核心代码:
  254. ```cpp
  255. Status DBImpl::DeleteIndex(const std::string& fieldName) {
  256. auto it = std::find(fieldWithIndex_.begin(), fieldWithIndex_.end(), fieldName);
  257. if (it == fieldWithIndex_.end()) {
  258. return Status::NotFound("Index not found for this field");
  259. }
  260. // 从已创建索引列表中移除字段
  261. fieldWithIndex_.erase(it);
  262. // 遍历索引数据库,删除相关条目
  263. leveldb::ReadOptions read_options;
  264. leveldb::Iterator* it_index = indexDb_->NewIterator(read_options);
  265. for (it_index->SeekToFirst(); it_index->Valid(); it_index->Next()) {
  266. std::string index_key = it_index->key().ToString();
  267. // ...
  268. }
  269. delete it_index;
  270. return Status::OK();
  271. }
  272. ```
  273. ---
  274. ### 5. **`Put` 和 `Delete` 方法的内容**
  275. 以下是实验报告中对 `Put``Delete` 方法的描述,以及如何通过事务和回滚机制实现数据插入与删除的原子性。
  276. #### `Put` 方法描述
  277. `Put` 方法用于向主数据库和二级索引数据库中插入或更新数据。其关键步骤如下:
  278. 1. **主数据库写入**:首先尝试向主数据库插入或更新数据。
  279. 2. **二级索引更新**:遍历需要创建索引的字段 (`fieldWithIndex_`),从新值中提取字段对应的索引键和值,并将索引插入到二级索引数据库。
  280. 3. **提交事务**
  281. - 提交主数据库的写入操作。
  282. - 提交二级索引数据库的写入操作。
  283. 4. **回滚机制**:如果二级索引数据库的写入失败,会回滚主数据库的插入操作以确保数据一致性。
  284. 关键代码:
  285. ```cpp
  286. Status DBImpl::Put(const WriteOptions& o, const Slice& key, const Slice& val) {
  287. ...
  288. // 在主数据库写入新数据
  289. batch.Put(key, val);
  290. // 遍历字段并更新索引
  291. for (const auto& field : fieldWithIndex_) {
  292. ...
  293. indexBatch.Put(Slice(indexKey), Slice(indexValue));
  294. }
  295. // 提交主数据库事务
  296. s = this->Write(o, &batch);
  297. if (!s.ok()) {
  298. return s;
  299. }
  300. // 提交二级索引数据库事务
  301. s = indexDb_->Write(o, &indexBatch);
  302. if (!s.ok()) {
  303. // 如果二级索引写入失败,回滚主数据库写入
  304. for (const auto& insertedKey : keysInserted) {
  305. batch.Delete(insertedKey);
  306. }
  307. this->Write(o, &batch);
  308. return s;
  309. }
  310. ...
  311. }
  312. ```
  313. #### `Delete` 方法描述
  314. `Delete` 方法用于从主数据库和二级索引数据库中删除数据。其关键步骤如下:
  315. 1. **获取原始数据**:在删除前从主数据库读取原始值,确保在删除失败时可以回滚。
  316. 2. **主数据库删除**:从主数据库中删除目标键。
  317. 3. **二级索引删除**:遍历字段,计算对应的索引键,并将其从二级索引数据库中删除。
  318. 4. **提交事务**
  319. - 提交主数据库的删除操作。
  320. - 提交二级索引数据库的删除操作。
  321. 5. **回滚机制**:如果二级索引数据库的删除失败,会尝试将主数据库的删除操作回滚为原始状态。
  322. 关键代码:
  323. ```cpp
  324. Status DBImpl::Delete(const WriteOptions& options, const Slice& key) {
  325. ...
  326. // 从主数据库删除目标键
  327. batch.Delete(key);
  328. // 遍历字段并删除索引
  329. for (const auto& field : fieldWithIndex_) {
  330. ...
  331. indexBatch.Delete(Slice(indexKey));
  332. }
  333. // 提交主数据库事务
  334. s = this->Write(options, &batch);
  335. if (!s.ok()) {
  336. return s;
  337. }
  338. // 提交二级索引数据库事务
  339. s = indexDb_->Write(options, &indexBatch);
  340. if (!s.ok()) {
  341. // 如果二级索引删除失败,回滚主数据库删除
  342. if (!originalValue.empty()) {
  343. batch.Put(key, originalValue);
  344. } else {
  345. batch.Put(key, "");
  346. }
  347. this->Write(options, &batch);
  348. return s;
  349. }
  350. ...
  351. }
  352. ```
  353. ### 6. **数据插入与删除的原子性实现**
  354. 通过以下策略确保数据插入与删除操作的原子性:
  355. 1. **事务机制**
  356. - 主数据库和二级索引数据库的写入操作分别使用 `WriteBatch` 封装,并在提交前记录必要的数据以支持回滚。
  357. 2. **错误处理与回滚**
  358. - 如果二级索引数据库的写入或删除操作失败,主数据库的写入或删除操作将被回滚。
  359. - 在回滚过程中,主数据库会恢复为操作前的状态(插入操作时删除新数据,删除操作时恢复原始数据)。
  360. #### 实现意义
  361. 这种设计确保了主数据库和二级索引数据库的一致性,即便在部分写入或删除操作失败的情况下,仍能通过回滚机制保证数据的完整性和原子性。
  362. <br>
  363. ---
  364. ### 7. **持久化与恢复机制**
  365. #### **1. 持久化机制**
  366. 持久化是确保数据在系统崩溃或断电后依然能够恢复的关键功能。
  367. 在二级索引设计中,我们使用 `Write-Ahead Logging (WAL)` 技术和日志同步来实现持久化:
  368. - **主数据库持久化:**
  369. 主数据库的 `Put``Delete` 操作会先记录到日志文件中,再执行磁盘写入操作。
  370. 日志结构如下:
  371. ```
  372. makefileCopy codeOperation: PUT
  373. Key: k_1
  374. Value: name:Customer#000000001|address:IVhzIApeRb|phone:25-989-741-2988
  375. ```
  376. - **索引数据库持久化:**
  377. `indexDb_` 的每一次 `Put``Delete` 操作也需要写入 WAL。
  378. 示例日志:
  379. ```
  380. makefileCopy codeOperation: PUT
  381. Key: name:Customer#000000001-k_1
  382. Value: k_1
  383. ```
  384. - **同步写入策略:**
  385. - 对主数据库和 `indexDb_` 的写操作采用 **事务** 来实现同步写入,确保一致性。
  386. - 如果主数据库操作失败,回滚索引的更新;反之亦然。
  387. #### **2. 恢复机制**
  388. 恢复机制在系统崩溃后发挥作用,通过解析日志重建数据:
  389. - **主数据库恢复:**
  390. 通过 WAL 日志文件重放,将最近一次写入操作重新应用到主数据库。
  391. - **索引数据库恢复:**
  392. 索引数据库的恢复流程如下:
  393. 1. 检查 `indexDb_` 的日志文件,逐条读取并应用日志操作。
  394. 2. 如果`indexDb_`的日志不完整,基于主数据库的快照重新构建索引:
  395. - 遍历主数据库中的每条记录,根据 `fieldWithIndex_` 中的字段生成索引条目并写入 `indexDb_`
  396. - **一致性校验:**
  397. 恢复完成后,通过校验主数据库和索引数据库的一致性来验证数据完整性:
  398. - 对每个索引字段,随机抽样检查索引值是否能正确定位主数据库中的记录。
  399. <br>
  400. ---
  401. ## 五,性能测试
  402. ### 1.测试流程
  403. **单元测试:**
  404. 1. 插入原始数据:
  405. ```
  406. k_1 : name:Customer#000000001|address:IVhzIApeRb|phone:25-989-741-2988
  407. k_2 : name:Customer#000000002|address:XSTf4,NCwDVaW|phone:23-768-687-3665
  408. ```
  409. 2. 创建索引:
  410. - 调用 `CreateIndexOnField("name")`,索引数据库生成条目:
  411. ```
  412. name:Customer#000000001-k_1 : k_1
  413. name:Customer#000000002-k_2 : k_2
  414. ```
  415. 3. 查询索引:
  416. - 调用 `QueryByIndex("name:Customer#000000001")`,返回 `["k_1"]`
  417. 4. 删除索引:
  418. - 调用 `DeleteIndex("name")`,移除所有 `name:` 开头的索引条目。
  419. 测试结果:
  420. ![error](Report/png/test_result.png)
  421. **性能测试:**
  422. **Benchmark测试运行结果及分析:**
  423. ![error](./Report/png/benchmark.png)
  424. ### 2.结果分析
  425. 1. **插入时间 (Insertion time for 100001 entries: 516356 microseconds)**
  426. 这个时间(516356 微秒,约 516 毫秒)看起来是合理的,特别是对于 100001 条记录的插入操作。如果数据插入过程没有特别复杂的计算或操作,这个时间应该是正常的,除非硬件性能或其他因素导致延迟。
  427. 2. **没有索引的查询时间 (Time without index: 106719 microseconds)**
  428. 这个时间是查询在没有索引的情况下执行的时间。106719 微秒(大约 107 毫秒)对于没有索引的查询来说是可以接受的,尤其是在数据量较大时。如果数据库没有索引,查找所有相关条目会比较耗时。
  429. 3. **创建索引的时间 (Time to create index: 596677 microseconds)**
  430. 这个时间(596677 微秒,约 597 毫秒)对于创建索引来说是正常的,尤其是在插入了大量数据后。如果数据量非常大,索引创建时间可能会显得稍长。通常情况下,创建索引的时间会随着数据量的增加而增大。
  431. 4. **有索引的查询时间 (Time with index: 68 microseconds)**
  432. 这个时间(68 微秒)非常短,几乎可以认为是一个非常好的优化结果。通常,索引查询比没有索引时要快得多,因为它避免了全表扫描。因此,这个时间是非常正常且预期的,说明索引大大加速了查询。
  433. 5. **查询结果 (Found 1 keys with index)**
  434. 这里显示索引查询找到了 1 个键。是正常的, `name=Customer#10000` 应该返回 1 条记录。
  435. 6. **数据库统计信息 (Database stats)**
  436. ```
  437. Compactions
  438. Level Files Size(MB) Time(sec) Read(MB) Write(MB)
  439. --------------------------------------------------
  440. 0 2 6 0 0 7
  441. 1 5 8 0 16 7
  442. ```
  443. 这些信息表明数据库的压缩(Compaction)过程。`Level 0` 和 `Level 1` 显示了数据库的文件数和大小。此部分数据正常,意味着数据库在处理数据时有一些 I/O 操作和文件整理。
  444. 7. **删除索引的时间 (Time to delete index on field 'name': 605850 microseconds)**
  445. 删除索引的时间(605850 微秒,约 606 毫秒)比创建索引的时间稍长。这个时间是合理的,删除索引通常会涉及到重新整理数据结构和清理索引文件,因此可能比创建索引稍慢。
  446. > 根据这些结果,以下是吞吐、延迟、和写放大的分析:
  447. >
  448. > ------
  449. >
  450. > ### **吞吐量**
  451. >
  452. > 吞吐量衡量的是系统在单位时间内能处理的操作量。根据插入时间和查询时间,可以推算吞吐量。
  453. >
  454. > 1. **插入操作的吞吐量**
  455. >
  456. > - 插入时间为 516,356 微秒(约 516 毫秒)。
  457. >
  458. > - 记录总数为 100,001。
  459. >
  460. > $$
  461. > 吞吐量 = 总操作数总时间\frac{\text{总操作数}}{\text{总时间}}。
  462. > $$
  463. >
  464. >
  465. >
  466. > $$
  467. > 吞吐量=100,0010.516≈193,798 条/秒吞吐量 = \frac{100,001}{0.516} \approx 193,798 \, \text{条/秒}
  468. > $$
  469. >
  470. >
  471. >
  472. > 2. **查询操作的吞吐量**
  473. >
  474. > - 有索引的查询时间为 68 微秒(非常快)。
  475. >
  476. > $$
  477. > 查询操作吞吐量 = 10.000068≈14,705,882 次/秒\frac{1}{0.000068} \approx 14,705,882 \, \text{次/秒}。
  478. > $$
  479. >
  480. >
  481. >
  482. > - 无索引的查询时间为 106,719 微秒。
  483. >
  484. >
  485. > $$
  486. > 无索引的查询吞吐量=10.106719≈9.37 次/秒无索引的查询吞吐量 = \frac{1}{0.106719} \approx 9.37 \, \text{次/秒}
  487. > $$
  488. >
  489. >
  490. > ------
  491. >
  492. > ### **延迟**
  493. >
  494. > 延迟是单个操作所需的时间,可以从实验结果中直接得出:
  495. >
  496. > 1. **插入延迟**
  497. >
  498. >
  499. > $$
  500. > 平均插入延迟 = 516,356100,001≈5.16 微秒/条\frac{516,356}{100,001} \approx 5.16 \, \text{微秒/条}。
  501. > $$
  502. >
  503. >
  504. > 2. **查询延迟**
  505. >
  506. > - 有索引的查询延迟:68 微秒。
  507. > - 无索引的查询延迟:106,719 微秒。
  508. >
  509. > ------
  510. >
  511. > ### **写放大**
  512. >
  513. > 写放大是写入数据量与实际写入磁盘数据量的比值。根据数据库统计信息,可以估算写放大:
  514. >
  515. > 1. **统计数据**
  516. >
  517. > - 数据量:插入 100,001 条记录,假设每条记录大小为 64 字节,总大小约为
  518. >
  519. >
  520. > $$
  521. > 100,001×64=6.4 MB100,001 \times 64 = 6.4 \, \text{MB}
  522. > $$
  523. >
  524. >
  525. > - 压缩日志显示:
  526. >
  527. > - 读取数据量:16 MB。
  528. > - 写入数据量:14 MB(Level 0 和 Level 1 总写入量)。
  529. >
  530. > 2. **计算写放大**
  531. >
  532. >
  533. > $$
  534. > 写放大 = 总写入量数据量=146.4≈2.19\frac{\text{总写入量}}{\text{数据量}} = \frac{14}{6.4} \approx 2.19。
  535. > $$
  536. >
  537. >
  538. > - 这个写放大值在 LSM 树中属于合理范围,尤其是数据量较大时。
  539. >
  540. > ------
  541. >
  542. > ### **总结**
  543. >
  544. > - **吞吐量**:
  545. > - 插入操作约为 **193,798 条/秒**。
  546. > - 有索引的查询吞吐量为 **14,705,882 次/秒**,而无索引的查询吞吐量仅为 **9.37 次/秒**。
  547. > - **延迟**:
  548. > - 插入操作的平均延迟为 **5.16 微秒/条**。
  549. > - 有索引的查询延迟远低于无索引的查询延迟(68 微秒 vs 106,719 微秒)。
  550. > - **写放大**:
  551. > 写放大约为 **2.19**,表明索引的写入效率较高,但仍需注意在高频写入场景中的性能影响。
  552. >
  553. > 如果进一步优化,建议从减少写放大(例如改进合并机制)和提升无索引查询性能入手,以平衡系统资源。
  554. **benchmark运行结果总结:**
  555. 整体来看,输出结果是正常的:
  556. - **插入和索引创建时间**:插入数据和创建索引所需的时间相对较长,但考虑到数据量和索引的生成,时间是合理的。
  557. - **有索引的查询时间**:索引加速了查询,这部分的时间(68 微秒)非常短,表现出色。
  558. - **删除索引的时间**:删除索引需要稍长时间,这也是常见的现象。
  559. <br>
  560. ---
  561. ## 六,问题与解决方案
  562. ### 1. **问题:如何避免** `**indexDb_**` **的递归调用?**
  563. 在实现 `Put``Delete` 方法时,由于 `indexDb_` 也调用了 `DBImpl` 的方法,可能导致递归调用的问题。具体表现为在 `indexDb_` 内部操作时仍会试图更新索引。
  564. #### **解决方案:**
  565. `Put``Delete` 方法中,添加检查逻辑。如果当前对象是 `indexDb_`,则仅对主数据库进行操作,而不再更新索引。例如:
  566. ```
  567. if (indexDb_ != nullptr) {
  568. // 仅更新主数据库的事务
  569. } else {
  570. // 更新索引
  571. }
  572. ```
  573. ### 2. **问题:二级索引的事务回滚机制如何设计?**
  574. 在二级索引更新失败时,需要确保主数据库的修改也能回滚,以保持数据一致性。
  575. #### **解决方案:**
  576. 使用 `WriteBatch` 记录每次操作。在二级索引更新失败后,通过读取原始值或删除新插入的键,恢复主数据库的状态。示例代码如下:
  577. ```
  578. if (!s.ok()) {
  579. for (const auto& insertedKey : keysInserted) {
  580. if (!originalValue.empty()) {
  581. batch.Put(insertedKey, Slice(originalValue));
  582. } else {
  583. batch.Delete(insertedKey);
  584. }
  585. }
  586. this->Write(o, &batch); // 执行回滚
  587. }
  588. ```
  589. ### 3. **问题:如何高效管理多字段的动态索引?**
  590. 如果需要为多个字段动态创建和删除索引,可能导致额外的开销以及管理复杂性。
  591. #### **解决方案:**
  592. 1. 使用 `fieldWithIndex_` 字段集中管理所有需要创建索引的字段。
  593. 2. 在动态操作中,通过扫描主数据库快速生成或删除指定字段的索引条目。
  594. 3. 提供统一的接口,用于添加和移除字段。
  595. ### 4. **问题:**`**Put**` **和** `**Delete**` **方法如何确保原子性?**
  596. 在更新主数据库和二级索引时,如果某一步骤失败,可能导致不一致。
  597. #### **解决方案:**
  598. 通过事务 (`WriteBatch`) 确保多个操作要么全部成功,要么全部回滚。例如:
  599. ```
  600. s = this->Write(o, &batch);
  601. if (!s.ok()) {
  602. return s; // 确保写入失败时停止后续操作
  603. }
  604. s = indexDb_->Write(o, &indexBatch);
  605. if (!s.ok()) {
  606. // 回滚主数据库操作
  607. }
  608. ```
  609. 通过以上方案,有效解决了实验中遇到的问题,并提高了系统的稳定性和一致性。
  610. <br>
  611. ---
  612. ## 七,总结
  613. 本实验通过在 `DBImpl` 中集成索引管理功能,实现了对二级索引的创建、查询和删除。二级索引数据存储在独立的 `indexDb_` 中,通过高效的键值映射提升了字段值查询的效率。