Spring学习(五):事务管理

概述

什么是事务

事务是数据库操作最基本单元。逻辑上一组操作,要么都成功,如果有一个失败所有操
作都失败。

事务的四个特性

  • 原子性 Atomicity
  • 一致性 Consistency
  • 隔离性 Isolation
  • 持久性 Durability

搭建环境

模拟转账场景:Lucy给Mary转账,Lucy少钱,Mary多钱。

配置步骤

  • 引入相关jar包
    druid-1.1.9.jar
    mysql-connector-java-5.1.7-bin.jar
    spring-jdbc-5.2.6.RELEASE.jar
    spring-orm-5.2.6.RELEASE.jar
    spring-tx-5.2.6.RELEASE.jar
    引入后所有包:
    com.springsource.net.sf.cglib-2.2.0.jar
    com.springsource.org.aopalliance-1.0.0.jar
    com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
    commons-logging-1.1.1.jar
    druid-1.1.9.jar
    mysql-connector-java-5.1.7-bin.jar
    spring-aop-5.2.6.RELEASE.jar
    spring-aspects-5.2.6.RELEASE.jar
    spring-beans-5.2.6.RELEASE.jar
    spring-context-5.2.6.RELEASE.jar
    spring-core-5.2.6.RELEASE.jar
    spring-expression-5.2.6.RELEASE.jar
    spring-jdbc-5.2.6.RELEASE.jar
    spring-orm-5.2.6.RELEASE.jar
    spring-tx-5.2.6.RELEASE.jar

  • 创建数据库和建表
    数据库user_db、表t_account
    插入数据后表结果:

    1
    2
    3
    4
    id      username   money  
    ------ -------- --------
    1 Lucy 1000
    2 Mary 1000
  • spring的配置文件注入连接池和jdbcTemplate

  • 创建 service和dao并完成对象的创建和注入关系

  • 在dao中创建多钱和少钱的方法,在service中创建转账的方法

示例代码

代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─src
│ bean.xml

└─com
└─spring5
│ Test.java

├─dao
│ UserDao.java
│ UserDaoImpl.java

└─service
UserService.java

bean.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--组件扫描-->
<context:component-scan base-package="com.spring5"></context:component-scan>
<!--数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql:///user_db?useUnicode=true&amp;characterEncoding=utf8" />
<property name="username" value="root" />
<property name="password" value="root" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
</bean>

<!-- JdbcTemplate 对象 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--set方式注入dataSource-->
<property name="dataSource" ref="dataSource"></property>
</bean>

UserDao接口:

1
2
3
4
5
public interface UserDao {

void addMoney();
void reduceMoney();
}

UserDaoImpl类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Repository
public class UserDaoImpl implements UserDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
public void addMoney() {
String sql = "update t_account set money=money+? where username=?";
jdbcTemplate.update(sql,100, "Mary");
}

@Override
public void reduceMoney() {
String sql = "update t_account set money=money-? where username=?";
jdbcTemplate.update(sql,100, "Lucy");
}
}

UserService类:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {

@Autowired
private UserDao userDao;

public void accountMoney(){
// 两个方法是为了更好的看清楚事务案例
userDao.reduceMoney();
userDao.addMoney();
}
}

Test类:

1
2
3
4
5
6
7
8
9
public class Test {

@org.junit.Test
public void testAccount(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.accountMoney();
}
}

运行后程序没有异常,数据库效果:

1
2
3
4
id      username   money  
------ -------- --------
1 Lucy 900
2 Mary 1100

场景引入

上面的例子若运行有异常,如下所示:

1
2
3
4
5
6
public void accountMoney(){
// 两个方法是为了更好的看清楚事务案例
userDao.reduceMoney();
int a = 100/0;
userDao.addMoney();
}

数据库效果:

1
2
3
4
id      username   money  
------ -------- --------
1 Lucy 800
2 Mary 1100

Lucy少钱,而Mary没多钱。这时候就需要用到事务。

Sping事务管理介绍

  • 一般把事务添加到service层
  • 事务管理方式
    编程式事务管理 :1、开启事务 2、执行业务逻辑 3、若业务逻辑没有异常提交事务,若有异常事务回滚。
    声明式事务管理:1、基于注解实现 2、基于XML实现
  • Spring的声明式事务底层用的是AOP原理

声明式事务管理

注解实现

配置步骤

  • 在spring配置文件中配置事务管理器

  • 在spring配置文件中开启事务注解
    需要引入tx空间

    1
    2
    xmlns:tx="http://www.springframework.org/schema/tx"
    http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd
  • 使用@Transactional注解开启事务。
    如果把这个注解添加类上面,这个类里面所有的方法都添加事务,如果把这个注解添加方法上面,为这个方法添加事务。

Spring事务的传播行为

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

前两种比较常用,需要掌握。
在这里插入图片描述

参考链接1
参考链接2

spring事务的隔离级别

问题

事务有隔离性,多事务操作之间不会产生影响。不考虑隔离性产生很多问题,比如脏读、不可重复读和幻读。

  • 脏读:一个未提交事务读取到另一个未提交事务的数据。
    比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。
  • 不可重复读:一个未提交事务读取到另一提交事务修改数据。一个事务对同一行数据重复读取两次,但是却得到了不同的结果。
    比如银行取钱,事务A开启事务–>查出银行卡余额为1000元,此时切换到事务B事务B开启事务–>事务B取走100元–>提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。
  • 幻读:一个未提交事务读取到另一提交事务添加或删除数据。
    比如学生信息,事务A开启事务–>修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务–>事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。
    在这里插入图片描述
解决方法
隔离级别/是否解决问题 脏读 不可重复读 幻读
READ_UNCOMMITTED(读未提交)
READ_COMMITED(读已提交)
REPEATABLE_READ(可重复读)
SERLALIZABLE(串行化)

Spring隔离级别DEFAULT将使用底层数据库的默认事务隔离级别。MySQL默认隔离级别是REPEATABLE_READ,代码如下:

1
2
3
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ)
public class UserService {
}

Spring事务的其他参数

  • timeout :超时时间
    事务需要在一定时间内进行提交,如果不提交进行回滚;默认值是 -1表示不失效,设置时间以秒单位进行计算
  • readOnly :是否只读
    readOnly 默认值 false,可以进行增删查改操作;设置为true后只能进行查询操作
  • rollbackFor :回滚
    设置出现哪些异常进行事务回滚
  • noRollbackFor :不回滚
    设置出现哪些异常不进行事务回滚

示例代码如下:

1
@Transactional(timeout = 100,readOnly = false,rollbackFor = {ArrayIndexOutOfBoundsException.class,RuntimeException.class})

XML实现

步骤

  • 配置事务管理器
  • 配置通知
  • 配置切入点和切面

相关代码

java代码中去掉@Transactional注解,其他不变。spring配置文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!--组件扫描-->
<context:component-scan base-package="com.spring5"></context:component-scan>
<!--数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql:///user_db?useUnicode=true&amp;characterEncoding=utf8" />
<property name="username" value="root" />
<property name="password" value="root" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
</bean>

<!-- JdbcTemplate 对象 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--set方式注入dataSource-->
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--1 创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name= "dataSource" ref= "dataSource"></property>
</bean>

<!--2 配置通知-->
<tx:advice id= "txadvice">
<!--配置事务参数-->
<tx:attributes>
<!--指定哪种规则的方法上面添加事务 *表示匹配所有-->
<!--<tx:method name="account*"/>-->
<tx:method name= "accountMoney" propagation= "REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--3 配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id= "pt" expression= "execution(* com.spring5.service.UserService.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref= "txadvice" pointcut-ref= "pt"/>
</aop:config>