MongoDB客户端分片:按月归档冷数据的实践及spring-data-mongo适配问题
可行的Spring Data MongoDB动态月份集合实践方案
这个场景太常见了——不想搞复杂的分片,只是想按月归档冷数据,同时还想保住Spring Data MongoDB带来的便捷性对吧?完全有办法,我给你分享几个实际项目里用过的方案:
1. 动态指定集合名的Repository实现
Spring Data MongoDB其实支持动态集合名,不用给每个月份集合都写一个Repository。
方式A:利用SpEL表达式动态绑定集合名
你可以在实体类的@Document注解里用SpEL来动态生成集合名,比如配合一个专门的集合名提供者Bean:
@Component public class CollectionNameProvider { public String getOrderCollection(LocalDate date) { return "order_" + DateTimeFormatter.ofPattern("yyyyMM").format(date); } // 如果是默认查询当前月份的集合,也可以写无参方法 public String getCurrentOrderCollection() { return getOrderCollection(LocalDate.now()); } }
然后实体类上这样写:
@Document(collection = "#{@collectionNameProvider.getCurrentOrderCollection()}") public class Order { // 实体字段... }
这种方式适合大部分时候操作当前月份集合的场景,如果要操作历史月份,还可以自定义Repository方法,通过参数传递日期来指定集合。
方式B:自定义Repository扩展动态集合方法
先定义一个BaseRepository接口:
public interface DynamicCollectionRepository<T, ID> extends MongoRepository<T, ID> { List<T> findByCollectionName(String collectionName, Specification<T> spec); void saveToCollection(T entity, String collectionName); }
然后实现这个接口的自定义方法,利用MongoTemplate来操作指定集合:
public class DynamicCollectionRepositoryImpl<T, ID> extends SimpleMongoRepository<T, ID> implements DynamicCollectionRepository<T, ID> { private final MongoTemplate mongoTemplate; private final EntityInformation<T, ID> entityInformation; public DynamicCollectionRepositoryImpl(EntityInformation<T, ID> entityInformation, MongoTemplate mongoTemplate) { super(entityInformation, mongoTemplate); this.mongoTemplate = mongoTemplate; this.entityInformation = entityInformation; } @Override public List<T> findByCollectionName(String collectionName, Specification<T> spec) { Query query = new Query(spec.toPredicate(entityInformation.getJavaType(), null, null)); return mongoTemplate.find(query, entityInformation.getJavaType(), collectionName); } @Override public void saveToCollection(T entity, String collectionName) { mongoTemplate.save(entity, collectionName); } }
最后在你的OrderRepository里继承这个动态接口:
public interface OrderRepository extends DynamicCollectionRepository<Order, String> { // 其他自定义方法... }
这样你就能在业务层灵活指定集合名来操作了。
2. 封装MongoTemplate实现动态集合操作
如果觉得Repository的方式不够灵活,直接封装MongoTemplate是更直接的方案。比如写一个专门的服务类:
@Service public class OrderArchiveService { private final MongoTemplate mongoTemplate; private static final String ORDER_COLLECTION_PREFIX = "order_"; @Autowired public OrderArchiveService(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } // 保存订单到对应月份的集合 public void saveOrderToMonthlyCollection(Order order) { LocalDate createDate = order.getCreateTime().toLocalDate(); String collectionName = getMonthlyCollectionName(createDate); mongoTemplate.save(order, collectionName); } // 查询指定月份的订单 public Page<Order> getOrdersByMonth(LocalDate month, Pageable pageable) { String collectionName = getMonthlyCollectionName(month); Query query = new Query().with(pageable); long count = mongoTemplate.count(query, collectionName); List<Order> orders = mongoTemplate.find(query, Order.class, collectionName); return new PageImpl<>(orders, pageable, count); } // 生成月份集合名 private String getMonthlyCollectionName(LocalDate date) { return ORDER_COLLECTION_PREFIX + DateTimeFormatter.ofPattern("yyyyMM").format(date); } }
这个方案的好处是完全掌控集合的选择逻辑,而且能直接使用MongoTemplate的所有强大功能,比如复杂查询、聚合、分页等等,完全不丢Spring Data的优势。
3. 定时归档任务结合实体复用
如果你的需求是把主集合里的冷数据定期归档到月份集合,可以配合Spring Task或者Quartz写定时任务:
@Service public class OrderArchiveTask { private final MongoTemplate mongoTemplate; private static final String MAIN_ORDER_COLLECTION = "order_main"; @Autowired public OrderArchiveTask(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } // 每月1号凌晨执行,归档上个月的数据 @Scheduled(cron = "0 0 0 1 * ?") public void archiveLastMonthOrders() { LocalDate now = LocalDate.now(); LocalDate lastMonth = now.minusMonths(1); String targetCollection = "order_" + DateTimeFormatter.ofPattern("yyyyMM").format(lastMonth); // 构造查询条件:上个月及之前的数据 LocalDate currentMonthStart = now.withDayOfMonth(1); Query query = Query.query(Criteria.where("createTime").lt(currentMonthStart)); // 查询并迁移数据 List<Order> ordersToArchive = mongoTemplate.find(query, Order.class, MAIN_ORDER_COLLECTION); if (!ordersToArchive.isEmpty()) { mongoTemplate.insert(ordersToArchive, targetCollection); // 迁移完成后删除主集合里的归档数据 mongoTemplate.remove(query, MAIN_ORDER_COLLECTION); } } }
这里复用同一个Order实体类就可以,因为所有月份集合的结构都是一致的,Spring Data会自动映射。
几个关键注意事项
- 索引同步:每个月份集合最好和主集合保持相同的索引,不然查询历史数据时会很慢。可以在创建集合后自动同步索引,或者用MongoDB的索引管理工具。
- 跨集合查询:如果需要查询多个月份的数据,比如查2018年全年的订单,需要遍历对应的所有集合,然后合并结果。可以封装一个工具方法来实现。
- 结构一致性:确保所有月份集合的文档结构和实体类一致,避免字段变更导致的映射错误。如果需要修改结构,要同步更新所有相关集合。
- 事务支持:如果涉及跨集合的操作(比如迁移数据),需要MongoDB开启副本集才能支持多集合事务,否则要做好失败重试的机制。
内容的提问来源于stack exchange,提问作者hongshuwei
相关产品推荐
相关产品推荐

