数据库中间件zebra分享
一、数据库中间件设计思路
1.1.背景
最开始学习jdbc的时候,直接通过jdbc就能访问DB。但是这时候缺少了数据源、缺少了ORM框架,不能在生产环境使用,在后期更换DB类型时也存在很大问题。
所以出现了ORM框架。在未进行读写分离/分库分表的情况下,我们是直接在应用中通过数据源(c3p0、druid、dbcp2等)与数据库建立连接,进行读写操作,架构如下所示:
ORM框架作用:
- 隐藏了对象的访问细节
- ORM使我们构造固化数据结构变得简单易行
可以看到在操作单库单表的情况下,我们是直接在应用中通过数据源(c3p0、druid、dbcp等)与数据库建立连接,进行读写操作。
随着互联网的发展,数据规模越来越大,分库分表、读写分离成了通用的解决方案。
大部分开发人员对于访问单库的应用的架构都是很熟悉的。但是在进行读写分离/分库分表后,底层的数据库实例就会有多个,读写分离情况下一个master多个slave;分库分表的情况下,有多个不同的分库。
从应用的角度来说,除了要与多个不同的数据库建立连接,还需要处理分库分表/读写分离特定场景下的问题:
- 在读写分离的情况下,应用需要对读sql/写sql进行区分,读sql走从库,写sql走主库,并考虑主从同步延迟、高可用等一系列问题。
- 在分库分表的情况下,应用需要能对sql进行解析、改写、路由、结果集合并等一些操作,以及分布式事务、分布式id生成器等。
这无疑是个复杂的工作,而**数据库中间件的作用,**是让开发人员可以像操作单库单表那样去操作数据库,屏蔽底层复杂的实现。
1.2.数据库中间件设计思路
典型的数据库中间件设计方案有2种:服务端代理(proxy:代理数据库)、客户端代理(datasource:代理数据源)。下图演示了这两种方案的架构:
可以看到不论是代理数据库还是代理数据源,底层都操作了多个数据库实例。不同的是:
-
服务端代理(proxy:代理数据库)中:
我们独立部署一个代理服务,这个代理服务背后管理多个数据库实例。而在应用中,我们通过一个普通的数据源(c3p0、druid、dbcp等)与代理服务器建立连接,所有的sql操作语句都是发送给这个代理,由这个代理去操作底层数据库,得到结果并返回给应用。在这种方案下,分库分表和读写分离的逻辑对开发人员是完全透明的。
-
客户端代理(datasource:代理数据源):
应用程序需要使用一个特定的数据源,其作用是代理,内部管理了多个普通的数据源(c3p0、druid、dbcp等),每个普通数据源各自与不同的库建立连接。应用程序产生的sql交给数据源代理进行处理,数据源内部对sql进行必要的操作,如sql改写等,然后交给各个普通的数据源去执行,将得到的结果进行合并,返回给应用。数据源代理通常也实现了JDBC规范定义的API,因此能够直接与orm框架整合。在这种方案下,用户的代码需要修改,使用这个代理的数据源,而不是直接使用c3p0、druid、dbcp这样的连接池。
二、zebra客户端整体架构
其中:
- 最上层的是ShardDataSource,用于进行分库分表。ShardDataSource包含了若干个GroupDataSource,每个连接的数据库集群相当于1个分片(Shard)。
- 中间一层是GroupDataSource,主要用于读写分离。下面通过一组SingleDataSource连接一个数据库服务组集群,分为一个主和若干个从。
- 最下一层是SingleDataSource,主要用于和mysql集群中的单个mysql实例直接建立连接。支持6种连接池:dbcp、dbcp2、druid、tomcat-jdbc、c3p0,hikaricp。用户无需直接使用SingleDataSource。
SingleDataSource适配各个DataSource、DataSourcePool。通过SingleDataSourceManagerFactory管理各个数据源的适配。
ShardDataSource、GroupDataSource都实现了JDBC协议的javax.sql.DataSource接口,因此你可以把二者都当做一个普通的数据库连接池来使用。所有读写分离、分库分表的底层实现逻辑,都对用户进行了屏蔽。
三、SQL执行流程
思考:通过对mapper文件的调用,如何触发zebra的逻辑?
1.SqlSessionFactory/SqlSession
2.MapperProxy
3.Excutor
四、分库分表执行流程
1.1.查询流程
以一次分库分表的查询请求来说明一下整个sql执行的生命周期,测试代码如下,返回的结果是多个:
代码块
@Test
public void shard() {
ShardDataSourceHelper.setShardParams("passenger_id", Lists.newArrayList(1000000110685L));
ShardDataSourceHelper.setExtractParamsOnlyFromThreadLocal(true);
System.out.println("查询结果:" + ugcDriverCommentPassengerService.selectByIdLimit(0, 300, 0).size());
ShardDataSourceHelper.clearAllThreadLocal();
}
mapper文件都会动态代理生成proxy,MapperProxy生成代理类(描述不够准确,这里还有zebra的AsyncMapperProxy发挥作用)。
里面包含的信息有:
- sqlSession
- sqlSessionFactory
- executorType
- sqlSessionProxy ,DefaultSqlSession
- mapperInterface (mapper 接口类)
- methodCache
MapperProxy.invoke调用MapperMethod的execute方法。
mapperMethod.execute(sqlSession, args); execute的实现如下:
代码块
SQL
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
因为返回结果是多个,所以最终会走executeForMany方法:
代码块
SQL
else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
}
executeForMany方法如下,最终会走SqlSessionTemplate(实现了SqlSession)的selectList方法。
代码块
SQL
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
后续交给BaseExecutor.query方法执行,baseExecutor调用SimpleExecutor的query方法。
-> PreparedStatementHandler.query
-> ShardPreparedStatement.execute().
->ShardPreparedStatement.executeQueryWithFilter
->ShardStatement.routingAndCheck
->routingAndCheck中会调路由(DefaultShardRouter.router() )的方法,这是zebra的核心代码了
这里主要处理sql解析,分表规则解析
代码块
SQL
@Override
public RouterResult router(final String sql, List<Object> params) throws ShardRouterException, ShardParseException {
RouterResult routerResult = new RouterResult();
SQLParsedResult parsedResult = SQLParser.parseWithCache(sql);
boolean optimizeIn = false;
SQLHint sqlHint = parsedResult.getRouterContext().getSqlhint();
if (sqlHint != null) {
routerResult.setConcurrencyLevel(sqlHint.getConcurrencyLevel());
Boolean optimizeInObj = sqlHint.getOptimizeIn();
optimizeIn = (optimizeInObj == null) ? this.optimizeShardKeyInSql : optimizeInObj;
}
List<TableShardRule> findShardRules = findShardRules(parsedResult.getRouterContext(), params);
if (findShardRules.size() == 1) {
TableShardRule tableShardRule = findShardRules.get(0);
ShardEvalResult shardResult = tableShardRule.eval(new ShardEvalContext(parsedResult, params, optimizeIn));
routerResult.setMergeContext(new MergeContext(parsedResult.getMergeContext()));
if (shardResult.isBatchInsert()) {
routerResult.setBatchInsert(true);
buildBatchInsertSqls(shardResult, parsedResult, tableShardRule.getTableName(), routerResult);
routerResult.setParams(buildParams(params, routerResult));
} else {
if (optimizeIn && shardResult.isOptimizeShardKeyInSql()) {
routerResult.setSqls(buildSqls(shardResult.getDbAndTables(), parsedResult, tableShardRule.getTableName(),
shardResult.getSkInExprWrapperMap(), shardResult.getShardColumns()));
routerResult.setOptimizeShardKeyInSql(true);
} else {
routerResult.setSqls(buildSqls(shardResult.getDbAndTables(), parsedResult, tableShardRule.getTableName()));
}
routerResult.setParams(buildParams(params, routerResult));
}
return routerResult;
} else if(findShardRules.size() > 1) {
List<ShardEvalResult> shardResults = new ArrayList<ShardEvalResult>();
ShardEvalContext shardEvalContext = new ShardEvalContext(parsedResult, params);
for (TableShardRule tableShardRule : findShardRules) {
shardResults.add(tableShardRule.eval(shardEvalContext));
}
Map<String, List<Map<String, String>>> dbAndTables = new HashMap<String, List<Map<String, String>>>();
for (ShardEvalResult shardResult : shardResults) {
String logicalTable = shardResult.getLogicalTable();
for (Entry<String, Set<String>> entry : shardResult.getDbAndTables().entrySet()) {
String db = entry.getKey();
List<Map<String, String>> tableMappingList = dbAndTables.get(db);
if (tableMappingList == null) {
int size = entry.getValue().size();
tableMappingList = new ArrayList<Map<String, String>>(size);
for (int i = 0; i < size; i++) {
tableMappingList.add(new HashMap<String, String>());
}
dbAndTables.put(db, tableMappingList);
}
int index = 0;
for (String physicalTable : entry.getValue()) {
Map<String, String> tableMapping = tableMappingList.get(index++);
tableMapping.put(logicalTable, physicalTable);
}
}
}
routerResult.setMergeContext(new MergeContext(parsedResult.getMergeContext()));
routerResult.setSqls(buildSqls(dbAndTables, parsedResult));
routerResult.setParams(buildParams(params, routerResult));
return routerResult;
} else {
// add for default strategy
List<RouterTarget> routerSqls = new ArrayList<RouterTarget>();
RouterTarget targetedSql = new RouterTarget(defaultDatasource);
targetedSql.addSql(sql);
routerSqls.add(targetedSql);
List<Object> newParams = null;
if (params != null) {
newParams = new ArrayList<Object>(params);
}
routerResult.setMergeContext(new MergeContext(parsedResult.getMergeContext()));
routerResult.setSqls(routerSqls);
routerResult.setParams(newParams);
return routerResult;
}
}
zebra版本:2.10.4
ShardPreparedStatement.execute()分析:
ShardPreparedStatement类图结构:
- sql类型解析
zebra定义了16种sql类型,字符串解析,确定sql类型。
代码块
SQL
SELECT(true, true, false, 0), //
INSERT(false, false, true, 1), //
UPDATE(false, false, true, 2), //
DELETE(false, false, true, 3), //
SELECT_FOR_UPDATE(false, true, true, 4), //
REPLACE(false, false, true, 5), //
TRUNCATE(false, false, true, 6), //
CREATE(false, false, true, 7), //
DROP(false, false, true, 8), //
LOAD(false, false, true, 9), //
MERGE(false, false, true, 10), //
SHOW(true, true, false, 11), //
EXECUTE(false, false, true, 12), //
SELECT_FOR_IDENTITY(false, true, false, 13), //
EXPLAIN(true, true, false, 14), //
ALTER(false, false, true, 15), //
UNKNOWN_SQL_TYPE(false, false, true, -100); //
如果是select/select_for_update类型,走executeQuery方法,其他的则走executeUpdate方法。
executeQuery方法:
疑问:
1.List
2.分库分表不是不支持select identity insert这种形式嘛,为啥还有beforeQuery方法处理,获取上次insert的返回值
有filters且未执行到最后一个时执行jdbcFilter.executeShardQuery方法
filters执行完的情况执行executeQueryWithFilter方法
filters为空时执行executeQueryWithFilter()
executeQueryWithFilter实现如下:
routingAndCheck功能:
-
sql解析。通过sql解析出sqlType,RouterContext(tableSet、sqlHint(是否强制主库,分表列、并发度等))、MergeContext(join、union、orderBy、groupBy等)、SQLStatement(DBType),具有缓存功能
-
判断是否multi queries,如果是则进入multiQueriesRouter(),获取分表规则、用分表规则路由。
-
获取sqlParsedResults,遍历result找出分表规则,multi queries不支持多个分表规则的查询,会抛出异常。
-
当只有一个分表规则时,执行routerOneRule方法。主要设置RouterResult的值。里面处理四种类型(batchInsert、optimizeShardKeyInSql、multiQueries、other),多种方法设置execute sql
- 拿到logicTable、dbAndTables、parseResult进行rewrite sql
- rewrite方法:主要的处理逻辑在ShardRewriteTableOutputVisitor,分为普通查询、优化的in sql、批量插入等的重写sql。
-
多表查询,数据聚合处理
代码块
SQL
"/*+zebra:w*/";
"/*+zebra:sk=UserId|w*/"
"/*+zebra:sk=UserId*/";
sk:表示分表列,w:强制读主库
- 非multi queries,走普通的routerOneRule
- 如果分表规则有多个,走routerMultiRules,否则走routerDefault。
- 无分表规则,则是普通的单表查询,走routerDefault。
- 拿到sql解析结果,开始执行sql。如果有orderBy limit 切分成多个的方式进行数据获取。默认是normalSelectExecute。
executeOrderyByLimitQuery逻辑分析:
代码块
SQL
if (isSingleTarget(routerTarget)) {
// 单表查询,直接设置下限为0,并执行原始sql
routerTarget.getMergeContext().setOffset(MergeContext.NO_OFFSET);
return normalSelectExecute(srs, sql, routerTarget);
}
如果单表查询,执行normalSelectExecute()
1.2.update流程
六、核心模块
核心功能
1.config
testCase : XmlDataSourceRouterConfigLoaderTest
XmlDataSourceRouterConfigLoader.loadConfig(String routerRuleFile)
配置加载:
项目启动时加载,从xml文件中获取配置并解析,config初始化。
2.jdbc
ShardDataSource.init()
数据源初始化逻辑。dataSourcePool,routerFactory不能为空
ShardConnection
获取数据库连接:
实现了jdbc规范
1.commit() // 提交一个命令
2.createStatement() // 创建操作数据库SQL语句的对象
3.prepareStatement(String sql) //预编译
4.rollback() // 回滚
5.close // 关闭连接
ShardStatement
生成对象用于执行静态的sql描述并且返回执行结果
实现了jdbc规范
executeInternal
本地执行sql
executeUpdateInternal
本地执行更新sql(insert、delete、update)
executeQuery
执行查询操作
ShardPreparedStatement
3.jdbc.parallel
4.merge
ShardResultSetMerger.merge()
数据合并器:
/*
-
- 处理步骤:
-
- 如果路由结果中仅包含一个数据源或者路由结果包含多个数据源但是SQL不包含order
- by子句和聚合函数,且没有distinct,则直接把真实ResultSet List进行limit处理后保存于dataPool中。
- 非第一种情况,则需要从ResultSet
- List中弹出所有记录,进行distinct处理,聚合函数计算,排序,limit计算。并把结果保存于dataPool中。
/
DistinctDataMerger.process()
去重处理
GroupByDataMerger.process()
处理分组
OrderByDataMerger.process()
处理排序
类图:
5.parser
SQLHint.parseHint(String line);
配置初步解析功能:
1.解析分表键 shardColumn
2.判断sql是否要强制走主库 forceMaster. 配置里面通常的格式如:/+zebra:sk=UserId|w/
SQLParser.parse(String sql);
SQL解析功能:
1.通过sql语句解析操作的db数量、DB名称
2.sql类型解析,insert、update、delete、select
3.sql聚合分组过滤等条件解析。limit、order by ,group by ,distinct,min,max
DefaultSQLRewrite.rewrite
sql重写功能:
1.把SQLParser.parse(String sql)解析的结果重写为可执行的sql语句。
2.虚拟表名替换为物理表名(表达不够恰当,表名是通过传参进入rewrite方法的)
6.router
RuleEngine.eval();
分库分表规则执行引擎:
通过GroovyRuleEngine解析分库分表结果,可支持任何复杂的规则解析
DefaultTableSetsManager
表名后缀解析:
1.通过tableName,dbIndexs,tbSuffix,解析库名后缀,表名后缀。
例:把tableName = UOD_Order,dbindexs = a,b_[2-4],tbsuffix = alldb:[_Operation0,_Operation31] .
解析的最终结果是:
解析为三个库,每个库的表名后缀是0~31
代码块
SQL
{a=[UOD_Order_Operation3, UOD_Order_Operation4, UOD_Order_Operation1, UOD_Order_Operation2, UOD_Order_Operation7, UOD_Order_Operation5,
UOD_Order_Operation6, UOD_Order_Operation0], b_2=[UOD_Order_Operation8, UOD_Order_Operation9, UOD_Order_Operation15, UOD_Order_Operation14,
UOD_Order_Operation13, UOD_Order_Operation12, UOD_Order_Operation11, UOD_Order_Operation10],
b_4=[UOD_Order_Operation31, UOD_Order_Operation30, UOD_Order_Operation29, UOD_Order_Operation28,
UOD_Order_Operation27, UOD_Order_Operation26, UOD_Order_Operation25, UOD_Order_Operation24],
b_3=[UOD_Order_Operation20, UOD_Order_Operation19, UOD_Order_Operation18, UOD_Order_Operation17,
UOD_Order_Operation16, UOD_Order_Operation23,UOD_Order_Operation22, UOD_Order_Operation21]}
7.utils
七、优化
1、分表键IN语句的优化
示例
在使用下面分库分表规则的ShardDataSource执行一条带有In的查询语句
展开代码
XML
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="TestTable" global="false" generatedPK="Uid">
<shard-dimension dbRule="#Uid#.intdiv(2)%2"
dbIndexes="db[0-1]"
tbRule="#Uid#%2"
tbSuffix="alldb:[0,3]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
<!-- 一共两个库,每个库有两张表:db0.TestTable0, db0.TestTable1, db1.TestTable0, db1.TestTable1 -->
要执行的查询SQL:
展开代码
SQL
SELECT * FROM TestTable WHERE Uid IN (0, 1, 2, 3,4) AND Name = 'abc';
未开启In语句优化时,经过路由及SQL改写后,在每张表上执行的SQL:
展开代码
SQL
SELECT * FROM TestTable WHERE Uid IN (0, 1, 2, 3, 4) AND Name = 'abc'; //db0.TestTable0
SELECT * FROM TestTable WHERE Uid IN (0, 1, 2, 3, 4) AND Name = 'abc'; //db0.TestTable1
SELECT * FROM TestTable WHERE Uid IN (0, 1, 2, 3, 4) AND Name = 'abc'; //db1.TestTable0
SELECT * FROM TestTable WHERE Uid IN (0, 1, 2, 3, 4) AND Name = 'abc'; //db1.TestTable1
开启In语句优化后:
展开代码
SQL
SELECT * FROM TestTable WHERE Uid IN (0, 4) AND Name = 'abc'; //db0.TestTable0
SELECT * FROM TestTable WHERE Uid IN (1) AND Name = 'abc'; //db0.TestTable1
SELECT * FROM TestTable WHERE Uid IN (2) AND Name = 'abc'; //db1.TestTable0
SELECT * FROM TestTable WHERE Uid IN (3) AND Name = 'abc'; //db1.TestTable1
参考:6. In语句参数优化
2、批量插入优化
在zebra之前的版本中,对于Insert into x(...) values(...),(...)...这种批量插入的语句,只会按照values后的第一组值进行路由,因为业务需要提前进行分组,保证values的值是同一张分表上的。这样在使用时比较麻烦,因此在2.10.1中对批量插入做了优化。
1.单表批量插入
代码块
SQL
// 业务需要自行保证VALUES后的值都是同一张分表内的
INSERT INTO Tb(Uid,...) VALUES(1, ...), (5, ...), (9, ...), ... // db0.Tb1
INSERT INTO Tb(Uid,...) VALUES(2, ...), (6, ...), (10, ...), ... // db1.Tb0
** 2.多表批量插入**
在2.10.1中,zebra支持多表批量插入,在单库多表的场景下,并发度为一的话可以保证事务,但在多库或并发度大于一的时候是无法保证事务的,这时请慎用!
为保证zebra批量插入行为一致,当一条insert语句内values带有多组值默认是走单表批量插入,如果要开多表批量插入需要增加hint配置
代码块
SQL
/* 错误,默认走db0.Tb1,但实际应该是多表的数据 */
INSERT INTO Tb(Uid,...) VALUES(0, ...), (1, ...), (2, ...), (3, ...)...
/* 正确,分别插入到四张表中,因为是两个库,所以无法保证事务 */
/*+zebra:bi*/INSERT INTO Tb(Uid,...) VALUES(0, ...), (1, ...), (2, ...), (3, ...)...
/* 正确,分别插入到同一单库的两张表中,单库内可以保证事务 */
/*+zebra:bi*/INSERT INTO Tb(Uid,...) VALUES(0, ...), (1, ...), (4, ...), (5, ...)...
/* 正确,分别插入到同一单库的两张表中,但因为开启了并发执行无法保证事务 */
/*+zebra:bi|cl=2*/INSERT INTO Tb(Uid,...) VALUES(0, ...), (1, ...), (4, ...), (5, ...)...
目前zebra只支持单库事务,所以插入目标为多库或多表并发插入时无法保证事务
参考:7. 批量插入优化
3、sql hint的作用。sql断言(走主库,bi)
- 强制走主库:/+zebra:w/
- 批量插入优化:(/+zebra:bi/)
- 指定使用shardkey:(/+zebra:sk=col1+col2+...+coln/)
- 拆分In条件,进行精准路由:/+zebra:oi=true/
- 指定分库分表查询并发度:/+zebra:cl=n/
4、并发执行,concurrencyLevel
zebra分库分表在执行某些SQL时如果不带分表键会去扫描所有的分表,zebra原来是给每个库分配一个线程并发执行,每个库上的SQL在一个线程上串行执行,如果一个库中表很多或每条SQL执行较慢的话可能会出现整体执行时间过长的问题。因此zebra新增自定义配置这种全表扫的SQL并发度的功能(>=2.10.0)。
注意:
- 开启了事务(目前zebra只支持单库事务)的SQL不会走并发的逻辑,配置了全局的并发度后也可以使用hint将一个SQL设为非并发执行。
- 并发执行时因为每个线程会获取一个数据库连接,所以不能保证事务,另外并发度的值最好不要设置的太大,一定不能超过单个数据库连接池的最大连接数。
参考:分库分表并发执行
5.路由策略:
描述:
新建的库默认同中心优先
如果同机房内找不到对应DB 则会进行退化 退化顺序 同机房 -> 同中心 -> 同地域
有就近路由需求的业务接入前请先与DBA进行沟通。
1. 就近路由策略
1.1 同机房优先
应用访问DB时会优先选择同一个机房内的DB,适合同机房内部署DB并且对网络延时高的应用。
同机房内找不到DB时会退化到同中心内优先。
1.2 同中心优先
应用访问DB时会优先选择同一个中心内的DB,适合同中心内部署DB并且对网络延时较高的应用。
中心由1个或多个机房组成
现有中心分配:
北京中心1 | 光环 大兴 次渠 |
---|---|
北京中心2 | 永丰 |
上海中心 | 上海所有机房均属于上海中心 |
同中心内找不到DB时会退化到同地域内优先。
1.3 同地域优先
应用访问DB时会优先选择同一个地域内的DB,适合北京-上海跨地域部署的应用。
现有地域:
地域2 | 上海 |
---|---|
地域1 | 北京 |