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 mapper
        PUBLIC "-//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 {
    @Autowired
    PeopleMapper 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/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://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{
    @Autowired
    PeopleService peopleService;

    @Autowired
    TransactionTemplate template;

    @Test
    public void test_01(){

        template.execute(new TransactionCallback<Object>() {
            @Override
            public 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: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

执行结果如下:

在这里插入图片描述
在这里插入图片描述

同一个事务中,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;
    }

    @Override
    public 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>() {
        @Override
        public 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;
        }
    });

}

执行结果如下:

在这里插入图片描述

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接口,亦可指定其执行的顺序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值