How to already feel transactions in MongoDB
In the summer of 2018 (that is, right now, at the time of this writing), an incredible thing happened - honest ACID transactions were delivered to MongoDB . With the release of the fourth version of this document-oriented DBMS , it can be used for slightly more serious applications.
For those who are in the tank, in a nutshell: transactions allow us to make a series of changes in several documents and save them at once, or cancel all changes made as part of a transaction at once, if something went wrong, or the application failed .
Unfortunately, the developer is not so easy to use this super feature. Below I will tell you why, and what to do about it.
If you open the DBMS documentation on the Transactions section , we can see the following remark:
Multi-document transactions are available for replica sets only. Transactions for sharded clusters are scheduled for MongoDB 4.2
This tells us that a simple MongoDB server does not support transactions, only a cluster in the replica set mode . Support in sharded clusters will also be later, in version 4.2.
At the same time, a normal server will allow us to start a transaction, save it, or cancel it, but nothing can be done within it, something like this will be displayed:
WriteCommandError({
"ok" : 0,
"errmsg" : "Transaction numbers are only allowed on a replica set member or mongos",
"code" : 20,
"codeName" : "IllegalOperation"
})
Fortunately, everyone can run a MongoDB cluster consisting of one server. On my own machines on which I develop, I launch all the DBMS in docker containers. For example, starting a regular MongoDB server looks like this:
docker run -v ~/mongo/:/data/db --name mongo --restart=always -p 27017:27017 -d mongo mongod --smallfiles
Let's sort the startup keys:
- -v ~ / mongo /: / data / db means to mount the local ~ / mongo / directory in / data / db of the container, so the database itself will be stored on the host machine, which will allow us to delete the running container, update versions, etc. d. with the preservation of our data;
- --name mongo sets the name of the container;
- --restart = always says that for any service crashes in the container, it should be restarted, as well as, start the container after the operating system is loaded;
- -p 27017: 27017 "pushes" the port to the host machine;
- -d indicates that the container should be started as a daemon;
- mongo - the name of the image to run the container;
- mongod - smallfiles - the command to start the service in the container.
How to run a simple server, I just cited for reference. Now let's figure out what needs to be done to start a server that supports transactions.
First of all, you should create a new network inside the docker, in which all the servers of our cluster will work. Yes, I wrote above that the server will be one, but the network must be created, otherwise it will not work.
docker network create mongo-cluster
Next, in the container startup parameters, you need to specify the use of the new network - net mongo-cluster , and also to transfer the parameter to the server, to work in the replica set mode : - replSet rs0 . Also, I intentionally lowered the key --restart = always , since I don’t always use MongoDB at work now and I don’t want it to start with the operating system.
docker run -v ~/mongo/:/data/db --name mongo -p 27017:27017 -d mongo mongod --smallfiles --replSet rs0
Great, the container is running, which we can verify by running the docker ps command and seeing something like this:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2292d7e0778b mongo "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:27017->27017/tcp mongo
Next, we need to initialize the cluster, to do this, enter the console of the running server, create the configuration of our cluster and perform the initialization:
docker exec -it mongo mongo
# output omited #
> config = {
"_id" : "rs0",
"members" : [
{
"_id" : 0,
"host" : "mongo:27017"
}
]
}
> rs.initiate(config)
{
"ok" : 1,
"operationTime" : Timestamp(1531248932, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1531248932, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
rs0:SECONDARY>
rs0:PRIMARY>
Done! We got a cluster from one MongoDB server. Now you can check that everything works as expected.
rs0:PRIMARY> session = db.getMongo().startSession()
session { "id" : UUID("7eb81006-983f-4398-adc7-5ed23e027377") }
rs0:PRIMARY> database = session.getDatabase("test")
test
rs0:PRIMARY> // Создадим несколько документов
rs0:PRIMARY> database.col.insert({name: "1"})
WriteResult({ "nInserted" : 1 })
rs0:PRIMARY> database.col.insert({name: "2"})
WriteResult({ "nInserted" : 1 })
rs0:PRIMARY> database.col.insert({name: "3"})
WriteResult({ "nInserted" : 1 })
rs0:PRIMARY> database.col.insert({name: "4"})
WriteResult({ "nInserted" : 1 })
rs0:PRIMARY> // Посмотрим, что у нас получилось
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "1" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "4" }
rs0:PRIMARY> // Начинаем транзакцию
rs0:PRIMARY> session.startTransaction()
rs0:PRIMARY> // Изменим один документ
rs0:PRIMARY> database.col.update({name: "4"}, {name: "44"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
rs0:PRIMARY> // Проверим изменения
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "1" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "44" }
rs0:PRIMARY> // Можно открыть соседний терминал и убедиться в другой сесии, что документ выглядит по-прежнему:
rs0:PRIMARY> // { "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "4" }
rs0:PRIMARY> // Сохраняем изменения
rs0:PRIMARY> session.commitTransaction()
rs0:PRIMARY> // Проверяем результат
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "1" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "44" }
rs0:PRIMARY> // Попробуем изменить несколько документов
rs0:PRIMARY> session.startTransaction()
rs0:PRIMARY> database.col.update({name: "44"}, {name: "42"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "1" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "42" }
rs0:PRIMARY> database.col.update({name: "1"}, {name: "21"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "21" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "42" }
rs0:PRIMARY> session.commitTransaction()
rs0:PRIMARY> // Проверяем результат
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "21" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "42" }
rs0:PRIMARY> // А теперь убедимся, что работает отмена изменений
rs0:PRIMARY> session.startTransaction()
rs0:PRIMARY> database.col.update({name: "21"}, {name: "1"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "1" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "42" }
rs0:PRIMARY> // Отменим изменения
rs0:PRIMARY> session.abortTransaction()
rs0:PRIMARY> database.col.find({})
{ "_id" : ObjectId("5b45026edc396f534f11952f"), "name" : "21" }
{ "_id" : ObjectId("5b450272dc396f534f119530"), "name" : "2" }
{ "_id" : ObjectId("5b450274dc396f534f119531"), "name" : "3" }
{ "_id" : ObjectId("5b450276dc396f534f119532"), "name" : "42" }
rs0:PRIMARY> // Отлично! Данные вернулись в прежнее состояние!
rs0:PRIMARY>
Thus, without straining at all, you can try Mongov transactions right now without starting a multi-server cluster. I advise you to look into the documentation and read about the limitations of transactions. For example, that the transactions "live" no more than 1 minute, if you do not have time to save the changes, they will be canceled.
PS: the purpose of this article is not to learn how to use a docker or work with Monga, but just a quick way to try new tools of this interesting DBMS.