事务

概念

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

典型场景:银行转账。

特性

事务必须服从ISO/IEC所制定的ACID原则。

特性 概念
A 原子性(atomicity) 表示事务执行过程中的任何失败都将导致事务所做的任何修改失效。
C 一致性(consistency) 表示执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的;
I 隔离性(isolation) 并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发 事务之间数据库是独⽴的;
D 持久性(durability) ⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。

环境搭建

创建一个Maven项目spring_tx

pom.xml

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
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.2</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.2</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.4</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

bean.xml

image-20210324174446067

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

<!--数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql:///spring"/>
<property name="username" value="root"/>
<property name="password" value="kylin"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>

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

</beans>

dao

image-20210324174118559

1
2
3
4
5
6
7
8
9
10
11
public interface UserDao {

//加钱
void addMoney();

//减钱
void reduceMoney();


}

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

@Autowired
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");
}
}

service

image-20210324174245486

1
2
3
4
public interface UserService {

void accountMoney();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
//@Transactional
public class UserServiceImpl implements UserService {


@Autowired
UserDao userDao;

@Override
public void accountMoney() {
userDao.reduceMoney();

//int a = 4 / 0;

userDao.addMoney();
}
}

sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` int(11) NOT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`money` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES (1, 'lucy', '1000');
INSERT INTO `t_account` VALUES (2, 'mary', '1000');

SET FOREIGN_KEY_CHECKS = 1;

image-20210324173942690

测试

调用service的accountMoney()方法,结果就是lucky转账100给Mary

image-20210324174623027

image-20210324174651164

image-20210324174707260

环境搭建,方法成功!

1
2
3
4
5
6
7
@Test
public void testAccount() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");

UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.accountMoney();
}

接下来我们在业务方法中添加一个异常。

image-20210324174803047

再次运行测试。

image-20210324174858303

控制台出现异常。再次查看数据库(money初始化为1000)

image-20210324174957197

发现lucky转钱方法运行成功,但是mary收钱方法因为异常没有执行。而导致lucky的钱少了100,在实际生活中是不允许的。所以就要我们使用到事务。

声明式事务

编程式事务:所谓编程式事务指的是通过编码方式实现事务,即类似于JDBC编程实现事务管理。管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

声明式事务:管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。

声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

首先要导入相关jar包,tx,aop,aspects。

注解方式

首先我们要在我们的配置文件中加入事务的命名空间

image-20210324181543654

1
xmlns:tx="http://www.springframework.org/schema/tx"
1
2
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"

image-20210324181624795

配置文件中配置事务管理器,注入数据源,开启事务注解。

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

<!--数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql:///spring"/>
<property name="username" value="root"/>
<property name="password" value="kylin"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>

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

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

<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>

接着我们只要在我们想开启事务的类或者方法上标注@Transactional即可。(标注类上,说明该类所有方法都开启事务)

image-20210324181835942

接着我们点击测试方法测试,数据库是否会出现刚才的情况。

image-20210324182013277

image-20210324182041591

事务开启成功。一个失败全失败。进行了回滚。

配置文件

通过配置文件bean2.xml来开启事务。

image-20210324182404862

使用配置文件配置,则需要配置aop相关的配置。将事务操作当作是一个通知。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?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
https://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 https://www.springframework.org/schema/aop/spring-aop.xsd">

<!--注解扫描-->
<context:component-scan base-package="com.kylin"></context:component-scan>

<!--数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql:///spring"/>
<property name="username" value="root"/>
<property name="password" value="kylin"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>

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

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

<!--配置通知-->
<tx:advice id="txAdvice">
<!--配置事务参数-->
<tx:attributes>
<tx:method name="accountMoney" propagation="REQUIRED"/>
<!--<tx:method name="account*"/>-->
</tx:attributes>
</tx:advice>

<!--配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="pt" expression="execution(* com.kylin.service.UserServiceImpl.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>
</beans>

image-20210324182718546

将注解注释。

image-20210324182826397

运行测试。

image-20210324182906030

image-20210324182922518

事务配置成功。

完全注解

将spring的配置文件,用javaConfig创建一个配置类来代替。

image-20210325102845987

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
40
41
42
43
44
package com.kylin.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration//配置类
@ComponentScan("com.kylin")//组件扫描
@EnableTransactionManagement//开启事务
public class TxConfig {

//创建数据库连接池
@Bean
public DruidDataSource getDruidDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///spring");
dataSource.setUsername("root");
dataSource.setPassword("kylin");
return dataSource;
}

//创建JdbcTemplate对象
@Bean
public JdbcTemplate getJdbcTemplate(DruidDataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
//注入dataSource
jdbcTemplate.setDataSource(getDruidDataSource());
return jdbcTemplate;
}

//创建事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DruidDataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}

service使用@Transactional注解

image-20210325102953415

测试。

image-20210325103110838

1
2
3
4
5
6
7
@Test
public void testAccount3() {
ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);

UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.accountMoney();
}

image-20210325103141244

事务属性

我们可以通过@Transactional注解看看Spring事务的相关属性。(配置文件一样)

image-20210324183724696

propagation

事务传播行为。事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

Spring定义了七种传播行为

支持当前事务的情况

  • TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事 务;如果当前没有事务,则创建一个新的事务。 默认.在应用开发中,80%的情况下都会使用这种事务传播属性。

  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

  • TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

不支持当前事务的情况

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。

  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。

  • TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况

  • TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务 作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED

优质博文链接

isolation

事务隔离级别。通过设置事务隔离级别,解决读问题。

数据库隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交)
REPEATABLE READ(可重复读)
SERIALIZABLE(串行化)

Spring事务的隔离级别多了一个DEFAULT使用后端数据库默认的隔离级别(默认

  • MySQL默认隔离级别采用的是REPEATABLE READ
  • Oracle默认采用的隔离级别是READ COMMITTED

timeout

超时时间。事务需要在一定时间内进行提交,如果不提交进行回滚。

默认值为-1也就是没有限制,单位为秒。

readOnly

是否只读。默认值为false,可以查询和添加修改删除操作。

true只能查询。

rollbackFor

回滚。设置出现了哪些异常,进行事务回滚。

noRollbackFor

不回滚。设置出现了哪些异常,不进行事务回滚。