在上一篇,介绍了docker搭建mysql主从复制集群 docker搭建mysql主从复制
读写分离的方案也可通过中间件代理,如mysql-proxy,mycat。
通过中间件代理,可以很好的做到负载均衡,以及自动故障切换,高可用性
这里用另一种方式,springboot通过aop和druid来实现mybatis的多数据源设置,从而实现读写分离
下面直接贴代码
需要加入的依赖,直接贴代码
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-devtools
true
true
com.baomidou
mybatis-plus-boot-starter
3.1.0
mysql
mysql-connector-java
com.alibaba
druid
1.1.9
org.projectlombok
lombok
true
commons-lang
commons-lang
2.6
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默认会帮我们自动配置单数据源,所以,如果想在项目中使用多数据源就需要排除它,手动指定多数据源
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;
}
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数据源了
#下面的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是从节点
内部使用 ThreadLocal 类,通过ThreadLocal 可以给每个线程设置和获取数据,起作用是在 AOP拦截到对应的 方法时,实现读写分离。
package com.example.ms.config.mybatisConfig;
public class DatabaseContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
}
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;
}
}
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 mainKeys = new ArrayList<>();
public List readKeys = new ArrayList<>();
@Value("${my.datasource.driver}")
private String driver;
/**
* 读取配置文件获取。
*/
private List 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
读取配置文件,根据配置文件的type是mian还是read,来判断走哪个数据库。
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 readKeys = multDataSource.getMainKeys();
m ++;
m = m%readKeys.size();
return readKeys.get( m );
}
int i = 0;
public String getReadKey(){
List readKeys = multDataSource.getReadKeys();
i ++;
i = i%readKeys.size();
return readKeys.get( i );
}
}
到此为止,spring通过设置mybatis多数据源的方式实现读写分离,就配置好了,下面我们写一个测试案例
上一篇介绍了搭建mysql主从集群,这里在主库里添加一个表,加入2行数据。同样从库
也会出现相同的数据。
Mysql为1主2从的结构,我们把2个从的节点数据从数据库里改一下。
从节点2
@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
感谢文章作者