开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第38天,点击查看活动详情
事务
概念
事务是数据库操作最基本单元,逻辑上的一组操作要么都成功。如果有一个失败所以操作都失败,回滚。
典型场景:银行转账。
特性
事务必须服从ISO/IEC所制定的ACID原则。
| 特性 | 概念 |
|---|---|
| A 原子性(atomicity) | 表示事务执行过程中的任何失败都将导致事务所做的任何修改失效。 |
| C 一致性(consistency) | 表示执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的; |
| I 隔离性(isolation) | 并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发 事务之间数据库是独⽴的; |
| D 持久性(durability) | ⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。 |
环境搭建
创建一个Maven项目spring_tx
pom.xml
<!--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
<?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
public interface UserDao {
//加钱
void addMoney();
//减钱
void reduceMoney();
}
@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
public interface UserService {
void accountMoney();
}
@Service
//@Transactional
public class UserServiceImpl implements UserService {
@Autowired
UserDao userDao;
@Override
public void accountMoney() {
userDao.reduceMoney();
//int a = 4 / 0;
userDao.addMoney();
}
}
sql
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;
测试
调用service的accountMoney()方法,结果就是lucky转账100给Mary
环境搭建,方法成功!
@Test
public void testAccount() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.accountMoney();
}
接下来我们在业务方法中添加一个异常。
再次运行测试。
控制台出现异常。再次查看数据库(money初始化为1000)
发现lucky转钱方法运行成功,但是mary收钱方法因为异常没有执行。而导致lucky的钱少了100,在实际生活中是不允许的。所以就要我们使用到事务。
声明式事务
**编程式事务:**所谓编程式事务指的是通过编码方式实现事务,即类似于JDBC编程实现事务管理。管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
**声明式事务:**管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。**声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,**只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。
声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。**和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。**但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
首先要导入相关jar包,tx,aop,aspects。
注解方式
首先我们要在我们的配置文件中加入事务的命名空间
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"
配置文件中配置事务管理器,注入数据源,开启事务注解。
<?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即可。(标注类上,说明该类所有方法都开启事务)
接着我们点击测试方法测试,数据库是否会出现刚才的情况。
事务开启成功。一个失败全失败。进行了回滚。
配置文件
通过配置文件bean2.xml来开启事务。
使用配置文件配置,则需要配置aop相关的配置。将事务操作当作是一个通知。
<?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>
将注解注释。
运行测试。
事务配置成功。
完全注解
将spring的配置文件,用javaConfig创建一个配置类来代替。
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注解
测试。
@Test
public void testAccount3() {
ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.accountMoney();
}
事务属性
我们可以通过@Transactional注解看看Spring事务的相关属性。(配置文件一样)
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
不回滚。设置出现了哪些异常,不进行事务回滚。