분산 캐시에서 데이터 관계 관리

저자: 이크발 칸

개요

분산 캐시를 사용하면 애플리케이션 성능과 확장성을 크게 향상시킬 수 있습니다. 메모리 내 캐시는 데이터베이스보다 데이터 액세스 속도가 훨씬 빠르기 때문입니다. 마찬가지로, 데이터베이스와 달리 사용하는 캐시 서버 수를 늘려 저장 용량을 늘리고 초당 더 많은 요청을 처리할 수 있으므로 확장성이 향상됩니다.

안타깝게도 많은 인메모리 캐시는 키-값 쌍을 사용하는 단순한 해시 테이블을 사용하여 데이터를 저장한다는 점에서 문제가 있는데, 이는 관계형 데이터에는 효과적이지 않습니다. 기본적으로 각 항목은 다른 관련 항목에 대한 정보 없이 캐시에 독립적으로 저장됩니다. 이로 인해 애플리케이션이 캐시된 항목 간의 관계를 추적하기 어렵습니다. 데이터베이스에서 한 항목이 업데이트되거나 제거되면 관련 항목도 변경될 수 있지만, 캐시는 이를 인지하지 못하고 이러한 변경 사항을 반영하지 못합니다.

일반적인 실제 응용 프로그램은 데이터베이스의 다른 데이터 요소와 일대일, 다대일, 일대다 및 다대다 관계가 있는 관계형 데이터를 처리합니다. 이를 위해서는 서로 다른 관련 데이터 요소에서 참조 무결성을 유지해야 합니다. 따라서 하기 위해서는 캐시의 데이터 무결성 유지, 캐시는 이러한 관계를 이해하고 동일한 참조 무결성을 유지해야 합니다.

이러한 관계를 유지하는 한 가지 방법은 Microsoft에서 도입한 ASP.NET 캐시 종속성입니다. 이 경우 캐시된 항목이 업데이트되거나 제거되면 데이터 무결성을 유지하기 위해 관련된 모든 항목이 자동으로 무효화됩니다. 애플리케이션이 캐시에서 관련 항목을 찾을 수 없는 경우, 데이터베이스에서 최신 버전을 쿼리하여 캐시에 복원하고 참조 무결성을 다시 설정합니다.

그러나 ASP.NET Cache는 단일 서버 환경으로 제한되므로 확장성을 위해 분산 캐시 여러 서버에서 독립적으로 운영해야 합니다. 다행히도 NCache 분산 환경에서 동일한 캐시 종속성 기능을 제공합니다.

이기는하지만 NCache 다음을 포함한 다양한 유형의 종속성을 제공합니다. 데이터 종속성, 파일 종속성, SQL 종속성예산 및 사용자 지정 종속성, 이 문서에서는 캐시된 항목 간의 관계를 처리하기 위한 데이터 종속성에 대해서만 설명합니다.

캐시에서 데이터 종속성을 사용하시나요?

아래는 데이터 종속성을 사용하여 다중 수준 종속성을 지정하는 방법에 대한 간단한 예입니다.

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

데이터 관계

이 문서에서는 다음 예를 통해 캐시에서 다양한 유형의 관계가 어떻게 처리되는지 보여줍니다.

데이터 관계 관리
그림 2: 데이터베이스의 관계

위에 표시된 다이어그램에서는 다음과 같은 관계가 표시됩니다.

  • 일대다: 이러한 관계는 두 가지가 있습니다.
    1. 고객 주문
    2. 주문할 제품
  • 다대일: 이러한 관계는 두 가지가 있습니다.
    1. 고객에게 주문
    2. 제품 주문
  • 많은 많은: 고객과 제품 간의 관계(주문을 통해)는 다음과 같습니다.

위의 관계에 대해 다음과 같은 도메인 개체가 설계되었습니다.

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

보시다시피 Customer 및 Product 클래스에는 _주문 목록이 고객과 관련된 모든 Order 객체 목록을 포함하는 클래스입니다. 마찬가지로 Order 클래스에는 다음이 포함됩니다. _고객 _제품 관련 Customer 또는 Product 객체를 가리키는 데이터 멤버. 이제 데이터 로드를 담당하는 소스 코드는 Customer 객체를 가져올 때마다 관련된 모든 Order 객체도 로드되도록 해야 합니다.

다음 예제는 이러한 각 관계가 캐시에서 어떻게 관리되는지 보여줍니다.

일대일/다대일 관계 처리

캐시에서 다른 객체와 일대일 또는 다대일 관계를 가진 객체를 가져올 때마다 소스 코드에서 관련 객체도 로드했을 수 있습니다. 하지만 애플리케이션에서 해당 객체가 필요하지 않을 수도 있으므로 항상 관련 객체를 로드할 필요는 없습니다. 소스 코드에서 관련 객체를 로드했다면 해당 객체를 처리해야 합니다.

이 문제를 해결하는 방법은 두 가지가 있습니다. 하나는 낙관적인 방법이고 다른 하나는 비관적인 방법이라고 부르고, 아래에서 각각에 대해 설명하겠습니다.

  1. 관계의 낙관적 처리: 여기서는 관계가 존재하더라도 다른 누구도 관련 객체를 개별적으로 수정하지 않는다고 가정합니다. 관련 객체를 수정하려는 사람은 캐시에 있는 기본 객체를 통해 객체를 가져오므로 기본 객체와 관련 객체를 모두 수정할 수 있습니다. 이 경우 두 객체를 캐시에 별도로 저장할 필요가 없습니다. 따라서 기본 객체는 관련 객체를 포함하고, 두 객체는 ​​캐시에 하나의 캐시된 항목으로 저장됩니다. 또한, 두 객체 간에 데이터 종속성은 생성되지 않습니다.
  2. 비관적인 관계 처리: 이 경우, 관련 객체를 다른 사용자가 독립적으로 가져오고 업데이트할 수 있다고 가정하므로 관련 객체는 별도의 캐시된 항목으로 저장되어야 합니다. 그런 다음 누군가 관련 객체를 업데이트하거나 제거하면 기본 객체도 캐시에서 제거되도록 해야 합니다. 이 경우 두 객체 간에 데이터 종속성을 생성합니다.

낙관적 접근 방식을 처리하는 소스 코드는 다음과 같습니다. 기본 객체와 관련 객체는 모두 하나의 항목으로 캐시됩니다. 기본 객체의 직렬화에는 관련 객체도 포함되기 때문입니다.

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

낙관적 시나리오에서는 데이터 종속성을 사용할 필요가 없으므로, 비관적 접근 방식을 처리하기 위한 소스 코드는 아래와 같습니다.

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

위 코드는 데이터베이스에서 Order 객체를 로드하지만, Customer 객체와 Product 객체는 모두 다대일 관계이므로 자동으로 로드됩니다. 애플리케이션은 Customer 객체와 Product 객체를 먼저 캐시에 추가한 후, 두 객체 모두에 종속되도록 설정된 Order 객체를 추가합니다. 이렇게 하면 캐시에서 이러한 객체 중 하나가 업데이트되거나 제거될 경우, 데이터 무결성을 유지하기 위해 Order 객체가 캐시에서 자동으로 제거됩니다. 애플리케이션은 이러한 관계를 추적할 필요가 없습니다.

일대다 관계 처리

캐시에서 다른 객체와 일대다 관계를 가진 객체를 가져올 때마다 소스 코드는 기본 객체와 모든 관련 객체의 컬렉션을 모두 로드할 수 있습니다. 그러나 애플리케이션에 현재 관련 객체가 필요하지 않을 수 있으므로 항상 관련 객체를 로드할 필요는 없습니다. 하지만 관련 객체를 로드하더라도 해당 객체가 컬렉션에 존재한다는 사실 때문에 아래에서 설명하는 추가적인 문제가 발생합니다.

  1. 관계에 대한 낙관적인 처리: 이 경우, 관련 객체는 이전 데이터 관계에 종속되어 있음에도 불구하고 개별적으로 수정됩니다. 관련 객체를 수정하려는 사람은 캐시의 기본 객체를 통해 객체를 가져오므로 기본 객체와 관련 객체를 모두 수정할 수 있습니다. 따라서 두 객체를 캐시에 별도로 저장할 필요가 없습니다.
  2. 관계에 대한 약간 비관적인 처리: 이 방식에서는 관련 객체를 개별적으로 가져오지 않고 전체 컬렉션의 일부로만 가져옵니다. 결과적으로 전체 컬렉션은 기본 객체에 대한 종속성을 가진 단일 캐시 항목으로 저장됩니다. 이렇게 하면 기본 객체가 업데이트되거나 제거될 경우 관련 컬렉션도 무효화되어 일관성을 유지합니다.
  3. 관계에 대한 매우 비관적인 처리: 이 경우, 관련 컬렉션의 모든 객체를 애플리케이션에서 개별적으로 가져와 수정할 수 있다고 가정합니다. 따라서 컬렉션뿐만 아니라 해당 컬렉션의 모든 개별 객체도 캐시에 별도로 저장해야 합니다. 네트워크상의 캐시 서버에 상주하는 캐시에 여러 번 접근하게 되므로 성능 문제가 발생할 수 있습니다.

아래는 일대다 관계를 낙관적으로 처리하는 방법에 대한 예입니다.

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

아래는 일대다 관계를 약간 비관적으로 처리하는 방법에 대한 예입니다.

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

위 예에서 별도의 캐시는 고객과 관련된 주문 객체 목록을 저장합니다. 전체 컬렉션은 하나의 항목으로 캐시되는데, 이는 아무도 개별 주문 객체를 개별적으로 직접 수정하지 않을 것이라고 가정하기 때문입니다. 애플리케이션은 항상 고객을 통해 해당 객체를 가져오고 전체 컬렉션을 수정하여 다시 캐시합니다.

또 다른 경우는 일대다 관계를 비관적으로 처리하는 것인데, 이는 캐시에서 컬렉션을 처리하는 방법과 유사합니다. 해당 주제는 다음 섹션에서 논의됩니다.

캐시에서 컬렉션 처리

데이터베이스에서 객체 컬렉션을 가져오는 상황은 다양합니다. 이는 실행한 쿼리 때문일 수도 있고, "다" 측에서 관련 객체 컬렉션을 반환하는 일대다 관계 때문일 수도 있습니다. 두 경우 모두 캐시에서 적절하게 처리해야 하는 객체 컬렉션을 가져오게 됩니다.

아래에 설명된 대로 컬렉션을 처리하는 방법에는 두 가지가 있습니다.

  1. 컬렉션의 낙관적 처리: 여기에서는 컬렉션 내부에 보관된 객체를 개별적으로 가져오거나 수정하는 사람이 없으므로 전체 컬렉션을 하나의 항목으로 캐시해야 한다고 가정합니다.
  2. 컬렉션의 비관적 처리: 이 경우, 컬렉션 내의 개별 객체를 개별적으로 가져와서 수정할 수 있다고 가정합니다. 따라서 전체 컬렉션을 캐시하는 동시에 각 개별 객체도 캐시하고, 컬렉션에서 개별 객체로의 종속성을 생성합니다.

다음은 컬렉션을 낙관적으로 처리하는 방법의 예입니다.

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

위에서 설명한 예에서는 전체 컬렉션이 하나의 항목으로 캐시되고, 컬렉션 내에 저장된 모든 Customer 객체는 자동으로 직렬화됩니다. 따라서 여기서는 데이터 종속성을 생성할 필요가 없습니다.

다음은 컬렉션을 비관적으로 처리하는 방법의 예입니다.

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

위에 표시된 예에서 컬렉션의 각 객체는 개별 항목으로 캐시되고, 전체 컬렉션과 한 항목이 캐시됩니다. 컬렉션에는 데이터 종속성 별도로 캐시된 모든 객체에 적용됩니다. 이렇게 하면 이러한 객체 중 하나라도 업데이트되거나 제거되면 해당 컬렉션도 캐시에서 제거됩니다.


저자: 이크발 칸(Iqbal Khan)이 근무하는 곳 Alachisoft.NET 분산 캐싱 솔루션을 제공하는 선도적인 소프트웨어 회사입니다. iqbal@alachisoft.com.

문의하기

전화

+1 214-619-2601 (미국)

© 저작권 Alachisoft 2002 - . 판권 소유. NCache 는 Diyatech Corp.의 등록상표입니다.