基于mmap的KV数据存储实现(一)概述

来源:互联网 发布:华为云计算开发工程师 编辑:程序博客网 时间:2024/05/29 08:39

前言

工作中遇到一些对kv存储的需求,比如推荐系统中需要存储一个商品id对应的相似商品id的 list,或者是一个用户的浏览过的商品id的list,这就需要一个键值对去存储。本文描述的存储基于这个需求简单的实现了一个版本,实际工作中要复杂的多,为了让读者便于理解,就基于这个分析一个主键为string的key,value是id的list的存储实现。本存储用java实现,底层采用mmap的方式,可以实现落盘,并且性能优秀。

存储系统结构

  • index
    数据的索引,每个key都会在index中记录它对应的value存储的位置。
  • block
    value实际存储的位置。
  • table
    一个完整的数据表,包含了block,index以及config。
  • config
    一些配置信息,记录比如当前使用的是哪一个索引。
  • log
    记录每条数据的日志信息

整个系统由以上几个主要模块构成,每写一条数据如下图所示。
这里写图片描述
如上图所示,如果有一个写入数据(key,value)的请求,首先会把这一条记录追加写在日志文件里,然后在通过配置信息选取一个索引(index)文件里,从索引文件里找到这个数据的key的记录,如果没有记录,新建一条记录,这个记录就是这个key的value在block中的位置,如果索引文件有这个记录,可以获取这个位置,然后在block的对应位置写下这个数据,一次写入请求完成。
图中的模块index部分分有index0和index1,存在两个的原因是用于在系统扩容时,需要重新建立索引,可以在两个索引中切换。block的部分其实是有许多块的,可以理解为这些小的block快组成一个大得Block,每个block的大小是固定的一个值,不够就会新增一个block的块,图中如果不够会新增一个block5,。当然事实上index0或者index1本身也是有许多数据块组成的,但这些数据块整体上是属于一份索引,也就是说index0或者index1本身是包含所有索引信息的,设置两个只是用于扩容重建的时候。而block部分实际上读者可以理解为就是一个整体就是大的Block。就相当于index0对应整个Block,重建索引时索引信息切换到index1,此时index1对应Block,这样做的好处是Block只需要存一份数据,而且index扩容和Block的扩容可以分离。
之后的文章会对每一个模块的实现进行详细分析。

设计要点

底层读写文件

如何将数据落盘有很多种做法,简单的介绍两种

  • 数据在内存积累到一定值之后后台另启一个线程存到文件,可以理解为定时将数据刷入磁盘。
  • 写日志文件,这种方式就是当系统挂了或者需要重启时通过遍历操作日志重新将操作进行模拟写入,恢复原先数据。

本存储系统采用的是借助操作系统的mmap功能,将刷磁盘的工作交给操作系统管理。mmap可以将一个文件映射到内存,进程可以直接读写内存,比普通写文件性能快很多(关于mmap建议读者自行去了解)。有了这个工具,这个刷磁盘的操作变得十分容易,性能也有保障,唯一的缺点就是因为mmap是操作系统控制刷到磁盘的,并不是实时的,在系统遇到问题时挂了会造成数据丢失,索性这个系统还有日志信息,从而也能实现恢复数据功能。
补充一个常识,内存读写最快,磁盘中顺序读写比随机读写快,所以index 和 block部分都采用mmap的方式,并且是随机读写的,log日志不需要随机读写,可以采用直接追加文件的顺序读写方式。
采用mmap管理之后,仍旧会存在一些磁盘整理的问题,比如index扩容,block扩容,或者数据被删除之后有很多碎片等等问题,也将在之后的文章中细讲。

并发问题

存储系统性能要好必然面临着一些多线程的问题。很多系统会采用读写都加锁,这样会导致性能有很大的瓶颈。由于需求是读的频率远高于写的频率,所以系统最初的时候采用了读写锁的方式,即在写的时候只能是一个线程在写,读的时候可以多线程读。后来发现其实性能也不好,因为写的频率并不是非常低,最后采用了写的时候也可以读,但是写的时候不能有另一个线程再写。这个就会牵涉到一个并发问题,正在写一个key的时候,有线程正好读这个key的值,这样会导致数据错误,解决办法简单来讲是版本号check或者是双buffer切换,之后的文章会详细分析。

数据结构与算法

本存储用到许多数据结构和算法,在index部分会用到hash算法,包括一些数论分析,string的hash。在block中会用到类似内存分配的算法,treap的应用等,也会在之后做详细分析。

1 0
原创粉丝点击