You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot微服务关联数据存储报错排查及级联实现方案咨询

微服务问题排查与关联实现咨询

一、问题现象

现有Movie和Show两个SpringBoot微服务,调用/show/addShow接口时,Show能成功保存,但调用Movie服务的addShowToMovie方法时出现Feign 500错误,错误日志如下:

feign.FeignException$InternalServerError: [500] during [POST] to [http://MOVIE-SERVICE/movie/1/shows] [ApiGatewayClient#addShowToMovie(Integer,Show)]: [{"status":500,"error":"Internal Server Error","message":"Cannot invoke \"java.util.List.size()\" because \"this.this$0.operationQueue\" is null","path":"/movie/1/shows","timestamp":"2025-03-23T17:19:12.090776"}]     at feign.FeignException.serverErrorStatus(FeignException.java:281)  at feign.FeignException.errorStatus(FeignException.java:226)

二、相关代码

Movie服务代码

MovieEntity.java

@Entity
@Table(name = "MOVIES")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(nullable = false)
    private String movieName;

    private Double duration;

    @Column(scale = 2)
    private Double rating;

    private Date releaseDate;

    @Enumerated(value = EnumType.STRING)
    private Genre genre;

    @Enumerated(value = EnumType.STRING)
    private Language language;

    @OneToMany(mappedBy = "movie",cascade = CascadeType.ALL)
    @JsonManagedReference
    private List<Show> shows = new ArrayList<>();
}

MovieService.java

public void addShowToMovie(Integer movieId, Show show) {
    Movie movie = movieRepository.findById(movieId)
            .orElseThrow(() -> new MovieDoesNotExistsException(movieId));
    //Show show = Transformer.showDtoToShow(showEntryDto);
    if (movie.getShows() == null) {
        movie.setShows(new ArrayList<>());
    }
    show.setMovie(movie); // Ensure it's linked
    movie.getShows().add(show);
    movieRepository.save(movie);
}

Show服务代码

Show.java

@Entity
@Table(name = "SHOWS")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Show {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer showId;

    private Time time;

    private Date date;

    @ManyToOne
    @JoinColumn
    @JsonBackReference
    private Movie movie;

    @OneToMany(mappedBy = "show", cascade = CascadeType.ALL)
    private List<ShowSeat> showSeatList = new ArrayList<>();

    @OneToMany(mappedBy = "show", cascade = CascadeType.ALL)
    private List<Ticket> ticketList = new ArrayList<>();

    @ManyToOne
    @JoinColumn
    private Theater theater;
}

ShowService.java

public String addShow(ShowEntryDto showEntryDto) {
    // Validate movieId
    Movie movie = getMovieById(showEntryDto.getMovieId());
    Show show = Transformer.showDtoToShow(showEntryDto);
    show.setMovie(movie);

    // Step 1: Save Show in Show Microservice
    show = showRepository.save(show);
    try {
        // Step 2: Call Movie Microservice to link Show
        apiGatewayClient.addShowToMovie(movie.getId(), show);
    } catch (Exception e) {
        // Step 3: Rollback (Delete the Show) if API call fails
        e.printStackTrace();
        showRepository.delete(show);
        throw new RuntimeException("Failed to link Show to Movie. Rolling back Show creation.", e);
    }

    return "Show has been added Successfully";
}

private Movie getMovieById(Integer movieId) {
    try {
        return apiGatewayClient.getMovie(movieId).getBody();
    } catch (FeignException.NotFound e) {
        System.out.println("Movie not found: " + movieId);
        throw new MovieDoesNotExistsException(movieId);
    } catch (FeignException e) {
        System.out.println("Movie Service error: " + e.getMessage());
        throw new RuntimeException("Movie Service unavailable");
    }
}

三、错误原因排查

从错误日志Cannot invoke "java.util.List.size()" because "this.this$0.operationQueue" is null来看,问题根源在于Feign传递JPA实体时的序列化/反序列化异常,具体原因如下:

  • 循环引用与代理对象问题:Movie和Show之间的双向关联使用了@JsonManagedReference和@JsonBackReference,但Show服务通过Feign传递的Show实体中包含从Movie服务获取的Movie对象,该对象可能是Hibernate懒加载代理(比如PersistentBag),序列化时访问代理集合的size()方法时,因代理的操作队列(operationQueue)未初始化导致空指针异常。
  • 直接传递JPA实体的风险:JPA实体包含ORM相关的代理、关联关系,跨服务传递时容易触发未预期的序列化行为,这是微服务架构的常见误区。

临时修复方案

  1. 改用DTO传递数据:定义ShowRequestDTO(只包含showId、time、date等核心字段,不包含Movie关联对象),在Show服务调用Movie服务的addShowToMovie时传递该DTO,Movie服务接收后仅关联showId即可(无需完整Show实体)。
  2. 剥离关联对象:在调用Feign接口前,手动将Show实体中的Movie对象置为null,因为Movie服务已经通过movieId知晓关联的Movie,无需重复传递关联对象。

四、微服务间关联的最佳实现方式(除DTO存储showId列表)

微服务架构下,跨服务的级联操作不能依赖JPA的CascadeType(仅适用于单库),推荐以下两种方案:

1. 事件驱动实现最终一致性

  • 流程:Show服务成功保存Show后,通过消息中间件(如RabbitMQ、Kafka)发布ShowCreatedEvent事件(包含showId、movieId等关键信息);Movie服务监听该事件,收到后更新对应Movie的关联关系(如存储showId列表)。
  • 优势:服务解耦,避免同步调用的依赖问题;即使Movie服务暂时不可用,事件可持久化,待服务恢复后再处理,实现最终一致性。

2. 单向关联+跨服务查询

  • 设计:仅在Show服务的Show实体中保留movieId的关联(@ManyToOne),Movie服务的Movie实体不存储Show的关联列表;当需要查询某个Movie的所有Show时,调用Show服务的专用接口(如/show/list-by-movie/{movieId})获取数据。
  • 优势:彻底避免跨服务的关联维护复杂度,每个服务仅负责自身数据,逻辑清晰,易于维护。

3. 分布式事务(谨慎使用)

如果业务要求强一致性(Show创建与Movie关联必须同时成功/失败),可使用分布式事务框架(如Seata),但会增加系统复杂度,仅适用于对一致性要求极高的场景。


内容的提问来源于stack exchange,提问作者Dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 17:34:55