ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

FastKV:一个真的很快的KV存储组件,kotlin协程池

2022-01-17 15:30:56  阅读:195  来源: 互联网

标签:kotlin 写入 value FastKV 协程池 flag key 数据


  • 读取相对较慢

SP在加载的时候已经将value反序列化存在HashMap中了,读取的时候索引到之后就能直接引用了。

而MMKV每次读取时都需要重新解码,除了时间上的消耗之外,还需要每次都创建新的对象。

不过这不是大问题,相对SP没有差很多。

  • 需要引入so, 增加包体积

引入MMKV需要增加的体积还是不少的,且不说jar包和aidl文件,光是一个arm64-v8a的so就有四百多K。

虽然说现在APP体积都不小,但毕竟增加体积对打包、分发和安装时间都多少有些影响。

  • 文件只增不减

MMKV的扩容策略还是比较激进的,而且扩容之后不会主动trim size。

比方说,假如有一个大value,让其扩容至1M,后面删除该value,哪怕有效内容只剩几K,文件大小还是保持在1M。

  • 可能会丢失数据

前面的问题总的来说都不是什么“要紧”的问题,但是这个丢失数据确实是硬伤。

MMKV官方有这么一段表述:

通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。

这个表述对一半不对一半。

如果数据完成写入到内存块,如果系统不崩溃,即使进程崩溃,系统也会将buffer刷入磁盘;

但是如果在刷入磁盘之前发生系统崩溃或者断电等,数据就丢失了,不过这种情况发生的概率不大;

另一种情况是数据写一半的时候进程崩溃或者被杀死,然后系统会将已写入的部分刷入磁盘,再次打开时文件可能就不完整了。

例如,MMKV在剩余空间不足时会回收无效的空间,如果这期间进程中断,数据可能会不完整。 MMKV官方的说明可以佐证:

CRC校验失败之后,MMKV有两种应对策略:直接丢弃所有数据,或者尝试读取数据(用户可以在初始化时设定)。

尝试读取数据不一定能恢复数据,甚至可能会读到一些错误的数据,得看运气。

这个过程是比较容易复现的,下面是其中一种复现路径:

  1. 新增和删除若干key-value 得到数据如下:

  1. 插入一个大字符串,触发扩容,扩容前会触发垃圾回收

  2. 断点打在执行memmove的循环中,执行一部分memmove, 然后在手机上杀死进程

  1. 再次打开APP,数据丢失

相比之下,SP虽然低效,但至少不会丢失数据。

二、FastKV


在总结了之前的经验和感悟之后,笔者实现了一个高效且可靠的版本,且将其命名为: FastKV

2.1 特性

FastKV有以下特性:

  1. 读写速度快
  • FastKV采用二进制编码,编码后的体积相对XML等文本编码要小很多。

  • 增量编码:FastKV记录了各个key-value相对文件的偏移量,更新数据时,可以直接在对应的位置写入数据。

  • 默认用mmap的方式记录数据,更新数据时直接写入到内存即可,没有IO阻塞

  1. 支持多种写入模式
  • 除了mmap这种非阻塞的写入方式,FastKV也支持常规的阻塞式写入方式, 并且支持同步阻塞和异步阻塞(分别类似于SharePreferences的commit和apply)。
  1. 支持多种类型
  • 支持常用的boolean/int/float/long/double/String等基础类型。

  • 支持ByteArray (byte[])。

  • 支持存储自定义对象。

  • 内置StringSet编码器 (为了兼容SharePreferences)。

  1. 方便易用
  • FastKV提供了了丰富的API接口,开箱即用。

  • 提供的接口其中包括getAll()和putAll()方法, 所以迁移SharePreferences等框架的数据到FastKV很方便,当然,迁移FastKV的数据到其他框架也很方便。

  1. 稳定可靠
  • 通过double-write等方法确保数据的完整性。

  • 在API抛IO异常时提供降级处理。

  1. 代码精简
  • FastKV由纯Java实现,编译成jar包后体积仅30多K。

2.2 实现原理

2.2.1 编码

文件的布局:

[data_len | checksum | key-value | key-value|…]

  • data_len: 占4字节, 记录所有key-value所占字节数。

  • checksum: 占8字节,记录key-value部分的checksum。

key-value的数据布局:

±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±+

| delete_flag | external_flag | type | key_len | key_content | value |

±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±+

| 1bit | 1bit | 6bits | 1 byte | | |

  • delete_flag :标记当前key-value是否删除。

  • external_flag: 标记value部分是否写到额外的文件。

注:对于数据量比较大的value,放在主文件会影响其他key-value的访问性能,因此,单独用一个文件来保存该value, 并在主文件中记录其文件名。

  • type: value类型,目前支持boolean/int/float/long/double/String/ByteArray以及自定义对象。

  • key_len: 记录key的长度,key_len本身占1字节,所以支持key的最大长度为255。

  • key_content: key的内容本身,utf8编码。

  • value: 基础类型的value, 直接编码(little-end);

其他类型,先记录长度(用varint编码),再记录内容。

String采用UTF-8编码,ByteArray无需编码,自定义对象实现Encoder接口,分别在Encoder的encode/decode方法中序列化和反序列化。

2.2.2 存储

  • mmap

为了提高写入性能,FastKV默认采用mmap的方式写入。

  • 降级

当mmap API发生IO异常时,降级到常规的blocking I/O,同时为了不影响当前线程,会将写入放到异步线程中执行。

  • 数据完整性

如果在写入一部分的过程中发生中断(进程或系统),则文件可能会不完整。

故此,需要用一些方法确保数据的完整性。

当用mmap的方式打开时,FastKV采用double-write的方式:数据依次写入A/B两个文件,确保任何时刻总有一个文件完整的;

加载数据时,通过checksum、标记、数据合法性检验等方法验证数据的正确性。

double-write可以防止进程崩溃后数据不完整,但由于mmap是系统定时刷盘,若在刷盘前系统崩溃或者断电,仍会丢失未落盘的更新(之前的数据还在);对于非常重要的key-value,在写入后,可接着调用force()强制将脏页刷盘。

  • 更新策略(增/删/改)

新增:写入到数据的尾部。

删除:delete_flag设置为1。

修改:如果value部分的长度和原来一样,则直接写入原来的位置; 否则,先写入key-value到数据尾部,再标记原来位置的delete_flag为1(删除),最后再更新文件的data_len和checksum。

  • gc/truncate

删除key-value时会收集信息(统计删除的个数,以及所在位置,占用空间等)。

GC的触发时机:

1、新增key-value时剩余空间不足,且已删除的空间达到阈值,且腾出删除空间后足够写入当前key-value, 则触发GC;

2、删除key-value时,如果删除空间达到阈值,或者删除的key-value个数达到阈值,则触发GC。

GC后如果空闲的空间达到设定阈值,则触发truncate(缩小文件大小)。

2.3 使用方法

2.3.1 导入

dependencies {

implementation ‘io.github.billywei01:fastkv:1.0.4’

}

2.3.2 初始化

FastKVConfig.setLogger(FastKVLogger)

FastKVConfig.setExecutor(ChannelExecutorService(4))

初始化可以按需设置日志回调和Executor。

建议传入自己的线程池,以复用线程。

日志接口提供三个级别的回调,按需实现即可。

public interface Logger {

void i(String name, String message);

void w(String name, Exception e);

void e(String name, Exception e);

}

2.3.3 数据读写

  • 基本用法

FastKV kv = new FastKV.Builder(path, name).build();

if(!kv.getBoolean(“flag”)){

kv.putBoolean(“flag” , true);

}

  • 保存自定义对象

FastKV.Encoder<?>[] encoders = new FastKV.Encoder[]{LongListEncoder.INSTANCE};

id w(String name, Exception e);

void e(String name, Exception e);

}

2.3.3 数据读写

  • 基本用法

FastKV kv = new FastKV.Builder(path, name).build();

if(!kv.getBoolean(“flag”)){

kv.putBoolean(“flag” , true);

}

  • 保存自定义对象

FastKV.Encoder<?>[] encoders = new FastKV.Encoder[]{LongListEncoder.INSTANCE};

标签:kotlin,写入,value,FastKV,协程池,flag,key,数据
来源: https://blog.csdn.net/m0_66264655/article/details/122540866

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有