MySQL与Spring事务管理

时间:2022-09-21 20:08:21

数据库事务是保证在并发情况下能够正确执行的重要支撑,MySQL常见的数据库引擎中支持事务的是InnoDB,事务就是一系列操作,正确执行并提交,如果中途出现错误就回滚。事务要保证能够正常的执行,就必须要保持ACID特性,这在前面的文章当中有提到,本文也偏重spring的事务管理配置demo因此不做过多的概念介绍,网上的资料已经比较丰富。

这是HeidiSQl截图查看不同引擎的特性:
MySQL与Spring事务管理

数据库的事务的隔离性是有级别的,不同的级别具有不同的特性,应该在合适的条件下选择合适的隔离级别,不同的数据库产品支持的隔离级别可能不同,甲骨文支持三种,MySQL数据库支持四种隔离级别,分别是:读未提交,读已提交,可重复读,可串行。不同的隔离级别分别可以避免脏读,不可重复读,幻读的情况。值得一提的是,避免不可重复读和幻读都是进行加锁,不同的是一个是对行进行加锁,避免幻读是对表进行加锁。还有就是锁可以分为共享锁和独占锁,一般来说为了避免事务的更新丢失,读写之间会进行加锁,分为悲观锁与乐观锁,悲观锁就是普通的锁,乐观锁就是不加锁,在更新的时候再去验证是否有被修改,如果被修改则读取的数据为脏数据不能修改。这样在修改比较多的情况下比较适合悲观锁,在读取比较多的情况下就比较适合乐观锁。

spring作为开发平台,对数据库的访问支持的很完善,对事务的支持也比较完善,主要分为两种编程式事务与声明式事务,编程式事务能够更细的控制事务回滚与提交的粒度,但是需要在代码中编写,耦合性更高,并且与业务代码混合在一起,这与spring的无入侵的特性相违背,因此一般会采用声明式事务的方式进行事务管理,因为在很多需要操作数据的逻辑都需要进行事务管理,抛开不同开发者写的代码差异性不谈,单从重复性的代码导致系统难以维护,内聚性很差这点就足以证明这种方式不可取,但是这不正是AOP的使用场景吗,因此使用切面编程技术生成动态代理完成事务管理,业务代码更加的单纯,。主要分为三大部分,数据源,事务管理器,代理机制。其中处于核心地位的接口为PlatformTransactionManager,它的定义比较简单:
// 获取spring配置的事务传播机制
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
void commit(TransactionStatus status) throws TransactionException;
// 或则回滚
void rollback(TransactionStatus status) throws TransactionException;

getTransaction定义了事务的开启范围,这个可以在网上找到更详细的讲解,这里不累述。commit与rollback就是基本的事务控制机制。
AbstractPlatformTransactionManager实现了该接口,这个抽象类定义一些基本的对事务支持的操作,但是抽象了不同的orm框架对事务管理的细节。jdbc是通过dataSource Connection来进行事务管理,而hibernate是通过SqlSession进行事务管理。使用jdbc进行事务管理,就注入DataSourceTransactionManager即可。并制定对应的数据源,然后只需要制定需要代理的类即可。
数据源的配置:

<bean id="driver" class="com.mysql.jdbc.Driver"></bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<property name="url" value="${url}" />
<property name="driver" ref="driver" />
</bean>

首先我们制定事务管理器:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

然后我们定义需要事务传播机制,我们来创建一个常见,也可以采用其他的方式创建advice,这里的advice通知就是定义了需要做什么,这里就是 如果有事务加入事务,没有的话创建新事务:

<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 如果有事务加入事务,没有的话创建新事务 -->
<tx:method name="*" propagation="REQUIRED" timeout="3"/>
</tx:attributes>
</tx:advice>

做什么定义完了,就需要指定在哪里做(pointcut)即切点,代理的方式有很多种,可以使用默认的代理DefaultAdvisorAutoProxyCreator。也可以使用注解的方式,很灵活,主要就是明白定义好切点(一般spring支持较好的是方法级别的拦截)即可。这里我们使用比较方便的schema标签支持aop代理:

<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* micro.test.spring.service..*Service*.*(..))" />
<aop:advisor advice-ref="advice" pointcut-ref="txPointcut"/>
</aop:config>

很多时候我们引入新的xml标签会报错,这个时候检查xml文件头是否引入了相关的schema即可,这点是初学者很容易犯错的地方。

<!-- 扫描到advice -->
<context:component-scan base-package="micro.test.spring" />
<!-- mybatis中sqlSession由这个类来完成 -->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations">
<list>
<value>classpath:/sqlmap/ComputerMapper.xml</value>
</list>
</property>
</bean>

其他必要的配置跟本文不太相关就不贴了。
前面execution(* micro.test.spring.service..Service.*(..))就表示在这个包下面的所有方法都是切点,会被代理。这是service的一个方法,执行它:

        public void testTransaction2() {
Computer computer = new Computer();
computer.setBrand("test");
computerMapper.insert(computer);//事务会回滚

ComputerExample computerExample = new ComputerExample();
computerExample.or().andBrandEqualTo(computer.getBrand());

int a = 1;
a /= 0; // 制造异常

computerMapper.insert(computer);
}

如果没事务管理机制,会插入一条记录。引入了事务管理机制,抛出异常(spring要求运行时异常)就不会插入记录,因为遇到了异常,当然如果去掉异常,两条都会插入进去,值得一提的是我数据库设置的默认应该不是读未提交,因为在调试到还没抛异常的时候也不会看到一条记录,如果是读未提交应该是看到出现了一条记录然后又消失了(被回滚),测试了几次,要么有0条,要么有两条(正常执行)。没有造成脏读的情况至少应该是读已提交。查询了一下:
MySQL与Spring事务管理
果然可重复读,是能够不免脏读和不可重复度两个问题的。但是无法避免幻读。