Java+MongoDB注册流程避免重复记录的替代策略咨询
解决Java+MongoDB注册流程重复记录的替代策略
针对你提到的复杂唯一性判定、无法依赖全量同步或单一索引的场景,以下是几种可行的替代方案:
1. MongoDB原子操作结合自定义唯一性校验
将"检查存在+插入/更新"合并为MongoDB的原子操作,避免多线程下的竞态条件。利用findOneAndUpdate配合upsert:true,把复杂的唯一性规则作为查询条件,预处理后的内容作为更新字段——如果匹配到记录则跳过(或更新),未匹配到则插入新数据。
// 构造复杂唯一性查询条件(示例:手机号匹配,或邮箱匹配,或身份证前缀匹配) Query query = new Query(); query.addCriteria(Criteria.where("phone").is(user.getPhone()) .orOperator( Criteria.where("email").is(user.getEmail()), Criteria.where("idCard").regex("^" + user.getIdCardPrefix() + ".*") )); // 组装预处理后的用户数据 Update update = new Update(); update.set("username", preProcessUsername(user.getUsername())); update.set("phone", user.getPhone()); update.set("email", user.getEmail()); update.set("createTime", new Date()); // 原子执行:无匹配则插入,有匹配则更新(returnNew:true返回操作后的文档) User resultUser = mongoTemplate.findOneAndUpdate( query, update, new FindAndModifyOptions().upsert(true).returnNew(true), User.class ); // 判断是否为新插入的记录(可通过createTime或版本号区分) boolean isNewUser = resultUser.getCreateTime().getTime() >= System.currentTimeMillis() - 1000; if (!isNewUser) { throw new RuntimeException("用户已存在"); }
2. 请求幂等性设计
给每个注册请求生成唯一的幂等键(基于业务唯一性规则生成,比如手机号+邮箱+身份证前缀的哈希值),在执行预处理前通过原子操作判断幂等键是否存在——存在则直接拦截重复请求,不存在则继续处理并标记幂等键为"处理中"。
// 生成幂等键:基于业务唯一标识组合哈希 String bizUniqueKey = user.getPhone() + "_" + user.getEmail() + "_" + user.getIdCardPrefix(); String idempotentKey = "register:idempotent:" + DigestUtils.md5Hex(bizUniqueKey); // Redis SETNX原子操作:仅当键不存在时设置,避免重复请求 Boolean isFirstRequest = stringRedisTemplate.opsForValue() .setIfAbsent(idempotentKey, "processing", 5, TimeUnit.MINUTES); if (!isFirstRequest) { throw new RuntimeException("请勿重复提交注册请求"); } try { // 执行数据预处理 user.setUsername(preProcessUsername(user.getUsername())); // 插入MongoDB mongoTemplate.insert(user); // 处理完成后删除幂等键(或让其自动过期) stringRedisTemplate.delete(idempotentKey); } catch (Exception e) { // 异常时清理幂等键,允许重试 stringRedisTemplate.delete(idempotentKey); throw e; }
3. 细粒度分布式锁
放弃全流程同步,仅在"检查存在+插入"的关键阶段加分布式锁,锁的粒度是业务唯一性规则的哈希值——只有同一业务规则的请求会竞争锁,不同请求互不干扰,大幅降低性能损耗。
// 生成锁键:基于业务唯一标识组合哈希 String lockKey = "register:lock:" + DigestUtils.md5Hex(user.getPhone() + user.getEmail() + user.getIdCardPrefix()); RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁:等待1秒,持有锁5分钟(根据业务调整超时时间) if (lock.tryLock(1, 5, TimeUnit.MINUTES)) { // 先检查是否存在重复记录 Query query = new Query(); query.addCriteria(Criteria.where("phone").is(user.getPhone()) .orOperator( Criteria.where("email").is(user.getEmail()), Criteria.where("idCard").regex("^" + user.getIdCardPrefix() + ".*") )); User existingUser = mongoTemplate.findOne(query, User.class); if (existingUser != null) { throw new RuntimeException("用户已存在"); } // 预处理数据并插入 user.setUsername(preProcessUsername(user.getUsername())); mongoTemplate.insert(user); } else { throw new RuntimeException("请求过于频繁,请稍后再试"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("请求被中断"); } finally { // 确保锁被正确释放 if (lock.isHeldByCurrentThread()) { lock.unlock(); } }
4. 最终一致性异步去重
如果业务允许短暂的重复记录存在,可以先插入预处理后的数据(标记为"待校验"状态),再通过异步任务实时或定期校验重复记录,发现重复后标记为无效或删除。适合对实时性要求不高的场景。
// 插入待校验状态的用户数据 user.setStatus("PENDING"); mongoTemplate.insert(user); // 异步启动重复校验任务 registerDuplicateChecker.checkAndMarkDuplicate(user.getId()); // 异步校验方法(需配置@EnableAsync) @Async public void checkAndMarkDuplicate(String userId) { User currentUser = mongoTemplate.findById(userId, User.class); if (currentUser == null || !"PENDING".equals(currentUser.getStatus())) { return; } // 查询符合唯一性规则的重复记录(排除自身) Query query = new Query(); query.addCriteria(Criteria.where("_id").ne(userId) .andOperator( Criteria.where("phone").is(currentUser.getPhone()), Criteria.where("email").is(currentUser.getEmail()) )); List<User> duplicates = mongoTemplate.find(query, User.class); Update update = new Update(); if (!duplicates.isEmpty()) { // 标记当前用户为无效 update.set("status", "INVALID"); } else { // 标记为有效用户 update.set("status", "VALID"); } mongoTemplate.updateFirst(Query.query(Criteria.where("_id").is(userId)), update, User.class); }
内容的提问来源于stack exchange,提问作者Mansi Joshi
相关产品推荐
相关产品推荐

