開発プロセスではトランザクションの失敗という問題に遭遇することがありますので、開発中に特に注意する必要がありますが、以下にトランザクションが有効にならないシナリオをまとめ、自分自身を思い出させます。
問題は通常、いくつかのカテゴリに分類されます。
- 構成の問題
- Spring AOPプロキシの問題
- 基礎となるデータベースはトランザクションの問題をサポートしていません
- @トランザクションの設定ミス
- 開発中のエラー
1. 構成の問題
1. サービスクラスは Spring によって管理されていません
//@Service
public class TransactionalUserServiceImpl implements TransactionalUserService {
@Resource
TransactionalUserMapper transactionalUserMapper;
@Override
@Transactional
public void transactionalError() throws Exception {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效,修改成功");
transactionalUser.setId("1");
transactionalUserMapper.updateById(transactionalUser);
throw new Exception();
}
}
- 失敗の理由: @Service アノテーション アノテーションの後、Spring トランザクション (@Transactional) は有効になりません。これは、Spring トランザクションが AOP メカニズムによって実装されるためです。つまり、Bean が Spring IOC コンテナーから取得されるとき、 Spring は、トランザクションをサポートするターゲット クラスのプロキシを作成します。ただし、 @Service にアノテーションが付けられると、サービス クラスは Spring によって管理されなくなるため、プロキシを作成できなくなります。
- 解決策: @service アノテーションを追加しますが、通常はこの種の間違いを犯すことはありません。
2. トランザクション マネージャーが有効になっていない
- @Transactional を単独で使用しても、トランザクションを開始するにはトランザクション マネージャーを構成する必要があるため、有効になりません。
- 解決策: springboot プロジェクトのスタートアップ クラスを追加する必要があります: @EnableTransactionManagement。もちろん、新しいバージョンではトランザクション マネージャーが自動的に構成され、デフォルトでトランザクション サポートが有効になります。
2. Spring AOP プロキシの問題
1. メソッドは、final キーワードと static キーワードによって変更されます
@Transactional
public final void add(Addreq req) {
//保存实体数据库记录
addMapper.save(req);
//保存流水数据库记录
addFlowMapper.saveFlow(buildFlowById(req));
}
- 理由: メソッドが Final または static として宣言されている場合、そのメソッドはサブクラスによってオーバーライドできません。つまり、メソッドに対して動的プロキシを実行できません。そのため、Spring はトランザクションを管理するためのトランザクション プロキシ オブジェクトの生成に失敗します。
- 解決策: 追加トランザクション メソッドを Final または static で変更しないでください。
2. 同じクラス内で、メソッドが内部的に呼び出されます。
@Override
public void transactionalErrorV4() {
this.transactionalUser();
}
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void transactionalUser() {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
}
- 理由: トランザクションは Spring AOP プロキシを通じて実装されており、同じクラス内で、あるメソッドが別のメソッドを呼び出すと、呼び出し側メソッドはプロキシ クラスを介して呼び出すのではなく、ターゲット メソッドのコードを直接呼び出します。つまり、上記のコードでは、ターゲットのtransactionalUserメソッドの呼び出しはプロキシクラスを介して実行されないため、トランザクションは有効になりません。
- 解決:
- もう 1 つのクラスを作成して、これら 2 つのメソッドを分離し、異なるクラスに含めることができます。
- @Transactional をメイン メソッドに追加して、トランザクションを使用することもできます。
3. メソッドのアクセス権限は非公開です
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
private void transactionalUser() {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
}
- 理由: Spring トランザクション メソッドtransactionalUser のアクセス権限はパブリックではないため、トランザクションは有効になりません。 Spring トランザクションは AOP メカニズムによって実装されており、AOP メカニズムの本質は動的プロキシであり、トランザクション メソッドがプロキシがパブリックではない場合、computeTransactionAttribute() は null を返します。つまり、現時点ではトランザクション属性が存在しません。AbstractFallbackTransactionAttributeSource のソース コードを確認できます。
3. 基礎となるデータベースはトランザクションの問題をサポートしていません
Spring トランザクションの最下層は依然としてデータベース自体のトランザクション サポートに依存しています。MySQL では、MyISAM ストレージ エンジンはトランザクションをサポートしませんが、InnoDB エンジンはトランザクションをサポートします。したがって、開発段階でテーブルを設計するときは、選択したストレージ エンジンがトランザクションをサポートしていることを確認してください。
4. @トランザクション構成エラー
1. 読み取り専用トランザクションとして構成
@Transactional(readOnly = true)
public void updateUser(User user) {
userDao.updateUser(user);
}
- 理由: @Transactional 注釈が使用されていますが、注釈の readOnly=true 属性は、これが読み取り専用トランザクションであることを示しているため、User エンティティが更新されると例外がスローされます。
- 解決策: readOnly 属性を false に設定するか、@Transactional アノテーションの readOnly 属性を削除します。
2. トランザクションタイムアウト設定が短すぎます
@Transactional(timeout = 1)
public void doSomething() {
}
- 理由: 上記の例では、タイムアウト属性が 1 秒に設定されています。これは、トランザクションが 1 秒以内に完了できない場合、トランザクションはタイムアウトとして報告されることを意味します。
3. 間違ったトランザクション伝播メカニズムの使用
//以非事务方式执行操作,如果当前存在一个事务,则将该事务挂起。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
Spring が 7 つのトランザクション伝播メカニズムを提供するという一般的な科学は次のとおりです。
- REQUIRED (デフォルト): トランザクションが現在存在する場合はトランザクションに参加し、それ以外の場合は新しいトランザクションを作成します。この伝播レベルは、メソッドがトランザクション内で実行される必要があることを示します。
- サポート: トランザクションが現在存在する場合は、トランザクションに参加します。それ以外の場合は、非トランザクション方式で実行を継続します。
- 必須: トランザクションが現在存在する場合はトランザクションに参加し、存在しない場合は例外をスローします。
- REQUIRES_NEW: 新しいトランザクションを作成し、トランザクションが存在する場合はトランザクションを一時停止します。
- NOT_SUPPORTED: 操作は非トランザクション方式で実行され、現在のトランザクションがある場合、トランザクションは一時停止されます。
- NEVER: 操作は非トランザクション方式で実行され、トランザクションが現在存在する場合は例外がスローされます。
- NESTED: トランザクションが現在存在する場合、そのトランザクションはネストされたトランザクション内で実行されます。トランザクションがない場合、トランザクションは REQUIRED 伝播レベルで実行されます。ネストされたトランザクションは外部トランザクションの一部であり、外部トランザクションがコミットまたはロールバックするときに部分的にコミットまたはロールバックできます。
4. RollbackFor プロパティ設定エラー
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Error.class)
public void transactionalErrorV6() throws Exception {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("3");
transactionalUserMapper.updateById(transactionalUser);
throw new Exception();
}
- 理由: 実際、rollbackFor 属性で指定される例外は Throwable またはそのサブクラスである必要があります。デフォルトでは、例外とエラーの両方が自動的にロールバックされます。ただし、上記のコード例では、rollbackFor = Error.class が指定されていますが、スローされる例外は Exception であるため、トランザクションは有効になりません。
- 解決策: rollbackFor 属性で指定された例外は、スローされた例外と一致する必要があります。
5. 不正使用
1. トランザクションのアノテーションが上書きされ、トランザクションが失敗します。
public interface A{
@Transactional
void save(String data);
}
public class AImpl implements A{
@Override
public void save(String data) {
// 数据库操作
}
}
public class BService {
@Autowired
private A A;
@Transactional
public void doSomething(String data) {
A.save(data);
}
}
public class CService extends BService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomething(String data) {
super.doSomething(data);
}
}
- 理由:
- CService は BService のサブクラスであり、doSomething() メソッドをオーバーライドします。
- このメソッドでは、別の伝播動作 (REQUIRES_NEW) を使用して、親クラスの @Transactional アノテーションをオーバーライドします。
- この場合、CService の doSomething() メソッドが呼び出されると、サブクラスのメソッドのアノテーションが親クラスのアノテーションをオーバーライドするため、Spring フレームワークは親クラスのメソッドでトランザクションを開始しません。
- したがって、クラス A の save() メソッドが呼び出された場合、トランザクションは開始されず、ロールバックされません。
- A の save() メソッドで実行されたデータベース操作がロールバックされないため、データの不整合が発生します。
- 解決策: 通常、ビジネス コールのリンクが深すぎることはお勧めできません。また、問題のトラブルシューティングが容易ではありません。
2. ネストされたトランザクション
開発では複数の保存操作を行うことがありますが、すべてをロールバックする必要がない場合もありますが、特別な処理がなければ問題があればロールバックされます。
@Service
public class AService {
@Autowired
private BService bService;
@Autowired
private AMapper aMapper ;
@Transactional
public void addOrder(Order order) throws Exception {
aMapper.save(order);
bService.saveFlow(order);
}
}
@Service
public class BService {
@Autowired
private BMapper bMapper;
@Transactional(propagation = Propagation.NESTED)
public void saveFlow(Order order) {
bMapper.save(order);
throw new RuntimeException();
}
}
- 理由: 上記のコードはネストされたトランザクションを使用しているため、saveFlow で実行時例外が発生すると、外側の addOrder メソッドに上向きにスローされ続け、aMapper.save(order); がロールバックされます。
- 解決策: 内部のネストされたトランザクションの影響を受けたくない場合は、次のように try-catch でラップします。
@Transactional
public void addOrder(Order order) throws Exception {
aMapper.save(order);
try {
bService.saveFlow(order);
} catch (Exception e) {
log.error("savefail,message:{}",e.getMessage());
}
}
3. トランザクションのマルチスレッド呼び出し
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void transactionalAsV11() {
new Thread(() -> {
try {
transactionalUser();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
public void transactionalUser() throws Exception {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new Exception();
}
- 理由: Spring トランザクションはスレッド バインディングに基づいており、各スレッドには独自のトランザクション コンテキストがあり、マルチスレッド環境では同じトランザクション コンテキストを共有する複数のスレッドが存在する可能性があり、その結果トランザクションが有効にならないためです。Spring トランザクション マネージャーは、スレッド ローカル変数 (ThreadLocal) を使用してスレッド セーフを実現します。
- 解決策: TransactionAspectSupport を通じてロールバックを手動で設定します。
public void transactionalUser() throws Exception {
//设置回滚点,只回滚以下异常
Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
try {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new Exception();
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
}
}
もう 1 つの方法は、springboot のスレッド アノテーションを使用することです: @Async
@Override
@Async("MyExecutor")
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = RuntimeException.class)
public void transactionalAsV12() throws InterruptedException {
Thread.sleep(10000);
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new RuntimeException();
}
上記で記述したトランザクションは有効ですが、アノテーションを使用する場合はいくつか注意点があります。
以下のメソッドは @Async を無効にしてしまいます。
- 非同期メソッドは静的装飾を使用します
- 非同期クラスは @Component アノテーション (または他のアノテーション) を使用しないため、Spring が非同期クラスのスキャンに失敗します。
- 非同期メソッドは、呼び出された非同期メソッドと同じクラスに含めることはできません
- クラスには @Autowired や @Resource などのアノテーションを自動的に挿入する必要があり、手動でオブジェクトを自分で新規作成することはできません。
- SpringBoot フレームワークを使用する場合は、スタートアップ クラスに @EnableAsync アノテーションを追加する必要があります
4. 例外はスローされない
@Transactional
public void addOrder(Order order) {
try {
aMapper.save(order);
bFlowMapper.saveFlow(order);
} catch (Exception e) {
log.error("add error,id:{},message:{}", order.getId(),e.getMessage());
}
}
- 理由: トランザクション内の例外はビジネス コードによって捕捉されて処理されましたが、トランザクション マネージャーに正しく伝播されていないため、トランザクションをロールバックできません。Spring のソース コード (TransactionAspectSupport クラス) から答えを見つけることができます
。invokeWithinTransaction メソッドでは、Spring が Throwable 例外をキャッチすると、completeTransactionAfterThrowing() メソッドを呼び出してトランザクション ロールバック ロジックを実行します。ただし、addOrder トランザクションメソッドでは例外を直接キャッチして再スローしないため、当然 Spring は例外をキャッチできず、トランザクションのロールバックのロジックが実行されず、トランザクションは無効になります。
解決策: 例外をスローするだけです
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = RuntimeException.class)
public void transactionalAsV13() {
try {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new RuntimeException();
} catch (RuntimeException e) {
System.out.println("吞并异常");
throw new RuntimeException();
}
}