+1

Phần 1: Php - Cache manager với File store

File luôn là một cách lưu trữ quen thuộc và dễ sử dụng, không cần thêm extension như Redis hay Memcache. Như đã giới thiệu ở series Php - Tạo cache manager , chúng ta sẽ đi vào phần đầu tiên. Tạo cache manager với File store - lưu cache bằng file.

Một số chú ý

Trong bài viết này, mình sẽ sử dụng Storage được viết bởi chính mình tại đây. Về cơ bản, Storage này giúp lưu trữ file dễ dàng thông qua các disk như local storage, aws s3 storage... Bạn cũng có thể sử dụng thư viện khác để lưu trữ file, ví dụ như illuminate/filesystem được dùng trong framework phổ biến nhất hiện nay - laravel. Tuy nhiên, thư viện này chỉ hỗ trợ từ php version 7.0 trở lên. Nhưng tiêu chí ban đầu của mình là hạn chế sử dụng thư viện để control tốt hơn nên mình sẽ sử dụng Storage trong repo pure_php của mình, và nó cũng hỗ trợ cả php 5.6.

Tạo Cache interface

Đầu tiên, tạo 1 interface chung cho các drive. Mình sẽ giải thích chi tiết trong comment code nhé

<?php

namespace Contract;

/**
 * Cache contract.
 */
interface Cache
{
    /**
     * Retrieve an item from the cache.
     * Lấy 1 item ra từ cache
     *
     * @param  string  $key - Key cần lấy
     * @param  mixed   $default Nếu không có trong bộ nhớ đệm thì trả về giá trị mặc định là gì. Nếu bạn không truyền tham số này, hàm sẽ trả về null
     * @return mixed    Trả về giá trị mixed, tuỳ thuộc vào value bạn đã put vào bộ nhớ đệm
     */
    public function get($key, $default = null);

    /**
     * Determine if an item exists in the cache.
     * Xác định xem 1 item có tồn tại trong bộ nhớ đệm hay không
     *
     * @param  string  $key Key cần kiểm tra
     * @return bool Trả về true nếu key có tồn tại, false nếu key không tồn tại
     */
    public function has($key);

    /**
     * Retrieve an item from the cache and then delete the item.
     * Truy xuất một mục từ bộ nhớ đệm rồi xóa mục đó. Giống với hàm get, tuy nhiên sau khi get xong thì sẽ xoá nó ra khỏi bộ nhớ đệm
     * 
     * @param  string  $key 
     * @param  mixed   $default 
     * @return mixed
     */
    public function pull($key, $default = null);

    /**
     * Store an item in the cache for a given number of seconds.
     * Lưu một item vào bộ đệm trong một số giây nhất định.
     *
     * @param  string  $key Key cần lưu
     * @param  mixed   $value giá trị cần lưu
     * @param  int     $seconds thời gian tồn tại trong bộ nhớ đệm
     * @return bool Trả về true nếu lưu thành công, false nếu thất bại
     */
    public function put($key, $value, $seconds = null);

    /**
     * Remove items from the cache.
     * Xoá 1 item ra khỏi bộ nhớ đệm
     *
     * @param  string  $key Key cần xoá
     * @return bool Trả về true nếu xoá thành công, false nếu thất bại
     */
    public function forget($key);

    /**
     * Increment the value of an item in the cache.
     * Tăng giá trị của một item trong bộ đệm
     *
     * @param  string  $key Key cần tăng
     * @param  mixed   $value Giá trị cần tăng - mặc định là 1
     * @return int|bool Trả về giá trị sau khi tăng, nếu thất bại thì trả về false
     */
    public function increment($key, $value = 1);

    /**
     * Decrement the value of an item in the cache.
     * Giảm giá trị của một item trong bộ đệm
     *
     * @param  string  $key Key cần giảm
     * @param  mixed   $value Giá trị cần giảm - mặc định là 1
     * @return int|bool Trả về giá trị sau khi giảm, nếu thất bại thì trả về false
     */
    public function decrement($key, $value = 1);
}

Ở các hàm increment và decrement, tại sao $value lại là mixed chứ không phải là int. Vì đơn giản là 2 hàm này sẽ dùng phép tính cộng (+) và trừ (-) để thay đổi giá trị. Và không chỉ riêng int mới hỗ trợ 2 phép tính này, bạn cũng có thể cộng string, array...

Tạo File store

Tạo file Cache\FileStore.php implements Cache contract ở trên

<?php

namespace Cache;

use Contract\Cache;
use Core\Support\Facades\Storage;

/**
 * File store cache class.
 */
class FileStore implements Cache
{
    /**
     * The Filesystem instance.
     *
     * @var \Core\Support\Facades\Storage
     */
    protected $files;

    /**
     * The file cache directory.
     *
     * @var string
     */
    protected $directory;

    /**
     * Khởi tạo FileStore instance.
     *
     * @param string $directory Thư mục bạn sẽ lưu cache, phần này về sau sẽ được config trong 1 file cache.php riêng biệt. Tuy nhiên, chúng ta sẽ làm nó sau khi tạo CacheManager để quản lý các drive ở phần sau
     */
    public function __construct($directory)
    {
        $this->files = new Storage; // Khởi tạo Storage
        $this->directory = $directory; // Gán thư mục cần lưu vào property $this->directory
    }

    public function get($key, $default = null)
    {
        // Gọi hàm getPayload để get value cho $key, nếu giá trị rỗng thì trả về giá trị của biến $default
        return $this->getPayload($key)['data'] ?: $default;
    }

    public function has($key)
    {
        // Gọi hàm get để lấy giá trị của $key, nếu không null thì trả về true, ngược lại trả về false
        return ! is_null($this->get($key));
    }

    public function pull($key, $default = null)
    {
        // Gọi hàm get để lấy giá trị
        $value = $this->get($key, $default);
        // Gọi hàm forget để xoá key đó ra khỏi bộ nhớ đệm
        $this->forget($key);

        // Trả về $value đã get ở trên
        return $value;
    }

    public function put($key, $value, $seconds = null)
    {
        // Gọi hàm put của storage để đẩy giá trị vào bộ nhớ đệm
        return $this->files->put(
            $this->path($key), // Gọi hàm path để trả về đường dẫn tương ứng với thư mục đã được khai báo ở hàm khởi tạo __contruct
            $this->expiration($seconds).serialize($value) Gọi hàm expiration để nhận thời gian hết hạn dựa trên số giây đã cho, sau đó nối với chuỗi được trả về bởi hàm serialize cho biến $value
        );
    }

    public function forget($key)
    {
        // Kiểm tra nếu có tồn tại file với key tương ứng hay không
        if ($this->files->exists($file = $this->path($key))) {
            // Nếu tồn tại thì gọi hàm delete để xoá
            return $this->files->delete($file);
        }
        // Nếu không tồn tại thì trả về false
        return false;
    }

    public function increment($key, $value = 1)
    {
        $raw = $this->getPayload($key); // Get raw data bằng hàm getPayload

        $newValue = (int) $raw['data']) + $value; //Cộng giá trị cũ với biến $value

        $this->put($key, $newValue, $raw['time'] ?: null); // put giá trị mới vào $key. Nếu giá trị cũ có thời gian tồn tại thì put kèm thời gian tồn tại đó, nếu không thì tồn tại vô hạn

        return $newValue;
    }

    public function decrement($key, $value = 1)
    {
        // Tại đây chúng ta chỉ cần gọi hàm increment với tham số $value = $value * -1 Tức là tăng lên số đối nghịch với $value. VD value bạn truyền là 1 thì sẽ gọi hàm increment để tăng giá trị lên -1, tương đương với việc giảm đi 1
        return $this->increment($key, $value * -1);
    }

    /**
     * Get the expiration time based on the given seconds.
     * Nhận thời gian hết hạn dựa trên số giây đã cho
     *
     * @param  int  $seconds Số giây
     * @return int  Trả về số nguyên tương ứng
     */
    protected function expiration($seconds)
    {
        // Nếu bạn truyền $seconds = null hoặc lớn hơn 9999999999 thì trả về 9999999999, nếu không thì trả về thời gian chính xác tại thời điểm hiện tại + số giây
        // vì hàm strtotime trên các hệ thống unix 32bit trả về giá trị tối đa là 2147483647, có 10 chữ số nên mình lấy 9999999999 để max 10 chữ số. Nếu set thời gian là 9999999999 thì sẽ hiểu là cache không có thời gian hết hạn nhé
        return (is_null($seconds) || $seconds > 9999999999) ? 9999999999 : strtotime("now + {$seconds} seconds");
    }

    /**
     * Get the full path for the given cache key.
     * Nhận đường dẫn đầy đủ cho khóa bộ đệm đã cho.
     *
     * @param  string  $key
     * @return string
     */
    public function path($key)
    {
        // Khởi tạo biến $hash = sha1($key) để lấy giá trị hash cho $key
        // Biến $parts sẽ là mảng chưa lần lượt là 2 ký tự đầu và 2 ký tự tiếp theo của $hash. Mục đích tăng tốc khi tìm kiếm file
        $parts = array_slice(str_split($hash = sha1($key), 2), 0, 2);

        return $this->directory.'/'.implode('/', $parts).'/'.$hash; // Trả về chuỗi directory được config ở constructor nối với $parts
    }

    /**
     * Retrieve an item and expiry time from the cache by key.
     * Truy xuất một mục và thời gian hết hạn từ bộ đệm bằng khóa
     *
     * @param  string  $key Key cần get
     * @return array  Trả về mảng chứa data và thời gian hết hạn của nó
     */
    protected function getPayload($key)
    {
        // Lấy đường dẫn đầy đủ theo key
        $path = $this->path($key);

        try {
           // Thời gian hết hạn sẽ là 10 ký tự đầu của nội dung file(vì file được nối bởi 10 ký tự trả về từ hàm expiration và serialize của value)
            $expire = substr(
                $contents = $this->files->get($path), 0, 10
            );
        } catch (\Exception $e) {
           // Nếu không thể get được thì trả về giá trị rỗng 
            return $this->emptyPayload();
        }

        // Nếu thời gian hiện tại lớn hơn thời gian hết hạn ở trên, xoá key đó ra khỏi bộ nhớ đệm và trả về giá trị rỗng
        if (time() >= $expire) {
            $this->forget($key);

            return $this->emptyPayload();
        }

        try {
            // Trả về giá trị sau khi unserialize string sau khi cắt chuỗi thời gian hết hạn ra khỏi content 
            $data = unserialize(substr($contents, 10));
        } catch (\Exception $e) {
           // Nếu có ngoại lệ xảy ra thì xoá key đó và trả về giá trị rỗng 
            $this->forget($key);

            return $this->emptyPayload();
        }

        $time = $expire - time(); // Số giây còn tồn tại trong bộ nhớ đệm bằng thời gian hết hạn trừ thời gian hiện tại

        return compact('data', 'time'); // Trả về data đã get ở trên kèm thời gian tồn tại còn lại
    }

    /**
     * Get a default empty payload for the cache.
     * Trả về giá trị trống mặc định
     *
     * @return array
     */
    protected function emptyPayload()
    {
        return ['data' => null, 'time' => null];
    }
}

Ở file trên, chúng ta sử dụng hàm serialize để tạo ra một string có thể lưu trữ của một giá trị và unserialize để khôi phục lại giá trị ban đầu sau khi get được chuỗi đã serialize từ bộ nhớ đệm. Do chúng ta có thể lưu nhiều giá trị khác nhau vào bộ nhớ đệm chứ không chỉ riêng string, nên không thể nối tất cả chúng vào 1 chuỗi để lưu vào file được. Hàm serialize sẽ tạo ra một string có thể lưu trữ và sau khi unserialize thì chúng ta sẽ được giá trị ban đầu của một value, với cùng một giá trị và kiểu dữ liệu.

Tuy nhiên, nhược điểm chết người của hàm serialize là không thể serialize value là resource hoặc có chứa Closure. Có một cách là biến Closure đó thành 1 chuỗi trước khi serialize. Tuy nhiên để triển khai nó thì khá là phức tạp nên nếu các bạn yêu cầu mình sẽ làm một bài riêng nhé. Nếu dùng FileStore chúng ta đã viết ở trên, chỉ cần chú ý không sử dụng để lưu resource và Closure là được nhé.

Tạo test thử

<?php
    $fileStore = new \Cache\FileStore('cache');
    $a = $fileStore->put('123', 123); // return true
    $b = $fileStore->get('123'); return int 123

Tuỳ thuộc vào config storage bạn lưu ở đâu, giả sử thư mục mặc định là /var/www/app. Lúc này nếu bạn tìm trong thư mục /var/www/app/cache sẽ thấy 1 file tên 40bd001563085fc35165329ea1ff5c5ecbdbbeef là chuỗi hash của key '123' ở trên. và nằm trong thư mục /var/www/app/cache/40/bd/. Nội dung file là 9999999999i:123; với 9999999999 là thời gian hết hạn(tương đương với vô thời hạn do khi put vào cache mình không set thời gian sống), i:123; là chuỗi 123 sau khi serialize.

Nhận xét ưu nhược điểm

Ưu điểm

  1. Dễ sử dụng
  2. Không đòi hỏi extension thêm

Nhược điểm

  1. Không thể tự xoá các bộ nhớ đệm đã hết hạn. Do vậy, bạn cần setup 1 tiến trình tự rà soát và xoá cache đã hết hạn riêng. Nếu không thì không gian lưu trữ sẽ tăng dần theo thời gian

Finally

Bài viết đến đây là kết thúc, hẹn gặp lại các bạn ở phần sau - Php - Cache manager với Redis store. Các bạn có bất cứ câu hỏi nào xin hãy comment cho mình biết nhé. Cuối cùng, đừng quên upvote nếu bạn thấy bài viết hữu ích nhé ❤️


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í