├── README.md ├── database.md ├── delete.png ├── insert1.png ├── insert2.png ├── no_sql.md └── redis_datastructure.png /README.md: -------------------------------------------------------------------------------- 1 | # CMU15-445 2 | 3 | 2021实验在2021分支上,2022在2022分支上,22只写前两个实验(b+树并发访问还没有完成,有时间补上),这里是2021的实验笔记,对于2022年b+树思路的笔记,参考帆船书 4 | 5 | 卡内基梅隆大学2021秋数据库实验 6 | 7 | ## lab1 8 | 9 | 实验1是手撸一个内存池。如果有os基础的话,相信应该非常容易上手。只需看完CMU数据库视频buffer pool相关章节即可开始。实验被分成三个子任务。 10 | 11 | 12 | ### LRU REPLACEMENT POLICY 13 | 14 | 这个子任务是写一个lru算法替代类,一开始我觉得这个任务估计是最简单的,后面才发现玄学多着。 15 | 16 | 17 | #### PARAM OUT 18 | 19 | cpp的这个注释我一开始是不知道的,输出参数。即虽然这个参数是传入的形参,但它是个指针变量,起到输出的作用,你需要将要输出的参数值写入此指针所指向的地址,是为输出参数。如 20 | victim函数。起初我很疑惑,为啥这个函数删除的一个frameid,但是不返回呢?这样调用者怎么获得你删除的那个frameid呢,后面一看注释,才知道这个知识点。妙啊 21 | 22 | 23 | #### 哈希表与链表结合 24 | 25 | 为了实现lru算法,我使用链表按到来的先后顺序存储frameid,使用哈希表存储frameid和其在链表中的指针。这样我需要对链表中指定数据的删除时可以直接做到o1时间复杂度,而我要找到 26 | victim也只需pop链表头,从哈希表中删除即可,时间复杂度也为o1,需要添加framid也只需要将其push到链表和加入哈希表(重复则不加),时间复杂度也为o1。这样可以大大减少时间开销。一 27 | 开始我是单纯使用哈希表存储frameid加时间戳,这样寻找victim的时间复杂度是on,线上测试内存这块会超时。这大大减少了时间复杂度,从on变为o1,只能说妙啊,不愧是cmu的数据库实 28 | 验!!! 29 | 30 | 31 | #### UNPIN 32 | 33 | 这个方法需要注意,当然你看本地的测试文件时也会得出这个结论。就是unpin多次同一个frameid时,时间应该以第一次unpin时为准,而不是最后一次。因为第一次已经unpin了,就不在这个lru 34 | 队列中,后面的unpin是无效的。 35 | 36 | 37 | #### 多线程安全 38 | 39 | 最后为了使lru是多线程安全的,我们在victim、pin、unpin三个需要修改lru相关数据结构的方法最开始加上lock_gaurd保护相关数据结构读写。 40 | 41 | 42 | 43 | ### BUFFER POOL MANAGER INSTANCE 44 | 45 | 这个算这个实验的核心与重点吧,下面分别介绍一些实现的函数思路与坑点 46 | 47 | #### NewPgImp 48 | 49 | - 首先我们遍历page_数组,寻找没有被pin的页来承载。如果没有找到,我们直接返回空指针 50 | 51 | - 然后我们需要调用分配pageid相关方法分配新的pageid,作为我们new生成的pageid,这在os中的虚拟内存管理中类似于虚拟内存,或者说虚拟页号 52 | 53 | - 而接下来我们需要找到可以映射的物理内存,或者说物理页号。这分两步,首先在freelist中寻找,不为空我们直接popfirst即可。 54 | 55 | - 找不到,我们调用上面lrureplacer的victim方法寻找受害者。这里就需要特别注意了。此处的victim受害者有可能在page_table_存在和pageid有相关映射关系,类比虚拟内存,相当于在页表 56 | 中这块物理页面和虚拟页面存在映射关系,此处我们必须要删除这映射关系!!!同时,在pages_数组中承载这块frameid,所以若其为脏,我们必须写回磁盘,这样相当于将其安全地从内存写 57 | 回磁盘,它在内存的一切活动已经结束,最后标记为不脏,为啥标记为不脏?因为这接下来这frameid要映射的是我们的新建的pageid,而我们新建的pageid目前肯定是不脏的。故接下来我们可 58 | 以放心的使用这块frameid来和我们新建的pageid进行映射。这里我们要说一下为啥我们这里可以确定在freelist和vimtim中成功找到frameid呢?我们第一步就确定了哦 59 | 60 | - 接下来我们在page_table_中建立映射关系,相当于虚拟内存中页表的map方法。同时pages_数组中frmeid的page承载的内容也要发生变化,修改pageid,修改pincount,关键一点是要调用 61 | lrureplacer的pin方法pin住,这是一开始我不知道的,后面通过测试得出结论。最后返回之。 62 | 63 | 64 | #### FetchPgImp 65 | 66 | - 首先会在page_table_中寻找,找到则pin,然后返回。 67 | 68 | - 找不到,则和上面的new方法及其相似,除了不会分配新的pageid。在内容上,最后需要从磁盘读入相关内容到承载的page上。 69 | 70 | 71 | #### DeletePgImp 72 | 73 | - 一开始DeallocatePage(page_id) 74 | 75 | - 然后在page_table_寻找映射关系,有可能是不存在映射关系的,这种情况我们直接返回true即可,这步不能少,如果你直接find,不判断这个find值是否是end(若是end表示不存在映射关系, 76 | 你接下来使用这end会出大错,我就是UnpinPgImp忽视这一步,导致后面的PARALLEL BUFFER POOL MANAGER的NewPgImp奇奇怪怪的错误。 77 | 78 | - 若存在映射关系,我们检查pincount,不为0我们是删除不了的,这种情况返回false 79 | 80 | - 然后将frameid代表的page所有字段清0,pageid标记为INVALID_PAGE_ID,从page_table_删除映射关系 81 | 82 | - 最后加入freelist中,返回true 83 | 84 | 85 | #### UnpinPgImp 86 | 87 | - 首先在page_table_寻找映射关系,有可能是不存在映射关系的,这种情况我们直接返回true即可,这步不能少,和上面一样 88 | 89 | - 然后通过传入的脏标志判断是否需要标记page为脏 90 | 91 | - 接下来判断pincount是否已经为0,为0则不能unpin,返回false 92 | 93 | - 否则将pincount减1,判断是否为0,为0则调用lrureplacer的unpin加入lru相关数据结构 94 | 95 | 96 | #### 线程安全的BUFFER POOL MANAGER INSTANCE 97 | 98 | 上面四个方法均对相关数据结构进行修改读写,所以,在方法最开始加上lock_gaurd保护相关数据结构读写。 99 | 100 | 101 | ### PARALLEL BUFFER POOL MANAGER 102 | 103 | 首先这个模块的设立是减少竞争,通过打破锁的粒度来达到,在xv6中的lock实验也很好的涉及到了。这个子任务估计是最简单的,只需要按照mod的映射关系,调用相关instance的相关接口即 104 | 可。提一下NewPgImp,这个方法注意你starting_index加的时机,这里注释没有说明白,不是说你一轮寻找完了才对starting_index加1,而是每找一个instance,都对其进行加1,直到成功, 105 | 成功后再加1。 106 | 107 | #### 构造方法与析构函数 108 | 109 | 需要通过num_instances来判断新建instance的个数,并且保存之,可以使用数组或者链表。注意,如果使用new则记住在析构函数中delete之 110 | 111 | #### 映射关系 112 | 113 | 按照取余数的映射关系,调用对应instance接口即可 114 | 115 | #### NewPgImp 116 | 117 | 这里的a round robin manner算法得重点提一下 118 | 119 | - 首先从start开始新建,start被初始化为0,若新建失败,start需要自增1到了num_instances则退回0,这里可以通过取余数来实现 120 | 121 | - 直到新建成功后,start继续自增1,然后返回即可 122 | 123 | ### 相关数据结构在bustub数据库中的位置层级 124 | 125 | 实验1主要是实现内存缓存池的管理,下面一块很好的说明了bustub对内存缓冲区管理的实现 126 | 127 | 继承 128 | ------> BufferPoolManager < 129 | / | 130 | / | 131 | ParallelBufferPoolManager | 132 | | | 继承 133 | | | 134 | BufferPoolManagerInstance[] | 135 | | | 136 | -------------------> BufferPoolManagerInstance 137 | | 138 | / | \ 139 | 140 | Page[] DiskManager LogManager unordered_map Replacer list 141 | | 142 | | 143 | / \ 144 | 继承 / \ 继承 145 | / \ 146 | lru_replacer clock_replacer 147 | 148 | 149 | ## lab2 150 | 151 | 实验二是实现extendible哈希表,其实我更想写b+树的,可惜这个好像是2020年的,以后有机会写一下。 152 | 153 | 三个子任务,一开始是高估的第一个子任务的难度,其实看测试文件,只需要实现一点点即可。 154 | 155 | 156 | ### PAGE LAYOUTS 157 | 158 | #### DIRECTORY 159 | 160 | 首先hashtabledirectorypage保存哈希表的根page,是整个哈希表最基本的数据结构,整个哈希表能够通过该数据结构获取整个哈希表在磁盘中的分布。由于必须要能够存储在一块磁盘下,所以 161 | 对整个数据结构大小有限制,下面是相关字段 162 | 163 | - page_id_t page_id_ 指示该hashtabledirectorypage存储在哪个pageid中 164 | 165 | - lsn_t lsn_ 166 | 167 | - uint32_t global_depth_ 全局深度,为2的整数次幂,初始值为0 168 | 169 | - uint8_t local_depths_[DIRECTORY_ARRAY_SIZE] 记录局部bucket深度,最大为DIRECTORY_ARRAY_SIZE,防止超出一块磁盘大小 170 | 171 | - page_id_t bucket_page_ids_[DIRECTORY_ARRAY_SIZE] 记录局部bucket的pageid号,最大为DIRECTORY_ARRAY_SIZE,防止超出一块磁盘大小 172 | 173 | ##### IncrGlobalDepth 174 | 175 | 先是简单的深度+1,然后复制前一半的pageid和localdepth到后一半,去HASH TABLE IMPLEMENTATION去实现这个机理也可。这里我们发现,我们并不需要数组扩容,其实,我们的globaldepth已 176 | 经是控制好了我们能够访问的数组下标范围,为[0, 2^globaldepth - 1]。插入的数据容量不超过约20w,应该是不会造成数组溢出的,其实为了健壮性考虑,我在上层实现了溢出检测,在写实验 177 | 的时候没有实现溢出检测,导致出现了非常奇怪的错误。 178 | 179 | ##### GetSplitImageIndex 180 | 这里的SplitImage概念指导书说自己会明白,定义: 181 | 182 | - 真镜像:a为xxxxx1000,b为xxxxx0000,a的真镜像是b,x的个数是globaldepth-localdepth,代表0或者1,二者所有x代表的位值应该相同而数字的位数则是localdepth。所以获取的真镜像 183 | 是bucket_idx ^ (1 <<(local_depth_[bucket_idx] - 1)),注意^符号是异或。称a的真镜像是b 184 | 185 | - 镜像:a为xxxxx1000,b位yyyyy0000,x和y不一定相等,x和y的个数均是globaldepth-localdepth,称b是a的镜像之一 186 | 187 | - 镜像族:所有的b就是a的镜像族 188 | 189 | 镜像是镜像族的一个,真镜像是只有第localdepth位不同,其他都相同,而镜像族则是所有localdepth-1位都相同,但是第localdepth位不同的所有index集合,(最高位限制到globaldepth位), 190 | 在分裂和合并时镜像族的概念非常重要!!! 191 | 192 | ##### CanIncr 193 | 194 | 判断是否能增长,条件为(1 << (global_depth_ + 1)) <= DIRECTORY_ARRAY_SIZE,很容易理解 195 | 196 | ##### CanShrink 197 | 198 | 判断是否能收缩,只需要所有localdepth小于globaldepth即可 199 | 200 | 201 | 202 | #### BUCKET 203 | 204 | hashtablebucketpage存储了bucket的内容,大小也是限制在一块磁盘大小,相关字段如下: 205 | 206 | - char occupied_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1] 位图,每一个比特位表示该槽是否被占据 207 | 208 | - char readable_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1] 位图,每一个比特位表示该槽是否可读,这里解释下占据和可读的关系,可读表示有数据,并且该数据合法(没有被删除), 209 | 被占据表示有数据,但是合法性不保证(可能被删除,插入数据可以直接写入被占据但是不可读的槽) 210 | 211 | - MappingType array_[1] 每一个数组位代表一个槽,存储插入哈希表的数据,这是个0长数组或者1长数组,可自行google,又在cmu学到一个新技巧 212 | 213 | ##### GetFirstNoOcpBkt 214 | 215 | 辅助方法之一,获取第一个没有被occupy的位,算法就是简单遍历occupied_数组,之所以没有设置相关字段保存第一个没有被occupy的位,是因为整个数据结构大小已经刚好存入一整块磁盘, 216 | 所以没有添加字段。这几个要存入整块磁盘的数据结构我都没有添加字段 217 | 218 | ##### GetFirstNoReadBkt 219 | 220 | 辅助方法之一,获取第一个不可读的位,算法同上 221 | 222 | ##### Insert 223 | 224 | 插入,首先遍历哈希表看是否有要插入的kv,若有则直接返回false,没有则找到第一个不可读的位写入要插入的kv(上面提到了),如果找第一个没有被占据的位,这样会大大浪费空间,会导致 225 | 后续10w级别的测试插入后删除,再插入时可能根本插入不进去,因为删除时只会将可读位置为不可读,但是occupied位是不会被修改的 226 | 227 | ##### Remove 228 | 229 | 删除,和插入相反,是位删除,只需简单标记位不可读即可,不能修改occupied位 230 | 231 | 232 | ### HASH TABLE IMPLEMENTATION 233 | 234 | 大boss来了。首先说明下extendiblehashtable相关字段,需要说明这个数据结构是不需要存入磁盘的。 235 | 236 | - page_id_t directory_page_id_ 记录存储hashtabledirectory的pageid,是整个哈希表最关键的字段 237 | 238 | - BufferPoolManager *buffer_pool_manager_ 内存缓冲池管理器,实验一我们已经拿捏 239 | 240 | - KeyComparator comparator_ 比较器,用于比较键值对 241 | 242 | - ReaderWriterLatch table_latch_ 读写锁,用于保护整个哈希表 243 | 244 | - HashFunction hash_fn_ 哈希函数,传入的值通过此函数计算出哈希值,哈希值取低globaldepth位即为要被存入的bucketid 245 | 246 | 247 | 接下来介绍哈希表的最主要的一些方法 248 | 249 | #### 构造方法 250 | 251 | - 首先需要新建directory_page的page,调用实验一写的new方法即可,然后unpin之 252 | - 接下来新建一个bucket页,同时再获取directory_page,将新建的bucketid写入数组下标为0的位置即可,这样我们的哈希表成功新建了directory_page,同时我们还新建了一个bucketpage, 253 | 用于装填数据。pageid已经写入directorypage中。完成哈希表初始化。 254 | 255 | - 最后在这里我要说明下带page和不带page的数据结构区别,如hashtabledirectorypage和hashtabledirectory,前者主要是侧重该数据结构存储在磁盘,后者侧重数据结构。当从磁盘读 256 | 出这块数据结构时,是一段字节序列,我们使用cpp自带的强转reinterpret_cast转化为相关数据结构。应该是很好理解。下面的辅助方法会提到 257 | 258 | #### 辅助方法 259 | 260 | - Hash 计算哈希值,哈希值为bucketid号 261 | 262 | - KeyToDirectoryIndex 计算出哈希值,哈希值取低globaldepth位即为要被存入的bucketid 263 | 264 | - FetchDirectoryPage 从内存缓冲区管理器通过directoryid号获取存储hashtabledirectorypage的page,将其中的字节序列强转为hashtabledirectorypage返回 265 | 266 | - FetchBucketPage 同上 267 | 268 | #### GetValue 269 | 270 | - 首先FetchDirectoryPage 271 | 272 | - 再计算哈希值,获取bucket_idx号,通过DirectoryPage获取数组下标为bucket_idx,得到bucketpageid。 273 | 274 | - 再FetchBucketPage。这一系列操作后面的方法一开始基本会用到,这就是从内存缓冲池管理器那里获取DirectoryPage,再通过DirectoryPage的bucket_page_ids_获取bucketpageid, 275 | 通过这个从内存缓冲池管理器那里获取对应bucketpage。后面的方法我就简称初始化。 276 | 277 | - 接下来调用其GetValue即可,最后unpin初始化阶段fetch两个page,否则缓冲区会爆,而且test的检查会很严格,一般对于此哈希表最少只需要3个page即可支持整个哈希表的内存运 278 | 作,这个我会在SplitInsert详细讨论。 279 | 280 | - 对于线程安全的GetValue,我们只需一开始对整个哈希表加读锁,而对于bucket页,我们也是请求读锁,这样可以减少锁的竞争。 281 | 282 | #### Insert 283 | 284 | - 初始化 285 | 286 | - 调用对应bucketpage的insert方法 287 | 288 | - 开始检查,是否因为bucket满了而插入失败,判断条件是!res && bucket_page->IsFull(),因为也有可能存在相同键值对而插入失败。 289 | 290 | - 如果是,我们unpin两个page,开始调用SplitInsert,否则我们我们unpin两个page,结束插入 291 | 292 | - 对于unpin脏位的判断,我一开始是认认真真写,但是后面开始出现数据不一致性问题,故除了GetValue之外,我全部都将传入的脏标志改为true 293 | 294 | - 对于线程安全的Insert,我们一开始对整个哈希表加读锁,而对于bucket页,我们请求写锁,这样可以减少锁的竞争。而万一我们需要修改directory,即bucket满了,我们需要分裂插入, 295 | 我们将读锁升级为写锁,释放bucket的写锁,因为我们已经对整个哈希表加写锁了。 296 | 297 | #### SplitInsert 298 | 299 | 整个实验最关键最难的两个方法之一,要插入的这个bucket满了。我们需要分裂。 300 | 301 | - 初始化,然后进行判断,局部深度和全局深度是否一致 302 | 303 | - 一致则看是能否扩展哈希表,即调用CanIncr() 304 | 305 | - 如果不能,我们在unpin两个page之后exit退出程序。如果可以的话我们调用IncrGlobalDepth() 306 | 307 | - 扩展哈希表之前,将所有原来属于同一个bucketid的bucketidx集合,成为B,深度通通加一 308 | 309 | - 然后我们再新建一个page,注意上面提到至少三个page就可以满足我们哈希表在内存的运行,ag测试程序也是这样测试,这里说一下哪三个呢,directory,满的bucket,和新建的bucket,一共 310 | 三个 311 | 312 | - 下面获取原先满bucketidx的镜像族C,其个数刚好为B的二分之一,将其bucketid改为新建的bucketid。这样就成功完成了分裂 313 | 314 | - 取出原先满bucket中所有数据,并且清空之,将它们重新计算哈希分入新建的bucket和原先满(现在被清空)的bucket。有一个很极端的情况,就是分裂后所有数据还是集中在一个bucket中, 315 | 此时我们需要unpin另一个空的bucket,继续循环上述操作进行分裂。 316 | 317 | - 若极端情况没有出现,我们获取待插入的键值对要插入的page,另一个不需要插入的page我们unpin掉,执行插入,最后unpin两个page,结束。 318 | 319 | 320 | 321 | #### Remove 322 | 323 | - 初始化 324 | 325 | - 调用对应bucketpage的remove方法 326 | 327 | - 开始检查,和insert很相似。如果出现res && bucket_page->IsEmpty(),即本次的确删除了一个键值对并且导致了bucket空,如果单纯判断bucket_page->IsEmpty()有可能之前已经空了, 328 | 这样如果调用合并是浪费成本,因为之前已经操作过了,本次删除是啥也没有干。那我们就unpin两个page,调用Merge方法合并。 329 | 330 | - 如果上述条件不满足,我们unpin两个page结束 331 | 332 | - 线程安全的Remove方法,我们采取的策略和Insert一模一样 333 | 334 | #### Merge 335 | 336 | 整个实验最关键最难的两个方法之一,删除后的这个bucket为空。我们检查是否能够合并。 337 | 338 | - 初始化 339 | 340 | - 获取空镜像B的真镜像C,进行条件判断:B为空并且B和C的局部深度相等,第二个条件很重要,因为有可能B和C局部深度不相等,C大于B,这样的话,相当于B统一了但是C自己内部还没有统一 341 | 342 | - 满足条件,开始循环 343 | 344 | - 将所有是B的bucketidx全部改为C,然后将所有是C的bucketidx局部深度减1,相当于SplitInsert中相关的逆操作 345 | 346 | - 更新全局遍历,开始新一轮循环,具体为unpin掉B,求C的真镜像D,将C赋值给B,将D赋值给C。 347 | 348 | - 循环结束 349 | 350 | - 收缩到不能收缩为止,unpin两个page,结束 351 | 352 | 353 | ### 相关数据结构在bustub数据库中的位置层级 354 | 355 | 实验二实现的是哈希索引,其实不论是哈希索引还是b+树索引,它们的数据结构和存储的数据都是分成一块块地存入磁盘,而相关的机制则是由实验一实现的内存缓冲池实现。 356 | 357 | 358 | 359 | ## lab3 360 | 361 | 实验三是实现火山模型,将上层的sql查询(可以看成经过SQL解析后的结果)转换为底层对数据库的操作,在实验三,一共分成九大模块,我将不仅仅说明这九大sql操作的实现,还会深入底层 362 | 分析这些操作在底层是如何进行的。首先我将对需要阅读的所有类进行分析,下面是这些类的层次结构 363 | 364 | ### ExecutorContext 365 | + Transaction *transaction_ 366 | + Catalog *catalog_ 367 | + BufferPoolManager *bpm_ 368 | + TransactionManager *txn_mgr_ 369 | + LockManager *lock_mgr_ 370 | 371 | ### Catalog *catalog_ 372 | + BufferPoolManager *bpm_ 373 | + LockManager *lock_manager_ 374 | + LogManager *log_manager_ 375 | + unordered_map> tables_ 存储tableid和table的映射关系 376 | + unordered_map table_names_ 存储tablename和tableid的映射关系 377 | + atomic next_table_oid_ 原子类型,生成tableid 378 | + unordered_map> indexes_ 存储indexid和index的关系 379 | + unordered_map> index_names_ 存储tablename indexname和indexid的关系 380 | + atomic next_index_oid_ 原子类型,生成indexid 381 | 382 | ### TableInfo(table) 383 | + Schema schema_ 相当于表结构 384 | + string name_ 表名字 385 | + unique_ptr table_ 按表结构存储表的数据,组织形式为tuple,是一个指针 386 | + table_oid_t oid_ 表id 387 | 388 | ### Schema(存储表项) 389 | + uint32_t length_ 一个tuple的长度 390 | + vector columns_ 所有的列 391 | + bool tuple_is_inlined_ 是否所有的列都是inlined 392 | + vector uninlined_columns_ 所有uninlined的列 393 | 394 | ### Column(存储每一个表项内容) 395 | + string column_name_ 列名 396 | + TypeId column_type_ 列类型 397 | + uint32_t fixed_length_ 398 | + uint32_t variable_length_ 列变量长度 399 | + uint32_t column_offset_ 该列在tuple中的偏移量 400 | + AbstractExpression *expr_ 用于创建该列的表达式,通过调用表达式的evaluate方法,传入tuple和schema,可以获取对应列的值value,而tuple则可以通过value数组构造 401 | 402 | ### TableHeap(存储tuple,是数据库存储数据的数据结构) 403 | + BufferPoolManager *buffer_pool_manager_ 404 | + LockManager *lock_manager_ 405 | + LogManager *log_manager_ 406 | + page_id_t first_page_id_ 存储tuple的第一个pageid,其中记录了pageid链的信息,sql查询的底层就是对这些进行操作 407 | 408 | ### tuple(数据库中数据载体) 409 | + bool allocated_ 是否被分配 410 | + RID rid_ 411 | + uint32_t size_ 大小 412 | + char *data_ 数据 413 | 414 | ### IndexInfo(index) 415 | + Schema key_schema_ 相当于表结构 416 | + string name_ index名字 417 | + unique_ptr index_ 存储索引值 418 | + index_oid_t index_oid_ indexid 419 | + string table_name_ 对应的table名字 420 | + const size_t key_size_ 索引大小 421 | 422 | ### Index 423 | + unique_ptr metadata_ index存储的数据 424 | 425 | ### ExtendibleHashTableIndex entend Index 426 | + KeyComparator comparator_ 比较器 427 | + ExtendibleHashTable container_ 哈希表,存储index 428 | 429 | 下面分析一下底层table存储tuple的构造,即分析TablePage类的字段,和文件系统十分相似其实。 430 | 431 | 这是整体的结构: 432 | 433 | --------------------------------------------------------- 434 | | HEADER | ... FREE SPACE ... | ... INSERTED TUPLES ... | 435 | --------------------------------------------------------- 436 | 437 | 这是header的结构: 438 | 439 | ---------------------------------------------------------------------------- 440 | | PageId (4)| LSN (4)| PrevPageId (4)| NextPageId (4)| FreeSpacePointer(4) | 441 | ---------------------------------------------------------------------------- 442 | ---------------------------------------------------------------- 443 | | TupleCount (4) | Tuple_1 offset (4) | Tuple_1 size (4) | ... | 444 | ---------------------------------------------------------------- 445 | 446 | 接下来介绍查询执行需要涉及到的类及其继承关系 447 | 448 | ### AbstractExpression 449 | 表达式类,计算相关比较结果和取值等,关键方法:evaluate 450 | + AggregateValueExpression 451 | + ColumnValueExpression 452 | + ComparisonExpression 453 | + ConstantValueExpression 454 | 455 | ### AbstractExecutor 456 | 执行类,执行相关查询操作,也是我们本次实验需要补充的,关键方法:init、next 457 | + SeqScanExecutor 458 | + InsertExecutor 459 | + DeleteExecutor 460 | + UpdateExecutor 461 | ... 462 | 463 | ### AbstractPlanNode 464 | 计划类,存储相关查询对应的信息 465 | + SeqScanPlanNode 466 | + InsertPlanNode 467 | + DeletePlanNode 468 | + UpdatePlanNode 469 | ... 470 | 471 | 472 | 而我们需要写的executer则是在上面表示的数据库中进行操作,对于每个execute,我们实现其init和next方法 473 | 474 | ### SEQUENTIAL SCAN 475 | 等价于:select * from table where 476 | table存储在传入的ExecutorContext,而where存储在SeqScanPlanNode,通过GetPredicate()获取 477 | 478 | #### init 479 | 480 | - 通过exec_ctx_获取底层table,再获取这个table的迭代器,保存此迭代器 481 | 482 | #### next 483 | 484 | - 首先通过exec_ctx_获取要输入的scheme格式 485 | 486 | - 再通过plan_->OutputSchema()获取输出tuple的格式 487 | 488 | - 然后进入while循环,当迭代器没有消费完则持续循环 489 | 490 | - 通过plan_->GetPredicate()获取select条件predict,是一个ComparisonExpression 491 | 492 | - 若predict为空或者predict调用evaluate为true,依次调用输出scheme的所有column的evaluate方法计算value存入value数组,通过value数组构造tuple,返回之。注意是一次性返回一个 493 | 494 | - 否则即不满足条件,我们开启新一轮循环 495 | 496 | - 若循环结束,表示没有tuple可消费,table已经走到底,返回false 497 | 498 | ### INSERT 499 | 等价于:insert into table(field1,field2) values(value1,value2) 500 | table存储在传入的exec_ctx_,value1存储有两种情况,如果是rawinsert,则存储在plan_中,如果不是则调用child_executor_的next方法获取。插入更新删除特别相似 501 | 502 | #### init 503 | 504 | - 若child_executor_不为空,调用child_executor_的init 505 | 506 | #### next 507 | 508 | - 通过exec_ctx_获取要执行插入的table 509 | 510 | - 通过plan_调用IsRawInsert()进行判断,若是则从plan处调用RawValues()获取待插入值 511 | 512 | - 不是则从child_executor_的Next方法获取插入值。 513 | 514 | - 调用TableHeap的InsertTuple插入值, 515 | 516 | - 调用exec_ctx_->GetCatalog()->GetTableIndexes获取索引数组,更新索引 517 | 518 | - 注意必须一口气插入所有value,然后返回false即可 519 | 520 | 521 | ### UPDATE 522 | 等价于:update table set field1=value1 where 523 | table存储在传入的exec_ctx_ 524 | 525 | #### init 526 | 527 | - 若child_executor_不为空,调用child_executor_的init 528 | 529 | #### next 530 | 531 | - 待修改的原tuple全部通过child_executor的Next方法获取,child_executor_的next方法作为循环判断条件 532 | 533 | - 循环体中调用下面提供的GenerateUpdatedTuple进行更新,注意索引也要同步更新 534 | 535 | - 调用table的UpdateTuple方法完成底层更新 536 | 537 | - 完成所有更新之后返回false 538 | 539 | ### DELETE 540 | 等价于:delete from table where 541 | table存储在传入的exec_ctx_ 542 | 543 | #### init 544 | 545 | - 若child_executor_不为空,调用child_executor_的init 546 | 547 | #### next 548 | 549 | - 待修改的原tuple全部通过child_executor的Next方法获取,child_executor_的next方法作为循环判断条件 550 | 551 | - 循环体中调用table的MarkDelete进行删除,注意索引也要同步删除 552 | 553 | - 完成所有更新之后返回false 554 | 555 | 556 | ### NESTED LOOP JOIN 557 | 558 | #### init 559 | 560 | - 对left_executor_和right_executor_执行init,各自调用GetOutputSchema()获取二者输出格式 561 | 562 | - 调用NestedLoopJoinPlanNode的Predicate()获取是否进行连接条件 563 | 564 | - left_executor_的next为外循环,right_executor_的next为内循环,一定要这样,因为ag会测试磁盘花费。注意内循环一开始right_executor_调用Init初始化迭代器 565 | 566 | - 通过是否连接条件判断连接。连接后将连接的tuple存储在数组ret_中即可,这样留给next使用,为啥要在init中干好这些事呢,因为连接是pipeline breaker,所以下一个也是和这个相似 567 | 568 | #### next 569 | 570 | - 使用ret_的数组迭代器,每次返回一个即可 571 | 572 | ### HASH JOIN 573 | 首先在头文件中加入哈希表相关代码.其次添加hash_map_字段存储key->vector 574 | 575 | #### init 576 | 577 | - 对left_executor_和right_executor_执行init 578 | 579 | - 计算所有left_executor_的tuple的key,加入hash_map_ 580 | 581 | - 计算所有right_executor_的tuple的key,在哈希表中查找,将key相同的全部连接之。保存于tuple数组供next使用。 582 | 583 | #### next 584 | 585 | - 使用ret_的数组迭代器,每次返回一个即可 586 | 587 | 588 | ### AGGREGATION 589 | groupby和having,实现MIN 最小值,MAX 最大值,SUM 求和,AVG 求平均,COUNT 计数。 590 | 591 | #### init 592 | 593 | - 首先对children调用init 594 | 595 | - 其次存储所有结果,因为这也是pipelinebreaker,在children的next循环中 596 | 597 | - MakeAggregateKey获取groupby的key数组,调用MakeAggregateValue获取value数组,或者说计算value,这个value就是上面诸如MAX,MIN,然后将二者作为键值对插入aht_ 598 | 599 | #### next 600 | 601 | - 若aht_迭代器指向末尾,返回false 602 | 603 | - 获取当前迭代器指向的key和value 604 | 605 | - 若plan_->GetHaving()为空或者plan_->GetHaving()->EvaluateAggregate满足条件,我们返回输出,否则我们接着下一轮迭代 606 | 607 | ### LIMIT 608 | 限制执行次数,只需设立相关字段保存执行次数,每执行一次加1即可 609 | 610 | #### init 611 | - 初始化执行次数time,调用child_executor_的Init 612 | 613 | #### next 614 | - 调用LimitPlanNode的GetLimit()获取最大执行次数 615 | 616 | - 当执行次数和child_executor_的Next执行结果为真,我们则返回true,并且将执行次数加1,否则返回false 617 | 618 | ### DISTINCT 619 | 620 | #### init 621 | 622 | - 调用GetDistinctKey和GetDistinctValue计算child_executor_->Next获取的tuple,保存在哈希表中 623 | 624 | #### next 625 | 626 | - 使用init新建的哈希表迭代器,一次返回一对键值对即可 627 | 628 | ## lab4 629 | 630 | 实验四是实现事务机制,实现线程安全的事务机制,其实和os类似,这里可以把事务类比成os的线程或进程,同时实现四大隔离机制。 631 | 632 | 首先说明事务的四个状态 633 | 634 | + GROWING 此阶段可以获取锁,但是不能释放锁 635 | 636 | + SHRINKING 一旦GROWING开始释放锁,将转换为此阶段 637 | 638 | + COMMITTED 事务成功提交 639 | 640 | + ABORTED 事务失败 641 | 642 | 事务四大特征acid: 643 | 644 | + 原子性: 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行 645 | 646 | + 一致性: 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的 647 | 648 | + 隔离性: 多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离 649 | 650 | + 持久性: 表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中 651 | 652 | 四大隔离机制: 653 | 654 | ### LOCK MANAGER and DEADLOCK PREVENTION 655 | 656 | + 读未提交(Read Uncommitted) 可能出现脏读、不可重复读、幻读 657 | 658 | + 读已提交(Read Committed) 不会出现脏读 659 | 660 | + 可重复读(Repeatable Read) 不会出现脏读、不可重复读 661 | 662 | + 串行化(Serializable) 最高级别,啥都没有 663 | 664 | ### 死锁预防(Would-Wait) 665 | 666 | + 年轻事务如果和老事务有上锁冲突, 年轻事务需要等老事务解锁 667 | + 老事务如果和年轻事务有上锁冲突, 老事务直接将年轻的事务统统杀掉(回滚), 非常的野蛮hh 668 | 669 | 这样破环了循环等待条件 670 | 671 | 下面是本次实验涉及到的数据结构 672 | 673 | #### LockManager 674 | + mutex latch_ 675 | + unordered_map lock_table_ 存储锁队列的哈希表 676 | 677 | #### LockRequestQueue 678 | + list request_queue_ 679 | + condition_variable cv_ 条件变量,保护request_queue_ 680 | + txn_id_t upgrading_ 681 | 682 | #### LockRequest 683 | + txn_id_t txn_id_ 事务id 684 | + LockMode lock_mode_ s锁还是x锁(相当于读锁与写锁) 685 | + bool granted_ 686 | 687 | 688 | 689 | 四大隔离机制均会在本次实验底层详细实现(其实串行化没有实现,因为这里没有实现给index加锁) 690 | 691 | #### LockShared 692 | 693 | 获取s锁 694 | 695 | + 若事务状态为ABORTED,返回false 696 | 697 | + 若事务状态为SHRINKING,此时不能获取锁,返回false 698 | 699 | + 若隔离级别为读未提交,此时没有s锁,也返回false(读未提交要对数据库进行读操作,直接读,不需上锁,这样才可能会出现脏读,也就是在读的过程中,其他事务对数据进行了修改) 700 | 701 | + 如果该事务对该rid已经上了s锁或者x锁,返回true 702 | 703 | + 下面获取条件变量的锁 704 | 705 | + 将锁请求添加到相关锁队列 706 | 707 | + 新建判断函数 708 | 709 | + 进入循环,若判断函数为假,或者事务状态为ABORTED,表面在等待过程被杀,我们直接退出循环,返回false。 710 | 711 | + 否则我们调用对应锁队列的条件变量的wait函数,即释放锁,等待唤醒,唤醒时抢锁 712 | 713 | + 最后获取s锁,我们在事务中记录,并且返回true 714 | 715 | + 接下来,我们需要重点说一下判断函数的过程我们遍历锁队列,通过txn_id_来判断年轻与年老,将所有在自己之前并且比自己年轻的,并且请求锁类型为x锁的事务通通杀掉,即相当于将状态改 716 | 为ABORTED,并且将其在所队列中删除,将自己的相关字段删去锁。记住,三个条件,在自己之前、请求x锁、比自己年轻。三个条件缺一不可。若自己之前出现了比自己年老的x锁请求,我们只 717 | 能返回false。否则返回true。这里注意cpp的深拷贝与浅拷贝。我就是图方便直接复制锁队列,导致根本没有修改锁队列。最后要返回时,若自己杀了事务,必须调用条件变量的notify_all唤 718 | 醒所有事务,因为有可能前面的事务由于被杀导致部分事务已经可以获取锁,不手动唤醒可能会导致永远没有锁获取,因为原来作为唤醒者的事务现在被杀 719 | 720 | 721 | #### LockExclusive 722 | 723 | 获取x锁 724 | 725 | + 若事务状态为ABORTED,返回false 726 | 727 | + 如果该事务对该rid已经上了s锁,返回false 728 | 729 | + 若事务状态为SHRINKING,此时不能获取锁,返回false 730 | 731 | + 如果该事务对该rid已经上了x锁,返回false 732 | 733 | + 下面获取条件变量的锁 734 | 735 | + 将锁请求添加到相关锁队列 736 | 737 | + 新建判断函数 738 | 739 | + 进入循环,若判断函数为假,或者事务状态为ABORTED,表面在等待过程被杀,我们直接退出循环,返回false。 740 | 741 | + 否则我们调用对应锁队列的条件变量的wait函数,即释放锁,等待唤醒,唤醒时抢锁 742 | 743 | + 最后获取x锁,我们在事务中记录,并且返回true 744 | 745 | + 上述过程和获取s锁差不多,这里说一下判断函数和上面也差不多,不同地方在于杀事务时,前面所有比自己年轻的事务通通杀死,前面是比自己年轻的x锁请求杀死。杀完后若自己前面还有锁, 746 | 则等待,而前者是只要杀完后前面没有x锁即可获取 747 | 748 | #### LockUpgrade 749 | 750 | 锁升级,可以看成先删除s锁,再获取x锁,删除s锁时注意notify_all()即可,可以参照上面两个和下面一个函数 751 | 752 | #### Unlock 753 | 754 | 释放锁 755 | 756 | + 首先若队列中没有自己的锁请求,返回false(不能虚假释放嘻嘻) 757 | 758 | + 否则我们将该锁从队列删除,调用notify_all(),自己相关获取锁字段也要擦除 759 | 760 | + 下面就是对状态的修改,若目前状态是GROWING,因为不可能我们也就是SHRINKING或者是ABORTING了我们还修改状态为SHRINKING 761 | 762 | + 若当前要释放的是s锁并且隔离级别为可重复读,或者是x锁,我们修改状态为SHRINKING。这里详细解释一下:因为RC是可以重复的加S锁/解S锁的, 这样他才可以在一个事务周期中读到不同的 763 | 数据 764 | 765 | 766 | 767 | ### CONCURRENT QUERY EXECUTION 768 | 769 | 并行查询,这里我们修改四个地方:SEQUENTIAL_SCAN、INSERT、UPDATE、DELETE 770 | 771 | #### SEQUENTIAL_SCAN 772 | 773 | + 在next方法中,一开始我们需要获取s锁,若不是读未提交,上面讲过了,我们调用LockShared获取s锁 774 | 775 | + 读完后,如果隔离级别是读已提交RC,我们需要释放s锁,因为RC可以随意加s锁释放s锁而不进入SHRINKING状态,而若是可重复读则不需要 776 | 777 | #### INSERT 778 | 779 | + 我们需要获取x锁,但是插入之前它不存在怎么办呢,我们先插入,然后再获取x锁即可这样RU可以读到,但是其他的读不到哦 780 | 781 | + 注意最后我们需要更新索引,table更新已经帮我们实现了 782 | 783 | #### DELETE 784 | 785 | + 获取x锁,但是我们是不能释放的,一旦释放我们就不能获取锁了 786 | 787 | + 同样注意索引更新哦 788 | 789 | #### UPDATE 790 | 791 | + 同上 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | -------------------------------------------------------------------------------- /database.md: -------------------------------------------------------------------------------- 1 | # 数据库 2 | 3 | 4 | ## STORAGE 5 | 6 | #### Disk Manager 7 | 8 | #### Bufferpool 9 | 10 | + 控制块与缓冲页 11 | 12 | + free链表 13 | 14 | + flush 链表 15 | 16 | + lru链表 17 | 18 | 19 | 20 | ## INDEX索引 21 | 22 | #### 索引分类 23 | 24 | ##### 数据结构 25 | 26 | + B+树索引 27 | 28 | + 哈希索引 29 | 30 | ##### 物理存储 31 | 32 | + 聚簇索引 33 | 34 | + 二级索引 35 | 36 | #### B+树 37 | 38 | ##### 数据结构 39 | 40 | 插入 41 | 42 | ![insert1](insert1.png) 43 | 44 | ![insert2](insert2.png) 45 | 46 | 删除 47 | 48 | ![delete](delete.png) 49 | 50 | ##### 并发控制 51 | 52 | 获取父节点的闩 53 | 54 | 获取孩子节点的闩 55 | 56 | 如果是安全的,则释放父节点的闩 57 | 58 | 一个 安全节点 是不能分裂或者合并的节点: 59 | 60 | 不是满的(在插入的时候) 61 | 62 | 不少于一半(在删除的时候) 63 | 64 | #### 动态哈希表 65 | 66 | ##### 数据结构 67 | 68 | ##### 并发控制 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ## SQL的执行 80 | 81 | #### SQL解析 82 | 83 | 从sql语句生成sql语法分析树 84 | 85 | #### query processing 86 | 87 | 将语法分析树生成一个树状的查询计划,数据从树叶流向根,有三种模型 88 | 89 | ##### Processing Model 90 | 91 | + Iterator Model(火山模型,open next close) 92 | 93 | + Materialization Model(物化模型) 94 | 95 | + Vectorized Model(向量化模型) 96 | 97 | #### SQL优化 98 | 99 | ##### 连接算法 100 | 101 | M个page,m个tuple 连接 N个page,n个tuple,3块磁盘 102 | 103 | + Simple Nested Loop Join 104 | 105 | + Block Nested Loop Join 106 | 107 | + Sort Merge Join:对整体不停n-1归并排序 108 | 109 | + Hash Join 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ## 日志 118 | 119 | 120 | #### 概述 121 | 122 | ##### Write-Ahead Log策略 123 | 124 | ##### io写入原子性 125 | 126 | ##### 日志类型 127 | 128 | + 物理日志 129 | 130 | + 逻辑日志 131 | 132 | + 逻辑物理结合 133 | 134 | 135 | 136 | #### redo log 137 | 138 | 作用:记录对一个页面(索引页,数据页,undo页)的修改 139 | 140 | 过程:对一个页面修改完后(还在内存),写入redo页 141 | 142 | ##### redo log格式 143 | 144 | + 物理日志:在某个偏移量修改len长度字节 145 | 146 | + 逻辑物理日志:在某页面插入、删除记录,或者创建页面 147 | 148 | ##### redo log block 149 | 150 | + 所有的redo 日志写入redo日志文件 151 | 152 | + 而redo日志文件被划分为一个个block,512字节 153 | 154 | + 前四个blog特殊,第一个为log file header,2和4为checkpoint1和2 155 | 156 | ##### redo log buffer 157 | 158 | + 159 | 160 | ##### redo log组和mini transaction 161 | 162 | + 163 | 164 | ##### log sequence number(lsn) 165 | 166 | lsn值记录写入的redo log字节数,初始值为8704 167 | 168 | + flush_lsn 169 | 170 | + buffer pool flush链表的lsn 171 | 172 | ##### checkpoint的执行 173 | 174 | redo log可以被覆盖,意味着,该redo log对应的脏页,已经刷盘了 175 | 176 | + 通过flush链表最末端计算checkpoint lsn 177 | 178 | + 写入到checkpoint block 179 | 180 | ##### 崩溃恢复 181 | 182 | + 通过最近的checkpoint lsn确定恢复起点 183 | 184 | + 注意,恢复起点到终点的这些日志,并不是全部可以执行!这段日志的操作结果有的已经后台线程刷盘了!这些日志不能重复执行,不是```幂等``` 185 | 186 | + 每个页面有一个值,记录最近一次持久化的lsn,如果该值大于checkpoint,则不需执行! 187 | 188 | 189 | 190 | 191 | 192 | 193 | #### undo log 194 | 195 | 作用:事务在执行一半,忽然宕机,开机后,必须回滚到该事务执行之前的状态 196 | 197 | 过程:对记录更新之前,写入undo log 198 | 199 | ##### row pointer 200 | 201 | 指向undo日志的指针 202 | 203 | ##### insert的undo log 204 | 205 | + 206 | 207 | ##### delete的undo log 208 | 209 | + 中间态,暂时不移入垃圾链表,等事务提交再移入 210 | 211 | + 注意还存储了旧纪录的roll pointer 212 | 213 | ##### update的undo log 214 | 215 | + 不更新主键(直接删除再插入,日志类似delete) 216 | 217 | + 更新主键(二级索引)(delete+insert两日志) 218 | 219 | 220 | 221 | ##### undo页 222 | 223 | + insert类型 224 | 225 | + update类型 226 | 227 | ##### undo链表(undo段) 228 | 229 | + 每个事务唯一 230 | 231 | + 重用原则 232 | 233 | ##### 回滚(rollback)段 234 | 235 | + slot 236 | 237 | + cache 238 | 239 | + history 240 | 241 | ##### undo日志分配、写入过程 242 | 243 | + 分配回滚段 244 | 245 | + 寻找是否有对应缓存slot 246 | 247 | + 没有则寻找一个可用slot 248 | 249 | + 如果非cache,则分配undo seg,将firstpage填入slot 250 | 251 | #### undo log删除时机 252 | 253 | + insert类型立马删除 254 | 255 | + update类型,当需要purge的时候,系统最早的readview的事务no若大于等于该history uodo log,则可以放心删除了 256 | 257 | ##### undo log buffer 258 | 259 | 刷盘时机 260 | 261 | #### arise算法 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ## 事务 270 | 271 | #### ACID 272 | 273 | #### 事务并发执行的问题 274 | 275 | + 脏写:修改了另一个事务未提交的数据 276 | 277 | + 脏读:读取到另一个事务未提交的数据 278 | 279 | + 不可重复读:一个事务修改了另一个未提交事务读取的数据 280 | 281 | + 幻读:一个事务两次查询出某些符合条件的数据数量不一致(insert导致) 282 | 283 | #### 四大隔离级别 284 | 285 | + 读未提交 286 | 287 | + 读已提交 288 | 289 | + 可重复读 290 | 291 | + 可串行化 292 | 293 | #### MVCC 294 | 295 | ##### readview 296 | 297 | 注意下面四个值 298 | 299 | + 生成该readview时活跃的读写事务列表:a 300 | 301 | + 列表里最小的事务id:b 302 | 303 | + 生成readview时系统应该分配给下一个事务的id:c 304 | 305 | + 生成该readview的事务id:d 306 | 307 | 访问版本: 308 | 309 | + 等于d可以读取 310 | 311 | + 小于b能读取 312 | 313 | + 大于等于c则不能读取 314 | 315 | + 在a中则不能读取 316 | 317 | + 不在则可以读取 318 | 319 | 320 | + 读未提交:直接读取最新版本 321 | 322 | + 读已提交:每一次读取生成readview 323 | 324 | + 可重复读:第一次读取生成readview(其实,按mvcc策略,它还能避免幻读) 325 | 326 | + 可串行化:加锁读取 327 | 328 | 329 | #### 锁 330 | 331 | ##### 共享锁和排他锁 332 | 333 | ##### 全局锁 334 | 335 | 锁住整个数据库 336 | 337 | ##### 表级锁 338 | 339 | + 表锁 340 | 341 | + 元数据锁 342 | 343 | + 意向锁 344 | 345 | + auto_inc锁 346 | 347 | ##### 行级锁 348 | 349 | + record lock 350 | 351 | + gap lock 352 | 353 | + next-key lock 354 | 355 | + 隐式锁 356 | 357 | #### 当前读和快照读 358 | 359 | 当前读:每次读取的都是最新数据,但是加锁 360 | 361 | 快照读:按mvcc版本链读取数据 362 | 363 | 364 | 365 | ##### 2PL 366 | 367 | + 2PL 368 | 369 | + 死锁检测 检测wait_for_map 370 | 371 | + 死锁预防 wait-die wound-wait 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | -------------------------------------------------------------------------------- /delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shootfirst/CMU15-445/c20aac58e05fd86f0c63b0f8fed5ecdf59f54104/delete.png -------------------------------------------------------------------------------- /insert1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shootfirst/CMU15-445/c20aac58e05fd86f0c63b0f8fed5ecdf59f54104/insert1.png -------------------------------------------------------------------------------- /insert2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shootfirst/CMU15-445/c20aac58e05fd86f0c63b0f8fed5ecdf59f54104/insert2.png -------------------------------------------------------------------------------- /no_sql.md: -------------------------------------------------------------------------------- 1 | # No SQL数据库 2 | 3 | ## Sql与NoSql数据库 4 | 5 | ### 关系型数据 6 | 7 | 是创建在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据 8 | 9 | 优点: 10 | 11 | 事务一致性 12 | 13 | 复杂查询 14 | 15 | 缺点: 16 | 17 | 海量数据 18 | 19 | 高并发 20 | 21 | 表结构修改 22 | 23 | ### NoSql数据库 24 | 25 | 极高并发读写:redis 26 | 27 | 海量数据访问:mongodb 28 | 29 | 30 | 31 | 32 | 33 | 34 | ## Redis 35 | 36 | ### 数据类型 37 | 38 | ![redis数据结构和数据类型](redis_datastructure.png) 39 | 40 | 41 | #### String 42 | 43 | ##### 底层数据结构 44 | 45 | SDS 46 | 47 | ##### 编码方式 48 | 49 | + int 50 | 51 | + embstr 52 | 53 | + raw 54 | 55 | ##### 应用 56 | 57 | + 缓存对象 58 | 59 | + 计数 60 | 61 | + 分布式锁: 62 | 63 | - SET $uuid EX NX 64 | 65 | - 检测是否是自己的锁,是则释放 66 | 67 | 68 | #### List 69 | 70 | ##### 底层数据结构 71 | 72 | quicklist 73 | 74 | 用ziplist作为链表节点的链表 75 | 76 | ziplist 77 | 78 | prelen encoding type content 79 | 80 | ##### 应用 81 | 82 | + 消息队列(破产版) 83 | 84 | 85 | #### hash 86 | 87 | ##### 底层数据结构 88 | 89 | + listpack:encoding content len 90 | 91 | + 哈希表:渐进式哈希 92 | 93 | 94 | ##### 应用 95 | 96 | + 当购物车 97 | 98 | 99 | #### set 100 | 101 | ##### 底层数据结构 102 | 103 | + 整数集合 104 | 105 | + 哈希表 106 | 107 | ##### 应用 108 | 109 | + 点赞 110 | 111 | + 共同关注 112 | 113 | 114 | #### zset 115 | 116 | ##### 底层数据结构 117 | 118 | + listpack 119 | 120 | + 跳表 121 | 122 | ##### 应用 123 | 124 | + 排行榜 125 | 126 | 127 | #### bitmap 128 | 129 | #### hyperloglog 130 | 131 | #### geo 132 | 133 | #### stream 134 | 135 | 136 | 137 | 138 | 139 | ### 持久化 140 | 141 | #### AOF 142 | 143 | 每一条命令,写入内存后,在将命令写入aof文件 144 | 145 | ##### 落盘时机 146 | 147 | + 每次 148 | 149 | + 每秒 150 | 151 | + 从不 152 | 153 | ##### AOF重写 154 | 155 | + 子进程cow,对AOF文件进行重写 156 | 157 | + 父进程执行客户端发来的命令,将执行后的写命令追加到AOF 缓冲区;将执行后的写命令追加到AOF 重写缓冲区; 158 | 159 | + 子进程结束,给父进程信号,父进程将AOF重写缓冲区数据写入AOF文件,然后替换之 160 | 161 | #### RDB 162 | 163 | 记录某一时刻内存数据 164 | 165 | ##### RDB时机 166 | 167 | ##### RDB写 168 | 169 | 170 | #### 混合持久化 171 | 172 | 173 | ### redis连接 174 | 175 | #### redis多线程模型 176 | 177 | redis主线程将套接字交给io线程去解析,然后将解析结果返回,由主线程执行,执行完毕后,对于写io,主线程仍然交给io线程 178 | 179 | 180 | ### 内存淘汰与过期淘汰 181 | 182 | #### 过期淘汰 183 | 184 | 过期字典 185 | 186 | ##### 删除策略 187 | 188 | + 定时删除 189 | 190 | + 惰性删除 191 | 192 | + 定期删除 193 | 194 | #### 内存淘汰 195 | 196 | + random 197 | 198 | + lru 199 | 200 | + lfu 201 | 202 | + ttl 203 | 204 | 205 | 206 | 207 | 208 | ### 缓存 209 | 210 | ##### 缓存雪崩 211 | 212 | 大量数据同时过期,或者redis宕机,导致大量访问mysql 213 | 214 | + 随机过期时间 215 | 216 | + 加互斥锁,保证同一时间只有一次访问redis 217 | 218 | ##### 缓存击穿 219 | 220 | 热点数据过期,导致访问mysql 221 | 222 | ##### 缓存穿透 223 | 224 | redsi和mysql都尼玛没有 225 | 226 | + 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在 227 | 228 | ##### redis与mysql一致性 229 | 230 | 先更新数据库,再删除缓存 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | ### 主从复制 243 | 244 | #### 主从复制过程 245 | 246 | + 建立连接 247 | 248 | + 主服务器发送rdb 249 | 250 | + 主服务器发送 replication buffer 251 | 252 | + 维护连接,命令传播 253 | 254 | 255 | #### 主从断连后 256 | 257 | 增量复制:repl_backlog_buffer 258 | 259 | 260 | #### 分摊主服务器压力 261 | 262 | 263 | #### 主从不一致 264 | 265 | + 异步复制 266 | 267 | + 脑裂 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | ## LSM Tree 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /redis_datastructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shootfirst/CMU15-445/c20aac58e05fd86f0c63b0f8fed5ecdf59f54104/redis_datastructure.png --------------------------------------------------------------------------------