Java常用开源库梳理(1)

Java集合数据结构

1. List:
线程不安全:ArrayList, LinkedList, BitSet
线程安全:Vector, CopyOnWriteArrayList
(1)ArrayList:使用数组实现的List,相当于动态数组,读取速度快,插入与删除速度慢(因为插入与删除时要移动后面的元素),保持插入顺序,适合于随机访问。允许重复元素,包括null。
(2)LinkedList:使用双向链表实现的List,删除与插入速度快,读取速度较慢,因为它读取时是从头向尾(如果节点在链的前半部分),或尾向头(如果节点在链的后半部分)查找元素。允许重复元素,包括null。因此适合于元素的插入与删除操作。LinkedList还实现了Dueue接口,因此也是一个双端队列。使用LinkedList的场景:没有大量的随机访问,但有很多add/remove的操作。
(3)Vector:线程安全的ArrayList,也是用数组实现。使用synchronized来同步可变操作。添加元素的时候,当超过初始容量的时候,Vector和ArrayList需要更多的空间,Vector需要将数组的大小增加一倍,而ArrayList需要增加50%。
(4)CopyOnWriteArrayList:一个线程安全的ArrayList变体。使用写时复制保证线程安全,所有可变操作通过对底层数组的最新副本实现。用于读多写少的并发场景,比如白名单,黑名单,商品类目的访问和更新场景。

2. Set:
线程不安全:HashSet, LinkedHashSet, TreeSet, EnumSet, BitSet
线程安全:CopyOnWriteArraySet, ConcurrentSkipListSet
(1)HashSet:使用哈希表实现的无序Set,元素无序,且不重复。是最常用的,查询速度最快,因为内部以HashMap来实现,所以插入元素不能保持插入次序。能插入一个null。
(2)LinkedHashSet:基于哈希表和链表实现的Set。保持元素的插入次序,输出的顺序就是插入时的顺序。允许一个null键与多个null值。
(3)TreeSet:基于树结构TreeMap实现的有序Set。一个总是处于排序状态的Set,元素按自然顺序排序或通过Comparator指定排序规则。不能插入null。
(4)EnumSet:存放枚举类型的Set,集合中所有元素必须来自同一个枚举类型,适用于只有有限个可选值的应用场景。内部使用紧凑高效的位向量实现,存取的空间和时间效率都非常高。
(5)BitSet:位图数据结构,是一个可按需增长的位向量。注意它并没有实现Set接口,只是从效果上看相当于存放一系列bit的集合。常用于海量整型数据的存储、统计、日志分析等。
(6)CopyOnWriteArraySet:线程安全的无序Set,内部使用CopyOnWriteArrayList来实现,写时复制。适用于Set大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间冲突的场景。
(7)ConcurrentSkipListSet:线程安全的、基于跳表实现的高并发有序Set,元素有序,按自然顺序排序或通过Comparator指定排序规则。内部使用ConcurrentSkipListMap实现,适用于高并发场景。

3. Map:
线程不安全:HashMap, LinkedHashMap, TreeMap, EnumMap, WeakeHashMap, IdentityHashMap
线程安全:Hashtable, ConcurrentHashMap, ConcurrentSkipListMap, Properties
(1)HashMap:基于Hash表实现的无序Map。键无序,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的。允许一个null键与多个null值。
(2)LinkedHashMap:基于哈希表和链表实现的Map。保持键的插入顺序。允许一个null键与多个null值。
(3)TreeMap:基于红黑树实现的有序Map。键按自然顺序排序或通过Comparator指定排序规则。不能插入null。
(4)EnumMap:键为枚举类型的Map,所有键必须来自同一个枚举类型,适用于键只有有限个可选值的场景。内部使用紧凑高效的数组实现,键按枚举类型的常量值来排序。
(5)WeakeHashMap:基于哈希表实现的无序Map,其键是弱键,当某个键不再正常使用时,对应条目会从WeakHashMap中自动删除。它与HashMap有类似的性能,具有初始容量和负载因子的相同效率参数。
(6)IdentityHashMap:使用哈希表实现的无序Map,比较两个键相等时使用引用相等k1 == k2,而不是对象内容相等(k1null ? k2null : e1.equals(e2))。因此这个Map允许多个key值重复,但这些key对象引用不能相同。要想真正实现一个key映射多个value,可以用google guava的MultiMap。
(7)Hashtable:线程安全的HashMap,但key和value都不能为null。可变操作都使用了synchronized同步。
(8)ConcurrentHashMap:适用于高并发场景的哈希表,遵从与Hashtable一样的功能和使用规范,只不过底层实现时的同步细节不同。key和value都不能为null,键无序。注意并发容器一般都不允许null元素。
(9)ConcurrentSkipListMap:基于跳表实现的高并发有序Map,增删改查的平均时间为O(log n)。元素按自然顺序排序或通过Comparator指定排序规则。key和value都不能为null。
(10)Properties:特殊的Map,继承自Hashtable,线程安全,表示一组持久的K/V属性。属性列表中的每个键及其对应的值都是一个字符串。Properties可以保存到流中或从流中加载。

4. Queue:
线程不安全:ArrayDeque, LinkedList, PriorityQueue
线程安全(可阻塞可非阻塞):LinkedTransferQueue
线程安全且非阻塞:ConcurrentLinkedQueue, ConcurrentLinkedDeque;
线程安全且阻塞:ArrayBlockingQueue, LinkedBlockingQueue, LinkedBlockingDeque, PriorityBlockingQueue, DelayQueue, SynchronousQueue
(1)ArrayDeque:使用数组实现的无界双端队列Deque。自动扩展队列大小的,非线程安全的,不支持并发访问和修改(并发修改时会fast-fail,抛异常),当作堆栈使用比Stack快,当作队列使用时LinkedList快,不允许存放null元素。
(2)LinkedList:基于双链表实现的List和无界Deque。允许null元素,删除与插入速度快,读取速度较慢。
(3)PriorityQueue:基于小根堆的无界优先级队列Queue。不允许null元素,该队列的头部是指定顺序的最小元素,因此每次取出的是最小元素。元素比较通过自然顺序或构造时传入的Comparator。
(4)LinkedTransferQueue:基于链接节点的、可阻塞的无界传输队列,队列元素FIFO。JDK1.7才引入。生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。新添加的transfer方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递。Doug Lea说从功能角度来讲,LinkedTransferQueue实际上是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集。而且LinkedTransferQueue更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。
(5)ConcurrentLinkedQueue:基于链接节点的无界、线程安全、非阻塞的FIFO队列。新元素插入队列尾部,队列检索操作获取头部的元素。当许多线程对一个公共集合共享访问时ConcurrentLinkedQueue是比较好的选择。不允许null元素,收集关于队列大小的信息会很慢,需要遍历队列。
(6)ConcurrentLinkedDeque:基于链接节点的无界并发、非阻塞deque。并发插入、删除和访问操作可以跨多个线程安全执行。不允许插入null元素。
(7)ArrayBlockingQueue:用数组实现的、有界阻塞的FIFO队列。默认不保证访问者的公平性。在构造时需要指定容量,并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理。通常为了保证公平性会降低吞吐量,只有在的确非常需要的时候再使用它。
(8)LinkedBlockingQueue:用链表实现的、可选有界、阻塞的FIFO队列。可选容量构造参数用于防止队列过度扩展。容量如果未指定则等于默认的Integer.MAX_VALUE。链接节点在每次插入时动态创建,除非这将使队列高于容量。
(9)LinkedBlockingDeque:用链表实现的、可选有界、阻塞的双端队列。可选容量构造参数用于防止队列过度扩展。容量如果未指定则等于默认的Integer.MAX_VALUE。
(10)PriorityBlockingQueue:基于小根堆的无界、阻塞的优先级队列。使用与PriorityQueue类相同的排序规则,并提供阻塞检索操作。
(11)DelayQueue:带延迟属性的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的getDelay(TimeUnit.NANOSECONDS)方法返回一个小于或等于零的值时,则出现期满,poll就可以移除这个元素了。
(12)SynchronousQueue:无任何元素的同步阻塞队列。队列里面没有任何数据缓冲,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。同步队列类似于CSP和Ada中使用的会合通道。它们非常适用于切换设计,其中运行在一个线程中的对象必须与在另一个线程中运行的对象同步,以便交付一些信息,事件或任务。

Google Guava数据结构

1. Multiset
线程不安全:HashMultiset, LinkedHashMultiset, TreeMultiset, EnumMultiset
线程安全:ConcurrentHashMultiset, ImmutableMultiset, ImmutableSortedMultiset
(1)多重集合Multiset是集合(set)概念的延伸,它的元素可以重复出现。可以看作是无元素顺序限制的ArrayList<E>,或者Map<E,Integer>>,键为元素,值为计数。Guava的Multiset实现考虑了这两种视角。
(2)有无序ArrayList的特点:add(E)添加单个给定元素,iterator()返回的迭代器是包含重复元素的所有元素,size()返回包含重复元素的所有元素总个数。
(3)有Map<E,Integer>的特点:提供了符合性能期望的查询操作,count(Object)返回给定元素的计数。HashMultiset.count的复杂度为O(1),TreeMultiset.count的复杂度为O(log n)。entrySet()返回Set<Multiset.Entry<E>>,和Map的entrySet类似。elementSet()返回所有不重复元素的Set<E>,和Map的keySet()类似。所有Multiset实现的内存消耗随着不重复元素的个数线性增长。
(4)应用场景:例如跟踪每种对象的数量,所以你可以用来进行数字统计。

2. Multimap
线程不安全:ArrayListMultimap, LinkedListMultimap, HashMultimap, LinkHashMultimap, TreeMultimap
线程安全:ImmutableListMultimap, ImmutableSetMultimap
(1)多重映射Multimap是映射(map)概念的延伸,允许一个键映射到多个值,可以看作是Map<K,List<V>>或Map<K,Set<V>>。很少会直接使用Multimap接口,更多时候你会用ListMultimap或SetMultimap接口,它们分别把键映射到List或Set。
(2)multimap是把键映射到任意多个值的一般方式,有两种方式看待。一种是"键-单个值映射",并且键可以重复;一种是"键-值集合映射"。Guava的实现考虑了这两种视角。Multimap.size()返回所有”键-单个值映射”的个数,而非不同键的个数。要得到不同键的个数,请改用Multimap.keySet().size()。asMap()为Multimap<K,V>提供Map<K,Collection<V>>形式的视图。Multimap.entries()返回Multimap中所有”键-单个值映射”——包括重复键。如果你想要得到所有”键-值集合映射”,请使用asMap().entrySet()。

3. BiMap
线程不安全:HashBiMap, EnumBiMap, EnumHashBiMap
线程安全:ImmutableBiMap
(1)双向映射BiMap是特殊的Map,即数学中的一一映射,键和值都没有重复。因此键-值映射可以反转成值-键映射(即取逆映射)。传统上实现双向映射需要维护两个单独的map,并保持它们间的同步。BiMap对它们做了封装。
(2)BiMap<K,V>可以用inverse()反转键值映射。由于值是唯一的无重复,因此value()返回Set而不是普通的Collection。在BiMap中,如果你想把键映射到已经存在的值,会抛出IllegalArgumentException异常。如果对特定值,你想要强制替换它的键,请使用 BiMap.forcePut(key, value)。
(3)HashBiMap内部的键-值map和值-健map都是HashMap。EnumBiMap维护的两个map都是EnumMap。EnumHashBiMap的键-值是EnumMap而值-键是HashMap。ImmutableBiMap的两个map都是ImmutableMap。
(4)应用场景:有一一映射的场景,例如国家和首都的对应和查询。

4. Table
线程不安全:HashBasedTable, TreeBasedTable, ArrayTable
线程安全:ImmutableTable
(1)Table是二维矩阵数据结构,可以是稀疏矩阵。本质上类似于Map<K1, Map<K2, V>>的实现,用多个键做索引,K1键为”行”,K2健为”列”。不过guava的Table实现更高效。
(2)Table<R, C, V>提供多种视图,以便你从各种角度使用它:
rowMap():用Map<R, Map<C, V>>表现Table<R, C, V>。同样的, rowKeySet()返回”行”的集合Set<R>。
row® :用Map<C, V>返回给定”行”的所有列,对这个map进行的写操作也将写入Table中。
类似的列访问方法:columnMap()、columnKeySet()、column©。(基于列的访问会比基于的行访问稍微低效点)
cellSet():用元素类型为Table.Cell<R, C, V>的Set表现Table<R, C, V>。Cell类似于Map.Entry,但它是用行和列两个键区分的。
(3)HashBasedTable:基于Linked Hash表的Table,本质上用HashMap<R, HashMap<C, V>>实现。
(4)TreeBasedTable:基于树结构的Table,本质上用TreeMap<R, TreeMap<C,V>>实现。行和列Key按自然顺序或Comparator排序。
(5)ArrayTable:用二维数组实现,并且在构造时要指定行和列的大小。这样在密集数据集下可以提升访问速度和内存空间利用率。
(6)ImmutableTable:基于ImmutableMap<R, ImmutableMap<C, V>>的实现。注意ImmutableTable对稀疏和密集数据集做了优化。

5. ClassToInstanseMap
线程不安全:MutableClassToInstanceMap
线程安全:ImmutableClassToInstanceMap
(1)ClassToInstanceMap是一种特殊的Map:它的键是类型,而值是符合键所指类型的对象(该类对象或其子类对象)。为了扩展Map接口,它额外声明了两个方法:T getInstance(Class<T>) 和T putInstance(Class<T>, T),从而避免强制类型转换,同时保证了类型安全。它有唯一的泛型参数B,代表Map支持的所有类型的上界。
(2)MutableClassToInstanceMap默认背后使用HashMap来存储,也可以在构造时指定一个背后的Map<Class<? extends B>,B>作为存储。

6. EvictingQueue, MinMaxPriorityQueue
(1)EvictingQueue:非阻塞的有界驱逐队列。队列元素FIFO,非线程安全,不支持null元素。当队列已满并再向添加新元素时,队头元素自动被删除。这不同于传统的有界队列,它们是阻塞或者直接拒绝新元素并返回null。该队列在逻辑上相当于一个环形缓存(Ring Buffer)。create()创建队列,add()/offer()/remove()为入队出队操作。remainingCapacity()返回剩余容量。
(2)MinMaxPriorityQueue:双端优先级队列,对最小元素和最大元素提供常数时间的访问性能。元素比较通过自然顺序或构造时传入的Comparator。功能与PriorityQueue一样,但是它底层是用min-max堆来实现,而不是传统的堆数据结构。

7. RangeSet
线程不安全:TreeRangeSet
线程安全:Range, DiscreteDomain, ContiguousSet, ImmutableRangeSet
(1)Range:区间的概念,是特定域中的凸性(非正式说法为连续的或不中断的)部分。在形式上凸性表示对a<=b<=c,range.contains(a)且range.contains©意味着range.contains(b)。有开区间、闭区间、半开区间、无上界区间、无下界区间等。基本运算contains©判断是否包含某个值。常用的有查询运算、关系运算(包含、相连、交集、合并)。
(2)DiscreteDomain:通常有一些可比较类型(但不是全部)是离散的,也就是区间内的所有值是可枚举出来的,例如整数集。DiscreteDomain<C>实现类型C的离散形式操作。一个离散域总是代表某种类型值的全集,它不能代表类似”素数”、”长度为5的字符串”或”午夜的时间戳”这样的局部域。离散域有三个基本操作next©, previous©, distance(C, C)。对有界类型还可以获取minValue(), maxValue()。两个常用的离散域integers(), longs()。你可以通过创建DiscreteDomain子类来创建自己的离散域。
(3)ContiguousSet:给定离散域上的一个区间表示的不可变有序集,包含这个区间的所有元素。ContiguousSet.create(range,domain)是用ImmutableSortedSet<C>形式表示Range<C>中符合离散域定义的元素,并增加一些额外操作。注意它并没有真的构造整个集合,而是返回了区间的set形式。
(4)RangeSet:类似于并查集的数据结构,描述了一组不相连的、非空的区间。当把一个区间添加到可变的RangeSet时,所有相连的区间会被合并,空区间会被忽略。有各种视图操作、元素或区间的查询操作。
(5)TreeRangeSet是后端基于TreeMap实现的可变RangeSet,ImmutableRangeSet则是不可变的。
(6)应用场景:并查集是一种树形数据结构,用于处理一些不相交集合的合并及查询问题,常常在使用中以森林来表示。应用很多,常作为另一种复杂数据结构或算法的存储结构。常见应用有求无向图的连通分量个数,最近公共祖先(LCA),带限制的作业排序,实现Kruskar算法求最小生成树。

8. RangeMap
线程不安全:TreeRangeMap
线程安全:ImmutableRangeMap
(1)RangeMap:描述了”不相交的、非空的区间”到特定值的映射。和RangeSet不同,RangeMap不会合并相邻的映射,即便相邻的区间映射到相同的值。提供两个视图,asMapOfRanges():用Map<Range<K>, V>表现RangeMap。这可以用来遍历RangeMap。subRangeMap(Range<K>):用RangeMap类型返回RangeMap与给定Range的交集视图。这扩展了传统的headMap、subMap和tailMap操作。
(2)TreeRangeMap是后端基于TreeMap实现的可变RangeMap,ImmutableRangeMap则是不可变的。

9. Graph
线程不安全:MutableGraph, MutableValueGraph, MutableNetwork
线程安全:ImmutableGraph, ImmutableValueGraph, ImmutableNetwork, Traverser
(1)Graph表示图数据结构。常用邻接表Map<N,Set<N>>或邻接矩阵Node[][]表示,这种表示表达性不强,guava提供了完整的图实现,支持无向图、有向图、带权图、允许有自环(self-loops)的图、允许有平行边的图(也叫multigraph)、节点和边可以无序、按Comporator排序、或按插入顺序排序。图的节点必须可以作为Map的key来使用。
(2)Graph:无权图,边不带权重或label,只关注节点和节点间的相连。主要操作获取节点的邻接节点集、节点度数、获取图的所有节点集或边集、判断节点间是否相连、对有向图获取节点前向节点集、后继节点集、获取节点入度数、出度数、图的所有节点集或边集。
(3)ValueGraph:带权图,边带有权重或属性。除了图的操作外,还有获取边的属性值。
(4)Network:网络,同时关注节点和边,每条边有唯一性,因此网络允许节点之间有平行边。除了图的操作外,还有获取节点之间的平行边集、节点的关联边集、边的关连节点集等。
(5)Traverser:图的遍历器。breadthFirst(N)广度优先遍历、depthFirstPreOrder(N)深度优先的前序遍历、depthFirstPostOrder(N)深度优先的后序遍历
(6)应用场景:广泛。例如web页面以及页面之间的链接关系 、作者和他发表的文章的关系 、一个人和他的家庭成员之间的关系、机场和航班的关系<GraphAirport>, ValueGraph<Airport,Integer>, Network<Airport, Flight>、网络中节点之间的关系。

10. 散列和布隆过滤器
线程安全:HashFunction, Hashing, HashCode, BloomFilter
(1)HashFunction:散列函数,将任意的数据块映射成一个固定位数的散列码,并且保证相同的输入一定产生相同的输出,不同的输入尽可能产生不同的输出。HashCode则表示产生的散列码。HashFunction的实例可以提供有状态的Hasher,Hasher提供了流畅的语法把数据添加到散列运算,然后获取散列值。Hasher可以接受所有原生类型、字节数组、字节数组的片段、字符序列、特定字符集的字符序列等等,或者任何给定了Funnel实现的对象。Hasher实现了PrimitiveSink接口,这个接口为接受原生类型流的对象定义了fluent风格的API。
(2)Hashing:各种散列算法的静态实现,返回对应的HashFunction实例。
alder32(): 使用32位Adler-32校验和算法的散列函数
crc32(): 使用32位CRC-32校验和算法的散列函数
crc32c(): 使用32位CRC32C校验和算法的散列函数
farmHashFingerprint64(): 使用FarmHash Fingerprint64开源算法的散列函数
goodFastHash(): 返回一个通用的、临时使用、非加密的散列函数
hmacMd5(): 使用128位MD5函数实现的MAC散列算法
hmacSha1(): 使用160位SHA-1函数实现的MAC散列算法
hmacSha256(): 使用256位SHA-256函数实现的MAC散列算法
hmacSha512(): 使用512位SHA-512函数实现的MAC散列算法
md5(): MD5散列函数,不再安全,建议使用sha256()或其他更高级的散列函数
murmur3_128(): 使用128位murmur3算法的散列函数
murmur3_32(): 使用32位murmur3算法的散列函数
sha1(): SHA-1散列函数,不再安全,建议使用sha256()或其他更高级的散列函数
sha256(): SHA-256散列函数
sha384(): SHA-384散列函数
sha512(): SHA-512散列函数
sipHash24(): 64位SipHash-2-4散列算法
consistentHash(h, n): 一致性哈希算法实现,为给定桶大小n,分配[0, n)内的一个哈希桶。当桶增长时该方法保证最小程度的哈希映射变化。
(2)一致性哈希算法:一种分布式哈希实现算法,当hash容器大小调整时尽可能小的改变已存在的hash映射关系,尽可能地保持平衡性和单调性。广泛应用于分布式存储和路由、负载均衡等场景中。
(3)BloomFilter:是一个很长的二进制向量和一系列随机映射函数组成的随机数据结构。拥有很高的空间和时间效率,插入/查询都是常数时间。其基本原理是使用长度为m的位数组来存储数据集信息,同时使用k个相互独立的哈希函数将数据映射到位数组空间。通过k个映射,每个元素在位数组中都占有k位,对应的位置置1。另外Hash函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。布隆过滤器缺点是有一定的误判率和删除困难。若k位不全为1,则元素肯定不在集合中,因此没有识别错误的情形;若k位全为1,则元素可能在集合中(也可能不在)。随着存入的元素数量增加,误判率随之增加。可以通过给定一个误检率来减小误差。
(4)Guava的BloomFilter只要提供Funnel就可以使用它。你可以使用create()方法获取BloomFilter<T>,缺省误检率[falsePositiveProbability]为3%。BloomFilter<T>提供了boolean mightContain(T) 和void put(T),它们的含义都不言自明了。
(3)应用场景:一致性哈希算法在分布式系统中有很多应用,例如Nginx负载均衡策略、各种Redis,Memcached的客户端的请求路由策略。BloomFilter则在海量数据查询,并允许一定误判率的场景中有很广泛的应用,如缓存,K/V存储。
1)黑名单。保存海量的黑名单数据以减少存储成本。对用户名称、IP、Email等进行过滤,每次检查时用key进行hash后,如果不在黑名单内的,肯定可以通行,如果在的则不允许通过,误判情况(将正常用户判定为黑名单用户)增加一个排除名单来进行排除。
2)爬虫重复URL检测。在爬取网站URL时,要检测这条URL是否已经访问过。误判情况为没有访问过的误判为访问过。
3)拼写检查。检查单词拼写是否正确,误判情况为错误的单词误判为正确。
4)磁盘文件检测。将磁盘中或数据库中数据key存入该结构中,检测要访问的数据是否在磁盘或数据库中,然后再发起访问,避免空查询造成磁盘或数据库压力。误判情况为不存在该数据却误判为有该数据。Google著名的分布式数据库 Bigtable 使用了布隆过滤器来查找不存在的行或列,以减少磁盘查找的IO次数。Venti文档存储系统也采用布隆过滤器来检测先前存储的数据。
5)Squid代理的缓存技术。在Squid的cache digests中使用了布隆过滤器,先查找本地有无cache,如果没有则到其他兄弟 cache服务器上去查找。为了避免无谓的查询,在每个cache服务器上保存其兄弟服务器的缓存关键字,以bloomfilter方式存储,在去其他cache服务器查找之前,先检查该结构是否有url,如果有存在url,再去对应服务器查找。误判情况为对应服务器不存在该URL的缓存。
6)SPIN模型检测器。也使用布隆过滤器在大规模验证问题时跟踪可达状态空间。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务。
7)在很多Key-Value系统中也使用了布隆过滤器来加快查询过程。如Hbase,Accumulo,Leveldb,一般而言,Value保存在磁盘中,访问磁盘需要花费大量时间,然而使用布隆过滤器可以快速判断某个Key对应的Value是否存在,因此可以避免很多不必要的磁盘IO操作,只是引入布隆过滤器会带来一定的内存消耗。

11. Cache
线程安全:LoadingCache, CacheLoader, CacheBuilder
(1)Guava Cache是单个应用运行时的本地缓存。缓存中存放的数据总量不会超出内存容量,它不把数据存放到文件或外部服务器。它与ConcurrentHashMap很相似,但也不完全一样。ConcurrentMap会一直保存所有添加的元素,而Cache为了限制内存占用,通常都设定为自动回收元素。
(2)Cache加载:有三种方式CacheLoader、调用get时传入一个Callable实例、Cache.put方法直接插入。LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。
(3)Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存通过LRU策略尝试回收最近没有使用或总体上很少使用的缓存项。
(4)CacheBuilder提供两种定时回收的方法:
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
(5)基于引用的回收:通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收。
(6)从功能上来看Guava Cache已经比较完善了,基本满足了绝大部分本地缓存的需求。它是基于LRU所实现的本地缓存对象,并且能够很好的支持多种淘汰策略(基于对象个数、基于访问频率等等)。但LRU并不是最好缓存淘汰策略,比如有ARC,LIRS和W-TinyLFU等都提供了接近最理想的命中率,他们这些算法进一步提高了本地缓存的效率。Caffeine缓存就使用W-TinyLFU策略来实现,它的性能和缓存命中率更好。Spring5就是由Guava Cache转向了Caffeine。Caffeine的API的操作功能和Guava是基本保持一致的,并且Caffeine为了兼容之前Guava的用户,还做了一个Guava的Adapter给大家使用也是十分的贴心。
(7)Caffeine中的实现:LoadingCache, AsyncLoadingCache, CacheLoader, Caffeine(相当于CacheBuilder)。Caffeine缓存在很多知名开源产品都有应用,如Spring, Cassandra, Akka, Neo4j。

12. RateLimiter, TimeLimiter
线程安全:RateLimiter, SimpleTimeLimiter, FakeTimeLimiter
(1)RateLimiter:速率限制器,会在可配置的速率下分配许可证。如果必要的话,每个acquire()会阻塞当前线程直到许可证可用后获取该许可证。一旦获取到许可证,不需要再释放许可证。它使用一种叫令牌桶的流控算法,按照一定的频率平稳地往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌。RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率。与Semaphore相比,Semaphore限制了并发访问的数量而不是使用速率。通过设置许可证的速率来定义RateLimiter。在默认配置下,许可证会在固定的速率下被分配,速率单位是每秒多少个许可证。为了确保维护配置的速率,许可会被平稳地分配,许可之间的延迟会做调整。可以配置一个拥有预热期的RateLimiter,在预热期内每秒分配的许可数会稳定地增长直到达到稳定的速率。
(2)TimeLimiter:时间限制器,用于限制方法执行的时长,实现超时中断。传统的实现方式是在方法执行前后加入计时功能,如果超过阈值主动终止线程,抛出异常。当然为了不影响主服务运行,这段代码应该独立运行在一个子线程中,而不是耦合在主线程中。
(3)SimpleTimeLimiter:常用于正式方法中,调用方法超时即抛出异常。SimpleTimeLimiter是在Java ExecutorService上做了一层薄的封装,添加了超时中断功能。有两种方式实现超时中断。一是callWithTimeout()通过Callable回调实现超时拦截。ExecutorService.submit()提交任务时返回Future<?>,Future类中的get()有超时中断功能,不过这个方法是阻塞的,因此callWithTimeout()也是阻塞式的。你如果想不阻塞主线程的执行,可以在新的子线程中执行get()操作。另外一种方式是newProxy(),通过JDK动态代理配合callWithTimeout实现超时拦截。FakeTimeLimiter则是一个伪时间限制器,实际上直接执行方法,并不做超时拦截,常用于调试或单元测试。

13. 可注册回调的ListenableFuture
ListenableFuture, FluentFuture, AbstractFuture, SettableFuture, Futures
ListeningExecutorService, ListenableFutureTask, MoreExecutors
(1)ListenableFuture:可监听的Future。继承自JDK中的Future,并添加了回调注册机制。允许你注册回调方法,每个回调关联一个executor,在Future任务完成时回调立即被executor执行。基础方法addListener(Runnable,Executor)添加回调,在多线程运算完时Runnable参数传入的对象会被指定的Executor执行。如果你喜欢以抽象类的方式来使用future而不是实现接口中的方法,可以考虑继承抽象类,它们提供更高级的功能。
(2)FluentFuture支持流式操作,from(),transform(),catching()等,addCallback()添加FutureCallback回调。AbstractFuture是ListenableFuture的一个抽象实现,并实现了FluentFuture抽象类。SettableFuture是可以设置特定值的Future。Futures类包含操作Future的一些工具方法。
(3)ListeningExecutorService:继承自JDK中的ExecutorService,在submit()任务时返回ListenableFuture。若要将ExecutorService转为ListeningExecutorService,可以使用MoreExecutors.listeningDecorator(ExecutorService)进行装饰。
(4)ListenableFutureTask:继承自JDK的FutureTask。在create()任务时返回ListenableFutureTask。MoreExecutors类则包含操作ExecutorService, Executor和ThreadFactory的一些额外方法。
(5)应用场景:在JDK线程池中执行任务时需要注册回调的情况,以便任务执行完后执行回调逻辑。

14. Service框架
Service, AbstractIdleService, AbstractExecutionThreadService, AbstractScheduledService, AbstractService, ServiceManager
(1)Service:表示带有一个运行状态的服务线程对象,包括异步的startAsync()和stopAsync()等方法,调用时会进行状态转换。JDK传统的Thread或Runnable定义的服务只有简单的start()和run()流程,没有细粒度的服务状态转换管理。Service提供了增强功能。服务状态包括NEW, STARTING, RUNNING, STOPPING, TERMINATED, FAILED。 addListener()方法异步添加监听器Service.Listener,它会在每次服务状态转换的时候被调用。注意最好在服务启动之前添加Listener(这时的状态是NEW),否则之前已发生的状态转换事件是无法在新添加的Listener上被重新触发的。state()返回服务状态。awaitRunning()这个方法不能被打断、不强制捕获异常、一旦服务启动就会返回。如果服务没有成功启动,会抛出IllegalStateException异常。同样的awaitTerminated()方法会等待服务达到终止状态(TERMINATED或者FAILED)。两个方法都有重载方法允许传入超时时间。Service接口本身实现起来会比较复杂,且容易碰到一些捉摸不透的问题。因此我们不推荐直接实现这个接口。而是请继承已经封装好的基础抽象类。每个基础类支持一种特定的线程模型。
(2)AbstractIdleService:空服务。在running状态时不需要线程,不会执行任何动作。但在启动/关闭时需要一个线程。子类可以实现startUp()和shutDown()方法,每个方法在一个拥有单个线程的executor中执行。
(3)AbstractExecutionThreadService:单线程服务。子类可以实现startUp(),run()和shutDown()操作。内部使用单个线程来执行整个操作。另外还可以重载triggerShutdown()方法让run()方法结束。如果想手动管理线程,考虑使用AbstractService。
(4)AbstractScheduledService:周期性服务。在RUNNING状态会周期性地执行一个任务。子类可以实现的生命周期方法包括startUp(),shutDown()和周期性的任务runOneIteration()方法。为了能够描述执行周期,你需要实现scheduler()方法。通常情况下你可以使用AbstractScheduledService.Scheduler类提供的两种调度器newFixedRateSchedule()和newFixedDelaySchedule()。内部使用executor()返回的ScheduledExecutorService来串行执行这些方法。stopAsync()取消定期任务但不会中断它,会一直等待直到运行shutDown()方法。
(5)AbstractService:通用性的服务。没有指定任何执行线程,因此子类必须自己定义线程管理,还要实现doStart()和doStop()。首次调用startAsync()时会同时调用doStart(),doStart()内部需要处理所有的初始化工作、如果启动成功则调用notifyStarted()方法;启动失败则调用notifyFailed()。首次调用stopAsync()会同时调用doStop(),doStop()要做的事情就是停止服务,如果停止成功则调用 notifyStopped()方法;停止失败则调用notifyFailed()方法。doStart和doStop方法的实现需要考虑下性能,尽可能的低延迟。如果初始化的开销较大,如读文件,打开网络连接,或者其他任何可能引起阻塞的操作,建议移到另外一个单独的线程去处理。
(6)ServiceManager:服务管理器。用于管理和监控一组服务。包括startAsync(), stopAsync(), addListener(), awaitHealthy(), awaitStopped(), isHealthy(), startupTimes()等。我们建议整个服务的生命周期都能通过ServiceManager来管理,不过即使状态转换是通过其他机制触发的、也不影响ServiceManager方法的正确执行。例如当一个服务不是通过startAsync()、而是其他机制启动时,listeners仍然可以被正常调用、awaitHealthy()也能够正常工作。ServiceManager 唯一强制的要求是当其被创建时所有的服务必须处于New状态。
(7)应用场景:微服务架构中,用于实现各种服务。由于服务在自己单独的线程中执行,各个服务并发执行互不干扰,可以很好地实现服务故障隔离。

15. EventBus
(1)EventBus:事件总线,用于实现发布订阅模式(观察者模式)。JDK传统上的Observable/Observer接口有很多局限,它是通过发布者和订阅者之间的显式注册实现的。EventBus就是为了取代这种显示注册方式,使组件间有更好的解耦。EventBus不是通用型的发布-订阅实现,不适用于进程间通信。EventBus就开放了三个方法,register/unregister/post,用于注册或注销事件监听者,发布事件消息。
1)事件订阅者:事件监听器,可以是任意的类,不用再继承特定的接口,只需要在订阅方法上加上@Subscribe注解即可,方法的唯一参数为事件对象,以便接收其中的事件消息。而传统实现则需要定义具体的事件监听者类并实现接口,如CustomerChangeEventListener。
2)事件定义:任意的对象。
3)事件监听器的注册:EventBus在实例上调用register(Object)来注册监听器,总线维护一个事件和事件处理器的关联关系,在内存中。而传统实现是调用事件生产者的注册方法,这些方法很少定义在公共接口中,因此开发者必须知道所有事件生产者的类型,才能正确地注册监听者。
4)事件发布:使用EventBus.post(Object)来发布事件消息,订阅者接收消息的顺序与它们被注册时的顺序一致。异步分发可以直接用EventBus的子类AsyncEventBus。传统的实现则需要开发者自己写代码,包括事件类型匹配、异常处理、异步分发。
5)事件监听处理过程:EventBus自动把事件分发给所有@Subscribe注解的监听器。具体的内部实现通常有同步处理和异步处理方式,事件提交之后事件队列维护在本地缓存中,同步的方式直接当前线程去执行,异步的处理策略是在初始化事件总线的时候就搞了一个线程池出来,由线程池去异步执行。这个EventBus内部已经做好了,而传统实现很困难,需要开发者自己去实现处理逻辑。
6)检测没有监听者的事件:EventBus监听DeadEvent。它会把所有发布后无监听者处理的事件包装为DeadEvent(对调试很便利)。而传统实现则需要在每个事件分发方法中添加逻辑代码(也可能要用AOP)。
(2)EventBus是对传统java Observable/Observer的替代。实际上从java 1.9开始Observable/Observer已经被标为@Deprecated了,它们没有指定通知的顺序,状态改变也不是与通知者一对一的。对更丰富的事件模型可使用java.beans包。对线程间可靠的顺序消息发送可以使用java并发包中的数据结构。对响应流风格的编程,可以使用并发包中的Flow API。
**(3)应用场景:进程内事件发布和订阅,更新通知。将发布者和订阅者解耦。**你可以在应用程序中按照不同的组件、上下文或业务主题分别使用不同的事件总线。这样在测试时开启和关闭某个部分的事件总线,也会变得更简单,影响范围更小。当然如果你想在进程范围内使用唯一的事件总线,你也可以自己这么做。比如在容器中声明EventBus为全局单例,或者用一个静态字段存放EventBus。

Apache Curator数据结构

应用领域:广泛应用于需要保持数据一致性的分布式环境中,如进程协调、跨进程同步、服务注册发现、配置更新等。

1. CuratorFrameworkFactory, CuratorFramework, CuratorTempFramework
(1)CuratorFrameworkFactory:通过指定连接字符串和重试策略,newClient()用于获取一个CuratorFramework实例,表示一个ZK客户端连接。与ZK自带客户端相比它提供重试机制、自动连接管理、zk客户端实例管理、以及各种使用场景支持。
(2)CuratorFramework:ZK客户端连接,与ZK服务端交互的核心。你必须调用它的start()启动,调用close()方法关闭。它用来向指定ZK路径存取数据、对指定ZK路径及数据的CRUD等。使用流式接口:
create(): 创建节点操作,可以用withMode()指定节点类型是临时节点还持久节点,缺省为持久节点。在最后调用forPath()指定要操作的ZNode。
delete(): 删除节点操作。
checkExists(): 检查节点是否存在。
getData()/setData(): 获取/设置节点数据。
getChildren(): 获取子节点列表
getACL()/setACL(): 获取/设置节点的ACL设置
getConfig()/reconfig(): 获取/修改节点的配置信息
transaction(): 开始是原子ZooKeeper事务。可以复合create, setData, check, delete等操作然后调用commit()作为一个原子操作提交。
addListener(): 注册CuratorListener以监控节点改变事件,实现eventReceived()来处理接收到的事件消息。
inBackground(): 操作放在后台异步地执行。
(3)CuratorTempFramework:临时的CuratorFramework,一定时间不活动后连接会被关闭。创建builder时不是调用build()而是调用buildTemp()。3分钟不活动连接就被关闭,你也可以指定不活动的时间。
**(4)应用场景:临时节点驻存在ZK中,当连接和session断掉时会被自动删除。这种特性广泛应用于服务或机器的注册和发现、配置更新等。**比如通过ZK发布服务,服务启动时将自己的信息注册为临时节点,当服务断掉时ZK将此临时节点删除,这样client就不会得到服务的信息了。

2. LeaderLatch, LeaderSelector
(1)领导选举:在分布式计算中,leader election是很重要的一个功能,这个选举过程是这样子的,指派一个进程作为组织者,将任务分发给各节点。在任务开始前,哪个节点都不知道谁是leader或者coordinator. 当选举算法开始执行后, 每个节点最终会得到一个唯一的节点作为任务leader。除此之外, 选举还经常会发生在leader意外宕机的情况下,新的leader要被选举出来。Curator有两种选举recipe,你可以根据你的需求选择合适的。
(2)LeaderLatch:领导锁存器。一旦启动,LeaderLatch会和其它使用相同latch path的其它LeaderLatch交涉,然后随机的选择其中一个作为leader,hasLeadership()可以随时查看一个给定的实例是否是leader。类似JDK的CountDownLatch,LeaderLatch在请求成为leadership时使用阻塞方法await(),一旦不使用LeaderLatch了,必须调用close方法。如果它是leader,会释放leadership,其它的参与者将会选举一个leader。LeaderLatch实例可以添加ConnectionStateListener来监听网络连接问题。当SUSPENDED或LOST时,leader不再认为自己还是leader。当LOST连接重连后RECONNECTED,LeaderLatch会删除先前的ZNode然后重新创建一个。LeaderLatch用户必须考虑导致leadership丢失的连接问题。强烈推荐你使用ConnectionStateListener。
(3)LeaderSelector:领导选举器。一旦启动,当实例取得领导权时你的listener的takeLeadership()方法被调用。而takeLeadership()方法只有领导权被释放时才返回。当你不再使用LeaderSelector实例时,应该调用它的close方法。LeaderSelectorListener类继承ConnectionStateListener。LeaderSelector必须小心连接状态的改变,如果实例成为leader, 它应该相应SUSPENDED或LOST。当SUSPENDED状态出现时,实例必须假定在重新连接成功之前它可能不再是leader了。如果LOST状态出现,实例不再是leader,takeLeadership方法返回。推荐处理方式是当收到SUSPENDED或LOST时抛出CancelLeadershipException异常,这会导致LeaderSelector实例中断并取消执行takeLeadership方法的异常。这非常重要,你必须考虑扩展LeaderSelectorListenerAdapter,它提供了推荐的处理逻辑。
(4)应用场景:分布式环境中多进程下的领导选举。

3. Lock
互斥锁:InterProcessMutex, InterProcessSemaphoreMutex
读写锁:InterProcessReadWriteLock
信号量:InterProcessSemaphoreV2
多锁对象:InterProcessMultiLock
(1)InterProcessMutex:分布式可重入锁。跨越多个JVM进程,持有同一ZK锁路径的所有进程将得到一个跨进程的临界区。锁是公平的,多个用户按请求顺序得到锁。锁是可重入的,同一个客户端在拥有锁的同时,可以多次acquire()获取,不会被阻塞。通过acquire()获得锁,并提供超时机制。通过release()方法释放锁。支持可协商的撤销机制,makeRevocable()将锁设为可撤销的,当别的进程或线程想让你释放锁时Listener会被调用。attemptRevoke()请求撤销当前的锁。强烈推荐你使用ConnectionStateListener处理连接状态的改变,当连接LOST时你不再拥有锁。一般不应该在多个线程中用同一个InterProcessMutex,你可以在每个线程中都生成一个InterProcessMutex实例,它们的path都一样,这样它们可以共享同一个锁。
(2)InterProcessSemaphoreMutex:分布式不可重入锁。跨越多个JVM进程,持有同一ZK锁路径的所有进程将得到一个跨进程的临界区。使用方法和InterProcessMutex类似。再次acquire()会阻塞,也就是此锁不是可重入的。
(3)InterProcessReadWriteLock:分布式可重入的读写锁,并且是公平锁。一个读写锁管理一对相关的InterProcessMutex锁。一个负责读操作,另外一个负责写操作。读操作在写锁没被使用时可同时由多个进程使用,而写锁使用时不允许读(阻塞)。此锁是可重入的。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。这也意味着写锁可以降级成读锁,比如请求写锁 —>读锁 ---->释放写锁。 从读锁升级成写锁是不成的。readLock()返回读锁,writeLock()返回写锁。
(4)InterProcessSemaphoreV2:分布式计数信号量,跨越多个JVM进程。一个信号量维护一组租约许可Lease,持有同一ZK锁路径的所有进程将得到一个跨进程的有限租约数集合。信号量是公平的,多个用户按请求顺序得到信号量。有两种方式可以决定semaphore的最大租约数。第一种方式是由用户给定的path决定,第二种方式使用SharedCountReader类。如果不使用SharedCountReader,没有内部代码检查进程是否假定有10个租约而进程B假定有20个租约。因此要确保所有的实例必须使用相同的numberOfLeases值。调用acquire()会返回一个租约对象。客户端必须在finally中close这些租约对象,否则这些租约会丢失掉。但是如果客户端session由于某种原因crash丢掉,那么这些客户端持有的租约会自动close,这样其它客户端可以继续使用这些租约。租约还可以通过returnAll(), returnLease()返回。你一次可以请求多个租约,如果Semaphore当前的租约不够,则请求线程会被阻塞,同时还提供了超时机制。
(5)InterProcessMultiLock:分布式锁容器,维护多个锁。构造函数需要包含的锁的集合,或者一组ZooKeeper的path。当调用acquire,所有的锁都会被acquire,如果请求失败,所有的锁都会被release。 同样调用release时所有的锁都被release(失败被忽略)。基本上它就是组锁的代表,在它上面的请求释放操作都会传递给它包含的所有的锁。
(6)应用场景:分布式环境中的跨进程同步、跨进程临界区的访问,以保持数据一致性。

4. Barrier
DistributedBarrier, DistributedDoubleBarrier
(1)DistributedBarrier:分布式栅栏。分布式系统使用栅栏来阻塞所有节点上的等待进程,直到某一个条件被满足,然后所有的节点放行继续执行。比如赛马比赛中,等赛马陆续来到起跑线前,一声令下,所有的赛马都飞奔而出。一个进程用setBarrier()设置栅栏,它将阻塞在它上面等待的进程。其他进程用waitOnBarrier()在栅栏上等待放行条件,注意DistributedBarrier会监控连接状态,当连接断掉时waitOnBarrier()方法会抛出异常。当进程用removeBarrier()移除栅栏时,所有在栅栏上等待的进程将放行继续执行。
(2)DistributedDoubleBarrier:分布式双栅栏。允许客户端对一个计算的开始和结束进行同步。当足够的进程加入到双栅栏时,进程开始计算,当计算完成时,离开栅栏。构造时需要指定成员数量,当enter方法被调用时,成员被阻塞,直到所有的成员都调用了enter。当leave方法被调用时,它也阻塞调用线程,直到所有的成员都调用了leave。就像百米赛跑比赛,发令枪响,所有的运动员开始跑,等所有的运动员跑过终点线,比赛才结束。
(3)应用场景:分布式环境中多进程执行任务时的顺序控制。

5. Counter
SharedCount, DistributedAtomicInteger, DistributedAtomicLong
(1)SharedCount:分布式的共享Integer计数器。监听同一路径的所有客户端将拥有最新的计数器值。可以为它增加一个SharedCountListener,当计数器改变时此Listener可以监听到改变的事件,而SharedCountReader可以读取到最新的值,包括字面值和带版本信息的值VersionedValue。getCount()返回当前的共享计数器值,setCount()强制更新计数器值而不管它之前的状态。trySetCount()尝试更新计数器值,第一个参数指定版本,如果期间其它client更新了此版本的计数器值,则这次更新不会成功,并且这个client getCount()看到的是最新值。
(2)DistributedAtomicInteger,DistributedAtomicLong:分布式的整数计数器。除了计数的范围比SharedCount大了之外,它首先尝试使用乐观锁的方式设置计数器,如果不成功(比如期间计数器已经被其它client更新了),它使用InterProcessMutex方式来更新计数值。主要操作:
get(): 获取当前值
increment(): 加一
decrement(): 减一
add(): 增加特定的值
subtract(): 减去特定的值
trySet(): 尝试设置计数值
forceSet(): 强制设置计数值
你必须检查返回结果的succeeded(),它代表此操作是否成功。如果操作成功,preValue()代表操作前的值,postValue()代表操作后的值。
(3)应用场景:分布环境中多进程对同一个整数的存取和修改。

6. Cache
PathChildrenCache, NodeCache, TreeCache
(1)PathChildrenCache:分布式的ZK路径缓存。数据被缓存到一个ZK路径及其所有子路径节点中。它会监控整个ZK路径,响应子节点创建/更新/删除事件,然后更新状态以包含当前最新子节点、子节点的数据和状态。想使用cache,必须调用它的start方法,不用之后调用close方法。addListener()添加listener监听缓存的改变,getCurrentData()返回一个List对象,包含所有子节点路径及其数据。注意所有缓存实现类只提供查询缓存数据、监听数据改变并获取更新通知的操作。缓存数据的保存、更新和清除则是使用相应的client(CuratorFramework)来进行。
(2)NodeCache:分布式的ZK节点缓存。数据被缓存到单个ZK节点下。它会监控单个ZNode,响应该节点的创建/更新/删除事件。当节点的数据修改或者节点被删除时,它会更新状态以包含最新数据修改。addListener()添加listener监听缓存的改变,getCurrentData()得到节点的数据和状态。
(3)TreeCache:分布式的ZK树缓存。它即可以监控节点的状态,还监控节点的子节点的状态,类似上面两种cache的组合,这也就是Tree的概念。它监控整个树中节点的状态。getCurrentChildren()返回cache的状态,类型为Map<String, ChildData>。而getCurrentData()返回监控path的数据。addListener()添加listener来监控状态的改变。
**(4)应用场景:适用于分布式环境中多进程的小规模数据缓存,并需要及时获取数据更新通知时。**注意ZK并不适合用来存储海量的缓存数据(这种场景可以选用Redis或Memcached),这些缓存实现也没有提供具体的缓存过期策略,它们比较适用于当别的进程更新数据时,需要及时自动地获取数据更新通知的场合。

7. Node
PersistentNode, PersistentTtlNode, GroupMember
(1)PersistentNode:常驻ZK的持久节点,即使在连接或session断掉后。用start()启动,close()关闭,这将删除节点。getData()/setData()获取或设置节点数据,addListener()添加节点改变的监听器。
(2)PersistentTtlNode:TTL型的持久节点,即有存活期限制的持久节点。构造时的ttlMs参数指定存活跳数,start()启动,close()不会立即删除节点,当达到TTL跳数时才自动删除节点。比较适用于当你想创建一个TTL节点,但又不想通过定期更新数据来保持节点存活的场景。
(3)GroupMember:组成员管理。维护一组ZK节点缓存成员,表示它们属于同一个组。每个节点有一个唯一的成员ID。start()启动当前组成员资格,close()移除当前节点成员。getCurrentMembers()获取组成员列表,包括每个节点的成员ID和数据。idFromPath()获取指定节点的成员ID,newPersistentEphemeralNode()添加一个节点,newPathChildrenCache()添加一个路径缓存。
(5)应用场景:分布式环境中,多进程的持续通信,多进程的分组管理

8. Queue
DistributedQueue, DistributedIdQueue, DistributedPriorityQueue, DistributedDelayQueue, SimpleDistributedQueue
(1)Curator的分布式队列实现,利用ZK的PERSISTENT_SEQUENTIAL节点,可以保证放入到队列中的项目是按照顺序排队的。如果单一的消费者从队列中取数据,那么它是先入先出的,这也是队列的特点。如果你严格要求顺序,你就的使用单一的消费者,可以使用leader选举只让leader作为唯一的消费者。注意ZK并不适合做大规模的消息队列(这种场景可以选用Kafka)。ZK有1MB的传输限制。实践中ZNode必须相对较小,而队列包含成千上万的消息,非常的大。如果有很多节点,ZK启动时相当的慢。而使用queue会导致好多ZNode。你需要显著增大initLimit和syncLimit。ZNode很大的时候很难清理。Netflix不得不创建了一个专门的程序做这事。当很大量的包含成千上万的子节点的ZNode时,ZK的性能变得不好。ZK的数据库完全放在内存中。大量的Queue意味着会占用很多的内存空间。
(1)DistributedQueue:分布式队列。队列中的数据有顺序保证。用QueueBuilder.buildQueue()来创建,构造参数QueueConsumer是消费者,它可以接收队列的数据。消费队列数据的代码可以放在QueueConsumer.consumeMessage()中。QueueSerializer提供了对队列中的对象的序列化和反序列化。正常情况下先将消息从队列中移除,再交给消费者消费。但这是两个步骤不是原子的。可以调用Builder的lockPath()消费者加锁,当消费者消费数据时持有锁,这样其它消费者不能消费此消息。如果消费失败或者进程死掉,消息可以交给其它进程。这会带来一点性能的损失。最好还是单消费者模式使用队列。put()消息入队。
(2)DistributedIdQueue:带消息ID的分布式队列。和上面的队列类似,但是可以为队列中的每一个元素设置一个ID。可以通过ID把队列中任意的元素移除。用QueueBuilder.buildIdQueue()来创建。put()元素入队,remove()移除指定ID的元素。
(3)DistributedPriorityQueue:分布式优先级队列。队列中的元素按照优先级进行排序。Priority越小,元素月靠前,越先被消费掉。通过builder.buildPriorityQueue()方法创建。当优先级队列得到元素增删消息时,它会暂停处理当前的元素队列,然后刷新队列。minItemsBeforeRefresh指定刷新前当前活动的队列的最小数量。主要设置你的程序可以容忍的不排序的最小值。put()放入队列时需要指定优先级。
(4)DistributedDelayQueue:分布式延迟队列。元素有个delay值,消费者隔一段时间才能收到元素。通过builder.buildDelayQueue()方法创建。put()元素入队时要指定delay值。
(5)SimpleDistributedQueue:一个分布式的阻塞式的队列,可替换DistributedQueue使用。实现了和JDK一致性的接口(但是没有实现Queue接口)。offer(), take(), peek(), poll(), remove(), element()。
(6)应用场景:多进程下的小规模消息队列通信,只支持非持久的消息队列。

9. AsyncCuratorFramework
AsyncCuratorFramework, ModeledFramework, CachedModeledFramework, VersionedModeledFramework, TypedModeledFramework2
(1)AsyncCuratorFramework:一个DSL,对已有CuratorFramework实例的做异步封装,用于对java 8的支持。用wrap()方法封装后的client支持异步操作和java 8的lambda表达式。
(2)ModeledFramework:强类型的DSL,可以将Curator风格的clent映射到一个ZK路径(支持参数化的替换)、一个数据序列化器(数据将存储到ZK路径下)、节点创建删除的一个特定选项、节点的ACL等。CachedModeledFramework, VersionedModeledFramework等都是具体的DSL实现,要用到java 8的lambda表达式。

10. ServiceDiscovery
ServiceInstance, ServiceProvider, ServiceDiscovery, ServiceCache
RandomStrategy, RoundRobinStrategy, StickyStrategy
(1)ServiceInstance:表示一个服务实例的POJO。它包含一个服务的描述信息,使用builder来创建。一个服务实例包括名称、唯一ID、地址、端口或ssl端口、注册时间、服务类型(动态、静态、动态顺序型、持久型服务等)、Uri格式、可选的载荷payload。ServiceInstance序列化并存储在Zookeeper中。
(2)ServiceProvider:服务提供者,提供服务发现的主要API。它是对给定命名服务的具体发现策略进行封装。发现策略是从给定服务的一组服务实例中选择一个实例的具体方式。有3种策略,轮询选择、随机选择、粘住(总是选择相同的一个)。通过ServiceProviderBuilder.build()来创建提供者,创建时可指定服务名称、发现策略(缺省为轮询)、出错处理策略等。start()/close()启动或关闭当前提供者,getAllInstances()获取所有服务实例,getInstance()获取单个服务实例,noteError()在服务实例出错时(如I/O错误)发送一个错误记录,该服务实例将被确定为Down实例,根据down策略来关闭该服务实例。
(3)ServiceDiscovery:使用ZK实现的更细粒度的服务注册和发现机制。通过ServiceDiscoveryBuilder.build()来创建,创建时指定ZK路径、关联的服务描述实例、序列化器(缺省为JsonInstanceSerializer)。启动时它会向ZK自动注册该服务实例。一般各个服务发布者用ServiceDiscovery向ZK中心注册自己服务实例信息,服务消费者则用ServiceDiscovery从ZK中心查找指定的服务实例并进行调用。
start()/close(): 启动或注销当前服务发现者。它会在ZK上自动注册或注销关联的服务实例。
registerService()/unregisterService()/updateService():手动注册、删除、更新一个服务实例。
queryForInstance():查询一个服务实例
queryForInstances():查询给定服务的所有服务实例
queryForNames():返回所有服务的名称列表
serviceProviderBuilder(): 创建具体的ServiceProvider,从而可以用指定的发现策略自动地查找服务实例
serviceCacheBuilder():创建服务缓存
(4)ServiceCache:服务缓存。使用ServiceCacheBuilder.build()来创建。如果你需要经常查询的服务可以使用ServiceCache。它在内存中缓存给定服务的实例列表。ServiceCache对象开始必须调用start()方法。getInstances()返回当前已知的服务实例列表。ServiceCache支持实例更新时的通知,需要用addListener()增加ServiceCacheListener监听器。
**(5)应用场景:微服务架构的环境中实现基于ZK的服务注册和发现中心。**部署在分布式环境中的每个自定义服务发布类都关联一个服务描述信息ServiceInstance,和一个注册器ServiceDiscovery。并指定统一的ZK路径和连接client。服务启动方法中调用serviceDiscovery.start()主动向ZK注册,服务关闭方法中调用serviceDiscovery.close()从ZK注销该服务。当服务关闭或服务的ZK连接断开时会自动删除ZK上注册的服务信息。服务消费者则通过ServiceDiscovery连接到ZK中心,查找指定的服务实例,然后调用该服务进行消费。

Dropwizard Metrics数据结构

应用领域:系统监控指标度量、统计

1. MetricRegistry, SharedMetricRegistries
(1)MetricRegistry:统计度量指标实例的注册表,是Metrics的核心,它是存放应用中所有metrics的容器,也是我们使用Metrics库的起点。内部使用ConcurrentMap来存储。每一个metric都有它独一无二的名字,Metrics中使用句点名字,如com.example.Queue.size。当你在com.example.Queue下有两个metric实例,可以指定地更具体com.example.Queue.requests.size和com.example.Queue.response.size。使用MetricRegistry类的静态,可以非常方便地生成名字。
(2)SharedMetricRegistries:用于保存多个度量注册表的map。如果应用中有多个MetricRegistry,想让一个MetricRegistry用在多个地方,可以对它命名并存放到全局的SharedMetricRegistries中,以便能够共用。

2. Metric
Gauge, RatioGauge, CachedGauge, DerivativeGauge
Counter, Histogram, Meter, Timer
(1)Gauge:最简单的度量类型,只返回一个值getValue(),用于记录某个瞬时值。比如某一时刻CPU核心线程数、OS文件句柄数、队列大小等。RatioGauge, CachedGauge, DerivativeGauge是三个特殊的Gauge实现。RatioGauge记录两个值的比值。CachedGauge会让值缓存一段时间,构造时指定缓存时长,getValue()返回缓存的值。这样可以避免频繁计算,提高性能。DerivativeGauge记录的值派生自另一个Gauge。
(3)Counter:计数器,一个可以增加inc()或减少dec()的AtomicLong整型值,用于统计频率值。例如统计服务的访问次数、Api调用量、特定事件发生次数。
(4)Histogram:直方图,统计一个数据流中的数据分布情况,比如最小值,最大值,中间值,还有中位数,75百分位, 90百分位, 95百分位, 98百分位, 99百分位, 和 99.9百分位的值(percentiles)。传统上统计这些值需要拿到整个数据集,排序,然后获取相关的统计值。对小的数据集可以这样做,但对海量数据集这不合适,比如统计Api的访问时间分布,一直有访问进来。Metrics库的解决方式是使用蓄水池采样(reservoir sampling)技术,用一个数据池类型Reservoir来保存数据,在需要统计时取出一个快照Snapshot,然后获取相关统计值。为了保证数据池不会过大,并且能高性能的写入数据,Metrics库定义了几种核心的数据池类型:
UniformReservoir:提供直方图完整的生命周期内的有效的中位数,它会返回一个中位值。它使用了一种叫做Vitter’s R的算法,随机选择了一些线性递增的样本。内部默认保存1028条记录,每次进行update操作的时候,首先会依次地将值填入1028条记录中,当记录满了之后,就会使用随机替换0-1027中的一条。因为是随机替换,所以也不需要进行加锁和解锁。当你需要长期的测量,请使用这种数据池。在你想要知道流数据的分布中是否最近变化的话,那么不要使用这种。
SlidingWindowReservoir:固定大小的数据池,从0到n-1填入数据,不断循环。也不会进行加锁和解锁。
SlidingTimeWindowArrayReservoir:非固定大小的数据池,但是只会存储过去N秒的数据。使用数组进行存储。
ExponentiallyDecayingReservoir:固定大小的数据池。提供代表最近5分钟数据的分位数,它使用了一种forward-decayingpriority sample的算法,这个算法通过对最新的数据进行指数加权,不同于Uniform算法,它体现的是最新的数据,可以让你快速的知道最新的数据分布发生了什么变化。它首先会逐个数据填满数据池,随后会将老的数据替换为新的数据,内部使用ConcurrentSkipListMap进行存储。可以说是SlidingWindowReservoir与SlidingTimeWindowReservoir的结合。Timer中使用了这种直方图。
(5)Meter:测量仪,度量一系列事件发生的速率,例如TPS, QPS,每秒请求速率。它会统计整个时间内的平均吞吐率、和1分钟,5分钟,15分钟内的指数加权移动平均(EWMA)吞吐率。
(6)Timer:时间序列统计仪,统计数据分布情况和吞吐率,实际上是Histogram和Meter功能的结合。
(7)应用场景:广泛用于各种系统监控度量和统计的场景,如Netflix, Kafka, Storm,Spring Cloud等。

3. Reporter
ConsoleReporter, CsvReporter, Slf4jReporter, ScheduledReporter
(1)Reporter:用于输出metrics获取到的统计数据。如控制台、文件等。
(2)ConsoleReporter, CsvReporter, Slf4jReporter:分别输出到控制台、Csv文件、Slf4j日志等,它们都实现了ScheduledReporter抽象类,因此可以定期不停地输出统计数据。
(3)其他的Reporter:JmxReporter将指标注册成为JMX MBean,通过JMX来展示。GanliaReporter将度量指标以流式的方式返回给Ganglia服务器。GraphiteReporter 将度量指标以流式的方式返回给Graphite服务器。其他的还有kafka, cassandra, elasticsearch, hadoop, influxdb, mongodb, spark, zabbix等系统的reporter。

4. HealthCheck
HealthCheck, HealthCheckRegistry
(1)HealthCheck:健康检测器,用来检测某个系统是否健康,如数据库连接、网络连接、应用进程健康检测等。
(2)HealthCheck是抽象类,需要创建自己的具体类,如DatabaseHealthCheck,AppHealthCheck,覆盖check()方法实现自己检测逻辑,然后把所有检测器注册到HealthCheckRegistry,每个检测器有一个唯一名称。运行HealthCheckRegistry的runHealthChecks(),将得到所有的健康检测结果Map<String, HealthCheck.Result>。

5. Metric JVM
BufferPoolMetricSet, MemoryUsageGaugeSet
JvmAttributeGaugeSet, ClassLoadingGaugeSet, GarbageCollectorMetricSet
ThreadStatesGaugeSet, FileDescriptorRatioGauge
ThreadDeadlockDetector, ThreadDump
(1)BufferPoolMetricSet:JVM直接缓存池和映射缓存池的使用统计,包括缓存个数 、已使用量、总容量度量集合。都是使用JMX BufferPoolMXBean获取的。metric指标列表:
direct.count: 直接缓存池中的缓存个数
direct.used: 直接缓存池中已使用的内存字节数
direct.capacity: 直接缓存池总内存字节数
mapped.count: 映射缓存池中的缓存个数
mapped.used: 映射缓存池已使用的内存字节数
mapped.capacity: 映射缓存池总内存字节数
(2)MemoryUsageGaugeSet:JVM内存使用情况统计。包括堆内、堆外和GC相关的所有内存池使用统计。都是使用JMX MemoryMXBean, MemoryPoolMXBean获取的。指标列表:
total.init: 初始的总内存字节数,包括堆内存和堆外内存
total.used: 已使用的总内存字节数,包括堆内存和堆外内存
total.committed: 保证可用的总内存字节数,包括堆内存和堆外内存
total.max: 最大可用的总内存字节数
heap.init: 初始的堆内存字节数
heap.used: 已使用的堆内存字节数
heap.committed: 保证可用的堆内存字节数
heap.max: 最大可用的堆内存字节数
heap.usage: 堆内存的使用百分比,是已使用堆内存量与最大可用堆内存量的比值
non-heap.init: 初始的堆外内存字节数
non-heap.used: 已使用的堆外内存字节数
non-heap.committed: 保证可用的堆外内存字节数
non-heap.max: 最大可用的堆外内存字节数
non-heap.usage: 堆外内存使用百分比
<poolName>.init: 各个内存池的使用情况统计
<poolName>.used:
<poolName>.committed:
<poolName>.max:
<poolName>.usage:
<poolName>.used-after-gc:
(3)JvmAttributeGaugeSet:JVM名称name, 厂商信息vendor, 运行时间毫秒数uptime统计。使用JMX RuntimeMXBean获取。
(4)ClassLoadingGaugeSet:JVM类加载情况统计。包括已加载的类个数loaded, 未加载的类个数unloaded。使用JMX ClassLoadingMXBean获取。
(5)GarbageCollectorMetricSet:JVM垃圾回收情况统计。包括各个GC实例的已收集次数<gc_name>.count,收集耗间毫秒数<gc_name>.time。使用JMX GarbageCollectorMXBean获取。
(6)ThreadStatesGaugeSet:JVM各种状态的线程数统计。使用JMX ThreadMXBean获取。指标列表:
count: 总线程数
daemon.count: 守护线程数
deadlock.count: 死锁线程数
deadlocks: 死锁的线程列表
new.count: 新建状态线程数
runnable.count: 运行状态的线程数
blocked.count: 阻塞状态的线程数
waiting.count: 等待状态的线程数
timed_waiting.count: 有等待时间限制的线程数
terminated.count: 终止状态的线程数
(7)FileDescriptorRatioGauge:文件描述符使用情况统计。返回打开的文件描述符数量占系统最大文件描述符数量的百分比。一般在Unix类的操作系统中使用。使用JMX OperatingSystemMXBean获取。
(8)ThreadDeadlockDetector, ThreadDump:两个有用的工具类,前者用于检测死锁的线程,后者用于dump出jvm的线程信息列表
(9)应用场景:用于开发JVM监控系统。在大型的监控系统时,通常都包含对机器上的JVM进程进行监控。

6. Metric Log4j2/Logback
(1)InstrumentedAppender:跟踪Log4j/Logback的日志消息测量指标的appender。可以在应用启动时创建InstrumentedAppender,然后添加到root logger中。或者在配置文件中用<MetricsAppender>配置好appender并添加到root logger中,可以指定appender的名称和MetricRegistry名称。Meter指标有all, trace, debug, info, warn, error, fatal,表示相应级别日志消息每秒产生的速率。
(2)其他的Metrics扩展:包括Metrics的Ehcache, Apache HttpClient, JDBI, Jersey, Jetty, Servlet, Apache Camel, Aspectj, Guice, CDI, OkHttp, Feign, Play Framework, Spring等系统的扩展。

7. Metric Servlets
MetricsServlet, HealthCheckServlet, ThreadDumpServlet
PingServlet, CpuProfileServlet, AdminServlet
(1)MetricsServlet:响应GET请求,将给定MetricRegistry中的指标以application/json响应格式展现的Servlet。返回json对象,使用了jackson来序列化。MetricRegistry可以在创建Servlet时指定,也可以扩展MetricsServlet.ContextListener来注入。
(2)HealthCheckServlet:响应GET请求,运行指定HealthCheckReistry的所有健康检查,全部通过返回200,有失败返回500,如果没有注册任何health check,则返回501。返回结果为易读的text/plain类型。注册表可以在创建Servlet时指定,也可以扩展HealthCheckServlet.ContextListener来注入。
(3)ThreadDumpServlet:响应GET请求,返回JVM上的所有活动线程表示,包括线程堆栈、状态和锁待,text/plain格式。
(4)PingServlet:响应GET请求,直接返回200,返回数据为text/plain格式的"pong",表示该进程活着。例如在负载均衡器中用于确定目的地的存活性。
(5)CpuProfileServlet:处理GET请求,返回gperftools的pprof工具可解析的CPU性能分析数据。
(6)AdminServlet:聚合HealthCheckServlet,MetricsServlet,ThreadDumpServlet,PingServlet,CpuProfileServlet成单个易使用的Servlet。响应一组GET请求的URI来分别展现这些信息,包括/healthcheck, /metrics, /ping, /threads, /pprof。返回一个Html页面,页面中包含了这些uri链接。

发布了167 篇原创文章 · 获赞 159 · 访问量 195万+
展开阅读全文