一个数据库事务是一个被视为单一的工作单元的操作序列。这些操作应该要么完整地执行,要么完全不执行。事务管理是一个重要组成部分,以确保数据完整性和一致性。事务的概念可以描述为具有以下四个关键属性说成是 ACID:
• 原子性:事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败的。
• 一致性:这表示数据库的引用完整性的一致性,表中唯一的主键等。
• 隔离性:可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏。
• 持久性:一个事务一旦完成全部操作后,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。
局部事物 vs 全局事务
局部事务是特定于一个单一的事务资源,如一个 JDBC 连接,而全局事务可以跨多个事务资源事务,如在一个分布式系统中的事务。
局部事务管理在一个集中的计算环境中是有用的,该计算环境中应用程序组件和资源位于一个单位点,而事务管理只涉及到一个运行在一个单一机器中的本地数据管理器。局部事务更容易实现。
全局事务管理需要在分布式计算环境中,所有的资源都分布在多个系统中。在这种情况下事务管理需要同时在局部和全局范围内进行。分布式或全局事务跨多个系统执行,它的执行需要全局事务管理系统和所有相关系统的局部数据管理人员之间的协调。
使用 SQL 发布到数据库中的事务的简单步骤如下:
1:使用 begin transaction 命令开始事务。
2:使用 SQL 查询语句执行各种删除、更新或插入操作。
3:如果所有的操作都成功,则执行 提交 操作,否则 回滚 所有操作。
在实际开发中,操作数据库时会涉及到事务管理问题,为此Spring提供了专门用于事务处理的API。Spring的事务管理简化了传统的事务管理流程,并且在一定程度上减少了开发者的工作量。
事务管理的3个核心接口
PlatformTransactionManager 接口是 Spring 提供的平台事务管理器,用于管理事务。该接口中提供了三个事务操作方法,具体如下。
TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。
void commit(TransactionStatus status):用于提交事务。
void rollback(TransactionStatus status):用于回滚事务。
在项目中,Spring 将 xml 中配置的事务详细信息封装到对象 TransactionDefinition 中,然后通过事务管理器的 getTransaction() 方法获得事务的状态(TransactionStatus),并对事务进行下一步的操作。
TransactionDefinition 接口是事务定义(描述)的对象,它提供了事务相关信息获取的方法,其中包括五个操作,具体如下。
String getName():获取事务对象名称。
int getIsolationLevel():获取事务的隔离级别。
int getPropagationBehavior():获取事务的传播行为。
int getTimeout():获取事务的超时时间。
boolean isReadOnly():获取事务是否只读。
在上述五个方法的描述中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务。
传播行为的种类如下表所示。
属性名称 | 值 | 描述 |
---|---|---|
PROPAGATION_REQUIRED | required | 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将创建新事务 |
PROPAGATION_SUPPORTS | supports | 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将以非事务状态执行 |
PROPAGATION_MANDATORY | mandatory | 支持当前事务。如果 A 方法没有事务,则抛出异常 |
PROPAGATION_REQUIRES_NEW | requires_new | 将创建新的事务,如果 A 方法已经在事务中,则将 A 事务挂起 |
PROPAGATION_NOT_SUPPORTED | not_supported | 不支持当前事务,总是以非事务状态执行。如果 A 方法已经在事务中,则将其挂起 |
PROPAGATION_NEVER | never | 不支持当前事务,如果 A 方法在事务中,则抛出异常 |
PROPAGATION.NESTED | nested | 嵌套事务,底层将使用 Savepoint 形成嵌套事务 |
在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务。
通常情况下,数据的查询不会改变原数据,所以不需要进行事务管理,而对于数据的增加、修改和删除等操作,必须进行事务管理。如果没有指定事务的传播行为,则 Spring3 默认的传播行为是 required。
TransactionStatus 接口是事务的状态,它描述了某一时间点上事务的状态信息。其中包含六个操作,具体如下表所示。
名称 | 说明 |
---|---|
void flush() | 刷新事务 |
boolean hasSavepoint() | 获取是否存在保存点 |
boolean isCompleted() | 获取事务是否完成 |
boolean isNewTransaction() | 获取是否是新事务 |
boolean isRollbackOnly() | 获取是否回滚 |
void setRollbackOnly() | 设置事务回滚 |
事务管理的方式
Spring中的事务管理分为两种方式:一种是传统的编程事务管理,另一种是声明式事务管理。
编程式事务管理:是通过编写代码实现的事务管理,包含定义事务的开始、正常执行后的事务提交和异常时的事务回滚。这意味着你在编程的帮助下有管理事务。这给了你极大的灵活性,但却很难维护。
声明式事务管理:是通过AOP技术实现的事务管理。其主要的思想是将事务管理作为一个"切面"代码单独编写,然后通过AOP技术将事务管理的"切面"织入到业务目标类中。阅读Spring AOP相关内容
上面两种方式中,一般使用的是声明式事务管理,因为这样可以让开发者无需通过编程的方式来管理事务,只需在配置文件中进行事务规则声明就可以将事务规则应用到逻辑中。在一定程度上减少了工作量,提高了工作效率。下面只介绍声明式事务管理。
声明式事务管理
Spring的声明式事务管理有两种方式:一种是基于XML的方式,一种是基于Annotation的方式。
基于XML方式的声明式事务
Spring2.0以后,提供了tx命名空间来配置事务,tx命名空间下提供了<tx:advice>
元素来配置事务通知(通知增强)。当使用<tx:advice>
元素配置了事务增强处理之后,就可以通过编写AOP配置,让Spring自动对目标生成代理。
<tx:advice>
元素及其子元素如下图所示:
基于XML方式的声明式事务示例
创建数据表并插入数据
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(12) NOT NULL AUTO_INCREMENT COMMENT '账户id',
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`balance` double(20, 0) NOT NULL COMMENT '用户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `account` VALUES (1, 'joy', 100);
INSERT INTO `account` VALUES (2, 'Tom', 1000);
INSERT INTO `account` VALUES (3, 'Jack', 2000);
INSERT INTO `account` VALUES (4, 'Rose', 500);
在src下创建com.happy.test包,添加account实体类
package com.happy.test;
public class Account {
private Integer id; // 账户id
private String username; // 用户名
private Double balance; // 账户余额
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public String toString() {
return "Account [id=" + id + ", "
+ "username=" + username +
", balance=" + balance + "]";
}
}
创建AccountDao接口
package com.happy.test;
import java.util.List;
public interface AccountDao {
// 转账
public void transfer(String outUser,String inUser,Double money);
}
创建AccountDaoImpl类
package com.happy.test;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class AccountDaoImpl implements AccountDao {
// 声明JdbcTemplate属性及其setter方法
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* 转账
* inUser:收款人
* outUser:汇款人
* money:收款金额
*/
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update account set balance = balance +? "
+ "where username = ?",money, inUser);
// 模拟系统运行时的突发性问题
//int i = 1/0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update account set balance = balance-? "
+ "where username = ?",money, outUser);
}
}
在src下添加配置文件application.xml
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 1.配置数据源 -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--数据库驱动 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!--连接数据库的url -->
<property name="url" value="jdbc:mysql://localhost/spring" />
<!--连接数据库的用户名 -->
<property name="username" value="root" />
<!--连接数据库的密码 -->
<property name="password" value="root" />
</bean>
<!-- 2.配置JDBC模板 -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认必须使用数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!--3.定义id为accountDao的Bean -->
<bean id="accountDao" class="com.itheima.jdbc.AccountDaoImpl">
<!-- 将jdbcTemplate注入到AccountDao实例中 -->
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
<!-- 4.事务管理器,依赖于数据源 -->
<bean id="transactionManager" class=
"org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 5.编写通知:对事务进行增强(通知),需要编写对切入点和具体执行事务细节 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- name:*表示任意方法名称 -->
<tx:method name="*" propagation="REQUIRED"
isolation="DEFAULT" read-only="false" />
</tx:attributes>
</tx:advice>
<!-- 6.编写aop,让spring自动对目标生成代理,需要使用AspectJ的表达式 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="execution(* com.itheima.jdbc.*.*(..))"
id="txPointCut" />
<!-- 切面:将切入点与通知整合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" />
</aop:config>
</beans>
创建测试类TransactionTest
package com.happy.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import
org.springframework.context.support.ClassPathXmlApplicationContext;
//测试类
public class TransactionTest {
@Test
public void xmlTest(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao)applicationContext.getBean("accountDao");
// 调用实例中的转账方法
accountDao.transfer("Jack", "Rose", 100.0);
// 输出提示信息
System.out.println("转账成功!");
}
}
当事务正常提交时,控制台输出如下:
数据表中数据修改成功
在AccountDaoImpl类中的transfer方法中添加int i = 10/0;
后,事务提交失败,junit控制台输出如下:
数据表中数据修改失败
基于Annotation方式的声明式事务
Spring的声明式事务管理还可以通过Annotation(注解)的方式实现,这种方式的操作非常简单,只需你做两件事:
- 1:在Spring容器中注册事务注解驱动,其代码如下:
<tx:annotation-driven transaction-manager="transactionManager"/>
- 2:在需要使用事务的Spring Bean类或者Bean类的方法上添加注解@Transactional。如果将注解添加在Bean类上,则表示事务的设置对整个类的方法都起作用,如果将注解添加在方法上,则表示事务的设置只对此方法有效。
基于XML方式的声明式事务需要在<tx:method>
中添加属性值<tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" />
,而基于Annotation方式的声明式事务只需在使用@Transactional注解是添加参数配置@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
基于Annotation方式的声明式事务示例
在src下添加applicationContext-annotation.xml配置文件
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 1.配置数据源 -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--数据库驱动 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!--连接数据库的url -->
<property name="url" value="jdbc:mysql://localhost/spring" />
<!--连接数据库的用户名 -->
<property name="username" value="root" />
<!--连接数据库的密码 -->
<property name="password" value="root" />
</bean>
<!-- 2.配置JDBC模板 -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认必须使用数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!--3.定义id为accountDao的Bean -->
<bean id="accountDao" class="com.happy.test.AccountDaoImpl">
<!-- 将jdbcTemplate注入到AccountDao实例中 -->
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
<!-- 4.事务管理器,依赖于数据源 -->
<bean id="transactionManager" class=
"org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 5.注册事务管理器驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
修改AccountDaoImpl中的transfer方法
package com.happy.test;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class AccountDaoImpl implements AccountDao {
// 声明JdbcTemplate属性及其setter方法
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* 转账
* inUser:收款人
* outUser:汇款人
* money:收款金额
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT, readOnly = false)
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update account set balance = balance +? "
+ "where username = ?",money, inUser);
// 模拟系统运行时的突发性问题
// int i = 1/0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update account set balance = balance-? "
+ "where username = ?",money, outUser);
}
}
在TransactionTest测试类中添加annotationTest()测试方法
package com.happy.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import
org.springframework.context.support.ClassPathXmlApplicationContext;
//测试类
public class TransactionTest {
// @Test
// public void xmlTest(){
// ApplicationContext applicationContext =
// new ClassPathXmlApplicationContext("applicationContext.xml");
// // 获取AccountDao实例
// AccountDao accountDao =
// (AccountDao)applicationContext.getBean("accountDao");
// // 调用实例中的转账方法
// accountDao.transfer("Jack", "Rose", 100.0);
// // 输出提示信息
// System.out.println("转账成功!");
// }
@Test
public void annotationTest(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext-annotation.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao)applicationContext.getBean("accountDao");
// 调用实例中的转账方法
accountDao.transfer("Jack", "Rose", 100.0);
// 输出提示信息
System.out.println("转账成功!");
}
}
当事务正常提交时,控制台输出如下:
数据表中数据修改成功
在AccountDaoImpl类中添加int i = 10/0;
后,事务提交失败,junit控制台输出如下:
数据表中数据修改失败