记一次分库方案选型

记一次电商项目扩展为支持多品牌的技术选型

背景

公司有个电商项目,是为公司的一个电商品牌开发的,其中涉及到 微信小程序(C端用户购物)、门店店长和店员们使用的 收银系统(web)、公司管理人员使用的后台管理项目(web)。最初设计的时候是按照商家就是我们自己的单一固定品牌来设计的,没考虑做成类似淘宝和京东那样的平台支持多品牌入驻。
现在老板提了一个需求,我部分小店想使用我们的整套系统,包括小程序+门店系统+后台管理系统。同时,这些小店是没有成本去购买自己的服务器和维护自己的服务的。我们老板这边想把自己的系统改造成既能符合自己品牌使用,也能让其它品牌使用,业务逻辑都一样。

1、问题分析

这次的项目改造,我们存在问题和如下:

  1. 我们的人手不足,前后端只能分别投入一个人,短期内需要完成旧系统改造+新需求的开发。所以方案必须用最少工作量的方式
  2. 第三方品牌方是没有IT能力的,所有系统、数据库等均由我们公司完成并部署在一套系统中;
  3. 用户使用到的 业务数据是完全不会交叉 的,包括小程序也是每个品牌方申请各自的小程序,再交给我们维护(如果第三方品牌需要线上小程序);
  4. 我们原有的数据库表中不包含品牌信息,是根据单品牌设计的;

2、方案选型

针对原本的单品牌改为支持多品牌,有两种改造方案:

  1. 直接在所有表中增加品牌字段,区分是哪个品牌的商家来做的操作。收银系统端登录的时候就能获得登录用户所属的品牌,C端用户从不同的小程序登录自然也有办法知道是哪个品牌。
  2. 根据品牌分库。每个品牌我们初始化数据的时候设计个唯一编码(该编码做为数据库分库的名称变量,比如w_1、w_2、w_3),收银端登录的时候需要选择品牌或者输入品牌编码。在后台系统中我们加个过滤器,将所有请求都拦截到并获取到品牌编码(这里可以考虑首次登陆带过来编码后存到session中,后续每次请求前端肯定会带登陆标识token,根据token取出该用户session中编码信息),过滤器取到编码后放入ThreadLocal中。

两种方案的优缺点:

  1. 直接在所有表中增加品牌字段,优点是 a)实现方案简单传统,是个开发人员都能改,b)风险小。缺点是 a)工作量大!我们的背景就是缺人手,所有业务代码从数据库层面到内部接口参数、http请求参数都要加品牌字段,改造工作量较大。b)且后续所有表都需要加品牌标识,有点恶心。
  2. 根据品牌分库。优点是 a)业务代码无需做大改动,符合我们的人手少时间紧的实际情况。缺点也很明显,a)每个品牌加入,我们就要初始化一套数据库给它。如果后续加入的品牌多了,每次系统升级需要执行的sql就要在每个数据库中执行一遍,超级恶心(要考虑sql执行自动化)。b)商家登录体验较差,还需要去输入或选择品牌。c)还会有些其它缺点,比如定时任务需要改造成所有库轮询执行、第三方回调请求到哪个数据库等细节需要考虑解决方案。

经过最终对比,我们选择了 根据品牌分库 的方案。主要原因是至少半年~1年内我们前后端开发维护人手紧缺,加入的品牌方也少,重点考虑的还是节约劳动力的方式。

3、代码实现

1.先定义一个自己的 CompanyCodeThreadLocal,继承自ThreadLocal

package com.link.common.filter;

public class CompanyCodeThreadLocal extends ThreadLocal<String> {

    private static final CompanyCodeThreadLocal instance = new CompanyCodeThreadLocal();

    private CompanyCodeThreadLocal() {
    }

    public static CompanyCodeThreadLocal getInstanse() {
        return instance;
    }

    public static String getCompanyCode() {
        CompanyCodeThreadLocal companyCodeThreadLocal = getInstanse();
        String companyCode = (String) companyCodeThreadLocal.get();
        return null == companyCode ? "" : companyCode;
    }

    public static void setCompanyCode(String companyCode) {
        CompanyCodeThreadLocal companyCodeThreadLocal = getInstanse();
        companyCodeThreadLocal.set(companyCode);
    }

}

2.过滤器中代码如下:

package com.link.common.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.logging.log4j.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;


@Component
public class SessionFilter implements Filter {

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


    @Override
    public void destroy() {
        LOGGER.info("RequestIdFilter destroy...");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        try {
            HttpServletRequest req = (HttpServletRequest) request;
            // 获得品牌编码,放入ThreadLocal
            String companyCode = req.getHeader("companyCode111");
            CompanyCodeThreadLocal.setCompanyCode(companyCode);
            
            filterChain.doFilter(request, response);
        } finally {
            ThreadContext.remove(KEY_REQUEST_ID);
            RequestIdThreadLocal.removeRequestId();
        }
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        LOGGER.info("RequestIdFilter init...");
    }

}

  1. mybatis拦截器拦截到所有sql,从ThreadLocal中获取到companyCode并处理所有表名使表名前带上库名前缀
package com.link.common.filter;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.Properties;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;


@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class MybatisInterceptor implements Interceptor {

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

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 通过MetaObject优雅访问对象的属性,这里是访问statementHandler的属性;:MetaObject是Mybatis提供的一个用于方便、
        // 优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
        // 先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // id为执行的mapper方法的全路径名,如com.uv.dao.UserMapper.insertUser
        String id = mappedStatement.getId();
        // sql语句类型 select、delete、insert、update
        String sqlCommandType = mappedStatement.getSqlCommandType().toString();
        // 数据库连接信息
        // Configuration configuration = mappedStatement.getConfiguration();
        // ComboPooledDataSource dataSource = (ComboPooledDataSource)configuration.getEnvironment().getDataSource();
        // dataSource.getJdbcUrl();

        BoundSql boundSql = statementHandler.getBoundSql();

		// 获取到品牌编码
		String companyCode = CompanyCodeThreadLocal.getCompanyCode();
        System.out.println("========mybatis intercept msql:【" + mSql + "】=====companyCode:[" + companyCode
                + "]");
        // 获取到原始sql语句
        String sql = boundSql.getSql();
        String mSql = sql + " limit 2"; // 使用companyCode加工sql,这边还没实现

       
        // 通过反射修改sql语句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, mSql); // 最终执行的是加工后的sql

        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
        // 此处可以接收到配置文件的property参数
        System.out.println(properties.getProperty("name"));
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值