ObjectRepository - รูปแบบพื้นที่เก็บข้อมูลในหน่วยความจำ .NET สำหรับโปรเจ็กต์บ้านของคุณ

ทำไมต้องเก็บข้อมูลทั้งหมดไว้ในหน่วยความจำ?

ในการจัดเก็บข้อมูลเว็บไซต์หรือแบ็กเอนด์ ความปรารถนาแรกของคนที่มีสติส่วนใหญ่จะเลือกฐานข้อมูล SQL 

แต่บางครั้งความคิดก็เข้ามาในใจว่าโมเดลข้อมูลไม่เหมาะกับ SQL ตัวอย่างเช่น เมื่อสร้างการค้นหาหรือกราฟโซเชียล คุณต้องค้นหาความสัมพันธ์ที่ซับซ้อนระหว่างออบเจ็กต์ 

สถานการณ์ที่เลวร้ายที่สุดคือเมื่อคุณทำงานเป็นทีมและเพื่อนร่วมงานไม่ทราบวิธีสร้างแบบสอบถามอย่างรวดเร็ว คุณใช้เวลาเท่าไรในการแก้ปัญหา N+1 และสร้างดัชนีเพิ่มเติมเพื่อให้ SELECT บนหน้าหลักเสร็จสิ้นภายในระยะเวลาที่เหมาะสม

อีกแนวทางหนึ่งที่ได้รับความนิยมคือ NoSQL เมื่อหลายปีก่อนมีกระแสฮือฮามากมายในหัวข้อนี้ - สำหรับโอกาสที่สะดวกใดๆ พวกเขาปรับใช้ MongoDB และพอใจกับคำตอบในรูปแบบของเอกสาร json (ยังไงก็ตาม คุณต้องใส่ไม้ค้ำยันกี่อันเพราะลิงก์แบบวงกลมในเอกสาร).

ฉันขอแนะนำให้ลองใช้วิธีอื่น - ทำไมไม่ลองจัดเก็บข้อมูลทั้งหมดไว้ในหน่วยความจำของแอปพลิเคชันโดยบันทึกลงในที่เก็บข้อมูลแบบสุ่มเป็นระยะ (ไฟล์, ฐานข้อมูลระยะไกล) 

หน่วยความจำมีราคาถูกลง และข้อมูลที่เป็นไปได้สำหรับโปรเจ็กต์ขนาดเล็กและขนาดกลางส่วนใหญ่จะพอดีกับหน่วยความจำขนาด 1 GB (ยกตัวอย่างโครงการบ้านที่ฉันชอบคือ ติดตามทางการเงินซึ่งเก็บสถิติรายวันและประวัติค่าใช้จ่าย ยอดคงเหลือ และธุรกรรมของฉันเป็นเวลาหนึ่งปีครึ่ง ใช้หน่วยความจำเพียง 45 MB)

จุดเด่น:

  • การเข้าถึงข้อมูลจะง่ายขึ้น - คุณไม่จำเป็นต้องกังวลเกี่ยวกับการสืบค้น การโหลดแบบ Lazy Loading ฟีเจอร์ ORM คุณทำงานกับออบเจ็กต์ C# ธรรมดาได้
  • ไม่มีปัญหาที่เกี่ยวข้องกับการเข้าถึงจากเธรดที่แตกต่างกัน
  • รวดเร็วมาก - ไม่มีการร้องขอจากเครือข่าย ไม่มีการแปลโค้ดเป็นภาษาคิวรี ไม่จำเป็นต้อง (ยกเลิก) การทำให้เป็นอนุกรมของออบเจ็กต์
  • เป็นที่ยอมรับในการจัดเก็บข้อมูลในรูปแบบใดก็ได้ ไม่ว่าจะเป็นในรูปแบบ XML บนดิสก์ หรือใน SQL Server หรือใน Azure Table Storage

จุดด้อย:

  • การปรับขนาดแนวนอนหายไป และส่งผลให้การปรับใช้การหยุดทำงานเป็นศูนย์ไม่สามารถทำได้
  • หากแอปพลิเคชันขัดข้อง คุณอาจสูญเสียข้อมูลบางส่วน (แต่แอปพลิเคชันของเราไม่เคยขัดข้องใช่ไหม?)

มันทำงานอย่างไร

อัลกอริทึมมีดังนี้:

  • เมื่อเริ่มต้น การเชื่อมต่อกับที่จัดเก็บข้อมูลจะถูกสร้างขึ้น และจะมีการโหลดข้อมูล
  • มีการสร้างโมเดลออบเจ็กต์ ดัชนีหลัก และดัชนีเชิงสัมพันธ์ (1:1, 1:จำนวนมาก)
  • การสมัครสมาชิกถูกสร้างขึ้นสำหรับการเปลี่ยนแปลงคุณสมบัติของออบเจ็กต์ (INotifyPropertyChanged) และสำหรับการเพิ่มหรือลบองค์ประกอบในคอลเลกชัน (INotifyCollectionChanged)
  • เมื่อการสมัครสมาชิกถูกทริกเกอร์ ออบเจ็กต์ที่เปลี่ยนแปลงจะถูกเพิ่มลงในคิวสำหรับการเขียนลงที่จัดเก็บข้อมูล
  • การเปลี่ยนแปลงที่เก็บข้อมูลจะถูกบันทึกเป็นระยะ (ตามตัวจับเวลา) ในเธรดพื้นหลัง
  • เมื่อคุณออกจากแอปพลิเคชัน การเปลี่ยนแปลงจะถูกบันทึกลงที่จัดเก็บข้อมูลด้วย

ตัวอย่างโค้ด

การเพิ่มการพึ่งพาที่จำเป็น

// Основная библиотека
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

เราอธิบายแบบจำลองข้อมูลที่จะจัดเก็บไว้ในที่จัดเก็บข้อมูล

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; }
}

จากนั้นโมเดลวัตถุ:

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;
}

และสุดท้ายคือคลาส repository สำหรับการเข้าถึงข้อมูล:

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();
    }
}

สร้างอินสแตนซ์ ObjectRepository:

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

หากโครงการจะใช้ HangFire

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

การแทรกวัตถุใหม่:

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

ด้วยการเรียกนี้วัตถุ รุ่นแม่ ถูกเพิ่มลงในแคชในเครื่องและคิวสำหรับการเขียนลงฐานข้อมูล ดังนั้นการดำเนินการนี้จะใช้เวลา O(1) และวัตถุนี้สามารถทำงานได้ทันที

ตัวอย่างเช่น หากต้องการค้นหาอ็อบเจ็กต์นี้ในที่เก็บและตรวจสอบว่าอ็อบเจ็กต์ที่ส่งคืนเป็นอินสแตนซ์เดียวกัน:

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

เกิดอะไรขึ้น? ชุด () ผลตอบแทน ตารางพจนานุกรม, ซึ่งประกอบด้วย พจนานุกรมพร้อมกัน และให้ฟังก์ชันเพิ่มเติมของดัชนีหลักและรอง ซึ่งช่วยให้คุณมีวิธีการค้นหาด้วย Id (หรือดัชนีผู้ใช้อื่นๆ ที่ต้องการ) โดยไม่ต้องวนซ้ำวัตถุทั้งหมด

เมื่อเพิ่มวัตถุเข้าไป ที่เก็บวัตถุ การสมัครสมาชิกถูกเพิ่มเพื่อเปลี่ยนแปลงคุณสมบัติ ดังนั้นการเปลี่ยนแปลงคุณสมบัติใดๆ ยังส่งผลให้อ็อบเจ็กต์นี้ถูกเพิ่มในคิวการเขียนด้วย 
การอัปเดตคุณสมบัติจากภายนอกมีลักษณะเหมือนกับการทำงานกับวัตถุ POCO:

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

คุณสามารถลบวัตถุได้ด้วยวิธีต่อไปนี้:

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

นอกจากนี้ยังเพิ่มวัตถุลงในคิวการลบด้วย

การบันทึกทำงานอย่างไร?

ที่เก็บวัตถุ เมื่ออ็อบเจ็กต์ที่ถูกมอนิเตอร์เปลี่ยนแปลง (ทั้งการเพิ่มหรือการลบ หรือการเปลี่ยนแปลงคุณสมบัติ) ทำให้เกิดเหตุการณ์ขึ้น เปลี่ยนรุ่นแล้วสมัครสมาชิก ไอเอสตอเรจ. การดำเนินการ ไอเอสตอเรจ เมื่อมีเหตุการณ์เกิดขึ้น เปลี่ยนรุ่นแล้ว การเปลี่ยนแปลงจะถูกจัดเป็น 3 คิว - สำหรับการเพิ่ม, การอัพเดต และการลบ

การนำไปใช้งานอีกด้วย ไอเอสตอเรจ เมื่อเริ่มต้น จะสร้างตัวจับเวลาที่ทำให้การเปลี่ยนแปลงถูกบันทึกทุกๆ 5 วินาที 

นอกจากนี้ยังมี API เพื่อบังคับให้บันทึกการโทร: ObjectRepository.บันทึก().

ก่อนการบันทึกแต่ละครั้ง การดำเนินการที่ไม่มีความหมายจะถูกลบออกจากคิวก่อน (เช่น เหตุการณ์ที่ซ้ำกัน - เมื่อมีการเปลี่ยนแปลงอ็อบเจ็กต์สองครั้งหรือเพิ่ม/ลบอ็อบเจ็กต์อย่างรวดเร็ว) จากนั้นจึงบันทึกเท่านั้น 

ในทุกกรณี ออบเจ็กต์ปัจจุบันทั้งหมดจะถูกบันทึก ดังนั้นจึงเป็นไปได้ที่ออบเจ็กต์จะถูกบันทึกในลำดับที่แตกต่างจากที่มีการเปลี่ยนแปลง รวมถึงเวอร์ชันที่ใหม่กว่าของออบเจ็กต์ในเวลาที่เพิ่มลงในคิวด้วย

มีอะไรอีกบ้าง?

  • ไลบรารีทั้งหมดใช้ .NET Standard 2.0 สามารถใช้ในโครงการ .NET สมัยใหม่ได้
  • API มีความปลอดภัยสำหรับเธรด คอลเลกชันภายในมีการใช้งานตาม พจนานุกรมพร้อมกันตัวจัดการเหตุการณ์อาจมีการล็อกหรือไม่ต้องการ 
    สิ่งเดียวที่ควรจำคือการโทร ObjectRepository.Save();
  • ดัชนีตามอำเภอใจ (ต้องไม่ซ้ำกัน):

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

ใครใช้มัน?

โดยส่วนตัวแล้ว ฉันเริ่มใช้แนวทางนี้กับโปรเจ็กต์งานอดิเรกทั้งหมด เนื่องจากสะดวกและไม่ต้องเสียค่าใช้จ่ายจำนวนมากในการเขียนชั้นการเข้าถึงข้อมูลหรือการปรับใช้โครงสร้างพื้นฐานจำนวนมาก โดยส่วนตัวแล้วการจัดเก็บข้อมูลในรูปแบบ litedb หรือไฟล์ก็เพียงพอแล้วสำหรับฉัน 

แต่ในอดีตเมื่อ EscapeTeams สตาร์ทอัพที่ตอนนี้ปิดตัวลงแล้ว (ฉันคิดว่านี่คือเงิน แต่ไม่มี ประสบการณ์อีกครั้ง) - ใช้เพื่อจัดเก็บข้อมูลใน Azure Table Storage

แผนสำหรับอนาคต

ฉันต้องการแก้ไขข้อเสียเปรียบหลักประการหนึ่งของวิธีนี้ - การปรับสเกลแนวนอน ในการทำเช่นนี้ คุณต้องมีธุรกรรมแบบกระจาย (sic!) หรือทำการตัดสินใจอย่างแน่วแน่ว่าข้อมูลเดียวกันจากอินสแตนซ์ที่แตกต่างกันไม่ควรเปลี่ยนแปลง หรือปล่อยให้พวกเขาเปลี่ยนแปลงตามหลักการ "ใครเป็นคนสุดท้ายคือถูก"

จากมุมมองทางเทคนิค ฉันเห็นโครงร่างต่อไปนี้มากที่สุด:

  • จัดเก็บ EventLog และ Snapshot แทนโมเดลออบเจ็กต์
  • ค้นหาอินสแตนซ์อื่นๆ (เพิ่มจุดสิ้นสุดของอินสแตนซ์ทั้งหมดไปยังการตั้งค่า udp การค้นพบ master/slave?)
  • ทำซ้ำระหว่างอินสแตนซ์ EventLog ผ่านอัลกอริธึมที่เป็นเอกฉันท์ เช่น RAFT

ยังมีอีกปัญหาหนึ่งที่ทำให้ฉันกังวล - การลบแบบเรียงซ้อนหรือการตรวจจับกรณีการลบวัตถุที่มีลิงก์จากวัตถุอื่น 

รหัสที่มา

หากคุณอ่านมาจนถึงตรงนี้แล้ว สิ่งที่เหลืออยู่คือการอ่านโค้ด ซึ่งสามารถพบได้บน GitHub:
https://github.com/DiverOfDark/ObjectRepository

ที่มา: will.com

เพิ่มความคิดเห็น