Author: Iqbal Khan
With the explosion of extremely high transaction web apps, SOA, grid computing, and other server applications, data storage is unable to keep up. The reason is data storage cannot keep adding more servers to scale out, unlike extremely scalable application architectures.
In these situations, in-memory distributed cache offers an excellent solution to data storage bottlenecks. It spans multiple servers (called a cluster) to pool their memory together and keep all cache synchronized across servers. And, it can keep growing this cache cluster endlessly, just like the application servers. This reduces pressure on data storage so that it is no longer a scalability bottleneck.
There are two main ways people use a distributed cache:
Cache-aside is a very powerful technique and allows you to issue complex database queries involving joins and nested queries and manipulate data any way you want. Despite that, Read-through / Write-through has various advantages over cache-aside as mentioned below:
Read-through/Write-through is not intended to be used for all data access in your application. It is best suited for situations where you're either reading individual rows from the database or reading data that can directly map to an individual cache item. It is also ideal for reference data that is meant to be kept in the cache for frequent reads even though this data changes periodically.
A read-through handler is registered with the cache server and allows the cache to directly read data from the database. The NCache server provides a Read-through handler interface that you need to implement. This enables NCache to call your Read-through handler.
public class SqlReadThruProvider : IReadThruProvider
{
private SqlConnection _connection;
// Called upon startup to initialize connection
public void Init(IDictionary parameters, string cacheId)
{
_connection = new SqlConnection(parameters["connstring"]);
_connection.Open();
}
// Called at the end to close connection
public void Dispose()
{
_connection.Close();
}
// Responsible for loading object from external data source
public ProviderCacheItem LoadFromSource(string key)
{
string sql = "SELECT * FROM Customers WHERE ";
sql += "CustomerID = @ID";
SqlCommand cmd = new SqlCommand(sql, _connection);
cmd.Parameters.Add("@ID", System.Data.SqlDbType.VarChar);
// Let's extract actual customerID from "key"
int keyFormatLen = "Customers:CustomerID:".Length;
string custId = key.Substring(keyFormatLen,
key.Length - keyFormatLen);
cmd.Parameters["@ID"].Value = custId;
// fetch the row in the table
SqlDataReader reader = cmd.ExecuteReader();
Customers customer = new Customers();
// copy data from "reader" to "cust" object
FillCustomers(reader, customer);
ProviderCacheItem cacheItem = new ProviderCacheItem(customer);
// specify a SqlCacheDependency for this object
CacheDependency dep = new SqlCacheDependency(_connection.ToString(), cmd.ToString());
cacheItem.Dependency = dep;
return cacheItem;
}
Init()
performs certain resource allocation tasks like establishing connections to the main data source, whereas Dispose()
is meant to reset all such allocations. LoadFromSource()
is what the cache calls to read- through the objects.
Write-through handler is invoked, when the cache needs to write to the database as the cache is updated. Normally, the application issues an update to the cache through add, insert or remove.
public class SqlWriteThruProvider : IWriteThruProvider
{
private SqlConnection _connection;
// Called upon startup to initialize connection
public void Init(IDictionary parameters, string cacheId)
{
_connection = new SqlConnection((string)parameters["connstring"]);
_connection.Open();
}
// Called at the end to close connection
public void Dispose()
{
_connection.Close();
}
public OperationResult WriteToDataSource(WriteOperation operation)
{
int rowsChanged = 0;
OperationResult result = new OperationResult(operation, OperationResult.Status.Failure);
ProviderCacheItem cacheItem = operation.ProviderItem;
Customers cust = cacheItem.GetValue<Customers>();
string[] customer = {cust.Id,cust.ContactName,cust.CompanyName,
cust.Address,cust.City, cust.Country,cust.PostalCode,
cust.Phone,cust.Fax};
SqlCommand cmd = _connection.CreateCommand();
cmd.CommandText = String.Format(CultureInfo.InvariantCulture,
"Update dbo.Customers " + "Set CustomerID='{0}'," +
"ContactName='{1}',CompanyName='{2}'," +
"Address='{3}',City='{4}'," +
"Country='{5}',PostalCode='{6}'," +
"Phone='{7}',Fax='{8}'" +
"Where CustomerID = '{0}'", customer);
rowsChanged = cmd.ExecuteNonQuery();
if (rowsChanged > 0)
{
result.OperationStatus = OperationResult.Status.Success;
return result;
}
return result;
}
}
Init()
performs resource allocation tasks like establishing connections to the data source, whereas Dispose()
is meant to reset all such allocations. Save is the method the cache calls to write-through objects.
The following sample code shows using the read-through/write-through capabilities of cache from a simple Windows application
using Alachisoft.NCache.Client;
...
internal class MainForm : System.Windows.Forms.Form
{
/// Fetches record from the cache, which internally accesses the
/// datasource using read-thru provider
private void OnClickFind(object sender, System.EventArgs e)
{
Customer customer;
Cache cache = NCache.Caches[CacheName];
string key = cboCustomerID.Text.Trim();
string providerName = cboReadThruProvider.Text;
customer = (Customer) cache.Get(key,
providerName,
DSReadOption.ReadThru);
...
}
/// Updates the record using the cache, which internally accesses
/// the datasource using write-thru provider
private void OnClickUpdate(object sender, System.EventArgs e)
{
Cache cache = NCache.Caches[CacheName];
Customer customer = new Customer();
...
string key = customer.CustomerID;
string providerName = cboWriteThruProvider.Text;
cache.Insert(key, new CacheItem(customer), DSWriteOption.WriteThru, providerName, null);
...
}
}
© Copyright Alachisoft 2002 - . All rights reserved. NCache is a registered trademark of Diyatech Corp.