【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的详细说明在下一章一起贴出来。