14.1. 概要
14.1.1. プログラムによるトランザクション
トランザクション関数の関連する操作はすべて、コードを自分で記述することによって実装されます。
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
プログラマティックトランザクションの欠点:
-
詳細はブロックされません。すべての詳細はプログラマー自身が完了する必要がありますが、これは比較的面倒です。
-
コードの再利用性は高くありません。関数を実装するたびに独自のコードを記述する必要があり、コードは再利用されません。
14.1.2. 宣言的トランザクション
トランザクション制御コードには従うべきルールがあるため、コードの構造は基本的に決まっており、フレームワークはそれに応じて固定パターンのコードを抽出してカプセル化できます。
カプセル化後は、構成ファイルで簡単な構成を実行するだけで操作が完了します。
宣言的トランザクションの利点:
-
開発効率の向上
-
冗長なコードを削除する
-
フレームワークは機能をより包括的に検討して実装します
14.1.3. 概要
-
プログラミング: 関数を実装するための独自のコードを作成します。
-
宣言型: フレームワークに設定を通じて機能を実装させます。
14.2. 環境設定
spring_transaction という名前の新しいモジュールを作成します (プロセスについてはセクション 13.1を参照してください)。
14.2.1. Spring設定ファイルの作成
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 导入外部属性文件 -->
<context:property-placeholder location="jdbc.properties"></context:property-placeholder>
<!-- 配置数据源 -->
<bean id="datasource" class="com.alibaba.druid.pool.DruidDataSource">
<!--通过${key}的方式访问外部属性文件的value-->
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源 -->
<property name="dataSource" ref="datasource"></property>
</bean>
</beans>
14.2.2. テーブルの作成とデータの入力
CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
注: このテーブルの在庫フィールドは負にならないように設定されています (符号なし)。
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);
++++++++++++++++++++++++++++++分割線++++++++++++++++++++++++++++++
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
注: このテーブルの残高フィールドは、負の数 (符号なし) にならないように設定されています。
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
14.3. トランザクションの実装を考慮していない
14.3.1. 永続化レイヤーインターフェイス BookDao とその実装クラスの作成
package org.rain.spring.dao;
/**
* @author liaojy
* @date 2023/8/27 - 0:35
*/
public interface BookDao {
/**
* 查询图书的价格
* @param bookId
* @return
*/
Integer getPriceByBookId(Integer bookId);
/**
* 更新图书的库存
* @param bookId
*/
void updateStockOfBook(Integer bookId);
/**
* 更新用户的余额
* @param userId
* @param price
*/
void updateBalanceOfUser(Integer userId,Integer price);
}
package org.rain.spring.dao.impl;
import org.rain.spring.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
/**
* @author liaojy
* @date 2023/8/27 - 0:45
*/
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
Integer price = jdbcTemplate.queryForObject(sql, Integer.class,bookId);
return price;
}
public void updateStockOfBook(Integer bookId) {
String sql = "update t_book set stock = stock -1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
}
public void updateBalanceOfUser(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql,price,userId);
}
}
14.3.2. ビジネス層インターフェース BookService とその実装クラスの作成
package org.rain.spring.service;
/**
* @author liaojy
* @date 2023/8/27 - 0:59
*/
public interface BookService {
void buyBook(Integer bookId,Integer userId);
}
package org.rain.spring.service.impl;
import org.rain.spring.dao.BookDao;
import org.rain.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author liaojy
* @date 2023/8/27 - 1:02
*/
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStockOfBook(bookId);
//更新用户的余额
bookDao.updateBalanceOfUser(userId,price);
}
}
14.3.3. コントロールレイヤー BookController の作成
注: したがって、制御層はインターフェイスを使用しないため、メソッドのアクセス修飾子を手動で設定する必要があります。
package org.rain.spring.controller;
import org.rain.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
/**
* @author liaojy
* @date 2023/8/27 - 1:07
*/
@Controller
public class BookController {
@Autowired
private BookService bookService;
public void buyBook(Integer bookId, Integer userId){
bookService.buyBook(bookId,userId);
}
}
14.3.4. アノテーションコンポーネントのスキャンの設定
<!--扫描注解组件-->
<context:component-scan base-package="org.rain.spring"></context:component-scan>
14.3.5. テストクラスの作成
シミュレーションシナリオ:
-
ユーザーが書籍を購入する場合、まず書籍の価格を確認し、次に書籍の在庫とユーザーの残高を更新します。
-
ID 1(残高 50)のユーザーが ID 1(価格 80)の書籍を購入したとします。
-
書籍購入後、ユーザーの残高は -30 になるはずですが、データベースの残高フィールドが unsigned に設定されているため、残高フィールドに -30 を挿入できません。このとき、SQL ステートメントを実行してユーザーの残高を更新します
。例外がスローされます。
package org.rain.spring.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.rain.spring.controller.BookController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author liaojy
* @date 2023/8/27 - 1:16
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-tx-annotation.xml")
public class TxByAnnotation {
@Autowired
private BookController bookController;
@Test
public void testBuyBook(){
bookController.buyBook(1,1);
}
}
14.3.6. テスト実行の影響
14.3.6.1. 実行前のデータ
この時点でID1の本の在庫は100冊です
この時点で、ID1のユーザーの残高は50です。
14.3.6.2. 実行中の例外
14.3.6.3. 実行後のデータ
現時点でID1の本の在庫は99冊あり、1冊欠品しています。
この時点で、id1のユーザーの残高は50で変化ありません。
14.3.6.4. 実行結果の概要
-
使用トランザクションがないため、本の在庫は更新されますが、ユーザーの残高は更新されません。
-
このような結果は間違っています。本の購入は、(本の) 在庫の更新と (ユーザー) の残高の更新という完全なプロセスであり、両方とも成功するか両方とも失敗するかのどちらかであるためです。
14.4. トランザクションの実装を検討する
14.4.1. トランザクション機能の関連設定を追加
<!--配置事务管理器-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 装配要进行事务管理的数据源 -->
<property name="dataSource" ref="datasource"></property>
</bean>
<!--
tx:annotation-driven标签:开启事务的注解驱动;
通过@Transactional注解所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
transaction-manager属性:设置使用的事务管理器;
属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>
注: tx:annotation-driven タグによってインポートされる名前空間には、tx で終わる名前空間が必要です
14.4.2. @Transactional アノテーションの使用
サービス層はビジネス ロジック層を表し、メソッドは完全な関数を表すため、トランザクションを処理する際には、一般に @Transactional アノテーションがサービス層で使用されます。
14.4.3. トランザクションの効果をテストする
14.4.3.1. 実行前のデータ
この時点でID1(修正済)の書籍在庫は100冊です。
この時点で、ID1のユーザーの残高は50です。
14.4.3.2. 実行中の例外
14.4.3.3. 実行後のデータ
Spring の宣言型トランザクションを使用しているため、在庫 (書籍) の更新と (ユーザー) 残高の更新は成功するか、両方とも失敗します。
この例では両方とも失敗するため、(書籍) 在庫も (ユーザー) 残高も変化しません。
14.4.4. @Transactional アノテーションの場所
-
メソッドで識別: このメソッドでのみトランザクション管理を実行します
-
クラスにマーク: クラスのすべてのメソッドに @Transactional をマークするのと同じです。
14.5. トランザクション属性
14.5.1、読み取り専用
14.5.1.1. 利用目的
-
一連のクエリ操作について、読み取り専用に設定すると、この一連の操作に書き込み操作が含まれていないことをデータベースに明確に伝えることができます。
-
これにより、データベースをクエリ操作用に最適化できます。
14.5.1.2. 使用方法
@Transactional(readOnly = true)
14.5.1.3. 注意事項
追加、削除、および変更操作に読み取り専用を設定すると、例外がスローされます。
14.5.2. タイムアウト
14.5.2.1. 利用目的
-
トランザクションの実行中に、特定の問題が発生してトランザクションが停止し、データベース リソースが長時間占有される場合があります。
-
リソースが長時間占有されている場合は、実行中のプログラムに問題があることが最も考えられます (Java プログラム、MySQL データベース、ネットワーク接続などの可能性があります)。
-
現時点では、問題が発生する可能性があるプログラムを強制的にロールバックし、実行した操作を元に戻し、トランザクションを終了し、他の通常のプログラムが実行できるようにリソースを解放する必要があります。
-
要約すると、タイムアウト後にロールバックし、リソースを解放するという 1 つの文になります。
14.5.2.2. 使用方法
注: タイムアウト属性のデフォルト値は -1 です。これは、トランザクションの実行時間が無限に長くなる可能性があることを意味します。
// 设置事务执行超过3秒,则强制回滚、结束事务、释放资源
@Transactional(timeout = 3)
14.5.2.3. 使用の効果
トランザクションの実行が設定時間を超えると、ロールバックの強制、トランザクションの終了、リソースの解放に加えて、例外 TransactionTimedOutException がスローされます。
14.5.3. ロールバック戦略
14.5.3.1. 利用目的
ロールバック戦略は、@Transactional の関連属性を通じて設定できます。
-
rollbackFor 属性 (一般的には使用されません): ロールバックを引き起こす例外を設定します。属性値には Class 型のオブジェクトが必要です。
-
rollbackForClassName 属性 (一般的には使用されません): ロールバックを引き起こす例外を設定します。属性値には文字列型の完全なクラス名文字列が必要です。
-
noRollbackFor 属性: ロールバックを引き起こさない例外を設定します。属性値には Class 型のオブジェクトが必要です。
-
noRollbackForClassName 属性: ロールバックを引き起こさない例外を設定します。属性値には文字列型の完全なクラス名文字列が必要です。
注: 宣言型トランザクションはデフォルトですべてのランタイム例外をロールバックするため、rollbackFor 属性と rollbackForClassName 属性は一般的に使用されません。
14.5.3.2. 使用方法
注: (この例では) 例外処理がないため、算術演算例外 (ArithmeticException) が発生するとプログラムは中止されます。また、
ロールバック ポリシーが設定されているため、算術演算例外 (ArithmeticException) が発生したときにロールバックする必要はありません。
データが間違っているため、例外が発生するコードは更新(帳簿在庫やユーザー残高)操作の後に配置するのがベストです。
// 设置当出现数学运算异常(ArithmeticException)时,不需要进行回滚
@Transactional(noRollbackFor = {ArithmeticException.class})
14.5.3.3. 使用の効果
14.5.3.3.1. 実行前のデータ
この時点でID2(価格50)の書籍の在庫は100冊です。
この時点で、ID1のユーザーの残高は50です。
14.5.3.3.2. 実行中の例外
ロールバック ポリシーでは、算術演算例外 (ArithmeticException) が発生した場合にロールバックを必要としないように設定されているため、書籍の購入操作は正常に実行されます。
14.5.3.3.3. 実行後のデータ
現時点でID2(価格50)の書籍の在庫は99冊あり、1冊欠品しています。
このとき、ID 1 のユーザーの残高は 50 減った 0 です(書籍の価格は 50)。
14.5.4. 分離レベル
14.5.4.1. 利用目的
-
データベース システムには、さまざまなトランザクションを分離して同時に実行する機能があるため、相互に影響を与えず、さまざまな同時実行の問題を回避できます。
-
トランザクションが他のトランザクションからどの程度分離されているかを分離レベルと呼びます。
-
SQL 標準ではさまざまなトランザクション分離レベルが指定されており、異なる分離レベルはさまざまな干渉の程度に対応します。
-
分離レベルが高いほど、データの一貫性は向上しますが、同時実行性は弱くなります。
4 つの分離レベルがあります。
-
READ UNCOMMITTED (READ UNCOMMITTED): Transaction01 が Transaction02 のコミットされていない変更を読み取ることを許可します。
-
コミットされた読み取り (READ COMMITTED): Transaction01 は、Transaction02 によって送信された変更を読み取ることのみを要求します。
-
反復可能な読み取り (REPEATABLE READ): Transaction01 がフィールドから同じ値を複数回読み取ることができることを確認します。つまり、
Transaction01 の実行中に他のトランザクションがこのフィールドを更新することは禁止されます。 -
シリアル化 (SERIALIZABLE): Transaction01 がテーブルから同じ行を複数回読み取ることができることを確認します。Transaction01 の
実行中、他のトランザクションはこのテーブルに対する操作の追加、更新、削除を禁止されます。
この分離レベルにより、同時実行の問題を回避できます。しかしパフォーマンスは非常に低い
同時実行性の問題を解決するための各分離レベルの機能を次の表に示します。
分離レベル | ダーティリード | 反復不可能な読み取り | 幻の読書 |
---|---|---|---|
コミットされていない読み取り | 持っている | 持っている | 持っている |
コミットされた読み取り | なし | 持っている | 持っている |
反復読み取り | なし | なし | 持っている |
シリアル化可能 | なし | なし | なし |
-
ダーティ リード: トランザクションは、コミットされていない別の並列トランザクションによって書き込まれたデータを読み取ります。
-
Non-RepeatableRead: トランザクションは、以前に読み取られたデータを再読み取りし、
そのデータが別のトランザクション (最初の読み取り後にコミットされた) によって変更されたことを検出します。 -
ファントム読み取り: トランザクションは、検索条件を満たす行のセットを返すクエリを再実行し、
最近コミットされた別のトランザクションによって条件を満たす行のセットが変更されたことを検出します。
さまざまなデータベース製品がトランザクション分離レベルをサポートする程度:
分離レベル | オラクル | MySQL | SQLサーバー | ダーメン | 人民金融経済大学 |
---|---|---|---|---|---|
コミットされていない読み取り | × | √ | √ | √ | × |
コミットされた読み取り | √ (デフォルト) | √ | √ (デフォルト) | √ (デフォルト) | √ (デフォルト) |
反復読み取り | × | √ (デフォルト) | √ | × | √ |
シリアル化可能 | √ | √ | √ | √ | √ |
14.5.4.2. 使用方法
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别(默认且常用)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
14.5.5. 通信動作
14.5.5.1. 利用目的
トランザクション メソッドが別のトランザクション メソッドによって呼び出される場合、トランザクションをどのように伝播するかを指定する必要があります。
14.5.5.1.1. チェックアウトサービス層インターフェース CheckoutService とその実装クラスを作成する
package org.rain.spring.service;
/**
* @author liaojy
* @date 2023/8/29 - 8:07
*/
public interface CheckoutService {
void checkout(Integer[] bookIds, Integer userId);
}
注: checkout メソッドはトランザクション管理を実行し、それが呼び出す buyBook メソッドもトランザクション管理を実行します。
package org.rain.spring.service.impl;
import org.rain.spring.service.BookService;
import org.rain.spring.service.CheckoutService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author liaojy
* @date 2023/8/29 - 8:10
*/
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Autowired
private BookService bookService;
//一次购买多本图书
@Transactional
public void checkout(Integer[] bookIds, Integer userId) {
for (Integer bookId : bookIds) {
bookService.buyBook(bookId,userId);
}
}
}
14.5.5.1.2. コントロール層 BookController にチェックアウトメソッドを追加する
@Autowired
private CheckoutService checkoutService;
public void checkout(Integer[] bookIds, Integer userId){
checkoutService.checkout(bookIds, userId);
}
14.5.5.1.3. チェックアウトテストメソッドの追加
@Test
public void testCheckout(){
Integer[] bookIds = {1,2};
bookController.checkout(bookIds,1);
}
14.5.5.1.4. チェックアウト実行前のデータ
このとき、ID 1 の書籍(価格 80)の在庫は 100 冊、ID 2 の書籍(価格 50 )の在庫は 100 冊です。
このとき、ID1のユーザーの残高は100です。
14.5.5.1.5. チェックアウト実行時の例外
14.5.5.1.6. チェックアウト実行後のデータ
このとき、ID1の書籍(価格80)の在庫は100、ID2の書籍(価格50)の在庫は100となっており、在庫は変化していません。
このとき、ID1のユーザの残高は100であり、残高は変化していない。
14.5.5.1.7. テストデータ結果の分析
-
@Transactional アノテーションの伝播属性のデフォルト値は: Propagation.REQUIRED;
すでに開かれているトランザクションが利用可能な場合、それがこのトランザクションで実行されることを示します。 -
観測後、checkout()内で書籍を購入するメソッドbuyBook()が呼び出されており、checkout()にはトランザクションアノテーションが付いているので、このトランザクション内で実行されます。
-
購入した 2 冊の本の価格は 80 と 50 で、ユーザーの残高は 100 です。
したがって、2 冊目の本を購入すると残高が失敗し、checkout() 全体がロールバックされます。 -
つまり、買えない本がある限り、誰も買えないのです。
14.5.5.2. 使用方法
14.5.5.2.1. 呼び出されたメソッドのトランザクション伝播属性を変更する
// 表示不管是否有已经开启的事务,都要开启新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
14.5.5.2.2. チェックアウト実行前のデータ
このとき、ID 1 の書籍(価格 80)の在庫は 100 冊、ID 2 の書籍(価格 50 )の在庫は 100 冊です。
このとき、ID1のユーザーの残高は100です。
14.5.5.2.3. チェックアウト実行時の例外
14.5.5.2.4. チェックアウト実行後のデータ
このとき、ID1の書籍(価格80)の在庫は99、ID2の書籍(価格50)の在庫は100となり、ID1の書籍の在庫が1つ減ります。
このとき、ID1のユーザーの残高は100で、残高は80(ID1の書籍の価格)未満です。
14.5.5.2.5. テストデータ結果の分析
-
同じシナリオで、本の購入はそれぞれ buyBook() のトランザクションで実行されます。
-
したがって、最初の本の購入は成功し、トランザクションは終了します。
-
2 番目の本の購入が失敗した場合、2 番目の buyBook() でロールバックされるだけで、最初の本の購入には影響しません。
-
言い換えれば、できるだけ多くのコピーを購入することです。