数据库中间件:Mycat读写分离(实战)

(1)理论基础

Mycat是数据库中间件,所谓中间件,是一类连接软件组件和应用的计算机软件,以便软件各部件之间的通信。

例如 tomcat,web的中间件。而数据库中间件是连接Java应用程序和数据库中间的软件。

为什么要用Mycat

Java与数据库的紧耦合

我们现在普遍的Java应用程序都是直接连接了MySQL软件进行读写操作,也就是我们在Java中的配置文件等定义了mysql的数据源,直接连接到了我们的mysql软件,但是当某些情况下我们可能需要用到了多个数据库,这个时候我们可能就需要配多个数据源去连接我们的多个数据库,这个时候我们进行sql操作的时候就会很麻烦,因为Java与数据库有了一个紧密的耦合度,但是如果我们在Java应用程序与mysql中间使用了mycat,我们只需要访问mycat就可以了,至于数据源等问题,mycat会直接帮我们搞定。

高访问量高并发对数据库的压力

再来说一下高访问量高并发,我们都知道mysql数据库实际上在数据查询上是有一个瓶颈的,当我们的数据太多的时候,已经互联网上有高并发的请求的时候,这个时候对我们mysql的压力是非常大的,当访问量一大,就可能会出现查不出数据,响应的时间太长等,这个时候我们可能需要有多个服务器对数据库进行读写分离,以及对数据库进行集群,这个时候我们的sql语句要进行分类,哪个sql语句要访问哪个数据库,这个时候只要交给mycat就可以了。

读写请求数据不一致

最后说一下,使用多个数据库的时候我们就会遇到一个读写数据不一致的问题,这个时候同样mycat可以进行主从复制,保证了数据的一致性。

mycat能干什么

1、读写分离

2、数据分片

3、多数据源整合

Mycat原理

Mycat 的原理中最重要的一个动词是“拦截”,它拦截了用户发送过来的 SQL 语句,首先对 SQL语句做了一些特定的分析:如分片分析、路由分析、读写分离分析、缓存分析等,然后将此 SQL 发往后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。

这种方式把数据库的分布式从代码中解耦出来,程序员察觉不出来后台使用 Mycat 还是MySQL。

基本概念:

物理数据库:真实的数据库

物理表:真实的表

逻辑数据库:相对于物理数据库,是数据节点聚合后的结果

逻辑表:相对于物理表,是分片表聚合后的结果,对于客户端来说跟真实的表没有区别

(2)schema.xml配置

注意:schema的name必须和mysql数据库名字一样一样!

<?xml version="1.0"?>

<!DOCTYPE mycat:schema SYSTEM "schema.dtd">

<mycat:schema xmlns:mycat="http://io.mycat/">

    <schema name="db01" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1"></schema>

    <dataNode name="dn1" dataHost="localhost1" database="db01" />

    <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.31.215:3306" user="root" password="root">

            <!-- 可以配置多个从库 -->

            <readHost host="hostS2" url="192.168.31.83:3306" user="root" password="root" />

        </writeHost>

    </dataHost>

</mycat:schema>

(3)server.xml配置

<property name="serverPort">8066</property> <property name="managerPort">9066</property>

<!-- 读写都可用的用户 -->

<user name="root" defaultAccount="true">

<property name="password">root</property>

<property name="schemas">db01</property>

</user>

<!-- 只读用户 -->

<user name="user">

<property name="password">user</property>

<property name="schemas">db01</property>

<property name="readOnly">true</property>

<property name="defaultSchema">TESTDB</property>

</user>

(4)运行

启动Mycat服务器:startup_nowrap.bat

读写账号

ip:192.168.31.215

port:8066

账号:root

密码:root

只读账号

ip:192.168.31.215

port:8066

账号:user

密码:user

(5)配置

        <dependency>

            <groupId>io.mycat</groupId>

            <artifactId>mycat-dao</artifactId>

            <version>0.3.1</version>

        </dependency>

        <dependency>

            <groupId>com.alibaba</groupId>

            <artifactId>druid-spring-boot-starter</artifactId>

            <version>1.2.9</version>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-aop</artifactId>

        </dependency>

spring:

  datasource:

    ####写数据源

    update:

      jdbc-url: jdbc:mysql://192.168.31.215:8066/db01?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true

      driver-class-name: com.mysql.cj.jdbc.Driver

      username: root

      password: 123456

    ###读数据源

    select:

      jdbc-url: jdbc:mysql://192.168.31.215:8066/db01?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true

      driver-class-name: com.mysql.cj.jdbc.Driver

      username: user

      password: 123456

    type: com.alibaba.druid.pool.DruidDataSource

(6)切面

@Aspect

@Component

@Lazy(false)

// Order设定AOP执行顺序 使之在数据库事务上先执行

@Order(0)

public class DataSourceAOP {

    //横切点

    @Before("execution(* com.zhaoyang.controller.*.*(..))")

    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")) {

            System.out.println("使用的是读数据源");

            DataSourceContextHolder.setDbType("selectDataSource");

        } else {

            System.out.println("使用的是写数据源");

            DataSourceContextHolder.setDbType("updateDataSource");

        }

    }

}

/**

* 配置读写数据源

*/

@Configuration

public class DataSourceConfig {

    @Bean(name = "selectDataSource")

    @ConfigurationProperties(prefix = "spring.datasource.select")

    public DataSource dataSource1() {

        return DataSourceBuilder.create().build();

    }

    @Bean(name = "updateDataSource")

    @ConfigurationProperties(prefix = "spring.datasource.update")

    public DataSource dataSource2() {

        return DataSourceBuilder.create().build();

    }

}

/**

* 保存本地多数据源

*/

@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();

    }

}

/**

* 该类继承自 AbstractRoutingDataSource 类,在访问数据库时会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key

*/

@Component

@Primary

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Autowired

    @Qualifier("selectDataSource")

    private DataSource selectDataSource;

    @Autowired

    @Qualifier("updateDataSource")

    private DataSource updateDataSource;

    /**

    * 返回生效的数据源名称

    */

    @Override

    protected Object determineCurrentLookupKey() {

        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();

    }

}

(7)使用

@RestController

public class PersonController {

    @Autowired

    PersonService personService;

    @RequestMapping("/getPerson")

    public Person getPerson(int id) {

        return personService.getById(id);

    }

    @RequestMapping("/findPerson")

    public Person findPerson(int id) {

        return personService.getById(id);

    }

    @RequestMapping("/save")

    public boolean save(Person person) {

        return personService.save(person);

    }

}