Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

บทความนี้อธิบายพื้นฐาน 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

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

ตามภาพ: ไม่ใช้ cache → ผู้ใช้แต่ละคน hit DB ทุกครั้ง เพิ่ม time complexity ลดประสิทธิภาพ (โดยเฉพาะข้อมูล static เหมือนกัน) ใช้ cache → ผู้ใช้คนแรก hit DB ดึงข้อมูล เก็บใน cache memory ผู้ใช้อื่นใช้จาก cache โดยไม่ hit DB ซ้ำ

ประเภทของ Cache

.NET Core รองรับ 2 ประเภทหลัก:

  1. In-Memory Caching
  2. Distributed Caching

In-Memory Cache: ข้อมูลเก็บใน memory ของ application server เอง

Distributed Caching: มีกลไกภายนอกหลายตัว เช่น Redis และอื่นๆ ส่วนนี้เน้น Redis Cache และการทำงานใน .NET Core

Distributed Caching

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

  • ใน 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 และเปิด

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

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

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

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

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

การนำไปใช้งานทีละขั้นตอน

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

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

ขั้นตอนที่ 2 กำหนดค่าโปรเจกต์ใหม่

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

ขั้นตอนที่ 3 ใส่รายละเอียดเพิ่มเติม

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

ขั้นตอนที่ 4 โครงสร้างโปรเจกต์

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

ขั้นตอนที่ 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

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

ขั้นตอนที่ 17 เรียก get product endpoint และเปิด Redis CLI ใน Azure Portal จะเห็น product list ถูกเก็บเมื่อ hit endpoint ครั้งแรก

การนำ Azure Redis Cache ไปใช้กับ .NET 6 Web API: คู่มือทีละขั้นตอน

กรณีนี้: ตรวจสอบก่อนว่ามีข้อมูลใน 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