一次mongo查询不存在字段引发的事故

本文讲述了作者因误操作查询MongoDB,导致CPU和IOPS异常的事故,揭示了全表扫描对性能的影响,强调了对数据库操作的谨慎和理解数据模型的重要性。

  话说今天的一个小小的查询失误给了我比较深刻的教训,也让我对mongo有了更深刻的理解,下面我们来说说这个事情的原委:

我们经常使用阿里云子账号在DMS上查询线上数据库数据,今天也是平常的一次操作

集合:

1

XXXX_message<br>数据量约 600万

我执行了下面的mongo查询:

1

db.XXXX_message.find({"channel_id""1000000009XXXX700XXXX"}).limit(20);

但是上述语句中的 "channel_id" 字段不存在,真实字段应该是channel(有索引),属于失误操作

在执行过程中,我发现查询时间很久,于是中断了查询又重试了两次,还是很久,最后中断了查询,我意识到我想查的字段可能错了,于是看了下集合索引,使用正确的字段检索得到结果

但就在这时候,一场事故也在悄然酝酿,2分钟后,阿里云监控中心打来告警电话,mongo数据库cpu、iops异常升高

 

 

起初并没有意识到是这个查询导致的,还以为是半小时前发布的版本可能有问题,于是立即回滚了版本并开始项目检查

查了许久,并没有查到可能造成本次数据库异常告警的原因,项目对该库的依赖的操作的地方非常少。

当我们苦苦想不到原因的时候,我们去查了下相关慢sql日志,果然一道耗时约1800000ms的慢sql日志引起了我们的注意

这时候我似乎意识到了点什么,我立马查阿里云控制台查询历史核对了我刚才查询的时间和数据库cpu、磁盘iops异常升高的时间节点

完全对上了,该起事故持续半小时左右,那条没有被成功中断的sql也执行了半小时左右

 

 

这让我很震惊,一次控制台查询居然导致整个数据库出现如此严重的问题,mongo底层没有考虑过不存在字段查询问题吗?

我慢慢平复心情,仔细回顾这件事情,我尝试着从mongo和mysql的底层去理解这个问题

mongo本身是集合型数据库,意味着每个集合文档都可以有自己独立的数据结构,和mysql等关系型数据库的很重要的区别就是它没有固定的表结构,它包容且随性

当在查询一个不存在的字段的时候,它仍然按照普通查询检索数据,这时候它会全表扫描,也就是说在上述失误语句中,mongo底层检索了整个集合的数据集,

遍历了该集合所有的磁盘块,这才导致磁盘iops升高且cpu升高。

这次经历让我觉得我有必要记录下相关心得,可能对于很多高级技术人员,这些东西都是很容易理解和规避的事情,但大多数人对此可能并没有深刻认识

这次事故让我对技术多了一层敬畏,这有助于我在今后的代码实践和操作中更加谨慎和多一层思考,希望大家以此为戒!

此文共勉!

@Slf4j @Profile("mongodb") @Service("MongoUserService") public class MongoUserServiceImpl implements UserService { @Value("${system.max-user-limit:10}") private int maxUserNumber; private final MongoTemplate mongoTemplate; private final MeterRegistry meterRegistry; private Counter emailDuplicateCounter; public MongoUserServiceImpl(MongoTemplate mongoTemplate, MeterRegistry meterRegistry) { this.mongoTemplate = mongoTemplate; this.meterRegistry = meterRegistry; } @PostConstruct public void init() { emailDuplicateCounter = Counter.builder("user.registration.duplicate.email") .description("Counts the number of duplicate email registration attempts") .register(meterRegistry); } /** * 用户认证 * * @param loginDTO 登录凭证 * @return 认证结果 */ @Override public boolean auth(UserLoginDTO loginDTO) { Query query = new Query(Criteria.where("email").is(loginDTO.getEmail())); MongoUser user = mongoTemplate.findOne(query, MongoUser.class); if (user == null) { return false; } try { return user.getPassword().equals(AESUtils.encrypt(loginDTO.getPassword())); } catch (Exception e) { throw new RuntimeException("Encryption error", e); } } /** * 用户注册 * * @param registryDTO 注册信息 * @return 注册结果 */ @Override public UserRegistryResultDTO registry(UserRegistryDTO registryDTO) { UserRegistryResultDTO result = new UserRegistryResultDTO(); validateRegistryInfo(registryDTO, result); if (result.getStatus() != RegistryStatusEnum.FAILED) { try { MongoUser newUser = dtoToUser(registryDTO); mongoTemplate.insert(newUser); result.setStatus(RegistryStatusEnum.SUCCESS); result.setMessage("Registration successful in MongoDB"); } catch (DuplicateKeyException e) { log.error("Duplicate email registration", e); result.setStatus(RegistryStatusEnum.FAILED); result.setMessage("Email already exists"); emailDuplicateCounter.increment(); } catch (Exception e) { log.error("MongoDB insert error", e); result.setStatus(RegistryStatusEnum.FAILED); result.setMessage("Database error"); } } return result; } /** * 注册信息验证 */ private void validateRegistryInfo(UserRegistryDTO registryDTO, UserRegistryResultDTO result) { if (!isValidEmail(registryDTO.getEmail())) { result.setStatus(RegistryStatusEnum.FAILED); result.setMessage("Invalid email format"); } if (mongoTemplate.exists(new Query(Criteria.where("email").is(registryDTO.getEmail())), MongoUser.class)) { result.setStatus(RegistryStatusEnum.FAILED); result.setMessage("Email already exists"); emailDuplicateCounter.increment(); } if (mongoTemplate.count(new Query(), MongoUser.class) >= maxUserNumber) { result.setStatus(RegistryStatusEnum.FAILED); result.setMessage("User limit reached"); } } /** * DTO转实体类 */ private MongoUser dtoToUser(UserRegistryDTO dto) { try { return MongoUser.builder() .id(String.valueOf(new ObjectId())) .username(dto.getUsername()) .email(dto.getEmail()) .password(Aragon2Util.hashPassword(dto.getPassword().toCharArray())) .address(AESUtils.encrypt(dto.getAddress())) .createTime(new Date()) .updateTime(new Date()) .build(); } catch (Exception e) { log.error("Encryption failed", e); throw new RuntimeException("User conversion error"); } } /** * 查询所有用户 */ @Override public List<UserInfoDTO> findAll() { return mongoTemplate.findAll(MongoUser.class).stream() .map(user -> new UserInfoDTO(user.getUsername(), user.getEmail(), user.getAddress())) .collect(Collectors.toList()); } /** * 通过邮箱查询用户 */ @Override public UserInfoDTO findByEmail(String email) { Query query = new Query(Criteria.where("email").is(email)); MongoUser user = mongoTemplate.findOne(query, MongoUser.class); return user != null ? new UserInfoDTO(user.getUsername(), user.getEmail(), user.getAddress()) : null; } @Override public ResponseDTO updateEmail(String email, String newEmail) { return null; } /** * 更新密码 */ @Override public ResponseDTO updatePassword(String email, String oldPassword, String newPassword) { Query query = new Query(Criteria.where("email").is(email)); MongoUser user = mongoTemplate.findOne(query, MongoUser.class); if (user == null) { return ResponseDTO.failed("User not found"); } try { if (!user.getPassword().equals(AESUtils.encrypt(oldPassword))) { return ResponseDTO.failed("Invalid old password"); } Update update = new Update(); update.set("password", AESUtils.encrypt(newPassword)); update.set("updateTime", new Date()); mongoTemplate.updateFirst(query, update, MongoUser.class); return ResponseDTO.success("Password updated"); } catch (Exception e) { log.error("Password update failed", e); return ResponseDTO.failed("Encryption error: " + e.getMessage()); } } /** * 删除用户 */ @Override public ResponseDTO deleteUser(String email) { Query query = new Query(Criteria.where("email").is(email)); DeleteResult result = mongoTemplate.remove(query, MongoUser.class); return result.getDeletedCount() > 0 ? ResponseDTO.success("User deleted") : ResponseDTO.failed("User not found"); } private boolean isValidEmail(String email) { return email.contains("@"); } } 在使用上面的service中registry方法插入数据时,报错如下: com.mongodb.MongoCommandException: Command failed with error 2 (BadValue): 'Field 'locale' is invalid in: { locale: "user" }' on server localhost:27017. The full response is {"ok": 0.0, "errmsg": "Field 'locale' is invalid in: { locale: \"user\" }", "code": 2, "codeName": "BadValue"}这是为什么
最新发布
09-03
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值