Spring Boot 整合Mybatis實現主從(多資料來源)分離方案
新建一個Maven專案,最終專案結構如下:
多資料來源注入到sqlSessionFactory
POM增加如下依賴:
<!--JSON--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-joda</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-parameter-names</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.11</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <!--mapper--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>1.1.0</version> </dependency> <!--pagehelper--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.1.0</version> <exclusions> <exclusion> <artifactId>mybatis-spring-boot-starter</artifactId> <groupId>org.mybatis.spring.boot</groupId> </exclusion> </exclusions> </dependency>
這裡需要注意的是:專案是通過擴充套件mybatis-spring-boot-starter的org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration來實現多資料來源注入的。在mybatis-spring-boot-starter:1.2.0中,該類取消了預設建構函式,因此本專案依舊使用1.1.0版本。需要關注後續版本是否會重新把擴充套件開放處理。
之所以依舊使用舊方案,是我個人認為開放擴充套件是合理的,相信在未來的版本中會迴歸。
增加主從庫配置(application.yml)
druid: type: com.alibaba.druid.pool.DruidDataSource master: url: jdbc:mysql://192.168.249.128:3307/db-test?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true driver-class-name: com.mysql.jdbc.Driver username: root password: root initial-size: 5 min-idle: 1 max-active: 100 test-on-borrow: true slave: url: jdbc:mysql://192.168.249.128:3317/db-test?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.jdbc.Driver username: root password: root initial-size: 5 min-idle: 1 max-active: 100 test-on-borrow: true
建立資料來源
@Configuration @EnableTransactionManagement public class DataSourceConfiguration { @Value("${druid.type}") private Class<? extends DataSource> dataSourceType; @Bean(name = "masterDataSource") @Primary @ConfigurationProperties(prefix = "druid.master") public DataSource masterDataSource(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix = "druid.slave") public DataSource slaveDataSource1(){ return DataSourceBuilder.create().type(dataSourceType).build(); } }
將多資料來源注入到sqlSessionFactory中
前面提到了這裡通過擴充套件mybatis-spring-boot-starter的org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration來實現多資料來源注入的
@Configuration
@AutoConfigureAfter({DataSourceConfiguration.class})
public class MybatisConfiguration extends MybatisAutoConfiguration {
private static Log logger = LogFactory.getLog(MybatisConfiguration.class);
@Resource(name = "masterDataSource")
private DataSource masterDataSource;
@Resource(name = "slaveDataSource")
private DataSource slaveDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
return super.sqlSessionFactory(roundRobinDataSouceProxy());
}
public AbstractRoutingDataSource roundRobinDataSouceProxy(){
ReadWriteSplitRoutingDataSource proxy = new ReadWriteSplitRoutingDataSource();
Map<Object,Object> targetDataResources = new ClassLoaderRepository.SoftHashMap();
targetDataResources.put(DbContextHolder.DbType.MASTER,masterDataSource);
targetDataResources.put(DbContextHolder.DbType.SLAVE,slaveDataSource);
proxy.setDefaultTargetDataSource(masterDataSource);//預設源
proxy.setTargetDataSources(targetDataResources);
return proxy;
}
}
實現讀寫分離(多資料來源分離)
這裡主要思路如下:
1-將不同的資料來源標識記錄在ThreadLocal中
2-通過註解標識出當前的service方法使用哪個庫
3-通過Spring AOP實現攔截註解並注入不同的標識到threadlocal中
4-獲取源的時候通過threadlocal中不同的標識給出不同的sqlSession
標識存放ThreadLocal的實現
public class DbContextHolder {
public enum DbType{
MASTER,SLAVE
}
private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<>();
public static void setDbType(DbType dbType){
if(dbType==null)throw new NullPointerException();
contextHolder.set(dbType);
}
public static DbType getDbType(){
return contextHolder.get()==null?DbType.MASTER:contextHolder.get();
}
public static void clearDbType(){
contextHolder.remove();
}
}
註解實現
/**
* 該註解註釋在service方法上,標註為連結slaves庫
* Created by Jason on 2017/3/6.
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnlyConnection {
}
Spring AOP對註解的攔截
@Aspect
@Component
public class ReadOnlyConnectionInterceptor implements Ordered {
public static final Logger logger = LoggerFactory.getLogger(ReadOnlyConnectionInterceptor.class);
@Around("@annotation(readOnlyConnection)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint,ReadOnlyConnection readOnlyConnection) throws Throwable {
try {
logger.info("set database connection to read only");
DbContextHolder.setDbType(DbContextHolder.DbType.SLAVE);
Object result = proceedingJoinPoint.proceed();
return result;
}finally {
DbContextHolder.clearDbType();
logger.info("restore database connection");
}
}
@Override
public int getOrder() {
return 0;
}
}
根據標識獲取不同源
這裡我們通過擴充套件AbstractRoutingDataSource來獲取不同的源。它是Spring提供的一個可以根據使用者發起的不同請求去轉換不同的資料來源,比如根據使用者的不同地區語言選擇不同的資料庫。通過檢視原始碼可以發現,它是通過determineCurrentLookupKey()返回的不同key到sqlSessionFactory中獲取不同源(前面已經展示瞭如何在sqlSessionFactory中注入多個源)
public class ReadWriteSplitRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
以上就完成了讀寫分離(多資料來源)的配置方案。下面是一個具體的例項
使用方式
Entity
@Table(name = "t_sys_dic_type")
public class DicType extends BaseEntity{
String code;
String name;
Integer status;
...
}
Mapper
public interface DicTypeMapper extends BaseMapper<DicType> {
}
Service
@Service
public class DicTypeService {
@Autowired
private DicTypeMapper dicTypeMapper;
@ReadOnlyConnection
public List<DicType> getAll(DicType dicType){
if (dicType.getPage() != null && dicType.getRows() != null) {
PageHelper.startPage(dicType.getPage(), dicType.getRows());
}
return dicTypeMapper.selectAll();
}
}
注意這裡的@ReadOnlyConnection註解
Controller
@RestController
@RequestMapping("/dictype")
public class DicTypeController {
@Autowired
private DicTypeService dicTypeService;
@RequestMapping(value = "/all")
public PageInfo<DicType> getALL(DicType dicType){
List<DicType> dicTypeList = dicTypeService.getAll(dicType);
return new PageInfo<>(dicTypeList);
}
}
通過mvn spring-boot:run啟動後
後臺打印出
c.a.d.m.ReadOnlyConnectionInterceptor : set database connection to read only
說明使用了從庫的連結獲取資料
備註:如何保證多源事務呢?br/>1-在讀寫分離場景中不會考慮主從庫事務,在純讀的上下文上使用@ReadOnlyConnection標籤。其他則預設使用主庫。
2-在多源場景中,Spring的@Transaction是可以保證多源的各自事務性的。