くそー、なぜ私のビジネスが有効にならないのですか?

序文

普段は誰もが事件について書くべきで、以前は事件を書いていたときに落とし穴に出くわしましたが、効果がありませんでした。その後、様々な事件の失敗のシナリオを確認・見直しました。恐れることはありません。それでは、最初にトランザクションに関する知識を確認しましょう。トランザクションとは、操作の最小の作業単位を指します。単一の分割できない単位操作として、すべてが成功するか、すべてが失敗します。トランザクションには4つの特性があります(ACID):

  • Atomicity(Atomicity):トランザクションに含まれる操作は、すべて成功するか、すべて失敗してロールバックされ、半分の成功と半分の失敗の中間状態はありません。たとえばAB最初に500人民元を持っているA場合、B送金する場合、お金が少ない100場合はより多くのお金を持っている必要があります。お金を失うことはできません。お金を受け取らない場合、お金は消えて、原子性に適合しなくなります。A100B100AB
  • 整合性(Consistency):整合性とは、トランザクションが実行される前後の状態全体の整合性を維持することを指します。たとえば、最初に人民元がAあり、合計が人民元です。これは前の状態です。転送の場合、次に、最後はい、2つです。合計しても、この全体的な状態を保証する必要があります。B5001000AB100A400B6001000
  • 分離(Isolation):最初の2つの機能は同じトランザクション用ですが、分離は異なるトランザクションを指します。複数のトランザクションが同じデータを同時に操作する場合、異なるトランザクションと同時実行性の間の影響を分離する必要があります。実行されたトランザクションは禁止されています。互いに干渉します。
  • 永続性(Durability):トランザクションがコミットされると、データベースへの変更は永続的です。データベースに障害が発生した場合でも、発生した変更が存在している必要があります。

トランザクションのいくつかの特徴は、データベーストランザクションに限定されません。広い意味で、トランザクションは、動作メカニズムであり、同時実行制御の基本単位です。これは、操作の結果を保証し、分散トランザクションなども含みますが、一般的にはトランザクションについて。特に言及しない場合は、データベースに関連しています。これは、私たちが通常話すトランザクションは、基本的にデータベースに基づいて完了するためです。

トランザクションはデータベースだけのものではありません。この概念を、キューサービスや外部システム状態などの他のコンポーネントに拡張できます。したがって、「一連のデータ操作ステートメントは、システムを一貫した状態に保つために、完了するか、完全に失敗する必要があります」

テスト環境

すでにいくつかのデモプロジェクトをデプロイし、Dockerを使用して環境をすばやく構築しました。この記事も以前の環境に基づいています。

  • JDK 1.8
  • Maven 3.6
  • Docker
  • Mysql

通常のトランザクションロールバックの例

通常のトランザクションの例には2つのインターフェースが含まれています。1つはすべてのユーザーのデータを取得するためのもので、もう1つはupdateユーザーデータを更新するためのものです。これは実際には各ユーザーの年齢です+1。例外として、最終結果を見てください。

@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource
    UserMapper userMapper;

    @Autowired
    RedisUtil redisUtil;

    @Override
    public List<User> getAllUsers() {
        List<User> users = userMapper.getAllUsers();
        return users;
    }

    @Override
    @Transactional
    public void updateUserAge() {
        userMapper.updateUserAge(1);
        int i= 1/0;
        userMapper.updateUserAge(2);
    }
}

データベース操作:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.aphysia.springdocker.mapper.UserMapper">
    <select id="getAllUsers" resultType="com.aphysia.springdocker.model.User">
        SELECT * FROM user
    </select>

    <update id="updateUserAge" parameterType="java.lang.Integer">
        update user set age=age+1 where id =#{id}
    </update>
</mapper>

まず、http://localhost:8081/getUserListすべてのユーザーを取得して、以下を確認します。

画像-20211124233731699

更新インターフェイスを呼び出すと、ページはエラーをスローします。

画像-20211124233938596

コンソールは例外もスローします。これは、0による除算、例外を意味します。

java.lang.ArithmeticException: / by zero
	at com.aphysia.springdocker.service.impl.UserServiceImpl.updateUserAge(UserServiceImpl.java:35) ~[classes/:na]
	at com.aphysia.springdocker.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$c8cc4526.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.12.jar:5.3.12]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.12.jar:5.3.12]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.12.jar:5.3.12]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.12.jar:5.3.12]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.12.jar:5.3.12]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.12.jar:5.3.12]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.12.jar:5.3.12]
	at com.aphysia.springdocker.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$25070cf0.updateUserAge(<generated>) ~[classes/:na]

次に、もう一度リクエストしhttp://localhost:8081/getUserListて、両方の11データがデータが変更されていないことを示していることを確認します。最初の操作が完了した後、それは異常であり、ロールバックは成功します。

[{"id":1,"name":"李四","age":11},{"id":2,"name":"王五","age":11}]

トランザクションが異常にロールバックされるのはいつですか?そして、私に詳しく聞いてください:

実験

1.エンジン設定が正しくない

サポートされているデータエンジンを表示するために使用Mysqlできるデータベースエンジンの概念が実際にあることを私たちは知っています。show enginesMysql

画像-20211124234913121

Transactionsその列、つまりトランザクションサポートのみInnoDB、つまりトランザクションのみをサポートしていることがわかりますInnoDB。したがって、エンジンが他のトランザクションに設定されている場合、その列は無効になります。

デフォルトのデータベースエンジンを使用show variables like 'default_storage_engine'して、デフォルトがInnoDB次のようになっていることを確認できます。

mysql> show variables like 'default_storage_engine';
+------------------------+--------+
| Variable_name          | Value  |
+------------------------+--------+
| default_storage_engine | InnoDB |
+------------------------+--------+

次に、デモンストレーションしたデータテーブルも使用されているかどうかをInnoDB確認します。実際に使用されていることがわかります。InnoDB

画像-20211124235353205

では、テーブルのエンジンをMyISAMこのようなものに変更するとどうなるでしょうか。ここでは、データテーブルのデータエンジンのみを変更してみてください。

mysql> ALTER TABLE user ENGINE=MyISAM;
Query OK, 2 rows affected (0.06 sec)
Records: 2  Duplicates: 0  Warnings: 0

それからupdate、当然のことながら、それでもエラーがスローされますが、これは何の違いもないようです。

画像-20211125000554928

ただし、すべてのデータが取得されると、最初のデータ更新は成功し、2番目のデータは正常に更新されないため、トランザクションは有効になりません。

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

結論:InnoDBトランザクションを有効にするには、エンジンに設定する必要があります。

2.メソッドをプライベートにすることはできません

トランザクションはメソッドである必要がありますpublic。メソッドで使用される場合private、トランザクションは自動的に失敗しますが、IDEAそれを記述している限り、エラーが報告されますMethods annotated with '@Transactional' must be overrideable。これは、トランザクションのアノテーションによってメソッドが追加されたことを意味します。書き換えが必要です。privateメソッドは書き換えできないため、エラーが報告されます。

画像-20211125083648166

同じ変更されたメソッドに注釈が付けられている場合、書き直されたくないfinalため、エラーも報告されます。final

画像-20211126084347611

Spring主にBean放射線によって得られたアノテーション情報を使用し、動的プロキシ技術を使用してトランザクション全体をカプセル化します。理論的には、メソッドの呼び出しに問題はないAOPと思います。メソッドレベルで使用できますが、チームはこの方法は開発者の意志であると感じてください。公開されたくないインターフェイスのカプセル化を破棄する必要はありません。これは、混乱を招きやすいものです。privatemethod.setAccessible(true);Springprivate

Protectedその方法は可能ですか?できません!

次に、実現するために、インターフェースが使用できないため、コード構造を魔法のように変更しますPortected。インターフェースを使用すると、protectedメソッドを使用できなくなり、エラーが直接報告されるため、で使用する必要があります。同じパッケージ。controller合計をservice同じパッケージに入れます。

画像-20211125090358299

テスト後、トランザクションが有効になっておらず、結果は1つの更新であり、もう1つの更新は更新されていないことがわかりました。

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

結論:publicメソッドではなく、メソッドでprivate使用する必要があります。そうしないとfinalstatic有効になりません。

3.例外は実行時例外である必要があります

Springboot例外を管理する場合、実行時例外(RuntimeExceptionおよびそのサブクラス)のみがロールバックされます。たとえば、前に説明したようi=1/0;に、実行時例外が生成されます。

ソースコードから、メソッドが例外であるrollbackOn(ex)であるかを判断することもわかります。RuntimeExceptionError

	public boolean rollbackOn(Throwable ex) {
		return (ex instanceof RuntimeException || ex instanceof Error);
	}

例外は主に次のタイプに分類されます。

すべての例外はエラーメッセージですがThrowable一般に、そのようなファイルがない、メモリオーバーフロー、突然のエラーなど、プログラムで制御できないエラーが発生しています。一方、を除いて、他のすべては処理できる例外であり、プログラムはこの例外が書き込まれるときに処理する必要があります。そうしないと、コンパイルはパスしません。ErrorIOExceptionRuntimeExceptionCheckExceptionJava

次の図からわかるように、いくつかの一般的なIO例外CheckedExceptionをリストしました。このメソッドは見つかりませんでした。このクラスは見つかりませんでしたが、いくつかの一般的な例外があります。IOExceptionNoSuchMethodExceptionClassNotFoundExceptionRunTimeException

  • 配列の範囲外の例外:IndexOutOfBoundsException
  • 型変換の例外:ClassCastException
  • ヌルポインタ例外:NullPointerException

トランザクションのデフォルトのロールバックは次のとおりです。ランタイム例外。つまりRunTimeException、次のコードなど、他の例外がスローされた場合、トランザクションは失敗します。

    @Transactional
     public void updateUserAge() throws Exception{
        userMapper.updateUserAge(1);
        try{
            int i = 1/0;
        }catch (Exception ex){
            throw new IOException("IO异常");
        }
        userMapper.updateUserAge(2);
    }

4.不適切な構成が原因

  1. @Transactionalトランザクションを開始するには、メソッドを使用する必要があります
  2. 複数のデータソースまたは複数のトランザクションマネージャーを構成する場合は、データベースを操作するAと使用できないトランザクションに注意してBください。この問題は非常に単純ですが、誤った使用で問題を見つけるのが難しい場合があります。
  3. の場合、トランザクションを開くSpringように構成する必要があります。これは構成ファイル@EnableTransactionManagementと同等ですが、では不要になり注釈に注釈が含まれているため、自動的に挿入されます。xml*<tx:annotation-driven/>*SpringbootspringbootSpringBootApplication@EnableAutoConfiguration

@EnableAutoConfiguration何が自動的に注入されますか?以下の下jetbrains://idea/navigate/reference?project=springDocker&path=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.6/spring-boot-autoconfigure-2.5.6.jar!/META-INF/spring.factoriesに自動注入された構成があります:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
...

その中に構成されているTransactionAutoConfigurationのは、トランザクション自動構成クラスです。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
		DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {
  ...
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBean(TransactionManager.class)
	@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
	public static class EnableTransactionManagementConfiguration {

		@Configuration(proxyBeanMethods = false)
		@EnableTransactionManagement(proxyTargetClass = false)   // 这里开启了事务
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
		public static class JdkDynamicAutoProxyConfiguration {

		}
    ...

	}

}

@Transactionalメソッドに使用されるだけでなく、クラスにも使用できることは注目に値しpublicます。これは、このクラスのすべてのメソッドがトランザクションを構成することを示しています。

5.トランザクションメソッドを同じクラスで呼び出すことはできません

トランザクション管理を実行したいメソッドは、現在のクラスではなく、他のクラスでのみ呼び出すことができます。そうでない場合は無効になります。この目的を達成するために、同じクラスに多くのトランザクションメソッドや他のメソッドがある場合、今回は後継者が同じクラスにトランザクションメソッドを書き込んだときに、階層化がより明確になり、混乱を避けるために、トランザクションクラスを抽出する必要があります。

トランザクションの失敗の例:

たとえば、serviceトランザクションメソッドを次のように変更します。

    public void testTransaction(){
        updateUserAge();
    }

    @Transactional
     public void updateUserAge(){
        userMapper.updateUserAge(1);
        int i = 1/0;
        userMapper.updateUserAge(2);
    }

そのcontroller中で、トランザクションアノテーションのないメソッドが呼び出され、次にトランザクションメソッドが間接的に呼び出されます。

    @RequestMapping("/update")
    @ResponseBody
    public int update() throws Exception{
        userService.testTransaction();
        return 1;
    }

呼び出し後、トランザクションが無効であり、一方が更新され、もう一方が更新されていないことがわかります。

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

なんでそうなの?

Springメソッドはアスペクトでラップされ、外部呼び出しメソッドのみがインターセプトされ、内部メソッドはインターセプトされません。

ソースコードを見てください。実際、transactionメソッドを呼び出すと、DynamicAdvisedInterceptor入力するメソッドは次のようになりpublic Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)()ます。

画像-20211128125711187

これは内部AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()で呼び出され、ここに呼び出しを取得するための呼び出しチェーンがあります。@TransactionalアノテーションのないメソッドuserService.testTransaction()の場合、プロキシ呼び出しチェーンはまったく取得できず、元のクラスのメソッドは引き続き呼び出されます。

springメソッドをプロキシする場合は、プロキシをaop使用する必要があるメソッドまたはクラスを識別するために識別子を使用する必要があります。これはポイントカットとしてspring定義され、この識別子を定義するとプロキシされます。@Transactional

エージェントになる時期はいつですか?

Spring管理が一元化されておりbean、プロキシのタイミングは当然bean作成の過程です。どのクラスにこのロゴがあるかを確認するために、プロキシオブジェクトが生成されます。

SpringTransactionAnnotationParserこのクラスにはTransactionAttribute、アノテーションを決定するために使用されるメソッドがあります。

	@Override
	@Nullable
	public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
		AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
				element, Transactional.class, false, false);
		if (attributes != null) {
			return parseTransactionAnnotation(attributes);
		}
		else {
			return null;
		}
  }

6.マルチスレッドでのトランザクションの失敗

マルチスレッドで次のようにトランザクションを使用すると、トランザクションを正常にロールバックできません。

    @Transactional
    public void updateUserAge() {
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        userMapper.updateUserAge(1);
                    }
                }
        ).start();
        int i = 1 / 0;
        userMapper.updateUserAge(2);
    }

異なるスレッドは異なる接続を使用するためSqlSession、別の接続と同等であるため、同じトランザクションはまったく使用されません。

2021-11-28 14:06:59.852 DEBUG 52764 --- [       Thread-2] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2021-11-28 14:06:59.930 DEBUG 52764 --- [       Thread-2] c.a.s.mapper.UserMapper.updateUserAge    : <==    Updates: 1
2021-11-28 14:06:59.931 DEBUG 52764 --- [       Thread-2] org.mybatis.spring.SqlSessionUtils       : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2e956409]

7.トランザクションネスティングの合理的な使用に注意を払う

まず、トランザクションの伝播メカニズムがあります。

  • REQUIRED(デフォルト):現在のトランザクションの使用をサポートします。現在のトランザクションが存在しない場合は、新しいトランザクションを作成します。存在する場合は、現在のトランザクションを直接使用します。

  • SUPPORTS:現在のトランザクションの使用をサポートします。現在のトランザクションが存在しない場合、トランザクションは使用されません。

  • MANDATORY:現在のトランザクションの使用をサポートします。現在のトランザクションが存在しない場合、スローされますException。つまり、現在トランザクション内にある必要があります。

  • REQUIRES_NEW:新しいトランザクションを作成し、現在のトランザクションが存在する場合は現在のトランザクションを一時停止します。

  • NOT_SUPPORTED:トランザクションは実行されません。現在のトランザクションが存在する場合は、現在のトランザクションを一時停止します。

  • NEVER:トランザクションは実行されません。現在トランザクションがある場合はスローされExceptionます。

  • NESTED:ネストされたトランザクション(現在のトランザクションが存在する場合)は、ネストされたトランザクションで実行されます。現在のトランザクションが存在しない場合、動作は`REQUIREDと同じです。

チェックすることはあまりありません。

デフォルトではREQUIRED、トランザクションで別のトランザクションを呼び出すと、実際にはトランザクションが再作成されませんが、現在のトランザクションが再利用されます。次に、このようにネストされたトランザクションを作成すると、次のようになります。

@Service("userService")
public class UserServiceImpl {
    @Autowired
    UserServiceImpl2 userServiceImpl2;
  
    @Resource
    UserMapper userMapper;
  
  	@Transactional
    public void updateUserAge() {
        try {
            userMapper.updateUserAge(1);
            userServiceImpl2.updateUserAge();
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

と呼ばれる別のトランザクション:

@Service("userService2")
public class UserServiceImpl2 {

    @Resource
    UserMapper userMapper;

    @Transactional
    public void updateUserAge() {
        userMapper.updateUserAge(2);
        int i = 1 / 0;
    }
}

次のエラーがスローされます。

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

ただし、実際のトランザクションは正常にロールバックされ、結果は正しいものでした。この問題の原因は、内部のメソッドで例外がスローされ、同じトランザクションが使用されたため、トランザクションをロールバックする必要があることを示していますが、外部レイヤーはブロックされcatchており、同じトランザクションです。1つはロールバックするように指示され、もう1つは認識されないようにブロックされます。それは矛盾ではcatchありませんか?したがって、エラーは次のようになります。このトランザクションはマークされており、ロールバックする必要があり、最終的にはロールバックされます。springExceptionspring

どのように対処しますか?

    1. 外層は積極的にエラーをスローします、throw new RuntimeException()
    1. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();アクティブIDによるロールバック
    @Transactional
    public void updateUserAge() {
        try {
            userMapper.updateUserAge(1);
            userServiceImpl2.updateUserAge();
        }catch (Exception ex){
            ex.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

8.ロールバックを外部ネットワーク要求に依存することを検討する必要があります

場合によっては、独自のデータベースを運用するだけでなく、データ同期や同期の失敗などの外部リクエストを考慮し、独自の状態をロールバックする必要があります。このシナリオでは、ネットワークリクエストが失敗するかどうかを検討する必要があります。エラーの処理方法。、これは成功するためのエラーコードです。

ネットワークがタイムアウトした場合、実際には成功しますが、失敗したと判断してロールバックするため、データの不整合が発生する可能性があります。これには、呼び出し先が再試行をサポートする必要があります。再試行するときは、べき等性をサポートする必要があり、保存された状態の整合性が複数回呼び出されます。メインプロセス全体は非常に単純ですが、詳細はまだたくさんあります。

画像-20211128153822791

要約する

トランザクションはSpring複雑に包まれており、多くのものに深いソースコードが含まれている可能性があります。それらを使用するときは、シミュレーションテストに注意して、呼び出しが正常にロールバックできるかどうかを確認する必要があります。当然のこととは言えません。人々は間違いを犯す可能性があります。多くの場合、ブラックボックステストはこの種の例外をテストするだけです。データが正常にロールバックされない場合は、後で手動で処理する必要があります。システム間の同期の問題を考慮すると、多くの不要な問題が発生します。データベースを手動で変更するプロセスを実行する必要があります。

画像-20211128154248397

【作者の簡単な紹介】
公認の作者である秦淮[秦淮食料品店]、技術の道はかつてなく、山は高く、川は長い、遅くても果てしなく続く。個人的な執筆の方向性:、、、、、、、、などすべてJava源码解析記事を注意深く書くヘッドラインパーティーが好きではない、ベルやホイッスルが好きではない、主に一連の記事を書く、私が書いたものを保証することはできませんは完全に正しいですが、私が書いたものはすべて実践されているか、情報を検索されていることを保証します。脱落や誤りについては、訂正してください。JDBCMybatisSpringredis分布式剑指OfferLeetCode

ソードポイントはすべての問題解決策を提供しますPDF

2020年に何を書きましたか?

オープンソースプログラミングノート

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/5077784/blog/5381599