Series duthaho đi phỏng vấn: MySQL InnoDB Page

0 0 0

Người đăng: duthaho

Theo Viblo Asia

Xin chào các bạn, lại là duthaho đi phỏng vấn đây, và hôm nay mình muốn chia sẻ một buổi phỏng vấn giả tưởng nhưng rất thực tế về chủ đề InnoDB Page/Block trong MySQL. Đây là những kiến thức quan trọng cho bất kỳ lập trình viên backend nào, đặc biệt khi làm việc với hệ thống cơ sở dữ liệu. Sau khi đọc, các bạn có thể kiểm tra kiến thức qua bài trắc nghiệm mình đã chuẩn bị tại: Quiz Hub - Kiểm Tra Kiến Thức MySQL. Hãy cùng bước vào buổi phỏng vấn nhé!

anh Minh:
Xin chào duthaho, cảm ơn bạn đã đến tham gia buổi phỏng vấn hôm nay. Chúng tôi đang tìm kiếm một Solution Architect có kiến thức sâu về cơ sở dữ liệu, đặc biệt là MySQL InnoDB, để thiết kế các hệ thống hiệu năng cao. Tôi thấy bạn đã nghiên cứu rất kỹ về InnoDB, đặc biệt là về các trang (pages) trong engine này. Để bắt đầu, bạn có thể giải thích trang trong InnoDB là gì và tại sao nó quan trọng trong việc quản lý dữ liệu không?

duthaho:
Xin chào anh Minh, cảm ơn anh đã cho tôi cơ hội này. Trong InnoDB, trang (page) là đơn vị lưu trữ cơ bản, thường có kích thước mặc định là 16 KB, dùng để lưu trữ dữ liệu, chỉ mục (index), và siêu dữ liệu (metadata) cả trên đĩa và trong bộ nhớ (Buffer Pool). Mỗi trang là một khối dữ liệu cố định, chứa các thành phần như tiêu đề trang (page header), vùng dữ liệu (data area), thư mục trang (page directory), và phần đuôi (trailer). Trang rất quan trọng vì:

  • Tối ưu hóa I/O: InnoDB đọc và ghi dữ liệu theo đơn vị trang, giúp giảm số lượng thao tác I/O so với việc xử lý từng hàng (row) riêng lẻ.
  • Quản lý bộ nhớ: Trang được lưu vào Buffer Pool để truy cập nhanh, giảm thời gian đọc từ đĩa.
  • Tính nhất quán: Trang chứa các siêu dữ liệu như checksum và LSN (Log Sequence Number) để đảm bảo toàn vẹn dữ liệu và phục hồi sau sự cố.
  • Hỗ trợ cây B+: Trang lưu trữ các nút của cây B+ trong chỉ mục, giúp truy vấn hiệu quả.

Vì vậy, hiểu cấu trúc và cách quản lý trang là nền tảng để tối ưu hóa hiệu năng cơ sở dữ liệu, đặc biệt khi thiết kế hệ thống có khối lượng giao dịch lớn.

anh Minh:
Rất tốt, duthaho, bạn giải thích rất rõ ràng. Bây giờ, hãy đi sâu hơn một chút. Tôi thấy bạn có nhắc đến việc nén trang (page compression). Bạn có thể giải thích chi tiết cách nén trang hoạt động trong InnoDB và những lợi ích, hạn chế của nó không?

duthaho:
Chắc chắn rồi, anh Minh. Nén trang trong InnoDB là một kỹ thuật giúp giảm kích thước lưu trữ của trang trên đĩa, từ đó tiết kiệm không gian và giảm I/O. Nó được kích hoạt khi tạo bảng với ROW_FORMAT=COMPRESSED và một KEY_BLOCK_SIZE nhỏ hơn kích thước trang mặc định (ví dụ, 8 KB hoặc 4 KB cho trang 16 KB). Cách hoạt động cụ thể như sau:

  1. Quá trình nén:

    • Khi một trang được ghi ra đĩa, InnoDB sử dụng thư viện zlib (thuật toán LZ77) để nén dữ liệu trong vùng dữ liệu của trang.
    • Trang nén được lưu với kích thước nhỏ hơn (ví dụ, 8 KB), và phần còn lại được đánh dấu là “lỗ” (hole) trong tệp thưa (sparse file) nhờ cơ chế “punch hole” của hệ thống tệp (như ext4 hoặc XFS).
    • Siêu dữ liệu trong tiêu đề trang lưu thông tin về kích thước nén và kích thước gốc.
  2. Quá trình giải nén:

    • Khi truy vấn cần dữ liệu, trang nén được đọc từ đĩa vào Buffer Pool và giải nén về kích thước gốc (16 KB) để xử lý.
    • Quá trình này trong suốt với ứng dụng, không yêu cầu thay đổi mã nguồn.

Lợi ích:

  • Tiết kiệm không gian đĩa: Giảm dung lượng lưu trữ, đặc biệt hữu ích cho dữ liệu dạng văn bản dễ nén.
  • Giảm I/O: Đọc trang nén nhỏ hơn từ đĩa nhanh hơn, cải thiện hiệu năng cho hệ thống bị giới hạn bởi I/O.
  • Tiết kiệm chi phí: Hữu ích trong môi trường đám mây hoặc khi dung lượng đĩa đắt đỏ.

Hạn chế:

  • Tăng tải CPU: Nén và giải nén đòi hỏi tài nguyên CPU, có thể gây chậm trễ trong hệ thống bị giới hạn bởi CPU.
  • Tỷ lệ nén không đồng đều: Dữ liệu nhị phân hoặc đã mã hóa nén kém, dẫn đến việc trang có thể được lưu không nén, làm giảm hiệu quả.
  • Phụ thuộc hệ thống tệp: Yêu cầu hệ thống tệp hỗ trợ tệp thưa (sparse file) như ext4 hoặc XFS.
  • Không tiết kiệm bộ nhớ: Trong Buffer Pool, trang luôn được giải nén về kích thước gốc, nên không giảm sử dụng RAM.

Vì vậy, tôi thường đề xuất kiểm tra tỷ lệ nén với dữ liệu thực tế và cân nhắc giữa lợi ích tiết kiệm đĩa và chi phí CPU, đặc biệt trong các hệ thống có khối lượng giao dịch cao.

anh Minh:
Rất chi tiết, duthaho. Tôi thấy bạn có kiến thức sâu về trang. Một câu hỏi nữa liên quan đến hiệu năng: chúng ta đã nói về phân tách trang (page splits) trong cây B+. Bạn có thể giải thích tại sao phân tách trang xảy ra, tác động của nó đến hiệu năng, và cách giảm thiểu chúng không?

duthaho:
Vâng, anh Minh. Phân tách trang xảy ra khi một trang chỉ mục (index page) trong cây B+ trở nên quá đầy để chứa một bản ghi mới hoặc bản ghi được cập nhật, thường trong quá trình INSERT hoặc UPDATE. Điều này phổ biến trong các chỉ mục (clustered hoặc secondary) được tổ chức dưới dạng cây B+. Hãy để tôi giải thích chi tiết:

  1. Nguyên nhân xảy ra:

    • Chèn ngẫu nhiên: Nếu khóa chính hoặc khóa chỉ mục được chèn không theo thứ tự (ví dụ, UUID), bản ghi mới có thể rơi vào giữa một trang đầy, gây phân tách.
    • Cập nhật tăng kích thước: Cập nhật một hàng làm tăng kích thước (ví dụ, sửa cột VARCHAR) có thể làm trang vượt quá dung lượng.
    • Chèn hàng loạt: Các thao tác chèn lớn (bulk insert) với dữ liệu không được sắp xếp trước có thể làm đầy nhiều trang nhanh chóng.
  2. Quá trình phân tách:

    • Trang đầy được chia thành hai trang, với các bản ghi được phân phối lại (thường gần 50/50) để duy trì thứ tự cây B+.
    • Trang cha (parent page) được cập nhật để thêm con trỏ đến trang mới.
    • Nếu trang cha cũng đầy, nó sẽ phân tách, gây ra phân tách liên tiếp (cascading splits), làm tăng độ sâu của cây B+.
  3. Tác động đến hiệu năng:

    • Tăng I/O: Mỗi phân tách ghi ít nhất hai trang mới và cập nhật trang cha, làm tăng thao tác ghi đĩa.
    • Tăng tải CPU: Phân phối lại bản ghi và cập nhật cây B+ đòi hỏi tính toán, đặc biệt nếu có nén trang.
    • Khóa và tranh chấp: Phân tách khóa trang liên quan, làm chậm các giao dịch đồng thời trong môi trường có độ đồng thời cao, như đã thảo luận về Buffer Pool.
    • Phân mảnh: Trang sau phân tách thường chỉ đầy một nửa, gây lãng phí không gian và làm tăng phân mảnh tablespace.
    • Hiệu năng truy vấn: Độ sâu cây B+ tăng có thể làm chậm truy vấn do cần đọc thêm trang.
  4. Cách giảm thiểu:

    • Sử dụng khóa chính tuần tự: Sử dụng AUTO_INCREMENT thay vì UUID để chèn bản ghi theo thứ tự, giảm phân tách ngẫu nhiên.
    • Tối ưu hóa chỉ mục phụ: Giảm số lượng chỉ mục phụ để hạn chế phân tách trên nhiều cây B+.
    • Sắp xếp trước dữ liệu: Với chèn hàng loạt, sắp xếp dữ liệu theo khóa chính trước khi chèn:
      LOAD DATA INFILE 'data.txt' INTO TABLE my_table ORDER BY id;
      
    • Chọn kích thước trang phù duthaho: Như đã thảo luận, trang nhỏ (4 KB, 8 KB) phân tách thường xuyên hơn nhưng chi phí mỗi lần thấp hơn; trang lớn (32 KB, 64 KB) ít phân tách hơn nhưng chi phí cao hơn.
    • Bảo trì định kỳ: Chạy OPTIMIZE TABLE để giảm phân mảnh, nhưng cần lên lịch vào thời điểm bảo trì để tránh khóa bảng.

Theo dõi tần suất phân tách bằng:

SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME LIKE '%split%';

Điều này giúp xác định các chỉ mục gây phân tách nhiều và tối ưu hóa chúng.

anh Minh:
Tuyệt vời, duthaho, bạn đã giải thích rất rõ ràng về phân tách trang và cách xử lý. Bây giờ, hãy nói về Buffer Pool, một thành phần quan trọng khác. Trong môi trường có độ đồng thời cao, Buffer Pool quản lý các trang như thế nào để đảm bảo hiệu năng?

duthaho:
Cảm ơn anh Minh. Trong môi trường có độ đồng thời cao, Buffer Pool là trung tâm của hiệu năng InnoDB, vì nó lưu trữ các trang trong bộ nhớ để giảm I/O đĩa. Cách Buffer Pool quản lý trang trong trường duthaho này như sau:

  1. Cấu trúc Buffer Pool:

    • Buffer Pool được chia thành nhiều instance (mặc định 8, điều chỉnh bằng innodb_buffer_pool_instances) để giảm tranh chấp mutex.
    • Mỗi instance có danh sách LRU (Least Recently Used), danh sách trống (free list), và danh sách bẩn (flush list) để quản lý trang.
    • Trang được ánh xạ qua bảng băm (hash table) để truy cập nhanh, với khóa là tablespace ID và số trang.
  2. Quản lý đồng thời:

    • Truy cập trang: Các luồng (thread) truy cập trang qua bảng băm, sử dụng khóa đọc (shared lock) cho truy vấn đọc và khóa độc quyền (exclusive lock) cho ghi. Khóa cấp trang giảm tranh chấp so với khóa toàn bộ Buffer Pool.
    • Mutex và khóa: Mỗi instance có mutex riêng cho danh sách LRU và flush list, giảm tranh chấp. Các phiên bản MySQL mới hơn sử dụng khóa tinh tế (fine-grained locking) để cải thiện hiệu năng.
    • Phân vùng AHI: Chỉ mục băm thích ứng (AHI), như chúng ta sẽ thảo luận sau, cũng được chia thành nhiều phân vùng (innodb_adaptive_hash_index_parts) để giảm tranh chấp.
  3. Quản lý trang bẩn:

    • Trang bẩn (dirty pages) được tạo khi trang bị sửa đổi (ví dụ, do phân tách hoặc cập nhật). Các luồng dọn trang (page cleaner threads, điều chỉnh bằng innodb_page_cleaners) ghi trang bẩn về đĩa không đồng bộ.
    • Tính năng adaptive flushing điều chỉnh tốc độ ghi dựa trên tỷ lệ trang bẩn (innodb_max_dirty_pages_pct) và mức sử dụng redo log, tránh gây tắc nghẽn I/O.
  4. Tác động của phân tách trang:

    • Như đã đề cập, phân tách trang tạo ra trang mới, tăng số trang bẩn và tiêu tốn không gian Buffer Pool. Trong môi trường đồng thời cao, điều này có thể gây tranh chấp khóa và tăng tỷ lệ trục xuất trang (eviction).
    • Tôi sẽ đảm bảo Buffer Pool đủ lớn để chứa tập dữ liệu hoạt động (working set), thường 50-80% bộ nhớ máy chủ.
  5. Tối ưu hóa:

    • Tăng kích thước Buffer Pool (innodb_buffer_pool_size) để giảm trục xuất trang.
    • Sử dụng nhiều instance (ví dụ, 16) và page cleaner threads (ví dụ, 8) để xử lý đồng thời.
    • Theo dõi tỷ lệ trúng (hit ratio):
      SHOW STATUS LIKE 'Innodb_buffer_pool_pages%';
      
      Tỷ lệ >95% là lý tưởng.
    • Giảm phân tách trang bằng khóa chính tuần tự để giảm tranh chấp.

anh Minh:
Rất ấn tượng, duthaho. Bạn đã đề cập đến Adaptive Hash Index (AHI) trong Buffer Pool. Có thể giải thích vai trò của AHI và khi nào nên bật hoặc tắt nó không?

duthaho:
Vâng, anh Minh. Adaptive Hash Index (AHI) là một cấu trúc băm trong bộ nhớ, được lưu trong Buffer Pool, nhằm tăng tốc độ truy vấn điểm (point queries) như SELECT * FROM table WHERE id = 100. Vai trò và cách hoạt động của nó như sau:

  1. Vai trò của AHI:

    • AHI là một bảng băm ánh xạ các khóa chỉ mục (như khóa chính hoặc khóa chỉ mục phụ) đến địa chỉ trang hoặc bản ghi trong Buffer Pool.
    • Thay vì duyệt cây B+ (có thể cần 2-3 lần đọc trang), AHI cung cấp truy cập O(1) cho các khóa được truy cập thường xuyên (hot keys).
    • Nó được xây dựng động dựa trên mẫu truy cập, tự động thêm/xóa các khóa phổ biến.
  2. Tích duthaho với Buffer Pool:

    • AHI tiêu tốn một phần bộ nhớ Buffer Pool (thường 1-10%), cạnh tranh với trang dữ liệu và chỉ mục.
    • Khi một trang bị trục xuất khỏi Buffer Pool (do LRU), các mục AHI liên quan được xóa để đảm bảo tính nhất quán.
    • AHI được chia thành nhiều phân vùng (innodb_adaptive_hash_index_parts, mặc định 8) để giảm tranh chấp mutex trong môi trường đồng thời cao.
  3. Lợi ích:

    • Giảm số lần đọc trang cho truy vấn điểm, cải thiện độ trễ trong các hệ thống OLTP đọc nhiều.
    • Giảm tranh chấp khóa trên trang chỉ mục B+, vì truy cập qua AHI không cần duyệt cây.
  4. Hạn chế:

    • Tăng sử dụng bộ nhớ: AHI có thể làm giảm số trang dữ liệu được lưu trong Buffer Pool, gây tăng I/O nếu Buffer Pool nhỏ.
    • Chi phí ghi: Các thao tác chèn, cập nhật, hoặc phân tách trang yêu cầu cập nhật AHI, gây tranh chấp mutex, đặc biệt trong môi trường ghi nhiều.
    • Chỉ hiệu quả cho truy vấn điểm, không hỗ trợ truy vấn dải (range queries).
  5. Khi nào nên bật/tắt:

    • Bật (mặc định): Trong các hệ thống OLTP đọc nhiều với truy vấn điểm lặp lại (ví dụ, tra cứu user_id trong ứng dụng thương mại điện tử). Theo dõi hiệu quả bằng:
      SHOW ENGINE INNODB STATUS;
      
      Tìm mục hash searches/s vs. non-hash searches/s. Tỷ lệ trúng cao (>80%) cho thấy AHI hiệu quả.
    • Tắt: Trong các hệ thống ghi nhiều (ví dụ, chèn UUID ngẫu nhiên gây nhiều phân tách) để giảm tranh chấp mutex:
      SET GLOBAL innodb_adaptive_hash_index = OFF;
      
    • Kiểm tra hiệu năng trước và sau khi tắt để xác nhận lợi ích.

Tôi cũng đề xuất tăng số phân vùng AHI (ví dụ, 16) trong môi trường đồng thời cao:

SET GLOBAL innodb_adaptive_hash_index_parts = 16;

anh Minh:
Rất xuất sắc, duthaho. Bạn đã thể hiện kiến thức rất sâu về InnoDB, từ trang, nén, phân tách, đến Buffer Pool và AHI. Một câu hỏi cuối: Nếu bạn thiết kế một hệ thống cơ sở dữ liệu cho một ứng dụng thương mại điện tử với khối lượng giao dịch cao, bạn sẽ áp dụng những gì đã học về trang InnoDB để tối ưu hóa hiệu năng?

duthaho:
Cảm ơn anh Minh. Để thiết kế một hệ thống thương mại điện tử với khối lượng giao dịch cao, tôi sẽ áp dụng các kiến thức về trang InnoDB như sau:

  1. Chọn kích thước trang phù duthaho:

    • Dùng kích thước trang 8 KB hoặc 16 KB (mặc định) để cân bằng giữa I/O ngẫu nhiên (cho truy vấn điểm) và bộ nhớ Buffer Pool. Trang nhỏ hơn (4 KB) có thể phù duthaho nếu hàng nhỏ, nhưng tôi sẽ kiểm tra với dữ liệu thực tế.
    • Tránh trang lớn (32 KB, 64 KB) vì hệ thống OLTP cần truy cập ngẫu nhiên nhanh, và trang lớn gây lãng phí I/O.
  2. Tối ưu hóa chỉ mục để giảm phân tách:

    • Sử dụng khóa chính AUTO_INCREMENT (ví dụ, BIGINT) cho các bảng như orders hoặc users để chèn tuần tự, giảm phân tách trang trong cây B+.
    • Giới hạn số lượng chỉ mục phụ để giảm phân tách trên nhiều cây B+.
    • Thiết kế chỉ mục bao phủ (covering indexes) cho các truy vấn phổ biến như tra cứu customer_id.
  3. Tối ưu hóa Buffer Pool:

    • Cấp phát 60-80% bộ nhớ máy chủ cho Buffer Pool (ví dụ, 12 GB cho máy chủ 16 GB) để lưu trữ tập dữ liệu hoạt động.
    • Sử dụng 16 instance Buffer Pool (innodb_buffer_pool_instances=16) và 8 page cleaner threads (innodb_page_cleaners=8) để xử lý đồng thời cao.
    • Theo dõi tỷ lệ trúng:
      SHOW STATUS LIKE 'Innodb_buffer_pool_pages%';
      
  4. Sử dụng AHI một cách thông minh:

    • Giữ AHI bật để tăng tốc truy vấn điểm (ví dụ, tra cứu đơn hàng), nhưng theo dõi hiệu quả qua SHOW ENGINE INNODB STATUS.
    • Nếu ghi nhiều (ví dụ, chèn đơn hàng liên tục), kiểm tra xem tắt AHI có cải thiện hiệu năng không.
  5. Xem xét nén trang:

    • Sử dụng nén trang (ROW_FORMAT=COMPRESSED, KEY_BLOCK_SIZE=8) cho các bảng có dữ liệu văn bản lớn (như mô tả sản phẩm) để tiết kiệm đĩa.
    • Kiểm tra tỷ lệ nén và chi phí CPU, đặc biệt trong môi trường ghi nhiều, để đảm bảo không gây tắc nghẽn.
  6. Phân vùng bảng:

    • Phân vùng bảng lớn (như orders) theo phạm vi ngày (date range) để phân phối phân tách và truy cập trên nhiều cây B+, giảm tranh chấp.
  7. Bảo trì và giám sát:

    • Chạy OPTIMIZE TABLE định kỳ để giảm phân mảnh từ phân tách trang.
    • Theo dõi phân tách và hiệu năng Buffer Pool:
      SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME LIKE '%split%';
      SELECT * FROM INFORMATION_SCHEMA.INNODB_BUFFER_POOL_STATS;
      
    • Sử dụng Performance Schema để phát hiện tranh chấp mutex hoặc khóa.

Bằng cách kết duthaho các tối ưu này, tôi có thể thiết kế một hệ thống thương mại điện tử với hiệu năng cao, khả năng mở rộng tốt, và đáp ứng được khối lượng giao dịch lớn.

anh Minh:
Rất ấn tượng, duthaho. Bạn không chỉ nắm vững lý thuyết mà còn biết cách áp dụng thực tiễn. Cảm ơn bạn đã chia sẻ, tôi nghĩ đội ngũ của chúng tôi sẽ rất muốn làm việc với bạn. Bạn có câu hỏi nào cho tôi không?

duthaho:
Cảm ơn anh Minh rất nhiều. Tôi muốn hỏi, trong các dự án thực tế của công ty, đội ngũ có thường sử dụng các công cụ như Percona XtraBackup để sao lưu các hệ thống InnoDB không? Và có thường gặp các vấn đề về hiệu năng liên quan đến phân tách trang hoặc AHI không?

anh Minh:
Câu hỏi hay, duthaho. Chúng tôi sử dụng XtraBackup khá thường xuyên vì nó hỗ trợ sao lưu nóng (hot backup) và giữ được các tệp thưa khi dùng nén trang. Về phân tách trang, chúng tôi từng gặp vấn đề trong các ứng dụng ghi nhiều với khóa ngẫu nhiên, và thường giải quyết bằng cách chuyển sang khóa tuần tự hoặc phân vùng. Với AHI, chúng tôi đôi khi tắt nó trong các hệ thống ghi nặng để giảm tranh chấp, nhưng điều này cần kiểm tra kỹ. Tôi rất mong chờ được thảo luận thêm với bạn trong các vòng tiếp theo!

duthaho:
Cảm ơn anh Minh, tôi rất mong được làm việc cùng đội ngũ và học hỏi thêm từ các dự án thực tế!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Mô hình quan hệ - thực thể (Entity – Relationship Model)

Mô hình quan hệ thực thể (Entity Relationship model - E-R) được CHEN giới thiệu vào năm 1976 là một mô hình được sử dụng rộng rãi trong các bản thiết kế cơ sở dữ liệu ở mức khái niệm, được xây dựng dựa trên việc nhận thức thế giới thực thông qua tập các đối tượng được gọi là các thực thể và các mối

0 0 146

- vừa được xem lúc

[Embulk #1] Công cụ giúp giảm nỗi đau chuyển đổi dữ liệu

Embulk là gì. Embulk là một công cụ open source có chức năng cơ bản là load các record từ database này và import sang database khác.

0 0 73

- vừa được xem lúc

Window Functions trong MySQL, Nâng cao và cực kì hữu dụng (Phần II).

Chào mọi người, lại là mình đây, ở phần trước mình đã giới thiệu với mọi người về Window Functions Phần I. Nếu chưa rõ nó là gì thì mọi người nên đọc lại trước nha, để nắm được định nghĩa và các key words, tránh mắt chữ O mồm chứ A vì phần này mình chủ yếu sẽ thực hành với các Window Functions.

0 0 123

- vừa được xem lúc

Window Functions trong MySQL, Nâng cao và cực kì hữu dụng (Phần I).

Chào mọi người, mình mới tìm hiểu đc topic Window Functions cá nhân mình cảm thấy khá là hay và mình đánh giá nó là phần nâng cao. Vì ít người biết nên Window Functions thấy rất ít khi sử dụng, thay vì đó là những câu subquery dài dằng dặc như tin nhắn nhắn cho crush, và người khác đọc hiểu được câu

0 0 995

- vừa được xem lúc

Disable và Enable trigger trong Oracle

Origin post: https://www.tranthanhdeveloper.com/2020/12/disable-va-enable-trigger-trong-oracle.html.

0 0 54

- vừa được xem lúc

Lưu trữ dữ liệu với Data Store

. Data Store là một trong những componet của bộ thư viện Android JetPack, nó là một sự lựa chọn hoàn hảo để thay thế cho SharedPreferences để lưu trữ dữ liệu đơn giản dưới dạng key-value. Chúng ta cùng làm một so sánh nhỏ để thấy sự tối ưu của Data Store với SharedPreferences nhé.

0 0 81