1. 程式人生 > 資料庫 >SpringBoot 整合 MyBatis 配置多資料來源操作MySQL資料庫

SpringBoot 整合 MyBatis 配置多資料來源操作MySQL資料庫

本文以多個 MySQL 資料庫為例,採用 SpringBoot 框架,整合 MyBatis 配置多資料來源進行資料庫操作。在實際專案中,為了減少流量高峰期間對資料庫的壓力,可對一些資料庫惰性資料(以查詢為主,且不經常更新的資料)快取到 JVM 記憶體中,可快速響應,且減少資料庫壓力。
專案原始碼 git 地址:https://github.com/piaoranyuji/muldb

一、MySQL 表結構(雙資料庫)

本專案中共用到了 2 個數據庫,分別為 testmgmdb 和 testonldb。

  1. testmgmdb 資料庫中表 tbl_mgm_menu 的建表語句如下。
SET FOREIGN_KEY_CHECKS=0;

DROP TABLE IF EXISTS `tbl_mgm_menu`;
CREATE TABLE `tbl_mgm_menu` (
  `menu_id` int(10) NOT NULL AUTO_INCREMENT COMMENT '選單主鍵',`menu_name` varchar(30) NOT NULL DEFAULT '' COMMENT '選單名稱',`menu_logo` varchar(64) NOT NULL DEFAULT '' COMMENT '選單logo',`menu_url` varchar(64) NOT NULL DEFAULT '' COMMENT '選單url',`menu_seq` int(2) NOT NULL DEFAULT '0' COMMENT '選單順序',`rec_st` char(1) NOT NULL DEFAULT '1' COMMENT '記錄狀態,0:無效;1:有效',`rec_crt_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',`rec_upd_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8;

INSERT INTO `tbl_mgm_menu` VALUES ('23','今日播報','https://www.baidu.com','0','1','2020-01-21 14:44:03','2020-01-21 14:44:03');
  1. testonldb 資料庫中表 tbl_onl_sp 的建表語句如下。
SET FOREIGN_KEY_CHECKS=0;

DROP TABLE IF EXISTS `tbl_onl_sp`;
CREATE TABLE `tbl_onl_sp` (
  `sp_id` char(16) NOT NULL DEFAULT '' COMMENT '服務方ID',`call_dt` char(8) NOT NULL DEFAULT '' COMMENT '介面呼叫時間(yyyyMMdd)',`sp_name` char(30) NOT NULL DEFAULT '' COMMENT '服務方名稱',PRIMARY KEY (`sp_id`,`call_dt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
二、SpringBoot 配置檔案配置資料庫連線資訊
spring.datasource.mgmdb.jdbc-url=jdbc:mysql://localhost:3306/testmgmdb?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false
spring.datasource.mgmdb.username=root
spring.datasource.mgmdb.password=
spring.datasource.mgmdb.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.mgmdb.max-idle=10
spring.datasource.mgmdb.max-wait=10000
spring.datasource.mgmdb.min-idle=5
spring.datasource.mgmdb.initial-size=5
spring.datasource.mgmdb.maximum-pool-size=200
spring.datasource.mgmdb.validation-query=SELECT 1
spring.datasource.mgmdb.test-on-borrow=false
spring.datasource.mgmdb.test-while-idle=true
spring.datasource.mgmdb.time-between-eviction-runs-millis=18800

spring.datasource.onldb.jdbc-url=jdbc:mysql://localhost:3306/testonldb?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false
spring.datasource.onldb.username=root
spring.datasource.onldb.password=
spring.datasource.onldb.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.onldb.max-idle=10
spring.datasource.onldb.max-wait=10000
spring.datasource.onldb.min-idle=5
spring.datasource.onldb.initial-size=5
spring.datasource.onldb.maximum-pool-size=200
spring.datasource.onldb.validation-query=SELECT 1
spring.datasource.onldb.test-on-borrow=false
spring.datasource.onldb.test-while-idle=true
spring.datasource.onldb.time-between-eviction-runs-millis=18800

mapper.not-empty=false
mapper.identity=MYSQL

server.port=8899

#出現錯誤時,直接丟擲異常
spring.mvc.throw-exception-if-no-handler-found=true
#不要為我們工程中的資原始檔建立對映
spring.resources.add-mappings=false

spring.aop.proxy-target-class=true

conf.file.path=D:/testconf/muldb/svcConfig.properties
log_dir=D:/testlog
三、配置多資料庫DataSource、SqlSessionFactory、SqlSessionTemplate 和 DataSourceTransactionManager
  1. Mgm 資料庫配置
package com.test.svc.conf;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = {"com.test.svc.dao.mgm"},sqlSessionTemplateRef = "mgmSqlSessionTemplate")
public class MgmConfig {

    @Bean(name = "mgmDataSource")
    @Primary //必須加此註解,不然報錯,下一個類則不需要新增
    @ConfigurationProperties(prefix = "spring.datasource.mgmdb") // prefix值必須是application.properteis中對應屬性的字首
    public DataSource mgmDataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        return dataSourceBuilder.build();
    }

    @Bean(name = "mgmSqlSessionFactory")
    @Primary
    public SqlSessionFactory mgmSqlSessionFactory(@Qualifier("mgmDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //新增XML目錄
        try {
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:sqlmapper/mgmMapper/*.xml"));
            return bean.getObject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    @Bean(name = "mgmSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate mgmSqlSessionTemplate(@Qualifier("mgmSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "mgmTransactionManager")
    @Primary
    public DataSourceTransactionManager mgmTransactionManager(@Qualifier("mgmDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

  1. Onl 資料庫配置
package com.test.svc.conf;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = {"com.test.svc.dao.onl"},sqlSessionTemplateRef = "onlSqlSessionTemplate")
public class OnlConfig {

    @Bean(name = "onlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.onldb")
    public DataSource onlDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "onlSqlSessionTemplate")
    public SqlSessionTemplate onlSqlSessionTemplate(@Qualifier("onlSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "onlSqlSessionFactory")
    public SqlSessionFactory onlSqlSessionFactory(@Qualifier("onlDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);

        try {
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:sqlmapper/onlMapper/*.xml"));
            return bean.getObject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    @Bean(name = "onlTransactionManager")
    public DataSourceTransactionManager onlTransactionManager(@Qualifier("onlDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

四、generater 外掛配置檔案說明

以 Mgm 資料庫表的配置檔案 generatorMgmDb.xml 為例。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!--資料庫驅動-->
    <context id="mysql4" targetRuntime="MyBatis3">
        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>
        <!--資料庫連結地址賬號密碼-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/testmgmdb"
                        userId="root"
                        password="">
        </jdbcConnection>

        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!--生成Model類存放位置-->
        <javaModelGenerator targetPackage="com.test.svc.model.mgm" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!--生成對映檔案存放位置-->
        <sqlMapGenerator targetPackage="sqlmapper.mgmMapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!--生成Dao類存放位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.test.svc.dao.mgm" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--生成對應表及類名-->
        <table tableName="tbl_mgm_menu" domainObjectName="Menu" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
               selectByExampleQueryId="false">
        </table>
    </context>
</generatorConfiguration>
五、專案中 generater 外掛配置

點選 idea 選單欄 Run,單擊 Edit Configurations,新增 Maven,配置具體的專案路徑和命令列(mybatis-generator:generate),點選 Apply 和 OK。
在這裡插入圖片描述
依次修改 pom.xml 中的 generater 配置檔案,依次為每個表生成對應的 Model、Dao 和 Mapper 檔案。

六、建立記憶體資料重新整理和讀取方法

在記憶體中,儲存雙份資料,即開闢 A、B Cache,並定時重新整理。具體應重新整理和讀取哪塊資料,依賴標記量來區分。當重新整理 A 區資料時,讀取 B 區資料;當重新整理 B 區資料時,讀取 A 區資料。

package com.test.svc.utils;

import com.test.svc.ApplicationContextHelper;
import com.test.svc.constants.SvcConstant;
import com.test.svc.model.mgm.Menu;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;

import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Slf4j
public class DualBlockCache {

    /**
     * 記憶體塊取用與重新整理狀態標誌
     * 0:不可用(初態),下次定時任務重新整理A cache塊
     * 1:A cache塊可用,下次定時任務重新整理B cache塊
     * 2:B cache塊可用,下次定時任務重新整理A cache塊
     */
    public static int indicator = 0;

    // A cache快取Object
    private static HashMap<String,Object> blockObjectA = new HashMap<>();

    // B cache快取Object
    private static HashMap<String,Object> blockObjectB = new HashMap<>();

    // 快取單執行緒
    private static ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    // 快取例項
    private static DualBlockCache cache = new DualBlockCache();

    private DualBlockCache() {
    }

    /**
     * 根據key值查詢Object
     *
     * @param key 記憶體中的key值
     * @return 返回Object物件
     */
    public Object getObject(String key) {
        switch (indicator) {
            case 1:
                return blockObjectA.get(key);
            case 2:
                return blockObjectB.get(key);
            default:
                return null;
        }
    }

    public static DualBlockCache getInstance() {
        return cache;
    }

    public void start() {

        // 定時重新整理記憶體資料
        executorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                // 重新整理記憶體引數
                flushData();

                // 根據reloadCache引數開關判斷是否執行下次重新整理任務,0:關閉,不重新整理記憶體;1:開啟,定時重新整理記憶體,程序shutdown後不能恢復
                Environment environment = ApplicationContextHelper.applicationContext.getEnvironment();
                String reloadCache = PropertyUtil.getProperties(environment.getProperty("conf.file.path"),"reloadCache");
                if ("0".equals(reloadCache)) {
                    executorService.shutdown();
                }
            }
        },1L,600L,TimeUnit.SECONDS); // 專案啟動後1秒開始執行,此後10分鐘重新整理一次
    }

    /**
     * 重新整理記憶體資料
     */
    public void flushData() {

        log.info(DateUtils.now() + " start to reload A-B Cache task ===");

        HashMap<String,Object> blockObject = (indicator == 1) ? blockObjectB : blockObjectA;

        blockObject.clear();

        QueryCacheManageService queryCacheManageService = new QueryCacheManageService();

        List<Menu> listMenu = queryCacheManageService.listMenu();
        for (Menu menu : listMenu) {
            blockObject.put(SvcConstant.MENUINFO + menu.getMenuId(),menu);
        }

        indicator = (indicator == 1) ? 2 : 1;
        log.info("blockObject size : " + blockObject.size());
        log.info("記憶體資料重新整理完畢,indicator = {}",indicator);
    }
}

選單表資料列表讀取方法

package com.test.svc.utils;

import com.test.svc.constants.RecSt;
import com.test.svc.dao.mgm.MenuMapper;
import com.test.svc.model.mgm.Menu;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;

/**
 * @description 更新記憶體資料
 */
@Component
public class QueryCacheManageService {

    @Resource
    private MenuMapper menuMapper;

    private static QueryCacheManageService dbCacheMapper;

    @PostConstruct
    public void init() {

        dbCacheMapper = this;
        // 以下5行程式碼可以註釋,也可以保留
        dbCacheMapper.menuMapper = this.menuMapper;
    }

    public List<Menu> listMenu() {
        Menu menu = new Menu();
        menu.setRecSt(RecSt._1.getCode());
        return dbCacheMapper.menuMapper.selectSelective(menu);
    }
}

七、編寫測試方法
package com.test.svc.controller;

import com.alibaba.fastjson.JSON;
import com.test.svc.model.mgm.Menu;
import com.test.svc.service.QueryService;
import com.test.svc.utils.DualBlockCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @description 重新整理記憶體資料後臺入口
 */
@RestController
@Slf4j
public class TestController {

    @Resource
    private QueryService queryService;

    // 手動重新整理記憶體業務引數
    @RequestMapping("/flushData")
    public void flushData() {
        DualBlockCache.getInstance().flushData();
        log.info("手動重新整理快取完成");
    }

    // 手動調整記憶體重新整理標誌位為初始態
    @RequestMapping("/setFlag")
    public int setFlag(String indicator) {
        DualBlockCache.indicator = Integer.parseInt(indicator);
        return DualBlockCache.indicator;
    }

    // 獲取記憶體標誌位
    @RequestMapping("/getFlag")
    public int getFlag() {
        return DualBlockCache.indicator;
    }

    // 根據 menuId 獲取選單詳情(記憶體查詢)
    @RequestMapping("/getMenu")
    public Menu getMenu(String menuId) {

        log.info("記憶體查詢選單詳情,收到選單主鍵:[{}]",menuId);
        Menu menu = queryService.getMenu(menuId);
        log.info("記憶體查詢選單詳情,應答資料:[{}]",JSON.toJSONString(menu));
        return menu;
    }

    // 根據 menuId 獲取選單詳情(資料庫查詢)
    @RequestMapping("/getMenu1")
    public Menu getMenu1(String menuId) {

        log.info("資料庫查詢選單詳情,收到選單主鍵:[{}]",menuId);
        Menu menu = queryService.getMenu1(menuId);
        log.info("資料庫查詢選單詳情,應答資料:[{}]",JSON.toJSONString(menu));
        return menu;
    }
}

經測試,資料庫操作正常,記憶體資料可定時重新整理和讀取。