前言
上文 ShardingSphere-JDBC 分片解析引擎中介绍了分片流程中的解析引擎,重点介绍了解析引擎的外围组件ANTLR
;本文持续介绍分片流程中的路由引擎,路由引擎能够说是整个分片流程的外围模块,用户自定义的分片算法都在路由引擎中执行;
开启日志
为了更加清晰具体的查看路由日志,开启 SQL_SHOW
性能:
- 引入 log4j 相干 jar,以及 log4j.xml 配置文件
-
配置
SQL_SHOW
属性为开启:Properties prop = new Properties(); prop.put(ConfigurationPropertyKey.SQL_SHOW.getKey(), true);
路由装璜器
路由引擎的外层包装了一层路由装璜器 RouteDecorator
,为什么要这么设计是因为除了咱们失常走路由算法的路由引擎,ShardingSphere-JDBC
还提供了读写拆散性能,这其实在肯定水平上来讲也是路由,而且这两种路由形式是能够叠加的;所有这里提供了一层形象,实现类包含:
- MasterSlaveRouteDecorator:读写拆散路由装璜器;
- ShardingRouteDecorator:分片路由装璜器,外部蕴含了各种路由引擎;
装璜器能够叠加,所以提供了优先级性能OrderAware
,同时每个装璜器都有对应的规定,大抵如下所示:
装璜器 -RouteDecorator | 配置 -Configuration | 规定 -BaseRule | 优先级 -Order |
---|---|---|---|
MasterSlaveRouteDecorator | MasterSlaveRuleConfiguration | MasterSlaveRule | 10 |
ShardingRouteDecorator | ShardingRuleConfiguration | ShardingRule | 0 |
依据优先级能够晓得首先执行 ShardingRouteDecorator
,有了路由后果再执行MasterSlaveRouteDecorator
;局部启动类代码在DataNodeRouter
中如下所示:
private final Map<BaseRule, RouteDecorator> decorators = new LinkedHashMap<>();
private RouteContext executeRoute(final String sql, final List<Object> parameters, final boolean useCache) {RouteContext result = createRouteContext(sql, parameters, useCache);
for (Entry<BaseRule, RouteDecorator> entry : decorators.entrySet()) {result = entry.getValue().decorate(result, metaData, entry.getKey(), properties);
}
return result;
}
decorators
会依据用户的配置来决定是否会启动对应的装璜器,能够参考下面的表格;上面依照优先级别离介绍两种装璜器;
分片路由装璜器
通过解析引擎获取到了SQLStatement
,想要做分片路由除了此参数还须要另外一个重要参数分片路由规定ShardingRule
;有了这两个外围参数分片路由大抵能够分为以下几步:
- 获取分片条件
ShardingConditions
- 获取具体分片引擎
ShardingRouteEngine
- 执行路由解决,获取路由后果
在具体介绍每一步之前,首先介绍以下几个外围参数RouteContext
、ShardingRule
;
外围参数
重点看一下 RouteContext
、ShardingRule
这两个外围参数;
RouteContext
路由上下文参数,次要蕴含如下几个参数:
public final class RouteContext {
private final SQLStatementContext sqlStatementContext;
private final List<Object> parameters;
private final RouteResult routeResult;
}
- sqlStatementContext:解析引擎获取的
SQLStatement
; - parameters:
PreparedStatement
中设置的参数,如执行 insert 操作setXxx
代替?
; - routeResult:路由之后用来寄存路由后果;
ShardingRule
分片规定,主要参数如下,这个其实和 ShardingRuleConfiguration
大同小异,只是从新做了一个包装;
public class ShardingRule implements BaseRule {
private final ShardingRuleConfiguration ruleConfiguration;
private final ShardingDataSourceNames shardingDataSourceNames;
private final Collection<TableRule> tableRules;
private final Collection<BindingTableRule> bindingTableRules;
private final Collection<String> broadcastTables;
private final ShardingStrategy defaultDatabaseShardingStrategy;
private final ShardingStrategy defaultTableShardingStrategy;
private final ShardingKeyGenerator defaultShardingKeyGenerator;
private final Collection<MasterSlaveRule> masterSlaveRules;
private final EncryptRule encryptRule;
- ruleConfiguration:路由规定配置,能够了解为是
ShardingRule
的原始文件; - shardingDataSourceNames:分片的数据源名称;
- tableRules:表路由规定,对应了用户配置的
TableRuleConfiguration
; - bindingTableRules:绑定表配置,分⽚规定⼀致的主表和⼦表;
- broadcastTables:播送表配置,所有的分⽚数据源中都存在的表;
- defaultDatabaseShardingStrategy:默认库分片策略;
- defaultTableShardingStrategy:默认表分片策略;
- defaultShardingKeyGenerator:默认主键生成策略;
- masterSlaveRules:主从规定配置,用来实现读写拆散的,可配置一个主表多个从表;
- encryptRule:加密规定配置,提供了对某些敏感数据进行加密的性能;
获取分片条件
在获取具体路由引擎和执行路由操作之前,咱们须要获取分片的条件,常见的分片条件次要在 Insert 语句和 Where 语句前面;局部获取分片条件的源码如下:
private ShardingConditions getShardingConditions(final List<Object> parameters,
final SQLStatementContext sqlStatementContext, final SchemaMetaData schemaMetaData, final ShardingRule shardingRule) {if (sqlStatementContext.getSqlStatement() instanceof DMLStatement) {if (sqlStatementContext instanceof InsertStatementContext) {return new ShardingConditions(new InsertClauseShardingConditionEngine(shardingRule).createShardingConditions((InsertStatementContext) sqlStatementContext, parameters));
}
return new ShardingConditions(new WhereClauseShardingConditionEngine(shardingRule, schemaMetaData).createShardingConditions(sqlStatementContext, parameters));
}
return new ShardingConditions(Collections.emptyList());
}
首先会判断是不是 DMLStatement
类型,也是最常见的 SQL 类型比方:增删改查;接下来判断是否是 Insert 语句,别离应用不同的分片条件生成引擎:
- InsertClauseShardingConditionEngine:解决 Insert 语句分片条件;
- WhereClauseShardingConditionEngine:解决非 Insert 语句 Where 语句的分片条件;
InsertClauseShardingConditionEngine
Insert 语句中蕴含分片条件次要有两个中央:
-
Insert 语句中指定的字段名称,当然是否是分片条件,还须要检测
ShardingRule
中的tableRules
是否配置了相干字段作为分片键,看一条简略的 Insert 语句:insert into t_order (user_id,order_id) values (?,?)
这一步其实就是检测 user_id 和 order_id 是否在 ShardingRule
中配置成分片键;
-
当然除了下面显示指定的字段,还有无需显示指定的主键,如果配置了主键生成策略,同样须要检测
ShardingRule
中的tableRules
是否配置了相干字段作为分片键;tableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id"));
生成的后果就是ShardingConditions
,外部蕴含多个ShardingCondition
:
public final class ShardingConditions {private final List<ShardingCondition> conditions;}
public class ShardingCondition {private final List<RouteValue> routeValues = new LinkedList<>();
}
这里的 RouteValue
实现包含ListRouteValue
、RangeRouteValue
、AlwaysFalseRouteValue
:
- ListRouteValue:能够了解分片键对应的是一个具体的值,能够是单个也能够多个;
- RangeRouteValue:分片键对应的是一个区间值;
- AlwaysFalseRouteValue:总是失败的路由值;
WhereClauseShardingConditionEngine
常见 Select 语句,Where 语句前面能够蕴含多个条件,每个条件同样须要检测 ShardingRule
中的 tableRules
是否配置了相干字段作为分片键;稍有不同的中央是,Where 条件须要做合并解决,比方:
String sql = "select user_id,order_id from t_order where order_id = 101 and order_id = 101";
String sql = "select user_id,order_id from t_order where order_id = 101 and order_id in(101)";
order_id 呈现多个值一样会进行合并解决,这里会合并成一个order_id = 101
,如果这里两个值不一样比方:
String sql = "select user_id,order_id from t_order where order_id = 101 and order_id = 102";
会返回一个AlwaysFalseRouteValue
,示意这个条件不可能成立;
获取路由引擎
ShardingSphere-JDBC
依据不同的 SQLStatement
提供了 10 种路由引擎,上面别离介绍,首先看一下大抵的流程图;
流程图
流程图大抵如上所示,具体查看 ShardingRouteEngineFactory
即可;上面具体介绍每个路由引擎;
路由引擎
ShardingDatabaseBroadcastRoutingEngine
全库路由引擎:用于解决对数据库的操作,包含用于库设置的 SET 类型的数据库治理命令,以及 TCL 这样的事务管制语句。在这种状况下,会依据逻辑库的名字遍历所有合乎名字匹配的实在库,并在实在库中执行该命令;
1. 属于 DALStatement
数据库拜访层,常见的命令包含:set,reset,show databases;
show databases;
以上 sql 会全库路由,路由 sql 如下所示:
Actual SQL: ds0 ::: show databases;
Actual SQL: ds1 ::: show databases;
2. 逻辑表都属于播送表
insert into t_config (k,v) values (?,?)
t_config
配置的是一张播送表,执行 insert
操作会将数据插入所有库中;当然前提须要配置播送表:
Collection<String> broadcastTables = new LinkedList<>();
broadcastTables.add("t_config");
shardingRuleConfig.setBroadcastTables(broadcastTables);
路由日志如下:
Actual SQL: ds0 ::: insert into t_config (k,v) values (?, ?) ::: [aa1, 1111]
Actual SQL: ds1 ::: insert into t_config (k,v) values (?, ?) ::: [aa1, 1111]
3. 属于 TCLStatement
事务管制语言,包含设置保留点,回滚等;
SET autocommit=0
路由日志如下:
Actual SQL: ds0 ::: SET autocommit=0;
Actual SQL: ds1 ::: SET autocommit=0;
ShardingTableBroadcastRoutingEngine
全库表路由用于解决对数据库中与其逻辑表相干的所有实在表的操作,次要包含不带分片键的 DQL 和 DML,以及 DDL 等;
1. 属于 DDLStatement
数据库定义语言,包含创立、批改、删除表等;
ALTER TABLE t_order MODIFY COLUMN user_id BIGINT(50) NOT NULL;
日志如下所示:
Actual SQL: ds0 ::: ALTER TABLE t_order0 MODIFY COLUMN user_id BIGINT(50) NOT NULL;
Actual SQL: ds0 ::: ALTER TABLE t_order1 MODIFY COLUMN user_id BIGINT(50) NOT NULL;
Actual SQL: ds1 ::: ALTER TABLE t_order0 MODIFY COLUMN user_id BIGINT(50) NOT NULL;
Actual SQL: ds1 ::: ALTER TABLE t_order1 MODIFY COLUMN user_id BIGINT(50) NOT NULL;
2. 属于 DCLStatement
数据库管制语言,包含受权,角色管制等;grant,deny 等命令
grant select on ds.t_order to root@'%'
给用户 root 受权 select 权限,这里须要指定惟一的表名,不能应用 * 代替;
Actual SQL: ds0 ::: grant select on t_order0 to root@'%'
Actual SQL: ds0 ::: grant select on t_order1 to root@'%'
Actual SQL: ds1 ::: grant select on t_order0 to root@'%'
Actual SQL: ds1 ::: grant select on t_order1 to root@'%'
ShardingIgnoreRoutingEngine
阻断路由用于屏蔽 SQL 对数据库的操作;
1.DALStatement
阻断路由次要针对 DALStatement
上面的 use 命令;
use ds0
ShardingDefaultDatabaseRoutingEngine
默认数据库路由,须要配置默认数据源名称,蕴含以下几种状况:
1. 属于 DALStatement
show create table t_order1
留神点:这里表是实在表,不能配置TableRuleConfiguration
,须要配置默认数据源名称;
shardingRuleConfig.setDefaultDataSourceName("ds0");
日志如下:
Actual SQL: ds0 ::: show create table t_order1
2. 逻辑表都属于默认数据源
select user_id,order_id from t_order0 where user_id = 102
留神点:这里表是实在表,不能配置TableRuleConfiguration
,须要配置默认数据源名称,不能配置为播送表;
Actual SQL: ds0 ::: select user_id,order_id from t_order0 where user_id = 102
3. 属于 DMLStatement
select 2+2
此 DMLStatement
没有表名,并且须要指定默认数据源名称;
Actual SQL: ds0 ::: select 2+2
ShardingUnicastRoutingEngine
单播路由用于获取某一实在表信息的场景,它仅须要从任意库中的任意实在表中获取数据即可;
1. 属于 DALStatement
desc t_order
日志如下:
Actual SQL: ds1 ::: desc t_order0
2. 逻辑表都属于播送表
播送表分片数据源中都存在的表,个别是字典表:
select * from t_config
须要配置播送表:
Collection<String> broadcastTables = new LinkedList<>();
broadcastTables.add("t_config");
shardingRuleConfig.setBroadcastTables(broadcastTables);
日志如下:
Actual SQL: ds0 ::: select * from t_config
3. 属于 DMLStatement
属于 DMLStatement
同时分片条件为AlwaysFalseShardingCondition
,或者没有指定表名,或者没有配置表规定;
select user_id,order_id from t_order where order_id = 101 and order_id = 102
where 前面指定的条件会导致分片条件为AlwaysFalseShardingCondition
Actual SQL: ds1 ::: select user_id,order_id from t_order0 where order_id = 101 and order_id = 102
ShardingDataSourceGroupBroadcastRoutingEngine
数据源组播送,从数据源组中随机抉择一个数据源;
1. 属于 DALStatement
SHOW STATUS
DALStatement
子类中除了 use、set、reset、show databases;根本都会走此引擎;
Actual SQL: ds1 ::: SHOW STATUS
ShardingMasterInstanceBroadcastRoutingEngine
全实例路由用于 DCL 操作,受权语句针对的是数据库的实例。无论一个实例中蕴含多少个 Schema,每个数据库的实例只执行一次;
1. 属于 DCLStatement
CREATE USER customer@127.0.0.1 identified BY '123'
注 :这里的主实例会检测各实例之间,不能有雷同的hostname
和雷同的port
,本地测试同一台 Mysql 不同库,配置 hostname 不统一即可,比方 localhost 和 127.0.0.1;
ShardingStandardRoutingEngine
规范路由是最常见的分片形式了,通过以上几种路由引擎的过滤,剩下的SQLStatement
,就会走剩下的两个引擎了,咱们配置分库分表策略,惯例应用的增删改查都会应用此引擎;
1. 单表查问
select user_id,order_id from t_order where order_id = 101
以上应用惯例配置,两个数据源别离是 ds0,ds1;user_id 作为分库键,order_id 作为分表键;
Actual SQL: ds0 ::: select user_id,order_id from t_order1 where order_id = 101
Actual SQL: ds1 ::: select user_id,order_id from t_order1 where order_id = 101
101 通过分片算法定位到物理表t_order1
,然而无奈定位数据库,所以别离到两个库执行;
2. 关联查问
select a.user_id,a.order_id from t_order a left join t_order_item b ON a.order_id=b.order_id where a.order_id = 101
如果是关联查问,则只有在两者配置了绑定关系,才会应用规范路由;
Collection<String> bindingTables = new LinkedList<>();
bindingTables.add("t_order,t_order_item");
shardingRuleConfig.setBindingTableGroups(bindingTables);
以上配置了两张表为绑定表,关联查问与单表查问复杂度和性能相当,不会进行笛卡尔路由;
Actual SQL: ds0 ::: select a.user_id,a.order_id from t_order1 a left join t_order1 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds1 ::: select a.user_id,a.order_id from t_order1 a left join t_order1 b ON a.order_id=b.order_id where a.order_id = 101
ShardingComplexRoutingEngine
笛卡尔路由是最简单的状况,它无奈依据绑定表的关系定位分片规定,因而非绑定表之间的关联查问须要拆解为笛卡尔积组合执行;
1. 关联查问
以上 SQL 如果不配置绑定关系,那么会进行笛卡尔路由,路由日志如下:
Actual SQL: ds0 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item0 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds0 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item1 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds1 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item0 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds1 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item1 b ON a.order_id=b.order_id where a.order_id = 101
执行路由解决
通过以上流程解决,曾经获取到了解决此 SQLStatement
对应的路由引擎,接下来只须要执行对应的路由引擎,获取路由后果即可;
public interface ShardingRouteEngine {RouteResult route(ShardingRule shardingRule);
}
传入分片规定 ShardingRule
,返回路由后果RouteResult
;上面以规范路由为例,来剖析是如何执行路由解决的;ShardingStandardRoutingEngine
的外围办法 getDataNodes
如下所示:
private Collection<DataNode> getDataNodes(final ShardingRule shardingRule, final TableRule tableRule) {if (isRoutingByHint(shardingRule, tableRule)) {return routeByHint(shardingRule, tableRule);
}
if (isRoutingByShardingConditions(shardingRule, tableRule)) {return routeByShardingConditions(shardingRule, tableRule);
}
return routeByMixedConditions(shardingRule, tableRule);
}
以上有三种路由形式:hint 形式路由、分片条件路由、混合条件路由;上面别离介绍;
Hint 形式路由
首先判断是否应用强制路由形式:
private boolean isRoutingByHint(final ShardingRule shardingRule, final TableRule tableRule) {return shardingRule.getDatabaseShardingStrategy(tableRule) instanceof HintShardingStrategy && shardingRule.getTableShardingStrategy(tableRule) instanceof HintShardingStrategy;
}
须要库和表路由策略都是 HintShardingStrategy
,这个只须要在配置TableRuleConfiguration
别离配置数据库和表的策略都为 HintShardingStrategyConfiguration
即可;
private Collection<DataNode> route0(final ShardingRule shardingRule, final TableRule tableRule, final List<RouteValue> databaseShardingValues, final List<RouteValue> tableShardingValues) {Collection<String> routedDataSources = routeDataSources(shardingRule, tableRule, databaseShardingValues);
Collection<DataNode> result = new LinkedList<>();
for (String each : routedDataSources) {result.addAll(routeTables(shardingRule, tableRule, each, tableShardingValues));
}
return result;
}
接下来就是别离执行路由库和路由表,不论是路由库还是表都须要两个外围的参数:以后可用的指标库或表、以后的分片值;
- 以后可用的指标库或表:这个就是初始化的库和表,比方指标库包含 ds0、ds1,指标表包含 t_order0、t_order1;
- 以后的分片值:以后的 hint 形式就是通过
HintManager
配置的分片值;
其余两种形式其实路由形式也都相似,只是分片值获取的形式不一样;有了这两个值就会调用咱们本人定义的分库分表算法 ShardingAlgorithm
,这样就返回了通过路由后的库表,将后果保留到DataNode
中:
public final class DataNode {
private final String dataSourceName;
private final String tableName;
}
一个实在的库对应一个实在的表;最初将 DataNode
封装到 RouteResult
即可;
分片条件路由
同样首先判断是否走分片条件路由:
private boolean isRoutingByShardingConditions(final ShardingRule shardingRule, final TableRule tableRule) {return !(shardingRule.getDatabaseShardingStrategy(tableRule) instanceof HintShardingStrategy || shardingRule.getTableShardingStrategy(tableRule) instanceof HintShardingStrategy);
}
库和表路由策略都不是 HintShardingStrategy
的状况下才会走分片条件路由;
private Collection<DataNode> routeByShardingConditions(final ShardingRule shardingRule, final TableRule tableRule) {return shardingConditions.getConditions().isEmpty()
? route0(shardingRule, tableRule, Collections.emptyList(), Collections.emptyList()) : routeByShardingConditionsWithCondition(shardingRule, tableRule);
}
而后会判断是否有 ShardingConditions
,对于ShardingConditions
下面章节会专门介绍;如果没有 ShardingConditions
阐明没有条件就会走全库表路由,如果有的话会从 ShardingConditions
中取出库表分片值,上面的逻辑就和 Hint 形式一样了;
混合条件路由
混合模式就是库和表路由策略都不全都是HintShardingStrategy
,要么表应用强制路由,要么库应用强制路由;
private Collection<DataNode> routeByMixedConditions(final ShardingRule shardingRule, final TableRule tableRule) {return shardingConditions.getConditions().isEmpty() ? routeByMixedConditionsWithHint(shardingRule, tableRule) : routeByMixedConditionsWithCondition(shardingRule, tableRule);
}
会判断是否有ShardingConditions
,如果没有阐明库或者表路由有一个应用HintShardingStrategy
,另外一个没有;否则就是 Hint 和 Condition 混合;这种状况就要看谁的优先级高了,很显著是 Hint 形式优先级高,可用看库分片值获取:
private List<RouteValue> getDatabaseShardingValues(final ShardingRule shardingRule, final TableRule tableRule, final ShardingCondition shardingCondition) {ShardingStrategy dataBaseShardingStrategy = shardingRule.getDatabaseShardingStrategy(tableRule);
return isGettingShardingValuesFromHint(dataBaseShardingStrategy)
? getDatabaseShardingValuesFromHint() : getShardingValuesFromShardingConditions(shardingRule, dataBaseShardingStrategy.getShardingColumns(), shardingCondition);
}
如果能获取到 Hint 分片值,那就应用 Hint 值,否则就从 Condition
中获取;
读写拆散路由装璜器
通过下面分片路由装璜器的解决,依据优先级,如果配置了读写拆散会执行读写拆散装璜器MasterSlaveRouteDecorator
;大抵流程如下所示:
读写拆散配置
List<String> slaveDataSourceNames0 = new ArrayList<String>();
slaveDataSourceNames0.add("ds01");
MasterSlaveRuleConfiguration masterSlaveRuleConfiguration0 = new MasterSlaveRuleConfiguration("ds0", "ds0",
slaveDataSourceNames0);
shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration0);
List<String> slaveDataSourceNames1 = new ArrayList<String>();
slaveDataSourceNames1.add("ds11");
MasterSlaveRuleConfiguration masterSlaveRuleConfiguration1 = new MasterSlaveRuleConfiguration("ds1", "ds1",
slaveDataSourceNames1);
shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration1);
首先必须配置了读写拆散策略 ds0 备库为 ds01,ds1 备库为 ds11;
库名称匹配
通过分片路由生成的 RouteUnit
中对应的库名称,和 MasterSlaveRule
中配置的名称能匹配;
读写路由
public String route(final SQLStatement sqlStatement) {if (isMasterRoute(sqlStatement)) {MasterVisitedManager.setMasterVisited();
return masterSlaveRule.getMasterDataSourceName();}
return masterSlaveRule.getLoadBalanceAlgorithm().getDataSource(masterSlaveRule.getName(), masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceNames()));
}
并不是配置了读写拆散都会进行路由解决,有些 SQLStatement
是必须走主表的:
private boolean isMasterRoute(final SQLStatement sqlStatement) {return containsLockSegment(sqlStatement) || !(sqlStatement instanceof SelectStatement) || MasterVisitedManager.isMasterVisited() || HintManager.isMasterRouteOnly();
}
private boolean containsLockSegment(final SQLStatement sqlStatement) {return sqlStatement instanceof SelectStatement && ((SelectStatement) sqlStatement).getLock().isPresent();
}
SelectStatement
中蕴含了锁- 非
SelectStatement
- 配置了
MasterVisitedManager
,外部应用 ThreadLocal 治理 - 配置了
HintManager
,外部应用 ThreadLocal 治理
以上四种状况都是走主表,其余状况走备库,如果有多台备库,会进行负载平衡解决;
替换路由库
通过读写拆散路由解决之后,获取到库名称须要替换原理的库名称;
总结
本文次要介绍了 ShardingSphere-JDBC
的分片路由引擎,重点和难点在于不同类型的 SQLStatement
应用不同的路由引擎,路由引擎泛滥,本文重点举例介绍了每种路由引擎在何种条件下应用;通过路由引擎会获取到路由后果上面就是对 SQL 进行改写了,改写成实在的库表,这样数据库能力执行。
参考
https://shardingsphere.apache…
感激关注
能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一 Java 源码、架构、算法和面试。