PHP核心基础模块设计之化学方程式两端的企业级数据

先来说一下企业级网站数据的多级缓存。
缓存的使用,对于有项目开发经验的技术人员来说,都不陌生。在PHP开发中,可以使用的缓存有很多,譬如文件缓存、APCU缓存、数据库缓存、SESSION缓存、COOKIE缓存、Redis和Memcache缓存等。但这些都只是存储的方式不同,已经体现在功效上。而在这往前一点,则是我们对于数据的理解。理解决定使用。构成系统有两大核心要素:数据与程序。其中对于流通在二进制中世界中的数据,它的品质和价值,不取决于我们缓存的方式有多好,而在于我们对它的理解有多深。
这里提供了一种数据可视化管理的新视角,基于这个新视角,进而讨论组织数据的方式,最后落地到多层缓存架构的设计与实现。

1 数据划分的新视角

在用户打开浏览器,访问网站的页面后,会看到很多形式多样的内容、信息和图片、视频等。这些都来源于后端代码处理、加工和整合的数据。当网站系统成长到一定规模的时候,这些数据会来自四面八方,有从数据库提取的,有从接口请求的,有读缓存的,也有根据公式、规则实时计算的,不一而足。更让人无可奈何的是,在过往项目迭代中,都缺少对这些数据的统一管理、规划和维护,以致于来一个新数据,就不加考虑“盲目”堆彻。最后造就了混沌、乏力的数据体系。

图1 传统网站数据的划分,混沌而乏力

那应该怎么组织企业级网站数据,才是行之有效的呢?
软件开发这一领域,有不少灵感是从其他行业、自然界启发而来的。我们不妨先来回想一下高中时代学习的化学方程式。可以说,有时化学方程式复杂到我怎么背都背不下来,但我知道在化学方程式的左边是反应物,右边是生成物,而中间是反应条件。实际上,化学方程式是很容易理解的,并且它能够清晰、准备、恰当地表示复杂化学反应的本质。
现在,作为软件开发工程师的我们,有没有发现错误复杂的网站数据与化学之间的微妙关系?
一方面,由分子组成的化学物质,有活泼、稳定之分。越活泼的物质变化越快,反之,越稳定的物质变化缓慢,可以放置很长一段时间。而对于网站数据,有的需要频繁更新甚至实时更新,如商品库存;有的则相对稳定,不需要频繁更新并在业务上可以接受一定时间的延时,如商品的信息;有的甚至非常稳定,通常很长的时间内都不会发生变化,如全站的通用开关配置。
另一方面,化学反应前的分子,会在特定的条件下,产生化学反应,从而生成最终新的分子和产物。对于网站数据,道理是类似的。所获取到的原始数据,通常都不会直接输出到页面,而是会通过一系列的操作,加工处理,包括数据在业务层面的二次转换、包装、排序,也包括了技术层面的缓存、队列、多线程等。经过各函数、类、接口、模块处理后,才会形成我们最终呈现给终端用户的信息。
这样,联想到原子的轨道图,根据网站数据自身的稳定程度,我们可以得到一种新的企业级网站数据新视角。

图2 数据的稳定程序是指数据变化的频率,频率越高越活泼,反之越稳定

当一个网站是企业级网站时,就整体页面而言,它需要很高的响应速度。细分下来,页面各数据需要具备合适的缓存策略,并能以一种简单明了的方式来进行管理。这些数据,肯定不是杂乱无章地存在系统,而是以某一种特定的方式,遵循某种规律,有序地分布在系统中。我们没找到,并不表示这种规律不存在。若按这里所说的新视角,可以明显发现,针对以往混沌的数据,现在有了全新井然有序的划分规律。

图3 传统数据分布与新视角的划分对比

在这基础再进一步,我们可以推演出正确使用企业数据的方式,通俗来说就是“打开企业级网站数据的正确姿势”。为了在高效缓存和实时性之间取得更好的平衡,需要为不同的业务数据,根据各自的稳定程度,为其选择不同的缓存策略。例如对于列表页的档期信息,由于访问频繁,不能每次都直接从数据库中读取,而是应该进行缓存之。又如对于成千上万个细粒度的商品信息,如果使用的是本地缓存,也是不合理的,因为本地缓存空间有限,根本容纳不了那么多组合情况的海量商品列表,最终会导致本地缓存频繁淘汰,反而适得其反。
结合数据的稳定程度,和数据访问的情况,可以引出数据稳定-访问象限分布图。

图4 数据稳定-访问象限分布

这个数据稳定-访问象限分布图很有意思,值得花点时间来研究和学习。一旦掌握其中的要点,对于后面设计和规划企业级网站数据将会大有裨益。可以看到,前面是按数据自身稳定程度单一维度进行划分,现在是结合数据的使用场景,即增加了数据访问的频率这个新维度,从而构成了此象限图。那个这象限图又可以给到我们什么启发呢?
就这里的图而言,左上角的实心大圆圈表示最稳定的数据但又是访问得最多的数据,例如全局配置数据,几乎每个页面都会用到,最常见的莫过于网站的标题、Logo图片链接这些基本公共信息。在这个区域要注意的一点是,缓存时要避免Hot Key问题。右上角表示极不稳定的数据,同样需要高频访问,例如商品的库存。尤其在秒杀活动的时候,对于库存的数量要求就更严格了,一个也不能少,一个也不能多,否则就会出现超卖的现象。这时,要求的数据都是具备实时性的,不能缓存,或者即便缓存也要快速同步,因为有时效性要求。
再来看下半部分,左下方区域表示既稳定、访问次数又少的数据,因此我们不用那么担心,可以缓存很长一段时间,也可以使用一些低速的缓存方案。例如新闻文章一经审核发布后,基本不会再作修改,但并不是每篇文章都会高频访问。最后对于右下方象限,则又是另一个需要特别关注的地方。在这个区域,数据是不稳定的,容易发生变化,但访问量时高时低,流量不确定。通常我们的核心业务数据就落在这个区域内,占据了大部分网站数据。这些数据可能是用户生成的,或者是通过管理后台发布编辑而来。例如用户发布的微博,在供应商管理系统录入的商品信息等。
针对网站数据本身的特质,我们可以根据数据的稳定程度来对数据进行归类划分。然后,结合数据的使用场景,考虑其访问量的情况,得出数据稳定-访问象限分布图。这是一个新的数据管理视角。下面我们再来看下,在这新视角的基础上,可以得出怎样的架构设计,实现多级缓存和可视化管理。

2 使用缓存的常见例子与弊端

如前面所说,在PHP中能使用的缓存方式有很多,例如文件缓存、Redis缓存、APCU缓存等。但现在我们缺的不是缓存的技术,而是缺乏正确使用缓存的方式,以及恰当组合使用多级缴存的策略。先来看一个普通使用缓存的简单例子。
一个常见的场景是获取用户的个人信息,根据用户的ID查询数据库中的用户表,然后返回相应的用户信息。这里暂时不关注底层实现,以下是调用的代码示例。

<?php
// 用户ID
$userId = 1;

// 从数据库获取用户信息
$model = new User();
$userInfo = $model->getUserInfo($userId);

// 输出用户信息
var_dump($userInfo);

当需要使用缓存时,下面的代码写法也是很常见的,也很容易理解。

<?php
// 用户ID
$userId = 1;

// 从缓存中获取
$cache = new Cache();
$key = 'userinfo_' . $userId;
$userInfo = $cache->get($key);

// 缓存没有
if (empty($userInfo)) {
    // 从数据库获取用户信息
    $model = new User();
    $userInfo = $model->getUserInfo($userId);

    // 同步写入缓存,缓存10分钟
    $cache->set($key, $userInfo, 600);
}

// 输出用户信息
var_dump($userInfo);

虽然这是大众使用缓存的方式,初步看起来也没有什么问题,但试想一下,打开一个企业级网站页面时,需要的数据数量从几份到十几份不等,而一个企业级系统中的数据规模更是可以达到成千上百个。再深入思考一下,如果都是按照这种初级的缓存使用方式,将会催生多少重复性的代码?除了混乱外,还增加了维护的成本。重复性的代码,又分为两类,一类是显而易见的重复代码,基本都是“拷贝-粘贴”式的代码;另一类是隐式的重复,不易察觉,表面上看起来有点类似,但没有明显的重复。而这里使用缓存的初级方式就属于后者。这也是很多编程开发人员没有发现,或者没有留心发现的原因。
至于为什么,后面加入的新团队成员,即便他们有这种洞察的能力,也没能去做出好的改变呢?我想这是社会认同感的作崇。当一个新成员加入到团队后,他在写新代码前都有意识或无意识参考之前代码的写法,然后模仿之。这是有原因的,我们很多团队都是约定自己的编程风格和规范,即要求新人不能标新立异。另一方面,新人模仿前人写的代码,也能很大程度上避免过往遇到过的缺陷。但是规则是需要树立的,但也需要打破的。遵循规则,可以保持顺畅;而打破规则,则可以实现创新,获得突破。
再回过头来看这里的例子,相信读者在目前工作的项目中也不乏发现这类情况。那它的问题是什么呢?我们又该如何改进或应对呢?
如果我们把上面使用缓存的例子,加以分析,就能发现在短短的十来行代码里,其实它做了很多事情。
第一步,构建查询参数。这里参数只有一个,即用户ID,但也有可能需要多个参数。
第二步,获取缓存数据。具体的缓存方式这里没有指定,可以是文件缓存、Redis缓存或者APCU缓存。
第三步,当缓存没有数据时,进入第四步;否则进入第六步。注意缓存的key要与参数相对应。
第四步,从数据源获取原始数据。这里的原始数据存放在数据库中,但也有可能需要从远程接口获取,或其他存储媒介。
第五步,将原始数据同步写入缓存,并设置缓存时间。
第六步,返回或输出数据结果。
从上面六个步骤中,我们不禁发问:数据应该采用何种缓存方式,应该设置缓存超时时间为多久?而这又是出于何种考虑或依据?这里混合了两个层级,一个是高层的概念层,一个是底层的实现层。作为客户端开发人员,他所关心的是获取到用户的信息,而不关注具体如何获取。那有什么方法可以把缓存机制使用的这套模板抽取出来,既然分离关注点,又能在到代码重用的效果呢?最后一个问题,假设我们希望在获取到数据后还想进行一些实时计算的操作,又该如何设计和开发?
这些问题,归根到底,都是技术与业务关注点混杂的后果。从业务角度上看,我们希望根据用户ID就能提取用户的信息;但落实到技术代码编程实现时,技术人员就无意识地考虑到技术实现的细节。我该怎么存数据,我又该怎么缓存数据呢?这里顺便延伸说一下,我们软件开发工程师所任职的大部分公司都可以分为三类:纯技术驱动型的以线上虚拟世界为主的信息公司、业务驱动型的互联网公司、和需要增设技术部门但不倚重技术的实体类公司。除了第一类公司外,大部分业务驱动型公司的核心竞争力都体现在产品业务上,而作为在这类公司内任职的开发人员,也就是我们,更多应该考虑的是如何通过技术快速支撑业务的发展。
顺着这个思维再稍微想多一步,我们就不难发现,回归到例子中用户信息的获取,这就是业务的需要。而为了更快地获取用户信息而使用缓存,则是我们技术本身的要求而已。如果不加考虑,盲目顺从的使用初级的方式,不断编写重复性代码使用缓存,不仅增加了偶然的复杂性,还拖慢了快速交付有价值业务的进度。并且,如果我们需要从宏观上对某一大类的数据进行调控时,例如有访问量低时,我们容许可以直接查询数据库以获得数据的实时性,当访问量增加时,我们则要使用缓存,以损失短暂数据有效性为代价换取高可用的服务质量。如果原来的代码像七龙珠那样,分散有世界各地,等我们历经千辛万苦找集时,世界早已发生巨大的变化。
我们应当始终关注业务的需要,然后把技术的关注点从中剥离出来,实现技术与业务的关注点分离。提取出来的抽象的技术解决方案,要么是一个微架构,要么就是一个行业通用的领域标准。有了这样的认识后,再来看下如何进行核心的设计,以满足技术上非功能性的各种要求。

3 数据供给器

为了实现技术与业务的关注点分离,接下来将探寻可行的解决方案。
最近,在我们团队开发的项目中,我发现了使用匿名函数来封装原始数据获取的方式。大致的实现方式,应用到我们的例子的示例代码如下:

<?php
// 用户ID
$userId = 1;
$key = 'userinfo_' . $userId;

// 通过匿名函数封装原始数据的获取
$userInfo = smart_cache_retrieve($key, 600, function () use ($userId){
    // 从数据库获取用户信息
    $model = new User();
    $userInfo = $model->getUserInfo($userId);
});

这种方式,确实抽取了缓存使用的模板,在某个程度上达到了重用的效果。但我觉得不够优雅,也不够正统。
首先,它使用了面向过程编程的匿名函数,并不是说面向过程编程的写法就不好,而是在于PHP本身是一个脚本解释性编程语法,其语法是很通俗易懂的,并且匿名函数不是PHP的一等公民,PHP的上下文与闭包函数并不是很完美,也不是它的侧重点。对于闭包函数支持较好的语言,我认为是Javascript。或许,我们能找到一种更符合PHP本质、更容易被接受的架构设计。好的架构设计不会让人觉得有意而为,或刻意要去学习和被迫接受,而是能让人顺其自然,按照约定成俗的方式去理解和使用。另一个不够友好的地方是,关于缓存的三要素:缓存的key,缓存的超时时间,缓存的数据。其中在这里缓存的key和缓存的超时时间是暴露给客户端的,难以统一控制。此外,如果客户端开发人员A与开发人员B,刚好起的缓存key名字冲突了怎么办?谁来发现,又谁来解决?这不是杞人忧天,而是为作理性的技术人员,我们要尽量做到严谨,考虑周全。

数据供给器的核心设计

在前面做了那么多铺垫,也对比了一个不怎么优雅的解决方案后,我们最后引出在面向对象的世界里,推荐的解决方案——三套数据模板的核心架构设计。这里不会作详细的具体实现,但会着重分享这个核心架构设计的高层建设与实现要点。
根据“针对接口编程,而不是针对实现编程”这一思想,我们需要设站在概念视角,站在业务的角度,构建设计我们的高层接口。我们把这类业务数据的获取封装到数据供给器这一DataProvider接口中。其肩负的责任,从以下的接口代码中可以一眼看出。使用抽象类的原因,是因为部分内部操作和钩子函数不需要对外暴露,要使用protected保护级别,而接口的操作全部在PHP中必须是public公开级别。另一个原因是有一些通用的模板实现和默认的钩子实现,需要在抽象基类中实现,减少具体实现子类的编程工作。下面是DataProvider抽象类的关键代码。

<?php
require_once dirname(__FILE__) . '/DataQuery.php';

/**
 * 数据供给器接口
 *
 * - 接口抽离,便于在高层统一接口
 * - 通过多级缓存来优化性能
 * - 支持全球追踪器链路收集
 *
 */

abstract class DataProvider {
    /**
     * 获取业务数据
     * @param DataQuery $query 获取业务数据的查询对象
     * @param string &$trace 全球追踪收集器
     * @return mixed/NULL 业务数据,NULL表示获取失败
     */
    public function getData(DataQuery $query, &$trace = '') {
        $cache = $this->getCacheInstance();
        $key = $this->getCacheKey($query);

        // 从缓存中获取
        $data = $cache->get($key);
        if ($data === NULL) {
            // 从数据源获取
            $data = $this->doGetData($query, $trace);

            // 写入到缓存
            $cache->set($key, $data, $this->getCacheExpireTime($query));
        }

        // 钩子函数回调
        $this->afterGetData($query, $data);

        return $data;
    }
}

稍微解释一下,DataProvider::getData(DataQuery $query, &$trace = '')
操作是提供给客户端开发人员使用的接口,通过这个接口,可以快速方便地获取业务数据。在这个接口内,会封装一系列的操作,包括对缓存的读取与写入,对原始业务数据的获取,以及获取到原始业务数据后的加工处理等。它的第一个参数是查询对象,为避免过长的参数列表,也为了统一接口签名,我们引入了DataQuery查询对象类,它相当于一个结构体,可以不需要getter/setter,并且使用了魔法方法丰富它的行为。第二个参数是全球追踪收集器,简单来说,业务数据走过的路,经历过的节点,都可以纪录在这上面,便于可视化管理和历史回放。关于全球追踪器,下一小节会重点讲解。

内部辅助操作

核心的代码实现,以及关键的操作,已经在DataProvider::getData(DataQuery $query, &$trace = '')
中完成。它作为对外的总入口,提供了清晰了解的使用方式。但客户端的使用越简单,内部的实现就要稍微复杂一点。所以我们需要保持一些耐心继续往下看DataProvider抽象类的两大内部辅助操作。
第一类内部辅助操作,是针对业务数据的处理,包括对原始业务数据的获取,以及在获取业务数据后的加工处理钩子函数。如下所示:

abstract class DataProvider {
    ……

    /**
     * 返回需要缓存的原始数据
     * @param DataQuery $query 获取业务数据的查询对象
     * @param string &$trace 全球追踪收集器
     * @return mixed 注意:没有数据时请返回空数组array(),或者'',切勿返回NULL/FALSE
     */
    abstract protected function doGetData(DataQuery $query, &$trace = '');

    /**
     * 钩子函数:数据的实时处理操作,将会在业务数据成功获取后进行回调
     * @param  mixed &$data 业务数据
     * @return mixed 加工处理后的业务数据
     */
    protected function afterGetData(DataQuery $query, &$data) {
        // 可选实现:加工处理
    }
}

第二类内部辅助操作,则是针对技术层面的缓存操作,包括获取缓存实例,方便内部提供缝纫点切换不同的缓存策略,和获取缓存的Key、返回缓存有效时间这两个操作。如下所示:

abstract class DataProvider {
    ……

    /**
     * 获取缓存实例
     * @return Cache 缓存实例
     */
    abstract protected function getCacheInstance();

    /**
     * 获取缓存的Key,返回唯一缓存key,这里将$query传入,以便同类数据根据不同的值生成不同的key
     *
     * @param DataQuery $query 获取业务数据的查询对象
     * @return string 缓存key
     */
    abstract protected function getCacheKey(DataQuery $query);

    /**
     * 返回缓存有效时间,单位为:秒
     *
     * @param DataQuery $query 获取业务数据的查询对象
     * @return int 缓存有效时间
     */
    abstract protected function getCacheExpireTime(DataQuery $query);
}

而对于getCacheKey()操作,没有提供默认实现的原因是因为查询参数里面的参数有可能是除了字符串类型外,还可能有数组、布尔值或者其他类型。如果统一处理,有可能造成数据丢失,或者因为特殊的符号影响缓存key的组成,出现非法缓存key的情况。而且,有时并不是全部的参数都需要作为缓存key的组成部分。

参数查询对象

完成对数据供给器DataProvider的塑造后,再来看下它的周边配套设施。其中最为关键的是参数查询对象类DataQuery。它的实现代码很简单,其中使用了两个魔法函数:__set()
__get()

<?php
class DataQuery {

    public function __set($name, $value) {
        $this->$name = $value;
    }

    public function __get($name) {
        return isset($this->$name) ? $this->$name : NULL;
    }
}

这是一个松散的结构体,业务在实现时,可以丰富其子类,清晰定义查询对象的具体参数名称,也可以使用此默认的实现类,然后填充必要的参数。但建议使用前者。
最后稍微补充一下,这时也会使用到缓存,但鉴于缓存本身的实现已经是广为人知的技术,这里就不再特别说明。

4 3+1套数据模板

至此,我们有了基本的核心元素:数据供给器和参数查询对象,以及缓存机制。还记得前面介绍过的数据稳定-访问象限分布吗?数据根据其稳定程度和访问频率可以分为四类:
稳定但高频访问的数据
稳定且低频访问的数据
不稳定且高频访问的数据
不稳定但低频访问的数据
下面将探讨如何使用基本的核心元素,为这四类数据提供合适的数据模板,逐渐丰富我们的数据体系。
3+1套数据模板,其中的3套数据模板是分别针对稳定且低频访问、稳定但高频访问、不稳定但低频访问这三类数据的模板,它们依次是:轻量级数据模板、重量级数据模板,和介于这两者之间的多级缓存数据模板。剩下的1套数据模板则是针对不稳定且高频访问的数据,也就是不适宜使用任何缓存的实时数据,对应的是实时数据模板。
前面3套数据模板的差异点主要是在于缓存的机制和策略不同,以符合业务数据本身的非功能性诉求。这里以其中一个模板为例,讲解如何实现。其他模板实现思路类似,则不再赘述。
以轻量级数据模板为例,它的抽象类代码如下:

<?php
require_once dirname(__FILE__) . '/DataProvider.php';

abstract class LightWeightDataProvider extends DataProvider {
    protected function getCacheInstance() {
        return new LightWeightCache(); 
    }

    protected function getCacheExpireTime(DataQuery $query) {
        return 600;
    }
}

它做了两件事情,分别是指定了使用LightWeightCache轻量级缓存机制,以及统一指定了缓存的有效时间为600秒,即10分钟。LightWeightCache的具体实现,可以是使用集群缓存,如Redis、Memcache或者其他NoSQL服务。
继续回到前面的用户信息的获取示例,在改用此轻量级数据模板后,它需要首先实现具体的数据模板子类,以及提供相应的查询对象子类。相碰代码如下:

<?php
require_once dirname(__FILE__) . '/LightWeightDataProvider.php';

// 实现具体的业务数据获取
class UserData extends LightWeightDataProvider {
    protected function doGetData(DataQuery $query, &$trace = '') {
        $model = new User();
        return $model->getUserInfo($query->userId);
    }

    protected function getCacheKey(DataQuery $query) {
        return 'userinfo_' . $query->userId;
    }
}

// 用户信息查询对象
class UserInfoDataQuery extends DataQuery {
    public $userId;
}

在完成前面的准备工作后,最后再来看下客户端调用的新代码。

// 实例化
$user = new UserData();
$query = new UserInfoDataQuery();

// 通过轻量级数据模板获取用户信息
$query->userId = 1;
$userInfo = $user->getData($query);

// 输出结果
var_dump($userInfo);

在完成这样的设计,以及编程工作后,如果需要切换不同的缓存策略,可以在不改动客户端调用代码的情况下,只需要切换UserData子类继承不同的数据模板基类即可。
这里轻量级数据模板实现的基本思路,剩下的重量级数据模板、多缓存缓存数据模板实现也类似,不同的是使用的缓存策略不一样,默认指定的缓存失效时间也不同。但实时数据模板比较有意思,它是一个特例,我们一起来看下。
首先,也是数据供给器的抽象子类,在这里它提供了一个空对象缓存实例,因为不需要缓存,所以缴存的key和缓存时间默认可以为空。

<?php
require_once dirname(__FILE__) . '/DataProvider.php';

abstract class RealTimeDataProvider extends DataProvider {
    protected function getCacheInstance() {
        return new NoneCache(); 
    }

    protected function getCacheKey(DataQuery $query) {
        return '';
    }

    protected function getCacheExpireTime(DataQuery $query) {
        return 0;
    }
}

如果哪天在获取用户信息时,不再需要任何缓存时,只需要将UserInfo类的基类改为RealTimeDataProvider即可,如下所示:

<?php

require_once dirname(__FILE__) . '/RealTimeDataProvider.php';

class UserInfo extends RealTimeDataProvider {
    protected function doGetData(DataQuery $query, &$trace = '') {
        $model = new User();
        return $model->getUserInfo($query->userId);
    }
}

其他的实现代码和客户端调用可以保持不变。由此可推,逆向也是成立的。即一开始没有使用缓存,如果需要使用缓存,也可通过改变基类来切换。
这就是我们所说的3+1套数据模板。最后作为小结部分,我们画一个整体的静态类结构UML图,来加深对此核心架构设计的理解。看下它是如何分离技术与业务关注点,又是如何把概念视角、规约视角、实现视角这三层进行划分的。

图5 数据供给器的核心设计

这个架构设计,把我们前面讲到的关键要素、3+1套数据模板、以及具体实现的业务类都有机地串联了起来。按照三层规约视角,从上往下依次是概念层、规约层、实现层。概念层与规约层的划分,有效地分离了技术与业务的关注点,在业务上,我只需要关注业务的数据的获取即可,而不用关心它是怎么来的。规约层与实现层的划分,则将架构的基础组件设计与具体实现的业务代码进行了分离,类的职责更为明确,架构师与开发工程师之间的合作也就更为明朗了。实现层是需要开发工程来根据业务具体实现的,耐规约层则可以由架构师来统一部署、搭建和设计。
在大学时代,我们就学到了,程序由算法和数据组成。数据中蕴含了很多有价值的信息,因此很值得我们花一点时间来深入讨论这一块。关于企业级网站数据,暂时分享到这里。这肯定不是放之四海而皆准的方案,不一定能适用于任何项目开发中,但它确实是行之有效的一种解决方案,并且值得我们去思考、去演进。

5 全球追踪器

再来看下企业级网站数据的可视化管理。
前面有提及到全球追踪器,这一节重点来详细说明。为了方便查看数据的来源和穿透情况,我们引入了对数据来源的追踪,并且各业务数据可以根据需要进行纪录累加。
在前端设计数据供给器时,已经为全球追踪器预留了占位符,为避免混淆关注点,这里继续完成全球追踪器的实现代码。以轻量级数据模板为例,追加全球追踪器,只需要修改三处地方。
首先,在客户端调用入口处,也就是追踪器的起点,添加业务数据的名称,并把此追踪链路串传递下去。

$query->userId = 1;
$trace = '|UserData-'; // 全球追踪器:用户信息(起点)
$userInfo = $user->getData($query, $trace);

这里以竖线为多个数据的分割符,并且以UserData作为用户数据的名称。为了减少最后输出的流量,以及避免外界知道内部的数据情况,也可以采用缩写的形式,例如使用:UD。
其次,修改轻量级数据模板,统一加上追踪标识。这时需要重载基类的getData()方法,在追加完标识后,继续调用父类的getData()方法。

abstract class LightWeightDataProvider extends DataProvider {
    public function getData(DataQuery $query, &$trace = '') {
        // 全球追踪器:轻量级数据模板
        $trace .= 'L';

        return parent::getData($query, $trace);
    }
}

L在这里表示数据经过了轻量级数据模板,依此类推,重量级数据模板可以用H表示,而多级缓存数据模板可以用M表示,最后实时数据模板可以用R来表示。都是取自类名称的首字母。
最后,在具体实现的业务数据子类中,追加最后数据源的来源标识。同样使用开头首字母来表示,例如D表示来源数据库,A表示来源于APCU缓存,对于Redis和Memcache,考虑到前面已被数据模板占用,可以起用新的名称,如取最后一个字母,S表示Redis,而E表示Memcache。这些都是可以内部约定的。在用户数据实现的子类中,则使用D来标识。

class UserData extends LightWeightDataProvider {
    protected function doGetData(DataQuery $query, &$trace = '') {
        // 全球追踪器:数据库
        $trace .= 'D';

        $model = new User();
        return $model->getUserInfo($query->userId);
    }
}

完成这些追踪器的填充后,最后可以把追踪的结果输出来。例如这里,最后会输出”|UserData-LD“。如果把整个网站页面用到的数据都这样加上追踪器,我们最后就可以得到数据可视化管理的全局情况。在页面渲染时,可以通过头部信息把追踪的结果返回给浏览器,便于我们监控、调试和排查。根据此业务数据链路追踪的情况,我们能更容易察觉现有系统中存在的问题。例如该用缓存的数据是否使用了合适的缓存,对于容易因穿透而造成雪崩的数据我们有没有提前通过计划任务来生成并预热,如果数据未能及时更新,是因为缓存还没刷新还是因为确实数据本身未被修正,诸如此类。可以说全球追踪器,如何应用恰当,会是我们项目开发中一大利器。
如果想追求更极致的数据供给器,考虑到很多时候,网站都要提供预览的功能,我们可以在对象查询对象添加缓存读、写的开关控制,以便可以随时切换是否使用缓存。当缓存读开关开启时才读取缓存,同样当缓存写开关开启时才写入到缓存。这里的实现很简单,留给读者作为课后作业,有兴趣的同学可以尝试一下。
作为本节小结,我们通过一个表格来汇总此次新的数据管理方式,与传统做法的差异有哪些。

表1 传统数据管理与新数据管理方式的对比

 

分类与要点

 

 

传统数据管理

 

 

新的数据管理方式

 

 

面向运营的预览

 

 

手动支持,需要手动切换是否使用缓存

 

 

统一自动支持切换

 

 

面向运营的强制刷新

 

 

通常不支持或手动支持

 

 

支持,页面访问时可使用强刷模式

 

 

业务路径触达

 

 

只能靠人工经验判断业务路径

 

 

可视化,通过专业术语标识能精确描述业务场景

 

 

数据规划中的缓存策略

 

 

不明确、不清晰

 

 

有序,按稳定-访问象限清晰,并且可视化

 

 

数据规划中的缓存时间

 

 

各自指定,较为凌乱

 

 

统一规划,支持自定义

 

 

用于故障排查的页面追踪

 

 

不支持

 

 

支持,能可视化

 

 

用于故障排查的穿透查看

 

 

不支持

 

 

支持,可以清楚知道数据从哪里来

 

 

在线调试

 

 

不支持

 

 

支持,

 

 

用于后台作业的计划任务生成

 

 

难以支持

 

 

支持,可快速模拟数据获取,以触发高效缓存填充

 

 

快速开发与效率的提升

 

 

拷贝-粘贴式重复开发,容易出错

 

 

模板支持,便于快速开发

 

 

开发规范与标准化

 

 

缺乏标准

 

 

有利于形成企业级架构设计,因为形成了统一的接口规约

 

这套数据管理的架构,能明显提高代码开发的效率,因为开发人员不再需要关注具体重复实现的缓存策略,取而代之是选择合适的数据模板即可,甚至可以轻松切换不同的缓存策略。另外一个角度,我们对各种稳定数据、活跃数据、实时数据都有了更为清晰的划分,因此也就大大降低了对业务数据维护的成本,并且使得原来混乱的业务数据变得更有序可循。此外,全球追踪器为测试、开发人员提供了重要的场景信息。可以说,某种程度上,这是一种企业级的架构设计。

发表评论