Created
October 16, 2013 17:00
-
-
Save Korko/7011182 to your computer and use it in GitHub Desktop.
Memcache class to manage multiple datas to store later (reduces calls to sql and manage race conditions)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Memcache class to manage multiple datas to store later (reduces calls to sql and manage race conditions) | |
*/ | |
abstract class CacheData extends Memcached { | |
const ACTION_SET = 'set'; | |
const ACTION_INCREMENT = 'inc'; | |
/** | |
* Security gap bigger than the number of cache keys updated between 2 cache requests (see save method) | |
*/ | |
const SECURITY_GAP = 10; | |
/** | |
* Number of max actions stored before forcing to save (to prevent memcache overload) | |
*/ | |
const MAX_ENTRIES = 100000; | |
/** | |
* Log a new key modified | |
*/ | |
private function logAction($action, $key, $value) { | |
$index = $this->increment('cachedata.inc', 1, TRUE); | |
$this->set('cachedata.actions.'.$index, array( | |
'action' => $action, | |
'key' => $key, | |
'value' => $value | |
)); | |
// Do we have to force save ? | |
if($index === floor(self::MAX_ENTRIES/2) || $index === self::MAX_ENTRIES) { | |
$this->save(); | |
} | |
} | |
/** | |
* Increment this key by $incrementValue | |
*/ | |
public function incrementData($key, $incrementValue = 1) { | |
$this->logAction(self::ACTION_INCREMENT, $key, $incrementValue); | |
} | |
/** | |
* Set the value for this key | |
*/ | |
public function setData($key, $value) { | |
$this->logAction(self::ACTION_SET, $key, $value); | |
} | |
/** | |
* Force save of datas | |
*/ | |
public function save() { | |
// Get the start and end of current keys to update | |
$minIndex = $this->get('cachedata.start') ?: 0; | |
$maxIndex = $this->get('cachedata.inc'); | |
// Nothing to save | |
if($maxIndex === $minIndex) { | |
return; | |
} | |
// Let's define the new index to start with | |
// Always keep the security gap in case of delay between get and reset of inc | |
$newIndex = $maxIndex + self::SECURITY_GAP; | |
// Caution, reaching MAX_ENTRIES means we have to restart from 0 | |
// (we should already have saved from 0 to MAX_ENTRIES/2 see logAction) | |
if($newIndex >= self::MAX_ENTRIES) { | |
$newIndex = 0; | |
} | |
// Move and reset counter with a certain gap (to be sure no new cache key where added while we save) | |
$this->set('cachedata.inc', $newIndex); | |
// Now save everything | |
// Do not use newIndex as it may have been reset to 0 | |
$error = $this->saveCachedData($minIndex, $maxIndex + self::SECURITY_GAP); | |
if(!$error) { | |
// Now that everything's saved, ignore all these actions | |
$this->set('cachedata.start', $newIndex); | |
} | |
} | |
// Get and save all data in cache | |
private function saveCachedData($minIndex, $maxIndex) { | |
// Let's get old data (for increment) | |
$data = $this->getCachedData($minIndex, $maxIndex); | |
// Ok now get the values for increment fields | |
$keys = array(); | |
foreach($data as $key => $currentData) { | |
if(array_key_exists($currentData, 'inc')) { | |
$keys[] = $key; | |
} | |
} | |
// Now get previous values | |
$previousData = $this->getData($keys); | |
// Update all increment so they are similar to set | |
foreach($keys as $key) { | |
$data[$key]['value'] = (array_key_exists($previousData, $key) ? $previousData[$key] : 0) + $data[$key]['inc']; | |
unset($data[$key]['inc']); | |
} | |
// Now save everything | |
$error = FALSE; | |
foreach($data as $key => $currentData) { | |
if($this->saveData($currentData)) { | |
// Remove actions for this update | |
foreach($currentData['indexes'] as $index) { | |
$this->delete('cachedata.actions.'.$index); | |
} | |
} else { | |
$error = TRUE; | |
} | |
} | |
return $error; | |
} | |
// Get all data and resolve increment/set concurrencies | |
private function getCachedData($minIndex, $maxIndex) { | |
$data = array(); | |
for($i = $minIndex; $i < $maxIndex; $i++) { | |
// We get each keys to update | |
if(($action = $this->get('cachedata.actions.'.$i)) !== FALSE) { | |
// Construct list of indexes where this key was updated | |
$indexes = array_key_exists($data, $action['key']) ? $data[$action['key']]['indexes'] : array(); | |
$indexes[] = $i; | |
// What is the action to do? Set or Increment? | |
switch($action['action']) { | |
// Simply | |
case self::ACTION_SET: | |
$data[$action['key']] = array( | |
'key' => $action['key'], | |
'value' => $action['value'], | |
'indexes' => $indexes | |
); | |
break; | |
case self::ACTION_INCREMENT: | |
$currentData = array( | |
'key' => $action['key'], | |
'inc' => $action['value'], | |
'indexes' => $indexes | |
); | |
if(isset($data[$action['key']])) { | |
// Was it a set or an increment before? | |
if(isset($data[$action['key']]['inc'])) { | |
$currentData['inc'] += $data[$action['key']]['inc']; | |
} else { | |
$currentData['value'] += $data[$action['key']]['value']; | |
} | |
} | |
$data[$action['key']] = $currentData; | |
break; | |
default: | |
trigger_error('Invalid action : '.var_export($action, TRUE)); | |
} | |
} | |
} | |
return $data; | |
} | |
// Get stored data (e.g. in DB) for those keys and return them indexed by $key | |
abstract protected function getData(array $keys); | |
// Save this current data (e.g. in DB) and return a boolean if error or no | |
abstract protected function saveData(array $currentData); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment