Mysql, Redis dan Mongo semuanya adalah toko yang sangat populer, dan masing-masing memiliki kelebihannya sendiri. Dalam aplikasi praktis, adalah umum untuk menggunakan beberapa toko pada saat yang sama dan memastikan konsistensi data di beberapa toko menjadi persyaratan.
Artikel ini memberikan contoh penerapan transaksi terdistribusi di beberapa mesin toko, Mysql, Redis, dan Mongo. Contoh ini didasarkan pada Kerangka Transaksi Terdistribusi https://github.com/dtm-labs/dtm dan mudah-mudahan akan membantu memecahkan masalah Anda dalam konsistensi data di seluruh layanan mikro.
Kemampuan untuk menggabungkan beberapa mesin penyimpanan secara fleksibel untuk membentuk transaksi terdistribusi pertama kali diusulkan oleh DTM, dan tidak ada kerangka kerja transaksi terdistribusi lainnya yang menyatakan kemampuan seperti ini.
Skenario masalah
Mari kita lihat skenario masalahnya terlebih dahulu. Misalkan pengguna sekarang berpartisipasi dalam promosi:dia memiliki saldo, mengisi ulang tagihan telepon, dan promosi akan memberikan poin mal. Saldo disimpan di Mysql, tagihan disimpan di Redis, poin mal disimpan di Mongo. Karena waktu promosi terbatas, ada kemungkinan partisipasi gagal, sehingga diperlukan dukungan rollback.
Untuk skenario masalah di atas, Anda dapat menggunakan transaksi Saga DTM, dan kami akan menjelaskan solusinya secara detail di bawah ini.
Menyiapkan Data
Langkah pertama adalah menyiapkan data. Untuk memudahkan pengguna memulai dengan cepat dengan contoh, kami telah menyiapkan data yang relevan di en.dtm.pub, yang meliputi Mysql, Redis dan Mongo, dan nama pengguna dan kata sandi koneksi khusus dapat ditemukan di https:// github.com/dtm-labs/dtm-examples.
Jika Anda ingin menyiapkan sendiri lingkungan data secara lokal, Anda dapat menggunakan https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml untuk memulai Mysql, Redis, Mongo; lalu jalankan skrip di https://github.com/dtm-labs/dtm/tree/main/sqls untuk menyiapkan data untuk contoh ini, di mana
busi.*
adalah data bisnis danbarrier.*
adalah tabel bantu yang digunakan oleh DTM
Menulis Kode Bisnis
Mari kita mulai dengan kode bisnis untuk MySQL yang paling familiar.
Kode berikut ada di Golang. Bahasa lain seperti C#, PHP, Java dapat ditemukan di sini:DTM SDKs
func SagaAdjustBalance(db dtmcli.DB, uid int, amount int) error {
_, err := dtmimp.DBExec(db, "update dtm_busi.user_account set balance = balance + ? where user_id = ?" , amount, uid)
return err
}
Kode ini terutama melakukan penyesuaian saldo pengguna di database. Dalam contoh kita, bagian kode ini digunakan tidak hanya untuk operasi penerusan Saga, tetapi juga untuk operasi kompensasi, di mana hanya jumlah negatif yang perlu dimasukkan untuk kompensasi.
Untuk Redis dan Mongo, kode bisnis ditangani dengan cara yang sama, hanya menambah atau mengurangi saldo yang sesuai.
Bagaimana Memastikan Idempotensi
Untuk pola transaksi Saga, ketika kami mengalami kegagalan sementara dalam layanan sub-transaksi, operasi yang gagal akan dicoba kembali. Kegagalan ini dapat terjadi sebelum atau setelah sub-transaksi dilakukan, sehingga operasi sub-transaksi harus idempoten.
DTM menyediakan tabel pembantu dan fungsi pembantu untuk membantu pengguna mencapai idempotensi dengan cepat. Untuk Mysql, itu akan membuat tabel bantu barrier
di database bisnis, ketika pengguna memulai transaksi untuk menyesuaikan saldo, pertama-tama ia akan memasukkan Gid
di barrier
meja. Jika ada baris duplikat, maka penyisipan akan gagal, lalu lewati penyesuaian keseimbangan untuk memastikan idempoten. Kode yang menggunakan fungsi helper adalah sebagai berikut:
app.POST(BusiAPI+"/SagaBTransIn", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
return SagaAdjustBalance(tx, TransInUID, reqFrom(c).Amount, reqFrom(c).TransInResult)
})
}))
Mongo menangani idempotensi dengan cara yang mirip dengan Mysql, jadi saya tidak akan membahasnya secara detail lagi.
Redis menangani idempotensi secara berbeda dari Mysql, terutama karena perbedaan prinsip transaksi. Transaksi redis terutama dijamin oleh eksekusi atom Lua. fungsi pembantu DTM akan menyesuaikan keseimbangan melalui skrip Lua. Sebelum menyesuaikan saldo, itu akan meminta Gid
di Redis. Jika Gid
ada, itu akan melewati penyesuaian keseimbangan; jika tidak, itu akan merekam Gid
dan melakukan penyesuaian keseimbangan. Kode yang digunakan untuk fungsi helper adalah sebagai berikut:
app.POST(BusiAPI+"/SagaRedisTransOut", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), -reqFrom(c).Amount, 7*86400)
}))
Bagaimana melakukan Kompensasi
Untuk Saga, kita juga perlu berurusan dengan operasi kompensasi, tetapi kompensasi bukan hanya penyesuaian terbalik, dan ada banyak jebakan yang harus diwaspadai.
Di satu sisi, kompensasi perlu memperhitungkan idempotensi, karena kegagalan dan percobaan ulang yang dijelaskan pada subbagian sebelumnya juga ada dalam kompensasi. Di sisi lain, kompensasi juga perlu mempertimbangkan "kompensasi nol", karena operasi maju Saga dapat mengembalikan kegagalan, yang mungkin terjadi sebelum atau setelah penyesuaian data. Untuk kegagalan di mana penyesuaian telah dilakukan, kami perlu melakukan penyesuaian terbalik; tetapi untuk kegagalan di mana penyesuaian belum dilakukan, kita harus melewati operasi sebaliknya.
Dalam tabel helper dan fungsi helper yang disediakan oleh DTM, di satu sisi, ini akan menentukan apakah kompensasi adalah kompensasi nol berdasarkan Gid yang dimasukkan oleh operasi maju, dan di sisi lain, itu akan memasukkan Gid+'kompensasi' lagi untuk menentukan apakah kompensasi adalah operasi duplikat. Jika ada operasi kompensasi normal, maka itu akan melakukan penyesuaian data pada bisnis; jika ada kompensasi nol atau kompensasi duplikat, itu akan melewati penyesuaian pada bisnis.
Kode MySQL adalah sebagai berikut.
app.POST(BusiAPI+"/SagaBTransInCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
return SagaAdjustBalance(tx, TransInUID, -reqFrom(c).Amount, "")
})
}))
Kode untuk Redis adalah sebagai berikut.
app.POST(BusiAPI+"/SagaRedisTransOutCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), reqFrom(c).Amount, 7*86400)
}))
Kode layanan kompensasi hampir identik dengan kode operasi penerusan sebelumnya, kecuali jumlahnya dikalikan dengan -1. Fungsi pembantu DTM secara otomatis menangani idempotensi dan kompensasi nol dengan benar.
Pengecualian lainnya
Saat menulis operasi maju dan operasi kompensasi, sebenarnya ada pengecualian lain yang disebut "Penangguhan". Transaksi global akan mundur ketika batas waktu atau percobaan ulang telah mencapai batas yang dikonfigurasi. Kasus normal adalah bahwa operasi maju dilakukan sebelum kompensasi, tetapi dalam kasus penundaan proses, kompensasi dapat dilakukan sebelum operasi maju. Jadi operasi forward juga perlu menentukan apakah kompensasi telah dijalankan, dan jika demikian, penyesuaian data juga perlu dilewati.
Untuk pengguna DTM, pengecualian ini telah ditangani dengan baik dan benar dan Anda, sebagai pengguna, hanya perlu mengikuti MustBarrierFromGin(c).Call
panggilan yang dijelaskan di atas dan tidak perlu peduli sama sekali. Prinsip DTM menangani pengecualian ini dijelaskan secara rinci di sini:Pengecualian dan hambatan sub-transaksi
Memulai Transaksi Terdistribusi
Setelah menulis layanan sub-transaksi individu, kode kode berikut memulai transaksi global Saga.
saga := dtmcli.NewSaga(dtmutil.DefaultHTTPServer, dtmcli.MustGenGid(dtmutil.DefaultHTTPServer)).
Add(busi.Busi+"/SagaBTransOut", busi.Busi+"/SagaBTransOutCom", &busi.TransReq{Amount: 50}).
Add(busi.Busi+"/SagaMongoTransIn", busi.Busi+"/SagaMongoTransInCom", &busi.TransReq{Amount: 30}).
Add(busi.Busi+"/SagaRedisTransIn", busi.Busi+"/SagaRedisTransOutIn", &busi.TransReq{Amount: 20})
err := saga.Submit()
Di bagian kode ini, transaksi global Saga dibuat yang terdiri dari 3 sub-transaksi.
- Mentransfer 50 dari Mysql
- Transfer dalam 30 ke Mongo
- Transfer dalam 20 ke Redis
Sepanjang transaksi, jika semua sub-transaksi berhasil diselesaikan, maka transaksi global berhasil; jika salah satu sub-transaksi menghasilkan kegagalan bisnis, maka transaksi global akan dibatalkan.
Jalankan
Jika Anda ingin menjalankan contoh lengkap di atas, langkah-langkahnya adalah sebagai berikut.
- Jalankan DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
- Jalankan contoh yang berhasil
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
- Jalankan contoh yang gagal
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback
Anda dapat memodifikasi contoh untuk mensimulasikan berbagai kegagalan sementara, situasi kompensasi nol, dan berbagai pengecualian lain di mana datanya konsisten ketika seluruh transaksi global selesai.
Ringkasan
Artikel ini memberikan contoh transaksi terdistribusi di Mysql, Redis, dan Mongo. Ini menjelaskan secara rinci masalah yang perlu ditangani, dan solusinya.
Prinsip-prinsip dalam artikel ini cocok untuk semua mesin penyimpanan yang mendukung transaksi ACID, dan Anda dapat dengan cepat memperluasnya untuk mesin lain seperti TiKV.
Selamat datang untuk mengunjungi github.com/dtm-labs/dtm. Ini adalah proyek khusus untuk membuat transaksi terdistribusi dalam layanan mikro lebih mudah. Mendukung banyak bahasa, dan beberapa pola seperti pesan 2 fase, Saga, Tcc, dan Xa.