
Pruebas multiusuario
David Glasser de Meteoro escribió un blog sobre una consulta en MongoDB en la que faltan documentos coincidentes con el que se encontró. Es sencillo reproducir el problema tanto en MongoDB MMAPv1 como en el motor MongoDB WiredTiger. Estas son las conclusiones de su artículo (el énfasis es mío)
Resumiendo...
-
Este problema no afecta a las consultas que no utilizan un índice, como las consultas que sólo buscan un documento por ID.
-
No afecta a las consultas que realizan explícitamente una coincidencia de igualdad de valor único en todos los campos utilizados en la clave del índice.
-
No afecta a las consultas que utilizan índices cuyos campos nunca se modifican tras la inserción original del documento.
-
Pero cualquier otro tipo de consulta a MongoDB puede no incluir todos los documentos coincidentes.
He aquí otra forma de verlo. En MongoDB, si la consulta puede recuperar dos documentos usando un índice secundario (índice sobre algo distinto de _id) cuando se están realizando operaciones concurrentes, los resultados podrían ser erróneos. Este es un escenario común en muchas aplicaciones de bases de datos.
Aquí está la prueba:
-
Crear un contenedor: Cubo, tabla o colección.
-
Cargue los datos con un pequeño conjunto de datos, digamos 300K documentos.
-
Cree un índice sobre el campo que desea filtrar (predicados).
-
En una sesión, actualiza el campo indexado en una sesión y consulta en la otra.
Pruebas de MongoDB
Pasos para reproducir el problema en MongoDB:
-
Instale MongoDB 3.2.
-
Sube a mongod con MMAPv1 o WiredTiger.
-
Cargar datos con tpcc.py
-
python tpcc.py -almacenes 1 -no-ejecutar mongodb
-
Obtener el recuento
> use tpcc
> db.ORDER_LINE.find().count();
299890
-
db.ORDER_LINE.ensureIndex({state:1});
Experimento 1 de MongoDB: Actualización a un valor superior
Configure el campo de estado con el valor aaaaaa y, a continuación, actualice simultáneamente este valor a zzzzzz y consulte el número total de documentos con los dos valores ['aaaaaa','zzzzzz'] que coinciden con el campo. Cuando el valor del campo indexado pasa del valor más bajo (aaaaaa) al más alto (zzzzzz), estas entradas se mueven de un lado al otro del árbol B. Ahora, estamos tratando de ver si el escaneo devuelve un valor duplicado, traducido a un valor mayor de count().
> db.ORDER_LINE.update({OL_DIST_INFO:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 299890, "nUpserted" : 0, "nModified" : 299890 })
> db.ORDER_LINE.find({state:{$in:['aaaaaa','zzzzzz']}}).count();
299890
> db.ORDER_LINE.find({state:{$in:['aaaaaa','zzzzzz']}}).explain();
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "tpcc.ORDER_LINE",
"indexFilterSet" : falso,
"parsedQuery" : {
"state" : {
"$in" : [
"aaaaaa",
"zzzzzz"
]
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"etapa" : "IXSCAN",
"keyPattern" : {
"estado" : 1
},
"indexName" : "state_1",
"isMultiKey" : falso,
"direction" : "forward",
"indexBounds" : {
"estado" : [
"["aaaaaa"", "aaaaaa"]",
"["zzzzzz", "zzzzzz"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "Keshavs-MacBook-Pro.local",
"puerto" : 27017,
"version" : "3.0.2",
"gitVersion" : "6201872043ecbbc0a4cc169b5482dcf385fc464f"
},
"ok" : 1
}
-
Declaración de actualización 1: Actualizar todos los documentos para establecer state = "zzzzzz"
db.ORDER_LINE.update({OL_DIST_INFO:{$gt:""}},
{$set: {estado: "zzzzzz"}}, {multi:true});
-
Declaración de actualización 2: Actualizar todos los documentos para establecer state = "aaaaaa"
db.ORDER_LINE.update({OL_DIST_INFO:{$gt:""}},
{$set: {estado: "aaaaaa"}}, {multi:true});
3. Declaración de recuento: Contar documentos:(estado en ["aaaaaa", "zzzzzz"])
db.ORDER_LINE.find({state:{$in:['aaaaaa','zzzzzz']}}).count();
|
Tiempo |
Sesión 1: Declaración de actualización de temas1 (actualizar estado = "zzzzzz") |
Sesión 2: Emisión continua de la declaración de recuento. |
|
T0 |
Comienza la declaración de actualización |
Recuento = 299890 |
|
T1 |
Continúa la declaración de actualización |
Recuento = 312736 |
|
T2 |
Continúa la declaración de actualización |
Recuento = 312730 |
|
T3 |
Continúa la declaración de actualización |
Recuento = 312778 |
|
T4 |
Continúa la declaración de actualización |
Recuento = 312656 |
|
T4 |
Continúa la declaración de actualización |
Recuento = 313514 |
|
T4 |
Continúa la declaración de actualización |
Recuento = 303116 |
|
T4 |
Declaración de actualización Realizada |
Recuento = 299890 |
Resultado: En este escenario, el índice hace doble recuento de muchos documentos e informa de más de los que realmente tiene.
Causa: Los datos en el nivel de hoja del B-Tree están ordenados. Cuando el B-Tree se actualiza de aaaaaa a zzzzzz, las claves del extremo inferior se mueven al extremo superior. Los escaneos concurrentes no son conscientes de este movimiento. MongoDB no implementa un escaneo estable y cuenta las entradas según van llegando. Así, en un sistema de producción con muchas actualizaciones, podría contar el mismo documento dos veces, tres veces o más. Sólo depende de las operaciones concurrentes.
Experimento 2 de MongoDB: Actualización a un valor inferior
Hagamos la operación inversa para actualizar los datos de 'zzzzzz' a 'aaaaaa'. En este caso, las entradas del índice pasan de un valor más alto a un valor más bajo, lo que hace que el escaneo pase por alto algunos de los documentos calificados, mostrando un recuento insuficiente.
|
Tiempo |
Sesión 1: Declaración de actualización2 (actualizar estado = "aaaaaa") |
Sesión 2: Emisión continua de la declaración de recuento. |
|
T0 |
Comienza la declaración de actualización |
Recuento = 299890 |
|
T1 |
Continúa la declaración de actualización |
Recuento = 299728 |
|
T2 |
Continúa la declaración de actualización |
Recuento = 299750 |
|
T3 |
Continúa la declaración de actualización |
Recuento = 299780 |
|
T4 |
Continúa la declaración de actualización |
Recuento = 299761 |
|
T4 |
Continúa la declaración de actualización |
Recuento = 299777 |
|
T4 |
Continúa la declaración de actualización |
Recuento = 299815 |
|
T4 |
Declaración de actualización Realizada |
Recuento = 299890 |
Resultado: En este escenario, el índice pasa por alto muchos documentos e informa de menos documentos de los que realmente tiene.
Causa: Esto expone el efecto contrario. Cuando las claves con valores zzzzzz se modifican a aaaaaa, los elementos pasan del extremo superior al inferior del B-Tree. De nuevo, como no hay estabilidad en los escaneos, se perderían las claves que se movieran del extremo superior al inferior.
Experimento 3 de MongoDB: ACTUALIZACIONES concurrentes
Dos sesiones actualizan el campo indexado de forma simultánea y continua. En este caso, basándonos en las observaciones anteriores, cada una de las sesiones se encuentra con un problema tanto de sobreconteo como de infraconteo. El resultado nModified varía porque MongoDB sólo informa de las actualizaciones que han cambiado el valor.
Pero el número total de documentos modificados nunca es superior a 299980. Por lo tanto, MongoDB evita actualizar el mismo documento dos veces, manejando así la clásico problema de halloween. Como no tienen un escaneo estable, supongo que manejan esto manteniendo listas de objectIDs actualizados durante esta declaración de actualización múltiple y evitando la actualización si el mismo objeto aparece como un documento calificado.
SESIÓN 1
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 299890, "nUpserted" : 0, "nModified" : 299890 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 303648, "nUpserted" : 0, "nModified" : 12026 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 194732, "nUpserted" : 0, "nModified" : 138784 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 334134, "nUpserted" : 0, "nModified" : 153625 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 184379, "nUpserted" : 0, "nModified" : 146318 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 335613, "nUpserted" : 0, "nModified" : 153403 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 183559, "nUpserted" : 0, "nModified" : 146026 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 335238, "nUpserted" : 0, "nModified" : 149337 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 187815, "nUpserted" : 0, "nModified" : 150696 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 335394, "nUpserted" : 0, "nModified" : 154057 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 188774, "nUpserted" : 0, "nModified" : 153279 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 334408, "nUpserted" : 0, "nModified" : 155970 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 299890, "nUpserted" : 0, "nModified" : 0 })
>
SESIÓN 2:
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 302715, "nUpserted" : 0, "nModified" : 287864 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 195248, "nUpserted" : 0, "nModified" : 161106 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 335526, "nUpserted" : 0, "nModified" : 146265 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 190448, "nUpserted" : 0, "nModified" : 153572 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 336734, "nUpserted" : 0, "nModified" : 146487 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 189321, "nUpserted" : 0, "nModified" : 153864 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 334793, "nUpserted" : 0, "nModified" : 150553 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 186274, "nUpserted" : 0, "nModified" : 149194 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 336576, "nUpserted" : 0, "nModified" : 145833 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({"nMatched" : 183635, "nUpserted" : 0, "nModified" : 146611 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({"nMatched" : 336904, "nUpserted" : 0, "nModified" : 143920 })
>
Pruebas de Couchbase
-
Instale Couchbase 4.5.
-
Cargar datos con tpcc.py
-
python tpcc.py -almacenes 1 -no-ejecutar n1ql
-
Obtener el recuento
> SELECT COUNT(*) FROM LINEA_PEDIDOS;
300023
-
CREATE INDEX i1 ON ORDER_LINE(state);
-
UPDATE ORDER_LINE SET state = 'aaaaaa';
Experimento Couchbase 1: Actualización a un valor superior
Realiza la configuración inicial.
> UPDATE ORDER_LINE SET state = 'aaaaaa' WHERE OL_DIST_INFO > "";
Compruebe que el campo (atributo) Count. state con valores "aaaaaa" es 300023.
> select count(1) a_cnt FROM ORDER_LINE where state = 'aaaaaa'
UNIÓN TODOS
select count(1) z_cnt FROM ORDER_LINE where state = 'zzzzzz';
"resultados": [
{
"a_cnt": 300023
},
{
"z_cnt": 0
}
],
Asegurémonos de que la exploración del índice se produce en la consulta.
EXPLAIN SELECT COUNT(1) AS totalcnt
DE LÍNEA_PEDIDO
WHERE estado = 'aaaaaa' o estado = 'zzzzzz';
"~children": [
{
"#operator": "DistinctScan",
"escanear": {
"#operator": "IndexScan",
"cubiertas": [
"tapa ((LÍNEA DE PEDIDO.estado))”,
" cubierta ((meta(LÍNEA DE PEDIDO).id))”
],
"index": "i2",
"index_id": "665b11a6c36d4136",
"keyspace": "ORDER_LINE",
"namespace": "default",
"spans": [
{
"Rango": {
"Alto": [
""aaaaaa""
],
"Inclusión": 3,
"Bajo": [
""aaaaaa""
]
}
},
{
"Rango": {
"Alto": [
""zzzzzz""
],
"Inclusión": 3,
"Bajo": [
""zzzzzz""
]
}
}
],
"using": "gsi"
}
},
También podemos utilizar UNION ALL de dos predicados separados (state = 'aaaaaa') y (state = 'zzzzzz') para que el índice cuente de forma eficiente.
cbq> explain select count(1) a_cnt
DE LÍNEA_PEDIDO
donde estado = 'aaaaaa'
UNIÓN TODOS
select count(1) z_cnt
DE LÍNEA_PEDIDO
donde estado = 'zzzzzz';
{
"requestID": “ef99e374-48f5-435c-8d54-63d1acb9ad22”,
"signature": "json",
"resultados": [
{
"plan": {
"#operator": "UnionAll",
"niños": [
{
"#operator": "Secuencia",
"~children": [
{
"#operator": "IndexCountScan",
"cubiertas": [
"tapa ((LÍNEA DE PEDIDO.estado))”,
" cubierta ((meta(LÍNEA DE PEDIDO).id))”
],
"index": "i2",
"index_id": "665b11a6c36d4136",
"keyspace": "ORDER_LINE",
"namespace": "default",
"spans": [
{
"Rango": {
"Alto": [
""aaaaaa""
],
"Inclusión": 3,
"Bajo": [
""aaaaaa""
]
}
}
],
"using": "gsi"
},
{
"#operator": "IndexCountProject",
"result_terms": [
{
"as": "a_cnt",
"expr": "count(1)"
}
]
}
]
},
{
"#operator": "Secuencia",
"~children": [
{
"#operator": "IndexCountScan",
"cubiertas": [
"tapa ((LÍNEA DE PEDIDO.estado))”,
" cubierta ((meta(LÍNEA DE PEDIDO).id))”
],
"index": "i2",
"index_id": "665b11a6c36d4136",
"keyspace": "ORDER_LINE",
"namespace": "default",
"spans": [
{
"Rango": {
"Alto": [
""zzzzzz""
],
"Inclusión": 3,
"Bajo": [
""zzzzzz""
]
}
}
],
"using": "gsi"
},
{
"#operator": "IndexCountProject",
"result_terms": [
{
"as": "z_cnt",
"expr": "count(1)"
}
]
}
]
}
]
},
"text": "select count(1) a_cnt FROM ORDER_LINE where state = 'aaaaaa' UNION ALL select count(1) z_cnt FROM ORDER_LINE where state = 'zzzzzz'"
}
],
"estado": "éxito",
"métricas": {
"elapsedTime": "2.62144ms",
"executionTime": "2.597189ms",
"resultCount": 1,
"resultSize": 3902
}
}
Configure el campo de estado con el valor aaaaaa. A continuación, actualice este valor a zzzzzz y consulte simultáneamente el número total de documentos con cualquiera de los dos valores.
Sesión 1: Actualizar el campo de estado al valor zzzzzz
UPDATE ORDER_LINE SET state = 'zzzzzz' WHERE OL_DIST_INFO > "";
{ "mutationCount": 300023 }
Sesión 2: consulta el recuento de 'aaaaaa' y 'zzzzzz' de ORDER_LINE.
|
Tiempo |
Sesión 1: Declaración de actualización de temas1 (actualizar estado = "zzzzzz") |
a_cnt |
z_cnt |
Total |
|
T0 |
Comienza la declaración de actualización |
300023 |
0 |
300023 |
|
T1 |
Continúa la declaración de actualización |
288480 |
11543 |
300023 |
|
T2 |
Continúa la declaración de actualización |
259157 |
40866 |
300023 |
|
T3 |
Continúa la declaración de actualización |
197167 |
102856 |
300023 |
|
T4 |
Continúa la declaración de actualización |
165449 |
134574 |
300023 |
|
T5 |
Continúa la declaración de actualización |
135765 |
164258 |
300023 |
|
T6 |
Continúa la declaración de actualización |
86584 |
213439 |
300023 |
|
T7 |
Declaración de actualización Realizada |
0 |
300023 |
300023 |
Resultado: Las actualizaciones del índice se producen a medida que se actualizan los datos. Cuando los valores pasan de "aaaaaa" a "zzzzzz", no se cuentan dos veces.
Los índices de Couchbase proporcionan escaneos estables haciendo snapshots del índice con una frecuencia regular. Aunque se trata de un esfuerzo de ingeniería considerable, como hemos visto en este problema, proporciona estabilidad en las respuestas incluso en situaciones de concurrencia extrema.
Los datos que recupere la exploración de índices serán los del punto en que se inicie la exploración. Las actualizaciones posteriores que se produzcan simultáneamente no se devolverán. Esto también proporciona otro nivel de estabilidad.
Es importante tener en cuenta que la estabilidad de los escaneos se proporciona para cada escaneo de índice. Los índices toman instantáneas cada 200 milisegundos. Cuando se tiene una consulta de larga ejecución con múltiples escaneos de índices en el mismo o múltiples índices, la consulta podría utilizar múltiples instantáneas. Por lo tanto, diferentes exploraciones de índices en una consulta de larga duración devolverán resultados diferentes. Este es un caso de uso que mejoraremos en una futura versión para utilizar la misma instantánea en todos los escaneos de una misma consulta.
Experimento Couchbase 2: Actualización a un valor inferior
Hagamos la operación inversa para actualizar los datos de 'zzzzzz' a 'aaaaaa'.
Sesión 1: Actualizar el campo de estado al valor aaaaaa
UPDATE ORDER_LINE SET state = 'aaaaaa' WHERE OL_DIST_INFO > "";
{ "mutationCount": 300023 }
Sesión 2: consulta el recuento de 'aaaaaa' y 'zzzzzz' de ORDER_LINE.
|
Tiempo |
Sesión 1: Declaración de actualización de temas1 (actualizar estado = "aaaaaa") |
a_cnt |
z_cnt |
Total |
|
T0 |
Comienza la declaración de actualización |
0 |
300023 |
300023 |
|
T1 |
Continúa la declaración de actualización |
28486 |
271537 |
300023 |
|
T2 |
Continúa la declaración de actualización |
87919 |
212104 |
300023 |
|
T3 |
Continúa la declaración de actualización |
150630 |
149393 |
300023 |
|
T4 |
Continúa la declaración de actualización |
272358 |
27665 |
300023 |
|
T5 |
Continúa la declaración de actualización |
299737 |
286 |
300023 |
|
T6 |
Declaración de actualización Realizada |
0 |
300023 |
300023 |
Experimento Couchbase 3: ACTUALIZACIONES concurrentes
Dos sesiones actualizan el campo indexado de forma concurrente y continua. Los escaneos estables del índice siempre devuelven la lista completa de documentos calificados: 30023 y Couchbase actualiza todos los documentos e informa del recuento de mutaciones como 30023. Una actualización es una actualización independientemente de si el valor antiguo es el mismo que el nuevo valor o no.
Sesión 1:
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
Segunda sesión:
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
Conclusiones
MongoDB:
-
Las consultas de MongoDB perderán documentos si hay actualizaciones concurrentes que muevan los datos de una porción del índice a otra porción del índice (de mayor a menor).
-
Las consultas de MongoDB devolverán el mismo documento varias veces si hay actualizaciones concurrentes que mueven los datos de una parte del índice a otra parte del índice (de menor a mayor).
-
Las actualizaciones múltiples concurrentes en MongoDB se encontrarán con el mismo problema y no actualizarán todos los documentos requeridos, pero evitan actualizar el mismo documento dos veces en una sola sentencia.
-
Cuando desarrolle aplicaciones que utilicen MongoDB, debe diseñar un modelo de datos de forma que sólo seleccione y actualice un documento por cada consulta. Evite las consultas multidocumento en MongoDB ya que devolverá resultados incorrectos cuando haya actualizaciones concurrentes.
Couchbase:
-
Couchbase devuelve el número esperado de documentos calificados incluso cuando hay actualizaciones concurrentes. Los escaneos de índices estables en Couchbase proporcionan la protección a la aplicación que MongoDB no ofrece.
-
Las actualizaciones concurrentes se benefician de escaneos de índices estables y procesan todos los documentos cualificados cuando se emite la declaración de solicitud.
Agradecimientos
Gracias a Sriram Melkote y Deepkaran Salooja del equipo de indexación de Couchbase por su revisión y valiosa aportación al esfuerzo.
Referencias
-
Consistencia sólida de MongoDB: https://www.mongodb.com/nosql-explained
-
Concurrencia en MongoDB: https://docs.mongodb.com/manual/faq/concurrency/
-
Las consultas a MongoDB no siempre devuelven todos los documentos coincidentes: https://engineering.meteor.com/mongodb-queries-dont-always-return-all-matching-documents-654b6594a827#.s9y0yheuv
-
Couchbase: http://www.couchbase.com