序文
普段は誰もが事件について書くべきで、以前は事件を書いていたときに落とし穴に出くわしましたが、効果がありませんでした。その後、様々な事件の失敗のシナリオを確認・見直しました。恐れることはありません。それでは、最初にトランザクションに関する知識を確認しましょう。トランザクションとは、操作の最小の作業単位を指します。単一の分割できない単位操作として、すべてが成功するか、すべてが失敗します。トランザクションには4つの特性があります(ACID
):
- Atomicity(
Atomicity
):トランザクションに含まれる操作は、すべて成功するか、すべて失敗してロールバックされ、半分の成功と半分の失敗の中間状態はありません。たとえばA
、B
最初に500
人民元を持っているA
場合、B
送金する場合、お金が少ない100
場合は、より多くのお金を持っている必要があります。お金を失うことはできません。お金を受け取らない場合、お金は消えて、原子性に適合しなくなります。A
100
B
100
A
B
- 整合性(
Consistency
):整合性とは、トランザクションが実行される前後の状態全体の整合性を維持することを指します。たとえば、最初に人民元がA
あり、合計が人民元です。これは前の状態です。転送の場合、次に、最後は、はい、2つです。合計しても、この全体的な状態を保証する必要があります。B
500
1000
A
B
100
A
400
B
600
1000
- 分離(
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
すべてのユーザーを取得して、以下を確認します。
更新インターフェイスを呼び出すと、ページはエラーをスローします。
コンソールは例外もスローします。これは、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 engines
Mysql
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
では、テーブルのエンジンをMyISAM
このようなものに変更するとどうなるでしょうか。ここでは、データテーブルのデータエンジンのみを変更してみてください。
mysql> ALTER TABLE user ENGINE=MyISAM;
Query OK, 2 rows affected (0.06 sec)
Records: 2 Duplicates: 0 Warnings: 0
それからupdate
、当然のことながら、それでもエラーがスローされますが、これは何の違いもないようです。
ただし、すべてのデータが取得されると、最初のデータ更新は成功し、2番目のデータは正常に更新されないため、トランザクションは有効になりません。
[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]
結論:InnoDB
トランザクションを有効にするには、エンジンに設定する必要があります。
2.メソッドをプライベートにすることはできません
トランザクションはメソッドである必要がありますpublic
。メソッドで使用される場合private
、トランザクションは自動的に失敗しますが、IDEA
それを記述している限り、エラーが報告されますMethods annotated with '@Transactional' must be overrideable
。これは、トランザクションのアノテーションによってメソッドが追加されたことを意味します。書き換えが必要です。private
メソッドは書き換えできないため、エラーが報告されます。
同じ変更されたメソッドに注釈が付けられている場合、書き直されたくないfinal
ため、エラーも報告されます。final
Spring
主にBean
放射線によって得られたアノテーション情報を使用し、動的プロキシ技術を使用してトランザクション全体をカプセル化します。理論的には、メソッドの呼び出しに問題はないAOP
と思います。メソッドレベルで使用できますが、チームはこの方法は開発者の意志であると感じてください。公開されたくないインターフェイスのカプセル化を破棄する必要はありません。これは、混乱を招きやすいものです。private
method.setAccessible(true);
Spring
private
Protected
その方法は可能ですか?できません!
次に、実現するために、インターフェースが使用できないため、コード構造を魔法のように変更しますPortected
。インターフェースを使用すると、protected
メソッドを使用できなくなり、エラーが直接報告されるため、で使用する必要があります。同じパッケージ。controller
合計をservice
同じパッケージに入れます。
テスト後、トランザクションが有効になっておらず、結果は1つの更新であり、もう1つの更新は更新されていないことがわかりました。
[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]
結論:public
メソッドではなく、メソッドでprivate
使用する必要があります。そうしないとfinal
、static
有効になりません。
3.例外は実行時例外である必要があります
Springboot
例外を管理する場合、実行時例外(RuntimeException
およびそのサブクラス)のみがロールバックされます。たとえば、前に説明したようi=1/0;
に、実行時例外が生成されます。
ソースコードから、メソッドが例外であるか:rollbackOn(ex)
であるかを判断することもわかります。RuntimeException
Error
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
例外は主に次のタイプに分類されます。
すべての例外はエラーメッセージですが、Throwable
一般に、そのようなファイルがない、メモリオーバーフロー、突然のエラーなど、プログラムで制御できないエラーが発生しています。一方、を除いて、他のすべては処理できる例外であり、プログラムはこの例外が書き込まれるときに処理する必要があります。そうしないと、コンパイルはパスしません。Error
IO
Exception
RuntimeException
CheckException
Java
次の図からわかるように、いくつかの一般的なIO例外CheckedException
をリストしました。このメソッドは見つかりませんでした。このクラスは見つかりませんでしたが、いくつかの一般的な例外があります。IOException
NoSuchMethodException
ClassNotFoundException
RunTimeException
- 配列の範囲外の例外:
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.不適切な構成が原因
@Transactional
トランザクションを開始するには、メソッドを使用する必要があります- 複数のデータソースまたは複数のトランザクションマネージャーを構成する場合は、データベースを操作する
A
と使用できないトランザクションに注意してB
ください。この問題は非常に単純ですが、誤った使用で問題を見つけるのが難しい場合があります。 - の場合、トランザクションを開く
Spring
ように構成する必要があります。これは構成ファイル@EnableTransactionManagement
と同等ですが、では不要になり、注釈に注釈が含まれているため、自動的に挿入されます。xml
*<tx:annotation-driven/>*
Springboot
springboot
SpringBootApplication
@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)()
ます。
これは内部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
ありませんか?したがって、エラーは次のようになります。このトランザクションはマークされており、ロールバックする必要があり、最終的にはロールバックされます。spring
Exception
spring
どのように対処しますか?
-
- 外層は積極的にエラーをスローします、
throw new RuntimeException()
- 外層は積極的にエラーをスローします、
-
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
アクティブIDによるロールバック
@Transactional
public void updateUserAge() {
try {
userMapper.updateUserAge(1);
userServiceImpl2.updateUserAge();
}catch (Exception ex){
ex.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
8.ロールバックを外部ネットワーク要求に依存することを検討する必要があります
場合によっては、独自のデータベースを運用するだけでなく、データ同期や同期の失敗などの外部リクエストを考慮し、独自の状態をロールバックする必要があります。このシナリオでは、ネットワークリクエストが失敗するかどうかを検討する必要があります。エラーの処理方法。、これは成功するためのエラーコードです。
ネットワークがタイムアウトした場合、実際には成功しますが、失敗したと判断してロールバックするため、データの不整合が発生する可能性があります。これには、呼び出し先が再試行をサポートする必要があります。再試行するときは、べき等性をサポートする必要があり、保存された状態の整合性が複数回呼び出されます。メインプロセス全体は非常に単純ですが、詳細はまだたくさんあります。
要約する
トランザクションはSpring
複雑に包まれており、多くのものに深いソースコードが含まれている可能性があります。それらを使用するときは、シミュレーションテストに注意して、呼び出しが正常にロールバックできるかどうかを確認する必要があります。当然のこととは言えません。人々は間違いを犯す可能性があります。多くの場合、ブラックボックステストはこの種の例外をテストするだけです。データが正常にロールバックされない場合は、後で手動で処理する必要があります。システム間の同期の問題を考慮すると、多くの不要な問題が発生します。データベースを手動で変更するプロセスを実行する必要があります。
【作者の簡単な紹介】:
公認の作者である秦淮[秦淮食料品店]、技術の道はかつてなく、山は高く、川は長い、遅くても果てしなく続く。個人的な執筆の方向性:、、、、、、、、など、すべてJava源码解析
の記事を注意深く書く、ヘッドラインパーティーが好きではない、ベルやホイッスルが好きではない、主に一連の記事を書く、私が書いたものを保証することはできませんは完全に正しいですが、私が書いたものはすべて実践されているか、情報を検索されていることを保証します。脱落や誤りについては、訂正してください。JDBC
Mybatis
Spring
redis
分布式
剑指Offer
LeetCode