事务管理是指进行数据库操作时,要么全部成功,要么全部取消不执行,这一概念在学习数据库时都会提到。在这里,我将简单的对事务进行描述一下。

1. 事务

事务简单来说,就是我们对数据库数据所做的一次操作,例如:修改、删除、添加,是一种操作。在做出一次的操作时,可能涉及到多个数据的修改,这些修改要么全部成功,要么全部不成功,在学习数据库时,转账的案例都快听出茧子了,在这里我不叙述了,直接还原这种情况。

首先,创建一个表:

DROP TABLE IF EXISTS `account`;

CREATE TABLE `account` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(255) COLLATE utf8_general_ci DEFAULT NULL,
    `money` INT(11) COLLATE utf8_general_ci DEFAULT NULL,
    PRIMARY KEY(`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

INSERT INTO `account` VALUES (1, '儿子', 1000), (2, '父亲', 1000);

然后配置 JdbcTemplate,和上一篇文章一致,同时创建一个转账的方法:

@Repository
public class UserDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void addMoney(String username, Integer money) {
        jdbcTemplate.update("update account set money=money+? where username=?", money, username);
    }

    public void subMoney(String username, Integer money) {
        jdbcTemplate.update("update account set money=money-? where username=?", money, username);
    }
}
@Service
public class UserService {
    @Autowired
    private UserDao userDao;

    // 转账方法
    public void updateMoney() {
        userDao.subMoney("父亲", 100);
        int i = 1 / 0;
        userDao.addMoney("儿子", 100);
    }
}

再配置 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:component-scan base-package="com.careyq.transaction"/>
    <!--数据库的连接池-->
    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
        <property name="url" value="jdbc:mysql:///user_db?useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    </bean>
    <!--JdbcTemplate 对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

创建测试方法:

@Test
public void accountTest() {
    ApplicationContext context = new ClassPathXmlApplicationContext("transaction.xml");
    UserService userService = context.getBean("userService", UserService.class);
    userService.updateMoney();
}

运行结果:

java.lang.ArithmeticException: / by zero

UserService 中的转账方法中,是模拟父亲给儿子转账 100 元,理想结果是儿子有 1100,父亲有 900,但在父亲减少之后添加了int i = 1 / 0;,就导致这一次操作过程中会出现异常,从上图可以看出事务出问题了。

事务的 ACID 原则,有必要在这里提一嘴,也当我默写一下:

在上面的例子中,明显的违反了 ACID 原则。

2. 事务管理

以下操作都基于声明式事务管理,底层使用 AOP 原理。

2.1 事务管理器

我们在这里使用的是 JDBC 来进行持久化的,所以需要选择 DataSourceTransactionManager 作为事务管理器:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

ref 引用的是数据库连接池的 ID。

如果使用的是 Hibernate 来实现的,那么应该选择 HibernateTransactionManager 来作为事务管理器。

2.2 使用注解实现

除了上一步的添加事务管理器外,还需要开启事务注解,记得添加约束:

<tx:annotation-driven transaction-manager="transactionManager"/>

transaction-manager 引入的是事务管理器的 ID。

之后,在转账方法上加入注解:

// 转账方法
@Transactional
public void updateMoney() {
    userDao.subMoney("父亲", 100);
    int i = 1 / 0;
    userDao.addMoney("儿子", 100);
}

@Transactional 注解可以添加到方法上,也可以添加到类上,分别是作用域的不同,我想应该不用过多解释。

再次进行测试,观察结果,发生异常后,事务进行了回滚,不再像之前那种,一个成功,另外一个失败。

2.3 使用 XML 实现

使用 XML 配置的方式,首先需要创建事务管理器,这里和上面的一致。接着需要配置通知:

<tx:advice id="txAdvice">
    <tx:attributes>
        <!--表示以 update 开头的方法都添加上-->
        <tx:method name="update*"/>
    </tx:attributes>
</tx:advice>

原来的 <tx:annotation-driven transaction-manager="transactionManager"/> 就不需要了。

接着,配置切入点和切面:

<aop:config>
    <!--切入点-->
    <aop:pointcut id="pt" expression="execution(* com.careyq.transaction.*.*(..))"/>
    <!--切面-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>

之后就可以进行测试,观察结果。

2.4 使用完全注解实现

创建一个配置类:

@Configurable
@ComponentScan(basePackages = "com.careyq.transaction") // 组件扫描
@EnableTransactionManagement // 开启事务
public class Config {
    // 数据库连接池
    @Bean
    public DriverManagerDataSource getDateSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setUrl("jdbc:mysql:///user_db?useUnicode=true&characterEncoding=utf8");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    // JdbcTemplate 对象
    @Bean
    public JdbcTemplate getJdbcTemplate(DriverManagerDataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    // 事务管理器对象
    @Bean
    public DataSourceTransactionManager getManager(DriverManagerDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

创建测试方法:

@Test
public void accountTest2() {
    ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
    UserService userService = context.getBean("userService", UserService.class);
    userService.updateMoney();
}

运行观察结果。

通常情况下,不会将数据库连接信息放到 Java 代码中,这里只是为了演示,请悉知!

2.5 事务五大属性

2.5.1 传播行为

传播行为(propagation behavior)是指多个操作,什么时候创建事务,或者什么时候使用已有的事务。举例来说,假设一个操作中包含另外一个操作,在外面的操作上开启了事务,那么我里面的这个操作是否开启事务呢?或者是直接使用外层的事务。这个行为就是传播行为。

传播行为 含义
MANDATORY 该方法必须在事务中进行,如果当前事务不存在,则会抛出一个异常
NESTED 如果当前已存在一个事务,那么该方法在嵌套事务中运行。否则,行为与 REQUIRED 一样
NEVER 方法不运行在事务上下文中,如果当前正有一个 事务在运行,则会抛出异常
NOT_SUPPORTED 方法不应该运行在事务上下文中,如果存在当前事务,在该方法运行期间,当前事务会被挂起
REQUIRED 方法必须运行在事务中,如果事务不存在,则启动一个新的事务
REQUIRED_NEW 方法必须运行在它自己新启动的事务中,如果存在当前事务,当前事务会被挂起
SUPPORTS 方法不需要事务上下文,如果存在当前事务,那么该方法会在这个事务中运行

使用方式如下:

@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.MANDATORY)
...

2.5.2 隔离级别

隔离性通常是在多个事务并发情况下产生出问题:

解决上面的问题,就需要通过设置事务隔离性:

隔离级别 含义
DEFAULT 使用后端数据库默认的隔离级别
READ_UNCOMMITTED 允许读取尚未提交的数据。可能导致脏读、不可重复读、幻读
READ_COMMITTED 允许读取并发事务已经提交的数据。可以阻止脏读,但不可重复读,幻读也可能发生
REPEATABLE_READ 多次读取结果一致,除非数据是本事务自己修改的。可以阻止脏读、不可重复读,但仍可能发生幻读
SERIALIZABLE 完全服从事务的 ACID 原则,避免脏读、不可重复读、幻读

使用方式如下:

@Transactional(isolation = Isolation.SERIALIZABLE)
...

2.5.3 事务超时

超时(timeout)是指:事务运行时间过长,一致没有提交结束,则会影响效率,所以可以设置超时属性,超时后执行自动回滚。

超时的默认值是 -1,表示不设置超时。设置值是按照 作为单位的:

@Transactional(timeout = 5) // 设置超时时间为 5 秒

超时会在事务开启时启动,只有对具备启动一个新的事务的传播行为(REQUIREDREQUIRED_NEWNESTED)才有意义。

2.5.4 只读

只读(read-only) 设置之后,数据库只能进行读操作,不可以修改数据。

@Transactional(readOnly = true)

只读是在事务启动,由数据库实施的,只有对具备启动一个新的事务的传播行为(REQUIREDREQUIRED_NEWNESTED)才有意义。

2.5.5 回滚

默认情况下,事务遇到运行期异常就回滚。

通过设置 rollbackFor 可以定义遇到哪些异常才 进行回滚

@Transactional(rollbackFor = NullPointerException.class)

还可以设置 noRollbackFor 定义遇到哪些异常 不进行回滚

@Transactional(noRollbackFor = RuntimeException.class)