Penerapan CQRS & Event Sourcing untuk membuat platform lelang online

Kolega, selamat sore! Nama saya Misha, saya bekerja sebagai programmer.



Dalam artikel ini, saya ingin berbicara tentang bagaimana tim kami memutuskan untuk menerapkan pendekatan CQRS & Event Sourcing dalam proyek yang merupakan situs lelang online. Dan juga tentang apa yang keluar dari ini, kesimpulan apa yang bisa diambil dari pengalaman kami dan apa yang penting untuk tidak menginjak mereka yang menggunakan CQRS & ES.

gambar





Pendahuluan



Untuk mulai dengan, sedikit sejarah dan latar belakang bisnis. Seorang pelanggan mendatangi kami dengan sebuah platform untuk mengadakan apa yang disebut lelang berjangka waktu, yang sudah dalam produksi dan yang mengumpulkan sejumlah umpan balik. Klien ingin kami membuat platform untuk lelang langsung untuknya.



Sekarang sedikit terminologi. Lelang adalah saat barang tertentu dijual - lot, dan pembeli (penawar) mengajukan penawaran. Pemilik lot adalah pembeli yang menawarkan penawaran tertinggi. Lelang berjangka waktu adalah ketika setiap lot memiliki waktu penutupan yang telah ditentukan. Pembeli memasang taruhan, di beberapa titik lot ditutup. Mirip dengan ebay.



Platform berjangka waktu dibuat dengan cara klasik, menggunakan CRUD. Lot ditutup oleh aplikasi terpisah, mulai dari jadwal. Semua ini tidak bekerja dengan andal: beberapa taruhan hilang, beberapa dibuat seolah-olah atas nama pembeli yang salah, banyak yang tidak ditutup atau ditutup beberapa kali.



Lelang langsung adalah peluang untuk berpartisipasi dalam lelang offline nyata dari jarak jauh melalui Internet. Ada sebuah ruangan (dalam terminologi internal kami - "kamar"), itu berisi host lelang dengan palu dan penonton, dan di sana di sebelah laptop duduk petugas yang disebut, yang, dengan menekan tombol di antarmuka-nya, menyiarkan jalannya lelang ke Internet, dan terhubung Pada saat lelang, pembeli melihat tawaran yang ditempatkan offline dan dapat menempatkan penawaran mereka.



Kedua platform bekerja pada prinsipnya secara real time, tetapi jika dalam kasus waktunya semua pembeli berada pada posisi yang sama, maka dalam kasus live, sangat penting bahwa pembeli online dapat berhasil bersaing dengan mereka yang ada di dalam ruangan. Artinya, sistem harus sangat cepat dan dapat diandalkan. Pengalaman menyedihkan dari platform berjangka waktu memberi tahu kami dengan tegas bahwa CRUD klasik tidak cocok untuk kami.



Kami tidak memiliki pengalaman sendiri bekerja dengan CQRS & ES, jadi kami berkonsultasi dengan kolega yang memilikinya (kami memiliki perusahaan besar), menyajikan kepada mereka realitas bisnis kami dan bersama-sama sampai pada kesimpulan bahwa CQRS & ES harus sesuai dengan kami.



Apa lagi yang spesifik dari lelang online:



  • — . , « », , . — , 5 . .
  • , , .
  • — - , , — .
  • , .
  • Solusinya harus scalable - beberapa lelang dapat diadakan secara bersamaan.


Tinjauan singkat tentang pendekatan CQRS & ES



Saya tidak akan membahas pertimbangan pendekatan CQRS & ES, ada materi tentang ini di Internet dan khususnya tentang Habré (misalnya, di sini: Pengantar CQRS + Event Sourcing ). Namun, saya akan secara singkat mengingatkan Anda tentang poin utama:



  • Hal terpenting dalam event sourcing: sistem tidak menyimpan data, tetapi sejarah perubahannya, yaitu peristiwa. Keadaan sistem saat ini diperoleh dengan aplikasi peristiwa berurutan.
  • Model domain dibagi menjadi beberapa entitas yang disebut agregat. Unit ini memiliki versi. Acara diterapkan ke agregat. Menerapkan suatu acara ke agregat akan menambah versinya.
  • write-. , .
  • . . , , . «» . .
  • , , - ( N- ) . «» . , .
  • - , , , , write-.
  • write-, read-, , . read- . Read- .
  • , — Command Query Responsibility Segregation (CQRS): , , write-; , , read-.






. .





Untuk menghemat waktu, dan juga karena kurangnya pengalaman khusus, kami memutuskan bahwa kami perlu menggunakan semacam kerangka kerja untuk CQRS & ES.



Secara umum, tumpukan teknologi kami adalah Microsoft, mis. NET dan C #. Database - Microsoft SQL Server. Semuanya di-host di Azure. Platform berjangka waktu dibuat pada tumpukan ini, logis untuk membuat platform langsung di atasnya.



Pada saat itu, seperti yang saya ingat sekarang, Chinchilla adalah satu-satunya pilihan yang cocok untuk kami dalam hal tumpukan teknologi. Jadi kami membawanya.



Mengapa kita membutuhkan kerangka kerja CQRS & ES sama sekali? Dia dapat "keluar dari kotak" memecahkan masalah seperti itu dan mendukung aspek implementasi seperti:



  • Entitas agregat, perintah, acara, versi agregat, rehidrasi, mekanisme snapshot.
  • Antarmuka untuk bekerja dengan DBMS berbeda. Menyimpan / memuat acara dan snapshot dari agregat ke / dari dasar penulisan (event store).
  • Antarmuka untuk bekerja dengan antrian - mengirim perintah dan acara ke antrian yang sesuai, membaca perintah dan acara dari antrian.
  • Antarmuka untuk bekerja dengan soket web.


Jadi, dengan mempertimbangkan penggunaan Chinchilla, kami menambahkan ke tumpukan kami:



  • Bus Layanan Azure sebagai bus perintah dan acara, Chinchilla mendukungnya di luar kebiasaan;
  • Database tulis dan baca adalah Microsoft SQL Server, yaitu keduanya adalah database SQL. Saya tidak akan mengatakan bahwa ini adalah hasil dari pilihan sadar, tetapi lebih karena alasan historis.


Ya, frontend dibuat dalam Angular.



Seperti yang sudah saya katakan, salah satu persyaratan untuk sistem adalah bahwa pengguna belajar secepat mungkin tentang hasil tindakan mereka dan tindakan pengguna lain - ini berlaku untuk pelanggan dan petugas. Karena itu, kami menggunakan SignalR dan soket web untuk memperbarui data di frontend dengan cepat. Chinchilla mendukung integrasi SignalR.



Pemilihan unit



Salah satu hal pertama yang harus dilakukan ketika menerapkan pendekatan CQRS & ES adalah menentukan bagaimana model domain akan dibagi menjadi agregat.



Dalam kasus kami, model domain terdiri dari beberapa entitas utama, seperti ini:



public class Auction
{
     public AuctionState State { get; private set; }
     public Guid? CurrentLotId { get; private set; }
     public List<Guid> Lots { get; }
}

public class Lot
{
     public Guid? AuctionId { get; private set; }
     public LotState State { get; private set; }
     public decimal NextBid { get; private set; }
     public Stack<Bid> Bids { get; }
}
 
public class Bid
{
     public decimal Amount { get; set; }
     public Guid? BidderId { get; set; }
}




Kami mendapat dua agregat: Lelang dan Lot (dengan Tawaran). Secara umum, ini logis, tetapi kami tidak memperhitungkan satu hal - bahwa dengan pembagian seperti itu, keadaan sistem tersebar di dua unit, dan dalam beberapa kasus, untuk menjaga konsistensi, kami harus membuat perubahan pada kedua unit, dan bukan ke satu. Misalnya, pelelangan dapat dijeda. Jika pelelangan dijeda, Anda tidak dapat menawar banyak. Dimungkinkan untuk menjeda lot itu sendiri, tetapi pelelangan yang dihentikan sementara tidak dapat memproses perintah selain dari "berhenti sebentar".



Atau, hanya satu agregat yang dapat dibuat, Lelang, dengan semua lot dan penawaran di dalamnya. Tetapi objek seperti itu akan sangat sulit, karena bisa ada beberapa ribu lot dalam pelelangan dan mungkin ada beberapa lusin tawaran untuk satu lot. Selama masa lelang, agregat semacam itu akan memiliki banyak versi, dan rehidrasi agregat semacam itu (aplikasi berurutan dari semua peristiwa ke agregat), jika tidak ada snapshot dari agregat yang dibuat, akan memakan waktu yang cukup lama. Yang tidak dapat diterima untuk situasi kita. Jika Anda menggunakan snapshot (kami menggunakannya), maka snapshot itu sendiri akan sangat menimbang.



Di sisi lain, untuk memastikan bahwa perubahan diterapkan pada dua agregat dalam pemrosesan tindakan satu pengguna, Anda harus mengubah kedua agregat dalam perintah yang sama menggunakan transaksi, atau menjalankan dua perintah dalam transaksi yang sama. Keduanya, pada umumnya, merupakan pelanggaran arsitektur.



Keadaan seperti itu harus diperhitungkan saat memecah model domain menjadi agregat.



Pada tahap ini dalam evolusi proyek, kita hidup dengan dua unit, Lelang dan Lot, dan kita memecah arsitektur dengan mengubah kedua unit dalam beberapa perintah.



Menerapkan perintah ke versi unit tertentu



Jika beberapa pembeli melakukan penawaran pada lot yang sama pada waktu yang bersamaan, yaitu, mereka mengirim perintah “place a bid” ke sistem, hanya satu dari penawaran yang akan berhasil. Banyak adalah agregat, ia memiliki versi. Selama pemrosesan perintah, peristiwa dihasilkan, masing-masing meningkatkan versi agregat. Ada dua cara:



  • Kirim perintah yang menentukan versi agregat yang ingin kita terapkan. Kemudian pengendali perintah dapat segera membandingkan versi dalam perintah dengan versi unit saat ini dan tidak melanjutkan jika tidak cocok.
  • Jangan menentukan versi unit dalam perintah. Kemudian agregat direhidrasi dengan beberapa versi, logika bisnis yang sesuai dijalankan, peristiwa dihasilkan. Dan hanya ketika mereka disimpan dapat eksekusi muncul bahwa versi seperti unit sudah ada. Karena orang lain melakukannya lebih awal.


Kami menggunakan opsi kedua. Ini memberi tim peluang yang lebih baik untuk dieksekusi. Karena di bagian aplikasi yang mengirimkan perintah (dalam kasus kami, ini adalah frontend), versi agregat saat ini dengan beberapa probabilitas akan tertinggal di belakang versi nyata di backend. Terutama dalam kondisi ketika banyak perintah dikirim, dan versi unit sering berubah.



Kesalahan saat menjalankan perintah menggunakan antrian



Dalam implementasi kami, didorong oleh Chinchilla, penangan perintah membaca perintah dari antrian (Microsoft Azure Service Bus). Kami dengan jelas membedakan situasi ketika tim gagal karena alasan teknis (batas waktu, kesalahan dalam menghubungkan ke antrian / pangkalan) dan kapan karena alasan bisnis (upaya untuk membuat tawaran pada banyak jumlah yang sama dengan yang telah diterima, dll.). Dalam kasus pertama, upaya untuk mengeksekusi perintah diulangi hingga jumlah pengulangan yang ditentukan dalam pengaturan antrian tercapai, setelah itu perintah dikirim ke Antrian Surat Mati (topik terpisah untuk pesan yang tidak diproses di Bus Layanan Azure). Dalam kasus eksekusi bisnis, tim dikirim ke Antrian Surat Mati segera.







Kesalahan saat menangani acara menggunakan antrian



Acara yang dihasilkan sebagai hasil dari eksekusi perintah, tergantung pada implementasinya, juga dapat dikirim ke antrian dan diambil dari antrian oleh penangan acara. Dan saat menangani acara, kesalahan juga terjadi.



Namun, berbeda dengan situasi dengan perintah yang tidak dieksekusi, semuanya lebih buruk di sini - mungkin terjadi bahwa perintah dieksekusi dan acara ditulis ke pangkalan tulis, tetapi pemrosesan oleh penangan gagal. Dan jika salah satu dari penangan ini memperbarui basis baca, maka basis baca tidak akan diperbarui. Artinya, itu akan berada dalam keadaan tidak konsisten. Karena mekanisme coba lagi pemrosesan acara baca, database hampir selalu akhirnya diperbarui, tetapi kemungkinan bahwa setelah semua upaya itu akan tetap rusak masih tetap ada.







Kami mengalami masalah ini di rumah. Alasannya, bagaimanapun, sebagian besar disebabkan oleh fakta bahwa kami memiliki beberapa logika bisnis dalam pemrosesan acara, yang, dengan aliran taruhan yang kuat, memiliki peluang bagus untuk gagal dari waktu ke waktu. Sayangnya, kami menyadari ini terlambat, tidak mungkin untuk mengulangi implementasi bisnis dengan cepat dan sederhana.



Akibatnya, sebagai tindakan sementara, kami berhenti menggunakan Bus Layanan Azure untuk mentransfer acara dari bagian tulis aplikasi ke bagian baca. Alih-alih, yang disebut In-Memory Bus digunakan, yang memungkinkan Anda untuk memproses perintah dan peristiwa dalam satu transaksi dan, jika terjadi kegagalan, kembalikan semuanya.







Solusi semacam itu tidak berkontribusi pada skalabilitas, tetapi di sisi lain, kami mengecualikan situasi ketika basis baca kami rusak, yang, pada gilirannya, merusak frontend dan menjadi tidak mungkin untuk melanjutkan pelelangan tanpa menciptakan kembali basis baca dengan memutar ulang semua peristiwa.



Mengirim perintah sebagai respons terhadap suatu peristiwa



Ini, pada prinsipnya, tepat, tetapi hanya dalam kasus ketika kegagalan untuk mengeksekusi perintah kedua ini tidak merusak keadaan sistem.



Menangani beberapa peristiwa dari satu perintah



Secara umum, eksekusi satu perintah menghasilkan beberapa peristiwa. Kebetulan bahwa untuk setiap peristiwa kita perlu membuat beberapa perubahan dalam database baca. Itu juga terjadi bahwa urutan peristiwa juga penting, dan dalam urutan yang salah, pemrosesan peristiwa tidak akan berfungsi sebagaimana mestinya. Semua ini berarti bahwa kita tidak dapat membaca dari antrian dan memproses peristiwa dari satu perintah secara mandiri, misalnya, dengan contoh kode yang berbeda yang membaca pesan dari antrian. Selain itu, kami memerlukan jaminan bahwa acara dari antrian akan dibaca dalam urutan yang sama dengan yang dikirim ke sana. Atau kita perlu bersiap untuk fakta bahwa tidak semua acara perintah akan berhasil diproses pada percobaan pertama.







Menangani satu acara dengan banyak penangan



Jika sistem perlu melakukan beberapa tindakan berbeda sebagai respons terhadap satu peristiwa, biasanya beberapa penangan untuk acara ini dibuat. Mereka dapat bekerja secara paralel atau berurutan. Dalam hal peluncuran berurutan, jika salah satu penangan gagal, seluruh urutan dimulai ulang (ini adalah kasus di Chinchilla). Dengan implementasi seperti itu, penting bahwa penangan idempoten sehingga menjalankan kedua penangan yang berhasil dieksekusi tidak gagal. Kalau tidak, ketika pawang kedua jatuh dari rantai, itu, rantai, pasti tidak akan berfungsi sepenuhnya, karena pawang pertama akan jatuh pada upaya kedua (dan selanjutnya).



Misalnya, pengendali acara di pangkalan baca menambahkan tawaran untuk banyak 5 rubel. Upaya pertama untuk melakukan ini akan berhasil, dan yang kedua tidak akan membiarkan kendala dieksekusi dalam database.







Kesimpulan / Kesimpulan



Sekarang proyek kami berada pada tahap ketika, seperti yang terlihat bagi kami, kami telah menginjak sebagian besar garu yang ada yang relevan dengan spesifik bisnis kami. Secara umum, kami menganggap pengalaman kami cukup sukses, CQRS & ES sangat cocok untuk bidang subjek kami. Pengembangan lebih lanjut dari proyek ini terlihat dalam pengabaian Chinchilla yang mendukung kerangka kerja lain yang memberikan lebih banyak fleksibilitas. Namun, mungkin juga untuk menolak menggunakan kerangka kerja sama sekali. Kemungkinan juga akan ada beberapa perubahan dalam arah menemukan keseimbangan antara keandalan di satu sisi dan kecepatan dan skalabilitas solusi di sisi lain.



Adapun komponen bisnis, di sini juga beberapa pertanyaan masih tetap terbuka - misalnya, membagi model domain menjadi agregat.



Saya ingin berharap bahwa pengalaman kami akan berguna bagi seseorang, membantu menghemat waktu dan menghindari menyapu. Terima kasih atas perhatian anda



All Articles