SaaSマルチテナントシステムにおけるデータ分離の実装について話す

SaaSマルチテナントシステムにおけるデータ分離の実装について話す

SaaS システム プラットフォームを開発したことがある人は、マルチテナントの概念に精通しているはずです。簡単に言えば、テナントは企業の顧客であり、複数のテナントが同じ SaaS システムを共有します。 SaaS システムが利用できなくなると、すべてのテナントが利用できなくなります。 SaaS システムを建物と考えれば、テナントは建物内のオフィスフロアを借りる企業です。通常、各社はそれぞれ独自の業務を行っており、相互に干渉することはありませんが、ビル内のエレベーターが故障すると、すべての会社に影響が及びます。

マルチテナント問題とは、1 台または複数のサーバー上で実行される SaaS システムが複数のテナント (顧客) にサービスを提供できるというアーキテクチャ設計方法です。目的は、複数のテナントがインターネット環境で同じプログラム セットを使用できるようにし、テナント間のデータの分離を確保することです。このアーキテクチャ設計モデルから、マルチテナント アーキテクチャの焦点は、同じプログラム セットの下で複数のテナント データを分離することにあることは容易にわかります。テナント データは集中的に保存されるため、データ セキュリティを実現するには、テナント データを分離し、テナント データが誤ってまたは悪意を持って他者に取得されたり改ざんされたりすることを防止できるかどうかが重要です。マルチテナント データ分離の実装について説明する前に、まず SaaS システムとは何かを見てみましょう。

SaaS システムとは何ですか?

SaaS プラットフォームは、SaaS ソフトウェアを運用するためのプラットフォームです。 SaaS プロバイダーは、企業の情報化に必要なすべてのネットワーク インフラストラクチャとソフトウェアおよびハードウェアのオペレーティング プラットフォームを構築し、初期の実装とその後のメンテナンスを含む一連のサービスを担当します。テナント(企業)はソフトウェアやハードウェアの購入、コンピュータルームの構築、IT 担当者の雇用などの必要がなく、インターネットを通じて情報システムを利用できます。 SaaS は、アプリケーションがネットワーク配信用に設計され、ユーザーがインターネット経由で簡単にホスト、展開、アクセスできるようにするソフトウェア展開モデルです。

簡単に言えば、テナントは SaaS プラットフォームに賃料を支払い、プラットフォームが提供する機能サービスを利用できます。現在、最も典型的な例としては、さまざまなクラウド プラットフォームとクラウド サービス プロバイダーが挙げられます。

マルチテナントデータ分離アーキテクチャ設計

現在、SaaS マルチテナント システムにおけるデータ分離のアーキテクチャ設計には、各テナントに独立したデータベースと独立したテーブルスペースを提供する、フィールドによってテナントを区別するという 3 つの設計があります。各ソリューションには独自の適用性があります。

1つのテナントには1つの独立したデータベースがあります

テナントがデータベースを独立して使用する場合、SaaS システムは複数のデータベースに接続する必要があることを意味します。この実装は、実際にはサブデータベースとサブテーブルのアーキテクチャ設計と同じです。利点は、データの分離レベルが高く、セキュリティが優れていることです。結局のところ、テナントは 1 つのデータベースのみを使用します。ただし、物理的なハードウェアのコストとメンテナンスのコストも高くなります。

独立したテーブルスペース

このソリューションは、すべてのテナントがデータベース システムを共有するように実装されますが、各テナントはデータベース システム内に独立したテーブル スペースを持ちます。

テナントIDフィールドでテナントを分離する

このソリューションは、マルチテナント ソリューションにおける最も単純なデータ分離方法です。つまり、各テーブルにテナントを区別するためのフィールド (tenant_id や org_id など) を追加して、各データがどのテナントに属しているかを識別します。クエリを実行する場合、各ステートメントでこのフィールドをフィルター条件として追加する必要があります。すべてのテナントのデータが同じテーブルに保存され、データの分離が最も低いのが特徴です。完全にフィールドごとに区別されており、データの混同や間違いが起きやすいです。

3 つのデータ分離アーキテクチャ設計の比較は次のとおりです。

隔離計画

料金

サポートされるテナントの数

アドバンテージ

欠点

独立したデータベースシステム

高い

少し

高いデータ分離レベルとセキュリティを備え、個々のテナントのニーズに合わせてカスタマイズできます。

データベースは独立してインストールされるため、物理的コストとメンテナンスコストが比較的高くなります。

独立したテーブルスペース

真ん中

もっと

ある程度の論理的なデータ分離を提供し、1つのデータベースシステムで複数のテナントをサポートできます。

データベース管理が難しく、テーブルが多く、データの修復が少し複雑

テナントIDフィールドで区別する

低い

多くの

メンテナンスと取得コストが最も低く、データベースごとにサポートされるテナント数が最大

最も低い隔離レベルと最も低いセキュリティ

ほとんどの企業は、マルチテナント データの分離を実現するために、テナント ID フィールドによってテナント アーキテクチャ設計を分離する 3 番目の方法を採用しています。次に、コード レベルでマルチテナント データの分離を実現する方法を見てみましょう。公式アカウント「Mayuan Technology Column」をフォローし、キーワード「1111」を返信すると、Alibaba の内部 Java パフォーマンス チューニング マニュアルが入手できます。

Mybatis-plus は、マルチテナントのデータ権限分離をエレガントに実装します。

前述のように、テナント ID フィールドによるテナントの分離は、データを取得する際に各 SQL 文にテナント ID をフィルター条件として追加することで、テナント データを分離する方法です。ただし、これは、各クエリ SQL でテナント ID をフィルター条件として追加する必要があることを意味します。省略すると、異なるテナントからのデータが照会されますが、これは絶対に許可されません。同時に、各クエリ インターフェイスではフィルター条件を手動で設定する必要がありますが、これは反復的な作業であり、まったくエレガントではありません。今回は、mybatis-plus のマルチテナント プラグインについて説明し、マルチテナント分離をどのようにエレガントに実装するかを確認する必要があります。先に進む前に、データ分離をエレガントに実現する方法について考えてみましょう。まず、各 SQL ステートメントをテナント ID でフィルター処理する必要があります。つまり、元の SQL を解析し、適切な場所にテナント ID フィルター条件を追加する必要があります。 mybatis によって提供される拡張ポイントはインターセプターであり、SQL ステートメントが処理される前後のロジックを強化できることがわかっています。ページング プラグインはこれを実行するため、当然ここで SQL を拡張する必要があります。次に、mybatis-plus マルチテナント プラグインがマルチテナント データ分離をどのように実装するかを見てみましょう。プラグイン公式サイト紹介アドレスは、https://www.baomidou.com/pages/aef2f2/#tenantlineinnerinterceptor です。インターセプターのソースコードは次のとおりです。

 public class TenantLineInnerInterceptor extends JsqlParserSupport implements InnerInterceptor { // 多租户处理器private TenantLineHandler tenantLineHandler; // 改SQL,添加多租户id条件public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { if (!InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) { MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); mpBs.sql(this.parserSingle(mpBs.sql(), (Object)null)); } } public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); MappedStatement ms = mpSh.mappedStatement(); SqlCommandType sct = ms.getSqlCommandType(); if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) { return; } MPBoundSql mpBs = mpSh.mPBoundSql(); mpBs.sql(this.parserMulti(mpBs.sql(), (Object)null)); } } // 碍于篇幅问题,下面省略的代码就是继承抽象类JsqlParserSupport解析SQL然后添加多租户id条件的,可以自行查看源码...... }

次に、カスタム実装を提供し、マルチテナント関連の構成を指定する必要があるインターフェースであるプロセッサ TenantLineHandler を見てみましょう。

 public class TenantDatabaseHandler implements TenantLineHandler { private final Set<String> ignoreTables = new HashSet<>(); public TenantDatabaseHandler(TenantProperties properties) { // 将配置文件配置的忽略表名同步大小写,适配不同写法properties.getIgnoreTables().forEach(table -> { ignoreTables.add(table.toLowerCase()); ignoreTables.add(table.toUpperCase()); }); } /** * 获取租户字段名* <p> * 默认字段名叫: tenant_id,我这里使用org_id * * @return 租户字段名*/ @Override public String getTenantIdColumn() { return "org_id"; } @Override public Expression getTenantId() { // 这里通过登录信息上下文返回租户id给多租户拦截器增强SQL使用return new LongValue(RequestUserHolder.getCurrentUser().getOrgId()); } @Override public boolean ignoreTable(String tableName) { // 忽略多租户的表return CollUtil.contains(ignoreTables, tableName); } }

構成プロパティは次のとおりです。

 @ConfigurationProperties(prefix = "ptc.tenant") @Data public class TenantProperties { /** * 全局控制是否开启多租户功能*/ private Boolean enable = Boolean.TRUE; /** * 需要忽略多租户的表* * 即默认所有表都开启多租户的功能,所以记得添加对应的tenant_id 字段哟*/ private Set<String> ignoreTables = Collections.emptySet(); }

次に、インターセプター プラグインを挿入します。

 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 必须保证多租户插件在分页插件之前,这个是MyBatis-plus 的规定if (properties.getEnable()) { mybatisPlusInterceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantDatabaseHandler(properties))); } // 分页插件mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor; }

使用例は次のとおりです。一般的なケースは次のとおりです。ユーザーとロールの関連付けを照会するSQL: getUserList()

 <select id="getUserList" resultType="com.plasticene.textile.entity.User"> select u.* from user u left join user_role r on u.id = r.user_id <where> <if test="query.status != null"> and u.status = #{query.status} </if> <if test="query.roleId != null"> and r.role_id = #{query.roleId} </if> <if test="query.keyword != null"> and ((u.name like concat('%',#{query.keyword},'%')) or (u.mobile like concat(#{query.keyword},'%'))) </if> <if test="query.startEntryTime != null"> and u.entry_time >= #{query.startEntryTime} </if> <if test="query.endEntryTime != null"> <![CDATA[ and u.entry_time <= #{query.endEntryTime}]]> </if> </where> group by u.id order by u.id desc </select>

プロジェクトを開始し、最初にログインしてから、トークン インターフェイスを使用して次のコード ロジックを実行します。

 public PageResult<UserDTO> getList(UserQuery query) { Page<UserDTO> page = new Page<>(query.getPageNo(), query.getPageSize()); List<User> userList = userDAO.getUserList(page, query); List<UserDTO> userDTOS = toUserDTOList(userList); return new PageResult<>(userDTOS, page.getTotal(), page.getPages()); }

コンソールで次の項目を確認します:

 [1658720355293990912] [DEBUG] [2023-05-17 14:25:25.504] [http-nio-16688-exec-1@23652] com.plasticene.textile.dao.UserDAO.getUserList debug : ==> Preparing: SELECT u.* FROM user u LEFT JOIN user_role r ON u.id = r.user_id AND r.org_id = 3 WHERE u.org_id = 3 GROUP BY u.id ORDER BY u.id DESC LIMIT ? [1658720355293990912] [DEBUG] [2023-05-17 14:25:25.505] [http-nio-16688-exec-1@23652] com.plasticene.textile.dao.UserDAO.getUserList debug : ==> Parameters: 20(Long)

ユーザーテーブル u にマルチテナント フィルタ条件 u.org_id=3 が追加され、user_role も追加され、マルチテナント プラグインが有効であることがわかります。

もちろん、ユーザー テーブルを無視したい場合は、構成ファイルで次のように構成するだけです。

 ptc: tenant: ignore-tables: user

この方法では、マルチテナント フィルタ条件 u.org_id=3 はユーザー テーブル u に追加されません。ただし、注意すべき詳細事項があります。 MySQL では user がキーワードなので、標準化するために次のように SQL を記述することがあります。

 select u.* from `user` u left join user_role r on u.id = r.user_id

この時点で、ユーザー テーブルを無視する上記の構成は機能せず、マルチテナント フィルター条件 u.org_id=3 が引き続き追加されることがわかります。ソース コードを確認すると、カスタム マルチテナント プロセッサ TenantLineHandler がテーブル名を大文字と小文字のみに適合させていることがわかります。ただし、ここで SQL によって解析されるテーブル名は **user** であるため、構成が一致せず、機能しません。

もちろん、単一の SQL ステートメントに対してマルチテナント フィルタリング条件を除外する必要がある場合もあります。 @InterceptorIgnore アノテーションを使用できます。

 public interface UserDAO extends BaseMapperX<User> { @InterceptorIgnore(tenantLine = "true") List<User> getUserList(IPage<UserDTO> userPage, @Param("query") UserQuery query); }

この方法では、getUserList() を呼び出しても、マルチテナント フィルタリング条件は追加されなくなります。

上記から、このマルチテナント プラグインは、SQL を解析し、マルチテナント ID フィルター条件を結合することで、実際に SQL 拡張とデータ分離を実装していることがわかります。 SQL を解析するためのフレームワークは JSqlParser と呼ばれ、公式ドキュメントは https://github.com/JSQLParser/JSqlParser/wiki です。以前、Druid による動的 SQL の解析に関する記事をまとめました。 Druid は SQL を解析することもできます。 SQL ステートメントが構文ツリーを生成することは誰もが知っています。どちらが SQL 解析 (特に複雑な SQL) に優れているかは不明です。自分で検証・比較することができます。ここでは、JSqlParser が誤って解析するケースを示します。上記のSQL文user_role rをuser_role urに変更します。

 select u.* from user u left join user_role ur on u.id = ur.user_id

上記のように getUserList() を呼び出すと、解析エラーが発生します。

 Caused by: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: select u.* from user u left join user_role ur on u.id = ur.user_id group by u.id order by u.id desc at com.baomidou.mybatisplus.core.toolkit.ExceptionUtils.mpe(ExceptionUtils.java:39) at com.baomidou.mybatisplus.extension.parser.JsqlParserSupport.parserSingle(JsqlParserSupport.java:52) at com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.beforeQuery(TenantLineInnerInterceptor.java:65) at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:78) at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62) at com.sun.proxy.$Proxy178.query(Unknown Source) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151) ... 101 common frames omitted Caused by: net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: "ur" <K_ISOLATION> at line 2, column 29.

私は公式の mybatis-plus: https://github.com/baomidou/mybatis-plus/issues/5086 で問題を提起しましたが、公式のメンテナーはすぐに、これは mybatis-plus の問題ではなく、JSqlParser の解析の問題であると返答しました~~~。提案されたのは、エイリアス ur を別のものに変更するか、JSqlParser の最新バージョンにアップグレードすることです。

4. まとめ

ここまで、マルチテナント システムのデータ分離の実装スキーム、アーキテクチャ設計、およびグローバル操作データ分離をエレガントに実装する方法について説明しました。同時に、mybati-plus マルチテナント プラグインの実装原理とソース コード プロセス ルーチンについても簡単に分析し、実際のアプリケーション事例を引用して関連する詳細について説明しました。もちろん、データのアクセス許可はテナント (会社) レベルだけに留まりません。ほとんどのシステムのデータ権限は、ビジネス組織構造の役割に応じて制御されます。データの権限は、ロールに基づいてメニューの権限を決定することと同じです。データ権限は通常、会社の業務に関連しており、よりパーソナライズされているため、各会社の業務組織構造は異なります。そのため、実際の開発プロジェクトのデータ権限分離は、実際のニーズに応じて変更する必要がありますが、一般的には、マルチテナント分離の実装方法を模倣できます。たとえば、業務システムの組織構造には会社 (org_id) があり、会社には複数の部門 (dept_id) があり、部門には複数のチームグループ (team_id) があり、チームには複数の人員 (user_id) があります。異なる役割では異なるデータのみを表示できます。部門マネージャーは自分の部門のデータのみを表示でき、チームリーダーは自分のチームのデータのみを表示できます。これらの実装ロジック ルーチンは、マルチテナント プラグイン方式を模倣することでエレガントに実装できます。これも後で時間ができたら勉強したいことです。データ権限の別の実装計画の概要は後ほど公開します。

<<:  クラウドコンピューティングの未来を明らかにする

>>:  バックアップデータをオンプレミス環境に移行するためのベストプラクティス

推薦する

dedicube-$50/E3-1240v2/16g メモリ/1T ハードディスク/10T トラフィック/G ポート

Dedicubeは設立してまだ半年も経っていないサーバーレンタル会社です。1ヶ月前にサイトにカウント...

digitalocean-20ドルを送金/最もコスト効率が高く信頼性の高い米国VPS

digitalocean は割引コード SSDCHP20 で 20 ドルをプレゼントしています。これ...

#クリスマス/元旦# spinservers: 米国無制限トラフィックサーバー、59ドルから、1Gbps帯域幅、E3-1280v5/32gメモリ/1T NVMe

spinservers はクリスマスと元旦のプロモーションを開始しました。米国西海岸のシリコンバレー...

新華網コメント:ウェブサイトは法律に従って「土地の守護者」になるべきだ

最近、インターネット上ではネット上の噂が横行している。インターネット上の噂は、広範囲に及び影響力を持...

2018 城外泉小紅書プロモーションの秘訣 - 中秋節

月収10万元の起業の夢を実現するミニプログラム起業支援プラン成外泉の中秋節小紅書プロモーションのヒン...

ソーシャルメディアトレンドレポート!

このレポートでは、ソーシャル メディアのインタラクティブな参加、ソーシャル メディアの見通し、ソーシ...

ランキングを獲得するために小説サイトを最適化する方法

現在、多くの友人が新しいウェブサイトを使用しています。新しいウェブサイトの利点は明らかです。PVが高...

Tuanbao.com が「賃金未払いスキャンダル」に巻き込まれる: 共同購入サイトの変革は避けられない

国内の共同購入サイトの数は2010年8月以降1,000サイトを突破した。非合理的な発展により、201...

Zookeeper が分散ロックとして誕生したのはなぜですか?

分散ロックとは何ですか?分散ロックは、分散システム間の共有リソースへの同期アクセスを制御する方法です...

Google アナリティクスの新しいユーザー インターフェース分析の最近の改善点

今日、GA の新しいユーザー インターフェイスに皆さんが注目したと思います。以前と比較すると、アイコ...

SEOの時代遅れの理論についての私の意見:変わらない理論に固執することはできない

A5 の Web サイトで「SEO は終わり、ソーシャル リアルタイム コンテンツが流行っている」と...

Dell、エッジコンピューティングからコアデータセンター、クラウドコンピューティングまでVMwareソリューションポートフォリオを拡大

Dell は本日、エッジ コンピューティングからコア データ センター、クラウド コンピューティング...

ソーシャル メディア マーケティング 2.0: 画像と動画でブランドをアピール

Facebook、Instagram、Pinterest などのソーシャル メディア サイトが 20...

Linode Windows VM への「ローカル」アクセスを取得する方法

ほとんどの場合、 IT の運用と保守作業はリモートで実行できます。管理対象システムにネットワーク経由...

実践的な操作:ダウングレードしたウェブサイトを復活させる方法

この記事は私が最近遭遇した実際のケースです。私たちのケース処理方法に従ってプロセス全体を記録し、皆さ...