手撕一个分库方案

一、需求

关系型数据库仍是大部分公司核心数据的主流存储引擎,它不像有些存储引擎天生支持分布式,可以横向扩容,性能不降低。为了解决关系型数据库横向扩容的问题,目前大部分公司的方案是通过应用架构改进来实现分库分表技术的。

二、方案

主流的分库方案有两种:

第一种方案在应用层实现动态切库(这次实现这种方案)

优点:方案实现起来简单,直连目标数据库,通信效率高,SQL百分百兼容;

缺点:无论是应用层扩容还是分库扩容时,数据库连接池的数量都会成正比增加,应用需要更多的连接维护开销,MySQL单机的连接数量越占越多,维护开销也是越来越大。

适合场景:分库数量不是太多的时候(小厂?)。

dae1d782518afa6e5a166dd17265cc03.png

优化方案,通过应用分组来降低每个应用上数据库连接

7737d03f21ae4cfda5e5a4711b17153d.png

第二种方案是通过数据库连接代理层来实现分库

优点:连接池的数量会少很多,复用的程度会大大增加。聚合查询数据比较容易实现。

缺点:1、代理中间件会成为性能瓶颈,它的高可用,扩容,高性能都需要维护成本  2、sql的执行性能会下降,毕竟中间多了一层代理  3、SQL能否百分百兼容?因为我们知道 代理中间件是需要解析sql来确定路由到哪一个分库上的

适用场景:有大量分库的场景(大厂?)

050f0f264ea8614f58aa88e952e2faa8.png

第三种方案是通过DAS(DataAccessService)服务来实现分库(类似中间件代理,但是不解析SQL,只是为了统一DB访问通道,最大程度复用DB连接。这种方案是我临时想出来的,不清楚有没有公司这么干的)

优点:能在一定程度上解决方案1的缺点,但是又没有第二种方案那么复杂,更像一个折中方案

缺点:多一层网络传输性能损耗

60d6b443a8a2c76279c613b3584f5a1b.png

三、实现

步骤0、总体实现思路图

e45e2f5b54b4b52a6187b791535126ba.png

步骤1、数据源配置表

dbKey,dbIp,dbUser,dbPwd,dbPort,dbName

步骤2、映射表(有了这一层映射,就灵活多了,可以实现很多特殊需求)

orgId,dbKey

步骤3、动态数据源的构建,动态路由接口的实现

系统库连接配置

@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.data-source")
@Data
public class DbConfig {
    private String url;
    private String username;
    private String password;
    private String publicKey; 
    private String connectionProperties;
    private String filters;
}

路由接口

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        {
            return TraceUtil.getShardingDBKey();
        }
    }
}

动态数据源自动构建

@Configuration
public class MybatisConfig {


    @Autowired
    private DbConfig dbConfig;


    @Bean("dataSource")
    public DataSource dataSource() {
        return getNewDataSourceInstance(dbConfig);
    }
    
    private DruidDataSource getNewDataSourceInstance(DbConfig dbConfig){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dbConfig.getUrl());
        druidDataSource.setUsername(dbConfig.getUsername());
        druidDataSource.setPassword(dbConfig.getPassword());
        druidDataSource.setConnectionProperties(dbConfig.getConnectionProperties());
        try {
            druidDataSource.setFilters(dbConfig.getFilters());
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return druidDataSource;
    }
    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource()  {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("dataSource", dataSource());
        //分库数据源--
        MysqlDbConfig mysqlDbConfig = new MysqlDbConfig();
        BeanUtils.copyProperties(dbConfig,mysqlDbConfig);
        List<SysDataSourceDto> dataSourceConfigList = DataSourceUtil.getDataSourceConfigList(mysqlDbConfig,null);
        for (SysDataSourceDto ds : dataSourceConfigList) {
            String dbKey = ds.getDbkey();
            String replace=ds.getDbip()+":"+ds.getDbport()+"/"+ds.getDbname();
            String fromUrl = this.dbConfig.getUrl();
            int beginIndex = fromUrl.indexOf("//");
            int endIndex = fromUrl.lastIndexOf("?");
            String toUrl = fromUrl.substring(0,beginIndex)+"//"+replace+fromUrl.substring(endIndex);
            DbConfig dbConfig = new DbConfig();
            BeanUtils.copyProperties(this.dbConfig,dbConfig);
            dbConfig.setUrl(toUrl);
            DruidDataSource druidDataSource= getNewDataSourceInstance(dbConfig);
            dataSourceMap.put(dbKey,druidDataSource);
        }
        // 将 dataSource 数据源作为默认指定的数据源
        dynamicDataSource.setDefaultTargetDataSource(dataSource());
        // 将 分库数据源作为目标数据源
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }


    @Bean("lazyDataSource")
    public DataSource lazyDataSource() {
        LazyConnectionDataSourceProxy proxy = new LazyConnectionDataSourceProxy();
        proxy.setTargetDataSource(dynamicDataSource());
        return proxy;
    }


    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource作为数据源则不能实现切换
        sessionFactory.setDataSource(lazyDataSource());
        sessionFactory.setTypeAliasesPackage("com.dhy.boss.dto");    // 扫描Model
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapping/*.xml"));    // 扫描映射文件
        return sessionFactory;
    }


    @Bean
    public PlatformTransactionManager transactionManager() {
        // 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(lazyDataSource());
        dataSourceTransactionManager.setNestedTransactionAllowed(false);
        return dataSourceTransactionManager;
    }




}

步骤4、分库标识的传递(传递过程要靠 Spring Token拦截器、Dubbo过滤器、ThreadLocal工具类相互协作完成)

public class TraceUtil {
    private static final InheritableThreadLocal<String> contextHolder_shardingDBKey = new InheritableThreadLocal<>();
    /**
     * 设置业务分库的dbkey
     */
    public static void setShardingDBKey(String dbKey) {
        contextHolder_shardingDBKey.set(dbKey);
    }
     /**
     * 获取业务分库的dbkey
     */
    public static String getShardingDBKey() {
        String dbKey = contextHolder_shardingDBKey.get();
        if (dbKey == null||dbKey.length() == 0) {
            throw new Exception("dbkey为空");
        }
        return dbKey;
    }
}

四、总结

1、没有最好的设计,只有适合当前业务特点的、当前业务规模的合理超前的设计,目前我的项目是SaaS型to B的业务,分库规模在50个左右,目前的设计还是能吼住的,扩容还是比较从容的

2、第三种方案更适合新项目,从头开始的项目。抽时间实现下。

3、感觉方案1的优化方案已经能满足大大部分to B 业务公司的需求了

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕哥架构

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值