Spring的事务使用教程
什么是事务?
事务(Transaction)是数据库操作最基本单元,逻辑上一组操作,要么都成功,要么都失败,如果操作之间有一个失败所有操作都失败 。
事务四个特性(ACID)
- 原子性
一组操作要么都成功,要么都失败。 - 一致性
一组数据从事务1合法状态转为事务2的另一种合法状态,就是一致。 - 隔离性
事务1操作数据,不影响事务2的操作,每一个事务之间都是隔离状态。 - 持久性
数据从内存加载到磁盘文件系统中,就是持久化。
事务的传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
传播行为 | 描述 |
---|---|
REQUIRED | 如果有事务在运行,当前的方法就在这个事务中运行,否则,启动新事务并在其中运行 |
REQUIRED_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行,如果有其它事务正在运行,则将它挂起 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中 |
NOT_SUPPORTED | 当前的方法不应该运行在事务中,如果有事务在运行,将它挂起 |
MANDATORY | 当前方法必须运行在事务内,如果没有正在运行的事务,就抛出异常 |
NEVER | 当前的方法不应该运行在事务内,如果有运行的事务,就抛出异常 |
NESTED | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动新事务,在其内部运行 |
事务的隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
-
读未提交:
READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。 -
读已提交:
READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。 -
可重复读:
REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。 -
串行化:
SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
事务的特性称为隔离性,多事务操作之间不会产生影响。不考虑隔离性会产生很多问题。
主要有三个读问题:脏读、不可重复读、虚(幻)读:
- 脏读:一个未提交事务读取到另一个未提交事务的数据
脏读是指一个事务在处理数据的过程中,读取到另一个未提交事务的数据。这可能导致读取到的是一个未提交的数据,而这些数据可能在后续的操作中被回滚,从而造成数据的不一致或丢失。脏读又称为无效数据读出,因为它可能读到的是一个最终不会被保存到数据库中的数据。 - 不可重复读:一个未提交事务读取到另一提交事务修改数据
不可重复读(Non-Repeatable Read, NRR)是指在某个事务内的多次读取操作中,由于其他事务的并发修改导致该事务读取到的数据状态发生变化,从而不能保持数据的可见性。具体来说,当一个事务多次读取相同的数据,但在每次读取之间,其他事务对其进行了修改,使得该事务在两次读取之间的数据状态不一致,这就是不可重复读的情况。 - 幻读:一个未提交事务读取到另一提交事务添加数据
幻读(Phantom Read, PHR)是指在某个事务的执行过程中,它依据某些查询条件读取了一些记录,随后另一个事务在该查询条件下的插入操作导致原本不会出现在结果集中的记录也被读取出来了。这种情况通常发生在事务A首先读取了一批符合特定查询条件的记录,然后在事务B插入新的记录之前,事务A再次读取这些记录,这次读取的结果包含了新插入的记录,这显然超出了事务A原来的预期,这就是所谓的幻读。
各个隔离级别解决并发问题的能力见下表:
各种数据库产品对事务隔离级别的支持程度:
设置事务的隔离级别
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交 @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读 @Transactional(isolation = Isolation.SERIALIZABLE)//串行化
timeout:超时时间
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常 程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
默认值是 -1
,设置时间以秒单位进行计算。
举例:
@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
readOnly:是否只读
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
readOnly 默认值 false
,表示可以查询,可以添加修改删除操作。
设置 readOnly 值是 true
,设置成 true
之后,只能查询。
举例:
@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
rollback:回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略:
rollbackFor
属性:设置出现哪些异常进行事务回滚noRollbackFor
属性:设置出现哪些异常不进行事务回滚
举例:
@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
不发生回滚,因此购买图书的操作正常执行。
Spring事务管理操作
- 事务添加到 JavaEE 三层结构里面 Service 层(业务逻辑层)
- 在 Spring 进行事务管理操作
- 有两种方式:编程式事务管理和声明式事务管理
- 声明式事务管理
- 基于注解方式
- 基于 xml 配置文件方式
- 在 Spring 进行声明式事务管理,底层使用 AOP 原理
- Spring 事务管理 API
提供一个接口,代表事务管理器,这个接口针对不同的框架提供不同的实现类
声明式事务:基于注解方式
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出 来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性 能等各个方面的优化
所以,我们可以总结下面两个概念: - 编程式:自己写代码实现功能
- 声明式:通过配置让框架实现功能
准备工作
创建数据表
CREATE TABLE `t_account` ( `id` int NOT NULL, `username` varchar(15) NOT NULL, `money` decimal(10,0) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
创建jdbc.properties文件
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/dbtest1?serverTimezone=UTC jdbc.username=root jdbc.password=123456
配置Spring的配置文件
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!--开启组件扫码--> <context:component-scan base-package="com.evan.spring5"/> <!-- 引入外部配置文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置数据源 --> <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置jdbc模板 --> <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate"> <!-- 注入数据源 --> <property name="dataSource" ref="dataSource"/> </bean>
创建实体类、service类和dao类
//实体类 public class Account { private int id; private String username; private BigDecimal money; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public BigDecimal getMoney() { return money; } public void setMoney(BigDecimal money) { this.money = money; } @Override public String toString() { return "Account{" + "id=" + id + ", username='" + username + '\'' + ", money=" + money + '}'; } }
//dao接口 public interface AccountDao { void reduceMoney(); void addMoney(); }
//dao接口实现类 @Repository public class AccountDaoImpl implements AccountDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public void reduceMoney() { String sql = "update t_account set money = money - ? where username = ?"; jdbcTemplate.update(sql,100,"luck"); } @Override public void addMoney() { String sql = "update t_account set money = money + ? where username = ?"; jdbcTemplate.update(sql,100,"mary"); } }
//service类 @Service public class AccountService { @Autowired private AccountDao accountDao; public void accountMoney() { accountDao.reduceMoney(); int number = 10 / 0; accountDao.addMoney(); } }
测试
此时在没有开启事务的情况下进行转账测试,可以看出在转账过程中出现异常,转账金额不一致,此时需要事务来解决,当出现转账中发送异常就会立即立即回滚数据,保证数据的一致性。
开启事务
- 在 spring 配置文件配置事务管理器
<!--创建事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean>
- 在 spring 配置文件,开启事务注解
- 引入命名空间tx
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"
- 开启事务注解
<!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就 是这个默认值,则可以省略这个属性 --> <tx:annotation-driven transaction-manager="transactionManager" />
- 添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理。
在 service 类上面(或者 service 类里面方法上面)添加事务注解:
(1) @Transactional
这个注解添加到类上面,也可以添加方法上面
(2) 如果把这个注解添加类上面,这个类里面所有的方法都会受到影响
(3) 如果把这个注解添加方法上面,则只会影响该方法
@Service @Transactional(propagation = Propagation.REQUIRED) //Spring默认行为 public class AccountService { @Autowired private AccountDao accountDao; public void accountMoney() { accountDao.reduceMoney(); int number = 10 / 0; accountDao.addMoney(); } }
测试
此时开始事务后,测试上述转账过程中出现数据不一致的问题,经测试转账过程出现问题,事务回滚操作,数据没有变化。
声明式事务:基于XML方式
修改Spring配置文件
将Spring配置文件中去掉tx:annotation-driven
标签,并添加配置:
- 第一步 配置事务管理器
- 第二步 配置通知
- 第三步 配置切入点和切面
<!-- 引入外部properties --> <context:property-placeholder location="jdbc.properties"/> <!-- 开启组件扫描 --> <context:component-scan base-package="cn.evan.spring5"/> <!-- 创建数据库连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务通知--> <tx:advice id="txadvice"> <!--配置事务属性参数--> <tx:attributes> <!-- 设置在哪些方法上配置相关事务 --> <!-- tx:method标签:配置具体的事务方法 --> <!-- name属性:指定方法名,可以使用星号代表多个字符 --> <tx:method name="get*" read-only="true"/> <tx:method name="query*" read-only="true"/> <tx:method name="find*" read-only="true"/> <!-- read-only属性:设置只读属性 --> <!-- rollback-for属性:设置回滚的异常 --> <!-- no-rollback-for属性:设置不回滚的异常 --> <!-- isolation属性:设置事务的隔离级别 --> <!-- timeout属性:设置事务的超时属性 --> <!-- propagation属性:设置事务的传播行为 --> <tx:method name="save*" read-only="false" rollbackfor=" java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="update*" read-only="false" rollbackfor=" java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="delete*" read-only="false" rollbackfor=" java.lang.Exception" propagation="REQUIRES_NEW"/> </tx:attributes> </tx:advice> <!-- 配置切入点和切面--> <aop:config> <!--配置切入表达式--> <aop:pointcut id="pt" expression="execution(* com.atguigu.spring5.service.UserService.*(..))"/> <!--配置事务通知切面--> <aop:advisor advice-ref="txadvice" pointcut-ref="pt"/> </aop:config>
声明式事务:纯注解方式
创建配置类,使用配置类替代 xml 配置文件。
@Configuration //配置类 @ComponentScan(basePackages = "com.atguigu") //组件扫描 @EnableTransactionManagement //开启事务 public class TxConfig { //创建数据库连接池 @Bean public DruidDataSource getDruidDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql:///user_db"); dataSource.setUsername("root"); dataSource.setPassword("root"); return dataSource; } //创建 JdbcTemplate 对象 @Bean public JdbcTemplate getJdbcTemplate(DataSource dataSource) { //到 ioc 容器中根据类型找到 dataSource JdbcTemplate jdbcTemplate = new JdbcTemplate(); //注入 dataSource jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } //创建事务管理器 @Bean public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性