本地事务
事务的定义
所谓事务,它是一个操作集合,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
事务有4个重要特性:ACID
原子性(Atomicity) | 当事务结束,它对所有资源状态的改变都被视为一个操作,这些操作要不同时成功,要不同时失败 |
---|---|
一致性(Consistency) | 操作完成后,所有数据必须符合业务规则,否则事务必须中止 |
隔离性(Isolation) | 事务以相互隔离的方式执行,事务以外的实体无法知道事务过程中的中间状态 |
持久性(Durable) | 事务提交后,数据必须以一种持久性方式存储起来 |
事务产生的问题:
场景:同一个事务内(同一个服务内)
名称 | 数据的状态 | 实际行为 | 产生原因 |
---|---|---|---|
脏读 | 未提交 | 打算提交但是数据回滚了,读取了提交的数据 | 数据的读取 |
不可重复读 | 已提交 | 读取了修改前的数据 | 数据的修改 |
幻读 | 已提交 | 读取了插入前的数据 | 数据的插入 |
事务隔离级:
名称 | 结果 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read UnCommitted(读未提交) | 什么都不解决 | √ | √ | √ |
Read Committed(读提交) | 解决了脏读的问题 | – | √ | √ |
Repeatable Read(重复读) | mysql的默认级别,解决了不可重复读 | – | – | √ |
Serializable(序列化) | 解决所有问题 | – | – | – |
READ UNCOMMITTED(读未提交数据):允许事务读取未被其他事务提交的变更数据,会出现脏读、不可重复读和幻读问题。
READ COMMITTED(读已提交数据):只允许事务读取已经被其他事务提交的变更数据,可避免脏读,仍会出现不可重复读和幻读问题。
REPEATABLE READ(可重复读):确保事务可以多次从一个字段中读取相同的值,在此事务持续期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读,仍会出现幻读问题。
SERIALIZABLE(序列化):确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,可避免所有并发问题,但性能非常低。
Spring中事务的使用
Spring 事务其实就是对数据库事务的支持。Spring 事务的隔离级别和数据库中的隔离级别是相对应的。Spring事务分为声明式事务和编程式事务(基本很少人使用)。
Spring事务5种隔离级别:
隔离级别 | 含义 |
---|---|
isolation_default | 使用数据库默认的事务隔离级别 |
isolation_read_uncommitted | 允许读取尚未提交的修改,可能导致脏读、幻读和不可重复读 |
isolation_read_committed | 允许从已经提交的事务读取,可防止脏读、但幻读,不可重复读仍然有可能发生 |
isolation_repeatable_read | 对相同字段的多次读取的结果是一致的,除非数据被当前事务自生修改。可防止脏读和不可重复读,但幻读仍有可能发生 |
isolation_serializable | 完全服从acid隔离原则,确保不发生脏读、不可重复读、和幻读,但执行效率最低。 |
Spring事务7种传播行为
传播行为 | 含义 |
---|---|
propagation_required(xml文件中为required) | 表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚) |
propagation_supports(supports) | 表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行 |
propagation_mandatory(mandatory) | 表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常 |
propagation_nested(nested) | 表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同propagation_required的一样 |
propagation_never(never) | 表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常 |
propagation_requires_new(requires_new) | 表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。 |
propagation_not_supported(not_supported) | 表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行 |
使用Spring中的事务非常简单只需要两步:
- 添加数据源配置,在启动类上添加@EnableTransactionManagement注解
- 在业务方法上添加@Transactional即可
@Transactional属性
属性 | 类型 | 描述 |
---|---|---|
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | enum: Propagation | 可选的事务传播行为设置 |
isolation | enum: Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 读写或只读事务,默认读写 |
timeout | int (in seconds granularity) | 事务超时时间设置 |
rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 |
noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 |
用法:
在需要事务管理的地方加@Transactional 注解。@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。
@Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。
注意仅仅 @Transactional 注解的出现不足于开启事务行为,它仅仅 是一种元数据。必须在配置文件中使用配置元素,才真正开启了事务行为。
通过 元素的 “proxy-target-class” 属性值来控制是基于接口的还是基于类的代理被创建。如果 “proxy-target-class” 属值被设置为 “true”,那么基于类的代理将起作用(这时需要CGLIB库cglib.jar在CLASSPATH中)。如果 “proxy-target-class” 属值被设置为 “false” 或者这个属性被省略,那么标准的JDK基于接口的代理将起作用。
Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
@Transactional 的事务开启 ,是基于接口的 或者是基于类的代理被创建。所以在同一个类中一个方法调用另一个方法有事务的方法,事务是不会起作用的。
详细使用可以参考:Spring事务隔离级别、传播机制以及简单配置
Spring事务失效问题
Spring事务失效的场景有:
- @Transactional 应用在非 public 修饰的方法上
- @Transactional 注解属性 propagation 设置错误
- @Transactional 注解属性 rollbackFor 设置错误
- 同一个类中方法调用,导致@Transactional失效
- 异常被catch捕获导致@Transactional失效
举个例子:
class AService(){
@Transactional(isolation = Isolation.READ_COMMITTED)
public void a(){
b();
c();
}
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void b(){
//....
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void c(){
//....
}
}
在以上这种情景中a调用了同一个类里面的b和c方法,那么b和c方法的事务会失效。a的事务设置会覆盖b和c的
class AService(){
@Transactional
void a(){
b();
c();
int i=10/0;
}
@Transactional(propagation = Propagation.REQUIRED)
void b(){
//....
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
void c(){
//....
}
}
以上情景中a和b将共用一个事务,c为一个新的事务。因此会造成a,b回滚事务,c不回滚
class AService(){
@Transactional
void a(){
bService.b();
cService.c();
int i=10/0;
}
}
这种情况下a,b,c事务均生效。原因在于Spring的事务是通过代理对象来实现的,同一个代理对象内调用方法会覆盖。
事务失效解决方法
同一个对象内事务方法互相调用默认失效的,原因在于绕过了代理对象。
因此解决方法是,使用代理对象来调用事务方法,具体实施如下:
引入aop依赖,aop中包含了aspectj
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
开启动态代理,在启动类添加@EnableAspectJAutoProxy,对外暴露代理对象
使用代理对象在本类互调
class AService{ @Transactional void a(){ AService aProxyService =(AService) AopContext.currentProxy(); aProxyService.b(); aProxyService.c(); } @Transactional(propagation = Propagation.REQUIRED) void b(){ //.... } @Transactional(propagation = Propagation.REQUIRES_NEW) void c(){ //.... } }
长事务问题
长事务产生原因?
@Transactional
注解,是使用 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个方法都是使用同一个connection连接。如果我们出现了耗时的操作,比如第三方接口调用,业务逻辑复杂,大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。
在一个事务中执行RPC操作导致数据库连接池撑爆属于是典型的长事务问题,类似的操作还有在事务中进行大量数据查询,业务规则处理等…
何为长事务?
顾名思义就是运行时间比较长,长时间未提交的事务,也可以称之为大事务。
长事务会引发哪些问题?
长事务引发的常见危害有:
- 数据库连接池被占满,应用无法获取连接资源;
- 容易引发数据库死锁;
- 数据库回滚时间长;
- 在主从架构中会导致主从延时变大。
如何避免长事务?
解决长事务的宗旨就是 对事务方法进行拆分,尽量让事务变小,变快,减小事务的颗粒度。
避免长事务最简单的方法就是不要使用声明式事务@Transactional
,而是使用编程式事务手动控制事务范围。