目录
一、MongoDB简介
二、MongoDB锁机制
三、锁的实践影响
3.1 高并发写入导致的写锁案例
一、MongoDB简介
MongoDB 作为一种非关系型文档数据库,在现代应用中扮演着极其重要的角色,尤其在处理大规模、高并发、灵活数据模型的场景下。MongoDB 具有如下特点:
- MongoDB 拥有丰富的数据模型,而且文档没有固定的模式,这为扩展性提供了便利
- 高可扩展性:MongoDB 设计之初就考虑到了扩展性,支持分片(sharding),可以轻松跨越多台服务器分配数据和处理请求,从而实现水平扩展。这对于处理海量数据等场景只管重要。
- 高性能:MongoDB 支持各种索引,能够加锁数据的查询;采用异步非阻塞 I/O 模型,避免了线程的同步等待,从而实现高效的读写性能;水平扩展能力也能提高性能,通过添加分片可来分散负载,保持高性能;采用预分配数据机制来提高性能,当要写入新数据时,不会等到写入时才去请求磁盘空间,而是提前预留一定空间;同时还支持批量操作,减少交互来提升性能。
- 高可用性:使用副本集来对数据进行冗余保存,当主节点故障时,数据也不会丢失。
MongoDB 作为一个多用户、多线程的 NoSQL 数据库,确保其在高并发访问下数据的一致性和完整性至关重要。如果没有有效的并发控制机制,可能会产生如脏读、不接重复度、幻读等一系列问题。
那如何确保 MongoDB 在并发环境下能够保障数据一致性和完整性呢?那就是 MongoDB 的锁机制。
二、MongoDB锁机制
MongoDB 锁机制是其并发控制的重要组成部分,目的是为了确保多线程多用户访问下数据的完整性和一致性,主要分类两大类:MMAPv1 引擎的锁机制和 WiredTiger 引擎的锁机制。
在使用 MMAPv1 存储引擎的 MongoDB 版本中,全局锁时其主要的并发控制手段。全局锁有两种模式:
- 读锁:允许多个读操作共享,但组织任何写操作
- 写锁:独占锁,一旦获取将组织其他读写操作,直至锁释放
WiredTiger 引擎使用了更细粒度的锁机制,主要为:
- 文档锁:锁定单个文档,允许多个并发读操作,但写操作会互斥。这大大减少了锁竞争,提高了并发写入能力,从而使得在高并发场景下也能保持较好的性能。
- 多版本并发控制(MVCC):WiredTiger 实现了一种 MVCC 机制,为每个事务创建数据的多个版本。这样,读操作可以不受写操作的影响,看到事务开始时的一致性视图,而写操作则在新版本上进行,直到事务提交后才会对外可见。这增强了系统的并发能力,同时保证了事务的隔离性。
- 范围锁:在某些情况下,为了保持数据一致性,WiredTiger可能会锁定一个文档范围,防止其他操作修改该范围内的数据。
- 乐观锁:除了传统的锁机制,WiredTiger还采用了乐观锁策略,尤其在处理读写操作时。乐观锁依赖于文档版本控制,每个文档都有一个内部版本号。写操作前先读取版本号,写入时检查版本号是否改变,若未变则成功,否则重试。这种方式减少了锁的使用,提高了并发效率
三、锁的实践影响
WiredTiger 提供文档级锁,减少了写操作之间的锁竞争,提高了并发性能。支持压缩,能够减少存储空间需求和I/O操作,进而提升读写速度。使用多版本并发控制(MVCC),允许读操作不阻塞写操作,提升了读取性能。支持事务,增强了数据一致性。
但是有更高的内存需求,因为其缓存策略会占用较多内存。在极端并发写入场景下,虽然概率较低,但仍有可能遇到锁竞争导致的性能瓶颈。
MongoDB 使用 WiredTiger 存储引擎时,具备一定的死锁检测和处理能力,旨在减少乃至避免死锁对系统的影响。
WiredTiger 存储引擎具有内置的死锁检测机制。当事务等待资源超过一定时间阈值,引擎会尝试检测是否存在死锁情况。如果检测到死锁,WiredTiger 会选择一个或多个事务进行回滚,通常是那些影响较小或者等待时间较长的事务,从而解除死锁状态。
可以通过以下方式避免死锁
- 尽可能缩短事务运行时间,减少对锁的持有时间,可以降低与其他事务冲突的概率。使用原子操作和批量操作可以减少单独锁的次数和持续时间。
- 设计事务时,尽量按照一致的顺序访问资源,减少循环等待的可能性。例如,如果多个事务总是按照相同的资源访问顺序执行,那么死锁就不会发生。
- 在应用层面实施适当的并发控制策略,如乐观锁或悲观锁的合理使用,以及合理的重试逻辑,可以在遇到锁冲突时优雅地处理,避免直接导致死锁。
3.1 高并发写入导致的写锁案例
在使用MongoDB 时,由于有高并发的写入,突然出现了大量获取链接的报错,具体报错信息如下:
org.springframework.data.mongodb.UncategorizedMongoDbException: Too many operations are already waiting for a connection. Max number of operations (maxWaitQueueSize) of 500 has been exceeded.; nested exception is com.mongodb.MongoWaitQueueFullException: Too many operations are already waiting for a connection. Max number of operations (maxWaitQueueSize) of 500 has been exceeded.
at org.springframework.data.mongodb.core.MongoExceptionTranslator.translateExceptionIfPossible(MongoExceptionTranslator.java:138)
at org.springframework.data.mongodb.core.MongoTemplate.potentiallyConvertRuntimeException(MongoTemplate.java:2902)
at org.springframework.data.mongodb.core.MongoTemplate.execute(MongoTemplate.java:587)
at org.springframework.data.mongodb.core.MongoTemplate.doUpdate(MongoTemplate.java:1636)
at org.springframework.data.mongodb.core.MongoTemplate.upsert(MongoTemplate.java:1570)
通过监控发现是高并发写入引起了写锁,导致某些操作长时间占用写锁,最终获取不到连接导致出现上面的问题。下面来分析下原因。
并发写入操作频繁且锁等待时间较长时,连接可能会被长时间占用,无法及时归还到连接池,导致可用连接耗尽。那如何来解决呢?
我们最后如何处理的呢?其实也很简单,可以采用批量写入来减少锁竞争或锁等待时间,或者采用异步的方式来进行写入操作。
还需要检查下设置的连接池是否合理,可以根据情况适当的调整连接池的大小。
往期经典推荐
Sentinel与Nacos强强联合,构建微服务稳定性基石的重要实践_sentinel和nacos-****博客
云原生:Kubernetes下的Java应用部署实战详解_kubernetes部署java-****博客
全面解读MongoDB高可用、高性能与高可扩展架构-****博客
深入剖析MongoDB集群架构设计_mongodb 架构-****博客
TiDB存储引擎TiKV揭秘-****博客