文章目录
一、Spring中的事务
1、引出事务
银行转账案例: 需求: 从id为10086账户给id为10010账户转1000元
- 数据库表设计略;

- Java代码
// domain包
@Data
public class Account {
private Long id;
private int balance;
}
// Dao包
public interface AccountDao {
/**
* 从指定账户转出多少钱
* @param outId
* @param money
*/
void transOut(Long outId, int money);
/**
* 从指定账户转入多少钱
* @param inId
* @param money
*/
void transIn(Long inId, int money);
}
public class AccountDaoImpl implements AccountDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource ds){
this.jdbcTemplate = new JdbcTemplate(ds);
}
public void transOut(Long outId, int money) {
String sql = "UPDATE account SET balance = balance - ? WHERE id = ?";
jdbcTemplate.update(sql, money, outId);
}
public void transIn(Long inId, int money) {
String sql = "UPDATE account SET balance = balance + ? WHERE id = ?";
jdbcTemplate.update(sql, money, inId);
}
}
// Service包
public interface AccountService {
/**
* 从指定账户转出指定金额给另一个账户
* @param outId
* @param inId
* @param money
*/
void trans(Long outId, Long inId, int money);
}
public class AccountServiceImpl implements AccountService {
private AccountDao dao;
public void setDao(AccountDao dao) {
this.dao = dao;
}
public void trans(Long outId, Long inId, int money) {
dao.transOut(outId, money);
int a = 1 / 0; // 抛出异常
dao.transIn(inId, money);
}
}
xml文件
<context:property-placeholder location="classpath:db.properties"/>
<!--配置一个druid的连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialSize" value="${jdbc.initialSize}"/>
</bean>
<!--配置dao-->
<bean id = "accountDao" class="com.sunny.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置service-->
<bean id="accountService" class="com.sunny.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"/>
</bean>
测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringTxTest {
@Autowired
private AccountService service;
@Test
public void test(){
service.trans(10086L, 10010L, 1000);
}
}
结果


分析原因:

出现问题的原因:
转入和转出的操作不在同一个事务之中
public void trans(Long outId, Long inId, int money) {
dao.transOut(outId, money);
int a = 1 / 0; // 抛出异常
dao.transIn(inId, money);
}
dao.transOut(outId, money); 这在一个事务1里, dao.transIn(inId, money);在另一个事务2里, 出现数据不一致问题是因为, 转钱操作完成了, 事务1也结束, 钱扣了。此时 1 / 0 发生异常, 另一个账户钱也没收到。根本原因就是 两个操作不在同一个事务里, 如果在同一个事务, 发生异常后, 事务1回滚, 转钱就会失败,不会扣钱

2、事务回顾
Spring的事务机制是Spring给我们提供的一套事务管理的方式,项目中我们就不需要手动去控制事务了
编程式事务:我们的事务控制逻辑(增强逻辑)和业务逻辑混合在一起,比如我们之前的tcf模式控制转账事务,这种方式就叫做编程式事务
声明式事务:通过配置,在不侵犯原有业务逻辑代码的基础上就添加了事务控制功能,这种方式叫做声明式事务(我们这里的Spring声明式事务控制就是通过AOP达到这个目的的)

关于事务
事务(Transaction,简写为tx):在数据库中,事务是指一组逻辑操作,不论成功与失败都作为一个整体进行工作,要么全部执行成功,要么全部不执行(执行失败)。
-
事务基本特性(ACID,是针对单个事务的一个完美状态)
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性:事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。
- 如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。
- 如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
- 隔离性:指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
- 持久性:事务一旦提交,事务提交,变化即生效。即使数据库服务器宕机,那么恢复之后,数据也应该是事务提交之后的状态,不应该回滚到以前了。
-
事务并发问题
-
脏读 (事务未提交,撤回事务)
财务人员今天心情不好,状态不好,误操作发起事务1给员工张三本月涨了1w块钱工资,但是还没有提交事务
张三发起事务2,查询当月工资,发现多了1W块钱,涨工资了,财务人员发现不对劲,把操作撤回,把涨工资的事务1给回滚了 -
幻读(幻读出现在增加insert和删除delete的时候)
- 比如事务1查询工资表中工资为1w的员工的个数(10个员工),此时事务1还没有结束
- 正在这个时候,事务2,人力部门有两个新员工入职,他们的工资也是1w,人力部门通过事务2向工资表插入了两条记录,并且提交事务了
- 这个时候,事务1又去查询工资为1w的员工个数,发现多了两个员工(12个人),见鬼了,这种情况就叫做 幻读
-
不可重复读(出现在修改update的时候)
- 员工发起事务1查询工资,工资为1w,事务1尚未关闭
- 人力部门发起事务2给你涨了工资,涨工资到1.2W(update你的工资表的字段信息),并且提交了事务了。
- 此时,事务1又再次查询自己的工资,发现工资为1.2W,原有的1w这个数据已经读不到了,这就叫做不可重复读
-
-
事务隔离级别(解决是事务并发问题的)
由低到高分别为 Read uncommitted > Read committed > Repeatable read > Serializable 。- Read_uncommited (读未提交),就好比十字路口没有红绿灯一样,效率高,但是风险也高,此时什么事务控制都没有。不要使用这种模式
- Read_commited (读已提交),顾名思义,其他事务提交之后,才能读取到这个事务提交的数据,这种模式能解决脏读(因为脏读事务没提交造成的)问题,解决不了幻读和不可重复读(因为这两个问题的产生就是insert delete update的时候提交了事务造成)
- Repeatable_Read (可重复读),可重复读解决脏读和不可重复读
- Serializable (可序化):所有的事务一个个来,不争不抢,一个事务处理完了,另外一个事务继续进行,这样不会出现并发问题。比如ATM机
MySQL数据库默认隔离级别可重复读
Repeatable_Read
Oracle数据库默认级别读已提交Read_commited -
设置事务隔离级别
- 1 read uncommitted 未提交读,脏读,不可重复读,虚读都可能发生.
- 2 read committed 已提交读,避免脏读,但是不可重复读和虚读有可能发生(Oracle默认)
- 4 repeatable read 可重复读,避免脏读,不可重复读,但是虚读有可能发生(MySql默认)
- 8 serializable 串行化的,避免脏读,不可重复读,虚读的发生
- 查看当前的事务隔离级别:SELECT @@TX_ISOLATION;
- 更改当前的事务隔离级别:SET TRANSACTION ISOLATION LEVEL 四个级别之一
隔离级别越高,数据库事务并发执行性能会越差,在项目中为了考虑并发性能一般使用 Read committed(读已提交),它能避免丢失更新和脏读,但是不可重复读和幻读不可避免。更多情况下使用悲观锁或乐观锁来解决。
一、基于 xml 配置声明式事务 (了解)
1、解决银行转账问题
在上面引出事务的代码基础上,只需要修改xml配置文件
<?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"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
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
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--从classpath的根路径去加载db.properties文件-->
<!--<context:property-placeholder location="classpath:db.properties" system-properties-mode="NEVER"/>-->
<context:property-placeholder location="classpath:db.properties"/>
<!--配置一个druid的连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialSize" value="${jdbc.initialSize}"/>
</bean>
<!--配置dao-->
<bean id = "accountDao" class="com.sunny.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置service-->
<!-- --------------原始功能---------------- -->
<bean id="accountService" class="com.sunny.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"/>
</bean>
<!-- ================================配置声明式事务==================================== -->
<!-- 1: 配置JDBC事务管理器 WHAT:做什么增强(这里做事务增强)-->
<!-- --------------额外功能---------------- -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 2: 配置事务管理器增强 WHEN-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="trans"/>
</tx:attributes>
</tx:advice>
<!-- 3: 配置切面 WHERE-->
<!-- --------------组装切面(切入点+额外功能)---------------- -->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.sunny.service.*Service.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
<!-- ====================================================================== -->
</beans>
测试


成功!
2、CRUD通用事务配置
<!--配置一个CRUD的通用事务的配置-->
<tx:advice id="crudAdvice" transaction-manager="txManager">
<tx:attributes>
<!--service中的查询方法-->
<tx:method name="get*" read-only="true" propagation="REQUIRED"/>
<tx:method name="list*" read-only="true" propagation="REQUIRED"/>
<tx:method name="query*" read-only="true" propagation="REQUIRED"/>
<!--service中其他方法(非查询)-->
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
上面设置的属性都是什么意思, 下面为讲解
二、基于注解配置声明式事务 (重点)
1、准备工作
①添加配置
在beans.xml添加配置
<!--扫描组件-->
<context:component-scan base-package="com.atguigu.spring6"></context:component-scan>
②创建表
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);
③创建组件
创建BookController:
package com.atguigu.spring6.controller;
@Controller
public class BookController {
@Autowired
private BookService bookService;
public void buyBook(Integer bookId, Integer userId){
bookService.buyBook(bookId, userId);
}
}
创建接口BookService:
package com.atguigu.spring6.service;
public interface BookService {
void buyBook(Integer bookId, Integer userId);
}
创建实现类BookServiceImpl:
package com.atguigu.spring6.service.impl;
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
}
}
创建接口BookDao:
package com.atguigu.spring6.dao;
public interface BookDao {
Integer getPriceByBookId(Integer bookId);
void updateStock(Integer bookId);
void updateBalance(Integer userId, Integer price);
}
创建实现类BookDaoImpl:
package com.atguigu.spring6.dao.impl;
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
}
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
}
@Override
public void updateBalance(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql, price, userId);
}
}
2、测试无事务情况
①创建测试类
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(locations = "classpath:beans.xml")
public class TxByAnnotationTest {
@Autowired
private BookController bookController;
@Test
public void testBuyBook(){
bookController.buyBook(1, 1);
}
}
②模拟出现错误的场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
③观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
3、加入事务
①添加事务配置
在spring配置文件中引入tx命名空间
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
在Spring的配置文件中添加配置:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--
开启事务的注解驱动
通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
-->
<!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager" />
②添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
在BookServiceImpl的buybook()添加注解 @Transactional
③观察结果
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
4、@Transactional注解标识的位置
@Transactional标识在方法上,则只会影响该方法
@Transactional标识的类上,则会影响类中所有的方法
5、事务属性:只读 (readOnly)
①介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
②使用方式
@Transactional(readOnly = true) // 查询设置只读
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
//System.out.println(1/0);
}
③注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
6、事务属性:超时 (timeout)
①介绍
事务超时时间, 如果超时就回滚, 释放资源
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
②使用方式
//超时时间单位秒
@Transactional(timeout = 3)
public void buyBook(Integer bookId, Integer userId) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
//System.out.println(1/0);
}
③观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022
7、事务属性:回滚策略
①介绍
声明式事务 默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
-
rollbackFor属性:需要设置一个Class类型的对象
-
rollbackForClassName属性:需要设置一个字符串类型的全类名
-
noRollbackFor属性:需要设置一个Class类型的对象
-
rollbackFor属性:需要设置一个字符串类型的全类名
②使用方式
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
System.out.println(1/0);
}
③观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行
8、事务属性:隔离级别 (默认数据库的隔离级别)
①介绍
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
-
读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
-
读已提交:READ COMMITTED、
要求Transaction01只能读取Transaction02已提交的修改。
-
可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
-
串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 有 | 有 | 有 |
| READ COMMITTED | 无 | 有 | 有 |
| REPEATABLE READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |
各种数据库产品对事务隔离级别的支持程度:
| 隔离级别 | Oracle | MySQL |
|---|---|---|
| READ UNCOMMITTED | × | √ |
| READ COMMITTED | √(默认) | √ |
| REPEATABLE READ | × | √(默认) |
| SERIALIZABLE | √ | √ |
②使用方式
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
9、事务属性:传播行为
①介绍
什么是事务的传播行为?
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
一共有七种传播行为:
REQUIRED:支持当前事务,如果不存在就新建一个(默认) 【没有就新建,有就加入】- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行 【有就加入,没有就不管了】
- MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常 【有就加入,没有就抛异常】
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起 【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
- NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务 【不支持事务,存在就挂起】
- NEVER:以非事务方式运行,如果有事务存在,抛出异常 【不支持事务,存在就抛异常】
- NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
②测试
创建接口CheckoutService:
package com.atguigu.spring6.service;
public interface CheckoutService {
void checkout(Integer[] bookIds, Integer userId);
}
创建实现类CheckoutServiceImpl:
package com.atguigu.spring6.service.impl;
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Autowired
private BookService bookService;
@Override
@Transactional
//一次购买多本图书
public void checkout(Integer[] bookIds, Integer userId) {
for (Integer bookId : bookIds) {
bookService.buyBook(bookId, userId);
}
}
}
在BookController中添加方法:
@Autowired
private CheckoutService checkoutService;
public void checkout(Integer[] bookIds, Integer userId){
checkoutService.checkout(bookIds, userId);
}
在数据库中将用户的余额修改为100元
③观察结果
可以通过@Transactional中的propagation属性设置事务传播行为
修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性
@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。
经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。
10、全注解配置事务
①添加配置类
package com.atguigu.spring6.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@ComponentScan("com.atguigu.spring6")
@EnableTransactionManagement
public class SpringConfig {
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
②测试
import com.atguigu.spring6.config.SpringConfig;
import com.atguigu.spring6.controller.BookController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
public class TxByAllAnnotationTest {
@Test
public void testTxAllAnnotation(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
BookController accountService = applicationContext.getBean("bookController", BookController.class);
accountService.buyBook(1, 1);
}
}
三、基于纯注解(JavaConfig)配置声明式事务 (重重点)
@Configuration标识当前类是Spring的一个配置类@ComponentScan替代xml中的<context:component-scan/>@Import引入其他配置类,被引入的配置类可以不加@Configuration注解@PropertySource:引入外部properties文件,注意加classpath:@Value对成员变量赋值@Bean将一个方法的返回值对象加入到Spring的容器当中管理@Qualifier可以使用在方法上,表明对应的形参引入/注入的对象类型
直接删除xml的配置文件,取而代之的是一个Config类
@Transactional注解,取代tx标签
@EnableTransactionManagement注解,开启事务注解
@Configuration标识当前类为一个配置类, 当前项目的配置类,好比是applicationContext.xml@Import(Xxx.class)在主配置类中包含Xxx的配置类@PropertySource("classpath:db.properties")读取配置文件@Bean("Xxx")相当于<bean id="Xxx" class="">, Bean()中不写参数,默认就是该方法创建的对象; 该对象就被Spring容器所管理了

Java代码
//@Repository("accountDaoImpl")
@Repository // 默认是 accountDaoImpl,相当于类名首字母小写,相当于<bean id="accountDaoImpl" class=""/>
public class AccountDaoImpl implements AccountDao {
private JdbcTemplate jdbcTemplate;
@Autowired // setting方法注入
public void setDataSource(DataSource ds){
this.jdbcTemplate = new JdbcTemplate(ds);
}
public void transOut(Long outId, int money) {
String sql = "UPDATE account SET balance = balance - ? WHERE id = ?";
jdbcTemplate.update(sql, money, outId);
}
public void transIn(Long inId, int money) {
String sql = "UPDATE account SET balance = balance + ? WHERE id = ?";
jdbcTemplate.update(sql, money, inId);
}
}
@Service
@Transactional
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao dao;
public void trans(Long outId, Long inId, int money) {
dao.transOut(outId, money);
//int a = 1 / 0; // 抛出异常
dao.transIn(inId, money);
}
}
Java配置类
//当前项目的配置类,好比是applicationContext.xml
@Configuration //标识当前类为一个配置类
@Import(DataSourceConfig.class) //包含其他的配置类
@ComponentScan("com.sunny") //IoC注解解析器
@EnableTransactionManagement//事务注解解析器
public class JavaConfig {
//创建事务管理的Bean
@Bean
public DataSourceTransactionManager txManager(DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}
// 当前项目的连接池的配置类
@Configuration
@PropertySource("classpath:db.properties")
public class DataSourceConfig {
// 将properties的内容注入到这些变量中
@Value("${jdbc.driverClassName}")
private String driverClassName;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Value("${jdbc.initialSize}")
private int initialSize;
//创建连接池的Bean
@Bean("dataSource")
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driverClassName);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
ds.setInitialSize(initialSize);
return ds;
}
}
测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=JavaConfig.class)
public class SpringTxTest {
@Autowired
private AccountService service;
@Test
public void test1(){
service.trans(10086L, 10010L, 1000);
}
}
成功!
使用纯注解和半注解和XML的对比图:



四、选择开发方式 (纯注解开发)

五、Spring的自动装配 (无需@Autowird)

本文深入讲解Spring中的事务管理,包括基于XML配置、注解配置及纯注解(JavaConfig)配置声明式事务的方法,对比不同配置方式的特点,帮助开发者选择最适合的开发方式。

350

被折叠的 条评论
为什么被折叠?



