12/08/2018, 15:59

Bài Toán GetMax dễ nhưng không đơn giản

Hi Các bạn. Đợt làm dự án vừa rồi mình có gặp 1 bài toán nhìn vào thì khá là đơn giản nhưng nó đã thực sự trở thành vấn đề khá là nan giải khi áp dụng ko đúng lúc và đúng chỗ. Đó chính là bài toán GetMax Mô tả bài toán Tôi có 1 table tên là Order có 1 column tên là SeqNo Với 1 record được ...

Hi Các bạn. Đợt làm dự án vừa rồi mình có gặp 1 bài toán nhìn vào thì khá là đơn giản nhưng nó đã thực sự trở thành vấn đề khá là nan giải khi áp dụng ko đúng lúc và đúng chỗ. Đó chính là bài toán GetMax

Mô tả bài toán

  • Tôi có 1 table tên là Order có 1 column tên là SeqNo
  • Với 1 record được insert vào thì SeqNo của record đó bằng Max(SeqNo) + 1
  • Qua ngày thì số SeqNo sẽ reset về 1 và lặp lại tương tự ở bước 2

Nhìn vào bài toán thì có vẻ đơn giản, chúng ta chỉ việc viết 1 function GetMax để lấy ra giá trị lớn nhất của SeqNo tại thời điểm hiện tại và +1, và Tôi đã làm như vậy, hệ thống cũng đã chạy ngon lành cành đào.

Nhưng vào 1 ngày đẹp trời thì khách hàng của dự án tôi tổ chức sự kiện và website của khách hàng có số lượng người dùng đăng ký tăng đột biến, cùng 1 thời điểm có đến hàng chục người đăng ký thì hệ thống xảy ra điều bất thường. Tất cả các record cùng thời điểm đăng ký đã có cùng 1 số SeqNo. What đờ heo chuyện gì đang xảy ra ở đây vậy, cả đội nháo nhào nhảy vào debug để tìm lỗi, nhưng ở local thì debug rất ngon lành cành đào, function GetMax vẫn lấy ra đúng, tất cả như vô vọng cho đến khi column CreatedDate của các record bị trùng có gì đó đặc biệt, tất cả dường như là cùng 1 thời điểm. Và đây chính là nguyên nhân issue này.

  • Do các record đăng ký cùng 1 thời điểm nên khi record thứ nhất chưa được insert vào DB thì function GetMax lại được gọi ở section insert record thứ 2 và nghiểm nhiên cả 2 record đều lấy về cùng 1 số SeqNo.

Bài toán của chúng ta đã rõ: LÀM CÁCH NÀO ĐỂ CÓ THỂ LẤY RA ĐÚNG MAX SeqNo cho dù nhiều request cùng 1 lúc.

Sau khi bài toán đã rõ thì tất cả dev trong dự án đều được họp lại để tìm ra solution. Và có nhiều solution đã được đề ra.

  • Sử dụng transaction để lock lại quá trình insert, đảm bảo chỉ 1 record đc insert tại thời điểm đó.
  • Sử dụng trigger sau khi insert để tự động update Seq
  • Sử dụng Tool chạy theo scheduler update Seq hàng ngày.
  • ...

Tất cả các phương án đều được triển khai thử nghiệm ở server dev và stg và 2 phương án được lựa chọn để đưa vào thử nghiệm chính thức là

  • Transaction được bọc trong function insert DB với hi vọng nó sẽ đảm bảo quá trình insert được bảo kê là duy nhất.
  • Dưới database 1 trigger đã được tạo để túm các record vừa mới đươc insert vào để update SeqNo

Sau khi được test ở server Dev và Stg thì kết quả có vẻ khả quan:

  • Các record được tạo cách nhau 0.02s đều được đánh đúng Seq
  • Transaction ko thấy có dấu hiệu gì là chết cả
  • 2 thằng dev ngồi đếm 1 2 3 để cùng click button đăng ký thì thấy kết quả đều ngon lành

Tưởng chừng như vấn đề đã được giải quyết ngon ăn và có thể vỗ ngực với khách hàng là bọn tôi fix xong rồi đó.

Lần này phải gọi là phốt nhé.

Khách hàng lại tiếp tục tổ chức sự kiện và lần này ko chỉ số lượng người đăng ký tăng cao mà cộng thêm đó còn bị ảnh hưởng bởi dis cáp Quang nữa, và điều khủng khiếp nhất lại ập đến SeqNo lại trùng. Cái từ SeqNo nó trở thành cái gì đó thật khủng khiếp mà chưa bao giờ trong đời của 1 thằng dev từng gặp.

Tất cả mọi người lại ngồi lại và phân tích và rút ra rằng:

  • Transaction nó ko chết mà đơn giản là nó ko chạy vì hệ thống đc deploy trên 2 server bằng 2 bộ code
  • Trigger có vẻ cover ngon khi mạng mẽo ổn định, khoảng cách giữa 2 lần insert là 0.02s nhưng thấp hơn nữa thì nó cũng bó tay và lại trở về với bài toán cả 2 record được insert vào cùng thời điểm và trigger của 2 record cũng chạy cùng thời điểm và lại lấy ra cùng 1 SeqNo.

Đúc rút kinh nghiệm lần thứ nhất ko thể trông mong vào trigger hay 1 cái gì đó tự code mà chạy auto nữa mà hướng tới sử dụng các tính năng có sẵn của SQL hay server và có 2 Phương án được đề ra

Sử dụng Identity của SQL server

Bạn nào ko biết cái này thì vào đây đọc nhé Link Here

  • Khi column đc setting IDentity thì nó sẽ tự động tăng lên khi record đc insert vào DB và dường như ko có độ trễ nào.
  • Sử dụng code để check xem ngày hiện tại đã có record nào được insert vào hay chưa, nếu ngày hiện tại chưa có record nào đc insert thì reset Identity về 1 và để nó tự tăng
public void ResetIdentity()
{
    using (var context = new DatabaseContext){
      var firstRecord = context.Order.FirstOrDefault(x=>x.CreatedDate.Date == DateTime.Now.Date)
      if(firstRecord == null){
          context.Database.ExecuteSqlCommand("SET Identity_Insert Order OFF; DBCC CHECKIDENT(Order, RESEED, 0);");
      }
    }
 }

Cách này có vẻ ngon lành, sử dụng LoadTest để spam insert vào DB thì nó chịu đc 30 user cùng insert tại 1 thời điểm vd "2017-09-07 20:21:29.984635345". Nhưng trên 30 user cùng 1 thời điểm thì nó lại bị trùng mà trùng 1 cách tập thể luôn. Cho nên cách này chỉ áp dụng cho các hệ thống ko có quá nhiều người đăng ký cùng 1 lúc thì có vẻ ok.

Sử dụng đệ quy

Khi nhắc đến cách này thì có vẻ nó hơi có vấn đề và mang tính chất đối phó nhưng đến tại thời điểm hiện tại nó đang là solution sáng giá nhất, bất chấp có bao nhiêurecord bị trùng thời điểm đăng ký. Vậy nó hoạt động ra sao

  • Tạo thêm 1 column có tên là SeqNoDate, để kết hợp với SeqNo làm 1 cặp khoá (unique).
  • Tạo 1 function GetMax để lấy số SeqNo lớn nhất của ngày hôm đó
  • Sử dụng đệ quy gọi lại function insert khi xảy ra lỗi do unique bị trùng
        public void AddNewOrder(Order orderModel)
        {
            try
            {
                AddOrder(orderModel);
            }
            catch
            {
                 using (var context = new DatabaseContext){
                {
                    var order = context.Order.FirstOrDefault(x => x.SeqNo == orderModel.SeqNo && x.SeqNoDate == orderModel.SeqNoDate);
                    if (order != null)
                    {
                        AddNewOrder(orderModel);
                    }
                }
            }
        }

Với cách này khi 1 model được truyền lên sẽ qua các bước sau:

  • GetMax lấy SeqNo
  • Insert Model to DB
  • nếu Cặp SeqNo và SeqNoDate Valid => Insert
  • Nếu Cặp SeqNo và SeqNoDate Invalid => gọi lại function để thực hiện từ bước 1.

Sau khi gặp vấn đề trên thì mình rút ra như này:

  • Code vẫn cần thời gian chạy để cho ra kết quả nên việc thời gian quá ngắn để lấy kết quả mong đợi là khó
  • Việc code là hữu hạn nhưng fix bug là vô hạn
  • Hãy thử solution đi vì khách hàng cho phép =))

Hiện tại thì mình đang sử dụng cách đệ quy để đảm bảo việc tạo SeqNo và chưa có solution nào có thể khả thi hơn. Nếu các bạn có solution thì vui long contact với mình qua: CW: tran.xuan.dat@framgia.com Skype: dattx198

Nếu có hãy cho mình 1 solution hay đơn giản chỉ là 1 đường link. Xin cám ơn tất cả mọi người

0