山一程--软件开发-Spring transaction
目的:整理掌握基于Spring的企业级技术体系之事务管理.
索引:
1.transaction
1. Spring transaction
背景:数据安全.
refer:
1.<Spring5 recipes> chapter10 Spring transaction management. p294 ( 2023.4.1 - 2023.4.8) 第一次
基于 <Spring5 recipes> 实践
1.mysql DDL
CREATE USER 'PRACTICE_TRANSACTION'@'localhost' identified BY 'admin'; CREATE SCHEMA PRACTICE_TRANSACTION; GRANT ALL PRIVILEGES ON PRACTICE_TRANSACTION .* TO 'PRACTICE_TRANSACTION'@'localhost'; FLUSH PRIVILEGES; SET GLOBAL time_zone = "+3:00";
create table BOOK( ISBN varchar(50) PRIMARY KEY, BOOK_NAME VARCHAR(100) NOT NULL, PRICE INTEGER ); create table BOOK_STOCK( ISBN varchar(50) PRIMARY KEY, STOCK int NOT NULL CHECK(STOCK >=0) ); create table ACCOUNT( USERNAME VARCHAR(50) PRIMARY KEY, BALANCE int not null CHECK(BALANCE >=0) )
insert into practice_transaction.book (`ISBN`,`BOOK_NAME`,`PRICE`) VALUES('0001','The first book',30); insert into practice_transaction.book_stock (`USERNAME`,`BALANCE`) VALUES('user1',10); insert into practice_transaction.account (`USERNAME`,`BALANCE`) VALUES('user1',20);
2.spring pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.7.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.21</version>
</dependency>
spring application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/practice_transaction?userUnicode=true&characterEncoding=utf-8&serverTimezone=GMT spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=PRACTICE_TRANSACTION spring.datasource.password=admin
java code
1.interface BookShop , purse(xx) method
public interface BookShop { void purchase(String isbn,String userName); }
实现
@Component("bookShop")
public class JdbcBookshop implements BookShop {
@Autowired
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void purchase(String isbn, String userName) {
Connection connection = null;
try {
connection = dataSource.getConnection();
PreparedStatement statement_query =
connection.prepareStatement("SELECT PRICE FROM BOOK WHERE ISBN = ?");
statement_query.setString(1, isbn);
ResultSet rs = statement_query.executeQuery();
rs.next();
int price = rs.getInt("PRICE");
statement_query.close();
PreparedStatement statement_decrease_stock =
connection.prepareStatement(
"UPDATE BOOK_STOCK SET STOCK = STOCK - 1 " + "WHERE ISBN = ?");
statement_decrease_stock.setString(1, isbn);
statement_decrease_stock.executeUpdate();
statement_decrease_stock.close();
PreparedStatement statement_up_account =
connection.prepareStatement(
"UPDATE ACCOUNT SET BALANCE = BALANCE - ? " + "where USERNAME = ? ");
statement_up_account.setInt(1, price);
statement_up_account.setString(2, userName);
statement_up_account.executeUpdate();
statement_up_account.close();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
main
@SpringBootApplication public class SpringTransactionApplication { public static void main(String[] args) { SpringApplication.run(SpringTransactionApplication.class, args); } @Component class App implements ApplicationRunner{ @Autowired BookShop bookShop; @Override public void run(ApplicationArguments args) throws Exception { bookShop.purchase("0001","user1"); } } }
问题:
三段 SQL ,第一个查询正常,第二个会库存扣减一本书,
第三个模拟扣减用户金额,但此时违反了数据库check 约束,20 少于 30,交易失败,抛出 SQLException.
结果是库存减少了1,交易失败。数据处于不一致的状态。

解决:使用事务,将三个SQL 归为一个事务。
JDBC 默认是在每条SQL执行后立刻提交,自动提交。该行为不允许你管理操作的事务.
JDBC支持的方式:显示调用连接的 commit() 与 rollback() 来实现,先关闭自动提交,默认是开启的。setAutoCommit(false)

这些样式代码, 特定于JDBC, Spring 事务支持不依赖于特定技术的设施,
包括事务管理:PlaformTransactionManager,
事务模板:Transaction Template
事务声明支持。
使用事务管理 API 以编程方式管理事务
Spring 核心事务管理管理抽象接口 PlatformTransaction,封装了一套独立于技术的方法.

其实现
1.应用只使用了单个数据源并使用 JDBC 访问,采用 DataSourceTransactionManager
2.在Java EE 服务中使用 JTA 进行事务管理, 使用 JtaTransactionManager 从应用服务器查找事务.,而且其适用分布式事务。经常使用 JTA 事务管理器来
集成应用服务器的事务管理器. 也可使用独立的 JTA事务管理器,如 Atomikos.
3.如果使用对象关系映射框架访问数据库,则应采用 HibernateTransactionManager 或 JpaTransactionManager.
事务管理器是作为一个普通的 bean 声明在 Spring IoC 容器中,bean 配置声明了一个 DataSourceTransactionManager 实例.设置它的 dataSource 属性,
就可以管理该数据源锁创建的连接事务了.
@Configuration @EnableAutoConfiguration public class BookstoreConfig { @Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } }
示例采用上面示例的基础 对象:
注意报错

以构造器方式注入 dataSource, 不能以 Setter方式。
TransactionDefinition def = new DefaultTransactionDefinition(); 默认的事务配置
TransactionStatus status = transactionManager.getTransacction ( def ); 此处 事务管理器使用该定义开启一个新事务了.
Spring JDBC 模板抛出的所有异常都是 DataAccessException 子类.
@Component("bookShop")
@EnableAutoConfiguration
public class TransactionJdbcBookshop extends JdbcDaoSupport implements BookShop {
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
TransactionJdbcBookshop(DataSource dataSource){
setDataSource(dataSource);
}
@Override
public void purchase(String isbn, String userName) {
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
try{
int price = getJdbcTemplate().queryForObject(
"SELECT PRICE FROM BOOK WHERE ISBN = ?",Integer.class,isbn);
getJdbcTemplate().update(
"UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?",isbn );
getJdbcTemplate().update( "UPDATE ACCOUNT SET BALANCE = BALANCE - ?",price);
transactionManager.commit(transactionStatus);
}catch (DataAccessException e){
transactionManager.rollback(transactionStatus);
}
}
}
@SpringBootApplication public class SpringTransactionApplication { public static void main(String[] args) { SpringApplication.run(SpringTransactionApplication.class, args); } @Component class App implements ApplicationRunner{ @Autowired BookShop bookShop; @Override public void run(ApplicationArguments args) throws Exception { bookShop.purchase("0001","user1"); } } }
以上的样板代码,替换为 TransactionTemplate 来控制整个事务管理过程和事务异常处理.
要将代码块封装到一个实现了 TransactionCallback<T> 接口的回调类中( 内部类实现需要将方法参数标记为 final )并将其传递给 TransactionTemplate 的 execute 方法执行即可.
轻量级。丢弃或重新创建不会产生性能影响.
1. 通过 DataSource 引用来创建 JDBC 模板
2. 事务管理其引用来创建 TransactionTemplate , 或 Spring 应用上下文创建.
TransactionCallback<T> 有返回值,通过 T execute 返回. 好处在于剥离了 开启,回滚与 提交事务的职责.
子接口 TransactionCallBackWithoutResult
回调对象执行过程中,如出现
1.未检查异常 RuntimeException 和 DataAccessException
2.在 doInTransactionWithoutResult 方法中显示调用了 TransactionStatus 参数的 setRollbackOnly()
事务回滚。否则,对象执行完毕后就提交.
@Configuration
public class BookshopConfig {
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
@Bean
public TransactionTemplate transactionTemplate(DataSource dataSource){
TransactionTemplate transactionTemplate = new TransactionTemplate();
transactionTemplate.setTransactionManager(transactionManager(dataSource));
return transactionTemplate;
}
}
@Component("bookShop")
@EnableAutoConfiguration
public class TransactionJdbcBookshop extends JdbcDaoSupport implements BookShop {
@Autowired
TransactionTemplate transactionTemplate;
TransactionJdbcBookshop(DataSource dataSource){
setDataSource(dataSource);
}
@Override
public void purchase(final String isbn,final String userName) {
this.transactionTemplate.execute(
new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
int price = getJdbcTemplate().queryForObject(
"SELECT PRICE FROM BOOK WHERE ISBN = ?",Integer.class,isbn);
getJdbcTemplate().update(
"UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?",isbn);
getJdbcTemplate().update(
"UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?",price,userName);
}
});
}
}
@Transaction , @EnableTransactionManagement 注解以声明的方式管理事务


事务传播
发生在 一个事务性方法被另一个方法调用的时候。另一个方法可是可不是事务性方法。

Spring 定义了7中传播行为,定义在 TransactionDefinition 接口中,并非所有类型的事务管理器都支持。页与数据库支持不同的隔离级别,约束了
事务管理器所能支持的传播行为:

1. REQUIRED : 如果过程中存在事务,则当前方法应该运行在该事务中,否则应该开启一个新事务并运行在自己的事务中。(默认)
2.REQUIRED_NEW : 当前方法必须要开启一个新事务,且运行在自己的事务中,如果过程中存在事务,则将其挂起.
3.SUPPORTS : 如果过程中存在事务,当前方法可运行在该事务中. 否则,没必要运行在事务中.
4.NOT_SUPPORTED : 当前方法不该运行在事务中,如果过程中存在事务,就应该将其挂起.
5.MANDATORY : 当前方法必须运行在事务中,如果过程中没有事务,则抛出异常.
6.NEVER: 当前方法不能运行在事务中,如果过程中存在事务, 就会抛出异常.
7.NESTED : 如果过程存在事务, 则当前方法运行在该事务的嵌套事务中(JDBC 3.0 安全特性),否则,开启一个新事务并运行在自己的事务中,
该特性为 Spring 特有. ( 对于长时间运行的可分段处理的场景,批处理) 100W 数据,截断,每1W条记录提交一次。如果出错, 可以回滚嵌套事务,
只会丢失这1W条记录,而不是 100W.
示例:用户金额 40, 买两本书,1本 30, 第二本 50,
1. 事务传播的默认, REQUIRED, 将导致整个购买失败。
2.事务传播 REQUIRED_NEW, 会导致购买第一本成功,第二本失败。



示例1:整个结算事务失败,数据集不变


purse 自身是 transaction, propagation 默认是REQUIRED. 其被check() 调用
1. check() 是个事务方法,因此 purse() 行为表现为自身不启用事务,事务的边界便由check() 决定.
2.如果check() 不是事务方法,且过程中不存在既有事务,则purse() 则会开启事务,第一个购买成功,第二个余额不足会失败.
将 check() 变成非事务性方法。



结果是 用户第一本书购买成功,第二本失败.
示例2: propagation.REQUIRED_NEW
check() 默认传播机制,purchase 设定 REQUIRED_NEW. 将会在过程中存在事务时,开启自己的独立事务,并将过程事务挂起。
此结果会使第一本成功扣减,第二本失败.



一共三个事务
1.checkout() 开启事务
2.遇到第一个 purse() 时, checkOut() 事务挂起, purse() _1 新事务开启,顺利完成提交.此时,purse_1() 事务结束,checkOut() 事务恢复
3.遇到第二个purse() 时, checkout() 事务挂起, purse()_2 新事务开启,不满足check, 事务失败并回滚, checkout()事务恢复,随后结束.
隔离事务
同一个应用或不同应用的多个事务同时操纵同一个数据集时,数据不安全。要设置事务隔离方式
并发事务问题4类
1.脏读。事务T1,T2, T2读取了T1更新但未提交的字段,T1 回滚,则T2 读取的字段变成了临时且无效的.
2.不可重复读,T2读取了一个字段,随后T1更新了这个字段,值不同,(值相同是不可预测的),T2如果再次读取该值,则是不同的。

3.幻读 phantom read,T2 从表中读取一些行,然后T1 向该表插入一些行, T2再次读取该表,则会新行. 表的数据改变,
幻读类似不可重复读,不过它涉及多行.
4.丢失更新: T2 更新表中的一行数据,随后T1也更新了该一行数据,数据覆盖。

事务隔离要由底层数据库引擎支持, 不应由应用或框架支持.
Java.sql.connection setTransactionIsolation() 方法修改 JDBC 连接的隔离级别.
Spring 支持的5中隔离级别
1.Default ,由底层数据库默认的隔离级别。大多数默认是 READ_COMMITTED.
2.READ_UNCOMMITED: 一个事务可读取其他事务尚提交的更改;
可能出现脏读,不可重复读,幻读问题.
3.READ_COMMITED: 一个事务只能读到其他事务已经提交的更改,
可避免脏读,不能避免 不可重复读 和 幻读
4.REPEATABLE_READ: 一个事务能多次从一个字段读到相同的值。在该事务期间禁止其他事务对该字段进行更新.
可避免脏读,不可重复读,不可避免幻读.
5.SERIALIZABLE: 可避免所有并发问题, 性能最差.
确保一个事务能够多次从一张表中读到相同的行,在该事务期间禁止其他事务对该表进行插入,更新和删除操作。
示例1: Isolation READ_UNCOMMIT , 未提交读,会导致 脏读,不可重复读,幻读,更新丢失.


@Component("bookShop")
@EnableAutoConfiguration
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
public JdbcBookShop(DataSource dataSource) {
setDataSource(dataSource);
}
@Transactional
@Override
public void purchase(final String isbn, final String userName) {
int price =
getJdbcTemplate()
.queryForObject("SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);
getJdbcTemplate().update("UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn);
getJdbcTemplate()
.update("UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, userName);
}
@Transactional
@Override
public void increaseStock(String isbn, int stock) {
String threadName = Thread.currentThread().getName();
System.out.println((threadName + " - Prepare to increase stock"));
getJdbcTemplate().update(
"UPDATE BOOK_STOCK SET STOCK = STOCK + ? WHERE ISBN = ?",
stock,isbn);
System.out.println((threadName + " - Book stock increased by " + stock));
sleep(threadName);
System.out.println((threadName + " - Book stock transaction rolled back"));
throw new RuntimeException("Increased by mistake");
}
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Override
public int checkShock(String isbn) {
String threadName = Thread.currentThread().getName();
System.out.println((threadName + " - Prepare to check book stock"));
int stock = getJdbcTemplate().queryForObject(
"SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?",
Integer.class,isbn);
System.out.println((threadName + " - Book stock is " + stock));
sleep(threadName);
return stock;
}
private void sleep(String threadName){
System.out.println((threadName + " - Sleeping"));
try{
Thread.sleep(10000);
}catch (InterruptedException e){
}
System.out.println(threadName+" - Wake up");
}
}
并发访问:
@SpringBootApplication @EnableTransactionManagement public class SpringTransactionApplication { public static void main(String[] args) { SpringApplication.run(SpringTransactionApplication.class, args); } @Component class App implements ApplicationRunner{ @Autowired BookShop bookShop; @Override public void run(ApplicationArguments args) throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(3); Runnable increaseTask = new Runnable() { @Override public void run() { try{ Thread.currentThread().setName("Thread 1"); bookShop.increaseStock("0001",5); }catch (RuntimeException e){} }; }; Runnable checkStockTask =new Runnable() { @Override public void run() { Thread.currentThread().setName("Thread 2"); bookShop.checkShock("0001"); } }; executorService.execute(increaseTask); try{ Thread.sleep(5000); }catch (InterruptedException e){ } executorService.execute(checkStockTask); } } }

thread2 脏读 thread1 的临时数据15,实际回滚后仍然为10.
解决办法是: Isolation.READ_COMMITTED
对于支持该隔离级别的数据库来说:需要获取到已更新但尚未提交的行的更新锁,
其他事务在读取该行时需要等待锁释放。
更新锁的释放出现在加锁的事务提交或回滚时。


再次运行,Thread2 会在 Thread1 回滚事务,事务完成后再读取.
示例2: 不可重复读:



线程1读取库存,得到数据10, 然后休眠,
这时,线程1的事务尚未提交,当线程1休眠时,线程2启动事务增加了库存。READ_COMMITTED 级别下,
线程2 更新了尚未提交的事务1所读取的库存值。
如果线程1再次读取库存,与第一次读取的值是不同的,这就是 non-repeatable read. 因为事务会读取到同一个字段的不同值。
解决办法:Isolation.REPEATBALE_READ


底层数据对 Repeatable_READ 的实现:获取已读但尚未提交的行的读锁。
其他的事务在更新该行时需要等待,直到读锁释放.
读锁的释放出现在 加锁的事务提交或回滚时。
示例3: 幻读:一个事务从一张表读取几行后,另一个事务向同一张表插入几个新行.
第一个事务随后再此读取相同表,则发现数据变化.
解决:隔离级别最高 Serializable。. 在整张表上获取读锁.

public interface BookShop { void purchase(String isbn,String userName); void increaseStock(String isbn,int stock); public int checkShock(String isbn); }
@Component("bookShop")
@EnableAutoConfiguration
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
public JdbcBookShop(DataSource dataSource) {
setDataSource(dataSource);
}
@Transactional
@Override
public void purchase(final String isbn, final String userName) {
int price =
getJdbcTemplate()
.queryForObject("SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);
getJdbcTemplate().update("UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn);
getJdbcTemplate()
.update("UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, userName);
}
@Transactional
@Override
public void increaseStock(String isbn, int stock) {
String threadName = Thread.currentThread().getName();
System.out.println((threadName + " - Prepare to increase stock"));
getJdbcTemplate().update(
"UPDATE BOOK_STOCK SET STOCK = STOCK + ? WHERE ISBN = ?",
stock,isbn);
System.out.println((threadName + " - Book stock increased by " + stock));
sleep(threadName);
/* System.out.println((threadName + " - Book stock transaction rolled back"));
throw new RuntimeException("Increased by mistake");*/
int currentStock = getJdbcTemplate().queryForObject(
"SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?",
Integer.class,isbn);
System.out.println((threadName + " - current stock is " + currentStock));
}
@Transactional(isolation = Isolation.SERIALIZABLE)
@Override
public int checkShock(String isbn) {
String threadName = Thread.currentThread().getName();
System.out.println((threadName + " - Prepare to check book stock"));
int stock = getJdbcTemplate().queryForObject(
"SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?",
Integer.class,isbn);
System.out.println((threadName + " - Book stock is " + stock));
sleep(threadName);
return stock;
}
private void sleep(String threadName){
System.out.println((threadName + " - Sleeping"));
try{
Thread.sleep(10000);
}catch (InterruptedException e){
}
System.out.println(threadName+" - Wake up");
}
}
@SpringBootApplication @EnableTransactionManagement public class SpringTransactionApplication { public static void main(String[] args) { SpringApplication.run(SpringTransactionApplication.class, args); } @Component class App implements ApplicationRunner{ @Autowired BookShop bookShop; @Override public void run(ApplicationArguments args) throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(3); Runnable increaseTask = new Runnable() { @Override public void run() { try{ Thread.currentThread().setName("Thread 1"); bookShop.increaseStock("0001",5); }catch (RuntimeException e){} }; }; Runnable checkStockTask =new Runnable() { @Override public void run() { Thread.currentThread().setName("Thread 2"); bookShop.checkShock("0001"); } }; executorService.execute(checkStockTask); try{ Thread.sleep(5000); }catch (InterruptedException e){ } executorService.execute(increaseTask); } } }
回滚事务属性:
默认只有未检查异常 ( 即 RuntimeException 或 Error ) 会导致事务回滚,
而检查异常则不会.
更改设置:自定义哪些异常会导致事务回滚,未显式指定的任何异常都会按照默认规则处理.

超时 和 只读
事务需要获取行和表的锁,因此长时间事务会占用资源并对整体性能产生影响.
只读属性会让数据库引擎优化事务。
timeout 事务 整数,单位秒, 表示事务在强制回滚前会持续多久.防止长事务.
read-only 只读不更新数据,如果出现了写操作,资源不一定会失败。

加载期编织 管理事务
默认 Spring 声明式事务时通过 AOP 框架开启。限制: Ioc容器中的bean的 public 方法添加通知,使用 Spring AOP 只能在这个范围内管理事务.
如果要管理 非 public 方法,或在 Spring IoC 容器外创建的对象的事务。
则: AnnotationTransactionAspect 的 Aspect 切面. 可管理任何对象的任何方法事务。该切面会管理使用了 @Transactional 注解的任何方法的事务。
可选择 AspectJ 的编译期编织或加载期编织来启用该切面.
要将该切面在加载期织入到领域类中,配置类上 @EnableLoadTimeWeaving 注解
@EnableTransactionManagement注解 model 两个值
ASPECTJ : 容器应该使用加载期或编译期编织来启用事务通知,要求 spring-instrument JAR 来类路径上,同时在加载器和编译期配置。
PROXY: 容器要使用 Spring AOP 机制。
为切面提供一个事务管理器。 默认寻找 transactionManager 名的
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.7</version>
</dependency>
<!-- 启用加载期编织,需引入一个Java代理,位于以下模块-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>6.0.7</version>
</dependency>


浙公网安备 33010602011771号