【Excel_To_DB】SpringBoot+EasyPoi+Redis消息隊列實現Excel批量異步導入數據庫(二)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章