一、事务
1.1 概念
事务是指数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向系统提交,要么都执行,要么都不执行。
通俗地事务就是一组不可再分割的操作集合,集合中的操作要么一起成功(事务提交),要么全部失败(事务回滚)
1.2 特性(ACID)
● 原子性(Atomicity)
事务中所有的操作都是不可再分割的原子单位,事务中的操作要么全部执行成功,要么全部执行失败
● 一致性(Consistency)
事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
● 隔离性(Isolation)
在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰
● 持久性(Durability)
一旦事务提交,事务中所有的数据操作都必须持久化到数据库中,即使提交事务后数据库马上崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据
1.2 并发事务存在的问题
问题名称 | 描述 | 对应隔离级别 |
脏读 | 事务A读取了事务B中尚未提交的数据,如果事务B回滚,则A读取使用了错误的数据 | READ_UNCOMMITTED(读未提交) |
不可重复读 | 对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了 | READ_COMMITTED(读提交) |
幻读 | 在事务的多次读取中,另一个事务对数据进行了新增操作,导致事务A多次读取的数据不一致(与不可重复读的区别在于不可重复读是修改数据,幻读是增删数据) | REPEATABLE_READ(重复读) |
回滚丢失 | 两个事务A、B同时在执行一个数据,事务B提交了,事务A回滚了,这样B事务的操作就由于A事务的回滚而丢失了 | |
覆盖丢失 | 两个事务A、B同时在执行一个数据,两个事务同时取到一个数据,事务B首先提交,之后事务A又提交数据,会覆盖B事务提交的数据内容 |
1.3 隔离级别
隔离级别即定义两个事务之间的影响程度,从低到高分别为:
事务级别 | 名称 | 说明 |
ISOLATION_READ_UNCOMMITTED | 读未提交 | 一个事务可以读取另一个事务未提交的数据 |
ISOLATION_READ_COMMITTED | 读提交 | 一个事务要等到另一个事务提交后才能读取数据,解决脏读问题(Oracle的默认级别),可能产生不可重复读 |
ISOLATION_REPEATABLE_READ | 重复读 | 在开始读取数据(事务开启)时,不再允许修改操作,解决不可重复读问题(MySQL默认级别),可能产生幻读 |
ISOLATION_SERIALIZABLE | 串行化 | 最高的事务隔离级别,此隔离级别下事务串行化顺序执行,解决前面的问题,但效率低下,一般不使用 |
1.5 事务传播特性
1.5.1 概念
事务的传播特性指的是当一个存在事务的方法被另一个存在事务的方法调用时,这个事务方法应该如何进行
举个例子,发起一次操作,转账10次,每次100元。那么就存在两个事务:转账10次,转账100元。这就有两个问题:
- 那么当其中某次转账出错,是否之前的事务都要回滚?
- 转账10次成功,但在这个事务的后续事务中出错,是否之前的10次转账都要回滚?
事务特性就是指定出现上述情况时,事务要怎么处理
1.5.2 传播特性类型
事务传播行为 | 说明 | 使用场景 |
PROPAGATION_REQUIRED(默认) | 支持当前事务,假设当前没有事务。就新建一个事务 | 增删修 |
PROPAGATION_SUPPORTS | 支持当前事务,假设当前没有事务,就以非事务方式运行 | 查询 |
PROPAGATION_MANDATORY | 支持当前事务,假设当前没有事务,就抛出异常 | |
PROPAGATION_REQUIRES_NEW | 新建事务,假设当前存在事务。把当前事务挂起 | |
PROPAGATION_NOT_SUPPORTED | 以非事务方式运行操作。假设当前存在事务,就把当前事务挂起 | |
PROPAGATION_NEVER | 以非事务方式运行,假设当前存在事务,则抛出异常 | |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
类型总结:
● 死活不要事务
PROPAGATION_NEVER:没有就非事务执行,有就抛出异常,即父子方法中如果存在事务就抛出异常
PROPAGATION_NOT_SUPPORTED:没有就非事务执行,有就将事务直接挂起,然后非事务执行,执行完后再启用事务。即若父方法中有事务,则在运行子方法时挂起,直到子方法运行完成,才继续事务
● 可有可无的
PROPAGATION_SUPPORTS: 有就用,没有就算了。即若父子方法中存在事务,则以这个事务的方法运行
● 必须有事物的
PROPAGATION_REQUIRES_NEW:有没有都新建事务,如果原来有,就将原来的挂起。即将原来的事务挂起,自己新建事务并执行
PROPAGATION_NESTED: 如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务,即如果父子都有事务,则同进退。外部事务会影响里面事务,里面事务不会影响外部事务
PROPAGATION_REQUIRED: 如果没有,就新建一个事务;如果有,就加入当前事务。即如果没有,则创建,若父方法有事务,则用父方法的事务
PROPAGATION_MANDATORY: 如果没有,就抛出异常;如果有,就使用当前事务。即若父方法没有事务,则抛异常,若有事务,则使用当前事务
二、 配置和使用
2.1 xml 方式配置
<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.tom"></context:component-scan>
<context:property-placeholder location="classpath:jdbcConfig.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 事务管理的模板 -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"></property>
</bean>
</beans>
使用
@Slf4j
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private UserDao userDao;
@Override
public void transfer(String fromName, String toName, Integer money) {
transactionTemplate.execute(status -> {
userDao.out(fromName, money);
int x = 10;
if (x == 10) {
throw new RuntimeException("出错啦!");
}
userDao.in(toName, money);
return null;
});
}
}
2.2. 注解配置方式
package com.tom.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
/**
* @File: JdbcConfig
* @Description:
* @Author: tom
* @Create: 2020-07-20 09:30
**/
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String user;
@Value("${jdbc.password}")
private String password;
@Bean(name = "dataSource")
public DataSource createDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
// 事务配置
@Bean
public TransactionTemplate createTransactionTemplate(DataSourceTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate();
transactionTemplate.setTransactionManager(transactionManager);
return transactionTemplate;
}
@Bean
public DataSourceTransactionManager createTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
使用
@Slf4j
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private UserDao userDao;
@Override
public void transfer(String fromName, String toName, Integer money) {
transactionTemplate.execute(status -> {
userDao.out(fromName, money);
int x = 10;
if (x == 10) {
throw new RuntimeException("出错啦!");
}
userDao.in(toName, money);
return null;
});
}
}
2.3 声明方式配置(重点)
2.3.1 xml 方式配置
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<context:component-scan base-package="com.tom"></context:component-scan>
<context:property-placeholder location="classpath:jdbcConfig.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- <!– 事务管理的模板 –>-->
<!-- <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">-->
<!-- <property name="transactionManager" ref="transactionManager"></property>-->
<!-- </bean>-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED"></tx:method>
<tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED"></tx:method>
<tx:method name="update*" isolation="DEFAULT" propagation="REQUIRED"></tx:method>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="mypointcut" expression="execution(* com.tom.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="mypointcut"></aop:advisor>
</aop:config>
</beans>
2.3.2 注解方式配置
修改配置类,增加 @EnableTransactionManagement
@Component
@ComponentScan({"com.tom"})
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
@EnableTransactionManagement
public class SpringConfiguration {
}
依然要写 TransactionManager
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String user;
@Value("${jdbc.password}")
private String password;
@Bean(name = "dataSource")
public DataSource createDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public DataSourceTransactionManager createTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
使用 @Transactional
@Transactional
public void transfer(String fromName, String toName, Integer money) {
userDao.out(fromName, money);
int x = 10;
if (x == 10) {
throw new RuntimeException("出错啦!");
}
userDao.in(toName, money);
}
三、传播方式使用以及死锁
3.1 由于事务被挂起导致表死锁
在多个方法都含有事务,且互相嵌套的情况下,可能导致死锁
如下面代码的情况,当方法运行到子方法的时候,父方法的事务被挂起,此时对应的表被锁住。由于子方法需要操作数据库,可是数据库表已经被父方法的事务锁住了。这就导致子方法无限期等待,造成死锁。解决方式有给父方法设置超时时间
@Transactional(propagation = Propagation.REQUIRED)
public void laoda(String formName, String toName, Integer money) {
userDao.out(formName, money);
try {
userService.xiaodi(toName, money);
} catch (Exception e) {
e.printStackTrace();
}
int x = 10;
if (x == 10) {
throw new RuntimeException("老大出错啦!");
}
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void xiaodi(String toName, Integer money) {
userDao.in(toName, money);
int x = 10;
if (x == 10) {
throw new RuntimeException();
}
}