MongoDB-ს შეუძლია მრავალდოკუმენტიანი ACID ტრანზაქციების შესრულება, მაგრამ მხოლოდ replica set-ზე ან შარდირებულ კლასტერზე. ჩვეულებრივი standalone mongod მათ უარყოფს შეცდომით Transaction numbers are only allowed on a replica set member or mongos. ერთ მანქანაზე გამოსავალია ერთწევრიანი replica set: ეს არის ერთი ხაზი კონფიგურაციაში და ერთი ერთჯერადი ბრძანება, რის შემდეგაც გექნება ტრანზაქციები, change streams და იგივე კოდის გზა, რომელსაც მოგვიანებით პროდაქშენში გამოიყენებ. მეორე პასუხი, ხშირად უკეთესი, ისაა, რომ მონაცემები ისე დააპროექტო, რომ ტრანზაქცია საერთოდ არ დაგჭირდეს: ერთ დოკუმენტში ჩაწერა ყოველთვის ატომურია, ხოლო კარგად აგებული დოკუმენტი ერთ ადგილას ინახავს ყველაფერს, რაც ერთად უნდა შეიცვალოს.
ეს სტატია განმარტავს, რა სჭირდება ტრანზაქციას, რას აკეთებს მის მაგივრად standalone სერვერი, როგორ გადააქციო ერთი კვანძი ერთწევრიან replica set-ად, რომელი ლიმიტები გაგაჩერებს (60 წამი ნაგულისხმევი სიცოცხლის ხანგრძლივობა, ჩაწერის კონფლიქტები, რა არ შეიძლება ტრანზაქციის შიგნით) და რა შაბლონები არსებობს იმ შემთხვევებისთვის, როცა ტრანზაქციის გამოყენება არ შეგიძლია ან არ გინდა. ვერსიებს იქ ვუთითებთ, სადაც ეს მნიშვნელოვანია; გადაამოწმე სახელმძღვანელო შენი სერვერის ვერსიისთვის, რადგან ტრანზაქციების ქცევა 4.0, 4.2, 4.4 და 5.0 ვერსიებს შორის შეიცვალა.
რა სჭირდება MongoDB-ის ტრანზაქციას#
ტრანზაქცია არის კითხვებისა და ჩაწერების ჯგუფი, რომელიც ან მთლიანად სრულდება, ან საერთოდ არ სრულდება, და სხვა კლიენტები მას ნახევრად დასრულებულს არასდროს ხედავენ. ერთი დოკუმენტისთვის MongoDB-ს ეს გარანტია ყოველთვის ჰქონდა. მრავალდოკუმენტიანი ტრანზაქციები გამოჩნდა 4.0 ვერსიაში replica set-ებისთვის და 4.2-ში შარდირებული კლასტერებისთვის. ტრანზაქციის დასაწყებად სამი რამ უნდა სრულდებოდეს:
- replica set ან შარდირებული კლასტერი. ტრანზაქციების მექანიზმი წერს oplog-ში, რეპლიკაციის ჟურნალში, და მისი დროის ნიშნულებით ყოველ ტრანზაქციას აძლევს თანმიმდევრულ სნეპშოტს. standalone სერვერს oplog არ აქვს.
- სესია. ყოველი ტრანზაქცია კლიენტის სესიის შიგნით ცხოვრობს. დრაივერები ამის უმეტესობას მალავენ, მაგრამ
startSession()ყოველ მაგალითში დაინახავ. - გონივრული დრო. ტრანზაქცია სიცოცხლის ლიმიტის ფარგლებში უნდა დასრულდეს, ნაგულისხმევად 60 წამი, და პრაქტიკაში მილიწამებში უნდა ეტეოდეს.
ჩვეულებრივი ფორმა არის მოკლე ფუნქცია, რომელიც დაკავშირებულ მონაცემებს ერთად ცვლის: ერთი ანგარიშიდან ჩამოწერა და მეორეზე ჩარიცხვა, მარაგის შემცირება და შეკვეთის ჩამატება, ამოცანის აღებულად მონიშვნა და აუდიტის ჩანაწერის დაწერა. თუ ორი კოლექცია უნდა შეთანხმდეს, ტრანზაქცია ყველაზე პირდაპირი ინსტრუმენტია. ფასი არის დამატებითი დაყოვნება, სნეპშოტისთვის დაკავებული დამატებითი მეხსიერება და განმეორების ლოგიკა აპლიკაციაში.
ტრანზაქციებს თავისი ლექსიკონიც აქვს, რომლის ცოდნაც გამოგადგება:
| ტერმინი | მნიშვნელობა |
|---|---|
readConcern: "snapshot" | შიგნით ყველა კითხვა ერთ თანმიმდევრულ დროის წერტილს ხედავს |
writeConcern: "majority" | commit ელოდება, სანამ ჩაწერა უმრავლესობაზე საიმედოდ შეინახება |
TransientTransactionError | მთელი ტრანზაქციის თავიდან გამეორება შეიძლება |
რატომ ამბობს standalone კვანძი უარს#
standalone mongod არის სერვერი, რომელიც რეპლიკაციის რაიმე პარამეტრის გარეშეა გაშვებული. ის ინახავს მონაცემებს და აჟურნალებს ჩაწერებს, სხვა არაფერს. oplog-ს არ აწარმოებს, primary და secondary ცნებები არ აქვს, ამიტომ მრავალდოკუმენტიან ტრანზაქციას არაფერზე ამყარებს. სცადე დაწყება და ამას მიიღებ:
MongoServerError: Transaction numbers are only allowed on a replica set member or mongosშეცდომა მოდის ტრანზაქციის შიგნით პირველი ოპერაციიდან და არა თვითონ startTransaction()-დან, ამიტომ აპლიკაციის სტეკში ხშირად გაუგებარ ადგილას ჩნდება. ეს არის კოდი 20, IllegalOperation. იგივე შეზღუდვა მოქმედებს change streams-ზეც: collection.watch()-საც oplog სჭირდება და standalone-ზე მსგავსი შეტყობინებით ვარდება.
ორი რამ უნდა გაირკვეს. პირველი, ერთი დოკუმენტის ჩაწერები standalone სერვერზეც ატომურია. updateOne, რომელიც ერთი დოკუმენტის ხუთ ველს ცვლის, ან ხუთივეს ცვლის, ან არცერთს; მკითხველი არასდროს ნახავს დოკუმენტს, სადაც მხოლოდ სამია შეცვლილი. მეორე, standalone-ში არაფერია გატეხილი. ეს სერვერის უფრო მარტივი რეჟიმია, რომელიც რეპლიკაციის შესაძლებლობებს ცვლის იმაზე, რომ ნაკლები გაქვს სამართავი. ბევრი საწარმოო დატვირთვა მას იყენებს, ჩვეულებრივ რეგულარულ dump-ებთან ერთად, როგორც აღწერილია სტატიაში mongodump და mongorestore.
მიერთების სტრიქონები ამ განსხვავებას ასახავს: standalone არის ერთი ჰოსტი replicaSet პარამეტრის გარეშე, replica set კი თავის სიმრავლეს სახელს არქმევს. დეტალები არის სტატიებში MongoDB-ის მიერთების სტრიქონები და URI ფორმატის ცნობარი.
ერთკვანძიანი replica set#
ერთი წევრის მქონე replica set სრულიად ლეგიტიმური კონფიგურაციაა. ის საკუთარ თავს ირჩევს primary-ად, აწარმოებს oplog-ს, მხარს უჭერს ტრანზაქციებსა და change streams-ს და იქცევა როგორც უფრო დიდი სიმრავლე, რომელიც ჯერ არ გაზრდილა. რასაც არ გაძლევს, არის დუბლირება: ერთი წევრით არ არის failover და არც მეორე ასლი. w: "majority" ერთწევრიან სიმრავლეზე ნიშნავს „ამ კვანძმა დაადასტურა“, ანუ იგივე საიმედოობა, რაც ჟურნალირებულ standalone-ს აქვს, და მეტი არაფერი.
გადაყვანა ერთნაირია ახალი სერვერისთვისაც და არსებული standalone-ისთვისაც, ხოლო მონაცემთა ფაილები უცვლელი რჩება. მანქანაზე, რომელსაც თვითონ მართავ, mongod.conf-ში მიუთითე replica set-ის სახელი:
replication: replSetName: rs0ან სწრაფი ლოკალური ტესტისთვის გადაეცი ბრძანების სტრიქონში:
$ mongod --dbpath ./data --replSet rs0 --bind_ip localhost --port 27017გადატვირთე სერვერი, დაუკავშირდი mongosh-ით და სიმრავლე ერთხელ ინიციალიზაცია გაუკეთე:
rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }]})host-ში ჩაწერე სახელი, რომელიც შენს კლიენტებს შეუძლიათ გადაწყვიტონ და მიაღწიონ, რადგან დრაივერი წევრებს სიმრავლის კონფიგურაციაში შენახული სახელებით პოულობს და არა იმ მისამართით, რომელიც შენ აკრიფე. localhost-ით ინიციალიზებული სიმრავლე მუშაობს იმავე მანქანაზე არსებული აპლიკაციისთვის და სხვებისთვის ვარდება. რამდენიმე წამში rs.status() წევრს PRIMARY-ად უნდა აჩვენებდეს:
rs.status().members.map(m => ({ name: m.name, state: m.stateStr }))რამდენიმე შედეგი წინასწარ უნდა გაითვალისწინო:
- ავტორიზაცია. replica set-ს ჩართული წვდომის კონტროლით წევრებს შორის შიდა ავტორიზაციის მექანიზმი სჭირდება, ჩვეულებრივ key ფაილი, რომელზეც
security.keyFileმიუთითებს. ერთი წევრის შემთხვევაშიც მიჰყევი სახელმძღვანელოს replica set-ის ავტორიზაციის ნაბიჯებს და ნუ გამოიცნობ. - დისკი. oplog არის capped კოლექცია და რეალურ ადგილს იკავებს. მისი ნაგულისხმევი ზომა თავისუფალი დისკის ადგილიდან გამოითვლება, ამიტომ შეამოწმე
db.getReplicationInfo()-ით და პატარა დისკზე ზომა პირდაპირ მიუთითე. - მიერთების სტრიქონები. დაამატე
replicaSet=rs0, რომ დრაივერმა replica set-ის პროტოკოლით ილაპარაკოს, ანdirectConnection=true, რომ აღმოჩენა გამოტოვოს და ერთ დასახელებულ ჰოსტს დაუკავშირდეს. - გადატვირთვები. გადატვირთვის შემდეგ სიმრავლე primary-ად დგება დამატებითი ბრძანებების გარეშე. ინიციალიზაცია მხოლოდ ერთხელ კეთდება.
ხელმისაწვდომია თუ არა ეს RE:NODE-ის მონაცემთა ბაზების ჰოსტინგში?#
RE:NODE-ის მონაცემთა ბაზების ჰოსტინგი გთავაზობს MongoDB-ს, თითოეული სერვერისთვის დაგენერირებული მონაცემებით და პირდაპირი მიერთებით ტარიფზე მითითებულ ჰოსტსა და პორტზე. ტარიფის გვერდზე ჩვენ არ ვამტკიცებთ, რომ ეს სერვერი replica set-ად მუშაობს, და არც შენ უნდა ივარაუდო. MongoDB სერვერი, რომელსაც ტარიფით ქირაობ, გაძლევს მიერთებას და არა mongod.conf-ს რედაქტირებისთვის, ამიტომ კითხვა, მუშაობს თუ არა ტრანზაქციები, სერვერის თვისებაა და არა შენი კოდის.
გასარკვევად ჯერ ჰკითხე, შემდეგ კი გამოსცადე:
- პანელიდან გახსენი ტიკეტი და ჰკითხე, მუშაობს თუ არა შენი MongoDB სერვერი replica set-ად და მხარდაჭერილია თუ არა მრავალდოკუმენტიანი ტრანზაქციები. პასუხად ამ სტატიას ნუ დაეყრდნობი.
- შენს მიერთებაზე გაუშვი მომდევნო თავის მოკლე ტესტი. standalone ზემოთ აღწერილი შეცდომით ვარდება პირველივე ჩაწერაზე; replica set commit-ს ასრულებს.
- თუ პასუხი უარყოფითია, გამოიყენე ერთი დოკუმენტის ატომურობისა და ორფაზიანი commit-ის შაბლონები ამავე სტატიაში ქვემოთ, რომლებიც ნებისმიერ MongoDB-ზე მუშაობს. თუ რეალური ტრანზაქციები გჭირდება, საკუთარი
mongodგაუშვი ერთწევრიანი replica set-ად მანქანაზე, რომელსაც თვითონ მართავ, მაგალითად VDS-ზე, და ზემოთ მოცემული თავი პირდაპირ შენზე ვრცელდება.
სახელმძღვანელოების ბაზების თავში აღწერილია, როგორ მუშაობს პანელში ბაზის მონაცემები. ჯერ ისინი გადაამოწმე, სანამ ისეთ მიერთებას გამართავ, რომელიც სინამდვილეში ავტორიზაციის პრობლემაა; გავრცელებულ ხაფანგებს აღწერს სტატია authentication failed და authSource.
ტრანზაქციის დაწერა#
რეკომენდებული ფორმაა დრაივერის callback API. ის იწყებს ტრანზაქციას, ასრულებს შენს ფუნქციას, აკეთებს commit-ს და დროებითი შეცდომისას მთელს თავიდან იმეორებს, ამიტომ შენს კოდს ამის გაკეთება არ უწევს. ეს Node.js მაგალითი ორ ანგარიშს შორის თანხას გადაიტანს და წიგნში ჩანაწერს დაწერს, ანუ სამი ჩაწერა, რომლებიც უნდა შეთანხმდეს:
const { MongoClient } = require("mongodb");const client = new MongoClient(process.env.MONGODB_URI);await client.connect();const db = client.db("bank");async function transfer(from, to, amount) { const session = client.startSession(); try { await session.withTransaction(async () => { const debit = await db.collection("accounts").updateOne( { _id: from, balance: { $gte: amount } }, { $inc: { balance: -amount } }, { session } ); if (debit.modifiedCount !== 1) throw new Error("insufficient funds"); await db.collection("accounts").updateOne( { _id: to }, { $inc: { balance: amount } }, { session }); await db.collection("ledger").insertOne( { from, to, amount, at: new Date() }, { session }); }, { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } }); } finally { await session.endSession(); }}სამი ჩვევა მნიშვნელოვანია. { session } გადაეცი შიგნით ყველა ოპერაციას, რადგან ოპერაცია მის გარეშე ტრანზაქციის გარეთ სრულდება და თავისით დასტურდება. callback-ში არ გქონდეს გვერდითი ეფექტები, როგორიცაა წერილის გაგზავნა ან API-ს გამოძახება, რადგან ის შეიძლება ერთზე მეტჯერ შესრულდეს. და შესაწყვეტად გამონაკლისი ესროლე: callback-იდან გამონაკლისი ტრანზაქციას აუქმებს, withTransaction კი მას ხელახლა აგდებს.
სწრაფი შემოწმება mongosh-იდან, რომელიც ამავე დროს შენი სერვერის ტესტია:
const session = db.getMongo().startSession();const coll = session.getDatabase("test").getCollection("t");session.startTransaction();coll.insertOne({ probe: 1 });session.commitTransaction();session.endSession();standalone-ზე insertOne გამოაგდებს შეცდომას Transaction numbers are only allowed. replica set-ზე ის დასტურდება და საცდელი დოკუმენტი მერე შეგიძლია წაშალო.
ლიმიტები, რომლებსაც წააწყდები#
ტრანზაქციები განკუთვნილია მოკლე და პატარა ოპერაციებისთვის. ქვემოთ ის ლიმიტებია, რომლებიც პრაქტიკაში მნიშვნელოვანია, და რამდენიმე მათგანის შეცვლა შეიძლება სერვერზე, რომელსაც თვითონ მართავ.
| ლიმიტი | ნაგულისხმევი |
|---|---|
| ტრანზაქციის სიცოცხლის ხანგრძლივობა | 60 წამი |
| დაბლოკვის მოლოდინი კონფლიქტისას | 5 მილიწამი |
| ერთი დოკუმენტის ზომა | 16 MB |
პირველია transactionLifetimeLimitSeconds, ნაგულისხმევად 60. ფონური პროცესი აუქმებს ყველა ტრანზაქციას, რომელიც ამაზე დიდხანს გრძელდება, და მასში შემდეგი ოპერაცია NoSuchTransaction-ით ვარდება. მეორეა maxTransactionLockRequestTimeoutMillis: რამდენ ხანს ელოდება ტრანზაქცია საკეტის აღებას, სანამ თავს დანებებს. სერვერზე, რომელსაც არ მართავ, არც ერთს ვერ შეცვლი, ამიტომ პროექტირებისას ისინი გაითვალისწინე.
სხვა ქცევა, რომელიც უნდა იცოდე:
- ჩაწერის კონფლიქტები სწრაფად ვარდება. თუ ორი ტრანზაქცია ერთსა და იმავე დოკუმენტს ცვლის, მეორე ელოდების ნაცვლად დაუყოვნებლივ იღებს
WriteConflictშეცდომას. შეცდომას აქვს ნიშნულიTransientTransactionError, რაც ნიშნავს „გაიმეორე მთელი ტრანზაქცია“. callback API ამას შენს მაგივრად აკეთებს; ძირითადი API-ით ნიშნული თვითონ უნდა დაიჭირო. - commit-ის შედეგი შეიძლება უცნობი იყოს. თუ კავშირი commit-ის დროს გაწყდა, დრაივერი ისვრის
UnknownTransactionCommitResult-ს და commit შეიძლება შესრულებულიც იყოს და არც. commit-ის გამეორება უსაფრთხოა; მთელი ტრანზაქციის გამეორება არა, თუ შენი ჩაწერები იდემპოტენტური არაა. - ზომა. 4.0-ში მთელი ტრანზაქცია ერთ 16 MB oplog ჩანაწერში უნდა ჩატეულიყო. 4.2-იდან ტრანზაქცია რამდენიმე oplog ჩანაწერად შეიძლება გადანაწილდეს, ამიტომ ეს ერთიანი ზღვარი აღარ მოქმედებს, მაგრამ 16 MB თითო დოკუმენტზე რჩება, ხოლო დიდი ტრანზაქციები მაინც ხარჯავს მეხსიერებასა და საკეტის დროს. თუ ძველ ზღვარს უახლოვდები, ტრანზაქციას პაკეტური დავალების მაგივრად იყენებ.
- კოლექციები. 4.4-მდე კოლექციები და ინდექსები ტრანზაქციის დაწყებამდე უნდა არსებულიყო. 4.4-იდან მათი შექმნა ტრანზაქციის შიგნითაც შეიძლება. ტრანზაქციებს არ შეუძლიათ
admin,configდაlocalბაზებს შეეხონ ან capped კოლექციებში ჩაწერა. - Read concern. შიგნით კითხვები ტრანზაქციის სნეპშოტს იყენებს. ისინი ვერ ხედავენ სხვების მიერ მისი დაწყების შემდეგ დადასტურებულ ჩაწერებს, რაც ზუსტად მიზანია, და ამავე დროს მიზეზია, რომ მისი ღიად დატოვება არ ღირს, სანამ მომხმარებელს ელოდები.
- მეხსიერება. სნეპშოტი ქეშში ინახავს შეცვლილი მონაცემების ძველ ვერსიებს, სანამ ტრანზაქცია ცოცხალია. გრძელი ტრანზაქცია არა მხოლოდ ნელია, არამედ სხვა სამუშაოს მეხსიერებიდან განდევნის.
რაც უნდა იყოს ზუსტი რიცხვები შენს ვერსიაში, წესი ერთია: კითხვები და გადაწყვეტილებები, სადაც შეძლებ, ტრანზაქციის გახსნამდე გააკეთე, შემდეგ გახსენი, ჩაწერე, დაადასტურე და მილიწამებში დაამთავრე. თუ სიცოცხლის ლიმიტის გაზრდას აპირებ, პრობლემა დიზაინშია და არა ლიმიტში.
ატომურობა ტრანზაქციის გარეშე#
გასაკვირად ბევრ სამუშაოს ტრანზაქცია არ სჭირდება, რადგან ერთ დოკუმენტში ჩაწერა ატომურია. MongoDB ერთი updateOne-ის ან findOneAndUpdate-ის ყველა ცვლილებას ერთ მთლიანობად იყენებს, ჩადგმული დოკუმენტებისა და მასში არსებული მასივების ცვლილებების ჩათვლით. თუ მოახერხებ, რომ ერთად შესაცვლელი მონაცემები ერთ დოკუმენტში ცხოვრობდეს, ატომურობას ნებისმიერ სერვერზე მიიღებ, standalone-ის ჩათვლით, განმეორების ლოგიკისა და სნეპშოტის გარეშე.
ინსტრუმენტებია განახლების ოპერატორები და ფილტრი, რომელიც წინაპირობას გამოხატავს:
// Reserve one seat: the filter is the check, the update is the actiondb.flights.findOneAndUpdate( { _id: "LH100", seatsLeft: { $gt: 0 } }, { $inc: { seatsLeft: -1 }, $push: { passengers: { name: "Ada", at: new Date() } } }, { returnDocument: "after" })თუ არცერთი დოკუმენტი არ ემთხვევა, არაფერი იცვლება და გამოძახება აბრუნებს null-ს, ამიტომ ზედმეტის გაყიდვა შეუძლებელია. შემოწმებასა და ჩაწერას სხვა კლიენტი ვერ გაყოფს. იგივე შაბლონი მუშაობს მარაგის მრიცხველებზე, მოთხოვნების სიხშირის შეზღუდვაზე, ამოცანების აღებაზე ({ status: "queued" } ხდება { status: "running", worker: id }) და ოპტიმისტურ დაბლოკვაზე version ველით, რომელსაც ფილტრი ადარებს და განახლება ზრდის.
საიმედოობა ორ წესზეა დამოკიდებული. პირობა ფილტრში ჩადე და არა აპლიკაციის კოდში, რომელიც ჯერ კითხულობს და მერე წერს, რადგან რბოლა სწორედ ამ ღრეჩოში წარმოიშობა. და გამოიყენე განახლების ოპერატორები, როგორიცაა $inc და $set, მთელი დოკუმენტის მოძველებული ასლით ჩანაცვლების ნაცვლად.
ჩადგმა და სქემის დაპროექტება#
შემდეგი კითხვაა, სად უნდა ცხოვრობდეს მონაცემები, და ეს განსაზღვრავს, რამდენად ხშირად გჭირდება ატომურობა დოკუმენტებს შორის. რელაციურ დიზაინში შეკვეთა, მისი სტრიქონები და გადახდა სამი ცხრილი და ტრანზაქციაა. MongoDB-ში ისინი ხშირად ერთ დოკუმენტად შეგიძლია აქციო:
{ "_id": "order-1042", "customer": "c-77", "status": "paid", "lines": [ { "sku": "A-100", "qty": 2, "price": 1800 }, { "sku": "B-220", "qty": 1, "price": 950 } ], "payment": { "method": "card", "paidAt": "2026-10-22T09:14:00Z" }, "total": 4550}ასეთი დოკუმენტი ერთი ოპერაციით იწერება და იკითხება, მისი ცვლილებები კი ატომურია. ჩადგმა ვარგა, როცა ნაწილები ერთად იკითხება, ერთ მშობელს ეკუთვნის და ზომით შეზღუდულია. ცუდად ერგება, როცა ჩადგმული მასივი შეუზღუდავად იზრდება (პოპულარული პოსტის კომენტარები), როცა შვილი ბევრ მშობელს ერთობლივად აქვს ან როცა დოკუმენტი 16 MB ლიმიტს მიუახლოვდება. მაშინ _id-ით მიუთითებ და ორ კოლექციას ეგუები.
შუალედური გზაც არსებობს: ჭეშმარიტების წყარო ერთ დოკუმენტში შეინახე და სხვა მოთხოვნებისთვის საჭირო რამდენიმე ველი დააკოპირე. ასლი შეიძლება დაშორდეს, ამიტომ გადაწყვიტე, რომელი ველია ავტორიტეტული, და ასლები მისგან განაახლე. კომპრომისები მათ მხარდამჭერ ინდექსებთან ერთად განხილულია სტატიაში MongoDB-ის ინდექსები და სქემის დაპროექტება, ხოლო კითხვა, საერთოდ გერგება თუ არა დოკუმენტური საცავი შენს მონაცემებს, სტატიაში Postgres თუ MongoDB. თუ შენი მონაცემები რელაციურია და ტრანზაქციები გინდა ნაგულისხმევად და არა სპეციალურ რეჟიმად, SQL მხარე მოცემულია სტატიაში MySQL-ის ტრანზაქციები, დაბლოკვა და deadlock-ები.
ორფაზიანი შაბლონები დოკუმენტებს შორის#
როცა მონაცემები მართლა სხვადასხვა დოკუმენტებში ცხოვრობს და ტრანზაქციები არ გაქვს, დამკვიდრებული მიდგომაა ორფაზიანი commit, რომელსაც თვითონ აგებ. MongoDB-ის სახელმძღვანელო მას თანხის გადარიცხვის მაგალითზე აღწერს. ამატებ კოლექციას transactions, რომელიც განზრახვასა და მის მდგომარეობას ინახავს, ხოლო ყოველი ზემოქმედებული დოკუმენტი იმახსოვრებს, რომელი დაუსრულებელი ტრანზაქცია შეეხო მას.
- ჩასვი ტრანზაქციის ჩანაწერი
state: "initial"-ით, წყაროთი, დანიშნულებითა და თანხით. - გადაიყვანე ჩანაწერი
pending-ში, შემდეგ გამოიყენე ცვლილება თითოეულ ანგარიშზე და დაამატე ტრანზაქციის_idამ ანგარიშისpendingTransactionsმასივში. ფილტრი უზრუნველყოფს, რომ თითოეული გამოყენება ერთხელ მოხდეს. - გადაიყვანე ჩანაწერი
applied-ში. - ამოშალე ტრანზაქციის
_idორივე ანგარიშისpendingTransactionsმასივებიდან. - გადაიყვანე ჩანაწერი
done-ში.
// Step 2, source account: only if not already applieddb.accounts.updateOne( { _id: "A", pendingTransactions: { $ne: tid }, balance: { $gte: 100 } }, { $inc: { balance: -100 }, $push: { pendingTransactions: tid } })თუ პროცესი ნებისმიერ მომენტში მოკვდა, აღდგენის დავალება გადაუყვება კოლექციას transactions, იპოვის pending-ში ან applied-ში გაჭედილ ჩანაწერებს და ან დაასრულებს, ან უკან დააბრუნებს იმავე მდგომარეობებით. შაბლონი კონსტრუქციით იდემპოტენტურია: შემოწმება $ne: tid ნიშნავს, რომ ნაბიჯის ორჯერ გაშვება არაფერს ცვლის. სწორედ ამ თვისებას აგებ და ღირს მისი ქონა მაშინაც კი, როცა რეალური ტრანზაქციები გაქვს, რადგან განმეორებულ მოთხოვნას ან დაკრაშულ worker-ს იგივე დაცვა სჭირდება.
ეს ასევე მნიშვნელოვნად მეტი კოდი და მეტი მდგომარეობაა, ვიდრე ტრანზაქცია. გამოიყენე, როცა ტრანზაქციის მიღება არ შეგიძლია ან როცა ოპერაცია ისეთ სისტემებს კვეთს, რომლებსაც მონაცემთა ბაზის ტრანზაქცია ვერ მოიცავს, მაგალითად გადახდის პროვაიდერს. ყველაფრისთვის, რაც ერთ replica set-ზეა, callback API უფრო მოკლეა და მასში შეცდომის დაშვება უფრო ძნელია.
როგორ აირჩიო#
მოკლე გადაწყვეტის გზა უმეტეს შემთხვევას ფარავს:
- ყველაფერი, რაც ერთად უნდა შეიცვალოს, ერთ დოკუმენტში ეტევა: გამოიყენე ერთი დოკუმენტის ატომური განახლებები. replica set საჭირო არაა.
- ორი-სამი დოკუმენტი, მოკლე ოპერაცია, სერვერი შენ გიმართავს: ერთწევრიანი replica set და
withTransaction. - replica set-ის ჩართვა არ შეგიძლია (ნაქირავები სერვერი, სადაც მხარდაჭერამ თქვა უარი): ჩადგი, სადაც შეგიძლია, ორფაზიანი შაბლონი გამოიყენე, სადაც ვერა, და ყოველი ჩაწერა იდემპოტენტური გახადე.
- სამუშაო გრძელი პაკეტია ან ათასობით დოკუმენტს ეხება: ეს ტრანზაქცია არაა. გამოიყენე bulk ჩაწერები ნაწილ-ნაწილ, ჩაიწერე პროგრესი და დავალება გადაუშვებადი გახადე.
რაც არ უნდა აირჩიო, გამოსცადე შეცდომების გზები: მოკალი პროცესი ნაბიჯებს შორის და დარწმუნდი, რომ მონაცემების გამოსწორება შესაძლებელია. და გააკეთე backup-ები, რადგან ვერც ტრანზაქცია და ვერც ორფაზიანი commit ვერ გიხსნის არასწორი წაშლისგან. როგორ გააკეთო ისინი და, რაც უფრო მნიშვნელოვანია, როგორ აღადგინო, აღწერილია სტატიაში ბაზების backup-ები და აღდგენა.
FAQ#
შემიძლია ტრანზაქციების გამოყენება standalone MongoDB-ზე?
არა. მრავალდოკუმენტიან ტრანზაქციებს replica set ან შარდირებული კლასტერი სჭირდება. standalone სერვერი მათ უარყოფს შეცდომით Transaction numbers are only allowed on a replica set member or mongos. ერთი დოკუმენტის განახლებები standalone-ზე ატომურია და მათზე ორფაზიანი შაბლონის აგება შეიძლება.
უფრო უსაფრთხოა თუ არა ჩემი მონაცემები ერთკვანძიანი replica set-ით?
არა. ის რთავს ტრანზაქციებსა და change streams-ს, მაგრამ მაინც ერთი მანქანაა მონაცემთა ერთი ასლით, ამიტომ არც failover არის და არც დუბლირება. გიცავს backup-ები.
რატომ ჩავარდა ჩემი ტრანზაქცია დაახლოებით ერთ წუთში?
ნაგულისხმევი transactionLifetimeLimitSeconds არის 60. ამ დროის შემდეგაც ღია ტრანზაქცია უქმდება და შემდეგი ოპერაცია NoSuchTransaction-ით ვარდება. ტრანზაქცია შეამოკლე ნელი სამუშაოს, მაგალითად ქსელური გამოძახებების, მის გარეთ გატანით.
აქვს თუ არა ტრანზაქციას ზომის ლიმიტი?
ყოველი დოკუმენტი კვლავ 16 MB-ით არის შეზღუდული. 4.0-ში მთელი ტრანზაქცია ასევე ერთ 16 MB oplog ჩანაწერში უნდა ჩატეულიყო; 4.2-იდან მას რამდენიმე შეუძლია დაიკავოს. მიუხედავად ამისა, დიდი ტრანზაქციები ნელია და მეხსიერებას იკავებს, ამიტომ მასობრივი სამუშაო ნაწილებად დაყავი.
მჭირდება თუ არა ტრანზაქციები მრიცხველისთვის ან მარაგის შესამცირებლად?
ჩვეულებრივ არა. ერთი updateOne ან findOneAndUpdate ისეთი ფილტრით, როგორიცაა { stock: { $gte: 1 } }, და $inc-ით ატომურია და ზედმეტს გაყიდვას არ დაუშვებს. ტრანზაქციას მაშინ მიმართე, როცა ერთზე მეტი დოკუმენტი უნდა შეთანხმდეს.
მხარს უჭერს თუ არა ჩემი MongoDB სერვერი RE:NODE-ზე ტრანზაქციებს?
ტარიფის გვერდზე არ წერია, რომ სერვერი replica set-ად მუშაობს, ამიტომ ეს არ ივარაუდო. პანელიდან გახსენი ტიკეტი და ჰკითხე, შემდეგ კი შენს მიერთებაზე გაუშვი ზემოთ მოცემული mongosh ტესტი.


