开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

使用Akka Actor和Java 8构建反应式应用

发表于 2017-12-15

本文要点

  • Actor模型为编写并发和分布式的系统提供了高层次的抽象,为开发人员屏蔽了显式锁定和线程管理的工作;
  • Actor模型为反应式系统提供了核心功能,这些功能在反应式宣言中定义为响应性、弹性、扩展性以及消息驱动;
  • Akka是一个基于Actor的框架,借助Java 8的Lambda支持它非常易于实现;
  • 通过使用Actor模型,开发人员在设计和实现系统时,能够更加关注于核心功能,忽略其他业务不相关的冗余内容;
  • 基于Actor的系统非常适合快速演化的微服务架构。

尽管“反应式(reactive)”这个术语已经存在很长时间了,但是只有到最近它才被行业实际应用到系统设计之中,并得到了主流的采纳。在2014年Gartner就写到,过去非常流行的三层架构已经日薄西山。随着企业在推进现代化方面的努力,这一点已经越发明晰了,企业必须要重新思考他们十多年来构建应用的方式。

微服务席卷了软件行业,它所带来的冲击波正在从根本上动摇传统开发流程。我们看到软件设计范式发生了变化,项目管理的方法论也随之发生了演化。我们正在向新的应用设计和实现方式转变,它以前所未有的势头在IT系统中实现。即便微服务这个术语不是全新的概念,我们的行业也正在意识到它不仅仅是解耦RESTful端点和拆分单体应用,它真正的价值在于更好的资源利用效率以及面对不可预知工作负载时更强的扩展性。反应式宣言(Reactive Manifesto)的原则很快变成了微服务架构的圣经,因为它们本质上就是分布式的反应式应用。

如今应用中的Actor模型

为了保持用户的兴趣,应用必须要保持很高的响应性,同时,为了满足受众不断变化的需求和预期,应用必须要快速演化。用于构建应用的技术在不断地快速演进;科学在不断发展,持续涌现的新需求不能依赖于昨天的工具和方法论。Actor模型正在不断发展起来,它是一种构建应用的高效工具,能够充分发挥多核、内存以及集群环境所带来的强大处理能力。

Actor提供了一种简单却强大的模型,通过该模型设计和实现的应用可以分布式的,并且能够跨系统中所有的资源共享工作任务,这些资源可以从线程和核心级别一直到服务器集群和数据中心级别。它提供了一个高效的框架来构建应用,所构建出的应用具有较高的并发性,并且能够提升资源的利用率。另外很重要的一点,Actor模型还提供了定义良好的方式来优雅地处理错误和故障,确保应用的可靠性级别,它能够隔离问题,防止级联故障和长时间的宕机。

在过去,构建高并发的系统通常涉及到大量的装配和非常技术化的编程,它们都是非常难以掌握的。这些技术方面的挑战会抢占我们对系统核心业务功能的注意力,因为很大一部分的工作都集中在业务细节上,这需要花费很多的时间和精力用于搭建处理管道和功能装配。如果我们使用Actor来构建系统的话,就能在一个较高的抽象层级完成这些任务,因为处理管道和功能装配已经内置在了Actor模型之中。这不仅能够将我们从繁琐的传统系统实现的细节中解放出来,还能让我们更加关注于系统的核心功能和创新。

使用Java 8和Akka实现Actor模型

Akka是一个在JVM上构建高并发、分布式、有弹性的消息驱动应用的工具集。Akka “actor”只是Akka工具集中一部分,它能够让我们在编写并发代码时,不用去思考低层级的线程和锁。Akka中其他的工具还包括Akka Streams和Akka http。尽管Akka是使用Scala编写的,但是它也有Java API,如下的样例运行在2.4.9版本以上(目前Akka的最新版本为2.5.7,但核心API与本文基本相同——译者注)。

在Akka中,Actor是基本的工作单元。Actor是状态和行为的一个容器,它可以创建和监管子Actor。Actor之间通过异步的消息实现相互的通信。这个模型保护了Actor的内部状态,使其能够实现线程安全,该模型还实现了事件驱动的行为,从而不会阻塞其他的Actor。作为开始,我们所需要知道的只是akka的Maven依赖。

1
2
3
4
5
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_2.11</artifactId>
<version>2.4.9</version>
</dependency>__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

改变Actor的状态

就像通过移动设备收发短信一样,我们需要使用消息来调用Actor。与短信类似,Actor之间的消息也必须是不可变的。在使用Actor的时候,最重要的就是定义它所能接受的消息。(这种消息通常被称为协议,因为它定义了Actor之间的交互点。)Actor接收消息,然后以各种方式对其作出反应,它们可以发送其他的消息、修改自己的状态或行为、创建其他的Actor。

Actor的初始行为是通过实现receive()方法来定义的,在实现这个方法时,可以在默认的constructor. receive()中借助ReceiveBuilder匹配传入的消息并执行相关的行为。每条信息的行为通过一个Lambda表达式来定义。在下面的样例中,ReceiveBuilder使用了对接口方法“onMessage”的引用。onMessage方法增加了一个计数器(内部状态)并通过AbstractLoggingActor.log方法记录了一条info级别的日志信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__static class Counter extends AbstractLoggingActor {
static class Message { }

private int counter = 0;

{
receive(ReceiveBuilder
.match(Message.class, this::onMessage)
.build()
);
}

private void onMessage(Message message) {
counter++;
log().info("Increased counter " + counter);
}
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

Actor就绪之后,还需要启动它。这需要通过ActorSystem实现,它控制着Actor的生命周期。但是,我们首先需要提供一些关于如何启动这个Actor所需的额外信息。akka.actor.Props是一个配置对象,能够将上下文范围内的配置暴露给框架的各个地方。它用来创建我们的Actor,这个对象是不可变的,因此线程安全,完全可以共享。

return Props.create(Counter.class);

Props对象描述了Actor的构造器参数。将其封装到一个工厂函数中并放到Actor的构造器附近通常是一种好的实践。ActorSystem本身是在main方法中创建的。

1
2
3
4
5
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__public static void main(String[] args) {
ActorSystem system = ActorSystem.create("sample1");
ActorRef counter = system.actorOf(Counter.props(), "counter");

}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

ActorSystem (“sample1”)和它所包含的Actor(“counter”)都可以给定名称,这样便于在Actor层级结构中进行导航,这个话题稍后会进行讨论。现在,ActorRef可以发送一条消息给Actor,如样例所示:

counter.tell(new Counter.Message(), ActorRef.noSender());

在这里,使用两个参数定义了要发送的Message以及消息的发送者。(顾名思义,noSender表明在本例中,并没有使用发送者。)如果运行上述样例的话,我们就能得到预期的输出:

[01/10/2017 10:15:15.400] [sample1-akka.actor.default-dispatcher-4] [akka://sample1/user/counter] Increased counter 1

这是一个非常简单的样例,但是它提供了我们所需的线程安全性。发送给Actor的消息来源于不同的线程,这些消息屏蔽了并发的问题,因为Actor框架会进行消息的序列化处理。读者可以在线查看完整的样例。

修改Actor的行为

读者可能已经注意到,我们的简单样例修改了Actor的状态,但是它并没有改变Actor行为,也没有发送消息给其他Actor。我们接下来考虑一个防盗报警系统,它可以通过密码来启用或禁用,它的传感器会探测活动。如果有人试图通过不正确的密码禁用告警的话,它就会发出声音。Actor能够响应三种消息,分别是通过密码(以负载的形式提供该值)进行禁用和启用的消息以及盗窃活动的消息。这三种消息都包含在了下面的协议中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__static class Alarm extends AbstractLoggingActor {
// contract
static class Activity {}
static class Disable {
private final String password;
public Disable(String password) {
this.password = password;
}
}
static class Enable {
private final String password;
public Enable(String password) {
this.password = password;
}
}

// ...
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

Actor有一个针对密码的预置属性,它也会传入到构造器中:

1
2
3
4
5
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__private final String password;
public Alarm(String password) {
this.password = password;
// ...
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

前面提到的akka.actor.Props配置对象也需要知道password属性,这样的话,才能在Actor系统启动的时候将其传递给实际的构造器。

1
2
3
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__public static Props props(String password) {
return Props.create(Alarm.class, password);
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

针对每种可能的消息,Alarm还需要对应的行为。这些行为是AbstractActor中receive方法的实现。receive方法应该定义一系列的match语句(每个都是PartialFunction<Object, BoxedUnit>类型),它定义了Actor能够处理的消息,另外还包含消息如何进行处理的实现。

1
2
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__private final PartialFunction<Object, BoxedUnit> enabled;
private final PartialFunction<Object, BoxedUnit> disabled;__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

如果这个签名看上去令人望而生畏的话,那么我们的代码可以通过前面所使用的ReceiveBuilder将细节隐藏起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__public Alarm(String password) {
this.password = password;

enabled = ReceiveBuilder
.match(Activity.class, this::onActivity)
.match(Disable.class, this::onDisable)
.build();

disabled = ReceiveBuilder
.match(Enable.class, this::onEnable)
.build();

receive(disabled);
}
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

需要注意最后对receive的调用,将默认行为设置为“disabled”。这三个行为是使用已有的三个方法(onActivity、onDisable、onEnable)来实现的。这些方法中最简单的是onActivity。如果接收到activity的话,报警会在控制台记录一条日志。在这里需要注意activity没有消息负载,所以我们将其命名为ignored。

1
2
3
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__private void onActivity(Activity ignored) {
log().warning("oeoeoeoeoe, alarm alarm!!!");
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

如果Actor接收到一条enable消息的话,新的状态将会记录下来并且状态将会变更为enabled。如果密码不匹配的话,会记录一条简短的警告日志。消息负载现在包含了密码,所以我们可以通过访问它来校验密码。

1
2
3
4
5
6
7
8
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__private void onEnable(Enable enable) {
if (password.equals(enable.password)) {
log().info("Alarm enable");
getContext().become(enabled);
} else {
log().info("Someone failed to enable the alarm");
}
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

当收到一条disable消息时,Actor需要检查密码,记录一条关于状态变化的简短消息然后将状态修改为disabled或者在密码不匹配的情况下记录一条警告信息。

1
2
3
4
5
6
7
8
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__private void onDisable(Disable disable) {
if (password.equals(disable.password)) {
log().info("Alarm disabled");
getContext().become(disabled);
} else {
log().warning("Someone who didn't know the password tried to disable it");
}
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

这样就完成了Actor的逻辑,我们接下来可以启动Actor系统并向其发送一些消息。注意,我们的正确密码“cats”是作为一个属性传递给Actor系统的。

1
2
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__ActorSystem system = ActorSystem.create();
final ActorRef alarm = system.actorOf(Alarm.props("cat"), "alarm");__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

消息:

1
2
3
4
5
6
7
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__    alarm.tell(new Alarm.Activity(), ActorRef.noSender());
alarm.tell(new Alarm.Enable("dogs"), ActorRef.noSender());
alarm.tell(new Alarm.Enable("cat"), ActorRef.noSender());
alarm.tell(new Alarm.Activity(), ActorRef.noSender());
alarm.tell(new Alarm.Disable("dogs"), ActorRef.noSender());
alarm.tell(new Alarm.Disable("cat"), ActorRef.noSender());
alarm.tell(new Alarm.Activity(), ActorRef.noSender());__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

产生的输出如下所示:

1
2
3
4
5
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__[01/10/2017 10:15:15.400] [default-akka.actor.default-dispatcher-4] [akka://default/user/alarm] Someone failed to enable the alarm
[01/10/2017 10:15:15.401] [default-akka.actor.default-dispatcher-4] [akka://default/user/alarm] Alarm enable
[WARN] [01/10/2017 10:15:15.403] [default-akka.actor.default-dispatcher-4] [akka://default/user/alarm] oeoeoeoeoe, alarm alarm!!!
[WARN] [01/10/2017 10:15:15.404] [default-akka.actor.default-dispatcher-4] [akka://default/user/alarm] Someone who didn't know the password tried to disable it
[01/10/2017 10:15:15.404] [default-akka.actor.default-dispatcher-4] [akka://default/user/alarm] Alarm disabled__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

你可以在GitHub上找到完整的可运行样例。到目前为止,我们只使用了一个Actor来处理消息。不过就像在业务组织中一样,Actor也能形成自然的层级结构。

Actor的层级结构

Actor可能会创建其他的Actor。当一个Actor创建另外一个Actor时,创建者也被称为监管者(supervisor),而被创建的Actor也被称为工作者(worker)。我们可能基于很多原因需要创建工作者Actor,最常见的原因是工作的委托。监管者创建一个或多个工作者Actor,然后将工作委托给它们。

监管者同时会成为工作者的看守人。就像父母会时刻关注孩子的行为那样,监管者也会照顾它的工作者Actor。如果Actor遇到问题的话,它会将自己挂起(也就是说在恢复之前,它不会处理正常的消息),并且会通知其监管者自己发生了故障。

到目前为止,我们创建了多个Actor并为其分配了名字。Actor的名字用来在层级结构中识别Actor。与Actor交互的一般都是用户所创建Actor的父Actor,也就是带有"/user"路径的guardian。使用原始system.actorOf()创建的Actor是该guardian的直接子Actor,如果它终止的话,系统中所有正常的Actor也都会关闭。在上面的alarm样例中,我们创建的是/user/alarm路径的用户Actor。因为Actor是按照严格的层级方式来创建的,所以Actor会存在一个由Actor名称组成的唯一序列,这个序列会从Actor系统的根逐级往下,按照父子关系形成。这个序列类似于文件系统中的封闭文件夹,因此采用了“路径(path)”这个名称来代指它,当然Actor层级结构与文件系统的层级结构还有一些基础的差异。

在Actor内部,我们可以调用getContext().actorOf(props, “alarm-child”)创建名为“alarm-child”的新Actor,它会作为alarm Actor的子Actor。子Actor的生命周期是绑定在父Actor之上的,这意味着如果我们停止“alarm” Actor的话,也会停掉其子Actor:

这种层级结构对于基于Actor系统的故障处理也有着直接影响。Actor系统的典型特点就是将任务进行分解和委托,直到它被拆分得足够小,能够从一个地方进行处理。通过这种方式,不仅任务本身能够非常清晰地进行结构化,所形成的Actor也能在如下方面变得非常明确:

  • 应该处理哪些消息
  • 应该怎样正确地应对接收到的消息
  • 应该如何处理故障。

如果某个Actor无法处理特定情景的话,它会发送对应的故障消息给它的监管者,请求帮助。面对故障,监管者有四种不同的可选方案:

  • 恢复(Resume)子Actor,保持其已有的内部状态,但是忽略掉导致故障的消息;
  • 重启(Restart)子Actor,通过启动新的实例,清理其已有的状态;
  • 永久停止(Stop)子Actor,将子Actor未来所有的消息发送至Dead-Letter Office;
  • 将故障传递至更高的层级(Escalate),这需要让监管者本身也发生故障。

接下来,我们将上面学到的所有内容通过一个样例来具体讲解一下:NonTrustWorthyChild接收Command消息,每当收到该消息时,会增加一个内部的计数器。如果消息数能够被4整除的话,会抛出一个RuntimeException,这个异常会向上传递给Supervisor。这里并没有什么新东西,Command消息本身并没有负载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__public class NonTrustWorthyChild extends AbstractLoggingActor {

public static class Command {}
private long messages = 0L;

{
receive(ReceiveBuilder
.match(Command.class, this::onCommand)
.build()
);
}

private void onCommand(Command c) {
messages++;
if (messages % 4 == 0) {
throw new RuntimeException("Oh no, I got four commands, can't handle more");
} else {
log().info("Got a command " + messages);
}
}

public static Props props() {
return Props.create(NonTrustWorthyChild.class);
}
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

Supervisor 在它的构造器中启动NonTrustWorthyChild,并将它所接收到的command消息直接转发给子Actor。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__public class Supervisor extends AbstractLoggingActor {
{
final ActorRef child = getContext().actorOf(NonTrustWorthyChild.props(), "child");

receive(ReceiveBuilder
.matchAny(command -> child.forward(command, getContext()))
.build()
);

}
//…
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

当Supervisor实际启动之后,所形成的层级结构将会是“/user/supervisor/child”。在我们完成该任务之前,需要预先定义所谓的监管策略(supervision strategy)。Akka提供了两种类型的监管策略:OneForOneStrategy和AllForOneStrategy。它们之间的差异在于前者会将指令应用于发生故障的子Actor,而后者则会将指令同时应用于子Actor的兄弟节点。正常情况下,我们应该使用OneForOneStrategy,如果没有明确声明的话,它也是默认方案。监管策略需要通过覆盖SupervisorStrategy方法来定义。

1
2
3
4
5
6
7
8
9
10
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__@Override
public SupervisorStrategy supervisorStrategy() {
return new OneForOneStrategy(
10,
Duration.create(10, TimeUnit.SECONDS),
DeciderBuilder
.match(RuntimeException.class, ex -> stop())
.build()
);
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

第一个参数定义了maxNrOfRetries,它指定了子Actor在停止之前允许尝试重启的次数。(如果设置为负数值,则代表没有限制)。withinTimeRange参数定义了maxNrOfRetries的持续时间窗口。按照上面的定义,该策略会在10秒钟之内尝试10次。DeciderBuilder的工作方式与ReceiveBuilder完全类似,它定义了要匹配的异常以及如何应对。在本例中,如果在10秒钟内尝试了10次的话,Supervisor会停止掉NonTrustWorthyChild,所有剩余的消息将会发送至dead
letter box。

Actor系统是通过Supervisor Actor来启动的。

1
2
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__ActorSystem system = ActorSystem.create();
final ActorRef supervisor = system.actorOf(Supervisor.props(), "supervisor");__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

当系统启动之后,我们发送10条command信息到Supervisor。需要注意,“Command”消息是定义在NonTrustWorthyChild中的。

1
2
3
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__for (int i = 0; i < 10; i++) {
supervisor.tell(new NonTrustWorthyChild.Command(), ActorRef.noSender());
}__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

输出的内容显示,在四条消息之后,异常传递到了Supervisor中,剩下的消息发送到了deadLetters收件箱中。如果SupervisorStrategy被定义为restart()而不是stop()的话,那么将会启动一个新的NonTrustWorthyChild Actor实例。

1
2
3
4
5
6
7
复制代码__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__[01/10/2017 12:33:47.540] [default-akka.actor.default-dispatcher-3] [akka://default/user/supervisor/child] Got a command 1
[01/10/2017 12:33:47.540] [default-akka.actor.default-dispatcher-3] [akka://default/user/supervisor/child] Got a command 2
[01/10/2017 12:33:47.540] [default-akka.actor.default-dispatcher-3] [akka://default/user/supervisor/child] Got a command 3
[01/10/2017 12:33:47.548] [default-akka.actor.default-dispatcher-4] [akka://default/user/supervisor] Oh no, I got four commands, I can't handle any more
java.lang.RuntimeException: Oh no, I got four commands, I can't handle any more
...
[01/10/2017 12:33:47.556] [default-akka.actor.default-dispatcher-3] [akka://default/user/supervisor/child] Message [com.lightbend.akkasample.sample3.NonTrustWorthyChild$Command] from Actor[akka://default/deadLetters] to Actor[akka://default/user/supervisor/child#-1445437649] was not delivered. [1] dead letters encountered.__Fri Dec 15 2017 10:03:39 GMT+0800 (CST)____Fri Dec 15 2017 10:03:39 GMT+0800 (CST)__

这个日志可以关闭或者进行调整,这需要修改 “akka.log-dead-letters”和“akka.log-dead-letters-during-shutdown”的配置。

读者可以在线查看完整的样例,并尝试调整SupervisorStrategy。

总结

借助Akka和Java 8,我们能够创建分布式和基于微服务的系统,这在几年前还是一种梦想。现在,所有行业的企业都迫切希望系统的演化速度能够跟上业务的速度和用户的需求。如今,我们能够弹性的扩展系统,使其支持大量的用户和庞大的数据。我们创建的系统有望具备一定级别的弹性,使停机时间不再是按照小时来计算,而是按照秒来计算。基于Actor的系统能够让我们创建快速演进的微服务架构,它可以进行扩展并且能够不停机运行。

Actor模型提供了反应式系统的核心功能,也就是反应式宣言所定义的响应性、弹性、扩展性以及消息驱动。

Actor系统可以进行水平扩展,从一个节点扩展到具有众多节点的集群,这样的话就为我们提供了灵活性以应对大规模的负载。除此之外,还可能实现有弹性的扩展,也就是扩展系统的处理能力,不管是手动的还是自动的,都能充分支持系统活动所出现的高峰和低谷状态。

借助Actor和Actor系统,故障探测和恢复就成为一种架构上的特性,而不是事后才考虑增补上去的功能。我们可以使用内置的Actor监管策略来处理下属工作者Actor遇到的问题,能够一直向上追溯到Actor系统层级,集群节点会积极监控集群的状态,在这种环境中处理故障已经植入到了Actor和Actor系统的DNA之中了。这其实始于Actor之间异步交换消息的最基础层级:如果你给我发送一条消息的话,你必须要考虑可能的输出,如果得到了预期的答复该怎么办,没有收到预期答复又该怎么办?这种处理策略会一直延伸到集群中节点的加入和离开。

在设计系统时,按照Actor的方式思考在很多方面都会更加直观。Actor的交互方式对我们来说会更加自然,因为简单来讲,它的交互方式与人类之间的交互方式更加接近。这样的话,我们在设计和实现系统时,能够更加关注核心功能,忽略其他业务不相关的冗余内容。

关于作者

Markus Eisele 是一位Java Champion、前Java EE专家组成员、JavaLand的创始人,在世界范围内的Java会议上是享有盛誉的讲师,在企业级Java领域颇为知名。他在Lightbend担任开发人员,读者可以在Twitter上@myfear联系到他。

查看英文原文:Building Reactive Applications with Akka Actors and Java 8

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何爬取gerrit

发表于 2017-12-14

起因

主要是公司想要规范化工程师的提交信息,虽说不是我来弄,但是我想试试用之前学的爬虫来尝试抓取,并且可以初步的数据处理分析,然后可视化。正好一整套的流程走一遍。说干就干,打开网页,F12。
发现数据在表格里。抓呗!

但是发现抓出来的数据长的乱七八糟,最关键的是并没有这个table。所以就猜想,应该是动态加载的,转到network,xhr。

再刷新下页面,发现数据都在这个接口里嘛,这就简单了!

设计流程:获取数据,处理数据,可视化。

第一步:先获取数据,存到表格里。

一定要记得cookie啊,血的教训,我折腾了好久,得到的都是几个字符,后来才醒悟过来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码def requesst(url,limit_time):
continue_flag = 1
# while(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
'Cookie': 'GerritAccount=aRqdfpU4Oa1tMBZglP3GWlz.y1oY.8am'
}
data = requests.get(url, headers=headers).text
# content = data.text #你要的数据,JSON格式的
remove = re.compile('\)\]\}\'')
#去掉多余的几个垃圾字符
# data = data.replace(')]}\'',"")
data = re.sub(remove, "", data)
data_json = json.loads(data)

with open("gerrit.csv", "a",newline='') as csvfile:
writer = csv.writer(csvfile)
for one in data_json:
one["updated"] = time_format(one["updated"])
if (time_cmp(one["updated"], limit_time) < 0):
print("已经发现有超过时间的数据")
continue_flag = 0
break
else:
writer.writerow([one["project"], one["branch"], one["subject"], one["owner"]["name"], one["updated"]])
print(one["project"], one["branch"], one["subject"], one["owner"]["name"], one["updated"], one["_sortkey"])
return continue_flag, DOWNLOAD_URL + "&N=" + data_json[-1]["_sortkey"]

continue_flag是为了主程序里超过我限定的时间后就不再爬取下一页。
gerrit的默认url是下面这个url,然后下一页按钮的url是这个url加上这一页最后一项的_sortkey(形如0049b98c0000f3b8)。所以我要把下一页的url也return出来。

1
2
复制代码DOWNLOAD_URL = 'http://192.168.8.40:8080/changes/?q=status:merged&n=25&O=1'
# DOWNLOAD_URL = 'http://192.168.8.40:8080/changes/?q=status:merged&n=25&O=1&N=0049b98c0000f3b8'

嘿嘿,这个是最后的结果,中间编写过程就不放了,总之是不断丰富,不断简化的过程。

第二步:处理数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码#用正则表达式统计不符合规范的人员及其出现的个数  得到一个DataFrame
def subject_format_count():
nonstandard_count={}
#newline=' '可以防止两行之间出现空行
with open(r"gerrit.csv",newline='') as csvfile:
readCSV = csv.reader(csvfile, delimiter=',')
for row in readCSV:
matchObj = re.match(r"^TFS_\d+:" + row[3] + "_\D+\w+:.+", row[2])
if matchObj:
pass
else:
if row[3] in nonstandard_count:
nonstandard_count[row[3]] += 1
else:
nonstandard_count[row[3]] = 1
#去掉统进来的标签
nonstandard_count.pop('owner')
#按出现次数递减排序
sort_nonstandard_count = sorted(nonstandard_count.items(), key=lambda v: v[1], reverse=True)
#这地方的设置可以先查看sort_nonstandard_count 然后看看具体要的是哪个值 根据这里再写可视化
df = pandas.DataFrame(sort_nonstandard_count, index=[item[0] for item in sort_nonstandard_count])
return df

正则表达式可以度娘在线正则表达式测试找到最合适的正则。如果符合正则表达式则不处理,如果不符合,则将其存到nonstandard_count并且值+1。然后得到的nonstandard_count按照值排序,大的在前面。返回一个DataFrame,方便后续处理。

第三步:可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码##可视化处理###############
def view_format_count(df):
# x为横坐标刻度
x = np.arange(len(df.index))
# 设置y轴的数值,取df的1列,0列为横坐标
y = np.array(df[1])
# 设置x横坐标显示为0列的名字
xticks1 = np.array(df[0])
# 设置横坐标格式 倾斜30°
plt.xticks(x, xticks1, size='small', rotation=30)
# 画出柱状图 appha为透明度
plt.bar(x, y, width=0.35, align='center', color='c', alpha=0.8)
# 在柱形图上方显示y值 zip(x,y)得到的是tuple列表 即各列顶点的坐标
# 然后再各列的顶点上方0.05设置一个文本 ha水平对齐left,right,center va垂直对齐 'center' , 'top' , 'bottom' ,'baseline'
for a, b in zip(x,y):
plt.text(a, b + 0.05, '%.0f' %b, ha='center', va='bottom', fontsize=11)
plt.show()

注释很详细了,就是画出df,然后在柱形图上加上这个值的text。

第四步 main

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码def main():
limit_time = input("请输入截止时间 格式:20171211000000\n")
url = DOWNLOAD_URL
continue_flag = 1
#写标题
with open("gerrit.csv", "w",newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["project", "branch", "subject", "owner", "updated"])
#循环爬下一页
while (continue_flag):
continue_flag, url = requesst(url,limit_time)
#处理数据并可视化
view_format_count(subject_format_count())

这个只是串起来了。关键的代码基本都贴出来了,其他的几个稍微处理数据的,没有贴出,可以访问我github查看。

github

这个直接爬是没用的哈,我是内网。

结果

额,我升级PyCharm前还好好的啊。升级完咋就这样了呢……我后面再琢磨琢磨。

##总结一下
总的来说收获很大,自己实打实的从0开始做一个小的工具(都不算项目)。整个过程虽说小困难不断,但是基本都能找到问题所在,网上找到解决方法。其实没想象的难的,只要拆分成一部分一部分然后开始动手就好。虽说之前学得部分实在生疏,但做这个确实熟练了很多,就好像以前学习的时候,看着老师讲都懂,但是自己实际做题就懵了。多练就好!然后建议大家也在学习的过程中不断的找一些对自己能用上的小项目去做,会有成就感的!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python爬虫实战 - 抓取BOSS直聘职位描述 和 数据

发表于 2017-12-14

Pyhton爬虫实战 - 抓取BOSS直聘职位描述 和 数据清洗

零、致谢

感谢BOSS直聘相对权威的招聘信息,使本人有了这次比较有意思的研究之旅。

由于爬虫持续爬取 www.zhipin.com 网站,以致产生的服务器压力,本人深感歉意,并没有 DDoS 和危害贵网站的意思。

2017-12-14 更新 : 在跑了一夜之后,服务器 IP 还是被封了,搞得本人现在家里、公司、云服务器三线作战啊

一、抓取详细的职位描述信息

1.1 前提数据

这里需要知道页面的 id 才能生成详细的链接,在 Python爬虫框架Scrapy实战 - 抓取BOSS直聘招聘信息 中,我们已经拿到招聘信息的大部分信息,里面有个 pid 字段就是用来唯一区分某条招聘,并用来拼凑详细链接的。

是吧,明眼人一眼就看出来了。


1.2 详情页分析

详情页如下图所示

P2

在详情页中,比较重要的就是职位描述和工作地址这两个

由于在页面代码中岗位职责和任职要求是在一个 div 中的,所以在抓的时候就不太好分,后续需要把这个连体婴儿,分开分析。


1.3 爬虫用到的库

使用的库有

  • requests
  • BeautifulSoup4
  • pymongo

对应的安装文档依次如下,就不细说了

  • 安装 Requests - Requests 2.18.1 文档
  • 安装 Beautiful Soup - Beautiful Soup 4.2.0 文档
  • PyMongo安装使用笔记

1.4 Python 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
复制代码"""
@author: jtahstu
@contact: root@jtahstu.com
@site: http://www.jtahstu.com
@time: 2017/12/10 00:25
"""
# -*- coding: utf-8 -*-
import requests
from bs4 import BeautifulSoup
import time
from pymongo import MongoClient

headers = {
'x-devtools-emulate-network-conditions-client-id': "5f2fc4da-c727-43c0-aad4-37fce8e3ff39",
'upgrade-insecure-requests': "1",
'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
'accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
'dnt': "1",
'accept-encoding': "gzip, deflate",
'accept-language': "zh-CN,zh;q=0.8,en;q=0.6",
'cookie': "__c=1501326829; lastCity=101020100; __g=-; __l=r=https%3A%2F%2Fwww.google.com.hk%2F&l=%2F; __a=38940428.1501326829..1501326829.20.1.20.20; Hm_lvt_194df3105ad7148dcf2b98a91b5e727a=1501326839; Hm_lpvt_194df3105ad7148dcf2b98a91b5e727a=1502948718; __c=1501326829; lastCity=101020100; __g=-; Hm_lvt_194df3105ad7148dcf2b98a91b5e727a=1501326839; Hm_lpvt_194df3105ad7148dcf2b98a91b5e727a=1502954829; __l=r=https%3A%2F%2Fwww.google.com.hk%2F&l=%2F; __a=38940428.1501326829..1501326829.21.1.21.21",
'cache-control': "no-cache",
'postman-token': "76554687-c4df-0c17-7cc0-5bf3845c9831"
}
conn = MongoClient('127.0.0.1', 27017)
db = conn.iApp # 连接mydb数据库,没有则自动创建


def init():
items = db.jobs_php.find().sort('pid')
for item in items:
if 'detail' in item.keys(): # 在爬虫挂掉再此爬取时,跳过已爬取的行
continue
detail_url = "https://www.zhipin.com/job_detail/%s.html?ka=search_list_1" % item['pid']
print(detail_url)
html = requests.get(detail_url, headers=headers)
if html.status_code != 200: # 爬的太快网站返回403,这时等待解封吧
print('status_code is %d' % html.status_code)
break
soup = BeautifulSoup(html.text, "html.parser")
job = soup.select(".job-sec .text")
if len(job) < 1:
continue
item['detail'] = job[0].text.strip() # 职位描述
location = soup.select(".job-sec .job-location")
item['location'] = location[0].text.strip() # 工作地点
item['updated_at'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) # 实时爬取时间
res = save(item) # 保存数据
print(res)
time.sleep(40) # 停停停


# 保存数据到 MongoDB 中
def save(item):
return db.jobs_php.update_one({"_id": item['_id']}, {"$set": item})


if __name__ == "__main__":
init()

代码 easy,初学者都能看懂。


1.5 再啰嗦几句

在 上一篇文章 中只是爬了 上海-PHP 近300条数据,后续改了代码,把12个城市的 PHP 相关岗位的数据都抓下来了,有3500+条数据,慢慢爬吧,急不来。

像这样
P7
P8

二、数据清洗

2.1 校正发布日期

1
2
3
复制代码"time" : "发布于03月31日",
"time" : "发布于昨天",
"time" : "发布于11:31",

这里拿到的都是这种格式的,所以简单处理下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码import datetime

from pymongo import MongoClient

db = MongoClient('127.0.0.1', 27017).iApp

def update(data):
return db.jobs_php.update_one({"_id": data['_id']}, {"$set": data})

# 把时间校正过来
def clear_time():
items = db.jobs_php.find({})
for item in items:
if not item['time'].find('布于'):
continue
item['time'] = item['time'].replace("发布于", "2017-")
item['time'] = item['time'].replace("月", "-")
item['time'] = item['time'].replace("日", "")
if item['time'].find("昨天") > 0:
item['time'] = str(datetime.date.today() - datetime.timedelta(days=1))
elif item['time'].find(":") > 0:
item['time'] = str(datetime.date.today())
update(item)
print('ok')

2.2 校正薪水以数字保存

1
2
3
4
5
6
7
8
复制代码"salary" : "5K-12K",

#处理成下面的格式
"salary" : {
"low" : 5000,
"high" : 12000,
"avg" : 8500.0
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码# 薪水处理成数字
def clear_salary():
items = db.jobs_php.find({})
for item in items:
if type(item['salary']) == type({}):
continue
salary_list = item['salary'].replace("K", "000").split("-")
salary_list = [int(x) for x in salary_list]
item['salary'] = {
'low': salary_list[0],
'high': salary_list[1],
'avg': (salary_list[0] + salary_list[1]) / 2
}
update(item)
print('ok')

2.3 根据 工作经验年限 划分招聘等级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码# 设置招聘的水平
def set_level():
items = db.jobs_php.find({})
for item in items:
if item['workYear'] == '应届生':
item['level'] = 1
elif item['workYear'] == '1年以内':
item['level'] = 2
elif item['workYear'] == '1-3年':
item['level'] = 3
elif item['workYear'] == '3-5年':
item['level'] = 4
elif item['workYear'] == '5-10年':
item['level'] = 5
elif item['workYear'] == '10年以上':
item['level'] = 6
elif item['workYear'] == '经验不限':
item['level'] = 10
update(item)
print('ok')

这里有点坑的就是,一般要求经验不限的岗位,需求基本都写在任职要求里了,所以为了统计的准确性,这个等级的数据,后面会被舍弃掉。

2017-12-14 更新:
从后续的平均数据来看,这里的经验不限,一般要求的是1-3年左右,但是还是建议舍弃掉。


2.4 区分开<岗位职责>和<任职要求>

对于作者这个初学者来说,这里还没有什么好的方法,知道的同学,请务必联系作者,联系方式在个人博客里

so , i’m sorry.

为什么这两个不好划分出来呢?

因为这里填的并不统一,可以说各种花样,有的要求在前,职责在后,有的又换个名字区分。目前看到的关于要求的有['任职条件', '技术要求', '任职要求', '任职资格', '岗位要求']这么多说法。然后顺序还不一样,有的要求在前,职责在后,有的又反之。

举个栗子

会基本的php编程!能够修改简单的软件!对云服务器和数据库能够运用!懂得微信公众账号对接和开放平台对接!我们不是软件公司,是运营公司!想找好的公司学习的陕西基本没有,要到沿海城市去!但是我们是实用型公司,主要是软件应用和更适合大众!

啥也不说的,这里可以认为这是一条脏数据了。

不行,再举个栗子

PHP中级研发工程师(ERP/MES方向)
1、计算机或相关学科本科或本科以上学历;
2、php和Java script的开发经验。
3、Linux和MySQL数据库的开发经验;
5、有ERP、MES相关开发经验优先;
6、英语的读写能力;
7、文化的开放性;
我们提供
1、有趣的工作任务;
2、多元的工作领域;
3、与能力相关的收入;
4、年轻、开放并具有创造力的团队和工作氛围;
5、不断接触最新科技(尤其是工业4.0相关);

这个只有要求,没职责,还多了个提供,我乐个趣 ╮(╯▽╰)╭

所以,气的想骂人。


ok ,现在我们的数据基本成这样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码{
"_id" : ObjectId("5a30ad2068504386f47d9a4b"),
"city" : "苏州",
"companyShortName" : "蓝海彤翔",
"companySize" : "100-499人",
"education" : "本科",
"financeStage" : "B轮",
"industryField" : "互联网",
"level" : 3,
"pid" : "11889834",
"positionLables" : [
"PHP",
"ThinkPHP"
],
"positionName" : "php研发工程师",
"salary" : {
"avg" : 7500.0,
"low" : 7000,
"high" : 8000
},
"time" : "2017-06-06",
"updated_at" : "2017-12-13 18:31:15",
"workYear" : "1-3年",
"detail" : "1、处理landcloud云计算相关系统的各类开发和调研工作;2、处理coms高性能计算的各类开发和调研工作岗位要求:1、本科学历,两年以上工作经验,熟悉PHP开发,了解常用的php开发技巧和框架;2、了解C++,python及Java开发;3、有一定的研发能力和钻研精神;4、有主动沟通能力和吃苦耐劳的精神。",
"location" : "苏州市高新区科技城锦峰路158号101park8幢"
}

由于还没到数据展示的时候,所以现在能想到的就是先这样处理了

项目开源地址:git.jtahstu.com/jtahstu/Scr…

三、展望和设想

首先这个小玩意数据量并不够多,因为爬取时间短,站点唯一,再者广度局限在 PHP 这一个岗位上,以致存在一定的误差。

所以为了数据的丰富和多样性,这个爬虫是一定要持续跑着的,至少要抓几个月的数据才算可靠吧。

然后准备再去抓下拉勾网的招聘数据,这也是个相对优秀的专业 IT 招聘网站了,数据也相当多,想当初找实习找正式工作,都是在这两个 APP 上找的,其他的网站几乎都没看。

最后,对于科班出身的学弟学妹们,过来人说一句,编程相关的职业就不要去志连、钱尘乌有、five eight桐城了,好吗?那里面都发的啥呀,看那些介绍心里没点数吗?

四、help

这里完全就是作者本人依据个人微薄的见识,主观臆断做的一些事情,所以大家有什么点子和建议,都可以联系作者,多交流交流嘛。

后续会公开所有数据,大家自己可以弄着玩玩吧。

我们太年轻,以致都不知道以后的时光,竟然那么长,长得足够让我们把一门技术研究到顶峰,乱花渐欲迷人眼,请不要忘了根本好吗。

生活总是让我们遍体鳞伤,但到后来,那些受伤的地方一定会变成我们最强壮的地方。 —海明威 《永别了武器》


本文章采用 知识共享署名2.5中国大陆许可协议 进行许可,欢迎转载,演绎或用于商业目的。


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux/Unix命令行 - 关于时间和时间戳的故事

发表于 2017-12-14
时间是一个神奇的东西,但我们却仅仅用一个数字来代表它

前话

在日常开发中,长期是在从事后端服务器开发,也避免不了会经常和时间打交道,例如:

  • 玩家的首次登录时间
  • 玩家的最后登录时间
  • 活动的开启时间和结束时间

而这些时间在程序中的表示就是用一个数字,我们把这个数字称为时间戳(Timestamp)。更多的情况下,我们是用一个整型数字来表示这个时间戳。

每一个不同的整型数字都有不同的含义,他们都代表一个独一无二的时间,例如:

  • 0 : 代表 1970年的元旦节(1970.1.1 00:00:00 UTC)
  • 60 : 代表 1970年元旦节刚过一分钟(1970.1.1 00:00:60 UTC)
  • 3600 : 代表 1970年元旦节过去一个小时了(1970.1.1 01:00:00 UTC)
  • 43200 : 代表 1970年元旦节午餐时间到了(1970.1.1 12:00:00 UTC)
  • 86400 : 代表 1970年元旦节过完了(1970.1.2 00:00:00 UTC)

结合上面的例子,对时间戳(Timestamp)的定义就清晰明了了,时间戳是一个数字,这个数字代表的是从1970年元旦节(1970.1.1 00:00:00)所经过(流逝)的秒数。

明确了这个概念,如果我们再看到一个数字,并且知道它就是代表一个时间戳,那我们应该会迫不及待的想把它转换成它代表的时间。

中话

其实我只是想记录一下Linux/Unix系统下关于date命令行工具的使用指南,不料却回忆了一下关于时间戳的简要概念。

事情的由来是,在很长一段时间内,我在拿到一个时间戳并且想看看这个时间戳究竟是具体的哪一天哪一年甚至于哪一分哪一秒的时候(通常在程序出现问题需要定位和时间有关的BUG的时候)。我都会习惯性的打开某搜索引擎然后输入时间戳这个关键词进行搜索,然后熟练的点击搜索结果的第一条Unix时间戳转换工具,直到某一天我觉得要纠正掉这个不好的习惯,我才开始试着去使用date这个Linux/Unix系统标配命令行工具去完成这个日常操作。哈哈,只能略表惭愧,我居然没有把这个这么好用的网站给收藏到浏览器的顶部栏!

正题

时间戳 -> 日期

和往常一样,我在调试的过程中发现了一个可疑的时间戳,它的数值是1506787200,我需要知道这个时间戳是对应的哪个具体日期,所以我又熟练的打开了某搜索引擎输入了时间戳……..

其实我这次是打开了Mac系统终端(Terminal),然后输入了如下命令:

1
复制代码date -r 1506787200

这个命令以最快的速度也给予了我准确的反馈输出:

从这一刻起,我感觉我自己终于摆脱了我的坏习惯,所以我觉得有必要多操作几次以达到强化的目的,于是我又熟练的依次输入了如下的命令:

1
2
3
4
5
复制代码date -r 0
date -r 60
date -r 3600
date -r 43200
date -r 86400

这些命令如饥似渴的以最快的速度分别的给予了我准确的反馈输出:

从这一刻起,我感觉我自己已经熟练掌握了date -r的奥义,就在我欣喜如狂之际,我的旁光突然扫描到date -r 0这个命令的输出: 1970年 1月 1日 星期四 08时00分00秒 CST,为什么是早上8时,和我在前话中提到的0代表1970年1月1日0时0分0秒有些出入呢。

其实这是一个误会,时间戳:0确实是代表1970年1月1日0时0分0秒,但是是代表零时区(UTC+0)的1970年1月1日0时0分0秒,还好我也是学过地理的,知道我们伟大的祖国幅员辽阔,地大物博,光时区就横跨了5个,而且全国统一使用首都北京所在时区东八区(UTC+8)的时间。

因为我的电脑系统设置的时区是东八区的缘故,这个date命令在默认情况下也是根据系统的时区优先显示所在时区的换算后的本地时间。以至于出现刚才的情况,虚惊一场。

为了验证刚才的推论,我打算重新操作一次,并且加上了一个神秘的参数-u,这个参数的作用很简单,就是以零时区(UTC+0)为标准输出时间,输入的命令如下:

1
2
3
4
5
复制代码date -u -r 0
date -u -r 60
date -u -r 3600
date -u -r 43200
date -u -r 86400

这些命令欣慰的以最快的速度分别的给予了我预料中的反馈输出:

从这一刻起,虽然我感觉到我的功力再次上了一个台阶,但在这个时候我觉得我应该稍微停顿一下我的脚步,来追忆一下往昔的时光。

很快我就进入了状态,回想起1998年的那个夏天,7月13日的凌晨3点,法兰西世界杯决赛法国VS巴西,一个让多少人睡不着觉的夜晚,但却是我第一次尝试在这么晚的时间或者说那么早的时间去看一场足球比赛,是因为在这之前我还一直是一名作息时间规律的小学生。回忆到此,我觉得我应该做点什么,我居然想查看一下那个快20年之前的夜晚的时间戳!

日期 -> 时间戳

于是我飞快的将这个熟悉又遥远的日期输入到命令中:

1
复制代码date -j 071303001998

命令给予了一个输出:

1
2
复制代码1998年 7月13日 星期一 03时00分00秒 CST
#法兰西世界杯决赛开始的北京时间

上面的命令中,我输入的是date -j 071303001998,其中的-j参数代表的是不要将他后面的那个日期字符串设置为当前系统时间,而只是将它以更友好的1998年 7月13日 星期一 03时00分00秒 CST这种形式输出。简单点说也就是如果我不加-j参数,那么date 071303001998命令会直接把我的电脑系统的当前时间设置为1998年那个夏天的7月13日凌晨3点,而不是在屏幕上输出那个时间。

但是我提供的071303001998这个字符串其实就是月日时分年的格式,07是7月,13是13日,03是凌晨3点,00是0分,1998是什么还用说么,我知道准确的日期,然后输入到这个date -j 071303001998命令中,显然不是为了让它输出告诉我1998年 7月13日 星期一 03时00分00秒 CST这个只是更好读的同样的日期,我是出于强烈的好奇想要知道这个伟大的时间的时间戳,所以我必须还要动点手脚:

1
复制代码date -j 071303001998 +%s

这次的命令只输出了一个数字,这个数字就是我想知道的那个时间戳:

1
复制代码900270000

得到了时间戳,我又迫不及待的使用如下命令:

1
复制代码date -r 900270000

命令再一次的快速输出了:

1
复制代码1998年 7月13日 星期一 03时00分00秒 CST

经过这一些列的操作,我不光学习到了新的参数-j,还再次温故了-r参数的用法,更重要的是我完成了对过往美好时光的追忆,可谓一石三鸟,大快人心。

格式化

前面的命令虽然用得很溜了,但是它们的输出都略显冗长,虽然很智能的根据我是中文用户给予了中文输出的最高待遇,但我觉得很普通~ 我现在迫切的希望改变它的输出的格式以彰显我独特的个性与品味,于是我又开始了我的操作,我现在显然还沉浸在刚在的追忆中,所以我还是打算用900270000这个代表1998年夏天的7月13日凌晨3点整的这个有特殊意义的时间戳来完成我的操作:

1
复制代码date -r 900270000 +%Y

它给予了我一个简单输出作为回应:

1
复制代码1998

我有些小激动,仿佛找到一些窍门,继续输入:

1
复制代码date -r 900270000 +%m

它再次给予了我一个更简洁的回应:

1
复制代码07

我更加激动,毫不犹豫的输入:

1
复制代码date -r 900270000 +%d

它果然没有让我失望,给予了我预料中的回应:

1
复制代码13

经过三次尝试,我尝到了成功的滋味,我觉得这个时候应该乘胜追击,于是我决定将%Y,%m,%d放在一起,我的第六感告诉我,我会再次获得成功:

1
复制代码date -r 900270000 +%Y%m%d

结果显而易见,年月日一起出现在了屏幕上:

1
复制代码19980713

我还不满足,我想获取更多的成就感,我想输出的格式更加的国际化,我继续尝试:

1
复制代码date -r 900270000 +%m/%d/%Y

结果达到我的初步要求:

1
复制代码07/13/1998

我依然不满足,因为我想起了王菲和那英所唱的那首红遍大江南北的歌曲《相约98》,我觉得我也要显示98而不是1998,在那个年代,没有人会说1998年,所以我再次摸索并尝试:

1
复制代码date -r 900270000 +%m/%d/%y

大写变小写,腐朽化神奇:

1
复制代码07/13/98

我果然还是不满足,心里想着如果能在后面附加上具体的时间那我应该就会收手了,于是我又一气呵成的输入了如下命令:

1
复制代码date -r 900270000 +%m/%d/%y %H:%M:%S

心里想着就快完成这次愉快的操作之旅时,现实却让我尝到了失败的味道:

1
复制代码date: illegal time format

在最后关头居然发生了意外着实让我有些不太爽快,但是凭借我大学英语四级的扎实基础,我还是从报错信息中领悟出了一些端倪,再结合我多年的工作经验,我感觉应该是我新增加的那段为了输出时分秒的格式字符串%H:%M:%S所造成的。我隐约感觉到应该是它之前的那个空格导致了date命令在读取的时候误以为它们是两个独立的字符串。这种时候,按照惯例应该需要用传说中的双引号""将它们包围在一起,以表示它们是一个整体。于是我重新整理了命令:

1
复制代码date -r 900270000 "+%m/%d/%y %H:%M:%S"

这次终于可以收工了,完美的输出,完美的夜晚,再也回去不的98:

1
复制代码07/13/98 03:00:00

做个总结

常用命令行

  • date
    默认格式输出当前日期。
  • date +%Y%m%d%H%M%S
    自定义格式%Y%m%d%H%M%S输出当前日期
  • date -r 900270000
    默认格式输出时间戳:900270000对应日期
  • date -r 900270000 +%Y%m%d%H%M%S
    自定义格式%Y%m%d%H%M%S输出时间戳:900270000对应日期
  • date -j 071303001998
    默认格式输出日期字符串:071303001998对应的日期
  • date -j 071303001998 +%Y%m%d%H%M%S
    自定义格式%Y%m%d%H%M%S输出日期字符串:071303001998对应的日期
  • date -j 071303001998 +%s
    自定义格式%s输出日期字符串:071303001998对应的日期的时间戳

格式字符串含义

注意大小写代表完全不同的含义

  1. %Y : 年(Year)
  2. %y : 年(year)后两位
  3. %m : 月(month)
  4. %d : 日(day)
  5. %H : 时(Hour)
  6. %M : 分(Minute)
  7. %S : 秒(Second)
  8. %s : 时间戳(stamp)

日期字符串默认格式

月日时分年(07 13 03 00 1998)

那些逝去的时间戳

  • 900270000
  • 1157040000
  • 967737600
  • 778348800
  • 555087600
  • 1430136000
  • 1461758400
  • 1493222400
  • 1277913600

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kaggle HousePrice (前15%),(2

发表于 2017-12-14

关键词:回归评估,整体检验,显著性,多重共线性,奇异矩阵

ftest, ttest, significance level, R2, multi_col linearity,singular matrix

Kaggle, 统计,机器学习,python

预计阅读时间: 20分钟,后15分钟,假定读者已经拥有相关的统计检验背景知识。

本文目标:

通过比较,引入传统的统计方法(上古魔法),打开数据集的黑盒子。探讨如下方法:

  1. 检验训练集和测试集是否相同分布。相同分布,是统计方法和机器学习的共同前提。 这可以帮助预判后面的机器学习的训练,调参和stacking是否有意义?
  2. 统计检验发现的概率(p value)帮助做feature selection辅助。
  3. 检查变量间是否存在共线性关系(奇异矩阵,不满秩) ? 后期机器学习,或者预处理,应该采用什么样的方式正则化处理? 例如:
  • 直接用PCA降维。
  • 是否需要采用Normalzier来正则化处理
  • Lasso(L1)还是Ridge(L2),
  • XGBoost,lightGBM应该怎么结合L1,L2

在知乎和Python中文社区发表了两篇经验分享文章,都是关于在Kaggle House Price的 文章,着重讲的是如何用搭积木的方式,稳定的将成绩提高。已经发表了两篇篇文章如下:

Kaggle HousePrice : LB 0.11666(排名前15%), 用搭积木的方式(1.原理)

Kaggle HousePrice : LB 0.11666(前15%), 用搭积木的方式(2.实践-特征工程部分)

原计划在第二篇文章收集满100个赞后发表第三篇文章,即:训练,调参和Ensemble。 尽管还没有100赞。 但是收到了许多呼友关注。 我在阅读呼友关注的其他文章时,也受到了挺多启发。 其中最有意思的就是统计(上古魔法)VS机器学习(开上帝视角)vs的讨论。

统计-上古魔法

机器学习-上帝视角

知乎和一些书籍上都有对比统计方法和机器学习方法,非常精彩。品味这些文章和书籍后,我的理解是:

  • 两者相同之处: 针对已有的数据,推导出来理解和认知。
  • 两者不同之处:
  • 统计方法就像上古魔法。 为啥:
  • 数据很少,数据很宝贵,
  • 计算能力也能昂贵。
  • 主要靠神秘的魔法师和神秘的魔法,即统计学家,统计和概率学。
  • 统计学家,开山鼻祖就是来自北方冰雪之国的Kolmogorov马尔可夫。

Kolmogorov马尔可夫

  • 统计和概率的基础:假设,中心极限定理,大数定理等。
  • 机器学习就像开了上帝视角的游戏玩家。为啥,通常:
  • 数据又多又广(足够多的话,就好比开了上帝视角的游戏玩家)
  • 训练数据可能有数百万条,甚至更多。例如:Kaggle 的Bosche 生产线优化案例,解压后数据文件超过了60G, 数据记录约5百万条(注:Dream competition 之一,可惜对机器内存,和算力要求太高。 Kaggle上的许多选手都只采用了部分数据抽样来作预测)
  • 训练数据(sample)足够广,甚至可以作为总体(population)来看待。
  • 上面这两点都成立的话,这简直就是游戏中的上帝视角。 所有尽在掌握了。
  • 计算能力便宜。云计算和GPU,TPU让计算机时不再成为约束。

等等,扯了半天。这和Kaggle HousePrice 2.实践-特征工程部分文章有啥关系? 

为啥在浪费时间,浪费口水,扯上面的东西?

为啥在浪费时间,浪费口水,扯上面的东西?

为啥在浪费时间,浪费口水,扯上面的东西?

答案是,

在House Price 机器学习的优势并不显著呀!

首先,数据机不多也不广.上帝视角没有开

训练集只有1460条记录,测试集和训练集几乎相等。上帝


其次,计算能力也不够。 自费玩Kaggle的限制。 文章第一篇,我说过参加比赛用的机器是阿里云-最低配版本(单核1Gcpu,1G内存)。GradientBoost,这类算法耗时太长,基本就不用。甚至传说中的XGBoost神器,也只是参考使用。在(n_estimator)小于3000时,RMSE成绩太差。大于3000后,计算单个Pipe就要用上0.5到1个小时。 更不要说,Stacking 和Ensemble时候的CV叠加时间,N小时。N>2。

那么,

为什么不用统计方法来看看?

为什么不用统计方法来看看?

为什么不用统计方法来看看?

说干就干。

应该是如下几个步骤:

1。检验训练集和测试集是否来自同一个分布?  如果不是,就洗洗睡吧。 统计方法或者机器学习没有意义的。如果是同一个分布,不能拒绝学习时有意义的这一假设。

上面的话比较拗口,简单说来。就好比,拿乐高得宝积木来玩普通乐高积木。两者虽然都是积木,但是大小尺寸不一样。"通常"不能直接一起玩。


2.用统计方法,来看看(采用经典二乘法)下面这些情况。

  回归的整体结果是否有意义(Ftest) 

  回归的数据集中的变量(Xi)是否有贡献(Ttest) 

  回归的可预测性R2(adjusted R2)高低

  回归的数据集中的变量(Xi)是否存在多重共线性(multicollinearity)或者说是否是奇异矩阵(Singular Matrix)

  存在多重共线性(或者说奇异矩阵)怎么办?  

好吧,首先来看看检验训练集和测试集是否来自同一个分布?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
python复制代码#训练集与测试集是否同分布KS检验
#假设H0: 训练,测试数据来自于同一个总体(分布),小于alpha则拒绝假设H0,大于alph则不能拒绝。
#H0: 相关变量将用于加入训练
#H1: 相关变量将从训练集中移出
#scipy.stats.ks_2samp
#a two-sided test for the null hypothesis that\
# 2 independent samples are drawn from the same continuous distribution.

import numpy as np
import pandas as pd
from scipy import stats
train = pd.read_csv("train.csv.gz")
test = pd.read_csv("test.csv.gz")
alpha =0.05 #设显著水平为0.05,

cols = train.drop(["SalePrice"],axis=1).select_dtypes(include = [np.number]).columns
cols_differ = {"ksvalue":[],"pvalue":[]}
for col in cols:
pvalue = None
ksvalue = None
ksvalue, pvalue = stats.ks_2samp(train[col],test[col])
cols_differ["ksvalue"].append(ksvalue)
cols_differ["pvalue"].append(pvalue)

KStest_df = pd.DataFrame(cols_differ,columns=["ksvalue","pvalue"],index=cols).sort_values(by="pvalue")
KStest_df["Same_distribution"] = KStest_df.pvalue>alpha
print(KStest_df["Same_distribution"].head())

Id False
2ndFlrSF True
GrLivArea True
TotRmsAbvGrd True
TotalBsmtSF True
Name: Same_distribution, dtype: bool

从上面的KS test 可以看出,除了Id 以外的Feature 列都通过了Kstest (预设显著性水平为0.05,两侧检验)

看来,训练集和测试集是相关。把Id列删除了,就能玩。

2.用统计方法,来看看(采用经典二乘法)下面这些情况。

  回归的整体结果是否有意义(Ftest) 

  回归的数据集中的变量(Xi)是否有贡献(Ttest) 

  回归的可预测性R2(adjusted R2)高低

  回归的数据集中的变量(Xi)是否存在多重共线性(multicollinearity)或者说是否是奇异矩阵(Singular Matrix)

上面列了许多问题,其实用Python测试起来非常简单。pandas+Statsmodel就可以搞定。 我在Kaggle HousePrice : LB 0.11666(前15%), 用搭积木的方式(2.实践-特征工程部分)一文中,最后一个test函数中已经写好了这部分内容。只要把注释去掉,就可以解锁新的姿势。 哈哈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码def pipe_r2test(df):
import statsmodels.api as sm
import warnings
warnings.filterwarnings('ignore')
print("****Testing*****")
train_df = df

ntrain = train_df["SalePrice"].notnull().sum()
train = train_df[:ntrain]
X_train = train.drop(["Id","SalePrice"],axis =1)
Y_train = train["SalePrice"]
  # 此处删除无关的代码

result = sm.OLS(Y_train, X_train).fit()


result_str= str(result.summary())
results1 = result_str.split("\n")[:20]
#此处解锁20行信息,用来解释
for result in results1:
print(result)
print('*'*20)
return df

其他函数不变。下面用搭积木的方式来生产两个预处理文件并比较测试。

注:pipe_PCA是一个新函数。这两天刚刚做出来的特征函数之一。基于statsmodels库,当然sklearn 和scipy 也有同样的库,我只是选用了其中的一个方法而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
prolog复制代码pipe_basic = [pipe_fillna_ascat,pipe_drop_cols,\
pipe_drop4cols,pipe_outliersdrop,\
pipe_extract,pipe_bypass,pipe_bypass,\
pipe_log_getdummies, \
pipe_bypass,
pipe_drop_dummycols,pipe_bypass,
pipe_r2test,pipe_bypass]
pipe_basic_pca = [pipe_fillna_ascat,pipe_drop_cols,\
pipe_drop4cols,pipe_outliersdrop,\
pipe_extract,pipe_bypass,pipe_bypass,\
pipe_log_getdummies, \
pipe_bypass,
pipe_drop_dummycols,pipe_PCA,
pipe_r2test,pipe_bypass]

pipes=[pipe_basic,pipe_basic_pca]
tmp = None

for i in range(len(pipes)):
print("*"*10,"\n")
pipe_output=pipes[i]
output_name ="_".join([x.__name__[5:] for x in pipe_output if x.__name__ is not "pipe_bypass"])
output_name = "PIPE_" +output_name
print(output_name)
tmp =(combined.pipe(pipe_output[0])
.pipe(pipe_output[1])
.pipe(pipe_output[2])
.pipe(pipe_output[3])
.pipe(pipe_output[4])
.pipe(pipe_output[5])
.pipe(pipe_output[6])
.pipe(pipe_output[7])
.pipe(pipe_output[8])
.pipe(pipe_output[9])
.pipe(pipe_output[10])
.pipe(pipe_output[11])
.pipe(pipe_output[12],output_name)
)

先上结论:

回归的整体结果有意义。Ftest 的概率=0,拒绝零假设(即:回归模型直线斜率=0)

  • Basic 小火车(Pipe测试):有意义,Prob (F-statistic): 0.00
  • Basic_PCA小货车(Pipe测试):有意义,Prob (F-statistic): 0.00

回归的数据集中较多变量(Xi)没有贡献

(P>|t|列,是计算出来的Xi的贡献概率。 假定alpha=0.05,许多列都大于0.05.不能拒绝零假设)零假设,是该Xi的系数(coe) =0. 不能拒绝零假设,意味着很可能有没有这个Xi特征变量,对于回归来说都没有关系。

变量( Xi)没有贡献,往往意味着可以直接从模型中删除,这样可以提高计算的速度和降低噪音。不过如何删除就是另一个特征工程话题。可以通过feature selection 或者PCA方式。 下文小火车2(Basic_PCA)就展示PCA进行了正交处理的功能。

例如:

  • Basic 小火车(Pipe测试):绝大多数Xi(变量)变量没有贡献
  • BsmtCond (0.9597,概率非常大)没有贡献,
  • BsmtFinSF2 (0.0515,概率大于alph),没有通过t-test, 也认为没有贡献。
  • Basic_PCA小火车:前10的Xi,有贡献,排名靠后的Xi 没有贡献。
  • comp_000~comp_007(只显示了前7个变量) (0,概率很小)拒绝零假设,有贡献。
  • comp_218(0.1159,概率大于alph),没有通过t-test, 也认为没有贡献。

回归的可预测性R2(adjusted R2)一样。

为了展示方便,小火车Basic_PCA管道没有进一步处理,故两者Adjusted R2 一样。

  • Basic 小火车(Pipe测试): 0.937
  • Basic_PCA小火车: 0.937

回归的数据集中的变量(Xi)存在多重共线性(multicollinearity)是奇异矩阵(Singular Matrix)

Statsmodel 提供了Condition number 作为共线性和奇异矩阵的判断标准。 这两个处理后的数据都有非常严重的共线性。

The condition number is large (1e+18)

The condition number is large (1e+16)

从上面可以看出,通过传统的统计方法(上古魔法),可以打开数据集的黑盒子。可以达到如下目标:

1。检验训练集和测试集是否相同分布。相同分布,是统计方法和机器学习的共同前提。 这可以帮助预判后面的机器学习的训练,调参和stacking是否有意义?

2。检查变量间是否存在共线性关系(奇异矩阵,不满秩) ? 后期机器学习,或者预处理,应该采用什么样的方式正则化处理?

例如:

  • 直接用PCA降维。
  • 是否需要采用Normalzier来正则化处理
  • Lasso(L1)还是Ridge(L2),
  • XGBoost,lightGBM应该怎么结合L1,L2
  1. feature 选择时的两种方法机器学习参数(lasso, randomforest) 还是用统计检验发现的概率(p value)

输出摘要:

1.小火车 - pipe_basic 测试结果

Results: Ordinary least squares

Model: OLS Adj. R-squared: 0.937
Dependent Variable: SalePrice AIC: -2375.2359
Date: 2017-12-11 14:27 BIC: -1249.5690
No. Observations:
1458 Log-Likelihood: 1400.6
Df Model: 212 F-statistic: 103.5
Df Residuals: 1245 Prob (F-statistic): 0.00
R-squared: 0.946 Scale: 0.010039


Coef. Std.Err.
t P>|t| [0.025 0.975]


1stFlrSF 0.0545 0.0453 1.2037 0.2290 -0.0343 0.1434
2ndFlrSF 0.0099 0.0064 1.5336 0.1254 -0.0028 0.0225
3SsnPorch 0.0036 0.0044 0.8201
0.4123 -0.0050 0.0123
BedroomAbvGr -0.0086 0.0060 -1.4314 0.1526 -0.0204 0.0032
BsmtCond -0.0015 0.0298 -0.0505 0.9597 -0.0599 0.0569
BsmtFinSF1 0.0052 0.0024 2.1491 0.0318 0.0004 0.0099
BsmtFinSF2 -0.0055 0.0028 -1.9492 0.0515 -0.0110
0.0000
BsmtFinType1 -0.0040 0.0033 -1.2350 0.2171 -0.0104 0.0024
SaleType_ConLD 0.1221 0.0391 3.1193 0.0019 0.0453 0.1989
SaleType_Oth 0.0685 0.0620 1.1049 0.2694 -0.0531 0.1901


Omnibus:
367.835 Durbin-Watson: 1.937
Prob(Omnibus): 0.000 Jarque-Bera (JB): 2577.551
Skew: -0.986 Prob(JB): 0.000
Kurtosis: 9.208 Condition No.: 1258412771503278080
======================================================================
*
The condition number is large (1e+18).
The condition number is large (1e+16).

2.小火车 - pipe_basic_pca 测试结果

Results: Ordinary least squares

Model: OLS Adj. R-squared: 0.937
Dependent Variable: SalePrice AIC: -2374.1451
Date: 2017-12-11 14:27 BIC: -1222.0541
No. Observations:
1458 Log-Likelihood: 1405.1
Df Model: 217 F-statistic: 101.4
Df Residuals: 1240 Prob (F-statistic): 0.00
R-squared: 0.947 Scale: 0.010019


Coef. Std.Err. t
P>|t| [0.025 0.975]


comp_000 -304.5691 4.4819 -67.9555 0.0000 -313.3620 -295.7761
comp_001 50.8477 0.7465 68.1139 0.0000 49.3832 52.3123
comp_002 3.7843 0.2682
14.1078 0.0000 3.2580 4.3105
comp_003 -798.4617 12.1768 -65.5722 0.0000 -822.3511 -774.5722
comp_004 -547.5712 8.2157 -66.6491 0.0000 -563.6894 -531.4529
comp_005 -989.4200 15.0826 -65.6002 0.0000 -1019.0102 -959.8298
comp_006 -2017.9360
30.6820 -65.7693 0.0000 -2078.1305 -1957.7416
comp_007 -190.4264 2.8626 -66.5213 0.0000 -196.0425 -184.8102
comp_218 0.3263 0.2075 1.5731 0.1159 -0.0807 0.7334
comp_219 -0.3834 0.3258 -1.1768 0.2395 -1.0225 0.2558


Omnibus:
357.382 Durbin-Watson: 1.941
Prob(Omnibus): 0.000 Jarque-Bera (JB): 2423.551
Skew: -0.963 Prob(JB): 0.000
Kurtosis: 9.015 Condition No.: 10685794842663918
===================================================================
*
The condition number is large (1e+16). This might indicate

最后,本人水平有限,试图下列内容用短短一篇文章表达处理。

  • 商务与经济统计,
  • Python for probility, statistics, and machine learning
  • 知乎中统计和机器学习的讨论

能力与愿望可能不太匹配。文章必然有大量的欠缺之处,或者逻辑跳跃不连贯,甚至基本概念错误。 期望知乎的朋友多多指点。

分享是对自己最好的投资。

相关链接:

Kaggle HousePrice : LB 0.11666(前15%), 用搭积木的方式(2.实践-特征工程部分)

Kaggle HousePrice : LB 0.11666(排名前15%), 用搭积木的方式(1.原理)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot集成Redis实现缓存处理(Spring

发表于 2017-12-14

第一章 需求分析

计划在Team的开源项目里加入Redis实现缓存处理,因为业务功能已经实现了一部分,通过写Redis工具类,然后引用,改动量较大,而且不可以实现解耦合,所以想到了Spring框架的AOP(面向切面编程)。
开源项目:github.com/u014427391/…
欢迎star(收藏)

第二章 SpringBoot简介

Spring框架作为JavaEE框架领域的一款重要的开源框架,在企业应用开发中有着很重要的作用,同时Spring框架及其子框架很多,所以知识量很广。 SpringBoot:一款Spring框架的子框架,也可以叫微框架,是2014年推出的一款使Spring框架开发变得容易的框架。学过Spring框架的都知识,Spring框架难以避免地需要配置不少XMl,而使用SpringBoot框架的话,就可以使用注解开发,极大地简化基于Spring框架的开发。SpringBoot充分利用了JavaConfig的配置模式以及“约定优于配置”的理念,能够极大的简化基于SpringMVC的Web应用和REST服务开发。

第三章 Redis简介

3.1 Redis安装部署(Linux)

Redis安装部署的可以参考我的博客(Redis是基于C编写的,所以安装前先安装gcc编译器):blog.csdn.net/u014427391/…

3.2 Redis简介

Redis如今已经成为Web开发社区最火热的内存数据库之一,随着Web2.0的快速发展,再加上半结构数据比重加大,网站对高效性能的需求也越来越多。 而且大型网站一般都有几百台或者更多Redis服务器。Redis作为一款功能强大的系统,无论是存储、队列还是缓存系统,都有其用武之地。

SpringBoot框架入门的可以参考我之前的博客:blog.csdn.net/u014427391/…

第四章 Redis缓存实现

4.1下面结构图

项目结构图:
这里写图片描述

4.2 SpringBoot的yml文件配置

添加resource下面的application.yml配置,这里主要配置mysql,druid,redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
复制代码spring:
datasource:

# 主数据源
shop:
url: jdbc:mysql://127.0.0.1:3306/jeeplatform?autoReconnect=true&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false
username: root
password: root

driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource

# 连接池设置
druid:
initial-size: 5
min-idle: 5
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
# Oracle请使用select 1 from dual
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
jpa:
database: mysql
hibernate:
show_sql: true
format_sql: true
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
#Jedis配置
jedis :
pool :
host : 127.0.0.1
port : 6379
password : password
timeout : 0
config :
maxTotal : 100
maxIdle : 10
maxWaitMillis : 100000

编写一个配置类启动配置JedisConfig.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码package org.muses.jeeplatform.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
//@ConfigurationProperties(prefix = JedisConfig.JEDIS_PREFIX )
public class JedisConfig {

//public static final String JEDIS_PREFIX = "jedis";

@Bean(name= "jedisPool")
@Autowired
public JedisPool jedisPool(@Qualifier("jedisPoolConfig") JedisPoolConfig config,
@Value("${spring.jedis.pool.host}")String host,
@Value("${spring.jedis.pool.port}")int port,
@Value("${spring.jedis.pool.timeout}")int timeout,
@Value("${spring.jedis.pool.password}")String password) {
return new JedisPool(config, host, port,timeout,password);
}

@Bean(name= "jedisPoolConfig")
public JedisPoolConfig jedisPoolConfig (@Value("${spring.jedis.pool.config.maxTotal}")int maxTotal,
@Value("${spring.jedis.pool.config.maxIdle}")int maxIdle,
@Value("${spring.jedis.pool.config.maxWaitMillis}")int maxWaitMillis) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMaxWaitMillis(maxWaitMillis);
return config;
}


}

4.3 元注解类编写

编写一个元注解类RedisCache.java,被改注解定义的类都自动实现AOP缓存处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码package org.muses.jeeplatform.annotation;

import org.muses.jeeplatform.common.RedisCacheNamespace;

import java.lang.annotation.*;

/**
* 元注解 用来标识查询数据库的方法
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
// RedisCacheNamespace nameSpace();
}

JDK 5提供的注解,除了Retention以外,还有另外三个,即Target 、Inherited 和 Documented。基于这个,我们可以实现自定义的元注解 我们设置RedisCache基于Method方法级别引用。

1.RetentionPolicy.SOURCE 这种类型的Annotations只在源代码级别保留,编译时就会被忽略
2.RetentionPolicy.CLASS 这种类型的Annotations编译时被保留,在class文件中存在,但JVM将会忽略
3.RetentionPolicy.RUNTIME 这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用.

4.4 调用JedisPool实现Redis缓存处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
复制代码package org.muses.jeeplatform.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.Resource;
@Component("redisCache")
public class RedisCache {

@Autowired
private JedisPool jedisPool;

private JedisPool getJedisPool(){
return jedisPool;
}

public void setJedisPool(JedisPool jedisPool){
this.jedisPool = jedisPool;
}

/**
* 从Redis缓存获取数据
* @param redisKey
* @return
*/
public Object getDataFromRedis(String redisKey){
Jedis jedis = jedisPool.getResource();
byte[] byteArray = jedis.get(redisKey.getBytes());

if(byteArray != null){
return SerializeUtil.unSerialize(byteArray);
}
return null;
}

/**
* 保存数据到Redis
* @param redisKey
*/
public String saveDataToRedis(String redisKey,Object obj){

byte[] bytes = SerializeUtil.serialize(obj);

Jedis jedis = jedisPool.getResource();

String code = jedis.set(redisKey.getBytes(), bytes);

return code;
}


}

对象序列化的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码package org.muses.jeeplatform.cache;

import java.io.*;

public class SerializeUtil {

/**
* 序列化对象
* @param obj
* @return
*/
public static byte[] serialize(Object obj){
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try{
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);

oos.writeObject(obj);
byte[] byteArray = baos.toByteArray();
return byteArray;

}catch(IOException e){
e.printStackTrace();
}
return null;
}

/**
* 反序列化对象
* @param byteArray
* @return
*/
public static Object unSerialize(byte[] byteArray){
ByteArrayInputStream bais = null;
try {
//反序列化为对象
bais = new ByteArrayInputStream(byteArray);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();

} catch (Exception e) {
e.printStackTrace();
}
return null;
}

}

这里记得Vo类都要实现Serializable 例如菜单信息VO类,这是一个JPA映射的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
复制代码package org.muses.jeeplatform.core.entity.admin;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

/**
* @description 菜单信息实体
* @author Nicky
* @date 2017年3月17日
*/
@Table(name="sys_menu")
@Entity
public class Menu implements Serializable {

/** 菜单Id**/
private int menuId;

/** 上级Id**/
private int parentId;

/** 菜单名称**/
private String menuName;

/** 菜单图标**/
private String menuIcon;

/** 菜单URL**/
private String menuUrl;

/** 菜单类型**/
private String menuType;

/** 菜单排序**/
private String menuOrder;

/**菜单状态**/
private String menuStatus;

private List<Menu> subMenu;

private String target;

private boolean hasSubMenu = false;

public Menu() {
super();
}

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
public int getMenuId() {
return this.menuId;
}

public void setMenuId(int menuId) {
this.menuId = menuId;
}

@Column(length=100)
public int getParentId() {
return parentId;
}

public void setParentId(int parentId) {
this.parentId = parentId;
}

@Column(length=100)
public String getMenuName() {
return this.menuName;
}

public void setMenuName(String menuName) {
this.menuName = menuName;
}

@Column(length=30)
public String getMenuIcon() {
return this.menuIcon;
}

public void setMenuIcon(String menuIcon) {
this.menuIcon = menuIcon;
}

@Column(length=100)
public String getMenuUrl() {
return this.menuUrl;
}

public void setMenuUrl(String menuUrl) {
this.menuUrl = menuUrl;
}

@Column(length=100)
public String getMenuType() {
return this.menuType;
}

public void setMenuType(String menuType) {
this.menuType = menuType;
}

@Column(length=10)
public String getMenuOrder() {
return menuOrder;
}

public void setMenuOrder(String menuOrder) {
this.menuOrder = menuOrder;
}

@Column(length=10)
public String getMenuStatus(){
return menuStatus;
}

public void setMenuStatus(String menuStatus){
this.menuStatus = menuStatus;
}

@Transient
public List<Menu> getSubMenu() {
return subMenu;
}

public void setSubMenu(List<Menu> subMenu) {
this.subMenu = subMenu;
}

public void setTarget(String target){
this.target = target;
}

@Transient
public String getTarget(){
return target;
}

public void setHasSubMenu(boolean hasSubMenu){
this.hasSubMenu = hasSubMenu;
}

@Transient
public boolean getHasSubMenu(){
return hasSubMenu;
}

}

4.5 Spring AOP实现监控所有被@RedisCache注解的方法缓存

先从Redis里获取缓存,查询不到,就查询MySQL数据库,然后再保存到Redis缓存里,下次查询时直接调用Redis缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
复制代码package org.muses.jeeplatform.cache;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
* AOP实现Redis缓存处理
*/
@Component
@Aspect
public class RedisAspect {

private static final Logger LOGGER = LoggerFactory.getLogger(RedisAspect.class);

@Autowired
@Qualifier("redisCache")
private RedisCache redisCache;

/**
* 拦截所有元注解RedisCache注解的方法
*/
@Pointcut("@annotation(org.muses.jeeplatform.annotation.RedisCache)")
public void pointcutMethod(){

}

/**
* 环绕处理,先从Redis里获取缓存,查询不到,就查询MySQL数据库,
* 然后再保存到Redis缓存里
* @param joinPoint
* @return
*/
@Around("pointcutMethod()")
public Object around(ProceedingJoinPoint joinPoint){
//前置:从Redis里获取缓存
//先获取目标方法参数
long startTime = System.currentTimeMillis();
String applId = null;
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
applId = String.valueOf(args[0]);
}

//获取目标方法所在类
String target = joinPoint.getTarget().toString();
String className = target.split("@")[0];

//获取目标方法的方法名称
String methodName = joinPoint.getSignature().getName();

//redis中key格式: applId:方法名称
String redisKey = applId + ":" + className + "." + methodName;

Object obj = redisCache.getDataFromRedis(redisKey);

if(obj!=null){
LOGGER.info("**********从Redis中查到了数据**********");
LOGGER.info("Redis的KEY值:"+redisKey);
LOGGER.info("REDIS的VALUE值:"+obj.toString());
return obj;
}
long endTime = System.currentTimeMillis();
LOGGER.info("Redis缓存AOP处理所用时间:"+(endTime-startTime));
LOGGER.info("**********没有从Redis查到数据**********");
try{
obj = joinPoint.proceed();
}catch(Throwable e){
e.printStackTrace();
}
LOGGER.info("**********开始从MySQL查询数据**********");
//后置:将数据库查到的数据保存到Redis
String code = redisCache.saveDataToRedis(redisKey,obj);
if(code.equals("OK")){
LOGGER.info("**********数据成功保存到Redis缓存!!!**********");
LOGGER.info("Redis的KEY值:"+redisKey);
LOGGER.info("REDIS的VALUE值:"+obj.toString());
}
return obj;
}


}

然后调用@RedisCache实现缓存

1
2
3
4
5
6
7
8
9
10
复制代码/**
* 通过菜单Id获取菜单信息
* @param id
* @return
*/
@Transactional
@RedisCache
public Menu findMenuById(@RedisCacheKey int id){
return menuRepository.findMenuByMenuId(id);
}

登录系统,然后加入@RedisCache注解的方法都会实现Redis缓存处理
这里写图片描述

这里写图片描述

可以看到Redis里保存到了缓存

这里写图片描述

项目代码:github.com/u014427391/…,欢迎去github上star(收藏)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

(002)Spring 之 AOP

发表于 2017-12-14

概述

Spring的最终目的是简化应用开发。通俗的讲减少重复代码,少写代码达到相同的目的。面向切面编程(AOP, Aspect Oriented Programming)就是一种减重复代码方式。我们都知道JAVA是一门面向对象编程(OOP, Object Oriented Programming)语言,在java中将一个个功能模块抽象成一个个对象。这些对象通过一定的联系完成我们所看到的一个个应用,一个个服务。它的核心就是对象(Object)。

要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。

类应该是纯净的,不应含有与本身无关的逻辑。单一职责原则。

切面编程,可以带来代码的解耦;同时,切面编程也是需要执行代码的,增加了一些额外的代码执行量。因地制宜,使用AOP。

他山之石

  • 轻松理解AOP思想(面向切面编程):www.cnblogs.com/Wolfmanlq/p…
  • AOP那点事儿:面向切面编程:my.oschina.net/huangyong/b…
  • OOP的完美点缀—AOP之SpringAOP实现原理:www.cnblogs.com/chenjunping…
  • Spring系列之AOP: www.cnblogs.com/xiaoxi/p/59…

具象化理解

我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。

按照正常的逻辑,我们可以这么做。

第1版

这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。

第2版

同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。

第3版

这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。

红框处,就是面向切面编程。

理论

使用AOP之前,我们需要理解几个概念。

各个概念关系

连接点(Join Point)

所有可能的需要注入切面的地方。如方法前后、类初始化、属性初始化前后等等。

切点(Poincut)

需要做某些处理(如打印日志、处理缓存等等)的连接点。如何来指明一个切点?spring使用了AspectJ的切点表达式语言来定义Spring切面。

切点定义方式

多个表达式之间,可用“and” 、“or”、“not”做逻辑连接。

其中较为复杂的是execution。

execution使用方式

目前,Spring只支持方法的切点定义

通知(Advice)

定义在什么时候做什么事情。spring支持5种方法上的通知类型

支持的通知类型

切面(Aspect)

通知+切点的集合,定义在什么地方什么时间做什么事情。

引入(Introduction)

允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

目标(Target)

引入中提到的目标类,也就是要被通知的对象。也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

织入(Weaving)

把切面应用到目标对象来创建新的代理对象的过程。

spring中AOP实现原理

AOP的实现实际上是用的是代理模式。

代理

代理概念:简单的理解就是通过为某一个对象创建一个代理对象,我们不直接引用原本的对象,而是由创建的代理对象来控制对原对象的引用。

按照代理的创建时期,代理类可以分为两种。

  • 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
  • 动态代理:在程序运行时,运用反射机制动态创建而成,无需手动编写代码。动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java反射机制可以生成任意类型的动态代理类。

代理原理:代理对象内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。

spring使用的是动态代理。

动态代理之JDK

java jdk 本身支持动态创建代理对象。

jdk动态代理

java的动态代理,有个缺点,它只支持实现了接口的类。因此可以引入cglib(Code Generation Library)三方库支持类的代理。
对JDK动态代理有兴趣的可以参考一下:blog.csdn.net/u012410733/…

动态代理之cglib

cglib动态代理

对cglib库有兴趣可以参考一下:blog.csdn.net/danchu/arti…

样例

有这么一个需求,对某些接口做缓存或打印日志。有不想每个接口中都调用缓存方法或打印日志方法。可以这么来做。
代码:

  • 定义动物抽象接口:Animal
  • 定义具体动物猫:Cat
  • 定义bean扫描配置类:AnimalConfig
  • 定义缓存注解:Cache
  • 定义缓存切面:CacheAspect
  • 定义日志切面:LogAspect
  • 定义测试入口类:App

1. Animal接口

1
2
3
4
5
6
复制代码package bean;

public interface Animal {

public String sayName(String name, Integer num);
}

2. Cat类

1
2
3
4
5
6
7
8
9
10
11
12
复制代码package bean;

import org.springframework.stereotype.Component;

@Component
public class Cat implements Animal {

@Cache(60)
public String sayName(String name, Integer num) {
return "this is cat " + name + "," + num;
}
}

3. AnimalConfig spirng配置扫描类

1
2
3
4
5
6
7
8
9
10
11
复制代码package bean;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AnimalConfig {
}

4.Cache注解(与CacheAspect配合使用)

1
2
3
4
5
6
7
8
9
10
11
复制代码package bean;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cache {
int value() default 0;
}

5.CacheAspect 缓存切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码package bean;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
@Aspect
@Order(1000)
public class CacheAspect {

// 定义切入点:带有Cache注解的方法
@Pointcut("@annotation(Cache)")
private void cache(){}

// 临时存储区
private static Map<String, Object> cacheList = new LinkedHashMap<String, Object>();

// 定义环绕通知,处理接口/方法添加缓存
@Around("cache()")
private Object cacheAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
Object object = proceedingJoinPoint.getTarget();
Object[] args = proceedingJoinPoint.getArgs();
String className = object.getClass().getName();
MethodSignature signature = (MethodSignature)proceedingJoinPoint.getSignature();
Method method = signature.getMethod();

// 组装cache key
String cacheKey = className + "_" + method.getName() + "_" + JSONObject.toJSONString(args);
if (cacheList.containsKey(cacheKey)){
System.out.println("data get cache");
return cacheList.get(cacheKey);
}else {
System.out.println("data get db");
Object result = proceedingJoinPoint.proceed();
cacheList.put(cacheKey, result);
return result;
}
}
}

6.LogAspect 日志切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码package bean;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Order(100)
public class LogAspect {
//定义切点:包含任意参数的、任意返回值、的公共方法sayName
@Pointcut("execution(public * *sayName(..))")
private void log(){}

//定义环绕通知:处理日志注入
@Around("log()")
private Object logAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object[] args = proceedingJoinPoint.getArgs();

System.out.println("before, params:" + JSONObject.toJSONString(args));
Object result = proceedingJoinPoint.proceed();
System.out.println("after, result:" + JSONObject.toJSONString(result));
return result;
}
}

7. APP 测试入口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码import bean.Animal;
import bean.AnimalConfig;
import bean.Cat;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {
public static void main(String[] args){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AnimalConfig.class);

Animal animal = applicationContext.getBean("cat", Cat.class);
String result = animal.sayName("rudytan", 12);
String result1 = animal.sayName("rudytan", 12);
String result2 = animal.sayName("rudytan", 12);
String result3 = animal.sayName("rudytan", 13);
String result4 = animal.sayName("rudytan", 13);
System.out.println(result);
}
}

说明:

  • @Cache为自定义注解(标记该方法需要缓存)
  • @EnableAspectJAutoProxy 为是否开启cglib动态代理
  • @Aspect为定义该类为切面类
  • @Order为当有多个切面类的时候,定义执行顺序,数值越大,约先执行。
  • @Pointcut定义当前函数为切点。
  • @Around定义环绕通知

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何愉快的使用 MQ - 详述各种功能场景 解耦 削峰填谷

发表于 2017-12-14

消息队列(MQ)是一种不同应用程序之间(跨进程)的通信方法。应用程序通过写入和检索出入列队的数据(消息)来通信,而无需通过专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用(Remote Procedure Call. RPC)的技术。排队指的是应用程序通过队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。这样天然的就实现了异步的目标。那么MQ还有哪些功能场景呢。下面逐一介绍。

解耦

解耦.png

解耦.png

MQ最直接的使用场景就是可以将两个系统进行解耦,比如我们的货款抵扣业务场景,用户生成订单发送MQ后立即返回,结算系统去消费该MQ进行用户账户金额的扣款。这样订单系统只需要关注把订单创建成功,最大可能的提高订单量,并且生成订单后立即返回用户。而结算系统重点关心的是账户金额的扣减,保证账户金额最终一致。这个场景里面还会涉及到重试幂等性问题,后面有介绍。

削峰填谷

还是以订单系统和结算系统场景为例,如果订单系统通过RPC框架来调用结算系统,在有高峰促销的情况下生成订单的量会非常大,而且由于生成订单的速度也非常快,这样势必会给结算系统造成系统压力,服务器利用率则会偏高,但在不是高峰的时间点订单量比较小,结算系统的服务器利用率则会偏低。对于结算系统来说就会出现下面这样的高峰波谷现象图。

削峰填谷.png

削峰填谷.png

那么如果通过MQ的方式,将订单存储到MQ队列中,消费端通过拉取的方式,并且拉去速度有消费端来控制,则就可以控制流量趋于平稳。这样对于结算系统来讲,就达到了削峰填谷的目的。或者说起到了流控的目标。接下来,我们介绍一下拉取方式。

拉取模式指用户在代码里主动调用pull方法,不需要在配置文件里面再配置<mq:listener />,拉取的速度由用户控制,调用一次拉取一次消息进行消费,这里要重视消费的速度如果消费性能下降一定会造成积压,因此用户自己启用多线程控制并行度以提高消费速度。
代码样例:

1
2
3
4
5
复制代码messageConsumer.start();
for (;;){
//手动拉取消息
messageConsumer.pull(topic,messageListener);
}

method: pull(String topic,MessageListener listener)
topic:指消费的主题名
listener:是一个回调对象,当pull拉取到消息后会主动调用listener.onMessage(),
与监听模式的区别是:监听模式由MQ客户端守护线程去不停的拉取消息进行消费,拉取模式由用户控制拉取的频率,不主动调用就不会消费消息。但是都不需要主动对消息进行确认。这种方式更适合写场景,保证最终结果落地即可,因为读是需要立即返回以免让用户长时间等待从而影响用户体验。

最终一致性

最终一致性.png

最终一致性.png

一致性问题分为强一致性、弱一致性、最终一致性。大多数互联网业务要求实现最终一致性。还是以订单系统和结算系统业务场景举例,订单系统创建成功一个订单后给用户返回的结果即是成功并明确告诉用户会从账户中扣除相应的金额。那么结算系统需要保持跟订单系统相同的状态即从用账户中实际扣除一致的金额。订单系统会涉及两个动作,一个是创建成功订单,一个是发送成功通知到MQ,我们就可以把这两个动作放入到一个本地事务中,要么成功要么失败。当一次发送MQ失败之后,可以结合定时任务进行补偿,这样可以保证生成订单的结果可以落地到mq的存储中。同样结算系统消费端依靠MQ重试机制一直发送消息,直到消费端最终确认扣款业务成功处理完成。这样我们通过消息落地加补偿,消费端从业务上面考虑重复消费的保障,也就是做好幂等性操作,利用MQ实现了最终一致性。

广播消费

MQ有两种消息模式一种是点对点模式,一种是发布/订阅模式(最常用的模式)。同时发布/订阅模式按照消费类型又可以分为集群消费和广播消费。大部分情况下我们使用的是集群消费。

集群消费:MQ发送任何一条消息,集群中只有一台服务器可随机消费到这条消息。如下图:

集群消费.png

集群消费.png

广播消费:MQ发送每一条消息,集群中的每一台服务器至少消费到一次。如下图:

广播消费.png

广播消费.png

广播消费举例:消息推送系统。首先某一个客户端与消息中心应用集群中的一台服务器建立长连接并将连接session信息保存到当前服务器内存中,集群在消费业务消息的时候,是不知道该客户端建立的长连接在哪一台服务器上面。这个时候通过广播消费,集群中的每一台服务器都可以消费到业务消息。在决定向用户推送通知之前会判断当前服务器内存中是否有该客户端的连接session信息,如果有则推送,进而客户端通过http协议拉取用户的消息实体。如果session信息不在当前服务器上面,则丢弃。如下图:

广播消费举例.png

广播消费举例.png

广播消费注意事项:
1、消费进度在消费端管理,比如默认会在主目录下创建offset文件夹,偏移量文件存储在offset目录下,出现重复的概率要大于集群消费。
2、MQ可以确保每条消息至少被每台消费方服务器消费一次,但是如果消费方消费失败,不会进入重试,因此业务方需要关注消费失败的情况。
3、由于广播消费消息不会进行确认,所以管理端上显示的积压数会一直不变,需要以出对数为准。

使用集群消费模拟广播

在发布/订阅模式中,如果是集群消费,那么一条消息只能被集群中的随机一台服务器消费到,如果我们有需要集群中的每台服务器消费到比如上面的消息推送的例子,我们使用广播消费来实现。但是广播消费有一些弊端比如不支持顺序消息,消费进度在客户端维护出现重复的几率要大于集群模式,广播模式下不能维护消费进度所以管理端上面的积压数一直保持不变,我们就必须以出队数为准,也就是不能够支持消息堆积的查询。如果要规避这些弊端,那么我们可以利用集群消费来模拟广播,在集群消费中,我们的每台服务器上面的消费APPID是相同的,如果要达到广播的效果,那么每台服务器上面的消费APPID保持不同就可以了。

模拟广播.png

模拟广播.png

重试之坑

重试之坑.png

重试之坑.png

MQ的重试功能可以保证数据结果最终得到处理,但同时也正因为有重试那么在业务处理的时候就需要格外注意幂等性的问题。比如货款抵扣业务,订单系统生成订单之后调用结算平台去扣除用户的账户金额。结算平台要根据流水号去计算,如果订单系统在调用结算平台的时候发生了网络异常,造成了结算平台实际上已经得到请求并且已处理。订单系统一侧认为发生异常需要重试,后续再发送到结算平台的订单就会造成重复扣款问题。所以流水号尤其要注意需要保证重试过程中每次发送的流水号是一致的,结算平台会根据流水号去做业务校验,如果已经处理,则丢弃,最终确保幂等性。

总结

我们介绍了MQ常见的使用场景,以及每种场景下的使用注意事项。尤其是在重试功能中,重试本来是MQ提供的一种保持数据最终可以得到确认的方法,但是如果业务使用上面不注意幂等性,则会带来业务数据的不一致甚至像重复扣款这样比较严重的后果。我们还介绍了发布/订阅模式下的广播消费的使用举例,也介绍了它的缺点以及可以使用集群消费来模拟广播。鉴于以上每种场景都给我们提供了很好的说明使得大家在以后使用MQ的过程中可以更好的发挥MQ的强大作用。

参考资料:https://tech.meituan.com/mq-design.html

关注公众号同步更新技术文章

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang中遇到的一些关于JSON处理的坑 前言 坑 参考

发表于 2017-12-14

前言

一个人不会两次掉进同一个坑里,但是如果他(她)忘记了坑的位置,那就不一定了。

这篇文章记录了最近使用Golang处理JSON遇到的一些坑。

坑

1号坑:omitempty的行为

C#中最常用的JSON序列化类库Newtonsoft.Json中,把一个类的实例序列化成JSON,如果我们不想让某个属性输出到JSON中,可以通过property annotation或者ShouldSerialize method等方法,告知序列化程序。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码// 通过ShouldSerialize method指示不要序列化ObsoleteSetting属性
class Config
{
public Fizz ObsoleteSetting { get; set; }

public bool ShouldSerializeObsoleteSetting()
{
return false;
}
}

// 通过JsonIgnore的annotation指示不需要序列化ObsoleteSetting属性
class Config
{
[JsonIgnore]
public Fizz ObsoleteSetting { get; set; }

public Bang ReplacementSetting { get; set; }
}

关于Newtonsoft.Json的Conditional Property Serialization的更多内容参考:

  • Conditional Property Serialization
  • Making a property deserialize but not serialize with json.net

开始使用Golang的时候,以为omitempty的行为和C#中一样用来控制是否序列化字段,结果使用的时候碰了一头钉子。回头阅读encoding/json package的官方文档,找到对omitempty行为的描述:

Struct values encode as JSON objects. Each exported struct field becomes a member of the object unless

  • the field’s tag is “-“, or
  • the field is empty and its tag specifies the “omitempty” option.

The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero. The object’s default key string is the struct field name but can be specified in the struct field’s tag value. The “json” key
in the struct field’s tag value is the key name, followed by an optional comma and options. Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> 复制代码> // Field is ignored by this package.
> > Field int `json:"-"`
> >
> > // Field appears in JSON as key "myName".
> > Field int `json:"myName"`
> >
> > // Field appears in JSON as key "myName" and
> > // the field is omitted from the object if its value is empty,
> > // as defined above.
> > Field int `json:"myName,omitempty"`
> >
> > // Field appears in JSON as key "Field" (the default), but
> > // the field is skipped if empty.
> > // Note the leading comma.
> > Field int `json:",omitempty"`
> >
>
>

Golang中,如果指定一个field序列化成JSON的变量名字为-,则序列化的时候自动忽略这个field。这种用法,才是和上面JsonIgnore的用法的作用是一样的。

而omitempty的作用是当一个field的值是empty的时候,序列化JSON时候忽略这个field(Newtonsoft.Json的类似用法参考这里和
例子)。这里需要注意的是关于emtpty的定义:

The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.

通过下面的例子,来加深对empty values的了解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
复制代码package main

import (
"bytes"
"encoding/json"
"log"
"os"
)

type S1 struct {
I1 int
I2 int `json:",omitempty"`

F1 float64
F2 float64 `json:",omitempty"`

S1 string
S2 string `json:",omitempty"`

B1 bool
B2 bool `json:",omitempty"`

Slice1 []int
Slice2 []int `json:",omitempty"`
Slice3 []int `json:",omitempty"`

Map1 map[string]string
Map2 map[string]string `json:",omitempty"`
Map3 map[string]string `json:",omitempty"`

O1 interface{}
O2 interface{} `json:",omitempty"`
O3 interface{} `json:",omitempty"`
O4 interface{} `json:",omitempty"`
O5 interface{} `json:",omitempty"`
O6 interface{} `json:",omitempty"`
O7 interface{} `json:",omitempty"`
O8 interface{} `json:",omitempty"`

P1 *int
P2 *int `json:",omitempty"`
P3 *int `json:",omitempty"`
P4 *float64 `json:",omitempty"`
P5 *string `json:",omitempty"`
P6 *bool `json:",omitempty"`
P7 *[]int `json:",omitempty"`
P8 *map[string]string `json:",omitempty"`
}

func main() {

p3 := 0
p4 := float64(0)
p5 := ""
p6 := false
p7 := []int{}
p8 := map[string]string{}

s1 := S1{
I1: 0,
I2: 0,

F1: 0,
F2: 0,

S1: "",
S2: "",

B1: false,
B2: false,

Slice1: []int{},
Slice2: nil,
Slice3: []int{},

Map1: map[string]string{},
Map2: nil,
Map3: map[string]string{},

O1: nil,
O2: nil,
O3: int(0),
O4: float64(0),
O5: "",
O6: false,
O7: []int{},
O8: map[string]string{},

P1: nil,
P2: nil,
P3: &p3,
P4: &p4,
P5: &p5,
P6: &p6,
P7: &p7,
P8: &p8,
}

b, err := json.Marshal(s1)
if err != nil {
log.Printf("marshal error: %v", err)
return
}

var out bytes.Buffer
json.Indent(&out, b, "", "\t")
out.WriteTo(os.Stdout)
//Output:
//{
// "I1": 0,
// "F1": 0,
// "S1": "",
// "B1": false,
// "Slice1": [],
// "Map1": {},
// "O1": null,
// "O3": 0,
// "O4": 0,
// "O5": "",
// "O6": false,
// "O7": [],
// "O8": {},
// "P1": null,
// "P2": 0
//}%
}

点击这里执行上面的程序

关于empty value的定义,这里面隐藏了一些坑。下面通过一个例子来说明。

假设我们有一个社交类App,通过Restful API形式从服务端获取当前登录用户基本信息及粉丝数量。如果服务端对Response中User对象的定义如下:

1
2
3
4
5
复制代码type User struct {
ID int `json:"id"` // 用户id
// 其它field
FansCount int `json:"fansCount,omitempty"` // 粉丝数
}

如果正在使用App时一个还没有粉丝的用户,访问Restful API的得到Response如下:

1
2
3
4
复制代码{
"id": 1000386,
...
}

这时候你会发现Response的User对象中没有fansCount,因为fansCount是个int类型且值为0,序列化的时候会被忽略。语义上,User对象中没有fansCount应该理解为粉丝数量未知,而不是没有粉丝。

如果我们希望做到能够区分粉丝数未知和没有粉丝两种情况,需要修改User的定义:

1
2
3
4
5
复制代码type User struct {
ID int `json:"id"` // 用户id
// 其它field
FansCount *int `json:"fansCount,omitempty"` // 粉丝数
}

将FansCount修改为指针类型,如果为nil,表示粉丝数未知;如果为整数(包括0),表示粉丝数。

这么修改语义上没有漏洞了,但是代码中要给FansCount赋值的时候却要多一句废话。必须先将从数据源查询出粉丝数赋给一个变量,然后再将变量的指针传给FansCount。代码读起来实在是啰嗦:

1
2
3
4
5
6
7
8
复制代码// FansCount是int类型时候
user := dataAccess.GetUserInfo(userId)
user.FansCount = dataAccess.GetFansCount(userId)

// FansCount是*int类型的时候
user := dataAccess.GetUserInfo(userId)
fansCount := dataAccess.GetFansCount(userId)
user.FansCount = &fansCount

2号坑:JSON反序列化成interface{}对Number的处理

JSON的规范中,对于数字类型,并不区分是整型还是浮点型。

对于如下JSON文本:

1
2
3
4
复制代码{
"name": "ethancai",
"fansCount": 9223372036854775807
}

如果反序列化的时候指定明确的结构体和变量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码package main

import (
"encoding/json"
"fmt"
)

type User struct {
Name string
FansCount int64
}

func main() {
const jsonStream = `
{"name":"ethancai", "fansCount": 9223372036854775807}
`
var user User // 类型为User
err := json.Unmarshal([]byte(jsonStream), &user)
if err != nil {
fmt.Println("error:", err)
}

fmt.Printf("%+v \n", user)
}
// Output:
// {Name:ethancai FansCount:9223372036854775807}

点击这里执行上面的程序

如果反序列化不指定结构体类型或者变量类型,则JSON中的数字类型,默认被反序列化成float64类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码package main

import (
"encoding/json"
"fmt"
"reflect"
)

func main() {
const jsonStream = `
{"name":"ethancai", "fansCount": 9223372036854775807}
`
var user interface{} // 不指定反序列化的类型
err := json.Unmarshal([]byte(jsonStream), &user)
if err != nil {
fmt.Println("error:", err)
}
m := user.(map[string]interface{})

fansCount := m["fansCount"]

fmt.Printf("%+v \n", reflect.TypeOf(fansCount).Name())
fmt.Printf("%+v \n", fansCount.(float64))
}

// Output:
// float64
// 9.223372036854776e+18

点击这里执行上面的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码package main

import (
"encoding/json"
"fmt"
)

type User struct {
Name string
FansCount interface{} // 不指定FansCount变量的类型
}

func main() {
const jsonStream = `
{"name":"ethancai", "fansCount": 9223372036854775807}
`
var user User
err := json.Unmarshal([]byte(jsonStream), &user)
if err != nil {
fmt.Println("error:", err)
}

fmt.Printf("%+v \n", user)
}

// Output:
// {Name:ethancai FansCount:9.223372036854776e+18}

点击这里执行上面的程序

从上面的程序可以发现,如果fansCount精度比较高,反序列化成float64类型的数值时存在丢失精度的问题。

如何解决这个问题,先看下面程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码package main

import (
"encoding/json"
"fmt"
"reflect"
"strings"
)

func main() {
const jsonStream = `
{"name":"ethancai", "fansCount": 9223372036854775807}
`

decoder := json.NewDecoder(strings.NewReader(jsonStream))
decoder.UseNumber() // UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.

var user interface{}
if err := decoder.Decode(&user); err != nil {
fmt.Println("error:", err)
return
}

m := user.(map[string]interface{})
fansCount := m["fansCount"]
fmt.Printf("%+v \n", reflect.TypeOf(fansCount).PkgPath() + "." + reflect.TypeOf(fansCount).Name())

v, err := fansCount.(json.Number).Int64()
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%+v \n", v)
}

// Output:
// encoding/json.Number
// 9223372036854775807

点击这里执行上面的程序

上面的程序,使用了func (*Decoder) UseNumber方法告诉反序列化JSON的数字类型的时候,不要直接转换成float64,而是转换成json.Number类型。json.Number内部实现机制是什么,我们来看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码// A Number represents a JSON number literal.
type Number string

// String returns the literal text of the number.
func (n Number) String() string { return string(n) }

// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}

// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
return strconv.ParseInt(string(n), 10, 64)
}

json.Number本质是字符串,反序列化的时候将JSON的数值先转成json.Number,其实是一种延迟处理的手段,待后续逻辑需要时候,再把json.Number转成float64或者int64。

对比其它语言,Golang对JSON反序列化处理真是易用性太差(“蛋疼”)。

JavaScript中所有的数值都是双精度浮点数(参考这里),反序列化JSON的时候不用考虑数值类型匹配问题。这里多说两句,JSON的全名JavaScript Object Notation(从名字上就能看出和JavaScript的关系非常紧密),发明人是Douglas Crockford,如果你自称熟悉JavaScript而不知道
Douglas Crockford是谁,就像是自称是苹果粉丝却不知道乔布斯是谁。

C#语言的第三方JSON处理library Json.NET反序列化JSON对数值的处理也比Golang要优雅的多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
复制代码using System;
using Newtonsoft.Json;

public class Program
{
public static void Main()
{
string json = @"{
'Name': 'Ethan',
'FansCount': 121211,
'Price': 99.99
}";

Product m = JsonConvert.DeserializeObject<Product>(json);

Console.WriteLine(m.FansCount);
Console.WriteLine(m.FansCount.GetType().FullName);

Console.WriteLine(m.Price);
Console.WriteLine(m.Price.GetType().FullName);

}
}

public class Product
{
public string Name
{
get;
set;
}

public object FansCount
{
get;
set;
}

public object Price
{
get;
set;
}
}

// Output:
// 121211
// System.Int64
// 99.99
// System.Double

点击这里执行上面的程序

Json.NET在反序列化的时候自动识别数值是浮点型还是整型,这一点对开发者非常友好。

3号坑:选择什么格式表示日期

JSON的规范中并没有日期类型,不同语言的library对日期序列化的处理也不完全一致:

Go语言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码package main

import (
"encoding/json"
"fmt"
"os"
"time"
)

func main() {
type Product struct {
Name string
CreatedAt time.Time
}
pdt := Product{
Name: "Reds",
CreatedAt: time.Now(),
}
b, err := json.Marshal(pdt)
if err != nil {
fmt.Println("error:", err)
}
os.Stdout.Write(b)
}
// Output
// {"Name":"Reds","CreatedAt":"2016-06-27T07:40:54.69292134+08:00"}

JavaScript语言:

1
2
3
4
5
复制代码➜  ~ node
> var jo = { name: "ethan", createdAt: Date.now() };
undefined
> JSON.stringify(jo)
'{"name":"ethan","createdAt":1466984665633}'

C#语言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
复制代码using System;
using Newtonsoft.Json;

public class Program
{
public static void Main()
{
Product product = new Product();
product.Name = "Apple";
product.CreatedAt = DateTime.Now;

string json = JsonConvert.SerializeObject(product,
Newtonsoft.Json.Formatting.Indented,
new JsonSerializerSettings {
NullValueHandling = NullValueHandling.Ignore
});
Console.WriteLine(json);
}
}

public class Product
{
public string Name
{
get;
set;
}

public DateTime CreatedAt
{
get;
set;
}
}
// Output:
// {
// "Name": "Apple",
// "CreatedAt": "2016-06-26T23:46:57.3244307+00:00"
// }

Go的encoding/json package、C#的Json.NET默认把日期类型序列化成ISO 8601标准的格式,JavaScript默认把Date序列化从1970年1月1日0点0分0秒的毫秒数。但JavaScript的dateObj.toISOString()能够将日期类型转成ISO格式的字符串,Date.parse(dateString)方法能够将ISO格式的日期字符串转成日期。

个人认为ISO格式的日期字符串可读性更好,但序列化和反序列化时的性能应该比整数更低。这一点从Go语言中time.Time的定义看出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码type Time struct {
// sec gives the number of seconds elapsed since
// January 1, year 1 00:00:00 UTC.
sec int64

// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
nsec int32

// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// Only the zero Time has a nil Location.
// In that case it is interpreted to mean UTC.
loc *Location
}

具体选择哪种形式在JSON中表示日期,有如下几点需要注意:

  • 选择标准格式。曾记得.NET Framework官方序列化JSON的方法中,会把日期转成如"\/Date(1343660352227+0530)\/"的专有格式,这样的专有格式对跨语言的访问特别不友好。
  • 如果你倾向性能,可以使用整数。如果你倾向可读性,可以使用ISO字符串。
  • 如果使用整数表示日期,而你的应用又是需要支持跨时区的,注意一定要是从1970-1-1 00:00:00 UTC开始计算的毫秒数,而不是当前时区的1970-1-1 00:00:00。

参考

文章:

  • package encoding/json in Go
  • docs.studygolang.com/src/encodin…
  • The Go Blog: JSON and Go
  • Go by example: JSON
  • JSON decoding in Go
  • go and json
  • Decode JSON Documents In Go
  • ffjson: faster JSON serialization for Golang
  • Serialization in Go

第三方类库:

  • ffjson: faster JSON serialization for Go
  • go-simplejson: a Go package to interact with arbitrary JSON
  • Jason: Easy-to-use JSON Library for Go
  • easyjson
  • gabs
  • jsonparser

工具:

  • JSON-to-Go: instantly converts JSON into a Go type definition

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL 性能调优技巧

发表于 2017-12-14

摘要:针对购物旺季网站流量会对数据库造成的压力,作者给出了MySQL性能调优的一些技巧,这些技巧极具参考价值,通过这些调优,可以有效避免因为流量过大造成服务器宕机,从而给企业造成经济损失。以下是译文

万圣节已经过去很久了,该是把注意力集中在即将到来的假日季节的时候了。首先是感恩节,接着就是黑色星期五和网络星期一,最终在圣诞节/节礼周(从12月26日的节礼日开始,到12月31日的除夕结束为期六天或更长时间。这个词是由零售业在2000年代中期左右发明的,试图延长他们的节礼日销售)达到购物高潮。对于企业主来说,一年的这个时候标志着人们期待已久的年底获利了结。对于一些DBA来说,它会带来恐惧,不安,甚至是不眠之夜,他们要努力使系统重新上线。

值得庆幸是,情况并非如此。通过对MySQL性能变量做一些主动调整,可以使数据库服务器免受购物旺季带来的需求增加的冲击。

技巧#1:确定MySQL的最大连接数

对于MySQL的最大连接数,一次最好是发送5个请求到Web服务器。对Web服务器的5个请求中的一部分将用于CSS样式表,图像和脚本等资源。由于诸如浏览器缓存等原因,要获得准确的MySQL到Web服务器的请求比率可能很困难; 要想得到一个确切的数字,就需要分析Web服务器的日志文件。例如,可以手动访问Apache的“access_log”日志文件,也可以通过Analog或Webalizer等实用程序访问日志文件。

一旦有了对特定使用情况的准确估计,请将该比率乘以Web服务器的最大连接数。例如,如果Web服务器配置为最多为256个客户端提供服务,MySQL请求与Web请求的比率为1/8,则最好将最大数据库连接数设置为32。还要考虑留有安全余量,把这个数乘以2,得到最终的数量。只有在基础设施支持的情况下,才能尝试将数据库连接数的最大数量与Web服务器的客户端限制相匹配。在大多数情况下,最好保持接近32。

在Monyog中查看MySQL连接

在MySQL数据库中,MySQL的最大并发连接数是存储在全局变量max_connections中的。Monyog报告变量“max_connections”作为当前连接监控组中的“最大允许”指标。它还将该数字除以打开的连接数,以生成连接使用百分比:

还有一个连接历史记录监控,可以帮助计算最佳的最大并发连接数。它包括尝试,拒绝和成功连接的数量。此外,允许达到的最大指标的百分比显示为一个进度条,可以让你快速评估服务器在过去达到的最大并发连接数:

技巧#2:为临时表分配足够的内存

在某些情况下,服务器在处理语句时会创建内部临时表。临时表用于内部操作如GROUP BY和distinct,还有一些ORDER BY查询以及UNION和FROM子句(派生表)中的子查询。这些都是在内存中创建的内存表。内存中临时表的最大大小由tmp_table_size和max_heap_table_size中较小的值确定。如果临时表的大小超过这个阈值,则将其转换为磁盘上的InnoDB或MyISAM表。此外,如果查询涉及BLOB或TEXT列,而这些列不能存储在内存表中,临时表总是直接指向磁盘。

这种转换的代价很大,所以考虑增加max_heap_table_size和tmp_table_size变量的大小来帮助减少在磁盘上创建临时表的数量。请记住,这将需要大量内存,因为内存中临时表的大小是基于“最坏情况”的。例如,内存表总是使用固定长度的列,所以字符列使用VARCHAR(255)。这可以使内存中的临时表比想象的要大得多—事实上,这比查询表的总大小要大很多倍!当增加max_heap_table_size和tmp_table_sizevariables的大小时,一定要监视服务器的内存使用情况,因为内存中的临时表可能会增加达到服务器内存容量的风险。

一般来说,32M到64M是建议值,从这两个变量开始并根据需要进行调优。

在Monyog中的临时表监测

临时表的监测是许多预定义的Monyog监测之一。它提供了一些临时表使用的指标,包括:

  • 允许的最大值:显示tmp_table_size服务器变量的值,它定义了在内存中创建的临时表的最大大小。与max_heap_table_size一起,这个值定义了可以在内存中创建的临时表的最大大小。如果内存临时表大于此大小,则将其存储在磁盘上。
  • 内存表的最大大小:显示max_heap_table_size服务器变量的值,该值定义了显式创建的MEMORY存储引擎表的最大大小。
  • 创建的临时表总数:显示created_tmp_tables服务器变量的值,它定义了在内存中创建的临时表的数量。
  • 在磁盘上创建的临时表:显示created_tmp_disk_tables服务器变量的值,该变量定义了在磁盘上创建的临时表的数量。如果这个值很高,则应该考虑增加tmp_table_size和max_heap_table_size的值,以便增加创建内存临时表的数量,从而减少在磁盘上创建临时表的数量。
  • 磁盘:总比率:基于created_tmp_disk_tables除以created_tmp_tables的计算值。由于tmp_table_size或max_heap_table_size不足而在磁盘上创建的临时表的百分比。Monyog将这个数字显示为一个进度条和百分比,以便快速确定有多少磁盘用于临时表,而不是内存。

趋势图可用于创建的总表,磁盘上创建的表和磁盘的总比值。这些让我们看到了它们随着时间的演变:

技巧#3:增加线程缓存大小

连接管理器线程处理服务器监听的网络接口上的客户端连接请求。连接管理器线程将每个客户端连接与专用于它的线程关联,该线程负责处理该连接的身份验证和所有请求处理。因此,线程和当前连接的客户端之间是一对一的比例。确保线程缓存足够大以容纳所有传入请求是非常重要的。

MySQL提供了许多与连接线程相关的服务器变量:

线程缓存大小由thread_cache_size系统变量决定。默认值为0(无缓存),这将导致为每个新连接设置一个线程,并在连接终止时需要处理该线程。如果希望服务器每秒接收数百个连接请求,那么应该将thread_cache_size设置的足够高,以便大多数新连接可以使用缓存线程。可以在服务器启动或运行时设置max_connections的值。

还应该监视缓存中的线程数(Threads_cached)以及创建了多少个线程,因为无法从缓存中获取线程(Threads_created)。关于后者,如果Threads_created继续以每分钟多于几个线程的增加,请考虑增加thread_cache_size的值。

使用MySQL show status命令显示MySQL的变量和状态信息。这里有几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码SHOW GLOBAL STATUS LIKE '%Threads_connected%';
 
+-------------------+-------+
 
| Variable_name     | Value |
 
+-------------------+-------+
 
| Threads_connected | 2     |
 
+-------------------+-------+
SHOW GLOBAL STATUS LIKE '%Threads_running%';
 
+-----------------+-------+
 
| Variable_name   | Value |
 
+-----------------+-------+
 
| Threads_running | 1     |
 
+-----------------+-------+

Monyog线程缓存监测

Monyog提供了一个监控线程缓存的屏幕,名为“线程”。与MySQL线程相关的服务器变量映射到以下Monyog指标:

  • thread_cache_size:可以缓存的线程数。
  • Threads_cached:缓存中的线程数。
  • Threads_created:创建用于处理连接的线程。

Monyog线程屏幕还包括“线程缓存命中率”指标。这是一个提示线程缓存命中率的指标。如果值较低,则应该考虑增加线程缓存。在状态栏以百分比形式显示该值;它的值越接近100%越好。

如果这些指标的值等于或超过指定值,则可以将每一个指标配置为发出警告和/或严重警报。

其他相关的服务器变量

除了上述指标以外,还应该监控以下内容:

  1. InnoDB缓冲池大小: InnoDB缓冲池大小在使用InnoDB的MySQL数据库中起着至关重要的作用。缓冲池同时缓存数据和索引。它的值应该尽可能的大,以确保数据库使用内存而不是硬盘驱动器进行读取操作。
  2. 临时表大小: MySQL使用max_heap_table_size和tmp_table_size中较小的一个来限制内存中临时表的大小。拥有较大的值可以帮助减少在磁盘上创建临时表的数量,但也会增加服务器内存容量的风险,因为这个指标适用于每个客户端。一般来说,32M到64M是建议的值,从这两个变量开始并根据需要进行调优。
  3. InnoDB日志缓冲区大小: MySQL每次写入日志文件时,它都会利用可用于处理销售数据的重要系统资源。因此,将InnoDB日志缓冲区大小设置为较大值才有意义。这样,服务器在大型事务中写入磁盘的次数就减少了,从而最大限度地减少了这些耗时的操作。64M是这个变量的一个很好的起点。

结论

虽然即便是最大的公司网站也会因宕机而遭受损失,但这种影响对于处理网上销售的中小型企业尤其关键。根据最近的一份调查报告显示,一分钟的宕机导致企业平均损失约5000美元。不要让你的业务成为那种统计数据(因为宕机造成的损失)的一部分。在假日繁忙之前,主动调优MySQL数据库服务器(S)并收获回报吧!

1 赞 1 收藏 2 评论

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…904905906…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%