KafkaProducer是多线程并发安全的,多线程环境下也不会导致数据错乱。

//将消息添加到内存缓冲里去,RecordAccumulator组件负责的
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);

首先要从内存缓冲区中找出这个partition对应的Deque队列,里面是这个分区的所有batch:

//从内存缓冲区中,获取一个分区对应的Deque队列,里面放了很多的batch
Deque<RecordBatch> dq = getOrCreateDeque(tp);

获取Deque队列,有就直接拿,没有就新建:

//如果有Deque就拿,没有就创建
        Deque<RecordBatch> d = this.batches.get(tp);
        if (d != null)
            return d;
        d = new ArrayDeque<>();

另外这个batches,是一个CopyOnWriteMap:

//1个分区对应1个Deque队列,每个Deque队列中有多个batch
private final ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches;

//本身就是线程安全的
this.batches = new CopyOnWriteMap<>();

CopyOnWrite:适用于读多写少的场景。每次先copy出1个副本出来,在副本里更新,更新整个副本。好处在于读写之间不会有长时间的锁互斥,写的时候更不会阻塞读。坏处就是对内存的占用很大。

CopyOnWriteMap的思路:一个分区创建一个Deque的行为,是低频率的写行为。主要还是读,大量的从Map里读一个分区对应的Deque。最后高并发频繁更新的就是分区对应的Deque

CopyOnWriteMap的做法:put时会先copy出来一个副本HashMap<K,V>,然后通过volatile的方式将副本HashMap写入Map中。

CopyOnWriteMap中用来存放数据的,就是一个非线程安全的Map。用volatile修饰,保证了内存可见性,如果更新了这个引用变量对应的实际的map对象的地址,其他人也能看见。

并发读的时候没有加锁,因为彼此之间不会相互影响。如果Deque存在,直接就返回了。

多线程并发调用putIfAbsent方法,方法内可保证线程安全:

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

这里可以保证同一时间,只有1个线程能执行这个方法。put的时候会有synchronized来保证线程安全的,保证同一时间只有一个线程来更新。

put写并不会阻塞get读,因为它是根据副本HashMap来进行kv设置,然后再将副本HashMap以volatile的方式更新。所以再去读,就会读到最新的值。

总之,就会拿到这个partition对应的Deque队列。

获取到Deque队列后,加锁,尝试将准备好的时间戳、key、value等放到Deque中去:

if (closed)
    throw new IllegalStateException("Cannot send after the producer is closed.");
RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);

此时有一个问题,如果此时没有创建batch,就会导致放入Deque失败。

然后就要基于Buffer pool,给batch分配一块内存出来:

private final BufferPool free;

ByteBuffer buffer = free.allocate(size, maxTimeToBlock);

之所以说是pool,是因为这个batch代表的内存空间是可复用的。免去了垃圾回收的烦恼。分配完内存后,已经可以往Deque里写入消息了。

接下来会将内存缓冲里的消息写入到batch中(封装成RecordBatch对象):

//将内存缓冲区里的消息,封装成MemoryRecords
MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
//将消息放到batch中去
RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());

然后将batch放到Deque中:

//把batch放到deque中去
dq.addLast(batch);

finally将目前还在执行append的线程数,递减:

//将当前还在执行append的线程数量,递减
appendsInProgress.decrementAndGet();