深入解析: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
这是最常见的反模式:

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

问题:

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

② Service 成了“上帝类”,几千行
直接造成:

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

③ 不加事务或滥用事务
例如:

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

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

④ 幂等没处理
高并发时产生:

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

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

在这里插入图片描述
企业级系统常用的业务分层如下:

解释一下:

① Application Service(应用服务层)
负责业务流程编排:

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

类似:

@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 内部逻辑

例如:

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

③ Manager(资源访问层)
负责:

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

示例:

public class ProductManager {
public Product get(Long id) {
return productMapper.selectById(id);
}
}

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

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

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

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

也就是说:

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

事务的最佳写法
推荐把事务写在 Application Service:

@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 删除后,不允许再次使用

示例代码:

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;
}

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

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

③ 状态机幂等(用于支付、物流、状态流转)
例如支付状态:
在这里插入图片描述

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

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

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

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

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

UserService
OrderService

而是写:

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

业务清晰很多。

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

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

正确写法:

public void createOrder() {
validateUser();
validateStock();
Order order = generateOrder();
reduceStock();
postEvent(order);
}

好处:

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

③ 方法名必须体现“业务含义”,而不是“技术动作”
错误:

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

正确:

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

可读性提升一个量级。

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

业务抽象的核心目的是:

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

① 提取共性逻辑:抽象成 Base Domain
例如支付抽象:

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);
}

支付宝、微信都继承它。

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

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

不污染业务代码。

③ 复杂业务状态抽象为领域对象(DDD)
例如订单:

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

@PostMapping("/create")
public Long createOrder(@RequestBody CreateOrderDTO dto) {
return orderApplicationService.createOrder(dto);
}

ApplicationService(事务层)


publicclass OrderApplicationService {

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

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

Manager

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

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

总结

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

  • 事务边界清晰
  • 业务逻辑易理解、易维护
  • 幂等可控,不怕高并发
  • Controller 轻,Manager 纯
  • 复杂业务结构自然、可测试、可拆分
  • 系统稳定性与扩展性大幅提升
posted @ 2025-12-26 10:41  gccbuaa  阅读(0)  评论(0)    收藏  举报