Lock-free memcache API
Good day, harazhiteli!
This post is a brief summary of many hours of thought, a paper-clip, code sketches and, in the end, really working code in production.
Our site (and hereinafter simply a site) actively uses memekes for hot data. The code filling the memkesh can work for a very long time (0.5 seconds is a long time) and at the same time, user requests manage to start another hundred update procedures. The consequences are understandable, but for a long time we simply could not notice them at the level of the total load. Only when we saw bursts of time for servicing some requests (from the increased load, they also fell into SLOW_QUERIES_LOG MySQL) - then the work began to boil.
Let us consider in more detail the scenario for requesting a key from a memkesh, in which the key is “rotten” or has not yet been installed.
To understand the problem, I drew a small diagram: The

update logic is controlled by code, i.e. the application itself knows if it returned FALSE, then you need to regenerate the key. As you can see from the diagram, the trouble arises at the moment when the data on the first request has not yet had time to “prepare”, and we are already asking for the same data.
Thanks to the evilbloodydemon habruiser for the exact definition - the situation is called the "Dog pile effect".
Numerous versions have been put forward - how to avoid this.
First, we thought about the lock system and the update queue. But this scenario is horribly slow.
Then they thought - if the code can regenerate data - let it do that. You just need to return it to FALSE. And immediately, reinstall the old data. Total: the update procedure will be launched once, and the data that will be returned to the application until the end of the regeneration process is “rotted out” only for the duration of the regeneration.
To do this, we must add in the memkesh not only the data itself, but also the timeout and the time the key is invalidated. An array for a time twice as large (to be sure) gets into a real memkesh. The documentation says that the longest key storage time is 30 days. Those. just put in the "wrapper" data for 15 days - 1 second for fidelity. The same applies to keys with a timeout = 0 (i.e., forever, until it is crowded out). Situations when the data in a memkesh are needed once in 15 days - I did not meet. If this happened to you, something needs to be changed.
We also quickly noticed a problem with increment. I had to agree that all increment keys end in "_inc", for example. And when such a key is found, we simply get the necessary data, which the memeksh itself has incremented. * I removed this fork or the Memcache_Proxy :: get () method.
The code is documented where it is needed :) I apologize in advance for the code sheet, but I can’t cut it anymore.
The MS class is needed to communicate with one instance of the memcache within the entire code without the need to explicitly declare a connection to the memcache. It will be created the first time you call the desired method in this class.
Code, just code. If the data is sour, start the generation by returning FALSE only to the requestor and reinstall the same data at the same time. Thus, the next requestor will receive the old data until the first process finishes the generation and executes MC :: set () with the actual data. Immediately after that, all processes will receive relevant data.
Those. continue to use memkesh as before. If there was a wrapper for accessing the memkesh, you can correct it and DO NOT touch anything in the application code. This, by the way, was one of the requirements: minimal refactoring for the introduction of a new class of memkesh.
You can close your eyes on the overhead of storing the timestamp and timeout, the memory is now cheap.
The fact that the data “go bad” by the amount of time equal to the time of data generation is not fatal and tolerable, however, new streams for the generation of the same data are not created. CTD!
PS
Suggestions and comments are welcome! Spelling - in PM, essentially - in the comments!
UPD
Comrades minus - argue your choice. Not everyone was born with the talents of Pushkin and Straustrup!
UPD 2 We
deal with the minuses:
1.
The MC class is needed so that nothing changes in the code. Absolutely. Replace it with the name of your wrapper over the memekesh, if any. If not, most decent IDEs support Refactor -> Change ClassName.
2.
The class of MS is static. This happened historically - a minimum of refactoring - the main requirement. I did not begin to redo the code for Habr - the main idea is reflected there.
This post is a brief summary of many hours of thought, a paper-clip, code sketches and, in the end, really working code in production.
Our site (and hereinafter simply a site) actively uses memekes for hot data. The code filling the memkesh can work for a very long time (0.5 seconds is a long time) and at the same time, user requests manage to start another hundred update procedures. The consequences are understandable, but for a long time we simply could not notice them at the level of the total load. Only when we saw bursts of time for servicing some requests (from the increased load, they also fell into SLOW_QUERIES_LOG MySQL) - then the work began to boil.
The problem is clear
Let us consider in more detail the scenario for requesting a key from a memkesh, in which the key is “rotten” or has not yet been installed.
To understand the problem, I drew a small diagram: The

update logic is controlled by code, i.e. the application itself knows if it returned FALSE, then you need to regenerate the key. As you can see from the diagram, the trouble arises at the moment when the data on the first request has not yet had time to “prepare”, and we are already asking for the same data.
Thanks to the evilbloodydemon habruiser for the exact definition - the situation is called the "Dog pile effect".
Decision
Numerous versions have been put forward - how to avoid this.
First, we thought about the lock system and the update queue. But this scenario is horribly slow.
Then they thought - if the code can regenerate data - let it do that. You just need to return it to FALSE. And immediately, reinstall the old data. Total: the update procedure will be launched once, and the data that will be returned to the application until the end of the regeneration process is “rotted out” only for the duration of the regeneration.
To do this, we must add in the memkesh not only the data itself, but also the timeout and the time the key is invalidated. An array for a time twice as large (to be sure) gets into a real memkesh. The documentation says that the longest key storage time is 30 days. Those. just put in the "wrapper" data for 15 days - 1 second for fidelity. The same applies to keys with a timeout = 0 (i.e., forever, until it is crowded out). Situations when the data in a memkesh are needed once in 15 days - I did not meet. If this happened to you, something needs to be changed.
We also quickly noticed a problem with increment. I had to agree that all increment keys end in "_inc", for example. And when such a key is found, we simply get the necessary data, which the memeksh itself has incremented. * I removed this fork or the Memcache_Proxy :: get () method.
The code
The code is documented where it is needed :) I apologize in advance for the code sheet, but I can’t cut it anymore.
class MC
{
private static $_proxy;
// Singleton for our class, extended of native Memcache class
private static function _proxy()
{
if (is_null(self::$_proxy) || self::$_proxy->closed) self::$_proxy = new Memcache_Proxy;
return self::$_proxy;
}
public static function get($key = '')
{
return self::_proxy()->get($key);
}
public static function set($key = '', $data = NULL, $flag = FALSE, $timeout = 3600)
{
return self::_proxy()->set($key, $data, $flag, $timeout);
}
public static function delete($key = '')
{
return self::_proxy()->delete($key);
}
public static function increment($key = '', $increment = 1)
{
return self::_proxy()->increment($key, $increment);
}
}
The MS class is needed to communicate with one instance of the memcache within the entire code without the need to explicitly declare a connection to the memcache. It will be created the first time you call the desired method in this class.
class Memcache_Proxy extends Memcache
{
public $closed = false;
public function __construct()
{
$this->connect(MEMCACHE_HOST, MEMCACHE_PORT, null);
$this->closed = false;
}
function __destruct()
{
$this->close();
$this->closed = true;
}
/**
* Mirror for $memcache->get() method
*/
public function get($key = '')
{
if (empty($key)) return FALSE;
$data = parent::get($key);
if ($data !== FALSE && $this->_is_valid_cache($data))
{
if (!isset($data['_dc_cache'])) $data['_dc_cache'] = NULL;
//check lifetime
if (time() > $data['_dc_life_end'])
{
//expired, save the same for a longer time for other connections
$this->set($key, $data['_dc_cache'], FALSE, $data['_dc_cache_time']);
return FALSE;
}
else
{
//still alive
return $data['_dc_cache'];
}
}
return FALSE;
}
/**
* Mirror for $memcache->set() method
*/
public function set($key = '', $data, $flag = FALSE, $timeout = 3600)
{
if (empty($key)) return FALSE;
// Place here "_inc" key check
if (is_int($data) || $data === FALSE)
parent::delete($key . '_increment');
// Maximum timeout = 15 days - 1 second
if ((int)$timeout == 0 || (int)$timeout > 1295999) $timeout = 1295999;
return $this->_set($key, $data, $flag, $timeout * 2);
}
/**
* Mirror for $memcache->delete() method
*/
public function delete($key = '')
{
if (empty($key)) return FALSE;
// Magic for increment. Place here "_inc" key check
parent::delete($key . '_increment');
return parent::delete($key);
}
public function increment($key, $increment = 1)
{
$inc_value = parent::increment($key . '_increment', $increment);
$data = parent::get($key);
if ($data === FALSE) return FALSE;
if ($this->_is_valid_cache($data))
{
if ($inc_value === FALSE)
{
$inc_value = $data['_dc_cache'] + $increment;
parent::set($key . '_increment', $inc_value, FALSE, $data['_dc_cache_time'] * 2);
}
$time = $data['_dc_life_end'] - time();
if ($time > 0)
{
$this->_set($key, $inc_value, FALSE, $time);
return $inc_value;
}
}
return $inc_value;
}
private function _set($key = '', $data, $flag = FALSE, $timeout = 3600)
{
$cache = array('_dc_cache' => $data, '_dc_life_end' => time() + $timeout, '_dc_cache_time' => $timeout);
return parent::set($key, $cache, $flag, $timeout);
}
// Maybe we have pure Memcache data, not our array structure
private function _is_valid_cache($value)
{
return (is_array($value) &&
isset($value['_dc_life_end']) && isset($value['_dc_cache_time']) &&
!empty($value['_dc_life_end']) && !empty($value['_dc_cache_time'])
) ? TRUE : FALSE;
}
}
Examples of using
Code, just code. If the data is sour, start the generation by returning FALSE only to the requestor and reinstall the same data at the same time. Thus, the next requestor will receive the old data until the first process finishes the generation and executes MC :: set () with the actual data. Immediately after that, all processes will receive relevant data.
$data = MC::get('some_key');
if ($data === FALSE)
{
// Может выполняться очень долго
$data = huge_generate_func_call();
MC::set('some_key', $data, FALSE, 3600);
}
Those. continue to use memkesh as before. If there was a wrapper for accessing the memkesh, you can correct it and DO NOT touch anything in the application code. This, by the way, was one of the requirements: minimal refactoring for the introduction of a new class of memkesh.
Summary
You can close your eyes on the overhead of storing the timestamp and timeout, the memory is now cheap.
The fact that the data “go bad” by the amount of time equal to the time of data generation is not fatal and tolerable, however, new streams for the generation of the same data are not created. CTD!
PS
Suggestions and comments are welcome! Spelling - in PM, essentially - in the comments!
UPD
Comrades minus - argue your choice. Not everyone was born with the talents of Pushkin and Straustrup!
UPD 2 We
deal with the minuses:
1.
The MC class is needed so that nothing changes in the code. Absolutely. Replace it with the name of your wrapper over the memekesh, if any. If not, most decent IDEs support Refactor -> Change ClassName.
2.
The class of MS is static. This happened historically - a minimum of refactoring - the main requirement. I did not begin to redo the code for Habr - the main idea is reflected there.