ObjectRepository - Mẫu kho lưu trữ trong bộ nhớ .NET cho các dự án tại nhà của bạn

Tại sao lưu trữ tất cả dữ liệu trong bộ nhớ?

Để lưu trữ dữ liệu trang web hoặc phụ trợ, mong muốn đầu tiên của hầu hết những người tỉnh táo sẽ là chọn cơ sở dữ liệu SQL. 

Nhưng đôi khi bạn nảy ra ý nghĩ rằng mô hình dữ liệu không phù hợp với SQL: ví dụ: khi xây dựng biểu đồ tìm kiếm hoặc xã hội, bạn cần tìm kiếm các mối quan hệ phức tạp giữa các đối tượng. 

Tình huống xấu nhất là khi bạn làm việc theo nhóm và đồng nghiệp không biết cách xây dựng các truy vấn nhanh. Bạn đã dành bao nhiêu thời gian để giải các bài toán N+1 và xây dựng các chỉ mục bổ sung để lệnh SELECT trên trang chính hoàn thành trong một khoảng thời gian hợp lý?

Một cách tiếp cận phổ biến khác là NoSQL. Vài năm trước, có rất nhiều sự cường điệu xung quanh chủ đề này - trong bất kỳ dịp thuận tiện nào, họ đã triển khai MongoDB và hài lòng với câu trả lời dưới dạng tài liệu json (nhân tiện, bạn đã phải dùng bao nhiêu chiếc nạng vì các liên kết vòng tròn trong tài liệu?).

Tôi khuyên bạn nên thử một phương pháp thay thế khác - tại sao không thử lưu trữ tất cả dữ liệu trong bộ nhớ ứng dụng, lưu định kỳ vào bộ nhớ ngẫu nhiên (tệp, cơ sở dữ liệu từ xa)? 

Bộ nhớ đã trở nên rẻ và mọi dữ liệu có thể có cho hầu hết các dự án vừa và nhỏ sẽ vừa với bộ nhớ 1 GB. (Ví dụ, dự án nhà yêu thích của tôi là theo dõi tài chính, lưu giữ số liệu thống kê và lịch sử hàng ngày về chi phí, số dư và giao dịch của tôi trong một năm rưỡi, chỉ tiêu tốn 45 MB bộ nhớ.)

Ưu điểm:

  • Việc truy cập dữ liệu trở nên dễ dàng hơn - bạn không cần phải lo lắng về các truy vấn, lười tải, các tính năng ORM, bạn làm việc với các đối tượng C# thông thường;
  • Không có vấn đề gì liên quan đến việc truy cập từ các luồng khác nhau;
  • Rất nhanh - không cần yêu cầu mạng, không cần dịch mã sang ngôn ngữ truy vấn, không cần tuần tự hóa (khử) các đối tượng;
  • Có thể chấp nhận lưu trữ dữ liệu dưới bất kỳ hình thức nào - có thể là dưới dạng XML trên đĩa hoặc trong SQL Server hoặc trong Bộ lưu trữ bảng Azure.

Nhược điểm:

  • Việc chia tỷ lệ theo chiều ngang bị mất và kết quả là không thể thực hiện được việc triển khai không có thời gian ngừng hoạt động;
  • Nếu ứng dụng gặp sự cố, bạn có thể bị mất một phần dữ liệu. (Nhưng ứng dụng của chúng tôi không bao giờ gặp sự cố, phải không?)

Nó hoạt động như thế nào?

Thuật toán như sau:

  • Khi bắt đầu, kết nối được thiết lập với bộ lưu trữ dữ liệu và dữ liệu được tải;
  • Một mô hình đối tượng, các chỉ mục chính và các chỉ mục quan hệ (1:1, 1:Many) được xây dựng;
  • Đăng ký được tạo để thay đổi thuộc tính đối tượng (INotifyPropertyChanged) và để thêm hoặc xóa các thành phần khỏi bộ sưu tập (INotifyCollectionChanged);
  • Khi đăng ký được kích hoạt, đối tượng đã thay đổi sẽ được thêm vào hàng đợi để ghi vào bộ lưu trữ dữ liệu;
  • Các thay đổi đối với bộ lưu trữ được lưu định kỳ (theo bộ hẹn giờ) trong luồng nền;
  • Khi bạn thoát khỏi ứng dụng, các thay đổi cũng được lưu vào bộ nhớ.

Ví dụ về mã

Thêm các phụ thuộc cần thiết

// Основная библиотека
Install-Package OutCode.EscapeTeams.ObjectRepository
    
// Хранилище данных, в котором будут сохраняться изменения
// Используйте то, которым будете пользоваться.
Install-Package OutCode.EscapeTeams.ObjectRepository.File
Install-Package OutCode.EscapeTeams.ObjectRepository.LiteDb
Install-Package OutCode.EscapeTeams.ObjectRepository.AzureTableStorage
    
// Опционально - если нужно хранить модель данных для Hangfire
// Install-Package OutCode.EscapeTeams.ObjectRepository.Hangfire

Chúng tôi mô tả mô hình dữ liệu sẽ được lưu trữ trong bộ lưu trữ

public class ParentEntity : BaseEntity
{
    public ParentEntity(Guid id) => Id = id;
}
    
public class ChildEntity : BaseEntity
{
    public ChildEntity(Guid id) => Id = id;
    public Guid ParentId { get; set; }
    public string Value { get; set; }
}

Sau đó, mô hình đối tượng:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Пример связи 1:Many
    public IEnumerable<ChildModel> Children => Multiple<ChildModel>(x => x.ParentId);
    
    protected override BaseEntity Entity { get; }
}
    
public class ChildModel : ModelBase
{
    private ChildEntity _childEntity;
    
    public ChildModel(ChildEntity entity)
    {
        _childEntity = entity;
    }
    
    public ChildModel() 
    {
        _childEntity = new ChildEntity(Guid.NewGuid());
    }
    
    public Guid ParentId
    {
        get => _childEntity.ParentId;
        set => UpdateProperty(() => _childEntity.ParentId, value);
    }
    
    public string Value
    {
        get => _childEntity.Value;
        set => UpdateProperty(() => _childEntity.Value, value
    }
    
    // Доступ с поиском по индексу
    public ParentModel Parent => Single<ParentModel>(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Và cuối cùng, lớp kho lưu trữ để truy cập dữ liệu:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Для тестов, позволяет не сохранять изменения в базу
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Если используется Hangfire и необходимо хранить модель данных для Hangfire в ObjectRepository
        // this.RegisterHangfireScheme(); 
    
        Initialize();
    }
}

Tạo một phiên bản ObjectRepository:

var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
    
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();

Nếu dự án sẽ sử dụng HangFire

public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
    services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}

Chèn một đối tượng mới:

var newParent = new ParentModel()
repository.Add(newParent);

Với lệnh gọi này, đối tượng Mô hình cha mẹ được thêm vào cả bộ đệm cục bộ và hàng đợi để ghi vào cơ sở dữ liệu. Do đó, thao tác này mất O(1) và đối tượng này có thể được làm việc ngay lập tức.

Ví dụ: để tìm đối tượng này trong kho lưu trữ và xác minh rằng đối tượng được trả về là cùng một phiên bản:

var parents = repository.Set<ParentModel>();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));

Điều gì xảy ra? Bộ () trả lại BảngTừ Điển, trong đó có chứa Từ điển đồng thời và cung cấp chức năng bổ sung của các chỉ mục chính và phụ. Điều này cho phép bạn có các phương thức tìm kiếm theo Id (hoặc các chỉ mục người dùng tùy ý khác) mà không cần lặp lại hoàn toàn trên tất cả các đối tượng.

Khi thêm các đối tượng vào Kho lưu trữ đối tượng đăng ký được thêm vào để thay đổi thuộc tính của chúng, do đó, bất kỳ thay đổi nào về thuộc tính cũng dẫn đến việc đối tượng này được thêm vào hàng đợi ghi. 
Cập nhật các thuộc tính từ bên ngoài trông giống như làm việc với đối tượng POCO:

myParent.Children.First().Property = "Updated value";

Bạn có thể xóa một đối tượng theo những cách sau:

repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove<ParentModel>(x => !x.Children.Any());

Điều này cũng thêm đối tượng vào hàng đợi xóa.

Tiết kiệm hoạt động như thế nào?

Kho lưu trữ đối tượng khi các đối tượng được giám sát thay đổi (thêm hoặc xóa hoặc thay đổi thuộc tính), sẽ xuất hiện một sự kiện Đã thay đổi mẫuđăng ký Lưu trữ. Triển khai Lưu trữ khi một sự kiện xảy ra Đã thay đổi mẫu các thay đổi được đưa vào 3 hàng đợi - để thêm, cập nhật và xóa.

Ngoài ra việc triển khai Lưu trữ khi khởi tạo, họ tạo một bộ đếm thời gian để lưu các thay đổi sau mỗi 5 giây. 

Ngoài ra, còn có một API để buộc lưu cuộc gọi: ObjectRepository.Save().

Trước mỗi lần lưu, các thao tác vô nghĩa trước tiên sẽ bị xóa khỏi hàng đợi (ví dụ: các sự kiện trùng lặp - khi một đối tượng được thay đổi hai lần hoặc các đối tượng được thêm/xóa nhanh chóng) và chỉ sau đó mới tự lưu. 

Trong mọi trường hợp, toàn bộ đối tượng hiện tại sẽ được lưu, do đó có thể các đối tượng được lưu theo thứ tự khác với thứ tự chúng đã được thay đổi, bao gồm các phiên bản mới hơn của đối tượng so với thời điểm chúng được thêm vào hàng đợi.

Còn gì nữa không?

  • Tất cả các thư viện đều dựa trên .NET Standard 2.0. Có thể được sử dụng trong bất kỳ dự án .NET hiện đại nào.
  • API là luồng an toàn. Bộ sưu tập nội bộ được thực hiện dựa trên từ điển đồng thời, trình xử lý sự kiện có khóa hoặc không cần chúng. 
    Điều duy nhất đáng nhớ là gọi ObjectRepository.Save();
  • Chỉ số tùy ý (yêu cầu tính duy nhất):

repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");

Ai sử dụng nó?

Cá nhân tôi bắt đầu sử dụng phương pháp này trong tất cả các dự án sở thích vì nó thuận tiện và không yêu cầu chi phí lớn để viết lớp truy cập dữ liệu hoặc triển khai cơ sở hạ tầng nặng. Cá nhân tôi, việc lưu trữ dữ liệu trong litdb hoặc một tệp thường là đủ đối với tôi. 

Nhưng trước đây, khi công ty khởi nghiệp EscapeTeams hiện không còn tồn tại (Tôi tưởng đây là tiền - nhưng không, hãy trải nghiệm lại) - dùng để lưu trữ dữ liệu trong Azure Table Storage.

Kế hoạch cho tương lai

Tôi muốn khắc phục một trong những nhược điểm chính của phương pháp này - chia tỷ lệ theo chiều ngang. Để làm điều này, bạn cần các giao dịch phân tán (sic!), hoặc đưa ra quyết định có chủ ý rằng cùng một dữ liệu từ các trường hợp khác nhau sẽ không thay đổi hoặc để chúng thay đổi theo nguyên tắc “ai là người cuối cùng là đúng”.

Từ quan điểm kỹ thuật, tôi thấy sơ đồ sau là khả thi:

  • Lưu trữ EventLog và Snapshot thay vì mô hình đối tượng
  • Tìm các phiên bản khác (thêm điểm cuối của tất cả các phiên bản vào cài đặt? khám phá udp? chính/nô lệ?)
  • Sao chép giữa các phiên bản EventLog thông qua bất kỳ thuật toán đồng thuận nào, chẳng hạn như RAFT.

Ngoài ra còn có một vấn đề khác khiến tôi lo lắng - xóa tầng hoặc phát hiện các trường hợp xóa đối tượng có liên kết từ đối tượng khác. 

Mã nguồn

Nếu bạn đã đọc đến đây thì tất cả những gì còn lại là đọc mã; nó có thể được tìm thấy trên GitHub:
https://github.com/DiverOfDark/ObjectRepository

Nguồn: www.habr.com

Thêm một lời nhận xét