提到事务,这个我们应该比较熟悉了,在数据库学习的过程中,我们或多或少接触过了事务,当然你可能没有用到,也可能用到了,这篇博客我们将围绕Spring的相关事务的概念进行,了解Spring中的事务和事务可以用来解决什么问题。
什么是事务?
我们回顾一下我们当初学习数据库的时候的一些知识点:
MySQL事务
MySQL事务是由数据库管理系统(DBMS)实现的一种机制,主要用于管理对数据库的更改操作,如插入(INSERT)、更新(UPDATE)和删除(DELETE)。事务具有以下四个特性,通常被称为ACID特性:
作为单个逻辑工作单元执行的一组操作,要么全部成功执行,要么全部失败回滚,以确保数据的一致性和完整性。事务具有以下特性:
- 原子性(Atomicity):事务是一个不可分割的工作单元,要么全部执行成功,要么全部失败回滚,保证数据的完整性。
- 一致性(Consistency):事务执行前后,数据的状态保持一致,不会破坏数据的完整性约束。
- 隔离性(Isolation):多个事务同时执行时,每个事务的操作应该互相隔离,避免相互影响,确保数据操作的正确性。
- 持久性(Durability):一旦事务提交,其结果应该是永久性的,不会因为系统故障而丢失,数据应该持久保存在系统中。
MySQL的InnoDB存储引擎支持事务,而MyISAM等其他存储引擎则不支持。
Spring 事务
Spring框架提供了一个事务管理的抽象层,允许开发者以声明式或编程式的方式管理事务。Spring事务管理器可以与多种数据库和JPA/Hibernate等ORM框架协同工作。
Spring事务管理的关键组件是PlatformTransactionManager接口,它提供了开始、提交和回滚事务的方法。Spring还提供了一些额外的功能,如:
- 传播行为(Propagation Behavior):定义了事务方法被调用时的行为,例如是否在一个现有事务中运行,或者总是创建一个新的事务。
- 隔离级别(Isolation Level):允许开发者指定事务的隔离级别,从而控制并发操作的影响。
- 回滚规则(Rollback Rules):定义了哪些类型的异常会导致事务回滚。
- 只读事务(Read-Only Transactions):如果事务只包含读取操作,可以将其标记为只读,这样可以提高性能并减少锁定。
Spring事务和MySQL事务的区别
- 实现方式:
- MySQL事务由数据库本身实现,依赖于存储引擎(如InnoDB)。
- Spring事务由框架实现,可以调用底层数据库的事务机制,也可以提供额外的事务管理策略。
- 传播行为:
- MySQL事务不理解应用层面的事务传播行为,它只是执行SQL命令。
- Spring事务提供了对事务传播行为的支持,可以控制事务在不同方法调用间的传播方式。
- 回滚规则:
- MySQL事务只根据SQL语句的状态决定是否回滚,通常在未提交时遇到错误会回滚。
- Spring事务可以根据Java级别的异常来决定是否回滚事务,提供更细粒度的控制。
- 应用范围:
- MySQL事务主要关注事务的ACID特性和隔离级别。
- Spring事务除了ACID特性外,还提供了更多事务管理的高级特性,如隔离级别、传播行为、只读标志和超时控制。
Spring事务是在应用层面提供的一层抽象,用于更方便地管理和协调跨多个数据库操作的事务边界,而MySQL事务则是数据库层面的事务管理机制。在使用Spring框架进行开发时,通常会结合这两种事务管理方式。
Spring的事务类型
- 声明式事务管理:通过在配置文件或注解中声明事务的属性和行为,Spring框架会自动为方法添加事务管理,无需手动编写事务管理代码。
- 编程式事务管理:通过编写代码显式地控制事务的开始、提交、回滚等操作,可以更灵活地控制事务的边界和行为。
- 注解驱动事务管理:通过在方法上使用注解(如
@Transactional
)来声明事务的属性和行为,简化事务管理的配置和使用。 - 基于AspectJ的事务管理:通过AspectJ切面来实现事务管理,可以更细粒度地控制事务的切入点和行为。
- JTA事务管理:用于分布式事务管理,通过Java Transaction API(JTA)实现全局事务的管理和协调。
我们实际开发过程中,主要用的是声明式事务和编程式事务。所以我们在之后主要围绕的是声明式事务和编程式事务进行讲解。
准备工作
JdbcTemplate
JdbcTemplate是Spring框架提供的一个简化数据库操作的工具类,用于执行SQL查询、更新等操作。它封装了JDBC的繁琐操作,提供了更简洁、易用的API,同时处理了资源的释放和异常的捕获,使得数据库操作更加方便和安全。
通过JdbcTemplate,开发者可以使用模板方法来执行SQL语句,如查询单个结果、查询列表、更新数据等操作,而无需手动管理数据库连接、Statement、ResultSet等资源。同时,JdbcTemplate还支持命名参数、批处理、存储过程等高级功能,使得数据库操作更加灵活和高效。
项目目录结构
引入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SSM</artifactId>
<groupId>com.miaow</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-aop-jdbcTemplate</artifactId>
<name>spring-aop-jdbcTemplate</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<!-- spring 持久层支持jar包 spring 在执行持久化层操作,与持久化层技术进行整合过程中,需要使用ORM
、jdbc、tx三个jar包
-->
<!--导入ORM包就可以通过Maven的依赖传递性把其他两个也导入-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- 配置数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
<!-- 基于xml实现的声明式事务,必须引入aspectj依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.8</version>
</dependency>
</dependencies>
</project>
创建jdbc.properties
jdbc.user=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver
创建Spring-jdbc.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:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/tool" 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/tool http://www.springframework.org/schema/tool/spring-tool.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 扫描组件 -->
<context:component-scan base-package="com.miaow"></context:component-scan>
<!-- 导入外部属性文件-->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
<!-- 配置数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
</bean>
<!-- 配置JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源头-->
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!-- 从现在添加事务 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!-- 开启事务注解驱动
通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
transaction-manager属性的默认值是transactionManager,如果事务管理器bean的ID就是这个默认的值,那么就可以忽略这个属性
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
创建实体类
public class User {
private Integer id;
private String userName;
private String name;
private String sex;
private String password;
private Integer age;
public User(Integer id, String userName, String name, String sex, String password, Integer age) {
this.id = id;
this.userName = userName;
this.name = name;
this.sex = sex;
this.password = password;
this.age = age;
}
public User() {
}
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 String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", userName='" + userName + '\'' +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
数据库自己创建根据实体类字段创建
业务层
public interface UserDao {
Integer getUserId(Integer id);
List<User> getUserName(String name);
int updateUser(User user);
void delete(Integer id);
}
public interface UserService {
int findUSerById(int id) ;
List<User> getUserName(String name);
int updateUser(User user);
}
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getUserId(Integer id) {
String sql = "select * from user where id = ?";
return jdbcTemplate.queryForObject(sql,Integer.class,id);
}
@Override
public List<User> getUserName(String name) {
String sql = "select * from user where name = ?";
return jdbcTemplate.query(sql,new BeanPropertyRowMapper<>(User.class),name);
}
@Override
public int updateUser(User user) {
String sql = "update user set name = ?,age = ? where id = ?";
return jdbcTemplate.update(sql,user.getName(),user.getAge(),user.getId());
}
@Override
public void delete(Integer id) {
String sql = "delete from user where id = ?";
jdbcTemplate.update(sql,id);
}
}
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao;
@Override
public int findUSerById(int id) {
return userDao.getUserId(id);
}
@Override
public List<User> getUserName(String name) {
return userDao.getUserName(name);
}
/**
* @Transactional标识在方法上,则只会影响该方法
* @Transactional标识的类上,则会影响类中所有的方法
* 对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
* readOnly = true 表示只读
* timeout 表示超时时间
* rollbackFor 表示回滚异常
* rollbackForClassName 表示回滚异常类名
* rollbackOn 表示回滚异常
* noRollbackFor 表示不回滚异常
* rollbackFor 需要设置一个字符串属性的全类名
* rollbackFor和 noRollbackFor 可以配合使用,表示同时支持多个异常
*
* 事务的传播行为
* 1.PROPAGATION_REQUIRED 默认值,如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新事务。
* 2.PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。
* 3.PROPAGATION_MANDATORY 如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。
* 4.PROPAGATION_REQUIRES_NEW 创建一个新事务,如果当前存在事务,则把当前事务挂起。
* 5.PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。
* 6.PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
* 7.PROPAGATION_NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
*
* 事务隔离级别
* 1.ISOLATION_DEFAULT 默认值,使用后端数据库默认的隔离级别。
* 2.ISOLATION_READ_UNCOMMITTED 最低隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
* 3.ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可能会导致不可重复读。
* 4.ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以防止脏读,但是可能会导致幻读。
* 5.ISOLATION_SERIALIZABLE 最高隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读与幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
*
*/
// @Transactional(readOnly = true) //添加注解,预防修改失败或者错误
// @Transactional(timeout = 3)
//事务隔离级别
// @Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
// @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
// @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
// @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
// @Transactional(isolation = Isolation.SERIALIZABLE)//串行化
//事务的传播行为
/*
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRED) //默认值
//如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.SUPPORTS)
//如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
//创建一个新事务,如果当前存在事务,则把当前事务挂起。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW)
//以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED)
//以非事务方式执行,如果当前存在事务,则抛出异常。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.NEVER)
//如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.NESTED)
*/
@Transactional
@Override
public int updateUser(User user) {
// try{
// //休眠5秒触发超时事务
// TimeUnit.SECONDS.sleep(5);
// }catch (InterruptedException e){
// e.printStackTrace();
// }
// userDao.getUserId(user.getId());
userDao.getUserName(user.getName());
return userDao.updateUser(user);
}
}
控制层
@Controller
public class UserController {
@Autowired
private UserService userService;
public void findAll(){
userService.findUSerById(12);
}
public List<User> findByName(String miaow) {
return userService.getUserName(miaow);
}
public int updateUser(User user) {
return userService.updateUser(user);
}
}
测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-jdbc.xml")
public class AppTest
{
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* Rigorous Test :-)
*/
@Test
public void shouldAnswerWithTrue()
{
assertTrue( true );
}
//利用jdbcTemplate查询数据库
@Test
public void testFind(){
String sql = "select * from user limit 100;";
List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
users.forEach(user->{
System.out.println(user);
});
}
//利用jdbcTemplate插入数据库
@Test
public void testInsert(){
String sql = "insert into user(id,userName,name,sex,password,age) values(?,?,?,?,?,?);";
int result = jdbcTemplate.update(sql, "1245","罗小黑","miaow","男","12dafaeeafaw", 18);
if(result > 0){
System.out.println("插入成功");
}
else{
System.out.println("插入失败");
}
}
//利用jdbcTemplate更新数据库
@Test
public void testUpdate(){
String sql = "update user set age = ? where name = ?;";
int result = jdbcTemplate.update(sql, 18, "miaow");
if (result > 0){
System.out.println("更新成功");
}else {
System.out.println("更新失败");
}
}
//利用jdbcTemplate删除数据库
@Test
public void testDelete(){
String sql = "delete from user where name = ?;";
int result = jdbcTemplate.update(sql, "miaow");
if (result > 0){
System.out.println("删除成功");
}else {
System.out.println("删除失败");
}
}
}
编程式事务
Spring的编程式事务是通过编程的方式在代码中显式控制事务的开启、提交、回滚等操作的一种事务管理方式。相比声明式事务,编程式事务需要开发者在代码中显式调用事务管理相关的方法,实现对事务的控制。
编程式事务的主要特点包括:
- 事务管理器:开发者需要获取事务管理器,并通过事务管理器的方法来控制事务的开启、提交、回滚等操作。
- 事务边界:开发者需要在代码中明确定义事务的边界,即事务开始的位置和结束的位置,确保事务的正确执行。
- 事务控制:开发者可以根据业务需求在代码中灵活地控制事务的行为,如根据条件选择是提交事务还是回滚事务。
- 资源管理:开发者需要手动管理数据库连接、事务状态等资源,确保资源的正确释放和事务的一致性。
尽管编程式事务相对于声明式事务更为灵活,但也增加了代码的复杂性和维护成本。通常情况下,建议优先选择声明式事务,只有在特定需求下才考虑使用编程式事务。
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
声明式事务
Spring声明式事务是通过在方法或类上使用注解或XML配置的方式来定义事务的属性和行为,而无需在代码中显式编写事务管理逻辑。通过声明式事务,开发者可以将事务管理与业务逻辑分离,提高了代码的可维护性和可读性。
具体来说,Spring声明式事务的概念包括以下几个要点:
- 注解或XML配置:可以通过在方法或类上使用 @Transactional 注解或在XML配置文件中配置 tx:advice 等元素来声明事务的属性,如事务的传播行为、隔离级别、超时设置等。
- 事务切面:声明式事务通过AOP(面向切面编程)实现,将事务逻辑织入到目标方法的执行过程中。在方法调用前后,事务管理器会根据配置来开启、提交或回滚事务。
- 事务传播行为:定义了方法调用时事务如何传播的规则,如REQUIRED、REQUIRES_NEW、NESTED等,用于控制事务的边界和范围。
- 事务隔离级别:定义了事务之间的隔离程度,如READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE等,用于控制事务并发访问数据的方式。
- 事务管理器:声明式事务需要配置一个事务管理器,用于实际管理事务的提交、回滚等操作,常见的事务管理器包括DataSourceTransactionManager、JpaTransactionManager等。
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
所以,我们可以总结下面两个概念:
- 编程式:自己写代码实现功能
- 声明式:通过配置让框架实现功能