当前位置: 首页 > article >正文

Spring声明式事务

1. 前言

        在上一篇博客中从一个案例 静态代理 -> 动态代理 -> AOP-CSDN博客 介绍了静态代理 -> 动态代理 -> SpringAOP相关内容。在Spring中声明式事务的底层就是通过AOP来实现的。趁热打铁,本篇博客介绍Spring的事务相关内容。

        在此之前,我们先来说明下什么是声明式编程。和声明式编程对应的是命令式编程。凡是非命令式编程都可归结为声明式编程。编程范式可分为两大类:

  • 命令式编程(Imperative Programming)
  • 声明式编程(Declarative Programming)
    • 函数式编程(Functional Programming,简称FP)
    • 逻辑式编程(Logic Programming,简称LP)
    • 属性式编程

其中命令式、函数式和逻辑式是最核心的三范式。这里引入网上看的一个图片,具体出处忘了是哪里。

在我们实际开发过程中,命令式编程和声明式编程的优缺点简单总结如下:

命令式编程

  • 优点:代码调试(Debug)容易
  • 缺点:代码量大,需要自己手动编写大量代码

声明式编程

  • 优点:代码简洁,使用方便
  • 缺点:封装太深,不易调试(Debug);  

2. 项目需求概述

        通过一个账户表和一个图书表来演示事务的作用。

2.1 SQL脚本

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
  `age` int(0) NULL DEFAULT NULL COMMENT '年龄',
  `balance` decimal(10, 2)  NULL DEFAULT NULL COMMENT '余额',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES (1, 'zhangsan', 18, 10000.00);
INSERT INTO `account` VALUES (2, 'lisi', 20, 10000.00);
INSERT INTO `account` VALUES (3, 'wangwu', 16, 10000.00);

-- ----------------------------
-- Table structure for book
-- ----------------------------
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '图书id',
  `bookName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图书名',
  `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '单价',
  `stock` int(0) NULL DEFAULT NULL COMMENT '库存量',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of book
-- ----------------------------
INSERT INTO `book` VALUES (1, '剑指Java', 100.00, 100);
INSERT INTO `book` VALUES (2, '剑指大数据', 100.00, 100);
INSERT INTO `book` VALUES (3, '剑指Offer', 100.00, 100);

SET FOREIGN_KEY_CHECKS = 1;

2.2. 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

2.3. 项目目录结构及代码

Account

import lombok.Data;

import java.math.BigDecimal;

@Data
public class Account {
    private Integer id;
    private String userName;
    private Integer age;
    private BigDecimal balance;
}

Book

import lombok.Data;

import java.math.BigDecimal;

@Data
public class Book {
    private Integer id;
    private String bookName;
    private BigDecimal price;
    private Integer stock;
}

AccountDao

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class AccountDao {
    private final JdbcTemplate jdbcTemplate;

    public AccountDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 按照userName扣减账户余额
     *
     * @param userName 用户名
     * @param money    要扣减的金额
     */
    public void updateBalanceByUserName(String userName, BigDecimal money) {
        String sql = "update account set balance = account.balance - ? where username = ?";
        jdbcTemplate.update(sql, money, userName);
    }
}

BookDao

import com.shg.spring.tx.bean.Book;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Component
public class BookDao {

    private final JdbcTemplate jdbcTemplate;

    public BookDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 按照图书id删除图书
     *
     * @param bookId 图书id
     */
    public void deleteBookById(int bookId) {
        String sql = "delete from book where id=?";
        jdbcTemplate.update(sql, bookId);
    }

    /**
     * 更新图书库存
     *
     * @param bookId 图书id
     * @param num    需要减去的库存数量
     */
    public void updateBookStockById(int bookId, Integer num) {
        String sql = "update book set stock=stock - ? where id=?";
        jdbcTemplate.update(sql, num, bookId);
    }

    /**
     * 新增一个图书
     *
     * @param book
     */
    public void addBook(Book book) {
        String sql = "insert into book (bookName,price,stock) values (?, ?, ?)";
        jdbcTemplate.update(sql, book.getBookName(), book.getPrice(), book.getStock());
    }

    /**
     * 根据id查询书籍信息
     *
     * @param id 图书id
     * @return Book
     */
    @Transactional
    public Book getBookById(Integer id) {
        String sql = "select * from book where id = ?";
        return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
    }

}

UserService

import java.io.IOException;

public interface UserService {
    void checkout(String userName, Integer bookId, int buyNum);
}

UserServiceImpl

import com.shg.spring.tx.bean.Book;
import com.shg.spring.tx.dao.AccountDao;
import com.shg.spring.tx.dao.BookDao;
import com.shg.spring.tx.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.math.BigDecimal;

@Service
public class UserServiceImpl implements UserService {

    private final BookDao bookDao;
    private final AccountDao accountDao;

    public UserServiceImpl(BookDao bookDao, AccountDao accountDao) {
        this.bookDao = bookDao;
        this.accountDao = accountDao;
    }

    @Override
    public void checkout(String userName, Integer bookId, int buyNum) {
        
        // 1. 查询图书信息
        Book bookById = bookDao.getBookById(bookId);
        
        // 2. 计算总价
        BigDecimal totalPrice = bookById.getPrice().multiply(new BigDecimal(buyNum));
        
        // 3. 扣减库存
        bookDao.updateBookStockById(bookId, buyNum);
        
        // 4. 扣减余额
        accountDao.updateBalanceByUserName(userName, totalPrice);
    }
}

3. Spring事务案例

3.1. 没有事务的情况

        在上述的UserServiceImp代码中,理想情况下代码执行后,图书book表 和 账户account表都表现正常。即图书库存减少,账户余额扣减成功。但是如果在执行结账(checkout)方法时,抛出了异常,理论上对数据库做的操作都应该回滚。但实际上数据库并没有回滚。所以默认情况下的业务逻辑是没有事务控制的。

3.2. 使用Spring声明式事务

(1)在启动类上标注 @EnableTransactionManagement注解;

(2)在需要事务的方法上标注 @Transactional

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement
@SpringBootApplication
public class Spring03TxApplication {

    public static void main(String[] args) {
        SpringApplication.run(Spring03TxApplication.class, args);
    }

}

事务方法

    @Transactional
    @Override
    public void checkout(String userName, Integer bookId, int buyNum) throws InterruptedException, IOException {
        
        // 1. 查询图书信息
        Book bookById = bookDao.getBookById(bookId);
        
        // 2. 计算总价
        BigDecimal totalPrice = bookById.getPrice().multiply(new BigDecimal(buyNum));
        
        // 3. 扣减库存
        bookDao.updateBookStockById(bookId, buyNum);
        
        // 4. 扣减余额
        accountDao.updateBalanceByUserName(userName, totalPrice);
        
        // 5. 模拟异常回滚
         int i = 1 / 0;
    }

3.3. Spring声明式事务的底层原理

(1)事务管理器 TransactionManager:控制事务的提交和回滚

(2)事务拦截器 TransactionInterceptor:控制事务何时提交和回滚

在TransactionInterceptor中有一个调用目标方法的逻辑:

进入 invokeWithTransaction方法,进入到TransactionAspectSupport类中,源码核心方法如下:

	/**
	 * General delegate for around-advice-based subclasses, delegating to several other template
	 * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
	 * as well as regular {@link PlatformTransactionManager} implementations and
	 * {@link ReactiveTransactionManager} implementations for reactive return types.
	 * @param method the Method being invoked
	 * @param targetClass the target class that we're invoking the method on
	 * @param invocation the callback to use for proceeding with the target invocation
	 * @return the return value of the method, if any
	 * @throws Throwable propagated from the target invocation
	 */
	@Nullable
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		TransactionAttributeSource tas = getTransactionAttributeSource();
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
		final TransactionManager tm = determineTransactionManager(txAttr);

		if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
			boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
			boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
					COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());

			ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
				Class<?> reactiveType =
						(isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());
				ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
				if (adapter == null) {
					throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" +
							method.getReturnType() + "] with specified transaction manager: " + tm);
				}
				return new ReactiveTransactionSupport(adapter);
			});

			return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
		}

		PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

			Object retVal;
			try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}

			if (retVal != null && txAttr != null) {
				TransactionStatus status = txInfo.getTransactionStatus();
				if (status != null) {
					if (retVal instanceof Future<?> future && future.isDone()) {
						try {
							future.get();
						}
						catch (ExecutionException ex) {
							if (txAttr.rollbackOn(ex.getCause())) {
								status.setRollbackOnly();
							}
						}
						catch (InterruptedException ex) {
							Thread.currentThread().interrupt();
						}
					}
					else if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
						// Set rollback-only in case of Vavr failure matching our rollback rules...
						retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
					}
				}
			}

			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

		else {
			Object result;
			final ThrowableHolder throwableHolder = new ThrowableHolder();

			// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
			try {
				result = cpptm.execute(txAttr, status -> {
					TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
					try {
						Object retVal = invocation.proceedWithInvocation();
						if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
							// Set rollback-only in case of Vavr failure matching our rollback rules...
							retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
						}
						return retVal;
					}
					catch (Throwable ex) {
						if (txAttr.rollbackOn(ex)) {
							// A RuntimeException: will lead to a rollback.
							if (ex instanceof RuntimeException runtimeException) {
								throw runtimeException;
							}
							else {
								throw new ThrowableHolderException(ex);
							}
						}
						else {
							// A normal return value: will lead to a commit.
							throwableHolder.throwable = ex;
							return null;
						}
					}
					finally {
						cleanupTransactionInfo(txInfo);
					}
				});
			}
			catch (ThrowableHolderException ex) {
				throw ex.getCause();
			}
			catch (TransactionSystemException ex2) {
				if (throwableHolder.throwable != null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
					ex2.initApplicationException(throwableHolder.throwable);
				}
				throw ex2;
			}
			catch (Throwable ex2) {
				if (throwableHolder.throwable != null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
				}
				throw ex2;
			}

			// Check result state: It might indicate a Throwable to rethrow.
			if (throwableHolder.throwable != null) {
				throw throwableHolder.throwable;
			}
			return result;
		}
	}

上面这段源码的重点:

completeTransactionAfterThrowing(txInfo, ex); 【异常通知】出现异常时回滚
commitTransactionAfterReturning(txInfo); 【返回通知】方法正常执行后,提交事务

通常我们不会自己去实现事务管理器的接口,而是使用Spring给我们提供的事务管理器。Spring声明式事务的底层原理就是:

Spring底层通过 事务管理器 和 事务拦截器 实现Spring的声明式事务。具体来说就是:

(1)事务管理器的顶层接口是 TransactionManage,其子接口中定义了获取事务getTransaction方法、提交事务方法和回滚事务方法。控制事务的提交和回滚。我们默认是使用JdbcTransactionManage这个实现类。


(2)事务拦截器:是一个切面,如果目标方法正常执行,就会调用事务管理器的提交事务方法;如果目标方法执行出现异常,就会调用事务管理器的回滚事务方法。

3.4. 事务注解@Transaction的属性介绍

在@Transactional注解中有许多可以设置的属性,如下图:

下面针对这些属性进行介绍。 

3.4.1. value 或 transactionManager属性

用来设置要使用的事务管理器,通常我们不指定,而是使用Spring提供的默认的事务管理器。如下图

3.4.2. label属性 

通常不使用,不介绍  

 3.4.3. propagation属性 

先理解什么是事务的传播行为:当一个大的事务方法里面嵌套小的事务方法时,该如何控制大事物和小事物的关系。比如一个结账方法是一个大事物,此方法内部调用了两个事务方法:扣减库存方法 和 扣减余额方法。伪代码如下:

大事物:结账方法
@Transaction

public void checkout(String userName, Integer bookId, int buyNum) {

        // 调用扣减库存方法

        bookDao.updateBookStockById(bookId, buyNum);

        

        // 调用扣减余额方法

        accountDao.updateBalanceByUserName(userName, totalPrice);

}

小事物:扣减库存方法

@Transactional
public void updateBookStockById(int bookId, Integer num) {
    String sql = "update book set stock=stock - ? where id=?";
    jdbcTemplate.update(sql, num, bookId);
}

小事物:扣减余额方法

@Transactional
public void updateBalanceByUserName(String userName, BigDecimal money) {
    String sql = "update account set balance = account.balance - ? where username = ?";
    jdbcTemplate.update(sql, money, userName);
}

有了上面的调用逻辑,当执行结账方法时,已经开启了一个事务;那么结账方法调用扣减库存或扣减余额方法时,扣减库存(后面直接以扣减库存为例)方法该如何 “使用事务”呢?


此时就需要传播行为这个属性进行控制了。
 

控制事务的传播行为有如下七种:

(1)propagation = Propagation.REQUIRED:支持当前事务,如果不存在,则创建一个新事务。
【解释】指的是内层的方法(updateBookStockById方法)支持当前外面这个方法(checkout方法)的事务。如果当前外面这个方法有事务,我就用你的事务;如果你没有事务我就自己新创建一个事务。

(2)propagation = Propagation.SUPPORTS:支持当前事务,如果不存在,则执行非事务。

【解释】指的是内层的方法(updateBookStockById方法)支持当前外面这个方法(checkout方法)的事务。如果当前外面这个方法有事务,我就用你的事务;如果你没有事务我就以非事务方式运行。


(3)propagation = Propagation.MANDATORY:支持当前事务,如果不存在则抛出异常。
【解释】指的是内层的方法(updateBookStockById方法)支持当前外面这个方法(checkout方法)的事务。如果当前外面这个方法没有事务,则抛出异常。

(4)propagation = Propagation.REQUIRES_NEW:创建一个新事务,并挂起当前事务(如果存在)。
【解释】指的是内层的方法(updateBookStockById方法)会创建一个新事物,并在新事务里面执行。会挂起外面这个方法(checkout方法)的事务(如果外面方法存在事务)。

(5)propagation = Propagation.NOT_SUPPORTED:非事务性地执行,挂起当前事务(如果存在)

【解释】指的是内层的方法(updateBookStockById方法)以非事务的方式执行,如果当前外面这个方法(checkout方法)有事务,则挂起当前事务。

(6)propagation = Propagation.NEVER:非事务执行,如果存在事务则抛出异常。

【解释】指的是内层的方法(updateBookStockById方法)必须以非事务的方式执行,如果当前外面这个方法有事务,则抛出异常。

(7)paopagation = Propagation.NESTED:如果当前事务存在,则在嵌套事务中执行,否则表现为REQUIRED。

【解释】指的是内层的事务方法(updateBookStockById方法)在执行时,判断当前外面的方法(checkout方法)是否有事务,如果有,就在外面这个方法的事务中再开启一个事务进行执行。如果外面的方法没有事务,则表现和 propagation = Propagation.REQUIRED 一样。

一张图总结:



总结来说:就是当大事物存在时,里面的小事物要不要和大事物进行绑定(和大事物的共生关系)
 

【属性传播】
注意:
当内层的小事物和外层的大事物共用一个事务,内层小事物的其他一些属性就都失效了(使用外层大事物属性)。比如timeout属性,readOnly属性,isolation属性等。

3.4.4. isolation属性 

isolation可以设置如下四种属性:

(1)@Transactional(isolation = Isolation.READ_UNCOMMITTED)

(2)@Transactional(isolation = Isolation.READ_COMMITTED)

(3)@Transactional(isolation = Isolation.REPEATABLE_READ)
(4)@Transactional(isolation = Isolation.SERIALIZABLE)

MySQL的默认隔离级别是可重复读,Oracle的默认隔离级别是读已提交。在实际开发中,隔离级别通常从这两个中间选一个。一般使用默认的。


设置事务的隔离级别,针对关系型数据库的事务特性和隔离级别,可以参考我之前写的一篇博文:事务的特性和隔离级别-CSDN博客

(1)隔离级别的目的是,当多个读写事务并发执行的时候,防止出现的脏读、不可重复读和幻读等情况的发生。

(2)不同的隔离级别,其可以解决的问题如下表所示:

隔离级别
级别/问题脏读不可重复读幻读
读未提交
读已提交×
可重复读(快照读)××
串行化×××

(3)思考:为什么隔离级别叫 未提交、已提交、可重复读。而不是未提交、已提交和可重复呢?这是因为隔离级别是控制 的。为啥控制读呢?

       
这是因为数据库底层在针对写(更新操作)时,会对其进行加锁,即使有并发写(并发修改)操作,也是要一个一个排队去执行更新操作。所以不会有并发问题。所以数据库的写操作会比较慢。

如果只是并发读,也不会出现并发问题,因为数据没有改变,读多少次数据都是一样的。

所以并发问题会出现在同时存在读写并发的场景。所以说读写一旦并发的时候,就需要有一种机制来控制有一个人在写时,控制这个读的人,应该何时可以读(而不用控制其他写的人,因为写操作,数据库本身就会加锁)。

(a)如果写的人,写了一半就可以让另一个人去读到还未写完的数据。这就是读未提交

(b)如果写的人,只有写完了,才可以让另一个人去读到刚才写入的数据。这就是读已提交

(c)如果写的人,写完了,事务也提交了,但是另一个读的人,再次读取还是和自己之前读取的数据一样,即:没有读到刚才那个人已经写入的数据。这就是可重复读。

3.4.5. timeout 或 timeoutString属性 

控制事务的超时时间(以秒为单位),一旦超过约定时间,事务就会回滚。

注意:事务的超时时间是指从方法开始,到最后一次数据库操作结束经过的时间。代码示例如下:

    @Transactional(timeout = 3)
    @Override
    public void checkout(String userName, Integer bookId, int buyNum) throws InterruptedException, IOException {
        
        // 1. 查询图书信息
        Book bookById = bookDao.getBookById(bookId);
        
        // 2. 计算总价
        BigDecimal totalPrice = bookById.getPrice().multiply(new BigDecimal(buyNum));
        
        // 3. 扣减库存
        bookDao.updateBookStockById(bookId, buyNum);
        
        // 模拟事务超时
        //Thread.sleep(3000);
        
        // 4. 扣减余额(这是事务方法最后一次执行数据库操作,事务执行时间是以这次操作执行完成进行耗时统计的)
        accountDao.updateBalanceByUserName(userName, totalPrice);
        
        // 如果在这里进行睡眠(模拟业务耗时操作),则Spring事务不会统计超时。
        Thread.sleep(3000);
    }

3.4.6. readOnly属性 

如果整个事务都只是一个读操作,则可以把readOnly设置成true,可以实现底层运行时优化。 总结就是:readOnly可以做到只读优化

3.4.7. rollbackFor 或 rollbackForClassName属性 

(1)指明哪些异常出现时,事务进行回滚。默认并不是所有异常都一定进行回滚。

(2)默认运行时异常是可以进行回滚的,即Error和RuntimeException及其子类异常可以进行回滚。而编译时异常(也叫已检查异常)(除了运行时异常,其他都是编译时异常)默认是不回滚的。

(3)如果我们设置了rollbackFor属性,那么可以回滚的异常就是 运行时异常 + rollbackFor指定的异常。

(4)通常在实际业务中,我们都设置rollbackFor={Exception.class}

 

3.4.8. noRollbackFor 或 noRollbackForClassName 属性 

指明哪些异常不需要回滚,默认不回滚的异常是:编译时异常+noRollbackFor指定的异常

4. 如果此篇文章对你有帮助,感谢关注并点个赞~ 后续持续输出高质量的原创技术文章~


http://www.kler.cn/a/508300.html

相关文章:

  • Python爬虫学习前传 —— Python从安装到学会一站式服务
  • 【时时三省】(C语言基础)柔性数组的使用
  • 如何使用Ultralytics训练自己的yolo5 yolo8 yolo10 yolo11等目标检测模型
  • 掌握C语言内存布局:数据存储的智慧之旅
  • idea上git log面板的使用
  • MDX语言的数据库交互
  • 第22篇 基于ARM A9处理器用汇编语言实现中断<四>
  • “AI智能防控识别系统:守护安全的“智慧卫士”
  • 【进程与线程】进程的基础
  • 深度学习-88-大语言模型LLM之基于langchain的检索链
  • 【网络协议】【http】【https】AES-TLS1.2
  • 软考信安24~工控安全需求分析与安全保护工程
  • AXIOS的引入和封装
  • 对MySQL滴MVCC理解(超详细)
  • 【蓝桥杯选拔赛真题62】C++求和 第十四届蓝桥杯青少年创意编程大赛 算法思维 C++编程选拔赛真题解
  • “AI开放式目标检测系统:开启智能识别新时代
  • Linux《Linux简介与环境的搭建》
  • React 表单处理与网络请求封装详解[特殊字符][特殊字符]
  • java请编写程序,分别定义8种基本数据类型的变量,并打印变量的值。
  • 左神算法基础提升--2
  • MySQL(高级特性篇) 06 章——索引的数据结构
  • 深入浅出:Go语言中的Unicode与字符编码详解
  • C++ K2 (4)
  • 【专题一 递归】面试题 08.06. 汉诺塔问题
  • 20250117在Ubuntu20.04.6下使用灵思FPGA的刷机工具efinity刷机
  • STM32入门教程-示例程序(按键控制LED光敏传感器控制蜂鸣器)