使用disruptor队列实现本地异步消费

        disruptor不同于其他的消息队列中间件,不需要额外安装消息中间件服务转件;可以直接在项目中引入依赖包,就可以本地实现生产者和消费者模式,但是不能跨项目分布式进行数据传输同步;像rabbitmq,rocketmq,activemq和kafka这类的消息中间件可以称为分布式的消息中间件,可以跨项目,跨服务器使用,不过disruptor就是本地式的消息队列,不具备跨项目分布式的能力。

接下来详细介绍disruptor的用法;首先在项目中引入依赖

       <dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
            <version>3.4.4</version>
        </dependency>

然后定义一个需要作为传输数据的对象,可以是任意的java对象

例如:

package com.xiaomifeng1010.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xiaomifeng1010.dao.mapping.BaseModel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * @author  xiaomifeng1010
 * @date:  
 * @version 1.0
 * @Description 
 */

@Data
@EqualsAndHashCode(callSuper=true)
@TableName(value = "enterprise_info")
public class EnterpriseInfo extends BaseModel<EnterpriseInfo> implements Serializable {
    private static final long serialVersionUID = 46789999999898L;
    /**
     * 企业名称
     */
    @TableField(value = "enterprise_name")
    @ApiModelProperty(value="企业名称")
    private String enterpriseName;

    /**
     * 统一社会信用代码
     */
    @TableField(value = "unifiedSocialCreditCode")
    @ApiModelProperty(value="统一社会信用代码")
    private String unifiedSocialCreditCode;

    /**
     * 法定代表人
     */
    @TableField(value = "legal_representative")
    @ApiModelProperty(value="法定代表人")
    private String legalRepresentative;

    /**
     * 所属行业
     */
    @TableField(value = "industry")
    @ApiModelProperty(value="所属行业")
    private String industry;

   
    /**
     * 注册日期
     */
    @TableField(value = "registry_date")
    @ApiModelProperty(value="注册日期")
    private LocalDate registryDate;

}

然后创建对应的对象工厂类

package com.xiaomifeng1010.disruptor.factory;

import com.xiaomifeng1010.entity.EnterpriseInfo;
import com.lmax.disruptor.EventFactory;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date:
 * @Description
 */
public class EnterpriseEventFactory implements EventFactory<EnterpriseInfo> {
    @Override
    public EnterpriseInfo newInstance() {
        return new EnterpriseInfo();
    }
}

然后创建消费者(事件处理器)

package com.xiaomifeng1010.disruptor.handler;

import cn.hutool.json.JSONUtil;
import com.xiaomifeng1010.entity.EnterpriseInfo;
import com.xiaomifeng1010.esindex.EnterpriseIndexLib;
import com.xiaomifeng1010.esmapper.EnterpriseIndexLibMapper;
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.WorkHandler;
import lombok.extern.slf4j.Slf4j;

import java.time.LocalDateTime;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date:
 * @Description
 */
@Slf4j
public class EnterpriseLibIndexEventHandler implements EventHandler<EnterpriseInfo> , WorkHandler<EnterpriseInfo> {

    @Override
    public void onEvent(EnterpriseInfo enterpriseInfo, long sequence, boolean endOfBatch)  {
        try {
            EnterpriseIndexLibMapper enterpriseIndexLibMapper = SpringUtil.getBean(EnterpriseIndexLibMapper.class);
            EnterpriseIndexLib enterpriseIndexLib = new EnterpriseIndexLib();
            BeanUtils.copyProperties(enterpriseInfo, enterpriseIndexLib);
            log.info("消费到对象enterpriseInfo:{}", JSONUtil.toJsonPrettyStr(enterpriseInfo));
            LocalDateTime now = LocalDateTime.now();
            enterpriseIndexLib.setCreateTime(now);
            enterpriseIndexLib.setUpdateTime(now);
            enterpriseIndexLib.setId(String.valueOf(enterpriseInfo.getId()));
            log.info("sequence:{}",sequence);
            log.info("enterpriseIndexLib:{}",JSONUtil.toJsonPrettyStr(enterpriseIndexLib));
            log.info("开始往es中插入数据");
            Integer integer = enterpriseIndexLibMapper.insert(enterpriseIndexLib);
            String indexName = EnterpriseIndexLib.class.getAnnotation(IndexName.class).value();
            log.info("成功插入{}条数据到ES的{}索引",integer,indexName);
        } catch (Exception e) {
            log.error("获取enterpriseInfo出错{}",e);
        }
    }


    @Override
    public void onEvent(EnterpriseInfo enterpriseInfo)  {
        try {
            EnterpriseIndexLibMapper enterpriseIndexLibMapper = SpringUtil.getBean(EnterpriseIndexLibMapper.class);
            EnterpriseIndexLib enterpriseIndexLib = new EnterpriseIndexLib();
            BeanUtils.copyProperties(enterpriseInfo, enterpriseIndexLib);
            log.info("消费到对象enterpriseInfo:{}", JSONUtil.toJsonPrettyStr(enterpriseInfo));
            LocalDateTime now = LocalDateTime.now();
            enterpriseIndexLib.setCreateTime(now);
            enterpriseIndexLib.setUpdateTime(now);
            enterpriseIndexLib.setId(String.valueOf(enterpriseInfo.getId()));
            log.info("enterpriseIndexLib:{}",JSONUtil.toJsonPrettyStr(enterpriseIndexLib));
            log.info("开始往es中插入数据");
            Integer integer = enterpriseIndexLibMapper.insert(enterpriseIndexLib);
            String indexName = EnterpriseIndexLib.class.getAnnotation(IndexName.class).value();
            log.info("成功插入{}条数据到ES的{}索引",integer,indexName);
        } catch (Exception e) {
            log.error("获取enterpriseInfo出错{}",e);
        }
    }
}

注意实现了EventHandler接口的消费者,只能是普通的消费者,如果在ringbuffer中指定消费者时,即使指定了多个消费者,本质上实现的是消息广播模式,同一条消息,多个消费者同时消费,也就是说虽然有多个消费者(可以配置成多个消费者是多个线程),但实际上多个消费者同时消费的是同一个消息,不是多个消费者同时处理不同的多条消息,如果是要多个消费者同时处理的是多个不同的消息,则需要实现WorkHandler接口

接下来创建一个disruptor配置类

ackage com.xiaomifeng1010.disruptor.configuration;

import com.xiaomifeng1010.entity.EnterpriseInfo;
import com.xiaomifeng1010.manage.disruptor.factory.EnterpriseEventFactory;
import com.xiaomifeng1010.disruptor.handler.EnterpriseLibIndexEventHandler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.lmax.disruptor.BlockingWaitStrategy;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.*;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Configuration
public class DisruptorConfig {


    @Bean
    public RingBuffer<EnterpriseInfo> enterpriseInfoRingBuffer() {

        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("queue-thread-pool-%d").build();
//        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS,
//                new LinkedBlockingQueue<>(1024), namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());


        //指定事件工厂
        EnterpriseEventFactory factory = new EnterpriseEventFactory();

        //指定ringbuffer字节大小,必须为2的N次方(能将求模运算转为位运算提高效率),否则将影响效率
        int bufferSize = 1024 * 256;

        //单线程模式,获取额外的性能
        Disruptor<EnterpriseInfo> disruptor = new Disruptor<>(factory, bufferSize, namedThreadFactory,
                ProducerType.SINGLE, new BlockingWaitStrategy());

        EnterpriseLibIndexEventHandler[] arr1 =new EnterpriseLibIndexEventHandler[10];

        for(int i=0;i<10;i++) {
            arr1[i] = new EnterpriseLibIndexEventHandler();
        }
        //设置多线程消费者
        disruptor.handleEventsWithWorkerPool(arr1);

        // 启动disruptor线程
        disruptor.start();

        //获取ringbuffer环,用于接取生产者生产的事件
        RingBuffer<EnterpriseInfo> ringBuffer = disruptor.getRingBuffer();

        return ringBuffer;
    }
}

关键的一步设置是 //设置多线程消费者

  disruptor.handleEventsWithWorkerPool(arr1);

 这样才是开启了10个线程同时处理不同的数据

而如果使用

disruptor.handleEventsWith(arr1);

 则是开启10个线程,但是每次10个消费者都是消费同一条数据,无法同时处理多条数据 因为这个api方法的参数是workhandler类型 disruptor.handleEventsWithWorkerPool(arr1); 而disruptor.handleEventsWith(arr1);传入的参数是eventhandler类型

然后具体的调用方法:

package com.xiaomifeng1010.disruptor.controller;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.xiaomifeng1010.common.service.EnterpriseInfoService;
import com.xiaomifeng1010..EnterpriseInfo;
import com.lmax.disruptor.RingBuffer;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@RestController
@Api(tags = "测试disruptor")
@RequestMapping("/testDisruptor")
@AllArgsConstructor
@Slf4j
public class DisruptorTestController {

    private RingBuffer<EnterpriseInfo>  enterpriseInfoRingBuffer;

    private EnterpriseInfoService enterpriseInfoService;

    @PostMapping("/web/insertEnterprise")
    @ApiOperation("从mysql中同步数据到ElasticSearch")
    public void testSendMessage() {
        List<EnterpriseInfo> enterpriseInfoList= enterpriseInfoService.list(Wrappers.<EnterpriseInfo> lambdaQuery()
                .select(EnterpriseInfo::getEnterpriseName,EnterpriseInfo::getUscc,EnterpriseInfo ::getId));
        String tableName = EnterpriseInfo.class.getAnnotation(TableName.class).value();
        log.info("从mysql中{}表查出{}条数据",tableName,enterpriseInfoList.size());

        for (EnterpriseInfo enterpriseInfo : enterpriseInfoList) {
            long next = enterpriseInfoRingBuffer.next();
            EnterpriseInfo enterpriseInfo1 = enterpriseInfoRingBuffer.get(next);
            enterpriseInfo1.setEnterpriseName(enterpriseInfo.getEnterpriseName());
            enterpriseInfo1.setUscc(enterpriseInfo.getUscc());
            enterpriseInfo1.setId(enterpriseInfo.getId());
//            往disruptor队列中放数据
            enterpriseInfoRingBuffer.publish(next);
        }
    }
}

这里为了方便写了一个接口测试类,测试方便,项目中我使用的是定时任务,每天固定时间去同步数据:

package com.xiaomifeng1010.schedule;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.xiaomifeng1010.common.service.EnterpriseInfoService;
import com.xiaomifeng1010.entity.EnterpriseInfo;
import com.lmax.disruptor.RingBuffer;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Slf4j
@Component
@AllArgsConstructor
public class DataSyncTaskSchedule {

    private RingBuffer<EnterpriseInfo> enterpriseInfoRingBuffer;

    private EnterpriseInfoService enterpriseInfoService;


    /**
     * @param
     * @description: 每周日从mysql更新同步信息到elasticsearch
     * @author: xiaomifeng1010
     * @date: 
     * @return: void
     **/

    @Scheduled(cron = "${cron.sync-task}")
    public void asyncEnterpriseArchive() {

        List<EnterpriseInfo> enterpriseInfoList = enterpriseInfoService.list(Wrappers.<EnterpriseInfo>lambdaQuery()
                .select(EnterpriseInfo::getEnterpriseName, EnterpriseInfo::getUscc, EnterpriseInfo::getId));
        String tableName = EnterpriseInfo.class.getAnnotation(TableName.class).value();
        log.info("从mysql中{}表查出{}条数据", tableName, enterpriseInfoList.size());

        for (EnterpriseInfo enterpriseInfo : enterpriseInfoList) {
            long next = enterpriseInfoRingBuffer.next();
            EnterpriseInfo enterpriseInfo1 = enterpriseInfoRingBuffer.get(next);
            enterpriseInfo1.setEnterpriseName(enterpriseInfo.getEnterpriseName());
            enterpriseInfo1.setUscc(enterpriseInfo.getUscc());
            enterpriseInfo1.setId(enterpriseInfo.getId());
            enterpriseInfoRingBuffer.publish(next);
        }
    }


}

cron表达式配置在yml文件中

还有就是实现的EnterpriseLibIndexEventHandler 类中涉及到的三个类

EnterpriseIndexLib和EnterpriseIndexLibMapper以及SpringUtil

创建EnterpriseIndexLib和EnterpriseIndexLibMapper需要再引入一下ES客户端操作jar包,需要操作Elastic Search

       <dependency>
            <groupId>cn.easy-es</groupId>
            <artifactId>easy-es-boot-starter</artifactId>
            <version>0.9.60</version>
        </dependency>

可以使用这个easy-es来替代spring官方的spring-data-elasticsearch

创建ES索引对应的实体类: EnterpriseIndexLib

package com.xiaomifeng1010.esindex;

import cn.easyes.annotation.IndexField;
import cn.easyes.annotation.IndexId;
import cn.easyes.annotation.IndexName;
import cn.easyes.common.constants.Analyzer;
import cn.easyes.common.enums.FieldStrategy;
import cn.easyes.common.enums.FieldType;
import cn.easyes.common.enums.IdType;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Data
@IndexName("enterprise_index_lib")
@JsonIgnoreProperties(ignoreUnknown = true)
public class EnterpriseIndexLib {

    @IndexId(type = IdType.CUSTOMIZE)
    private String id;

    @IndexField(strategy = FieldStrategy.NOT_EMPTY,fieldType = FieldType.KEYWORD_TEXT,analyzer = Analyzer.IK_SMART,searchAnalyzer = Analyzer.IK_SMART)
    private String enterpriseName;

    @IndexField(strategy = FieldStrategy.NOT_EMPTY,fieldType = FieldType.KEYWORD)
    private String unifiedSocialCreditCode;

    @IndexField(dateFormat = "yyyy-MM-dd HH:mm:ss",fieldType = FieldType.DATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone ="GMT+8")
//    说明!不要使用java.uti包下的Date类型,存储到ES中的时候会被转换成Long类型存储
    private LocalDateTime createTime;

    @IndexField(dateFormat = "yyyy-MM-dd HH:mm:ss",fieldType = FieldType.DATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone ="GMT+8")
    private LocalDateTime updateTime;
}

easy-es的查询借鉴了mybatis-plus,所以查询依赖mapper接口,接着创建对应的查询mapper

package com.xiaomifeng1010.esmapper;

import cn.easyes.core.conditions.interfaces.BaseEsMapper;
import com.xiaomifeng1010.esindex.EnterpriseIndexLib;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
public interface EnterpriseIndexLibMapper extends BaseEsMapper<EnterpriseIndexLib> {
}

还有一个SpringUtil工具类:

package com.dcboot.base.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component("springUtil")
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public SpringUtil() {
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    public static <T> T getBean(Class<T> tClass) {
        return getApplicationContext().getBean(tClass);
    }
}

 还有注意点就是启动类上加了mybatis的mapper扫描注解,是为了只扫描mysql的查询mapper,所以ES的mapper查询类需要另外创建package


import cn.easyes.starter.register.EsMapperScan;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;


/**
 *
 */
@Slf4j
@SpringBootApplication
@EnableKnife4j
@EnableMongoAuditing
@EnableScheduling
@EnableRetry
@EsMapperScan("com.xiaomifeng1010.**.esmapper")
public class TestApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext application = SpringApplication.run(TestApplication .class, args);

 
    }


}

es的mapper类存放在

 项目中的elastic-search连接配置信息

 对应的elastic search的index表是这样的:

 推荐使用这个国产的ElasticView可视化管理客户端,个人感觉比elasticsearch head和kibana更好用,更方便

如何安装部署可以到ElasticView官网查看,推荐使用docker安装,非常快速方便,首页是这样的

 教程使用的是我们项目中的其中的一个index

以navicat形式可视化展示es的index,展示效果类似关系型数据库的表格形式(也比dbeaver的可视化好用)

使用disruptor的测试接口测试一下

从控制台打印的日志可以看出,disruptor正在往队列中存放从mysql总查询出来的数据,而handler则从队列中消费,然后存入到ES中

同时从queue-thread-pool-数字,可以看到多个线程的消费者,消费的数据是不同的

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值