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

JPA仓库连续更新异常:短时间内部分消息未标记为已读

问题

我有一个名为message的PostgreSQL表,其中包含message_seen布尔类型字段。在聊天应用中,当user_1打开与user_2的对话页面时,需要将所有收到的消息的message_seen设为true。

我在JPA仓库中定义了如下更新方法:

@Modifying
@Query("UPDATE Message m SET m.message_seen = true WHERE m.message_id = :messageId")
void setMessageSeen(@Param("messageId") Integer messageId);

并在通过STOMP接收消息的控制器中调用该方法:

@Transactional
@MessageMapping("/setSeen")
public void setSeen(Map<String, Integer> messageIdObj){
  System.out.println(messageIdObj.get("messageId"));
  messageRepository.setMessageSeen(messageIdObj.get("messageId"));
}

但当短时间内(1-2秒)连续发送请求时,控制台能打印所有传入的messageId,但数据库中并非所有消息的message_seen都被设为true;拉长请求间隔则一切正常。尝试添加@Lock(LockModeType.PESSIMISTIC_WRITE)无效,请问该问题是否与并发有关,如何解决?

附Message实体类代码

@Entity
@Table(name = "message")
public class Message {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "message_id")
    private Integer messageId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "from_user", referencedColumnName = "id", nullable = false)
    private User fromUser;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "to_user", referencedColumnName = "id", nullable = false)
    private User toUser;

    @Column(name = "message_text", nullable = false)
    private String messageText;

    @Column(name = "sent_date", nullable = false)
    private LocalDateTime sentDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "friendship_id", referencedColumnName = "id", nullable = false)
    private Friendship friendship;

    @Column(name="type")
    private String type;

    @Column(name="message_seen")
    private Boolean messageSeen;

//constructor, getters, setters
}

附MessageRepository代码

public interface MessageRepository extends JpaRepository<Message, Integer> {

    List<Message> findByFriendshipId(int friendshipId);

    @Query("SELECT m FROM Message m WHERE m.toUser.id = :toUser AND m.messageSeen = false")
    List<Message> findUnreadMessagesByToUser(@Param("toUser") Integer toUser);

    @Modifying
    @Query("UPDATE Message m SET m.message_seen = true WHERE m.messageId = :messageId")
    void setMessageSeen(@Param("messageId") Integer messageId);
}

分析与解决方案

这个问题确实和并发事务处理有关,核心原因是JPA的单条更新在高并发下会触发大量独立事务,部分事务因提交延迟或MVCC机制导致更新丢失,加上单个更新的方式放大了冲突概率。以下是具体解决思路:

1. 用批量更新替代单条更新

当前每次请求更新一条消息,短时间内大量请求会产生多个独立事务,容易出现更新遗漏。改成一次请求处理对话中所有未读消息,大幅减少事务数量:

在MessageRepository新增批量更新方法:

@Modifying
@Query("UPDATE Message m SET m.messageSeen = true WHERE m.toUser.id = :toUserId AND m.fromUser.id = :fromUserId AND m.messageSeen = false")
void markMessagesSeenByUsers(@Param("toUserId") Integer toUserId, @Param("fromUserId") Integer fromUserId);

修改控制器方法,接收对话双方ID直接批量更新:

@Transactional
@MessageMapping("/setSeen")
public void setSeen(Map<String, Integer> params){
  Integer toUserId = params.get("toUserId");
  Integer fromUserId = params.get("fromUserId");
  messageRepository.markMessagesSeenByUsers(toUserId, fromUserId);
}

2. 强制事务提交与上下文刷新

如果必须保留单条更新逻辑,要确保每次更新后事务立即提交并刷新JPA上下文:

要么给仓库方法单独加@Transactional(避免控制器事务复用导致延迟):

@Modifying
@Transactional
@Query("UPDATE Message m SET m.message_seen = true WHERE m.messageId = :messageId")
void setMessageSeen(@Param("messageId") Integer messageId);

要么在控制器中注入EntityManager手动刷新:

@Autowired
private EntityManager entityManager;

@Transactional
@MessageMapping("/setSeen")
public void setSeen(Map<String, Integer> messageIdObj){
  Integer messageId = messageIdObj.get("messageId");
  messageRepository.setMessageSeen(messageId);
  entityManager.flush();
}

3. 修改事务传播行为

控制器默认的@Transactional传播行为是REQUIRED,会复用现有事务,短时间大量请求可能导致事务堆积。改成REQUIRES_NEW确保每个请求开启独立事务:

@Transactional(propagation = Propagation.REQUIRES_NEW)
@MessageMapping("/setSeen")
public void setSeen(Map<String, Integer> messageIdObj){
  System.out.println(messageIdObj.get("messageId"));
  messageRepository.setMessageSeen(messageIdObj.get("messageId"));
}

4. 确认数据库事务隔离级别

PostgreSQL默认隔离级别是READ COMMITTED,完全适配这种更新场景。如果应用修改过隔离级别,可在配置中显式指定:

spring.jpa.properties.hibernate.connection.isolation=2
# 2对应READ COMMITTED,为PostgreSQL默认值

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 04:03:16