(1)Router实现读写分离,程序不是直接连接数据库IP,而是固定连接到mysql router。MySQL Router对前端应用是透明的。
应用程序把MySQL Router当作是普通的mysql实例,把查询发给MySQL Router,而MySQL Router会把查询结果返回给前端的应用程序。
(2)从数据库服务器故障,业务可以正常运行。由MySQL Router来进行自动下线不可用服务器。程序配置不需要任何修改。
(3)主数据库故障,由MySQL Router来决定主从自动切换,业务可以正常访问。程序配置不需要做任何修改。
高可用: 当与InnoDB Cluster一起使用时,MySQL Router充当代理,向应用程序隐藏网络上的多个MySQL实例,并将数据请求映射到其中一个集群实例。只要有足够的在线副本并且组件之间的通信完好无损,客户端就能够连接其中一个实例,保持对外服务的连续性
bind_address router 地址
bind_port router 端口号
mode 读写或只读模式(read-write)(read-only)
高可用性: 根据mode分辨,当主库(read-write)宕机时 自动去连接下一个集群中 模式为read-write的数据库实例
(read-write模式)
采用“首个可用”算法, 如果没有可用的这个端口上的所有请求将被终断 此时所属的路由策略将不可用。这个算法只会遍历一次列表,不会循环,一旦找不到可用的servers 内置状态设定为aborted,即使此后服务重新上线后,也不能继续对其客户端提供服务,因为router不会与读写servers保持心跳检测,只有重启router节点才能解决
(read-only模式)
采用“轮询算法”,依次对只读模式的数据库服务进行连接,如果某一个服务连接不上将会重试下一个服务,如果所有的服务都连接不上,这个代理端口的所有请求将被中断,
但是router会与每个server保持心跳检测,当一个服务恢复后,将恢复客户端的请求并分发给恢复的servers
缺点: Router不会将已有的连接重新分配给“新加入”列表的Server,比如Router有2个Server地址(S1,S2),某时刻S1不可达,那么在S1上连接的客户端连接也将被断开,
新建连接将会全部在S2上,此后S1恢复正常,那么在S2上的旧的连接将不会迁移到S1上,此时S1只会接收新的连接,如果没有新连接请求,那么S1将会在一段时间看起来是“不提供服务”的
destinations 要代理的服务器地址
被代理服务器防火墙要关闭,主数据库用户名密码要一致,所有的从数据库密码要一致
官网下载 选择liunx通用的版本 复制下载链接 然后在ubuntu20.04执行下载命令
wget https://dev.mysql.com/get/Downloads/MySQL-Router/mysql-router-8.0.20-linux-glibc2.12-x86_64.tar.xz
解压这个文件
tar -xvf mysql-router-8.0.20-linux-glibc2.12-x86_64.tar.xz
移动解压后的文件到 自定义的地方 稍后我们还要配置
mv mysql-router-8.0.20-linux-glibc2.12-x86_64 /usr/local/mysql-router
cd /usr/local/mysql-router #进入这个文件夹下面
ls #查看下目录
#文件目录
bin data include lib run share
mkdir /usr/local/mysql-router/conf/msyqlrouter.cnf #新建配置文件
sudo vim /usr/local/mysql-router/conf/msyqlrouter.cnf #编辑配置文件
#添加配置
[DEFAULT]
logging_folder=/usr/local/mysql-router/log #初始化log
runtime_folder=/usr/local/mysql-router/run #初始化run
data_folder=/usr/local/mysql-router/data #初始化data
connect_timeout=30
read_timeout=30
[logger]
level = INFO #日志级别
[routing:failover]
bind_address = 192.168.31.55 #代理所在地址
bind_port = 7001 #代理端口
max_connections = 1024 #最大连接数
mode = read-only #模式(主库为read-write,从库为read-only)
destinations = 192.168.31.122:3306 #被代理的数据库服务器地址,多个用逗号隔开
[routing:balancing]
bind_address = 192.168.31.55
bind_port = 7002
max_connections = 1024
mode = read-write
destinations = 192.168.31.55:3306
#保存退出
#改变一下环境变量
echo "export PATH=$PATH:/usr/local/mysql-router/bin/" >> /etc/profile
chown +x mysql: /usr/local/mysql-router
source /etc/profile
#验证安装配置是否成功
mysqlrouter -V
MySQL Router Ver 8.0.20 for Linux on x86_64 (MySQL Community - GPL) #出现即为成功
#启动mysqlrouter
mysqlrouter --config /usr/local/mysql-router/conf/mysqlrouter.cnf & #后台运行
#出现log初始化成功提示 说明启动成功
#查看 7001 7002端口
netstat -tnlp
我们上面配置的是 7002为主库端口(读写)70001为从库端口(只读)
因为我们用的是代理 所以要自定义数据源,不能在 spring : datasource节点定义数据源了
在yml文件中自定义数据源
mysql:
datasource:
type-aliases-package: com.smartvideo.mysql.mapper
mapperLocations: classpath:mapper/**/*.xml
num: 1
write:
url: jdbc:mysql://192.168.31.55:7002/smartvideo?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
password: mqttiot2020!a
driver-class-name: com.mysql.cj.jdbc.Driver
read:
url: jdbc:mysql://192.168.31.55:7001/smartvideo?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
password: mqttiot2020!a
driver-class-name: com.mysql.cj.jdbc.Driver
一个读数据源一个写数据源,(注意这里的配置只是值,没有实际作用,我们要在程序中添加数据源)
创建一个DataSourceConfig类
@Configuration
public class DataSourceConfig {
@Value("${mysql.datasource.type-aliases-package}")
String typeAliasesPackage;
@Value("${mysql.datasource.mapperLocations}")
String mapperLocation;
@Primary
@Bean
@ConfigurationProperties(prefix = "mysql.datasource.write")
public DataSource writeDataSource() {
return new DruidDataSource();
}
@Bean
@ConfigurationProperties(prefix = "mysql.datasource.read")
public DataSource read1() {
return new DruidDataSource();
}
//配置数据源
@Bean
public AbstractRoutingDataSource routingDataSource() {
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DataSourceContext.WRITE, writeDataSource());
targetDataSources.put(DataSourceContext.READ, read1());
proxy.setDefaultTargetDataSource(writeDataSource());
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
//定义一个注解 任何方法加上该注解 标注为只读方法
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
//配置sqlsession工厂 让它走我们自定义的数据源
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSource());
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
实体类对应的位置
bean.setTypeAliasesPackage(typeAliasesPackage);
mybatis的XML的配置
bean.setMapperLocations(resolver.getResources(mapperLocation));
return bean.getObject();
}
}
创建一个DataSourceContext类型 定义数据源
@Component
public class DataSourceContext {
private static Logger logger = LoggerFactory.getLogger(DataSourceContext.class);
public static final String WRITE = "write";
public static final String READ = "read";
将ThreadLocal设置为静态的,可以让当前线程中所有的类都能够共享
private static ThreadLocal<String> dataSourcePool = new ThreadLocal<>();
public static void setDbType(String dbType) {
if (dbType == null) {
logger.error("dbType为空");
throw new NullPointerException();
}
logger.info("设置dbType为:{}", dbType);
dataSourcePool.set(dbType);
}
public static String getDbType() {
return dataSourcePool.get() == null ? WRITE : dataSourcePool.get();
}
public static void clearDbType() {
dataSourcePool.remove();
}
}
再定义一个MyAbstractRoutingDataSource类 自定义数据源规则
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
private final Logger logger = LoggerFactory.getLogger(MyAbstractRoutingDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContext.getDbType();
if (typeKey == DataSourceContext.WRITE) {
logger.info("使用了写库");
return typeKey;
}
logger.info("使用了读库{}" + DataSourceContext.READ);
return DataSourceContext.READ;
}
}
最后定义只读方法的切面
@Aspect
@Component
public class ReadOnlyInterceptor implements Ordered {
private static final Logger logger = LoggerFactory.getLogger(ReadOnlyInterceptor.class);
//切入的条件 标注该注解的方法 (只能标注在service层)
@Around("@annotation(readOnly)")
public Object setRead(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
try {
DataSourceContext.setDbType(DataSourceContext.READ);
return joinPoint.proceed();
} finally {
//清楚DbType一方面为了避免内存泄漏,更重要的是避免对后续在本线程上执行的操作产生影响
DataSourceContext.clearDbType();
logger.info("清除threadLocal");
}
}
@Override
public int getOrder() {
return 0;
}
}
直接运行项目就好了,如果上面的步骤你都认真做了的话 是没有任何问题的