微服务下无侵入式动态路由数据库

标签: dev | 发表时间:2018-06-27 00:00 | 作者:
出处:http://itindex.net/relian

by zhouzhipengfrom https://blog.zhouzhipeng.com/dynamic-datasource-in-rpc.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
本文可全文转载,但需要保留原作者和出处。

背景

项目结构

项目主要采用 springboot + dubbo + mybatis框架,大体分为 webservice两层。 web提供api接口给 sdk客户端使用, service则提供mysql数据库表等操作,为 web提供 dubbo服务支持。

业务场景

简单看下这张图:

《微服务下无侵入式动态路由数据库》

如图所示,大概流程为 :sdk端传入appkey (某app的唯一标识), web 端通过dubbo远程调用service,service 需要根据不同的appkey路由到不同的数据库。 每个appkey所代表的数据库的表结构都是一样的。

现在问题来了,appkey是非业务字段标识(表中不落地,仅在库名上体现),如何在不改变原dao方法签名(无appkey入参)、service方法签名(无appkey入参)、mapper xml文件(无appkey标识)等的前提下,根据上报的不同appkey,在对应的数据库上执行sql操作?

解决方案

总结一下上面问题的要求: 如何无侵入的按key进行数据库路由?

这里的“无侵入”涉及到两个难点:

  1. web调service的时候如何可以不传入appkey 却将appkey带到service端?
  2. 假设第一点ok,service端又如何能不显示改变数据源而在指定库上执行sql呢?

先考虑上述难点中的第二点:

思路一

关于mybatis的多数据源例子很多,但是并不太适合上述这种场景。 它需要先将多个数据源配置“写死”,在用到的具体dao方法上需要显式指定所使用的数据源。 它可能更适用于读写分离等场景,而对于本案例中的“动态” 数据源却显得力不从心!

思路二

数据源没办法很好的动态选择,是否可以动态创建数据源呢?答案是肯定的。但是这样一来会让架构更加复杂,不同的appkey下的sql需要使用不同的数据源来执行,一般情况下每个数据源都要维护一个连接池,如此一来,当appkey很多的情况下会很耗资源。所以动态创建数据源的方式不太可行!

可行方案

其实sql本身已经给了我们答案:

   mysql> create database test;
Query OK, 1 row affected (0.01 sec)

mysql> create database test_1;
Query OK, 1 row affected (0.00 sec)

mysql> create table test.person(id integer,name varchar(100));
Query OK, 0 rows affected (0.27 sec)

mysql> create table test_1.person(id integer,name varchar(100));
Query OK, 0 rows affected (0.27 sec)
   mysql> insert into test.person values(1,'zhou');
Query OK, 1 row affected (0.21 sec)

mysql> insert into test_1.person values(1,'zhou');
Query OK, 1 row affected (0.20 sec)

mysql> select * from test.person;
+------+------+
| id   | name |
+------+------+
|    1 | zhou |
+------+------+
1 row in set (0.00 sec)

mysql> select * from test_1.person;
+------+------+
| id   | name |
+------+------+
|    1 | zhou |
+------+------+
1 row in set (0.00 sec)

从以上过程可以看出,整个过程并没有使用 use test等任何切换数据库的命令,这意味着所有的sql操作都是在一个数据源中进行的(我登陆的是root用户,默认mysql库)。

由此一来,事情变得明朗化了,剩下的目标是: 改写sql,表名加上库名前缀

恰好mybatis也是支持这种操作的,通过mybatis的拦截器, 示例代码(已验证过)如下:

   @Component
@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})})
public class MybatisInterceptor implements Interceptor {
    private static Log logger = LogFactory.getLog(MybatisInterceptor.class);


    static int MAPPED_STATEMENT_INDEX = 0;// 這是對應上面的args的序號
    static int PARAMETER_INDEX = 1;
    static int ROWBOUNDS_INDEX = 2;
    static int RESULT_HANDLER_INDEX = 3;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        //如果有appkey 参数在dubbo上下文,则改写sql,切换到指定的库。
        String appkey = RpcContext.getContext().getAttachment(Constants.APPKEY);
        if (StringUtils.isNotEmpty(appkey)) {

            final Object[] queryArgs = invocation.getArgs();
            final MappedStatement mappedStatement = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
            final Object parameter = queryArgs[PARAMETER_INDEX];
            final BoundSql boundSql = mappedStatement.getBoundSql(parameter);

            String sql = boundSql.getSql();

            String tableName = getTableName(sql);
            //将tablename加上指定数据库前缀
//        String dbname= DBSwitcher.getDBName();

            logger.info("old sql is :" + sql);


            String dbname = Constants.DB_PREFIX + appkey;
            if (StringUtils.isNotEmpty(tableName)) {
                sql = sql.replaceFirst(tableName, dbname + "." + tableName);

                logger.info("new sql is :" + sql);

                // 重新new一個查詢語句對像
                BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
                // 把新的查詢放到statement裏
                MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
                for (ParameterMapping mapping : boundSql.getParameterMappings()) {
                    String prop = mapping.getProperty();
                    if (boundSql.hasAdditionalParameter(prop)) {
                        newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
                    }
                }
                queryArgs[MAPPED_STATEMENT_INDEX] = newMs;
            }

        }


        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    public static class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }


    // 根据sql获取表名
    private String getTableName(String sql) {
        String[] sqls = sql.split("\\s+");
        switch (sqls[0]) {
            case "select": {
                // select aa,bb,cc from tableName
                for (int i = 0; i < sqls.length; i++) {
                    if (sqls[i].equals("from")) {
                        return sqls[i + 1];
                    }
                }
                break;
            }
            case "update": {
                // update tableName
                return sqls[1];
            }
            case "insert": {
                // insert into tableName
                return sqls[2];
            }
            case "delete": {
                // delete tableName
                return sqls[1];
            }
        }
        return null;
    }
}

细心的你不难发现这一行:

《微服务下无侵入式动态路由数据库》

至此其实上述两大难点中的第一点也几乎要解决了,没错,这里的 RpcContext对象就是 dubbo为我们提供的一个工具,让你可以方便的在调用dubbo服务时传递上下文参数,其背后原理是基于 ThreadLocal进行包装。

service层的问题大体上已解决,现在web层的问题也相对简单了,写一个filter全局拦截appkey参数,放入dubbo上下文,其他所有controller 只要会调用dubbo服务就会将appkey参数带上。如下:

   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("过滤器正在执行...");
        // pass the request along the filter chain
        String appkey = ((HttpServletRequest) request).getHeader("key");

        logger.info("the requested appkey is :" + appkey);

        RpcContext.getContext().setAttachment(APPKEY, appkey);

        chain.doFilter(request, response);
    }

还差一点

以上的过程看似很完美了,但是由于dubbo的设计原因,事情并没有想象中那么顺利,为了方便说明,看下以下代码:

   /**
 * test接口
 */
@Controller
@RequestMapping(path = "/test")
public class TestController {

    private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);

    @Reference
    private TestService testService;

    @PostMapping(path = {"/test"})
    @ResponseBody
    public String hello() {

        // key 在请求头中,filter拦截后会放入dubbo RpcContext 上下文
        // 假设 当前默认数据源是 test库,而 key =  test_1

        boolean ret1=testService.method1();  // 在 test 库上执行

        boolean ret2=testService.method1();  // 在  test_1 库上执行


        return "hello";
    }
}

可以仔细看下上面我写的代码注释部分,以上结论是我试验过的。

在一个 controller中调用多次dubbo 服务其实是很普遍的,但是放入 RpcContext中的数据会在第一次调用 testService.method1()之后被销毁,第二次的dubbo调用时就获取不到了。而 filter也只会拦截一次controller。

前面说到, RpcContext是基于java 原生 ThreadLocal设计,而 ThreadLocal是跟当前线程绑定的,而上面controller的hello方法在调完第一次的 testService.method1()之后,当前线程仍然是在的啊!看来数据并不是凭空消失的,这样一来应该是dubbo源码中显式的移除掉了上下文数据。

经过一番搜索+断点调试(限于篇幅,省略步骤),最终找到以下两处地方在销毁数据:

《微服务下无侵入式动态路由数据库》

《微服务下无侵入式动态路由数据库》

上面红框处的代码之前是未被注释的,有需要的朋友可以自行修改dubbo源码,放入私仓使用。

至此,一种微服务下的无侵入式动态路由数据库方案算是达成了!

总结

  1. mybatis的 interceptor 确实很实用,改写sql、统计执行时间等,用处很大。
  2. dubbo原本已经考虑到在远程调用中携带上下文参数了,用好这个可以做一些类似切面但更轻量的设计。

相关 [微服务 侵入 路由] 推荐:

微服务下无侵入式动态路由数据库

- - IT瘾-dev
本文可全文转载,但需要保留原作者和出处. 项目主要采用 springboot + dubbo + mybatis框架,大体分为 web和 service两层. web提供api接口给 sdk客户端使用, service则提供mysql数据库表等操作,为 web提供 dubbo服务支持.

初识微服务

- - ITeye博客
微服务架构越来越火,有必要学习一下. 软件开发过程中碰到什么问题. 一个简单的应用会随着时间推移逐渐变大. 在每次的sprint中,开发团队都会面对新“故事”,然后开发许多新代码. 几年后,这个小而简单的应用会变成了一个巨大的怪物. 一旦你的应用变成一个又大又复杂的怪物,那开发团队肯定很痛苦. 敏捷开发和部署举步维艰,其中最主要问题就是这个应用太复杂,以至于任何单个开发者都不可能搞懂它.

谈微服务架构

- - 人月神话的BLOG
其实在前面很多文章谈到SOA,特别是系统内的SOA和组件化的时候已经很多内容和微服务架构思想是相同的,对于微服务架构,既然出现了这个新名称,那就再谈下微服务架构本身的一些特点和特性. 从这个图可以看到微服务架构的第一个重点,即业务系统本身的组件化和服务化,原来开发一个业务系统本身虽然分了组件和模块,但是本质还是紧耦合的,这关键的一个判断标准就是如果要将原有的业务系统按照模块分开部署到不同的进程里面并完成一个完整业务系统是不可能实现的.

微服务性能模式

- - 互联网 - ITeye博客
前言:基于微服务系统越来越普遍. 下面我们就来看看五种常见的特定微服务性能的挑战,以及如何应解他们. 背景:在IT界微服务架构为基础的系统越来越多, 每一个应用系统都集成了不同的组件和服务,几乎所有的特定业务应用程序都需要集成一个或更多的应用服务. 但是一个综合性系统集成不同的服务这无疑是一个巨大的挑战.

微服务与架构师

- - 乱象,印迹
因为工作的关系,最近面试了很多软件架构师,遗憾的是真正能录用的很少. 很多候选人有多年的工作经验,常见的框架也玩得很溜. 然而最擅长的是“用既定的技术方案去解决特定的问题”,如果遇到的问题没有严格对应的现成框架,就比较吃力. 这样的技能水平或许适合某些行业,但很遗憾不符合我们的要求. 软件架构师到底应该做什么,又为什么这么难做好,这都是近来的热门问题,我也一直在和朋友们讨论.

从Excel到微服务

- - 乱象,印迹
Excel很老,Excel很土,Excel一点也不sexy;微服务新,微服务很潮门,微服务很高大上. 那么,Excel和微服务有什么关系. 上个月看了篇文章,The Unbunlding of Excel. 作者认为,对于初创公司(尤其是非“纯IT”初创公司)来说,Excel几乎包办各种工作. 想做轻量级的CRM,可用Excel.

微服务拆分之道

- - DockOne.io
微服务在最近几年大行其道,很多公司的研发人员都在考虑微服务架构,同时,随着 Docker 容器技术和自动化运维等相关技术发展,微服务变得更容易管理,这给了微服务架构良好的发展机会. 在做微服务的路上,拆分服务是个很热的话题. 我们应该按照什么原则将现有的业务进行拆分. 接下来一起谈谈服务拆分的策略和坚持的原则.

微服务之saga模式

- -
你已经使用 database ber service 模式. 每个service拥有自己的database. 一些业务事务会跨越多个service,所以你需要来确保data consistency. 例如,假设你正在构建一个电子商务网站,这个网站的用户的会有一个最大欠款限制,应用程序必须确保一个新订单不能超过用户的最大前款限制,但是orders表和customers表不在同一个数据库,所以应用程序不能简单的使用本地的ACID事务.

微服务安全简介

- - 掘金 架构
​由于其可扩展性、灵活性和敏捷性,微服务架构已经变得越来越受欢迎. 然而,随着这种架构的分布和复杂性增加,确保强大的安全措施变得至关重要. 微服务的安全性超越了传统的方法,需要采用全面的策略来保护免受不断演变的威胁和漏洞的影响. 通过理解核心原则并采取有效的安全措施,组织可以加强其微服务架构,并保护敏感数据和资源.

微服务框架Spring Cloud介绍 Part2: Spring Cloud与微服务

- - skaka的博客
之前介绍过 微服务的概念与Finagle框架, 这个系列介绍Spring Cloud.. Spring Cloud还是一个相对较新的框架, 今年(2016)才推出1.0的release版本. 虽然Spring Cloud时间最短, 但是相比我之前用过的Dubbo和Finagle, Spring Cloud提供的功能最齐全..