Spring-Transaction(事务)

Spring-Transaction(事务)

JdbcTemplate

JdbcTemplate是Spring框架提供的一个用于简化JDBC操作的模版类。它封装了JDBC的很多细节,使得开发这可以更轻松地执行数据库操作(CRUD)。

JdbcTemplate的特点

  1. 简化JDBC操作:封装了JDBC的样板代码,如获取连接、创建语句、处理结果集等。
  2. 异常处理:将SQLException转换为Spring的DataAccessException,提供了更丰富的异常信息。
  3. 资源管理:自动管理数据库连接的获取和释放,减少了资源泄漏的风险。
  4. 支持命名参数:允许使用命名参数而不是传统的"?"占位符,使用代码更易读。
  5. 集成Spring事务管理:可以与Spring的事务管理集成,支持声明式和编程式事务。
  6. 灵活的查询方法:提供了多种查询方法,包括查询单一对象、查询列表、分页查询等。

使用JdbcTemplate的基本步骤

  1. 配置数据源:在Spring配置文件中配置数据库连接池和数据源。
  2. 创建JdbcTemplate实例:通过构造函数或setter方法将数据源注入到JdbcTemplate实例中。
  3. 执行数据库操作:使用JdbcTemplate提供的方法执行增删改查操作。

使用JdbcTemplate的示例

1.引用相关依赖

    <dependencies>
        <!--spring context依赖-->
        <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--spring对junit的支持相关依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.9.0</version>
        </dependency>
        <!--log4j2的依赖-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>
        <!--spring jdbc  Spring 持久化层支持jar包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!-- 数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>
    </dependencies>

2.添加日志配置文件(用于控制台执行后的日志打印)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <loggers>
        <!--
            level指定日志级别,从低到高的优先级:
                TRACE < DEBUG < INFO < WARN < ERROR < FATAL
                trace:追踪,是最低的日志级别,相当于追踪程序的执行
                debug:调试,一般在开发中,都将其设置为最低的日志级别
                info:信息,输出重要的信息,使用较多
                warn:警告,输出警告的信息
                error:错误,输出错误信息
                fatal:严重错误
        -->
        <root level="DEBUG">
            <appender-ref ref="console"/>
            <appender-ref ref="rolling-file"/>
        </root>
    </loggers>

    <appenders>
        <!--输出日志信息到控制台-->
        <console name="console" target="SYSTEM_OUT">
            <!--控制日志输出的格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
        </console>

        <!-- 这个会打印出所有的信息,
            每次大小超过size,
            则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,
            作为存档-->
        <RollingFile name="rolling-file" fileName="/opt/logs/spring6-demo/app.log"
                     filePattern="log/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <SizeBasedTriggeringPolicy size="50MB"/>
            <!-- DefaultRolloverStrategy属性如不设置,
            则默认为最多同一文件夹下7个文件,这里设置了20 -->
            <DefaultRolloverStrategy max="10"/>
        </RollingFile>
    </appenders>
</configuration>

3.添加数据源配置参数

以下是jdbc.properties的文件内容

jdbc.user=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver

4.在mysql数据库中创建数据库以及数据表

以下是建库与建表SQL语句

# 建库语句
CREATE DATABASE `test`;
# 建表语句
CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5.配置Spring的配置文件

以下是bean.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"
       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">

    <!--    导入外部属性文件-->
    <context:property-placeholder location="classpath:jdbc.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>

    <!--    配置JDBC template-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>
</beans>

6.新增学生信息实体类与student表对应

package com.shen.jdbc;

/**
 * 学生实体类与数据库表对应
 */
public class StudentEntity {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;

    @Override
    public String toString() {
        return "StudentEntity{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

7.新增测试类,依赖注入JdbcTemplate的实例进行增删改查操作

@SpringJUnitConfig(locations = "classpath:bean.xml")
public class JdbcTemplateTest {
    private static final Logger logger = LoggerFactory.getLogger(JdbcTemplateTest.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;

    //测试SQL中数据操作功能:DML-新增,更新,删除
    @Test
    public void testDML() {
        //新增操作
        String insertSql = "insert into student values(null,?,?,?)";
        Object[] insertParams = {"小王", "20", "未知"};
        int row;
        row = jdbcTemplate.update(insertSql, insertParams);
        logger.info("新增结果为:{}", row);
        //更新操作
        String updateSql = "update student set sex=? where id=?";
        row = jdbcTemplate.update(updateSql, "男性", 1);
        logger.info("更新结果为:{}", row);
        //删除操作
        String deleteSql = "delete from student where id =?";
        row = jdbcTemplate.update(deleteSql, 1);
        logger.info("删除结果为:{}", row);
    }

    //测试SQL中数据查询功能:DQL
    @Test
    public void testDQL() {
        //插入一些数据
        insertData();
        //查询一个对象数据
        selectObjectData();
        //查询一个对象集合
        selectObjectList();
        //查询特定的值
        selectOneValue();
    }

    //插入数据
    public void insertData() {
        String insertSql = "insert into student values(null,?,?,?)";
        Object[] insertOneParams = {"小王", "20", "男"};
        Object[] insertTwoParams = {"小张", "18", "女"};
        Object[] insertThreeParams = {"小刘", "21", "女"};
        int row;
        row = jdbcTemplate.update(insertSql, insertOneParams);
        logger.info("新增结果为:{}", row);
        row = jdbcTemplate.update(insertSql, insertTwoParams);
        logger.info("新增结果为:{}", row);
        row = jdbcTemplate.update(insertSql, insertThreeParams);
        logger.info("新增结果为:{}", row);
    }

    //查询一个对象数据
    public void selectObjectData() {
        //写法一,自定义RowMapper
        String selectSql = "select * from student where id =?";
        StudentEntity studentEntityOne = jdbcTemplate.queryForObject(selectSql,
                (rs, rowNum) -> {
                    StudentEntity student = new StudentEntity();
                    student.setId(rs.getInt("id"));
                    student.setName(rs.getString("name"));
                    student.setAge(rs.getInt("age"));
                    student.setSex(rs.getString("sex"));
                    return student;
                }, 2);
        logger.info("[查询一个对象] 写法一:查询一个对象的结果为:{}", studentEntityOne);
        //写法二,使用RowMapper实现类 BeanPropertyRowMapper
        StudentEntity studentEntityTwo = jdbcTemplate.queryForObject(selectSql, new BeanPropertyRowMapper<>(StudentEntity.class), 2);
        logger.info("[查询一个对象] 写法二:查询一个对象的结果为:{}", studentEntityTwo);
    }

    //查询一个对象集合
    public void selectObjectList() {
        String selectSql = "select * from student";
        List<StudentEntity> studentEntityList = jdbcTemplate.query(selectSql, new BeanPropertyRowMapper<>(StudentEntity.class));
        logger.info("[查询一个对象集合] 结果为:{}", studentEntityList);
    }

    //查询特定的值
    public void selectOneValue() {
        String selectSql = "select count(*) from student";
        Integer integer = jdbcTemplate.queryForObject(selectSql, Integer.class);
        logger.info("[查询一个特定值] 结果为:{}", integer);
    }

}

事务概述

事务是数据库管理系统中一个非常重要的概念,它用于确保数据的一致性、完整性、可靠性。事务是一系列数据库操作的集合,这些操作要么全部成功执行,要么全部不执行,不会结束在中间某个环节。

事务的特性(ACID)

  • 原子性(Atomicity):事务中所有操作要么全部完成,要么全部不全成。如果事务中的某个操作失败,那边整个事务都会回滚,回到事务开始之前的状态。
  • 一致性(Consistency):事务必须保证数据库从一个一致性状态转移到另一个一致性状态。也就是说,事务执行的结果必须是使数据库的数据满足所有完整性约束。
  • 隔离性(Isolation):事务的执行不能被其他事务干扰。即事务内部的操作及其使用的数据对并发的其他事务是隔离的,并发执行的事务之间不会相互影响。
  • 持久性(Durability):一旦事务提交,则其所做的更改将永久保存在数据库中。即使系统发生故障,更改的数据也不会丢失。

事务的实现机制

  • 锁定机制:用于保证事务的隔离性,防止多个事务同时修改同一数据。
  • 日志机制:用于记录事务的每一个操作,以便在发生故障时能够恢复到事务开始之前的状态。
  • 事务管理器:负责事务的启动、提交、回滚等操作。

编程式事务

编程式事务是指通过编写代码来显式控制事务的边界,即事务的开始、提交、回滚。这种方式提供了最大的灵活性,允许开发者在代码中精准控制事务的每个环节

编程式事务的特点

  • 灵活性高:可以精确控制事务的每个步骤。
  • 复杂性高:需要编写更多的事务管理代码,增加了开发的复杂性。
  • 错误倾向:容易因为代码错误而导致事务管理不当

编程式事务的示例

Connection conn = ...; 
try {
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    // 核心操作
	...   
    // 提交事务
    conn.commit(); 
}catch(Exception e){   
    // 回滚事务
    conn.rollBack();    
}finally{
    // 释放数据库连接
    conn.close(); 
}

声明式事务(常用)

声明式事务是指通过配置文件或注解来管理事务,而不是在代码中显示控制。这种方式简化了事务管理,使得开发者可以专注于业务逻辑而不是事务控制。

声明式事务的特点

  • 简单易用:通过配置或注解即可管理事务,减少了代码量。
  • 解耦:业务逻辑与事务管理代码分离,降低了代码的耦合度。
  • 灵活性优先:相对于编程式事务,声明式事务灵活性较低,因为事务的行为是由配置或注解决定的。

声明式事务的示例

@Service
public class ManageDataBaseService {
    //声明式事务注解
    @Transactional
    public void queryAndUpdateDataBase() {
        // 执行数据库操作
        queryTableA;
        updateTableB;
    }
}

基于注解的声明式事务

业务场景构建

构建一个用户买书的场景,用户有用户id、用户名、用户余额,图书有图书id、图书名、价格、库存。用户购买一本书,如果用户有余额足够买该书且该书有库存的情况下,进行数据库更新操作,用户余额扣除图书价格,图书库存减一。

在mysql数据库中构建图片表与用户表

# 图书表构建sql
CREATE TABLE `book` (
  `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
  `price` int(11) DEFAULT NULL COMMENT '价格',
  `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
  PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
# 图书表插入数据
INSERT INTO `test`.`book` (`book_id`, `book_name`, `price`, `stock`) VALUES (1, '新华词典', 80, 93);
INSERT INTO `test`.`book` (`book_id`, `book_name`, `price`, `stock`) VALUES (2, '本草纲目', 50, 100);
# 用户表构建sql
CREATE TABLE `user` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(20) DEFAULT NULL COMMENT '用户名',
  `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
# 用户表插入数据
INSERT INTO `test`.`user` (`user_id`, `username`, `balance`) VALUES (1, '小明', 20);

创建配置类,配置数据源和开启组件扫描

@Configuration
@ComponentScan("com.shen.annoation")
public class SpringConfig {
    @Bean
    public DataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test?serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}

数据访问层

public interface BookDao {
    Integer getBookPriceById(Integer bookId);

    void updateBookStock(Integer bookId);

    void updateUserBalance(Integer userId, Integer bookPrice);
}
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getBookPriceById(Integer bookId) {
        String selectPriceSql = "select price from book where book_id = ?";
        return jdbcTemplate.queryForObject(selectPriceSql, Integer.class, bookId);
    }

    @Override
    public void updateBookStock(Integer bookId) {
        String updateStackSql = "update book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(updateStackSql, bookId);
    }

    @Override
    public void updateUserBalance(Integer userId, Integer bookPrice) {
        String updateBalanceSql = "update user set balance = balance - ? where user_id = ?";
        jdbcTemplate.update(updateBalanceSql, bookPrice, userId);
    }
}

业务层

public interface BookService {

    /**
     * 买一本书
     *
     * @param bookId 图片id
     * @param userId 用户id
     */
    void buyBook(Integer bookId, Integer userId);
}
@Service
public class BookServiceImpl implements BookService {
    private static final Logger logger = LoggerFactory.getLogger(BookServiceImpl.class);
    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer bookId, Integer userId) {
        // 查询书的价格
        Integer bookPrice = bookDao.getBookPriceById(bookId);
        logger.info("图书价格为:{}", bookPrice);
        //更新图书库存
        bookDao.updateBookStock(bookId);
        //更新用户余额
        bookDao.updateUserBalance(userId, bookPrice);
    }
}

控制层

@Controller
public class BookController {
    @Autowired
    private BookService bookService;

    /**
     * 买书的方法
     *
     * @param bookId 图书id
     * @param userId 用户id
     */
    public void buyBook(Integer bookId, Integer userId) {
        bookService.buyBook(bookId, userId);
    }
}

junit5测试类

@SpringJUnitConfig(classes = SpringConfig.class)
public class TestAllAnnoationBuyBook {
    @Autowired
    private BookController bookController;

    @Test
    void buyBook() {
        bookController.buyBook(1, 1);
    }
}

业务场景构建中没有使用事务管理存在的问题

当前构建的业务场景中,预期是让用户id为1的购买图书id为1的书,执行构建书的逻辑后会进行书籍价格查询,书籍库存减一,用户余额扣除的操作。由于之前构建用户余额balance字段的数据类型为无符号的int类型,因此在用户余额扣除操作时余额为负数的情况下更新数据库时会报错。
由于没有添加事务管理,不会保证事务中的ACID特性,因此只会在用户余额扣除操作报异常而书籍库存减一操作正常执行,不会对买书进行的数据库操作进行全部回滚。这样会导致用户余额不变但书籍库存缺减少一本的情况,由此可见添加事务管理的必要性。

开启事务的注解驱动

开启事务的注解驱动后,就可以使用@Transactional注解标识方法或者类(即类中的所有方法),标记的方法会被事务管理器进行管理。开启事务的注解驱动有两种:通过配置文件开启和通过配置类开启

通过配置类开启

@Configuration
@ComponentScan("com.shen.annoation")
//开启事务管理
@EnableTransactionManagement
public class SpringConfig {
    @Bean
    public DataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test?serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

通过配置文件开启

<?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
       http://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.shen.annoation"></context:component-scan>

    <!--    导入外部属性文件-->
    <context:property-placeholder location="classpath:jdbc.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>

    <!--    配置JDBC template-->
    <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"></tx:annotation-driven>
</beans>

事务注解(@Transactional)

@Transactional是Spring框架中用于声明事务的注解,它可以将方法或类标记为事务性的,即该方法或类中所有数据库操作将在一个事务中执行。使用@Transactional注解可以简化事务管理,使得开发者无需编写复杂的事务控制代码。

事务注解的源码

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {
	@AliasFor("transactionManager")
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";

	String[] label() default {};

	Propagation propagation() default Propagation.REQUIRED;

	Isolation isolation() default Isolation.DEFAULT;

	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

	String timeoutString() default "";

	boolean readOnly() default false;

	Class<? extends Throwable>[] rollbackFor() default {};

	String[] rollbackForClassName() default {};

	Class<? extends Throwable>[] noRollbackFor() default {};

	String[] noRollbackForClassName() default {};
}

事务注解的关键特性

  1. 事务传播行为:定义事务如何在方法之间传播。例如一个方法可以在已存在的事务中运行,或者总是开始一个新的事务。
  2. 事务隔离级别:定义事务的隔离级别,以解决并发事务中的问题,如脏读、不可重复读、幻读。
  3. 事务回滚规则:指定哪些异常会事务回滚。
  4. 事务超时:指定事务在多长时间后自动回滚。
  5. 只读事务:指定事务是否为只读,只能事务可以优化性能。

事务注解的属性

只读属性(readOnly)

是否为只读事务,true为只读事务,默认是false

@Transactional(readOnly = true)
超时属性(timeout)

事务超时时间,单位为秒

@Transactional(timeout = 1)
回滚策略属性(rollbackFor)

归滚策略属性有四种,rollbackFor是指定哪些异常类型会导致事务回滚,noRollbackFor是指定哪些异常类型不会导致事务回滚

  • rollbackFor属性:设置一种或多种异常的Class类型的对象,该异常会导致事务回滚。
  • rollbackForClassName:设置一种或多种字符串类型的全类名,该异常会导致事务回滚。
  • noRollbackFor:设置一种或多种异常的Class类型的对象,该异常不会导致事务回滚。
  • noRollbackForClassName:设置一种或多种字符串类型的全类名,该异常会导致事务回滚。
@Transactional(rollbackFor = ArithmeticException.class)
@Transactional(rollbackForClassName = "java.lang.ArithmeticException")
@Transactional(noRollbackFor = ArithmeticException.class)
@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
隔离级别属性(isolation)

事务的隔离级别,默认使用数据库莫的隔离级别

事务的四种隔离级别
隔离级别 描述
read uncommitted(读未提交的数据) 允许事务读取未被其他事务提交的变更(脏读、不可重复读、幻读问题都会出现)
read commited(读已提交的数据) 只允许事务读取已经被其他事务提交的变更(可以避免脏读)
repeatable read(可重复读) 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新(可以避免脏读、不可重复读)
serializable(串行化) 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表进行插入、更新、删除操作,避免所有并发但性能低(可以避免脏读、不可重复读、幻读)
不同数据库默认的事务隔离级别
数据库 事务隔离级别
MySQL repeatable read(可重复读)
Oracle read commited(读已提交的数据)
SQL Server read commited(读已提交的数据)
PostgreSQL read commited(读已提交的数据)
传播行为属性(propagation)

用于定义事务的传播行为。事务的传播行为决定了当多个事务方法被嵌套调用时,事务如何在这些方法之间传播。

事务的传播行为
传播行为 说明
Propagation.REQUIRED 默认的传播行为。
如果当前没有事务,就新建一个事务并加入这个事务。
如果当前已经存在一个事务,就加入这个事务,不会重新创建事务。
Propagation.SUPPORTS 如果当前存在事务,就加入这个事务。
如果当前没有事务,就以非事务方式执行。
Propagation.MANDATORY 如果当前存在事务,就加入这个事务。
如果当前没有事务,就抛出异常。
Propagation.REQUIRES_NEW 无论当前是否存在事务,都会新建一个事务,并在新的事务中执行。
如果当前已经存在事务,就将当前事务挂起。
Propagation.NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就将当前事务挂起。
Propagation.NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
Propagation.NESTED 如果当前存在事务,则在嵌套事务内执行。
如果当前没有事务,就新建一个事务。
@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.SUPPORTS)
@Transactional(propagation = Propagation.MANDATORY)
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional(propagation = Propagation.NEVER)
@Transactional(propagation = Propagation.NESTED)

声明式事务的示例

此示例是基于上文中业务场景构建代码中添加了事务管理,使得买书方法中所有的数据库相关操作为一个事务,保证ACID原则。这里只展示修改的部分代码。

通过配置类开启事务的注解驱动

@Configuration
@ComponentScan("com.shen.annoation")
//开启事务管理
@EnableTransactionManagement
public class SpringConfig {
    @Bean
    public DataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test?serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

业务类中添加事务注解

@Service
//事务属性-只读
//@Transactional(readOnly = true)
//事务属性-超时3秒直接返回,-1则为没有超时时间
//@Transactional(timeout = 3)
//事务属性-设置回滚策略,针对运行时异常回滚,rollbackFor 设置哪些异常回滚,noRollbackFor 设置哪些不进行回滚
//@Transactional(noRollbackFor = ArithmeticException.class)
//事务属性设置隔离级别 READ_UNCOMMITTED 读未提交 ,READ_COMMITTED 读已提交 ,REPEATABLE_READ 可重复读 ,SERIALIZABLE 串行化,DEFAULT按数据库默认隔离级别
//@Transactional(isolation = Isolation.READ_COMMITTED)
//事务属性-事务的传播行为 总共7种,常见的 REQUIRED 支持当前事务,如果不存在就新建一个 。REQUIRES_NEW 开启一个新事务
//@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional
public class BookServiceImpl implements BookService {
    private static final Logger logger = LoggerFactory.getLogger(BookServiceImpl.class);
    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer bookId, Integer userId) {

        //测试超时
//        try {
//            Thread.sleep(5000);
//        } catch (InterruptedException e) {
//            logger.error("", e);
//            Thread.currentThread().interrupt();
//        }
        // 查询书的价格
        Integer bookPrice = bookDao.getBookPriceById(bookId);
        logger.info("图书价格为:{}", bookPrice);

        //更新图书库存
        bookDao.updateBookStock(bookId);

        //更新用户余额
        bookDao.updateUserBalance(userId, bookPrice);
//        测试异常不回滚
//        int a = 1 / 0;
    }
}

基于XML配置文件的声明式事务(不常用)

基于XML配置文件的声明式事务,需要结合SpringAOP来对指定方法进行增强,不如注解方式常用,下面代码也是基于用户买书业务场景的示例

引入依赖

由于使用了SpringAOP,除了数据库连接的依赖外还需要引入AOP的相关依赖

    <dependencies>
        <!--spring context依赖-->
        <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!--spring jdbc  Spring 持久化层支持jar包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!-- 数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>

        <!--spring对junit的支持相关依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.9.0</version>
        </dependency>

        <!--spring aop依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!--spring aspects依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.11</version>
        </dependency>
    </dependencies>

数据访问层

public interface BookDao {
    Integer getBookPriceById(Integer bookId);

    void updateBookStock(Integer bookId);

    void updateUserBalance(Integer userId, Integer bookPrice);
}

@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getBookPriceById(Integer bookId) {
        String selectPriceSql = "select price from book where book_id = ?";
        return jdbcTemplate.queryForObject(selectPriceSql, Integer.class, bookId);
    }

    @Override
    public void updateBookStock(Integer bookId) {
        String updateStackSql = "update book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(updateStackSql, bookId);
    }

    @Override
    public void updateUserBalance(Integer userId, Integer bookPrice) {
        String updateBalanceSql = "update user set balance = balance - ? where user_id = ?";
        jdbcTemplate.update(updateBalanceSql, bookPrice, userId);
    }
}

业务层

public interface BookService {

    /**
     * 买一本书
     *
     * @param bookId 图片id
     * @param userId 用户id
     */
    void buyBook(Integer bookId, Integer userId);
}
@Service
public class BookServiceImpl implements BookService {
    private static final Logger logger = LoggerFactory.getLogger(BookServiceImpl.class);
    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer bookId, Integer userId) {

        // 查询书的价格
        Integer bookPrice = bookDao.getBookPriceById(bookId);
        logger.info("图书价格为:{}", bookPrice);

        //更新图书库存
        bookDao.updateBookStock(bookId);

        //更新用户余额
        bookDao.updateUserBalance(userId, bookPrice);
    }
}

控制层

@Controller
public class BookController {
    @Autowired
    private BookService bookService;

    /**
     * 买书的方法
     *
     * @param bookId 图书id
     * @param userId 用户id
     */
    public void buyBook(Integer bookId, Integer userId) {
        bookService.buyBook(bookId, userId);
    }
}

XML配置文件(组件扫描、导入外部属性文件、定义JdbcTemplate的bean、配置事务增强织入到指定方法中)

<?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"
       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.shen.xml"></context:component-scan>

    <!--    导入外部属性文件-->
    <context:property-placeholder location="classpath:jdbc.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>

    <!--    配置JDBC template-->
    <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>

    <!--    配置事务增强-->
    <tx:advice id="tx" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"></tx:method>
            <tx:method name="buy*" read-only="false" propagation="REQUIRED"></tx:method>
        </tx:attributes>
    </tx:advice>

    <!--    配置切入点-->
    <aop:config>
        <aop:pointcut id="pc" expression="execution(* com.shen.xml.*.*(..))"/>
        <aop:advisor advice-ref="tx" pointcut-ref="pc"></aop:advisor>
    </aop:config>

</beans>

junit5测试类

@SpringJUnitConfig(locations = "classpath:bean-xml.xml")
public class TestBuyBook {
    @Autowired
    private BookController bookController;

    @Test
    void buyBook() {
        bookController.buyBook(1, 1);
    }
}

参考资料

https://docs.spring.io/spring-framework/reference/data-access/transaction.html

https://www.bilibili.com/video/BV1kR4y1b7Qc/

posted @ 2025-02-10 19:59  柯南。道尔  阅读(51)  评论(0)    收藏  举报