(二)事务:事务配置的规律-Spring MVC+ mybatis 环境

事务隔离特性和传播特性

基于上一篇文章,我们使用 Spring MVC+mybatis+mysql 的环境,通过在 Controller 层增加多线程的方式,实验并发环境下事务的传播特性和隔离特性。

脏读、不可重复读和幻读问题
  • 一个独立事务读取到另一个独立事务尚未提交的数据,这叫脏读
  • 一个独立事务两次读取同一条数据库数据,期间另一个独立事务对该数据修改并已提交,导致第一个独立事务在一个事务内两次读取同一条数据对值不一样,这叫不可重复读
  • 一个独立事务在相同的条件内,两次读取数据库的一定数量的数据,期间另一个独立事务新增或者删除了数据,导致第一个独立事务在相同条件下搜索的结果集合不一样,这叫幻读
  • 脏读、不可重复读和幻读问题 都是一个独立事务受到了另一个独立事务的操作的影响,单独一个事务本身并不会引起这些问题
注意事项
  • 事务是数据库或者驱动的特性,需要数据库开启事务配置,而 mybatis 和 Spring 都可以接管具体的事务配置。

  • 对于controller 访问,实际上服务器是按照多线程模式处理的,即两次访问互不影响,但是如果使用同一个浏览器做连续访问,服务器会做出串行访问的处理,造成访问速度下降,而且,当首次访问时间超时的时候,后面的访问会被安排开始执行。所以我们采取 Controller 层增加多线程的方式,运用单例模式共享变量的方式控制多个线程之间访问的事务,以模拟多种事务并发或者传播的规律。

  • mybatis 有本地缓存默认开启,即同一个Mapper方法在一个事务里执行多次实际上只查询了一次,由于事务是锁定于数据库的数据的,我们可以通过写冗余方法(需要mybatis 采取默认的本地缓存SESSION,二级缓存关闭)来简单规避这个问题,否则不可重复读就无法出现 (三)事务:Mybatis的本地缓存和二级缓存

  • 一般的事务都是加在 Service 层,本文就直接论述为 Service层配置事务

传播特性的规律
  • 多次调用同一个Service,从传播特性来说,二者的事务相互独立,互相不影响。
  • 分开访问两个不同的Service也是相互独立的,二者的事务相互独立,互相不影响。
  • 事务的传播特性仅在Service调用Service的情况时才需要将两个 Service 合起来考虑,而且是出于调用本Service的来源是否具有事务配置的出发点来考虑的。比如已存在事务则共享,如果没有事务则报错
  • 对于 Service 来说如果存在事务,一旦程序执行失败,或者主动抛出未捕获的异常,事务都会回滚,而且,事务内所有程序要么全部执行并提交,要么全部回滚。
  • Service1调用了Service2,从传播特性上来说,Service2可以和Service1保持为同一事务,也可以新起事务,也可以不使用事务,也可以检查事务。
  • 事务1调用事务2,如果事务1配置为有事务,事务2配置为和事务1保持为同一事务,则任何一个报异常,两个Service的事务都会回滚,而且,后面的事务可以读取前面事务的未提交数据,即两个Service 之间的隔离特性失效。
  • 当传播特性设置为不使用事务时,包括隔离特性在内的一切事务相关配置失效。
  • 如果没有显式的配置事务,则数据库的默认事务配置会发挥作用,不同数据库是不同的
隔离特性的规律
  • 一个事务内,隔离特性失效,如果两个Service使用同一个事务,隔离特性就失效了。
  • 两个事务单独访问,本事务设置为 读未提交,则本事务可以读取其他事务的未提交数据。无法避免脏读、不可重复读、幻读。事务之间不会相互阻塞
  • 两个事务单独访问,读已提交,本事务只能读取其他事务已经提交的数据。可以避免脏读,无法避免不可重复读、幻读。事务之间不会相互阻塞
  • 两个事务单独访问,可重复读,本事务内重复读取某一条数据结果始终不变,期间允许其他事务对该数据进行修改和删除。可以避免脏读、不可重复读,无法避免幻读。事务之间不会相互阻塞
  • 两个事务单独访问,序列化,涉及到本事务操作的数据或者表,只有本事务提交后,其他事务才可以执行,反之,如果其他事务正在执行,本事务需要等待,需要强调的是,序列化并不是完全意义的串行,而是在涉及到表操作的时候串行,其他代码是并行的关系,这算是一种优化吧。序列化设置可以避免脏读、不可重复读和幻读。事务之间会相互阻塞。
  • 当传播特性设置为不使用事务时,包括格里特性在内的一切事务相关配置失效。
  • 本事务内的任何操作在本事务内都是即时感知的,而不可重复读、脏读、幻读都是被其他事务修改本线程不知道才发生的现象,所以本事务内导致的数据变更不涉及脏读、不可重复读和幻读问题
  • 如果没有显式的配置事务,则数据库的默认事务配置会发挥作用,不同数据库是不同的
对于常规事务配置的建议
  • 对于普通查询
@Transactional(readOnly=true,isolation=Isolation.READ_COMMITTED,
propagation=Propagation.SUPPORTS,rollbackFor=Exception.class)
  • 对于普通新增或者修改
@Transactional(readOnly=false,isolation=Isolation.READ_COMMITTED,
propagation=Propagation.REQUIRED,rollbackFor=Exception.class)
数据库事务特性 和 Spring 默认事务属性

首先事务是数据库的特性,以mysql来说,其事务又和具体的存储引擎有关,在mysql中常用的 InnoDB存储引擎是支持事务的,MyISAM 存储引擎不支持事务。
也就是说,如果数据库不支持事物,Spring 对事务的配置是无效的。
那么如果数据库支持事务,默认配置是怎么的呢?
在Mysql InnoDB 中,默认的事务配置是 可重复读。
Spring 的事务默认配置是

/**传播类型默认为 REQUIRED*/
Propagation propagation() default Propagation.REQUIRED;

/**隔离级别,默认和数据库统一,比如mysql 就是 可重复读了*/
Isolation isolation() default Isolation.DEFAULT;

/**是否只读,默认否*/
boolean readOnly() default false;

/**遇到异常回滚,默认只有 RuntimeException才回滚,且需要将该异常抛出,此外你可以指定具体类*/
Class<? extends Throwable>[] rollbackFor() default {};

/**遇到异常不会滚,也可以设置*/
Class<? extends Throwable>[] noRollbackFor() default {};

/**事务超时时间,默认是-1,即不计超时时间*/
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
事务不回滚的问题

默认情况下,切面配置层仅对捕获到的 RuntimeExeption 异常进行回滚操作,这意味着需要注意两点,1、异常需要抛出,2、默认仅捕获 Runtimexception.
针对1,你需要注意,要么代码里不要try catch异常,要么在catch里再次抛出异常,这样AOP才能捕获这个异常进行事物回滚。
针对2,你可以在配置事务中增加 rollbackFor = Exception.class,可以是多个

@Transactional(readOnly=false,isolation=Isolation.READ_UNCOMMITTED,propagation=Propagation.REQUIRES_NEW,rollbackFor = {Exception.class,OutOfMemoryError.class})
配置与运行效果举例

(一)事务:Spring 的 Controller 访问特性 中的代码为基准,通过不同的读配置感受事务的运行机制,修改参数的事务配置保持不变

允许脏读,每次新起一个事务

可能出现脏读、不可重复读、幻读问题
各独立事务不会相互阻塞

@Transactional(readOnly=true,propagation=
Propagation.REQUIRES_NEW,isolation=Isolation.READ_UNCOMMITTED)
读已提交,每次新起一个事务

不会出现脏读,可能出现不可重复读、幻读
如果 mybatis 本地缓存设置为 SESSION 则不会出现不可重复读
各独立事务不会相互阻塞

@Transactional(readOnly=true,propagation=
Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED)
可重复读,每次新起一个事务

不会出现脏读、不可重复读,可能出现幻读
各独立事务不会相互阻塞

@Transactional(readOnly=true,propagation=
Propagation.REQUIRES_NEW,isolation=Isolation.REPEATABLE_READ)
序列化,每次新起一个事务

不会出现脏读、不可重复读、幻读
独立事务需要等到配置 SERIALIZABLE 的事务结束才可以运行

@Transactional(readOnly=true,propagation=
Propagation.REQUIRES_NEW,isolation=Isolation.SERIALIZABLE)
第一次搜索:1544332645889;1544334190118
线程二休眠结束:1544334191038;即将修改为1544334190007
线程一休眠结束:1544332645889;1544334195120
第二次搜索:1544332645889;1544334195122
线程二修改结束:1544334195124
  • 对于方法级别的配置,只能在 public 修饰的方法上进行配置才有效

对于方法级别的配置,只能在 public 修饰的方法上进行配置才有效。你的 IDE 应该会提醒你注意这一点。

一个典型的报错:org.springframework.jdbc.datasource.DataSourceTransactionManager getTransaction 警告: Custom isolation level specified but no actual transaction initiated; isolation level will effectively be ignored:PROPAGATION_SUPPORTS,ISOLATION_READ_UNCOMMITTED,readOnly; ‘’

在启动 SpringMVC 项目的时候出现了如上的报错。

org.springframework.jdbc.datasource.DataSourceTransactionManager getTransaction
警告: Custom isolation level specified but no actual transaction initiated; isolation level will effectively be ignored: PROPAGATION_SUPPORTS,ISOLATION_READ_UNCOMMITTED,readOnly; ''

经过仔细认真的排查发现,是因为在 Service 层配置了错误的事务选项造成的。

声明式事务

事务可以分为编程式事务和声明式事务,声明式事务需要在代码中配合 @Transactional 注解使用

<!-- 事务管理器 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- 事物设置 -->
	
	<!-- 方式一:编程式 -->
	<tx:annotation-driven transaction-manager="transactionManager"/>  

提前说好,下面是一个错误的示范

@Service
@Transactional(readOnly=true,isolation=Isolation.READ_UNCOMMITTED,propagation=Propagation.SUPPORTS)
public class UserInfoServiceImpl implements UserInfoService {
	
编程式事务

编程式事务是在xml文件中设置事务,下面是编程式事务的例子

<!-- 事务管理器 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- 事物设置 -->
		
	<!-- 方式二: 声明式事务-->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			事物传播行为和隔离级别
			<tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED"/>
			<tx:method name="insert*" propagation="REQUIRED" />
			<tx:method name="delete*" propagation="REQUIRED" />
			<tx:method name="update*" propagation="REQUIRED" />
			<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
			<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
		</tx:attributes>
	</tx:advice> 
	<!-- 切面 -->
	<aop:config>
		<aop:advisor advice-ref="txAdvice"
			pointcut="execution(* com.bestcxx.stu.springmvc.serviceimpl.*.*(..))"/>
	</aop:config>
	
@Transactional 的正确姿势

该注解可以用在类上,也可以用在方法上。
但是必须是public修饰的类或者方法,加载 private、protected修饰的方法上是无效的。
经常配置的属性如下

@Transactional(readOnly=false,isolation=Isolation.READ_UNCOMMITTED,propagation=Propagation.REQUIRES_NEW,rollbackFor = Exception.class)

你可以给@Transactional 指定名字,当你当系统存在多个 Transactional 配置时,否则回报错

@Transactional(value="transactionManagerName")
默认 Propagation.REQUIRED

默认 propagation=Propagation.REQUIRED

解决问题

从新看报错信息,由于自定义配置导致无法获取 transaction 实例,因为 isolation (隔离)级别的配置如下时会被忽略:PROPAGATION_SUPPORTS,ISOLATION_READ_UNCOMMITTED,readOnly; ‘’
即事务扩散级别为 SUPPORTS(有就用,没有事务就不用事务),隔离级别为 READ_UNCOMMITTED(读未提交),readOnly 为 默认 false

org.springframework.jdbc.datasource.DataSourceTransactionManager getTransaction
警告: Custom isolation level specified but no actual transaction initiated; isolation level will effectively be ignored: PROPAGATION_SUPPORTS,ISOLATION_READ_UNCOMMITTED,readOnly; ''

所以问题的最终解决方案就是 不要在类上使用下面的配置PROPAGATION_SUPPORTS,ISOLATION_READ_UNCOMMITTED,readOnly
当然,最好就简化为 ,然后具体配置在方法上再加,这样方法上肯定会覆盖类上的配置,并且不会报错了。

@Transactional(readOnly=false)
附件 传播机制

REQUIRED(默认):支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 required
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。 supports
MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。 mandatory
REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。 requires_new
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 not_supported
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。 never
NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务(嵌套事物互不影响),如果当前没有事务,就新建一个事务,此时等同于 REQUIRED。 例如:方法A(事务1)中调用了方法B(事务2),B(事务2)中try catch手动回滚,A(事务1)不会回滚。

附件 隔离级别

有一篇文章可以参考下 innodb 的表锁和行锁

DEFAULT
READ_UNCOMMITTED
READ_COMMITTED行锁(MVCC,如果没有命中索引,会锁全表)+间隙锁,当行锁和间隙锁都存在时,此时被称为 next-key
REPEATABLE_READ行锁(MVCC,如果没有命中索引,会锁全表)+间隙锁,当行锁和间隙锁都存在时,此时被称为 next-key
SERIALIZABLE表锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值