Created
June 12, 2020 18:38
-
-
Save hbackman/82fc0e86fd484001dd354f64e668b28b to your computer and use it in GitHub Desktop.
Laravel 7.x Database Cache Locking for 6.x
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Providers; | |
use App\Cache\DatabaseStore; | |
use Illuminate\Cache\CacheManager; | |
use Illuminate\Foundation\Application; | |
use Illuminate\Support\ServiceProvider; | |
class CacheServiceProvider extends ServiceProvider | |
{ | |
/** | |
* Register the service provider. | |
*/ | |
public function boot() | |
{ | |
$this->registerDatabaseDriver(); | |
} | |
// This driver is copied from the laravel/framework github repo. | |
// This should be removed if we upgrade to laravel 7, because | |
// it's already built in. | |
private function registerDatabaseDriver() | |
{ | |
$cacheManager = $this->app->make(CacheManager::class); | |
$cacheManager->forgetDriver('database'); | |
$registerDriver = | |
function (Application $app, array $config) { | |
/** @var CacheManager $self */ | |
$self = $this; | |
// Get the current database connection and prefix for | |
// the given configured connection. | |
$connection = $app['db'] | |
->connection($config['connection'] ?? null); | |
// Build and return the reposistory | |
return $self->repository( | |
new DatabaseStore( | |
$connection, | |
$config['table'], | |
$self->getPrefix($config) | |
) | |
); | |
}; | |
$cacheManager->extend('database', $registerDriver); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Cache; | |
use Illuminate\Database\QueryException; | |
use Illuminate\Database\Connection; | |
use Illuminate\Cache\Lock; | |
class DatabaseLock extends Lock | |
{ | |
/** | |
* The database connection instance. | |
* | |
* @var Connection | |
*/ | |
protected $connection; | |
/** | |
* The database table name. | |
* | |
* @var string | |
*/ | |
protected $table; | |
/** | |
* The prune probability odds. | |
* | |
* @var array | |
*/ | |
protected $lottery; | |
/** | |
* Create a new lock instance. | |
* | |
* @param Connection $connection | |
* @param string $table | |
* @param string $name | |
* @param int $seconds | |
* @param string|null $owner | |
* @param array $lottery | |
* @return void | |
*/ | |
public function __construct(Connection $connection, $table, $name, $seconds, $owner = null, $lottery = [2, 100]) | |
{ | |
parent::__construct($name, $seconds, $owner); | |
$this->connection = $connection; | |
$this->table = $table; | |
$this->lottery = $lottery; | |
} | |
/** | |
* Attempt to acquire the lock. | |
* | |
* @return bool | |
*/ | |
public function acquire() | |
{ | |
$acquired = false; | |
try { | |
$this->connection->table($this->table)->insert([ | |
'key' => $this->name, | |
'owner' => $this->owner, | |
'expiration' => $this->expiresAt(), | |
]); | |
$acquired = true; | |
} catch (QueryException $e) { | |
$updated = $this->connection->table($this->table) | |
->where('key', $this->name) | |
->where(function ($query) { | |
return $query->where('owner', $this->owner)->orWhere('expiration', '<=', time()); | |
})->update([ | |
'owner' => $this->owner, | |
'expiration' => $this->expiresAt(), | |
]); | |
$acquired = $updated >= 1; | |
} | |
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) { | |
$this->connection->table($this->table)->where('expiration', '<=', time())->delete(); | |
} | |
return $acquired; | |
} | |
/** | |
* Get the UNIX timestamp indicating when the lock should expire. | |
* | |
* @return int | |
*/ | |
protected function expiresAt() | |
{ | |
return $this->seconds > 0 ? time() + $this->seconds : now()->addDays(1)->getTimestamp(); | |
} | |
/** | |
* Release the lock. | |
* | |
* @return bool | |
*/ | |
public function release() | |
{ | |
if ($this->isOwnedByCurrentProcess()) { | |
$this->connection->table($this->table) | |
->where('key', $this->name) | |
->where('owner', $this->owner) | |
->delete(); | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Releases this lock in disregard of ownership. | |
* | |
* @return void | |
*/ | |
public function forceRelease() | |
{ | |
$this->connection->table($this->table) | |
->where('key', $this->name) | |
->delete(); | |
} | |
/** | |
* Returns the owner value written into the driver for this lock. | |
* | |
* @return string | |
*/ | |
protected function getCurrentOwner() | |
{ | |
return optional($this->connection->table($this->table)->where('key', $this->name)->first())->owner; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Cache; | |
use Illuminate\Cache\DatabaseStore as LaravelDatabaseStore; | |
use Illuminate\Database\ConnectionInterface; | |
class DatabaseStore extends LaravelDatabaseStore implements LockGenerator | |
{ | |
/** | |
* The name of the cache locks table. | |
* | |
* @var string | |
*/ | |
protected $lockTable; | |
/** | |
* A array representation of the lock lottery odds. | |
* | |
* @var array | |
*/ | |
protected $lockLottery; | |
/** | |
* Create a new database store | |
* | |
* @param ConnectionInterface $connection | |
* @param $table | |
* @param string $prefix | |
* @param string $lockTable | |
* @param array $lockLottery | |
*/ | |
public function __construct( | |
ConnectionInterface $connection, | |
$table, | |
$prefix = '', | |
$lockTable = 'cache_locks', | |
$lockLottery = [2, 100]) | |
{ | |
parent::__construct($connection, $table, $prefix); | |
$this->lockLottery = $lockLottery; | |
$this->lockTable = $lockTable; | |
} | |
/** | |
* Get a lock instance | |
* | |
* @param string $name | |
* @param int $seconds | |
* @param string|null $owner | |
* @return \Illuminate\Contracts\Cache\Lock | |
*/ | |
public function lock($name, $seconds = 0, $owner = null) | |
{ | |
return new DatabaseLock( | |
$this->connection, | |
$this->lockTable, | |
$this->prefix.$name, | |
$seconds, | |
$owner, | |
$this->lockLottery | |
); | |
} | |
/** | |
* Restore a lock instance using the owner identifier. | |
* | |
* @param string $name | |
* @param string $owner | |
* @return \Illuminate\Contracts\Cache\Lock | |
*/ | |
public function restoreLock($name, $owner) | |
{ | |
return $this->lock($name, 0, $owner); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Cache; | |
interface LockGenerator | |
{ | |
/** | |
* Get a lock instance | |
* | |
* @param string $name | |
* @param int $seconds | |
* @param string|null $owner | |
* @return \Illuminate\Contracts\Cache\Lock | |
*/ | |
public function lock($name, $seconds = 0, $owner = null); | |
/** | |
* Restore a lock instance using the owner identifier. | |
* | |
* @param string $name | |
* @param string $owner | |
* @return \Illuminate\Contracts\Cache\Lock | |
*/ | |
public function restoreLock($name, $owner); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment