Bagikan melalui


Menangani Konflik Konkurensi

Tip

Anda dapat melihat contoh artikel ini di GitHub.

Dalam sebagian besar skenario, database digunakan secara bersamaan oleh beberapa instans aplikasi, masing-masing melakukan modifikasi pada data secara independen satu sama lain. Ketika data yang sama dimodifikasi pada saat yang sama, inkonsistensi dan kerusakan data dapat terjadi, misalnya ketika dua klien memodifikasi kolom yang berbeda dalam baris yang sama yang terkait dalam beberapa cara. Halaman ini membahas mekanisme untuk memastikan bahwa data Anda tetap konsisten dalam menghadapi perubahan bersamaan tersebut.

Konkurensi optimis

EF Core menerapkan konkurensi optimis, yang mengasumsikan bahwa konflik konkurensi relatif jarang terjadi. Berbeda dengan pendekatan pesimis - yang mengunci data di depan dan hanya kemudian melanjutkan untuk memodifikasinya - konkurensi optimis tidak mengambil kunci, tetapi mengatur agar modifikasi data gagal disimpan jika data telah berubah sejak dikueri. Kegagalan konkurensi ini dilaporkan ke aplikasi, yang berkaitan dengannya, mungkin dengan mencoba kembali seluruh operasi pada data baru.

Di EF Core, konkurensi optimis diimplementasikan dengan mengonfigurasi properti sebagai token konkurensi. Token konkurensi dimuat dan dilacak saat entitas dikueri - sama seperti properti lainnya. Kemudian, ketika operasi pembaruan atau penghapusan dilakukan selama SaveChanges(), nilai token konkurensi pada database dibandingkan dengan nilai asli yang dibaca oleh EF Core.

Untuk memahami cara kerjanya, mari kita asumsikan kita berada di SQL Server, dan menentukan jenis entitas Orang yang khas dengan properti khusus Version :

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [Timestamp]
    public byte[] Version { get; set; }
}

Di SQL Server, ini mengonfigurasi token konkurensi yang secara otomatis berubah dalam database setiap kali baris diubah (detail selengkapnya tersedia di bawah). Dengan konfigurasi ini, mari kita periksa apa yang terjadi dengan operasi pembaruan sederhana:

var person = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
context.SaveChanges();
  1. Pada langkah pertama, Orang dimuat dari database; ini termasuk token konkurensi, yang sekarang dilacak seperti biasa oleh EF bersama dengan properti lainnya.
  2. Instans Person kemudian dimodifikasi dalam beberapa cara - kami mengubah FirstName properti .
  3. Kami kemudian menginstruksikan EF Core untuk mempertahankan modifikasi. Karena token konkurensi dikonfigurasi, EF Core mengirimkan SQL berikut ke database:
UPDATE [People] SET [FirstName] = @p0
WHERE [PersonId] = @p1 AND [Version] = @p2;

Perhatikan bahwa selain PersonId dalam klausa WHERE, EF Core juga telah menambahkan kondisi Version ; ini hanya memodifikasi baris jika Version kolom tidak berubah sejak saat kami mengkuerinya.

Dalam kasus normal ("optimis"), tidak ada pembaruan bersamaan yang terjadi dan PEMBARUAN berhasil diselesaikan, memodifikasi baris; database melaporkan ke EF Core bahwa satu baris dipengaruhi oleh UPDATE, seperti yang diharapkan. Namun, jika pembaruan bersamaan terjadi, PEMBARUAN gagal menemukan baris dan laporan yang cocok bahwa nol terpengaruh. Akibatnya, EF Core SaveChanges() melempar DbUpdateConcurrencyException, yang harus ditangkap dan ditangani aplikasi dengan tepat. Teknik untuk melakukan ini dirinci di bawah ini, di bawah Menyelesaikan konflik konkurensi.

Sementara contoh di atas membahas pembaruan untuk entitas yang ada. EF juga melemparkan DbUpdateConcurrencyException saat mencoba menghapus baris yang telah dimodifikasi secara bersamaan. Namun, pengecualian ini tidak pernah dilemparkan saat menambahkan entitas; sementara database mungkin memang menimbulkan pelanggaran batasan unik jika baris dengan kunci yang sama sedang dimasukkan, ini menghasilkan pengecualian khusus penyedia yang dilemparkan, dan bukan DbUpdateConcurrencyException.

Token konkurensi asli yang dihasilkan database

Dalam kode di atas, kami menggunakan [Timestamp] atribut untuk memetakan properti ke kolom SQL Server rowversion . Karena rowversion secara otomatis berubah ketika baris diperbarui, ini sangat berguna sebagai token konkurensi upaya minimum yang melindungi seluruh baris. Mengonfigurasi kolom SQL Server rowversion sebagai token konkurensi dilakukan sebagai berikut:

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [Timestamp]
    public byte[] Version { get; set; }
}

Jenis yang rowversion ditunjukkan di atas adalah fitur khusus SQL Server; detail tentang menyiapkan token konkurensi yang diperbarui secara otomatis berbeda di seluruh database, dan beberapa database tidak mendukungnya sama sekali (misalnya SQLite). Lihat dokumentasi penyedia Anda untuk detail yang tepat.

Token konkurensi yang dikelola aplikasi

Daripada meminta database mengelola token konkurensi secara otomatis, Anda dapat mengelolanya dalam kode aplikasi. Ini memungkinkan penggunaan konkurensi optimis pada database - seperti SQLite - di mana tidak ada jenis pembaruan asli yang diperbarui secara otomatis. Tetapi bahkan di SQL Server, token konkurensi yang dikelola aplikasi dapat memberikan kontrol terperinci tentang perubahan kolom mana yang menyebabkan token diregenerasi. Misalnya, Anda mungkin memiliki properti yang berisi beberapa nilai yang di-cache atau tidak penting, dan tidak ingin perubahan pada properti tersebut memicu konflik konkurensi.

Berikut ini mengonfigurasi properti GUID untuk menjadi token konkurensi:

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }

    [ConcurrencyCheck]
    public Guid Version { get; set; }
}

Karena properti ini tidak dihasilkan database, Anda harus menetapkannya dalam aplikasi setiap kali terus berubah:

var person = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
person.Version = Guid.NewGuid();
context.SaveChanges();

Jika Anda ingin nilai GUID baru selalu ditetapkan, Anda dapat melakukan ini melalui pencegatSaveChanges. Namun, salah satu keuntungan mengelola token konkurensi secara manual adalah Anda dapat mengontrol dengan tepat ketika diregenerasi, untuk menghindari konflik konkurensi yang tidak perlu.

Mengatasi konflik konkurensi

Terlepas dari bagaimana token konkurensi Anda disiapkan, untuk menerapkan konkurensi optimis, aplikasi Anda harus menangani kasus dengan benar di mana konflik konkurensi terjadi dan DbUpdateConcurrencyException dilemparkan; ini disebut menyelesaikan konflik konkurensi.

Salah satu opsinya adalah hanya memberi tahu pengguna bahwa pembaruan gagal karena perubahan yang bertentangan; pengguna kemudian dapat memuat data baru dan mencoba lagi. Atau jika aplikasi Anda melakukan pembaruan otomatis, aplikasi hanya dapat mengulang dan mencoba kembali segera, setelah mengkueri ulang data.

Cara yang lebih canggih untuk mengatasi konflik konkurensi adalah dengan menggabungkan perubahan yang tertunda dengan nilai baru dalam database. Detail yang tepat dari nilai mana yang digabungkan tergantung pada aplikasi, dan prosesnya dapat diarahkan oleh antarmuka pengguna, di mana kedua set nilai ditampilkan.

Ada tiga set nilai yang tersedia untuk membantu mengatasi konflik konkurensi:

  • Nilai saat ini adalah nilai yang coba ditulis aplikasi ke database.
  • Nilai asli adalah nilai yang awalnya diambil dari database, sebelum pengeditan apa pun dibuat.
  • Nilai database adalah nilai yang saat ini disimpan dalam database.

Pendekatan umum untuk menangani konflik konkurensi adalah:

  1. Tangkap DbUpdateConcurrencyException selama SaveChanges.
  2. Gunakan DbUpdateConcurrencyException.Entries untuk menyiapkan serangkaian perubahan baru untuk entitas yang terpengaruh.
  3. Refresh nilai asli token konkurensi untuk mencerminkan nilai saat ini dalam database.
  4. Coba lagi proses hingga tidak ada konflik yang terjadi.

Dalam contoh berikut, Person.FirstName dan Person.LastName disiapkan sebagai token konkurensi. Ada // TODO: komentar di lokasi tempat Anda menyertakan logika khusus aplikasi untuk memilih nilai yang akan disimpan.

using var context = new PersonContext();
// Fetch a person from database and change phone number
var person = context.People.Single(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";

// Change the person's name in the database to simulate a concurrency conflict
context.Database.ExecuteSqlRaw(
    "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

var saved = false;
while (!saved)
{
    try
    {
        // Attempt to save changes to the database
        context.SaveChanges();
        saved = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Person)
            {
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();

                foreach (var property in proposedValues.Properties)
                {
                    var proposedValue = proposedValues[property];
                    var databaseValue = databaseValues[property];

                    // TODO: decide which value should be written to database
                    // proposedValues[property] = <value to be saved>;
                }

                // Refresh original values to bypass next concurrency check
                entry.OriginalValues.SetValues(databaseValues);
            }
            else
            {
                throw new NotSupportedException(
                    "Don't know how to handle concurrency conflicts for "
                    + entry.Metadata.Name);
            }
        }
    }
}

Menggunakan tingkat isolasi untuk kontrol konkurensi

Konkurensi optimis melalui token konkurensi bukanlah satu-satunya cara untuk memastikan bahwa data tetap konsisten dalam menghadapi perubahan bersamaan.

Salah satu mekanisme untuk memastikan konsistensi adalah tingkat isolasi transaksi bacaan berulang. Di sebagian besar database, tingkat ini menjamin bahwa transaksi melihat data dalam database seperti saat transaksi dimulai, tanpa terpengaruh oleh aktivitas bersamaan berikutnya. Mengambil sampel dasar kami dari atas, ketika kami meminta Person untuk memperbaruinya dalam beberapa cara, database harus memastikan tidak ada transaksi lain yang mengganggu baris database tersebut sampai transaksi selesai. Bergantung pada implementasi database Anda, ini terjadi dengan salah satu dari dua cara:

  1. Saat baris dikueri, transaksi Anda mengambil kunci bersama di atasnya. Setiap transaksi eksternal yang mencoba memperbarui baris akan diblokir hingga transaksi Anda selesai. Ini adalah bentuk penguncian pesimis, dan diimplementasikan oleh tingkat isolasi "baca berulang" SQL Server.
  2. Daripada mengunci, database memungkinkan transaksi eksternal untuk memperbarui baris, tetapi ketika transaksi Anda sendiri mencoba melakukan pembaruan, kesalahan "serialisasi" akan dimunculkan, menunjukkan bahwa konflik konkurensi terjadi. Ini adalah bentuk penguncian optimis - tidak seperti fitur token konkurensi EF - dan diimplementasikan oleh tingkat isolasi rekam jepret SQL Server, serta oleh tingkat isolasi bacaan berulang PostgreSQL.

Perhatikan bahwa tingkat isolasi "dapat diserialisasikan" memberikan jaminan yang sama seperti baca yang dapat diulang (dan menambahkan yang tambahan), sehingga berfungsi dengan cara yang sama sehubungan dengan yang di atas.

Menggunakan tingkat isolasi yang lebih tinggi untuk mengelola konflik konkurensi lebih sederhana, tidak memerlukan token konkurensi, dan memberikan keuntungan lainnya; misalnya, pembacaan berulang menjamin bahwa transaksi Anda selalu melihat data yang sama di seluruh kueri di dalam transaksi, menghindari inkonsistensi. Namun, pendekatan ini memang memiliki kelemahannya.

Pertama, jika implementasi database Anda menggunakan penguncian untuk menerapkan tingkat isolasi, maka transaksi lain yang mencoba memodifikasi baris yang sama harus memblokir seluruh transaksi. Ini bisa berdampak buruk pada performa bersamaan (jaga agar transaksi Anda tetap pendek!), meskipun perhatikan bahwa mekanisme EF melemparkan pengecualian dan memaksa Anda untuk mencoba kembali sebagai gantinya, yang juga berdampak. Ini berlaku untuk tingkat baca yang dapat diulang SQL Server, tetapi tidak ke tingkat rekam jepret, yang tidak mengunci baris yang dikueri.

Lebih penting lagi, pendekatan ini memerlukan transaksi untuk menjangkau semua operasi. Jika Anda, katakanlah, mengkueri Person untuk menampilkan detailnya kepada pengguna, lalu menunggu pengguna membuat perubahan, maka transaksi harus tetap hidup untuk waktu yang berpotensi lama, yang harus dihindari dalam banyak kasus. Akibatnya, mekanisme ini biasanya sesuai ketika semua operasi yang terkandung segera dijalankan dan transaksi tidak bergantung pada input eksternal yang dapat meningkatkan durasinya.

Sumber Daya Tambahan:

Lihat Deteksi konflik di EF Core untuk sampel inti ASP.NET dengan deteksi konflik.