Spring Boot服务端的自增ID计数器在应用重新启动时与MongoDB已持久化的文档发生冲突

后端开发 2026-07-09

背景

我有一个Spring Boot应用,提供三种可互换的持久化层:内存、PostgreSQL和 MongoDB。活动的持久化层通过 app.storageapplication.properties 中选择。我的 TreeService 通过一个简单的内存中ID计数器来管理节点创建:

java

@Service
public class TreeService {

    private final TreeRepository repo;
    private final TreeAlgorithmStrategy strategy;

    private Long idCounter = 1L;

    public Node createRoot(String value) {
        Node root = new Node(idCounter++, value, null);
        return repo.save(root);
    }

    public Node addChild(Long parentId, String value) {
        Node child = new Node(idCounter++, value, parentId);
        return repo.save(child);
    }
}

MongoDB文档直接使用 Long ID作为文档的 _id 字段:

java

@Document(collection = "nodes")
public class MongoNodeDocument {

    @Id
    private Long id;
    private String value;
    private Long parentId;
}

观察到的行为

使用 app.storage=memory 时,一切都能正确工作,因为所有数据都是易失性的,并会随JVM重启而重置。然而,使用 app.storage=mongo 时:

  1. 在第一会话中,节点被正确创建并持久化,ID为 1、2、3……
  2. 应用重启时,idCounter 重置为1
  3. 再次调用 POST /nodes/root 会在MongoDB中抛出重复键异常,因为已经存在具有 _id: 1 的文档

期望行为

ID计数器应与MongoDB中已存在的文档保持一致,这样新节点的ID就不会与已持久化的数据冲突。

我尝试过的做法

  • @PostConstruct 方法中通过查询存储库中已有的最大ID来初始化 idCounter — 能工作,但感觉有hack的嫌疑,且并非线程安全
  • 让MongoDB自行生成 ObjectId — 破坏了共享的 TreeRepository 接口,因为内存层和PostgreSQL层都使用 Long 类型的ID
  • 使用来自JPA的 @GeneratedValue — 对MongoDB文档不可用

问题

在一个多持久化的Spring Boot应用中,所有存储库共享同一个接口,且使用 Long 作为ID类型,如何以最干净的策略将服务层的ID计数器与MongoDB文档进行同步?ID生成的职责应该从服务转移到每个存储库实现吗?

解决方案

是的,将ID生成的职责移交给存储库实现(仓库驱动)将是最佳策略。服务不应该管理顺序ID的状态或计数器。它属于持久化层。你的 TreeService 应保持对数据库无关,并避免生命周期管理方面的错误。

首先,定义你的领域模型和接口:

public interface TreeRepository 
{
    Node save(Node node);
    // ...
}

然后,在实体保存之前,使用一个 AbstractMongoEventListener 进行拦截,它将在后台处理ID的生成,同时让你的 MongoDB 存储库接口保持完全标准:

@Component
@ConditionalOnProperty(name = "app.storage", havingValue = "mongo")
public class MongoBeforeConvertListener extends AbstractMongoEventListener<MongoNodeDocument> 
{
    // Uses the findAndModify sequence tool
    private final MongoSequenceGenerator sequenceGenerator;

    public MongoBeforeConvertListener(MongoSequenceGenerator sequenceGenerator) 
    {
        this.sequenceGenerator = sequenceGenerator;
    }

    @Override
    public void onBeforeConvert(BeforeConvertEvent<MongoNodeDocument> event) 
    {
        MongoNodeDocument doc = event.getSource();

        if (doc.getId() == null)
            doc.setId(sequenceGenerator.generateSequence("nodes_sequence"));
    }
}

接着,让内存中的存储库在本地管理自己对 AtomicLong 的线程安全计数器:

@Repository
@ConditionalOnProperty(name = "app.storage", havingValue = "memory")
public class InMemoryTreeRepository implements TreeRepository 
{    
    private final Map<Long, Node> storage = new ConcurrentHashMap<>();
    private final AtomicLong idCounter = new AtomicLong(1L);

    @Override
    public Node save(Node node) 
    {
        // Assign next ID automatically inside the repo
        if (node.getId() == null) 
            node = new Node(idCounter.getAndIncrement(), node.getValue(), node.getParentId());

        storage.put(node.getId(), node);
        return node;
    }
}

并用 @GeneratedValue(strategy = GenerationType.SEQUENCE) 对实体进行映射,使得 HibernatePostgreSQL 能在 repo.save() 上原生协同生成序列:

import jakarta.persistence.*;

@Entity
@Table(name = "nodes")
public class PostgresNodeEntity 
{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "node_seq_gen")
    @SequenceGenerator(
        name = "node_seq_gen", 
        sequenceName = "nodes_id_seq", 
        allocationSize = 1
    ) 
    private Long id;

    private String value;

    @Column(name = "parent_id")
    private Long parentId;

    // Constructors
    public PostgresNodeEntity() {}

    public PostgresNodeEntity(Long id, String value, Long parentId) 
    {
        this.id = id;
        this.value = value;
        this.parentId = parentId;
    }

    // Getters / setters...
}

最后,你的服务方法将大致如下:

public Node addChild(Long parentId, String value) 
{
    // ID starts as null
    Node child = new Node(null, value, parentId); 

    // The specialized repo assigns the ID safely
    return repo.save(child); 
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章