前情提要 :
一套已经正式上线的程序(在生产环境上锤炼了一个多月,期间,程序稳定性,数据准确性性能良好).
因为业务所需,需要修改配置,额外部署一套到新的集群
修改完配置,确认无误之后,部署执行. 然后惨案就开始了~
本篇博客要点如下:
从茫茫多的报错信息截取出一条完整的报错如下:
2019-08-25 17:06:40.700 ERROR 19144 --- [ntainer#0-4-C-1] c.y.service.impl.HisDetailServiceImp : 更新线下交易信息异常, LOG_NO : 786105028046, AC_DT: 20190825
org.springframework.dao.DataIntegrityViolationException: An upsert on a sharded collection must contain the shard key and have the simple collation. Update request: { q: { _id: "ID_20190825_786105028046" }, u: { $set: { TRAN_SOURCE: "03", TRANMONTH: "201908", LOG_NO: "786105028046", TXN_CD: "2080001", SETTLE_AMT_SUM: 109.67, ETL_SOURCE: "03", LAST_UP_TIME: "20190825170640", _id: "ID_20190825_786105028046", TRAN_TYPE: "01", SETTLE_AMT_100: 10967.0, TRANNUM: 1.0, SETTLE_AMT: 109.67, CHAN_TXN_DT: "0824" } }, upsert: true }, shard key pattern: { ORDERBY_ID: 1.0 }; nested exception is com.mongodb.MongoWriteException: An upsert on a sharded collection must contain the shard key and have the simple collation. Update request: { q: { _id: "ID_20190825_786105028046" }, u: { $set: { TRAN_SOURCE: "03", TRANMONTH: "201908", LOG_NO: "786105028046", TXN_CD: "2080001", SETTLE_AMT_SUM: 109.67, ETL_SOURCE: "03", LAST_UP_TIME: "20190825170640", _id: "ID_20190825_786105028046", TRAN_TYPE: "01", SETTLE_AMT_100: 10967.0, TRANNUM: 1.0, SETTLE_AMT: 109.67, CHAN_TXN_DT: "0824" } }, upsert: true }, shard key pattern: { ORDERBY_ID: 1.0 }
at org.springframework.data.mongodb.core.MongoExceptionTranslator.translateExceptionIfPossible(MongoExceptionTranslator.java:112)
at org.springframework.data.mongodb.core.MongoTemplate.potentiallyConvertRuntimeException(MongoTemplate.java:2750)
at org.springframework.data.mongodb.core.MongoTemplate.execute(MongoTemplate.java:537)
at org.springframework.data.mongodb.core.MongoTemplate.doUpdate(MongoTemplate.java:1547)
at org.springframework.data.mongodb.core.MongoTemplate.upsert(MongoTemplate.java:1501)
at com.yspay.service.impl.HisDetailServiceImp.updateOffLineTrade(HisDetailServiceImp.java:249)
at com.yspay.service.impl.HisDetailServiceImp.doSave(HisDetailServiceImp.java:123)
at com.yspay.kaf_listener.HisDetailListener.listen0(HisDetailListener.java:39)
at sun.reflect.GeneratedMethodAccessor77.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:119)
at org.springframework.kafka.listener.adapter.HandlerAdapter.invoke(HandlerAdapter.java:48)
at org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:283)
at org.springframework.kafka.listener.adapter.BatchMessagingMessageListenerAdapter.invoke(BatchMessagingMessageListenerAdapter.java:144)
at org.springframework.kafka.listener.adapter.BatchMessagingMessageListenerAdapter.onMessage(BatchMessagingMessageListenerAdapter.java:138)
at org.springframework.kafka.listener.adapter.BatchMessagingMessageListenerAdapter.onMessage(BatchMessagingMessageListenerAdapter.java:59)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeBatchListener(KafkaMessageListenerContainer.java:984)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeBatchListener(KafkaMessageListenerContainer.java:917)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:900)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:753)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.lang.Thread.run(Thread.java:748)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
从报错信息上面来看,这明显和Mongo的分片键有关
(之前部署的集群因为数据量不是很大,所以集群没有分片.新集群按照我们的业务场景进行了分片)
提到Mongo的分片,首先想到的就是分片键不能被更新
检查代码,发现代码在更新操作有这样的设置:
/*
AC_DT, SORT_ID, ORDERBY_ID几个字段是分片键,不能更新
*/
if (!WideCollectionFields.AC_DT.equals(key) && !WideCollectionFields.SORT_ID.equals(key) && !WideCollectionFields.ORDERBY_ID.equals(key)) {
String value = updateJson.getString(key);// 这里可以根据实际
if (!CommonUtil.isEmpty(value)) {
update.set(key, value);
}
}
证明了程序设置的时候,已经考虑到了该问题
继续分析报错日志:
将报错的更新队列日志,拿出来在mongo客户端下,使用update语法进行更新,发现可以更新成功,
因此确认更新的语法没有问题
看日志里抛出的错误栈, 最后定位到了upsert方法,推测是该方法的使用导致的问题.
之前在使用的过程中曾经大致阅读了这个方法的源码,知道该方法在底层调用了doUpdate方法
doUpdate(collectionName, query, update, entityClass,upsert: true,multi: false);
- 1
而Mongo默认的update语法:
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>, //(默认值false)
multi: <boolean>, //(默认值false)
writeConcern: <document>
}
)
思考了一下两种更新方式的细节差异, 发现只有
{upsert:<boolean>}
这个参数有所区别
于是,将更新操作的api由upsert替换为updateFirst
// upsert api
upsert(Query query, Update update, Class<?> entityClass, String collectionName)
// upsert api底层调用方法
doUpdate(collectionName, query, update, entityClass,upsert: true,multi: false);
// updateFirst api
updateFirst(final Query query, final Update update, Class<?> entityClass, String collectionName)
// updateFirst api底层调用方法
doUpdate(collectionName, query, update, entityClass, false, false);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
重新打包,修改配置,清空有问题的数据,重跑,问题解决
但是,过了一段时间,发现日志里又出现这种报错,但这次的更新条件不是主键(虽然我能确定根据更新条件只能确定一条记录)~
于是我推测,可能是分片键的原因导致的单条更新出现问题,
于是,把代码里面所有更新操作的api替换为:
// updateMuliti api
updateMulti(final Query query, final Update update, Class<?> entityClass, String collectionName)
// updateMulti api底层调用方法
doUpdate(collectionName, query, update, entityClass, false, true);
从版本上线,发现问题,到问题解决,前后总共耗时一个小时左右,因为是在周末进行的操作,
基本上没有产生什么影响,但还是惊出了一身冷汗!
上面解决问题的过程,完全是生产上的应急处理,一半靠经验,一半靠推测~
生产上面的问题解决了之后,又特别针对Mongo的分片键,思考和整理了一下
首先整理一下出现上面问题的原因 :
An upsert on a sharded collection must contain the shard key and have the simple collation
// 对于存在分片键集合的更新操作必须包含分片键,并且具有简单的排列规则
在存在分片的Mongo集合里,使用既不唯一,又不是分片键来标识要更新的文档,并且指定只需要更新一个文档.即:{multi: false}
因为在进行不包含分片键的查询时, Mongodb需要将查询分散到所有分片,因为mongos无法确定文档可能位于哪个分片上.
因此, 如果mongos需要将查询路由到所有分片,那么其中的两个或者多个分片可能会找到与查询条件相匹配的文档,并且会尝试更新,这样就会与设置的{multi: false}的条件相违背.所以会抛出错误
那么, 为什么我的查询条件明明只能确定一个结果出来,它还是会抛出问题呢?
因为,我们知道我们的查询条件在业务上是唯一的,但是mongoDB集群其实是不清楚的, 这种情况下, 可是使用_id来替代,或者是像我一样把所有的api都替换成批量更新
口说无凭,接下来,我通过各种场合的测试来佐证我的观点
首先,按照生产环境的分片键方式在测试环境进行分片,以下是详细命令及执行结果:
以AC_DT,SORT_ID两个字段作为集合的分片键:
sh.enableSharding("XMR_TEST") # 允许数据库分片
db.getCollection('shared_test').ensureIndex({"AC_DT" : 1, "SORT_ID" : 1},{background:true}) # 为分片键创建索引
sh.shardCollection('XMR_TEST.shared_test' ,{"AC_DT" : 1, "SORT_ID" : 1})
#为集合创建分片键
#注意,这个命令必须在admin库下面执行,并且必须带上库名表名的全称
- 1
- 2
- 3
- 4
- 5
db.shared_test.getShardDistribution({}) #查看分片键详情
最后, 从测试环境的数据抽两条有代表性的添加进集合以供测试
以下是,插入两条数据的关键字段信息
1.用主键或者分片键单条更新数据(没问题)
主键更新:分片键更新 :
2.使用唯一(业务上唯一),更新单条数据(报错)
db.getCollection('shared_test').update({"LOG_NO" : "786105028046"}, {"$set": { TRAN_SOURCE: "03", TRANMONTH: "201908", LOG_NO: "786105028046",
TXN_CD: "2080001", SETTLE_AMT_SUM: 109.67, ETL_SOURCE: "03", LAST_UP_TIME: "20190825170640",
TRAN_TYPE: "01", SETTLE_AMT_100: 10967.0, TRANNUM: 1.0, SETTLE_AMT: 109.67, CHAN_TXN_DT: "0824"}})
报错信息:
A single update on a sharded collection must contain an exact match on _id (and have the collection default collation) or contain the shard key (and have the simple collation).
Update request: { q: { LOG_NO: "786105028046" }, u: { $set: { TRAN_SOURCE: "03", TRANMONTH: "201908", LOG_NO: "786105028046", TXN_CD: "2080001", SETTLE_AMT_SUM: 109.67, ETL_SOURCE: "03", LAST_UP_TIME: "20190825170640", TRAN_TYPE: "01", SETTLE_AMT_100: 10967.0, TRANNUM: 1.0, SETTLE_AMT: 109.67, CHAN_TXN_DT: "0824" } }, multi: false, upsert: false },
shard key pattern: { AC_DT: 1.0, SORT_ID: 1.0 }
3.使用分片键(部分),但数据唯一,更新单条数据(报错)
db.getCollection('shared_test').update({AC_DT:'20190323'}, {"$set": { TRAN_SOURCE: "03", TRANMONTH: "201908"}})
- 1
这里分片键组成是AC_DT,SORT_ID,但是我设置条件只设置分片键里面的一个字段
同时, AC_DT为20190323的数据在数据库里面只有一条,同样会报错
报错信息类似上面
A single update on a sharded collection must contain an exact match on _id (and have the collection default collation) or contain the shard key (and have the simple collation).
- 1
通过上面的试验证明了 :
- 在有分片键的存在的集合中,必须根据分片键或者是主键这种Mongos可以确定唯一的方式来进行单条更新
- 如果根据分片键进行更新,分片键要具有一定的排列规则
4.使用主键(报错)或者分片键进行单条更新(没问题),但是 设置{upsert:true}
使用分片键进行更新:使用主键进行更新:
db.getCollection('shared_test').update({"_id" : "ID_20190501_304318017251"},
{"$set": { TRAN_SOURCE: "03", TRANMONTH: "201908", LOG_NO: "786105028046"}},{upsert:true})
- 1
- 2
报错信息如下:
// 对于存在分片键集合的更新操作必须包含分片键,并且具有简单的排列规则
An upsert on a sharded collection must contain the shard key and have the simple collation
- 我们注意到, 使用上述的方法进行更新,
实际上就相当于我生产问题报错时使用的upsert api,也就是说如果设置,{upsert:true}
在进行单条更新的时候,必须根据分片键进行更新!
那么到这里,读者可能会产生疑问?
同样是使用主键进行更新(保证唯一性),仅仅是一个参数的区别,为什么这里就失败了呢?
原因如下:
这是因为如果没有shard key,mongos既不能在所有shard实例上执行这条语句(可能会导致每个shard都插入数据),
也无法选择在某个shard上执行这条语句,于是出错了
虽然mongos也知道 _id是唯一的,但是_id不是分片键,它不清楚_id落在哪个分片上
- 1
- 2
- 3
- 4
5.批量更新(更新内容包括分片键)
设置分片键的值和数据库本身的值一致(没问题):设置分片键的值和数据库现有的值不一致(报错)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190902183902472.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MDg2MTcwNw==,size_16,color_FFFFFF,t_70
"After applying the update to the document {AC_DT: \"20190323\" , ...}, the (immutable) field 'AC_DT' was found to have been altered to AC_DT: \"201903234\""
- 1
报错信息的大意是 :
在尝试将该文档AC_DT的值变更为 : 201903234的时候发现, 该文档的AC_DT的值已经被设置为20190323
使用分片键作为查询条件更新分片键(报错)
综上所述 : 分片键的值一经设定,就不能随意更改!
6.使用不唯一的查询条件在有分片的集合里进行批量更新(成功)
通过上面,我们能够知道: 我插入的两条数据BUSI_SN都为空,所以查询条件对应于多条记录,
进行批量更新时,没有问题
经过生产和测试得到如下结论,
对于设置有分片键的集合存在如下特性:
1. Mongo集合的分片键字段值一经设置,不可变更!
2. 对于有分片键的集合,更新操作(无论是单条,还是批量)优先使用分片键的排列组合作为更新条件
3. 如果因业务特性,不能使用分片键作为更新条件, 并且要求{upsert:true}的场景,必须使用批量更新操作
4. 对于mongos能够确定唯一记录的(主键),可以使用update等单条更新动作进行更新,否则必须使用批量更新操作