提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
使用rabbitmq和异步线程的方式进行同步数据
前言
场景:在添加数据到mysql后,需要同步到ck中
提示:以下是本篇文章正文内容,下面案例可供参考
一、使用异步线程同步
写一个线程池
@Bean("insertClickhouseDataExecutor")
ThreadFactory threadFactory = new ThreadFactoryBuilder.setNameFormate("insertClickHouseDataExecutor-%s").build();
return new ThreadPoolExecutor(NUMBER_OF_CORES+1,NUMBER_OF_CORES<<1,KEEP_ALIVE_TIME,TimeUtil.SECONDS,new LinkedBlockingQueue<>(CAPACITY),threadFactory,new ThreadPoolExecutor.CallerRunsPolicy());
public void asyncInsertDataToClickhouse(List<> 插入的数据集合){
insertClickhouseDataExecutor.submit(()->{
syncDataToClickhouse(插入的数据集合)
})
}
syncDataToClickhouse(){
//使用xml手写批量插入即可(foreach)
Mapper.batchSave(插入的集合)
}
二、使用rabbitmq进行同步
MySQLTableDataDTO mySQLTableDataDTO = new MySQLTableDataDTO();
//把信息封装进一个实体类中,为了让生成的id作为redis的key,解决幂等性问题,也就是下面的第四步
msgSender(JSONObjet.toJSONString(mySQLTableDataDTO),rabbitProperties.getExchage(),rabbitProperties.getDataRouting());
public void megSender(String content,String exchange,String routing){
//开启confirm机制,里面是函数式接口,提供性接口
rabbitTemplate.setConfirmCallback((correlationData,ack,cause)->{
log.info("confimCallback触发回调");
if(!ack){
log.info("消息没有到达exchange!")
}
})
//开启returns机制
rabbitTemplate.setReturnsCallback(returned -> {
String msg = returned.getMessage().getBody();
log.info("消息入路由失败,内容:{}",msg);
})
//发送消息
//下拉姆达表达式 就是MessagePostProcessor 这个函数式接口 返回的类。 他是返回型接口
rabbitTemplate.convertAndSend(exchange,routing,content,message -> {
//消息持久化,确保rabbitmq宕机重启之后消息丢失
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
})
}
三、考虑confirm回调中消息不丢失
消息的持久化是基于队列的持久化,如果消息在发送给消费者之前宕机了,那么消息还存在,假如队列不是持久化,那么消息的持久化毫无意义。还有缺点就是严重影响到了RabbitMq的性能,写入硬盘的速度比写入内存的速度慢的不只一点点。对于可靠性不是那么高的消息可以不采用这个方式,需要在可靠性和吞吐量之间做一个全权衡。
//如果想在confirm回调中进行重发,需要一个类 继承CorreLationData ,
/**
* CorrelationData的自定义实现,用于拿到消息内容
*/
public class CorrelationDataExt extends CorrelationData {
//数据
private volatile Object data;
//队列
//交换机
//routingkey
}
public void megSender(String content,String exchange,String routing){
//使用自定义的数据对象
CorrelationDataExt correlationData = new CorrelationDataExt();
correlationData.setData(json);
//开启confirm机制,里面是函数式接口,提供性接口
rabbitTemplate.setConfirmCallback((correlationData,ack,cause)->{
log.info("confimCallback触发回调");
if(!ack){
log.info("消息没有到达exchange!")
if(correlationData instanceof CorrelationDataExt ){
CorrelationDataExt correlationDataExt = (CorrelationDataExt) correlationData;
//从correlationDataExt中获取各种数据,然后重新发送
}
}
})
//下拉姆达表达式 就是MessagePostProcessor 这个函数式接口 返回的类。 他是返回型接口
rabbitTemplate.convertAndSend(exchange,routing,content,message -> {
//消息持久化
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
},correlationData)
}
四、幂等性问题
场景:当消费者返回ack时,由于网络断开或者其他原因导致RabbitMQ并没有接收到这个ack,那么rabbitmq不会把这个消息给删除,当重新建立连接后,会重新发送消息给消费者,导致重复消费。
解决:
1、消费者监听到消息后获取消息的MsgId(这个MsgId是我们自定义消息的字段,是主键),我们利用第二步的JSONObject.parseObject(msg,MySQLTableDTO.class) 转为DTO类,再getId即可
先去Redis中查询这个MsgId是否存在。也可以生产者发送消息时指给消息对象设置唯一的 MessageID,只有该 MessageID 没有被消费者存入到Redis中即该消息未被消费,这样重发的消息才能在重试机制中再次被消费。 可以使用redis的setnx
2、如果不存在,则正常消费消息,并把消息的id存入Redis中。
3、如果存在则丢弃或者拒绝此消息并不返回队列。(在消费者中判断)(channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);) true代表丢弃,或给死信队列。
五、重试机制(retry)
1:xml中进行重试
//重试次数超过设定的多少次之后,就会进入死信队列中(需要自己写死信交换机和死信队列)
default-requeue-rejected: false
2:定时任务重试
1:在第三步添加的消息实体类中,在发消息的实体类中添加一个参数:count,代表重试几次,count
2:设置定时任务(@Scheduled),判断每次count为多少,如果大于3次(自己设定的)就丢弃或给到死信队列中,如果小于3次就修改数据库,把count+1即可。