200行代码实现简易KV数据库

120 阅读4分钟

最近一直有粉丝来信咨询,面试中项目经历怎么写?感觉自己平时参与的项目比较少,没有拿得出手的,怎么丰富自己的项目经验?有没有什么办法?

当然有!有一台能上网的电脑就可以动手肝--动手自己从0实现一些开源的框架&轮子!可能有人就要问了,都有那么成熟的框架了,为啥自己还要去实现?自己实现的又不能在生产环境去使用。

个人从事软件开发这么多年,总结的一条非常重要的经验就是:学习一个框架最好的方法就是去动手实现它!实现一个麻雀虽小五脏俱全的版本,自己写出其核心逻辑,能够将自己学到的知识串起来。在实现的过程中会遇到很多问题,以及技术方案的选型,各种优化操作,debug,能够极大的加深自己对知识的理解。

我把自己最近一段时间动手实现的轮子&框架的文档解析、全部源码全部放在了github,方便大家参考,也欢迎大家帮我点个小星星STAR: github.com/xiajunhust/…

图片

本文从0实现一个简易的KV数据库,代码行数不多,核心代码不超过200行。

设计思路

  • 查询语法:支持String类型的Key和Value,暂不支持其他复杂类型,不支持SQL语法解析和执行计划优化。
  • 存储引擎:基于顺序日志进行写入,每条执行命令的数据信息按行存在文本日志文件里。暂不支持类似hbase中的合并等复杂的逻辑。

图片

代码模块

代码模块比较简单,主要是:

(1)API定义,定义核心的接口。

(2)核心实现,本文实现的是基于顺序日志的KV数据库。

(3)枚举类和领域模型。

图片

下面给大家介绍核心代码解析。

核心API定义:

image.png

基于顺序日志的实现:


/**
 * 基于日志顺序写入的KV数据库实现
 *
 * @author summer
 * @version : LogBasedKV.java, v 0.1 2022年05月05日 4:06 PM summer Exp $
 */
@Log
public class LogBasedKV implements SimpleKvClient {

    /**
     * 日志文件
     */
    private File logFile;

    /**
     * 构造函数
     *
     * @param fileName 文件名
     */
    public LogBasedKV(String fileName) {
        logFile = new File(fileName);
    }

    @Override
    public void put(String key, String value) {
        BufferedWriter bufferedWriter = null;
        try {
            FileWriter fileWriter = new FileWriter(logFile, true);
            bufferedWriter = new BufferedWriter(fileWriter);

            //日志写入内容构造
            CommandRequestModel commandRequestModel = new CommandRequestModel();
            commandRequestModel.setCommandTypeEnum(CommandTypeEnum.SET);
            commandRequestModel.setKey(key);
            commandRequestModel.setValue(value);

            //往日志文件中写入内容
            bufferedWriter.write(JSON.toJSONString(commandRequestModel));
            bufferedWriter.newLine();
        } catch (Exception e) {
            log.warning("put exception,[" + key + "," + value + "]");
        } finally {
            try {
                bufferedWriter.close();
            } catch (Exception e) {
                log.warning("bufferedWriter close exception");
            }
        }
    }

    @Override
    public String get(String key) {
        try {
            FileReader fileReader = new FileReader(logFile);
            BufferedReader bufferedReader = new BufferedReader(fileReader);

            //按行读取日志文件的内容,查找到最后一条修改类操作命令
            CommandRequestModel lastUpdateCommand = null;
            String line = bufferedReader.readLine();
            while (line != null) {
                CommandRequestModel commandRequestModel = JSON.parseObject(line, CommandRequestModel.class);
                if ((CommandTypeEnum.SET == commandRequestModel.getCommandTypeEnum()
                        || CommandTypeEnum.DEL == commandRequestModel.getCommandTypeEnum())
                    && StringUtils.equals(key, commandRequestModel.getKey())) {
                    lastUpdateCommand = commandRequestModel;
                }
                line = bufferedReader.readLine();
            }

            if (lastUpdateCommand == null || CommandTypeEnum.DEL == lastUpdateCommand.getCommandTypeEnum()) {
                return null;
            }

            return lastUpdateCommand.getValue();
        } catch (Exception e) {
            log.warning("get exception,[" + key + "]");
        }
        return null;
    }

    @Override
    public void del(String key) {
        BufferedWriter bufferedWriter = null;
        try {
            FileWriter fileWriter = new FileWriter(logFile, true);
            bufferedWriter = new BufferedWriter(fileWriter);

            //日志写入内容构造
            CommandRequestModel commandRequestModel = new CommandRequestModel();
            commandRequestModel.setCommandTypeEnum(CommandTypeEnum.DEL);
            commandRequestModel.setKey(key);

            //往日志文件中写入内容
            bufferedWriter.write(JSON.toJSONString(commandRequestModel));
            bufferedWriter.newLine();
        } catch (Exception e) {
            log.warning("del exception,[" + key + "]");
        } finally {
            try {
                bufferedWriter.close();
            } catch (Exception e) {
                log.warning("bufferedWriter close exception");
            }
        }
    }
}

性能分析

因为是追加写入,所以写入的性能非常快,比如Hbase就是采用的顺序写入的方式。关于读取,因为需要读取扫描整个文件来得到key对应的记录,因此性能较差,是O(N)。像实际生产环境使用,是会做一些优化工作的,比如把日志内容进行刷盘处理,同样的key的多条记录会进行合并处理,然后建立索引,查询性能会快很多。此处只是给大家做个demo展示,还有相当多的优化工作需要做。

工程测试


import com.summer.simplekv.api.SimpleKvClient;
import com.summer.simplekv.core.LogBasedKV;
import lombok.extern.java.Log;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Log
@SpringBootApplication
public class SimplekvApplication {

    public static void main(String[] args) {
        String logFileName = "simplekv.log";
        SimpleKvClient kvClient = new LogBasedKV(logFileName);

        //写入数据
        for (int index = 0; index < 5; ++index) {
            kvClient.put("k-" + index, "v-" + index);
        }

        //查询数据
        for (int index = 0; index < 5; ++index) {
            String value = kvClient.get("k-" + index);
            log.info("get [" + "k-" + index + " value is [" + value + "]");
        }

        //删除一行数据
        kvClient.del("k-" + 3);

        log.info("after del 3");

        //再次查询已被删除的数据
        String value = kvClient.get("k-" + 3);
        log.info("get [" + "k-" + 3 + " value is [" + value + "]");

        SpringApplication.run(SimplekvApplication.class, args);
    }
}

测试比较简单,直接构建的实例,调用其查询、写入、删除方法即可。

运行日志:

五月 05, 2022 4:59:56 下午 com.summer.simplekv.SimplekvApplication main
信息: get [k-0 value is [v-0]
五月 05, 2022 4:59:56 下午 com.summer.simplekv.SimplekvApplication main
信息: get [k-1 value is [v-1]
五月 05, 2022 4:59:56 下午 com.summer.simplekv.SimplekvApplication main
信息: get [k-2 value is [v-2]
五月 05, 2022 4:59:56 下午 com.summer.simplekv.SimplekvApplication main
信息: get [k-3 value is [v-3]
五月 05, 2022 4:59:56 下午 com.summer.simplekv.SimplekvApplication main
信息: get [k-4 value is [v-4]
五月 05, 2022 4:59:56 下午 com.summer.simplekv.SimplekvApplication main
信息: after del 3
五月 05, 2022 4:59:56 下午 com.summer.simplekv.SimplekvApplication main
信息: get [k-3 value is [null]

还在等什么,赶紧把代码从github clone下来跑起来吧!传送门「觉得有用的话帮忙点个STAR,你们的点赞是我创作的动力!」.