新建一个 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.YAML)
- 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 是可以保证多源的各自事务性的.
来源: http://blog.51cto.com/13981400/2323314