首页  > 时光论坛

深入解析: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(资源访问层)

负责:

DBRedisHTTP(第三方)MQOSS示例:

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

而是写:

UserDomainServiceUserProfileServiceUserPointServiceOrderCreateServiceOrderCancelServiceOrderRefundService业务清晰很多。

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

错误写法:

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(事务层)

@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

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 纯复杂业务结构自然、可测试、可拆分系统稳定性与扩展性大幅提升