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
相关产品推荐
相关产品推荐

