MyCat介绍
MyCat是一款由阿里Cobar演变而来的用于支持数据库读写分离、分表分库的分布式中间件。mycatz支持oracle、mssql、mysql、pg、db2关系型数据库,也支持mongodb等非关系型数据库.
mycat原理主要是通过对sql的拦截,然后经过一定规则的分片解析、路由分析、读写分离分析、缓存分析等,然后将sql发给后端真实的数据块,并将返回的结果做适当处理返回给客户端。
基于MyCat实现读写分离
读写分离,简单地说是把对数据库的读和写操作分开,以对应不同的数据库服务器。主数据库提供写操作,从数据库提供读操作,这样能有效地减轻单台数据库的压力。主数据库进行写操作后,数据及时同步到从数据库,尽可能保证读、写数据库的数据一致,比如mysql的主从复制,oracle的data guard、sql server的复制订阅等。
一般来说,要实现读写分离,那么就要分清楚角色与权限,比如
读的数据库角色,只有select的权限。
写的数据库角色,有select、update、insert、create、delete权限。
那么当客户端执行一个对数据库的操作时,我们怎么根据这个操作,来判断所对应的权限,并且路由到对应的数据库去访问,这个就是我们需要解决的问题。
mycat就是用来解决这个问题的。
mycat会拦截客户端的所有jdbc操作,根据sql语句判断转发到不同的数据库执行,类似于nginx,也是可以实现反向代理,隐藏数据库ip,负载均衡等。
一般这样子部署需要用到3台服务器,一台是mycat,一台主数据库,一台从数据库。数据库服务器可以不用有公网ip,只需要mycat服务器有公网ip就可以了,这样客户端只能访问mycat,由mycat转发到数据库。
MyCat下载与安装配置
下载地址
http://dl.mycat.io/1.6.7.3/20190927161129/Mycat-server-1.6.7.3-release-20190927161129-linux.tar.gz
下载好之后上传到linux服务器。
1、解压 tar -zxvf Mycat-server-1.6.7.3-release-20190927161129-linux.tar.gz
2、运行 cd到mycat根目录输入指令 ./bin/mycat start
3、查看mycat是否运行成功 cat ./logs/wrapper.log
看到successfully就说明运行成功了。
4、关掉mycat。 ./bin/mycat stop
修改mycat的conf目录下的配置文件实现读写分离
schema.xml
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!-- TESTDB1 是mycat的逻辑库名称,链接需要用的 -->
<schema name="TESTDB1" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1"></schema>
<!-- database 是MySQL数据库的库名 -->
<dataNode name="dn1" dataHost="localhost1" database="test" />
<!--
dataNode节点中各属性说明:
name:指定逻辑数据节点名称;
dataHost:指定逻辑数据节点物理主机节点名称;
database:指定物理主机节点上。如果一个节点上有多个库,可使用表达式db$0-99, 表示指定0-99这100个数据库;
dataHost 节点中各属性说明:
name:物理主机节点名称;
maxCon:指定物理主机服务最大支持1000个连接;
minCon:指定物理主机服务最小保持10个连接;
writeType:指定写入类型;
0,只在writeHost节点写入;
1,在所有节点都写入。慎重开启,多节点写入顺序为默认写入根据配置顺序,第一个挂掉切换另一个;
dbType:指定数据库类型;
dbDriver:指定数据库驱动;
balance:指定物理主机服务的负载模式。
0,不开启读写分离机制;
1,全部的readHost与stand by writeHost参与select语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且M1与 M2互为主备),正常情况下,M2,S1,S2都参与select语句的负载均衡;
2,所有的readHost与writeHost都参与select语句的负载均衡,也就是说,当系统的写操作压力不大的情况下,所有主机都可以承担负载均衡;
3,所有读请求随机的分发到wiriterHost 对应的readhost 执行,writerHost 不负担读压力,注意balance=3 只在1.4 及其以后版本有。
-->
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<!-- 可以配置多个主库 -->
<writeHost host="hostM1" url="192.168.0.108:3306" user="root" password="123456">
<!-- 可以配置多个从库 -->
<readHost host="hostS2" url="192.168.0.107:3306" user="root" password="123456" />
</writeHost>
</dataHost>
</mycat:schema>
server.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- - - Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License. - You
may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0
- - Unless required by applicable law or agreed to in writing, software -
distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the
License for the specific language governing permissions and - limitations
under the License. -->
<!DOCTYPE mycat:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/">
<system>
<property name="nonePasswordLogin">0</property> <!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0,设置为1则需要指定默认账户-->
<property name="useHandshakeV10">1</property>
<property name="useSqlStat">0</property> <!-- 1为开启实时统计、0为关闭 -->
<property name="useGlobleTableCheck">0</property> <!-- 1为开启全加班一致性检测、0为关闭 -->
<property name="sequnceHandlerType">2</property>
<property name="subqueryRelationshipCheck">false</property> <!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false -->
<!-- <property name="useCompression">1</property>--> <!--1为开启mysql压缩协议-->
<!-- <property name="fakeMySQLVersion">5.6.20</property>--> <!--设置模拟的MySQL版本号-->
<!-- <property name="processorBufferChunk">40960</property> -->
<!--
<property name="processors">1</property>
<property name="processorExecutor">32</property>
-->
<!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool -->
<property name="processorBufferPoolType">0</property>
<!--默认是65535 64K 用于sql解析时最大文本长度 -->
<!--<property name="maxStringLiteralLength">65535</property>-->
<!--<property name="sequnceHandlerType">0</property>-->
<!--<property name="backSocketNoDelay">1</property>-->
<!--<property name="frontSocketNoDelay">1</property>-->
<!--<property name="processorExecutor">16</property>-->
<!--
<property name="serverPort">8066</property> <property name="managerPort">9066</property>
<property name="idleTimeout">300000</property> <property name="bindIp">0.0.0.0</property>
<property name="frontWriteQueueSize">4096</property> <property name="processors">32</property> -->
<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
<property name="handleDistributedTransactions">0</property>
<!--
off heap for merge/order/group/limit 1开启 0关闭
-->
<property name="useOffHeapForMerge">1</property>
<!--
单位为m
-->
<property name="memoryPageSize">64k</property>
<!--
单位为k
-->
<property name="spillsFileBufferSize">1k</property>
<property name="useStreamOutput">0</property>
<!--
单位为m
-->
<property name="systemReserveMemorySize">384m</property>
<!--是否采用zookeeper协调切换 -->
<property name="useZKSwitch">false</property>
<!-- XA Recovery Log日志路径 -->
<!--<property name="XARecoveryLogBaseDir">./</property>-->
<!-- XA Recovery Log日志名称 -->
<!--<property name="XARecoveryLogBaseName">tmlog</property>-->
</system>
<!-- 全局SQL防火墙设置 -->
<!--白名单可以使用通配符%或着*-->
<!--例如<host host="127.0.0.*" user="root"/>-->
<!--例如<host host="127.0.*" user="root"/>-->
<!--例如<host host="127.*" user="root"/>-->
<!--例如<host host="1*7.*" user="root"/>-->
<!--这些配置情况下对于127.0.0.1都能以root账户登录-->
<!--
<firewall>
<whitehost>
<host host="1*7.0.0.*" user="root"/>
</whitehost>
<blacklist check="false">
</blacklist>
</firewall>
-->
<user name="root" defaultAccount="true">
<property name="password">123456</property>
<property name="schemas">TESTDB1</property>
<!-- 表级 DML 权限设置 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<user name="user">
<property name="password">user</property>
<property name="schemas">TESTDB1</property>
<property name="readOnly">true</property>
</user>
</mycat:server>
启动mycat之后即可。
查看效果
用navicat连接mycat查看效果。
这里连接的ip地址是mycat服务器地址,端口号是mycat默认的端口号8066,帐号密码是mycat的server配置文件配置的,这个账号有读写权限。
如果连接之后报Unknow character set utf8mb4错误,可以通过修改schema配置文件的连接方式为jdbc。
这个错误的原因应该是mysql版本太低的关系,也可以通过安装5.5以上版本的mysql解决。
连接成功之后可以执行数据库的增删改操作查看效果。
再添加一个连接,使用user帐号查看效果,进行增删改的操作,会报一个错误
说明已经实现了读写分离。
SpringBoot整合读写分离
使用动态数据源,spring2.0.1中引入了AbstractRoutingDataSource,该类充当了DataSource的路由中介,能在运行时,根据某种key值来动态切换到真正的DataSource上。那么根据该类,我们可以通过aop的方式,拦截根据方法前缀来路由到对应的数据源中,例如方法前缀为select、get、find、cout等,就路由到读数据源,其他的就路由到写数据源,由此来实现读写分离。
实现步骤:
1、创建读和写的数据源。
2、将读和写的数据源注册到RoutingDataSource。
3、使用AOP技术拦截业务逻辑层方法,判断方法的前缀是否需要做读或者写。
实践
数据库
项目目录
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>routingdatatest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>routingdatatest</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.23</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件application.yml
spring:
datasource:
#可读数据源
select:
jdbc-url: jdbc:mysql://192.168.0.105:8066/TESTDB1
driver-class-name: com.mysql.jdbc.Driver
username: user
password: user
#可写数据源
update:
jdbc-url: jdbc:mysql://192.168.0.105:8066/TESTDB1
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
数据源配置文件
package com.example.routingdatatest;
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;
@Configuration
public class DataSourceConfig {
//创建可读数据源
@Bean
@ConfigurationProperties(prefix = "spring.datasource.select")
public DataSource selectDataSource(){
return DataSourceBuilder.create().build();
}
//创建可写数据源
@Bean
@ConfigurationProperties(prefix = "spring.datasource.update")
public DataSource updateDataSource(){
return DataSourceBuilder.create().build();
}
}
存放当前数据源的工具类
package com.example.routingdatatest;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
@Lazy(false)
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
//设置数据源类型
public static void setDbType(String dbType){
contextHolder.set(dbType);
}
public static String getDbType(){
return contextHolder.get();
}
public static void clearDbType(){
contextHolder.remove();
}
}
RoutingDataSource类
package com.example.routingdatatest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Component
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
@Qualifier("selectDataSource")
private DataSource selectDataSource;
@Autowired
@Qualifier("updateDataSource")
private DataSource updateDataSource;
//这个是主要的方法,返回的是生效的数据源名称
@Override
protected Object determineCurrentLookupKey() {
System.out.println("DataSourceContextHolder:::" + DataSourceContextHolder.getDbType());
return DataSourceContextHolder.getDbType();
}
//配置数据源信息
@Override
public void afterPropertiesSet() {
Map<Object,Object> map = new HashMap<>();
map.put("selectDataSource",selectDataSource);
map.put("updateDataSource",updateDataSource);
setTargetDataSources(map);
setDefaultTargetDataSource(updateDataSource);
super.afterPropertiesSet();
}
}
aop类
package com.example.routingdatatest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Lazy(false)
@Order(0) //设定aop执行顺序,使之在数据库事务之前执行。
public class SwitchDataSourceAOP {
//指定方法目录
@Before("execution(* com.example.routingdatatest.service.*.*(..))")
public void process(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
if (methodName.startsWith("get") || methodName.startsWith("count") || methodName.startsWith("find")
|| methodName.startsWith("list") || methodName.startsWith("select") || methodName.startsWith("check")) {
DataSourceContextHolder.setDbType("selectDataSource");
} else {
// 切换dataSource
DataSourceContextHolder.setDbType("updateDataSource");
}
}
}
实体类
package com.example.routingdatatest.entity;
public class UserEntity {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
mapper类
package com.example.routingdatatest.mapper;
import java.util.List;
import com.example.routingdatatest.entity.UserEntity;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
@Select("SELECT * FROM user_info ")
public List<UserEntity> findUser();
@Select("insert into user_info values (#{userName}); ")
public List<UserEntity> insertUser(@Param("userName") String userName);
}
service类
package com.example.routingdatatest.service;
import java.util.List;
import com.example.routingdatatest.entity.UserEntity;
import com.example.routingdatatest.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<UserEntity> findUser() {
return userMapper.findUser();
}
public List<UserEntity> insertUser(String userName) {
return userMapper.insertUser(userName);
}
}
controller类
package com.example.routingdatatest.controller;
import java.util.List;
import com.example.routingdatatest.entity.UserEntity;
import com.example.routingdatatest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/findUser")
public List<UserEntity> findUser() {
return userService.findUser();
}
@RequestMapping("/insertUser")
public List<UserEntity> insertUser(String userName) {
return userService.insertUser(userName);
}
}
springboot入口类
package com.example.routingdatatest;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.example.routingdatatest.mapper")
public class RoutingdatatestApplication {
public static void main(String[] args) {
SpringApplication.run(RoutingdatatestApplication.class, args);
}
}
启动项目
打开浏览器访问http://localhost:8080/findUser
可以看到控制台打印了
访问http://localhost:8080/insertUser?userName=qweqwe
可以看到控制台打印了
说明已经实现了读写分离。
也可以修改配置文件,将写数据库的账号改成user。
再次访问http://localhost:8080/insertUser?userName=qweqwe
可以发现报了一个User readonly的错误
而访问http://localhost:8080/findUser却没有报错。
也说明了已经实现了读写分离,因为写数据库配置的账号用的是只读权限的账号,所以写操作报错了。