Using Redis for implementing Mutex to prevent high load in the database

Let's consider a simple web application. A role-based ticket management system. Each user page has a widget that shows his work progress. The work progress value is changed based on the ticket status the user worked on.

Without any cache system implementation for each load page, it will query the database to retrieve the work progress value for the user. this impacts high SQL query and high CPU utilization.

// every page load
$value = $db->getWorkProgressForUser($userId);
return $value;

Implement Cache

Return the value from the cache if exists. This will reduce the calls to the database on every page load.

$key = MyWorkProgress:{$userId} //create cache key

$value = $cache->get($key) // Fetch from cache if present

if (!$value) { // if not present in cache
    $value = $db->query()     //database query
    $cache->set($key, $value) //set result to cache
}

return $value;

Invalidate cache key

Do not forget to invalidate cache data, if the ticket status is changed. If the ticket status changes frequently before retrieving the value from DB and setting it to the cache. Concurrent requests can result in bad data and high CPU utilization as it tries to re-query the database, currently being calculated by another request.

$ticketStatus = $app->getTicketStatus($ticketId); // Check ticket status
if ($ticketStatus->changed()) { // If status is changed
    $key = MyWorkProgress:{$userId}
    $cache->del($key) // invalidate cache data
}

Implement Mutex

Implementing mutex helps to avoid recalculation if currently being processed by another request.

$key = MyWorkProgress:{$userId}
$lockKey = MyWorkProress:{$userId}:lock

$value = $cache->get($key);
// Fetch from cache if present
if ($value) {
    return $value;
}
// Avoid recalculation if currently being processed by another request
if ($lockKey->exists()) {
    return empty()
}

// Add lock before we start processing
$cache->set($lockKey);

try {
    //database query
    $value = $db->query();
    //set result to cache
    $cache->set($key, $value);
} finally {
    // always release lock when done, even on failure
    $cache->del($lockKey);
}

return $value;