美文网首页
Swoole实现四种高性能静态API方案

Swoole实现四种高性能静态API方案

作者: Bing的天涯路 | 来源:发表于2020-01-02 21:47 被阅读0次

    什么是静态化API?
    静态化API可以理解成把一些接口的数据存储在服务器本地。常用的是存成json文件,也可以是放在swoole的table中,总之是用户不从数据库直接读取数据,而是从本地加载的方式来大幅提高性能,因为很多系统的性能瓶颈是在数据库的位置。

    解决方案
    方案1 easySwoole + crontab
    方案2 easySwoole定时器
    方案3 Swoole table
    方案4 Redis

    实现
    这里做的分页的场景,不包含分页的源码,只从拿到了分页的数据看看定时生成json和获取json的部分

    原始的方法 – 读取mysql

    这是原始的方法,每个用户访问都会去数据库里面读取一次,每一次分页也会访问数据库,会造成大量的资源开销。

    public function lists0(){
        $condition = [];
        if(!empty($this->params['cat_id'])){
            $condition['cat_id'] = intval($this->params['cat_id']);
        }
     
        try {
            $videoModel = new VideoModel();
            $data = $videoModel->getVideoData($condition, $this->params['page'], $this->params['size']);
        } catch (\Exception $e) {
            //$e->getMessage();
            return $this->writeJson(Status::CODE_BAD_REQUEST,"服务异常");
        }
     
        if(!empty($data['lists'])){
            foreach ($data['lists'] as &$list){
                $list['create_time'] = date("Ymd H:i:s",$list['create_time']);
                $list['video_duration'] = gmstrftime("%H:%M:%S",$list["video_duration"]);
            }
        }
     
        return $this->writeJson(Status::CODE_OK,'OK',$data);
    }
    

    知识点:

    • 获取以秒飞单位的时间长度使用gmstrftime(“%H:%M:%S”,$list[“video_duration”])
    • 在做模型的时候写一个基类,把连接数据库的工作放在这个基类的构造方法当中,这样每次实例化的时候就自动连接了,提高代码的复用性
    <?php
    /**
     * Created by bingxiong.
     * Date: 12/23/18
     * Time: 1:20 AM
     * Description:
     */
     
    namespace App\Model;
     
     
    use EasySwoole\Core\Component\Di;
     
    class Base
    {
        public $db = "";
     
        public function __construct()
        {
            if(empty($this->tableName)){
                throw new \Exception("table error");
            }
            $db = Di::getInstance()->get("MYSQL");
            if($db instanceof \MysqliDb){
                $this->db = $db;
            }else{
                throw new \Exception("db error");
            }
        }
     
        public function add($data){
            if(empty($data) || !is_array($data)){
                return false;
            }
            return $this->db->insert($this->tableName,$data);
        }
     
    }
    

    ### 方案一、方案二 使用easySwoole定时器以及contab的生成静态化API

    这是直接读取静态化API的方法,其实就是读文件然后返回给前端

    /**
     * 第二套方法 - 直接读取静态化json数据
     * @return bool
     */
    public function lists(){
        $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
        $videoFile = EASYSWOOLE_ROOT."/webroot/video/json/".$catId.".json";
        $videoData = is_file($videoFile) ? file_get_contents($videoFile) : [];
        var_dump($videoData);
        $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
     
        $count = count($videoData);
        // var_dump($videoData);
        return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
    }
    

    这里的getPagingData使用了array_slice来切割数组来做分页

    /**
     * 获取分页信息
     * @param $count
     * @param $data
     * @return array
     */
    public function getPagingData($count, $data){
        $totalPage = ceil($count / $this->params['size']);
        $data = $data ?? [];
        $data = array_slice($data, $this->params['from'], $this->params['size']);
        return [
            'total_page' => $totalPage,
            'page_size' => $this->params['page'],
            'count' => intval($count),
            'list' => $data
        ];
     
    }
    

    定时生成Json文件代码 – 核心部分 全局事件easySwooleEvent->mainServiceCreate中执行,这样easySwoole启动了之后就会执行,这里使用的原生的crontab,只能够精确到分

    $cacheVideoObj = new VideoCache();
    // 使用cronTab处理定时任务
    // 这里是闭包 要use $cacheVideoObj之后才能获取,实例化不放在function中是为了防止每次都实例化浪费资源
    CronTab::getInstance()
        ->addRule("test_bing_crontab", '*/1 * * * *', function () use($cacheVideoObj) {
            $cacheVideoObj->setIndexVideo();
        });
    

    也可以使用easySwoole的定时器来实现也是放在mainServiceCreate中执行,这里的代码一定要注意要注册一个onWorkerStart,然后指定一个进程去执行这个定时任务注意一下这里闭包里面又有一个闭包,外面的变量要use两次,一定要注意easwoole定时器的使用,这里的坑比较多一定要注意,优势是swoole的定时器可以到毫秒级而contab只能到分级

    // easySwoole自带定时器
    $register->add(EventRegister::onWorkerStart,function (\swoole_server $server,$workerId)use($cacheVideoObj){
        //让第一个进程去执行,否则每个进程都会执行
        if($workerId == 0){
            Timer::loop(1000*2,function ()use ($cacheVideoObj){
                $cacheVideoObj->setIndexVideo();
            });
        }
    });
    

    方案三 Swoole table的解决方案

    swoole_table一个基于共享内存和锁实现的超高性能,并发数据结构。用于解决多进程/多线程数据共享和同步加锁问题。性能十分强悍。使用起来有一点像是缓存。

    这里不再是生成一个json文件去读取了,

    读取table中数据给前端的方法,注意要use cache这个类,这里直接使用get就可以根据key获取到table中的数据,注意这个是一个单例模式,因此需要getInstance

    public function lists(){
        $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
        $videoFile = EASYSWOOLE_ROOT."/webroot/video/json/".$catId.".json";
     
        //  第三套方案 table
        $videoData = Cache::getInstance()->get("index_video_data_cat_id".$catId);
        $videoData = !empty($videoData) ? $videoData : [];
     
        $count = count($videoData);;
        return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
    }
    

    设置直接set(key,data)就可以了,注意这里的代码不是很严谨,比如要判断一下是不是设置成功了,没有设置成功发短信之类的,也要处理一下空值的场景,这里只是做一个演示。

    <?php
    /**
     * Created by bingxiong.
     * Date: 12/23/18
     * Time: 10:13 PM
     * Description:
     */
     
    namespace App\Lib\Cache;
     
    use App\Model\Video as VideoModel;
    use EasySwoole\Config;
    use EasySwoole\Core\Component\Cache\Cache;
     
    class Video
    {
        public function setIndexVideo()
        {
            $catIds = array_keys(Config::getInstance()->getConf("category"));
            array_unshift($catIds, 0);
     
            $modelObj = new VideoModel();
     
            foreach ($catIds as $catId) {
                $condition = [];
                if (!empty($catId)) {
                    $condition['cat_id'] = $catId;
                }
     
                try {
                    $data = $modelObj->getVideoCacheData($condition);
                } catch (\Exception $e) {
                    // 短信报警
                    $data = [];
                }
                if (empty($data)) {
     
                }
                foreach ($data as &$list) {
                    $list['create_time'] = date("Ymd H:i:s", $list["create_time"]);
                    $list["video_duration"] = gmstrftime("%H:%M:%s", $list["video_duration"]);
                }
                // 由于table存在内存所以重启服务器时数据会丢失,要将配置中的PERSISTENT_TIME设置为1进行落盘操作
                Cache::getInstance()->set("index_video_data_cat_id".$catId, $data);
            }
        }
    }
    

    注意,一定要去config里面开启

    'PERSISTENT_TIME'=>1//如果需要定时数据落地,请设置对应的时间周期,单位为秒
    

    否则会在重启服务的时候没有数据,因为每次启动的时候swoole table会清空,要等到定时去set table的时候才会有数据,因此要开启数据落盘,这样会生成两个文件:


    Screen-Shot-2018-12-24-at-3.57.35-PM-1024x487.png

    每次启动的时候由于还没有执行定时任务,就会先读取这两个落盘的文件中的数据,防止服务启动时等待table生成造成业务中断。

    方案四 Redis

    用redis来做数据缓存,每次从缓存里面度读

    先重写一下set方法,更加严谨一点

    /**
     * 重写set方法 处理一下失效时间以及数组转json
     * @param $key
     * @param $value
     * @param $time
     * @return bool|string
     */
    public function set($key, $value, $time){
        if(empty($key)){
            return '';
        }
        if(is_array($value)){
            $value = json_encode($value);
        }
        if(!$time){
            return $this->redis->set($key,$value);
        }
        return $this->redis->setex($key, $time, $value);
    }
    

    使用起来很简单啦,在之前的代码中

    Di::getInstance()->get("REDIS")->set("index_video_data_cat_id".$catId, $data);
    

    然后取出的数据的部分

    public function lists(){
        $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
        //redis
        $videoData = Di::getInstance()->get("REDIS")->get("index_video_data_cat_id".$catId);
        $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
        $count = count($videoData);
        return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
    }
    

    高度封装

    只需要在配置文件中进行配置即可选择相应方法

    设置静态化API和获取静态化API的方法

    <?php
    /**
     * Created by bingxiong.
     * Date: 12/23/18
     * Time: 10:13 PM
     * Description:
     */
     
    namespace App\Lib\Cache;
     
    use App\Model\Video as VideoModel;
    use EasySwoole\Config;
    use EasySwoole\Core\Component\Cache\Cache;
    use EasySwoole\Core\Component\Di;
    use EasySwoole\Core\Http\Message\Status;
     
    class Video
    {
        /**
         * 设置静态API的方法
         * @throws \Exception
         */
        public function setIndexVideo()
        {
            $catIds = array_keys(Config::getInstance()->getConf("category"));
            array_unshift($catIds, 0);
     
            // 获取配置
            try {
                $cacheType = Config::getInstance()->getConf("base.indexCacheType");
            } catch (\Exception $e) {
                return $this->writeJson(Status::CODE_BAD_REQUEST,"请求失败");
            }
     
            $modelObj = new VideoModel();
     
            foreach ($catIds as $catId) {
                $condition = [];
                if (!empty($catId)) {
                    $condition['cat_id'] = $catId;
                }
     
                try {
                    $data = $modelObj->getVideoCacheData($condition);
                } catch (\Exception $e) {
                    // 短信报警
                    $data = [];
                }
                if (empty($data)) {
     
                }
     
                foreach ($data as &$list) {
                    $list['create_time'] = date("Ymd H:i:s", $list["create_time"]);
                    $list["video_duration"] = gmstrftime("%H:%M:%s", $list["video_duration"]);
                }
     
                switch ($cacheType) {
                    case 'file':
                        $res = file_put_contents($this->getVideoCatIdFile($catId), json_encode($data));
                        break;
                    case 'table':
                        $res = Cache::getInstance()->set($this->getCatKey($catId), $data);
                        break;
                    case 'redis':
                        $res = Di::getInstance()->get("REDIS")->set($this->getCatKey($catId), $data);
                        break;
                    default:
                        throw new \Exception("请求不合法");
                        break;
                }
     
                if(empty($res)){
                //    记录日志
                //    报警
     
                }
     
            }
     
        }
     
        /**
         * 获取数据的方法
         * @param $catId
         * @return array|bool|mixed|null|string
         * @throws \Exception
         */
        public function getCache($catId){
            $cacheType = Config::getInstance()->getConf("base.indexCacheType");
            switch ($cacheType){
                case 'file':
                    $videoFile = $this->getVideoCatIdFile($catId);
                    $videoData = is_file($videoFile) ? file_get_contents($videoFile) : [];
                    $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
                    break;
                case 'table':
                    $videoData = Cache::getInstance()->get($this->getCatKey($catId));
                    $videoData = !empty($videoData) ? $videoData : [];
                    break;
                case 'redis':
                    $videoData = Di::getInstance()->get("REDIS")->get($this->getCatKey($catId));
                    $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
                    break;
                default:
                    throw new \Exception("请求不合法");
                    break;
            }
            return $videoData;
        }
     
        public function getVideoCatIdFile($catId = 0){
            return EASYSWOOLE_ROOT . "/webroot/video/json/" . $catId . ".json";
        }
     
     
        public function getCatKey($catId = 0){
            return "index_video_data_cat_id".$catId;
        }
    }
    

    只需修改配置文件

    <?php
    /**
     * Created by bingxiong.
     * Date: 12/24/18
     * Time: 5:12 PM
     * Description:
     */
    return [
        "indexCacheType" => "redis" // redis file table
    ];
    

    控制器获取数据给前端

    public function lists(){
        $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
        $videoData = (new VideoCache())->getCache($catId);
        $count = count($videoData);
        return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
    }
    

    总结总结

    以上讲解了文件缓存、swoole table内存型缓存以及redis缓存,新浪、网易用的是文件型,并且进行高度封装,应用层只需要直接结合配置文件调用,代码非常优雅。之后还会nginx+lua,这将利用一个开源工具openResty,这些工具在实际的工作中使用得非常广。

    本文作者熊冰,个人网站Bing的天涯路,转载请注明出处。

    相关文章

      网友评论

          本文标题:Swoole实现四种高性能静态API方案

          本文链接:https://www.haomeiwen.com/subject/dyhaoctx.html