บทความนี้อธิบายพื้นฐาน Azure Redis Cache และการนำไปใช้ด้วย .NET Core Web API
Agenda
- บทนำ
- Cache คืออะไร
- ประเภทของ Cache
- Redis Cache
- การตั้งค่า Azure Redis Cache
- การนำไปใช้งานทีละขั้นตอน
ข้อกำหนดเบื้องต้น
- Visual Studio 2022
- Azure Account
- .NET Core 6
บทนำ
Caching เป็นที่นิยมในอุตสาหกรรมซอฟต์แวร์เพราะปรับปรุงประสิทธิภาพและ scalability ของแอปพลิเคชัน เราใช้เว็บแอปอย่าง Gmail, Facebook ที่ตอบสนองดี มี UX ดี มีผู้ใช้มาก ถ้าแอปมี network traffic และ demand สูง ต้องดูแลหลายอย่างเพื่อปรับปรุงประสิทธิภาพและความตอบสนอง นั่นคือที่มาของ caching
Cache คืออะไร?
Cache คือหน่วยความจำที่เก็บข้อมูลที่เข้าถึงบ่อยในที่เก็บชั่วคราว ปรับปรุงประสิทธิภาพอย่างมาก หลีกเลี่ยง database hit ที่ไม่จำเป็น และเก็บข้อมูลที่ใช้บ่อยใน cache


ตามภาพ: ไม่ใช้ cache → ผู้ใช้แต่ละคน hit DB ทุกครั้ง เพิ่ม time complexity ลดประสิทธิภาพ (โดยเฉพาะข้อมูล static เหมือนกัน) ใช้ cache → ผู้ใช้คนแรก hit DB ดึงข้อมูล เก็บใน cache memory ผู้ใช้อื่นใช้จาก cache โดยไม่ hit DB ซ้ำ
ประเภทของ Cache
.NET Core รองรับ 2 ประเภทหลัก:
- In-Memory Caching
- Distributed Caching
In-Memory Cache: ข้อมูลเก็บใน memory ของ application server เอง
Distributed Caching: มีกลไกภายนอกหลายตัว เช่น Redis และอื่นๆ ส่วนนี้เน้น Redis Cache และการทำงานใน .NET Core
Distributed Caching

- ใน distributed caching ข้อมูลเก็บและแชร์ระหว่างหลายเซิร์ฟเวอร์
- ง่ายต่อการปรับปรุง scalability และประสิทธิภาพหลังจัดการโหลดระหว่างหลายเซิร์ฟเวอร์เมื่อใช้ multi-tenant application
- ในอนาคต หากเซิร์ฟเวอร์หนึ่ง crash และ restart แอปไม่ได้รับผลกระทบ เพราะมีหลายเซิร์ฟเวอร์ตามต้องการ
Redis เป็นแคชยอดนิยมที่บริษัทหลายแห่งใช้ มาดูรายละเอียด Redis กัน
Redis Cache
- Redis เป็น Open Source (BSD Licensed) in-memory Data Structure store ใช้เป็น database
- ใช้เก็บข้อมูลที่ใช้บ่อยและ static data บางส่วนในแคช ใช้และสำรองตามความต้องการ
- มีโครงสร้างข้อมูลมากมายใน Redis เช่น List, Set, Hashing, Stream และอื่นๆ
การตั้งค่า Azure Redis Cache
ขั้นตอนที่ 1 ล็อกอิน Azure portal
ขั้นตอนที่ 2 ค้นหา Azure Cache for Redis ใน marketplace และเปิด

ขั้นตอนที่ 3 คลิก Create และกรอกข้อมูลอื่นๆ




ขั้นตอนที่ 4 ไปที่ Access keys ภายใน cache ที่สร้าง คัดลอก primary connection string สำหรับ .NET Core Web API

การนำไปใช้งานทีละขั้นตอน
ขั้นตอนที่ 1 เปิด Visual Studio สร้าง .NET Core Web API project ใหม่

ขั้นตอนที่ 2 กำหนดค่าโปรเจกต์ใหม่
ขั้นตอนที่ 3 ใส่รายละเอียดเพิ่มเติม
ขั้นตอนที่ 4 โครงสร้างโปรเจกต์
ขั้นตอนที่ 5 สร้าง ProductDetails class
namespace AzureRedisCacheDemo.Models {
public class ProductDetails {
public int Id { get; set; }
public string ProductName { get; set; }
public string ProductDescription { get; set; }
public int ProductPrice { get; set; }
public int ProductStock { get; set; }
}
}
ขั้นตอนที่ 6 เพิ่ม DbContextClass ในโฟลเดอร์ Data
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
namespace AzureRedisCacheDemo.Data {
public class DbContextClass: DbContext {
public DbContextClass(DbContextOptions < DbContextClass > options): base(options) {}
public DbSet < ProductDetails > Products { get; set; }
}
}
ขั้นตอนที่ 7 เพิ่ม SeedData class สำหรับ insert ข้อมูลเริ่มต้น
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace AzureRedisCacheDemo.Data {
public class SeedData {
public static void Initialize(IServiceProvider serviceProvider) {
using (var context = new DbContextClass( serviceProvider.GetRequiredService<DbContextOptions<DbContextClass>>())) {
if (context.Products.Any()) { return; }
context.Products.AddRange(
new ProductDetails { Id = 1, ProductName = "IPhone", ProductDescription = "IPhone 14", ProductPrice = 120000, ProductStock = 100 },
new ProductDetails { Id = 2, ProductName = "Samsung TV", ProductDescription = "Smart TV", ProductPrice = 400000, ProductStock = 120 });
context.SaveChanges();
}
}
}
}
ขั้นตอนที่ 8 กำหนด Azure Redis Cache connection string ใน appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"RedisURL": "<valuefromportal>"
}
ขั้นตอนที่ 9 สร้าง ConfigurationManager และ ConnectionHelper ในโฟลเดอร์ Helper
Configuration Manager
namespace AzureRedisCacheDemo.Helper {
static class ConfigurationManager {
public static IConfiguration AppSetting { get; }
static ConfigurationManager() {
AppSetting = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
}
}
}
Connection Helper
using StackExchange.Redis;
namespace AzureRedisCacheDemo.Helper {
public class ConnectionHelper {
static ConnectionHelper() {
ConnectionHelper.lazyConnection = new Lazy<ConnectionMultiplexer>(() => {
return ConnectionMultiplexer.Connect(ConfigurationManager.AppSetting["RedisURL"]);
});
}
private static Lazy<ConnectionMultiplexer> lazyConnection;
public static ConnectionMultiplexer Connection {
get { return lazyConnection.Value; }
}
}
}
ขั้นตอนที่ 10 เพิ่ม IProductService ใน Repositories
using AzureRedisCacheDemo.Models;
namespace AzureRedisCacheDemo.Repositories {
public interface IProductService {
public Task<List<ProductDetails>> ProductListAsync();
public Task<ProductDetails> GetProductDetailByIdAsync(int productId);
public Task<bool> AddProductAsync(ProductDetails productDetails);
public Task<bool> UpdateProductAsync(ProductDetails productDetails);
public Task<bool> DeleteProductAsync(int productId);
}
}
ขั้นตอนที่ 11 สร้าง ProductService class implement IProductService
using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AzureRedisCacheDemo.Repositories {
public class ProductService: IProductService {
private readonly DbContextClass dbContextClass;
public ProductService(DbContextClass dbContextClass) {
this.dbContextClass = dbContextClass;
}
public async Task<List<ProductDetails>> ProductListAsync() {
return await dbContextClass.Products.ToListAsync();
}
public async Task<ProductDetails> GetProductDetailByIdAsync(int productId) {
return await dbContextClass.Products.Where(ele => ele.Id == productId).FirstOrDefaultAsync();
}
public async Task<bool> AddProductAsync(ProductDetails productDetails) {
await dbContextClass.Products.AddAsync(productDetails);
var result = await dbContextClass.SaveChangesAsync();
return result > 0;
}
public async Task<bool> UpdateProductAsync(ProductDetails productDetails) {
var isProduct = ProductDetailsExists(productDetails.Id);
if (isProduct) {
dbContextClass.Products.Update(productDetails);
var result = await dbContextClass.SaveChangesAsync();
return result > 0;
}
return false;
}
public async Task<bool> DeleteProductAsync(int productId) {
var findProductData = dbContextClass.Products.Where(_ => _.Id == productId).FirstOrDefault();
if (findProductData != null) {
dbContextClass.Products.Remove(findProductData);
var result = await dbContextClass.SaveChangesAsync();
return result > 0;
}
return false;
}
private bool ProductDetailsExists(int productId) {
return dbContextClass.Products.Any(e => e.Id == productId);
}
}
}
ขั้นตอนที่ 12 สร้าง IRedisCache interface
namespace AzureRedisCacheDemo.Repositories.AzureRedisCache {
public interface IRedisCache {
T GetCacheData<T>(string key);
bool SetCacheData<T>(string key, T value, DateTimeOffset expirationTime);
object RemoveData(string key);
}
}
ขั้นตอนที่ 13 สร้าง RedisCache class implement interface
using AzureRedisCacheDemo.Helper;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace AzureRedisCacheDemo.Repositories.AzureRedisCache {
public class RedisCache : IRedisCache {
private IDatabase _db;
public RedisCache() { ConfigureRedis(); }
private void ConfigureRedis() { _db = ConnectionHelper.Connection.GetDatabase(); }
public T GetCacheData<T>(string key) {
var value = _db.StringGet(key);
if (!string.IsNullOrEmpty(value)) {
return JsonConvert.DeserializeObject<T>(value);
}
return default;
}
public object RemoveData(string key) {
bool _isKeyExist = _db.KeyExists(key);
if (_isKeyExist == true) { return _db.KeyDelete(key); }
return false;
}
public bool SetCacheData<T>(string key, T value, DateTimeOffset expirationTime) {
TimeSpan expiryTime = expirationTime.DateTime.Subtract(DateTime.Now);
var isSet = _db.StringSet(key, JsonConvert.SerializeObject(value), expiryTime);
return isSet;
}
}
}
ขั้นตอนที่ 14 สร้าง Products Controller
using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace AzureRedisCacheDemo.Controllers {
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase {
private readonly IProductService _productService;
private readonly IRedisCache _redisCache;
public ProductsController(IProductService productService, IRedisCache redisCache) {
_productService = productService;
_redisCache = redisCache;
}
[HttpGet]
public async Task<ActionResult<List<ProductDetails>>> ProductListAsync() {
var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
if (cacheData != null) { return new List<ProductDetails>(cacheData); }
var productList = await _productService.ProductListAsync();
if(productList != null) {
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
_redisCache.SetCacheData<List<ProductDetails>>("product", productList, expirationTime);
return Ok(productList);
} else { return NoContent(); }
}
[HttpGet("{productId}")]
public async Task<ActionResult<ProductDetails>> GetProductDetailsByIdAsync(int productId) {
var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
if (cacheData != null) {
ProductDetails filteredData = cacheData.Where(x => x.Id == productId).FirstOrDefault();
return new ActionResult<ProductDetails>(filteredData);
}
var productDetails = await _productService.GetProductDetailByIdAsync(productId);
if(productDetails != null) { return Ok(productDetails); }
else { return NotFound(); }
}
[HttpPost]
public async Task<IActionResult> AddProductAsync(ProductDetails productDetails) {
var isProductInserted = await _productService.AddProductAsync(productDetails);
_redisCache.RemoveData("product");
if (isProductInserted) { return Ok(isProductInserted); }
else { return BadRequest(); }
}
[HttpPut]
public async Task<IActionResult> UpdateProductAsync(ProductDetails productDetails) {
var isProductUpdated = await _productService.UpdateProductAsync(productDetails);
_redisCache.RemoveData("product");
if (isProductUpdated) { return Ok(isProductUpdated); }
else { return BadRequest(); }
}
[HttpDelete]
public async Task<IActionResult> DeleteProductAsync(int productId) {
var isProductDeleted = await _productService.DeleteProductAsync(productId);
_redisCache.RemoveData("product");
if (isProductDeleted) { return Ok(isProductDeleted); }
else { return BadRequest(); }
}
}
}
ขั้นตอนที่ 15 ลงทะเบียน services ใน Program class
using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using System;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddDbContext<DbContextClass>(o => o.UseInMemoryDatabase("RedisCacheDemo"));
builder.Services.AddScoped<IRedisCache, RedisCache>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
using(var scope = app.Services.CreateScope()) {
var services = scope.ServiceProvider;
var context = services.GetRequiredService<DbContextClass>();
SeedData.Initialize(services);
}
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
ขั้นตอนที่ 16 รันแอปพลิเคชัน จะเห็น Swagger UI พร้อม API endpoints
ขั้นตอนที่ 17 เรียก get product endpoint และเปิด Redis CLI ใน Azure Portal จะเห็น product list ถูกเก็บเมื่อ hit endpoint ครั้งแรก
กรณีนี้: ตรวจสอบก่อนว่ามีข้อมูลใน cache หรือไม่ ถ้าไม่มี ดึงจาก database และ save ลง cache ครั้งต่อไปจะได้จาก cache วาง debugger ใน controller จะเข้าใจง่าย
GitHub URL
https://github.com/Jaydeep-007/AzureRedisCacheDemo/tree/master/AzureRedisCacheDemo
สรุป
ที่นี่เราดู cache introduction และการ config บน Azure รวมถึงการนำไปใช้ทีละขั้นตอนด้วย .NET Core Web API