好的,作为一个合格的bug生产者,我们直接进入主题,多数据源和读写分离实现方案。
首先多数据源和读写分离什么时候我们才需要呢?
多数据源:一个单体项目过于复杂,需要操作多个业务库的时候,就需要多数据源操作不同的数据
读写分离:数据库压力较大时,我们考虑读写分离,主库写,从库读,减少数据库的压力。多个库数据是一样的。
理解完使用场景后,再入主题,怎么实现呢?这里说三种实现方式
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并且支持分布式事物的。
如果需要可以下载代码试试的dynamic-datasource-spring、dynamic-datasource-mybatis,到此完美收工:
https://github.com/servef-toto/luu_yinchuishiting.gitgithub.com