+2

Tích hợp RDBMS và Elasticsearch để tối ưu hệ thống lưu trữ và tìm kiếm (Phần 2)

Trong bài viết trước, mình đã chia sẻ về cách tích hợp RDBMS và Elasticsearch để tối ưu hệ thống lưu trữ và tìm kiếm. Hi vọng các bạn đã có cái nhìn tổng quan phương án thực hiện.

Hôm nay, mình sẽ đi sâu hơn vào việc triển khai CRUD (Create, Read, Update, Delete) cho 1 đối tượng Article, từ phương án cơ bản đến nâng cao, kèm theo ví dụ code cụ thể bằng C#.

1. CRUD cơ bản cho Article vào Database

Khi bắt đầu xây dựng một hệ thống, việc đầu tiên mình thường làm là triển khai các thao tác CRUD cơ bản trên cơ sở dữ liệu quan hệ (RDBMS). Đây là nền tảng quan trọng để đảm bảo dữ liệu được lưu trữ và quản lý một cách chính xác.

ArticleRepository - triển khai CRUD cơ bản với C#

Mình sử dụng Entity Framework Core để tương tác với database. Dưới đây là code CRUD cơ bản:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Models
{
    public class Article
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public DateTime PublishedAt  { get; set; }
    }

    public class AppDbContext : DbContext
    {
        public DbSet<Article> Articles { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Your_Connection_String_Here");
        }
    }

    public class ArticleRepository
    {
        private readonly AppDbContext _context;

        public ArticleRepository(AppDbContext context)
        {
            _context = context;
        }

        public async Task AddAsync(Article article)
        {
            await _context.Articles.AddAsync(article);
            await _context.SaveChangesAsync();
        }

        public async Task<Article> GetByIdAsync(int id)
        {
            return await _context.Articles.FindAsync(id);
        }

        public async Task UpdateAsync(Article article)
        {
            _context.Articles.Update(article);
            await _context.SaveChangesAsync();
        }

        public async Task DeleteAsync(int id)
        {
            var article = await _context.Articles.FindAsync(id);
            if (article != null)
            {
                _context.Articles.Remove(article);
                await _context.SaveChangesAsync();
            }
        }
    }
}

2. Tích hợp CRUD và Index trong Elasticsearch

Để khắc phục nhược điểm tìm kiếm sử dụng RDBMS, mình tích hợp Elasticsearch vào hệ thống.

Elasticsearch sẽ đảm nhận việc tìm kiếm full-text, còn RDBMS vẫn là nguồn lưu trữ dữ liệu chính.

2.1. Cách thức hoạt động

  • Khi thực hiện CRUD trên RDBMS, hệ thống sẽ đồng thời cập nhật dữ liệu vào Elasticsearch.
  • Ví dụ:
    • Create: Thêm dữ liệu vào RDBMS và index vào Elasticsearch.
    • Update: Cập nhật dữ liệu trong RDBMS và Elasticsearch.
    • Delete: Xóa dữ liệu trong RDBMS và Elasticsearch.

2.2. Triển khai code - Elasticsearch

Mình sử dụng thư viện NEST để tương tác với Elasticsearch. Dưới đây là code tích hợp:

using Nest;
using System.Threading.Tasks;

namespace Elasticsearch
{
    public interface IElasticsearchService
    {
        Task IndexArticleAsync(Article article);
        Task UpdateArticleAsync(Article article);
        Task DeleteArticleAsync(int id);
    }

    public class ElasticsearchService : IElasticsearchService
    {
        private readonly IElasticClient _elasticClient;

        public ElasticsearchService(IElasticClient elasticClient)
        {
            _elasticClient = elasticClient;
        }

        public async Task IndexArticleAsync(Article article)
        {
            await _elasticClient.IndexDocumentAsync(article);
        }

        public async Task UpdateArticleAsync(Article article)
        {
            await _elasticClient.UpdateAsync<Article>(article.Id, u => u.Doc(article));
        }

        public async Task DeleteArticleAsync(int id)
        {
            await _elasticClient.DeleteAsync<Article>(id);
        }
    }
}

2.3. Kết hợp CRUD và Elasticsearch - IArticleService

Mình tạo một service để kết hợp cả hai:

using Repositories;
using Elasticsearch;
using Models;
using System.Threading.Tasks;

namespace Services
{
    public interface IArticleService
    {
        Task CreateArticleAsync(Article article);
        Task<Article> GetArticleAsync(int id);
        Task UpdateArticleAsync(Article article);
        Task DeleteArticleAsync(int id);
    }

    public class ArticleService : IArticleService
    {
        private readonly IArticleRepository _articleRepository;
        private readonly IElasticsearchService _elasticsearchService;

        public ArticleService(IArticleRepository articleRepository, IElasticsearchService elasticsearchService)
        {
            _articleRepository = articleRepository;
            _elasticsearchService = elasticsearchService;
        }

        public async Task CreateArticleAsync(Article article)
        {
            await _articleRepository.AddAsync(article);
            await _elasticsearchService.IndexArticleAsync(article);
        }

        public async Task<Article> GetArticleAsync(int id)
        {
            return await _articleRepository.GetByIdAsync(id);
        }

        public async Task UpdateArticleAsync(Article article)
        {
            await _articleRepository.UpdateAsync(article);
            await _elasticsearchService.UpdateArticleAsync(article);
        }

        public async Task DeleteArticleAsync(int id)
        {
            await _articleRepository.DeleteAsync(id);
            await _elasticsearchService.DeleteArticleAsync(id);
        }
    }
}

2.4. Cấu hình ở Startup

Mình sử dụng Dependency Injection để quản lý các dịch vụ một cách hiệu quả. Dưới đây là cách cấu hình trong Program.cs:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Nest;
using Repositories;
using Services;
using Elasticsearch;
using Models;

var services = new ServiceCollection();

// Cấu hình DbContext
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer("Your_SQL_Server_Connection_String"));

// Cấu hình Elasticsearch
var settings = new ConnectionSettings(new Uri("http://localhost:9200"))
    .DefaultIndex("articles");
services.AddSingleton<IElasticClient>(new ElasticClient(settings));

// Đăng ký các dịch vụ
services.AddScoped<IArticleRepository, ArticleRepository>();
services.AddScoped<IElasticsearchService, ElasticsearchService>();
services.AddScoped<IArticleService, ArticleService>();

var serviceProvider = services.BuildServiceProvider();

// Sử dụng dịch vụ
var articleService = serviceProvider.GetService<IArticleService>();

// Ví dụ tạo mới bài viết
var article = new Article { Id = 1, Title = "Title 1", Content = "Content 1", Author = "Author 1" };
await articleService.CreateArticleAsync(article);

3. Ưu, nhược điểm và cách khắc phục

3.1. Ưu điểm

3.1.1. Hiệu năng tìm kiếm vượt trội

  • Tìm kiếm full-text: Tận dụng Elasticsearch được thiết kế để tìm kiếm full-text cực kỳ nhanh chóng và hiệu quả. Nó hỗ trợ các tính năng như phân tích văn bản, tìm kiếm gần đúng (fuzzy search), và tìm kiếm theo ngữ nghĩa.
  • Tốc độ phản hồi: Với các truy vấn phức tạp, Elasticsearch có thể trả về kết quả trong vài mili giây, ngay cả khi dữ liệu lên đến hàng triệu bản ghi.

3.1.2. Khả năng mở rộng

  • Horizontal Scaling: Elasticsearch có thể dễ dàng mở rộng bằng cách thêm các node vào cluster, giúp xử lý lượng dữ liệu lớn và tăng khả năng chịu tải.
  • Distributed Nature: Dữ liệu được phân tán trên nhiều node, đảm bảo tính sẵn sàng cao (high availability) và khả năng phục hồi khi có sự cố.

3.1.3. Linh hoạt trong tích hợp

  • Hỗ trợ nhiều loại dữ liệu: Elasticsearch không chỉ hỗ trợ dữ liệu văn bản mà còn có thể lưu trữ và tìm kiếm dữ liệu có cấu trúc, bán cấu trúc, và thậm chí là dữ liệu địa lý (geospatial data).
  • Dễ dàng tích hợp với các hệ thống khác: Elasticsearch có API RESTful, giúp tích hợp dễ dàng với các hệ thống khác như Logstash, Kibana, hoặc các ứng dụng custom.

3.2. Nhược điểm

3.2.1. Độ trễ đồng bộ dữ liệu

  • Vấn đề: Khi thực hiện CRUD trên RDBMS, việc đồng bộ dữ liệu sang Elasticsearch có thể gây độ trễ, đặc biệt là khi hệ thống có lượng dữ liệu lớn hoặc tần suất ghi dữ liệu cao.
  • Ví dụ: Nếu một bài viết được cập nhật trong RDBMS nhưng chưa kịp đồng bộ sang Elasticsearch, người dùng có thể nhận được kết quả tìm kiếm không chính xác.

3.2.2. Phức tạp trong quản lý

  • Vấn đề: Việc quản lý cả RDBMS và Elasticsearch đòi hỏi kiến thức và công sức nhiều hơn so với việc chỉ sử dụng RDBMS.
  • Ví dụ: Bạn cần đảm bảo rằng dữ liệu trong RDBMS và Elasticsearch luôn đồng bộ, đồng thời xử lý các trường hợp lỗi khi đồng bộ.

3.2.3. Khả năng mất dữ liệu

  • Vấn đề: Nếu quá trình đồng bộ dữ liệu giữa RDBMS và Elasticsearch gặp sự cố (ví dụ: mất kết nối mạng, Elasticsearch downtime), dữ liệu có thể bị mất hoặc không đồng bộ kịp thời.
  • Ví dụ: Một bài viết được thêm vào RDBMS nhưng không được index vào Elasticsearch do lỗi kết nối.

3.3. Cách khắc phục

3.3.1. Sử dụng Message Queue để đồng bộ bất đồng bộ

  • Giải pháp: Thay vì đồng bộ dữ liệu trực tiếp từ RDBMS sang Elasticsearch, bạn có thể sử dụng một Message Queue (ví dụ: Azure message queue, RabbitMQ, Kafka) để gửi thông báo về các thay đổi dữ liệu.Một consumer sẽ nhận các thông báo này và thực hiện đồng bộ dữ liệu sang Elasticsearch.
  • Lợi ích:
    • Giảm độ trễ: Việc ghi dữ liệu vào RDBMS và gửi thông báo đến Message Queue diễn ra nhanh chóng.
    • Đảm bảo tính nhất quán: Message Queue có thể lưu trữ các thông báo và đảm bảo chúng được xử lý đúng thứ tự.

3.3.2. Thêm cơ chế Retry và Dead Letter Queue

  • Giải pháp: Khi quá trình đồng bộ dữ liệu sang Elasticsearch gặp lỗi, bạn có thể thêm cơ chế retry để thử lại sau một khoảng thời gian. Nếu sau nhiều lần retry vẫn không thành công, thông báo sẽ được chuyển vào Dead Letter Queue để xử lý sau.
  • Lợi ích:
    • Giảm thiểu mất dữ liệu: Các thông báo lỗi sẽ được lưu trữ và xử lý lại khi hệ thống hoạt động bình thường.
    • Dễ dàng theo dõi và xử lý lỗi: Dead Letter Queue giúp bạn dễ dàng phát hiện và xử lý các lỗi đồng bộ.

3.3.3. Sử dụng Change Data Capture (CDC)

  • Giải pháp: CDC là một kỹ thuật theo dõi các thay đổi dữ liệu trong RDBMS (ví dụ: thông qua transaction logs) và tự động đồng bộ sang Elasticsearch. Các công cụ như Debezium có thể giúp bạn triển khai CDC một cách dễ dàng.
  • Lợi ích:
    • Đồng bộ thời gian thực: CDC giúp đồng bộ dữ liệu gần như ngay lập tức khi có thay đổi.
    • Giảm tải cho ứng dụng: Việc đồng bộ được thực hiện ở tầng database, không ảnh hưởng đến hiệu năng của ứng dụng.

3.3.4. Giám sát và cảnh báo

  • Giải pháp: Triển khai các công cụ giám sát (ví dụ: Prometheus, Grafana) để theo dõi trạng thái của RDBMS, Elasticsearch, và quá trình đồng bộ dữ liệu. Thiết lập các cảnh báo khi có sự cố xảy ra.
  • Lợi ích:
    • Phát hiện sớm các vấn đề: Giúp bạn nhanh chóng phát hiện và xử lý các sự cố trước khi chúng ảnh hưởng đến người dùng.
    • Đảm bảo tính ổn định của hệ thống: Giám sát liên tục giúp hệ thống hoạt động ổn định và đáng tin cậy hơn.

Kết luận

Qua bài viết này, mình đã chia sẻ cách triển khai CRUD, đồng thời tích hợp RDBMS và Elasticsearch, kèm theo ví dụ code cụ thể bằng C#.

Việc tích hợp RDBMS và Elasticsearch mang lại nhiều lợi ích về hiệu năng và khả năng mở rộng, nhưng cũng đi kèm với những thách thức về đồng bộ dữ liệu và quản lý hệ thống. Bằng cách áp dụng các giải pháp như sử dụng Message Queue, cơ chế Retry, CDC, và giám sát hệ thống, bạn có thể khắc phục các nhược điểm và xây dựng một hệ thống mạnh mẽ, đáng tin cậy.

Hy vọng bài viết sẽ giúp ích cho các bạn trong quá trình xây dựng và tối ưu hệ thống của mình một cách hiệu quả hơn. Nếu có thắc mắc hoặc cần trao đổi thêm, đừng ngần ngại để lại bình luận nhé!

Happy coding!

/Son Do

Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí