Spring Boot: Service 层与事务管理

最后更新:2026-08-26

事务是数据一致性的守护者——下单扣库存必须原子完成,要么全成功,要么全回滚。

1. 你将学到


2. 一个业务开发者的真实故事

(1) 痛点:下单扣库存不一致

Alice 发现 OrderFlow 有一个严重 bug:用户下单成功后,商品库存没有扣减。原因是创建订单和扣减库存是两个独立操作,创建订单成功但扣库存失败时,数据出现不一致。Charlie 报告说已有 5 thousand 个超卖订单,公司损失超过 10 thousand USD。

(2) 声明式事务的解法

Spring 的 @Transactional 注解让事务管理变成一行代码:

JAVA
@Transactional
public Order createOrder(CreateOrderRequest request) {
    Product product = productRepository.findById(request.productId()).orElseThrow();
    product.deductStock(request.quantity());    // Deduct stock
    Order order = new Order(product, request.quantity());
    return orderRepository.save(order);         // Create order
}

扣库存失败?整个事务自动回滚,订单也不会创建。

(3) 收益

Alice 加上 @Transactional 后,下单扣库存保证原子性,超卖问题彻底消除。Charlie 的投诉量降为零。


3. Service 层设计模式

(1) 分层架构

100%
graph TD
    A["Controller<br/>Request/Response"] --> B["Service<br/>Business Logic<br/>@Transactional"]
    B --> C["Repository<br/>Data Access"]
    C --> D["Database"]
职责 注解 命名约定
Controller 接收请求、参数校验、返回响应 @RestController *Controller
Service 业务逻辑、事务编排 @Service *Service / *ServiceImpl
Repository 数据访问 @Repository *Repository

▶ 示例: Service 接口与实现

JAVA
// OrderService.java (interface)
package com.orderflow.service;

import com.orderflow.model.Order;
public interface OrderService {
    Order createOrder(Long productId, Integer quantity);
    Order cancelOrder(Long orderId);
    Order getOrder(Long orderId);
}

// OrderServiceImpl.java (implementation)
package com.orderflow.service;

import com.orderflow.model.*;
import com.orderflow.repository.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderServiceImpl(OrderRepository orderRepository,
                            ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }

    @Override
    @Transactional
    public Order createOrder(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }

    @Override
    @Transactional(readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

4. @Transactional 传播行为

(1) 七种传播行为

传播行为 含义 典型场景
REQUIRED(默认) 有事务加入,没有新建 绝大多数业务方法
REQUIRES_NEW 总是新建事务,挂起当前事务 日志记录(不受外层回滚影响)
NESTED 嵌套事务(savepoint),外层回滚则内层也回滚 子操作允许独立回滚
SUPPORTS 有事务加入,没有非事务执行 查询方法
NOT_SUPPORTED 非事务执行,挂起当前事务 不需要事务的操作
MANDATORY 必须在事务中调用,否则抛异常 强制要求事务的方法
NEVER 必须非事务调用,否则抛异常 不允许事务的操作
100%
sequenceDiagram
    participant C as Controller
    participant S1 as createOrder (REQUIRED)
    participant S2 as recordAudit (REQUIRES_NEW)
    participant S3 as deductStock (REQUIRED)

    C->>S1: Begin Tx1
    S1->>S3: Join Tx1
    S3-->>S1: OK
    S1->>S2: Suspend Tx1, Begin Tx2
    S2-->>S1: Commit Tx2, Resume Tx1
    alt Tx1 rollback
        S1-->>C: Rollback Tx1 (Tx2 not affected)
    end

▶ 示例: REQUIRES_NEW 保证审计日志

JAVA
@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    public AuditService(AuditLogRepository auditLogRepository) {
        this.auditLogRepository = auditLogRepository;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAudit(String action, String detail) {
        AuditLog log = new AuditLog(action, detail, Instant.now());
        auditLogRepository.save(log);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功
📌 重点: REQUIRES_NEW 让审计日志在独立事务中提交,即使外层事务回滚,日志也不会丢失。


5. 事务回滚规则

(1) 默认回滚行为

Spring 事务默认只对 RuntimeExceptionError 回滚,不对受检异常回滚

▶ 示例: rollbackFor 配置

JAVA
@Service
public class OrderServiceImpl implements OrderService {

    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) throws Exception {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功
配置 含义 适用场景
rollbackFor = Exception.class 所有异常都回滚 默认推荐配置
rollbackFor = BusinessException.class 仅自定义异常回滚 精细控制
noRollbackFor = ValidationException.class 指定异常不回滚 某些异常不触发回滚

6. 只读事务优化

▶ 示例: 只读事务

JAVA
@Service
public class OrderQueryService {

    private final OrderRepository orderRepository;

    public OrderQueryService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional(readOnly = true)
    public Order getOrder(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    @Transactional(readOnly = true)
    public Page<Order> listOrders(String status, Pageable pageable) {
        return orderRepository.findByStatus(status, pageable);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功
维度 读写事务 只读事务
脏检查 启用(逐字段对比) 跳过
性能 较慢 快 20-30%
写操作 允许 禁止(写会抛异常)
适用 CUD 操作 查询操作
💡 提示: 所有查询方法都应加 @Transactional(readOnly = true),这是零成本的性能优化。


7. 下单扣库存原子事务

▶ 示例: 完整下单流程

JAVA
@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final AuditService auditService;

    public OrderServiceImpl(OrderRepository orderRepository,
                            ProductRepository productRepository,
                            AuditService auditService) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
        this.auditService = auditService;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) {
        // 1. Find product
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found: " + productId));

        // 2. Check stock
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock: available="
                + product.getStock() + ", requested=" + quantity);
        }

        // 3. Deduct stock (in same transaction)
        product.deductStock(quantity);

        // 4. Create order with order item
        Order order = new Order();
        order.addItem(product, quantity);
        Order saved = orderRepository.save(order);

        // 5. Audit log (in separate transaction via REQUIRES_NEW)
        auditService.logAudit("CREATE_ORDER",
            "Order " + saved.getId() + " created for product " + productId);

        return saved;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found: " + orderId));

        if (!"PENDING".equals(order.getStatus())) {
            throw new RuntimeException("Cannot cancel order with status: " + order.getStatus());
        }

        // Restore stock for each item
        for (OrderItem item : order.getItems()) {
            item.getProduct().addStock(item.getQuantity());
        }

        order.setStatus("CANCELLED");
        auditService.logAudit("CANCEL_ORDER", "Order " + orderId + " cancelled");
        return order;
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

8. 综合示例:OrderFlow Service 层完整实现

JAVA
// ProductService.java
package com.orderflow.service;

import com.orderflow.model.Product;
import com.orderflow.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional(readOnly = true)
    public Product getProduct(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    @Transactional(readOnly = true)
    public Page<Product> listProducts(Pageable pageable) {
        return productRepository.findAll(pageable);
    }

    @Transactional(readOnly = true)
    public List<Product> searchProducts(String keyword) {
        return productRepository.findByNameContaining(keyword);
    }

    @Transactional
    public Product createProduct(String name, BigDecimal price, Integer stock) {
        return productRepository.save(new Product(name, price, stock));
    }

    @Transactional
    public Product updateProduct(Long id, String name, BigDecimal price, Integer stock) {
        Product product = getProduct(id);
        product.setName(name);
        product.setPrice(price);
        product.setStock(stock);
        return product;
    }
}

// OrderService.java
package com.orderflow.service;

import com.orderflow.model.Order;
import com.orderflow.model.Product;
import com.orderflow.repository.OrderRepository;
import com.orderflow.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderService(OrderRepository orderRepository,
                        ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }

    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }

    @Transactional(rollbackFor = Exception.class)
    public Order cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
        order.getItems().forEach(item -> item.getProduct().addStock(item.getQuantity()));
        order.setStatus("CANCELLED");
        return order;
    }

    @Transactional(readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findByIdWithItems(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
    }
}

❓ 常见问题

Q @Transactional 加在接口还是实现类上?
A 推荐加在实现类的方法上。加在接口上时,基于 CGLIB 的代理无法拦截接口注解。Spring 官方也推荐加在具体类上。
Q 同一个类中方法互调 @Transactional 会生效吗?
A 不会。Spring 事务基于代理,同类方法互调不走代理,事务不生效。解决方案:1)拆分到不同 Service;2)注入自身代理 @Lazy private OrderService self;
Q readOnly=true 时执行写操作会怎样?
A 取决于数据库和连接池配置。Hibernate 会抛异常,某些数据库驱动忽略。不要在只读事务中执行写操作。
Q NESTED 和 REQUIRES_NEW 有什么区别?
A NESTED 是嵌套事务(savepoint),外层回滚时内层也回滚。REQUIRES_NEW 是独立事务,外层回滚不影响内层。需要独立提交用 REQUIRES_NEW,需要部分回滚用 NESTED。
Q 事务失效的常见原因有哪些?
A 1)同类方法互调;2)方法非 public;3)异常被 catch 吞掉;4)默认只回滚 RuntimeException,受检异常不回滚。
Q 如何验证事务是否生效?
A 1)开启 spring.jpa.show-sql=true 观察 SQL 执行;2)设置 logging.level.org.springframework.transaction=DEBUG 查看事务日志;3)故意抛异常验证回滚。

📖 小节


📝 作业

  1. 基础题(难度⭐):为 OrderFlow 的 ProductService 和 OrderService 添加 @Transactional 注解,验证下单扣库存的原子性——在扣库存后故意抛异常,确认订单也不会创建。

  2. 进阶题(难度⭐⭐):创建 AuditService 使用 REQUIRES_NEW 传播行为,确保即使订单创建事务回滚,审计日志也不丢失。

  3. 挑战题(难度⭐⭐⭐):模拟并发下单场景(两个请求同时购买库存仅剩 1 件的商品),观察超卖问题,思考如何用乐观锁(@Version)或悲观锁(SELECT FOR UPDATE)解决。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏