当前位置: 首页 > 工具软件 > MySQL Router > 使用案例 >

SpringBoot+MySQLRouter 8 (mysql读写分离的代理)ubuntu20.04 配置mysqlrouter8

冉弘化
2023-12-01

mysql router 特性

(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   要代理的服务器地址

被代理服务器防火墙要关闭,主数据库用户名密码要一致,所有的从数据库密码要一致

 

安装mysqlrouter 8

官网下载  选择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 boot连接MySQL router 配置读写分离

因为我们用的是代理 所以要自定义数据源,不能在 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;
	}

}

直接运行项目就好了,如果上面的步骤你都认真做了的话 是没有任何问题的

 类似资料: