Verwalten von Datenbeziehungen im verteilten Cache

Autor: Iqbal Khan

Einführung

Mit einem verteilten Cache können Sie die Anwendungsleistung und Skalierbarkeit deutlich verbessern. Denn ein In-Memory-Cache ermöglicht einen deutlich schnelleren Datenzugriff als eine Datenbank. Anders als bei Datenbanken können Sie die Anzahl der verwendeten Cache-Server erhöhen, um die Speicherkapazität zu erhöhen und mehr Anfragen pro Sekunde zu verarbeiten. Dies erhöht die Skalierbarkeit.

Leider leiden viele In-Memory-Caches darunter, dass sie zur Datenspeicherung einfache Hash-Tabellen mit Schlüssel-Wert-Paaren verwenden, die für relationale Daten nicht effektiv sind. Im Wesentlichen wird jedes Element unabhängig im Cache gespeichert, ohne dass andere zugehörige Elemente bekannt sind. Dies erschwert es Anwendungen, die Beziehungen zwischen zwischengespeicherten Elementen zu verfolgen. Wird ein Element in der Datenbank aktualisiert oder entfernt, können sich auch zugehörige Elemente ändern, ohne dass der Cache davon Kenntnis erhält und diese Änderungen berücksichtigen kann.

Eine typische reale Anwendung befasst sich mit relationalen Daten, die Eins-zu-Eins-, Viele-zu-Eins-, Eins-zu-Viele- und Viele-zu-Viele-Beziehungen zu anderen Datenelementen in der Datenbank haben. Dies erfordert die Aufrechterhaltung der referenziellen Integrität über verschiedene verwandte Datenelemente hinweg. Deshalb, um Wahrung der Datenintegrität im Cache, muss der Cache diese Beziehungen verstehen und die gleiche referenzielle Integrität beibehalten.

Eine Möglichkeit, diese Beziehungen aufrechtzuerhalten, ist die von Microsoft eingeführte ASP.NET-Cache-Abhängigkeit. In diesem Fall werden beim Aktualisieren oder Entfernen eines zwischengespeicherten Elements alle zugehörigen Elemente automatisch ungültig, um die Datenintegrität zu gewährleisten. Wenn die Anwendung die zugehörigen Elemente im Cache nicht finden kann, fragt sie die Datenbank nach den neuesten Versionen ab, stellt diese im Cache wieder her und stellt die referenzielle Integrität wieder her.

Allerdings ist ASP.NET Cache auf Einzelserverumgebungen beschränkt. Aus Skalierbarkeitsgründen ist daher ein verteilter Cache ist erforderlich, um unabhängig und über mehrere Server hinweg zu arbeiten. Glücklicherweise NCache bietet dieselbe Cache-Abhängigkeitsfunktion in einer verteilten Umgebung.

Obwohl NCache bietet verschiedene Arten von Abhängigkeiten, einschließlich Datenabhängigkeit, Dateiabhängigkeit, SQL-Abhängigkeit und Benutzerdefinierte AbhängigkeitIn diesem Artikel wird nur die Datenabhängigkeit für die Handhabung von Beziehungen zwischen zwischengespeicherten Elementen erläutert.

Datenabhängigkeit im Cache verwenden?

Nachfolgend finden Sie ein kurzes Beispiel für die Verwendung der Datenabhängigkeit zum Festlegen einer mehrstufigen Abhängigkeit.

public static void CreateDependencies(ICache _cache)
{
    try
    {
        string keyC = "objectC-1000";
        Object objC = new Object();
        string keyB = "objectB-1000";
        Object objB = new Object();
        string keyA = "objectA-1000";
        Object objA = new Object();
        // Initializing cacheItems
        var itemOne = new CacheItem(objA);
        var itemTwo = new CacheItem(objB);
        var itemThree = new CacheItem(objC);
        // Adding objA dependent on objB
        itemOne.Dependency = new KeyDependency(keyB);
      // Adding objB dependent on objC
        itemTwo.Dependency = new KeyDependency(keyC);
        //Adding items to cache
        _cache.Add(keyC, itemThree);
        _cache.Add(keyB, itemTwo);
        _cache.Add(keyA, itemOne);

        // Removing "objC" automatically removes "objB" as well as "objA"
        _cache.Remove(keyC);
        _cache.Dispose();
    }
    catch (Exception e)
    {
        throw;
    }
}

Datenbeziehungen

Das folgende Beispiel wird in diesem Artikel verwendet, um zu demonstrieren, wie verschiedene Arten von Beziehungen im Cache behandelt werden.

Verwalten von Datenbeziehungen
Abbildung 2: Beziehungen in der Datenbank

Im oben gezeigten Diagramm sind folgende Zusammenhänge dargestellt:

  • Einer zu vielen: Es gibt zwei solcher Beziehungen:
    1. Kunde zur Bestellung
    2. Produkt auf Bestellung
  • Viele zu eins: Es gibt zwei solcher Beziehungen:
    1. Bestellung an den Kunden
    2. Bestellung zum Produkt
  • Viele zu Viele: Es gibt eine solche Beziehung: Kunde zu Produkt (über Bestellung)

Für die oben genannten Beziehungen werden die folgenden Domänenobjekte entworfen.

class Customer
    {
        public string CustomerID;
        public string CompanyName;
        public string ContactName;
        public string ContactTitle;
        public string Phone;
        public string Country;
        public IList<Order> _OrderList;
    }
 class Product
    {
        public int ProductID;
        public string ProductName;
        public Decimal UnitPrice;
        public int UnitsInStock;
        public int UnitsOnOrder;
        public int ReorderLevel;
        public IList<Order> _OrderList;
    }
 class Order
    {
        public int OrderId;
        public string CustomerID;
        public DateTime OrderDate;
        public DateTime RequiredDate;
        public DateTime ShippedDate;
        public int ProductID;
        public Decimal UnitPrice;
        public int Quantity;
        public Single Discount;
        public Customer _Customer;
        public Product _Product;
    }

Wie Sie sehen können, enthalten die Klassen „Customer“ und „Product“ eine _Bestellliste, die eine Liste aller Order-Objekte enthält, die mit diesem Kunden verknüpft sind. Ebenso enthält die Klasse Order die _Kunde und _Produkt Datenelemente, die auf das zugehörige Kunden- oder Produktobjekt verweisen. Der für das Laden der Daten verantwortliche Quellcode muss nun sicherstellen, dass beim Abrufen eines Kunden auch alle zugehörigen Bestellungen geladen werden.

Das folgende Beispiel zeigt, wie jede dieser Beziehungen im Cache verwaltet wird.

Umgang mit Eins-zu-Eins-/Viele-zu-Eins-Beziehungen

Wenn Sie ein Objekt aus dem Cache abrufen, das eine 1:1- oder n:1-Beziehung zu einem anderen Objekt hat, hat Ihr Quellcode möglicherweise auch das zugehörige Objekt geladen. Dies ist jedoch nicht immer erforderlich, da die Anwendung es möglicherweise gerade nicht benötigt. Wenn Ihr Quellcode das zugehörige Objekt geladen hat, müssen Sie es verarbeiten.

Es gibt zwei Möglichkeiten, damit umzugehen. Ich werde die eine als optimistisch und die andere als pessimistisch bezeichnen und sie im Folgenden jeweils erläutern:

  1. Optimistischer Umgang mit Beziehungen: Dabei gehen wir davon aus, dass trotz bestehender Beziehungen niemand das zugehörige Objekt separat ändert. Wer die zugehörigen Objekte ändern möchte, ruft sie über das primäre Objekt im Cache ab und kann so sowohl das primäre als auch das zugehörige Objekt ändern. In diesem Fall müssen wir diese beiden Objekte nicht separat im Cache speichern. Das primäre Objekt enthält also das zugehörige Objekt, und beide werden als ein Cache-Element gespeichert. Es entsteht keine Datenabhängigkeit zwischen ihnen.
  2. Pessimistischer Umgang mit Beziehungen: In diesem Fall gehen Sie davon aus, dass das zugehörige Objekt von einem anderen Benutzer unabhängig abgerufen und aktualisiert werden kann. Daher muss das zugehörige Objekt als separates Cache-Element gespeichert werden. Wenn dann jemand das zugehörige Objekt aktualisiert oder entfernt, soll auch Ihr primäres Objekt aus dem Cache entfernt werden. In diesem Fall erstellen Sie eine Datenabhängigkeit zwischen den beiden Objekten.

Nachfolgend finden Sie den Quellcode für den optimistischen Ansatz. Bitte beachten Sie, dass sowohl das primäre Objekt als auch die zugehörigen Objekte als ein Element zwischengespeichert werden, da die Serialisierung des primären Objekts auch die zugehörigen Objekte einschließen würde.

static void Main(string[] args)
{
    string cacheName = "myReplicatedCache";
    ICache _cache = CacheManager.GetCache(cacheName);
    OrderFactory oFactory = new OrderFactory();
    Order order = new Order();
    order.OrderId = 1000;
    oFactory.LoadFromDb(order);
    Customer cust = order._Customer;
    Product prod = order._Product;
    var itemOne = new CacheItem(order);
    // please note that Order object serialization will
    // also include Customer and Product objects
    _cache.Add(order.OrderId.ToString(), itemOne);
    _cache.Dispose();
}

Unten finden Sie den Quellcode für die Handhabung des pessimistischen Ansatzes, da das optimistische Szenario keine Verwendung von Datenabhängigkeit erfordert.

static void Main(string[] args)
{
    string cacheName = "myReplicatedCache";
    ICache _cache = CacheManager.GetCache(cacheName);
    OrderFactory oFactory = new OrderFactory();
    Order order = new Order();
    order.OrderId = 1000;
    oFactory.LoadFromDb(order);
    Customer cust = order._Customer;
    Product prod = order._Product;
    string custKey = "Customer:CustomerID:" + cust.CustomerID;
    _cache.Insert(custKey, cust);
    string prodKey = "Product:ProductID:" + prod.ProductID;
    _cache.Insert(prodKey, prod);
    string[] depKeys = { prodKey, custKey };
    string orderKey = "Order:OrderID:" + order.OrderId;
    // We are setting _Customer and _Product to null so they
    // don't get serialized with Order object
    order._Customer = null;
    order._Product = null;
    var item = new CacheItem(order);
    item.Dependency = new CacheDependency(null, depKeys);
    _cache.Add(orderKey, item);
    _cache.Dispose();
}

Der obige Code lädt ein Order-Objekt aus der Datenbank, während die Customer- und Product-Objekte automatisch geladen werden, da das Order-Objekt eine n:1-Beziehung zu ihnen hat. Die Anwendung fügt zuerst die Customer- und Product-Objekte zum Cache hinzu, gefolgt vom Order-Objekt, das von beiden abhängig ist. Auf diese Weise wird das Order-Objekt automatisch aus dem Cache entfernt, wenn eines dieser Objekte aktualisiert oder im Cache gelöscht wird, um die Datenintegrität zu wahren. Die Anwendung muss diese Beziehung nicht nachverfolgen.

Umgang mit Eins-zu-Viele-Beziehungen

Wenn Sie ein Objekt aus dem Cache holen, das eine 1:n-Beziehung zu einem anderen Objekt hat, lädt Ihr Quellcode möglicherweise sowohl das primäre Objekt als auch eine Sammlung aller zugehörigen Objekte. Das Laden der zugehörigen Objekte ist jedoch nicht immer notwendig, da die Anwendung diese möglicherweise gerade nicht benötigt. Wenn die zugehörigen Objekte geladen werden, führt die Tatsache, dass sie sich in einer Sammlung befinden, zu weiteren Problemen, die im Folgenden erläutert werden:

  1. Optimistischer Umgang mit Beziehungen: In diesem Fall werden die verknüpften Objekte separat geändert, obwohl sie den vorherigen Datenbeziehungen unterliegen. Wer die verknüpften Objekte ändern möchte, ruft diese über das primäre Objekt im Cache ab und kann somit sowohl das primäre als auch das verknüpfte Objekt ändern. Somit müssen wir diese beiden Objekte nicht separat im Cache speichern.
  2. Leicht pessimistischer Umgang mit Beziehungen: Bei diesem Ansatz werden verwandte Objekte nicht einzeln, sondern nur als Teil einer vollständigen Sammlung abgerufen. Dadurch wird die gesamte Sammlung als einzelnes zwischengespeichertes Element gespeichert, das vom primären Objekt abhängig ist. Auf diese Weise wird beim Aktualisieren oder Entfernen des primären Objekts auch die verwandte Sammlung ungültig, um die Konsistenz zu wahren.
  3. Wirklich pessimistischer Umgang mit Beziehungen: In diesem Fall gehen Sie davon aus, dass alle Objekte der zugehörigen Sammlung auch einzeln von der Anwendung abgerufen und bearbeitet werden können. Daher müssen Sie nicht nur die Sammlung, sondern auch alle darin enthaltenen Einzelobjekte separat im Cache speichern. Beachten Sie, dass dies wahrscheinlich zu Leistungsproblemen führen würde, da Sie mehrfach auf den Cache zugreifen, der sich möglicherweise über das Netzwerk auf einem Cache-Server befindet.

Nachfolgend finden Sie ein Beispiel, wie Sie Eins-zu-viele-Beziehungen optimistisch handhaben können.

static void Main(string[] args)
{
    string cacheName = "ltq";
    ICache _cache = CacheManager.GetCache(cacheName);
    CustomerFactory cFactory = new CustomerFactory();
    Customer cust = new Customer();
    cust.CustomerID = "ALFKI";
    cFactory.LoadFromDb(cust);
    // please note that _OrderList will automatically get
    // serialized along with the Customer object
    string custKey = "Customer:CustomerID:" + cust.CustomerID;
    _cache.Add(custKey, cust);
    _cache.Dispose();
}

Nachfolgend finden Sie ein Beispiel für den leicht pessimistischen Umgang mit einer Eins-zu-viele-Beziehung.

static void Main(string[] args)
{
    string cacheName = "myReplicatedCache";
    ICache _cache = CacheManager.GetCache(cacheName);
    CustomerFactory cFactory = new CustomerFactory();
    Customer cust = new Customer();
    cust.CustomerID = "ALFKI";
    cFactory.LoadFromDb(cust);
    IList<Order> orderList = cust._OrderList;
    // please note that _OrderList will not be 
    // serialized along with the Customer object
    cust._OrderList = null;
    string custKey = "Customer:CustomerID:" + cust.CustomerID;
    var custItem = new CacheItem(cust);
    _cache.Add(custKey, custItem);
    // let's reset the _OrderList back
    cust._OrderList = orderList;
    string[] depKeys = { custKey };
    string orderListKey = "Customer:OrderList:CustomerId" + cust.CustomerID;
    IDictionary<string, CacheItem> dictionary = new Dictionary<string, CacheItem>();
    foreach (var order in orderList)
    {
        var orderItem = new CacheItem(order);
        orderItem.Dependency = new CacheDependency(null, depKeys);
        dictionary.Add(orderListKey, orderItem);

    }
    _cache.AddBulk(dictionary);
    _cache.Dispose();
}

Im obigen Beispiel speichert ein separater Cache die Liste der mit dem Kunden verknüpften Bestellobjekte. Die gesamte Sammlung wird als ein Element zwischengespeichert, da wir davon ausgehen, dass niemand einzelne Bestellobjekte direkt separat ändert. Die Anwendung ruft sie immer über den Kunden ab und ändert und speichert die gesamte Sammlung erneut zwischen.

Ein anderer Fall ist der pessimistische Umgang mit Eins-zu-vielen-Beziehungen, der dem Umgang mit Sammlungen im Cache ähnelt. Dieses Thema wird im nächsten Abschnitt behandelt.

Umgang mit Sammlungen im Cache

Es gibt viele Situationen, in denen Sie eine Sammlung von Objekten aus der Datenbank abrufen. Dies kann auf eine von Ihnen ausgeführte Abfrage zurückzuführen sein oder auf eine 1:n-Beziehung, die eine Sammlung verwandter Objekte auf der n-Seite zurückgibt. In beiden Fällen erhalten Sie eine Sammlung von Objekten, die im Cache entsprechend behandelt werden müssen.

Es gibt zwei Möglichkeiten, mit Sammlungen umzugehen, wie unten erläutert:

  1. Optimistischer Umgang mit Sammlungen: Dabei gehen wir davon aus, dass die gesamte Sammlung als ein Element zwischengespeichert werden sollte, da niemand die in der Sammlung gespeicherten Objekte einzeln abrufen und ändern wird.
  2. Pessimistischer Umgang mit Sammlungen: In diesem Fall gehen wir davon aus, dass einzelne Objekte innerhalb der Sammlung separat abgerufen und geändert werden können. Daher cachen wir die gesamte Sammlung, cachen dann aber auch jedes einzelne Objekt und erstellen eine Abhängigkeit von der Sammlung zu den einzelnen Objekten.

Nachfolgend finden Sie ein Beispiel für den optimistischen Umgang mit Sammlungen.

static void Main(string[] args)
{
    string cacheName = "myReplicatedCache";
    ICache _cache = CacheManager.GetCache(cacheName);
    CustomerFactory cFactory = new CustomerFactory();
    Customer cust = new Customer();
    string custListKey = "CustomerList:LoadByCountry:Country:United States";
    IList<Customer> custList = cFactory.LoadByCountry("United States");
    IDistributedList<Customer> list = _cache.DataTypeManager.CreateList<Customer>(custListKey);

    // please note that all Customer objects kept in custList
    // will be serialized along with the custList
    foreach (var customer in custList)
    {
        // Add products to list
        list.Add(customer);
    }
    _cache.Dispose();
}

Im oben beschriebenen Beispiel wird die gesamte Sammlung als ein Element zwischengespeichert und alle in der Sammlung enthaltenen Kundenobjekte werden automatisch serialisiert. Daher ist es hier nicht erforderlich, eine Datenabhängigkeit zu erstellen.

Nachfolgend finden Sie ein Beispiel für den pessimistischen Umgang mit Sammlungen.

static void Main(string[] args)
{
    string cacheName = "myReplicatedCache";
    ICache _cache = CacheManager.GetCache(cacheName);
    CustomerFactory cFactory = new CustomerFactory();
    Customer cust = new Customer();
    IList<Customer> custList = cFactory.LoadByCountry("United States");
    ArrayList custKeys = new ArrayList();
    // Let's cache individual Customer objects and also build
    // an array of keys to be used later in CacheDependency
    foreach (Customer c in custList)
    {
        string custKey = "Customer:CustomerID:" + c.CustomerID;
        custKeys.Add(custKey);
        _cache.Insert(custKey, c);
    }
    string custListKey = "CustomerList:LoadByCountry:Country:United States";
    // please note that this collection has a dependency on all
    // objects in it separately. So, if any of them are updated or
    // removed, this collection will also be removed from cache
    IDistributedList<Customer> list = _cache.DataTypeManager.CreateList<Customer>(custListKey);
    foreach (var customer in custList)
    {
        var item = new CacheItem(customer);
        item.Dependency = new CacheDependency(null, (string[])custKeys.ToArray());
        list.Add(customer);
    }

    _cache.Dispose();
}

Im oben gezeigten Beispiel wird jedes Objekt in der Sammlung als separates Element zwischengespeichert, und dann wird die gesamte Sammlung sowie ein Element zwischengespeichert. Die Sammlung hat eine Datenabhängigkeit auf alle darin enthaltenen Objekte, die separat zwischengespeichert werden. Auf diese Weise wird die Sammlung auch aus dem Cache entfernt, wenn eines dieser Objekte aktualisiert oder entfernt wird.


Autor: Iqbal Khan arbeitet für Alachisoft, ein führendes Softwareunternehmen, das verteilte Caching-Lösungen für .NET anbietet. Sie erreichen ihn unter iqbal@alachisoft.com.

© Copyright Alachisoft 2002 - Alle Rechte vorbehalten NCache ist eine eingetragene Marke der Diyatech Corp.