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相关的代理、关联关系,跨服务传递时容易触发未预期的序列化行为,这是微服务架构的常见误区。
临时修复方案
- 改用DTO传递数据:定义
ShowRequestDTO(只包含showId、time、date等核心字段,不包含Movie关联对象),在Show服务调用Movie服务的addShowToMovie时传递该DTO,Movie服务接收后仅关联showId即可(无需完整Show实体)。 - 剥离关联对象:在调用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
相关产品推荐
相关产品推荐

