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 纯
  • 复杂业务结构自然、可测试、可拆分
  • 系统稳定性与扩展性大幅提升
相关推荐
用户268516121075618 分钟前
GMP 三大核心结构体字段详解
后端·go
一路向北⁢21 分钟前
短信登录安全防护方案(Spring Boot)
spring boot·redis·后端·安全·sms·短信登录
古城小栈24 分钟前
Tokio:Rust 异步界的 “霸主”
开发语言·后端·rust
进击的丸子27 分钟前
基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践
java·后端·github
while(1){yan}34 分钟前
SpringAOP
java·开发语言·spring boot·spring·aop
techdashen36 分钟前
Go 1.18+ slice 扩容机制详解
开发语言·后端·golang
浙江巨川-吉鹏36 分钟前
【城市地表水位连续监测自动化系统】沃思智能
java·后端·struts·城市地表水位连续监测自动化系统·地表水位监测系统
fliter1 小时前
Go 1.18+ slice 扩容机制详解
后端
RemainderTime1 小时前
从零搭建Spring Boot3.x生产级单体脚手架项目(JDK17 + Nacos + JWT + Docker)
java·spring boot·架构
黯叶1 小时前
基于 Docker+Docker-Compose 的 SpringBoot 项目标准化部署(外置 application-prod.yml 配置方案)
java·spring boot·redis·docker