Facebook、Google、Amazon 是如何高效开会的

会议是工作中绕不开的一部分,许多人都听说过,在一项研究中发现,语言在我们的沟通中只占了 7% 的比例。虽然这个研究结果仅仅是面向单个字眼的沟通,在现实中比例不至于这么夸张,但不可否认的是,冰山下的许多信息,都是依靠语调、身体语言来传递的。这也是为什么,即使在线沟通如此多样和便捷,会议依旧不可被取代。

然而,许多人却会对会议有抵触情绪。冗长的节奏,即兴的跑题,敌对的氛围,模糊的结论……这些都让会议要不然变得漫长而无效,要不然变得沉闷而无趣。那么,如果才能开一场高效的会议呢?一起来看看科技巨头,如 Facebook、Google、Amazon 等这些公司,是如何高效开会的。

Facebook

  • 明确会议类型:在 Facebook,CEO Zuckerberg 要求所有人在开会前,都先想好一个最基础的问题:本次会议的目标是什么?在他看来,会议可以分为两大类:决策型会议、讨论型会议。前者必须输出一个明确的决定,如项目评审会、预算审批会,而后者则可能是召集所有人一起讨论问题,共享信息,如晨会、头脑风暴、项目沟通会等。
  • 为会议制作进度表:把会议议题制作成一张进度表,每讨论完一个议题,就打一个勾,这是 Facebook 的 COO Sheryl Sandberg 的建议。一方面,确保每一个议题都已经有了一个明确的结果;另一方面,如果你提前勾完了这份进度表,就让会议提前结束。
  • 在中午 12 点开站会:Facebook 的研发经理 Mark Tonkelowitz 往往会在中午开一个 15 分钟左右的站会,所有人一会都要去吃饭,自然不会愿意花时间跑题。

image

Google

  • 保持会议精简:Google 的历任高管都曾强调会议精简的重要性。想要让会议精简,首先必须控制人,然后是控制时间。2011 年 Larry Page 回归 Google 重新出任 CEO 时,甚至要求全公司每个会议最多不超过 10 个人。而曾在雅虎、Google 担任高管的 Marissa Mayer,她要求会议尽量保持在 5-10 分钟之间,快速讨论,快速结束。
  • 提前制定会议议程:Google 曾经的 CEO Eric Schmidt 在《How Google Works》一书中提到过,一定要提前 24 小时,将会议议程发给所有的参会人。议程应该包含会议目标、参会人、待讨论的事项等内容。
  • 每个会议都有一个 Leader:每一个会议上都需要有一个 Leader 负责做决定,并且在会后 48 小时内,将所有的结论总结成邮件发送给参会人与相关方。
  • 紧急决定不需要等待会议:在 Google,会议是为了得出结论的。不要让决定等待会议,如果你马上需要做一个紧急决定,要不就跳过会议直接做决定,要不就立即召集会议。
  • 用数据替代争论:在会议中难免会陷入分歧,不同的人持有不同的观点,这时候会议往往会陷入无休止的争吵和讨论。Google 强调在这类问题上,尽可能抛弃主观色彩,而使用数据说话。
  • 准备一个计时器:Google Ventures 的合伙人 Jake Knapp 推荐任何会议都应该有一个计时器,而且是实体的计时器。他特别推荐 Time Timer的计时器,表盘上红色的区域会随着时间的流逝而逐渐减少,更让参会人有时间的紧迫感。

image

Amazon

  • 只准备两份批萨:对于如何控制会议规模,Amazon 的 CEO Jeff Bezos 曾有一个非常有趣的衡量标准:一场会议的人数,订两份批萨就够了。如果超出这个规模,说明这个会议有太多的人参加了。
  • 永远准备一张空椅子:Jeff Bezos 还做过一个有趣的提议:永远在会议室准备一张空的椅子来代表顾客。Amazon 的核心文化理念之一,就是从各个方面不断完善和提升顾问体验。这张空椅子就是一个符号性的象征,提醒每个人不要忘记这一点。

xMentalHelpChat-iStock516454908-Gestalt-Therapy-The-Empty-Chair-Technique-1024x585.jpg.pagespeed.ic.KFNGU7vI9a

可以看到,无论是 Amazon、Facebook 还是 Google,这些公司对会议都有一些共同的要求,例如只邀请必要的人,控制会议时长,事先准备议程,事后有结论等等。你也可以为自己的工作环境制定一些会议准则,来更高效地开会。

参考文章:
1. http://www.businessinsider.com/googles-rules-for-a-great-meeting-2014-9
2. https://www.inc.com/larry-kim/jeff-bezos-surprising-meeting-strategy.html
3. http://www.businessinsider.com/this-is-how-larry-page-changed-meetings-at-google-after-taking-over-last-spring-2012-1
4. https://www.getminute.com/how-to-run-a-meeting-like-google-apple-amazon-and-facebook/
5. https://qz.com/94701/top-business-leaders-meeting-tips/

1+

谈谈到底什么是抽象,以及软件设计的抽象原则

我们在日常开发中,我们常常会提到抽象。但很多人常常搞不清楚,究竟什么是抽象,以及如何进行抽象。今天我们就来谈谈抽象。

什么是抽象?

首先,抽象这个词在中文里可以作为动词也可以作为名词。作为动词的抽象就是指一种行为,这种行为的结果,就是作为名词的抽象。Wikipedia 上是这么定义抽象的:

Conceptual abstractions may be formed by filtering the information content of a concept or an observable phenomenon, selecting only the aspects which are relevant for a particular subjectively valued purpose.

也就是说,抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个皮质的足球,我们可以过滤它的质料等信息,得到更一般性的概念,也就是。从另外一个角度看,抽象就是简化事物,抓住事物本质的过程。

需要注意的是,抽象是分层次的。还是用 Wikipedia 上的例子,以下是对一份报纸在多个不同层次的抽象:

  1. 我的 5 月 18 日的《旧金山纪事报》
  2. 5 月 18 日的《旧金山纪事报》
  3. 《旧金山纪事报》
  4. 一份报纸
  5. 一个出版品

可以看到,在不同层次的抽象,就是过滤掉了不同的信息。这里没有展现出来的是,我们需要确保最终留下来的信息,都是当前抽象层需要的信息。

生活中的抽象

其实我们生活中每时每刻都在接触或者进行各种抽象。接触最多的,应该就是数字了。其实原始人类并没有数字这个概念,他们可能能够理解三个苹果,也能够理解三只鸭子,但是对他们来说,是不存在数字“三”这个概念的。在他们的理解里,三个苹果和三只鸭子是没有任何联系的。直到某一天,某个原始人发现了这两者之间,有那么一个共性,也即是数字“三”,于是就有了数字这个概念。从那以后,人们就开始用数字对各类事物进行计数。

赫拉利在《人类简史》里说,人类之所以成为人类,是因为人类能够想象。这里的想象,我认为很大程度上也是指抽象。只有人类能够从具体的事物本身,抽象出各种概念。可以说,人类的几乎所有事情,包括政治(例如民族、国家)、经济(例如货币、证券)、文学、艺术、科学等等,都是建立在抽象的基础上的。绘画有一个流派叫抽象主义,很多人(包括我)都表示看不懂,但下面几幅毕加索画的牛,也许能够从直观上让我们更好的理解什么是抽象。

毕加索的牛

科学里的抽象就更广泛了,我们可以认为所有的科学理论和定理都是一种抽象。物体的质量是一种抽象,它不关注物体是什么以及它的形状或质地;牛顿定律是对物体运动规律的抽象,我们现在知道它不准确,但它在常规世界里,却依然是一个相当可靠的抽象。在科学和工程里,常常需要建立一些模型或者假设,比如量子力学的标准粒子模型、经济学的理性人假设,这些都是抽象。甚至包括现在 AI 里通过训练生成的模型,某种程度上说,也是一种抽象。

当然,哲学上对抽象有很多讨论,什么本体论、白马非马之类的,这些已经在本人的理解范围之外了,就不讨论了。

开发中的抽象

现在我们应该能大致理解抽象这个概念了,让我们回到软件开发领域。

在软件开发里面,最重要的抽象就可能是分层了。分层随处可见,例如我们的系统就是分层的。最早的程序是直接运行在硬件上的,开发成本非常高。然后慢慢开始有了操作系统,操作系统提供了资源管理、进程调度、输入输出等所有程序都需要的基础功能,开发程序时调用操作系统的接口就可以了。再后来发现操作系统也不够,于是又有了各种运行环境(如 JVM)。

编程语言也是一种分层的抽象。机器理解的其实是机器语言,即各种二进制的指令。但我们不可能直接用机器语言编程,于是我们发明了汇编语言、C 语言以及 Java 等各种高级语言,一直到 Ruby、Python 等动态语言。

开发中,我们应该也都听说过各种分层模型。例如经典的三层模型(展现层、业务逻辑层、数据层),还有 MVC 模型等。有一句名言:“软件领域的任何问题,都可以通过增加一个间接的中间层来解决”。分层架构的核心其实就是抽象的分层,每一层的抽象只需要而且只能关注本层相关的信息,从而简化整个系统的设计。

其实软件开发本身,就是一个不断抽象的过程。我们把业务需求抽象成数据模型、模块、服务和系统,面向对象开发时我们抽象出类和对象,面向过程开发时我们抽象出方法和函数。也即是说,上面提到的模型、模块、服务、系统、类、对象、方法、函数等,都是一种抽象。可想而知,设计一个好的抽象,对我们软件开发有多么重要。

抽象的原则

那么到底应如何做到好的抽象呢?在软件开发领域,其实早就有 SOLID 等原则,虽然很多人都听说过,但其实真正能理解这些原则的开发者并不多。那么我们就从抽象的角度,再来看下这些原则,也许会有更好的理解。

单一职责原则(Single Responsibility Principle, SRP)

单一职责是指一个模块应该只做一件事,并把这件事做好。其实对照应抽象的定义,可以发现这个原则本身就是抽象的核心体现。如果一个类包含了很多方法,或者一个方法特别长,就要引起我们的特别注意了。例如下面这个 Employee 类,既有业务逻辑(calculatePay)、又有数据库逻辑(saveToDb),那它其实至少做了两件事情,也就不符合单一职责原则,当然也就不是一个好的抽象。

class Employee {
  public Pay calculatePay() {...}
  public void saveToDb() {...}
}  

有些人觉得单一职责不太好理解,有时候很那分辨一个模块到底是不是单一职责。其实单一职责的概念,常常需要结合抽象的分层去理解。

在同一个抽象层里,如果一个类或者一个方法做了不止一件事,一般是比较容易分辨的。例如一个违反单一职责原则的典型征兆是,一个方法接受一个布尔类型或者枚举类型的参数,然后一个大大的 if/else 或者 switch/case,分支里也是大段的代码处理各种情况下的逻辑。这时我们可以用简单工厂模式、策略模式等设计模式去优化设计。

假如说我们用了简单工厂模式,改进了一段代码,重构后代码可能像是下面是这样的。

    public Instance getInstance(final int type){ 
        switch (type) {
            case 1: return new AAInstance;
            case 2: return new BBInstance;
            default: return new DefaultInstance();
        }
    }

有人可能会有疑问,代码里依然还是存在 if/else 或者 switch/case,这不还是做了不止一件事情么?其实不是的,使用了简单工厂模式,其实就是增加了一个抽象层。在这个抽象层里,getInstance 的职责很明确,就是创建对象。而原来分支里的逻辑处理,则下沉到了另外一个抽象层里去了,也就是 Instance 的实现所在的抽象层。

再看下面 Scala 实现的 updateOrder 方法,它似乎也只是做了一件事情:处理订单,那算不算单一职责呢?

protected def updateOrder(t: TransationEntity) = {
  // 1 获取订单
  ManagedRepo.find[Order]("orderNo" -> t.tradeNo).map { order =>
    // 2 检查订单是否已支付
    val ps = SQL("""select statue from Order where id ={id} for update""").on("id" -> order.id).as(scalar[Long].singleOpt).getOrElse(0l)
    if (ps == PAID) {
      throw ServiceException(ApiError.SUBSCRIPTION_UPDATE_FAIL)        
    } else {
      // 3 更新订单信息,标记为已支付            
      val updatedOrder = // 略...
      updatedOrder.saveOrUpdate()
      // 4 生成收入记录
      createIncome(updatedOrder)
    }
  }
}

答案当然是不算,因为很明显,这个方法里面既有业务逻辑的代码,又有数据库处理的代码,这两类应该是在不同的抽象层的。我们把数据库处理的代码抽取出来,下沉到数据层,它就能符合单一职责原则了。

protected def updateOrder(t: TransationEntity) = {
  findUnpaidOrder(rtent.tradeNo).map { order =>
    val updatedOrder = updateOrderForPayment(rtent)
    createIncome(updatedOrder)
  }
}  

开放封闭原则(Open/Closed Principle, OCP)

开放封闭原则是指对扩展开放,对修改封闭。当需求改变时,我们可以扩展模块以满足新的需求;但扩展时,不应该需要修改原模块的实现。

下面两段代码都实现了方形、矩形以及圆形的面积计算。第一种用的是面向过程的方法,第二种用的是面向对象的方法。那么,到底哪一种更符合开放封闭原则呢?

面向过程方法:

public class Square { 
    public double side;
}
public class Rectangle { 
    public double height;
    public double width;
}
public class Circle { 
    public double radius;
}
public class Geometry {
    public double area(Object shape) {
        if (shape instanceof Square) {
            Square s = (Square) shape;
            return s.side * s.side;
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.height * r.width;
        } else if (shape instanceof Square) {
            Circle c = (Circle) shape;
            return PI * c.radius * c.radius;
        } else {
            throw new NoSuchShareException();
        }
    }
}

面向对象方法:

public class Square implements Share { 
    public double side;
    public double area() {
        return side * side;
    }
}
public class Rectangle implements Share { 
    public double height;
    public double width;
    public double area() {
        return height * width;
    }
}
public class Circle implements Share { 
    public double radius;
    public double area() {
        return PI * radius * radius;
    }
}

估计很多人会觉得面向对象的方式更好,更符合开放封闭原则。但真相其实没那么简单。想象如果我们需要添加一个新的形状,比如说椭圆,那面向对象的实现肯定更方便,我们只需要实现一个椭圆的类以及它的 area 方法。这时候我们可以说面向对象的方法更符合开放封闭原则。

但如果我们需要添加一个新的方法呢?比如说,我们发现我们还需要计算形状的周长。这时候,面向对象的实现似乎就没那么方便了,要在每个类里面添加计算周长的方法。而面向过程的方法,则只需要添加一个方法就行了。这时候,我们反而发现面向过程的方法更符合开放封闭原则。

所以开放封闭其实是相对的,有时候,如何进行抽象,取决于我们对未来最有可能的扩展的预判。

依赖倒置原则(Dependency Inversion Principle, DIP)

依赖倒置原则是指高层模块不应该依赖于低层模块的实现,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖与抽象。前面提到,“软件领域的任何问题,都可以通过增加一个间接的中间层来解决” ,DIP 就是最典型的增加中间层的方式,也是我们需要解耦两个模块的最重要的方法之一。

依赖倒置原则的一个例子是 Java 的 JDBC。如果没有 JDBC,那我们的系统就会严格依赖我们使用的那个数据库。这时如果我们想要切换到另外一个数据库,就需要修改大量代码。但 Java 提供了 JDBC 接口,而所有关系数据库的连接库都实现了这个接口,我们的系统也只需要调用 JDBC 即可完成数据库操作。这时我们的系统和数据库的依赖就解除了。除了 JDBC,其实 SQL 本身也是一种依赖倒置的实现。另外一个很典型的例子就是 Java 的日志接口 Slf4j。
依赖倒置JDBC

其实所有的协议和标准化都是 DIP 的一种实现。包括 TCP、HTTP 等网络协议、操作系统、JVM、Spring 框架的 IOC 等等。设计模式里有不少模式,也是典型的依赖倒置,例如状态模式、工厂模式、代理模式、策略模式等等,下图是策略模式的结构图。
Strategy

我们日常生活中也有很多依赖倒置的例子。比如电源插座,家庭的供电只需要提供符合国家标准的电源插座,我们购买电器产品时,就不用担心买回来无法接入电源。汽车和轮胎、铅笔和笔、USB/耳机接口等等,也都是同一思想的体现。

里氏替换原则(Liskov Substitution Principle, LSP)

里氏替换原则是指子类必须能够替换成它们的基类。例如下面这个最常见的例子,Square 可以是 Rectangle 的子类吗?

public class Rectangle { 
    public double height;
    public double width;

    public void setHeight(int height) { ... }
    public void setWidth(int width) { ... }
}

public class Square extends Rectangle { 
    ???
}

虽然几何上说,Square 是一个特殊的 Rectangle,但把 Square 作为 Rectangle 的子类,却未必合适,因为它已经不存在宽和高的概念了。如果一个抽象不能符合里氏替换原则,那我们就需要考虑下这个抽象是不是合适了。

接口隔离原则(Interface Segregation Principle, ISP)

接口隔离原则是指客户端不应该被迫依赖它们不使用的方法。例如下面的 Square 类如果继承了Shape 接口,该如何计算体积以实现volume方法?

interface Shape {
    public function area();
    public function volume();
}
public class Square extends Shape { 
    ???
}

同样,如果一个抽象不符合接口隔离原则,那可能就不是一个合适的抽象。

迪米特法则(Law of Demeter)

迪米特法则不属于 SOLID 原则,但我觉得也值得说一下。它是指模块不应该了解它所操作的对象的内部情况。想象一下,如果你想让你的狗狗快点跑的话,你会对狗狗说,还是对四条狗腿说?如果你去店里买东西,你会把钱交给店员,还是会把钱包交给店员让他自己拿?

下面是一段违反迪米特法则的典型代码。这样的代码把对象内部实现暴露了出来,应该考虑讲将功能直接暴露为接口,或者合理使用设计模式(如 Facade)。

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

总结

关于抽象,今天我们就说到这里。不过要注意的是,软件开发并不是仅仅只依靠抽象能力就能完成的,最终我们还是要把我们抽象出来的架构、模型等,落地到真正的代码层面,那就还需要逻辑思维能力、系统分析能力等。以后如果有机会,我们可以继续探讨。

我希望各位看完本文,对抽象的理解能够更加深入一点。我们以奥卡姆剃刀原则来结束吧:一个抽象应该足够简单,但又不至于过于简单。这其实就是抽象的真谛。

2+

喜欢该文章的用户:

  • avatar

后端系统的缓存使用浅谈

1. 什么是缓存

缓存有很多种,从 CPU 缓存、磁盘缓存到浏览器缓存等,本文所说的缓存,主要针对后端系统的缓存。也就是将程序或系统经常要使用的对象存在内存中,以便在使用时可以快速调用,也可以避免加载数据或者创建重复的实例,以达到减少系统开销,提高系统效率的目的。

2. 为什么要用缓存

我们一般都会把数据存放在关系型数据库中,不管数据库的性能有多么好,一个简单的查询也要消耗毫秒级的时间,这样我们常说的 QPS 就会被数据库的性能所限制,我们想要提高QPS,只能选择更快的存储设备。

在日常开发有这样的一种场景:某些数据的数据量不大、不经常变动,但访问却很频繁。受限于硬盘 IO 性能或者远程网络等原因,每次都直接获取会消耗大量的资源。可能会导致我们的响应变慢甚至造成系统压力过大,这在一些业务上是不能忍的,而缓存正是解决这类问题的神器。

但是有一点需要注意,就是缓存的占用空间以及缓存的失效策略,下文也会提到。

使用缓存的场景

对于缓存来说,数据不常变更且查询比较频繁是最好的场景,如果查询量不够大或者数据变动太频繁,缓存也就是失去了意义。

3. 缓存的使用

日常工作使用的缓存可以分为内部缓存和外部缓存。

内部缓存一般是指存放在运行实例内部并使用实例内存的缓存,这种缓存可以使用代码直接访问。

外部缓存一般是指存放在运行实例外部的缓存,通常是通过网络获取,反序列化后进行访问。

一般来说对于不需要实例间同步的,都更加推荐内部缓存,因为内部缓存有访问方便,性能好的特点;需要实例间同步的数据可以使用外部缓存。

下面对这两种类型的缓存分别的进行介绍。

3.1 内部缓存

为什么要是用内部缓存

在系统中,有些数据量不大、不常变化,但是访问十分频繁,例如省、市、区数据。针对这种场景,可以将数据加载到应用的内存中,以提升系统的访问效率,减少无谓的数据库和网路的访问。

内部缓存的限制就是存放的数据总量不能超出内存容量,毕竟还是在 JVM 里的。

最简单的内部缓存 - Map

如果只是需要将一些数据缓存起来,避免不必要的数据库查询,那么 Map 就可以满足。

对于字典型的数据,在项目启动的时候加载到 Map 中,程序就可以使用了,也很容易更新。

// 配置存放的Map
Map<String, String> configs = new HashMap<String, String>();

// 初始化或者刷新配置的Map
public void reloadConfigs() {
    Map<String, String> m = loadConfigFromDB();
    configs = m;
}

// 使用
configs.getOrDefault("auth.id", "1");

功能强大的内部缓存 - Guava Cache / Caffeine

如果你需要缓存有强大的性能,或者对缓存有更多的控制,可以使用 Guava 里的 Cache 组件。

它是 Guava 中的缓存工具包,是非常简单易用且功能强大的 JVM 内缓存,支持多种缓存过期策略。

LoadingCache<String, String> configs = CacheBuilder.newBuilder()
        .maximumSize(1000) // 设置最大大小
        .expireAfterWrite(10, TimeUnit.MINUTES) // 设置过期时间, 10分钟
        .build(
            new CacheLoader<String, String>() {
            // 加载缓存内容
                public String load(String key) throws Exception {
                    return getConfigFromDB(key);
                }
                public Map<String, String> loadAll() throws Exception {
                return loadConfigFromDB();
            }
        });

//CacheLoader.loadAll

// 获取某个key的值
try {
    return configs.get(key);
} catch (ExecutionException e) {
    throw new OtherException(e.getCause());
}

// 显式的放入缓存
configs.put(key, value)
// 个别清除缓存
configs.invalidate(key)
// 批量清除缓存
configs.invalidateAll(keys)
// 清除所有缓存项
configs.invalidateAll()

本地缓存的优点:

  • 直接使用内存,速度快,通常存取的性能可以达到每秒千万级
  • 可以直接使用 Java 对象存取

本地缓存的缺点:

  • 数据保存在当前实例中,无法共享
  • 重启应用会丢失
Guava Cache 的替代者 Caffeine

Spring 5 使用 Caffeine 来代替 Guava Cache,应该是从性能的角度考虑的。从很多性能测试来看 Caffeine 各方面的性能都要比 Guava 要好。

Caffeine 的 API 的操作功能和 Guava 是基本保持一致的,并且 Caffeine 为了兼容之前 Guava 的用户,做了一个 Guava 的 Adapter, 也是十分的贴心。

如果想了解更多请参考:是什么让 Spring 5 放弃了使用 Guava Cache?

3.2 外部缓存

最著名的外部缓存 - Redis / Memcached

也许是 Redis 太有名,只要一提到缓存,基本上都会说起 Redis。但其实这类缓存的鼻祖应该是 LiveJournal 开发的 Memcached。

Redis / Memcached 都是使用内存作为存储,所以性能上要比数据库要好很多,再加上Redis 还支持很多种数据结构,使用起来也挺方便,所以作为很多人的首选。

Redis 确实不错,不过即便是使用内存,也还是需要通过网络来访问,所以网络的性能决定了 Reids 的性能;

我曾经做过一些性能测试,在万兆网卡的情况下,对于 Key 和 Value 都是长度为 20 Byte 的字符串的 get 和 set 是每秒10w左右的,如果 Key 或者 Value 的长度更大或者使用数据结构,这个会更慢一些;

作为一般的系统来使用已经绰绰有余了,从目前来看,Redis 确实很适合来做系统中的缓存。

如果考虑多实例或者分布式,可以考虑下面的方式:

  • Jedis 的 ShardedJedis( 调用端自己实现分片 )
  • twemproxy / codis( 第三方组件实现代理 )
  • Redis Cluster( 3.0 之后官方提供的集群方案 )

这些方案各有特点,这次先不展开讨论,有兴趣的可以先研究一下。

Redis有很多优点:

  • 很容易做数据分片、分布式,可以做到很大的容量
  • 使用基数比较大,库比较成熟

同时也有一些缺点:

  • Java 对象需要序列化才能保存
  • 如果服务器重启,再不做持久化的情况下会丢失数据,即使有持久化也容易出现各种各样的问题

4. 缓存的更新策略

使用缓存时,更新策略是非常重要的。最常见的缓存更新策略是 Cache Aside Pattern:

  • 失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从 cache 中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

不管是内部缓存还是外部缓存,都可以使用这样的更新策略,如果缓存系统支持,也可以通过设置过期时间来更新缓存。

更多的更新策略可以参考左耳朵耗子的这篇缓存更新的套路

5. 缓存使用常见误区

序列化方案的选择

序列化的选择,尽量避免使用 Java 原生的机制,因为原生的序列化依赖 serialVersionUID 来判断版本,如果改变就无法正常的反序列化。

一般推荐使用 Json 或者 Hessian、ProtoBuf 等二进制方式。

缓存大对象

在缓存中存放大对象,存取的代价都比较高。实际使用时,往往只是需要其中的一部分,这样会导致每一次读取都消耗更多的网络和内存资源,也会浪费缓存的容量。

当然如果每次都是用完整的对象,这样做是没有问题的。

使用缓存进行数据共享

使用缓存来当作线程甚至进程之间的数据共享方式,会让系统间产生隐形的依赖,并且也可能会产生一些竞争,常常会发生问题。所以不推荐使用这种方式来共享数据。

没有及时更新或者删除缓存中已经过期或失效的数据

这个理解起来就很简单了,如果没有及时更新或者删除,就有可能读取到错误的数据,从而导致业务的错误。

对于支持设置过期时间的缓存系统,可以对每一个数据设置合适的过期时间,来尽量避免这样的情况。


以上,我们简单介绍了后端系统常用的缓存的一些基本知识,欢迎大家和我们讨论。

1+

从 React 到 Preact 迁移指南

为什么选 Preact

Fast 3kB alternative to React with the same ES6 API.
React 的 3kb 轻量化方案,拥有同样的 ES6 API

  • 体积小。React v15.6.1 有 49.8kb,最新的 React v16.0 小了很多,有 34.8kb。而 Preact 官网声称只有 3kb,实测 v8.2.5 版本有
    4.1kb,但这已经比 React 小了至少 30kb,在移动端网页开发中占了不少优势。
  • 性能高。是最快的虚拟 DOM 框架之一,具体可以查看 Results for js web frameworks benchmark – round 6
  • 生态好。官方提供 preact-compat,可以无缝使用 React 生态系统中的各类组件。
  • 更方便。相比 React,Preact 添加了几个更为便捷的特性,包括可以直接使用标准的 HTML 属性(如 classfor),propsstatecontext 作为参数传进了 render() 等。

除了 Preact,React-like 中比较出名的还有 Inferno,它大小在9kb左右。它和前面两者相比,还加了一个新的特性就是无状态组件(stateless components)也支持生命周期事件,从而可以减少 class 的使用,做到更加轻量。性能比 React 和 Preact 都要好,其它方面和 Preact 差不多。但是有个问题是 Inferno 用了一些相对较新的特性,比如 PromiseMapSetWeakMap 等,所以浏览器兼容性比 Preact 要差点。

迁移指南

官方文档

官方文档提供了2种途径,都是在原项目上把 React 替换成 Preact,我们为了保持项目干净采用创建新项目的方式来做。

准备

1. 创建项目

Preact 官方提供了2种方式来创建项目:

  1. preact-cli
  2. Preact Boilerplate / Starter Kit

官方推荐用方式一,但我们为了方便定制采用了方式二来创建,去掉了 preact-compat 等暂时没用到库。

2. 复制代码

创建完项目,把原项目中 src 目录下的代码(index.js、组件等)拷贝到新项目 src 目录下就可以了。

这样准备工作就做好了,当然现在项目还是跑不起来,会各种报错,接下来我们需要修改代码。

修改代码

react

react 替换成 preact

import React from 'react';
// =>
import { h } from 'preact';

Preact 的 h() 相当于 React 中的 createElement(),可以阅读 WTF is JSX 了解更多。

import { render } from 'react-dom';
// =>
import { render } from 'preact';
import { Component } from 'react';
// =>
import { Component } from 'preact';

redux

直接替换成 preact-redux 就可以,其它都一样:

import { Provider } from 'react-redux';
// =>
import { Provider } from 'preact-redux';

router

我们原项目采用的是 React Router v3,Preact 官方提供了 preact-router,它并不是 React Router 的 preact 兼容版本,而是另一种轻量级的路由方案。如果你还是想用 React Router,可以它的 v4 版本,因为 v4 版本可以直接和 Preact 一起使用,而不需要 preact-compat 来做兼容。

所以如果你有比较复杂的需求的话,比如路由嵌套、视图合成等等,可以考虑用 React Router v4;如果只是想要比较基本的路由需求的话,那就用 preact-router 好了。我们项目路由需求不复杂,为了追求轻量,就采用了 preact-router,毕竟 React Router 比 preact-router 大了很多。

因为不兼容,所以改动也是比较大的:

import { Router, Route, IndexRoute } from 'react-router';
// =>
import Router from 'preact-router';
import { hashHistory } from 'react-router';
// =>
import createHashHistory from 'history/createHashHistory';
const hashHistory = createHashHistory();
const routes = (
  <Router history={hashHistory}>
    <IndexRoute component={Home} />
    <Route path="about" component={About} />
    <Route path="inbox" component={Inbox} />
  </Router>
);
// =>
const routes = (
  <Router history={hashHistory}>
    <Home path="/" />
    <About path="/about" />
    <Inbox path="/inbox" />
  </Router>
);

<Route/>onEnter/onLeave 钩子也没了,不过没关系,因为用组件的生命周期才是更合理的。

这样做完路由配置之后,组件中用到路由的部分也不一样了。原先取当前页面的 URL 或者 query 是这样的:

class Home extends Component {
  componentWillMount() {
    const { router } = this.context;
    const { params } = this.props;

    const url = router.location.pathname;
    const { id } = params;
  }
}

现在就只需要:

class Home extends Component {
  componentWillMount() {
    const { url, id } = this.props;
  }
}

是不是更简单了。preact-router 引入了更少的 API,语法也更加简洁。

有了路由,当然也少不了链接。原先我们会写成这样:

import { Link } from 'react-router';

<Link to="/about">关于</Link>

现在只需要写成普通的 <a/> 标签就可以了,因为 preact-router 会自动匹配 <a/> 标签到路由:

<a href="/about">关于</a>

当然如果你要修改激活状态样式的话还是可以用 <Link/>,但注意这里用的是 href="/" 而不是 to="/"

import { Link } from 'preact-router/match';

<Link activeClassName="active" href="/">Home</Link>

总结

上面只是梳理了我们在做迁移时需要改动的地方,因为之前并没有用第三方的 React 组件,所以也没有用 preact-compat,除了 router 改得比之前更简单了,其它基本不需要怎么改动,整体来说迁移成本不高。

迁移后效果最明显的就是编译出来的 js 小了很多,基本是之前的1/3,所以移动端网页开发推荐用 Preact 替换 React。

2+

如何成为一名数据分析师:数据的初步认知

对所有从事数据相关工作的人而言,都有一个老生常谈的问题:数据认知!毕竟在真正开始分析、BI 报表开发或者建模前,对数据进行一定的审查和认知是必须的。今天,就在此和大家一同探讨下数据的初步认知。在本文的讲解中,会将数据的初步认知划分为三大步骤:数据质量检查、数据类型认知、指标值统计

数据初步认知

一、数据质量检查

1) 关注不同数据源在统计质量上的差异

不同的数据来源,因统计、管控、可共享程度等原因在数据粒度和数据质量的保障上都有天壤之别。根据数据来源的渠道主要可将它划分为:内部数据和外部数据,下面逐个介绍它们之间的特点和差异。

1. 内部数据源
  • 业务数据:主要指后端研发主动存储的业务数据,一般是对公司运营非常核心的数据,如订单数据、用户信息等。这类数据的准确性一般是最高的,因为它往往关系到公司产品能否正常运转,统计的正确性也就至关重要;
  • 埋点数据:通过埋点技术采集的用户访问数据,不论是自建埋点还是采用第三方埋点工具,因为埋点实施、统计上传机制等,都会造成埋点数据的准确性远不如业务数据;
  • 数据仓库的数据:数据仓库数据它是由生产库数据经过一轮或者多轮次的数据转换,中间可能发生的异常情况比生产库的数据更多。诸如:无人维护、转换逻辑与理解不一致等。

对于业务数据、埋点数据、数据仓库数据三种类型的数据源我们检查的侧重点有所不同:

  • 业务数据:业务数据的复杂度主要在于字段含义、表之间关联关系以及字段与业务的实际对应关系,主要检查的也是这三点;
  • 埋点数据:埋点数据主要需要检查埋点是否与你所期望的业务事件匹配,包括埋点采集的是页面访问还是按钮点击、埋点采集时机等;
  • 数据仓库数据:主要了解其中业务指标统计逻辑、计算转换逻辑、脚本更新机制等。
2. 外部数据源
  • 用户调研数据:通过市场调研得到用户反馈数据,存在的风险主要在于市场调研人员的敷衍执行自行捏造数据以及被调研对象自身对自身判断的错误;
  • 行业发展数据:通过百度指数、微信指数、阿里指数或者其它行业观察机构统计的数据来观察行业发展情况的数据;
  • 合作方数据:合作方提供的数据,不同公司之间在指标定义和统计规范上都可能有明显差异,需要重点关注。此外,两家公司之间的用户匹配也是一大难点,需要被重点关注。

外部数据源的数据粒度一般较粗糙,数据质量上也比较难以保证,需要做更多的观察和验证。我们可实施的检查措施也相对较少,只能在使用保持更高的警惕性,慎之又慎才能更多地规避错误。

2) 关注取数过程,检查取数代码

我们通过各种方法获取数据,SQL 查询是数据类工作人员最常见的取数方式。SQL 语句的出错将导致得到的数据集出错,以下是进行 SQL 检查时需要被重点关注的点:

  • 关注 join 处理的逻辑关系,包括采用的 SQL 连接方式 inner、left 还是 outer、两张表之间数据对应关系是 1:1、1:n 还是
    n:m 等;
  • 关注 SQL 细节,包括是否采用 distinct 去重、采用 case 语句划分类别时的分类区间边界、group by 进行数据聚合的指标粒度是否正确;
  • 多版本代码检查时关注选择条件,对于 SQL 复用的场景,我们要重点关注数据选择条件的更新替换是否完全;
  • 聚合处理时,最好结合 if 条件排除极端值、异常值。

3) 关注处理数据集的空值和异常值

在对数据集是否正确的检查中,最容易发现需要被处理的情况就是空值和异常值。空值出现在数据集中往往一眼便能识别;异常值则需要一定经验性地判断,例如:数值特别夸张、文本特别长、不匹配的数据类型。在后续步骤的数据认知中,对指标进行统计汇总、分布观察等也能帮助识别异常值。

1. 空值处理

空值,如果在平时的汇总统计中可忽略则忽略,如果不可忽略则可采用以下方法来处理:

  • 替换:使用平均值、众数进行替换或者使用最接近的数据替换它,需要仔细对比寻找该行数据的其它值是否相近;
  • 推断:运用模型结合使用非空变量进行推断、预测计算得到这个空值,如:时间序列、回归模型等;
  • 删除:实在无法处理的空值,而且你已经确定它会影响到后续的计算、分析,那么你可以考虑将该行记录删除。如果不确定是否会影响,可考虑暂不处理。
2. 异常值处理

初步观察寻找异常值:

  • 在 Excel 中可以通过筛选功能或去除重复值对数据列进行观察;
  • 在 SQL 中可以通过 distinct 进行去重观察;
  • 在 Python 中,可以通过 pandas.drop_duplicates() 等方式进行去重观察。

垃圾数据或者异常值能采取的处理手段较少,当数据记录占比较大,我们首先应去寻找造成数据异常的原因,尝试从源头解决它;当数据记录占比不多时,我们可以采取直接删除的方式。

二、数据类型认知

数据类型的认知主要可从类型、数据单位、数据量纲三个角度去观察、去认知数据。这一过程后,我们一般对数据整体有一个比较粗线条的认识,知道各列的统计单位、各列的数据类型、量纲或者说数量级等。

  1. 类型:同一列数据的数据类型必须保持一致!如:时间序列不得与数值型数据混合、数值型数据不得与文本数据混合;
  2. 数据单位:同一列数据的单位必须保持一致!否则量级将完全不一致,不具备任何可比性。如:成交金额,不能既有以分为单位也有以元为单位的混合;
  3. 数据量纲:不同数据列的量纲有时会有明显差异,主要指整数型数据和百分比数据。如:活跃用户数与平台用户活跃率。当需要进行作图对比观察时候,我们需要对量纲进行处理,这涉及到标准化/归一化,常见的归一化方法有:
    • 标准差标准化:标准差标准化
    • 离差标准化:离差标准化
    • 对数标准化:对数标准化

三、指标值统计

1) 通过描述统计对数据集中趋势、离散程度、分布作认知

描述统计指对数据进行一些描述性的统计,包括均值、中位数、方差等。它主要包含三个方面:

  • 通过均值、众数、中位数等观察平均水平或说是集中趋势;
  • 通过方差与标准差等指标观察离散程度、波动大小;
  • 通过分位数、最大最小值、数据分布图等观察指标的区间分布情况。
1. 平均数

常常说的是算术平均数,即“N 个数字相加后除以 N“。在实际业务中,我们还会使用加权平均数,即“给不同维度的指标赋予不同业务权重后再相加除以权重总和,一般权重可以设为 1”。

平均数的表示含义是:一个群体在某项数据上的一般水平或者集中趋势。

2. 众数

众数,即序列中出现最多的那个数字。

众数真正的价值,不在于数值型数据中的使用而在于用在类别型的数据中。在数值型数字中,可能因为数字精度太细,导致数字出现次数都很少,几乎没有众数;而类别型数据中,众数有时会比较具有代表性。比如:系统每 5 分钟从天气预报网站读取一次实时天气,以小时为单位预测未来天气时,我们可以简单取 12 次读取中出现次数最多的记录作为这个小时的平均天气。

3. 中位数

顾名思义,中位数就是指排在中间位置的数字,将序列分为两部分。

中位数的优势在于它能避免数据的平均水平受到异常值的影响。在数据未进行较完整的清洗时,强烈建议采用中位数代表序列的中间水平。

4. 方差与标准差

方差和标准差是在概率论和统计方差衡量随机变量或一组数据时离散程度的度量,衡量数据序列的波动情况。

方差

以上为方差计算公式,开方的结果即为标准差。

5. 四分位数

百分位即降数据升序排列后,具体数据值的序号除以数据值的总数,所得出的百分比,即该数据值对应的百分位数。我们一般比较关心:25%、50%、75% 分位数。

6. 最大值、最小值

顾名思义,最大值、最小值本身没有什么好解释的。

四分位数组合最大值、最小值,可以让我们初步认知数据的分布特征。

7. 数据分布

进行了简单的描述统计,我们想对数据的分布进行简单的观察,得到一个更加直观的感受,可以制作频率分布图、箱线图来进行观察。

2) 相关系数统计,对指标间的相互作用关系进行认知

当我们需要观察两个字段之间是否存在相互影响的关系时,我们可以简单的使用相关系数。以下介绍三种相关系数,在不通场景有不同的适用度。

1. 皮尔逊相关

皮尔逊相关

用于度量两个变量X和Y之间的相关(线性相关),其值介于-1和1之间。

  • 当 r>0 时,表示两变量正相关,r<0 时,两变量为负相关;
  • 当 |r|=1 时,表示两变量为完全线性相关,即为函数关系;
  • 当 r=0 时,表示两变量间无线性相关关系;
  • 当 0<|r|<1 时,表示两变量存在一定程度的线性相关。且 |r| 越接近 1,两变量间线性关系越密切;|r| 越接近于 0,表示两变量的线性相关越弱;
  • 一般可按三级划分:0.8-1.0 极强相关,0.6-0.8 强相关,0.4-0.6 中等程度相关,0.2-0.4 弱相关,0.0-0.2 极弱相关或无相关。

适用条件:

  • 数据(近似)服从正态分布
  • 尽可能没有异常点
  • 用于描述线性相关

缺点:当样本量 n 较小时,相关系数的波动较大;

2. 斯皮尔曼等级相关

斯皮尔曼等级相关是根据等级资料研究两个变量间相关关系的方法,是依据两列成对等级的各对等级数之差来进行计算的。它与相关系数一样,取值在 -1 到 +1 之间,所不同的是它是建立在等级的基础上计算的。

斯皮尔曼等级相关

适用条件:斯皮尔曼等级相关对原始变量的分布不作要求,属于非参数统计方法,使用范围更广。

缺点:

  • 斯皮尔曼等级相关系数和皮尔逊相关系数都与样本的容量有关,尤其是在样本容量比较小的情况下,其变异程度较大;
  • 需要先对数据进行等级划分。
3. 肯德尔和谐系数

肯德尔和谐系数是计算多个等级变量相关程度的一种相关量。

前述的斯皮尔曼等级相关讨论的是两个等级变量的相关程度,用于评价时只适用于两个评分者评价 N 个人或N件作品,或同一个人先后两次评价 N 个人或 N 件作品,而肯德尔和谐系数则适用于数据资料是多列相关的等级资料,即可是 k 个评分者评 (N) 个对象,也可以是同一个人先后 k 次评 N 个对象。

通过求得肯德尔和谐系数,可以较为客观地选择好的作品或好的评分者。

肯德尔和谐系数

3) 多维交叉观察,利用数据进行业务分析

多维交叉观察,其实已经是分析阶段的主要工作。在初步的数据观察中,我们不会进行过多的交叉对比,除非不可避免的要对某些维度进行观察、验证。


以上就是关于数据初步认知的介绍.

1+