andybeak
5/26/2015 - 7:48 AM

Storing large values in Memcached

Storing large values in Memcached

    /**
     * store
     *
     * Stores a large array into memcache as a number of key=>values in order to overcome the limitation placed
     * on memcache.  You'll need to use discretion when choosing to do this.
     *
     * The principle is to split the data into several smaller chunks.  The original key is used to store an
     * index array which provides retrieveBigCacheValue() with information on where to look for the data.
     *
     * See retrieveBigCacheValue for the inverse
     *
     * @version 1.0.0
     * @author Andy Beak
     * @since 1.0.0
     * @access public
     * @param $tag
     * @param $key
     * @param array $value
     */
    public function storeBigCacheValue($tag, $key, array $value) {

        $randomKey = $this->createUid();

        $splitSize = 1000 * 900;    // 900kb (less than 1 meg)

        $value = gzcompress(serialize($value));

        $chunks = str_split($value, $splitSize);

        $cacheTime = Config::get('app.cacheLifetime');

        foreach ($chunks as $index => $store) {

            $indexKey = $randomKey.'-'.$index;

            Log::debug(__METHOD__.' : Storing ' . $indexKey . ' for ' . $cacheTime . ' minutes');

            Cache::tags($tag)->put($indexKey, $store, $cacheTime);

        }

        if (!Cache::tags($tag)->has($randomKey.'-0')) {
            throw new \Exception(__METHOD__.' : Could not find first cache entry - Split size is ' . $splitSize . ' bytes');
        }

        $info = [
            'key' => $randomKey,
            'count' => count($chunks)
        ];

        Cache::tags($tag)->put($key, $info, $cacheTime);

    }
    
    /**
     * retrieveBigCacheValue
     *
     * Retrieves a big cache value that was stored in a split-up format by storeBigCacheValue
     *
     *
     * @version 1.0.0
     * @author Andy Beak
     * @since 1.0.0
     * @access public
     * @param $tag
     * @param $key
     * @return mixed
     */
    public function retrieveBigCacheValue($tag, $key) {

        if (!Cache::tags($tag)->has($key)) {
            Log::debug(__METHOD__.' : Key ' . $key . ' in tag ' . $tag . ' is not present.  Has Memcached already removed it?');
            return false;
        }

        $index = Cache::tags($tag)->get($key);

        if (!is_array($index) || !isset($index['key']) || !isset($index['count'])) {
            Log::debug(__METHOD__.' : Key ' . $key . ' in tag ' . $tag . ' is not one stored by storeBigCacheValue');
            return false;
        }

        $lookupKey = $index['key'];

        $count = $index['count'];

        $chunks = [];

        for ($i = 0; $i < $count; $i++) {

            if (!Cache::tags($tag)->has($lookupKey.'-'.$i)) {
                Log::debug(__METHOD__.' : The chunk ' . $i . ' is missing from ' . $key);
                return false;
            }

            $chunks[] = Cache::tags($tag)->get($lookupKey.'-'.$i);
        }

        $compressed = implode('', $chunks);

        unset($chunks);

        return unserialize(gzuncompress($compressed));

    }

    /**
     * createUid
     *
     * Pattern defines the number of characters in each part of the uuid.  We loop through the
     * pattern and replace the number with the proper amount of random hex chars.
     *
     * @version 1.0.0
     * @author Andy Beak
     * @since 1.0.0
     * @access public
     * @return string
     */
    public function createUid() {

        $pattern = [8, 4, 4, 4, 12];

        foreach ($pattern as $key => $length) {

            $bytes = openssl_random_pseudo_bytes($length / 2, $cstrong);

            if (false === $cstrong) {
                Log::warning(__METHOD__.' : Host has insecure crypto method - try updating');
            }

            $pattern[$key] = bin2hex($bytes);
        }

        return implode('-', $pattern);
    }