Spring Boot:Service 层的正确写法 - 事务、幂等、聚合、拆分与业务抽象

企业级开发中,Controller 层更多扮演"流量入口",DAO 层负责与数据库对话,而真正承载业务逻辑、规则聚合、流程协调的核心层------是 Service 层。

Service 层写得好,系统结构清晰、扩展性强、维护成本低。 Service 层写得乱,你会看到:

  • 事务乱用,脏数据频发

  • Service 方法几千行,变成"业务垃圾桶"

  • 幂等没有处理,重复订单、重复扣费

  • Controller 业务化、Service 空壳化

  • 复杂业务写成一坨,根本无法复用

  • 新人看不懂老代码,维护成本指数上升

1、Service 层到底应该做什么?(三大职责)

很多人写 Service 的第一天,就已经写歪了。

真正标准的 Service 层职责有三点:

  • 业务流程编排(Process Orchestration) 串联多个步骤、模块、服务,让业务以正确顺序执行。

  • 业务规则聚合(Business Logic) 校验业务规则,如"库存是否足够""用户是否可下单"。

  • 事务边界控制(Transaction Boundary) 负责 "这段操作必须原子化执行"。

一句话总结:

Service 层负责------对外承接需求,对内协调流程。

它不应该做:

  • 复杂 SQL(应由 Mapper 负责)
  • 直接操作缓存(应在 Manager 层)
  • 维护业务状态(应由 Domain 层)
  • 操作文件、第三方请求(应由 Infrastructure 层)

2、Service 层最容易写错的几件事(也是你要避开的雷区)

① Controller 写业务、Service 写 CRUD

这是最常见的反模式:

java 复制代码
@PostMapping("/order")
public OrderVO createOrder(@RequestBody OrderDTO dto) {
    // 业务校验、扣库存、保存订单、发消息都写在 Controller......
}

问题:

  • Controller 没有事务
  • 业务分散、无法复用
  • 修改极其困难

② Service 成了"上帝类",几千行

直接造成:

  • 新人无法维护
  • 无法测试
  • 功能耦合严重

③ 不加事务或滥用事务

例如:

java 复制代码
@Transactional
public void createOrder() {
    deductStock();
    saveOrder();
    sendMQ(); // 这里不应该在事务里
}

外部系统调用(MQ、短信、HTTP)一旦放进事务,极易导致数据不一致。

④ 幂等没处理

高并发时产生:

  • 重复扣库存
  • 重复下单
  • 重复转账
  • 重复计算积分

3、Service 层的正确结构:三层模型

企业级系统常用的业务分层如下:

解释一下:

① Application Service(应用服务层)

负责业务流程编排:

  • 入参校验(增强)
  • 组合多个 Domain Service 的功能
  • 控制事务边界

类似:

java 复制代码
@Transactional
public Order createOrder(CreateOrderDTO dto) {
    userDomain.checkUserStatus(dto.getUserId());
    productDomain.checkStock(dto.getProductId());
    Order order = orderDomain.create(dto);
    orderDomain.notify(order);
    return order;
}

② Domain Service(领域服务层)

负责业务核心规则:

  • 校验复杂业务规则
  • 维护领域对象状态
  • Domain 内部逻辑

例如:

java 复制代码
public void checkStock(Long productId) {
    Product product = productManager.get(productId);
    if (product.getStock() <= 0) {
        throw new BizException("库存不足");
    }
}

③ Manager(资源访问层)

负责:

  • DB
  • Redis
  • HTTP(第三方)
  • MQ
  • OSS

示例:

java 复制代码
public class ProductManager {
    public Product get(Long id) {
        return productMapper.selectById(id);
    }
}

Manager 就是 集中的资源访问入口。

4、Service 层的事务设计:如何真正防止脏数据?

事务的终极规则只有一条:

事务应该包住"改变系统状态的一整套操作"。

也就是说:

  • 查询不要写事务
  • 调用 MQ / 外部接口不要放事务内
  • 循环写 DB 时要分段控制
  • 跨 Service 的事务最好回归 Application Service 来管

事务的最佳写法

推荐把事务写在 Application Service:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public Long createOrder(OrderDTO dto) {
    productDomain.checkStock(dto.getProductId());
    orderDomain.saveOrder(dto);
    productDomain.reduceStock(dto.getProductId());
    // 业务完成后再发消息,而不是写在事务里
    eventPublisher.publishOrderCreated(dto);
    return dto.getId();
}

哪些操作一定不能写在事务里?

  • Redis 写入
  • MQ 发送
  • HTTP 调用
  • OSS 上传
  • 文件写入
  • 非数据库型资源访问

理由非常简单:

事务失败不会回滚这些操作,会导致严重不一致。

4、Service 层的幂等性设计:如何避免重复执行?

幂等(Idempotency)的核心思想是:

同一请求执行 1 次与执行 N 次,结果必须一致。

常见场景:

  • 创建订单不能重复
  • 扣费不能重复
  • 创建支付单必须唯一
  • 积分发放不能重复
  • 消息重复消费

企业级幂等的三种方案
① 幂等 Token(推荐用于前端请求)

流程:

  • 前端请求生成 token
  • 后端缓存 token → Redis SETNX
  • 调用业务时携带 token
  • 缓存中 token 删除后,不允许再次使用

示例代码:

java 复制代码
public boolean checkIdempotent(String key) {
    Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.MINUTES);
    if (!Boolean.TRUE.equals(success)) {
        throw new BizException("请勿重复提交");
    }
    return true;
}

② 去重表(用于业务唯一性)

订单:

java 复制代码
insert into order(id, sn, user_id) values(?,?,?)
-- sn 做唯一索引
违反唯一约束 = 重复提交。

③ 状态机幂等(用于支付、物流、状态流转)

例如支付状态:

如果当前状态是 SUCCESS,再执行扣费,必须拒绝。

6、业务的拆分与聚合:如何避免"上帝 Service"?

Service 之所以被写成几千行,是因为不会拆。

给你一套「企业级拆分规则」:

① 按业务流程拆分,而不是按 CRUD 拆分

不要写:

UserService

OrderService

而是写:

  • UserDomainService
  • UserProfileService
  • UserPointService
  • OrderCreateService
  • OrderCancelService
  • OrderRefundService

业务清晰很多。

② 复杂流程抽象成独立方法,不要写成一坨

错误写法:

java 复制代码
public void createOrder() {
    // 校验用户
    // 校验库存
    // 创建订单
    // 扣库存
    // 发消息
}

正确写法:

java 复制代码
public void createOrder() {
    validateUser();
    validateStock();
    Order order = generateOrder();
    reduceStock();
    postEvent(order);
}

好处:

  • 可阅读
  • 可复用
  • 单元测试更简单
  • 新人更容易理解

③ 方法名必须体现"业务含义",而不是"技术动作"

错误:

  • save()
  • update()
  • handler()
  • process()

正确:

  • checkUserStatus()
  • validateProduct()
  • generateOrder()
  • ockStock()
  • publishOrderEvent()

可读性提升一个量级。

7、业务抽象:如何让 Service 层可扩展、可维护?

业务抽象的核心目的是:

变化的隔离 & 稳定部分共享。

① 提取共性逻辑:抽象成 Base Domain

例如支付抽象:

java 复制代码
public abstract class PayService {

    public final PayResult pay(PayRequest request) {
        validate(request);
        doPay(request);
        return afterPay(request);
    }

    protected abstract void validate(PayRequest request);
    protected abstract void doPay(PayRequest request);
    protected abstract PayResult afterPay(PayRequest request);
}

支付宝、微信都继承它。

② 横切逻辑抽象:如幂等/日志/权限

例如幂等:

java 复制代码
@Around("@annotation(Idempotent)")
public Object idempotent(ProceedingJoinPoint pjp) {
    String key = buildKey(pjp);
    if (!tryAcquire(key)) {
        throw new BizException("请勿重复操作");
    }
    return pjp.proceed();
}

不污染业务代码。

③ 复杂业务状态抽象为领域对象(DDD)

例如订单:

java 复制代码
public class Order {

    private OrderStatus status;

    public void pay() {
        if (status != OrderStatus.CREATED) {
            throw new BizException("状态非法");
        }
        status = OrderStatus.PAID;
    }
}

业务规则放在对象内部,Service 更轻。

8、一个综合示例:从 Controller 到 Service 到 Domain

完整链路示例:

POST /order/create

Controller

java 复制代码
@PostMapping("/create")
public Long createOrder(@RequestBody CreateOrderDTO dto) {
    return orderApplicationService.createOrder(dto);
}

ApplicationService(事务层)

dart 复制代码
@Service
publicclass OrderApplicationService {

    @Transactional
    public Long createOrder(CreateOrderDTO dto) {

        userDomain.checkUserStatus(dto.getUserId());
        productDomain.checkStock(dto.getProductId());

        Order order = orderDomain.create(dto);

        productDomain.reduceStock(dto.getProductId());

        orderEventPublisher.publishOrderCreated(order);

        return order.getId();
    }
}

DomainService

dart 复制代码
public class OrderDomainService {

    public Order create(CreateOrderDTO dto) {
        return Order.create(dto);
    }
}

Manager

dart 复制代码
public class OrderManager {

    public void save(Order order) {
        orderMapper.insert(order);
    }
}

整个流程清晰、职责明确、结构可控。

总结

Service 层是整个后端的"中枢神经",写好它,你的系统会有以下变化:

  • 事务边界清晰
  • 业务逻辑易理解、易维护
  • 幂等可控,不怕高并发
  • Controller 轻,Manager 纯
  • 复杂业务结构自然、可测试、可拆分
  • 系统稳定性与扩展性大幅提升
相关推荐
踏浪无痕14 小时前
JobFlow已开源:面向业务中台的轻量级分布式调度引擎 — 支持动态分片与延时队列
后端·架构·开源
Pitayafruit14 小时前
Spring AI 进阶之路05:集成 MCP 协议实现工具调用
spring boot·后端·llm
ss27315 小时前
线程池:任务队列、工作线程与生命周期管理
java·后端
不像程序员的程序媛15 小时前
Spring的cacheEvict
java·后端·spring
踏浪无痕15 小时前
JobFlow 实战:无锁调度是怎么做到的
后端·面试·架构
shoubepatien15 小时前
JAVA -- 11
java·后端·intellij-idea
喵个咪15 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:kratos-bootstrap 入门教程(类比 Spring Boot)
后端·微服务·go
uzong15 小时前
从大厂毕业后,到小公司当管理,十年互联网老兵的思维习惯阶段复盘
后端
追逐时光者15 小时前
一个 WPF 开源、免费的 SVG 图像查看控件
后端·.net
谷哥的小弟16 小时前
Spring Framework源码解析——PropertiesLoaderUtils
java·后端·spring·框架·源码