From: Tim Düsterhus Date: Tue, 18 Oct 2016 11:57:25 +0000 (+0200) Subject: Use Redis to store the user storage if Redis is the cache X-Git-Tag: 3.0.0_Beta_3~16 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=522764203eaf97b4a0ee5aa2c5afaa2548f2c352;p=GitHub%2FWoltLab%2FWCF.git Use Redis to store the user storage if Redis is the cache Using Redis avoids the fairly complex and deadlock prone shutdown() function and thus improves performance and stability in high traffic boards. This patch deliberately avoids modifying any lines of the existing MySQL based implementation, instead opting to only add if-guarded code for the Redis implementation to ensure that no bugs are accidentally introduced into the existing MySQL based user storage. This Redis implementation has been tested in production for over a year, with billions of commands processed, exposing not a single bug. --- diff --git a/wcfsetup/install/files/lib/data/option/OptionEditor.class.php b/wcfsetup/install/files/lib/data/option/OptionEditor.class.php index 42ca690ecb..4d3782b21d 100644 --- a/wcfsetup/install/files/lib/data/option/OptionEditor.class.php +++ b/wcfsetup/install/files/lib/data/option/OptionEditor.class.php @@ -5,6 +5,7 @@ use wcf\data\IEditableCachedObject; use wcf\system\cache\builder\OptionCacheBuilder; use wcf\system\cache\CacheHandler; use wcf\system\io\AtomicWriter; +use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; use wcf\util\FileUtil; @@ -94,10 +95,12 @@ class OptionEditor extends DatabaseObjectEditor implements IEditableCachedObject if ($flushCache) { // flush caches (in case register_shutdown_function gets not properly called) CacheHandler::getInstance()->flushAll(); + UserStorageHandler::getInstance()->clear(); // flush cache before finishing request to flush caches created after this was executed register_shutdown_function(function() { CacheHandler::getInstance()->flushAll(); + UserStorageHandler::getInstance()->clear(); }); } } diff --git a/wcfsetup/install/files/lib/system/user/storage/UserStorageHandler.class.php b/wcfsetup/install/files/lib/system/user/storage/UserStorageHandler.class.php index 3981805fa0..df44919e39 100644 --- a/wcfsetup/install/files/lib/system/user/storage/UserStorageHandler.class.php +++ b/wcfsetup/install/files/lib/system/user/storage/UserStorageHandler.class.php @@ -1,5 +1,7 @@ getCacheSource() instanceof RedisCacheSource) { + $this->redis = CacheHandler::getInstance()->getCacheSource()->getRedis(); + } + } + /** * Loads storage for a given set of users. * * @param integer[] $userIDs */ public function loadStorage(array $userIDs) { + if ($this->redis) return; + $tmp = []; foreach ($userIDs as $userID) { if (!isset($this->cache[$userID])) $tmp[] = $userID; @@ -72,6 +91,15 @@ class UserStorageHandler extends SingletonFactory { public function getStorage(array $userIDs, $field) { $data = []; + if ($this->redis) { + foreach ($userIDs as $userID) { + $data[$userID] = $this->redis->hget($this->getRedisFieldName($field), $userID); + if ($data[$userID] === false) $data[$userID] = null; + } + + return $data; + } + foreach ($userIDs as $userID) { if (isset($this->cache[$userID][$field])) { $data[$userID] = $this->cache[$userID][$field]; @@ -105,6 +133,12 @@ class UserStorageHandler extends SingletonFactory { return null; } + if ($this->redis) { + $result = $this->redis->hget($this->getRedisFieldName($field), $userID); + if ($result === false) return null; + return $result; + } + // make sure stored data is loaded if (!isset($this->cache[$userID])) { $this->loadStorage([$userID]); @@ -125,8 +159,14 @@ class UserStorageHandler extends SingletonFactory { * @param string $fieldValue */ public function update($userID, $field, $fieldValue) { + if ($this->redis) { + $this->redis->hset($this->getRedisFieldName($field), $userID, $fieldValue); + $this->redis->expire($this->getRedisFieldName($field), 86400); + return; + } + $this->updateFields[$userID][$field] = $fieldValue; - + // update data cache for given user if (isset($this->cache[$userID])) { $this->cache[$userID][$field] = $fieldValue; @@ -140,6 +180,13 @@ class UserStorageHandler extends SingletonFactory { * @param string $field */ public function reset(array $userIDs, $field) { + if ($this->redis) { + foreach ($userIDs as $userID) { + $this->redis->hdel($this->getRedisFieldName($field), $userID); + } + return; + } + foreach ($userIDs as $userID) { $this->resetFields[$userID][] = $field; @@ -155,6 +202,11 @@ class UserStorageHandler extends SingletonFactory { * @param string $field */ public function resetAll($field) { + if ($this->redis) { + $this->redis->del($this->getRedisFieldName($field)); + return; + } + $sql = "DELETE FROM wcf".WCF_N."_user_storage WHERE field = ?"; $statement = WCF::getDB()->prepareStatement($sql); @@ -171,6 +223,8 @@ class UserStorageHandler extends SingletonFactory { * Removes and inserts data records on shutdown. */ public function shutdown() { + if ($this->redis) return; + $toReset = []; // remove outdated entries @@ -215,7 +269,7 @@ class UserStorageHandler extends SingletonFactory { $conditions = new PreparedStatementConditionBuilder(); $conditions->add("userID IN (?)", [$userIDs]); $conditions->add("field = ?", [$field]); - + $sql = "DELETE FROM wcf".WCF_N."_user_storage ".$conditions; $statement = WCF::getDB()->prepareStatement($sql); @@ -264,10 +318,36 @@ class UserStorageHandler extends SingletonFactory { * Removes the entire user storage data. */ public function clear() { + if ($this->redis) { + $this->redis->setnx('ush:_flush', TIME_NOW); + $this->redis->incr('ush:_flush'); + return; + } + $this->resetFields = $this->updateFields = []; $sql = "DELETE FROM wcf".WCF_N."_user_storage"; $statement = WCF::getDB()->prepareStatement($sql); $statement->execute(); } + + /** + * Returns the field name for use in Redis. + * + * @param string $fieldName + * @return string + */ + protected function getRedisFieldName($fieldName) { + $flush = $this->redis->get('ush:_flush'); + + // create flush counter if it does not exist + if ($flush === false) { + $this->redis->setnx('ush:_flush', TIME_NOW); + $this->redis->incr('ush:_flush'); + + $flush = $this->redis->get('ush:_flush'); + } + + return 'ush:'.$flush.':'.$fieldName; + } }