事务的属性
只读
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化
BookServiceImpl.java
java
@Override
@Transactional(readOnly = true)
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
}
对于增删改操作会抛出下面异常:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
超时
超时回滚,释放资源
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行
BookServiceImpl.java
java
@Override
@Transactional(timeout = 3) //默认为-1,代表一直等待
public void buyBook(Integer bookId, Integer userId) {
try { //让程序休眠5秒触发超时
TimeUnit.SECONDS.sleep(5); //SECONDS代表"秒",可以换成MINUTES、HOURS其他计量单位
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
}
执行后会自动回滚并抛出异常:org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Sat Feb 04 17:34:43 CST 2023
回滚策略
声明式事务默认只针对运行时异常回滚 ,编译时异常不回滚
可以通过@Transactional中相关属性设置回滚策略(rollback:因为什么回滚
,noRollback:不因什么而回滚
),默认策略为任何运行异常都会导致回滚
- rollbackFor属性:需要设置一个Class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名
- noRollbackFor属性:需要设置一个Class类型的对象
- noRollbackForClassName属性:需要设置一个字符串类型的全类名
BookServiceImpl.java
java
@Override
//出现数学运行异常时不进行回滚
@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
//另外一种写法:noRollbackFor = {ArithmeticException.class}
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
System.out.println(1/0);
}
默认策略会进行回滚,但是这里设置了不进行回滚,最终还是购买成功(购买图书2)
隔离级别
数据库系统必须具有隔离并发运行各个事务的能力 ,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱
隔离级别一共有四种:
- 读未提交:READ UNCOMMITTED,允许Transaction01读取Transaction02未提交的修改(可以读取到未提交事务的信息)-->出现脏读
- 读已提交:READ COMMITTED,要求Transaction01只能读取Transaction02已提交的修改(可以读取到已提交事务的信息)-->不可重复读
- 可重复读:REPEATABLE READ,确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务 对这个字段进行更新 -->出现幻读(MySQL的可重复读避免了这种情况)
- 串行化:SERIALIZABLE,确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作(可以避免任何并发问题,但性能十分低下)
各个隔离级别解决并发问题的能力见下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
脏读:读取出来的数据没有任何意义-->读取到未提交事务的信息,信息最后进行了回滚
无论是脏写还是脏读,都是因为一个事务去更新或者查询了另外一个还没提交的事务更新过的数据。因为另外一个事务还没提交,所以它随时可能会回滚,那么必然导致你更新的数据就没了,或者你之前查询到的数据就没了,这就是脏写和脏读两种场景
幻读:幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的"全部数据行"。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入"一行新数据"。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读
各种数据库产品对事务隔离级别的支持程度:
隔离级别 | Oracle | MySQL |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
使用方式
java
@Transactional(isolation = Isolation.DEFAULT) //使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED) //读未提交
@Transactional(isolation = Isolation.READ_COMMITTED) //读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ) //可重复读
@Transactional(isolation = Isolation.SERIALIZABLE) //串行化
传播行为
当事务方法被另一个事务方法调用 时,必须指定事务应该如何传播,这就是事务的传播行为。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行
创建CheckOutService.java
java
package com.atguigu.tx.service;
public interface CheckOutService {
//购买多本书,并进行结账
void checkout(Integer userId, Integer[] bookIds);
}
创建对应的实现类CheckOutServiceImpl.java
java
package com.atguigu.tx.service.impl;
import com.atguigu.tx.service.BookService;
import com.atguigu.tx.service.CheckOutService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CheckOutServiceImpl implements CheckOutService {
//在这里调用已有service对象中的dao类实现复用即可
@Autowired
private BookService bookService;
@Override
@Transactional //默认使用的是"买多本书"的事务,并不是"买一本书"的事务,也就是这里购买多本书,只要一本书买失败了就全部回滚
public void checkout(Integer userId, Integer[] bookIds) {
for(Integer bookId : bookIds){
bookService.buyBook(userId,bookId);
}
}
}
BookController.java
java
@Autowired
private CheckOutService checkOutService;
public void checkout(Integer userId,Integer[] bookIds){
checkOutService.checkout(userId,bookIds);
}
因为是Dao类方法的复用,所以不用添加新的方法,将账户余额修改为100(只够购买一本书),再进行运行测试
TxByAnnotationTest.java
java
@Test
public void testCheckOutBook(){
bookController.checkout(1,new Integer[]{1,2});
}
这里默认使用的是自身方法的事务,所以会因为余额不足而回滚,最终没有成功购买任何一本书
如果需要使用"买一本书"的事务,则需要设置传播行为,可以通过@Transactional
中的propagation
属性设置事务传播行为
java
@Transactional(noRollbackForClassName = "java.lang.ArithmeticException",
propagation = Propagation.REQUIRES_NEW)//设置事务传播行为为自身的"买一本书"的事务
@Transactional(propagation = Propagation.REQUIRED)
为默认情况:表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW)
:表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本