Usage Guide & Core Concepts
Moving beyond basic Key-Value storage, php-memcached offers advanced features required for high-traffic, production environments. This guide covers connection management, race condition prevention, multi-key operations, and payload optimization.
1. Persistent Connections
By default, a new Memcached connection is created on every PHP request and destroyed when the request ends. In high-traffic applications, tearing down and rebuilding TCP connections hundreds of times a second adds significant overhead.
You can enable Persistent Connections by passing a string ID to the Memcached constructor. All Memcached instances instantiated with the same ID will share the same underlying connection pool across PHP requests.
<?php
// The string 'my_app_pool' identifies this specific connection pool
$m = new Memcached('my_app_pool');
// IMPORTANT: Because the object persists across requests,
// you must ensure you don't keep adding the same servers over and over.
// Use getServerList() to check if the pool is already populated.
if (empty($m->getServerList())) {
$m->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
$m->addServers([
['10.0.0.1', 11211, 50],
['10.0.0.2', 11211, 50]
]);
}
2. Server Pools & Consistent Hashing
When you add multiple servers to a pool, php-memcached hashes the key to decide which server holds the data. By default, it uses standard modulo hashing.
The Problem with Modulo Hashing: If a server goes offline (or you add a new one), the number of servers changes. This alters the math for every key, invalidating nearly your entire cache instantly (a "thundering herd" event).
The Solution: Consistent Hashing (Ketama). Consistent hashing maps keys to a continuous circle. If a node goes down, only the keys mapped to that specific node are re-routed to the next available node. The rest of your cache remains intact.
Always enable consistent hashing in production:
$m->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
// Or via ini settings:
// ini_set('memcached.default_consistent_hash', true);
3. Preventing Cache Stampedes with add and replace
While set() unconditionally overwrites data, add() and replace() are conditional operations.
add($key, $value): Fails if the key already exists. Useful for distributed locks or ensuring a worker is the only one generating a report.replace($key, $value): Fails if the key does not exist. Useful for updating data only if it is already actively being cached.
// Attempt to acquire a "lock" for 30 seconds
if ($m->add('report_generation_lock', 'locked', 30)) {
// We got the lock! Generate the expensive report.
$data = generateExpensiveReport();
$m->set('report_data', $data, 3600);
// Release the lock early
$m->delete('report_generation_lock');
} else {
// Another PHP process is already generating the report.
// Wait, or serve stale data.
}
4. Multi Operations (Batching)
Network latency is the biggest bottleneck in caching. Asking Memcached for 50 keys sequentially requires 50 network round-trips. Using getMulti() retrieves all 50 keys in a single round-trip.
$keys = ['user_1', 'user_2', 'user_3'];
// Retrieve multiple keys at once.
// Passing Memcached::GET_PRESERVE_ORDER ensures the returned array
// matches the order of the requested keys, padding misses with NULL.
$results = $m->getMulti($keys, Memcached::GET_PRESERVE_ORDER);
foreach ($results as $key => $value) {
if ($value === null) {
echo "Cache miss for {$key}\n";
} else {
echo "Cache hit for {$key}: {$value}\n";
}
}
You can similarly use setMulti(array $items, int $expiration) and deleteMulti(array $keys) for batch writes and deletes.
5. Check and Set (CAS) - Preventing Race Conditions
If two processes fetch a counter, increment it locally, and save it back simultaneously, one update will overwrite the other. This is a race condition.
Memcached solves this with Check and Set (CAS). When you fetch a value using the Memcached::GET_EXTENDED flag, you also receive a unique 64-bit cas token representing the current version of that data.
When you attempt to write back using cas(), Memcached verifies the token. If another process updated the data in the meantime, the token on the server will have changed, and your cas() operation will fail.
$key = 'product_inventory_123';
// 1. Fetch value AND the CAS token
$result = $m->get($key, null, Memcached::GET_EXTENDED);
if ($m->getResultCode() === Memcached::RES_SUCCESS) {
$inventory = $result['value'];
$casToken = $result['cas'];
// 2. Modify data locally
$inventory -= 1; // Someone bought an item
// 3. Save back ONLY if the CAS token matches
if ($m->cas($casToken, $key, $inventory)) {
echo "Inventory successfully updated!\n";
} else {
echo "Collision detected! Someone else updated inventory.\n";
// You would typically loop and retry the whole process here.
}
}
6. Optimizing Payloads: Serializers & Compression
Memcached memory is a precious resource. How PHP formats the data before sending it heavily impacts performance.
Serializers
By default, php-memcached uses standard PHP serialize(). This is slow and creates bloated string representations of arrays and objects.
If you compiled php-memcached with igbinary or msgpack support, you should switch to them. They are binary serializers that represent data in a fraction of the size, saving RAM and network bandwidth.
// Highly recommended for production
$m->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY);
Compression
You can enable automatic compression for payloads. If a payload exceeds the compression_threshold (default 2000 bytes), the extension will compress it. If the compression saves enough space (dictated by compression_factor, default 1.3 or 23%), it stores the compressed version.
$m->setOption(Memcached::OPT_COMPRESSION, true);
// Choose your algorithm.
// FASTLZ is very fast but compresses less. ZSTD is the modern standard.
$m->setOption(Memcached::OPT_COMPRESSION_TYPE, Memcached::COMPRESSION_ZSTD);
Note: You cannot use append() or prepend() on keys that have been compressed, as modifying compressed binary data directly will corrupt it.