CopyOnWriteMap

    private final ConcurrentMap<TopicPartition, Deque<ProducerBatch>> batches;

前面解析RecordAccumulator提到了batches是用来存放每个TopicPartition对应的批次队列的,因为会在多线程环境下使用所以声明为ConcurrentMap,但是batches是一个读多写少的场景,所以kafka设计了CopyOnWriteMap这种数据结构通过CopyOnWrite这种模式,加锁写保证数据不会有并发问题,读的是不可变的HashMap来保证性能,但是COW模式会有短暂的数据延迟,kafka是怎么解决的呢?

    private Deque<ProducerBatch> getOrCreateDeque(TopicPartition tp) {
        Deque<ProducerBatch> d = this.batches.get(tp);
        if (d != null)
            return d;
        d = new ArrayDeque<>();
        Deque<ProducerBatch> previous = this.batches.putIfAbsent(tp, d);
        if (previous == null)
            return d;
        else
            return previous;
    }

COW会数据不一致问题的原因是因为COW只加写锁不加读锁,
先分析下既有读锁又有写锁的情况,读写互斥运行,即同一时间读和写只能执行一个,这样保证读的时候就是最新值。如果去掉读锁只加写锁呢,那么读的时候如果写在执行读的就不是最新值,但是batches的场景比较特殊,它只会插入一次,不会更新。所以TopicPartition存在的话读就是不需要加锁的,这样和COW的场景完全吻合,所以只需要先get,为空的时候再从无锁升级到写锁保证不会重复插入。

    @Override
    public synchronized V putIfAbsent(K k, V v) {
        if (!containsKey(k))
            return put(k, v);
        else
            return get(k);
    }

这个场景决定了写操作执行的机会很少,无论消息数有多少,加锁的次数只和TopicPartition数相关,而读又是HashMap无锁操作,这样既提升了性能又规避掉了数据一致性问题。

总结

kafka的CopyOnWriteMap给我们在日常工作设计并发数据结构提供了一个很好的思路,先分析场景,再根据场景的特征(比如读写频率),并且再利用一些合理的设计模式。到这里RecordAccumulator的相关组件就解析完了。下节打算分析kafka的网络设计。


hello123
5 声望1 粉丝