手把手教你实现MySQL读写分离+故障转移,不信你学不会!(中)

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 手把手教你实现MySQL读写分离+故障转移,不信你学不会!(中)

编写StudentDao接口,并进行测试


接口:


package cn.objectspace.springtestdemo.dao;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import cn.objectspace.springtestdemo.domain.Student;
@Mapper
public interface StudentDao {
    @Insert("INSERT INTO student(student_id,student_name)VALUES(#{studentId},#{studentName})")
    public Integer insertStudent(Student student);
    @Select("SELECT * FROM student WHERE student_id = #{studentId}")
    public Student queryStudentByStudentId(Student student);
}


测试类:


@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ApplicationStarter.class})// 指定启动类
public class DaoTest {
    @Autowired StudentDao studentDao;
    @Test
    public void test01() {
        Student student = new Student();
        student.setStudentId("20191130");
        student.setStudentName("Object6");
        studentDao.insertStudent(student);
        studentDao.queryStudentByStudentId(student);
    }
}


如果可以正确往数据库中插入数据,如下图,则MyBatis搭建成功。


image.png


正式搭建


通过上面的准备工作,我们已经可以实现对数据库的读写,但是并没有实现读写分离,现在才是开始实现数据库的读写分离。


修改application.yml


刚才我们的配置文件中只有单数据源,而读写分离肯定不会是单数据源,所以我们首先要在application.yml中配置多数据源。


server: 
  port: 10001
spring: 
  datasource:
    master: 
      url: jdbc:mysql://192.168.43.201:3306/springtestdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
      username: Object
      password: Object971103.
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave: 
      url: jdbc:mysql://192.168.43.202:3306/springtestdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
      username: Object
      password: Object971103.
      driver-class-name: com.mysql.cj.jdbc.Driver
#MyBatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    cache-enabled: true #开启二级缓存
    map-underscore-to-camel-case: true


DataSource的配置


首先要先创建两个ConfigurationProperties类,这一步不是非必须的,直接配置DataSource也是可以的,但是我还是比较习惯去写这个Properties。


MasterProperpties


package cn.objectspace.springtestdemo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "spring.datasource.master")
@Component
public class MasterProperties {
    private String url;
    private String username;
    private String password;
    private String driverClassName;
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getDriverClassName() {
        return driverClassName;
    }
    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }
}


SlaveProperties


package cn.objectspace.springtestdemo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "spring.datasource.slave")
@Component
public class SlaveProperties {
    private String url;
    private String username;
    private String password;
    private String driverClassName;
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getDriverClassName() {
        return driverClassName;
    }
    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }
}


DataSourceConfig


这个配置主要是对主从数据源进行配置。


@Configuration
public class DataSourceConfig {
    private Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
    @Autowired
    private MasterProperties masterProperties;
    @Autowired
    private SlaveProperties slaveProperties;
    //默认是master数据源
    @Bean(name = "masterDataSource")
    @Primary
    public DataSource masterProperties(){
        logger.info("masterDataSource初始化");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(masterProperties.getUrl());
        dataSource.setUsername(masterProperties.getUsername());
        dataSource.setPassword(masterProperties.getPassword());
        dataSource.setDriverClassName(masterProperties.getDriverClassName());
        return dataSource;
    }
    @Bean(name = "slaveDataSource")
    public DataSource dataBase2DataSource(){
        logger.info("slaveDataSource初始化");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(slaveProperties.getUrl());
        dataSource.setUsername(slaveProperties.getUsername());
        dataSource.setPassword(slaveProperties.getPassword());
        dataSource.setDriverClassName(slaveProperties.getDriverClassName());
        return dataSource;
    }
}


动态数据源的切换


这里使用到的主要是Spring提供的AbstractRoutingDataSource,其提供了动态数据源的功能,可以帮助我们实现读写分离。其determineCurrentLookupKey()可以决定最终使用哪个数据源,这里我们自己创建了一个DynamicDataSourceHolder,来给他传一个数据源的类型(主、从)。


package cn.objectspace.springtestdemo.dao.split;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 
* @Description: spring提供了AbstractRoutingDataSource,提供了动态选择数据源的功能,替换原有的单一数据源后,即可实现读写分离:
* @Author: Object
* @Date: 2019年11月30日
 */
public class DynamicDataSource extends AbstractRoutingDataSource{
    //注入主从数据源
    @Resource(name="masterDataSource")
    private DataSource masterDataSource;
    @Resource(name="slaveDataSource")
    private DataSource slaveDataSource;
    @Override
    public void afterPropertiesSet() {
        setDefaultTargetDataSource(masterDataSource);
        Map<Object, Object> dataSourceMap = new HashMap<>();
        //将两个数据源set入目标数据源
        dataSourceMap.put("master", masterDataSource);
        dataSourceMap.put("slave", slaveDataSource);
        setTargetDataSources(dataSourceMap);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        //确定最终的目标数据源
        return DynamicDataSourceHolder.getDbType();
    }
}


DynamicDataSourceHolder的实现


这个类由我们自己实现,主要是提供给Spring我们需要用到的数据源类型。


package cn.objectspace.springtestdemo.dao.split;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description: 获取DataSource
* @Author: Object
* @Date: 2019年11月30日
*/
public class DynamicDataSourceHolder {
    private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceHolder.class);
    private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
    public static final String DB_MASTER = "master";
    public static final String DB_SLAVE="slave";
    /**
     * @Description: 获取线程的DbType
     * @Param: args
     * @return: String
     * @Author: Object
     * @Date: 2019年11月30日
     */
    public static String getDbType() {
        String db = contextHolder.get();
        if(db==null) {
            db = "master";
        }
        return db;
    }
    /**
     * @Description: 设置线程的DbType
     * @Param: args
     * @return: void
     * @Author: Object
     * @Date: 2019年11月30日
     */
    public static void setDbType(String str) {
        logger.info("所使用的数据源为:"+str);
        contextHolder.set(str);
    }
    /**
     * @Description: 清理连接类型
     * @Param: args
     * @return: void
     * @Author: Object
     * @Date: 2019年11月30日
     */
    public static void clearDbType() {
        contextHolder.remove();
    }
}


MyBatis拦截器的实现


最后就是我们实现读写分离的核心了,这个类可以对SQL进行判断,是读SQL还是写SQL,从而进行数据源的选择,最终调用DynamicDataSourceHolder的setDbType方法,将数据源类型传入。


package cn.objectspace.springtestdemo.dao.split;
import java.util.Locale;
import java.util.Properties;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
 * @Description: MyBatis级别拦截器,根据SQL信息,选择不同的数据源
 * @Author: Object
 * @Date: 2019年11月30日
 */
@Intercepts({ 
    @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
    @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class })
    })
@Component
public class DynamicDataSourceInterceptor implements Interceptor {
    private Logger logger = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class);
    // 验证是否为写SQL的正则表达式
    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
    /**
     * 主要的拦截方法
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 判断当前是否被事务管理
        boolean synchronizationActive = TransactionSynchronizationManager.isActualTransactionActive();
        String lookupKey = DynamicDataSourceHolder.DB_MASTER;
        if (!synchronizationActive) {
            //如果是非事务的,则再判断是读或者写。
            // 获取SQL中的参数
            Object[] objects = invocation.getArgs();
            // object[0]会携带增删改查的信息,可以判断是读或者是写
            MappedStatement ms = (MappedStatement) objects[0];
            // 如果为读,且为自增id查询主键,则使用主库
            // 这种判断主要用于插入时返回ID的操作,由于日志同步到从库有延时
            // 所以如果插入时需要返回id,则不适用于到从库查询数据,有可能查询不到
            if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)
                    && ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                lookupKey = DynamicDataSourceHolder.DB_MASTER;
            } else {
                BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                // 正则验证
                if (sql.matches(REGEX)) {
                    // 如果是写语句
                    lookupKey = DynamicDataSourceHolder.DB_MASTER;
                } else {
                    lookupKey = DynamicDataSourceHolder.DB_SLAVE;
                }
            }
        } else {
            // 如果是通过事务管理的,一般都是写语句,直接通过主库
            lookupKey = DynamicDataSourceHolder.DB_MASTER;
        }
        logger.info("在" + lookupKey + "中进行操作");
        DynamicDataSourceHolder.setDbType(lookupKey);
        // 最后直接执行SQL
        return invocation.proceed();
    }
    /**
     * 返回封装好的对象,或代理对象
     */
    @Override
    public Object plugin(Object target) {
        // 如果存在增删改查,则直接拦截下来,否则直接返回
        if (target instanceof Executor)
            return Plugin.wrap(target, this);
        else
            return target;
    }
    /**
     * 类初始化的时候做一些相关的设置
     */
    @Override
    public void setProperties(Properties properties) {
        // TODO Auto-generated method stub
    }
}


代码梳理


通过上文中的程序,我们已经可以实现读写分离了,但是这么看着还是挺乱的。所以在这里重新梳理一遍上文中的代码。


其实逻辑并不难:


  • 通过@Configuration实现多数据源的配置。
  • 通过MyBatis的拦截器,DynamicDataSourceInterceptor来判断某条SQL语句是读还是写,如果是读,则调用DynamicDataSourceHolder.setDbType("slave"),否则调用DynamicDataSourceHolder.setDbType("master")
  • 通过AbstractRoutingDataSource的determineCurrentLookupKey()方法,返回DynamicDataSourceHolder.getDbType();也就是我们在拦截器中设置的数据源。
  • 对注入的数据源执行SQL。


测试


@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ApplicationStarter.class})// 指定启动类
public class DaoTest {
    @Autowired StudentDao studentDao;
    @Test
    public void test01() {
        Student student = new Student();
        student.setStudentId("20191130");
        student.setStudentName("Object6");
        studentDao.insertStudent(student);
        studentDao.queryStudentByStudentId(student);
    }
}


测试结果:


image.png


至此,代码层读写分离已完整地实现。


基于MyCat中间件实现读写分离、故障转移


简介


在上文中我们已经实现了使用手写代码的方式对数据库进行读写分离,但是不知道大家发现了没有,我只使用了一主一从。那么为什么我有一主二从的环境却只实现一主一从的读写分离呢?因为,在代码层实现一主多从的读写分离我也不会写。那么假设数据库集群不止于一主二从,而是一主三从,一主四从,多主多从呢?如果Master节点宕机了,又该怎么处理?


每次动态增加一个节点,我们就要重新修改我们的代码,这不但会给开发人员造成很大的负担,而且不符合开闭原则。


所以接下来的MyCat应该可以解决这样的问题。并且我会直接使用一主二从的环境演示。


MyCat介绍


这里直接套官方文档。


  • 一个彻底开源的,面向企业应用开发的大数据库集群
  • 支持事务、ACID、可以替代MySQL的加强版数据库
  • 一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群
  • 一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server
  • 结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品
  • 一个新颖的数据库中间件产品


环境说明


MyCat 192.168.43.90

MySQL master 192.168.43.201

MySQL slave1 192.168.43.202

MySQL slave2 192.168.43.203


接上篇博客的MySQL数据库一主二从,不过MySQL版本需要从8.0改为5.7,否则会出现

密码问题无法连接。


另外,我们需要在每个数据库中都为MyCat创建一个账号并赋上权限:


CREATE USER  'user_name'@'host'  IDENTIFIED BY  'password';
GRANT privileges ON  databasename.tablename  TO  ‘username’@‘host’;
--可以使用下面这句 赋予所有权限
GRANT ALL PRIVILEGES ON *.* TO  ‘username’@‘host’;
--最后刷新权限
FLUSH PRIVILEGES;


在开始之前,先保证主从库的搭建是成功的:


image.png


如何安装MyCat在这里我就不说了,百度上有很多帖子有,按照上面的教程一步一步来其实没有多大问题。我们着重说说和我们MyCat配置相关的两个配置文件——schema.xml和server.xml,当然还有一个rules.xml,但是这里暂时不介绍分库分表,所以这个暂且不提。


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
存储 关系型数据库 MySQL
MySQL 读写分离原理
MySQL 读写分离原理
62 0
MySQL 读写分离原理
|
3月前
|
SQL 关系型数据库 MySQL
Mycat【Mycat部署安装(核心配置及目录结构、安装以及管理命令详解)Mycat高级特性(读写分离概述、搭建读写分离、MySQL双主双从原理)】(三)-全面详解(学习总结---从入门到深化)
Mycat【Mycat部署安装(核心配置及目录结构、安装以及管理命令详解)Mycat高级特性(读写分离概述、搭建读写分离、MySQL双主双从原理)】(三)-全面详解(学习总结---从入门到深化)
71 0
|
4月前
|
Java 关系型数据库 MySQL
②⑩② 【读写分离】Sharding - JDBC 实现 MySQL读写分离[SpringBoot框架]
②⑩② 【读写分离】Sharding - JDBC 实现 MySQL读写分离[SpringBoot框架]
40 0
|
1月前
|
关系型数据库 MySQL 数据库
使用 Docker 搭建一个“一主一从”的 MySQL 读写分离集群(超详细步骤
使用 Docker 搭建一个“一主一从”的 MySQL 读写分离集群(超详细步骤
59 0
|
3月前
|
缓存 NoSQL 关系型数据库
MySQL缓存策略(一致性问题、数据同步以及缓存故障)
MySQL缓存策略(一致性问题、数据同步以及缓存故障)
53 1
|
4月前
|
关系型数据库 MySQL 数据库
分库分表之基于Shardingjdbc+docker+mysql主从架构实现读写分离(一)
分库分表之基于Shardingjdbc+docker+mysql主从架构实现读写分离(一)
|
4月前
|
关系型数据库 MySQL Java
分库分表之基于Shardingjdbc+docker+mysql主从架构实现读写分离(二)
分库分表之基于Shardingjdbc+docker+mysql主从架构实现读写分离(二)
|
11天前
|
关系型数据库 MySQL 数据库
mysql卸载、下载、安装(window版本)
mysql卸载、下载、安装(window版本)
|
1月前
|
关系型数据库 MySQL 数据库连接
关于MySQL-ODBC的zip包安装方法
关于MySQL-ODBC的zip包安装方法
|
29天前
|
关系型数据库 MySQL 数据库
rds安装数据库客户端工具
安装阿里云RDS的数据库客户端涉及在本地安装对应类型(如MySQL、PostgreSQL)的客户端工具。对于MySQL,可选择MySQL Command-Line Client或图形化工具如Navicat,安装后输入RDS实例的连接参数进行连接。对于PostgreSQL,可以使用`psql`命令行工具或图形化客户端如PgAdmin。首先从阿里云控制台获取连接信息,然后按照官方文档安装客户端,最后配置客户端连接以确保遵循安全指引。
82 1