【上級】MySQLの読み書き分離について詳しく解説


ここに画像の説明を挿入

0. 序文

たとえば、あなたのチームが e コマース プラットフォームを開発および保守しているとします。ビジネスの発展とオンライン ショッピングの習慣の継続的な成熟に伴い、現在、何百万ものユーザーが毎日プラットフォーム上で商品を閲覧および購入しています。しかし、貴チームによる具体的な分析の結果、1 つの製品に対して 1,000 個の製品が注文されていることがわかりました。つまり、多数のリクエストが製品の問い合わせと配送業者情報の問い合わせに対応していることがわかりました。

特にピーク時間帯には、サーバーは大量の読み取りおよび書き込みリクエストを処理する必要がある場合があります。すべての読み取りリクエストがデータベース サーバーによって処理される場合、サーバーの応答が遅くなったり、注文リクエストにサーバーが応答できなくなったり、サーバーが直接クラッシュしたりする可能性があります。

現時点では、上級者として、読み取りと書き込みの分離戦略の使用をすでに検討しているはずです。具体的には、すべての書き込み操作 (注文の作成、ユーザー登録など) を処理するマスター ライブラリを設定し、次に読み取り操作 (製品の参照、注文のクエリなど) を処理する複数のスレーブ ライブラリを設定できることを考慮する必要があります。これにより、メインライブラリの負荷が大幅に軽減されると同時に、読み込みリクエストを複数のスレーブライブラリに分散できるため、ユーザークエリの応答速度も向上します。データベースとテーブルを分離し、読み書きを分離するのは、実際には大量のデータの問題を解決するためですが、読み書きの分離が完了すると、必要な一連の問題が見つかります。この記事では、読み取りと書き込みの分離に焦点を当てます。


サブデータベースとテーブルの詳細については、以前のブログ「MySQL のサブデータベースとテーブルの詳細説明」を参照してください。この記事では、MySQL の読み取りと書き込みの分離の一般的な実装に焦点を当てます。そして問題解決。

読み取りと書き込みの分離を実装するかどうかは、特定のビジネス要件 (システムの同時実行性、ユーザー エクスペリエンス要件など) およびシステムの条件 (ハードウェア リソース、メンテナンス コストなど) に応じて検討する必要があります。

MySQL の 2,000 万レコードのパフォーマンスのボトルネックに関する噂

3 ~ 5 年以上働いている開発学生なら、先輩やオンライン投稿で MySQL の単一テーブルのパフォーマンスのボトルネックについて聞いたことがあるはずです。つまり、単一テーブルのデータ量が 2,000 万行を超えると、パフォーマンスが大幅に低下します。実際、この噂は常に存在し、語り継がれてきましたが、少なくともパフォーマンスの最適化の観点からは、悪いことではないと思います。私が見た非公式の歴史は次のとおりです。MySQL の単一テーブルのパフォーマンスのボトルネック、つまり単一テーブルのデータ量が 2,000 万行を超えると、パフォーマンスが大幅に低下します。ノシ氏によると、この発言は最初はBaiduから出たものと言われ、その後Baiduのエンジニアによって他社にも持ち込まれ、徐々に業界に広まっていったという。


昔、気になってテスト検証をしてみました。其实在8核、16G、 机械硬盘、单表32个字段 情况下。数据库表数据达到1000多万条,没有经过索引优化的情况下。时候性能大概在查询一次的时间5-8秒不等ただし、インデックスの最適化後は、3 秒以内に短縮できます。実際、これは 2,000 万未満のパフォーマンスのボトルネックと見なすことができます。

アリババが提供するベストプラクティス

アリババの「Java 開発マニュアル」では、単一テーブルの行数が 500 万行を超えるか、単一テーブルの容量が 2GB を超えることを提案しています。ただし、この値は固定されておらず、MySQL の構成とマシンのハードウェアに関連しています。単一テーブル データベースが特定の上限に達すると、メモリにインデックスを保存できなくなり、後続の SQL クエリでディスク IO が生成され、パフォーマンスが低下します。ハードウェア構成を増やすと、パフォーマンスが向上する可能性があります。

アリババの「Java 開発マニュアル」には、データ量が 3 年以内にこのレベルに達しないことが予想される場合は、テーブルを作成するときにデータベースとテーブルを分割しないでくださいと付け加えられています。独自の機械条件を総合的に評価し、統一規格として500万ラインの使用が可能です。実際のテストによると、InnoDB エンジンでは、800 万のデータ量を持つ単一の MySQL テーブルのクエリ パフォーマンスが低下する可能性があります。MyISAM エンジンを使用するとクエリ速度が速くなる可能性がありますが、データの整合性とトランザクションのサポートに関しては InnoDB ほど優れていません。したがって、実際のニーズに応じて適切な最適化スキームを選択する必要があります。

1. MySQL の読み取りと書き込みの分離

1. 読み取りと書き込みの分離が必要な理由

ビジネス システムでは、大量の読み取り操作が発生し、データベースに対する読み取り操作の負荷が増大し、書き込み操作の効率が低下する場合、読み取りと書き込みの分離を使用する必要があります。読み取りと書き込みを分離することで、データベースへの負荷を効果的に分散し、システムのパフォーマンスを最適化し、データ処理効率を向上させることができます。

たとえば、電子商取引 Web サイトでは、製品の閲覧やユーザーによる注文の表示などの操作は読み取り操作であり、注文の送信や注文の変更などの操作は書き込み操作です。大規模なプロモーション期間中は、ユーザーのアクセス数が増加し、製品情報の読み取りリクエストが急増します。すべての読み取りおよび書き込み操作を 1 つのデータベースで実行すると、データベースへの負荷が増大し、書き込み操作の効率に影響を及ぼします。注文処理に遅延が生じる可能性があります。

このとき、読み取りと書き込みの分離が必要です。書き込み操作用に 1 つのマスター データベースを構成し、読み取り操作用に複数のスレーブ データベースを構成します。このようにして、マスター データベースの書き込み操作に影響を与えることなく、複数のスレーブ データベース間で読み取り操作を共有できるため、システムの処理効率が向上します。

同時に、読み取りと書き込みを分離することにより、システムの可用性も向上します。プライマリ データベースに障害が発生した場合、読み取りおよび書き込み操作をセカンダリ データベースに即座に切り替えることができるため、システムのダウンタイムが削減されます。

2. MySQL のパフォーマンスのボトルネック、読み取りと書き込みの分離スキーム

MySQL のパフォーマンスのボトルネックについては、序文からもわかるように、主に次の 2 つの側面があります。

  1. IO ボトルネック ディスクの読み取りおよび書き込み速度はメモリの読み取りおよび書き込み速度よりもはるかに遅いため、多数のディスク IO 操作がパフォーマンスのボトルネックになります。
  2. CPU ボトルネック 複雑なクエリが多数ある場合、CPU リソースが大量に占有され、データベースのパフォーマンスに影響を与えます。

これらのボトルネックに対して、読み取りと書き込みを分離するスキームを使用して、MySQL のパフォーマンスを向上させることができます。

読み取り/書き込み分離とは、データベースの読み取り操作と書き込み操作を分離し、異なるデータベース サーバーで処理することです。一般に、書き込み操作用にマスター データベース (Master) を設定し、読み取り操作用に複数のスレーブ データベース (Slave) を設定します。新しいデータが書き込まれると、マスター データベースはそのデータを各スレーブ データベースにコピーします。

  1. 単一のデータベース サーバーの負荷を軽減する: 読み取りおよび書き込み操作を異なるサーバーに分散することで、単一のデータベース サーバーの負荷を効果的に軽減し、応答速度を向上させることができます。
  2. データベースの同時処理能力の向上: 読み取りが多く書き込みが少ないアプリケーション シナリオでは、スレーブ データベースの数を増やすことでデータベースの同時処理能力を向上できます。
  3. データの可用性とセキュリティの向上: プライマリ データベースに障害が発生した場合、読み取りおよび書き込み操作をセカンダリ データベースに迅速に切り替えて、データの可用性を確保できます。同時に、データが複数のサーバーに分散されるため、データのセキュリティが向上します。

よくある質問

また、読み取りと書き込みを分離すると、データの同期遅延が発生し、読み取ったデータが古くなってしまうなどの問題も発生します。したがって、読み取りと書き込みの分離を実装する場合は、これらの要因を考慮する必要があります。

その他の一般的な読み取り/書き込み分離シナリオ

では、他にどのような状況で読み取りと書き込みの分離が必要になるのでしょうか?
読み取りと書き込みの分離はデータベース アーキテクチャにおける一般的な戦略であり、主に次の状況を解決するために使用されます。

  1. データベースの同時読み取りおよび書き込みリクエストが非常に多い場合、単一のデータベース サーバーではその負荷に耐えられない可能性があるため、読み取りと書き込みを分離することで負荷を分散する必要があります。読み取りリクエストは複数のスレーブ ライブラリに分散でき、書き込みリクエストはマスター ライブラリによって処理されます。

  2. リソースの最適化、読み取りと書き込みの分離により、メイン ライブラリは書き込み操作とトランザクション操作の処理に集中し、スレーブ ライブラリは読み取り操作を処理できます。このようにして、マスター/スレーブ ライブラリのさまざまな特性に応じて、ハードウェアとシステム リソースの最適な構成を実行できます。

  3. データのバックアップとフェイルオーバー、これは間接的な役割です。読み取りと書き込みの分離により、リアルタイムのデータ バックアップを実現できます。マスター ライブラリに障害が発生すると、すぐにスレーブ ライブラリに切り替わり、サービスの継続的な可用性が確保されます。

  4. クエリのパフォーマンスを向上させるために、読み取りと書き込みを分離することで、複雑なクエリ操作を複数のスレーブ ライブラリに分散して実行できるため、クエリのパフォーマンスが向上します。

2. プロジェクトの実践

私たちのプロジェクトでは、読み取りと書き込みを分離するために Java および MySQL データベースを使用します。私たちの目標は、システムの安定性とパフォーマンスを確保するだけでなく、ビジネスの急速な発展とデータ量の継続的な増加にも対応することです。以下は、読み取りと書き込みの分離を実現するプロセスです。

1. データベースのコピーを作成する

MySQL でマスター ライブラリ (書き込み操作用) と複数のスレーブ ライブラリ (読み取り操作用) を作成しました。マスターデータベースとスレーブデータベースはそれぞれ独立したデータベースサーバーであり、MySQLのレプリケーション機能により、スレーブデータベースはマスターデータベースのデータをリアルタイムに同期することができます。

2. データソースを構成する

Java プロジェクトでは、2 つのデータ ソースを構成しました。1 つはメイン ライブラリに接続され、もう 1 つはスレーブ ライブラリに接続されました。Spring Boot のデータ ソース構成を使用して、複数のデータ ソースを簡単に管理します。

3. データベースルーティングを実装する

ORM フレームワークとして MyBatis を使用し、RoutingDataSourceインターフェイスを実装することでカスタム データ ソース ルーティング戦略を実装します。データベース操作を実行するときは、操作の種類 (読み取りまたは書き込み) に応じてどのデータ ソースを処理するかを決定します。

DbContextHolder現在のスレッドのデータベース タイプを保存するクラスを定義しますRoutingDataSource。 を継承するクラスを作成しますAbstractRoutingDataSource

public class RoutingDataSource extends AbstractRoutingDataSource {
    
    

    @Override
    protected Object determineCurrentLookupKey() {
    
    
        return DbContextHolder.getDbType();
    }

}

次に、MyBatis 構成ファイルでデータ ソースを構成する必要があります。

@Configuration
public class DataSourceConfig {
    
    

    @Bean
    public DataSource masterDataSource() {
    
    
        // 配置主数据源
    }

    @Bean
    public DataSource slaveDataSource() {
    
    
        // 配置从数据源
    }

    @Bean
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
    
    
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DbContextHolder.DbType.MASTER, masterDataSource);
        targetDataSources.put(DbContextHolder.DbType.SLAVE, slaveDataSource);

        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        routingDataSource.setTargetDataSources(targetDataSources);
        return routingDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("routingDataSource") DataSource routingDataSource) throws Exception {
    
    
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(routingDataSource);
        return sessionFactory.getObject();
    }

}

データ ソースを切り替える必要がある場合は、DbContextHolder.setDbType(DbType)メソッドを使用してデータ ソースを切り替えることができます。
さて、基本的なロジックはほぼ整理できたので、コード内で使用する際に便利な読み書き用のアノテーションを別途実装してみましょう。
@ReadOnlyカスタム アノテーションは、データを読み取るだけでデータを変更しないメソッドまたはクラスをマークするために使用されます。このアノテーションは、データベースの読み取りと書き込みが分離されているシナリオで使用され、システムがこのアノテーションを検出すると、データベースからの操作に自動的に切り替わります。次に、この注釈はデータ ソース切り替えロジックで検出される必要があり、この注釈が検出された場合は、セカンダリ データ ソースに切り替えます。コードのこの部分は他の路盤判定によって複雑になる可能性があるため、これは私が簡略化して書いたものです。実際には、コードはこれよりも複雑であり、フォールトトレラントである必要があります。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({
    
    ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
    
    
}

このアノテーションを解決するには、読み取り操作の処理時にデータ ソースから切り替えるアスペクト (Aspect) を作成します。

@Aspect
@Component
public class DataSourceAspect {
    
    

    @Around("@annotation(ReadOnly)")
    public Object setReadDataSourceType(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        try {
    
    
            DbContextHolder.setDbType(DbContextHolder.DbType.SLAVE);
            return joinPoint.proceed();
        } finally {
    
    
            DbContextHolder.clearDbType();
        }
    }

}

4. データソースの切り替え

カスタム アノテーション を作成しましたReadOnly。これをサービス層のメソッドで使用して、メソッドが使用する必要があるデータベース操作を指定できます。次に、AOP アスペクトを実装しました。アスペクト コードの例は上記のとおりです。アノテーション付きReadOnlyメソッドが呼び出されると、アノテーションのパラメーターに従って、対応するデータ ソースに切り替わります。
読み取り操作を使用する必要があるメソッドで @ReadOnly アノテーションを使用すると、このメソッドが実行されると、システムが自動的にスレーブ データ ソースに切り替わります。たとえば、getUserByIdメソッドを呼び出すときはデータ ソースが使用されます。

@Service
public class UserService {
    
    

    @Autowired
    private UserDao userDao;

    @ReadOnly
    public User getUserById(int id) {
    
    
        return userDao.getUserById(id);
    }

}

書き込み操作の場合、特別なマーキングは必要なく、システムはデフォルトでプライマリ データ ソースを使用します。メソッドが呼び出されるとaddUser、プライマリ データ ソースが使用されます。読み取りリクエストを処理する場合、システムは自動的にセカンダリ データ ソースに切り替わり、システムの読み取りパフォーマンスが向上します。書き込みリクエストを処理する場合、システムはプライマリ データ ソースを使用してデータの一貫性を確保します。

@Service
public class UserService {
    
    

    @Autowired
    private UserDao userDao;

    public void addUser(User user) {
    
    
        userDao.addUser(user);
    }

}

5. 負荷分散

複数のスレーブがあるため、読み取りリクエストをさまざまなスレーブに均等に分散するための単純な負荷分散戦略を実装しました。

2. 参考資料

Meituan が詳しく書いたこの記事を読むことをお勧めします。
「Meituan での SQL パーサーのアプリケーション 著者: Guangyou」https://tech.meituan.com/2018/05/20/sql-parser-used-in-mtdp.html

おすすめ

転載: blog.csdn.net/wangshuai6707/article/details/132657321