之前六月份的时候有参加过阿里举办的第四届中间件性能大赛,学到了不少东西,所以之后经常会关注一下天池那边阿里举办的程序设计大赛,九月底的时候注意到了这一届的POLARDB数据库性能大赛,很早就报了名。预热赛10月25日开始了~,初赛11月5日正式开始,11月19日结束,我这篇文章发布的时候初赛就已经结束了。因为11月之前一直在找实习,所以一直没有做什么准备,11月10号左右开始编写第一版的代码,到11月18号晚上放弃继续尝试。最后初赛成绩是42名,时间是240.69秒,和大佬们比起来还是有很大差距的。写这篇博客,主要还是想分享一下这段时间参赛的思路,和一点一点慢慢提升的经历。
GitHub: https://github.com/AlexZFX/engine 当前只️更新了初赛代码,复赛结束后会继续更新。
题目介绍
PolarDB作为软硬件结合的代表, 充分使用新硬件, 榨干硬件的红利来为用户获取极致的数据性能, 其中在PolarDB 的设计中, 我们使用 Optane SSD作为所有热数据的写入缓冲区, 通过kernel bypass的方式, 实现了极致的性能。所以本次比赛就以Optane SSD盘为背景,参赛者在其基础之上探索实现一种高效的kv存储引擎
以上是阿里云官方给的比赛背景,具体的题目内容如下
初赛赛题(完整请点击查看): 实现一个简化、高效的kv存储引擎,支持Write、Read接口。
程序评测逻辑 评测程序分为2个阶段:
1)Recover正确性评测 此阶段评测程序会并发写入特定数据(key 8B、value
4KB)同时进行任意次kill
-9来模拟进程意外退出(参赛引擎需要保证进程意外退出时数据持久化不丢失),接着重新打开DB,调用Read接口来进行正确性校验2)性能评测
- 随机写入:64个线程并发随机写入,每个线程使用Write各写100万次随机数据(key 8B、value 4KB)
- 随机读取:64个线程并发随机读取,每个线程各使用Read读取100万次随机数据 注:2.2阶段会对所有读取的kv校验是否匹配,如不通过则终止,评测失败
总体说来我们能得到的要求和信息为以下几点:
- 实现一个KV型数据库的核心逻辑,主要为open、read、write三个接口。
- 支持多线程并发读写。
- 保证在方法成功返回的情况下数据不丢失(kill保证不丢失的前提是已经正确返回了,如果没有的话是不算做丢失的)。
- key和value的长度是确定的 key为8B,value为4KB。
- 可以使用Java语言或者C++,Java内存限制3G,C++限制2G。
- 磁盘占用不超过 320G
编写过程
完整的参赛过程大概是一周时间,这一周进行了非常多的尝试,成绩也从第一次跑通时的900多s到最后稳定在240s,接下来会细细的说一说每一版的思路和进阶过程。(下面的标题写的key value分别表示采用的文件数)
大体思路
先做一些简单的计算,
key + offset = ( 8 + 8 ) * 64000000 / 1024 / 1024 = 977M
value = 4096 * 64000000 / 1024 / 1024 / 1024 = 245G
可见磁盘和内存的限制相对来说不会造成很大的影响,对合理的设计来说还是充足的。
因为key是一个8B的byte数组,故转化成一个long型的数字很简单并且非常有利于接下来计算的事情。所以下文讨论的key都是建立在long型的基础上的。
初始主体的思路是这样的
- 用HashMap在内存中维护所有的key-offset对,数据库open时完成文件中的key和offset的加载工作,read时只需要找到对应key的offset,然后在相应value文件中进行读取即可。这里的hashmap选取是一个很重要的事情,因为Java自带的hashmap是对对象的存储,故一个Long型的KV对要占用约40B的内存,这样的话3G的内存会爆掉,最后选择的是HPPC开源的基础类型的HashMap,选择的原因主要参考了群里大佬的文章《应用JMH测试大型HashMap的性能》,他也写了一些关于本次比赛的总结与分析,推荐大家关注并学习,我和大佬的差距还是很大的。
- 因为value共有250G左右的内容,必然要进行分片,初始打算是对key进行hash,然后将key的hash结果相同的value存储在同一个文件中,key和该value对应的offset存在同一个文件中。
- 所有的key和value不论是否重复,都直接在文件尾利用追加写,这样在加载的时候,后面出现的key必然会覆盖掉出现过的key,可以不用考虑key的重复问题。
- 因为要考虑到在进程被kill的时候能保证数据不丢失,故不能对key或者value进行缓存或者异步写入,否则可能会导致校验阶段的失败,则write接口被调用的时候都会直接对数据进行落盘操作。
第一版 FileChannel读写 1 key + 128 value 381.79s
首先想的是要跑出成绩,把所有的key都写在了一个文件里,一开始忽略了一个小点,把key和offset分开写入了文件,导致出现了一些key和value不匹配的问题。很显然的问题是写key和写offset会出现线程问题,可能导致本来应该是KeyValueKeyValue形式的数据,被写成KeyKeyValueValue的形式,所以出错之后直接加了个synchronized关键字,得出第一次的成绩968s,很快修改了这个简单的小问题,得到一个明显有大幅提升的成绩381.79s,这时的代码主要是这样的。
@Override
public void open(String path) throws EngineException {
File file = new File(path);
// 创建目录
if (!file.exists()) {
if (!file.mkdir()) {
throw new EngineException(RetCodeEnum.IO_ERROR, "创建文件目录失败:" + path);
} else {
logger.info("创建文件目录成功:" + path);
}
}
//创建 FILE_COUNT个FileChannel 顺序写入
RandomAccessFile randomAccessFile;
if (file.isDirectory()) {
for (int i = 0; i < FILE_COUNT; i++) {
try {
randomAccessFile = new RandomAccessFile(path + File.separator + i + ".data", "rw");
FileChannel channel = randomAccessFile.getChannel();
fileChannels[i] = channel;
// 从 length处直接写入
offsets[i] = new AtomicLong(randomAccessFile.length());
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
throw new EngineException(RetCodeEnum.IO_ERROR, "path不是一个目录");
}
File keyFile = new File(path + File.separator + "key");
if (!keyFile.exists()) {
try {
keyFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
// 从 index 文件建立 hashmap
try {
randomAccessFile = new RandomAccessFile(keyFile, "rw");
keyFileChannel = randomAccessFile.getChannel();
ByteBuffer keyBuffer = ByteBuffer.allocate(KEY_LEN);
ByteBuffer offBuffer = ByteBuffer.allocate(KEY_LEN);
keyFileOffset = new AtomicLong(randomAccessFile.length());
long temp = 0, maxOff = keyFileOffset.get();
while (temp < maxOff) {
keyBuffer.position(0);
keyFileChannel.read(keyBuffer, temp);
temp += KEY_LEN;
offBuffer.position(0);
keyFileChannel.read(offBuffer, temp);
temp += KEY_LEN;
keyBuffer.position(0);
offBuffer.position(0);
keyMap.put(keyBuffer.getLong(), offBuffer.getLong());
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(byte[] key, byte[] value) throws EngineException {
long numkey = Util.bytes2long(key);
int hash = hash(numkey);
long off = offsets[hash].getAndAdd(VALUE_LEN);
keyMap.put(numkey, off + 1);
try {
//key和offset写入文件
localKey.get().putLong(0, numkey).putLong(8, off + 1);
localKey.get().position(0);
keyFileChannel.write(localKey.get(), keyFileOffset.getAndAdd(KEY_AND_OFF_LEN));
//将value写入buffer
localBufferValue.get().position(0);
localBufferValue.get().put(value, 0, VALUE_LEN);
//buffer写入文件
localBufferValue.get().position(0);
fileChannels[hash].write(localBufferValue.get(), off);
} catch (IOException e) {
throw new EngineException(RetCodeEnum.IO_ERROR, "写入数据出错");
}
}
@Override
public byte[] read(byte[] key) throws EngineException {
long numkey = Util.bytes2long(key);
int hash = hash(numkey);
// key 不存在会返回0,避免跟位置0混淆,off写加一,读减一
long off = keyMap.get(numkey);
if (off == 0) {
throw new EngineException(RetCodeEnum.NOT_FOUND, numkey + "不存在");
}
try {
localBufferValue.get().position(0);
fileChannels[hash].read(localBufferValue.get(), off - 1);
} catch (IOException e) {
throw new EngineException(RetCodeEnum.IO_ERROR, "读取数据出错");
}
localBufferValue.get().position(0);
localBufferValue.get().get(localByteValue.get(), 0, VALUE_LEN);
return localByteValue.get();
}
- 这里的localKey 和 localBufferValue 都是 ThreadLocal 的 DirectByteBuffer(关于HeapByteBuffer和Direct的的一些差别后面会提到一些),用于作为 FileChannel(FileChannel参数都是在open的时候进行的初始化) 写入的参数,避免了每次写入都要allocate一块新内存的消耗。
- 这里的Hash采用的方法是与 0x7F 进行求 & ,得到的结果划分为128个value文件(这种hash的很简单且高效,带来的隐患是在key的分布不均匀情况下可能导致某个文件非常大之类的现象,在后面也会提及)。
这个时候open的时间将近90s,很显然是一个超出可承受范围的结果。所以接下来很快对这一部分进行了优化。
第二版 FileChannel 64线程open 64 key 128 value 260.96s
open时间过长,所以这成了我们关注的一个重点,这段时间我们做了很多改动,改动的过程主要是这样的。
- 单key文件,单个map,将完整的offset分为64份读取 无成绩
这一做法其实还没跑出成绩就被我们过渡掉了,因为本地进行的测试一直过不去,我们第一时间想到的原因是因为,单个key文件并发初始化的时候,后面出现的相等的key不一定会把前面的key覆盖掉,所以会出现值不对的状况。所以解决方案只能是所有相同的key必须要严格有序的读取。 - 64个key文件,单个map 301.49s
因为上面所述的原因,所以选择对key也进行一次hash,按照hash的结果将key划分在了64个不同的key文件中,这样的结果是相同的key一定会在相同文件中按照先后顺序被写入,故读取的时候一定是严格有序的。
这个版本本地的小量测试也通过了,以为没有问题,但线上失败,这时我们才开始关注hppc的map本身的线程安全性,给map的put加锁后提交,果然通过了,得分301.49s。
简单看了一下源码,显然是线程不安全的,所以促使我们接下来的一次分map改动。 - 64个key文件,64个map 260.96s
于是我们进行了一次map的拆分,根据key的文件个数直接拆为了64个hashmap,差别是这样拆分让我们的map容量无法确定,简单线上用log测试了一下之后完成了64map的版本。并发问题解决好之后,这一版本的分数又有了不少提升,260.96s。这时候的open已经被压到了10s以内,但其实还是有提升的空间。
这版的主要改动在open地方,下面贴出了这版的open方法。
@Override
public void open(String path) throws EngineException {
File file = new File(path);
// 创建目录
if (!file.exists()) {
if (!file.mkdir()) {
throw new EngineException(RetCodeEnum.IO_ERROR, "创建文件目录失败:" + path);
} else {
logger.info("创建文件目录成功:" + path);
}
}
RandomAccessFile randomAccessFile;
// file是一个目录时进行接下来的操作
if (file.isDirectory()) {
try {
//先构建keyFileChannel 和 初始化 map
for (int i = 0; i < THREAD_NUM; i++) {
randomAccessFile = new RandomAccessFile(path + File.separator + i + ".key", "rw");
FileChannel channel = randomAccessFile.getChannel();
keyFileChannels[i] = channel;
keyOffsets[i] = new AtomicInteger((int) randomAccessFile.length());
}
ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);
for (int i = 0; i < THREAD_NUM; i++) {
if (!(keyOffsets[i].get() == 0)) {
final long off = keyOffsets[i].get();
final int finalI = i;
executor.execute(() -> {
int start = 0;
long key;
int keyHash;
while (start < off) {
try {
localKey.get().position(0);
keyFileChannels[finalI].read(localKey.get(), start);
start += KEY_AND_OFF_LEN;
localKey.get().position(0);
key = localKey.get().getLong();
keyHash = keyFileHash(key);
keyMap[keyHash].put(key, localKey.get().getInt());
} catch (IOException e) {
e.printStackTrace();
}
}
countDownLatch.countDown();
});
} else {
countDownLatch.countDown();
}
}
countDownLatch.await();
executor.shutdownNow();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
//创建 FILE_COUNT个FileChannel 供write顺序写入
for (int i = 0; i < FILE_COUNT; i++) {
try {
randomAccessFile = new RandomAccessFile(path + File.separator + i + ".data", "rw");
FileChannel channel = randomAccessFile.getChannel();
fileChannels[i] = channel;
// 从 length处直接写入
valueOffsets[i] = new AtomicInteger((int) (randomAccessFile.length() >>> SHIFT_NUM));
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
throw new EngineException(RetCodeEnum.IO_ERROR, "path不是一个目录");
}
}
这一部分里我们还做了一些小事
- 通过在close方法中添加log,统计了一些线上key和value的数量,发现key和value都非常均匀,每个key和value文件的大小都很相近(其实这里有个让我们后面踩到的小坑,偷偷的感受到了测评程序的一些阴险)。
- 将offset替换为了int型的数值,每次读取的时候进行一次12位的移位操作之后再从value文件中读取,这样的好处是节约了一些内存,可以用来做一些其他的事情。
第三版 用mmap读open,64key 64value 245.18s
之前一直考虑着用mmap,在java里面对应的就是MappedByteBuffer,因为我不确定mmap能不能在kill -9 被杀进程的情况保证数据的完整性,同时,如果都用mmap写入的话,会让我无法确定文件的大小(mmap映射时要预先指定文件大小),无法在kill之后能从指定的位置追加写入。所以打算一步一步,最后再考虑使用这个。
但是open的时候使用mmap读一定是没有风险的,所以又进行了一次对open的改动,这时还是64个key文件和128个value文件,得到的跑分是248.58,open过程被压缩在了1s以内,大约600ms左右,这个open速度我们就基本已经满足了。
后来改成了64个value文件,每次只进行一次hash就可以确定key和value文件的位置,并且读写速度似乎都有略微进步,达到了245.18s。
这时的open代码如下
@Override
public void open(String path) throws EngineException {
File file = new File(path);
// 创建目录
if (!file.exists()) {
if (!file.mkdir()) {
throw new EngineException(RetCodeEnum.IO_ERROR, "创建文件目录失败:" + path);
} else {
logger.info("创建文件目录成功:" + path);
}
}
RandomAccessFile randomAccessFile;
// file是一个目录时进行接下来的操作
if (file.isDirectory()) {
try {
//先构建keyFileChannel 和 初始化 map
for (int i = 0; i < THREAD_NUM; i++) {
randomAccessFile = new RandomAccessFile(path + File.separator + i + ".key", "rw");
FileChannel channel = randomAccessFile.getChannel();
keyFileChannels[i] = channel;
keyOffsets[i] = new AtomicInteger((int) randomAccessFile.length());
}
ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);
for (int i = 0; i < THREAD_NUM; i++) {
if (!(keyOffsets[i].get() == 0)) {
final long off = keyOffsets[i].get();
final int finalI = i;
executor.execute(() -> {
int start = 0;
try {
MappedByteBuffer mappedByteBuffer = keyFileChannels[finalI].map(FileChannel.MapMode.READ_ONLY, 0, off);
while (start < off) {
start += KEY_AND_OFF_LEN;
keyMap[finalI].put(mappedByteBuffer.getLong(), mappedByteBuffer.getInt());
}
unmap(mappedByteBuffer);
countDownLatch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
});
} else {
countDownLatch.countDown();
}
}
countDownLatch.await();
executor.shutdownNow();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
//创建 FILE_COUNT个FileChannel 供write顺序写入
for (int i = 0; i < FILE_COUNT; i++) {
try {
randomAccessFile = new RandomAccessFile(path + File.separator + i + ".data", "rw");
FileChannel channel = randomAccessFile.getChannel();
fileChannels[i] = channel;
// 从 length处直接写入
valueOffsets[i] = new AtomicInteger((int) (randomAccessFile.length() >>> SHIFT_NUM));
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
throw new EngineException(RetCodeEnum.IO_ERROR, "path不是一个目录");
}
}
这一版当中我们也发现了一些问题,阅读了许多文章,总结主要如下:
- 在open过程中遇到过一次OOM,但是按照我的JVM参数和预计是不会出现这一现象,同时在这期间我有测试过使用mmap读256个文件的value,但是却出现了MappedByteBuffer爆掉的情况(Java的MappedByteBuffer有限制一次映射不能超过2G内存)。通过线上的log测试,发现测评时最开始的一部分是写入了大量的hash值相同的key,value也超过了2G的大小。但这一部分的测评其实没有体现在官方的log中,这个发现也为最后的改动提供了一点帮助。
- 阅读了千里码赛码会的总结分享,学到了一些mmap的内容,知道了在程序异常退出的时候,哪怕mmap的内存数据并没有落盘,kernel也会在你的进程被kill之后,回写到磁盘。这里就已经是内核态的操作了,只要服务器不真正的断电,数据的安全性是有保证的,这还是非常有帮助的。
第四版 mmap读写key,FileChannel读写value,64 + 64 240.69s
有第三版最后发现的内容,我们打算再对key的写入做一些改动,也就是将fileChannel写入key的方式改动为mmap写入。而mmap映射的文件大小选择一个稍大的值,open之后的写入offset通过value文件的大小来确定(valuelen / 4096 * 12),这一优化带来的大约2~3s的提升。
除此之外,还进行了简单的jvm调优工作,将新生代和老年代的比例进行了调整,将原来1:1的比例调整为了6:1,这部分优化带来了大约2s的性能提升。
最后完整的代码这一块我就直接贴在下面,对整个过程有兴趣的也可以去我的github上clone下来查看。
package com.alibabacloud.polar_race.engine.common;
import com.alibabacloud.polar_race.engine.common.exceptions.EngineException;
import com.alibabacloud.polar_race.engine.common.exceptions.RetCodeEnum;
import com.carrotsearch.hppc.LongIntHashMap;
import io.netty.util.concurrent.FastThreadLocal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class EngineRace extends AbstractEngine {
private static Logger logger = LoggerFactory.getLogger(EngineRace.class);
// key+offset 长度 16B
private static final int KEY_AND_OFF_LEN = 12;
// 线程数量
private static final int THREAD_NUM = 64;
// value 长度 4K
private static final int VALUE_LEN = 4096;
//每个map存储的key数量
private static final int PER_MAP_COUNT = 1024000;
private static final int SHIFT_NUM = 12;
// 存放 value 的文件数量 128
private static final int FILE_COUNT = 64;
private static final int HASH_VALUE = 0x3F;
private static final LongIntHashMap[] keyMap = new LongIntHashMap[THREAD_NUM];
static {
for (int i = 0; i < THREAD_NUM; i++) {
keyMap[i] = new LongIntHashMap(PER_MAP_COUNT, 0.98);
}
}
//key 文件的fileChannel
private static FileChannel[] keyFileChannels = new FileChannel[THREAD_NUM];
private static AtomicInteger[] keyOffsets = new AtomicInteger[THREAD_NUM];
private static MappedByteBuffer[] keyMappedByteBuffers = new MappedByteBuffer[THREAD_NUM];
//value 文件的fileChannel
private static FileChannel[] fileChannels = new FileChannel[FILE_COUNT];
private static AtomicInteger[] valueOffsets = new AtomicInteger[FILE_COUNT];
private static FastThreadLocal localBufferValue = new FastThreadLocal() {
@Override
protected ByteBuffer initialValue() throws Exception {
return ByteBuffer.allocate(VALUE_LEN);
}
};
@Override
public void open(String path) throws EngineException {
File file = new File(path);
// 创建目录
if (!file.exists()) {
if (!file.mkdir()) {
throw new EngineException(RetCodeEnum.IO_ERROR, "创建文件目录失败:" + path);
} else {
logger.info("创建文件目录成功:" + path);
}
}
RandomAccessFile randomAccessFile;
// file是一个目录时进行接下来的操作
if (file.isDirectory()) {
try {
//先 创建 FILE_COUNT个FileChannel 供write顺序写入,并由此文件获取value文件的大小
for (int i = 0; i < FILE_COUNT; i++) {
try {
randomAccessFile = new RandomAccessFile(path + File.separator + i + ".data", "rw");
FileChannel channel = randomAccessFile.getChannel();
fileChannels[i] = channel;
// 从 length处直接写入
valueOffsets[i] = new AtomicInteger((int) (randomAccessFile.length() >>> SHIFT_NUM));
keyOffsets[i] = new AtomicInteger(valueOffsets[i].get() * KEY_AND_OFF_LEN);
} catch (IOException e) {
e.printStackTrace();
}
}
//先构建keyFileChannel 和 初始化 map
for (int i = 0; i < THREAD_NUM; i++) {
randomAccessFile = new RandomAccessFile(path + File.separator + i + ".key", "rw");
FileChannel channel = randomAccessFile.getChannel();
keyFileChannels[i] = channel;
keyMappedByteBuffers[i] = channel.map(FileChannel.MapMode.READ_WRITE, 0, PER_MAP_COUNT * 20);
}
CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);
for (int i = 0; i < THREAD_NUM; i++) {
if (!(keyOffsets[i].get() == 0)) {
final long off = keyOffsets[i].get();
final int finalI = i;
final MappedByteBuffer buffer = keyMappedByteBuffers[i];
new Thread(() -> {
int start = 0;
while (start < off) {
start += KEY_AND_OFF_LEN;
keyMap[finalI].put(buffer.getLong(), buffer.getInt());
}
countDownLatch.countDown();
}).start();
} else {
countDownLatch.countDown();
}
}
countDownLatch.await();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
} else {
throw new EngineException(RetCodeEnum.IO_ERROR, "path不是一个目录");
}
}
@Override
public void write(byte[] key, byte[] value) throws EngineException {
long numkey = Util.bytes2long(key);
int hash = valueFileHash(numkey);
int off = valueOffsets[hash].getAndIncrement();
try {
ByteBuffer keyBuffer = keyMappedByteBuffers[hash].slice();
keyBuffer.position(keyOffsets[hash].getAndAdd(KEY_AND_OFF_LEN));
keyBuffer.putLong(numkey).putInt(off);
//将value写入buffer
ByteBuffer valueBuffer = localBufferValue.get();
valueBuffer.clear();
valueBuffer.put(value);
valueBuffer.flip();
fileChannels[hash].write(valueBuffer, ((long) off) << SHIFT_NUM);
} catch (IOException e) {
throw new EngineException(RetCodeEnum.IO_ERROR, "写入数据出错");
}
}
@Override
public byte[] read(byte[] key) throws EngineException {
long numkey = Util.bytes2long(key);
int hash = valueFileHash(numkey);
long off = keyMap[hash].getOrDefault(numkey, -1);
ByteBuffer buffer = localBufferValue.get();
if (off == -1) {
throw new EngineException(RetCodeEnum.NOT_FOUND, numkey + "不存在");
}
try {
buffer.clear();
fileChannels[hash].read(buffer, off << SHIFT_NUM);
} catch (IOException e) {
throw new EngineException(RetCodeEnum.IO_ERROR, "读取数据出错");
}
return buffer.array();
}
@Override
public void range(byte[] lower, byte[] upper, AbstractVisitor visitor) throws EngineException {
}
@Override
public void close() {
for (int i = 0; i < FILE_COUNT; i++) {
try {
keyFileChannels[i].close();
fileChannels[i].close();
} catch (IOException e) {
logger.error("close error");
}
}
}
private static int valueFileHash(long key) {
return (int) (key & HASH_VALUE);
}
}
这一版写的代码和之前有点不同如下:
- 这里value读写的时候用的ByteBuffer是HeapByteBuffer,本来用DirectByteBuffer是想着对外内存相对来说写入会更快,但实际上将byte[] value写入buffer的时候避免不了将 其从堆内拷贝到堆外的过程。而查看了FileChannel的write方法源码时,发现其对文件的写入都是基于DirectByteBuffer进行的,其本身会维护一个堆外内存的缓存,测试之后发现两者的性能相差无几,所以也没有再关注这个
- mmap写入key的时候调用了slice方法,目的是获取MappedByteBuffer的一个切片,因为其写入是非线程安全的,实质上内部是通过调用unsafe的putByte实现的。
总结
初赛刚写的时候有一点中间件性能大赛复赛类似的地方,不过相比来说还是多学会了许多知识。我其实也尝试了利用unsafe来实现内存拷贝的一部分,但是似乎并没有起到一个好的效果,感觉主要还是我的使用姿势有些不正确,我把这一部分的有关代码放在了github的unsafe分支中,有兴趣也可以简单查看一下。
正在进行的是复赛,相比初赛来说增加了一个全量顺序遍历的需求,难度更大,也更有意思了,感觉复赛更考的是一部分设计方面的东西了,接下来还是会使用Java继续参加,如果有所收获的话,还会再写一篇博客进行相应的总结。