1.引言
读写分离就是把一条SQL放到自己想要的那个数据库去执行,我们要做的就是实现SQL怎么自己去对应的数据库执行。
两种实现方式。第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;第二种是应用程序自己去做分离。目前只做了第二种,主要是利用Spring提供的路由数据源,以及AOP;后续会利用mycat来做,我想无非是集成配置吧。
在这里有所感悟的是,代理是个流行的好东西,java在程序中可以自己做代理,让程序更简洁,代码更可能复用,功能更强大(高端);而对于各种编程语言而言,更多的第三方插件,在我这里的想法也是一种功能的代理,其实一般都可以自己实现,不过这种封装好的,往往功能更加全面,虽然更多时候你也只需要用到最火最实用的那一两个点,但是这种技术封装的产生,也让我们的学习成本不断增加,有时候甚至太多类似的东西,让人感到无力迷茫。我觉得不断学习是对的,也是必须的,但是一昧的追求和推崇所谓的“新技术”,个人认为会忘记和学习许多原本就能实现的简单的东西。适合的才是最好的,还有可以注重语言本身就可以做到的事情。
目前,应用程序层面去做读写分离最大的不足之处在于无法动态增加数据库节点,因为数据源配置都是写在配置中的,新增数据库意味着新加一个数据源,必然改配置,并重启应用。当然,好处就是相对简单。不过此处也应该会有方法实现热加载对应的配置,我想利用mycat这种中间件,也不过是如此,或是本身就有脚本语言的动态加载的好处。
2.主从数据库配置
这里有几个问题:
1.我是在win10上用Hyper-V创建的虚拟机,两个linux,本机网络是连的公司wifi,想配置两个虚拟机每次开机都保持Ip相同(因为数据库装在上面不能每次都改配置),配置了静态ip虚拟机就无法上网,远程连接也连不上,不配置静态ip就能上网,现在也没解决,找了很多网上的方法,基本都一样(network,ifcfg-eht0)本身也是这样改的,是不是缺少什么配置了,若有老铁能分享一下解决办法,真实万分感谢了。(已解决,可能是自己配置的问题)
参考:https://blog.csdn.net/mzjacob/article/details/77891023
虚拟机与本机网络桥接
#这是 /etc/sysconfig/network-scripts/ifcfg-eth0的配置
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
#BOOTPROTO=dhcp
BOOTPROTO=static
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=eth0
UUID=fa936de6-6047-45c3-bc41-d13907146078
DEVICE=eth0
ONBOOT=yes
IPADDR=192.168.0.119
NETMASK=255.255.255.0
GATEWAY=192.168.0.1 #网卡对应的网络地址,也就是所属的网段
# /etc/sysconfig/network
# Created by anaconda
NETWORKING=yes #表示系统是否使用网络,一般设置为yes。如果设为no,则不能使用网络。
HOSTNAME=localhost.localdomain #设置本机的主机名,这里设置的主机名要和/etc/hosts中设置的主机名对应,看/etc/hostname更直观
GATEWAY=192.168.0.1 #设置本机连接的网关的IP地址。
# vi /etc/resolv.conf
# ping 域名
nameserver 8.8.8.8 #google域名服务器
nameserver 8.8.4.4 #google域名服务器
2.使用的源码安装方式,安装mysql5.7.27
(1)启动的时候可能会报错
这是因为mysql启动的时候需要配置文件,而在安装centos的时候,哪怕是mini版本都会有个默认的配置在/etc目录中
执行命令
/usr/local/mysql/bin/mysqld --verbose --help |grep -A 1 'Default options'
3.代码及配置
(1)AbstractRoutingDataSource
基于特定的查找key路由到特定的数据源。它内部维护了一组目标数据源,并且做了路由key与目标数据源之间的映射,提供基于key查找数据源的方法。
(2)我是直接从前面的项目基础上做的,依赖无需更多,暂且把jpa的依赖和代码注释掉,因为,多数据源配置,mybatis配置了,jpa同样需要配置,在此就更显麻烦,统一用Mybatis,否则会出现启动失败异常(a bean named 'entityManagerFactory' that could not be found)
application.properties(注释的是之前,本机单个数据源的配置mysql 8.x,现在虚拟机装的都是5.7.x驱动也改了)
#数据源
#MYSQL MYBATIS POOL
#mysql8对应驱动
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#mysql8加上时区设置
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/first_love?characterEncoding=utf8&useSSL=false&serverTimezone=GMT&rewriteBatchedStatements=true
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/firs_love?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
#spring.datasource.username=root
#spring.datasource.password=root
#主从数据库配置
#master
spring.datasource.master.jdbc-url=jdbc:mysql://172.17.253.12:3306/mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
spring.datasource.master.username=dev
spring.datasource.master.password=123
spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
#slave
spring.datasource.slave1.jdbc-url=jdbc:mysql://172.17.253.4:3306/mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
spring.datasource.slave1.username=reader
spring.datasource.slave1.password=123456
spring.datasource.slave1.driver-class-name=com.mysql.jdbc.Driver
需要注意的是:spring.datasource.master.jdbc-url 添加了master或slave的节点,url改成了jdbc-url
spring.datasource.url 数据库的 JDBC URL。
spring.datasource.jdbc-url 用来重写自定义连接池
官方文档的解释是:
因为连接池的实际类型没有被公开,所以在您的自定义数据源的元数据中没有生成密钥,而且在IDE中没有完成(因为DataSource接口没有暴露属性)。另外,如果您碰巧在类路径上有Hikari,那么这个基本设置就不起作用了,因为Hikari没有url属性(但是确实有一个jdbcUrl属性)。在这种情况下,您必须重写您的配置
(3)多数据源配置(这里的动态数据源配置,需要创建一个继承了AbstractRoutingDataSource的数据源路由类)
package cn.penghf.lovemaster.config;
import cn.penghf.lovemaster.bean.MyRoutingDataSource;
import cn.penghf.lovemaster.enums.DBTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 多数据源配置
*/
@Configuration
@Slf4j
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource(){
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource(){
return DataSourceBuilder.create().build();
}
@Bean(name = "myRoutingDataSource")
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource) {
log.info("动态数据源配置开始");
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);//默认目标数据源
myRoutingDataSource.setTargetDataSources(targetDataSources);//动态目标数据源
log.info("动态数据源配置成功");
return myRoutingDataSource;
}
}
这里,我们配置了3个数据源,1个master,1个slave,1个路由数据源。前2个数据源都是为了生成第3个数据源,而且后续我们只用这最后一个路由数据源。
(3-1)相关配置-设置路由key / 查找数据源
package cn.penghf.lovemaster.bean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
@Slf4j
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
log.info("key路由");
return DBContextHolder.get();
}
}
关键
package cn.penghf.lovemaster.bean;
import cn.penghf.lovemaster.enums.DBTypeEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Description 这里切换读/写模式
* 原理是利用ThreadLocal保存当前线程是否处于读模式(通过开始READ_ONLY注解在开始操作前设置模式为读模式,
* 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式
* @author fxb
* @date 2018-08-31
*/
@Slf4j
public class DBContextHolder {
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1);
public static void set(DBTypeEnum dbType) {
contextHolder.set(dbType);
}
public static DBTypeEnum get() {
log.info("切换数据源");
DBTypeEnum dbTypeEnum = contextHolder.get();
return dbTypeEnum;
}
public static void master() {
set(DBTypeEnum.MASTER);
log.info("切换到master");
}
public static void slave() {
// 轮询 若有多个从库 数据源,可做分流降压
int index = counter.getAndIncrement() % 2;
if (counter.get() > 9999) {
counter.set(-1);
}
//if (index == 0) {
set(DBTypeEnum.SLAVE1);
log.info("切换到slave1");
/*}else {
set(DBTypeEnum.SLAVE2);
System.out.println("切换到slave2");
}*/
}
}
代表数据源的枚举
package cn.penghf.lovemaster.enums;
public enum DBTypeEnum {
MASTER,SLAVE1,SLAVE2;
}
mybatis配置
package cn.penghf.lovemaster.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
@Configuration
@AutoConfigureAfter({ DataSourceConfiguration.class })
//扫描mapper,目录到mapper类所在的包,解决mapper找不到的问题
@MapperScan(basePackages = {"cn.penghf.lovemaster.mapper.*"},sqlSessionFactoryRef = "sqlSessionFactory")
@EnableTransactionManagement
@Slf4j
public class MybatisConfiguration {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
//自定义mybatis配置,需扫描mapper.xml文件,解决文件找不到的问题
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml"));
//若还有自定义mybatis配置文件,也要扫描
//sqlSessionFactoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
log.info("动态数据源事务配置");
return new DataSourceTransactionManager(myRoutingDataSource);
}
/* @Bean(name = "sqlSessionTemplate")
@Autowired
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public MapperScannerConfigurer scannerConfigurer(){
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
configurer.setSqlSessionTemplateBeanName("sqlSessionTemplate");
configurer.setBasePackage("cn.penghf.lovemaster.mapper.*");
configurer.setMarkerInterface(Mapper.class);
return configurer;
}*/
}
4.使用AOP
package cn.penghf.lovemaster.aspect;
import cn.penghf.lovemaster.bean.DBContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class DataSourceAop {
/*@Pointcut("!@annotation(cn.penghf.lovemaster.annotation.Master) " +
"&& (execution(* cn.penghf.lovemaster.service.*.find*(..)) " +
"|| execution(* cn.penghf.lovemaster.service.*.get*(..))" +
"|| execution(* cn.penghf.lovemaster.service.*.select*(..)))")
public void readPointcut() {
}
@Pointcut("@annotation(cn.penghf.lovemaster.annotation.Master) " +
"|| execution(* cn.penghf.lovemaster.service.*.insert*(..)) " +
"|| execution(* cn.penghf.lovemaster.service.*.add*(..)) " +
"|| execution(* cn.penghf.lovemaster.service.*.pdate*(..)) " +
"|| execution(* cn.penghf.lovemaster.service.*.edit*(..)) " +
"|| execution(* cn.penghf.lovemaster.service.*.delete*(..)) " +
"|| execution(* cn.penghf.lovemaster.service.*.remove*(..))" +
"|| execution(* cn.penghf.lovemaster.service.*.save*(..))")
public void writePointcut() {
}*/
/* @Before("readPointcut()")
public void read() {
log.info("读");
DBContextHolder.slave();
}
@Before("writePointcut()")
public void write() {
log.info("写");
DBContextHolder.master();
}*/
/**
* 上述写法失效,每次@Pointcut条件多几个就没反应了
* if...else... 判断哪些需要读从数据库,其余的走主数据库
*/
@Before("execution(public * cn.penghf.lovemaster.service.*.*.*(..))")
public void before(JoinPoint jp) {
String methodName = jp.getSignature().getName();
if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
log.info("读");
DBContextHolder.slave();
}else {
log.info("写");
DBContextHolder.master();
}
}
}
这里的问题是,我基本上用@Poincut注解,判断条件一般是有两个相同的逻辑运算符或多于两个条件时,就无法生效了,这个问题,也希望大家试一下,有解决的,希望也能告知分享
5.测试
测试大家可以就用之前的controller测试(除了那些注释的jpa的代码),也可以自己写个测试用例,这里我就不多说了。
上面基本是,我初次尝试时,路过的坑和解决好的,还有些未解决的,但是现在是可以用的版本,有问题,可以继续讨论,后续还是因动态新增数据节点的,会利用mycat做,毕竟大流量下,做分库分表的流行的东西。
参考博文:https://www.jianshu.com/p/f2f4256a2310