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

การรวม Redis Caching เข้ากับ .NET Core API เพื่อประสิทธิภาพและความสามารถในการขยายตัว

บทนำ

เราจะพูดถึง Caching ใน .NET Core และวิธีการทำงาน มาดูทีละหัวข้อกัน:

  • บทนำเรื่อง Caching
  • Cache คืออะไร
  • ประเภทของ Cache
  • การนำ Cache ไปใช้งาน

Caching เป็นที่นิยมในอุตสาหกรรมซอฟต์แวร์ปัจจุบันเพราะมันปรับปรุงประสิทธิภาพและความสามารถในการขยายตัวของแอปพลิเคชัน เราใช้เว็บแอปมากมายอย่าง Gmail และ Facebook และเห็นว่าตอบสนองได้ดี มีประสบการณ์ผู้ใช้ที่ดี มีผู้ใช้จำนวนมากใช้งานอินเทอร์เน็ต และหากแอปพลิเคชันมี network traffic และความต้องการสูง เราต้องดูแลหลายอย่างที่ช่วยปรับปรุงประสิทธิภาพและความตอบสนองของแอปพลิเคชัน นั่นคือที่มาของ caching

มาเริ่มทีละอย่างกัน

Caching คืออะไร?

Cache คือหน่วยความจำที่ใช้เก็บข้อมูลที่เข้าถึงบ่อยในที่เก็บชั่วคราว จะปรับปรุงประสิทธิภาพอย่างมาก หลีกเลี่ยงการ hit ฐานข้อมูลที่ไม่จำเป็น และเก็บข้อมูลที่ใช้บ่อยไว้ใน buffer เพื่อใช้เมื่อต้องการ

การรวม Redis Caching เข้ากับ .NET Core API เพื่อประสิทธิภาพและความสามารถในการขยายตัว

การรวม Redis Caching เข้ากับ .NET Core API เพื่อประสิทธิภาพและความสามารถในการขยายตัว

ตามภาพด้านบน มี 2 สถานการณ์: ไม่ใช้ cache และใช้ cache เมื่อไม่ใช้ cache ผู้ใช้แต่ละคนจะ hit ฐานข้อมูลทุกครั้ง เพิ่ม 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: ข้อมูลเก็บในหน่วยความจำของ application server เอง

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

Distributed Caching

การรวม Redis Caching เข้ากับ .NET Core API เพื่อประสิทธิภาพและความสามารถในการขยายตัว

  • ใน distributed caching ข้อมูลถูกเก็บและแชร์ระหว่างหลายเซิร์ฟเวอร์
  • ง่ายต่อการปรับปรุง scalability และประสิทธิภาพหลังจัดการโหลดระหว่างหลายเซิร์ฟเวอร์เมื่อใช้ multi-tenant application
  • ในอนาคต หากเซิร์ฟเวอร์หนึ่ง crash และ restart แอปพลิเคชันไม่ได้รับผลกระทบ เพราะมีหลายเซิร์ฟเวอร์ตามต้องการ

Redis เป็นแคชยอดนิยมที่บริษัทหลายแห่งใช้เพื่อปรับปรุงประสิทธิภาพและ scalability มาดูรายละเอียด Redis กัน

Redis Cache

  • Redis เป็น Open Source (BSD Licensed) in-memory Data Structure store ใช้เป็น database
  • ใช้เก็บข้อมูลที่ใช้บ่อยและข้อมูล static บางส่วนในแคช ใช้และสำรองตามความต้องการผู้ใช้
  • มีโครงสร้างข้อมูลมากมายใน Redis เช่น List, Set, Hashing, Stream และอื่นๆ

การติดตั้ง Redis Cache

ขั้นตอนที่ 1. ดาวน์โหลด Redis Server จาก URL ต่อไปนี้:

https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504

ขั้นตอนที่ 2. แตกไฟล์ zip และเปิด Redis Server และ Redis CLI

การรวม Redis Caching เข้ากับ .NET Core API เพื่อประสิทธิภาพและความสามารถในการขยายตัว

การนำ Redis Cache ไปใช้กับ .NET Core API

ขั้นตอนที่ 1. สร้าง .NET Core API Web Application

ขั้นตอนที่ 2. ติดตั้ง NuGet Packages ต่อไปนี้ทีละขั้นตอน:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Swashbuckle.AspNetCore
  • StackExchange.Redis

ขั้นตอนที่ 3. สร้างโฟลเดอร์ Model และสร้าง Product Class

namespace RedisCacheDemo.Model {
    public class Product {
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public string ProductDescription { get; set; }
        public int Stock { get; set; }
    }
}

ขั้นตอนที่ 4. สร้าง DbContextClass สำหรับ Database operations

using Microsoft.EntityFrameworkCore;
using RedisCacheDemo.Model;
namespace RedisCacheDemo.Data {
    public class DbContextClass: DbContext {
        public DbContextClass(DbContextOptions < DbContextClass > options): base(options) {}
        public DbSet < Product > Products { get; set; }
    }
}

ขั้นตอนที่ 5. สร้าง ICacheService Interface และ CacheService Class สำหรับ Redis Cache

using System;
namespace RedisCacheDemo.Cache {
    public interface ICacheService {
        T GetData<T>(string key);
        bool SetData<T>(string key, T value, DateTimeOffset expirationTime);
        object RemoveData(string key);
    }
}
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache {
    public class CacheService: ICacheService {
        private IDatabase _db;
        public CacheService() {
            ConfigureRedis();
        }
        private void ConfigureRedis() {
            _db = ConnectionHelper.Connection.GetDatabase();
        }
        public T GetData<T>(string key) {
            var value = _db.StringGet(key);
            if (!string.IsNullOrEmpty(value)) {
                return JsonConvert.DeserializeObject<T>(value);
            }
            return default;
        }
        public bool SetData<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;
        }
        public object RemoveData(string key) {
            bool _isKeyExist = _db.KeyExists(key);
            if (_isKeyExist == true) {
                return _db.KeyDelete(key);
            }
            return false;
        }
    }
}

ขั้นตอนที่ 6. สร้าง ProductController และเมธอดต่างๆ

using Microsoft.AspNetCore.Mvc;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
using RedisCacheDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RedisCacheDemo.Controllers {
    [Route("api/[controller]")]
    [ApiController]
    public class ProductController: ControllerBase {
        private readonly DbContextClass _dbContext;
        private readonly ICacheService _cacheService;
        public ProductController(DbContextClass dbContext, ICacheService cacheService) {
            _dbContext = dbContext;
            _cacheService = cacheService;
        }
        [HttpGet("products")]
        public IEnumerable<Product> Get() {
            var cacheData = _cacheService.GetData<IEnumerable<Product>>("product");
            if (cacheData != null) {
                return cacheData;
            }
            var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
            cacheData = _dbContext.Products.ToList();
            _cacheService.SetData<IEnumerable<Product>>("product", cacheData, expirationTime);
            return cacheData;
        }
        [HttpGet("product")]
        public Product Get(int id) {
            Product filteredData;
            var cacheData = _cacheService.GetData<IEnumerable<Product>>("product");
            if (cacheData != null) {
                filteredData = cacheData.Where(x => x.ProductId == id).FirstOrDefault();
                return filteredData;
            }
            filteredData = _dbContext.Products.Where(x => x.ProductId == id).FirstOrDefault();
            return filteredData;
        }
        [HttpPost("addproduct")]
        public async Task<Product> Post(Product value) {
            var obj = await _dbContext.Products.AddAsync(value);
            _cacheService.RemoveData("product");
            _dbContext.SaveChanges();
            return obj.Entity;
        }
        [HttpPut("updateproduct")]
        public void Put(Product product) {
            _dbContext.Products.Update(product);
            _cacheService.RemoveData("product");
            _dbContext.SaveChanges();
        }
        [HttpDelete("deleteproduct")]
        public void Delete(int Id) {
            var filteredData = _dbContext.Products.Where(x => x.ProductId == Id).FirstOrDefault();
            _dbContext.Remove(filteredData);
            _cacheService.RemoveData("product");
            _dbContext.SaveChanges();
        }
    }
}

ขั้นตอนที่ 7. เพิ่ม SQL Server connection string และ Redis URL ใน appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "RedisURL": "127.0.0.1:6379",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=Server;Initial Catalog=RedisCache;User Id=sa;Password=***;"
  }
}

ขั้นตอนที่ 8. ลงทะเบียน ICacheService ใน ConfigureServices ของ Startup Class และเพิ่ม config Swagger

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
namespace RedisCacheDemo {
    public class Startup {
        public Startup(IConfiguration configuration) {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services) {
            services.AddControllers();
            services.AddScoped<ICacheService, CacheService>();
            services.AddDbContext<DbContextClass>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddSwaggerGen(c => {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "RedisCacheDemo", Version = "v1" });
            });
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
            if (env.IsDevelopment()) {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "RedisCacheDemo v1"));
            }
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints => {
                endpoints.MapControllers();
            });
        }
    }
}

ขั้นตอนที่ 9. สร้าง ConfigurationManager Class สำหรับ config app setting

using Microsoft.Extensions.Configuration;
using System.IO;
namespace RedisCacheDemo {
    static class ConfigurationManager {
        public static IConfiguration AppSetting { get; }
        static ConfigurationManager() {
            AppSetting = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
        }
    }
}

ขั้นตอนที่ 10. สร้าง Connection Helper Class สำหรับ Redis Connection

using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache {
    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; }
        }
    }
}

ขั้นตอนที่ 11. รัน Migration และ Database Update สำหรับสร้าง DB

add-migration "FirstMigration"
update-database

ขั้นตอนที่ 12. รันแอปพลิเคชัน เพิ่มข้อมูลผ่าน Swagger UI และทดสอบ caching ใน endpoints products และ product

พื้นฐานแล้ว ผมเพิ่ม cache ใน product และ products endpoints ใน controller เมื่อผู้ใช้ต้องการข้อมูล products ทั้งหมด จะตรวจสอบก่อนว่ามีใน Redis Cache หรือไม่ หากมี ส่งคืนจาก cache หากไม่มี ดึงจาก database และเซ็ตลง cache ครั้งต่อไปจะได้จาก cache โดยไม่ hit database

สำหรับ product by ID: ดึงข้อมูลทั้งหมดจาก cache แล้ว filter ตาม ID หากมี ส่งคืนจาก cache หากไม่มี ดึงจาก database

ใน update, delete, post endpoints ใช้ remove method เพื่อลบ product key ออกจาก cache

ยังมีสถานการณ์หนึ่งที่ต้องระวัง: เมื่อมี 2 ผู้ใช้ร้องขอข้อมูลพร้อมกัน อาจเกิด race condition ที่ทั้งคู่ hit database พร้อมกัน วิธีแก้คือใช้ Lock Mechanism

สร้าง private object lock บนคลาส:

private static object _lock = new object()

แก้ไข Get method:

public IEnumerable<Product> Get() {
    var cacheData = _cacheService.GetData<IEnumerable<Product>>("product");
    if (cacheData != null) {
        return cacheData;
    }
    lock(_lock) {
        var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
        cacheData = _dbContext.Products.ToList();
        _cacheService.SetData<IEnumerable<Product>>("product", cacheData, expirationTime);
    }
    return cacheData;
}

เมื่อ request ที่ 2 มาก่อน request ที่ 1 เสร็จ จะอยู่ในคิว รอจน request แรกเสร็จ

สามารถดูรายละเอียด key ใน Redis ผ่าน Redis CLI ได้

การรวม Redis Caching เข้ากับ .NET Core API เพื่อประสิทธิภาพและความสามารถในการขยายตัว

นี่คือเรื่อง Redis Cache ใน .NET Core ทั้งหมด หวังว่าคุณเข้าใจแล้วครับ

Happy Coding!