
在 Java 企业级开发中,事务管理是保证数据一致性的核心机制。想象一下:用户支付订单后,扣减库存和创建订单记录必须同时成功或同时失败,否则就会出现超卖或财务对账不平的严重问题。Spring 框架通过声明式事务(@Transactional注解)将开发者从复杂的事务管理代码中解放出来,但这个简单注解背后却隐藏着精妙的设计与实现。
本文将带你揭开 Spring 事务的神秘面纱,从数据库事务原理讲起,深入 Spring 事务的实现机制,剖析传播行为、隔离级别等核心概念,并通过可运行的实战案例展示事务在实际开发中的应用与陷阱。无论你是想夯实基础的初级开发者,还是希望深入理解底层原理的资深工程师,都能从本文获得有价值的知识。
在探讨 Spring 事务之前,我们需要先理解数据库事务的本质。事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,它能够保证一组数据库操作要么全部成功,要么全部失败。
事务必须满足 ACID 特性,这是保证数据一致性的基础:

数据库通过以下机制实现事务的 ACID 特性:
Spring 并不直接管理事务,而是提供了事务管理的抽象层,通过封装不同的事务资源(如 JDBC、Hibernate、JPA 等),为开发者提供统一的事务操作接口。
Spring 事务的核心接口:

Spring 提供了两种事务管理方式:编程式事务和声明式事务。其中,声明式事务通过@Transactional注解实现,是实际开发中最常用的方式,它将事务管理与业务逻辑分离,提高了代码的可读性和可维护性。
@Transactional注解的实现基于 Spring 的 AOP(面向切面编程)机制,其核心原理是:
@Transactional注解的类或方法。
Spring 事务管理涉及多个核心组件,它们协同工作完成事务的创建、提交和回滚:

TransactionInterceptor 是事务管理的核心拦截器,其工作流程如下:

核心源码片段(简化版):
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取目标类和方法
Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
Method method = invocation.getMethod();
// 执行目标方法并进行事务管理
return invokeWithinTransaction(method, targetClass, invocation::proceed);
}
}
public abstract class TransactionAspectSupport {
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 获取事务属性
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = tas != null ? tas.getTransactionAttribute(method, targetClass) : null;
// 获取事务管理器
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// 创建事务信息
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 执行目标方法
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// 发生异常,回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
// 正常返回,提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
@Transactional注解提供了多个属性,用于配置事务的行为特征。理解这些属性是正确使用 Spring 事务的关键。
传播行为定义了当一个事务方法被另一个事务方法调用时,事务如何传播。Spring 定义了 7 种传播行为:
传播行为 | 说明 |
|---|---|
REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。 |
SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 |
MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 |
REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 |
NOT_SUPPORTED | 以非事务的方式运行,如果当前存在事务,则把当前事务挂起。 |
NEVER | 以非事务的方式运行,如果当前存在事务,则抛出异常。 |
NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。 |
最常用的传播行为是REQUIRED和REQUIRES_NEW,我们通过一个实例来理解它们的区别:
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private PaymentService paymentService;
/**
* 创建订单,使用默认传播行为REQUIRED
*/
@Transactional
public void createOrder(Order order) {
log.info("创建订单开始");
orderMapper.insert(order);
// 调用支付服务的支付方法
paymentService.processPayment(order.getId(), order.getAmount());
log.info("创建订单结束");
}
}
@Service
@Slf4j
public class PaymentService {
@Autowired
private PaymentMapper paymentMapper;
/**
* 处理支付,使用REQUIRES_NEW传播行为
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment(Long orderId, BigDecimal amount) {
log.info("处理支付开始");
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
paymentMapper.insert(payment);
// 模拟异常
// if (true) throw new RuntimeException("支付处理失败");
log.info("处理支付结束");
}
}
processPayment方法使用REQUIRED时,它会加入createOrder方法的事务中。如果processPayment抛出异常,整个事务会回滚,订单和支付记录都不会保存。processPayment方法使用REQUIRES_NEW时,它会创建一个新的事务。如果processPayment抛出异常,只会回滚自己的事务(支付记录不保存),而createOrder方法的事务不受影响(订单记录会保存)。隔离级别定义了多个并发事务之间的隔离程度,用于解决并发事务带来的问题(脏读、不可重复读、幻读)。
并发事务可能带来的问题:
Spring 支持的隔离级别:
隔离级别 | 说明 | 解决的问题 |
|---|---|---|
DEFAULT | 使用数据库默认的隔离级别。这是默认值。 | - |
READ_UNCOMMITTED | 允许读取未提交的数据。 | 无 |
READ_COMMITTED | 只能读取已提交的数据。 | 脏读 |
REPEATABLE_READ | 保证多次读取同一数据结果一致。 | 脏读、不可重复读 |
SERIALIZABLE | 完全串行化执行,最高隔离级别。 | 脏读、不可重复读、幻读 |
不同数据库的默认隔离级别:
隔离级别与并发性能成反比:隔离级别越高,并发性能越差。实际开发中需要根据业务需求平衡数据一致性和性能。
true时,数据库可以进行一些优化,提高查询性能。适用于只查询数据的方法。示例:
/**
* 查询订单列表,只读事务
*/
@Transactional(readOnly = true, timeout = 10)
public List<Order> queryOrders(Long userId) {
return orderMapper.selectByUserId(userId);
}
/**
* 更新订单状态,指定异常回滚
*/
@Transactional(rollbackFor = {BusinessException.class, SQLException.class})
public void updateOrderStatus(Long orderId, Integer status) {
Order order = new Order();
order.setId(orderId);
order.setStatus(status);
orderMapper.updateById(order);
}
/**
* 处理退款,指定异常不回滚
*/
@Transactional(noRollbackFor = InsufficientFundsException.class)
public void processRefund(Long orderId) {
// 处理退款逻辑
}
接下来,我们通过一个完整的实战案例,展示 Spring 事务在实际开发中的应用。案例实现一个简单的订单支付系统,包含创建订单、扣减库存、处理支付等功能,重点展示事务如何保证这些操作的一致性。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-transaction-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-transaction-demo</name>
<description>Demo project for Spring Transaction</description>
<properties>
<java.version>17</java.version>
<lombok.version>1.18.30</lombok.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mysql-connector.version>8.0.33</mysql-connector.version>
<springdoc.version>2.4.0</springdoc.version>
<fastjson2.version>2.0.47</fastjson2.version>
<guava.version>33.0.0-jre</guava.version>
</properties>
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
<scope>runtime</scope>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
spring:
application:
name: spring-transaction-demo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/transaction_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
# 自动提交从连接池返回的连接
auto-commit: false
# 连接池名称
pool-name: TransactionDemoPool
# 最大连接数
maximum-pool-size: 10
# 最小空闲连接数
minimum-idle: 5
# 连接超时时间(毫秒)
connection-timeout: 30000
# 连接最大存活时间(毫秒)
max-lifetime: 600000
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.springtransactiondemo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志配置
logging:
level:
com.example.springtransactiondemo: debug
# 服务器配置
server:
port: 8080
# SpringDoc配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
-- 创建数据库
CREATE DATABASE IF NOT EXISTS transaction_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE transaction_demo;
-- 商品表
CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`name` varchar(100) NOT NULL COMMENT '商品名称',
`stock` int NOT NULL COMMENT '库存数量',
`price` decimal(10,2) NOT NULL COMMENT '商品价格',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-- 订单表
CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`quantity` int NOT NULL COMMENT '购买数量',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`status` tinyint NOT NULL COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
-- 支付记录表
CREATE TABLE `payment` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '支付记录ID',
`order_id` bigint NOT NULL COMMENT '订单ID',
`amount` decimal(10,2) NOT NULL COMMENT '支付金额',
`payment_time` datetime NOT NULL COMMENT '支付时间',
`payment_method` tinyint NOT NULL COMMENT '支付方式:1-微信,2-支付宝',
`status` tinyint NOT NULL COMMENT '支付状态:0-处理中,1-成功,2-失败',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';
-- 初始化数据
INSERT INTO `product` (`name`, `stock`, `price`) VALUES ('测试商品', 100, 99.99);
Product.java:
package com.example.springtransactiondemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商品实体类
*
* @author ken
*/
@Data
@TableName("product")
public class Product {
/**
* 商品ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 商品名称
*/
private String name;
/**
* 库存数量
*/
private Integer stock;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 创建时间
*/
private LocalDateTime createdTime;
/**
* 更新时间
*/
private LocalDateTime updatedTime;
}
Order.java:
package com.example.springtransactiondemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
*
* @author ken
*/
@Data
@TableName("`order`")
public class Order {
/**
* 订单ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 商品ID
*/
private Long productId;
/**
* 购买数量
*/
private Integer quantity;
/**
* 订单总金额
*/
private BigDecimal totalAmount;
/**
* 订单状态:0-待支付,1-已支付,2-已取消
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createdTime;
/**
* 更新时间
*/
private LocalDateTime updatedTime;
}
Payment.java:
package com.example.springtransactiondemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 支付记录实体类
*
* @author ken
*/
@Data
@TableName("payment")
public class Payment {
/**
* 支付记录ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 订单ID
*/
private Long orderId;
/**
* 支付金额
*/
private BigDecimal amount;
/**
* 支付时间
*/
private LocalDateTime paymentTime;
/**
* 支付方式:1-微信,2-支付宝
*/
private Integer paymentMethod;
/**
* 支付状态:0-处理中,1-成功,2-失败
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createdTime;
/**
* 更新时间
*/
private LocalDateTime updatedTime;
}
CreateOrderDTO.java:
package com.example.springtransactiondemo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 创建订单请求DTO
*
* @author ken
*/
@Data
@Schema(description = "创建订单请求参数")
public class CreateOrderDTO {
@NotNull(message = "用户ID不能为空")
@Schema(description = "用户ID", example = "1001")
private Long userId;
@NotNull(message = "商品ID不能为空")
@Schema(description = "商品ID", example = "1")
private Long productId;
@Min(value = 1, message = "购买数量不能小于1")
@Schema(description = "购买数量", example = "2")
private Integer quantity;
@NotNull(message = "支付方式不能为空")
@Schema(description = "支付方式:1-微信,2-支付宝", example = "1")
private Integer paymentMethod;
}
ApiResponse.java:
package com.example.springtransactiondemo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 通用响应DTO
*
* @author ken
*/
@Data
@Schema(description = "通用响应结果")
public class ApiResponse<T> {
@Schema(description = "状态码:200表示成功,其他表示失败", example = "200")
private int code;
@Schema(description = "响应消息", example = "操作成功")
private String message;
@Schema(description = "响应数据")
private T data;
/**
* 成功响应
*
* @param data 响应数据
* @return 成功响应对象
*/
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("操作成功");
response.setData(data);
return response;
}
/**
* 成功响应(无数据)
*
* @return 成功响应对象
*/
public static <T> ApiResponse<T> success() {
return success(null);
}
/**
* 失败响应
*
* @param code 错误码
* @param message 错误消息
* @return 失败响应对象
*/
public static <T> ApiResponse<T> fail(int code, String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(code);
response.setMessage(message);
response.setData(null);
return response;
}
/**
* 失败响应(默认错误码)
*
* @param message 错误消息
* @return 失败响应对象
*/
public static <T> ApiResponse<T> fail(String message) {
return fail(500, message);
}
}
BusinessException.java:
package com.example.springtransactiondemo.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 业务异常类
*
* @author ken
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
/**
* 错误码
*/
private int code;
/**
* 构造方法
*
* @param code 错误码
* @param message 错误消息
*/
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
/**
* 构造方法(默认错误码)
*
* @param message 错误消息
*/
public BusinessException(String message) {
this(500, message);
}
}
GlobalExceptionHandler.java:
package com.example.springtransactiondemo.exception;
import com.example.springtransactiondemo.dto.ApiResponse;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*
* @author ken
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*
* @param e 业务异常
* @return 错误响应
*/
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e) {
log.error("业务异常: {}", e.getMessage(), e);
return ApiResponse.fail(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常
*
* @param e 参数校验异常
* @return 错误响应
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String errorMsg = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.error("参数校验异常: {}", errorMsg);
return ApiResponse.fail(400, errorMsg);
}
/**
* 处理参数校验异常(非实体类参数)
*
* @param e 参数校验异常
* @return 错误响应
*/
@ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<Void> handleConstraintViolationException(ConstraintViolationException e) {
String errorMsg = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
log.error("参数校验异常: {}", errorMsg);
return ApiResponse.fail(400, errorMsg);
}
/**
* 处理其他异常
*
* @param e 异常
* @return 错误响应
*/
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
log.error("系统异常: {}", e.getMessage(), e);
return ApiResponse.fail("系统异常,请联系管理员");
}
}
ProductMapper.java:
package com.example.springtransactiondemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springtransactiondemo.entity.Product;
import org.apache.ibatis.annotations.Param;
/**
* 商品Mapper接口
*
* @author ken
*/
public interface ProductMapper extends BaseMapper<Product> {
/**
* 扣减商品库存
*
* @param productId 商品ID
* @param quantity 扣减数量
* @return 影响行数
*/
int decreaseStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
}
ProductMapper.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.example.springtransactiondemo.mapper.ProductMapper">
<!-- 扣减商品库存,带库存充足校验 -->
<update id="decreaseStock">
UPDATE product
SET stock = stock - #{quantity},
updated_time = NOW()
WHERE id = #{productId} AND stock >= #{quantity}
</update>
</mapper>
OrderMapper.java:
package com.example.springtransactiondemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springtransactiondemo.entity.Order;
/**
* 订单Mapper接口
*
* @author ken
*/
public interface OrderMapper extends BaseMapper<Order> {
}
PaymentMapper.java:
package com.example.springtransactiondemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springtransactiondemo.entity.Payment;
/**
* 支付记录Mapper接口
*
* @author ken
*/
public interface PaymentMapper extends BaseMapper<Payment> {
}
ProductService.java:
package com.example.springtransactiondemo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.springtransactiondemo.entity.Product;
/**
* 商品服务接口
*
* @author ken
*/
public interface ProductService extends IService<Product> {
/**
* 扣减商品库存
*
* @param productId 商品ID
* @param quantity 扣减数量
* @return 是否扣减成功
*/
boolean decreaseStock(Long productId, Integer quantity);
}
ProductServiceImpl.java:
package com.example.springtransactiondemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.springtransactiondemo.entity.Product;
import com.example.springtransactiondemo.exception.BusinessException;
import com.example.springtransactiondemo.mapper.ProductMapper;
import com.example.springtransactiondemo.service.ProductService;
import com.example.springtransactiondemo.util.ObjectUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 商品服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Override
public boolean decreaseStock(Long productId, Integer quantity) {
log.info("开始扣减商品库存,商品ID:{},扣减数量:{}", productId, quantity);
// 检查商品是否存在
Product product = getOne(new LambdaQueryWrapper<Product>()
.eq(Product::getId, productId));
if (ObjectUtils.isEmpty(product)) {
throw new BusinessException("商品不存在");
}
// 检查库存是否充足
if (product.getStock() < quantity) {
throw new BusinessException("商品库存不足");
}
// 扣减库存
int rows = baseMapper.decreaseStock(productId, quantity);
if (rows <= 0) {
log.error("扣减商品库存失败,商品ID:{},扣减数量:{}", productId, quantity);
return false;
}
log.info("商品库存扣减成功,商品ID:{},扣减数量:{}", productId, quantity);
return true;
}
}
OrderService.java:
package com.example.springtransactiondemo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.springtransactiondemo.dto.CreateOrderDTO;
import com.example.springtransactiondemo.entity.Order;
/**
* 订单服务接口
*
* @author ken
*/
public interface OrderService extends IService<Order> {
/**
* 创建订单并完成支付
*
* @param createOrderDTO 订单信息
* @return 创建的订单
*/
Order createOrderAndPay(CreateOrderDTO createOrderDTO);
}
OrderServiceImpl.java:
package com.example.springtransactiondemo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.springtransactiondemo.dto.CreateOrderDTO;
import com.example.springtransactiondemo.entity.Order;
import com.example.springtransactiondemo.entity.Payment;
import com.example.springtransactiondemo.entity.Product;
import com.example.springtransactiondemo.exception.BusinessException;
import com.example.springtransactiondemo.mapper.OrderMapper;
import com.example.springtransactiondemo.service.OrderService;
import com.example.springtransactiondemo.service.PaymentService;
import com.example.springtransactiondemo.service.ProductService;
import com.example.springtransactiondemo.util.ObjectUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final ProductService productService;
private final PaymentService paymentService;
public OrderServiceImpl(ProductService productService, PaymentService paymentService) {
this.productService = productService;
this.paymentService = paymentService;
}
/**
* 创建订单并完成支付
* 使用REQUIRED传播行为(默认),确保所有操作在同一个事务中
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrderAndPay(CreateOrderDTO createOrderDTO) {
log.info("开始创建订单并支付,用户ID:{},商品ID:{},数量:{}",
createOrderDTO.getUserId(), createOrderDTO.getProductId(), createOrderDTO.getQuantity());
// 1. 查询商品信息
Product product = productService.getById(createOrderDTO.getProductId());
if (ObjectUtils.isEmpty(product)) {
throw new BusinessException("商品不存在");
}
// 2. 计算订单总金额
BigDecimal totalAmount = product.getPrice()
.multiply(BigDecimal.valueOf(createOrderDTO.getQuantity()));
// 3. 创建订单
Order order = new Order();
order.setUserId(createOrderDTO.getUserId());
order.setProductId(createOrderDTO.getProductId());
order.setQuantity(createOrderDTO.getQuantity());
order.setTotalAmount(totalAmount);
order.setStatus(0); // 待支付
boolean saveOrderResult = save(order);
if (!saveOrderResult) {
log.error("创建订单失败");
throw new BusinessException("创建订单失败");
}
try {
// 4. 扣减库存
boolean decreaseStockResult = productService.decreaseStock(
createOrderDTO.getProductId(), createOrderDTO.getQuantity());
if (!decreaseStockResult) {
throw new BusinessException("扣减库存失败");
}
// 5. 处理支付
Payment payment = new Payment();
payment.setOrderId(order.getId());
payment.setAmount(totalAmount);
payment.setPaymentTime(LocalDateTime.now());
payment.setPaymentMethod(createOrderDTO.getPaymentMethod());
payment.setStatus(1); // 支付成功
boolean savePaymentResult = paymentService.savePayment(payment);
if (!savePaymentResult) {
throw new BusinessException("保存支付记录失败");
}
// 6. 更新订单状态为已支付
order.setStatus(1); // 已支付
boolean updateOrderResult = updateById(order);
if (!updateOrderResult) {
log.error("更新订单状态失败,订单ID:{}", order.getId());
throw new BusinessException("更新订单状态失败");
}
log.info("订单创建并支付成功,订单ID:{}", order.getId());
return order;
} catch (Exception e) {
log.error("订单处理过程中发生异常,订单ID:{}", order.getId(), e);
// 抛出异常,触发事务回滚
throw new BusinessException("订单处理失败:" + e.getMessage());
}
}
}
PaymentService.java:
package com.example.springtransactiondemo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.springtransactiondemo.entity.Payment;
/**
* 支付服务接口
*
* @author ken
*/
public interface PaymentService extends IService<Payment> {
/**
* 保存支付记录
*
* @param payment 支付记录
* @return 是否保存成功
*/
boolean savePayment(Payment payment);
}
PaymentServiceImpl.java:
package com.example.springtransactiondemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.springtransactiondemo.entity.Payment;
import com.example.springtransactiondemo.exception.BusinessException;
import com.example.springtransactiondemo.mapper.PaymentMapper;
import com.example.springtransactiondemo.service.PaymentService;
import com.example.springtransactiondemo.util.ObjectUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 支付服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class PaymentServiceImpl extends ServiceImpl<PaymentMapper, Payment> implements PaymentService {
@Override
public boolean savePayment(Payment payment) {
log.info("开始保存支付记录,订单ID:{},支付金额:{}",
payment.getOrderId(), payment.getAmount());
// 检查订单是否已支付
Payment existingPayment = getOne(new LambdaQueryWrapper<Payment>()
.eq(Payment::getOrderId, payment.getOrderId()));
if (!ObjectUtils.isEmpty(existingPayment)) {
throw new BusinessException("订单已支付,不能重复支付");
}
// 保存支付记录
boolean saveResult = save(payment);
if (!saveResult) {
log.error("保存支付记录失败,订单ID:{}", payment.getOrderId());
return false;
}
log.info("支付记录保存成功,订单ID:{}", payment.getOrderId());
return true;
}
}
OrderController.java:
package com.example.springtransactiondemo.controller;
import com.example.springtransactiondemo.dto.ApiResponse;
import com.example.springtransactiondemo.dto.CreateOrderDTO;
import com.example.springtransactiondemo.entity.Order;
import com.example.springtransactiondemo.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单控制器
*
* @author ken
*/
@RestController
@RequestMapping("/api/order")
@Tag(name = "订单管理", description = "订单创建、支付等接口")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
/**
* 创建订单并支付
*
* @param createOrderDTO 订单信息
* @return 创建的订单
*/
@PostMapping("/createAndPay")
@Operation(summary = "创建订单并支付", description = "创建订单、扣减库存、处理支付的完整流程,保证事务一致性")
public ApiResponse<Order> createOrderAndPay(@Valid @RequestBody CreateOrderDTO createOrderDTO) {
Order order = orderService.createOrderAndPay(createOrderDTO);
return ApiResponse.success(order);
}
}
package com.example.springtransactiondemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Spring事务演示项目启动类
*
* @author ken
*/
@SpringBootApplication
@MapperScan("com.example.springtransactiondemo.mapper")
@EnableTransactionManagement // 启用事务管理,Spring Boot中可省略,会自动配置
public class SpringTransactionDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringTransactionDemoApplication.class, args);
}
}
我们通过以下测试场景验证事务的正确性:
调用/api/order/createAndPay接口,传入正确的参数:
{
"userId": 1001,
"productId": 1,
"quantity": 2,
"paymentMethod": 1
}
预期结果:
修改OrderServiceImpl类,在扣减库存后手动抛出异常:
// 4. 扣减库存
boolean decreaseStockResult = productService.decreaseStock(
createOrderDTO.getProductId(), createOrderDTO.getQuantity());
if (!decreaseStockResult) {
throw new BusinessException("扣减库存失败");
}
// 手动抛出异常,测试事务回滚
throw new RuntimeException("测试事务回滚");
再次调用相同接口,预期结果:
这说明事务正常工作,当发生异常时,所有操作都被回滚。
修改PaymentService的savePayment方法,添加REQUIRES_NEW传播行为:
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public boolean savePayment(Payment payment) {
// 方法实现不变
}
在OrderServiceImpl的支付操作后抛出异常:
// 5. 处理支付
Payment payment = new Payment();
// ... 设置支付信息 ...
boolean savePaymentResult = paymentService.savePayment(payment);
if (!savePaymentResult) {
throw new BusinessException("保存支付记录失败");
}
// 手动抛出异常
throw new RuntimeException("测试传播行为");
调用接口,预期结果:
savePayment使用了REQUIRES_NEW,事务独立提交)这验证了REQUIRES_NEW传播行为的效果:创建新的独立事务,与外部事务互不影响。
在使用 Spring 事务的过程中,开发者常常会遇到各种问题。本节将介绍一些常见问题及其解决方案。
方法不是 public 的:Spring 事务只能应用在 public 方法上,非 public 方法上的@Transactional注解不会生效。
解决方案:将事务方法改为 public 修饰。
自调用问题:在同一个类中,一个非事务方法调用事务方法,事务不会生效,因为自调用不会经过 AOP 代理。
@Service
public class OrderService {
// 非事务方法
public void processOrder(Order order) {
// 自调用事务方法,事务不生效
createOrder(order);
}
// 事务方法
@Transactional
public void createOrder(Order order) {
// 业务逻辑
}
}
解决方案:
异常被捕获:如果事务方法内部捕获了异常,没有重新抛出,事务不会回滚。
@Transactional
public void createOrder(Order order) {
try {
// 业务逻辑
throw new RuntimeException("发生异常");
} catch (Exception e) {
// 捕获异常但未重新抛出,事务不会回滚
log.error("发生异常", e);
}
}
解决方案:捕获异常后重新抛出,或使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚。
错误的异常类型:默认情况下,只有 RuntimeException 和 Error 会导致事务回滚,检查异常(如 SQLException)不会。
解决方案:通过rollbackFor属性指定需要回滚的异常类型。
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) throws SQLException {
// 业务逻辑
}
数据源未配置事务管理器:如果没有为数据源配置对应的事务管理器,事务注解不会生效。
解决方案:确保配置了合适的事务管理器,如DataSourceTransactionManager。
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
即使正确使用了事务,仍然可能遇到并发问题,需要根据业务场景选择合适的隔离级别。
在分布式系统中,单个 Spring 事务无法跨多个数据源保证一致性,需要使用分布式事务解决方案:
Spring 事务是保证数据一致性的重要机制,通过@Transactional注解,开发者可以轻松实现声明式事务管理。本文从数据库事务原理出发,深入剖析了 Spring 事务的实现机制,讲解了传播行为、隔离级别等核心概念,并通过完整的实战案例展示了事务的应用。
readOnly = truerollbackFor属性,避免因异常类型问题导致事务不回滚通过合理使用 Spring 事务,我们可以在保证数据一致性的同时,兼顾系统性能和开发效率。深入理解事务的原理和机制,不仅能帮助我们解决实际开发中的问题,还能提升对整个系统设计的理解。