美文网首页IT必备技能Java技术升华Java
SpringBoot+Mybatis实现读写分离

SpringBoot+Mybatis实现读写分离

作者: 秃头猿猿 | 来源:发表于2020-07-01 12:16 被阅读0次

    1.简介

    在早期项目开发过程中,我们都是把数据存储在单个数据库中,这样无论是对数据库的读还是写都是对单个数据库此操作。这样带来的问题是巨大的:

    • 单个数据库服务器挂了,数据库里面所有的数据都挂了
    • 所有的读写请求都是对单个数据库操作,数据库服务器压力巨大

    基于上述原因,我们就需要将对数据库服务器的读写操作分离,也就是读写分离。具体原理图如下:

    image-20200630201939806
    • 主数据库与多个从数据库实现了主从复制
    • 当应用发起对数据库的写操作时,那么就去操作主数据库
    • 当应用发起对数据库的读操作时,那么通过负载均衡算法去访问从数据库。
    • 系统一般来说时“读多写少”,因此这样在一定程度上减轻了数据库的压力。

    从上面的解释我们可以看出,实现读写分离的前提是,数据库一定要配置好主从复制,如果数据库没有实现主从复制,那么就无法实现读写分离,数据库的主从复制请学习:《基于Docker方式实现MySql主从复制

    2.实现方式

    实现读写分离大致有两种方式:

    • 利用中间件进行读写分离

      例如:Mycat,Oneproxy等等。

      这些中间基本上都可以实现数据库的读写分离,分库分表等其他诸多功能,但是如果只是想实现读写分离,中间件反而显得有点臃肿

      优缺点:

      • 代码层面不需要任何改动,该怎么去写就怎么写
      • 应用不再直接操作数据库,直接操作中间件,通过中间件去操作数据库
      • 访问运维人员进行维护。
      • 配置比较繁琐,同时如果修改了数据库,同时也要修改中间件
    • 在应用层面利用aop去实现读写分离(重点介绍)

      所谓的读写分离,就是让不同的请求去操作不同的数据库,那么其实就可以在访问数据库之前,先判断该请求是什么请求,

      读请求就让它访问从数据库,写请求就访问主数据库。

      优缺点:

      • 省略了中间件配置步骤,简化开发时间
      • 思想间件,实现起来比较容易
      • 在代码层面修改,运维人员不好修改
      • 如果增加数据库,需要修改代码

    3.原理

    在这里我们重点去掌握怎么在代码层面去实现读写分离。

    其实原理在上面的介绍中已经提到过了,总而言之就是动态切换数据源。具体原理如下:

    • 在调用业务层方法之前先判断该方法对数据库的操作
      • 如果是写操作那么将数据源切换成主数据库
      • 如果是读操作就将数据源切换到从数据库。

    这样我们就实现了读写分离。具体原理图如下:

    image-20200630204635875

    4.实现步骤

    • 准备工作

      • 搭建mysql的主从复制,这里我们就搭建一主一从,具体参考:《基于Docker方式实现MySql主从复制

      • 熟悉mybatis通用mapper

      • 在主数据库创建数据库以及表,建表语句如下:

        SET NAMES utf8mb4;
        SET FOREIGN_KEY_CHECKS = 0;
        
        -- ----------------------------
        -- Table structure for person
        -- ----------------------------
        DROP TABLE IF EXISTS `person`;
        CREATE TABLE `person`  (
          `id` int(11) NOT NULL AUTO_INCREMENT,
          `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
          `age` int(11) NULL DEFAULT NULL,
          PRIMARY KEY (`id`) USING BTREE
        ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
        
        SET FOREIGN_KEY_CHECKS = 1;
        
        
        image-20200701085259186
      • 创建项目

        image-20200630205027900

        pom.xml文件内容如下:

        <?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>
           <!--
                SpringBoot 版本
           -->
            <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.1.15.RELEASE</version>
                <relativePath/> <!-- lookup parent from repository -->
            </parent>
            <groupId>com.readwite</groupId>
            <artifactId>application</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <name>application</name>
            <description>Demo project for Spring Boot</description>
        
            <properties>
                <java.version>1.8</java.version>
            </properties>
        
            <dependencies>
                <!--
                    mysql 驱动包
                -->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <scope>runtime</scope>
                </dependency>
        
                <!--
                    lombok 依赖包
                -->
                <dependency>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <optional>true</optional>
                </dependency>
        
        
                <!---
                  单元测试
                -->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-test</artifactId>
                    <scope>test</scope>
                </dependency>
        
                <!--
                  aop依赖
                -->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-aop</artifactId>
                </dependency>
        
                <!--
                    mybatis 通用mapper
                -->
                <dependency>
                    <groupId>tk.mybatis</groupId>
                    <artifactId>mapper-spring-boot-starter</artifactId>
                    <version>2.1.5</version>
                </dependency>
        
                <!--
                    日志依赖
                -->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </dependency>
            </dependencies>
        
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                    </plugin>
                </plugins>
            </build>
        </project>
        
        
    • 搭建环境

      • application.yml增加以下内容

        spring:
          datasource:
            master:
              # 驱动类
              driver-class-name: com.mysql.jdbc.Driver
              # 主数据库服务器用户名
              username: root
              # 主数据库服务器密码
              password: root
              # 主数据库服务器用户名,到时候更换自己的ip端口
              jdbc-url: jdbc:mysql://192.168.169.133:3306/test?characterEncoding=utf-8
              # 从数据库服务器配置
            slave:
              # 从数据库服务器用户名
              username: root
              # 从数据库服务器密码
              password: root
              # 从数据库服务器url,到时候更换自己的ip端口
              jdbc-url: jdbc:mysql://192.168.169.133:33306/test?characterEncoding=utf-8
        
        
        logging:
          level:
            com.readwite.application: debug
        
        image-20200701112111525
      • 创建两个注解read,write

        只要在方法上加了read注解那么表示该方法对数据库的操作是读操作

        image-20200701092337386

        只要在方法上加了write注解那么表示该方法对数据库的操作是写操作

        image-20200701092421376 image-20200701092506270
      • 创建枚举类,定义MASTER,SLAVE两个枚举对象

        因为我们只有一个主库,一个从库,因此我们定义两个枚举对象来表示数据库类型

        package com.readwite.application.config;
        
        /**
         * 表示数据库类型
         */
        public enum DBTypeEnum {
            /**
             * 表示主数据库
             */
            MASTER,
        
            /**
             * 表示从数据库
             */
            SLAVE;
        }
        
        
        image-20200701112144028
      • 创建一个动态切换数据源的工具类

        package com.readwite.application.config;
        
        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        
        /**
         * 动态切换数据源的工具类
         */
        public class DynamicSwitchDBTypeUtil {
            /**
             * 用来存储代表数据源的对象
             *  如果是里面存储是SLAVE,代表当前线程正在使用主数据库
             *  如果是里面存储的是SLAVE,代表当前线程正在使用从数据库
             */
            private static final ThreadLocal<DBTypeEnum>  CONTEXT_HAND = new ThreadLocal<>();
        
            /**
             * 日志对象
             */
            private static final Logger log =  LoggerFactory.getLogger(DynamicSwitchDBTypeUtil.class);
        
            /**
             * 切换当前线程要使用的数据源
             * @param dbTypeEnum
             */
            public static void set(DBTypeEnum dbTypeEnum) {
                CONTEXT_HAND.set(dbTypeEnum);
                log.info("切换数据源:" + dbTypeEnum);
            }
        
            /**
             * 切换到主数据库
             */
            public static void master() {
                set(DBTypeEnum.MASTER);
            }
        
            /**
             * 切换到从数据库
             */
            public static void slave() {
                /*
                    目前我们只有一个从数据库,可以直接设置
                    但是如果我们拥有多个从数据库那么就需要
                    考虑怎么使用什么样的算法去负载均衡从数据库
                 */
                set(DBTypeEnum.SLAVE);
            }
        
            /**
             * 移除当前线程使用的数据源
             */
            public static void remove() {
                CONTEXT_HAND.remove();
            }
        
            /**
             * 获取当前线程使用的枚举值
             * @return
             */
            public static DBTypeEnum get() {
                return CONTEXT_HAND.get();
            }
        }
        
        
        
        
        image-20200701112211159
      • 编写AbstractRoutingDataSource的实现类

        在SpringBoot中提供了AbstractRoutingDataSource,用户可以根据自己定义的规则去

        选择当前要使用的数据源,我们利用这个特性,在调用业务层方法之前去扫描注解,如果方法上是read注解我们就切换到从数据库,否则切换到主数据库。

        实现动态的数据源,是由该里面的抽象方法determineCurrentLookupKey决定,具体源码如下图所示:

        image-20200701112251222

        部分解释如下:

        package org.springframework.jdbc.datasource.lookup;
        
        import java.sql.Connection;
        import java.sql.SQLException;
        import java.util.HashMap;
        import java.util.Map;
        
        import javax.sql.DataSource;
        
        import org.springframework.beans.factory.InitializingBean;
        import org.springframework.jdbc.datasource.AbstractDataSource;
        import org.springframework.lang.Nullable;
        import org.springframework.util.Assert;
        
        public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
            /**
                用来存储数据源
                map的key-value解释如下:
                key: 数据源的key值
                value: 表示数据源
            */
            @Nullable
            private Map<Object, Object> targetDataSources;
        
            /**
                默认的数据源
            */
            @Nullable
            private Object defaultTargetDataSource;
        
            private boolean lenientFallback = true;
        
            private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
        
            @Nullable
            private Map<Object, DataSource> resolvedDataSources;
        
            @Nullable
            private DataSource resolvedDefaultDataSource;
        
        
            /**
             * 设置数据源,具体使用哪一个数据源由determineCurrentLookupKey()方法返回
             * 的key决定
             */
            public void setTargetDataSources(Map<Object, Object> targetDataSources) {
                this.targetDataSources = targetDataSources;
            }
        
            /**
             *设置默认的数据源
             */
            public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
                this.defaultTargetDataSource = defaultTargetDataSource;
            }
        
            
            /**
             * 决定使用数据源的方法
             * 从源码可知:
                    1.调用 determineCurrentLookupKey 获取key值
                    2.拿到key值后再从map里面获取数据源,然后返回
             */
            protected DataSource determineTargetDataSource() {
                Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
                Object lookupKey = determineCurrentLookupKey();
                DataSource dataSource = this.resolvedDataSources.get(lookupKey);
                if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
                    dataSource = this.resolvedDefaultDataSource;
                }
                if (dataSource == null) {
                    throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
                }
                return dataSource;
            }
        
            /**
             * 抽象方法,返回数据源的key值,由开发者自己去实现
             */
            @Nullable
            protected abstract Object determineCurrentLookupKey();
        
        }
        
        

        创建该类的实现类,并实现determineCurrentLookupKey方法

        package com.readwite.application.config;
        
        import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
        
        /**
         * 决定返回哪个数据源的key
         */
        public class RouttingDataSource extends AbstractRoutingDataSource {
        
            @Override
            protected Object determineCurrentLookupKey() {
                /**
                 * 返回当前线程正在使用的代表数据库的枚举对象
                 */
                return DynamicSwitchDBTypeUtil.get();
            }
        }
        
        
        image-20200701112312923
      • 配置数据源

        经过上面的准备后,我们就可以去配置数据源了

        package com.readwite.application.config;
        
        import org.springframework.beans.factory.annotation.Qualifier;
        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;
        import java.util.HashMap;
        import java.util.Map;
        
        @Configuration
        public class DataSourceConfig {
        
            /**
             * 将创建的master数据源存入Spring容器中,并且注入内容
             * key值为方法名
             * @return master数据源
             */
            @Bean
            @ConfigurationProperties("spring.datasource.master")
            public DataSource masterDataSource() {
                return DataSourceBuilder.create().build();
            }
        
            /**
             * 将创建的slave数据源存入Spring容器中,并且注入内容
             * key值为方法名
             * @return slave数据源
             */
            @Bean
            @ConfigurationProperties("spring.datasource.slave")
            public DataSource slaveDataSource() {
                return DataSourceBuilder.create().build();
            }
        
            /**
             * 决定最终要使用的数据源
             * @return
             */
            @Bean
            public DataSource targetDataSource(@Qualifier("masterDataSource") DataSource masterDataSoure,
                                               @Qualifier("slaveDataSource") DataSource slaveDataSource) {
                // 用来存放主数据源和从数据源
                Map<Object, Object> targetDataSource = new HashMap<>();
        
                // 往map中添加主数据源
                targetDataSource.put(DBTypeEnum.MASTER,masterDataSoure);
        
                // 往map中添加从数据源
                targetDataSource.put(DBTypeEnum.SLAVE,slaveDataSource);
        
                // 创建 routtingDataSource 用来实现动态切换
                RouttingDataSource routtingDataSource = new RouttingDataSource();
        
                // 绑定所有的数据源
                routtingDataSource.setTargetDataSources(targetDataSource);
        
                // 设置默认的数据源
                routtingDataSource.setDefaultTargetDataSource(masterDataSoure);
        
                return routtingDataSource;
            }
        
        }
        
        
        image-20200701112421485
      • 配置Mybatis

        因为我们已经有了多个数据源,因此我们就需要去配置mybatis的SqlSessionFactory.

        那么问题来了,为什么之前我们在SpringBoot整合mybatis的时候不需要配置,这是因为之前整合的时候只有一个数据源,SpringBoot底部已经帮我们做好了了封装,所以我们不要配置。

        而现在有多个数据源我们就需要手动配置了。新建一个配置类,如下:

        package com.readwite.application.config;
        
        import org.apache.ibatis.session.SqlSessionFactory;
        import org.mybatis.spring.SqlSessionFactoryBean;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
        import org.springframework.jdbc.datasource.DataSourceTransactionManager;
        import org.springframework.transaction.PlatformTransactionManager;
        import org.springframework.transaction.annotation.EnableTransactionManagement;
        
        import javax.annotation.Resource;
        import javax.sql.DataSource;
        
        @Configuration
        @EnableTransactionManagement
        public class MybatisConfig {
        
            /**
             * 注入先前配置的数据源
             */
            @Resource(name = "targetDataSource")
            private DataSource dataSource;
        
            /**
             * 配置SqlSessionFactory
             * @return
             * @throws Exception
             */
            @Bean
            public SqlSessionFactory sqlSessionFactory() throws Exception {
                // 创建SqlSessionFactoryBean对象
                SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
                factoryBean.setDataSource(dataSource);
                // 因为我们使用的是通用mapper,不需要映射文件,因此不需要配置映射文件位置
                //factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*Mapper.xml"));
        
                return factoryBean.getObject();
            }
        
        
            /**
             * 配置事务管理
             * @return
             * @throws Exception
             */
            @Bean
            public PlatformTransactionManager transactionManager(){
                return new DataSourceTransactionManager(dataSource);
            }
        }
        
        
        image-20200701112500251
      • 配置AOP

        经过上面的配置我们基本上配置好了读写分离大部分解释,但是现在存在的问题是

        程序如何得知哪些方法上加了read或者write注解。即使知道了哪些方法上加了注解

        难道我们需要每一个方法都去切换数据源吗,那样效率太低了。

        我们可以利用aop思想,配置切入点和通知,在调用每个方法之前去判断,然后切换。就跟提交事务原理一样。

        配置如下:

        新建一个AOP配置类:

        package com.readwite.application.config;
        
        import org.aspectj.lang.annotation.Aspect;
        import org.aspectj.lang.annotation.Before;
        import org.aspectj.lang.annotation.Pointcut;
        import org.springframework.stereotype.Component;
        
        /**
         * 切面配置类
         */
        @Aspect
        @Component
        public class DataSourceAOP {
            /**
             * 只要加了@Read注解的方法就是一个切入点
             */
            @Pointcut("@annotation(com.readwite.application.config.Read)")
            public void readPointcut() {}
        
            /**
             * 只要加了@Write注解的方法就是一个切入点
             */
            @Pointcut("@annotation(com.readwite.application.config.Write)")
            public void writePointcut() {}
        
            /**
             * 配置前置通知,如果是readPoint就切换数据源为从数据库
             */
            @Before("readPointcut()")
            public void readAdvise() {
                DynamicSwitchDBTypeUtil.slave();
            }
        
            /**
             * 配置前置通知,如果是writePoint就切换数据源为主数据库
             */
            @Before("writePointcut()")
            public void writeAdvise() {
                DynamicSwitchDBTypeUtil.master();
            }
        }
        
        
        image-20200701112549535

    5.测试

    经过上面的配置我们已经配置好了,接下来我们来测试一下:

    • 编写pojo

      package com.readwite.application.bean;
      
      import lombok.Data;
      
      import javax.persistence.*;
      
      @Data
      @Table(name = "person")
      public class Person {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private int id;
          @Column(name = "name")
          private String name;
          @Column(name = "age")
          private int age;
      }
      
      
    • 编写dao层代码

      package com.readwite.application.dao;
      
      import com.readwite.application.bean.Person;
      import tk.mybatis.mapper.common.Mapper;
      
      public interface PersonMapper extends Mapper<Person> {
      }
      
      
      image-20200701112713376
    • 编写service

      package com.readwite.application.service;
      
      import com.readwite.application.bean.Person;
      import com.readwite.application.config.Read;
      import com.readwite.application.config.Write;
      import com.readwite.application.dao.PersonMapper;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      
      import java.util.List;
      
      @Service
      public class PersonService {
          @Autowired
          private PersonMapper personMapper;
          /**
           * 代表该方法对数据库的操作是一个写操作
           * @param person
           */
          @Write
          public void add(Person person) {
              personMapper.insert(person);
          }
      
      
          /**
           * 代表该方法对数据库的操作是一个读操作
           */
          @Read
          public List<Person> findAll() {
              return personMapper.selectAll();
          }
      }
      
      
      image-20200701105144174
    • 配置启动类

      注意:不要导包错误。导入的是tk.mybatis.spring.annotation.MapperScan

      image-20200701112739252
    • 编写测试代码

      编写插入代码

      package com.readwite.application;
      
      import com.readwite.application.bean.Person;
      import com.readwite.application.service.PersonService;
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class ApplicationTests {
      
          @Autowired
          private PersonService personService;
      
          @Test
          public void contextLoads() {
              Person person = new Person();
              person.setName("wangwu");
              person.setAge(18);
              personService.add(person);
          }
      }
      
      

      执行结果如下:

      image-20200701110437998

      数据库结果:

      image-20200701110605664

      编写查询代码:

          @Test
          public void testQuery() {
              List<Person> all = personService.findAll();
              all.forEach(System.out::println);
          }
      
      
      image-20200701110718987

      结果如下:

      image-20200701110752802

      这样我们就实现了一个读写分离。

      代码地址:https://github.com/smallCodeWangzh/application.git

    相关文章

      网友评论

        本文标题:SpringBoot+Mybatis实现读写分离

        本文链接:https://www.haomeiwen.com/subject/qmycqktx.html