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会提供可选条目过期。条目过期的条件如下:
以下代码将设置自创建、上次更新以及自上次访问以来的到期时间:
// 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();