Vấn đề thực tế với wp_cache_flush_group()
Trên bề mặt, wp_cache_flush_group() trông rất tiện: một lệnh duy nhất xóa sạch toàn bộ cache của một group. Nhưng vấn đề nằm ở cách nó hoạt động bên dưới, đặc biệt khi bạn dùng Redis làm object cache backend.
Với Redis, wp_cache_flush_group() thực chất thực hiện lệnh SCAN để tìm tất cả key theo pattern của group đó — đây là thao tác O(n) trên toàn bộ keyspace của Redis. Nếu site bạn có hàng chục nghìn key, lệnh SCAN sẽ block Redis trong nhiều giây, thậm chí gây timeout hoàn toàn.
Kết quả thực tế đã gặp: sau khi user đăng bình luận, site gọi wp_cache_flush_group('init_recent_comments') — Redis crash, trang trắng, xử lý mất gần 6 giây. Sau khi thay bằng wp_cache_delete() đúng key: phản hồi tức thì, dưới 1ms.
Tại sao flush_group lại nguy hiểm?
Ba lý do chính:
1. Redis SCAN không phải atomic. Nó lặp qua keyspace theo từng batch, có thể bỏ sót key mới được thêm trong lúc scan, hoặc xóa nhầm key của group khác nếu naming convention không chặt.
2. Không phải backend nào cũng hỗ trợ. File cache, APCu, và nhiều custom backend không implement flush_group đúng nghĩa. Kết quả là cache không được xóa mà bạn không hay biết — stale data tồn tại mãi.
3. Xóa thừa, tốn tài nguyên. Flush cả group đồng nghĩa với việc xóa luôn những cache key không cần thiết phải xóa, buộc hệ thống phải rebuild toàn bộ — tăng tải DB không đáng có.
Giải pháp 1: Purge đúng key — đơn giản, hiệu quả ngay
Nếu bạn biết chính xác cache key nào cần xóa, hãy xóa thẳng nó. Không cần flush group, không cần scan, không cần lo Redis crash.
Ví dụ thực tế: widget hiển thị bình luận gần đây với number=10:
add_action( 'wp_insert_comment', function( $comment_id, $comment ) {
if ( ! in_array( $comment->comment_approved, [ '1', 'approve' ], true ) ) {
return;
}
$args = [
'number' => 10,
'status' => 'approve',
'type' => 'comment',
'offset' => 0,
];
$args = apply_filters( 'init_html_recent_comments_query_args', $args );
$key = 'irc_' . md5( maybe_serialize( $args ) );
wp_cache_delete( $key, 'init_recent_comments' );
}, 10, 2 );
Cách này hoàn toàn phù hợp khi cache key của bạn có thể dự đoán được — ví dụ chỉ có một vài biến như number, paged, limit.
Giải pháp 2: Registry Pattern — track key tự động, purge chính xác
Khi cache key phức tạp và không thể đoán trước — ví dụ key chứa chapter number, user ID, hay nhiều tham số động — bạn cần một cơ chế tự động track tất cả key đã được set, để khi purge có thể xóa đúng và đủ.
Ý tưởng: mỗi khi wp_cache_set() được gọi, đồng thời lưu cache key đó vào một “registry key” trong cùng group. Khi cần purge, đọc registry ra, xóa từng key, xóa luôn registry.
Hàm track key
/**
* Track cache key vào registry của manga.
*
* @param int $manga_id
* @param string $key
*/
function init_html_register_chapter_cache_key( int $manga_id, string $key ): void {
$group = "init_html_chapters_{$manga_id}";
$registry_key = "registry_{$manga_id}";
$registry = wp_cache_get( $registry_key, $group ) ?: [];
if ( ! isset( $registry[ $key ] ) ) {
$registry[ $key ] = true;
wp_cache_set( $registry_key, $registry, $group, 0 ); // TTL = 0: không expire
}
}
Hàm purge cache
/**
* Xóa toàn bộ cache chương của một manga dựa theo registry.
* Không dùng wp_cache_flush_group() — an toàn với mọi backend.
*
* @param int $manga_id
*/
function init_html_clear_chapter_cache( int $manga_id ): void {
$group = "init_html_chapters_{$manga_id}";
$registry_key = "registry_{$manga_id}";
$registry = wp_cache_get( $registry_key, $group );
if ( ! empty( $registry ) ) {
foreach ( array_keys( $registry ) as $key ) {
wp_cache_delete( $key, $group );
}
wp_cache_delete( $registry_key, $group );
}
}
Cách tích hợp vào hàm cache
Sau mỗi wp_cache_set(), thêm một dòng gọi init_html_register_chapter_cache_key():
// Ví dụ trong hàm lấy danh sách chương
function init_html_get_manga_chapters( int $manga_id, int $limit = 2 ): array {
$group = "init_html_chapters_{$manga_id}";
$key = "manga_chapters_limit_{$limit}";
$cached = wp_cache_get( $key, $group );
if ( false !== $cached ) {
return $cached;
}
// ... query DB ...
wp_cache_set( $key, $data, $group, 10 * MINUTE_IN_SECONDS );
init_html_register_chapter_cache_key( $manga_id, $key ); // track key
return $data;
}
// Ví dụ trong hàm kiểm tra chapter hẹn giờ
function init_html_has_future_schedule( int $manga_id ): bool {
$group = "init_html_chapters_{$manga_id}";
$key = 'has_future_schedule';
// ... query DB ...
wp_cache_set( $key, $has_future, $group, HOUR_IN_SECONDS );
init_html_register_chapter_cache_key( $manga_id, $key ); // track key
return $has_future;
}
// Ví dụ trong hàm lấy chương kề (prev/next)
function init_html_get_adjacent_chapters( int $manga_id, float $current_number ): array {
$group = "init_html_chapters_{$manga_id}";
$key = 'adjacent_' . sprintf( '%.2f', round( $current_number, 2 ) );
// ... query DB ...
wp_cache_set( $key, $result, $group, 10 * MINUTE_IN_SECONDS );
init_html_register_chapter_cache_key( $manga_id, $key ); // track key
return $result;
}
Gọi purge khi cần
// Sau khi insert hoặc delete chapter
add_action( 'init_html_after_chapter_inserted', function( int $manga_id ) {
init_html_clear_chapter_cache( $manga_id );
} );
add_action( 'init_html_after_chapter_deleted', function( int $manga_id ) {
init_html_clear_chapter_cache( $manga_id );
} );
So sánh hai cách tiếp cận
wp_cache_flush_group() |
Purge đúng key | Registry Pattern | |
|---|---|---|---|
| Redis SCAN | ✅ Có — O(n), nguy hiểm | ❌ Không | ❌ Không |
| Thời gian xử lý | ~6s (có thể crash) | <1ms | <1ms |
| Hỗ trợ mọi backend | ❌ Không | ✅ Có | ✅ Có |
| Key động/phức tạp | ✅ Xử lý được | ❌ Phải hardcode | ✅ Tự động track |
| Độ chính xác | Xóa thừa (cả group) | Chính xác tuyệt đối | Chính xác tuyệt đối |
Kết luận
wp_cache_flush_group() là một API tiện lợi trên giấy tờ nhưng nguy hiểm trong thực tế — đặc biệt với Redis trên các site có lưu lượng lớn. Thay vào đó, hãy áp dụng nguyên tắc đơn giản: chỉ xóa đúng key cần xóa, không hơn không kém.
Nếu key của bạn đơn giản và có thể đoán trước — purge thẳng. Nếu key phức tạp và động — dùng registry pattern để track tự động. Cả hai cách đều an toàn với mọi cache backend, không gây tải Redis, và đảm bảo cache được invalidate chính xác mà không ảnh hưởng hiệu năng.
Bình luận