在上一篇,介绍了docker搭建mysql主从复制集群 docker搭建mysql主从复制
读写分离的方案也可通过中间件代理,如mysql-proxy,mycat。
通过中间件代理,可以很好的做到负载均衡,以及自动故障切换,高可用性
这里用另一种方式,springboot通过aop和druid来实现mybatis的多数据源设置,从而实现读写分离
druid
Druid是阿里巴巴开源的一个数据源,主要用于java数据库连接池,相比spring推荐的DBCP和hibernate推荐的C3P0、Proxool数据库连接池,Druid在市场上占有绝对的优势;为什么选择Druid作为数据库连接池?
文章从市场占有率、性能上比较C3P0、DBCP、HikariCP和Druid,说明了Druid数据源由于有强大的监控特性、可拓展性等特点值得作者推荐。虽说 HikariCP 的性能比 Druid 高,但是因为 Druid 包括很多维度的统计和分析功能,所以大家都选择使用Druid 的更多;下面直接贴代码
springboot项目结构
pom.xml
需要加入的依赖,直接贴代码
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--devtools热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</dependency>
<!--=========mybatis plus======================-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 引入 Druid 数据源依赖:https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--StringUtils工具包-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
springboot启动类
package com.example.ms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MsApplication {
public static void main(String[] args) {
SpringApplication.run(MsApplication.class, args);
}
}
启动类的注解上加入:(exclude = DataSourceAutoConfiguration.class)
该注解的作用是,排除自动注入数据源的配置(取消数据库配置),一般使用在客户端(消费者)服务中,这样就去掉了spring boot 默认的mybatis 自动配置
至于为什么要做这个设置呢,下面详细做出解释:
exclude,排除此类的AutoConfig,即禁止 SpringBoot 自动注入数据源配置,怎么讲?
DataSourceAutoConfiguration.class 会自动查找 application.yml 或者 properties 文件里的
spring.datasource.* 相关属性并自动配置单数据源「注意这里提到的单数据源」。
那么问题来了,排除了自动配置,Spring还怎么识别到数据库配置呢?
答:显然接下来就需要手动配置,what?那我为什么要排除?然后手动指定数据源?
如果你发现项目中存在这个排除的骚操作,可以在项目中搜一下Java关键字@ConfigurationProperties("spring.datasource ,你可能会发现手动配置数据源的类。
再来回答为何要手动配置数据源,因为要配置多数据源,上边有提到DataSourceAutoConfiguration.class默认会帮我们自动配置单数据源,所以,如果想在项目中使用多数据源就需要排除它,手动指定多数据源
MyDatabase数据源实体类
package com.example.ms.pojo;
import lombok.Data;
@Data
public class MyDatabase {
//数据库连接url
private String url;
//数据库连接用户名
private String username;
//数据库连接密码
private String password;
//数据库是主or从
private String type;
}
application.yml
spring:
devtools:
restart:
enabled: true #设置开启热部署
additional-paths: src/main/java #重启目录
exclude: WEB-INF/** #排除项,无须自动重启,但是会重新加载。
freemarker:
cache: false #页面不加载缓存,修改即时生效
thymeleaf:
cache: true #即页面修改后会立即生效 true
mybatis-plus:
# 如果是放在src/main/java目录下 classpath:/com/yourpackage/*/mapper/*Mapper.xml
mapper-locations: classpath:mybatis/*Mapper.xml
typeAliasesPackage: com.example.ms.pojo
global-config:
db-config:
#id-type: uuid
#字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
field-strategy: 1
#驼峰下划线转换
#db-column-underline: true
#刷新mapper 调试神器
#refresh-mapper: true
#数据库大写下划线转换
#capital-mode: true
# Sequence序列接口实现类配置
#key-generator: com.baomidou.mybatisplus.incrementer.OracleKeyGenerator
#逻辑删除配置(下面3个配置)
#logic-delete-value: 1
#logic-not-delete-value: 0
#sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector
#自定义填充策略接口实现
#meta-object-handler: com.baomidou.springboot.MyMetaObjectHandler
configuration:
#配置返回数据库(column下划线命名&&返回java实体是驼峰命名),自动匹配无需as(没开启这个,SQL需要写as: select user_id as userId)
map-underscore-to-camel-case: false
cache-enabled: false
#配置JdbcTypeForNull, oracle数据库必须配置
jdbc-type-for-null: 'null'
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
database-id: mysql
注意,这里就不需要配置datasource数据源了
multidatabase.properties 数据源配置文件
#下面的main 和 read 数据将会以轮询的方式 被 访问。
#mian 循环main
#read 循环read 具体代码查看DataSourceAOP
#自定义多数据源配置
my.datasource.driver=com.mysql.jdbc.Driver
# main 代表主服务器 可读写 read = 只读
my.datasource[0].type=main
my.datasource[0].url=jdbc:mysql://192.168.0.201:3306/test
my.datasource[0].username=root
my.datasource[0].password=zaqxsw
my.datasource[1].type=read
my.datasource[1].url=jdbc:mysql://192.168.0.202:3306/test
my.datasource[1].username=root
my.datasource[1].password=zaqxsw
my.datasource[2].type=read
my.datasource[2].url=jdbc:mysql://192.168.0.203:3306/test
my.datasource[2].username=root
my.datasource[2].password=zaqxsw
这里配置了3个数据源type=main是主节点,type=read是从节点
DatabaseContextHolder
内部使用 ThreadLocal 类,通过ThreadLocal 可以给每个线程设置和获取数据,起作用是在 AOP拦截到对应的 方法时,实现读写分离。
package com.example.ms.config.mybatisConfig;
public class DatabaseContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
}
DynamicDataSource
spring boot jdbc 提供了一个 AbstractRoutingDataSource,通过实现,我们可以在操作数据库之前,动态的设置 数据源
package com.example.ms.config.mybatisConfig;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dataSourceType = DatabaseContextHolder.getDataSourceType();
System.out.println("动态获取到的 数据源key == "+dataSourceType);
return dataSourceType;
}
}
MultDataSource 读取配置文件
package com.example.ms.config.mybatisConfig;
import com.alibaba.druid.pool.DruidDataSource;
import com.example.ms.pojo.MyDatabase;
import lombok.Data;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
@Configuration
@MapperScan(basePackages = "com.example.ms.dao")
@PropertySource(value = "classpath:multidatabase.properties", encoding = "utf-8")
@ConfigurationProperties("my")
@Data
public class MultDataSource {
public static final String MAIN = "main";
public static final String READ = "read";
public List<String> mainKeys = new ArrayList<>();
public List<String> readKeys = new ArrayList<>();
@Value("${my.datasource.driver}")
private String driver;
/**
* 读取配置文件获取。
*/
private List<MyDatabase> datasource;
public DruidDataSource getDataSource(MyDatabase database) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl(database.getUrl());
druidDataSource.setUsername(database.getUsername());
druidDataSource.setDriverClassName(driver);
druidDataSource.setPassword(database.getPassword());
druidDataSource.setInitialSize(1);
druidDataSource.setMaxWait(6000);
druidDataSource.setMinIdle(8);
return druidDataSource;
}
@Bean
@Primary
public DynamicDataSource dataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
for (int i = 0; i < datasource.size(); i++) {
String type = datasource.get(i).getType();
DruidDataSource dataSource = getDataSource(datasource.get(i));
if (MAIN.equals(type)) {
mainKeys.add(MAIN+i);
targetDataSources.put(MAIN+i,dataSource);
} else {
readKeys.add(READ+i);
targetDataSources.put(READ+i,dataSource);
}
}
DynamicDataSource dataSource = new DynamicDataSource();
// 该方法是AbstractRoutingDataSource的方法
dataSource.setTargetDataSources(targetDataSources);
// 默认的datasource设置为myTestDbDataSource
dataSource.setDefaultTargetDataSource(targetDataSources.get(mainKeys.get(0)));
return dataSource;
}
}
读取配置文件,根据配置文件的type是mian还是read,来判断走哪个数据库。
- @MapperScan(basePackages = “com.example.ms.dao”),给这个路径下的class类都统一加入@mapper注解。
- @PropertySource 加载指定配置文件
- @ConfigurationProperties 可以和@PropertySource配合使用,代表将本类的全局变量和配置文件指定的属性相互绑定,注解后面的(“my”)对应multidatabase.properties里的my,本类中的“ private List datasource;”全局变量,就和配置文件My绑定。
- @Data lombok注解,自动生成类属性的get,set方法
- getDataSource(MyDatabase database),获取数据源实体属性,放入DruidDataSource,再根据DruidDataSource动态切换数据源
- dataSource()。遍历配置文件里的数据源,根据数据源的读写分类,动态切换数据源
- @Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常
aopAspectConfig
aop配置,拦截mybatis-plus以及dao层的方法,通过方法名,来判断走主库还是从库。
这里就要注意,因为是用aop来判断方法名字的办法来拦截的,数据交互层的方法名就要统一规范,否则拦截不到。Mybatis-plus内部自己封装的方法也是可以拦截到的,比如insert,selectById等等,切入点设置同样也是设置到dao层
package com.example.ms.aop;
import com.example.ms.config.mybatisConfig.DatabaseContextHolder;
import com.example.ms.config.mybatisConfig.MultDataSource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
@Component
@Aspect
public class aopAspectConfig {
private Logger logger = LoggerFactory.getLogger(aopAspectConfig.class);
@Autowired
MultDataSource multDataSource;
//切入点当前有效
@Before("execution(* com.example.ms.dao..*.insert*(..)) ||" +
"execution(* com.example.ms.dao..*.update*(..)) ||" +
"execution(* com.example.ms.dao..*.delete*(..)) "
)
public void setReadDataSource(){
DatabaseContextHolder.setDataSourceType(getMainKey());
System.out.println("主库的写操作");
}
@Before("execution(* com.example.ms.dao..*.select*(..))")
public void setWriteDataSource(){
DatabaseContextHolder.setDataSourceType(getReadKey());
System.out.println("从库的读操作");
}
/**
* 轮询方式
*/
int m = 0;
public String getMainKey(){
List<String> readKeys = multDataSource.getMainKeys();
m ++;
m = m%readKeys.size();
return readKeys.get( m );
}
int i = 0;
public String getReadKey(){
List<String> readKeys = multDataSource.getReadKeys();
i ++;
i = i%readKeys.size();
return readKeys.get( i );
}
}
测试
到此为止,spring通过设置mybatis多数据源的方式实现读写分离,就配置好了,下面我们写一个测试案例
上一篇介绍了搭建mysql主从集群,这里在主库里添加一个表,加入2行数据。同样从库
也会出现相同的数据。
Mysql为1主2从的结构,我们把2个从的节点数据从数据库里改一下。
主节点
从节点1
从节点2
然后配置好了实体类以及dao层以后,写个接口测试
@RestController
@RequestMapping("test")
public class testController {
@Autowired
public WagesInfoMapper wagesInfoMapper;
@RequestMapping(value="insert",method = RequestMethod.POST)
public String insert(){
WagesInfo w = new WagesInfo();
w.setName("xxx");
w.setWages(3000);
wagesInfoMapper.insert(w);
return"ddd";
}
@RequestMapping(value="getOne",method = RequestMethod.POST)
public WagesInfo getOne(){
WagesInfo w = wagesInfoMapper.selectById(1);
return w;
}
}
因为在aop配置里,加了输出语句,执行insert的时候,控制台会打印 “主库的写操作”.执行正确
执行getOne接口的时候,控制台会打印"从库的读操作",并且每一次请求,都会轮询请求从节点数据库,刚才数据库里的值修改了,通过Postman来测试,就可以看出来是每一次是请求不同的从节点数据库
文章来源:https://blog.csdn.net/zhanglinlang/article/details/88938264
感谢文章作者