ทำไมต้องเก็บข้อมูลทั้งหมดไว้ในหน่วยความจำ?
ในการจัดเก็บข้อมูลเว็บไซต์หรือแบ็กเอนด์ ความปรารถนาแรกของคนที่มีสติส่วนใหญ่จะเลือกฐานข้อมูล SQL
แต่บางครั้งความคิดก็เข้ามาในใจว่าโมเดลข้อมูลไม่เหมาะกับ SQL ตัวอย่างเช่น เมื่อสร้างการค้นหาหรือกราฟโซเชียล คุณต้องค้นหาความสัมพันธ์ที่ซับซ้อนระหว่างออบเจ็กต์
สถานการณ์ที่เลวร้ายที่สุดคือเมื่อคุณทำงานเป็นทีมและเพื่อนร่วมงานไม่ทราบวิธีสร้างแบบสอบถามอย่างรวดเร็ว คุณใช้เวลาเท่าไรในการแก้ปัญหา N+1 และสร้างดัชนีเพิ่มเติมเพื่อให้ SELECT บนหน้าหลักเสร็จสิ้นภายในระยะเวลาที่เหมาะสม
อีกแนวทางหนึ่งที่ได้รับความนิยมคือ NoSQL เมื่อหลายปีก่อนมีกระแสฮือฮามากมายในหัวข้อนี้ - สำหรับโอกาสที่สะดวกใดๆ พวกเขาปรับใช้ MongoDB และพอใจกับคำตอบในรูปแบบของเอกสาร json (ยังไงก็ตาม คุณต้องใส่ไม้ค้ำยันกี่อันเพราะลิงก์แบบวงกลมในเอกสาร).
ฉันขอแนะนำให้ลองใช้วิธีอื่น - ทำไมไม่ลองจัดเก็บข้อมูลทั้งหมดไว้ในหน่วยความจำของแอปพลิเคชันโดยบันทึกลงในที่เก็บข้อมูลแบบสุ่มเป็นระยะ (ไฟล์, ฐานข้อมูลระยะไกล)
หน่วยความจำมีราคาถูกลง และข้อมูลที่เป็นไปได้สำหรับโปรเจ็กต์ขนาดเล็กและขนาดกลางส่วนใหญ่จะพอดีกับหน่วยความจำขนาด 1 GB (ยกตัวอย่างโครงการบ้านที่ฉันชอบคือ
จุดเด่น:
- การเข้าถึงข้อมูลจะง่ายขึ้น - คุณไม่จำเป็นต้องกังวลเกี่ยวกับการสืบค้น การโหลดแบบ 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:
ที่มา: will.com