为您推荐:
精华内容
最热下载
问答
  • 4星
    39.09MB haojunjun 2021-08-01 19:29:38
  • 5星
    159KB qq_17556735 2021-04-05 22:35:13
  • 5星
    9.68MB weixin_44042579 2020-12-12 11:50:12
  • 5星
    1.37MB cuihao1995 2021-05-22 15:04:41
  • 1.1、如何实现 MySQL 的读写分离? 其实很简单,就是基于主从复制架构。简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从去,多个从库用于读。 读写分离就是对于一...

    前言:高并发这个阶段,肯定是需要做MySQL读写分离的。实际上大部分的互联网网站或者App,其实都是读多写少。所以针对这个情况,就是写一个主库,但是主库挂多个从库,然后从多个从库来读,那不就可以支撑更高的读并发压力了吗?

    一、 MySQL 读写分离

    1.1、如何实现 MySQL 的读写分离?

    其实很简单,就是基于主从复制架构。简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从去,多个从库用于读。

    读写分离就是对于一条SQL该选择哪一个数据库去执行,至于谁来做选择数据库这件事,有两个,要么使用中间件帮我们做,要么程序自己做。一般来说,读写分离有两种实现方式。第一种是依靠中间件MyCat或Sharding-JDBC,也就是说应用程序连接到中间件,中间件帮我们做SQL分离,去选择指定的数据源;第二种是应用程序自己去做分离。这里我用程序自己来做,主要是利用Spring提供的路由数据源,以及AOP。

     1.2、MySQL 主从复制原理?

    主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL,这样就可以保证自己跟主库的数据是一样的。

    mysql-master-slave

     这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

    而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。

    所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

    这个所谓半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。

    所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

    1.3、MySQL 主从同步延时问题(精华)

    线上会发现,每天总有那么一些数据,我们期望更新一些重要的数据状态,但在高峰期时候却没更新。用户跟客服反馈,而客服就会反馈给我们。

    (1) 主从同步延迟的原因

    一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长或者由于某个SQL要进行锁表就会导致,主服务器的SQL大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。

    (2) 主从同步延迟的解决办法

    一般来说,如果主从延迟较为严重,有以下解决方案:

    • 分库:将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。

    • 需要走主库的强制走主库查询:如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库。

    • 业务层面妥协,重写代码写代码的同学要慎重,插入数据时立马查询可能查不到。是否操作完之后马上要马上进行读取?


    二、SpringBoot+AOP+MyBatis实现MySQL读写分离

    代码环境是 SpringBoot+MyBatis+AOP。想要读写分离就需要配置多个数据源,在进行写操作是选择写的数据源(主库),读操作时选择读的数据源(从库)。

    2.1、AbstractRoutingDataSource

    SpringBoot提供了AbstractRoutingDataSource类根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源

    想要读写分离就需要配置多个数据源,在进行写操作是选择写的数据源,读操作时选择读的数据源。其中有两个关键点:

    • 如何切换数据源

    • 如何根据不同的方法选择正确的数据源

    2.2、如何切换数据源

     通常用 springboot 时都是使用它的默认配置,只需要在配置文件中定义好连接属性就行了,但是现在我们需要自己来配置了,spring 是支持多数据源的,多个 datasource 放在一个 HashMapTargetDataSource中,通过dertermineCurrentLookupKey获取 key 来觉定要使用哪个数据源。因此我们的目标就很明确了,建立多个 datasource 放到 TargetDataSource 中,同时重写 dertermineCurrentLookupKey 方法来决定使用哪个 key。

    2.3、如何选择数据源

     事务一般是注解在 Service 层的,因此在开始这个 service 方法调用时要确定数据源,有什么通用方法能够在开始执行一个方法前做操作呢?相信你已经想到了那就是**切面 **。怎么切有两种办法:

    • 注解式,定义一个只读注解,被该数据标注的方法使用读库

    • 方法名,根据方法名写切点,比如 getXXX 用读库,setXXX 用写库


    三 、代码实现

    3.0、工程目录结构

     3.1、引入Maven依赖

    <dependencies>
    
            <!--SpringBoot集成Aop起步依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
            <!--SpringBoot集成WEB起步依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <!--mybatis集成SpringBoot起步依赖-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.3</version>
            </dependency>
    
            <!--MySQL驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <!--SpringBoot单元测试依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
     </dependencies>
    

    3.2、编写配置文件,配置主从数据源

    spring:
      datasource:
          #主数据源
          master:
            name: test
            jdbc-url: jdbc:mysql://xxxxxx:3306/test?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=utf-8
            username: root
            password: xxxxxx
            driver-class-name: com.mysql.cj.jdbc.Driver
            hikari:
              maximum-pool-size: 20
              max-lifetime: 30000
              idle-timeout: 30000
              data-source-properties:
                prepStmtCacheSize: 250
                prepStmtCacheSqlLimit: 2048
                cachePrepStmts: true
                useServerPrepStmts: true
          #从数据源
          slave:
            name: test
            jdbc-url: jdbc:mysql://xxxxxx:3306/test?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=utf-8
            username: root
            password: xxxxxx
            driver-class-name: com.mysql.cj.jdbc.Driver
            hikari:
              maximum-pool-size: 20
              max-lifetime: 30000
              idle-timeout: 30000
              data-source-properties:
                prepStmtCacheSize: 250
                prepStmtCacheSqlLimit: 2048
                cachePrepStmts: true
                useServerPrepStmts: true
    
    #MyBatis:
    # mapper-locations: classpath:mapper/*.xml
    #  type-aliases-package: com.hs.demo.entity

    3.3、Enum类,定义主库从库

    定义一个枚举类来代表这三个数据源

    package com.hs.demo.config;
     
    /**
     * Enum类,定义主库从库两个数据源
     */
    public enum DBTypeEnum {
     
        MASTER, SLAVE;
    }

    3.4、ThreadLocal定义数据源切换

    通过ThreadLocal将数据源绑定到每个线程上下文中,ThreadLocal 用来保存每个线程的是使用读库还是写库。操作结束后清除该数据,避免内存泄漏。

    package com.hs.demo.config;
    
    /**
     *ThreadLocal定义数据源切换,通过ThreadLocal将数据源绑定到每个线程上下文中
     */
    public class DBContextHolder {
     
        /**
         * ThreadLocal 不是 Thread,是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,对数据存储后,只有在线程中才可以获取到存储的数据,对于其他线程来说是无法获取到数据。
         * 大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的,通过get和set方法就可以得到当前线程对应的值。
         */
        private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
     
        public static void set(DBTypeEnum dbTypeEnum){
            contextHolder.set(dbTypeEnum);
        }
     
        public static DBTypeEnum get() {
            return contextHolder.get();
        }
     
        public static void master() {
            set(DBTypeEnum.MASTER);
            System.out.println("--------以下操作为master(写操作)--------");
        }
     
        public static void slave() {
            set(DBTypeEnum.SLAVE);
            System.out.println("--------以下操作为slave(读操作)--------");
        }
     
        public static void clear() {
            contextHolder.remove();
        }
    }

    3.5、重写路由选择类

    重写 determineCurrentLookupKey 方法,获取当前线程上绑定的路由key。Spring 在开始进行数据库操作时会通过这个方法来决定使用哪个数据库源,因此我们在这里调用上面 DbContextHolder 类的getDbType()方法获取当前操作类别。

    • AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。

    • AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。

    • 基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。

    package com.hs.demo.config;
     
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import org.springframework.lang.Nullable;
     
    /**
     *  重写路由选择类:获取当前线程上绑定的路由key
     */
    public class MyRoutingDataSource extends AbstractRoutingDataSource {
     
        /**
         * determineCurrentLookupKey()方法决定使用哪个数据源、
         * 根据Key获取数据源的信息,上层抽象函数的钩子
         */
        @Nullable
        @Override
        protected Object determineCurrentLookupKey() {
            return DBContextHolder.get();
        }
    }

    3.6、配置多数据源

    这里配置了3个数据源,1个master,1个slave,1个路由数据源。前2个数据源都是为了生成第3个数据源,而且后续我们只用这最后一个路由数据源。

    package com.hs.demo.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;
     
    /**
     * 增加了 DataSourceConfig 这个配置文件之后,需要添加Hikari连接池,单数据源自动装载时不会出这 
     * 样的问题
     *
     * @Configuration 注解,表明这就是一个配置类,指示一个类声明一个或者多个@Bean 声明的方法并且由Spring容器统一管理,以便在运行时为这些bean生成bean的定义和服务请求的类。
     */
    @Configuration
    public class DataSourceConfig {
    
        /**
         * 注入主库数据源
         */
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDataSource() {
    
            return DataSourceBuilder.create().build();
    
            //DataSourceProperties properties放在方法参数里
    //        return DataSourceBuilder.create(properties.getClassLoader())
    //                .type(HikariDataSource.class)
    //                .driverClassName(properties.getDriverClassName())
    //                .url(properties.getUrl())
    //                .username(properties.getUsername())
    //                .password(properties.getPassword())
    //                .build();
        }
     
        /**
         * 注入从库数据源
         */
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDataSource() {
    
            return DataSourceBuilder.create().build();
        }
     
     
        /**
         * 配置选择数据源
         * @param masterDataSource
         * @param slaveDataSource
         * @return DataSource
         */
        @Bean
        public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource)
        {
            Map<Object, Object> targetDataSource = new HashMap<>();
            targetDataSource.put(DBTypeEnum.MASTER, masterDataSource);
            targetDataSource.put(DBTypeEnum.SLAVE, slaveDataSource);
     
            MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
            //找不到用默认数据源
            myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
            //可选择目标数据源
            myRoutingDataSource.setTargetDataSources(targetDataSource);
    
            return myRoutingDataSource;
     
        }
    }

    3.7、配置Mybatis指定数据源

    修改SqlSessionFactory 和事务管理器

    package com.hs.demo.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.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    import javax.annotation.Resource;
    import javax.sql.DataSource;
     
    /**
     * 配置Mybatis指定数据源:SqlSessionFactory和事务管理器
     */
    @Configuration
    @EnableTransactionManagement
    public class MyBatisConfig {
     
        /**
         * 注入自己重写的数据源
         */
        @Resource(name = "myRoutingDataSource")
        private DataSource myRoutingDataSource;
     
        /**
         * 配置SqlSessionFactory
         * @return SqlSessionFactory
         * @throws Exception
         */
        @Bean
        public SqlSessionFactory sqlSessionFactory() throws Exception
        {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
     
            //ResourcePatternResolver(资源查找器)定义了getResources来查找资源
            //PathMatchingResourcePatternResolver提供了以classpath开头的通配符方式查询,否则会调用ResourceLoader的getResource方法来查找
    //        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    //        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocation));
     
            return sqlSessionFactoryBean.getObject();
        }
     
     
        /**
         * 事务管理器,不写则事务不生效:事务需要知道当前使用的是哪个数据源才能进行事务处理
         */
        @Bean
        public PlatformTransactionManager platformTransactionManager() {
            return new DataSourceTransactionManager(myRoutingDataSource);
        }
    
    //    /**
    //     * 当自定义数据源,用户必须覆盖SqlSessionTemplate,开启BATCH处理模式
    //     *
    //     * @param sqlSessionFactory
    //     * @return
    //     */
    //    @Bean
    //    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
    //        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
    //    }
    
    }
     

    3.8、AOP切面实现数据源切换

    通过Aop的前置通知来设置要使用的路由key(数据源)

    package com.hs.demo.config;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    import java.lang.reflect.Method;
    
    /**
     * 默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)
     *
     * 切面不能建立在DAO层,事务是在service开启的,到dao层再切换数据源,那事务就废了
     *
     */
    @Aspect
    @Component
    public class DataSourceAop {
    
        /**
         * 第一个”*“符号	表示返回值的类型任意;
         * com.sample.service.impl	AOP所切的服务的包名,即,我们的业务部分
         * 包名后面的”..“	表示当前包及子包
         * 第二个”*“	表示类名,*即所有类。此处可以自定义,下文有举例
         * .*(..)	表示任何方法名,括号表示参数,两个点表示任何参数类型
         */
        @Pointcut("!@annotation(com.hs.demo.config.Master) " +
                "&& (execution(* com.hs.demo.service.*.select*(..)) " +
                "|| execution(* com.hs.demo.service..*.find*(..)))")
        public void readPointcut() {
    
        }
    
        @Pointcut("@annotation(com.hs.demo.config.Master) " +
                "|| execution(* com.hs.demo.service..*.save*(..)) " +
                "|| execution(* com.hs.demo.service..*.add*(..)) " +
                "|| execution(* com.hs.demo.service..*.insert*(..)) " +
                "|| execution(* com.hs.demo.service..*.update*(..)) " +
                "|| execution(* com.hs.demo.service..*.edit*(..)) " +
                "|| execution(* com.hs.demo..*.delete*(..)) " +
                "|| execution(* com.hs.demo..*.remove*(..))")
        public void writePointcut() {
    
        }
    
        @Before("readPointcut()")
        public void read(JoinPoint jp) {
    
            //获取当前的方法信息
            MethodSignature methodSignature = (MethodSignature) jp.getSignature();
            Method method = methodSignature.getMethod();
    
            //判断方法上是否存在注解@Master
            boolean present = method.isAnnotationPresent(Master.class);
            if (!present)
            {
                //如果不存在,默认走从库读
                DBContextHolder.slave();
            }
            else
            {
                //如果存在,走主库读
                DBContextHolder.master();
            }
    
        }
    
        @Before("writePointcut()")
        public void write() {
            DBContextHolder.master();
        }
    
    
        /**
         * 另一种写法:if...else...  判断哪些需要读从数据库,其余的走主数据库
         */
    //    @Before("execution(* com.cjs.example.service.impl.*.*(..))")
    //    public void before(JoinPoint jp) {
    //        String methodName = jp.getSignature().getName();
    //
    //        if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
    //            DBContextHolder.slave();
    //        }else {
    //            DBContextHolder.master();
    //        }
    //    }
    
    }
    

    3.9、如果有强制走主库的操作,可以定义注解

    package com.hs.demo.config;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 有时候主从延迟,需要强制读主库的注解
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Master
    {
        //设置数据源类型
       //String value();
    }

    3.10、自行定义CRUD读写操作

    (1)UserEntity

    package com.hs.demo.entity;
    
    import lombok.Data;
    
    /**
     * @author heshi
     * @date 2021/10/20 15:14
     */
    
    @Data
    public class UserEntity {
    
        private Integer user_id;
        private String account;
        private String nickname;
        private String password;
        private String headimage_url;
        private String introduce;
    
    }
    

    (2)UserMapper

    package com.hs.demo.mapper;
    
    import com.hs.demo.entity.UserEntity;
    import org.apache.ibatis.annotations.*;
    
    import java.util.List;
     
    /**
     * Spring通过@Mapper注解实现动态代理,mybatis会自动创建Dao接口的实现类代理对象注入IOC容器进行管理,这样就不用编写Dao层的实现类
     *
     */
    @Mapper
    public interface UserMapper {
     
        @Select("SELECT * FROM user")  //使用@Select、@Insert等注解方式来实现对应的持久化操作,使得我们可以不配置XML格式的Mapper文件
        List<UserEntity> findAll();
    
        @Insert("insert into user(account,nickname,password) values(#{account}, #{nickname}, #{password})")
        int insert(UserEntity user);
    
        @Update("UPDATE user SET account=#{account},nickname=#{nickname} WHERE id =#{id}")
        void update(UserEntity user);
    
        @Delete("DELETE FROM user WHERE id =#{id}")
        void delete(Long id);
    }

    (3)UserService(重要)

    package com.hs.demo.service;
    
    import com.hs.demo.entity.UserEntity;
    import com.hs.demo.mapper.UserMapper;
    import com.hs.demo.mysql.Master;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.List;
    
    /**
     * @author heshi
     * @date 2021/10/21 10:36
     */
    
    @Service
    public class UserService {
    
        @Autowired
        UserMapper userMapper;
    
    //    @Master
        public List<UserEntity> findAll()
        {
            List<UserEntity> userEntities = userMapper.findAll();
            return userEntities;
        }
    
        public int insertUser(UserEntity user)
        {
            int i = userMapper.insert(user);
            return i;
        }
    
    //    void update(UserEntity user);
    //
    //    void delete(Long id);
    
    }
    

      (4)  UserController

    package com.hs.demo.controller;
    
    import com.hs.demo.entity.UserEntity;
    import com.hs.demo.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
     
    @RestController
    public class UserController
    {
        @Autowired
        UserService userService;
     
        @RequestMapping("/listUser")
        public List<UserEntity>  listUser()
        {
            List<UserEntity> users = userService.findAll();
            return users;
        }
    
        @RequestMapping("/insertUser")
        public void insertUser()
        {
           UserEntity userEntity = new UserEntity();
           userEntity.setAccount("22222");
           userEntity.setNickname("hshshs");
           userEntity.setPassword("123");
           userService.insertUser(userEntity);
        }
     
    }

    运行结果如下图所示

     总结:通过AOP来确定所使用数据源类型,然后通过路由来进行数据源选择。


    参考链接:

    springboot实现读写分离(基于Mybatis,mysql)

    springboot实现mysql的读写分离

    SpringBoot AOP Mysql主从复制

    展开全文
    CSDN2497242041 2021-10-21 23:15:22
  • 传统的MySql读写分离方案是通过在代码中根据SQL语句的类型动态切换数据源来实现的,那么有没有什么中间件可以自动实现读写分离呢?小米开源的数据库中间件Gaea就可以实现,接下来我们将详细讲解如何使用Gaea来实现...

    简介

    传统的MySql读写分离方案是通过在代码中根据SQL语句的类型动态切换数据源来实现的,那么有没有什么中间件可以自动实现读写分离呢?小米开源的数据库中间件Gaea就可以实现,接下来我们将详细讲解如何使用Gaea来实现MySql的读写分离。
    Gaea是小米中国区电商研发部研发的基于MySql协议的数据库中间件,目前在小米商城大陆和海外得到广泛使用,包括订单、社区、活动等多个业务。Gaea支持分库分表、SQL路由、读写分离等基本特性,其中分库分表方案兼容了mycat和kingshard两个项目的路由方式。

    前提

    要实现读写分离的前提条件是要先实现MySql主从复制,具体实现可在这里查看:Mysql主从复制

    安装Gaea

    目前官方提供的是在Linux下直接安装的方式

    安装Go语言环境

    由于Gaea是使用Go语言编写的,所以我们需要先安装Go语言的环境。

    • 先下载安装包
    wget https://dl.google.com/go/go1.13.5.linux-amd64.tar.gz
    

    如果提示wegt命令找不到,可执行

    yum -y install wget
    
    • 下载完成后解压到/mydata目录下
    tar -zxvf go1.13.5.linux-amd64.tar.gz -C /mydata/
    
    • 添加/mydata/go/bin目录到PATH变量中
    # 编辑环境变量配置文件
    vim /etc/profile
    # 在最后一行添加
    export GOROOT=/mydata/go
    export PATH=$PATH:$GOROOT/bin
    # 刷新配置文件
    source /etc/profile
    
    • 查看版本号,测试是否安装成功
    go version
    
    • 返回以下信息表示Go语言环境已经安装成功了
    go version go1.13.5 linux/amd64
    

    安装Gaea

    由于Gaea并没有提供安装包,所以我们需要自行编译源码获取可执行文件

    • 下载Gaea的源码,直接下载zip包即可
      下载地址:https://github.com/XiaoMi/Gaea
    • 将下载好的压缩包进行解压操作,这里我们解压到/mydata/gaea/目录下:
    unzip Gaea-master.zip
    
    • 进入/mydata/gaea/目录下,使用make命令对源码编译:
    make build
    

    tip:某些Go的依赖会下载不下来导致编译失败,这里可能会出现连接失败
    这时可以执行一下命令:

    #切换成一个国内能访问的代理地址
    go env -w GOPROXY=https://goproxy.cn;
    ##配置 GOPROXY 环境变量
    export GOPROXY=https://goproxy.io;
    

    这时再去执行make build 命令就能编译成功了

    • 编译完成后在/mydata/gaea/bin目录下会生成Gaea的执行文件gaea
      在这里插入图片描述
    • 由于我们没有搭建etcd配置中心,所以需要修改本地配置文件/mydata/gaea/etc/gaea.ini,将配置类型改为file
    config_type=file
    

    在这里插入图片描述

    • 添加namespace配置文件,用于配置我们的主从数据库信息,配置文件地址:/mydata/gaea/etc/file/namespace/wdj_namespace_1.json
      在这里插入图片描述
    • 配置文件内容如下
    {
        "name": "wdj_namespace_1",
        "online": true,
        "read_only": false,
        "allowed_dbs": {
            "mall": true
        },
        "slow_sql_time": "1000",
        "black_sql": [
            ""
        ],
        "allowed_ip": null,
        "slices": [
            {
                "name": "slice-0",
                "user_name": "root",
                "password": "root",
                "master": "192.168.131.3:3307",
                "slaves": ["192.168.131.3:3308"],
                "statistic_slaves": null,
                "capacity": 12,
                "max_capacity": 24,
                "idle_timeout": 60
            }
        ],
        "shard_rules": null,
        "users": [
            {
                "user_name": "wdj",
                "password": "123456",
                "namespace": "wdj_namespace_1",
                "rw_flag": 2,
                "rw_split": 1,
                "other_property": 0
            }
        ],
        "default_slice": "slice-0",
        "global_sequences": null
    }
    

    namespace配置文件

    namespace的配置格式为json,包含分表、非分表、实例等配置信息,都可在运行时改变

    • 整体配置说明
      在这里插入图片描述
    • slice配置
      在这里插入图片描述
    • users配置
      在这里插入图片描述

    在Docker容器中运行

    打包成Docker镜像

    Docker Hub 中并没有打包好的Gaea镜像,我们需要自行构建一个

    • 这里我们使用Dockerfile构建Docker镜像,Dockerfile中的内容如下
    # 该镜像需要依赖的基础镜像
    FROM golang:latest
    # 将当前目录下的gaea源码包复制到docker容器的/go/Gaea-master目录下,对于.tar.gz文件会自动解压
    ADD Gaea-master.tar.gz /go/Gaea-master
    # 将解压后的源码移动到/go/gaea目录中去
    RUN bash -c 'mv /go/Gaea-master/Gaea-master /go/gaea'
    # 进入/go/gaea目录
    WORKDIR /go/gaea
    RUN bash -c 'go env -w GOPROXY=https://goproxy.cn'
    RUN bash -c 'export GOPROXY=https://goproxy.io'
    # 将gaea源码进行打包编译
    RUN bash -c 'make build'
    # 声明服务运行在13306端口
    EXPOSE 13306
    # 指定docker容器启动时执行的命令
    ENTRYPOINT ["/go/gaea/bin/gaea"]
    # 指定维护者的名字
    MAINTAINER wdj
    

    TIP;在此之前我们需要把Gaea的源码压缩包转换为.tar.gz格式
    转换命令:

    #将xxx.zip解压  
    unzip xxx.zip
    #会得到xxx文件夹
    #将xxx压缩成tar.gz   
    tar -czf  xxx.tar.gz  xxx
    #就得到了xxx.tar.gz
    
    • 之后使用Docker命令构建Gaea的Docker镜像
    docker build -t gaea:1.0.2 .
    
    • 构建成功控制台输出
      在这里插入图片描述
    • 将本地安装的Gaea配置文件复制到/mydata/gaea-docker/etc/目录下
    cp -r /mydata/gaea/etc/ /mydata/gaea-docker/etc/
    
    • 使用Docker命令启动Gaea容器
    docker run -p 13306:13306 --name gaea \
    -v /mydata/gaea-docker/etc:/go/gaea/etc \
    -d gaea:1.0.2
    

    测试读写分离

    测试思路:首先我们关闭从实例的主从复制,然后通过Gaea代理来操作数据库,插入一条数据,如果主实例中有这条数据而从实例中没有,说明写操作是走的主库。然后再通过Gaea代理查询该表数据,如果没有这条数据,表示读操作走的是从库,证明读写分离成功。

    • 通过Navicat连接到Gaea代理,注意此处账号密码为Gaea的namespace中配置的内容,端口为Gaea的服务端口
      在这里插入图片描述
    • 通过Navicat分别连接到主库和从库,用于查看数据,此时建立了以下三个数据库连接
      在这里插入图片描述
    • 通过stop slave命令关闭mysql-slave实例的主从复制功能
      在这里插入图片描述
    • 通过Gaea代理在test表中插入一条数据
      在这里插入图片描述
    • 在主库中查看test表的数据,发现已有该数据
      在这里插入图片描述
    • 在从库中查看test表的数据,发现没有该数据,证明写操作走的是主库
      在这里插入图片描述
    • 直接在代理中查看test表中的数据,发现没有该数据,证明读操作走的是从库
      在这里插入图片描述

    结合SpringBoot使用

    在我们的SpringBoot应用中,我们只需要把Gaea的代理服务直接当做数据库服务来使用就可以实现读写分离了。

    测试连接代码

    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.ResultSet;
    import java.sql.Statement;
    /**
     * @author wdj
     * Created on 2021/2/5 11:40
     */
    
    public class JdbcTest {
        //属性
        private static String driver="com.mysql.jdbc.Driver";
        //JDBC驱动类名
        private static final String URL="jdbc:mysql://192.168.131.3:13306/mall?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=false";
        private static final String USER="wdj";
        //用户名
        private static final String PASSWORD="123456";
        //密码
        //创建连接
        public static Connection getConnection(){
            Connection conn=null;
            try {
                Class.forName(driver);
                //加载jdbc驱动
                conn=DriverManager.getConnection(URL, USER, PASSWORD);
    //创建连接
                //System.out.println("创建连接成功!");
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return conn;
        }
        //关闭连接
        public static void close(Connection conn,Statement st,ResultSet rs){
            if(rs!=null){
                try {
                    rs.close();
                    //System.out.println("关闭rs成功!");
                } catch (Exception e) {
                    // TODO: handle exception
                    e.printStackTrace();
                }
            }
            if(st!=null){
                try {
                    st.close();
                    //System.out.println("关闭st成功!");
                } catch (Exception e) {
                    // TODO: handle exception
                    e.printStackTrace();
                }
            }
            if(conn!=null){
                try {
                    conn.close();
                    //System.out.println("关闭conn成功!");
                } catch (Exception e) {
    
                    // TODO: handle exception
                    e.printStackTrace();
                }
            }
        }
    
    
        public static void main(String[] args) {
    
            try        {
                Connection conn = JdbcTest.getConnection();
                if(conn!=null)
                {
                    System.out.println("数据库连接正常!");
                }
                else            {
                    System.out.println("数据库连接异常!");
                }
            }
            catch(Exception ex)
            {
                ex.printStackTrace();
            }
    
        }
    }
    
    展开全文
    weixin_39387961 2021-02-04 16:57:32
  • 前言如果主库只负责所有的读写操作,而从库只实现备份功能,这样的主从架构看起来性价比似乎不是很高。...实现读写分离,实际上有两种经典方式第一种方式即我们在代码层实现逻辑,对到达的读/写请求进行解析,...

    前言

    如果主库只负责所有的读写操作,而从库只实现备份功能,这样的主从架构看起来性价比似乎不是很高。

    我们所希望的主从架构是,当我们在写数据时,请求全部发到Master节点上,当我们需要读数据时,请求全部发到Slave节点上。

    并且多个Slave节点最好可以存在负载均衡,那么这样的架构就能够让集群的效率最大化。

    实现读写分离,实际上有两种经典方式

    第一种方式即我们在代码层实现逻辑,对到达的读/写请求进行解析,针对性地分发到不同的数据库中,从而实现读写分离;

    第二种方式是基于类似MyCat中间件来帮助我们实现读写分离的效果;

    最简单的主从架构99e73bf9eb126e2714fc2f442155ff7f.png

    代码层来实现读写分离

    默认大家对MyBatis和Spring都有所了解,最简单的实现方式:

    (1)Spring注入数据源,包括Master-Slave;

    (2)手写MyBatis拦截器,判断一个SQL语句是读还是写;

    (3)选择对应的数据源进行执行

    实现的方式很直观,但弊端也显而易见,这样对代码的侵入性太强,当需要对数据源进行增删时,一定会对代码造成影响。这不但会给开发人员造成很大的困扰,而且不符合软件设计的开闭原则。

    专业的事情交给专业的人,这也是像MyCat这样的中间件出现的原因。

    通过MyCat来实现读写分离3d1ef3b4da977822705b4f56173c4fbe.png

    这种方式最主要的特点就是我们在整体架构中新建了一个虚拟节点,所有的请求先到达这个虚拟节点上,由这个虚拟节点来转发读写请求到相应的数据库。

    通过MyCat这样的专业中间件,可以更加有效率地实现读写分离、故障转移等功能。

    优劣分析

    关于这两种方式的优劣,相信如果仔细看完这篇文章的同学都会有一个深刻的体会。

    使用MyCat中间件实现读写分离,优点十分明显,我们只需要进行配置就可以享受读写分离带来的效率的提升,不用写一行代码,并且当主库宕机时,我们还可以通过配置的方式进行主从库的自动切换,这样即使主库宕机我们的整个集群也不会丧失写的功能。

    其缺点当然是需要我们维护这样一个虚拟节点,增加了运维成本,但相比较它所带来的好处而言,这完全是可以接受的。

    Ending

    我是无忌,Stay Tuned !!各位的喜爱是对我最大的鼓励,下篇文章再见~

    d0a31bea69a079dd9b09589e40089605.png

    展开全文
    weixin_33054119 2021-01-18 19:16:37
  • 实际项目中大都读多写少,如果查询出现瓶颈之后,我们可以考虑使用读写分离。 比如有三台Mysql服务器A、B、C,一主二从,先配置好主从复制之后,再来做读写分离,A用来做update操作,B和C用来做select操作。 网上...

    目录

    背景

    分析

    准备工作

    编码

    1、多数据源配置文件

    2、DataSource配置

    3、MybatisConfig配置

    4、创建 ClientDataSource 枚举 定义主从库

    5、创建 ClientDataSourceContextHolder 来保存 ClientDataSource

    6、继承 AbstractRoutingDataSource

    7、通过注解 DataSourceRouting 来标识走master/slave

    8、创建 DataSourceRoutingAspect,来处理注解 DataSourceRouting

    9、主类 ReadWriteSeparationMybatisApplication

    10、测试

    TestController:

     UserServiceImpl

    UserDAO

    10、总结

    11、源码


    背景

    实际项目中大都读多写少,如果查询出现瓶颈之后,我们可以考虑使用读写分离。

    比如有三台Mysql服务器A、B、C,一主二从,先配置好 主从复制 之后,再来做读写分离,A用来做update操作,B和C用来做select操作。

    网上很多文章都写的比较乱,这里我尽量简单优雅的完成。

    分析

    有很多中间件可以使用,比如:Mycat,当当的Sharding-JDBC,美团的DBProxy等,但是都需要依赖第三方组件,增加学习和money成本,

    这里我们使用Spring提供的轻量级数据路由类 AbstractRoutingDataSource 来实现

    准备工作

    1、我这边准备两个DB,maple_master,maple_slave,主从复制这里就不做了,感兴趣的可以看这里主从复制 ;

    DDL和DML为:

    CREATE TABLE `user` (
      `user_id` varchar(16) NOT NULL,
      `user_name` varchar(64) DEFAULT NULL,
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='user';
    
    INSERT INTO `maple_master`.`user`(`user_id`, `user_name`) VALUES ('1', 'maple_master');
    INSERT INTO `maple_slave`.`user`(`user_id`, `user_name`) VALUES ('1', 'maple_slave');
    

    2、我这里使用常规的技术栈:SpingBoot + Mybatis + Maven,首先导入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.4.3</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.gane.maple</groupId>
        <artifactId>read-write-separation-mybatisplus</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>read-write-separation-mybatis</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-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
                <version>2.4.2</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.10</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.3</version>
            </dependency>
    
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.9</version>
            </dependency>
    
            <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>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    编码

    1、多数据源配置文件

    #master
    spring.datasource.master.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/maple_master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
    spring.datasource.master.username=root
    spring.datasource.master.password=root
    
    #slave
    spring.datasource.slave.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/maple_slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
    spring.datasource.slave.username=root
    spring.datasource.slave.password=root

    2、DataSource配置

    package com.gane.maple.jdbc.datasource;
    
    import com.gane.maple.jdbc.routing.ClientDataSource;
    import com.gane.maple.jdbc.routing.component.ClientDataSourceRouter;
    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 org.springframework.context.annotation.Primary;
    
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author maple
     * @date 2021/3/3
     */
    @Configuration
    public class DataSourceConfig {
    
        @Bean(name = "masterDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name = "slaveDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Primary
        @Bean(name = "dynamicDatasource")
        public ClientDataSourceRouter dynamicDatasource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
    
            ClientDataSourceRouter dataSourceRouter = new ClientDataSourceRouter();
            dataSourceRouter.setDefaultTargetDataSource(masterDataSource);
    
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put(ClientDataSource.MASTER, masterDataSource);
            targetDataSources.put(ClientDataSource.SLAVE, slaveDataSource);
            dataSourceRouter.setTargetDataSources(targetDataSources);
    
            return dataSourceRouter;
        }
    }

    3、MybatisConfig配置

    package com.gane.maple.jdbc.datasource;
    
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    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 javax.sql.DataSource;
    
    /**
     * @author maple
     * @date 2021/3/3
     */
    @Configuration
    @MapperScan(MybatisConfig.MAPPER_PACKAGE)
    public class MybatisConfig {
    
        public static final String MAPPER_PACKAGE = "com.gane.maple.dao";
        public static final String TYPE_ALIASES_PACKAGE = "com.gane.maple.dao.entity";
        public static final String MAPPER_XML_LOCATIONS = "mapper/*Mapper.xml";
    
        @Bean
        public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDatasource") DataSource dataSource) throws Exception {
            SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
            factoryBean.setDataSource(dataSource);
            factoryBean.setTypeAliasesPackage(TYPE_ALIASES_PACKAGE);
            factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_LOCATIONS));
            return factoryBean.getObject();
        }
    
        @Bean
        public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDatasource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    

    4、创建 ClientDataSource 枚举 定义主从库

    package com.gane.maple.jdbc.routing;
    
    /**
     * @author maple
     * @date 2021/3/3
     */
    public enum ClientDataSource {
        MASTER, SLAVE
    }
    

    5、创建 ClientDataSourceContextHolder 来保存 ClientDataSource

    package com.gane.maple.jdbc.routing;
    
    import java.util.Objects;
    
    /**
     * Context Holder that will hold the value for datasource routing for each different thread 
     * (request).
     *
     * @author maple
     * @date 2021/3/3
     */
    public class ClientDataSourceContextHolder {
    
        private static final ThreadLocal<ClientDataSource> CONTEXT = new ThreadLocal<>();
    
        public static void set(ClientDataSource clientDataSource) {
            CONTEXT.set(Objects.requireNonNull(clientDataSource, "clientDatabase cannot be null"));
        }
    
        public static ClientDataSource getClientDatabase() {
            return CONTEXT.get();
        }
    
        public static void clear() {
            CONTEXT.remove();
        }
    }
    

    6、继承 AbstractRoutingDataSource

    重写 determineCurrentLookupKey 方法,返回所使用的数据源的Key(master/slave)给到 resolvedDataSources,从而通过Key从resolvedDataSources里拿到其对应的DataSource

    package com.gane.maple.jdbc.routing.component;
    
    import com.gane.maple.jdbc.routing.ClientDataSource;
    import com.gane.maple.jdbc.routing.ClientDataSourceContextHolder;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    /**
     * {@link javax.sql.DataSource} for spring framework that will gives the desired     
     * datasource based on the
     * current value stored in the {@link ClientDataSourceContextHolder}
     *
     * @author maple
     * @date 2021/3/3
     */
    @Slf4j
    public class ClientDataSourceRouter extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            ClientDataSource clientDataSource = ClientDataSourceContextHolder.getClientDatabase();
            if (clientDataSource == null) {
                log.debug("null client database, use default {}", ClientDataSource.MASTER);
                clientDataSource = ClientDataSource.MASTER;
            }
            log.trace("use {} as database", clientDataSource);
            return clientDataSource;
        }
    }
    

    7、通过注解 DataSourceRouting 来标识走master/slave

    package com.gane.maple.jdbc.routing.annotation;
    
    import com.gane.maple.jdbc.routing.ClientDataSource;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * Indicates that a method uses a specific datasource defined in {@link ClientDataSource}.
     *
     * @author maple
     * @date 2021/3/3
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DataSourceRouting {
    
        ClientDataSource value() default ClientDataSource.MASTER;
    }
    

    8、创建 DataSourceRoutingAspect,来处理注解 DataSourceRouting

    package com.gane.maple.jdbc.routing.component;
    
    import com.gane.maple.jdbc.routing.ClientDataSource;
    import com.gane.maple.jdbc.routing.ClientDataSourceContextHolder;
    import com.gane.maple.jdbc.routing.annotation.DataSourceRouting;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.springframework.stereotype.Component;
    
    /**
     * Aspect that will mark a method to route to the desired datasource before calling the 
     * method.
     *
     * @author maple
     * @date 2021/3/3
     */
    @Aspect
    @Component
    @Slf4j
    public class DataSourceRoutingAspect {
    
        @Around("@annotation(dataSourceRouting)")
        public Object aroundDataSourceRouting(ProceedingJoinPoint joinPoint, DataSourceRouting dataSourceRouting)
                throws Throwable {
            ClientDataSource previousClient = ClientDataSourceContextHolder.getClientDatabase();
            log.warn("Setting clientDatabase {} into DataSourceContext", dataSourceRouting.value());
            ClientDataSourceContextHolder.set(dataSourceRouting.value());
    
            try {
                return joinPoint.proceed();
            } finally {
                if (previousClient != null) {
                    // revert context back to previous state after execute the method
                    ClientDataSourceContextHolder.set(previousClient);
                } else {
                    // there is no value being set into the context before, just clear the context
                    // to prevent memory leak
                    ClientDataSourceContextHolder.clear();
                }
            }
        }
    }
    

    9、主类 ReadWriteSeparationMybatisApplication

    由于我们没有使用 spring.datasource.url、spring.datasource.username 默认的配置,而是自定义的 spring.datasource.master.jdbc-url、spring.datasource.master.username 等配置,

    所以我们需要排除Spring的自动配置类 DataSourceAutoConfiguration,防止在我们启动项目的时候,由于找不到  spring.datasource.url、spring.datasource.username 等配置而报了 “url” 未配置的 错误。

    package com.gane.maple;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
    
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class ReadWriteSeparationMybatisApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ReadWriteSeparationMybatisApplication.class, args);
        }
    }
    

    10、测试

    自测成功,可自行debug

    TestController:

    package com.gane.maple.controller;
    
    import com.gane.maple.entity.User;
    import com.gane.maple.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author maple
     * @date 2021/3/2
     */
    @RestController
    public class TestController {
    
        @Autowired
        private UserService userService;
    
        @GetMapping("/queryUser")
        public User queryUser() {
    
            User userFromMaster = userService.selectByUserId("1");
    
            User userFromSlave = userService.selectByUserName("maple_slave");
    
            User userFromMasterAndSlave = userService.selectFromMasterAndSlave("1", "maple_slave");
    
            User selectFromMasterAndSlaveWithDataSourceRouting = userService.selectFromMasterAndSlaveWithDataSourceRoutingInDao("1", "maple_slave");
    
            User selectFromMasterAndSlaveWithoutDataSourceRouting = userService.selectFromMasterAndSlaveWithoutDataSourceRoutingInDao("1", "maple_slave");
    
            return selectFromMasterAndSlaveWithDataSourceRouting;
        }
    }
    

     UserServiceImpl

    package com.gane.maple.service.impl;
    
    import com.gane.maple.dao.UserDAO;
    import com.gane.maple.entity.User;
    import com.gane.maple.jdbc.routing.ClientDataSource;
    import com.gane.maple.jdbc.routing.annotation.DataSourceRouting;
    import com.gane.maple.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @Description UserServiceImpl
     * @Date 2020/4/24 7:41
     * @Created by 王弘博
     */
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDAO userDAO;
    
        /**
         * 把 @DataSourceRouting 放进 dao 层
         *
         * @param userId
         * @return
         */
        @Override
        public User selectByUserId(String userId) {
            User userFromMaster = userDAO.selectByUserId(userId);
            System.out.println("查询master库:" + userFromMaster);
            return userFromMaster;
        }
    
        /**
         * 把 @DataSourceRouting 放进 dao 层
         *
         * @param userName
         * @return
         */
        @Override
        public User selectByUserName(String userName) {
            User userFromSlave = userDAO.selectByUserName(userName);
            System.out.println("查询slave库:" + userFromSlave);
            return userFromSlave;
        }
    
        /**
         * 把 @DataSourceRouting 放进 dao 层
         * 观察进入aspect几次
         *
         * @param userId
         * @param userName
         * @return
         */
        @Override
        public User selectFromMasterAndSlave(String userId, String userName) {
    
            User userFromMaster = userDAO.selectByUserId(userId);
            System.out.println("查询master库:" + userFromMaster);
    
            User userFromSlave = userDAO.selectByUserName(userName);
            System.out.println("查询slave库:" + userFromSlave);
    
            return userFromMaster;
        }
    
        /**
         * 把 @DataSourceRouting 放进 service 层 和 dao 层。判断具体以哪个datasource为准
         *
         * @param userId
         * @param userName
         * @return
         */
        @DataSourceRouting(value = ClientDataSource.SLAVE)
        @Override
        public User selectFromMasterAndSlaveWithDataSourceRoutingInDao(String userId, String userName) {
    
            User userFromMaster = userDAO.selectByUserId(userId);
            System.out.println("查询master库:" + userFromMaster);
    
            User userFromSlave = userDAO.selectByUserName(userName);
            System.out.println("查询slave库:" + userFromSlave);
    
            return userFromMaster;
        }
    
        @DataSourceRouting(value = ClientDataSource.SLAVE)
        @Override
        public User selectFromMasterAndSlaveWithoutDataSourceRoutingInDao(String userId, String userName) {
    
            User userFromMaster = userDAO.selectByUserIdWithoutDataSourceRouting(userId);
            System.out.println("查询master库:" + userFromMaster);
    
            User userFromSlave = userDAO.selectByUserNameWithoutDataSourceRouting(userName);
            System.out.println("查询slave库:" + userFromSlave);
    
            return userFromMaster;
        }
    
    
    }
    

    UserDAO

    package com.gane.maple.dao;
    
    import com.gane.maple.entity.User;
    import com.gane.maple.jdbc.routing.ClientDataSource;
    import com.gane.maple.jdbc.routing.annotation.DataSourceRouting;
    
    /**
     * @Description UserDAO
     * @Date 2020/4/24 7:39
     * @Created by 王弘博
     */
    public interface UserDAO {
    
        @DataSourceRouting(value = ClientDataSource.MASTER)
        User selectByUserId(String userId);
    
        @DataSourceRouting(value = ClientDataSource.SLAVE)
        User selectByUserName(String userName);
    
        User selectByUserIdWithoutDataSourceRouting(String userId);
    
        User selectByUserNameWithoutDataSourceRouting(String userName);
    }
    

    10、总结

    1. 我们可以把注解 DataSourceRouting 作用在 service 接口 上,也可以作用在 dao 接口 上
    2. 如果只作用在 service 接口上的话,比如配置的是 slave,那么该 service 里的所有调用 dao 的地方,都会走 slave 数据源;
    3. 如果只作用在 dao 接口上的话,比如配置的是 slave,那么该 service 里的所有调用 dao 的地方,都会走 slave 数据源;
    4. 如果 service 上配置的是 master,aDao配置的是 slave,bDao配置的是 master,当走到 service 的时候,会被 DataSourceRoutingAspect 拦截到,并赋值master给 ClientDataSourceContextHolder ,当执行到 aDao 的时候,又会被 DataSourceRoutingAspect 拦截到,拿到配置在aDao上的注解slave,重写determineCurrentLookupKey 方法里会返回 slave出去,最终走的是 slave 数据源;当执行到 bDao 的时候,又会被 DataSourceRoutingAspect 拦截到,拿到配置在bDao上的注解master,则最终执行 bDao 走的是 master 数据源;所以在dao接口上配置注解的优先级要高于在service上配置,遵循就近原则;这里需要开发人员根据自己的业务来做相应的处理。

    11、源码

    gitee地址:https://gitee.com/gane_maple/read-write-separation-mybatis

    下篇文章我们使用 MybatisPlus 来做读写分离

    【读写分离】SpringBoot整合多数据源实现读写分离(二)

    展开全文
    qq_33101675 2021-03-05 14:48:20
  • weixin_32546235 2021-01-21 16:35:37
  • qq_45066628 2021-11-04 21:19:49
  • weixin_34458671 2021-02-26 14:35:31
  • u012843873 2021-01-21 15:43:59
  • u012796085 2021-05-28 22:19:40
  • qq_33101675 2021-03-06 16:08:57
  • weixin_39926943 2021-01-30 07:11:43
  • fisea 2020-12-30 14:25:54
  • qq_40378034 2021-03-27 14:41:39
  • tianjingle_blog 2021-08-18 01:07:03
  • u013309797 2021-07-30 19:30:56
  • qq_41936090 2021-01-15 13:20:13
  • weixin_39740346 2020-12-22 08:23:23
  • weixin_42280959 2021-01-13 10:59:11
  • jianchileiliang 2021-02-08 11:13:33
  • jlq_diligence 2021-01-31 20:35:53
  • weixin_35363322 2021-01-27 07:40:43
  • qq_48598380 2021-01-24 21:38:07
  • weixin_35032861 2020-12-23 03:04:55
  • haifengbin1989 2021-03-10 13:02:43
  • weixin_29005499 2021-03-06 21:55:59
  • wlc19981111 2021-05-07 22:16:13
  • Y0Q2T57s 2021-08-11 00:14:20
  • weixin_36401868 2021-03-08 18:53:31
  • solihawk 2021-06-08 12:38:19

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 168,459
精华内容 67,383
关键字:

怎么实现读写分离