Indeed, many technologies can only be truly understood when you actually use them.
I once heard about the three scenarios of cache invalidation: cache avalanche, cache penetration, and cache breakdown. I found them very hard to understand back then, because I rarely used caching at the time and never encountered these problems, so I could not grasp these theories.
After using caching for a while recently, I have become much more familiar with these theories when revisiting them.
First, let's talk about the problem I encountered today:
When developing interfaces in PHP, I need to provide three interfaces for the client to display product-related information.
The data for all three interfaces comes from the same external API.
To avoid the three back-end interfaces calling the external API three separate times, I used caching: based on the product ID, the same product is cached after the first call, so subsequent interface calls can directly use the cached data without repeatedly calling the external API.
That sounds reasonable, right? Here is the initial code:
public function getGoodsData($goodsId, $pid = '', $relationId = '')
{
$cacheName = 'goods_' . $goodsId;
$responseArray = Cache::get($cacheName);
if (empty($responseArray)) {
$responseArray = $this->getGoodsDataFromApi($goodsId, $pid, $relationId);
Cache::set($cacheName, $responseArray, 7200);
}
return $responseArray;
}
But when I checked the logs, I found something was wrong: three external requests were still being sent every time, and the cache was not used at all.
Thinking about it, it makes sense: the client requests all three interfaces at the same time, there is no serial execution at all, which means this method is called simultaneously. At this point, there is still no data in the cache, so three requests are sent, and the cache is completely invalidated.
This is where mutual exclusion (mutex) locks come into play.
Mutex Locks
A mutual exclusion lock (Mutex, Mutual Exclusion Lock) is a synchronization mechanism used to control access to shared resources. It ensures that at any given time, only one thread or process can access the shared resource, and all other threads or processes must wait.
To implement a mutex lock in PHP you can use extensions provided by PHP, or implement it based on files, databases and other methods.
This article mainly uses file locking for implementation, which is the second method. It does not require installing any extra extensions, so it is relatively simple to implement.
If you prefer, you can also implement it using the PHP Mutex extension, which is covered in method one.
Method 1: Use the PHP Mutex Extension
PHP provides an extension called Mutex to implement mutex locks, which requires separate installation.
You can use the Mutex extension to ensure that access to shared resources across multiple processes is safe.
Usage example:
Method 2: Use File Locking
The principle of implementing a mutex lock with file locking (file lock) mainly relies on the operating system's support for file locking mechanisms. Specifically, it depends on the file locking functionality provided by the file system to ensure that only one process can get exclusive access to the target resource at the same time.
In PHP, you can use the flock() function to implement file locking. The flock() function provides a simple interface to use the operating system's native file locking functionality. We first try to acquire a file lock. If we can acquire the lock, we perform operations within the critical section: check if the cache exists, if not fetch the data from the API and cache it; then release the lock. If we cannot acquire the lock, we wait for a period of time and try again.
With this approach, when the same $goodsId is requested simultaneously, only one request will enter the critical section to access the API, and other requests will wait for that request to complete before continuing execution.
You can see the optimized code below:
public function getGoodsData($goodsId, $pid = '', $relationId = '')
{
$cacheName = 'goods_' . $goodsId;
// Try to get data from cache
$responseArray = Cache::get($cacheName);
// If the cache does not exist, try to acquire the file lock
if (empty($responseArray)) {
$lockFile = root_path() . "/runtime/temp/goods_lock_" . $this->safeFileName($goodsId) . ".lock";
$fp = fopen($lockFile, "w+");
// Try to acquire the file lock
if (flock($fp, LOCK_EX)) {
// Check cache again, because another request may have already fetched and cached the data after we started waiting for the lock
$responseArray = Cache::get($cacheName);
if (empty($responseArray)) {
// The cache still does not exist, fetch data from the API and cache it
$responseArray = $this->getGoodsDataFromApi($goodsId, $pid, $relationId);
Cache::set($cacheName, $responseArray, 7200);
}
// Release the lock
flock($fp, LOCK_UN);
// After completing operations in the critical section, delete the lock file
unlink($lockFile);
} else {
// Failed to acquire the lock, wait for a period of time and try again
usleep(10000); // Wait 10 milliseconds
fclose($fp);
// Recursively call this method to try fetching data again after other requests complete
return $this->getGoodsData($goodsId, $pid, $relationId);
}
fclose($fp);
}
return $responseArray;
}
Now when I check the request logs in the database, I can see that cache invalidation no longer occurs, only one request actually fetches remote data:
It should be noted that whether you use the Mutex extension or file locking, you must ensure that you release the lock in time after finishing operations on the shared resource in the critical section to avoid problems such as deadlocks.
Summary
File locking leverages the file locking mechanism provided by the operating system. Through file descriptors and atomic operations, it ensures that only one process can obtain exclusive access at the same time, thereby implementing mutex lock functionality.
This mechanism is simple and effective. If you do not want to add extra extensions, you can consider using this method.
This is a discussion topic separated from the original thread at https://juejin.cn/post/7368767452614770723
