好的,作为一个合格的bug生产者,我们直接进入主题,多数据源和读写分离实现方案。

首先多数据源和读写分离什么时候我们才需要呢?

多数据源:一个单体项目过于复杂,需要操作多个业务库的时候,就需要多数据源操作不同的数据

多数据源使用 java 多数据源实现_数据库

读写分离:数据库压力较大时,我们考虑读写分离,主库写,从库读,减少数据库的压力。多个库数据是一样的。

多数据源使用 java 多数据源实现_数据源_02

理解完使用场景后,再入主题,怎么实现呢?这里说三种实现方式

1、扩展Spring的AbstractRoutingDataSource
2、通过Mybatis 配置不同的 Mapper 使用不同的 SqlSessionTemplate
3、分库分表中间件,比如Sharding-JDBC 、Mycat等。

好的,再让我们直入主题

扩展Spring的AbstractRoutingDataSource

多数据源

基于Spring AbstractRoutingDataSource做扩展,通过继承AbstractRoutingDataSource抽象类,实现一个管理多个 DataSource的数据源管理类。Spring 在获取数据源时,可以通过 数据源管理类 返回实际的 DataSource 。

然后我们可以定义一个注解,添加到service、dao上,表示一个实际的对应的datasource。

不过这个方式,对于spring事物的支持不好,多个数据源无法保障事物。这个问题是多数据源的通用问题了。

废话不多说,下面我们说下具体实现把,首先pom要引入的依赖的话很简单,就是一个springboot项目。

pom.xml

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
 <artifactId>mybatis-spring-boot-starter</artifactId>
 <version>2.1.2</version>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <scope>runtime</scope>
</dependency>
<dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <optional>true</optional>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
 <exclusions>
 <exclusion>
 <groupId>org.junit.vintage</groupId>
 <artifactId>junit-vintage-engine</artifactId>
 </exclusion>
 </exclusions>
</dependency>
<!--实现对 Druid 连接池的自动化配置-->
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid-spring-boot-starter</artifactId>
 <version>1.1.21</version>
</dependency>

application配置多数据源

server:
 port: 8080

spring:
 application:
 name: dynamic
 datasource:
 mall:
 url: jdbc:mysql://rm-xxxxx.mysql.rds.aliyuncs.com/luu_mall?useSSL=false&useUnicode=true&characterEncoding=UTF-8
 driver-class-name: com.mysql.jdbc.Driver
 username: root # 数据库账号
 password: root0319@ # 数据库密码
 type: com.alibaba.druid.pool.DruidDataSource # 设置类型为 DruidDataSource
 min-idle: 0 # 池中维护的最小空闲连接数,默认为 0 个。
 max-active: 20 # 池中最大连接数,包括闲置和使用中的连接,默认为 8 个。
 # 用户数据源配置
 users:
 url: jdbc:mysql://rm-xxxxxx.mysql.rds.aliyuncs.com/luu_user_center?useSSL=false&useUnicode=true&characterEncoding=UTF-8
 driver-class-name: com.mysql.jdbc.Driver
 username: root # 数据库账号
 password: root0319@ # 数据库密码
 type: com.alibaba.druid.pool.DruidDataSource # 设置类型为 DruidDataSource
 # Druid 自定义配置,对应 DruidDataSource 中的 setting 方法的属性
 min-idle: 0 # 池中维护的最小空闲连接数,默认为 0 个。
 max-active: 20 # 池中最大连接数,包括闲置和使用中的连接,默认为 8 个。
 # Druid 自定义配置,对应 DruidDataSource 中的 setting 方法的属性
 druid: # 设置 Druid 连接池的自定义配置。然后 DruidDataSourceAutoConfigure 会自动化配置 Druid 连接池。
 filter:
 stat: # 配置 StatFilter ,对应文档 https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter
 log-slow-sql: true # 开启慢查询记录
 slow-sql-millis: 5000 # 慢 SQL 的标准,单位:毫秒
 merge-sql: true # SQL合并配置
 stat-view-servlet: # 配置 StatViewServlet ,对应文档 https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE
 enabled: true # 是否开启 StatViewServlet
 login-username: root # 账号
 login-password: root # 密码

mybatis:
 mapper-locations: classpath:mapper/*.xml
 type-aliases-package: com.luu.druid.druid_demo.entity.*

配置多数据源DynamicDataSourceConfig

@Configuration
public class DynamicDataSourceConfig {

 /**
 * 创建 orders 数据源
 */
 @Bean(name = "mallDataSource")
 @ConfigurationProperties(prefix = "spring.datasource.mall") // 读取 spring.datasource.orders 配置到 HikariDataSource 对象
 public DataSource ordersDataSource() {
 return DruidDataSourceBuilder.create().build();
 }

 /**
 * 创建 users 数据源
 */
 @Bean(name = "usersDataSource")
 @ConfigurationProperties(prefix = "spring.datasource.users")
 public DataSource usersDataSource() {
 return DruidDataSourceBuilder.create().build();
 }

 @Bean
 @Primary
 public DynamiDataSource dataSource(DataSource mallDataSource, DataSource usersDataSource) {
 Map<Object, Object> targetDataSources = new HashMap<>(2);
 targetDataSources.put(DataSourceFlag.DATA_SOURCE_FLAG_MALL, mallDataSource);
 targetDataSources.put(DataSourceFlag.DATA_SOURCE_FLAG_USER, usersDataSource);
 // 还有数据源,在targetDataSources中继续添加
 System.out.println("DataSources:" + targetDataSources);
 //默认的数据源是oneDataSource
 return new DynamiDataSource(mallDataSource, targetDataSources);
 }

}

常量DataSourceFlag装载这我们区分数据源的key

public interface DataSourceFlag {

    
 public static String DATA_SOURCE_FLAG_MALL = "mall";
    
 public static String DATA_SOURCE_FLAG_USER = "user";

}

DynamiDataSource用来继承Spring AbstractRoutingDataSource来实现数据源切换,并且设置默认数据源。

public class DynamiDataSource extends AbstractRoutingDataSource {

 /**
 * 配置DataSource, defaultTargetDataSource为主数据库
 */
 public DynamiDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
 //设置默认数据源
 super.setDefaultTargetDataSource(defaultTargetDataSource);
 //设置数据源列表
 super.setTargetDataSources(targetDataSources);
 super.afterPropertiesSet();
 }
 @Override
 protected Object determineCurrentLookupKey() {
 return DynamicDataSourceHolder.getRouteKey();
 }
}

通过DynamicDataSourceHolder操作ThreadLocal来保存当前线程操作的哪个数据源

/**
 * 数据源管路由
 */
public class DynamicDataSourceHolder {
 private static ThreadLocal<String> routeKey = new ThreadLocal<String>();
 /**
 * 获取当前线程的数据源路由的key
 */
 public static String getRouteKey() {
 String key = routeKey.get();
 return key;
 }

 /**
 * 绑定当前线程数据源路由的key
 * 使用完成后必须调用removeRouteKey()方法删除
 */
 public static void setRouteKey(String key) {
 routeKey.set(key);
 }

 /**
 * 删除与当前线程绑定的数据源路由的key
 */
 public static void removeRouteKey() {
 routeKey.remove();
 }
}

到这里配置基本完成来,那要怎么用呢,如何切换数据源呢,这里我们上面有说到,通过注解,来切换数据源。所以定义一个注解ChangeDataSource,不同的key切换不同的数据源

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ChangeDataSource {
 String value() default DataSourceFlag.DATA_SOURCE_FLAG_MALL;
}

我们把注解用在mapper方法上

@Mapper
public interface TestMapper {

 int test();

 String mallNoAnno();

 @ChangeDataSource(DataSourceFlag.DATA_SOURCE_FLAG_MALL)
 String mallExitAnno();

 @ChangeDataSource(DataSourceFlag.DATA_SOURCE_FLAG_USER)
 String userNoAnno();

 @ChangeDataSource(DataSourceFlag.DATA_SOURCE_FLAG_USER)
 String userExitAnno();

}

然后通过切面DataSourceAspect更换ThreadLocal中key实现数据源切换

@Aspect
@Component
public class DataSourceAspect implements Ordered {
 protected Logger logger = LoggerFactory.getLogger(getClass());

 /**
 * 切点: 所有配置 ChangeDataSource 注解的方法
 */
 @Pointcut("@annotation(com.luu.druid.druid_demo.common.ChangeDataSource)")
 public void dataSourcePointCut() {}

 @Around("dataSourcePointCut()")
 public Object around(ProceedingJoinPoint point) throws Throwable {
 MethodSignature signature = (MethodSignature) point.getSignature();
 Method method = signature.getMethod();
 ChangeDataSource ds = method.getAnnotation(ChangeDataSource.class);
 // 通过判断 @ChangeDataSource注解 中的值来判断当前方法应用哪个数据源
 DynamicDataSourceHolder.setRouteKey(ds.value());
 System.out.println("当前数据源: " + ds.value());
 logger.debug("set datasource is " + ds.value());
 try {
 return point.proceed();
 } finally {
 DynamicDataSourceHolder.removeRouteKey();
 logger.debug("clean datasource");
 }
 }
 @Override
 public int getOrder() {
 return 1;
 }
}

单元测试

@SpringBootTest
class DruidDemoApplicationTests {

 @Autowired
 TestMapper testMapper;

 @Test
 void contextLoads() {
 int i = testMapper.test();

 String id = testMapper.mallNoAnno();

 String id2 = testMapper.mallExitAnno();

 String name = testMapper.userNoAnno();

 String name2 = testMapper.userExitAnno();
 }
}

到这里呢,代码基本写完来。这里就是多数据源的配置,然后还有读写分离怎么实现呢。

而上面我们说到事物上不起效果的,因为事物上要拿到数据源的连接对象,而这里我们在mapper层有更换数据源,所以是不行的,所以数据源无法切换成果,然后执行的时候会报错的。但是如果我们整个是在Service上使用这个注解,整个方法上同一个数据源就可以的。

实现读写分离

其实读写分离的实现通过上面的方式稍微修改下就可以来,就是在切面中,不在通过注解,根据方法名的前缀来判断是走主库,还是走从库。比如find、select这样读数据的就走从库,而insert这样的就走主库。具体的代码的话,摸一摸我发量不多的头,算了,偷一偷就不贴来,反正思路就是这样的。

通过Mybatis 配置不同的 Mapper 使用不同的 SqlSessionTemplate

根据不同操作类(就是mapper),然后创建不同的SqlSessionTemplate ,这样每个SqlSessionTemplate 就可以设置不同的数据源和扫描不同的mapper咯。听起来是不是很简单呢。不用管什么切面不切面的,不像上面那么麻烦咯。但是多数据源的通病还是在滴,那就是多数据源事物用起来不方便啦。

多数据源

还是用刚才的springboot项目吧,改动一下咯。pom文件啥的就不说咯,跟上面一样的。然后我们看下配置文件,数据源还是一样,两个数据源一样的配置,只不过这里没有mybatis的配置咯。

application

server:
 port: 8080

spring:
 application:
 name: dynamic
 datasource:
 mall:
 url: jdbc:mysql://rm-wz9yy0528x91z1iqdco.mysql.rds.aliyuncs.com/luu_mall?useSSL=false&useUnicode=true&characterEncoding=UTF-8
 driver-class-name: com.mysql.cj.jdbc.Driver
 username: root # 数据库账号
 password: root0319@ # 数据库密码
 type: com.alibaba.druid.pool.DruidDataSource # 设置类型为 DruidDataSource
 min-idle: 0 # 池中维护的最小空闲连接数,默认为 0 个。
 max-active: 20 # 池中最大连接数,包括闲置和使用中的连接,默认为 8 个。
 # 用户数据源配置
 users:
 url: jdbc:mysql://rm-wz9yy0528x91z1iqdco.mysql.rds.aliyuncs.com/luu_user_center?useSSL=false&useUnicode=true&characterEncoding=UTF-8
 driver-class-name: com.mysql.cj.jdbc.Driver
 username: root # 数据库账号
 password: root0319@ # 数据库密码
 type: com.alibaba.druid.pool.DruidDataSource # 设置类型为 DruidDataSource
 # Druid 自定义配置,对应 DruidDataSource 中的 setting 方法的属性
 min-idle: 0 # 池中维护的最小空闲连接数,默认为 0 个。
 max-active: 20 # 池中最大连接数,包括闲置和使用中的连接,默认为 8 个。
 # Druid 自定义配置,对应 DruidDataSource 中的 setting 方法的属性
 druid: # 设置 Druid 连接池的自定义配置。然后 DruidDataSourceAutoConfigure 会自动化配置 Druid 连接池。
 filter:
 stat: # 配置 StatFilter ,对应文档 https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter
 log-slow-sql: true # 开启慢查询记录
 slow-sql-millis: 5000 # 慢 SQL 的标准,单位:毫秒
 merge-sql: true # SQL合并配置
 stat-view-servlet: # 配置 StatViewServlet ,对应文档 https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE
 enabled: true # 是否开启 StatViewServlet
 login-username: root # 账号
 login-password: root # 密码

DBConstants常量类

public class DBConstants {
 public static final String TX_MANAGER_MALL = "malltransactionManager";
 public static final String TX_MANAGER_SER = "usertransactionManager";
}

然后就是创建不同的SqlSessionTemplate和数据源了。

DataSourceMallConfig配置mall的数据源,并且mallSqlSessionTemplate设置了扫面mapper包位置

@Configuration
@MapperScan(basePackages = "com.luu.druid.druid_demo.mapper.mall", sqlSessionTemplateRef = "mallSqlSessionTemplate")
public class DataSourceMallConfig {

 /**
 * 创建 mall 数据源
 */
 @Bean(name = "mallDataSource")
 @ConfigurationProperties(prefix = "spring.datasource.mall")
 public DataSource mallDataSource() {
 return DruidDataSourceBuilder.create().build();
 }

 /**
 * 创建 MyBatis SqlSessionFactory
 */
 @Bean(name = "mallSqlSessionFactory")
 public SqlSessionFactory sqlSessionFactory(DataSource mallDataSource) throws Exception {
 SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
 // <2.1> 设置 orders 数据源
 bean.setDataSource(mallDataSource);
 // <2.2> 设置 entity 所在包
 bean.setTypeAliasesPackage("com.luu.druid.druid_demo.entity.*");
 // <2.3> 设置 config 路径
// bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
 // <2.4> 设置 mapper 路径
 bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/mall/*.xml"));
 return bean.getObject();
 }

 /**
 * 创建 MyBatis SqlSessionTemplate
 */
 @Bean(name = "mallSqlSessionTemplate")
 public SqlSessionTemplate sqlSessionTemplate(DataSource mallDataSource) throws Exception {
 return new SqlSessionTemplate(this.sqlSessionFactory(mallDataSource));
 }

 /**
 * 创建 mall 数据源的 TransactionManager 事务管理器
 */
 @Bean(name = DBConstants.TX_MANAGER_MALL)
 public PlatformTransactionManager transactionManager(DataSource mallDataSource) {
 return new DataSourceTransactionManager(mallDataSource);
 }

}

DataSourceUserConfig配置user的数据源,并且userSqlSessionTemplate设置了扫面mapper包位置

@Configuration
@MapperScan(basePackages = "com.luu.druid.druid_demo.mapper.user", sqlSessionTemplateRef = "userSqlSessionTemplate")
public class DataSourceUserConfig {

 /**
 * 创建 user 数据源
 */
 @Bean(name = "userDataSource")
 @ConfigurationProperties(prefix = "spring.datasource.users")
 public DataSource userDataSource() {
 return DruidDataSourceBuilder.create().build();
 }

 /**
 * 创建 MyBatis SqlSessionFactory
 */
 @Bean(name = "userSqlSessionFactory")
 public SqlSessionFactory sqlSessionFactory(DataSource userDataSource) throws Exception {
 SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
 // <2.1> 设置 orders 数据源
 bean.setDataSource(userDataSource);
 // <2.2> 设置 entity 所在包
 bean.setTypeAliasesPackage("com.luu.druid.druid_demo.entity.*");
 // <2.3> 设置 config 路径
// bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
 // <2.4> 设置 mapper 路径
 bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/user/*.xml"));
 return bean.getObject();
 }

 /**
 * 创建 MyBatis SqlSessionTemplate
 */
 @Bean(name = "userSqlSessionTemplate")
 public SqlSessionTemplate sqlSessionTemplate(DataSource userDataSource) throws Exception {
 return new SqlSessionTemplate(this.sqlSessionFactory(userDataSource));
 }

 /**
 * 创建 user 数据源的 TransactionManager 事务管理器
 */
 @Bean(name = DBConstants.TX_MANAGER_SER)
 public PlatformTransactionManager transactionManager(DataSource userDataSource) {
 return new DataSourceTransactionManager(userDataSource);
 }

}

到这多数据源配置就差不多了,不同的mapper对应不同的数据源。这里mapper,entity啥的就不贴出来了,秉承着能偷懒就偷懒的一贯风格。直接把单元测试贴一下看下。

@SpringBootTest
class DruidDemoApplicationTests {

 @Autowired
 UserMapper userMapper;
 @Autowired
 TestMapper testMapper;

 @Test
 void contextLoads() {
 String id = testMapper.mallNoAnno();

 String id2 = testMapper.mallExitAnno();

 String name = userMapper.userNoAnno();

 String name2 = userMapper.userExitAnno();
 }
}

当然,上面说到说,依然是多数据源,所以呢对于事物的支持依然是有问题的。

读写分离

这种方式实现读写分离,就不用多说咯吧,我这个专业bug制造者都想的明白,各位大佬也能想明白的。

分库分表中间件,比如Sharding-JDBC 、Mycat等

对于分库分表的中间件,会解析我们编写的 SQL ,路由操作到对应的数据源。那么,它们天然就支持多数据源。如此,我们仅需配置好每个表对应的数据源,中间件就可以透明的实现多数据源或者读写分离。Sharding-JDBC 、Mycat是比较常用的中间件,这里使用的话就不写了,后面会专门写如何去使用它们的,Sharding-JDBC并且支持分布式事物的。