一、基本概念
- 在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念
- 事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
- 事务四个特性(ACID)
- 原子性(atomicity):“原子”的本意是“不可再分”,要么都执行,要么都不执行。
- 一致性(consistency):“一致”指的是数据的一致,一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将数据恢复到事务执行之前的状态,这就是回滚。
- 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
- 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
二、Spring 事务管理
2.1 编程式事务
- 使用原生的JDBC API进行事务管理
- 获取数据库连接Connection对象
- 取消事务的自动提交
- ]执行操作
- 正常完成操作时手动提交事务
- 执行失败时回滚事务
- 关闭相关资源
- 评价:使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
2.2 声明式事务
- 大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
- 事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
- Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
- Spring既支持编程式事务管理,也支持声明式的事务管理
2.3 事务管理器
- Spring的核心事务管理抽象是
PlatformTransactionManager
。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。 - 事务管理器的主要实现
DataSourceTransactionManager
:在应用程序中只需要处理一个数据源,而且通过JDBC存取。JtaTransactionManager
:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理HibernateTransactionManager
:用Hibernate框架存取数据库
三、事务操作
- 事务一般添加到JavaEE三层结构里面Service层(业务逻辑层)
3.1 基于注解的配置
3.1.1 数据库
- 建立数据库
USE spring; DROP TABLE IF EXISTS account; CREATE TABLE account( id INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(30) DEFAULT NULL, money DECIMAL NOT NULL ); INSERT INTO account(`name`, money) VALUES ("Mary", 1000), ("Jerry", 1000);
3.1.2 工程目录
- 文件目录
3.1.3 配置文件
- 修改spring的配置文件的名称空间,添加以下内容
<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 http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd ">
- 开启组件注解扫描
<context:component-scan base-package="com.du.spring"/>
- 实例化数据库连接池
<!-- 实例化数据库连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url"> <value> <![CDATA[jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&rewriteBatchedStatements=true]]></value> </property> <property name="username" value="root"/> <property name="password" value="root"/> </bean>
- 实例化JdbcTemplate
<!--实例化JdbcTemplate--> <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean>
- 配置事务管理器
<!--配置事务管理器--> <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
- 开启事务注解
<!--开启事务注解--> <tx:annotation-driven transaction-manager="transactionManager"/>
3.1.4 Java文件
- 建立对应的Java Bean
@Data @NoArgsConstructor @AllArgsConstructor public class Account { private int id; String name; BigDecimal money; }
- DAO层
public interface AccountDao { /** * 修改账户余额,正为转入,负为转出 * @param id * @param money */ int changeAccount(int id, BigDecimal money); }
@Repository public class AccountDaoImpl implements AccountDao { JdbcTemplate jdbcTemplate; // 自动注入 @Override public int changeAccount(int id, BigDecimal money) { String sql = "update account set money = money + ? where id = ?"; return jdbcTemplate.update(sql, money, id); } @Autowired public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }
- Service层。
@Transactional
既可以在方法上标注也可以在类上做标注。@Service @Transactional // 事务注解,该类下面的所有方法都遵循事务 public class AccountService{ AccountDao accountDao; public int transfer(int fromId, int toId, BigDecimal money) { int res1 = accountDao.changeAccount(fromId, money.negate()); //System.out.println(10 / 0); int res2 = accountDao.changeAccount(toId, money); if (res1 > 0 && res2 > 0) return 1; else return 0; } @Autowired public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } }
注意:如果类有接口,事务的AOP会用JDK代理,然后就会报错…只能用没有接口的类,让AOP用cglib
- 测试
@Test public void transferTest() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean.xml"); AccountService accountService = context.getBean("accountService", AccountService.class); int res = accountService.transfer(1, 2, new BigDecimal("100")); if (res > 0) System.out.println("转账成功"); else System.out.println("转账失败"); }
四、@Transactional 添加事务注解
@Transactional
,可以添加在类上面,也可以添加到方法上面- 如果把这个注解添加类上面,这个类里面所有的方法都添加事务
- 如果把这个注解添加到方法上面,为这个方法添加事务
- 在这个注解内,可以添加一些参数
4.1 事务传播行为(propagation属性)
-
propagation
:事务传播行为(其实就是事务嵌套的处理方法)
-
常用的是
REQUIRED
和REQUIRED_NEW
。- 被标注为
REQUIRED
的相当于,如果大事务有开事务,就和它“坐在同一条船上”,如果中间翻车了,那就一起翻车,都不生效。 - 而如果标注为
REQUIRED_NEW
,就相当于自己开一条船,只要自己不翻车,完成了任务,就好立马提交,大事务翻车,不影响自己。(但是,如果还没轮到自己开,前面就翻车了,那就没得开,还是不行。)(如果自己翻车了,大事务没办法继续执行了,也会跟着翻车)
- 被标注为
-
上图的第一个是小事务所有都是
REQUIRED
的,第二个是所有小事务都是REQUIRED_NEW
的。 -
注意,大事务调用小事务的时候,不能在同一个类下面,不然就是简单的方法调用了。(因为事务是基于aop进行的,而aop是代理对象之后执行的,所以需要在类的外部拿到小事务所属类的代理对象,再调用,事务才会正常执行。)
4.2 隔离级别(isolation属性)
isolation
:事务隔离等级.多事务之间需要考虑隔离性,否则会出现一些问题.- 脏读:一个未提交事务读取到另一个未提交事务的数据,比如:
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表时,多出了一些行。
- 解决:设置隔离级别
- 读未提交:
READ UNCOMMITTED
,允许Transaction01
读取Transaction02
未提交的修改。 - 读已提交:
READ COMMITTED
,要求Transaction01
只能读取Transaction02
已提交的修改。 - 可重复读:
REPEATABLE READ
,确保Transaction01
可以多次从一个字段中读取到相同的值,即Transaction01
执行期间禁止其它事务对这个字段进行更新。 - 串行化:
SERIALIZABLE
,确保Transaction01
可以多次从一个表中读取到相同的行,在Transaction01
执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
4.3 超时(timeout属性)
timeout
:事务需要在一定时间内进行提交,如果不提交就会进行回滚。(输入的数字,单位为秒)
4.4 只读(read-only属性)
readOnly()
:是否只读。默认为false,表示可以查询,也可以添加修改操作;设置为true时,只能查询。- 设置成
read-only
效率会更高,但一定不能修改,否则会异常。
4.5 为…回滚
- 默认事务只会在遇到运行时异常
RuntimeException
或Error
时回滚,而捕获到编译时异常不回滚。 - 可以通过设置
rollbackFor
,添加某些编译时异常到触发回滚行列中。 - 设置
noRollbackFor
,将某些运行时异常从触发回滚的行列中删除。
五、基于XML文档的声明式事务配置
<!-- 配置事务切面 -->
<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>
- 默认数据库连接池、事务管理器都已经注册。目前需要对切面、切入点、事务管理器进行配置。
- 首先,如普通的aop一样,需要在
<aop:config>
中,对aop进行配置。通过<aop:pointcut>
设置切入点。但是,和往常设置切面不一样的是,在这不需要设置切面,而需要设置事务增强<aop:advisor>
。 - 通过
<tx:advice>
,设置事务增强的具体内容。需要绑定事务管理器。在<tx:attributes>
中设置具体的方法,在所有切入点中选择需要使用事务的方法,并且可以在其中设置一些注解中也有的属性。
六、完全注解开发
- 与xml配置的内容一致,只是将内容通过注解的方式配置
@Configuration @ComponentScan(basePackages = "com.du.spring") // 组件扫描 @EnableTransactionManagement // 开启事务管理器 public class TxConfig { // 实例化bean并返回 @Bean public DruidDataSource getDruidDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&rewriteBatchedStatements=true"); dataSource.setUsername("root"); dataSource.setPassword("root"); return dataSource; } @Bean public JdbcTemplate getJdbcTemplate(DruidDataSource dataSource) { // 将需要配置的对象放在形参中,会autowired到里面 JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } @Bean public DataSourceTransactionManager getDataSourceTransactionManager(DruidDataSource dataSource) { DataSourceTransactionManager manager = new DataSourceTransactionManager(); manager.setDataSource(dataSource); return manager; } }