【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的詳細說明在下一章一起貼出來。