MapDB 教程二

HTreeMap

HTreeMap为MapDB提供HashMap 和HashSet 集合。它可选择性地支持条目到期,可被用来作为缓存使用。在并发更新的场景下他是线程安全的。
线程安全性方面,通过使用多个段来支持并行写入,每个段具有单独的ReadWriteLock。JDK 7中的ConcurrentHashMap 以类似的方式工作。段的数量(也称为并发因子)是可配置的。
HTreeMap是一个分段哈希树。与其他HashMaps不同,它不使用固定大小的哈希表,并且在哈希表增长时不会重新哈希编码所有数据。HTreeMap使用自动扩展索引树,所以它不需要调整大小。它也占用较少的空间,因为空哈希槽不消耗任何空间。另一方面,树结构需要更多的寻求,在访问速度稍慢一些。

HTreeMap可以根据四个条件可选地支持条目过期:最大map大小,最大存储大小,自上次修改以来的生存时间以及自上次访问以来的生存时间。过期条目将自动删除。此功能使用FIFO队列,每个段都有独立的过期队列。

串行化

HTreeMap有很多参数。最重要的是name,它用来标识数据库对象内的Map 和处理Map中数据的序列化:

HTreeMap map = 
    db.hashMap("name_of_map").
    keySerializer(Serializer.STRING).
    create();

//or shorter form 
HTreeMap map2 = 
    db.hashMap("some_other_map", Serializer.STRING, Serializer.LONG).
    create();

也可以跳过序列化器定义,但是MapDB将使用较慢的通用序列化,不建议这样做:

HTreeMap map = db.hashMap("name_of_map").create();

推荐使用HTreeMap处理大键/值。在同样的情况下,您可能需要使用压缩。可以启用压缩存储范围,但是有一些开销。相反,最好将压缩应用于键或值上的特定序列化器。这是通过使用serializer wrapper来完成的:

HTreeMap map = 
    db.hashMap("map").
        valueSerializer(new SerializerCompressionWrapper(Serializer.STRING)).
        create();

哈希编码

大多数哈希映射使用由Object.hashCode()生成的32位哈希值,并检查其相等性
Object.equals(other)。但是很多类(byte [] ,int [] )不能正确实现。
MapDB使用Key Serializer生成哈希码并比较键。例如,如果使用Serializer.BYTE_ARRAY 作为键序列化器,byte [] 可以直接用作HTreeMap中的键:

HTreeMap map = 
    db.hashMap("map").
    keySerializer(Serializer.BYTE_ARRAY).
    valueSerializer(Serializer.LONG).
    create();

另一个问题是在一些类中的hashCode()是弱的,它会导致冲突并降低性能。String.hashCode()是弱的,但是是规范的一部分,所以它不能被改变。

JDK中的HashMap 以牺牲内存和性能开销为代价实现了许多解决方法。HTreeMap 没有这样的解决方法,而弱Hash将会大大减缓。
相反,HTreeMap 正在修复问题的根源,Serializer.STRING 使用更强的XXHash,这会产生较少的冲突。String.hashCode()仍然可用,但使用不同的序列化程序:

//this will use strong XXHash for Strings 
HTreeMap map = db.hashMap("map")
// by default it uses strong XXHash
.keySerializer(Serializer.STRING)
.valueSerializer(Serializer.LONG)
.create();

//this will use weak `String.hashCode()` 
HTreeMap map2 = db.hashMap("map2")
// use weak String.hashCode()
.keySerializer(Serializer.STRING_ORIGHASH)
.valueSerializer(Serializer.LONG)
.create();

哈希MAP容易受到哈希碰撞攻击。HTreeMap 增加了Hash Seed的保护。在创建集合时随机生成,并与其定义一起保持。用户还可以提供自己的哈希种子:

HTreeMap map = 
    db.hashMap("map", Serializer.STRING, Serializer.LONG)
    .hashSeed(111) //force Hash Seed value
    .create();

配置

HashMap 具有初始容量,负载因子等参数。MapDB具有不同的参数集,可以控制其访问时间和最大大小。这些被定义为术语“Map配置”一栏。
并发是通过使用多个段来实现的,每个段具有单独的读写锁定。每个并发段是独立的,它有自己的大小计数器,迭代器和到期队列。段数是可配置的。数量太少会导致并发更新拥塞,太大会增加内存开销。

HTreeMap 使用索引树而不是为其哈希表增加Object [] 。索引树是稀疏数组类结构,它使用数组的树状层次结构。它是稀疏的,所以未使用的条目不占用任何空间。它不会重做(将所有条目复制到更大的数组),但也不能超出其初始容量。

HTreeMap 配置由配置功能控制。它需要三个参数:并发,段数。默认值为8,它始终向上舍入到2的幂。

索引树目录节点的最大节点大小。默认值为16,它总是四舍五入到二的幂。最大值为128个条目。索引树中的级别数,默认值为4

最大哈希表大小计算为:segment * node size ^ level count 。默认的最大哈希表大小为8 * 16 ^ 4 = 50万条目。

如果哈希表大小设置太低,则哈希冲突将在其填满和性能降级后开始发生。即使在Hash表已满之后,HTreeMap 也会接受新的条目,但性能会降低。32位哈希强制哈希表大小上限:40亿条目。有一个计划支持64位散列。

其他参数

另一个参数是大小计数器。默认情况下,HTreeMap不会跟踪其大小,map.size()执行线性扫描来计算所有条目。您可以启用大小计数器,在这种情况下,map.size()是即时的,但是在插入时有一些开销。

HTreeMap map = 
    db.hashMap("map", Serializer.STRING, Serializer.LONG).
    counterEnable().
    create();

最后有一些有意思的事情。值加载器如果没有找到现有的键,则加载一个值的函数。新创建的键/值将插入到map中。这样map.get(key)就不会返回null。这主要用于各种生成器和高速缓存中。

HTreeMap map = db.hashMap("map", Serializer.STRING, Serializer.LONG).valueLoader(s -> 1L).create();
//return 1, even if key does not exist 
Long one = map.get("Non Existent");
// Value Creator output was added to Map 
map.size(); // => 1

分片存储获取更好的并发性

HTreeMap 被分割成单独的段。每个段是独立的,不与其他段共享任何状态。但是,它们仍然共享底层存储,并影响并发负载下的性能。可以通过为每个分段使用单独的存储来使分段真正独立。
这就是所谓的分片HTreeMap ,可以直接创造DBMaker :

HTreeMap map = DBMaker
//param is number of Stores (concurrency factor)
.memoryShardedHashMap(8)
.keySerializer(Serializer.STRING)
.valueSerializer(Serializer.BYTE_ARRAY)
.create();

//DB does not exist, so close map directly 
map.close();

Shached HTreeMap具有与DB 创建的HTreeMap类似的配置选项。但是没有与此HTreeMap相关的DB对象。所以为了关闭Sharded的HTreeMap,必须直接调用HTreeMap.close()方法。

超期限制

如果满足某些条件,HTreeMap会提供可选条目过期。条目过期的条件如下:

  • map中的条目比过期期间长。过期时间可能是自创建、上次修改或自上一次读取访问以来的时间。map中的条目数有可能超过最大数
  • map将消费比空间限制消耗更多的磁盘空间或内存

以下代码将设置自创建、上次更新以及自上次访问以来的到期时间:

// remove entries 10 minutes after their last modification,
// or 1 minute after last get() 
HTreeMap cache = 
    db.hashMap("cache").
    expireAfterUpdate(10, TimeUnit.MINUTES).
    expireAfterCreate(10, TimeUnit.MINUTES).
    expireAfterGet(1, TimeUnit.MINUTES).
    create();

以下代码将创建具有16GB空间限制的HTreeMap:

// Off-heap map with max size 16GB Map 
cache = db.hashMap("map").
    expireStoreSize(16 * 1024*1024*1024).
    expireAfterGet().
    create();

同样也可以限制map的最大尺寸:

HTreeMap cache = db.hashMap("cache").
    expireMaxSize(128).
    expireAfterGet().
    create();

HTreeMap为每个段维护LIFO超期队列,逐出遍历队列并删除最旧的条目。并非所有地图条目都被放置到过期队列中。为了说明,在本示例中,只有在更新(值更改)条目放入到期队列后,新条目才会过期。

HTreeMap cache = db.hashMap("cache").
    expireAfterUpdate(1000).
    create();

基于时间的逐出将始终将进入到期队列。但是,其他到期条件(大小和空间限制)也需要提示何时进入到期队列。在以下示例中,没有任何条目被放入队列,也没有条目过期。

HTreeMap cache = db.hashMap("cache").expireMaxSize(1000).create();

There are three possible triggers which will place entry into Expiration Queue:

有三个可能的触发器将进入到期队列:
expireAfterCreate(),expireAfterUpdate()和expireAfterGet()。注意没有TTL参数。
在其他方法内完成条目过期。如果你调用map.put()或map.get(),它可能会删除一些条目。但是驱逐有一些开销,这会减慢用户操作。可以选择向执行者提供HTreeMap,并在后台线程中执行驱逐。这将驱逐两个后台线程中的条目,每10秒钟将触发逐出:

DB db = DBMaker.memoryDB().make();

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

HTreeMap cache = db
.hashMap("cache")
.expireMaxSize(1000)
.expireAfterGet()
.expireExecutor(executor)
.expireExecutorPeriod(10000)
.create();

//once we are done, background threads needs to be stopped db.close();

过期可以与多个Sharded HTreeMap组合,以获得更好的并发性。在这种情况下,每个段都有独立的存储,并提高并行更新的可扩展性。

HTreeMap cache = DBMaker
.memoryShardedHashMap(16)
.expireAfterUpdate()
.expireStoreSize(128*1024*1024)
.create();

分片的HTreeMap应该与多个后台线程相结合才能逐出。随着时间的推移,存储变得分散,最终无法回收空间。如果有太多的可用空间,可以安排定期压缩的选项。压缩将回收可用空间。因为每个Store(segment)是分开压缩的,压缩不影响所有正在运行的线程。

HTreeMap cache = DBMaker
.memoryShardedHashMap(16)
.expireAfterUpdate()
.expireStoreSize(128*1024*1024)

//entry expiration in 3 background threads
.expireExecutor(
Executors.newScheduledThreadPool(3))

//trigger Store compaction if 40% of space is free
.expireCompactThreshold(0.4)

.create();

过期溢出

HTreeMap支持修改侦听器。它通知监听器关于HTreeMap的插入、更新和删除。可以将两个集合链接在一起。通常更快的内存有限的大小,而磁盘上的速度更慢,无限大小。一个条目从内存中过期后,它将被修改侦听器自动移动到磁盘上。而Value Loader会将值重新加载到内存映射中,如果没有找到map.get()操作。
要建立磁盘溢出,请使用以下代码:

DB dbDisk = DBMaker
.fileDB(file)
.make();

DB dbMemory = DBMaker
.memoryDB()
.make();

// Big map populated with data expired from cache 
HTreeMap onDisk = dbDisk.hashMap("onDisk").create();

// fast in-memory collection with limited size 
HTreeMap inMemory = dbMemory.hashMap("inMemory").expireAfterGet(1, TimeUnit.SECONDS)
//this registers overflow to `onDisk`
.expireOverflow(onDisk)
//good idea is to enable background expiration
.expireExecutor(Executors.newScheduledThreadPool(2))
.create();

一旦建立绑定,将从内存map中删除的每个条目都将被添加到
onDiskMap。这仅适用于过期条目,map.remove()也将删除onDisk 中的条目。

//insert entry manually into both maps for demonstration 
inMemory.put("key", "map");

//first remove from inMemory 
inMemory.remove("key"); 
onDisk.get("key"); // -> not found

如果调用了inMemory.get(key),并且值不存在,则Value Loader将尝试在onDisk中查找Map 。如果在onDisk中找到值,它将被添加到inMemory中。

onDisk.put(1,"one");
inMemory.size();
//onDisk has content, inMemory is empty
//> 0

// get method will not find value inMemory, and will get value from onDisk 
inMemory.get(1); //> "one"
// inMemory now caches result, it will latter expire and move to onDisk
inMemory.size(); //> 1

也可以清除整个主map并将所有数据移动到磁盘中:

inMemory.put(1,11); inMemory.put(2,11);
//expire entire content of inMemory Map 
inMemory.clearWithExpire();

你可能感兴趣的:(Java编程)