带薪学习:本文主要介绍主从数据库,在SpringBoot项目中我们需要连接多个数据源,多个数据库可能存在不同的服务上边,比如张三对MySQLA数据库只有读取数据的权限,对MySQLB数据库只有写数据的权限。这样的话一个项目中读取数据就要配置A的数据源,写入数据要配置B的数据源,这样就构成了多数据源切换问题。
对于大多数的Java应用,都使用了spring架,spring-jdbc模块提供了AbstractRoutingDataSource,其内部可以包含了多个DataSoure,,然后在运行时来动态切换的访问数据库。这种方式对访问数据库的架构图如下所示:
应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(dbs1、dbs2)选择一个,来访问对应的数据库。
application.yaml
spring:
datasource:
#主数据源
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://----:7788/guoguo?characterEncoding=utf-8&serviceTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 10
minIdle: 10
maxActive: 30
#从数据库
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://-----.mysql.rds.aliyuncs.com:3306/platform_crawler?characterEncoding=utf-8&serviceTimezone=UTC
username: -----
password: -----
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 10
minIdle: 10
maxActive: 30
masterslave:
# 读写分离配置
load-balance-algorithm-type: round_robin
# 最终的数据源名称
name: dataSource
# 主库数据源名称
master-data-source-name: master
# 从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props:
# 开启SQL显示,默认false
sql:
show: true
DataSourceConfig.java
package com.guo.mysql.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Slf4j
@Configuration
public class DataSourceConfig {
/**
* 主数据源
* @return DataSource
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
return DruidDataSourceBuilder.create().build();
}
/**
* 从 数据源
* @return DataSource
*/
@Bean //
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
return DruidDataSourceBuilder.create().build();
}
}
DynamicDataSource.java
package com.guo.mysql;
import com.guo.mysql.enums.DataSourceEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Component
@Primary //将该Bean设置为主要注入Bean implements DataSource, InitializingBean
public class DynamicDataSource extends AbstractRoutingDataSource {
//当前使用的数据源标识
public static ThreadLocal<String> name = new ThreadLocal<>();
//主写
@Autowired
DataSource masterDataSource;
//从读
@Autowired
DataSource slaveDataSource;
//返回当前数据源标识
@Override
public Object determineCurrentLookupKey() {
return name.get();
}
/**
* spring容器启动的时候被调用
*/
@Override
public void afterPropertiesSet() {
//为targetDataSources初始化所有数据源
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.主库.getName(), masterDataSource);
targetDataSources.put(DataSourceEnum.从库.getName(), slaveDataSource);
super.setTargetDataSources(targetDataSources);
//为defaultTargetDataSource设置默认的数据源
super.setDefaultTargetDataSource(slaveDataSource); //从读
super.afterPropertiesSet();
}
}
SpringbootSynchronizeMysqlApplicationTests.java
测试类,根据指定数据库名称,切换数据源连接,进行操作不同的数据库
package com.guo.mysql;
import com.guo.mysql.enums.DataSourceEnum;
import com.guo.mysql.service.UserOsskeyService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootSynchronizeMysqlApplicationTests {
@Test
void contextLoads() {
}
@Autowired
private UserOsskeyService userOsskeyService;
@Test
public void test(){
DynamicDataSource.name.set(DataSourceEnum.从库.getName());
System.out.println(userOsskeyService.count());
DynamicDataSource.name.set(DataSourceEnum.主库.getName());
System.out.println(userOsskeyService.count());
}
}
输出:
2022-11-15 11:03:11.165 INFO 11692 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
569548
2022-11-15 11:03:20.525 INFO 11692 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-2} inited
1
DataSourceEnum.java
为了提高代码的可用性,在代码中切换数据源按照名称切换,这里把名称写在枚举中,后续如有修改直接在枚举类中修改就可以了,不需要去每个类中去修改,提高代码的可用性
package com.guo.mysql.enums;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
public enum DataSourceEnum {
主库("master", "w"),
从库("slave", "r"),
NULL("-1", "");
private String name;
private String exec;
DataSourceEnum(String name, String exec) {
this.name = name;
this.exec = exec;
}
public String getName() {
return name;
}
public String getExec() {
return exec;
}
private static final Map<String, DataSourceEnum> codeMap = new HashMap<>((int) (values().length / .75f) + 1);
static {
for(DataSourceEnum item : values()) {
codeMap.put(item.name, item);
}
}
public static DataSourceEnum fromCode(String name) {
DataSourceEnum item = codeMap.get(name);
return item == null ? NULL : item;
}
public static Collection<DataSourceEnum> all() {
EnumSet<DataSourceEnum> enumSet = EnumSet.allOf(DataSourceEnum.class);
enumSet.remove(NULL);
return enumSet;
}
}
以上代码虽然可以实现基本数据源的切换,还存在一些问题
- 数据源表示设置代码耦合性过高,对后期维护造成很大的麻烦
为了解决数据库的读性能瓶颈(读比写性能更高,写锁会影响读阻塞,从而影响读的性能)
很对数据拥有主从架构,也就是,一台主
数据库服务器,是对外提供增删改
业务的生产服务器;另外一(多)台从
数据库服务器,主要进行读
操作。
可以通过中间件(ShardingSphere, mycat,mysql-proxy,TDDL …)
这里的架构上类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert,update,delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力
DynamicDataSourcePlugin.java
package com.guo.mysql.plugin;
import com.guo.mysql.DynamicDataSource;
import com.guo.mysql.enums.DataSourceEnum;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
//Mybatis拦截器注解
//@Intercepts:标识该类是一个拦截器
//@Signature:拦截器相关属性设置
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class,method = "query", args={MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class})
})
public class DynamicDataSourcePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//拿到当前方法(update, query)所有参数
Object[] objects = invocation.getArgs();
//MappedStatement 封装SQL
MappedStatement ms = (MappedStatement) objects[0];
//读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
DynamicDataSource.name.set(DataSourceEnum.从库.getName());
}else {
//写操作
DynamicDataSource.name.set(DataSourceEnum.主库.getName());
}
// 修改当前线程要选择的数据源的Key
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
package com.guo.mysql.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.guo.mysql.plugin.DynamicDataSourcePlugin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.plugin.Interceptor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Slf4j
@Configuration
public class DataSourceConfig {
/**
* 主数据源
* @return DataSource
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
return DruidDataSourceBuilder.create().build();
}
/**
* 从 数据源
* @return DataSource
*/
@Bean //
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
return DruidDataSourceBuilder.create().build();
}
//mybatisplus 注入切换数据源的插件bean对象
@Bean
public Interceptor dynamicDataSourcePlugin(){
return new DynamicDataSourcePlugin();
}
}
核心代码解读
//拿到当前方法(update, query)所有参数
Object[] objects = invocation.getArgs();
//MappedStatement 封装SQL
MappedStatement ms = (MappedStatement) objects[0];
//读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
DynamicDataSource.name.set(DataSourceEnum.从库.getName());
}else {
//写操作
DynamicDataSource.name.set(DataSourceEnum.主库.getName());
}
通过学习spring底层源码了解到,当我们操作查询SELECT操作的时候调用从库(从库读取数据),操作添加更新删除操作的时候 INSERT, UPDATE, DELETE,调用我们的主库(主库用来增删改操作);
通过上述判断修改当前线程要选择的数据源的Key
数据分布在不同的数据库中,数据库拆了,应用没有拆,一个公司多个子项目,各用各分数据库,涉及数据共享…
不同业务的数据源:一般利用AOP,结合自定义注解动态切换数据源
AOP+自定义注解
@EnableAspectJAutoProxy //启动AOP
package com.guo.mysql;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy //启动AOP
public class SpringbootSynchronizeMysqlApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootSynchronizeMysqlApplication.class, args);
}
}
WR.java
package com.guo.mysql.annotation;
import com.guo.mysql.enums.DataSourceEnum;
import javax.xml.bind.Element;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解
* 允许在方法和类型上使用该注解
*/
@Target({ElementType.METHOD,ElementType.TYPE})
//保留方式 SOURCE不会编译在class文件中 CLASS:会编译不会被JVM加载,通过反射获取不到 RUNTIME:可以通过反射调用
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
String value() default "slave"; //默认是从库(大多数场景都是在读取数据)
}
DynamicDataSourceAspect .java
package com.guo.mysql.aspect;
import com.guo.mysql.DynamicDataSource;
import com.guo.mysql.annotation.WR;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class DynamicDataSourceAspect {
//前置通知 within指定包下的所有类
@Before("within(com.guo.mysql.service.impl.*) && @annotation(wr)")
public void before(JoinPoint point, WR wr){
String name = wr.value();
DynamicDataSource.name.set(name);
System.out.println(name);
}
}
上边切面类中自定义切面的时指定的包为接口实现类中,所以这里需要在指定的包下使用注解,实现数据源的动态切换
package com.guo.mysql.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guo.mysql.annotation.WR;
import com.guo.mysql.entity.UserOsskey;
import com.guo.mysql.mapper.UserOsskeyMapper;
import com.guo.mysql.service.UserOsskeyService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserOsskeyServiceImpl extends ServiceImpl<UserOsskeyMapper, UserOsskey> implements UserOsskeyService {
@Resource
private UserOsskeyMapper userOsskeyMapper;
// 从库查询
@Override
@WR("slave")
public List<UserOsskey> queryList(QueryWrapper<UserOsskey> queryWrapper) {
return userOsskeyMapper.selectList(queryWrapper);
}
//主库插入
@Override
@WR("master")
public int saveData(UserOsskey userOsskey) {
return userOsskeyMapper.insert(userOsskey);
}
}