目次
1.背景
pgライブラリの単一テーブルのデータ量が数千万を超えたため、最大のものは1億のレベルに達しました。pg単一テーブルのパフォーマンスは、データ量が多すぎるために急激に低下しました。背後にある主なロジックこれは、データ量が特定のサイズを超えることです。B+ Treeインデックスの高さが増加し、レイヤーの高さを増加するたびに、インデックススキャン全体のIOが1つ増えます。したがって、改善するためにパフォーマンス、テーブルのこの部分はテーブルに分割する必要があり、一部のテーブルは将来、ビジネス機能に応じて分割されます。サブライブラリ操作を完了します。
2、DBプロキシ与JDBCプロキシ
テーブルを分割する必要があるため、データの分散とルーティングを処理する必要があり、DB層、中間層、アプリケーション層の3つの層に下から上に分割されます。ほとんどのソリューションは中間層に実装されており、中間層はDBバイアスかアプリケーションバイアスかに応じてDBプロキシとJDBCプロキシに分けられます。
DBプロキシ:mycatを例にとると、ミドルウェアサービスをデプロイして維持する必要があり、アプリケーション層はビジネスコードに注意を払うだけで済み、データベースの読み取り、書き込み、およびシャーディングはmycatによって完全に処理されます。
- 利点:ミドルウェアはクラスター管理を担当し、クラスター内のノードの変更を各クライアントに通知する必要はありません。グローバルな一意のIDとトランザクション管理を実現するのに便利です。メタデータは一元管理され、シャーディング戦略は柔軟に実行できます。カスタマイズ
- 短所:リンク全体が長すぎるため、各レイヤーで応答時間が長くなります。ミドルウェアは単一のポイントであることが多く、他の方法で高可用性を実現する必要があります。
JDBCプロキシ:例としてsharding-jdbcを取り上げます。必要なのは、アプリケーション層にjarパッケージを導入し、jdbcをカプセル化し、それを使用してデータベースの読み取り、書き込み、およびスライスを行うことだけです。
- 利点:パフォーマンスの低下が少ない。各アプリケーションのクライアントステータスが同じであるため、高可用性が提供されます。
- 短所:言語の制限、面倒なアクセス、グローバルな主キーの配布、クラスターの変更、トランザクション管理などには、ノード間の通信が必要です
検討に基づいて、DBプロキシはまだ比較的重いことがわかると思いますが、JDBCプロキシを使用することにしました。
3.JDBCプロキシソリューションの選択
代替の選択肢は、sharding-jdbcとmybatisです。一部のテーブルに対してテーブルシャーディング操作を実行するだけでよいため、sharding-jdbcはグローバルであり、制御が難しい場合があります。
ここでの主な目的は、user_testテーブルをuser_test_0とuser_test_1に分割し、次の2つのテーブルを作成することです。
create table user_test_0
(
id serial not null
constraint user_0_pk
primary key,
name varchar,
tenant_id varchar
);
create table user_test_1
(
id serial not null
constraint user_1_pk
primary key,
name varchar,
tenant_id varchar
);
insert into user_test_0(name, tenant_id) values ('王五', 'alibaba');
insert into user_test_0(name, tenant_id) values ('赵六', 'alibaba');
insert into user_test_0(name, tenant_id) values ('张三', 'baidu');
insert into user_test_1(name, tenant_id) values ('李四', 'jd');
レイジーデータを直接作成するために、次の2つのソリューションを示します。
シャーディング-jdbc
Sharding-jdbcは、Dangdangによってオープンソース化されたクライアントエージェントミドルウェアです。ライブラリシャーディングと読み取り/書き込み分離機能が含まれています。アプリケーションコードに影響を与えることはなく、ほとんど変更がありません。メインストリームのormフレームワークおよびメインストリームのデータベース接続プールと互換性があります。 .ShardingSphereは現在、Apacheのインキュベータープロジェクトです。
公式ドキュメントアドレス:http://shardingsphere.apache.org/document/legacy/2.x/cn/00-overview/
githubアドレス:https://github.com/apache/shardingsphere
コード:
sharding-jdbcの利点は、コードに影響を与えないことです。基本的に、元のコードに触れる必要はなく、関連するデータベース接続の構成をシャーディング構成に変更するだけです。
テーブルが分割されていない場合の元の構成:
spring:
datasource:
url: "jdbc:postgresql://xxx:5432/xx?currentSchema=xx"
username: xx
password: xx
driver-class-name: org.postgresql.Driver
シャーディング使用後の構成:
spring:
shardingsphere:
datasource:
# 数据源名称,多数据源以逗号分隔
names: maycur-pro
maycur-pro:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.95.143:5432/maycur-pro?currentSchema=team4
username: team4
password: maycur
sharding:
tables:
# 表名
user_test:
# inline表达式,${begin..end} 表示范围区间
actual-data-nodes: maycur-pro.user_test_$->{0..1}
# 分表配置,根据tenantId分表
table-strategy:
standard:
precise-algorithm-class-name: com.database.subtable.segment.MyPreciseShardingAlgorithm
sharding-column: tenant_id
# inline:
# sharding-column: tenant_id
# # 分表表达式采用groovy语法
# algorithm-expression: user_test_$->{tenant_id % 2}
# # 配置字段的生成策略,column为字段名,type为生成策略,sharding默认提供SNOWFLAKE和UUID两种,可以自己实现其他策略
# key-generator:
# column: tenantId
# type: SNOWFLAKE
# 属性配置(可选)
props:
# 是否开启sql显示,默认false
sql:
show: true
上記のテーブル分割アルゴリズムはインライン式を使用しませんが、主にサブテーブルフィールドのハッシュ値とサブテーブルの数に基づいて、メインストリームのテーブル分割アルゴリズム(hash + mod)を使用するカスタムアルゴリズム実装クラスMyPreciseShardingAlgorithmを使用します。テーブルモジュラスを取得して特定のサブテーブルのシリアル番号を取得し、各リクエストをuser_test_0またはuser_test_1に対応させます。コードは次のとおりです。
public class MyPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
for (String tableName : availableTargetNames) {
if (tableName.endsWith(Math.abs(shardingValue.hashCode() % 2) + "")) {
return tableName;
}
}
throw new IllegalArgumentException();
}
}
クエリを開始すると、シャーディングは自動的に分割されます。もちろん、すべてのSQLは次のようになります。最も原始的なニーズは特定のテーブルのみである可能性があり、大規模なプロジェクトで隠れた危険を引き起こす可能性があります。一部のSQLも少しサポートされておらず、互換性がないため、適切でない可能性があります。クエリSQLは次のとおりです。
Mybatis
Mybatisは、プラグインをサポートすることでサブテーブル操作を実装します。より一般的なのはインターセプターです。インターセプト用のParameterHandler / StatementHandler / Executor / ResultSetHandlerの4つのレベルをサポートします。これは、次のように簡単に要約できます。
- インターセプトパラメーター処理(ParameterHandler)
- SQL構文構築の処理をインターセプトします(StatementHandler)
- エグゼキュータを傍受する方法(エグゼキュータ)
- 結果セットの処理をインターセプトします(ResultSetHandler)
たとえば、StatementHandlerステージに属するsql rewriteは、実際には、元のテーブル名をサブテーブルのサブテーブルのテーブル名に置き換えるプロセスです。
コード:
mybatisのインターセプターはグローバルであるため、ターゲット/非ターゲットオブジェクト(データベーステーブル)を区別するために特定のアノテーションを導入する必要があります。最初に、テーブル戦略インターフェイスと特定の実装クラスを定義します。
public interface ShardTableStrategy {
/**
* 分表算法
* @param statementHandler
* @return 替换后的表名
*/
String shardAlgorithm(StatementHandler statementHandler);
}
public class UserStrategy implements ShardTableStrategy{
/**
* 原始表名
*/
private final static String USER_ORIGIN_TABLE_NAME = "user_test";
/**
* 下划线
*/
private final static String TABLE_LINE = "_";
/**
* 分表数量
*/
public final static Integer USER_TABLE_NUM = 2;
/**
* 分表字段
*/
private final static String USER_TABLE_SUB_FIELD = "tenantId";
@Override
public String shardAlgorithm(StatementHandler statementHandler) {
// 可以增加前置判断是否需要分表
BoundSql boundSql = statementHandler.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
// 参数值
Map param2ValeMap = JSONObject.parseObject(JSON.toJSONString(parameterObject), Map.class);
Object subFieldValue = param2ValeMap.get(USER_TABLE_SUB_FIELD);
if (param2ValeMap.size() == 0 || subFieldValue == null) {
throw new RuntimeException("User is subTable so must have subFiledValue!");
}
return USER_ORIGIN_TABLE_NAME + TABLE_LINE + Math.abs(subFieldValue.hashCode() % USER_TABLE_NUM);
}
}
定義アノテーション(一部のマッパーファイルには、テーブルに分割する必要のある複数の異なるテーブルがある場合があるため、ここでは配列として定義されています):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SegmentTable {
/**
* 表名
*/
String[] tableName();
/**
* 算法策略
*/
Class<? extends ShardTableStrategy>[] strategyClazz();
}
特定のインターセプターを作成します。
@Intercepts(@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class,Integer.class}))
public class ShardTableInterceptor implements Interceptor {
private final static Logger logger = LoggerFactory.getLogger(ShardTableInterceptor.class);
private final static String BOUND_SQL_NAME = "delegate.boundSql.sql";
private final static String MAPPED_STATEMENT_NAME = "delegate.mappedStatement";
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 全局操作读对象
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
// @SegmentTable
SegmentTable segmentTable = getSegmentTable(metaObject);
if (segmentTable == null) {
return invocation.proceed();
}
// 校验注解:表名与算法必须一致
Class[] classes = segmentTable.strategyClazz();
String[] tableNames = segmentTable.tableName();
if(classes.length != tableNames.length){
throw new RuntimeException("SegmentTable annotation's subTable tableNames and classes must same length!");
}
// 获取表名与算法的映射
Map<String, Class> tableName2StrategyClazzMap = buildTableName2StrategyClazzMap(classes,tableNames);
// 处理sql
String sql = handleSql(statementHandler, metaObject, tableName2StrategyClazzMap);
// 替换sql
metaObject.setValue(BOUND_SQL_NAME, sql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
private SegmentTable getSegmentTable(MetaObject metaObject) throws ClassNotFoundException {
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(MAPPED_STATEMENT_NAME);
// 在命名空间中唯一的标识符
String id = mappedStatement.getId();
id = id.substring(0, id.lastIndexOf("."));
Class cls = Class.forName(id);
SegmentTable segmentTable = (SegmentTable) cls.getAnnotation(SegmentTable.class);
logger.info("ShardTableInterceptor getSegmentTable SegmentTable={}", JSON.toJSONString(segmentTable));
return segmentTable;
}
private Map<String, Class> buildTableName2StrategyClazzMap(Class[] classes, String[] tableNames) {
Map<String, Class> tableName2StrategyClazzMap = new HashMap<>();
for (int i = 0; i < classes.length; i++) {
tableName2StrategyClazzMap.put(tableNames[i], classes[i]);
}
logger.info("ShardTableInterceptor buildTableName2StrategyClazzMap tableName2StrategyClazzMap={}", JSON.toJSONString(tableName2StrategyClazzMap));
return tableName2StrategyClazzMap;
}
private String handleSql(StatementHandler statementHandler, MetaObject metaObject, Map<String, Class> tableName2StrategyClazzMap) throws InstantiationException, IllegalAccessException {
String sql = (String) metaObject.getValue(BOUND_SQL_NAME);
logger.info("ShardTableInterceptor original sql={}", sql);
for (Map.Entry<String, Class> entry : tableName2StrategyClazzMap.entrySet()) {
String tableName = entry.getKey();
Class strategyClazz = entry.getValue();
// 没有分表名就不走算法
if (!sql.contains(tableName)) {
continue;
}
// 1.对value进行算法 -> 确定表名
ShardTableStrategy strategy = (ShardTableStrategy) strategyClazz.newInstance();
String replaceTableName = strategy.shardAlgorithm(statementHandler);
// 2.替换分表表名
sql = sql.replaceAll(tableName, replaceTableName);
logger.info("ShardTableInterceptor handleSql sql={},tableName = {},replaceTableName={}", sql, tableName, replaceTableName);
}
return sql;
}
}
最後に、mybatis構成ファイル(mybatis-config.xml)で構成し、マッパーファイルにコメントを追加します。
<plugins>
<plugin interceptor="com.database.subtable.segment.ShardTableInterceptor"/>
</plugins>
@SegmentTable(tableName = {"user_test"}, strategyClazz = {UserStrategy.class})
public interface UserMapper {
List<User> listUsers(@Param("tenantId") String tenantId);
}
印刷されたログによると、クエリをクリックすると、サブテーブル操作が実装されていることがわかります。
4.データ移行
テーブル分割ルールの決定は、実際にはテーブル分割の最初のステップにすぎません。厄介なのは、データ移行、つまりビジネスへの影響を最小限に抑えてデータ移行を行う方法です。私たちはAlibabaCloudに依存しているため、深夜にオンラインになったときにDataWorksを介してデータを移行することを選択しました。プロセス全体は1時間未満で完了しました。
postgreSqlでカスタムhash_code(テキスト)関数を作成します。この関数はjava hashCode()アルゴリズムと整合性があり、DataWorksデータ移行がキーフィールドハッシュに基づいて正しいテーブルをロックするようにします。
DROP FUNCTION IF EXISTS hash_code(text);
CREATE FUNCTION hash_code(text) RETURNS integer
LANGUAGE plpgsql
AS
$$
DECLARE
i integer := 0;
DECLARE
h bigint := 0;
BEGIN
FOR i IN 1..length($1)
LOOP
h = (h * 31 + ascii(substring($1, i, 1))) & 4294967295;
END LOOP;
RETURN cast(cast(h AS bit(32)) AS int4);
END;
$$;
アプリケーションが一定期間使用できないシーンを許可しない場合は、サブテーブルトランスフォーメーションの起動後に、新しく生成されたすべてのデータをサブテーブルに書き込むこともできますが、履歴データに対する操作は引き続き実行されます。古いテーブルであり、データを操作する前に行う必要があるだけです。ルーティングの判断。十分な数の新しいデータが生成されると(たとえば、2か月または3か月)、この時点でのほとんどすべての操作はサブテーブルに対するものであり、その後開始します。データ移行:データ移行が完了した後、元のルーティング判断を削除できます。