zoukankan      html  css  js  c++  java
  • [Mybatis]用AOP和mybatis来实现一下mysql读写分离

    小记

    看了看博客园发现有一阵子没写东西了,今天写点最近折腾的东西吧,由于工作的原因,平时就Springboot的技术栈用得不多,甚至现在对springboot的使用还不如我以前在学校的时候懂得多,没办法,工作里的东西是首要的,我也在努力摆脱这种困境,平时积累点知识,防止和外面脱节。这是作为一个打工人和程序员应该有的意识。

    搭一个简单的mysql主从集群(1主2从)

    原本想用virtualbox来自己开三台虚拟机来弄的,但是virtualbox我也使得不怎么溜,固定ip的问题解决了但是各个虚拟机之间的通信依旧有问题,与其在这里浪费时间我还不如在一台机器上模拟,于是我在我上学的时候用的阿里云服务器上用docker搭建了一个简单的mysql集群,集群搭建就不细说了,三个节点对应三个docker容器,对应服务器的三个不同的端口,用来模拟集群。

    集群搭建完,数据源的配置文件就可以写出来了。

    spring:
      datasource:
        master:
          pool-name: master
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://aliyunserver:33307/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456
          maximum-pool-size: 10
          minimum-idle: 5
        slave1:
          pool-name: slave1
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://aliyunserver:33308/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456
          maximum-pool-size: 10
          minimum-idle: 5
        slave2:
          pool-name: slave2
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://aliyunserver:33309/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456
          maximum-pool-size: 10
          minimum-idle: 5
      application:
        name: mysql-test
    server:
      port: 8080
    mybatis:
      config-location: mybatis.xml
      mapper-locations: mapper/*.xml
    

    配置数据源

    有了数据源,那么就来配置数据源,这里需要配置三个数据源,分别是主节点,从节点1,从节点2,这里暂且不讨论有一个节点挂了的情况。这三个节点由一个方法来进行管理,也就是下面的dynamicDataSource();

    @Configuration
    public class DataSourceConfig {
    
        /**
         * 主库
         * */
        @Bean("master")
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource master(){
            return DruidDataSourceBuilder.create().build();
        }
        /**
         * 从库1
         * */
        @Bean("slave1")
        @ConfigurationProperties(prefix = "spring.datasource.slave1")
        public DataSource slave1(){
            return DruidDataSourceBuilder.create().build();
        }
    
        @Bean("slave2")
        @ConfigurationProperties(prefix = "spring.datasource.slave2")
        public DataSource slave2(){
            return DruidDataSourceBuilder.create().build();
        }
    
        /**
         * 实例化数据源路由
         * */
        @Bean(name = "dynamicDatasource")
        public DataSourceRouter dynamicDataBase(@Qualifier("master")DataSource master,
                                                @Qualifier("slave1")DataSource slave1,
                                                @Qualifier("slave2")DataSource slave2){
            DataSourceRouter dynamicDataBase = new DataSourceRouter();
            Map<Object,Object> targetDataSources = new HashMap<Object, Object>(3);
            targetDataSources.put(DBType.MASTER,master());
            targetDataSources.put(DBType.SLAVE1,slave1());
            targetDataSources.put(DBType.SLAVE2,slave2());
            dynamicDataBase.setTargetDataSources(targetDataSources);
            //设置默认
            dynamicDataBase.setDefaultTargetDataSource(master());
            return dynamicDataBase;
        }
    }
    
    
    

    然后还有和mybatis相关的配置,一并加上。

    @Configuration
    @EnableTransactionManagement
    public class MybatisConfig {
    
        @Resource(name = "dynamicDatasource")
        private DataSource dynamicDatasource;
    
    
        @Bean
        public SqlSessionFactory sqlSessionFactory() throws Exception{
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dynamicDatasource);
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
            return bean.getObject();
        }
    
        @Bean
        public PlatformTransactionManager platformTransactionManager(){
            return new DataSourceTransactionManager(dynamicDatasource);
        }
    }
    

    DBType是一个枚举类,用来区分主从。

    public enum DBType {
        /**
         * 主库
         * */
        MASTER,
        /**
         * 从库1
         * */
        SLAVE1,
        /**
         * 从库2
         * */
        SLAVE2
    }
    

    DataSourceRouter用来路由数据源的,在写它之前还需要写一个动态获取数据源的类,这个类里面一般可以设置一定的策略,比如在读的时候设置轮询来平均对每个从库的读取压力。例如我这里两个从库那我设置对2取模来轮询切换数据源,但是我这里有弊端,那就是我这里不能动态地调整,比如增加一个数据源我就要改动代码,当然现在业界有解决办法,这里暂时不谈。

    public class DataSourceContextHolder {
    
        /**
         * 两种操作数据库的方式
         * */
        public static final String MASTER = "write";
    
        public static final String SLAVE = "read";
    
        private static final ThreadLocal<DBType> context = new ThreadLocal<>();
    
        /**
         * 计数器,用来对2取模决定用哪个从库
         * */
        private static final AtomicInteger counter = new AtomicInteger(-1);
        
        public static void set(DBType dbType){
            if(dbType==null||StringUtils.isEmpty(dbType)){
                throw new NullPointerException("DataSourceType 为空");
            }
            context.set(dbType);
        }
    
        public static DBType get(){
            return context.get();
        }
    
        /**
         * 切换到主数据源
         * */
        public static void setMaster(){
            set(DBType.MASTER);
        }
    
        /**
         * 切换到从节点数据源
         * */
        public static void setSlave(){
            int index = counter.getAndIncrement() % 2;
            if (index == 0){
                set(DBType.SLAVE1);
            }else {
                set(DBType.SLAVE2);
            }
        }
        /**
         * 移除
         * */
        public static void clear(){
            context.remove();
        }
    }
    
    

    然后再到数据源路由

    public class DataSourceRouter extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.get();
        }
    }
    

    用AOP来实现动态切换数据源

    数据源的部分到上面为止就搞定了,那么现在的问题是,我们需要怎么来切换数据源,总不可能在每个读的方法里都挨个调切换的方法,那么这时候AOP就排上用场了,我们知道切换数据源是根据这个操作是读还是写来的,那么自然对应到业务里就是是否涉及到操作数据了,而业务自然就是在Service层来开刀了。

    • 定义读操作和写操作的注解
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Master {
    }
    
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Slave {
    }
    

    有了这两个注解,就能在Service层对应的方法上来标记这个方法是读还是写了。然后就是切面了。

    @Slf4j
    @Aspect
    @Component
    public class DataSourceAspect {
    
        @Pointcut("@annotation(com.example.mysql.annotation.Slave) && execution(* com.example.mysql.service.impl..*.*(..))")
        public void readPointcut(){}
    
        @Pointcut("@annotation(com.example.mysql.annotation.Master) && execution(* com.example.mysql.service.impl..*.*(..))")
        public void writePointcut(){}
    
        @Before("readPointcut()")
        public void readBefore(JoinPoint joinPoint){
            DataSourceContextHolder.setSlave();
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            log.info("{}-{} use slave datasource",className,methodName);
            DataSourceContextHolder.clear();
    
        }
    
        @After("readPointcut()")
        public void readAfter(JoinPoint joinPoint){
            DataSourceContextHolder.setMaster();
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            log.info("after read,{}-{} switch to master datasource",className,methodName);
            DataSourceContextHolder.clear();
        }
    
        @Before("writePointcut()")
        public void writeBefore(JoinPoint joinPoint){
            DataSourceContextHolder.setMaster();
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            log.info("{}-{} use master datasource",className,methodName);
            DataSourceContextHolder.clear();
        }
    }
    

    测试

    我的测试比较简单,一个用户表里有id和username两个字段,一个读操作和写操作。

    public interface UserDao {
    
        /**
         * 获取用户名
         * */
        String getUserName(int id);
    
        /**
         * 添加用户
         * */
        void addUser(User user);
    }
    
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.mysql.dao.UserDao">
        <select id="getUserName" parameterType="java.lang.Integer" resultType="java.lang.String">
        select username from user where id = #{id}
      </select>
    
        <insert id="addUser" parameterType="com.example.mysql.model.User">
            insert into user(id,username) values(#{id},#{username})
        </insert>
    </mapper>
    

    然后在Service层里来调用,顺便设置对应的注解。

    public interface UserService {
    
        String getUserName(int userId);
    
        boolean addUser(User user);
    }
    
    
    @Service
    @Slf4j
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
        @Slave
        @Override
        public String getUserName(int userId) {
            return userDao.getUserName(userId);
        }
    
        @Master
        @Override
        public boolean addUser(User user) {
            try {
                userDao.addUser(user);
                return true;
            }catch (Exception e){
                log.error(e.getMessage());
                return false;
            }
        }
    }
    

    Controller层我就不写了,我预先在表里加了三条数据,分别是:

    id	username
    1	thyin
    2	xxx
    3	yyy
    

    然后调用一下getUserName的接口,http://localhost:8080/user/api/getUserName/1,通过日志可以看到使用的从库的数据源:

    avatar

    然后测试一下写的接口,http://localhost:8080/user/api/addUser?id=6&username=iqy,再看日志。

    avatar

    再检查主库是否有记录,并检查从库是否同步。

    主库:

    avatar

    从库1:

    avatar

    从库2:

    avatar

    ok。

    总结

    这里我只是简单暴力地做了一个读写分离的,但实际工作中这个肯定不够,后面我会整理一下mycat的使用,以及分表分库的知识,再接再厉。

  • 相关阅读:
    Python3标准库:fnmatch UNIX式glob模式匹配
    Python3标准库:glob文件名模式匹配
    Python3标准库:pathlib文件系统路径作为对象
    Python3标准库:os.path平台独立的文件名管理
    Python3标准库:statistics统计计算
    36-Docker 的两类存储资源
    第四章-操作列表
    35-外部世界如何访问容器?
    34-容器如何访问外部世界?
    33-容器间通信的三种方式
  • 原文地址:https://www.cnblogs.com/Yintianhao/p/14802488.html
Copyright © 2011-2022 走看看