Spring 学习

Spring 学习(十)事务基本使用

/ 14 阅读 / 约 8282 字

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

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

image20210528163112222.png

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

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

  • 原子性:要么全部完成,要么全部不完成
  • 一致性:操作前和操作后的总量不变
  • 隔离性:多个事务同时执行同一个数据时,不会产生影响
  • 持久性:操作完数据后,要保存到数据库中

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

2. 事务管理

TIP

以下操作都基于声明式事务管理,底层使用 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);
}
TIP

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

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

2.3 使用 XML 实现

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

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

原来的 <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();
}

运行观察结果。

TIP

通常情况下,不会将数据库连接信息放到 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 隔离级别

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

  • 脏读(Dirty reads):一个事务读取了另一个事务改写后但未提交的数据。
  • 不可重复读(Nonrepeatable read):事务执行两次以上查询同一个信息,却得到了不同的数据结果。通常是另外一个事务在这个过程中更新了数据。
  • 幻读(Phantom read):事务读取数据,另一个事务插入了几条数据,当第一个事务再次读取时发现多了几条原本没有的数据。

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

隔离级别 含义
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)
最后更新: 2021-06-21 11:44:26
版权声明: 本文著作权归 [ CareyQ ] 享有,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!
评论
  • 🇨🇳
  • 👆
  • 👇
  • 👈
  • 👉
  • 👊
  • 👋
  • 👌
  • 👍
  • 👎
  • 👏
  • 👨‍👦
  • 👨‍👧
  • 👨‍👨‍👦
  • 👨‍👨‍👧
  • 😀
  • 😁
  • 😂
  • 😃
  • 😄
  • 😅
  • 😆
  • 😇
  • 😈
  • 😉
  • 😊
  • 😋
  • 😌
  • 😍
  • 😎
  • 😏
  • 😐
  • 😑
  • 😒
  • 😓
  • 😔
  • 😕
  • 😖
  • 😗
  • 😘
  • 😙
  • 😚
  • 😛
  • 😜
  • 😝
  • 😞
  • 😟
  • 😠
  • 😡
  • 😢
  • 😣
  • 😤
  • 😦
  • 😧
  • 😨
  • 😩
  • 😪
  • 😫
  • 😬
  • 😭
  • 😮
  • 😯
  • 😰
  • 😱
  • 😲
  • 😳
  • 😴
  • 😵
  • 😶
  • 😷
  • 😸
  • 😹
  • 😺
  • 😻
  • 😼
  • 😽
  • 😾
  • 😿
  • 🙀
  • 🙁
  • 🙂
  • 🙃
  • 🙄
还没有评论 /(ㄒoㄒ)/~~