需求分析
Kafka 的生产端为了提升吞吐量会采取批量发送消息。 同时 Kafka 的服务端 定义了一个主题有多个分区,这样Kafka 的生产端的批量消息的发送就是根据主题分区来定义的,如下图:
大家可以看到生产端缓存中每一个主题分区对应一个 Deque, 每一个 Deque 对应一个主题分区。于是,Key 是主题分区的编号,而 Value 是要发送到某个主题分区的消息队列。同时 Kafka 中主题分区的变动率很低,也就是 Key的变化率很低,这样我们就自然的会想到一种处理读多写少的场景的 JAVA 集合类型: CopyOnWrite,这类集合在写的时候会另外开辟空间把所有数据都拷贝过去,这样的好处是不会影响集合的读操作。 但是,JAVA 集合类型并没有 CopyOnWriteMap, 这就需要我们自己进行开发。
功能设计
在 Kafak 源码 RecordAccumulator 类中会使用一个CopyOnWriteMap<K,V>
的集合。
CopyOnWriteMap<K,V>
是Kafka内部自己定义的读写分离的Map集合,并不是Java包中的集合类。在RecordAccumulator源码中我们可以看到,使用CopyOnWriteMap<K,V>
是batches,key是分区,value是向这个分区发送的Deque<ByteBuffer>
队列集合。某个生产者生产消息时,要发送的分区是很少变动的,于是put的操作就会很少。而生产者会不断地往某个分区生产消息,其实过程就是先get某个分区对应的队列然后再把ProducerBuffer放入队列尾部,所以get的操作是很频繁的,这样就是一个典型的读多写少
的场景。但是,Java包里却没有给提供一个读写分离的Map集合。于是Kafka内部自己实现了一个基于CopyOnWrite读写分离的Map集合。
所谓CopyOnWrite就是当写的时候不在当前的集合里操作,而是拷贝出一份出来做写的操作,写完了再替换原来的集合。
好,我们来学习以下CopyOnWriteMap的源码。
源码
重要字段
private volatile Map<K, V> map;
就一个重要的字段map,修饰map的是volatile,目的是在多线程的场景下,当map发生变化的时候其他的线程都是可见的。
重要方法
get()
方法,代码很简单,就一行,源码在下面:
public V get(Object k) {
return map.get(k);
}
并没有加锁,所以并发性很好,速度会很快,但多线程场景下不会出现并发问题。因为读之间是不会发生并发问题的,而写操作是在另一个集合做的,不会影响读。
put()
方法,下面是写操作的代码:
public synchronized V put(K k, V v) {
Map<K, V> copy = new HashMap<K, V>(this.map);
V prev = copy.put(k, v);
this.map = Collections.unmodifiableMap(copy);
return prev;
}
首先重新创建一个HashMap集合,再把要写的数据写到集合里。最后,把新的集合设置成不可修改的map赋给字段map。由此可见,写完全是在新的集合里完成的,写结束后把新的集合赋给字段map。这样,就实现了读写分离,提高了并发性的同时不会造成并发问题。