23

How do I check in PHP if a value is stored in Memcache without fetching it? I don't like fetching it because the values I have set are all 1MB in size and after I fetch it, I have no use for it, so I'm wasting resources. I'm using this in a script that checks if certain keys are cached in memcache and if not, it reads them from a slow data source and sets them in memcache.

Edit: What if I use Memcached::append to append NULL to the key I'm checking? Returns TRUE on success or FALSE on failure. The Memcached::getResultCode will return Memcached::RES_NOTSTORED if the key does not exist. This way I check whether the key exists and it should put the key on top of the LRU list right?

Thank you.

NHG
  • 5,807
  • 6
  • 34
  • 45
Matic
  • 261
  • 1
  • 2
  • 6

12 Answers12

7

I wonder why Memcached has no special method for it. Here is what I came down to after some considerations:

function has($key)
{
    $m->get($key)

    return \Memcached::RES_NOTFOUND !== $m->getResultCode();
}
user487772
  • 8,800
  • 5
  • 47
  • 72
  • 2
    The whole point of OP's question was *avoiding* a call to $m->get(), and all this does is wrap it. – blerg Jun 28 '19 at 21:07
6

I am not sure if this is of any help to you, but you can use

Memcache::add  (  string $key  ,  mixed $var)

It will return false if the key already exists.

In case true is returned you may use

Memcache::delete  (  string $key)

to remove the key you just set. That way you won't need to fetch the data.

Thariama
  • 50,002
  • 13
  • 138
  • 166
  • And if it returns true, don't forget to do a Memcached::delete after – Serty Oan Jun 22 '10 at 07:37
  • @Serty Oan: both wrote the same time :) – Thariama Jun 22 '10 at 07:44
  • 3
    A couple of additional additional notes: In my experience, this method is (for easily discernible reasons) dramatically faster than examining the results of a GET(). Make sure you ADD() an actual value for the var and not null as attempting to add a null returns false. – kiddailey Mar 08 '13 at 08:10
  • This is the wrong solution as there can be another process running a get between add and delete and getting the wrong data or a genuine set which gets deleted. Beware of race conditions! – chx Jul 10 '15 at 03:08
  • It's possible to call ADD without DELETE if you set the lifetime to something like 2678400. Memcached will treat lifetime values larger than 2592000 seconds (30 days) as Unix epoch, therefore, 2678400 being in the past means even if ADD returns true, it will immediately be invalidated. – stackunderflow Feb 06 '20 at 07:34
  • @stackunderflow I am fairly sure that it will NOT be immediately invalidated. There is a garbage collector that deletes expired keys. – Mike Furlender Jul 28 '20 at 14:30
3

I don't like the add/delete suggestion because it inserts invalid data your application may depend on being valid. If your application does make this assumption it would cause a subtle bug that occurs every so often due to the race condition it introduces.

One solution is to take a look at the value and if its temp data pretend its not there, but that would require all application code to use the same api or you'd need to update the application itself, neither are fun and somewhat error prone due to the additional complexity.

If the set of keys is small enough you could store that in memcached and use that determine whether or not to retrieve the data from source again. However, if its as large or larger the the value this method is worse then just getting the entire value from memcached.

You could then solve the new problem by using a index to separate your keys into smaller sets (of course a good way to shard this data so each bucket is a certain size is easier said then done.)

The implementation would consist of using memcached append or prepend to maintain your list of keys tied to some master key or a master key which points to a set of sub_keys which point to the keys themselves :)

Either way you're making the application more and more complex, so I'd only recommend doing this if there is indeed a bottleneck (like there would be if the keys existence needs to be checked often over a network), or if usability in a concern (latency).

In my case I will not do this because I am only going to be accessing memcached over localhost and am using it as more of an extension to my application to cache resources that take more then a few seconds to load normally.

Derek Litz
  • 10,529
  • 7
  • 43
  • 53
  • a way you could possible work around the add/delete is to have an id key only and do that check there that way you're only inserting id's to be checked that should always exist/pair if no data exists (2nd key that holds data, 1st key holds id), that id, then safely remove the invalid id. – exts Nov 04 '22 at 14:43
3

Basically what you want to do is to fill memcached with your data, right?

The thing is that asking if a key is there withouth retrieving the value is not very useful. See this case scenario:

You ask if a key exists and it does. Just after you ask, the data of the key is expelled from the cache. While your response is coming back saying that the date is there, the reality is that the data is not there. So you lost time asking, because the answer is different from reality.

I guess what you want to do is to ask if a key exists, and then if it does not, fill it with your data. Why don't you fill directly all the data? Btw, have you considered that while you are filling memcached with data, you could be expelling keys you just previously inserted?

pakore
  • 11,395
  • 12
  • 43
  • 62
  • 1
    Yes, but I first need to be sure it's not set already. Because if it's already set, then I'd waste reading the data from a slow source. If I use get() to check, then I will waste network IO because the value is 1MB is size. – Matic Jun 22 '10 at 07:41
  • @Matic the "issue" is that memcached was not designed with network IO in mind so I guess that's why such functionality was omitted. If network IO is an issue then you are better off using a kv database. I understand that sometimes you just don't want an extra allocation even if the network IO is not an issue but this is not possible with memcached. Redis has a such feature. – themihai Apr 22 '16 at 16:32
3

I've solved this by using Memcached::append. I try appending value NULL and if it returns TRUE, it means the key exists. If it returns FALSE it means the key doesn't exist. If the key exist it will also put it on top of LRU list.

alex
  • 479,566
  • 201
  • 878
  • 984
Matic
  • 261
  • 1
  • 2
  • 6
  • 2
    You need to ensure that the `Memcached::OPT_COMPRESSION` is set to false, otherwise the operation will always fail. Also, it may be better to append the empty string *''* instead of *NULL*.. – Shimon Rachlenko Apr 04 '13 at 06:39
  • For those who use `Memcache`, not `Memcached`: `append` method is available only in the latter. – TheFrost Jan 10 '14 at 01:51
3

memcached now has the cas command. you can use it with a cas unique as 0 to get the EXISTS or NOT_FOUND responses:

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
set hello 0 0 5
12345
STORED
gets hello
VALUE hello 0 5 6743
12345
END
cas hello 0 0 0 0

EXISTS
cas foo 0 0 0 0 

NOT_FOUND

In the transcript above, I first use the set command to set a key hello with value 12345. Then gets it back, which also returned a cas unique 6743. Then I try to use cas command to replace the value with nothing, but because the cas unique I used is 0, I got back EXISTS error.

Finally I try to use cas to set a key foo that doesn't exist and get back NOT_FOUND error.

Because memcached uses a global incrementing value for cas unique, using 0 is safe that it's not valid for the item you are trying to set and you would get back EXISTS error if it does exist.

Sorry, not familiar with php's memcache client to put it in php code.

Joel Chen
  • 270
  • 3
  • 7
1

Simply not possible, It is not possible to check for only key exists or not. Better you create a separate key with true/false values to check for existence of keys

Gohel Kiran
  • 1,191
  • 1
  • 12
  • 30
1

If the PHP extention Memcached is installed you can use getAllKeys(), like this for example:

$cache = new \Memcached();
$cache->addServer('localhost', '11211');

$key = 'my-awesome-key';
$cache->set($key, '1234');
$keys = $cache->getAllKeys();
$isFound = in_array($key, $keys, true);

var_dump($isFound); // bool(true)

Or write your own Memcached wrapper:

class CustomMemcached {

    private Memcached $memcached;

    public function __construct(Memcached $memcached) {
        $this->memcached = $memcached;
    }
    
    public function set($key, $value): bool
    {
        $this->memcached->set($key, $value);
    }

    public function has(string $key): bool
    {
        $keys = $this->memcached->getAllKeys();
        if ($keys === false) {
            return false;
        }
        return in_array($key, $keys, true);
    }
}


$memcached = new Memcached;
$memcached->addServer('localhost', '11211');
$cache = new CustomMemcached($memcached);
$cache->set('my-awesome-key', '1234');

$isFound = $cache->has('my-awesome-key');

var_dump($isFound); // bool(true)

Note: this wrapper is just for showing that the has() method does its job.

Link: https://www.php.net/manual/en/memcached.getallkeys.php

Julian
  • 4,396
  • 5
  • 39
  • 51
0

The easiest way is to get the given key and cast it to a boolean value.

(bool) Memcache::get(string $key)
WayneC
  • 5,569
  • 2
  • 32
  • 43
Eru
  • 187
  • 4
0

(a little late to the discussion, but) actually you can access all the keys/values stored in the memcache, per:

$allSlabs = $memcache->getExtendedStats('slabs');
$items = $memcache->getExtendedStats('items');

foreach ($allSlabs as $server => $slabs)
   foreach ($slabs as $slabId => $slabMeta)
       foreach ($memcache->getExtendedStats('cachedump', (int) $slabId) as $entries)
           if (!empty($entries))
               foreach ($entries as $key => $entry)
eithed
  • 3,933
  • 6
  • 40
  • 60
  • It would work, but its akin to using a sledgehammer to crack a walnut, the overhead in doing that every time just to check if a key exists, it would be less onerous to simply get the data and check the result code. – Steve Childs Nov 25 '15 at 14:19
  • @SteveChilds - it becomes useful if you don't know the exact key value. For example - if you're storing certain instances of objects in your memcache under similar keys (object_type-object_id#object_options) and you want to reload all the instances of object_type-object_id. Really, it all depends upon the setup and the codebase. – eithed Nov 27 '15 at 11:22
  • Ah, ok - yes in your use case it does make sense, but in the context of the OP, its overkill. – Steve Childs Nov 28 '15 at 09:50
0

This makes no sense from Memcached perspective. If you want to avoid the slow datasource (by a scheduled job, I presume?), save the data to a faster but still stable datasource (e.g. a file) in your scheduled job. When you need the data, try to read from Memcached first, and if that fails read the file and save it into Memcached.

Memcached cannot give you a good answer, as pakore have already answered. The architecture is not meant to be a stable datasource.

Emil Vikström
  • 90,431
  • 16
  • 141
  • 175
0

I needed a reliable test to know wether to use memcache Set or memcache replace.

This is what I ended up with.

Another option would be to set up a network socket to the memcache query, but in the end it would end up doing the same and this connection already exists, saving me the overhead of making and maintaining another connection like in Joel Chen's answer.

$key = 'test';
$value = 'foobarbaz';
/**
 * Intricate dance to test if key exists.
 * If it doesn't exist flags will remain a boolean and we need to use the method set.
 * If it does exist it'll be set to integer indicating the compression and what not, then we need to use replace.
 */
$storageFlag = (is_null($value) || is_bool($value) || is_int($value) || is_float($value) ? false : MEMCACHE_COMPRESSED);
$flags = false;
$memcache->get($key, $flags);
if(false === $flags) {
    $memcache->set($key, $value, storageFlag  , $minutes);
}
else {
    $memcache->replace($key, $value, storageFlag, $minutes);
}

Now if you have "large data" the solution is fairly simple. Use a second key in cojoin that contains something simple like an integer to check. Always use them together and you have no issues.

$key = 'test';
$value = 'foobarbaz';

$storageFlag = (is_null($value) || is_bool($value) || is_int($value) || is_float($value) ? false : MEMCACHE_COMPRESSED);
$flags = false;
$exist_key = $key.'_exists';

$memcache->get($exist_key, $flags);
if(false === $flags) {
    $memcache->set($key, $value, storageFlag  , $minutes);
    $memcache->set($exist_key, 42, false  , $minutes);
}
else {
    $memcache->replace($key, $value, storageFlag, $minutes);
    $memcache->replace($exist_key, 42, false  , $minutes);
}
Tschallacka
  • 27,901
  • 14
  • 88
  • 133