From 31367471ef46caf37412cefe025a3a296c0c136c Mon Sep 17 00:00:00 2001 From: IoTcat Date: Sat, 6 Apr 2019 12:55:54 +0800 Subject: [PATCH] add cache --- .../IoTgod-www/usr/plugins/TpCache/Plugin.php | 455 ++++++++++++++++++ .../IoTgod-www/usr/plugins/TpCache/README.md | 70 +++ .../TpCache/driver/cache.interface.php | 23 + .../TpCache/driver/typecho_memcache.class.php | 62 +++ .../driver/typecho_memcached.class.php | 59 +++ .../TpCache/driver/typecho_mysql.class.php | 104 ++++ .../TpCache/driver/typecho_redis.class.php | 59 +++ 7 files changed, 832 insertions(+) create mode 100644 server/cn/home/www/IoTgod-www/usr/plugins/TpCache/Plugin.php create mode 100644 server/cn/home/www/IoTgod-www/usr/plugins/TpCache/README.md create mode 100644 server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/cache.interface.php create mode 100644 server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_memcache.class.php create mode 100644 server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_memcached.class.php create mode 100644 server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_mysql.class.php create mode 100644 server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_redis.class.php diff --git a/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/Plugin.php b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/Plugin.php new file mode 100644 index 0000000..a64969c --- /dev/null +++ b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/Plugin.php @@ -0,0 +1,455 @@ +begin = array('TpCache_Plugin', 'C'); + Typecho_Plugin::factory('index.php')->end = array('TpCache_Plugin', 'S'); + + //页面编辑 + Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = array('TpCache_Plugin', 'post_update'); + Typecho_Plugin::factory('Widget_Contents_Page_Edit')->finishPublish = array('TpCache_Plugin', 'post_update'); + + //评论 + Typecho_Plugin::factory('Widget_Feedback')->finishComment = array('TpCache_Plugin', 'comment_update'); + + + return '插件安装成功,请设置需要缓存的页面'; + } + + /** + * 禁用插件方法,如果禁用失败,直接抛出异常 + * + * @static + * @access public + * @throws Typecho_Plugin_Exception + */ + public static function deactivate() + { + try { + $uninstall_sql = 'DROP TABLE IF EXISTS `%prefix%cache`'; + $db = Typecho_Db::get(); + $prefix = $db->getPrefix(); + $sql = str_replace('%prefix%', $prefix, $uninstall_sql); + $db->query($sql); + } catch (Exception $e) { + echo $e->getMessage(); + } + } + + /** + * 获取插件配置面板 + * + * @access public + * @param Typecho_Widget_Helper_Form $form 配置面板 + * @return void + */ + public static function config(Typecho_Widget_Helper_Form $form) + { + + $list = array( + 'index' => '首页', + 'archive' => '归档', + 'post' => '文章', + 'attachment' => '附件', + 'category' => '分类', + 'tag' => '标签', + 'author' => '作者', + 'search' => '搜索', + 'feed' => 'feed', + 'page' => '页面', + ); + $element = new Typecho_Widget_Helper_Form_Element_Checkbox('cache_page', $list, array('index', 'post', 'search', 'page', 'author', 'tag'), '需要缓存的页面'); + $form->addInput($element); + + $list = array('关闭', '开启'); + $element = new Typecho_Widget_Helper_Form_Element_Radio('login', $list, 1, '是否对已登录用户失效', '已经录用户不会触发缓存策略'); + $form->addInput($element); + + $list = array('关闭', '开启'); + $element = new Typecho_Widget_Helper_Form_Element_Radio('enable_ssl', $list, '0', '是否支持SSL'); + $form->addInput($element); + + $list = array( + '0' => '不使用缓存', + 'memcached' => 'Memcached', + 'memcache' => 'Memcache', + 'redis' => 'Redis', + 'mysql' => 'Mysql' + ); + $element = new Typecho_Widget_Helper_Form_Element_Radio('cache_driver', $list, '0', '缓存驱动'); + $form->addInput($element); + + $element = new Typecho_Widget_Helper_Form_Element_Text('expire', null, '86400', '缓存过期时间', '86400 = 60s * 60m *24h,即一天的秒数'); + $form->addInput($element); + + $element = new Typecho_Widget_Helper_Form_Element_Text('host', null, '127.0.0.1', '主机地址', '主机地址,一般为127.0.0.1'); + $form->addInput($element); + + $element = new Typecho_Widget_Helper_Form_Element_Text('port', null, '11211', '端口号', '端口号,memcache对应11211,Redis对应6379,其他类型随意填写'); + $form->addInput($element); + + $list = array('关闭', '开启'); + $element = new Typecho_Widget_Helper_Form_Element_Radio('is_debug', $list, 0, '是否开启debug'); + $form->addInput($element); + + $list = array('关闭', '清除所有数据'); + $element = new Typecho_Widget_Helper_Form_Element_Radio('is_clean', $list, 0, '清除所有数据'); + $form->addInput($element); + } + + /** + * 手动保存配置句柄 + * @param $config array 插件配置 + * @param $is_init bool 是否初始化 + */ + public static function configHandle($config, $is_init) + { + if ($is_init != true && $config['cache_driver'] != '0') { + + $driver_name = $config['cache_driver']; + $class_name = "typecho_$driver_name"; + $file_path = "driver/$class_name.class.php"; + require_once 'driver/cache.interface.php'; + require_once $file_path; + self::$cache = call_user_func(array($class_name, 'getInstance'), self::$plugin_config); + try { + if ($config['is_clean'] == '1') self::$cache->flush(); + } catch (Exception $e) { + print $e->getMessage(); + die; + } + // 删除缓存仅生效一次 + $config['is_clean'] = '0'; + } + + Helper::configPlugin('TpCache', $config); + } + + /** + * 个人用户的配置面板 + * + * @access public + * @param Typecho_Widget_Helper_Form $form + * @return void + */ + public static function personalConfig(Typecho_Widget_Helper_Form $form) + { + } + + /** + * 缓存前置操作 + */ + public static function C() + { + $start = microtime(true); + // 插件初始化 + if (self::init() == false) return false; + // 前置条件检查 + if (self::pre_check() == false) return false; + + //获取路径信息 + $pathInfo = self::$request->getPathInfo(); + + //判断是否需要缓存 + if (!self::needCache($pathInfo)) return false; + + try { + $data = self::get(self::$path); + if ($data != false) { + $data = unserialize($data); + //如果超时 + if ($data['c_time'] + self::$plugin_config->expire <= time()) { + + if (self::$plugin_config->is_debug) echo "Expired!\n"; + $data['c_time'] = $data['c_time'] + 20; + self::set(self::$path, serialize($data)); + } else { + if (self::$plugin_config->is_debug) echo "Hit!\n"; + if ($data['html']) echo $data['html']; + $end = microtime(true); + $time = number_format(($end - $start), 6); + if (self::$plugin_config->is_debug) echo 'This page loaded in ', $time, ' seconds'; + die; + } + } else { + if (self::$plugin_config->is_debug) echo "Can't find cache!"; + } + + } catch (Exception $e) { + echo $e->getMessage(); + } + // 先进行一次刷新 + ob_flush(); + + } + + /** + * 前置检查 + * @return bool + */ + public static function pre_check() + { + //对登录用户失效 + if (self::check_login()) return false; + //针对POST失效 + if (self::$request->isPost()) return false; + //是否支持SSL + if (self::$plugin_config->enable_ssl == '0' && self::$request->isSecure() == true) return false; + return true; + } + + /** + * 判断用户是否登录 + * @return bool + * @throws Typecho_Widget_Exception + */ + public static function check_login() + { + //http与https相互独立 + return (self::$plugin_config->login && Typecho_Widget::widget('Widget_User')->hasLogin()); + } + + /** + * 根据配置判断是否需要缓存 + * @param string 路径信息 + * @return bool + */ + public static function needCache($path) + { + //后台数据不缓存 + $pattern = '#^' . __TYPECHO_ADMIN_DIR__ . '#i'; + if (preg_match($pattern, $path)) return false; + + //action动作不缓存 + $pattern = '#^/action#i'; + if (preg_match($pattern, $path)) return false; + + $_routingTable = self::$sys_config->routingTable; + + $exclude = array('_year', '_month', '_day', '_page'); + + foreach ($_routingTable[0] as $key => $route) { + if ($route['widget'] != 'Widget_Archive') continue; + + if (preg_match($route['regx'], $path, $matches)) { + $key = str_replace($exclude, '', str_replace($exclude, '', $key)); + + if (in_array($key, self::$plugin_config->cache_page)) { + if (self::$plugin_config->is_debug) echo "This page needs to be cached!\n" . ' + Bug Report '; + self::$path = $path; + return true; + } + } + } + + return false; + } + + + /** + * 缓存后置操作 + */ + public static function S() + { + //对登录用户失效 + if (self::check_login()) return; + + //若self::$key不为空,则使用缓存 + if (is_null(self::$key)) return; + + + $html = ob_get_contents(); + + if (!empty($html)) { + $data = array(); + $data['c_time'] = time(); + $data['html'] = $html; + //更新缓存 + if (self::$plugin_config->is_debug) echo "Cache updated!\n"; + self::set(self::$key, serialize($data)); + } + + } + + + /** + * 编辑文章后更新缓存 + * @param $contents + * @param $class + */ + public static function post_update($contents, $class) + { + if ('publish' != $contents['visibility'] || $contents['created'] > time()) { + return; + } + //获取系统配置 + $options = Helper::options(); + + if(!$options->plugin('TpCache')->cache_driver){ + return; + } + //获取文章类型 + $type = $contents['type']; + //获取路由信息 + $routeExists = (NULL != Typecho_Router::get($type)); + + if (!is_null($routeExists)) { + $db = Typecho_Db::get(); + $contents['cid'] = $class->cid; + $contents['categories'] = $db->fetchAll($db->select()->from('table.metas') + ->join('table.relationships', 'table.relationships.mid = table.metas.mid') + ->where('table.relationships.cid = ?', $contents['cid']) + ->where('table.metas.type = ?', 'category') + ->order('table.metas.order', Typecho_Db::SORT_ASC)); + $contents['category'] = urlencode(current(Typecho_Common::arrayFlatten($contents['categories'], 'slug'))); + $contents['slug'] = urlencode($contents['slug']); + $contents['date'] = new Typecho_Date($contents['created']); + $contents['year'] = $contents['date']->year; + $contents['month'] = $contents['date']->month; + $contents['day'] = $contents['date']->day; + } + + //生成永久连接 + $path_info = $routeExists ? Typecho_Router::url($type, $contents) : '#'; + + self::init(); + + if (self::needCache($path_info)) self::delete(self::$path); + } + + /** + * 评论更新 + * + * @access public + * @param array $comment 评论结构 + * @param Typecho_Widget $post 被评论的文章 + * @param array $result 返回的结果上下文 + * @param string $api api地址 + * @return void + */ + public static function comment_update($comment) + { + $req = new Typecho_Request(); + self::delete(str_replace($req->getRequestRoot(), '', $req->getReferer())); + } + + /** + * 插件配置初始化 + * @return bool + * @throws Typecho_Plugin_Exception + */ + public static function init() + { + if (is_null(self::$sys_config)) { + self::$sys_config = Helper::options(); + } + if (is_null(self::$plugin_config)) { + self::$plugin_config = self::$sys_config->plugin('TpCache'); + } + + if (self::$plugin_config->cache_driver == '0') { + return false; + } + + if (is_null(self::$cache)) { + $driver_name = self::$plugin_config->cache_driver; + $class_name = "typecho_$driver_name"; + $file_path = "driver/$class_name.class.php"; + require_once 'driver/cache.interface.php'; + require_once $file_path; + self::$cache = call_user_func(array($class_name, 'getInstance'), self::$plugin_config); + } + if (is_null(self::$request)) { + self::$request = new Typecho_Request(); + } + + return true; + } + + + public static function set($path, $data) + { + + if (!is_null(self::$key)) return self::$cache->set(self::$key, $data); + $prefix = self::$request->getUrlPrefix(); + self::$key = md5($prefix . $path); + + return self::$cache->set(self::$key, $data); + } + + public static function add($path, $data) + { + + } + + public static function get($path) + { + if (!is_null(self::$key)) return self::$cache->get(self::$key); + $prefix = self::$request->getUrlPrefix(); + self::$key = md5($prefix . $path); + return self::$cache->get(self::$key); + } + + /** + * 删除指定路径 + * @param string $path 待删除路径 + * @param null $del_home 是否删除首页缓存 + */ + public static function delete($path, $del_home = null) + { + $prefixs = array( + 'http' + . '://' . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : + ($_SERVER['SERVER_NAME'] . (in_array($_SERVER['SERVER_PORT'], array(80, 443)) + ? '' : ':' . $_SERVER['SERVER_PORT'])) + ), + 'https' + . '://' . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : + ($_SERVER['SERVER_NAME'] . (in_array($_SERVER['SERVER_PORT'], array(80, 443)) + ? '' : ':' . $_SERVER['SERVER_PORT'])) + ), + ); + $keys = array(); + if (!is_array($path)) { + $keys[] = $path; + } else { + $keys = $path; + } + + + foreach ($keys as $v) { + foreach ($prefixs as $prefix) { + echo $prefix . $v; + @self::$cache->delete(md5($prefix . $v)); + } + } + + if (is_null($del_home)) { + foreach ($prefixs as $prefix) { + echo $prefix . '/'; + @self::$cache->delete(md5($prefix . '/')); + } + } + } +} \ No newline at end of file diff --git a/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/README.md b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/README.md new file mode 100644 index 0000000..52ccfd1 --- /dev/null +++ b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/README.md @@ -0,0 +1,70 @@ +## 功能 + +减缓网站并发压力而开发的缓存插件。 + +## 注意 + +1. 支持**Memcache**,**Redis**,**Mysql**三种驱动。 +1. **非js方式的**访问统计插件会失效 +1. BUG请在[缓存插件TpCache for Typecho][1]页汇报 + + + + + +## 使用说明 + +### 后台设置 + +![后台设置截图][2] + +### 组件支持 + +**请确保你的服务器memcache套件工作正常。** + +目前老高提供了php**memcache**与**memcached**的支持,请选择对应的驱动。 + +memcached配置请参考[Linux服务器配置memcached并启用PHP支持][3]。 + +Redis配置请参考[Linux服务器配置Redis并启用PHP支持][4]。 + +### 缓存更新机制 + +**目前以下操作会触发缓存更新** + +- 来自原生评论系统的评论 +- 后台文章或页面更新 +- 重启memcached +- 缓存到期 + +### 评论 + +原生评论简单测试过,没有大问题。 + +不过既然使用缓存了不如直接使用第三方评论系统,如多说。 + +## 性能 + +在老高的烂主机上随便就能跑到保守800的并发(CPU占用不到70%),什么概念呢? + +理论上支持每天**69120000**(60\*60\*24\*800)的PV。 + +## 下载 + + TpCache + + TpCache + +## 安装 + +请将文件夹**重命名**为TpCache。再拷贝至`usr/plugins/下`。 + +## 升级 + +请先**禁用此插件**后再升级,很多莫名其妙的问题都是因为没有先禁用而直接升级导致的! + + + [1]: http://www.phpgao.com/tpcache_for_typecho.html + [2]: http://www.phpgao.com/usr/uploads/2015/05/3901966986.jpeg + [3]: http://www.phpgao.com/php-memcached-extension-installation.html + [4]: http://www.phpgao.com/redis_php.html \ No newline at end of file diff --git a/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/cache.interface.php b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/cache.interface.php new file mode 100644 index 0000000..26d8eb9 --- /dev/null +++ b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/cache.interface.php @@ -0,0 +1,23 @@ +host = $option->host; + $this->port = $option->port; + $this->expire = $option->expire; + $this->init($option); + } + + static public function getInstance($option) + { + if (is_null(self::$_instance) || isset (self::$_instance)) { + self::$_instance = new self($option); + } + return self::$_instance; + } + + public function init($option) + { + try { + $this->mc = new Memcache; + $this->mc->addServer($this->host, $this->port); + } catch (Exception $e) { + echo $e->getMessage(); + } + } + + public function add($key, $value, $expire = null) + { + return $this->mc->add($key, $value, false, is_null($expire) ? $this->expire : $expire); + } + + public function delete($key) + { + return $this->mc->delete($key); + } + + public function set($key, $value, $expire = null) + { + return $this->mc->set($key, $value, false, is_null($expire) ? $this->expire : $expire); + } + + public function get($key) + { + return $this->mc->get($key); + } + + public function flush() + { + return $this->mc->flush(); + } +} \ No newline at end of file diff --git a/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_memcached.class.php b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_memcached.class.php new file mode 100644 index 0000000..cb0192d --- /dev/null +++ b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_memcached.class.php @@ -0,0 +1,59 @@ +host = $option->host; + $this->port = $option->port; + $this->expire = $option->expire; + $this->init($option); + } + + static public function getInstance($option) { + if (is_null ( self::$_instance ) || isset ( self::$_instance )) { + self::$_instance = new self($option); + } + return self::$_instance; + } + + public function init($option) + { + try{ + $this->mc = new Memcached; + $this->mc->addServer($this->host, $this->port); + }catch (Exception $e){ + echo $e->getMessage(); + } + } + + public function add($key, $value, $expire=null) + { + return $this->mc->add($key, $value, is_null($expire) ? $this->expire : $expire); + } + + public function delete($key) + { + return $this->mc->delete($key); + } + + public function set($key, $value, $expire=null) + { + return $this->mc->set($key, $value, is_null($expire) ? $this->expire : $expire); + } + + public function get($key) + { + return $this->mc->get($key); + } + + public function flush() + { + return $this->mc->flush(); + } +} \ No newline at end of file diff --git a/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_mysql.class.php b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_mysql.class.php new file mode 100644 index 0000000..3d2f153 --- /dev/null +++ b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_mysql.class.php @@ -0,0 +1,104 @@ +host = $option->host; + $this->port = $option->port; + $this->expire = $option->expire; + $this->init($option); + } + + static public function getInstance($option) + { + if (is_null(self::$_instance) || isset (self::$_instance)) { + self::$_instance = new self($option); + } + return self::$_instance; + } + + public function init($option) + { + $this->db = Typecho_Db::get(); + $prefix = $this->db->getPrefix(); + $table_name = $prefix . 'cache'; + $sql_detect = "SHOW TABLES LIKE '%" . $table_name . "%'"; + + if(count($this->db->fetchAll($sql_detect)) == 0){ + $this->install_db(); + }else{ + // 用访问触发缓存过期 + $this->db->query($this->db->delete('table.cache')->where('time <= ?', (time() - $this->expire) )); + } + } + + public function install_db() + { + $install_sql = ' +DROP TABLE IF EXISTS `%prefix%cache`; +CREATE TABLE `%prefix%cache` ( + `key` char(32) NOT NULL, + `data` text, + `time` bigint(20) DEFAULT NULL, + PRIMARY KEY (`key`) +) ENGINE=MyISAM DEFAULT CHARSET=%charset%'; + + $prefix = $this->db->getPrefix(); + $search = array('%prefix%', '%charset%'); + $replace = array($prefix, str_replace('UTF-8', 'utf8', Helper::options()->charset)); + $sql = str_replace($search, $replace, $install_sql); + $sqls = explode(';', $sql); + + foreach ($sqls as $sql) { + try{ + $this->db->query($sql); + }catch (Typecho_Db_Exception $e){ + echo $e->getMessage(); + } + } + } + + public function add($key, $value, $expire = null) + { + $this->db->query($this->db->insert('table.cache')->rows(array( + 'key' => $key, + 'data' => $value, + 'time' => time() + ))); + } + + public function delete($key) + { + return $this->db->query($this->db->delete('table.cache')->where('key = ?', $key)); + } + + public function set($key, $value, $expire = null) + { + $this->delete($key); + $this->add($key, $value); + } + + public function get($key) + { + $rs = $this->db->fetchRow($this->db->select('*')->from('table.cache')->where('key = ?', $key)); + if(count($rs) == 0){ + return false; + }else{ + return $rs['data']; + } + } + + public function flush() + { + return $this->db->query($this->db->delete('table.cache')); + } +} \ No newline at end of file diff --git a/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_redis.class.php b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_redis.class.php new file mode 100644 index 0000000..e9d70e8 --- /dev/null +++ b/server/cn/home/www/IoTgod-www/usr/plugins/TpCache/driver/typecho_redis.class.php @@ -0,0 +1,59 @@ +host = $option->host; + $this->port = $option->port; + $this->expire = $option->expire + 0; + $this->init($option); + } + + static public function getInstance($option) { + if (is_null ( self::$_instance ) || isset ( self::$_instance )) { + self::$_instance = new self($option); + } + return self::$_instance; + } + + public function init($option) + { + try{ + $this->redis = new Redis(); + $this->redis->connect($this->host, $this->port); + }catch (Exception $e){ + echo $e->getMessage(); + } + } + + public function add($key, $value, $expire=null) + { + return $this->redis->set($key, $value, is_null($expire) ? $this->expire : $expire); + } + + public function delete($key) + { + return $this->redis->delete($key); + } + + public function set($key, $value, $expire=null) + { + return $this->redis->set($key, $value, is_null($expire) ? $this->expire : $expire); + } + + public function get($key) + { + return $this->redis->get($key); + } + + public function flush() + { + return $this->redis->flushDB(); + } +} \ No newline at end of file