Use Redis to store the user storage if Redis is the cache
authorTim Düsterhus <duesterhus@woltlab.com>
Tue, 18 Oct 2016 11:57:25 +0000 (13:57 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Tue, 18 Oct 2016 11:57:25 +0000 (13:57 +0200)
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.

wcfsetup/install/files/lib/data/option/OptionEditor.class.php
wcfsetup/install/files/lib/system/user/storage/UserStorageHandler.class.php

index 42ca690ecbb712a985c935c9ece9ae889857f5a2..4d3782b21d335b2d607f7c733e371add46800fc0 100644 (file)
@@ -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();
                        });
                }
        }
index 3981805fa00255f38f1046ff8d982c73e5ec5317..df44919e39590b2d3b45c800a37a363cd64fac91 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 namespace wcf\system\user\storage;
+use wcf\system\cache\source\RedisCacheSource;
+use wcf\system\cache\CacheHandler;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\SingletonFactory;
 use wcf\system\WCF;
@@ -31,12 +33,29 @@ class UserStorageHandler extends SingletonFactory {
         */
        protected $updateFields = [];
        
+       /**
+        * redis instance
+        * @var Redis
+        */
+       protected $redis = null;
+       
+       /**
+        * Checks whether Redis is available.
+        */
+       protected function init() {
+               if (CacheHandler::getInstance()->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;
+       }
 }