基于注解的声明式事务
具体操作:先把价格查出来,在更新图书的库存,最后更新用户的余额,
也有可能库存不够的情况,或者余额不足,通过余额不够的情况测试声明式事务。
如果库存或余额不够的话,数据库层面不会报错,所以不符合业务逻辑,
所以得库存或者余额不够就不能让买,这就符合了。提交或者回滚的时候有没有异常的。
大部分情况都是因为业务逻辑问题需要进行回滚,也不会出现异常,
从两个方面解决:
①从数据库层面解决 :为当前字段设置关键字unsigned无符号,就是没有负号的意思
所以就可以从sql层面抛出异常。所以就可以以业务逻辑问题生异常,
最终实现一个回滚和提交的效果。
②java代码层面解决:可以在更新库存的时候判断库存够不够,如果够直接操作更新库存,
如果不够,手动抛出运行时异常。
所以事务就是来判断核心操作有没有异常,如果有异常就回滚,没有异常就提交。
如果是业务逻辑方面问题,需要从数据库或者java代码来给设置一个异常。
无事务功能实现
①创建表
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);
②创建实体类
③创建组件
Controller层
service接口
public interface BookService {
/**
* 买书操作
* @param userId
* @param bookId
*/
void buyBook(Integer userId, Integer bookId);
}
实现类
BookDao接口
/**
* 默认情况下,一个sql语句独占一个事务,且自动提交
* 所以没有设置事务的情况下,三个sql语句独占一个事务且自动提交的。
* 在mysql中,一个sql独占一个事务且自动提交
* 所以得关闭事务提交,如果不关闭每个sql语句独占一个事务
* 所以更新图书的库存成功,更新用户余额失败抛出sql异常
*/
public interface BookDao {
//1.根据图书id查询图书的价格
Integer getPriceByBookId(Integer bookId);
//2.更新图书的库存
void updateStock(Integer bookId);
//3.更新用户的余额
void updateBalance(Integer userId, Integer price);
}
实现类
④测试类
⑤模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
⑥观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
实现加入事务功能
@Transactional
不需要手动创建事务的切面,也不需要写通知的,
因为在spring中,提供了有事务管理切面和通知,叫做事务管理器。
三层架构中,是在service设置事务管理。
①基于注解的声明式事务的配置
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
...
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- ① 配置事务管理器(切面) -->
<!--
DataSourceTransactionManager: 前面单词处理事务必须有数据源,数据源管理连接的
事务相关代码都是通过连接对象设置的,所以事务管理必须依赖数据源的
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--
② 开启事务的注解驱动
注意:导入的名称空间需要 tx 结尾的那个。
作用是将当前事务管理器把切面里面的通知作用到当前连接点上
@Transactional注解:所标识的方法或类中所有的方法使用事务进行管理
就是把通知作用于连接点上的,注解加到哪个方法上就是连接点,
或者加到类上如果加载类上这个类中所有方法都是连接点
-->
<!-- 颜色变灰色用的就是默认值-->
<tx:annotation-driven transaction-manager="transactionManager"/>
② (添加事务注解)事务管理的方法上添加@Transactional
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
/**
* 声明式事务的配置步骤:
* 1.在spring的配置文件中配置事务管理器
* 2.开启事务的注解驱动
* 在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理
* @Transactional注解:标识的位置:①方法 ②类 :类中所有方法都被事务管理
* 实现的效果:要么都成功,要么都失败
*/
@Transactional
public void buyBook(Integer userId, Integer bookId) {
....
}
}
③观察结果
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行。
事务属性
1 . 只读
①介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,
这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
②使用方式
@Transactional(readOnly = true)
③注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification
are not allowed
2 . 超时
①介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,
从而长时间占用数据库资源。而长时间占用资源,
大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,
让其他正常程序可以执行。
@Transactional(
//超时时间是几秒,如果3秒内没执行完,当前事务强制回滚,并抛出异常
timeout = 3
)
public void buyBook(Integer userId, Integer bookId) {
//程序休眠
//SECONDS选的单位是谁:例如下面就是5秒
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
...
}
②观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out:
deadline was Fri Jun 04 16:25:39 CST 2022
3 . 回滚策略
①介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
默认的回滚策略就是任何的运行时异常都会造成回滚。
可以通过@Transactional中相关属性设置回滚策略
rollbackFor属性:需要设置一个Class类型的对象
rollbackForClassName属性:需要设置一个字符串类型的全类名
noRollbackFor属性:需要设置一个Class类型的对象
rollbackFor属性:需要设置一个字符串类型的全类名
@Transactional(
//noRollbackFor = {ArithmeticException.class}
// noRollbackForClassName = "java.lang.ArithmeticException"
//设置的是会造成回滚的异常,一般不设置,因为默认情况所以的运行时异常都会造成回滚
)
②观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),
但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,
因此购买图书的操作正常执行。
4 . 事务隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。
一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,
不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
①读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
②读已提交:READ COMMITTED、
要求Transaction01只能读取Transaction02已提交的修改。
③可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,
即Transaction01执行期间禁止其它事务对这个字段进行更新。
④串行化:SERIALIZABLE确保Transaction01可以多次从一个表中读取到相同的行,
在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。
可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:

@Transactional(
isolation = Isolation.DEFAULT //默认隔离级别
)
5 . 事务传播行为
①介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
例如:

现在有个结账的方法checkout,结账的方法要买两本书,每次买书的操作都是一个buybook的方法,
那checkout方法执行两次buybook方法,所以当前checkout方法就可以加上事务的注解,
那buybook方法本身也有事务的方法,那buybook到底用谁的,如果用结账的事务那么整个结账的事务算是一个事务,
所以买了这两本书中,第一本书买成功,第二本失败,只要有一本买不成功,那么当前结账的操作都要进行回滚,
所以只要有一本买不了,那都买不了,因为回滚的都是整个结账的操作。如果现在使用的是买书的操作,
哪一本书买不成功,只是在当前买书操作中进行回滚,那第一本书成功了,第二本书不成功只在当前买书的操作中回滚,
对买成功的书没有影响。
默认情况下用的是结账的事务。
创建接口CheckoutService:
public interface CheckoutService {
/**
* 结账
*/
void checkout(Integer userId, Integer[] bookIds);
}
创建实现类CheckoutServiceImpl:
@Service
public class CheckoutServiceIml implements CheckoutService {
@Autowired
private BookService bookService;
@Override
@Transactional
//一次购买多本图书
public void checkout(Integer userId, Integer[] bookIds) {
for (Integer bookId : bookIds) {
bookService.buyBook(userId,bookId);
}
}
}
在BookServiceImpl添加买书的操作事务
@Transactional(
//传播行为
//REQUIRES_NEW:使用自己的事务
propagation = Propagation.REQUIRES_NEW
)
在BookController中添加方法:
@Autowired
private CheckoutService checkoutService;
//买多本书
public void checkout(Integer userId,Integer[] bookIds){
checkoutService.checkout(userId,bookIds);
}
浙公网安备 33010602011771号