Skip to content

Instantly share code, notes, and snippets.

@Korko
Created October 16, 2013 17:00
Show Gist options
  • Save Korko/7011182 to your computer and use it in GitHub Desktop.
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)
<?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