บทนำ
ในวัฏจักรการพัฒนาซอฟต์แวร์ มักจะเน้นที่ประสิทธิภาพของแอปพลิเคชัน มีหลายวิธีในการปรับปรุงประสิทธิภาพ และหนึ่งในรูปแบบที่ใช้บ่อยที่สุดในการปรับปรุงประสิทธิภาพในแอปพลิเคชันคลาวดสมัยใหม่คือ Cache-Aside Pattern ในบทความนี้ จะอธิบายสรุปเกี่ยวกับ Cache-Aside Pattern และการนำไปใช้กับ ASP.NET Core
Cache-Aside Pattern คืออะไร?
รูปแบบนี้ค่อนข้างตรงไปตรงมา และมีจุดประสงค์เพียงอย่างเดียวคือ โหลดข้อมูลตามความต้องการ (on-demand) เข้าไปในแคชจากแหล่งข้อมูล (data source) สิ่งนี้ช่วยรักษาความสอดคล้องระหว่างข้อมูลในแคชและแหล่งข้อมูลต้นทาง
ลักษณะสำคัญของรูปแบบนี้มีดังนี้:
- เมื่อแอปพลิเคชันต้องการข้อมูล จะตรวจสอบในแคชก่อน
- กรณีข้อมูลอยู่ในแคช แอปพลิเคชันจะใช้ข้อมูลจากแคชทันที
- กรณีข้อมูลไม่อยู่ในแคช จะดึงข้อมูลจากแหล่งข้อมูล (data source) มาใช้
แผนภาพประกอบรูปแบบ Cache-Aside Pattern:

ออบเจ็กต์แคชต้องถูกทำให้ล้มเหลว (invalidate) เมื่อมีการเปลี่ยนแปลงค่าโดยแอปพลิเคชัน

ลำดับการทำให้แคชล้มเหลวมีความสำคัญ: ต้องอัปเดตแหล่งข้อมูล (data source) ก่อน แล้วค่อยลบรายการออกจากแคช หากคุณลบรายการออกจากแคشก่อน มีโอกาสที่ไคลเอนต์อาจดึงรายการก่อนที่แหล่งข้อมูลจะถูกอัปเดต ซึ่งจะทำให้เกิดความไม่สอดคล้องของข้อมูลระหว่างแหล่งข้อมูลและแคช
เมื่อไหร่ควรใช้รูปแบบนี้?
- รูปแบบนี้ช่วยให้เราโหลดข้อมูลตามความต้องการ และสามารถใช้ได้เมื่อความต้องการทรัพยากรไม่สามารถคาดเดาได้
- แคชที่ไม่ให้บริการอпераชัน read-through และ write-through
หมายเหตุ
- Read-Through: เป็นแคชที่อยู่ในเส้นทางกับฐานข้อมูล และในกรณีของ cache miss สามารถโหลดข้อมูลจากฐานข้อมูลและเติมเข้าแคชได้
- Write-Through: แคชอยู่ในเส้นทางกับฐานข้อมูล และข้อมูลจะผ่านแคชไปยังฐานข้อมูลหลักเสมอ
สร้างทรัพยากร Azure
ตามที่แสดงไว้ข้างต้น เราต้องการฐานข้อมูล (Azure SQL Server) และแคช (Azure Redis Cache) คุณสามารถเลือกฐานข้อมูลและแคชที่สะดวกได้
$resourceGroup="<Resource Group>"
$location="<location>"
$redisCacheName="<Redis cache name>"
$sqlServerName="<Azure SQL Server Name>"
$sqlDBName="<Azure SQL DB Name>"
$adminName="<admin name of SQL server>"
$adminPassword="<admin password of SQL Server>"
# สร้าง Resource Group
az group create --name $resourceGroup --location $location
# สร้าง Redis Cache ด้วย SKU Basic
az redis create --name $redisCacheName --resource-group $resourceGroup --location $location --sku Basic --vm-size c0
# สร้าง SQL Server
az sql server create -l $location -g $resourceGroup -n $sqlServerName -u $adminName -p $adminPassword
# สร้าง SQL Database ด้วย SKU Basic
az sql db create -g $resourceGroup -s $sqlServerName -n $sqlDBName --service-objective Basic
การนำไปใช้งาน (Implementation)
เริ่มต้นการนำไปใช้งานโดยการสร้างโปรเจกต์ ASP.NET Core Web API และเพิ่ม NuGet packages ที่จำเป็นสำหรับ Redis Cache และ Entity Framework Core
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
ขั้นแรก สร้างโมเดล Country class
public class Country {
public int Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
}
ตอนนี้ ลงทะเบียน dependencies ของ EF Core และ Redis Cache ในเมธอด ConfigureServices ของคลาส Startup
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
services.AddDbContext<CountryContext>(optionsAction =>
optionsAction.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddStackExchangeRedisCache(setupAction => {
setupAction.Configuration = Configuration.GetConnectionString("RedisConnectionString");
});
}
แก้ไขไฟล์ appsettings.json เพื่อรองรับ connection strings ของ Redis Cache และ SQL Database
"ConnectionStrings": {
"RedisConnectionString": "<Redis Cache ConnectionString>",
"DefaultConnection": "<SQL Server Connection string>"
}
เพิ่มคลาส DbContext
public class CountryContext : DbContext {
public DbSet<Country> Countries { get; set; }
public CountryContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { }
}
เมธอด GetCountries พยายามดึงรายการจากแคชโดยใช้คีย์ หากพบข้อมูล จะคืนค่าทันที มิฉะนั้น จะดึงข้อมูลจากฐานข้อมูลและเติมเข้าแคช รายการที่แคชไว้จะตั้งค่าหมดอายุหลัง 5 นาที
[Route("api/[controller]")]
[ApiController]
public class CountryController : ControllerBase {
private readonly IDistributedCache cache;
private readonly CountryContext countryContext;
public CountryController(IDistributedCache cache, CountryContext countryContext) {
this.cache = cache;
this.countryContext = countryContext;
}
// GET: api/<CountryController>
[HttpGet]
public async Task<IEnumerable<Country>> GetCountries() {
var countriesCache = await cache.GetStringAsync("countries");
var value = (countriesCache == null) ? default :
JsonConvert.DeserializeObject<IEnumerable<Country>>(countriesCache);
if (value == null) {
var countries = countryContext.Countries.ToList();
if (countries != null && countries.Any()) {
await cache.SetStringAsync("Countries", JsonConvert.SerializeObject(countries),
new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
return countries;
}
}
return value;
}
}
เมธอด AddCountries แสดงวิธีการทำให้แคชล้มเหลว (invalidate) เมื่อเพิ่ม/อัปเดตข้อมูลในฐานข้อมูล
// POST api/<CountryController>
[HttpPost]
public async Task<ActionResult<string>> AddCountries([FromBody] Country country, CancellationToken cancellationToken) {
if (country == null) return BadRequest("country is null");
await countryContext.AddAsync(country);
await countryContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await cache.RemoveAsync("countries", cancellationToken).ConfigureAwait(false);
return Ok("cache has been invalidated");
}
สรุป
ในบทความนี้ ผมอธิบาย Cache-Aside Pattern และการนำไปใช้หลักด้วย ASP.NET Core และ Azure Redis Cache ขอให้สนุกกับการแคช!
หวังว่าคุณจะชอบบทความนี้ หากคุณพบว่าบทความน่าสนใจ กรุณาไลก์และแชร์ด้วยนะครับ