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

Spring Boot服务级ID计数器与MongoDB持久化文档重启冲突解决方案问询

解决方案:将ID生成职责下沉到仓库层

核心思路是把ID生成的逻辑从服务层转移到各个持久化仓库的实现类中,让每种存储方式自己适配ID生成规则,既保证接口一致性,又解决MongoDB重启后的ID冲突问题。

1. 重构公共仓库接口

给TreeRepository新增生成ID的方法,让所有仓库实现类各自实现该逻辑:

public interface TreeRepository {
    // 原有方法
    Node save(Node node);
    
    // 新增ID生成方法
    Long generateNextId();
}

2. 各仓库实现ID生成逻辑

内存仓库实现

内存仓库沿用原有计数器逻辑即可,因为内存数据随JVM重启重置,不会有冲突:

@Repository
@Profile("memory")
public class InMemoryTreeRepository implements TreeRepository {
    private final Map<Long, Node> nodes = new ConcurrentHashMap<>();
    private AtomicLong idCounter = new AtomicLong(1L);

    @Override
    public Long generateNextId() {
        return idCounter.getAndIncrement();
    }

    // 其他原有方法实现...
}

PostgreSQL仓库实现

直接利用JPA的自增序列获取ID:

@Repository
@Profile("postgres")
public class PostgresTreeRepository implements TreeRepository {
    @PersistenceContext
    private EntityManager em;

    @Override
    public Long generateNextId() {
        Query query = em.createNativeQuery("SELECT nextval('node_id_seq')");
        return ((BigInteger) query.getSingleResult()).longValue();
    }

    // 其他原有方法实现...
}

MongoDB仓库实现

用原子操作维护独立的计数器集合,彻底解决并发冲突和重启后ID重置问题:

@Repository
@Profile("mongo")
public class MongoTreeRepository implements TreeRepository {
    private final MongoTemplate mongoTemplate;
    private static final String COUNTER_COLLECTION = "counters";
    private static final String NODE_COUNTER_KEY = "node_id";

    public MongoTreeRepository(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
        // 初始化计数器(不存在则创建)
        initCounter();
    }

    private void initCounter() {
        Query query = Query.query(Criteria.where("_id").is(NODE_COUNTER_KEY));
        if (mongoTemplate.findOne(query, CounterDocument.class) == null) {
            mongoTemplate.insert(new CounterDocument(NODE_COUNTER_KEY, 0L));
        }
    }

    @Override
    public Long generateNextId() {
        // 原子递增并返回最新值
        Query query = Query.query(Criteria.where("_id").is(NODE_COUNTER_KEY));
        Update update = new Update().inc("seq", 1);
        FindAndModifyOptions options = FindAndModifyOptions.options().returnNew(true);
        
        CounterDocument counter = mongoTemplate.findAndModify(query, update, options, CounterDocument.class);
        return counter.getSeq();
    }

    // 计数器文档实体
    @Document(collection = COUNTER_COLLECTION)
    private static class CounterDocument {
        @Id
        private String id;
        private Long seq;

        public CounterDocument(String id, Long seq) {
            this.id = id;
            this.seq = seq;
        }

        public Long getSeq() {
            return seq;
        }
    }

    // 其他原有方法实现...
}

3. 修改TreeService,移除本地计数器

服务层不再维护ID计数器,直接调用仓库的generateNextId()获取ID:

@Service
public class TreeService {
    private final TreeRepository repo;
    private final TreeAlgorithmStrategy strategy;

    // 移除idCounter字段

    public TreeService(TreeRepository repo, TreeAlgorithmStrategy strategy) {
        this.repo = repo;
        this.strategy = strategy;
    }

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

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

方案优势

  • 职责单一:ID生成由各仓库自己负责,服务层专注业务逻辑,符合单一职责原则
  • 线程安全:MongoDB用原子操作实现计数器,避免并发场景下的ID冲突
  • 接口兼容:所有仓库都实现同一个TreeRepository接口,ID类型保持Long,不破坏原有架构
  • 无重启问题:MongoDB的计数器持久化在集合中,应用重启后能直接读取最新值,不会重置

内容的提问来源于stack exchange,提问作者JHONY ABRAHAM DE LEON PEREZ

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.02 06:03:09