Caching and locks
Two small modules for the same underlying problem: doing expensive work no more often than you have to. Cache remembers a value so it is not recomputed on every request. Lock lets only one process run a named section at a time, so work that must not overlap does not.
Cache: remember a computed value
The Cache module has a single entry point, Cache::Item. You give it a
lifetime in seconds, the module and item names that identify the value, a
dependencies array, and a callback that produces the value. If a fresh copy is
already stored, it is returned and the callback is never called; otherwise the
callback runs and its result is stored under those names before being returned.
$report = Cache::Item(
15, // keep for 15 minutes
'Shop',
'sales_summary',
[$year, $month], // dependencies: change these and the value is recomputed
function() use ($year, $month) {
return ShopReport::SalesSummary($year, $month);
}
);
The dependencies array is what keeps a cache honest. It is encoded and stored alongside the value, and a stored value is only reused when its dependencies match the ones passed in. Put every input the value is derived from into that array, and a change to any of them produces a fresh computation rather than a stale answer, with no separate step to remember to invalidate.
Two tiers: RAM and database
Cache has two tiers, and the lifetime you pass chooses between them. Every value is
held in a per-request RAM cache first, so the second call for the same item within
one request always returns the in-memory copy. When the lifetime is 0
the value lives only in RAM and is gone at the end of the request; that is the right
choice for something computed several times in one request but not worth persisting.
With a non-zero lifetime the value is also written to a database table with an expiry, so it is shared across requests until it ages out. The database read is wrapped so that a storage failure never breaks the caller: if the store cannot be read or written, the callback simply runs and its value is returned. A concurrent insert of the same item is caught and reconciled rather than surfaced as an error.
Clearing the RAM tier
When code changes a value mid-request and needs the next lookup to recompute rather
than return the in-memory copy, clear it with Cache::ClearRamCache,
passing the same module, item and dependencies used to store it.
Cache::ClearAllRamCache empties the whole per-request RAM cache. These
touch only the RAM tier; the database tier expires on its own timer.
Cache::ClearRamCache('Shop', 'sales_summary', [$year, $month]);
Lock: run a section once at a time
Where a cache avoids repeating work, a lock serialises work that must not overlap.
LockDatabase::Grab takes a name and a callback: it acquires the named
lock, runs the callback, and releases the lock when the callback returns or throws.
Only one process can hold a given name at a time, so a second caller with the same
name waits its turn rather than running alongside the first.
LockDatabase::Grab('shop_nightly_rollup', function() {
// only one process runs this at a time
ShopRollup::Run();
});
The lock is a row in the database keyed by a hash of the name, so it holds across
separate processes and servers, not just within one request. Acquiring it retries a
few times with a randomised, doubling backoff, so many processes arriving at once do
not all retry in lockstep. If the lock cannot be taken after those retries,
Grab throws rather than running the callback, and because the lock is
always removed in a finally it is released even when the callback throws.
When to reach for each
- Cache when a value is expensive to compute or fetch and can be reused: a report, a rendered fragment, a result read from a slow source. The dependencies array is your invalidation.
- Lock when a section of work must not run twice at once: a nightly rollup, a one-at-a-time import, or serialising the processing behind a queue so two workers do not act on the same batch.
The two often pair up. A lock guards the recomputation so that when a cached value expires, one process refreshes it while the others wait rather than every request recomputing it at once.
That is the end of the guide. Back to the Introduction.