Mybatis拦截器实现读写分离的思路和实践

2022-08-11 17:33:14 浏览数 (1)

上次文章我们采用原生的方式做了mybatis的多数据源,多数据源势必要决定采用那种数据源了,当然这里说的是数据一样的数据源了,也就是主从或者主备之类的,为啥要做读写分离就是因为业务读写压力比较大,放到同一台机器上会影响效率,所以我们可以让读和写分开,这样就降低了计算机的压力,相当于分流了。虽然读写分离优点多多,但是也不能无脑读写分离,对于写入立马回查的业务读写分离的模式大概率就要凉凉。所以这块在代码层面上要灵活的决定数据源采用写库还是读库就成为一个比较重要的问题。这块我们再思考一个问题,现在市面上有很多数据库中间件,比如mycat之类的貌似也解决不了主从之间的时间延迟问题,所以这块最为灵活的方式是在代码中决定数据源。当然在代码中决定采用数据源之后,再用mycat等中间件就有点搞笑了。所以个人总结来说代码中的灵活决定数据源比采用数据库中间件的优势更加明显。

说到这里,我们可以再思考一个问题,一般数据库都是主从,也就是很多都是一主多从,对应的就是一写多读,意思是写的时候写到主库,读的时候可以从从库的任意中读取。因此我们的插件要有写库的数据源已经多个读库的数据源。

上边说了那么多,如果没有读写识别的信号,那说的再多也没有价值。这块我们就需要解析SQL或者解析方法上边的特定注解了。前者为一般模式,后者是灵活配置。当然这块要注意的就是事务了,事务肯定要操作单库,也必然是主库,道理说了挺多哈。我们试着研究一下怎么做吧。

1.首先就是Mybatis插件了,记得之前我们说mybatis有4个阶段(Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler),每个阶段的各个方法都可以被拦截,当然这块拦截器的拦截原理责任链模式,过程还是比较难的。然后通过jdk代理的方式植入到mybatis执行过程中。这块的笔记已经忘的差不多了。再此贴个笔记。

Mybatis学习笔记(三)- Mybatis插件原理

Mybatis学习笔记(二)- Sql的执行过程

2.因为这块我们肯定要在sql执行执行前决定要选的数据库,所以看一下mybatis的执行流程(图片来自网络)。

我们要决定采用那种数据源要在最开始的地方做,因为后边的参数呀或者结果都不是我们关心的地方。statementHandler是执性的最早的,而且统领全局。而且最开始的方法是prepare,因此可以在这里就行一些操作。

基于上述思路,我们大概的写一个mybatis拦截器就是这样的。

代码语言:javascript复制
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})})
public class MybatisLanjieqi implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        System.out.println(statementHandler.toString());
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        System.out.println("enter the plugin");
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
}

插件写好之后,我们需要将我们的插件注入到SqlSessionFactoryBean中。我们继续在上篇文章中的demo中做演示。

考虑到我们要在代码中灵活决定采用那种类型的数据源,因此我们需要需要将现场的一些东西传过来,比如调用类的信息。当然还有sql的类型什么的。这块我们debug一下。看看我们的StatementHandler中有什么值。

我们发现这块的mappedstatement中的id就是我们要找的一个标志,这个id就是我们mybatis映射的接口。所以我们可以针对这个id做反射处理,拿到class类方法信息,当然这块我们要用的就是解析注解了。我们可以通过注解灵活决定采用写库还是读库。在确定了读写库之后,我们如何将这个标志信息传递到getConnectio层面?考虑线程安全,我们可以采用threadLocal去做。意思就是说这块我们可以解析mappedstatement的id,然后反射确定类信息,然后解析注解,通过threadlocal缓存通过注解方式确定的读写库信息。这块就不演示了,必然是可行的。

除此之外,对于一般模式的sql,我们记得sql都有类型的标志,这块也是mybatis做的。就是判断sql的类型,比如select,update这种。如果没有注解标志我们就要通过sql的类型来判断采用何种库类型。

我们继续debug,发现这个mappedstatement包含的内容还真是多,看到没,有个sqlCommendType,所以这块我们可以先解析注解,如果没有注解我们可以通过这个sqlCommedType来确定读写库,没有疑问我们的标志势必还是要存到threadLocal中的。

以上过程,我们大概的写了一下我们要在代码中灵活的做到读写分离一些比较细节的思路。但是我们还没有说我们如何获取读库和写库的问题。首先必然要从threadlocal中获取具体的读写标志位了。然后我们的多数据源初始化的时候也必然要按照读和写的方式进行保存。显然在原生的框架代码层面我们是改不了了,所以这块我们必然要重写获取采用代理模式,这块为啥能在最后获取原因是数据库连接是懒加载的。也就是getConnection的时候才走得,因此我们只需要在getConnnet之前加一层代理即可。这块我们采用继承做demo。

代码语言:javascript复制
public class MyDataSource extends DruidDataSource {


    private static DruidDataSource write;


    private static Listreader;

//模拟多库初始化....
    public static DruidDataSource getDruidDataSource(String url,String userName,String password) throws SQLException {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        try {
            ds.setFilters("stat,mergeStat,slf4j");
        } catch (Exception var18) {
        }
        ds.setMaxActive(50);
        ds.setInitialSize(1);
        ds.setMinIdle(1);
        ds.setMaxWait(60000);
        ds.setTimeBetweenEvictionRunsMillis(120000);
        ds.setMinEvictableIdleTimeMillis(300000);
        ds.setValidationQuery("SELECT 'x'");
        ds.setPoolPreparedStatements(true);
        ds.setMaxPoolPreparedStatementPerConnectionSize(30);
        ds.setTestWhileIdle(true);
        ds.setTestOnReturn(false);
        ds.setTestOnBorrow(false);
        ds.init();
        return ds;
    }

    static {
        //初始化write...
        try {
            write=getDruidDataSource("jdbc:mysql://127.0.0.1:3306/tianjl?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true","root","tianjingle");
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        //初始化读库
//        reader.add(write);
//        reader.add(write);
//        reader.add(write);
    }

    public DruidPooledConnection getConnection() throws SQLException {
        //这块可以写具体得库选择逻辑,读库随机可以从用random方法。
        return write.getConnection();
    }
}

相应的数据源改成我们新的数据源类,如下MyDataSource所示:

代码语言:javascript复制
@Bean(name = "dataO")
    public SqlSessionFactoryBean getSqlSessionFactoryOne1() throws Exception {
        //xml和实体的映射
        SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(new MyDataSource());
        sqlSessionFactoryBean.setTypeAliasesPackage("com.example.demo.one");
        Resource[] resources = new Resource[]{new ClassPathResource("tian/one/OneMapper.xml")};
        sqlSessionFactoryBean.setMapperLocations(resources);
        sqlSessionFactoryBean.setPlugins(new MybatisLanjieqi());
        return sqlSessionFactoryBean;
    }

    @Bean(name = "dataTwo")
    public MapperFactoryBean getSqlSessionFactoryTwo() throws Exception {
        //xml和实体的映射
        SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(new MyDataSource());
        sqlSessionFactoryBean.setTypeAliasesPackage("com.example.demo.two");
        sqlSessionFactoryBean.setMapperLocations(new ClassPathResource("tian/two/TwoMapper.xml"));
        //单个数据源所有的数据库映射
        MapperFactoryBean mapperFactoryBean=new MapperFactoryBean();
        //设置sqlSessionTemplate,zhuru yong de
        mapperFactoryBean.setMapperInterface(TwoMapper.class);
        mapperFactoryBean.setSqlSessionFactory(sqlSessionFactoryBean.getObject());
        return mapperFactoryBean;
    }

通过以上分析,我们大概对代码层面读写分离有了一些认识,估计让写个读写分离就容易多了,当然注解之前讲过了,所以也没啥难度。基本市面上的读写分离也都是这么搞的吧。今天就写到这吧。

晚安~

0 人点赞