事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败(出现异常),都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。
我们先来看一下事务并发,数据库可能会出现的问题:
**更新丢失(问题严重) **
当有两个并发执行的事务,更新同一行数据,那么有可能一个操作会把另一个操作的更新数据覆盖掉。
**脏读 (问题严重) **
一个事务读到另一个尚未提交的事务中的数据,即读到了事务的处理过程中的数据,而不是结果数据。 该数据可能会被回滚从而失效。 如果第一个事务拿着失效的数据去处理那就发生错误了。
不可重复读 (一般来说可以接受,比如你交话费,交完就查看可能没到账,过2分钟再查就到账了)
不可重复读的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:
虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。
幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果数量发生了变化。
不可重复读 与 脏读 的区别?
脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。
数据库一共有如下四种隔离级别:
Read uncommitted 读未提交
在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。
因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。
Read committed 读提交 (oracle、sqlserver默认的隔离级别)
在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。
Repeatable read 重复读 (mysql的默认隔离级别)
简单说就是:一个事务开始读或写数据时,不允许其他事务对该数据进行修改。在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。这个级别无法解决幻读问题。
Serializable 序列化
该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,应该由应用程序员采用悲观锁或乐观锁来控制。
事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。
ServiceA {
@Transactional(Propagation=XXX)
void methodA() {
//其他持久层操作数据库
ServiceB.methodB();
}
}
ServiceB {
@Transactional(Propagation=YYY)
void methodB() {
//持久层操作数据库
}
}
代码中methodA()
方法嵌套调用了methodB()
方法,methodB()
的事务传播行为由@Transactional(Propagation=YYY)
设置决定。
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
新建的Spring Boot
项目中,一般都会引用spring-boot-starter
或者spring-boot-starter-web
,而这两个起步依赖中都已经包含了对于spring-boot-starter-jdbc
或spring-boot-starter-data-jpa
的依赖。 当我们使用了这两个依赖的时候,框架会自动默认分别注入DataSourceTransactionManager
或JpaTransactionManager
。
所以我们不需要任何额外配置就可以用@Transactional
注解进行事务的管理。在spring框架内实现多个数据库持久层操作的事务,我们只需要在方法或类添加@Transactional
注解即可。@Transactional注解只能应用到public可见度的方法上,可以被应用于接口定义和接口方法,方法会覆盖类上面声明的事务。
@Transactional
public int xxx(){
// 增删改持久层操作一
// 增删改持久层操作二
// ……
}
当多个持久层操作在同一个Service层方法上时,能保证多个持久层操作要么都成功,要么都失败。
value | 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。 |
propagation | 事务的传播行为,默认值为 REQUIRED。 |
isolation | 事务的隔离度,默认值采用 DEFAULT。 |
timeout | 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
read-only | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 |
rollback-for | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。 |
no-rollback- for | 抛出 no-rollback-for 指定的异常类型,不回滚事务。 |
分布式事务分为两种:跨服务的分布式事务,跨库的分布式事务。
跨库的分布式事务:一个服务层函数,需要同时操作两个数据库。我们之前给大家讲的例子都是这一种,实际上总的思路:就是有一个“事务管理器”对象统一管理多个数据源事务的提交与回滚。事务管理器协调多数据源进行两段式提交。
为了大家方便理解:我以小故事方式给大家讲一下两段式提交:
跨服务分布式事务: 也就是说我在做一个服务A的时候,需要通过HTTP网络请求调用多个其他服务,有可能第一个服务B成功了,第二个服务C执行失败了。我们期望的结果是:服务B和服务C都成功。这种分布式单纯的依靠数据库层面就很难解决了。
这种情况一般都是通过最终一致性的方式解决。比如:通过MQ消息队列,给服务B发消息,服务B执行,然后真的做持久化操作数据入库了。
给服务C发消息,如果服务C执行失败,这个消息就会存在MQ里面,依照一定的策略还会发给服务C,直到服务C成功为止。这种策略被叫做“ Exactly-once”,精确的保证成功一次并且只成功一次。这样保障操作结果的最终一致性。