讓媳婦瞬間搞懂Spring 多數據源操作(SpringBoot + Durid)

1、快速理解 Spring 多數據源操作

最近在調研 Spring 如何配置多數據源的操作,結果被媳婦吐槽,整天就坐在那打電腦,啥都不幹。於是我靈光一現,跟我媳婦說了一下調研結果,第一版本原話如下:

Spring 提供了一套多數據源的解決方案,通過繼承抽象 AbstractRoutingDataSource 定義動態路由數據源,然後可以通過AOP, 動態切換配置好的路由Key,來跳轉不同的數據源。

Spring ?春天 ?我在這幹活你還想有春天,還有那個什麼什麼抽象,我現在有點想抽你。好好說話 !

媳婦,莫急莫急,嗯… 等我重新組織一下語言先。我想了一會緩緩的對媳婦說:

生活中我們去景點買票或者購買火車票,一般都有軍人和殘疾人專門的售票窗口,普通人通過正常的售票窗口進行購票,軍人和殘疾人通過專門的優惠售票窗口進行購票。

爲了防止有人冒充軍人或殘疾人去優惠售票窗口進行購票,這就需要你提供相關的證件來證明。沒有證件的走正常售票窗口,有證件的走優惠售票窗口。

那如何判斷誰有證件,誰沒有證件呢? 這就需要辛苦檢查證件的工作人員。默認情況下,正常售票窗口通道是打開的,大家可以直接去正常的售票窗口進行購票。

如果有軍人或殘疾人來購票,工作人員檢查相關證件後,則關閉正常售票窗口通道,然後打開優惠窗口通道。

在理解了購票的流程後,我們在來看 Spring 動態數據源切換的解決方案就會容易很多。Spring 動態數據源解決方案與購票流程中的節點的對應如下:

  • 具體 Dao 訪問數據庫獲取的數據 = 景點票或火車票
  • 具體 Dao = 購票人員。
  • 具體 Dao目標數據源註解類的value值 = 證明是否是軍人或殘疾人的證件。
  • Spring 動態數據源 = 售票點。
  • 不同的數據源 = 正常售票窗口和優惠售票窗口。
  • AOP 動態修改動態數據源狀態類Key值 = 檢查證件工作人員去關閉和打開正常售票窗口和優惠售票窗口通道。
  • 動態數據源狀態類 = 不同購票窗口通道門打開或關閉狀態。
  • 動態路由狀態Key值的枚舉類 = 不同購票窗口通道的門。

具體執行流程圖如下:

在這裏插入圖片描述
你要這麼說:我就明白了,媳婦這會的語氣緩和了很多。但是你講這麼多有個毛線用 ! 拖地去 !

好嘞 ! 我拿起拖把瘋狂的拖了起來。

到這裏Spring 多數據源操作流程介紹完畢! 如果你想了解代碼的實現請接着往下看,如果您就想看看操作流程那麼感謝您的閱讀。記得關注加點贊哈 😁

正所謂光說不練假把式,說了這麼多操作流程的介紹,接下來開始正式的實戰操作。在實戰操作前我先說一下實戰操作內容以及注意事項:

實戰操作的主要內容介紹瞭如何在 SpringBoot 項目下,通過 MyBatis + Durid 數據庫連接池來配置不同數據源的操作。

閱讀本文需要你熟悉 SpringBoot 和 MyBatis 的基本操作即可,另外需要注意的是實戰操作的代碼環境如下:

  • SpringBoot:2.1.0.RELEASE
  • MyBatis:3.4.0 (mybatis-spring-boot-starter:1.1.1)
  • JDK:1.8.0_251
  • Durid:1.1.10 (druid-spring-boot-starter:1.1.10 )
  • Maven:3.6.2

按照本文進行操作的過程中,請儘量保持你的環境版本和上述一致,如出現問題可以查看本文末尾處的 GitHub 項目倉庫的代碼進行對比。

2、整合多數源實戰操作

2.1、數據庫準備

這裏通過商品庫的商品表和旅館庫的旅館表來模擬多數據源的場景,具體建庫以及建表 SQL 如下:

需要注意的是,我本地環境使用的是 MySql 5.6 。

創建商品庫以及商品表的 Sql。

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `product`
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
  `id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `product_name` varchar(25) DEFAULT NULL COMMENT '商品名稱',
  `price` decimal(8,3) DEFAULT NULL COMMENT '價格',
  `product_brief` varchar(125) DEFAULT NULL COMMENT '商品簡介',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
--  Records of `product`
-- ----------------------------
BEGIN;
INSERT INTO `product` VALUES ('2', '蘋果', '20.000', '好喫的蘋果,紅富士大蘋果');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

創建旅館庫和旅館表 Sql

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `hotel`
-- ----------------------------
DROP TABLE IF EXISTS `hotel`;
CREATE TABLE `hotel` (
  `id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '旅館id',
  `city` varchar(125) DEFAULT NULL COMMENT '城市',
  `name` varchar(125) DEFAULT NULL COMMENT '旅館名稱',
  `address` varchar(256) DEFAULT NULL COMMENT '旅館地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
--  Records of `hotel`
-- ----------------------------
BEGIN;
INSERT INTO `hotel` VALUES ('1', '北京', '漢庭', '朝陽區富明路112號');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

2.2、SpringBoot 項目多數據源yml 配置

搭建 SpringBoot 項目這裏不在進行介紹,搭建好SpringBoot 項目的第一步就是進行項目的 yml 配置。

application.yml 的配置代碼如下:

server:
  port: 8080

#mybatis:
  #config-location: classpath:mybatis-config.xml
  #mapper-locations: classpath*:mapper/**/*Mapper.xml


spring:
  datasource:
    #初始化時建立物理連接的個數
    #type: com.alibaba.druid.pool.DruidDataSource
    druid:
      product:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/product?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
        initial-size: 15
        #最小連接池數量
        min-idle: 10
        #最大連接池數量
        max-active: 50
        #獲取連接時最大等待時間
        max-wait: 60000
        # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一個連接在池中最小生存的時間,單位是毫秒
        minEvictableIdleTimeMillis: 300000
        # 配置一個連接在池中最大生存的時間,單位是毫秒
        maxEvictableIdleTimeMillis: 9000000
        # 配置檢測連接是否有效
        validationQuery: SELECT 1 FROM DUAL
        #配置監控頁面訪問登錄名稱
        stat-view-servlet.login-username: admin
        #配置監控頁面訪問密碼
        stat-view-servlet.login-password: admin
        #是否開啓慢sql查詢監控
        filter.stat.log-slow-sql: true
        #慢SQL執行時間
        filter.stat.slow-sql-millis: 1000
      hotel:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/hotel?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
        initial-size: 15
        #最小連接池數量
        min-idle: 10
        #最大連接池數量
        max-active: 50
        #獲取連接時最大等待時間
        max-wait: 60000
        # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一個連接在池中最小生存的時間,單位是毫秒
        minEvictableIdleTimeMillis: 300000
        # 配置一個連接在池中最大生存的時間,單位是毫秒
        maxEvictableIdleTimeMillis: 9000000
        # 配置檢測連接是否有效
        validationQuery: SELECT 1 FROM DUAL
        #配置監控頁面訪問登錄名稱
        stat-view-servlet.login-username: admin
        #配置監控頁面訪問密碼
        stat-view-servlet.login-password: admin
        #是否開啓慢sql查詢監控
        filter.stat.log-slow-sql: true
        #慢SQL執行時間
        filter.stat.slow-sql-millis: 1000

多數據源情況下,原先的 Mybatis 相關配置不會起作用。Mybaies 配置均在定義多個數據源的配置類進行。

2.3、自定義動態路由數據源

自定義動態路由數據源是整個操作中最爲重要的環節,因爲整個切換數據源過程都是通過操作它來完成的。

第一步,創建動態數據源狀態類以及動態路由狀態Key值的枚舉類,具體代碼如下:

動態路由狀態Key值的枚舉類

public enum DataSourceKeyEnum {
    HOTEL, PRODUCT
}

動態數據源狀態類

public class DynamicDataSourceRoutingKeyState {

    private static Logger log = LoggerFactory.getLogger(DynamicDataSourceRoutingKeyState.class);
    // 使用ThreadLocal保證線程安全
    private static final ThreadLocal<DataSourceKeyEnum> TYPE = new ThreadLocal<DataSourceKeyEnum>();

    // 往當前線程裏設置數據源類型
    public static void setDataSourceKey(DataSourceKeyEnum dataSourceKey) {
        if (dataSourceKey == null) {
            throw new NullPointerException();
        }
        log.info("[將當前數據源改爲]:{}",dataSourceKey);
        TYPE.set(dataSourceKey);
    }

    // 獲取數據源類型
    public static DataSourceKeyEnum getDataSourceKey() {
        DataSourceKeyEnum dataSourceKey = TYPE.get();
        log.info("[獲取當前數據源的類型爲]:{}",dataSourceKey);
        System.err.println("[獲取當前數據源的類型爲]:" + dataSourceKey);
        return dataSourceKey;
    }

    // 清空數據類型
    public static void clearDataSourceKey() {
        TYPE.remove();
    }
}

第二步,通過繼承 Spring 提供的抽象類 AbstractRoutingDataSource 來創建動態數據源,具體代碼如下:

public class DynamicDataSourceRouting extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceKeyEnum dataSourceKey = DynamicDataSourceRoutingKeyState.getDataSourceKey();
        return dataSourceKey;
    }
}

2.4、定義多個數據源的配置類

第一步,配置商品庫數據源、旅館庫數據源、動態數據源、MyBatis SqlSessionFactory 。

配置商品庫數據源代碼。

@Configuration
public class DataSourceConfig {

    /**
     * 商品庫的數據源
     * @return
     */
    @Bean(name = "dataSourceForProduct")
    @ConfigurationProperties(prefix="spring.datasource.druid.product")
    public DruidDataSource dataSourceForProduct() {
        return DruidDataSourceBuilder.create().build();
    }
}    

配置旅館庫的數據源代碼。

    /**
     * 旅館庫的數據源
     * @return
     */
    @Bean(name = "dataSourceForHotel")
    @ConfigurationProperties(prefix="spring.datasource.druid.hotel")
    public DruidDataSource dataSourceForHotel() {
        return DruidDataSourceBuilder.create().build();
    }
    

配置動態路由的數據源代碼。

    /**
     * 動態切換的數據源
     * @return
     */
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {

        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceKeyEnum.PRODUCT, dataSourceForProduct());
        targetDataSource.put(DataSourceKeyEnum.HOTEL, dataSourceForHotel());
        //設置默認的數據源和以及多數據源的Map信息
        DynamicDataSourceRouting dataSource = new DynamicDataSourceRouting();
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(dataSourceForProduct());
        return dataSource;
    }

配置 MyBatis SqlSessionFactory 並指定動態數據源代碼。

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        //設置數據數據源的Mapper.xml路徑
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
        //設置Mybaties查詢數據自動以駝峯式命名進行設值
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session
                .Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        bean.setConfiguration(configuration);

        return bean.getObject();
    }

配置數據源注入事務 DataSourceTransactionManager 代碼。

/**
     * 注入 DataSourceTransactionManager 用於事務管理
     */
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dynamicDataSource);
        return new DataSourceTransactionManager(dynamicDataSource);
    }

配置商品庫數據源、配置旅館庫的數據源、配置動態路由的數據源、配置MyBatis SqlSessionFactory 、配置數據源注入事務 代碼均在 DataSourceConfig 配置類中。

第二步,配置數據源的事務 AOP 切面類。

添加事務AOP切面類,通過方法名前綴來配置其事務。

  • add、save、insert、update、delete、其他前綴的方法:讀寫事務。
  • get、find、query前綴的方法:只讀事務。
@Aspect
@Configuration
public class TransactionConfiguration {
    private static final int TX_METHOD_TIMEOUT = 5;
    private static final String AOP_POINTCUT_EXPRESSION = "execution( * cn.lijunkui.service.*.*(..))";

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Bean
    public TransactionInterceptor txAdvice() {
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        /* 只讀事務,不做更新操作 */
        RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
        readOnlyTx.setReadOnly(true);
        readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
        /* 當前存在事務就使用當前事務,當前不存在事務就創建一個新的事務 */
        RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
        requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
        requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        //requiredTx.setTimeout(TX_METHOD_TIMEOUT);
        Map<String, TransactionAttribute> txMap = new HashMap<String, TransactionAttribute>();
        txMap.put("add*", requiredTx);
        txMap.put("save*", requiredTx);
        txMap.put("insert*", requiredTx);
        txMap.put("update*", requiredTx);
        txMap.put("delete*", requiredTx);
        txMap.put("get*", readOnlyTx);
        txMap.put("find*", readOnlyTx);
        txMap.put("query*", readOnlyTx);
        txMap.put("*", requiredTx);
        source.setNameMap(txMap);
        TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager, source);
        return txAdvice;
    }

    @Bean
    public Advisor txAdviceAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

2.5、定義AOP動態切換配置數據源的操作

指定Dao目標數據源註解類。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    DataSourceKeyEnum value() default DataSourceKeyEnum.PRODUCT;
}

Dao 訪問數據庫進行攔截的 Aop 切面類。

@Aspect
@Component
public class DataSourceAop {

    Logger log = LoggerFactory.getLogger(DataSourceAop.class);

    @Pointcut("execution( * cn.lijunkui.dao.*.*(..))")
    public void daoAspect() {
    }
    @Before(value="daoAspect()")
    public void switchDataSource(JoinPoint joinPoint) throws NoSuchMethodException {
        log.info("開始切換數據源");

        //獲取HotelMapper or ProductMapper 類上聲明的TargetDataSource的數據源註解的值
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Class<?> declaringClass  = methodSignature.getMethod().getDeclaringClass();
        TargetDataSource annotation = declaringClass.getAnnotation(TargetDataSource.class);
        DataSourceKeyEnum value = annotation.value();
        log.info("數據源爲:{}",value);

        //根據TargetDataSource的value設置要切換的數據源
        DynamicDataSourceRoutingKeyState.setDataSourceKey(value);
    }
}

到這裏多數據源配置操作介紹完畢!

3、測試

正所謂沒有測試的代碼都是耍流氓,接下來通過分別定義訪問商品庫和旅館的 Controller、Service、Dao。並進行驗證上述配置是否有效。

旅館 Controller

@RestController
public class HotelController {

    @Autowired
    private HotelService hotelService;

    /**
     * 查詢所有的旅館信息
     * @return
     */
    @GetMapping("/hotel")
    public List<Hotel> findAll(){
        List<Hotel> hotelList = hotelService.findAll();
        return hotelList;
    }
}

旅館 Service

@Service
public class HotelService {

    @Autowired
    private HotelMapper hotelMapper;

    public List<Hotel> findAll(){
        List<Hotel> hotels = hotelMapper.selectList();
        return hotels;
    }
}

旅館 Dao

@Mapper
@TargetDataSource(value = DataSourceKeyEnum.HOTEL )
public interface HotelMapper {


	/**
	 * 查詢所有
	 * @return List<Hotel>
	 */
	List<Hotel> selectList();
}

旅館 Mapper.xml 文件配置。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lijunkui.dao.HotelMapper">
 

	 
	<sql id="baseSelect">
        select id, city, name, address from hotel
    </sql>
    
    <select id="selectList"  resultType="cn.lijunkui.domain.Hotel">
        <include refid="baseSelect"/>
    </select>
</mapper>

商品 Controller

@RestController
public class ProductController {

	@Autowired
	private ProductService productService;

	/**
	 * 查詢所有的商品信息
	 * @return
	 */
	@GetMapping("/product")
	public List<Product> findAll() {
        List<Product> productList = productService.findAll();
        return productList;
	}
}

商品Service

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    public List<Product> findAll(){
        return productMapper.selectList();
    }
}

商品Dao

@Mapper
@TargetDataSource(value = DataSourceKeyEnum.PRODUCT )
public interface ProductMapper {

    /**
     * 查詢所有
     * @param
     * @return List<Hotel>
     */
    List<Product> selectList();
}

商品的Mapper.xml 配置

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lijunkui.dao.ProductMapper">
    
    <select id="selectList"  resultType="cn.lijunkui.domain.Product">
        select * from product
    </select>
</mapper>

通過 http://localhost:8080/product 訪問獲取所有的商品信息,具體效果如下圖
在這裏插入圖片描述
後臺日誌信息如下:

2020-06-20 11:44:07.927  INFO 1234 --- [nio-8080-exec-1] cn.lijunkui.config.DataSourceAop         : 開始切換數據源
2020-06-20 11:44:07.928  INFO 1234 --- [nio-8080-exec-1] cn.lijunkui.config.DataSourceAop         : 數據源爲:PRODUCT
2020-06-20 11:44:07.930  INFO 1234 --- [nio-8080-exec-1] c.l.c.DynamicDataSourceRoutingKeyState   : [將當前數據源改爲]:PRODUCT
2020-06-20 11:44:07.942  INFO 1234 --- [nio-8080-exec-1] c.l.c.DynamicDataSourceRoutingKeyState   : [獲取當前數據源的類型爲]:PRODUCT

通過 http://localhost:8080/hotel 訪問獲取所有的旅館信息,具體效果如下圖
在這裏插入圖片描述
後臺日誌信息如下:

[獲取當前數據源的類型爲]:PRODUCT
2020-06-20 11:44:08.252  INFO 1234 --- [nio-8080-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] cn.lijunkui.config.DataSourceAop         : 開始切換數據源
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] cn.lijunkui.config.DataSourceAop         : 數據源爲:HOTEL
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] c.l.c.DynamicDataSourceRoutingKeyState   : [將當前數據源改爲]:HOTEL
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] [獲取當前數據源的類型爲]:HOTEL
c.l.c.DynamicDataSourceRoutingKeyState   : [獲取當前數據源的類型爲]:HOTEL

4、小結

本文介紹了 Spring 通過繼承抽象 AbstractRoutingDataSource 定義動態路由來完成多數據源切換的實戰以及代碼執行流程。

Spring 提供的動態數據源的機制就是將多個數據源通過 Map 進行維護,具體使用哪個數據源通過 determineCurrentLookupKey 方法返回的 Key 來確定。通過 AOP 動態修改 determineCurrentLookupKey 方法返回的Key,來完成切換數據源的操作。

本文介紹了訪問不同數據庫業務的實現,通過這種方式也可以搭建相同業務多個一致的數據庫的讀寫分離。你可以嘗試使用這種方式來實現,歡迎大家在評論區說說你實現讀寫分離的方案。

5、代碼示例

操作過程如出現問題可以在我的GitHub 倉庫 springbootexamples 中模塊名爲 spring-boot-2.x-mybaties-multipleDataSource 項目中進行對比查看

GitHub:https://github.com/zhuoqianmingyue/springbootexamples

如果您對這些感興趣,歡迎 star、或轉發給予支持!轉發請標明出處!

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