【Excel_To_DB】SpringBoot+EasyPoi+Redis消息队列实现Excel批量异步导入数据库(二)

版权声明:本文为博主原创文章,转载请标明出处 https://blog.csdn.net/YangDongChuan1995/article/details/79285341

【Excel_To_DB】SpringBoot+EasyPoi+Redis消息队列实现Excel批量异步导入数据库(一)
【Excel_To_DB】SpringBoot+EasyPoi+Redis消息队列实现Excel批量异步导入数据库(二)
【Excel_To_DB】SpringBoot+EasyPoi+Redis消息队列实现Excel批量异步导入数据库(三)
【效果演示】:JavaWeb毕业设计项目-足球队管理系统(四)引入Excel_To_DB项目+源码
【码云地址】:https://gitee.com/ydc_coding

Redis实现消息队列:
环境依赖:

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.38</version>
        </dependency>

这里写图片描述
说明:这里我通过使用fastjson将bean转换成String(也就是json字符串),存入redis中,而没有选择用谷歌的Protostuff,虽然效率上有所降低,但Protostuff序列化后的数据存入redis中,不具备可读性。用更好的可读性换那一块效率,我觉得在当前这个业务环境中更合适一些。
另一个使用Protostuff的Demo:【redis-demo】使用Jedis api 实现后端缓存优化

redis数据库配置:

#redis
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=666666
# Redis数据库索引(默认为0)
spring.redis.database=0
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=2000
#spring-session 使用

消息接收者:

package com.ydc.excel_to_db.redis;

import com.ydc.excel_to_db.domain.ExcelModel;
import com.ydc.excel_to_db.service.ImportService;
import com.ydc.excel_to_db.util.Constant;
import com.ydc.excel_to_db.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @Description: 消息接收者,将其在ExcelToDbApplication.java中注入消息监听容器(MessageListenerAdapter)中
 * @Author: 杨东川【http://blog.csdn.net/yangdongchuan1995】
 * @Date: Created in  2018-2-6
 */
@Service
public class Receiver {
    @Autowired
    ImportService importService;
    @Autowired
    RedisDao redisDao;
    private static final Logger log = LoggerFactory.getLogger(Receiver.class);

    /**
     * @Description: 用于接收单个对象,将对象同步至数据库,如果同步失败,则存入redis中
     * @Param: [message] “fastjson”转换后的json字符串
     * @Retrun: void
     */
    public void receiveSingle(String message) throws InterruptedException {
        // 将json字符串转换成实体对象
        ExcelModel excelModel = JsonUtil.stringToBean(message, ExcelModel.class);
        // 尝试同步数据库并返回同步结果
        boolean result = importService.save(excelModel);
        if (!result)
            // 同步失败,将其存入redis中
            redisDao.leftPushKey(Constant.failToDBKey, excelModel);
        else
            // 同步成功,输出至日志中
            log.info("成功插入数据库的数据:" + excelModel.getCol2());
        // 加上-1,其实也就是做减1操作
        redisDao.incrOrDecr(Constant.succSizeTempKey, -1);

    }

    /**
     * @Description: 用于接收对象集合,将集合遍历拆分成单个对象并进行发布
     * @Param: [message] “fastjson”转换后的json字符串
     * @Retrun: void
     */
    public void receiveList(String message) throws InterruptedException {
        // 将json字符串转换成对象集合
        List<ExcelModel> list = JsonUtil.stringToList(message, ExcelModel.class);
        // 遍历集合,并依次将其发布
        for (ExcelModel excelModel : list) {
            redisDao.publish(Constant.receiveSingle, excelModel);
        }
    }
}

配置消息监听器:

package com.ydc.excel_to_db;

import com.ydc.excel_to_db.redis.Receiver;
import com.ydc.excel_to_db.util.Constant;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@SpringBootApplication
@ServletComponentScan
public class ExcelToDbApplication {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapterSingle, MessageListenerAdapter listenerAdapterList) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 注入多个消息监听器(receiveSingle/receiveList)
        container.addMessageListener(listenerAdapterSingle, new PatternTopic(Constant.receiveSingle));
        container.addMessageListener(listenerAdapterList, new PatternTopic(Constant.receiveList));
        return container;
    }

    @Bean
    MessageListenerAdapter listenerAdapterSingle(Receiver receiver) {
        return new MessageListenerAdapter(receiver, Constant.singleMethodName);
    }

    @Bean
    MessageListenerAdapter listenerAdapterList(Receiver receiver) {
        return new MessageListenerAdapter(receiver, Constant.listMethodName);
    }

    @Bean
    Receiver receiver() {
        return new Receiver();
    }

    public static void main(String[] args) {
        SpringApplication.run(ExcelToDbApplication.class, args);
    }

}

RedisDao.java:

package com.ydc.excel_to_db.redis;

import com.ydc.excel_to_db.util.JsonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description: 通过StringRedisTemplate类对redis进行操作
 * @Author: 杨东川【http://blog.csdn.net/yangdongchuan1995】
 * @Date: Created in  2018-2-6
 */
@Repository
public class RedisDao {

    @Autowired
    private StringRedisTemplate template;

    /**
     * @Description: 操作redis中数据结构为String的数据,进行set操作
     * @Param: [key, value]
     * @Retrun: void
     */
    public <T> void setStringKey(String key, T value) {
        ValueOperations<String, String> ops = template.opsForValue();
        // 将参数value转换为String类型
        String str = JsonUtil.beanToString(value);
        ops.set(key, str);
    }

    /**
     * @Description: 操作redis中数据结构为String的数据,进行get操作,获取单个对象的json字符串
     * @Param: [key, clazz]
     * @Retrun: T
     */
    public <T> T getStringValue(String key, Class<T> clazz) {
        ValueOperations<String, String> ops = this.template.opsForValue();
        String str = ops.get(key);
        // 将json串转换成对应(clazz)的对象
        return JsonUtil.stringToBean(str, clazz);
    }

    /**
     * @Description: 操作redis中数据结构为String的数据,进行get操作,获取对象集合的json字符串
     * @Param: [key, clazz]
     * @Retrun: java.util.List<T>
     */
    public <T> List<T> getStringListValue(String key, Class<T> clazz) {
        ValueOperations<String, String> ops = this.template.opsForValue();
        String str = ops.get(key);
        // 将json串转换成对应(clazz)的对象集合
        return JsonUtil.stringToList(str, clazz);
    }

    /**
     * @Description: 操作redis中数据结构为List的数据,进行get操作,获取对应list中“所有”的数据
     * @Param: [key, clazz]
     * @Retrun: java.util.List<T>
     */
    public <T> List<T> getListValue(String key, Class<T> clazz) {
        ListOperations<String, String> ops = template.opsForList();
        // 获取对应list中的所有的数据
        List<String> list = ops.range(key, 0, -1);
        // 创建大小为对应list大小(ops.size(key)的ArrayList,避免后期进行扩容操作
        List<T> result = new ArrayList<T>(ops.size(key).intValue());
        // 遍历从redis中获取到的list,依次将其转换为对应(clazz)的对象并添加至结果集(result)中
        for (String s : list) {
            result.add(JsonUtil.stringToBean(s, clazz));
        }
        return result;
    }

    /**
     * @Description: 操作redis中数据结构为List的数据,进行push操作(这里默认从左left进行插入)
     * @Param: [key, value]
     * @Retrun: void
     */
    public <T> void leftPushKey(String key, T value) {
        ListOperations<String, String> ops = template.opsForList();
        // 将参数value转换为String类型
        String str = JsonUtil.beanToString(value);
        // 将转换后的json字符串存入redis
        ops.leftPush(key, str);

    }

    /**
     * @Description: 操作redis中数据结构为List的数据,进行pop操作(这里默认从右right进行取出)
     * @Param: [key, clazz]
     * @Retrun: T
     */
    public <T> T rightPopValue(String key, Class<T> clazz) {
        ListOperations<String, String> ops = template.opsForList();
        String str = ops.rightPop(key);
        return JsonUtil.stringToBean(str, clazz);
    }

    /**
     * @Description: 操作redis中数据结构为List的数据,进行size操作,获取对应的list的长度大小
     * @Param: [key]
     * @Retrun: java.lang.Long
     */
    public Long getListSize(String key) {
        ListOperations<String, String> ops = template.opsForList();
        return ops.size(key);
    }

    /**
     * @Description: 消息发布
     * @Param: [channelName, value] 频道名称
     * @Retrun: void
     */
    public <T> void publish(String channelName, T value) {
        // 将参数value转换为String类型
        String str = JsonUtil.beanToString(value);
        // 将消息(str)发布到指定的频道(channelName)
        template.convertAndSend(channelName, str);
    }

    /**
     * @Description: 操作redis中数据结构为String的数据,进行increment操作
     * @Param: [key, num]
     * @Retrun: java.lang.Long
     */
    public Long incrOrDecr(String key, long num) {
        ValueOperations<String, String> ops = template.opsForValue();
        return ops.increment(key, num);
    }

    /**
     * @Description: 清空参数keyList中的所有值(key)所对应的redis里的数据
     * @Param: [keyList]
     * @Retrun: void
     */
    public void cleanCache(List<String> keyList) {
        template.delete(keyList);
    }


}

以上就是通过Redis发布订阅模式实现消息队列的基本配置,RedisDao中的publish(String channelName, T value)方法, 将消息(发布到指定的频道,因为在此之前已经配置了对应频道的监听容器,所以消息也就直接能够传送至对应配置的方法中。同时,服务器会调用多个线程去同时“消费”消息,如下图:
这里写图片描述
接下来把其他一些与核心业务逻辑关联不大的模块先发出来,在下一章的时候,再把主要的业务逻辑及其对应的代码贴出来。

Druid连接池:
依赖环境:

        <!--  连接池druid  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.7</version>
        </dependency>
        <!--  mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

配置:

#   Mysql数据库-数据源配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/excelModel?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver

# druid
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 初始化大小,最小,最大
spring.datasource.initialSize=5  
spring.datasource.minIdle=5  
spring.datasource.maxActive=20  
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000  
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000  
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000  
# 校验SQL,Oracle配置 spring.datasource.validationQuery=SELECT 1 FROM DUAL,如果不配validationQuery项,则下面三项配置无用
spring.datasource.validationQuery=SELECT 'x'  
spring.datasource.testWhileIdle=true  
spring.datasource.testOnBorrow=false  
spring.datasource.testOnReturn=false  
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true  
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20  
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall  
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000  
# 合并多个DruidDataSource的监控数据
spring.datasource.useGlobalDataSourceStat=true 

说明:由于Druid暂时不在Spring Boot中的直接支持,故需要进行配置信息的定制
DruidConfiguration.java :

package com.ydc.excel_to_db.druid;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;


/**
 * @Description: 需要手动初始化DataSource
 * @Author: 杨东川【http://blog.csdn.net/yangdongchuan1995】
 * @Date: Created in  2018-2-6
 */
@Configuration
public class DruidConfiguration {
    private static final Logger log = LoggerFactory.getLogger(DruidConfiguration.class);

    @Value("${spring.datasource.url}")
    private String dbUrl;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;
    @Value("${spring.datasource.initialSize}")
    private int initialSize;
    @Value("${spring.datasource.minIdle}")
    private int minIdle;
    @Value("${spring.datasource.maxActive}")
    private int maxActive;
    @Value("${spring.datasource.maxWait}")
    private int maxWait;
    @Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;
    @Value("${spring.datasource.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;
    @Value("${spring.datasource.validationQuery}")
    private String validationQuery;
    @Value("${spring.datasource.testWhileIdle}")
    private boolean testWhileIdle;
    @Value("${spring.datasource.testOnBorrow}")
    private boolean testOnBorrow;
    @Value("${spring.datasource.testOnReturn}")
    private boolean testOnReturn;
    @Value("${spring.datasource.poolPreparedStatements}")
    private boolean poolPreparedStatements;
    @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
    private int maxPoolPreparedStatementPerConnectionSize;
    @Value("${spring.datasource.filters}")
    private String filters;
    @Value("${spring.datasource.connectionProperties}")
    private String connectionProperties;
    @Value("${spring.datasource.useGlobalDataSourceStat}")
    private boolean useGlobalDataSourceStat;

    /**
     * @Description: 声明其为Bean实例,@Primary代表当存在多数据源时,优先使用该数据源
     */
    @Bean
    @Primary
    public DataSource dataSource() {
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(this.dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);
        datasource.setInitialSize(initialSize);
        datasource.setMinIdle(minIdle);
        datasource.setMaxActive(maxActive);
        datasource.setMaxWait(maxWait);
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setValidationQuery(validationQuery);
        datasource.setTestWhileIdle(testWhileIdle);
        datasource.setTestOnBorrow(testOnBorrow);
        datasource.setTestOnReturn(testOnReturn);
        datasource.setPoolPreparedStatements(poolPreparedStatements);
        datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        datasource.setUseGlobalDataSourceStat(useGlobalDataSourceStat);
        try {
            datasource.setFilters(filters);
        } catch (SQLException e) {
            log.error(e.getMessage(), e);
        }
        datasource.setConnectionProperties(connectionProperties);
        return datasource;
    }
}

DruidStatFilter.java:

package com.ydc.excel_to_db.druid;

import com.alibaba.druid.support.http.WebStatFilter;

import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;

@WebFilter(filterName = "druidStatFilter", urlPatterns = "/*",
        initParams = {
                // 不拦截的资源名单
                @WebInitParam(name = "exclusions", value = "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*")
        })
public class DruidStatFilter extends WebStatFilter {

}

DruidStatViewServlet.java :

package com.ydc.excel_to_db.druid;

import com.alibaba.druid.support.http.StatViewServlet;

import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;

@WebServlet(urlPatterns = "/druid/*",
        initParams = {
//                @WebInitParam(name="allow",value="0.0.0.0"),// IP白名单 (没有配置或者为空,则允许所有访问)
//                @WebInitParam(name="deny",value="192.168.0.0"),// IP黑名单 (存在共同时,deny优先于allow)
                @WebInitParam(name = "loginUsername", value = "admin"),// druid监控页面登陆用户名
                @WebInitParam(name = "loginPassword", value = "admin"),// druid监控页面登陆密码
                @WebInitParam(name = "resetEnable", value = "true")// 禁用HTML页面上的“Reset All”功能
        })
public class DruidStatViewServlet extends StatViewServlet {
    private static final long serialVersionUID = 1L;
}

知识点补充:Druid连接池的功能?
- 可以监控数据库访问性能,Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。
- 替换DBCP和C3P0,Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
- 数据库密码加密,直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。
- SQL执行日志,Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,你可以按需要选择相应的LogFilter,监控你应用的数据库访问情况。

其他配置及其依赖环境:
Thymeleaf与Mybatis的环境依赖:

        <!--  thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>

配置:

#thymeleaf
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5
# mybatis
mybatis.type-aliases-package=com.ydc.excel_to_db.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-fetch-size=100
mybatis.configuration.default-statement-timeout=3000
#mybatis.mapperLocations = classpath:xxx.xml

说明:这里将对应mapper.xml配置注释掉,是因为这块是通过用注解的形式绑定sql语句,方便后期维护。
ExcelModelMapper.interface:

package com.ydc.excel_to_db.dao;

import com.ydc.excel_to_db.domain.ExcelModel;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

/**
 * @Description: 为了方便扩展, 这里直接使用注解的形式进行绑定sql语句,对应的实体类:com.ydc.excel_to_db.domain.ExcelModel
 * @Author: 杨东川【http://blog.csdn.net/yangdongchuan1995】
 * @Date: Created in  2018-2-6
 */
@Mapper
public interface ExcelModelMapper {
    /**
     * @Description: 通过“IGNORE”关键字,使插入数据的主键“已存在”时也不会报异常
     * @Param: [excelModel]
     * @Retrun: long 插入成功,返回 1,插入失败,返回 0;
     */
    // 达梦数据库sql
    //    @Insert("insert   into CQRECTIFY.excelmodel(A, B, C, D, E, F, G,H,I,J,K,L,M, N,O,P,Q)values("
    //           + "#{col1}, #{col2}, #{col3}, #{col4}, #{col5},#{col6},#{col7},#{col8} ,#{col9} ,#{col10} ,#{col11} ,#{col12} ,#{col13} ,#{col14} ,#{col15},#{col16},#{col7} )")
    // mysql数据库sql
    @Insert("insert  ignore into excelmodel(A, B, C, D, E, F, G,H,I,J,K,L,M, N,O,P,Q)values("
            + "#{col1}, #{col2}, #{col3}, #{col4}, #{col5},#{col6},#{col7},#{col8} ,#{col9} ,#{col10} ,#{col11} ,#{col12} ,#{col13} ,#{col14} ,#{col15},#{col16},#{col7} )")
    long insert(ExcelModel excelModel);
}

说明:这里因为某些原因,把原版的ExcelModelMapper.java里的sql语句全部删除掉了,暂时重新贴上了一条简单的插入语句,虽然字段名变了,但所想要表达的意思是一样的。这块与其对应的ExcelModel.java的详细说明在下一章一起贴出来。

猜你喜欢

转载自blog.csdn.net/YangDongChuan1995/article/details/79285341
今日推荐