13. sharding-jdbc源码之读写分离

阿飞Javaer,转载请注明原创出处,谢谢!

读写分离支持项

  • 提供了一主多从的读写分离配置,可独立使用,也可配合分库分表使用。
  • 同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。
  • Spring命名空间。
  • 基于Hint的强制主库路由。

读写分离不支持范围

  • 主库和从库的数据同步。
  • 主库和从库的数据同步延迟导致的数据不一致。
  • 主库双写或多写。

读写分离支持项和不支持范围摘自sharding-jdbc使用指南☞读写分离

源码分析

先执行sharding-jdbc-example-config-spring-masterslave模块中的的SQL脚本all_schema.sql,这里有读写分离测试的需要的数据库、表以及数据;

  • 两个主数据库dbtbl_0_masterdbtbl_1_master
  • 数据库dbtbl_0_master有两个从库dbtbl_0_slave_0dbtbl_0_slave_1,这个集群体系命名为dbtbl_0
  • 数据库dbtbl_1_master有两个从库dbtbl_1_slave_0dbtbl_1_slave_1,这个集群体系命名为dbtbl_1

SpringNamespaceWithMasterSlaveMain.java为入口,分析读写分离是如何实现的:

router()路由时,会尝试读写分离:

Collection<PreparedStatement> preparedStatements;
if (SQLType.DDL == sqlType) {
    // 路由这里生成PreparedStatement时会选主从(如果是主从的话)
    preparedStatements = generatePreparedStatementForDDL(each);
} else {
    // 路由这里生成PreparedStatement时会选主从(如果是主从的话)
    preparedStatements = Collections.singletonList(generatePreparedStatement(each));
}
routedStatements.addAll(preparedStatements);
    private PreparedStatement generatePreparedStatement(final SQLExecutionUnit sqlExecutionUnit) throws SQLException {
        // 先获取connection数据库连接,然后得到PreparedStatement,获取conntection时就会尝试选主从(如果有主从的话)
        Connection connection = getConnection().getConnection(sqlExecutionUnit.getDataSource(), routeResult.getSqlStatement().getType());
        return connection.prepareStatement(... ...);
    }
// 数据源名称与数据库连接关系缓存,例如:{dbtbl_0_master:Connection实例; dbtbl_1_master:Connection实例; dbtbl_0_slave_0:Connection实例; dbtbl_0_slave_1:Connection实例; dbtbl_1_slave_0:Connection实例; dbtbl_1_slave_1:Connection实例}
private final Map<String, Connection> cachedConnections = new HashMap<>();

/**
 * 根据数据源名称得到数据库连接
 */
public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException {
    // 首先尝试从local cache(map类型)中获取,如果已经本地缓存,那么直接从本地缓存中获取
    if (getCachedConnections().containsKey(dataSourceName)) {
        return getCachedConnections().get(dataSourceName);
    }
    DataSource dataSource = shardingContext.getShardingRule().getDataSourceRule().getDataSource(dataSourceName);
    Preconditions.checkState(null != dataSource, "Missing the rule of %s in DataSourceRule", dataSourceName);
    String realDataSourceName;
    // 如果是主从数据库的话(例如xml中配置<rdb:master-slave-data-source id="dbtbl_0" ...>,那么dbtbl_0就是主从数据源)
    if (dataSource instanceof MasterSlaveDataSource) {
        // 见后面的"主从数据源中根据负载均衡策略获取数据源"的分析
        NamedDataSource namedDataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType);
        realDataSourceName = namedDataSource.getName();
        // 如果主从数据库元选出的数据源名称(例如:dbtbl_1_slave_0)与数据库连接已经被缓存,那么从缓存中取出数据库连接
        if (getCachedConnections().containsKey(realDataSourceName)) {
            return getCachedConnections().get(realDataSourceName);
        }
        dataSource = namedDataSource.getDataSource();
    } else {
        realDataSourceName = dataSourceName;
    }
    Connection result = dataSource.getConnection();
    // 把数据源名称与数据库连接实例缓存起来
    getCachedConnections().put(realDataSourceName, result);
    replayMethodsInvocation(result);
    return result;
}

主从数据源中根据负载均衡策略获取数据源核心源码--MasterSlaveDataSource.java:


// 主数据源, 例如dbtbl_0_master对应的数据源
@Getter
private final DataSource masterDataSource;

// 主数据源下所有的从数据源,例如{dbtbl_0_slave_0:DataSource实例; dbtbl_0_slave_1:DataSource实例}
@Getter
private final Map<String, DataSource> slaveDataSources;

public NamedDataSource getDataSource(final SQLType sqlType) {
    if (isMasterRoute(sqlType)) {
        DML_FLAG.set(true);
        // 如果符合主路由规则,那么直接返回主路由(不需要根据负载均衡策略选择数据源)
        return new NamedDataSource(masterDataSourceName, masterDataSource);
    }
    // 负载均衡策略选择数据源名称[后面会分析]
    String selectedSourceName = masterSlaveLoadBalanceStrategy.getDataSource(name, masterDataSourceName, new ArrayList<>(slaveDataSources.keySet()));
    DataSource selectedSource = selectedSourceName.equals(masterDataSourceName) ? masterDataSource : slaveDataSources.get(selectedSourceName);
    Preconditions.checkNotNull(selectedSource, "");
    return new NamedDataSource(selectedSourceName, selectedSource);
}

// 主路由逻辑
private boolean isMasterRoute(final SQLType sqlType) {
    return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly();
}

主路由逻辑如下:

  1. 非查询SQL(SQLType.DQL != sqlType)
  2. 当前数据源在当前线程访问过主库(数据源访问过主库就会通过ThreadLocal将DML_FLAG置为true,从而路由主库)(DML_FLAG.get())
  3. HintManagerHolder方式设置了主路由规则(HintManagerHolder.isMasterRouteOnly())

当前线程访问过主库后,后面的操作全部切主,是为了防止主从同步数据延迟导致写操作后,读不到最新的数据?我想应该是这样的^^

主从负载均衡分析

从对MasterSlaveDataSource.java的分析可知,如果不符合强制主路由规则,那么会根据负载均衡策略选多个slave中选取一个slave;MasterSlaveLoadBalanceStrategy接口有两个实现类:RoundRobinMasterSlaveLoadBalanceStrategyRandomMasterSlaveLoadBalanceStrategy,简单分析其实现;

轮询策略

轮询方式的实现类为RoundRobinMasterSlaveLoadBalanceStrategy,核心源码如下:

public final class RoundRobinMasterSlaveLoadBalanceStrategy implements MasterSlaveLoadBalanceStrategy {
    
    private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>();
    
    @Override
    public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {
        // 每个集群体系都有自己的计数器,例如dbtbl_0集群,dbtbl_1集群;如果COUNT_MAP中还没有这个集群体系,需要先初始化;
        AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0);
        COUNT_MAP.putIfAbsent(name, count);
        // 如果轮询计数器(AtomicInteger count)长到slave.size(),那么归零(防止计数器不断增长下去)
        count.compareAndSet(slaveDataSourceNames.size(), 0);
        // 计数器递增,根据计算器的值就是从slave集合中选中的目标slave的下标
        return slaveDataSourceNames.get(count.getAndIncrement() % slaveDataSourceNames.size());
    }
}

随机策略

随机方式的实现类为RandomMasterSlaveLoadBalanceStrategy,核心源码如下:

public final class RandomMasterSlaveLoadBalanceStrategy implements MasterSlaveLoadBalanceStrategy {
    
    @Override
    public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {
        // 取一个随机数,就是从slave集合中选中的目标slave的下标
        return slaveDataSourceNames.get(new Random().nextInt(slaveDataSourceNames.size()));
    }
}

默认策略

@RequiredArgsConstructor
@Getter
public enum MasterSlaveLoadBalanceStrategyType {
    // 轮询策略 
    ROUND_ROBIN(new RoundRobinMasterSlaveLoadBalanceStrategy()),
    // 随机策略
    RANDOM(new RandomMasterSlaveLoadBalanceStrategy());
    
    private final MasterSlaveLoadBalanceStrategy strategy;
    
    // 默认策略为轮询
    public static MasterSlaveLoadBalanceStrategyType getDefaultStrategyType() {
        return ROUND_ROBIN;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容