MongoDB使用中问题汇总
如何集成MongoDB驱动包
推荐使用Maven管理包依赖关系:
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.13.2</version>
</dependency>
如何连接
主要有两种连接方式
- 单机直连
- Replica Set连接,自动发现Primary主机,在多集群情况下强烈推荐使用这种连接方式
示例代码
//单机直连
MongoClient mongoClient = new MongoClient( "localhost" , 27017 );
//Replica Set连接
MongoClientOptions options = MongoClientOptions.builder().autoConnectRetry(true).connectTimeout(60000).build();
MongoCredential credential = MongoCredential.createMongoCRCredential("username", "dbname", "password".toCharArray());
MongoClient mongoClient = new MongoClient(
Arrays.asList(
new ServerAddress("mongoserver1", 34001),
new ServerAddress("mongoserver2", 34001),
new ServerAddress("mongoserver3", 34001)
), Arrays.asList(credential), options);
//Replica Set连接 uri写法
String connectionString = "mongodb://username:password@mongoserver1:34001,mongoserver2:34001,mongoserver3:34001/dbname?AutoConnectRetry=true";
MongoClientURI mongoClientURI = new MongoClientURI(connectionString);
MongoClient mongoClient = new MongoClient(mongoClientURI);
//spring boot 请在properties里边使用uri方式进行连接
spring.data.mongodb.uri=mongodb://username:password@mongoserver1:34001,mongoserver2:34001,mongoserver3:34001/dbname?AutoConnectRetry=true
spring.data.mongodb.repositories.enabled=true
MongoDB Update的正确用法
通常一个文档只会有一小部分需要更新,如果我们把新文档做为update方法的参数显得很啰嗦很麻烦,特别是文档比较复杂的时候。而利用原子的更新修改器可以使得这种部分的更新极为方便高效。
更新修改器是种特殊的键,用来指定复杂的更新操作,比如调整,增加或者删除键,还可能是操作数组或者内嵌文档
$set用来指定一个键的值.如果这个键存在,就修改它;不存在,就创建它。
> db.name.find()
{ "_id" : ObjectId("505a5925f67c1b9a341caefb"), "fname" : "jeff", "lname" : "jiang" }
> db.name.update({"_id" : ObjectId("505a5925f67c1b9a341caefb")},{$set:{"fname" : "jeffery"}})
> db.name.find()
{ "_id" : ObjectId("505a5925f67c1b9a341caefb"), "fname" : "jeffery", "lname" : "jiang" }
# 可以看到,原文档的"fname"是存在的,所以$set修改器只修改了它的值("jeff"-->"jeffery")
> db.name.update({"_id" : ObjectId("505a5925f67c1b9a341caefb")},{$set:{age:23}})
> db.name.find()
{ "_id" : ObjectId("505a5925f67c1b9a341caefb"), "age" : 23, "fname" : "jeffery", "lname" : "jiang" }
示例代码
db.getCollection("restaurants").updateOne(new Document("name", "Juni"), new Document("$set", new Document("cuisine", "American (New)")) .append("$currentDate", new Document("lastModified", true)))
如何开启读写分离
默认情况下驱动是从Replica Set 集群中的 Primary 上进行读写的,应用可以在读多写少的场景下开启读写分离,提高效率。
这里介绍下 readPreference 这个参数:###
- primary
主节点,默认模式,读操作只在主节点,如果主节点不可用,报错或者抛出异常。 - primaryPreferred
首选主节点,大多情况下读操作在主节点,如果主节点不可用,如故障转移,读操作在从节点。 - secondary
从节点,读操作只在从节点, 如果从节点不可用,报错或者抛出异常。 - secondaryPreferred
首选从节点,大多情况下读操作在从节点,特殊情况(如单主节点架构)读操作在主节点。 - nearest
最邻近节点,读操作在最邻近的成员,可能是主节点或者从节点
示例代码
//uri写法
mongodb://username:password@mongoserver1:34001,mongoserver2:34001,mongoserver3:34001/dbname?AutoConnectRetry=true&readPreference=secondaryPreferred
//java写法
MongoClientOptions options = MongoClientOptions.builder().readPreference(ReadPreference.secondaryPreferred()).build();
MongoClient mongoClient = new MongoClient(
Arrays.asList(
new ServerAddress("mongoserver1", 34001),
new ServerAddress("mongoserver2", 34001),
new ServerAddress("mongoserver3", 34001)
), Arrays.asList(credential), options);
在Mongodb中最多能创建多少集合?
默认情况下,MongoDB 的每个数据库的命名空间保存在一个 16MB 的 .ns 文件中,平均每个命名占用约 628 字节,也即整个数据库的命名空间的上限约为 24000。
每一个集合、索引都将占用一个命名空间。所以,如果每个集合有一个索引(比如默认的 _id 索引),那么最多可以创建 12000 个集合。如果索引数更多,则可创建的集合数就更少了。同时,如果集合数太多,一些操作也会变慢。甚至使得MongoDB集群无法服务的情况发生!
MongoDB有传统数据库的事务和事务回滚么?
没有,请不要把它当成关系型数据库来使用,对于MongoDB集群来说,默认情况下数据也不是强一致性的,而是最终一致性。如果对数据一致性比较敏感建议更改WriteConcern级别,但后果是降低了性能,请酌情考虑。
MongoDB有命名规范么?
- 不能是空字符串
- 不能含有.、''、*、/、\、<、>、:、?、$、\0。建议只使用ASCII码中字母和数字
- 数据库名区分大小写
- 数据库名长度最多为64字节
- 集合名不能包含\0字符,这个字符表示集合名的结束
- 集合名不能是空字符串""
- 集合名不能使用系统集合的保留前缀"system."
- 集名名中不建议包含字符'$',虽然很多驱动程序可以支持包含此字符的集合名
MongoDB有系统保留库名么?
- admin
- local
- config
MongoDB有连接池么?
MongoDB驱动中其实已经是一个现成的连接池了,而且线程安全。这个内置的连接池默认初始了100个连接,每一个操作(增删改查等)都会获取一个连接,执行操作后释放连接。
【题外话】请务必记得关闭资源,并且设置合理的池子连接数和超时时间。
内置连接池有多个重要参数,分别是:###
- connectionsPerHost:每个主机答应的连接数(每个主机的连接池大小),当连接池被用光时,会被阻塞住,默认值为100
- threadsAllowedToBlockForConnectionMultiplier:线程队列数,它和上面connectionsPerHost值相乘的结果就是线程队列最大值。如果连接线程排满了队列就会抛出“Out of semaphores to get db”错误,默认值为5,则最多有500个线程可以等待获取连接
- maxWaitTime: 被阻塞线程从连接池获取连接的最长等待时间(ms)。默认值为120,000
- connectTimeout:在建立(打开)套接字连接时的超时时间(ms)。默认值为10,000
- socketTimeout:套接字超时时间(ms)。默认值为0,无限制(infinite)
- autoConnectRetry:这个控制是否在连接时,会自动重试,2.13驱动已经【废弃】,请使用connectTimeout代替它
连接池的MaximumPoolSize要有个合理值,否则这个值数据量的连接都被占用,后面再有新的连接创建时就要等待了,而不能超出池上限新建连接。除此之外还要设置合理的连接等待,连接超时时间,以防止一个连接占用时间过长,影响其它连接请求。
connectTimeout 和 socketTimeout 的区别:###
一次完整的请求包括三个阶段:
- 建立连接
- 数据传输
- 断开连接
如果与服务器(这里指数据库)请求建立连接的时间超过ConnectTimeout,就会抛 ConnectionTimeOutException,即服务器连接超时,没有在规定的时间内建立连接。
如果与服务器连接成功,就开始数据传输了。
如果服务器处理数据用时过长,超过了SocketTimeOut,就会抛出SocketTimeOutExceptin,即服务器响应超时,服务器没有在规定的时间内返回给客户端数据。
所以这该死的超时该怎么配?
这里有一份国外写的关于超时的建议:
http://blog.mongolab.com/2013/10/do-you-want-a-timeout/
上文给出的通常情况下:connectTimeout=5000,socketTimeout=0
附录
http://api.mongodb.org/java/2.13/com/mongodb/MongoClientOptions.Builder.html
http://api.mongodb.org/java/2.13/com/mongodb/MongoClientURI.html
关于WriteConcern
MongoDB提供了一个配置参数:write concern 来让用户自己衡量性能和写安全。分布式数据库中这样的参数比较常见,记得Cassandra中也有一个类似参数,不过那个好像是要写入几个节点返回成功。其实道理都一样分布式的集群环境考虑到性能因素不能确保每个成员都写入后在返回成功,所以只能交给用户根据实际场景衡量。
- Unacknowledged
这个级别也属于比较低的级别,以前这个级别是驱动配置的默认级别,不过后来调整成Acknowledged级别。在这个级别下,这个驱动会根据当前系统的网络配置进行网络问题的检测,不等待Mongd的返回。代码测试:本地网络问题是否有异常?本地网络无问题是远程server问题是否异常? - Acknowledged
这个级别算是中等级别的配置,这个级别能够拿到mongod的返回信息:dupkey Error,以及一些其他的问题。现在这个级别是驱动的默认级别,估计是10gen公司发现好多人评价Mongodb不靠谱后改的。一般系统这个级别也就够用了。由于默认级别是Acknowledged,内部用getLastError方法检查是否写入成功的时候是也不用设置任何参数,对与Replset来说可以在配置中进行getLastErrorDefaults的配置,如果没有的话默认则是Master收到就ok。 - Journaled
等到操作记录到Journal Log中才返回操作结果,也就是下一次JournaledLog提交。这种情况可以容忍服务器突然宕机,断电等意外的恢复。出去上边的配置还要在启动mongod的时候加上journaling 参数确保可以使用。commitlog提交间隔时间是可以配置的,单磁盘设备(physical volume, RAID device, or LVM volume)每100ms提交一次,和数据文件刷出相同频率,日志和数据分开磁盘设备的30ms提交一次。在插入数据是如果使用{j:true}则会缩短到已配置的默认设置1/3的时间。 - Replica Acknowledged
在副本集中如果w设置为2的话则至少已经吸入到一个secondary中,我猜测写入secondary这个级别是Acknowledged级别,majority是多个secondary已经写入。如果手贱设置w参数大于replset中需要复制的secondarys的话,操作就一直等待直到达到已写入数据的服务器数量符合要求,也可以设置timeout值来指明最长等待时间。{ getLastError: 1, w: 2, wtimeout:5000 }
附录
http://my.oschina.net/u/217548/blog/195995
http://docs.mongodb.org/manual/core/write-concern/
MongoDB的锁机制
MongoDB的锁机制和一般关系数据库如 MySQL(InnoDB), Oracle 有很大的差异,InnoDB 和 Oracle 能提供行级粒度锁,而 MongoDB v2 只能提供库级粒度锁,这意味着当 MongoDB 一个写锁处于占用状态时,其它的读写操作都得干等。
初看起来库级锁在大并发环境下有严重的问题,但是 MongoDB 依然能够保持大并发量和高性能,这是因为 MongoDB 的锁粒度虽然很粗放,但是在锁处理机制和关系数据库锁有很大差异,主要表现在:
- MongoDB 没有完整事务支持,操作原子性只到单个 document 级别,所以通常操作粒度比较小;
- MongoDB 锁实际占用时间是内存数据计算和变更时间,通常很快;
- MongoDB 锁有一种临时放弃机制,当出现需要等待慢速 IO 读写数据时,可以先临时放弃,等 IO 完成之后再重新获取锁。
通常不出问题不等于没有问题,如果数据操作不当,依然会导致长时间占用写锁,比如下面提到的前台建索引操作,当出现这种情况的时候,整个数据库就处于完全阻塞状态,无法进行任何读写操作,情况十分严重。
解决问题的方法,尽量避免长时间占用写锁操作,如果有一些集合操作实在难以避免,可以考虑把这个集合放到一个单独的 MongoDB 库里,因为 MongoDB 不同库锁是相互隔离的,分离集合可以避免某一个集合操作引发全局阻塞问题。
建索引导致数据库阻塞
上面提到了 MongoDB 库级锁的问题,建索引就是一个容易引起长时间写锁的问题,MongoDB 在前台建索引时需要占用一个写锁(而且不会临时放弃),如果集合的数据量很大,建索引通常要花比较长时间,特别容易引起问题。
解决的方法很简单,MongoDB 提供了两种建索引的访问,一种是 background 方式,不需要长时间占用写锁,另一种是非 background 方式,需要长时间占用锁。使用 background 方式就可以解决问题。
例如,为超大表 posts 建立索引
//千万不用使用
db.posts.ensureIndex({user_id: 1})
//而应该使用
db.posts.ensureIndex({user_id: 1}, {background: 1})
哪些操作会对数据库产生锁
操作 | 锁类型 |
---|---|
Issue a query | Read lock |
Get more data from a cursor | Read lock |
Insert data | Write lock |
Remove data | Write lock |
Update data | Write lock |
Map-reduce | Read lock and write lock, unless operations are specified as non-atomic. Portions of map-reduce jobs can run concurrently. |
Create an index | Building an index in the foreground, which is the default, locks the database for extended periods of time. |
db.eval() | Write lock. db.eval() blocks all other JavaScript processes. |
eval | Write lock. If used with the nolock lock option, the eval option does not take a write lock and cannot write data to the database. |
aggregate() | Read lock |
附录
https://ruby-china.org/topics/20128
http://docs.mongodb.org/v2.6/faq/concurrency/
http://docs.mongodb.org/v2.6/reference/method/db.collection.ensureIndex/#db.collection.ensureIndex