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();