实现平台间的数据联网,利用kafka传递消息,考虑到平台内可能有多个项目会进行数据推送,为了各项目间推送的消息进行数据处理不会受到彼此间的阻塞影响,同时保证消息的消费速度,因此需要各项目间独立异步批量的处理数据。
本地队列处理
将从kafka监听到的消息放入本地队列中,保证每个项目拥有自己的队列,细分还可以保证每个项目每个数据类型的消息拥有自己的队列,然后让对应的线程去取并异步处理该队列中的数据。
kafka消息监听:
//从kafka中获取消息
@KafkaListener(topics = {"#{'${kafka.consumer.topic}'.split(',')}"},containerFactory = "dataKafkaListenerContainerFactory")
public void handleData(List<String> messages){
for(String message:messages){
KafkaMessage kafkaMessage = JSONObject.parseObject(message, KafkaMessage.class);
if(Objects.isNull(kafkaMessage)){
logger.info("kafka handler处理消息不存在"+message);
continue;
}
}
根据本地缓存的项目信息创建不同的队列,并将消息放入对应的线程中进行处理:
//从本地缓存中获取该项目队列
BlockingQueue<KafkaMessage> dataQueue = DATA_MESSAGE_MAP.get(kafkaMessage.getProjectId());
//缓存中没有则创建新的队列启动新的线程
if(dataQueue==null){
dataQueue = new LinkedBlockingQueue<>();
DATA_MESSAGE_MAP.put(kafkaMessage.getProjectId(),dataQueue);
MessageHander messageHander = (MessageHander)AppContext.getBean("messageHander");
messageHander.createDataBlockingQueue(dataQueue);
new Thread(messageHander,"data-hander-"+kafkaMessage.getProjectId()).start();
}
dataQueue.put(kafkaMessage);
线程处理类,如果单线程处理某类数据时,该数据量过大,还可以在线程内部启动多线程处理:
public class EventMessageHander implements Runnable {
//创建一个消息处理队列
public BlockingQueue<KafkaMessage> EVENT_BLOCKING_QUEUE;
public void createEventBlockingQueue(BlockingQueue<KafkaMessage> queue) {
EVENT_BLOCKING_QUEUE = queue;
}
/** 是否结束线程 */
private Boolean isClose = false;
//线程专用的线程池
private ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 16, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10000),
new ThreadFactory() {
final AtomicInteger threadSeq = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
String threadName = "hand-event-thread-" + threadSeq.getAndIncrement();
Thread t = new Thread(Thread.currentThread().getThreadGroup(), r, threadName);
t.setDaemon(true);
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
logger.error("thread {} error", t.getName(), e);
}
});
return t;
}
}, new ThreadPoolExecutor.CallerRunsPolicy());
//线程处理数据
@Override
public void run() {
while (!isClose) {
try {
Runnable runnable = new Runnable() {
@Override
public void run() {
KafkaMessage kafkaMessage;
try {
kafkaMessage = EVENT_BLOCKING_QUEUE.take();
//数据具体处理
} catch (InterruptedException e) {
logger.error("内部线程出错",e);
}
}
};
executor.submit(runnable);
} catch (Exception e) {
logger.error("event handler occur exception!",e);
}
}
logger.info( "process handler event thread end");
}
该实现机制在获取到kafka消息后,将消息存到本地阻塞队列ArrayBlockingQueue中,一类消息拥有自己的队列,让对应的线程去取并处理该阻塞队列中的消息;一方面可以尽快的消费kafka的消息,防止消费者无法跟上数据生成的速度;另一方面容易扩展,具体的消息消费类实现通用方法,实现方法的具体逻辑即可在新线程中异步执行消费,不需要在具体的消费类中关注是否开启新线程执行。不过本地队列存储有个缺陷,如果队列中堆积着数据,如果服务挂掉的话,队列中的数据就会丢失。
kafka队列处理
将kafka从监听到的消息继续分类放入kafka中,利用kafka的topic进行消息分类,同时kafka本身支持对消息的批量处理,来保证数据的快速处理。
可根据消息类型创建不同的topic,再进行消息推送
//根据不同的消息类型将消息推往不同的topic
if(MessageType.TYPE_FACEEVENT.getValue().equals(kafkaMessage.getMessageType())){
KafkaSendUtil.send("event_handle",JSONObject.toJSONString(kafkaMessage));
}
kafka消息并发处理机制,kafka创建topick的时候,可通过分配多个分区(等同于多个线程)对同一个topic的消息进行并发处理,topic创建方式:
@Bean //创建一个kafka管理类,没有此bean无法自定义的使用adminClient创建topic
public KafkaAdmin kafkaAdmin() {
Map<String, Object> props = new HashMap<>();
//配置Kafka实例的连接地址
//kafka的地址,不是zookeeper
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
KafkaAdmin admin = new KafkaAdmin(props);
return admin;
}
@Bean //kafka客户端,在spring中创建这个bean之后可以注入并且创建topic,用于集群环境,创建多个副本
public AdminClient adminClient() {
return AdminClient.create(kafkaAdmin().getConfig());
}
@Bean//通过bean创建(bean的名字为initialTopic)
public NewTopic initialTopic() {
return new NewTopic("event_handle",40, (short) 1 );
}
同时在消费者配置里,工厂配置的时候需要配置并发数据,且并发数量最好不要大于分区数
@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> eventKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerEventFactory());
//并发数量,不大于40,最好同于分区数
factory.setConcurrency(40);
//批量获取
factory.getContainerProperties().setPollTimeout(3000);
factory.setBatchListener(true);
return factory;
}
监听消息,并发处理:
@KafkaListener(topics = "event_handle",containerFactory = "eventKafkaListenerContainerFactory")
public void handleEvent(List<String> messages){
//消息处理
}
利用kafka进行消息批量处理,同时还能有效保证数据不会因为服务挂断而丢失,但是就目前项目,无法动态根据项目进行消费,根据不同的项目可创建不同的topic进行消息推送,消息监听方法不使用注解动态创建的话,无法使用分区概念进行批量处理。