目录
1. 什么是事务
数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起成功,要么一起失败,是一个不可分割的工作单元。
在我们日常工作中,涉及到事务的场景非常多,一个 service 中往往需要调用不同的 dao 层方法,这些方法要么同时成功要么同时失败,我们需要在 service 层确保这一点
事务的四大特性:A:原子性 C:一致性 I:隔离性 D:持久性
2、Spring中事务的实现方式
1、编程式---实现事务
在applicationContext.xml中配置好数据源,和事务管理器:
不推荐使用,代码入侵太多。大量的处理事务的代码穿插到业务代码中
2、声明式---实现事务
(1)、声明式事务:xml形式 提前配置好数据源
配置事务管理器
配置通知,添加事务的切面
Aop的织入,将切面和切入点绑定起来
(2)、configration配置类的形式配置声明式事务
1、配置好数据源信息 2、配置事务管理器 3、开启事务的注解支持
将该配置类添加到包扫描路径下,接来下就可以直接在service的方法或者类上使用@Transactional注解给方法添加事务
(3)、xml+注解方式配置声明式事务
配置完成后,只需要在想要开启注解的方法上加上@Transactional注解就可以了
3、事务的隔离性
1、DEFAULT
DEFAULT 这是spring默认的隔离级别,表示使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。
2、READ_UNCOMMITTED(读未提交)
READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。
3、READ_COMMITTED(读已提交)
这是Sql Server , Oracle默认隔离级别READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。
4:REPEATABLE_READ(可重复读)
这是MySQL-InnoDB默认隔离级别REPEATABLE_READ这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。
5:SERIALIZABLE(可串行化)
SERIALIZABLE 事务被处理为顺序执行。防止脏读,不可重复读,防止幻读。
注意:事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。
4、脏读、不可重复读、幻读
脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
例如:
张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。与此同时,事务B正在读取张三的工资,读取到张三的工资为8000。随后,事务A发生异常,而回滚了事务。张三的工资又回滚为5000。最后,事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。
不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
例如:
在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。与此同时,事务B把张三的工资改为8000,并提交了事务。随后,在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。
幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
例如:
目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。此时,事务B插入一条工资也为5000的记录。这是,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。
提醒
脏读和不可重复读的区别是:
脏读 读取的是为提交的数据。不可重复度读 读取的是已提交的数据。
幻读和不可重复读的区别是:
不可重复读的重点是修改: 同样的条件,你读取过的数据,再次读取出来发现值不一样了
幻读的重点在于新增或者删除: 同样的条件,第 1 次和第 2 次读出来的记录数不一样
5、事务的传播特性
在一个事务执行的过程中,调用另一个事务时候(比如一个service方法调用另一个service方法),这个事务将以何种状态存在,是两个事务共存呢,还是一个事务是另一个事务的子事务,还是一个事务加入另一个事务的子事务呢……利用事务的传播性来解决这个问题。
1、REQUIRED:spring默认的事务的传播性
REQUIRED 表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
@Service
public class AccountService {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional
public void handle1() {
jdbcTemplate.update("update user set money = ? where id=?;", 1, 2);
}
}
@Service
public class AccountService2 {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
AccountService accountService;
public void handle2() {
jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
accountService.handle1();
}
}
-
如果 handle2 方法本身是有事务的,则 handle1 方法就会加入到 handle2 方法所在的事务中,这样两个方法将处于同一个事务中,一起成功或者一起失败(不管是 handle2 还是 handle1 谁抛异常,都会导致整体回滚)。
-
如果 handle2 方法本身是没有事务的,则 handle1 方法就会自己开启一个新的事务。
2、REQUIRES_NEW
REQUIRES_NEW 表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。换言之,不管外部方法是否有事务,REQUIRES_NEW 都会开启自己的事务。
3、NESTED
NESTED 表示如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
4、MANDATORY
MANDATORY 表示如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
5、SUPPORTS
NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。
6、NOT_SUPPORTED
NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。
7、NEVER
NEVER 表示以非事务方式运行,如果当前存在事务,则抛出异常。
6、事务失效的场景
1、数据库不支持事务 2、没有配置事务管理器
3、事务所在的方法没有被public修饰 4、异常被catch,没有抛出,事务会失效
5、异常类型错误,默认是runtimeException才会回滚的
解决方案:加上@Transactional(rollbackFor = Exception.class)注解;这样Exception也会回滚
6、用final或者static关键字修饰的方法事务会失效
7、事务需要从外部调用,Spring 自调事务用会失效。即相同类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效,这点尤其要注意,因为代理模式只拦截通过代理传入的外部方法调用,所以自调用事务是不生效的。
7、spring事务的实现原理
底层是通过aop进行实现,@Transactional注解使用环绕通知,在进入方法前开启事务 。使用try catch包含目标方法,执行目标方法,执行完成后如果没有抛出异常,就提交事务。如果抛出异常就进行回滚。
代码实现:
定义注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface rkTransactional {
}
切面:
@Aspect
@Component
@Slf4j
public class ExtrkThreadAop {
@Autowired
private RkTransaction rkTransaction;
/**
* 只要方法上有加上rkTransactional 走around()
* 异常通知
* @param joinPoint
* @throws Throwable
*/
@Around(value = "@annotation(com.rk.aop.rkTransactional)")
public Object around(ProceedingJoinPoint joinPoint) {
// 在目标方法之前开启事务 底层实现:将事务状态保存在当前线程里面
TransactionStatus transactionStatus = rkTransaction.begin();
try {
Object result = joinPoint.proceed();//目标方法
log.info("目标方法之后执行");
//提交事务
rkTransaction.commit(transactionStatus);
return result;
} catch (Throwable throwable) {
// 目标方法执行向外抛出异常之后 手动回滚
rkTransaction.rollback(transactionStatus);
return "fail";
}
}
}
注解类:
@Component
public class RkTransaction {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
// 开启事务
public TransactionStatus begin() {
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
return transaction;
}
// 提交事务
public void commit(TransactionStatus transactionStatus) {
dataSourceTransactionManager.commit(transactionStatus);
}
// 回滚事务
public void rollback(TransactionStatus transactionStatus) {
dataSourceTransactionManager.rollback(transactionStatus);
}
}
测试:
/**
* 使用事务注解 事务到底在什么时候提交呢?该方法没有抛出异常的情况下就会自动提交事务
* aop
* @param name
* @return
*/
@GetMapping("/insertUser")
@rkTransactional
public String insertUser(String name) {
int result = userMapper.insertUser(name);
if ("rk".equals(name)) {
int j = 1 / 0;
}
return result > 0 ? "ok" : "fail";
}
}