springboot+mybatis+druid實現mysql主從讀寫分離(五)

+小志+9發表於2020-11-18

一、如何自定義註解

1、使用格式

  • 修飾符

    訪問修飾符必須為public,不寫預設為public

  • 關鍵字

    關鍵字為@interface;

  • 註解名稱

    自定義名稱

  • 註解型別元素

    註解型別元素是註解中內容,可以理解成自定義介面的實現部分;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceSign {

	/**
     * 切換資料來源名稱
     */
    //返回型別  方法  預設值。
    DbType value() default DbType.MASTER;
}

2、使用元註解

JDK中有一些元註解,主要有@Target,@Retention,@Document,@Inherited用來修飾註解。

具體參考

二、資料來源配置

spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://192.168.102.31:3306/test
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave1:
      jdbc-url: jdbc:mysql://192.168.102.56:3306/test
      username: pig   # 只讀賬戶
      password: 123456
      driver-class-name: com.mysql.j
      dbc.Driver

三、主從分離程式碼實現

參考另一種實現方式

1、資料來源型別

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceSign {

	/**
     * 切換資料來源名稱
     */
    DbType value() default DbType.MASTER;
}

2、設定獲取資料來源

public class DynamicDbContextHolder {

    public enum DbType {
        MASTER, SLAVE, OTHER
    }

    /**
     * 使用ThreadLocal維護變數,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,
     *  所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。
     */
    private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();

    /**
     * 設定資料來源變數
     * @param dbType
     */
    public static void setDbType(DbType dbType) {
        if (dbType == null) {
        	throw new NullPointerException();
        }        	
        contextHolder.set(dbType);
    }

    /**
     * 獲取資料來源變數
     * @return
     */
    public static DbType getDbType() {
        return contextHolder.get() == null ? DbType.MASTER : contextHolder.get();
    }

    /**
     * 清空資料來源變數
     */
    public static void clearDbType() {
        contextHolder.remove();
    }

}

3、自定義資料來源註解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSign {
    DbTypeEnum value() default DbTypeEnum.MASTER;
}

4、動態切換資料來源AOP切面處理

/**
 * 問題:自定義資料來源註解與@Transaction同時使用時不生效
 * 自定義資料來源註解與@Transaction註解同一個方法,會先執行@Transaction註解,即獲取資料來源在切換資料來源之前,所以會導致自定義註解失效。
 * 解決方法:定義切換資料來源的註解的AOP切面(DynamicDataSourceAspect )上新增註解【@Order(-1),ordel的value越小,就越先執行】,
 * 保證該AOP在@Transactional之前執行
 */
@Aspect
@Order(-1)
@Component
public class DataSourceAspect {

    @Pointcut("@annotation(com.jht.jsicp.common.dynamicdb.annotation.DataSourceSign)")
    public void dsPointCut() {

    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSourceSign dataSource = method.getAnnotation(DataSourceSign.class);
        if (dataSource != null) {
        	DynamicDbContextHolder.setDbType(dataSource.value());
        }
        
        try {
            return point.proceed();
        } finally {
            // 銷燬資料來源 在執行方法之後
        	DynamicDbContextHolder.clearDbType();
        }
    }
}
——————————————————————————————————另一種寫法———————————————————————————————————————————————
@Aspect
@Component
public class DynamicDbAspect implements PriorityOrdered {
	
	public static final Logger logger = LoggerFactory.getLogger(DynamicDbAspect.class);
	
	
	/**
	 * 切換到master主庫
	 * @param proceedingJoinPoint
	 * @param Page
	 * @return
	 * @throws Throwable
	 */
	@Around("@annotation(master)")
	public Object proceed(ProceedingJoinPoint proceedingJoinPoint, Master master) throws Throwable {
		try {
			//logger.info("set database connection to master only");
			DynamicDbContextHolder.setDbType(DynamicDbContextHolder.DbType.MASTER);
			Object result = proceedingJoinPoint.proceed();
			return result;
		} finally {
			DynamicDbContextHolder.clearDbType();
			//logger.info("restore master database connection");
		}
	}/**
	 * 切換到slave從庫
	 * @param proceedingJoinPoint
	 * @param Slave
	 * @return
	 * @throws Throwable
	 */
	@Around("@annotation(slave)")
	public Object proceed(ProceedingJoinPoint proceedingJoinPoint, Slave slave) throws Throwable {
		try {
			//logger.info("set database connection to slave only");
			DynamicDbContextHolder.setDbType(DynamicDbContextHolder.DbType.SLAVE);
			Object result = proceedingJoinPoint.proceed();
			return result;
		} finally {
			DynamicDbContextHolder.clearDbType();
			//logger.info("restore slave database connection");
		}
	}
	
	@Override
	public int getOrder() {
		return 1;
	}
}

5、動態資料來源決策

Spring boot提供了AbstractRoutingDataSource 根據使用者定義的規則選擇當前的資料來源,這樣我們可以在執行查詢之前,設定使用的資料來源。實現可動態路由的資料來源,在每次資料庫查詢操作前執行。它的抽象方法 determineCurrentLookupKey() 決定使用哪個資料來源。

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

	public static final Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
	
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDbContextHolder.getDbType();
    }
}

6、資料庫(源)、事務配置類

//參考
@Configuration
@EnableTransactionManagement
public class DataSourceConfiguration {
	
	public static final Logger logger = LoggerFactory.getLogger(DataSourceConfiguration.class);
	
	@Value("${druid.type}") #druid.type=com.alibaba.druid.pool.DruidDataSource
	private Class<? extends DataSource> dataSourceType;
	
	#mybatis配置
    #mybatis.mapper-locations=classpath*:mapper/*.xml
	@Value("${mybatis.mapper-locations}")
	private String mapperLocations;
	
	
	#mybatis.configLocation=classpath:mybatis-config.xml
	@Value("${mybatis.configLocation}")
	private String configLocation;
	
	#mybatis.type-aliases-package=com.jht.jscc
	@Value("${mybatis.type-aliases-package}")
	private String entityPackage;

	/**
	 * 主資料庫
	 * @return
	 */
	@Primary
	@Bean(name = "masterDataSource")
	@ConfigurationProperties(prefix = "druid.jsicp.master")
	public DataSource masterDataSource() {
		return DataSourceBuilder.create().type(dataSourceType).build();
	}
	/**
	 * 從資料庫(如有多個按這種方式增加)
	 * @return
	 */	
	@Bean(name = "slaveDataSource")
	@ConfigurationProperties(prefix = "druid.jsicp.slave")
	public DataSource slaveDataSource() {
		return DataSourceBuilder.create().type(dataSourceType).build();
	}
	
	/**
	 * 從資料庫(如有多個按這種方式增加)
	 * @return
	 */	
	@Bean(name = "otherDataSource")
	@ConfigurationProperties(prefix = "druid.jsicp.other")
	public DataSource otherDataSource() {
		return DataSourceBuilder.create().type(dataSourceType).build();
	}
	@Bean(name = "dataSource")
    public AbstractRoutingDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource ,
    		@Qualifier("slaveDataSource") DataSource slaveDataSource,
    		@Qualifier("otherDataSource") DataSource otherDataSource) {
        DynamicRoutingDataSource proxy = new DynamicRoutingDataSource();
        
        Map<Object, Object> targetDataResources = new HashMap<Object, Object>();
        targetDataResources.put(DynamicDbContextHolder.DbType.MASTER, masterDataSource);
        targetDataResources.put(DynamicDbContextHolder.DbType.SLAVE, slaveDataSource);      
        targetDataResources.put(DynamicDbContextHolder.DbType.OTHER, otherDataSource);      
        
        //設定預設資料來源是master,沒有新增註解的都是預設資料來源
        proxy.setDefaultTargetDataSource(masterDataSource);
        proxy.setTargetDataSources(targetDataResources);
        // 在方法初始化之前執行
        proxy.afterPropertiesSet();
        return proxy;
    }@Bean
//    @ConfigurationProperties(prefix = "mybatis")
	@Primary
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dataSource") DataSource dataSource) throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setConfigLocation(resolver.getResource(configLocation));
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocations));
        // 設定typeAlias 包掃描路徑
        sqlSessionFactoryBean.setTypeAliasesPackage(entityPackage);

        return sqlSessionFactoryBean;
    }
	
	// 事務管理
	@Bean
	public PlatformTransactionManager annotationDrivenTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
		return new DataSourceTransactionManager(dataSource);
	}
	
}
-----------------------------------------------------------------------------------
//實戰
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource(){
        return DataSourceBuilder.create().build();
    }


    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource myRoutingDataSource(DataSource masterDataSource, DataSource slaveDataSource){
        DynamicRoutingDataSource myRoutingDataSource = new DynamicRoutingDataSource();
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DbTypeEnum.MASTER, masterDataSource);
        targetDataSource.put(DbTypeEnum.SLAVE, slaveDataSource);

        myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
        myRoutingDataSource.setTargetDataSources(targetDataSource);
        return myRoutingDataSource;
    }
}

7、mybatis配置

@EnableTransactionManagement
@Configuration
public class MyBatisConfig {

    @Resource(name = "myRoutingDataSource")
    private DataSource myRoutingDataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager() {
        return new DataSourceTransactionManager(myRoutingDataSource);
    }
}

相關文章