【问题标题】:Java On-Memory Efficient Key-Value StoreJava On-Memory 高效键值存储
【发布时间】:2012-04-21 07:22:40
【问题描述】:

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

  1. 快速插入(键、值)对[允许重复]
  2. 基于键快速检索值。

基于 MultiMap 的here 给出了一个很好的解决方案。但是,我想在主内存中存储更多的键值对,而没有/很少的性能损失。我从网络文章中研究了 B+ 树、R+ 树、B 树、紧凑型多重映射等可以是一个很好的解决方案。谁能帮帮我:

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

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

【问题讨论】:

  • 大部分空间将丢失以支持快速插入和删除。您的插入/移除工作越多,您就越紧凑。看来您应该能够轻松地将其存储在几 GB 中。您的内存要求是多少?
  • @PeterLawrey,我想将所有 1.1 亿个键值存储在 5/6 GB 中。
  • 所以如果你有 12 个字节的键和值,你仍然可以有大约 75% 的开销。
  • @PeterLawrey,是的。但是,使用 2.5 GB,我只能存储 3000 万(使用 Guava)。这就是为什么,我在问。

标签: java hashmap key-value b-tree


【解决方案1】:

是否有任何 Java 库可以很好地满足我的所有这些需求。

AFAIK 没有。或者至少,没有一个可以最大限度地减少内存占用。

但是,编写一个专门满足这些要求的自定义地图类应该很容易。

【讨论】:

  • 感谢您的回复。但是,您如何看待构建自定义地图/树状结构?
  • 或者你知道任何解决上述目的的库,内存占用更少(比较),因此我至少可以尝试获得一个基准。
  • @Arpssss - 我建议在这种情况下你可以编写自己的地图类......从头开始。
【解决方案2】:

寻找数据库是个好主意,因为这类问题正是它们的设计目标。近年来 Key-Value 数据库变得非常流行,例如对于 Web 服务(关键字“NoSQL”),所以你应该找到一些东西。

自定义数据结构的选择还取决于您是要使用硬盘驱动器存储数据(以及它的安全性)还是在程序退出时完全丢失。

如果手动实现并且整个数据库很容易放入内存中,我只需在 C 中实现一个哈希映射。创建一个哈希函数,从一个值中给出一个(良好分布的)内存地址。如果已经分配,​​则插入那里或旁边。然后分配和检索是 O(1)。如果你在 Java 中实现它,每个(原始)对象都会有 4 字节的开销。

【讨论】:

  • 谢谢。实际上,主要问题是:1)它们非常慢,因为我的磁盘 I/O 很慢。 2)他们有很多调整参数,我不可能为每个数据库调整。 3)他们不能像我在东京内阁看到的那样利用内存优势(可能经过大量调整后效果很好),但机器不同意味着调整参数不同。所以,我想要 On-Memory 实现。加载/存储性能不会成为问题。
  • 1) 有纯内存数据库,例如 MemcacheDB、Redis,这里建议的东西stackoverflow.com/questions/3122652/… stackoverflow.com/questions/2574689/… 2) 我知道复杂性可以驱使您使用自定义解决方案。
  • 这里有更多数据库链接:nosql-database.org(向下滚动到键值/元组存储)
  • 非常感谢。 C 是否有任何没有这种内存开销的内置 MultiMap?我知道一点C。
【解决方案3】:

如果你必须使用 Java,那么实现你自己的 hashtable/hashmap。表的一个重要属性是使用链表来处理冲突。因此,当您进行查找时,您可能会返回列表中的所有元素。

【讨论】:

    【解决方案4】:

    我认为 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.00 GB,比所需空间多三倍),我得到的平均时间为 394 纳秒。因此,即使您有 5 GB 可用空间,也不要全部使用。

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

    【讨论】:

    • 非常感谢。您能否告诉我:存储这 1.1 亿个条目需要多少空间(以 GB 为单位 - 或多或少)。您搜索每个密钥,意味着 1.1 亿个密钥17 秒内?
    • 酷。空间要求还可以。但是,搜索性能不是我所需要的。我想要像 Guava MultiMap 一样的性能,例如 0.5 万/秒(例如)。
    • 我意识到我的测试工具中有一个错误(不是上面的代码)。现在我已经修复它,1.1 亿个条目用于 80722005 个不同的键,因此每个键平均有 1.36 个条目。依次获取每个密钥需要 33 秒,或每个密钥 300 纳秒。这是每秒 330 万次查找。
    • 如果你的输入真的没有像这样分布均匀,那么解决办法就是在键上加一个搅拌功能。我已经编辑了代码来做到这一点——我将密钥乘以一个精心挑选的幻数。通过这一更改,您的测试将在 0 秒内完成。
    • 我添加了关于幻数的评论。我从concentric.net/~ttwang/tech/inthash.htm 得到这个想法,你可以看到有更好的,虽然更复杂,散列函数。幻数应该适用于正多头 - 它应该适用于所有多头,其中正数是一个子集。
    【解决方案5】:

    基于@Tom Andersons 解决方案,我不再需要分配对象,并添加了性能测试。

    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;
            }
        }
    }
    

    打印

    Generated 34.8 M keys/s, Added 11.1 M/s and looked up 7.6 M/s
    

    在带有 Java 7 更新 3 的 3.8 GHz 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());
                }
            });
    

    使用 5000 万个条目(大约使用 16 GB)和-mx20g 运行相同的测试,我得到以下结果。

     Generated 47.2 M keys/s, Added 0.5 M/s and looked up 0.7 M/s
    

    对于 1.1 亿个条目,您需要大约 35 GB 的内存和一台比我的 (3.8 GHz) 快 10 倍的机器才能每秒执行 500 万次添加。

    【讨论】:

    • 对不起,我没有得到代码。另一件事是:Tom 的解决方案存在以下问题:1)当所有查询匹配时以及 2)当更多键有重复时,性能最差。检查这个 textuploader.com/?p=6&id=hzGIp 代码,它需要 10 多分钟才能完成 500 万次 put/get。
    • 这段代码能解决这个问题吗?另一点是:我希望以一组值的形式返回结果。
    • 创建集合可能比查找更昂贵,但您可以这样做。
    • 无论如何它都不会很好,因为你有这么高的性能要求。您将无法为每个密钥创建一个 Set,因为仅此一项就需要每个密钥大约 40 个字节。除非你有快速的记忆力,否则你将处于劣势。顺便说一句:b = (long) Math.floor(i/2) + 1b = i/2 + 1 相同
    • 我已经为同一个测试尝试了这个集合并将我的结果添加到最后。
    【解决方案6】:

    可能我回答这个问题迟了,但弹性搜索会解决你的问题。

    【讨论】:

      猜你喜欢
      • 2021-06-11
      • 2017-05-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多