> 文章列表 > Spring:五、编程式事务

Spring:五、编程式事务

Spring:五、编程式事务

Spring:五、编程式事务

1 前言

spring支持声明式和编程式事务,因spring事务基于AOP,使用cglib作为代理,为父子类继承的代理模式,故而声明式事务@Transactional中,常见事务失效的场景,如方法内自调用(this.xxx的this不是代理对象)、方法修饰private(代理子类无法调用父类的private方法)、方法修饰final(因final修饰的方法,子类可以继承和重载,但无法重写)、类没有被spring管理等等,避免此类易被忽略而导致事务失效的问题,更推荐使用编程式事务

spring官方文档:

https://docs.spring.io/spring-framework/docs/5.3.25/reference/html/data-access.html#transaction-programmatic

2 使用

依赖:

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.4</version>
</parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${mapstruct.version}</version></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${mapstruct.version}</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><!--     spring连接驱动时,如com.mysql.cj.jdbc.Driver使用   --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-text</artifactId><version>1.9</version></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.1.1-jre</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.15</version></dependency></dependencies>

启动类:

@MapperScan(basePackages = "com.xiaoxu.boot.mapper")
@SpringBootApplication(scanBasePackages = "com.xiaoxu")
@ImportResource(locations = {"classpath*:pool/*.xml"})
public class MainApplication {public static void main(String[] args) {SpringApplication.run(MainApplication.class,args);}
}

PeopleMapper:

package com.xiaoxu.boot.mapper;import com.xiaoxu.boot.dto.PeopleDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;import java.util.List;//@Mapper
public interface PeopleMapper {List<PeopleDTO> queryPeopleByAge(int age);@Select("select * from my_people")List<PeopleDTO> queryAllPeople();int updatePeopleById(@Param("id") long id, @Param("myAge") String my_age);
}

PeopleDaoMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxu.boot.mapper.PeopleMapper"><select id="queryPeopleByAge" resultType="com.xiaoxu.boot.dto.PeopleDTO">select * from my_people where my_age = #{age}</select><update id="updatePeopleById">update my_people<set><if test="myAge != null">my_age = #{myAge}</if></set><where>id = #{id}</where></update></mapper>

PeopleService:

@Service
public class PeopleService {@AutowiredPeopleMapper peopleMapper;public List<PeopleDTO> getPeoples(int Age){return peopleMapper.queryPeopleByAge(Age);}public List<PeopleDTO> getAllPeople(){return peopleMapper.queryAllPeople();}public long updatePeopleAgeById(String age, int id){return peopleMapper.updatePeopleById(id, age);}}

druid.properties:

druid.driverClassName = com.mysql.cj.jdbc.Driver
druid.url = jdbc:mysql://localhost:3306/xiaoxu?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
druid.userName = root
druid.password = ******

DataSource.xml(配置TransactionTemplate bean,用于编程式事务):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context = "http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder location="classpath*:druid.properties"/><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"><property name="driverClassName" value="${druid.driverClassName}"/><property name="url" value="${druid.url}"/><property name="username" value="${druid.userName}"/><property name="password" value="${druid.password}"/></bean><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"><property name="transactionManager"><ref bean="transactionManager"/></property></bean></beans>

AbstractTest:

package mybatis;import com.xiaoxu.boot.MainApplication;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;/*** @author xiaoxu* @date 2023-02-03* spring_boot:mybatis.AbstractTest*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public abstract class AbstractTest {
}

单测类:

public class TestUserQuery extends AbstractTest{@AutowiredPeopleService peopleService;@AutowiredTransactionTemplate template;@Testpublic void test_01(){template.execute(new TransactionCallback<Object>() {@Overridepublic Object doInTransaction(TransactionStatus transactionStatus) {List<PeopleDTO> allPeople = peopleService.getAllPeople();allPeople.forEach(System.out::println);System.out.println("first over");List<PeopleDTO> allPeoples = peopleService.getAllPeople();allPeoples.forEach(System.out::println);System.out.println("second over");long l = peopleService.updatePeopleAgeById("16", 1);System.out.println("更新结果:" + l);List<PeopleDTO> allPeople1 = peopleService.getAllPeople();allPeople1.forEach(System.out::println);System.out.println("third over");String a = "1";if(a.equals("1")){
//                    throw new RuntimeException("1212");transactionStatus.setRollbackOnly();}return 0;}});}}

执行前于application.yml增加sql日志打印:

#mybatis的相关配置
mybatis:#mapper配置文件mapper-locations: classpath:mapper/*.xml
#  #mybatis配置文件
#  config-location: classpath:mybatis-config.xml
#  config-location和configuration不能同时存在#开启驼峰命名configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImpl

执行结果如下:

Spring:五、编程式事务
Spring:五、编程式事务

同一个事务中,mybatis的sqlSession是同一个(mybatis底层使用JDK动态代理,执行比如selectList方法时,如果上下文中开启了事务,那么sqlSession是同一个对象。而mybatis的1级缓存,在BaseExecutor中的PerpetualCache localCache中,是sqlSession维度的,即同一个sqlSession同享1级缓存),故而一个事务中多次查询,因使用的是同一个sqlSession,又因为mybatis的一级缓存是同一个sqlSession共用,故而连续两次查询一定得到的是同样的结果。如果第一次查询后有更新、插入、删除操作,那么mybatis一级缓存将会刷新。

故而上述第二次查询时,没有打印sql日志,取的数据为1级缓存中的数据。而后执行更新(或插入、删除)后,再次查询,此时打印查询sql日志。

另在TransactionTemplate事务执行中,由execute方法源码可知,默认捕获RuntimeException、Error后,执行事务回滚,当然,亦可同上述操作,在事务处理的代码逻辑捕获异常后,手动执行transactionStatus.setRollbackOnly(),亦可使事务回滚。

3 事务执行拓展

事务执行中,常见问题是,一般不能在事务执行中,执行非事务型操作,如非事务型的消息、rpc调用等等。因为若数据库事务回滚,但是消息已经发送(或rpc调用已造成影响),会造成数据不一致问题。若希望事务执行完成后,再执行部分操作如消息发送等,可以尝试如下拓展。

class DoTrans implements TransactionSynchronization{private final Runnable runnable;public DoTrans(Runnable runnable) {this.runnable = runnable;}@Overridepublic void afterCompletion(int status) {if(status == STATUS_COMMITTED){/* 0 */System.out.println("事务已提交");this.runnable.run();}else if(status == STATUS_ROLLED_BACK){/* 1 */System.out.println("事务已回滚, 不做处理.");}else if(status == STATUS_UNKNOWN){/* 2 */System.out.println("未知状态, 不做处理.");}}
}

单测方法:

@Test
public void test_02(){Runnable runnable = () -> {System.out.println("事务已执行, 发送消息.");};template.execute(new TransactionCallback<Object>() {@Overridepublic Object doInTransaction(TransactionStatus transactionStatus) {if(TransactionSynchronizationManager.isActualTransactionActive()){System.out.println("事务已开启.");TransactionSynchronizationManager.registerSynchronization(new DoTrans(runnable));}List<PeopleDTO> allPeople = peopleService.getAllPeople();allPeople.forEach(System.out::println);System.out.println("first over");List<PeopleDTO> allPeoples = peopleService.getAllPeople();allPeoples.forEach(System.out::println);System.out.println("second over");//                String a = "1";
//                if(a.equals("1")){
//                    transactionStatus.setRollbackOnly();
//                }return 0;}});}

执行结果如下:

Spring:五、编程式事务

TransactionSynchronization 源码:

public interface TransactionSynchronization extends Ordered, Flushable {int STATUS_COMMITTED = 0;int STATUS_ROLLED_BACK = 1;int STATUS_UNKNOWN = 2;default int getOrder() {return 2147483647;}default void suspend() {}default void resume() {}default void flush() {}default void beforeCommit(boolean readOnly) {}default void beforeCompletion() {}default void afterCommit() {}default void afterCompletion(int status) {}
}

源码中可见,afterCompletion会在事务执行后,执行对应的trigger方法进行调用。另可注册多个TransactionSynchronization对象,因实现了Ordered接口,亦可指定其执行的顺序。