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
java">import lombok.Data;import java.math.BigDecimal;@Data
public class Account {private Integer id;private String userName;private Integer age;private BigDecimal balance;
}
Book
java">import lombok.Data;import java.math.BigDecimal;@Data
public class Book {private Integer id;private String bookName;private BigDecimal price;private Integer stock;
}
AccountDao
java">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
java">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*/@Transactionalpublic Book getBookById(Integer id) {String sql = "select * from book where id = ?";return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);}}
UserService
java">import java.io.IOException;public interface UserService {void checkout(String userName, Integer bookId, int buyNum);
}
UserServiceImpl
java">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;}@Overridepublic 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
启动类
java">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);}}
事务方法
java"> @Transactional@Overridepublic 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类中,源码核心方法如下:
java"> /*** 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*/@Nullableprotected 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 exceptioncompleteTransactionAfterThrowing(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属性
先理解什么是事务的传播行为:当一个大的事务方法里面嵌套小的事务方法时,该如何控制大事物和小事物的关系。比如一个结账方法是一个大事物,此方法内部调用了两个事务方法:扣减库存方法 和 扣减余额方法。伪代码如下:
大事物:结账方法
@Transactionpublic 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属性
控制事务的超时时间(以秒为单位),一旦超过约定时间,事务就会回滚。
注意:事务的超时时间是指从方法开始,到最后一次数据库操作结束经过的时间。代码示例如下:
java"> @Transactional(timeout = 3)@Overridepublic 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指定的异常