一些Trouble Shooting
分片+ 副本集的问题
upsert问题
如果MongoDB使用分片+副本集方式且未明确指定分片键时,不能使用下面的方式调用update
命令。
> db.usergeo.update({userid:200},{$set:{userid:200,gispoint:{latitude:139.2658742,longitude:65.956483},updatetime:"2020-04-14 18:05:51"}}, true, false)
否则会报错。
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 61,
"errmsg" : "upsert { q: { userid: 300.0 }, u: { $set: { userid: 200.0, gispoint: { latitude: 140.0, longitude: 65.0 }, updatetime: \"2016-04-14 18:05:51\" } }, multi: false, upsert: true } does not contain shard key for pattern { _id: \"hashed\" }"
}
})
这是因为upsert
产生了新的_id
,_id
就是shard key
,但是如果query
里没有shard key
,它们不知道要到哪个shard
上执行这个命令,upsert
产生的shard key
可能并不是执行这条命令的shard的。
另外,如果_id
不是shard key
,也是不能成功的,因为没有shard key
,这个upsert
要在哪个shard
上执行呢?不能像普通update
那样给所有的shard
去做,否则可能导致插入多条。
因此,需要明确传递_id
,需要执行下面的语句。
> db.user.update({"_id" : ObjectId("57b3ff556e9bb26890c48888")},{$set:{userid:800,gispoint:{latitude:139.2658742,longitude:65.956483},updatetime:"2016-04-14 18:05:51"}}, true, false)
如果存在_id
为57b3ff556e9bb26890c48888
的记录,则更新,否则插入。
如果明确指定分片键。
> mongos> db.runCommand({"shardcollection":"chebian.user","key":{guid:'hashed'}})
就可以执行下面的update
。
> db.user.update({guid:1},{$set:{guid:1,gispoint:{latitude:139.2658742,longitude:65.956483},updatetime:"2016-04-14 18:05:51"}}, true, false)
geoNear问题
当使用分片副本集时,不能使用$near
来获得附近的位置,而只能使用$geoNear
方式。
db.user.ensureIndex({"gispoint":"2d"});
如果使用2dsphere
索引,spherical
就必须为true
。
db.user.ensureIndex({"gispoint":"2dsphere"});
db.user.aggregate({
"$geoNear":{
"near":{
"type":"Point",
"coordinates":[116.403999, 40.034971]
},
"distanceField":"dist.calculated",
"distanceMultiplier":6378137,
"num":5,
"maxDistance":20000,
"spherical":true
}
});
另外,使用$geoNear
获得的结果里距离的单位,有两种情况需要区分。
spherical
设为false
(默认),距离的单位与坐标的单位保持一致。如果保存的是longitude/latitude
,则dis的单位就是经度(或者纬度,单位是一致的),如果保存的是米,则距离的单位就是米。spherical
设为true
,距离的单位是弧度,想要换算成公里的话,要么在程序里做,要么使用distanceMultiplier
参数来定义转换方式。
所以,对于第一种情况。
如果要保存的坐标是经纬度,要计算的是公里,可设置
distanceMultiplier: 111.12
。如果要计算英里,可将
111.12
换成69.0467669
。
对于第二种情况。
如果要计算公里,可设置
distanceMultiplier: 6378137
。如果要计算英里,则需要把
6378137
换成3963190.591943
。
聚合操作问题
使用spring-data-mongodb
调用aggregate
操作时,可能会报“A pipeline stage specification object must contain exactly one field”
错误,可以使用Command
作为替代方案。
if (query == null) {
query = new BasicDBObject();
query.put("userid", "800");
}
DBObject aggregate = new BasicDBObject("$geoNear",
new BasicDBObject("near",
new BasicDBObject("type", "Point").append("coordinates", new double[] { longitude, latitude })))
.append("distanceMultiplier", Constant.DISTANCE_MULTIPLIER)
.append("distanceField", "dist.calculated").append("query", new BasicDBObject())//.append("num", limit)
.append("maxDistance", distance).append("spherical", true);
List<DBObject> pipeLine = new ArrayList<>();
pipeLine.add(aggregate);
Cursor cursor = user.aggregate(pipeLine, AggregationOptions.builder().build());
List<DBObject> list = new LinkedList<>();
while (cursor.hasNext()) {
list.add(cursor.next());
}
将上面的代码换成下面的就好了。
BasicDBObject geoCmd = new BasicDBObject();
geoCmd.append("geoNear", "user");
geoCmd.append("near", new double[] { longitude, latitude });
geoCmd.append("distanceMultiplier", Constant.DISTANCE_MULTIPLIER);
geoCmd.append("maxDistance", distance / Constant.DISTANCE_MULTIPLIER);
geoCmd.append("spherical", true);
System.out.println(geoCmd);
CommandResult myResults = db.command(geoCmd);
System.out.println(myResults.toString());
Read Timeout问题
WriteConcern
的几种抛出异常的级别参数。
WriteConcern.NONE:没有异常抛出。
WriteConcern.NORMAL:仅抛出网络错误异常,没有服务器错误异常
WriteConcern.SAFE:抛出网络错误异常、服务器错误异常;并等待服务器完成写操作。
WriteConcern.MAJORITY:抛出网络错误异常、服务器错误异常;并等待一个主服务器完成写操作。
WriteConcern.FSYNC_SAFE:抛出网络错误异常、服务器错误异常;写操作等待服务器将数据刷新到磁盘。
WriteConcern.JOURNAL_SAFE:抛出网络错误异常、服务器错误异常;写操作等待服务器提交到磁盘的日志文件。
WriteConcern.REPLICAS_SAFE:抛出网络错误异常、服务器错误异常;等待至少2台服务器完成写操作。
在开发及测试环境中只有一台MongoDB服务器的时候,是不能使用此参数的。
而在生产环境中需要保留,因此在开发及测试环境中需要将spring-context-mongodb.xml
配置文件中的如下内容注释掉。
# write-concern="${mongo.write-concern}"
所以,需要在恰当的地方使用MongoDB的WriteConcern.SAFE
参数。
Spring Mongo配置多个Mongos
由于数据存储使用MongoDB集群,在对外访问的时候,地址是mongos的地址,在使用的过程中没有发现任何问题,配置如下。
<mongo:mongo host="${mongodb.hostname}" port="${mongodb.port}">
<mongo:options connections-per-host="${mongodb.port}"
threads-allowed-to-block-for-connection-multiplier="${mongodb.threads-allowed-to-block-for-connection-multiplier}"
connect-timeout="${mongodb.connect-timeout}"
max-wait-time="${mongodb.max-wait-time}"
auto-connect-retry="${mongodb.auto-connect-retry}"
socket-keep-alive="${mongodb.socket-keep-alive}"
socket-timeout="${mongodb.socket-timeout}"
slave-ok="${mongodb.slave-ok}"
write-number="${mongodb.write-number}"
write-timeout="${mongodb.write-timeout}"
write-fsync="${mongodb.write-fsync}" />
</mongo:mongo>
但是,经过测试几轮性能测试以后,发现在高并发的时候mongos
机器负载过高,而其他存储mongod
的机器负载很小,经过分析后发现以下原因。
mongos
、config server
、mongod
三个进程都部署在同一台机器上。没有考虑使用多个
mongos
来均摊外部请求。
于是,另外部署几个mongos
,使用同一个配置库,问题解决。
<mongo:mongo id="mongo" replica-set="${mongodb.replica-set}">
<mongo:options connections-per-host="${mongodb.port}"
threads-allowed-to-block-for-connection-multiplier="${mongodb.threads-allowed-to-block-for-connection-multiplier}"
connect-timeout="${mongodb.connect-timeout}"
max-wait-time="${mongodb.max-wait-time}"
auto-connect-retry="${mongodb.auto-connect-retry}"
socket-keep-alive="${mongodb.socket-keep-alive}"
socket-timeout="${mongodb.socket-timeout}"
slave-ok="${mongodb.slave-ok}"
write-number="${mongodb.write-number}"
write-timeout="${mongodb.write-timeout}"
write-fsync="${mongodb.write-fsync}" />
</mongo:mongo>
Spring + MongoDB配置问题
组件版本分别如下。
Spring-4.2.2
MongoDB-3.2.5
mongo-java-driver-3.2.2
spring-data-mongodb-1.8.4
必须配置副本集才能起作用,且最好有三台机器作为副本集集群。
将MongoDB的认证方式从MONGODB-CR/3
改为SCRAM-SHA-1/5
(MongoDB3.2.5默认为MONGODB-CR/3)的具体实现方式。
> use admin
switched to db admin
> var schema = db.system.version.findOne({"_id" : "authSchema"})
> schema.currentVersion = 5
3
> db.system.version.save(schema)
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
在需要操作的数据库中添加SCRAM-SHA-1
认证用户。
> use itechthink
switched to db itechthink
> db.createUser({user:'itechthink',pwd:'Lb2016',roles:[{role:'dbOwner',db:'itechthink'}]})
true
write-concern="${mongo.write-concern}
最好配置为REPLICAS_SAFE
。
MongoDB3.0以上版本的认证问题
MongoDB3.0之后新增了一种认证机制(authenticationMechanisms)SCRAM-SHA-1
,并把他设置为默认的方式。而Spring Boot
里默认使用旧的认证机制,这就造成了组件调用时认证通不过的问题。
解决方式一:修改MongoDB认证方式
MongoDB支持如下几种认证机制。
SCRAM-SHA-1
MONGODB-CR
MONGODB-X509
GSSAPI (Kerberos)
PLAIN (LDAP SASL)
把Mongodb的认证方式改变一下自然能解决问题,还可以同时支持多个。
setParameter:
authenticationMechanisms: MONGODB-CR,SCRAM-SHA-1
enableLocalhostAuthBypass: false
logLevel: 4
但是既然MongoDB从3.0开始用SCRAM-SHA-1
作为默认的认证机制,应该是有道理的,比如安全性方面比MONGODB-CR
更好。
下面是具体的解决办法。
- 首先关闭认证,修改
system.version
文档里面的authSchema
版本为5
,即SCRAM-SHA-1
,如果是MONGODB-CR
则应该是3
。
> use admin
switched to db admin
> var schema = db.system.version.findOne({"_id" : "authSchema"})
> schema.currentVersion = 5
5
> db.system.version.save(schema)
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
如果现在开启认证,仍然会提示AuthenticationFailed MONGODB-CR credentials missing in the user document
。
原因是原来创建的用户已经使用了SCRAM-SHA-1
认证方式。
> use admin
switched to db admin
> db.system.users.find()
[...]
{ "_id" : "userdb.myuser", "user" : "myuser", "db" : "userdb", "credentials" : { "SCRAM-SHA-1" : { "iterationCount" : 10000, "salt" : "XXXXXXXXXXXXXXXXXXXXXXXX", "storedKey" : "XXXXXXXXXXXXXXXXXXXXXXXXXXX", "serverKey" : "XXXXXXXXXXXXXXXXXXXXXXXXXXX" } }, "roles" : [ { "role" : "dbOwner", "db" : "userdb" } ] }
解决方式就是删除刚刚创建的用户,重新创建。
> use itechthink
switched to db itechthink
> db.dropUser("itechthink")
true
> db.createUser({user:'itechthink',pwd:'Lb2016',roles:[{role:'dbOwner',db:'itechthink'}]})
然后关闭服务,开启认证,重启服务器,测试连接,一切OK。
解决方式二:修改Spring源码
Spring
连接MongoDB的源码在org.springframework.boot.autoconfigure.mongo.MongoProperties
类的createMongoClient()
方法中。
public MongoClient createMongoClient(MongoClientOptions options) throws UnknownHostException {
try {
if (hasCustomAddress() || hasCustomCredentials()) {
if (options == null) {
options = MongoClientOptions.builder().build();
}
List<MongoCredential> credentials = null;
if (hasCustomCredentials()) {
String database = this.authenticationDatabase == null ? getMongoClientDatabase() : this.authenticationDatabase;
credentials = Arrays.asList(MongoCredential.createMongoCRCredential(
this.username, database, this.password));
}
String host = this.host == null ? "localhost" : this.host;
int port = this.port == null ? DEFAULT_PORT : this.port;
return new MongoClient(Arrays.asList(new ServerAddress(host, port)), credentials, options);
}
// The options and credentials are in the URI
return new MongoClient(new MongoClientURI(this.uri, builder(options)));
} finally {
clearPassword();
}
}
可以看到它是用MongoCredential.createMongoCRCredential
方法来创建的认证信息,并且没有留出任何公开的接口可以改变这一行为。
找到问题所在,解决就非常简单了,使用createScramSha1Credential()
方法即可。
把MongoProperties
类复制一份,然后改代码。
首先创建一个MongoDBConfiguration
类,用于创建MongoClient
实例。
@Configuration
@EnableConfigurationProperties(MongoProperties.class)
public class MongoDBConfiguration {
@Autowired
private MongoProperties properties;
@Autowired(required = false)
private MongoClientOptions options;
private Mongo mongo;
@PreDestroy
public void close() {
if (this.mongo != null) {
this.mongo.close();
}
}
@Bean
public Mongo mongo() throws UnknownHostException {
this.mongo = this.properties.createMongoClient(this.options);
return this.mongo;
}
}
让MongoDBConfiguration
引用自己的写MongoProperties
类就行了。
解决方式三:更改mongo-java-driver
查看MongoDB的官方指南后知道,只需要将Java
驱动降级为2.14.2
就行了。
https://github.com/mongodb/mongo-java-driver/releases。
也就是修改pom.xml
中的内容就行了。
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.2.2</version>
</dependency>
改为下面的就行。
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.14.2</version>
</dependency>
即可认证通过,这是最简单的办法。
感谢支持
更多内容,请移步《超级个体》。