大きなビジネス上の問題
解決
@Transactional アノテーションの使用を減らし、プログラムによるトランザクション TransactionTemplate を使用する
実際のプロジェクト開発では、ビジネスメソッドに @Transactional アノテーションを付与してトランザクション機能を有効にすることが一般的であり、これを宣言型トランザクションと呼びます。
コードの一部は次のとおりです。
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
doSameThing...
}
@Transactional アノテーションの使用を減らす理由。
@Transactional アノテーションが Spring の AOP を通じて機能することはわかっていますが、不適切に使用するとトランザクション関数が失敗する可能性があります。経験が浅い場合、この種の問題のトラブルシューティングは簡単ではありません。トランザクションが失敗する状況については、
Spring トランザクションが有効にならないさまざまなシナリオを参照してください。
通常、@Transactional アノテーションは特定のビジネス メソッドに追加され、ビジネス メソッド全体が同じトランザクション内に存在します。トランザクションの範囲を制御するには粒度が粗すぎることが、大規模なトランザクションの問題の最も一般的な原因です。
だから何をすべきか?
プログラムによるトランザクションを使用すると、Spring プロジェクトの TransactionTemplate クラスのオブジェクトを使用してトランザクションを手動で実行できます。
コードの一部は次のとおりです。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
doSameThing...
return Boolean.TRUE;
})
}
上記のコードからわかるように、TransactionTemplate のプログラムによるトランザクション機能を使用してトランザクションの範囲を柔軟に制御することが、大規模なトランザクションの問題を回避するための最初の選択肢です。
もちろん、プロジェクト内の一部のビジネス ロジックが比較的単純で、頻繁に変更されない場合は、@Transactional アノテーションを使用してトランザクションをオープンしても、その方がシンプルで開発効率が高いため問題ありませんが、トランザクションの失敗の問題に注意する必要があります。
トランザクションの実行を必要としないメソッドを整理する
クエリ (選択) メソッドをトランザクションの外に置く
たとえば、次のコードが表示されます。
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
queryData1();
queryData2();
addData1();
updateData2();
}
2 つのクエリ メソッド queryData1 および queryData2 はトランザクションの外部で実行でき、トランザクション内で実際に実行する必要があるコード (addData1 メソッドや updateData2 メソッドなど) をトランザクション内に配置できるため、トランザクションの粒度を細かく設定できます。効果的に削減されます。
TransactionTemplate のプログラム トランザクションを使用する場合、ここでの変更は非常に簡単です。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
しかし、本当に @Transactional アノテーションを使用したい場合は、どのように分割すればよいでしょうか?
public void save(User user) {
queryData1();
queryData2();
doSave();
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
この例は非常に典型的なエラーです。@Transactional によって注釈が付けられた宣言トランザクションは Spring AOP を通じて機能し、Spring AOP はプロキシ オブジェクトを生成する必要があり、ダイレクト メソッド呼び出しは引き続き使用されるため、このダイレクト メソッド呼び出しトランザクションは有効になりません。元のオブジェクトなので、トランザクションは有効になりません。
これを回避する方法はありますか?
1. 新しい Service メソッドを追加します
この方法は非常に簡単で、新しい Service メソッドを追加し、その新しい Service メソッドに @Transactional アノテーションを追加し、トランザクションの実行が必要なコードを新しいメソッドに移動するだけです。具体的なコードは次のとおりです。
@Servcie
publicclass ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
publicclass ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
2. Service クラスに自分自身を挿入します。
新しい Service クラスを追加したくない場合は、自分自身を Service クラスに注入することもできます。具体的なコードは次のとおりです。
@Servcie
publicclass ServiceA {
@Autowired
prvate ServiceA serviceA;
public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
Spring IOC 内の 3 レベルのキャッシュがそれを保証しており、循環依存の問題は発生しません。
3. Service クラスの AopContext.currentProxy() を使用してプロキシ オブジェクトを取得します
上記の方法 2 は確かに問題を解決できますが、コードは直感的ではありません。Service クラスで AOPProxy を使用してプロキシ オブジェクトを取得することで同じ機能を実現できます。具体的なコードは次のとおりです。
@Servcie
publicclass ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
非コアメソッドはトランザクションの外部に配置されます
トランザクションを使用する前に、すべてのデータベース操作をトランザクションで実行する必要があるかどうかを検討する必要があります。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
addData();
addLog();
updateCount();
return Boolean.TRUE;
})
}
上記の例では、実際には、操作ログや統計量を扱う業務ではデータ量が少ないため、操作ログを増やすaddLogメソッドや統計量を更新するupdateCountメソッドをトランザクション内で実行することができません。矛盾。
MQによる非同期処理
トランザクション内のすべてのメソッドは同期的に実行する必要がありますか? メソッドの同期実行では、メソッドが返されるまで待機する必要があることは誰もが知っていますが、トランザクション内に同期実行メソッドが多すぎると、必然的に待ち時間が長くなりすぎてトランザクションの問題が大きくなります。上記の addLog は操作ログメソッドと updateCount 統計更新メソッドを追加しており、MQ 経由で非同期に処理できます。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
addData();
return Boolean.TRUE;
})
sendMq();
}
トランザクションでのリモート呼び出しを避ける
インターフェース内で他のシステムのインターフェースを呼び出すことは避けられません。ネットワークが不安定なため、このリモート呼び出しの応答時間は比較的長くなる可能性があります。リモート呼び出しのコードをあるものに配置すると、このものは大きな取引になるかもしれません。もちろん、リモート呼び出しにはインターフェイスの呼び出しだけを指すのではなく、MQ メッセージの送信や、データを保存するための Redis や mongodb への接続なども含まれます。
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
callRemoteApi();
addData1();
}
リモート呼び出しのコードは時間がかかる場合があるため、必ずトランザクションの外にコードを配置してください。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
callRemoteApi();
transactionTemplate.execute((status) => {
addData1();
return Boolean.TRUE;
})
}
友人の中には、リモートで呼び出されたコードがトランザクションに配置されていない場合に、データの一貫性をどのように確保すればよいのかと尋ねる人もいるかもしれません。これには、最終的なデータの一貫性を達成するために、再試行と補償のメカニズムを確立する必要があります。
トランザクションで一度に大量のデータを処理しないようにする
トランザクションで処理する必要があるデータが多すぎると、トランザクションに大きな問題が発生することになります。たとえば、操作の便宜のために、一度に 1,000 個のデータをバッチで更新する場合があります。これにより、特に同時実行性の高いシステムでは、大量のデータ ロックが待機することになります。
解決策はページング処理です。1,000 個のデータを 50 ページに分割し、一度に処理されるデータは 20 個だけなので、大規模なトランザクションの発生を大幅に減らすことができます。