专注Java教育14年 全国咨询/投诉热线:400-8080-105
动力节点LOGO图
始于2009,口口相传的Java黄埔军校
首页 hot资讯 MyBatis读写分离详解

MyBatis读写分离详解

更新时间:2022-04-06 09:58:38 来源:动力节点 浏览2219次

MyBatis读写分离是什么?对于初学者来说可能还不是很了解,下面动力节点小编来告诉大家。

ShardingSphere

ShardingSphere由JDBC、Proxy和Sidecar组成(规划中),可以独立部署,支持混合部署。ShardingSphere Proxy 与 MyCat 定位相同,而 ShardingSphere JDBC 在 Java 的 JDBC 层提供了额外的服务。

SpringBoot 集成 shardingsphere JDBC 也非常方便。引入包和编写配置文件后即可使用。但是事务中有个小问题,就是事务中的写操作之后,后面的读操作都是从主库中读取的;也就是说,在写操作之前,事务中的读仍然是从库中读取的,可能会造成脏写。

使用 Mybatis 拦截器

大部分代码层面的读写分离都是通过判断sql的读写类型来拦截sql并重定向数据库。Shardingsphere JDBC 也不例外。

Mybatis 允许我们自定义 Interceptor。我们需要实现Interceptor接口,在自定义的Interceptor类上添加@Intercepts注解。在@Intercepts注解中,我们可以指定拦截方式。

多个数据源

由于读写分离是在代码层面进行的,所以必须有读写库。这里使用了多数据源功能。不用mybatis/mybatis plus默认的多数据源生成方式,多数据源自己配置。其实也可以使用默认的生成方式。自己写的目的是为了更好的理解里面的原理。【配置文件中配置的格式是根据mybatis配置的格式加上多个数据源来配置的】

代码

多数据源配置

/**
 * 主数据库
 */
@ConfigurationProperties("spring.datasource.dynamic.datasource.master")
公共数据源 masterDataSource(){
    log.info("加载主数据源主数据源。");
    返回 DruidDataSourceBuilder.create().build();
}
/**
 * 数据库从库
 */
@ConfigurationProperties("spring.datasource.dynamic.datasource.slave1")
公共数据源 slave1DataSource(){
    log.info("从数据源 slave1 DataSource 加载。");
    返回 DruidDataSourceBuilder.create().build();
}
/**
 * 动态数据源
 */
@豆角,扁豆
公共数据源 myRoutingDataSource(@Qualifier("masterDataSource") 数据源 masterDataSource,
                                      @Qualifier("slave1DataSource") 数据源 slave1DataSource) {
    log.info("load[masterDataSource-slave1DataSource]设置为动态数据源DynamicDataSource。");
    Map<Object, Object> targetDataSources = new HashMap<>(2);
    targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
    targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
    动态数据源 dynamicDataSource = new DynamicDataSource();
    dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
    动态数据源.setTargetDataSources(targetDataSources);
    返回动态数据源;
}

DBTypeEnum

public enum DBTypeEnum {
    /**Main library*/
    MASTER,
    /**From library 1*/
    SLAVE1
}

动态数据源

此处指定数据源的键。在每条sql语句执行之前,都会执行determineCurrentLookupKey获取数据源。DbContextHolder.get()是获取当前线程中指定数据源的key,会在自定义拦截器中指定。

公共类 DynamicDataSource 扩展 AbstractRoutingDataSource { 
    @Nullable @Override
    受保护对象 determineCurrentLookupKey() { 
        return DbContextHolder.get(); 
    } 
}
公共类 DbContextHolder {
    私有静态最终 ThreadLocal<DBTypeEnum> CONTEXT_HOLDER = new ThreadLocal<>(); 
    私有静态最终 AtomicInteger COUNTER = new AtomicInteger(-1); 
    public static void set(DBTypeEnum dbType) { 
        log.debug("切换到{}", dbType.name()); 
        CONTEXT_HOLDER.set(dbType); 
    }
    公共静态 DBTypeEnum get() { 
        return CONTEXT_HOLDER.get(); 
    }
    公共静态 DBTypeEnum getMaster() { 
        return DBTypeEnum.MASTER; 
    } 
    public static DBTypeEnum getSlave() { 
        // 可以轮询多个从库
        int index = COUNTER.getAndIncrement() % 2;
        if (COUNTER.get() > 9999) { 
            COUNTER.set(-1); 
        }
        返回 DBTypeEnum.SLAVE1; 
    } 
}

拦截器

在上一步中,我们定义了多个数据源并设置了数据源选择的基础(DbContextHolder.get())。这一步就是按照一定的规则在拦截器中设置这个基础。

代码

拦截器

@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 }), 
        @Signature(type = Executor.class, method = "close", args = {boolean.class}) 
}) 
public class DbSelectorInterceptor implements Interceptor { 
    private静态最终字符串 REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; 
    private static final Map<String, DBTypeEnum> CACHE_MAP = new ConcurrentHashMap<>(); 
    @覆盖
    公共对象拦截(调用调用)抛出 Throwable { 
        String methodName = invocation.getMethod().getName(); 
        字符串 closeMethodName = "关闭"; 
        布尔同步活动 = TransactionSynchronizationManager.isSynchronizationActive(); 
        DBTypeEnum 数据库类型 = null; 
        if(!synchronizationActive && !closeMethodName.equals(methodName)) { 
            Object[] objects = invocation.getArgs(); 
            MappedStatement ms = (MappedStatement) 对象[0]; 
            if((databaseType = CACHE_MAP.get(ms.getId())) == null) { 
                //读取方法
                if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                    //! selectKey是自增ID查询主键(SELECT LAST_INSERT_ID())方法,使用主库
                    if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { 
                        databaseType = DbContextHolder.getMaster(); 
                    } else { 
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]); 
                        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", ""); 
                        if(sql.matches(REGEX)) { 
                            databaseType = DbContextHolder.getMaster(); 
                        } 别的 {
                            数据库类型 = DbContextHolder.getSlave(); 
                        } 
                    } 
                }else{
                    数据库类型 = DbContextHolder.getMaster(); 
                } 
                log.debug("设置方法[{}]使用[{}]策略,SqlCommandType [{}]..", ms.getId(), databaseType.name(), ms.getSqlCommandType().name()) ; 
                CACHE_MAP.put(ms.getId(), databaseType); 
            } 
        } else { 
            if (synchronizationActive) { 
                log.debug("事务使用 [{}] 策略", DBTypeEnum.MASTER.name()); 
            } 别的 {
                log.debug("关闭方法重置为 [{}] 策略", DBTypeEnum.MASTER.name()); 
            }
            数据库类型 = DbContextHolder.getMaster(); 
        } 
        DbContextHolder.set(databaseType); 
        返回调用.proceed(); 
    } 
    @Override 
    public Object plugin(Object target) { 
        if (target instanceof Executor) { 
            return Plugin.wrap(target, this); 
        } else {
            返回目标;
        } 
    } 
}

这段代码比较长,但核心逻辑只有三个:

如果事务启动,则使用主数据库;

如果当前连接已关闭,则重置到主库;【ps:忘记不加会怎样】

其他情况根据sql语句中的关键字select、update、delete判断;

配置拦截器

这里,拦截器是基于mybatis plus配置的。

    @Bean 
    public MybatisSqlSessionFactoryBean sqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource, 
                                               @Qualifier("slave1DataSource") DataSource slave1DataSource) throws Exception { 
        log.info("自定义配置mybatis-plus of SqlSessionFactory."); 
        MybatisSqlSessionFactoryBean mybatisPlus = new MybatisSqlSessionFactoryBean(); 
        mybatisPlus.setDataSource(myRoutingDataSource(masterDataSource, slave1DataSource)); 
        MybatisConfiguration 配置 = new MybatisConfiguration(); 
        configuration.setJdbcTypeForNull(JdbcType.NULL); 
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false); 
        ///自定义配置
        mybatisPlus.setConfiguration(configuration); 
         设置 mapper.xml 文件路径
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); 
        org.springframework.core.io.Resource[] resource = resolver.getResources("classpath:mapper/webservice/*.xml"); 
        mybatisPlus.setMapperLocations(resource); 
        //给SqlSessionFactory添加一个插件生效
        mybatisPlus.setPlugins(paginationInterceptor(), new DbSelectorInterceptor()); 
        globalConfig.setMetaObjectHandler(this); 
        mybatisPlus.setGlobalConfig(globalConfig); 
        返回 mybatisPlus;
    }

实际上,它指的是com baomidou。mybatisplus。自动配置。mybatisplusautoconfiguration #sqlsessionfactory,将DbSelectorInterceptor织入。如果大家想了解更多相关知识,可以关注一下动力节点的Mybatis实战教程,里面的课程内容细致全面,通俗易懂,适合小白学习,希望对大家能够有所帮助哦。

提交申请后,顾问老师会电话与您沟通安排学习

免费课程推荐 >>
技术文档推荐 >>