一、事务概述
-
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行,为了保证数据的完整性和一致性。
-
事务的四个关键属性(ACID)
-
原子性(
atomicity
):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。 -
一致性(
consistency
):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。 -
隔离性(
isolation
):在应用程序实际运行过程中,事务之间是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。 -
持久性(
durability
):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
二、Spring事务管理
2.1、编程式事务管理
使用原生的JDBC API进行事务管理
- 获取数据库连接Connection对象
- 取消事务的自动提交
- 执行操作
- 正常完成操作时手动提交事务
- 执行失败时回滚事务
- 关闭相关资源
使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚
。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
2.2、声明式事务管理
通过配置的形式,基于AOP的方式,动态的把事务管理的代码作用的目标方法上面。
大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理
。
事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。 Spring既支持编程式事务管理,也支持声明式的事务管理。
2.3、Spring提供的事务管理器
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。Spring的核心事务管理抽象是PlatformTransactionManager
。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。 事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
2.4、事务管理器的主要实现
-
DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。
-
JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理。
-
HibernateTransactionManager:用Hibernate框架存取数据库。
三、测试数据准备
3.1、需求
public interface BookShopDao { /** * 根据书号查询书的价格 */ Book findBookPriceByIsbn(String isbn); /** * 根据书号查询书的价格 * @param isbn * @param stock * @return */ int updateBookStock(String isbn); /** * 根据书号查询书的价格 * @param username * @param balance * @return */ int updateAccount(String username, Integer price); }
@Repository public class BookShopDaoImpl implements BookShopDao { @Autowired private JdbcTemplate template; @Override public Book findBookPriceByIsbn(String isbn) { String sql = "SELECT * FROM book WHERE isbn=?"; RowMapper<Book> rowMapper = new BeanPropertyRowMapper<>(Book.class); return template.queryForObject(sql, rowMapper, isbn); } @Override public int updateBookStock(String isbn) { //判断库存是否足够 String sql = "SELECT stock from book_stock WHERE isbn = ?"; Integer bookCount = template.queryForObject(sql, Integer.class, isbn); if (bookCount <= 0) { throw new RuntimeException("库存不足,没有书了"); } sql = "UPDATE book_stock SET stock = stock-1 WHERE isbn = ?"; return template.update(sql, isbn); } @Override public int updateAccount(String username, Integer price) { //判断余额是否足够 String sql = "SELECT balance from account WHERE username = ?"; Integer balance = template.queryForObject(sql, Integer.class, username); if (balance <= price) { throw new RuntimeException("余额不足"); } sql = "UPDATE account SET balance = balance - ? WHERE username = ?"; return template.update(sql, price, username); } }
public interface BookShopService { Book buyBook(String isbn, String username); }
@Service("bookShopServiceImpl") public class BookShopServiceImpl implements BookShopService { @Autowired private BookShopDao bookShopDao; /** * 买书 :账户余额减少,库存减少 * @param isbn * @param username * @param stock * @param balance * @return */ @Transactional @Override public Book buyBook(String isbn, String username) { //根据书号查询书的价格 Book book = bookShopDao.findBookPriceByIsbn(isbn); String price = book.getPrice(); //判断库存是否足够 int updateBookStockResult = bookShopDao.updateBookStock(isbn); //判断余额是否足够 int updateAccountResult = bookShopDao.updateAccount(username,Integer.valueOf(price)); return book; } }
- 配置声明式事务application_tx.xml
<!--包扫描--> <context:component-scan base-package="com.jdy.spring2020.scan"/> <!--引入外部配置文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 一、数据源--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/> <property name="driverClass" value="${jdbc.driverClass}"/> <property name="user" value="${jdbc.user}"/> <property name="password" value="${jdbc.password}"/> </bean> <!--二、JDBC模板--> <bean id="template" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 三、配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 四、启用事务注解 transaction-manager:用来指定事务管理器,如果事务管理器的id值时transaction-manager,可以省略不进行指定 --> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
- 测试
public class TX_Test { private BookShopDao bookShopDao; private BookShopService bookShopService; { ApplicationContext context = new ClassPathXmlApplicationContext("application_tx.xml"); bookShopService= context.getBean("bookShopServiceImpl", BookShopServiceImpl.class); bookShopDao= context.getBean("bookShopDaoImpl", BookShopDaoImpl.class); } /** * 买书 */ @Test public void test_method01() { bookShopService.buyBook("ISBN-001","Jerry"); } }
假设BookShopDaoImpl.buyBook上没有@Transactional注解,通过改变库的数据,是Jerry的账户金额不足买一本书,运行测试方法后,可以看到书本的数量减少,但是账户金额没有变,
加上@Transactional注解后,进行实物控制后,整个buyBook方法成功才会出现,余额减少,数量减少。
- 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
- 事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。
- 事务传播属性可以在@Transactional注解的propagation属性中定义。
传播属性 | 描述 |
Required | 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行【使用调用者的事务】 |
Required_new | 当前的方法必须启动新事物,并在自己的事务内部运行,如果有事务在运行,应该将他挂起【将调用者的事务挂起,重启开启事务来使用】 |
Support | 如果有事务,就在事务内运行,如果没有,可以不再事务内运行 |
Not_ support | 当前方法不应该运行在事务中,如果有事务,将事务挂起 |
Mandatory | 当前方法必须在事务内部运行,如果没有实物,就抛出异常 |
Never | 当前事务不应该在事务中运行,如果有就抛出异常 |
Nested | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行。 |
@Service("cashierimpl") public class Cashierimpl implements Cashier { @Autowired private BookShopService bookShopService; //结账操作,买了多本书,账户余额只够买一本 @Transactional @Override public void checkOut(List<String> isbns, String username) { for (String isbn : isbns) { Book book = bookShopService.buyBook(isbn,username); } } }
public interface Cashier { /** * 收银台结算 * @param isbns * @param username */ void checkOut(List<String> isbns, String username); }
@Service("cashierimpl")
public class Cashierimpl implements Cashier { @Autowired private BookShopService bookShopService; @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void checkOut(List<String> isbns, String username) { for (String isbn : isbns) { Book book = bookShopService.buyBook(isbn,username); } } }
5.1、数据库事务并发问题
假设现在有两个事务:Transaction01和Transaction02并发执行。
-
脏读
-
Transaction01将某条记录的AGE值从20修改为30。
-
Transaction02读取了Transaction01更新后的值:30。
-
Transaction01回滚,AGE值恢复到了20。
-
Transaction02读取到的30就是一个无效的值。
-
-
不可重复读
-
Transaction01读取了AGE值为20。
-
Transaction02将AGE值修改为30。
-
Transaction01再次读取AGE值为30,和第一次读取不一致。
-
-
幻读
-
Transaction01读取了STUDENT表中的一部分数据。
-
Transaction02向STUDENT表中插入了新的行。
-
Transaction01读取了STUDENT表时,多出了一些行。
-
5.2、隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
-
读未提交:READ UNCOMMITTED 允许Transaction01读取Transaction02未提交的修改。(问题:脏读)
-
读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。(问题:不可重复读(修改数据问题))
-
可重复读(默认):REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。(问题:幻读(增加数据问题))
-
串行(xing)化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下
-
各个隔离级别解决并发问题的能力见下表
脏读 | 不可重复读 | 幻读 | |
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
5.3、在Spring中指定事务隔离级别
-
注解
用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别
public class Cashierimpl implements Cashier { @Autowired private BookShopService bookShopService; @Override @Transactional(propagation = Propagation.REQUIRES_NEW ,isolation= Isolation.READ_COMMITTED) public void checkOut(List<String> isbns, String username) { for (String isbn : isbns) { Book book = bookShopService.buyBook(isbn,username); } } }
六、触发事务回滚的异常
默认情况,捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。通过注解@Transactional设置回滚的异常
-
-
rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个.
-
noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个
-
public class Cashierimpl implements Cashier { @Autowired private BookShopService bookShopService; @Override @Transactional(propagation = Propagation.REQUIRES_NEW ,isolation= Isolation.READ_COMMITTED, rollbackFor = {IOException.class, SQLException.class}, noRollbackFor= {NullPointerException.class} ) public void checkOut(List<String> isbns, String username) { for (String isbn : isbns) { Book book = bookShopService.buyBook(isbn,username); } } }
1、注解@Transactional
public class Cashierimpl implements Cashier { @Autowired private BookShopService bookShopService; @Override @Transactional(propagation = Propagation.REQUIRES_NEW ,isolation= Isolation.READ_COMMITTED, rollbackFor = {IOException.class, SQLException.class}, noRollbackFor= {NullPointerException.class}, readOnly = true, timeout = 30 ) public void checkOut(List<String> isbns, String username) { for (String isbn : isbns) { Book book = bookShopService.buyBook(isbn,username); } } }
<!-- 配置事务切面 --> <aop:config> <aop:pointcut expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))" id="txPointCut"/> <!-- 将切入点表达式和事务属性配置关联到一起 --> <aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/> </aop:config> <!-- 配置基于XML的声明式事务 --> <tx:advice id="myTx" transaction-manager="transactionManager"> <tx:attributes> <!-- 设置具体方法的事务属性 --> <tx:method name="find*" read-only="true"/> <tx:method name="get*" read-only="true"/> <tx:method name="purchase" isolation="READ_COMMITTED" no-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException" propagation="REQUIRES_NEW" read-only="false" timeout="10"/> </tx:attributes> </tx:advice>