基于Java的高效内存键值存储

基于Java的高效内存键值存储,java,hashmap,key-value,b-tree,Java,Hashmap,Key Value,B Tree,我存储了1.11亿个键-值对(一个键可以有多个值-最大值为2/3),它们的键是50位整数,值是32位(最大值)整数。现在,我的要求是: 快速插入(键、值)对[允许重复] 基于键快速检索值 基于多重映射给出了一个很好的解决方案。但是,我希望在主内存中存储更多的键值对,而不会造成性能损失。我从网络文章中了解到,B+树、R+树、B-树、紧凑多重映射等都是一个很好的解决方案。有人能帮我吗 是否有任何Java库可以适当地满足我的所有这些需求 (上述/其他ds也可接受。没有问题)? 实际上,我需要一个高效的

我存储了1.11亿个键-值对(一个键可以有多个值-最大值为2/3),它们的键是50位整数,值是32位(最大值)整数。现在,我的要求是:

  • 快速插入(键、值)对[允许重复]
  • 基于键快速检索值
  • 基于多重映射给出了一个很好的解决方案。但是,我希望在主内存中存储更多的键值对,而不会造成性能损失。我从网络文章中了解到,B+树、R+树、B-树、紧凑多重映射等都是一个很好的解决方案。有人能帮我吗

    是否有任何Java库可以适当地满足我的所有这些需求 (上述/其他ds也可接受。没有问题)? 实际上,我需要一个高效的java库数据结构来存储/检索 键值/值对占用较少内存,必须 内置内存

    注:我曾尝试过Louis Wasserman、京都/东京内阁等提到的HashMultiMap(番石榴,并用trove进行了一些修改)。我的经验不适用于磁盘烘焙解决方案。所以请避免:)。另一点是,对于选择库/ds,一个重要的点是:键是50位(因此,如果我们指定64位),14位将丢失,值是32位Int(最大值)-大多数是10-12-14位。因此,我们也可以在那里节省空间

    是否有任何Java库可以适当地满足我的所有这些需求

    答:不,或者至少,不是一个可以最小化内存占用的方案


    但是,编写一个专门针对这些需求的自定义映射类应该很容易。

    查找数据库是个好主意,因为这些问题正是它们设计的目的。近年来,键值数据库变得非常流行,例如web服务(关键字“NoSQL”),因此您应该找到一些东西

    自定义数据结构的选择还取决于您是否希望使用硬盘来存储数据(以及存储数据的安全程度),或者数据是否在程序退出时完全丢失


    如果手动实现,并且整个数据库可以很容易地放入内存,我只需要在C中实现一个hashmap。创建一个散列函数,从一个值中给出一个(分布良好的)内存地址。如果已指定,则在此处或旁边插入。分配和检索则是O(1)。如果用Java实现,每个(原语)对象将有4字节的开销。

    如果必须使用Java,则实现自己的哈希表/哈希映射。表的一个重要属性是使用linkedlist来处理冲突。因此,当您执行查找时,可能会返回列表中的所有元素。

    我认为JDK中没有任何东西可以做到这一点

    然而,实现这样一件事只是一个简单的编程问题。这是一个带线性探测的开放寻址哈希表,其中键和值存储在并行数组中:

    public class LongIntParallelHashMultimap {
    
        private static final long NULL = 0L;
    
        private final long[] keys;
        private final int[] values;
        private int size;
    
        public LongIntParallelHashMultimap(int capacity) {
            keys = new long[capacity];
            values = new int[capacity];
        }
    
        public void put(long key, int value) {
            if (key == NULL) throw new IllegalArgumentException("key cannot be " + NULL);
            if (size == keys.length) throw new IllegalStateException("map is full");
    
            int index = indexFor(key);
            while (keys[index] != NULL) {
                index = successor(index);
            }
            keys[index] = key;
            values[index] = value;
            ++size;
        }
    
        public int[] get(long key) {
            if (key == NULL) throw new IllegalArgumentException("key cannot be " + NULL);
    
            int index = indexFor(key);
            int count = countHits(key, index);
    
            int[] hits = new int[count];
            int hitIndex = 0;
    
            while (keys[index] != NULL) {
                if (keys[index] == key) {
                    hits[hitIndex] = values[index];
                    ++hitIndex;
                }
                index = successor(index);
            }
    
            return hits;
        }
    
        private int countHits(long key, int index) {
            int numHits = 0;
            while (keys[index] != NULL) {
                if (keys[index] == key) ++numHits;
                index = successor(index);
            }
            return numHits;
        }
    
        private int indexFor(long key) {
            // the hashing constant is (the golden ratio * Long.MAX_VALUE) + 1
            // see The Art of Computer Programming, section 6.4
            // the constant has two important properties:
            // (1) it is coprime with 2^64, so multiplication by it is a bijective function, and does not generate collisions in the hash
            // (2) it has a 1 in the bottom bit, so it does not add zeroes in the bottom bits of the hash, and does not generate (gratuitous) collisions in the index
            long hash = key * 5700357409661598721L;
            return Math.abs((int) (hash % keys.length));
        }
    
        private int successor(int index) {
            return (index + 1) % keys.length;
        }
    
        public int size() {
            return size;
        }
    
    }
    
    请注意,这是一个固定大小的结构。您需要创建一个足够大的数据库来保存所有的数据——我的1.1亿条记录占用1.32 GB。它越大,超出存储数据所需的容量,插入和查找的速度就越快。我发现,对于1.1亿个条目,负载因子为0.5(2.64 GB,所需空间的两倍),查找密钥平均需要403纳秒,而负载因子为0.75(1.76 GB,比所需空间多三分之一),则需要575纳秒。将负载因子降低到0.5以下通常不会产生太大的差异,事实上,负载因子为0.33(4.00GB,比需要的空间大三倍),平均时间为394纳秒。因此,即使您有5 GB可用空间,也不要全部使用


    还要注意,不允许将零作为键。如果这是一个问题,请将空值更改为其他值,并在创建时用该值预填充密钥数组。

    基于@Tom Anderson solution,我不再需要分配对象,并添加了性能测试

    import java.util.Arrays;
    import java.util.Random;
    
    public class LongIntParallelHashMultimap {
        private static final long NULL = Long.MIN_VALUE;
    
        private final long[] keys;
        private final int[] values;
        private int size;
    
        public LongIntParallelHashMultimap(int capacity) {
            keys = new long[capacity];
            values = new int[capacity];
            Arrays.fill(keys, NULL);
        }
    
        public void put(long key, int value) {
            if (key == NULL) throw new IllegalArgumentException("key cannot be " + NULL);
            if (size == keys.length) throw new IllegalStateException("map is full");
    
            int index = indexFor(key);
            while (keys[index] != NULL) {
                index = successor(index);
            }
            keys[index] = key;
            values[index] = value;
            ++size;
        }
    
        public int get(long key, int[] hits) {
            if (key == NULL) throw new IllegalArgumentException("key cannot be " + NULL);
    
            int index = indexFor(key);
    
            int hitIndex = 0;
    
            while (keys[index] != NULL) {
                if (keys[index] == key) {
                    hits[hitIndex] = values[index];
                    ++hitIndex;
                    if (hitIndex == hits.length)
                        break;
                }
                index = successor(index);
            }
    
            return hitIndex;
        }
    
        private int indexFor(long key) {
            return Math.abs((int) (key % keys.length));
        }
    
        private int successor(int index) {
            index++;
            return index >= keys.length ? index - keys.length : index;
        }
    
        public int size() {
            return size;
        }
    
        public static class PerfTest {
            public static void main(String... args) {
                int values = 110* 1000 * 1000;
                long start0 = System.nanoTime();
                long[] keysValues = generateKeys(values);
    
                LongIntParallelHashMultimap map = new LongIntParallelHashMultimap(222222227);
                long start = System.nanoTime();
                addKeyValues(values, keysValues, map);
                long mid = System.nanoTime();
                int sum = lookUpKeyValues(values, keysValues, map);
                long time = System.nanoTime();
                System.out.printf("Generated %.1f M keys/s, Added %.1f M/s and looked up %.1f M/s%n",
                        values * 1e3 / (start - start0), values * 1e3 / (mid - start), values * 1e3 / (time - mid));
                System.out.println("Expected " + values + " got " + sum);
            }
    
            private static long[] generateKeys(int values) {
                Random rand = new Random();
                long[] keysValues = new long[values];
                for (int i = 0; i < values; i++)
                    keysValues[i] = rand.nextLong();
                return keysValues;
            }
    
            private static void addKeyValues(int values, long[] keysValues, LongIntParallelHashMultimap map) {
                for (int i = 0; i < values; i++) {
                    map.put(keysValues[i], i);
                }
                assert map.size() == values;
            }
    
            private static int lookUpKeyValues(int values, long[] keysValues, LongIntParallelHashMultimap map) {
                int[] found = new int[8];
                int sum = 0;
                for (int i = 0; i < values; i++) {
                    sum += map.get(keysValues[i], found);
                }
                return sum;
            }
        }
    }
    
    使用Java7更新3在3.8GHz i7上运行

    这比之前的测试慢得多,因为您正在访问主内存,而不是随机访问缓存。这真的是对你记忆速度的测试。写入速度更快,因为它们可以异步执行到主内存


    使用此集合

    final SetMultimap<Long, Integer> map = Multimaps.newSetMultimap(
            TDecorators.wrap(new TLongObjectHashMap<Collection<Integer>>()),
            new Supplier<Set<Integer>>() {
                public Set<Integer> get() {
                    return TDecorators.wrap(new TIntHashSet());
                }
            });
    

    对于110M条目,您需要大约35GB的内存和一台比我的(3.8GHz)快10倍的机器,以每秒执行500万次加法。

    可能我回答这个问题晚了,但弹性搜索将解决您的问题。

    感谢您的回答。但是,你认为如何构建一个自定义的映射/树状结构?或者你是否知道任何一个库都可以解决上述问题,占用的内存更少(相对而言),这样我至少可以尝试获得一个基准。@arpsss-我建议在这种情况下,你可以编写自己的映射类。。。从头开始,谢谢。实际上,主要的问题是:1)它们非常非常慢,因为我的磁盘I/O速度很慢。2) 他们有很多调优参数,我不可能为每一个DB进行调优。3) 他们不能像我在东京机柜的例子中看到的那样利用内存(可能是经过多次调整后,它工作得很好),但机器不同意味着调整参数不同。所以,我想谈谈内存实现。加载/存储性能不会有问题。1)内存中有纯数据库,如MemcacheDB、Redis,这里建议的东西2)我理解复杂性可以驱使您使用自定义解决方案。这里有更多的数据库链接:(向下滚动到键值/元组存储)非常感谢。C是否有任何内置的多重映射没有这样的内存开销?我知道一点C。大部分空间将丢失以支持快速插入和移除。插入/移除的工作量越大,就越紧凑。看起来您应该能够轻松地将其存储在几GB中。你的内存需求是什么?@PeterLawrey,我想存储所有1.1亿的密钥值
    final SetMultimap<Long, Integer> map = Multimaps.newSetMultimap(
            TDecorators.wrap(new TLongObjectHashMap<Collection<Integer>>()),
            new Supplier<Set<Integer>>() {
                public Set<Integer> get() {
                    return TDecorators.wrap(new TIntHashSet());
                }
            });
    
     Generated 47.2 M keys/s, Added 0.5 M/s and looked up 0.7 M/s