专栏首页IT技术小咖数据库千万级分库分表和读写分离之「SpringBoot实战」

数据库千万级分库分表和读写分离之「SpringBoot实战」

“文章预计阅读 30 分钟”

一. 前言

前几天时间写了如何使用Sharding-Sphere进行分库分表和读写分离的例子,相信能够感受到Sharding-JDBC的强大了,而且使用配置都非常干净。官方支持的功能还很多功能分布式主键、强制路由等。这里是最终版介绍下如何在分库分表的基础上集成读写分离的功能。

二. 项目实战

主从数据库配置

在配置前,我们希望分库分表规则和之前保持一致:

基于user表,根据id进行分库,如果id mod 2为奇数则落在ds0库,偶数则落在ds1库根据age进行分表,如果age mod 2为奇数则落在user_0表,偶数则落在user_1表

读写分离规则:

读都落在从库,写落在主库

因为使用我们使用Sharding-JDBC Spring Boot Starter,所以还是只需要在properties配置文件配置主从库的数据源即可

# 可以看到配置四个数据源 分别是 主数据库两个 从数据库两个sharding.jdbc.datasource.names=master0,master1,master0slave0,master1slave0# 主第一个数据库sharding.jdbc.datasource.master0.type=com.zaxxer.hikari.HikariDataSourcesharding.jdbc.datasource.master0.hikari.driver-class-name=com.mysql.jdbc.Driversharding.jdbc.datasource.master0.jdbc-url=jdbc:mysql://192.168.0.4:3306/ds0?characterEncoding=utf-8&serverTimezone=Asia/Shanghaisharding.jdbc.datasource.master0.username=testsharding.jdbc.datasource.master0.password=12root# 主第二个数据库sharding.jdbc.datasource.master1.type=com.zaxxer.hikari.HikariDataSourcesharding.jdbc.datasource.master1.hikari.driver-class-name=com.mysql.jdbc.Driversharding.jdbc.datasource.master1.jdbc-url=jdbc:mysql://192.168.0.4:3306/ds1?characterEncoding=utf-8&serverTimezone=Asia/Shanghaisharding.jdbc.datasource.master1.username=testsharding.jdbc.datasource.master1.password=12root# 从第一个数据库sharding.jdbc.datasource.master0slave0.type=com.zaxxer.hikari.HikariDataSourcesharding.jdbc.datasource.master0slave0.hikari.driver-class-name=com.mysql.jdbc.Driversharding.jdbc.datasource.master0slave0.jdbc-url=jdbc:mysql://192.168.0.3:3306/ds0?characterEncoding=utf-8&serverTimezone=Asia/Shanghaisharding.jdbc.datasource.master0slave0.username=testsharding.jdbc.datasource.master0slave0.password=12root# 从第一个数据库sharding.jdbc.datasource.master1slave0.type=com.zaxxer.hikari.HikariDataSourcesharding.jdbc.datasource.master1slave0.hikari.driver-class-name=com.mysql.jdbc.Driversharding.jdbc.datasource.master1slave0.jdbc-url=jdbc:mysql://192.168.0.3:3306/ds1?characterEncoding=utf-8&serverTimezone=Asia/Shanghaisharding.jdbc.datasource.master1slave0.username=testsharding.jdbc.datasource.master1slave0.password=12root
# 读写分离配置# 从库的读取规则为round_robin(轮询策略),除了轮询策略,还有支持random(随机策略)sharding.jdbc.config.masterslave.load-balance-algorithm-type=round_robin# 逻辑主从库名和实际主从库映射关系# 主数据库0sharding.jdbc.config.sharding.master-slave-rules.ds0.master-data-source-name=master0# 从数据库0sharding.jdbc.config.sharding.master-slave-rules.ds0.slave-data-source-names=master0slave0# 主数据库1sharding.jdbc.config.sharding.master-slave-rules.ds1.master-data-source-name=master1# 从数据库1sharding.jdbc.config.sharding.master-slave-rules.ds1.slave-data-source-names=master1slave0

# 分库分表配置# 水平拆分的数据库(表) 配置分库 + 分表策略 行表达式分片策略# 分库策略sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=idsharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{id % 2}# 分表策略 其中user为逻辑表 分表主要取决于age行sharding.jdbc.config.sharding.tables.user.actual-data-nodes=ds$->{0..1}.user_$->{0..1}sharding.jdbc.config.sharding.tables.user.table-strategy.inline.sharding-column=age# 分片算法表达式sharding.jdbc.config.sharding.tables.user.table-strategy.inline.algorithm-expression=user_$->{age % 2}
# 主键 UUID 18位数 如果是分布式还要进行一个设置 防止主键重复#sharding.jdbc.config.sharding.tables.user.key-generator-column-name=id
# 打印操作的sql以及库表数据等sharding.jdbc.config.props.sql.show=truespring.main.allow-bean-definition-overriding=true

其他项目配置不变,和之前保持一致即可

三. 测试

1.查询全部数据库

打开浏览器输入 http://localhost:8080/select

控制台打印

2.插入数据

打开浏览器 分别访问

http://localhost:8080/insert?id=1&name=lhd&age=12http://localhost:8080/insert?id=2&name=lhd&age=13http://localhost:8080/insert?id=3&name=lhd&age=14http://localhost:8080/insert?id=4&name=lhd&age=15

控制台打印

结果和之前的一样 根据分片算法和分片策略,不同的id以及age取模落入不同的库表 达到了分库分表

3.查询全部数据

打开浏览器输入 http://localhost:8080/select

控制台打印

四. 问题

1. 无法知道走的到底是哪个数据源
相信大家也发现了,当读写分离和分库分表集成时虽然我们配置sql.show=true但是控制台最终打印不出所执行的数据源是哪个不知道是从库还是主库
2.读写分离实现

读写分离的流程

获取主从库配置规则,数据源封装成MasterSlaveDataSource根据ShardingMasterSlaveRouter路由计算,得到sqlRouteResult.getRouteUnits()单元列表,然后将结果addAll添加并返回执行每个RouteUnits的时候需要获取连接,这里根据轮询负载均衡算法RoundRobinMasterSlaveLoadBalanceAlgorithm得到从库数据源拿到连接后就开始执行具体的SQL查询了,这里通过PreparedStatementHandler.execute()得到执行结果结果归并后返回

MasterSlaveDataSource.class

package io.shardingsphere.shardingjdbc.jdbc.core.datasource;
import io.shardingsphere.api.ConfigMapContext;import io.shardingsphere.api.config.rule.MasterSlaveRuleConfiguration;import io.shardingsphere.core.constant.properties.ShardingProperties;import io.shardingsphere.core.rule.MasterSlaveRule;import io.shardingsphere.shardingjdbc.jdbc.adapter.AbstractDataSourceAdapter;import io.shardingsphere.shardingjdbc.jdbc.core.connection.MasterSlaveConnection;import io.shardingsphere.transaction.api.TransactionTypeHolder;import java.sql.Connection;import java.sql.DatabaseMetaData;import java.sql.SQLException;import java.util.Map;import java.util.Properties;import javax.sql.DataSource;import org.slf4j.Logger;import org.slf4j.LoggerFactory;
public class MasterSlaveDataSource extends AbstractDataSourceAdapter {    private static final Logger log = LoggerFactory.getLogger(MasterSlaveDataSource.class);    private final DatabaseMetaData databaseMetaData;    private final MasterSlaveRule masterSlaveRule;    private final ShardingProperties shardingProperties;
    public MasterSlaveDataSource(Map<String, DataSource> dataSourceMap, MasterSlaveRuleConfiguration masterSlaveRuleConfig, Map<String, Object> configMap, Properties props) throws SQLException {        super(dataSourceMap);        this.databaseMetaData = this.getDatabaseMetaData(dataSourceMap);        if (!configMap.isEmpty()) {            ConfigMapContext.getInstance().getConfigMap().putAll(configMap);        }
        this.masterSlaveRule = new MasterSlaveRule(masterSlaveRuleConfig);        // 从配置文件获取配置的主从数据源        this.shardingProperties = new ShardingProperties(null == props ? new Properties() : props);    }
    // 获取主从配置关系    public MasterSlaveDataSource(Map<String, DataSource> dataSourceMap, MasterSlaveRule masterSlaveRule, Map<String, Object> configMap, Properties props) throws SQLException {        super(dataSourceMap);        this.databaseMetaData = this.getDatabaseMetaData(dataSourceMap);        if (!configMap.isEmpty()) {            ConfigMapContext.getInstance().getConfigMap().putAll(configMap);        }
        this.masterSlaveRule = masterSlaveRule;        this.shardingProperties = new ShardingProperties(null == props ? new Properties() : props);    }
    // 获取数据库元数据    private DatabaseMetaData getDatabaseMetaData(Map<String, DataSource> dataSourceMap) throws SQLException {        Connection connection = ((DataSource)dataSourceMap.values().iterator().next()).getConnection();        Throwable var3 = null;
        DatabaseMetaData var4;        try {            var4 = connection.getMetaData();        } catch (Throwable var13) {            var3 = var13;            throw var13;        } finally {            if (connection != null) {                if (var3 != null) {                    try {                        connection.close();                    } catch (Throwable var12) {                        var3.addSuppressed(var12);                    }                } else {                    connection.close();                }            }
        }
        return var4;    }
    public final MasterSlaveConnection getConnection() {        return new MasterSlaveConnection(this, this.getShardingTransactionalDataSources().getDataSourceMap(), TransactionTypeHolder.get());    }
    public DatabaseMetaData getDatabaseMetaData() {        return this.databaseMetaData;    }
    public MasterSlaveRule getMasterSlaveRule() {        return this.masterSlaveRule;    }
    public ShardingProperties getShardingProperties() {        return this.shardingProperties;    }}

配置文件配置的主从规则 MasterSlaveRule.class

package io.shardingsphere.core.rule;
import com.google.common.base.Preconditions;import io.shardingsphere.api.algorithm.masterslave.MasterSlaveLoadBalanceAlgorithm;import io.shardingsphere.api.algorithm.masterslave.MasterSlaveLoadBalanceAlgorithmType;import io.shardingsphere.api.config.rule.MasterSlaveRuleConfiguration;import java.util.Collection;
public class MasterSlaveRule {    //名称(这里是ds0和ds1)    private final String name;    //主库数据源名称(这里是ds_master_0和ds_master_1)    private final String masterDataSourceName;    //所属从库列表,key为从库数据源名称,value是真实的数据源    private final Collection<String> slaveDataSourceNames;    //主从库负载均衡算法    private final MasterSlaveLoadBalanceAlgorithm loadBalanceAlgorithm;    //主从库路由配置    private final MasterSlaveRuleConfiguration masterSlaveRuleConfiguration;

轮询负载均衡算算法 RoundRobinMasterSlaveLoadBalanceAlgorithm.class

package io.shardingsphere.api.algorithm.masterslave;
import java.util.List;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicInteger;
//轮询负载均衡策略,按照每个从节点访问次数均衡public final class RoundRobinMasterSlaveLoadBalanceAlgorithm implements MasterSlaveLoadBalanceAlgorithm {    private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap();
    public RoundRobinMasterSlaveLoadBalanceAlgorithm() {    }
    public String getDataSource(String name, String masterDataSourceName, List<String> slaveDataSourceNames) {        AtomicInteger count = COUNT_MAP.containsKey(name) ? (AtomicInteger)COUNT_MAP.get(name) : new AtomicInteger(0);        COUNT_MAP.putIfAbsent(name, count);        count.compareAndSet(slaveDataSourceNames.size(), 0);        return (String)slaveDataSourceNames.get(Math.abs(count.getAndIncrement()) % slaveDataSourceNames.size());    }}

ShardingMasterSlaveRouter.class

//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//
package io.shardingsphere.core.routing.router.masterslave;
import io.shardingsphere.core.constant.SQLType;import io.shardingsphere.core.hint.HintManagerHolder;import io.shardingsphere.core.routing.RouteUnit;import io.shardingsphere.core.routing.SQLRouteResult;import io.shardingsphere.core.rule.MasterSlaveRule;import java.beans.ConstructorProperties;import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;import java.util.LinkedList;
public final class ShardingMasterSlaveRouter {    private final Collection<MasterSlaveRule> masterSlaveRules;
    // 得到最终的sql路由    public SQLRouteResult route(SQLRouteResult sqlRouteResult) {        Iterator var2 = this.masterSlaveRules.iterator();
        while(var2.hasNext()) {            MasterSlaveRule each = (MasterSlaveRule)var2.next();            this.route(each, sqlRouteResult);        }
        return sqlRouteResult;    }
    //进行计算筛选得到最终sql路由    private void route(MasterSlaveRule masterSlaveRule, SQLRouteResult sqlRouteResult) {        Collection<RouteUnit> toBeRemoved = new LinkedList();        Collection<RouteUnit> toBeAdded = new LinkedList();        Iterator var5 = sqlRouteResult.getRouteUnits().iterator();
        while(var5.hasNext()) {            RouteUnit each = (RouteUnit)var5.next();            if (masterSlaveRule.getName().equalsIgnoreCase(each.getDataSourceName())) {                toBeRemoved.add(each);                if (this.isMasterRoute(sqlRouteResult.getSqlStatement().getType())) {                    MasterVisitedManager.setMasterVisited();                    toBeAdded.add(new RouteUnit(masterSlaveRule.getMasterDataSourceName(), each.getSqlUnit()));                } else {                    toBeAdded.add(new RouteUnit(masterSlaveRule.getLoadBalanceAlgorithm().getDataSource(masterSlaveRule.getName(), masterSlaveRule.getMasterDataSourceName(), new ArrayList(masterSlaveRule.getSlaveDataSourceNames())), each.getSqlUnit()));                }            }        }        //路由移除(查询时 移除所有主库)        sqlRouteResult.getRouteUnits().removeAll(toBeRemoved);        //添加从库/主库 具体事件定        sqlRouteResult.getRouteUnits().addAll(toBeAdded);    }
    // 判断是不是主库    private boolean isMasterRoute(SQLType sqlType) {        return SQLType.DQL != sqlType || MasterVisitedManager.isMasterVisited() || HintManagerHolder.isMasterRouteOnly();    }
    @ConstructorProperties({"masterSlaveRules"})    public ShardingMasterSlaveRouter(Collection<MasterSlaveRule> masterSlaveRules) {        this.masterSlaveRules = masterSlaveRules;    }}

注: 判断是不是主库的规则为:

private boolean isMasterRoute(SQLType sqlType) {        return SQLType.DQL != sqlType || MasterVisitedManager.isMasterVisited() || HintManagerHolder.isMasterRouteOnly();    }

SQL语言的判断

SQL语言共分为四大类:数据查询语言DQL,数据操纵语言DML,数据定义语言DDL,数据控制语言DCL。

通过断点,查询全部数据时最终的sql路由为

走的从库的四个从表

前面的问题也就迎刃而解

目前读写分离和分库分表就完成

源码分析不对,如有错误请指点一二

源码地址:

github.com/LiHaodong888/SpringBootLearn

本文分享自微信公众号 - IT技术小咖(IT-arch),作者:小东啊

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 从 Java 代码如何运行聊到 JVM 和对象的创建-分配-定位-布局-垃圾回收

    概括一下:程序员小张编写好的 Java 源代码文件经过 Java 编译器编译成字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到 Java 虚拟机中解...

    IT技术小咖
  • 分布式系统「全链路日志追踪」实战之 RestTemplate & Feign

    (图片来源于 Google Dapper 的一篇论文,这是链路追踪理论基础的鼻祖)这张图看上去感觉很高大上的样子 ,但精髓在于日志追踪架构设计思维。即设计思维很...

    IT技术小咖
  • 一致性哈希(Consistent Hashing)算法的原理与实现

    分布式系统中对象与节点的映射关系,传统方案是使用对象的哈希值,对节点个数取模,再映射到相应编号的节点,这种方案在节点个数变动时,绝大多数对象的映射关系会失效而需...

    IT技术小咖
  • 【Spring Boot 实战】数据库千万级分库分表和读写分离实战

    因为使用我们使用Sharding-JDBC Spring Boot Starter,所以还是只需要在properties配置文件配置主从库的数据源即可

    小东啊
  • JDBC 动态查询

    用户2965768
  • centos 5.5 安装net-snm

    SNMPv3的安全报头采用用户安全模式(USM),其提供具有机密性和完整性的网络管理通信。机密性通过采用数据加密标准(DES)来提供。尽管,这一算法以脆弱性著称...

    py3study
  • 非Spring项目管理Quartz

    在Spring项目中我们可能并不会过于关注Quartz,因为一些常见的问题(包含上面提到的注入)Spring已经帮我们处理好,如果你在非Spring项目中,集成...

    每天学Java
  • ggalluvial|炫酷桑基图(Sankey),你也可以秀

    本文使用TCGA数据集中的LIHC的临床数据进行展示,大家可以根据数据格式处理自己的临床数据。也可后台回复“R-桑基图”获得示例数据以及R代码。

    西游东行
  • 用FBI通缉犯照片集,考验亚马逊人脸识别,意外发现了隐情

    自从亚马逊的人脸识别系统Rekognition,把28位美国议员认成了罪犯,他们就决定亲自实验一下。

    量子位
  • 提高你的被动收入

    工作前几年,相信不少程序员朋友都会沉浸在提升自我技能,获取更大职业进步的目标中,这样想这样做都没有错,但不应该忽略了职业之外的成长,这两种成长都是需要的,不可偏...

    歪脖贰点零

扫码关注云+社区

领取腾讯云代金券