这篇文章我们不谈技术,聊点轻松的,那聊什么呢?聊一下最近很火的目标管理 OKR。不知道小伙伴你们的公司什么情况,我的公司今年开始推行 OKR,用了大半年的时间,感觉效果还不错,上周六又参加了一天的复盘培训会,刚好借此机会总结一下顺便跟大家分享一下这个优秀的工具。

什么是 OKR

OKRs:Objectives & Key Results(目标与关键结果)。是一种企业,团队,个人目标设定与沟通的工具,是通过结果去衡量过程的方法与实践。

上面的解释说起来可能比较虚无缥缈,用通俗的语言来说就是一种自我激励督促的手段。对企业来说以前对员工的考核大部分情况都是采用 KPI 的方式,只要员工完成了 KPI 就可以了。OKR 是一种与 KPI 类似的工具,但是两者的出发点是完全不同的。KPI 强调的硬性指标,自上而下分配的任务,完全完全指导考核结果;但是 OKR 不一样,OKR 是一种激励督促的方式,强调的是创新性与挑战性,而且有自上而下或者自下而上两种方式,并且最大的特点是 OKR 的打分不直接与绩效考核挂钩。当然这里说的是不直接,但是还是会影响的~~。

简史

OKR 的创立是英特尔前首席执行官安迪·格鲁夫,在他的书《格鲁夫给经理人的第一课》中,他解释了自己为何成功创造出了 OKR,主要是通过问自己两个问题:

  1. 我要去哪里?答案是目标(Objective)
  2. 我如何知道能否达到那里?答案就是关键结果(Key Results)

格鲁夫的学生约翰杜尔在英特尔的时候全程跟着格鲁夫参与到了 OKR 的实施过程,亲眼目睹了 OKRs 是如何帮助英特尔管理层和员工的,最后成功完成转型。随后约翰杜尔作为早期谷歌的投资者将 OKR 介绍给了谷歌的创始人拉里-佩奇和谢尔盖-布林,之后 OKR 目标管理办法在谷歌里面发展的一发不可收拾,成为了全员使用的一种自我管理工具。

OKR 的制定

Objective 目标

OKR 是一种不限制场景使用的一个工具,可以是在公司内部,也可以是个人生活学习使用,基本的思路与原理都是一样的,只是不同场景所制定的 O 是不一样的。对于我们个人来说,不管是工作还是生活在一个周期(这里的周期我们通常是一个季度,当然根据个人情况,按月或者其他时间段都是可以的)里面一般设定 3-4 个 O 是最合适的,太多的 O 容易导致时间精力太分散不容易聚焦,太少可能不能很好的满足自己的需求,这个可以根据个人情况去制定。

O 的制定我们需要注意这么几点:

  1. 目标要积极阳光:一个积极阳光的目标是可以激发人的斗志的,在制定目标的时候尽量采用积极和正能量的词汇,比如:大幅度提升,远远超过,显著提升,最成功等,目标必须要让人看到就很激动,很有激情。但是也要注意不能太虚,避免制定一些根本不现实的目标和使用一些不恰当的词汇。
  2. 目标要有挑战:OKR 的核心是强调创新性和挑战性,所以我们的目标要尽量的有挑战,那种十拿九稳的目标就不要制定了,就需要制定那种刚好跳一跳就能够到目标,这样在目标实现过后才会有真正的成就感。太容易的目标,不具有挑战性会使人失去斗志。具有挑战但是也要符合实际,不能设定根本无法达成的目标。
  3. 对团队来说目标要可执行和有价值:这个是毋庸置疑的,对于一个团队来说设定的目标如果没有价值和无法执行那就是无效的目标,制定出来也没有任何效果,只能失败。
  4. 透明性:OKR 是完全透明的,意思是说任何人制定的 OKR 其他人都能查阅的,完全透明。

Key Results

在制定好了一个 O 过后,我们就需要列出为了能支撑这个 O 我们需要完成的哪些关键的结果。每一个 O 尽量列出 3-4 个 KR,在写 KR 的时候我们也要注意几点:

  1. KR 必须是可以量化的:意思是说写 KR 的时候一定要带上量化指标,比如:每个月写四篇公众号文章。而不能说"每个月都要写几篇文章"。这样不确定的 KR 是不会有效果,KR 就是要把关键的指标写出来,有些东西只有写出来才能真正的被落实。
  2. KR 是关键结果而非工作清单:很多人一开始写 OKR 的时候容易把 KR 写成一个个的任务清单,这是不对的,每个任务清单属于 KR 的下层,我们叫做 Plan,所以 KR 是对一系列 P 的组合和有量化的概括。
  3. KR 的编写使用积极简洁的语言:跟目标类似,KR 的编写也需要使用积极的语言,并且保证语言的简洁和明确负责人,避免误解。
  4. KR 一定要能支撑指定的 O: 编写的 KR 一定要能支撑上层的 O,编写的时候一定要确定这个 KR 是否能支撑 O,不能则说明这个不是关键 KR 就不需要写,另外 KR 一定要从多个维度去写。

OKR 案例

前面说了那么多都是理论,这里给一个实际的 OKR 案例给大家看一下具体是什么样子的。另外提一下 OKR 的编写格式没有固定的要求,有人喜欢用 Excel,有人喜欢用 Word 或者 PPT,都可以,找到一种自己喜欢的方式就好,我个人比较喜欢用脑图的方式,这里就采用思维导图给大家展示一个。

image-20190725004350573

(Ps: 这个思维导图是用 Mac 版的印象笔记画的,有点小 bug 不过能用)

可以看到,上面的 OKR 案例,目标中使用了"加速"积极字样,四个 KR 中都有量化的指标,目标和关键结果都比较清晰。图中的 (5/10) 这种表示的是完成这个 KR 的信心指数,这个信心指数很有讲究的,如果太低了说明这个 KR 实现的难度太大,不建议采用,如果太高了,说明这个 KR 太容易完成了,缺乏挑战。所以我们在填写 KR 的时候需要找到那些有六成七成把握的关键结果。而且最后在回顾的时候, OKR 的打分和考核也会参考这个信心指数。

OKR 落实的注意项

企业内部

OKR 的落实主要分为两个方面,一个是企业内部,一个是个人生活。在企业内部如果要落实 OKR 的话一定要确保透明,不管是领导制定的 OKR 还是员工制定的 OKR 都要无条件透明,最好的方式就是录入系统或者打印贴出来。而且 OKR 在制定的过程中一定要通过共识会达到相关人员的共识才可以,不能闷着头自己制定,制定完了就不管其他人了。因为工作中很多时候我们的目标是跟公司的愿景是一致的,而且大家很有可能自己的一个目标是需要其他团队的同事支持的,所以一定要有共识会,让大家都知道你制定的 OKR 是什么,从而达到一致。

企业内部的落实还有一个很重要的注意项就是考核问题,OKR 跟之前 KPI 的考核方式完全不一样,前面提到了 OKR 在一个季度或者月结束后是会有打分的,OKR 的打分不直接影响考核,但是会被参考。另外考核也需要看每个 KR 的信心指数,因为OKR 的打分是个人自己打分的,分打的高或者低都是自己的意愿。这里有小伙伴可能会说,既然是自己打分,那肯定打高一点啊,那么问题来了,如果一个 KR 的分打的很高,那么是不是可以认为这个 KR 不够挑战呢?是不是太容易完成了呢?当然不排除 KR 确实很难,但是就是完成的很出色的这种可能性。

所以总的来是,企业内部 OKR 的考核是需要参考 KR 的信心指数以及打分情况综合考虑的。另外还有一点 OKR 是鼓励创新和挑战,所以是不存在惩罚的。

个人生活

OKR 除了在工作中使用,也完全可以用到个人家庭生活中,按照个人习惯可以指定自己或者家人的年度,季度或者月度的 OKR。比如你可以指定自己每个月要阅读两本书,以及每个月要锻炼身体五次等有意义的 KR。另外你完全可以指定一个年度理财计划目标,通过具体的阅读投资书籍,或者购买基金股票,以及其他方式的投资来支撑你的目标,从而列出一系列的关键 KR。

总结

今天我们介绍了一个很好的目标管理工具 OKR,这个工具是这两年在国内流行起来的,但是其实很早在国外就已经使用了,国内的普及相比而言就晚了很多了。想要深入学习 OKR 目标管理办法的同学可以关注 Java 极客技术公众号,在后台回复关键字 "OKR" 获取由 Java 极客技术团队分享的几个关于 OKR 目标管理办法的几本电子书文档,进行深入的学习。更多优质的资料可以通过回复关键字 "关键字" 来获取,有更多优质的资源等你获取。

上面是了 OKR 是无条件透明的,下面贴一下我自己下半年的个人 OKR。

image-20190725004350573

我们都知道程序在运行的过程中经常需要进行服务间的通信和交互,特别是在当下微服务的架构下,每个系统都会庞大那么为了提高服务间的通信效率以及数据传输的性能,我们往往都会将需要传输的数据进行序列化,然后再进行传输。

什么是序列化

关于序列化相信大家都很了解,在 Java 中我们经常就可以看到很多实体类或者 POJO 都会实现 Serializable 接口,有了解过 Serializable 接口的小伙伴应该都知道,这个接口是一个空接口,只是用来标记的。所谓序列化简单来说就是在传输对象之前将对象转换成二进制字节进行传输,接收端在收到二进制数据后再反序列化转化成普通对象。

所以说序列化最终的目的是为了对象可以跨平台存储和进行网络传输。之所以需要序列化是因为在网络传输的时候,我们需要经过 IO,而 IO 传输支持的就是字节数组这种格式,所以序列化过后可以更好的传输。另外反序列化就是根据字节数组反向生成对象,是一个逆向过程。

常见的序列化方式

既然知道了什么是序列化,那么接下来我们看看有哪些常见的序列化方式。

JSON

当下最流行的序列化方式无非是 JSON 了,而且 JSON 作为前后端交互使用最广泛的格式,形式如下。作为最通用的格式,各种语言都支持,并且可以支持复杂的对象。

{"name":"鸭血粉丝","age":4,"sex":"男"}

JSON 作为一个序列化方案,它的优点是可读性很高,跨平台跨语言支持;但是有个缺点那就是体积较大,很存在很多冗余内容,比如双引号,花括号。

相信 JSON 大家在工作中使用的肯定会广泛,阿里提供的 fastjson 包是我们项目中必不可少的一个依赖。

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
</dependency>

XML

<?xml version="1.0"?>
<Person version="4.0">
	<name>鸭血粉丝</name>
	<age>4</age>
	<sex>男</sex>
</Person>

前些年不管是使用 SSM,还是使用 Spring 都会有很多 XML 的配置文件,现在很多被注解代替了,但是 XML 还是支持使用的。另外有一些广电或者银卡等老系统里面会有很多基于 XML 的协议开发的系统和服务。

阿粉之前做项目就遇到过银行的项目,里面都是很古老的 XML 协议,对接起来真是头疼呀~

通过上面例子我们可以看到,XML 协议的优缺点跟 JSON 类似,优点也是可读性很强,跨平台跨语言支持,缺点也是体积大,容易内容多。可以看到为了记录一个字段的值,每个标签都需要成对存在,过于冗余了。

Protobuf

Protobuf 是谷歌提出的一种序列化协议,Protobuf 是一种接口定义语言,它与语言和平台无关。它是一种序列化结构化数据并通过网络传输的方式,使用 Protobuf 传输二进制文件,与 JSON 的字符串格式相比,它提高了传输速度。

这里提到 Protobuf 是一种接口定义语言,说明也是一种语言,既然是语言那就有自己的关键字以及规则,所以对于Protobuf 协议,我们需要创建一个后缀为 .proto 的文件,在文件里面我们需要定义出我们的协议内容。

syntax = "proto2";
package com.demo;
message Request {
    required int32 version = 1;
    required string id = 2;
    message Model  {
        required int32 id = 1;
        required string pid = 2;
        optional int32 width = 3;
        optional int32 height = 4;
        optional int32 pos = 5;
    }
    repeated Model model = 3;
}

message 关键字表示定义一个结构体,required 表示必须,optional 表示可选,此外还有字段的名称和类型。这个原始的 proto 文件是通用的,只要定义一次就好,不管使用哪种语言都可以通过 proto 工具自动生成对应语言的代码。

比如要生成 Java 代码,我们可以执行下面的命令

protoc --java_out=. demo.proto 就会在指定的目录下,生成对应的 Demo.java,想生成其他语言的代码,只需要修改命令执行的参数即可,生成的代码内容会有很多,可以不用管直接使用就行。

我们定义模型的结构一次,然后就可以使用生成的源代码轻松地使用 JavaPythonGoRubyC++ 等各种语言在各种数据流中写入和读取结构化数据。

Protobuf 的优点主要是性能高,体积小,缺点就是要学习一下特定的关键词以及要下载按照 Protobuf 命令工具。

Thrift

Thrift 也是一种序列化协议,具体的使用方式跟 Protobuf 类似,只不过 Thrift Facebook 提出来的一种协议。

Thrift是一种接口描述语言和二进制通讯协议,原由Facebook于2007年开发,2008年正式提交Apache基金会托管,成为Apache下的开源项目。

Thrift 是一个 RPC 通讯框架,采用自定义的二进制通讯协议设计。相比于传统的HTTP协议,效率更高,传输占用带宽更小。另外,Thrift是跨语言的。Thrift的接口描述文件,通过其编译器可以生成不同开发语言的通讯框架。

Thrift 的使用方式跟 Protobuf 类似,也是有一个 .thrift 后缀的文件,然后通过命令生成各种语言的代码,这里就不演示了。

除了上面提到的四种序列化方式之外,还有 HessianJDK 原生等序列化方式,就不一一介绍了。

序列化协议选择

前面提到是几种序列化的协议方式,那么对于我们平常项目中使用的时候,我们应该如何选择自己的协议呢?需要关注哪几个方面的内容呢?

每个协议有每个协议的特点,具体选择哪种协议我们要根据实际的场景来选择,比如说如果是前后端对接,那么自然是 JSON 最合适,应该网页的交互要求不需要太高,秒级别是可以接受的,所以我们可以更加关注可读性。但是如果是微服务之间的数据传输,那我们就可以选择 Protobuf 或者 Thrift 这种更高效的协议来进行传输,因为这种场景我们对于协议序列化的体积和速度都有很高的要求。

总结

今天阿粉给大家介绍了几种序列化的协议,相信大家在日常工作中必然会用到,上面提到的协议你是否都用过呢?欢迎在评论区留言探讨。

日常工作中 Map 绝对是我们 Java 程序员高频使用的一种数据结构,那 Map 都有哪些遍历方式呢?这篇文章阿粉就带大家看一下,看看你经常使用的是哪一种。

通过 entrySet 来遍历

1、通过 formap.entrySet() 来遍历

第一种方式是采用 forMap.Entry 的形式来遍历,通过遍历 map.entrySet() 获取每个 entrykeyvalue,代码如下。这种方式一般也是阿粉使用的比较多的一种方式,没有什么花里胡哨的用法,就是很朴素的获取 map 的 keyvalue

public static void testMap1(Map<Integer, Integer> map) {
    long sum = 0;
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
      sum += entry.getKey() + entry.getValue();
    }
    System.out.println(sum);
  }

看过 HashMap 源码的同学应该会发现,这个遍历方式在源码中也有使用,如下图所示,

putMapEntries 方法在我们调用 putAll 方法的时候会用到。

2、通过 forIteratormap.entrySet() 来遍历

我们第一个方法是直接通过 forentrySet() 来遍历的,这次我们使用 entrySet() 的迭代器来遍历,代码如下。

public static void testMap2(Map<Integer, Integer> map) {
    long sum = 0;
    for (Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator(); entries.hasNext(); ) {
      Map.Entry<Integer, Integer> entry = entries.next();
      sum += entry.getKey() + entry.getValue();
    }
    System.out.println(sum);
  }

3、通过 whileIteratormap.entrySet() 来遍历

上面的迭代器是使用 for 来遍历,那我们自然可以想到还可以用 while 来进行遍历,所以代码如下所示。

 public static void testMap3(Map<Integer, Integer> map) {
    Iterator<Map.Entry<Integer, Integer>> it = map.entrySet().iterator();
    long sum = 0;
    while (it.hasNext()) {
      Map.Entry<Integer, Integer> entry = it.next();
      sum += entry.getKey() + entry.getValue();
    }
    System.out.println(sum);
  }

这种方法跟上面的方法类似,只不过循环从 for 换成了 while,日常我们在开发的时候,很多场景都可以将 forwhile 进行替换。2 和 3 都使用迭代器 Iterator,通过迭代器的 next(),方法来获取下一个对象,依次判断是否有 next

通过 keySet 来遍历

上面的这三种方式虽然代码的写法不同,但是都是通过遍历 map.entrySet() 来获取结果的,殊途同归。接下来我们看另外的一组。

4、通过 for 和 map.keySet() 来遍历

前面的遍历是通过 map.entrySet() 来遍历,这里我们通过 map.keySet() 来遍历,顾名思义前者是保存 entry 的集合,后者是保存 key 的集合,遍历的代码如下,因为是 key 的集合,所以如果想要获取 key 对应的 value 的话,还需要通过 map.get(key) 来获取。

public static void testMap4(Map<Integer, Integer> map) {
    long sum = 0;
    for (Integer key : map.keySet()) {
      sum += key + map.get(key);
    }
    System.out.println(sum);
  }

5、通过 forIteratormap.keySet() 来遍历

public static void testMap5(Map<Integer, Integer> map) {
    long sum = 0;
    for (Iterator<Integer> key = map.keySet().iterator(); key.hasNext(); ) {
      Integer k = key.next();
      sum += k + map.get(k);
    }
    System.out.println(sum);
  }

6、通过 whileIteratormap.keySet() 来遍历

public static void testMap6(Map<Integer, Integer> map) {
    Iterator<Integer> it = map.keySet().iterator();
    long sum = 0;
    while (it.hasNext()) {
      Integer key = it.next();
      sum += key + map.get(key);
    }
    System.out.println(sum);
  }

我们可以看到这种方式相对于 map.entrySet() 方式,多了一步 get 的操作,这种场景比较适合我们只需要 key 的场景,如果也需要使用 value 的场景不建议使用 map.keySet() 来进行遍历,因为会多一步 map.get() 的操作。

Java 8 的遍历方式

注意下面的几个遍历方法都是是 JDK 1.8 引入的,如果使用的 JDK 版本不是 1.8 以及之后的版本的话,是不支持的。

7、通过 map.forEach() 来遍历

JDK 中的 forEach 方法,使用率也挺高的。

public static void testMap7(Map<Integer, Integer> map) {
    final long[] sum = {0};
    map.forEach((key, value) -> {
      sum[0] += key + value;
    });
    System.out.println(sum[0]);
  }

该方法被定义在 java.util.Map#forEach 中,并且是通过 default 关键字来标识的,如下图所示。这里提个问题,为什么要使用 default 来标识呢?欢迎把你的答案写在评论区。

8、Stream 遍历

public static void testMap8(Map<Integer, Integer> map) {
    long sum = map.entrySet().stream().mapToLong(e -> e.getKey() + e.getValue()).sum();
    System.out.println(sum);
  }

9、ParallelStream 遍历

 public static void testMap9(Map<Integer, Integer> map) {
    long sum = map.entrySet().parallelStream().mapToLong(e -> e.getKey() + e.getValue()).sum();
    System.out.println(sum);
  }

这两种遍历方式都是 JDK 8Stream 遍历方式,stream 是普通的遍历,parallelStream 是并行流遍历,在某些场景会提升性能,但是也不一定。

测试代码

上面的遍历方式有了,那么我们在日常开发中到底该使用哪一种呢?每一种的性能是怎么样的呢?为此阿粉这边通过下面的代码,我们来测试一下每种方式的执行时间。

public static void main(String[] args) {
   int outSize = 1;
    int mapSize = 200;
    Map<Integer, Integer> map = new HashMap<>(mapSize);
    for (int i = 0; i < mapSize; i++) {
      map.put(i, i);
    }
    System.out.println("---------------start------------------");
    long totalTime = 0;
    for (int size = outSize; size > 0; size--) {
      long startTime = System.currentTimeMillis();
      testMap1(map);
      totalTime += System.currentTimeMillis() - startTime;
    }
    System.out.println("testMap1 avg time is :" + (totalTime / outSize));
		// 省略其他方法,代码跟上面一致
}

为了避免一些干扰,这里通过外层的 for 来进行多次计算,然后求平均值,当我们的参数分别是 outSize = 1,mapSize = 200 的时候,测试的结果如下

当随着我们增大 mapSize 的时候,我们会发现,后面几个方法的性能是逐渐上升的。

总结

从上面的例子来看,当我们的集合数量很少的时候,基本上普通的遍历就可以搞定,不需要使用 JDK 8 的高级 API 来进行遍历,当我们的集合数量较大的时候,就可以考虑采用 JDK 8forEach 或者 Stream 来进行遍历,这样的话效率更高。在普通的遍历方法中 entrySet() 的方法要比使用 keySet() 的方法好。

阿粉的答案是会部署服务就行了。

关于 Docker 阿粉相信作为后端程序员小伙伴们多多少少都听过或者用过,而且很多时候也都是因为要使用的时候才会去学习,毕竟学了用不上还不如不学。

对于后端程序员来说,如果不是专门运维或者喜欢研究运维的同学,对于 Docker 的使用只要能熟练的部署服务基本上就够用了。毕竟说实话作为后端程序员要学习的东西真的太多了,一名合格的后端程序员不仅要能看得懂改得了前端代码,也要能懂运维部署和数据库设计,而且每个领域还动不动就有很多新东西要学习,只能说太难了,毕竟每个人的精力是有限的。

为了掌握一下 Docker 再加上阿粉最近搞了一台轻量服务器,想折腾点东西,但是不知道搞什么好,所以就想起来了搭建一个网站来玩玩,可以用来写一些文章和分享啥的。

搭建网站有很多种方式,不过最强大的还是要数 WordPress,但是如果要完全靠自己搭建环境还是比较复杂,所以就想着还是使用 docker 来搭建,简单快速。

0. 安装 Docker

Linux 服务器中安装 docker 非常简单,直接执行yum install docker 命令即可,阿粉这边已经安装过了,所以就提示了这个,没有安装的话,会自行安装。

1. 安装 Docker Compose

安装完了 docker 后,我们再安装一下 Docker Compose 。安装 Docker Compose 的目的主要是为了管理很多的 Docker 容器,Docker Compose 就是一个工具,通过这个工具我们可以方便的管理各个有关联关系的容器,形成一个整体。使用 Compose,我们需要在一个 YAML 文件中定义所有服务,然后使用单个命令,就可以启动或停止所有服务。

我们这里使用 Docker Compose 在隔离的容器化环境中并排运行两个容器(WordPressMySQL),一个是站点,一个是保存用户数据的。

在终端中执行下面的命令

curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

然后将可执行权限应用于二进制文件:

sudo chmod +x /usr/local/bin/docker-compose

最后可以通过检查版本来验证安装是否成功:

docker-compose --version

显示下面内容表示安装成功。

更多关于 Docker Compose 的内容可以参考官方文档

2. 站点目录

接下来我们就开始搭建站点了,首先创建或者选择一个路径,阿粉这里的路径是 /srv ,然后创建一个名为wordpress 的目录来存放 WordPress 数据。

sudo mkdir -p /srv/wordpress 
cd /srv/wordpress/

3. 创建 YAML 文件

Docker Compose 工具中,运行容器所需的所有资源都必须在名为 docker-compose.yamlYAML 文件中定义。然后 Docker Compose 将读取此类文件并与 Docker 守护程序通信以创建、配置或销毁定义的资源。

在我们的例子中,该 docker-compose.yaml 文件将包含我们的服务定义。另外 Docker Compose 允许我们将这些服务与共享网络和卷链接在一起。因为我们知道 Docker 容器本身就不会将数据持久化的,所以我们需要一个数据卷。

因此,让我们从使用 Vim 编辑器在/srv/wordpress目录中创建一个新文件docker-compose.yaml开始 。内容如下:

version: '3'
services:
  mysql:
    image: mysql:latest
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_root_password
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress_user
      MYSQL_PASSWORD: your_wordpress_password
    volumes:
      - mysql_data:/var/lib/mysql
  wordpress:
    image: wordpress:latest
    depends_on:
      - mysql
    ports:
      - 8080:80
    restart: always
    environment:
      WORDPRESS_DB_HOST: mysql:3306
      WORDPRESS_DB_USER: wordpress_user
      WORDPRESS_DB_PASSWORD: your_wordpress_password
    volumes:
      - ./wp-content:/var/www/html/wp-content
volumes:
  mysql_data:

这里稍微解释一下:

  • 我们定义了两个自动相互链接的容器服务、mysqlwordpress
  • 两个服务都使用 Docker 镜像,并且指定了使用最新的版本;
  • mysql 环境配置:指定 wordpress 将使用这些环境变量连接到 mysql 容器;
  • wordpress 环境配置:设置数据库连接详细信息;
  • wordpress 镜像基于 Apache 默认的端口是 80,这里我们将本机的 8080 端口映射到容器的 80 端口上;
  • mysql_data:定义一个数据卷,流入这个数据库的数据将被持久化到一个名为mysql_data 的数据卷里面,这样即使我们删除了容器,数据仍然存在于我们的机器上,并且可以再次安装在新的容器中。

另外,wordpress 下面的volumes 参数是告诉 Docker 在本地文件系统中显示 wp-content目录。这样只要我们同时拥有数据库和 wp-content 文件夹,就可以随时恢复我们的站点,即使其他所有内容都丢失了也可以恢复。

4. 使用 Docker Compose 运行 WordPress

接下来我们就可以运行 WordPress 了,首先进入 /srv/wordpress 目录,运行以下命令:

sudo docker-compose up -d

该命令将开始运行脚本,当 Docker 拉入 MySQLWordPress 镜像时,应该会在终端中看到各种“正在下载”和“正在等待”消息。4-5分钟即可。完成后我们的目录下面就会多了一个wp-content 目录,如下所示:

OK,现在带有 MySQL 容器和 WordPress 容器的 WordPress 安装已成功。

5. 访问您的 WordPress 安装

最后,在我们的容器运行的情况下,我们可以通过 WordPress Web 界面完成安装。在 Web 浏览器中,导航到http://localhost:8080http://your_ip_address:8080。如果在本地安装的话就用本地地址,如果是在服务器上面安装就用服务器的地址进行访问即可。

这里需要注意一点,如果使用的是腾讯云或者阿里云,在控制台中可能需要打开防火墙开放对应的端口,否则访问不通。访问不通的小伙伴,一定要确保自己服务器的端口是开放的。

打开后将看到语言选择页面。选择您的语言,然后单击继续,接下来就是根据页面上显示的内容进行填写就好了,主要是一些账号密码,站点名称,这些后面都是可以在重新改的,所以不用太纠结。最后成功通过身份验证后,就可以开始使用 WordPress 了。

到这里其实整个搭建就已经完成了,但是并不代表站点就可以正常使用了,我们还需要做的是购买域名,备案域名,再配置 HTTPS,等这一系列都完成了过后,整个站点才算是搭建完毕。

下面这几个都是阿粉常用的wordpress 的网站,感兴趣的小伙伴可以去看看,有条件的也可以尝试自己去搭建一个,搭建过程中如果遇到问题,可以通过文末的形式进读者群,我们一起探讨。

http://www.itmind.net/

https://www.cxy521.com/

http://itmooc.tech/

image-20220818222907308

https://yuandifly.com/

6. Docker Compose 命令

首先,确保您在/srv/wordpress目录中。

docker-compose ps // 查看容器状态
docker-compose up -d  // 启动
docker-compose stop   // 停止

注意:阿粉这里配置的端口是 9996,上面给大家演示使用的是 8080,这个完全没有任何影响,主要是阿粉这边 8080 已经被使用了。

7. 结论

最后整篇的安装文章就到这里了,通过 Docker 的安装还是非常简单的,主要网络和端口没问题,基本上都能安装成功的,剩下的就是 docker 的一些使用技巧了,不在本文讨论的范围之内就不细说了,下篇文章教大家如果给 WordPress 站点配置域名,并通过 HTTPS 的方式进行访问。

在日常开发的过程中我们经常会需要调用第三方组件或者数据库,有的时候可能会因为网络抖动或者下游服务抖动,导致我们某次查询失败,这种时候我们往往就会进行重试,当重试几次后依旧还是失败的话才会向上抛出异常进行失败。接下来阿粉就给大家演示一下通常是如何做的,以及如何更优雅的进行重试。

常规做法

我们先来看一下常规做法,常规做法首先会设置一个重试次数,然后通过 while 循环的方式进行遍历,当循环次数没有达到重试次数的时候,直到有正确结果后就返回,如果重试依旧失败则会进行睡眠一段时间,再次重试,直到正常返回或者达到重试次数返回。

package com.example.demo.service;

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class HelloService {
  public String sayHello(String name) {
    String result = "";
    int retryTime = 3;
    while (retryTime > 0) {
      try {
        //
        result = name + doSomething();
        return result;
      } catch (Exception e) {
        System.out.println("send message failed. try again in 1's");
        retryTime--;
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException ex) {
          throw new RuntimeException(ex);
        }
      }
    }
    return result;
  }

  private int doSomething() {
    Random random = new Random();
    int i = random.nextInt(3);
    System.out.println("i is " + i);
    return 10 / i;
  }
}

这里为了模拟异常的情况,阿粉在 doSomething 函数里面进行了随机数的生成和使用,当随机出来的值为 0 的时候,则会触发 java.lang.ArithmeticException 异常,因为 0 不能作除数。

接下来我们再对外提供一个接口用于访问,代码如下

package com.example.demo.controller;

import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

  @Autowired
  private HelloService helloService;

  @GetMapping(value = "/hello")
  public String hello(@RequestParam("name") String name) {
    return helloService.sayHello(name);
  }
}

正常启动过后,我们通过浏览器进行访问

可以看到,我们第一次方法的时候就成功的达到了我们要的效果,随机数就是 0 ,在 1 秒后重试后结果正常。在多试了几次过后,会遇到三次都是 0 的情况,这个时候才会抛出异常,说明服务是真的有问题了。

上面的代码可以看到是有效果了,虽然不是很好看,特别是在还有一些其他逻辑的情况,看上去会很臃肿,但是确实是可以正常使用的,那么有的小伙伴就要问了,有没有一种优雅的方式呢?总不能在很多地方都重复的这样写重试的代码吧。

注解重试

要知道我们普通人在日常开发的时候,如果遇到一个问题肯定是别人都遇到过的,什么时候当我们遇到的问题,没有人遇到过的时候,那说明我们是很前卫的。

因此小伙伴能想到的是不是有简单的方式来进行重试,有的人已经帮我们想好了,可以通过 @Retryable 注解来实现一样的效果,接下来阿粉就给大家演示一下如何使用这个注解。

首先我们需要在启动类上面加入 @EnableRetry 注解,表示要开启重试的功能,这个很好理解,就像我们要开启定时功能需要添加 @EnableScheduling 注解一样,Spring@Enablexxx 注解也是很有意思的,后面我们再聊。

添加完注解以后,需要加入切面的依赖,如下

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.9.2</version>
</dependency>

如下不加入这个切面依赖,启动的时候会有如下异常

添加的注解和依赖过后,我们需要改造 HelloService 里面的 sayHello() 方法,简化成如下,增加 @Retryable 注解,以及设置相应的参数值。

  @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
  public String sayHello(String name){
    return name + doSomething();
  }

再次通过浏览器访问 http://127.0.0.1:8080/hello?name=ziyou 我们看到效果如下,跟我们自己写的重试一样。

@Retryable 详解

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.retry.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    String recover() default "";

    String interceptor() default "";

    Class<? extends Throwable>[] value() default {};

    Class<? extends Throwable>[] include() default {};

    Class<? extends Throwable>[] exclude() default {};

    String label() default "";

    boolean stateful() default false;

    int maxAttempts() default 3;

    String maxAttemptsExpression() default "";

    Backoff backoff() default @Backoff;

    String exceptionExpression() default "";

    String[] listeners() default {};
}

点到这个注解里面,我们可以看到这个注解的代码如下,其中有几个参数我们来解释一下

  • recover: 当前类中的回滚方法名称;
  • interceptor: 重试的拦截器名称,重试的时候可以配置一个拦截器;
  • value:需要重试的异常类型,跟下面的 include 一致;
  • include:包含的重试的异常类型;
  • exclude:不包含的重试异常类型;
  • label:用于统计的唯一标识;
  • stateful:标志表示重试是有状态的,也就是说,异常被重新抛出,重试策略是否会以相同的策略应用于具有相同参数的后续调用。如果是 false,那么可重试的异常就不会被重新抛出。
  • maxAttempts:重试次数;
  • backoff:指定用于重试此操作的属性;
  • listeners:重试监听器 bean 名称;

配合上面的一些属性的使用,我们就可以达到通过注解简单来实现方法调用异常后的自动重试,非常好用。我们可以在执行重试方法的时候设置自定义的重试拦截器,如下所示,自定义重试拦截器需要实现 MethodInterceptor 接口并实现 invoke 方法,不过要注意,如果使用了拦截器的话,那么方法上的参数就会被覆盖。

package com.example.demo.pid;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
import org.springframework.retry.interceptor.RetryOperationsInterceptor;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.stereotype.Component;

@Component
public class CustomRetryInterceptor implements MethodInterceptor {

  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    RetryOperationsInterceptor build = RetryInterceptorBuilder.stateless()
      .maxAttempts(2).backOffOptions(3000, 2, 1000).build();
    return build.invoke(invocation);
  }
}

自定义回滚方法,我们还可以在重试几次依旧错误的情况,编写自定义的回滚方法。

  @Retryable(value = Exception.class,
    recover = "recover", maxAttempts = 2,
    backoff = @Backoff(delay = 1000, multiplier = 2))
  public String sayHello(String name){
    return name + doSomething();
  }

  @Recover
  public String recover(Exception e, String name) {
    System.out.println("recover");
    return "recover";
  }

要注意:

  • 重试方法必须要使用 @Recover 注解;
  • 返回值必须和被重试的函数返回值一致;
  • 参数中除了第一个是触发的异常外,后面的参数需要和被重试函数的参数列表一致;

上面代码中的 @Backoff(delay = 1000, multiplier = 2) 表示第一次延迟 1000ms 重试,后面每次重试的延迟时间都翻倍。

总结

阿粉今天给大家介绍了一下 Spring@Retryable 注解使用,并通过几个 demo 来带大家编写了自己重试拦截器以及回滚方法的时候,是不是感觉用起来会很爽,那还在等什么赶紧用起来吧,其中还有很多细节,只有自己真正的使用过才能体会到。


更多优质内容欢迎关注公众号【Java 极客技术】,我准备了一份面试资料,回复【bbbb07】免费领取。希望能在这寒冷的日子里,帮助到大家。