理解 raft 算法

raft 算法是一种共识算法,其解决的分布式环境中的数据一致性的问题

leader election

在 raft 算法中,每个节点有三种状态:

  • Follower
  • Candidate
  • Leader

所有的节点初始都是 follower ,如果 follower 节点没有收到任何 leader 节点的消息,这些节点将变成 candidate ,candidate 节点开始向其他节点请求投票,节点会返回投票信息,如果一个 candidate 获得所有节点中的多数投票,则他会变成 leader 状态,这个过程称之为 leader election

在 raft 中有两个 timeout 设置控制着 election 的进行。

第一个是 election timeout,意思是 follower 要等待成为 candidate 的时间,这个时间是一个介于 150ms 到 300ms 的值,这个时间结束之后 follower 变成 candidate 开始选举,首先是自己对自己投票,然后向其他节点请求投票,如果接收节点在收到投票请求时还没有参与过投票,那么他会把票投给这个请求投票的 candidate,然后重置自身的 election timeout,一旦一个 candidate 拥有所有节点中的大多数投票,他变成一个 leader。

第二个是 heartbeat timeout,一旦一个 candidate 成为 leader,他开始向其他 follower 发送 append entries,这些消息发送的频率是通过 heartbeat timeout 指定,follower 会响应每条的 append entry,整个 election 会一直进行直到 follower 停止接受 heartbeat 并且变成 candidate 开始下一轮 election。

假设 leader 故障了,follower 不再收到 heartbeats,新一轮 election 开始,整个过程重复上述步骤。

需要节点中的多数节点的投票才能成为 leader 保证了在每轮选举中只有一个 leader 可以胜出,如果一轮选举中有两个节点同时成为 candidate 将会导致 split vote 发生,如果此时两个 candidate 都收到了相同的票数,他们重置 election timeout 重新开启新一轮选举。

log replication

leader 成功选举之后,之后 client 的请求都先经过 leader,每个请求的更改以日志的形势保存在 leader 节点,但这些更改是 uncommitted 状态,为了对这些更改进行提交,leader 首先 replicate 这些更改到 follower,等到 follower 中的大部分提交之后才会 commit 这些更改,commit 之后通知 follower 更改已经 commited,这个系统现在达到了一致的状态,这个过程称之为 log replication

network partitions

raft 算法可以应对 network partitions。

比如由于网络分区导致了 C、D、E 和 A、B 隔离,各自分区中会重新开始选举形各自形成新的 leader

在各自分区之内,各自 leader 会收到不同的 client 发送的请求,由于在 B 分区内,leader 无法获得多数节点的投票,因而 leader B 上发生的更改不会被提交,等网络分区修复之后,A 和 B 的 term 比较小,他们会自动下线,回滚之前的提交,等待新的 leader 发送 hearbeat

参考

技术提升的一个方法

最近看到知乎上的专栏https://zhuanlan.zhihu.com/c_183152541,感触颇多。

要突破自己首先的热爱编程,敬畏程序。毕竟有道无术,术尚可求,有术无道,止于术也。

你发现你的开发过程中总是需要重复复制-稍微改吧改吧-粘贴这个过程,你有没有去尝试阅读并实践《重构》?

你有没有尝试去自己发布一个公用的库?JCenter/Maven Central都是免费的。

你发现你每次修改完代码要抄起Postman点来点去

你有没有尝试去编写一个集成测试,代替手工的劳动?

你发现你碰到了很多奇奇怪怪你搞不明白的问题,只能一次次地尝试每个搜索结果中提到的解决方案,期望其中的某一个好使。
你有没有尝试过去阅读相关的书籍,查阅相关文档?

如何突破自己
如何成为顶级程序员
跳出弱鸡循环1
跳出弱鸡循环2

总结下来提升手段:
1.实践自动化测试

当你觉得技术上遇到瓶颈的时候,可以通过学习和实践自动化测试,来补全自己在相关工程领域的技术短板,提升自己的技术能力
从测试中学到的不仅是写测试用例的能力,而是一整套的工程化、自动化的能力。

实践流程
第一步,去看一下《Maven实战》,了解一下Maven的测试是怎么工作的。之所以让你去研究Maven,是因为你们这种系统,99%不会采用Gradle这种新技术的。
第二步,写第一个测试,代码如下:

public class MyTest {
    @Test
    public void 跳出弱鸡循环() {
    }
}

第三步,去搞一份你们线上数据库的表结构。各种数据库都有相应的命令dump表结构。有困难的的话,手写建表语句。
第四步,本地用Docker启动一个临时的数据库。
第五步,去研究一下flyway,用自动化方式把表结构灌到这个临时数据库里。
第六步,去了解一下你们的应用是怎么部署的,你们上线的应用不可能是通过在IDE里面点绿色三角来部署的。把部署的命令行要过来。
第七步,研究一下这个命令行,尝试在本地启动起来。碰到数据库没起来的问题,就把连接串改成刚刚那个Docker的临时数据库。
第八步,你平时怎么在网页上点点点测试的,把它翻译成Java。比如你平时会手工测试登录接口,那就用HttpClient写一段代码,模拟登录。
第九步,把上面这些整合起来:

public class MyTest {
    @Test
    public void 跳出弱鸡循环() {
        启动测试数据库();
        把表结构灌进去();
        本地启动应用();
        自动化方式测试接口();
    }
}

继续阅读“技术提升的一个方法”

Java CountDownLatch 和 CyclicBarrier 示例

复习了一下 JCIP 回顾一下同步工具类的使用

CountDownLatch


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchExample1 {

	private final static int threadCount = 200;

	public static void main(String[] args) throws Exception {

		ExecutorService exec = Executors.newCachedThreadPool();

		final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		for (int i = 0; i < threadCount; i++) {
			final int threadNum = i;
			exec.execute(() -> {
				try {
					test(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					countDownLatch.countDown();
				}
			});
		}
		countDownLatch.await();
		System.out.println("finish");
		exec.shutdown();
	}

	private static void test(int threadNum) throws Exception {
		Thread.sleep(100);
		System.out.println("{" + threadNum + "}");
		Thread.sleep(100);
	}
}

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CountDownLatchExample2 {
	private final static int threadCount = 200;

	public static void main(String[] args) throws Exception {

		ExecutorService exec = Executors.newCachedThreadPool();

		final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		for (int i = 0; i < threadCount; i++) {
			final int threadNum = i;
			exec.execute(() -> {
				try {
					test(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					countDownLatch.countDown();
				}
			});
		}
                //超时之后不阻塞,直接继续运行
		countDownLatch.await(10, TimeUnit.MILLISECONDS);
		System.out.println("finish");
		exec.shutdown();
	}

	private static void test(int threadNum) throws Exception {
		Thread.sleep(100);
		System.out.println("{" + threadNum + "}");
	}
}
import java.util.concurrent.CountDownLatch;

public class TestCountDownLatch {

	public long timeTasks(int nThreads, final Runnable task) throws InterruptedException {

		final CountDownLatch startGate = new CountDownLatch(1);
		final CountDownLatch endGate = new CountDownLatch(nThreads);

		for (int i = 0; i < nThreads; i++) {
			Thread thread = new Thread() {
				@Override
				public void run() {
					try {
						startGate.await();
						try {
							task.run();
						} finally {
							endGate.countDown();
						}
					} catch (InterruptedException e) {

					}

				}
			};
			thread.start();
		}

		long start = System.nanoTime();
		startGate.countDown();
		endGate.await();
		long end = System.nanoTime();
		return end - start;
	}

	public static void main(String[] args) {
		int nTask = 10;
		TestCountDownLatch testCountDownLatch = new TestCountDownLatch();
		try {
			long timeUse = testCountDownLatch.timeTasks(nTask, new Task());
			System.out.println(nTask + " use time " + timeUse);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

}

class Task implements Runnable {

	@Override
	public void run() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

CyclicBarrier

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierExample1 {

	private static CyclicBarrier barrier = new CyclicBarrier(5);

	public static void main(String[] args) throws Exception {

		ExecutorService executor = Executors.newCachedThreadPool();

		for (int i = 0; i < 10; i++) {
			final int threadNum = i;
			Thread.sleep(1000);
			executor.execute(() -> {
				try {
					race(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		executor.shutdown();
	}

	private static void race(int threadNum) throws Exception {
		Thread.sleep(1000);
		System.out.println("{" + threadNum + "} is ready ");
		barrier.await();
		System.out.println("{" + threadNum + "} continue ");
	}
}
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CyclicBarrierExample2 {
	private static CyclicBarrier barrier = new CyclicBarrier(5);

	public static void main(String[] args) throws Exception {

		ExecutorService executor = Executors.newCachedThreadPool();

		for (int i = 0; i < 10; i++) {
			final int threadNum = i;
			Thread.sleep(1000);
			executor.execute(() -> {
				try {
					race(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		executor.shutdown();
	}

	private static void race(int threadNum) throws Exception {
		Thread.sleep(1000);
		System.out.println("{" + threadNum + "} is ready");
		try {
                        //等待超时之后直接放开栅栏,让线程执行,不需要等到规定线程数都就位
			barrier.await(2000, TimeUnit.MILLISECONDS);
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("{" + threadNum + "} continue");
	}
}
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierExample3 {
        //开始之前执行回调函数
	private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
		System.out.println("callback is running");
	});

	public static void main(String[] args) throws Exception {

		ExecutorService executor = Executors.newCachedThreadPool();

		for (int i = 0; i < 10; i++) {
			final int threadNum = i;
			Thread.sleep(1000);
			executor.execute(() -> {
				try {
					race(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		executor.shutdown();
	}

	private static void race(int threadNum) throws Exception {
		Thread.sleep(1000);
		System.out.println("{" + threadNum + "} is ready");
		barrier.await();
		System.out.println("{" + threadNum + "} continue");
	}
}

C 语言字符串操作

#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <ctype.h>

char *strupr_t(char *str)
{
	char *orign=str;
	for (; *str!='\0'; str++)
		*str = toupper(*str);
	return orign;
}

char *strlowr_t(char *str)
{
	char *orign=str;
	for (; *str!='\0'; str++)
		*str = tolower(*str);
	return orign;
}

//字符串拷贝(strcpy, strncpy)
void testStrCpy(){
	char dest[1024] = {0};
	char *str = "abcde";
	//strcpy()函数会将源字符串中的结束符('\0')也拷贝到目的字符串中。
    //注意,strcpy()可能会导致溢出。
	strcpy(dest,str);

	printf("%s\n", dest);


	char dest2[7] = {0};
	char *src2 = "abcde";
	dest2[5] = 'A';
	//该函数从源字符串中拷贝n个字符到目的字符串;如果源字符串长度不足,则用 NULL 填充,以保证将n个字符写入目的字符串中;如果源字符串中前n个字符不包含字符串结束符,函数不会为目的字符串添加上结束符。
	//所以如果有需要,应该在拷贝后自己在目的字符串尾部添加结束符。
	strncpy(dest2, src2, 5);
	printf("%s\n", dest2);
}

void testStrCmp()
{
	char *s11 = "abcde";
	char *s12 = "abcef";
	char *s13 = "ad";
	//从这个结果可以发现,strcmp()是根据字典序来对字符串进行比较的。进一步的,还可以发现strcmp()的返回值是比较过程中最后一次比较时两个字符的值的差
	printf("compare(%s, %s) -> %d\n", s11, s12, strcmp(s11, s12));
	printf("compare(%s, %s) -> %d\n", s12, s13, strcmp(s12, s13));
	printf("compare(%s, %s) -> %d\n", s11, s13, strcmp(s11, s13));

	char *s21 = "abcde";
	char *s22 = "abcfg";
	char *s23 = "ad";
	//和strcmp()的区别是,strncmp()只对s1和s2的前n个字节进行比较。
	printf("compare(%s, %s, 4) -> %d\n", s21, s22, strncmp(s21, s22, 4));
	printf("compare(%s, %s, 2) -> %d\n", s21, s23, strncmp(s21, s23, 2));
	printf("compare(%s, %s, 3) -> %d\n", s22, s23, strncmp(s22, s23, 3));

	char *s31 = "AbcdE";
	char *s32 = "abcdE";
    //使用strcasecmp()应该包含 strings.h 而不是 string.h
    //strcasecmp()在比较时不区分大小写
    //strncasecmp()之于strcasecmp()就如strncmp()之于strcmp()
	printf("compare(%s, %s) with case -> %d\n", s31, s32, strcmp(s31, s32));
	printf("compare(%s, %s) ignore case -> %d\n", s31, s32, strcasecmp(s31, s32));
}

void testStrCat()
{
	char dest[1024] = "hello ";
	char *src = "world!";
	//strcat()首先会覆盖掉目的字符串的结束符,然后把源字符串的内容追加到后面,并在最后添加结束符。如果目的字符串缓冲区长度不够,将导致溢出。
    //strcat()在操作完成后,返回目的字符串的首地址,这样可以方便地进行链式操作。
	printf("%s\n", strcat(dest, src));

	char dest2[1024] = "hello ";
	char *src2 = "world!kkkkaaaa";
	//strncat()将最多n个字节的内容追加到目的字符串尾部,并且会在追加后添加终止符号。
    //同strcat()一样,它返回目的字符串的首地址。
	printf("%s\n", strncat(dest2, src2,6));
}

void testStrSearch()
{
	char *s1 = "hello world!";
	char c = 'l';
	//strchr()返回一个字符指针,指向指定字符在指定字符串中第一次出现的位置。如果在指定字符串中没有找到指定字符,则返回 NULL 。该函数的第二个参数按理来说应当是一个字符,不过标准库中确实是int类型。
	printf("%s\n", strchr(s1, c));
	//strrchr()和strchr()类似,但它返回的是指定字符在指定字符串中最后一次出现的位置。如果未找到,同样返回 NULL 。
	printf("%s\n", strrchr(s1, c));
	//strchrnul()的功能和strchr()只有细微的区别,那就是,当没有找到指定字符时,strchrnul()不返回 NULL ,而是返回字符串结束符的位置。
	//这里由于strchrnul()的特性,没办法通过打印字符串来了解strchrnul()的操作
	//'strchrnull' is invalid in C99
	//printf("%p, %p\n", s1, strchrnull(s1, 'f'));

	char *accept = "wo";
	//strpbrk()和strchr()的区别在于,strchr()是从字符串里搜索 一个字符 ,而strpbrk()则是在字符串里搜索 一个字符集中的字符 ,看第二个参数就明白了。strpbrk()遍历字符串,如果发现某个字符在指定的 字符集 中,则立即返回指向该字符的指针。如果最后没有找到任何在指定字符集中的字符,则返回 NULL 。
	printf("%s\n", strpbrk(s1, accept));
}

void testStrSplit()
{
	char s[1024] = "abc;lsdk:lskdj,;slsj";
	char *delm = ";:,";
	char *result = NULL;
	int len = strlen(s);
	int i = 0;
    //strtok()根据第二个参数指定的分隔符(可能存在多个不同的分隔符)将指定字符串分割成多个子串。通过多次调用strtok(),可以依次获得字符串的多个子串的首地址。要注意的是,除了第一次调用时将待分割字符串作为第一个参数,后续的调用要将第一个参数置为 NULL 。当字符串已经无法再分割时,strtok()返回 NULL 。
    //除了上面说过的strtok()的用法外,还要注意的是,作为待分割的字符串,它必须是 可更改的 。否则虽然可以通过编译,但运行会出错。要理解这个现象,首先要了解strtok()的内部机制。
	result = strtok(s, delm);
	while (result != NULL) {
		printf("Source:%s, Sub:%s\n", s, result);
		result = strtok(NULL, delm);
	}
}

/*
T	o	 	b	e	 	o	r	 	n	o	t	 	t	o	 	b	e
84	111	32	98	101	32	111	114	32	110	111	116	32	116	111	32	98	101
84	111	0	98	101	32	111	114	32	110	111	116	32	116	111	32	98	101
84	111	0	98	101	0	111	114	32	110	111	116	32	116	111	32	98	101
84	111	0	98	101	0	111	114	0	110	111	116	32	116	111	32	98	101
84	111	0	98	101	0	111	114	0	110	111	116	0	116	111	32	98	101
84	111	0	98	101	0	111	114	0	110	111	116	0	116	111	0	98	101
84	111	0	98	101	0	111	114	0	110	111	116	0	116	111	0	98	101

可以看到,s中的分隔符,逐次地被置为'\0'即字符串结束符。这就是strtok()分割字符串的内部原理了。而strtok()返回的指针,其实就是s中各个子串的起始位置了。如果s指向的内容是无法被修改的,那么strtok()自然也就无法将原先的分隔符置为字符结束符了。

当然了,由于源字符串会被修改,在实际中,如果需要,可以用strdup()来建立一个源字符串的副本。
*/
void testStrSplit2()
{
	char s[64] = "To be or not to be";
	char *delm = " ";
	char *result = NULL;
	int i =0,len = strlen(s);
	for (i =0;i<len;i++){
		printf("%c ", s[i]);
	}
	printf("\n");
	for (i=0;i<len;i++){
		printf("%d ",(int)s[i]);
	}
	printf("\n");

	result = strtok(s, delm);

	while (result != NULL){
		for (i=0;i<len;i++){
			printf("%d ",(int)s[i]);
		}
		printf("\n");
		result = strtok(NULL, delm);
	}
}

void testStrSplit3()
{
	char s[64] = "Hello World";
	char *delm = " ";
	char *result = NULL, *ptr = NULL;

	printf("Source:%p\n", s);
	//char *strtok_r(char *str, const char *delim, char **saveptr);
	//strtok_r()是Linux下的strtok()的可重入版本(线程安全版本),它比strtok()多了一个参数 saveptr ,这个参数用于在分割字符串时保存上下文。
	result = strtok_r(s, delm, &ptr);

	while (result != NULL){
		printf("Result:%p\t", result);
		printf("Saveptr:%p\t", ptr);
		printf("---%s\t", result);
		printf("---%s\n", ptr);
		//可以看到,saveptr这个指针在每次调用strtok_r()后就指向了未分割的部分的首地址。相对地,strtok()则是在内部有一个静态缓冲区,通过这个静态缓冲区来记录未处理的起始位置,所以strtok()不是线程安全的。
		result = strtok_r(NULL, delm, &ptr);
	}

}

//因为和strtok()的这个不同之处,strsep不需要区分第一次调用后后续的连续调用,可以用统一的操作来对字符串进行分割。
void testStrSplit4()
{
	char s[64] = "To be or not to be";
	char *source = s;
	char *delm = " ";
	char *result = NULL;

	while (source != NULL){
		printf("Source: %s | ", source);
		result = strsep(&source,delm);
		printf("result: %s | ", result);
	}
}

void testStrMatch()
{
	char *s = "To be or not to be.";
	char *p = "be";
	//strstr()返回字符串needle在字符串haystack中第一次出现的位置;如果没有匹配,则返回 NULL 。
	printf("%s\n", strstr(s, p));
}

void testStrDup()
{
	char *s = "aabbccddee";
	char *dup = strdup(s);
	//strdup()调用malloc()分配一块内存并将字符串s的内容拷贝进去,产生s的副本。要注意的是,在最后应该调用free()来释放副本。
	printf("%s\n",dup);
	free(dup);

	char *s1 = "poiuytrewq";
	//strndup()和strdup()类似,但最多只拷贝s的前n个字节。如果s的长度大于n,还会在副本后添加终止符。
	char *dup1 = strndup(s1, 4);
	printf("%s\n",dup1);
	free(dup1);

	//strdupa()和strdup()类似,但在分配内存时,它使用alloca()而不是malloc()。
	//strndupa()之于strdupa()就如strndup()之于strdup(),不再赘述。
}

/*
函数名: swab
功  能: 交换字节
用  法: void swab (char *from, char *to, int nbytes);
*/
void testStringSwab()
{
	char source[15] = "rFna koBlrna d";
	char target[15];

	swab(source,target,strlen(source));
	printf("%s\n",target);
}

/*
函数名: strupr
功  能: 将串中的小写字母转换为大写字母
用  法: char *strupr(char *str);
*/
void testStrUpper()
{
	char string[100] = "abcdefghijklmnopqrstuvwxyz";
	char *ptr;

   /* converts string to upper case characters */
	ptr = strupr_t(string);
	printf("%s\n", ptr);
}

/*
函数名: strtol
功  能: 将串转换为长整数
用  法: long strtol(char *str, char **endptr, int base);
*/
void testStrtol()
{
	char *string = "87654321", *endptr;
	long lnumber;

   /* strtol converts string to long integer  */
	lnumber = strtol(string, &endptr, 10);
	printf("string = %s  long = %ld\n", string, lnumber);
}

/*
函数名: strtod
功  能: 将字符串转换为double型值
用  法: double strtod(char *str, char **endptr);
*/
void testStrtod()
{
	char input[80], *endptr;
	double value;

	printf("Enter a floating point number:");
	gets(input);
	value = strtod(input, &endptr);
	printf("The string is %s the number is %lf\n", input, value);
}

/*
函数名: strspn
功  能: 在串中查找指定字符集的子集的第一次出现
用  法: int strspn(char *str1, char *str2);
*/
void testStrspan()
{
	char *string1 = "1234567890";
	char *string2 = "123DC8";
	int length;

	length = strspn(string1, string2);
	printf("Character where strings differ is at position %d\n", length);
}


/*
函数名: strset
功  能: 将一个串中的所有字符都设为指定字符
用  法: char *strset(char *str, char c);
*/
// void testStrset()
// {
// 	char string[10] = "123456789";
// 	char symbol = 'c';

// 	printf("Before strset(): %s\n", string);
// 	strset(string, symbol);
// 	printf("After strset():  %s\n", string);

// }

/*
函数名: strnset
功  能: 将一个串中的n个字符都设为指定字符
用  法: char *strnset(char *str, char ch, unsigned n);
*/
// void testStrnset()
// {
// 	char *string = "abcdefghijklmnopqrstuvwxyz";
// 	char letter = 'x';

// 	printf("string before strnset: %s\n", string);
// 	strnset(string, letter, 13);
// 	printf("string after  strnset: %s\n", string);
// }

void strrev(char *head)
{
  if (!head) return;
  char *tail = head;
  // find the 0 terminator, like head+strlen
  while(*tail) ++tail;
  // tail points to the last real char
  --tail;               
  // head still points to the first
  for( ; head < tail; ++head, --tail) {
      // walk pointers inwards until they meet or cross in the middle
      char h = *head, t = *tail;
      *head = t;           // swapping as we go
      *tail = h;
  }
}

void reverse(char s[])
{
    int length = strlen(s) ;
    int c, i, j;

    for (i = 0, j = length - 1; i < j; i++, j--)
    {
        c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

/*
函数名: strrev
功  能: 串倒转
用  法: char *strrev(char *str);
*/
void testStrrev()
{
	char forward[15] = "string";

	printf("Before strrev(): %s\n", forward);
	strrev(forward);
	printf("After strrev():  %s\n", forward);
	reverse(forward);
	printf("After reverse():  %s\n", forward);
}


/*
函数名: strcspn
功  能: 在串中查找第一个给定字符集内容的段
用  法: int strcspn(char *str1, char *str2);
*/
void testStrcspn()
{
	char *string1 = "1234567890";
	char *string2 = "747DC8";
	int length;

	length = strcspn(string1, string2);
	printf("Character where strings intersect is at position %d\n", length);
}



/*
atof(将字符串转换成浮点型数)
atoi(将字符串转换成整型数)
atol(将字符串转换成长整型数)
strtod(将字符串转换成浮点数)
strtol(将字符串转换成长整型数)
strtoul(将字符串转换成无符号长整型数)
toascii(将整型数转换成合法的ASCII 码字符)
toupper(将小写字母转换成大写字母)
tolower(将大写字母转换成小写字母)
atof(将字符串转换成浮点型数)
*/

int main(int argc, char *argv[])
{
	testStrCpy();
	testStrCmp();
	testStrCat();
	testStrSearch();
	testStrSplit();
	testStrSplit2();
	testStrSplit3();
	testStrSplit4();
	testStrMatch();
	testStrDup();

	testStrspan();
	// testStrset();
	// testStrnset();
	testStrrev();
	testStrcspn();
	testStrtod();
	testStrtol();
	testStrUpper();
	return 0;
}

How do I install Java on Mac OSX allowing version switching?

原文链接

note: These solutions work for various versions of Java including Java 8 and the new Java 13, and for any other previous Java version covered by the listed version managers. This includes alternative JDK’s from OpenJDK, Oracle, IBM, Azul, Amazon Correto, Graal and more. Easily work with Java 7, Java 8, Java 9, Java 10, Java 11, Java 12, and Java 13!

You have a few options of how to do the installation as well as manage JDK switching. Installation can be done by Homebrew, SDKMANJabba, or a manual install. Switching can be done by JEnvSDKMANJabba, or manually by setting JAVA_HOME. All of these are described below.


Installation

First, install Java using whatever method you prefer including Homebrew, SDKMAN or a manual install of the tar.gz file. The advantages of a manual install is that the location of the JDK can be placed in a standardized location for Mac OSX.

Install with SDKMAN

This is a simple model in that it handles both installation and version switching, with a caveat that it installs the JDK into a non-standard directory.

<see below “Installing and Switching versions with SDKMAN”>

Install using Jabba

This is also a simple model in that both installation and version switching are handled by the same tool. The installations are made to a non-standard directory.

<see below “Installing and Switching versions with Jabba”>

Install manually from OpenJDK download page:

  1. Download OpenJDK for Mac OSX from http://jdk.java.net/ (for example Java 13)
  2. Unarchive the OpenJDK tar, and place the resulting folder (i.e. jdk-13.jdk) into your /Library/Java/JavaVirtualMachines/ folder since this is the standard and expected location of JDK installs. You can also install anywhere you want in reality.

Install with Homebrew

The version of Java available in Homebrew Cask previous to October 3, 2018 was indeed the Oracle JVM. Now however, it has now been updated to OpenJDK. Be sure to update Homebrew and then you will see the lastest version available for install.

  1. install Homebrew if you haven’t already. Make sure it is updated:
    brew update
  2. Add the casks tap, if you haven’t already (or you are not seeing older Java versions anymore with step #3):
    brew tap homebrew/cask-versions

    and for the AdoptOpenJDK versions, add that tap:

    brew tap adoptopenjdk/openjdk

    These casks change their Java versions often, and there might be other taps out there with additional Java versions.

  3. Look for installable versions:
    brew search java   

    or for AdoptOpenJDK versions:

    brew search jdk     
  4. Check the details on the version that will be installed:
    brew cask info java

    or for the AdoptOpenJDK version:

    brew cask info adoptopenjdk
  5. Install a specific version of the JDK such as java11adoptopenjdk8, or just java or adoptopenjdk for the current. For example:
    brew cask install java

    You can use the fully qualified path to older versions as well:

    brew cask install homebrew/cask-versions/java11

And these will be installed into /Library/Java/JavaVirtualMachines/ which is the traditional location expected on Mac OSX.

Other installation options:

Some other flavours of openJDK are:

Azul Systems Java Zulu certified builds of OpenJDK can be installed by following the instructions on their site.

Zulu® is a certified build of OpenJDK that is fully compliant with the Java SE standard. Zulu is 100% open source and freely downloadable. Now Java developers, system administrators, and end users can enjoy the full benefits of open source Java with deployment flexibility and control over upgrade timing.

Amazon Correto OpenJDK builds have an easy to use an installation package for version 8 or version 11 (other versions are coming), and installs to the standard /Library/Java/JavaVirtualMachines/ directory on Mac OSX.

Amazon Corretto is a no-cost, multiplatform, production-ready distribution of the Open Java Development Kit (OpenJDK). Corretto comes with long-term support that will include performance enhancements and security fixes. Amazon runs Corretto internally on thousands of production services and Corretto is certified as compatible with the Java SE standard. With Corretto, you can develop and run Java applications on popular operating systems, including Linux, Windows, and macOS.


Where is my JDK?!?!

To find locations of previously installed Java JDK’s installed at the default system locations, use:

/usr/libexec/java_home -V

Matching Java Virtual Machines (6):
13, x86_64: “OpenJDK 13” /Library/Java/JavaVirtualMachines/openjdk-13.jdk/Contents/Home 12, x86_64: “OpenJDK 12” /Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home
11, x86_64: “Java SE 11” /Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home
10.0.2, x86_64: “Java SE 10.0.2” /Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home
9, x86_64: “Java SE 9” /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home
1.8.0_144, x86_64: “Java SE 8” /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home

You can also report just the location of a specific Java version using -v. For example for Java 13:

/usr/libexec/java_home -v 13

/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home

Knowing the location of the installed JDK’s is also useful when using tools like JEnv, or adding a local install to SDKMAN, or linking a system JDK in Jabba — and you need to know where to find them.

If you need to find JDK’s installed by other tools, check these locations:

  • SDKMAN installs to ~/.sdkman/candidates/java/
  • Jabba installs to ~/.jabba/jdk

Switching versions manually

The Java executable is a wrapper that will use whatever JDK is configured in JAVA_HOME, so you can change that to also change which JDK is in use.

For example, if you installed or untar’d JDK 13 to /Library/Java/JavaVirtualMachines/jdk-13.jdk if it is the highest version number it should already be the default, if not you could simply set:

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home

And now whatever Java executable is in the path will see this and use the correct JDK.

Using the /usr/libexec/java_home utility as previously described helps you to create aliases or to run commands to change Java versions by identifying the locations of different JDK installations. For example, creating shell aliases in your .profile or .bash_profile to change JAVA_HOME for you:

export JAVA_8_HOME=$(/usr/libexec/java_home -v1.8)
export JAVA_9_HOME=$(/usr/libexec/java_home -v9)
export JAVA_10_HOME=$(/usr/libexec/java_home -v10)
export JAVA_11_HOME=$(/usr/libexec/java_home -v11)
export JAVA_12_HOME=$(/usr/libexec/java_home -v12)
export JAVA_13_HOME=$(/usr/libexec/java_home -v13)

alias java8='export JAVA_HOME=$JAVA_8_HOME'
alias java9='export JAVA_HOME=$JAVA_9_HOME'
alias java10='export JAVA_HOME=$JAVA_10_HOME'
alias java11='export JAVA_HOME=$JAVA_11_HOME'
alias java12='export JAVA_HOME=$JAVA_12_HOME'
alias java13='export JAVA_HOME=$JAVA_13_HOME'

# default to Java 13
java13

Then to change versions, just use the alias.

java8
java -version

java version “1.8.0_144”

Of course, setting JAVA_HOME manually works too!


Switching versions with JEnv

JEnv expects the Java JDK’s to already exist on the machine and can be in any location. Typically you will find installed Java JDK’s in /Library/Java/JavaVirtualMachines/. JEnv allows setting the global version of Java, one for the current shell, and a per-directory local version which is handy when some projects require different versions than others.

  1. Install JEnv if you haven’t already, instructions on the site http://www.jenv.be/ for manual install or using Homebrew.
  2. Add any Java version to JEnv (adjust the directory if you placed this elsewhere):
    jenv add /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home
  3. Set your global version using this command:
    jenv global 13

You can also add other existing versions using jenv add in a similar manner, and list those that are available. For example Java 8:

jenv add /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home 
jenv versions

See the JEnv docs for more commands. You may now switch between any Java versions (Oracle, OpenJDK, other) at any time either for the whole system, for shells, or per local directory.

To help manage JAVA_HOME while using JEnv you can add the export plugin to do this for you.

$ jenv enable-plugin export
  You may restart your session to activate jenv export plugin echo export plugin activated

The export plugin may not adjust JAVA_HOME if it is already set, so you may need to clear this variable in your profile so that it can be managed by JEnv.

You can also use jenv exec <command> <parms...> to run single commands with JAVA_HOME and PATH set correctly for that one command, which could include opening another shell.


Installing and Switching versions with SDKMAN

SDKMAN is a bit different and handles both the install and the switching. SDKMAN also places the installed JDK’s into its own directory tree, which is typically ~/.sdkman/candidates/java. SDKMAN allows setting a global default version, and a version specific to the current shell.

  1. Install SDKMAN from https://sdkman.io/install
  2. List the Java versions available to make sure you know the version ID
    sdk list java
  3. Install one of those versions, for example, Java 13:
    sdk install java 13.0.0-open 
  4. Make 13 the default version:
    sdk default java 13.0.0-open

    Or switch to 13 for the session:

    sdk use java 13.0.0-open

When you list available versions for installation using the list command, you will see a wide variety of distributions of Java:

sdk list java

And install additional versions, such as JDK 8:

sdk install java 8.0.181-oracle

SDKMAN can work with previously installed existing versions. Just do a local install giving your own version label and the location of the JDK:

sdk install java my-local-13 /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home

And use it freely:

sdk use java my-local-13

More information is available in the SDKMAN Usage Guide along with other SDK’s it can install and manage.

SDKMAN will automatically manage your PATH and JAVA_HOME for you as you change versions.


Installing and Switching versions with Jabba

Jabba also handles both the install and the switching. Jabba also places the installed JDK’s into its own directory tree, which is typically ~/.jabba/jdk.

  1. Install Jabba by following the instructions on the home page.
  2. List available JDK’s
    jabba ls-remote
  3. Install Java JDK 12
    jabba install openjdk@1.12.0
  4. Use it:
    jabba use openjdk@1.12.0

You can also alias version names, link to existing JDK’s already installed, and find a mix of interesting JDK’s such as GraalVM, Adopt JDK, IBM JDK, and more. The complete usage guide is available on the home page as well.

Jabba will automatically manage your PATH and JAVA_HOME for you as you change versions.

shell后台并发执行实践

shell如何在后台执行

1.nohup命令
通常我们都是远程登录linux终端,而当我们退出终端时在之前终端运行的程序都会终止,有时候先想要退出终端也要程序继续执行这时nohup就登场了。nohup命令可以将程序以忽略挂起信号的方式运行起来,被运行的程序的输出信息将不会显示到终端。
nohup command > myout.file 2>&1 &

2.&后台执行
在命令后面加 & 可以让程序在后台执行
command &

3.Ctrl + z
当一个程序正在执行并且占用当前终端时我们同时按下 Ctrl + z ,这样就会把正在执行的前台程序放到后台挂起。

并发执行

1.正常执行

#!/bin/bash
Njob=10    #任务总数
for ((i=0; i<$Njob; i++)); do
{
	echo  "progress $i is sleeping for 1 seconds zzz…"
	sleep  1
}
done
echo -e "time-consuming: $SECONDS seconds"    #显示脚本执行耗时

执行结果

progress 0 is sleeping for 1 seconds zzz…
progress 1 is sleeping for 1 seconds zzz…
progress 2 is sleeping for 1 seconds zzz…
progress 3 is sleeping for 1 seconds zzz…
progress 4 is sleeping for 1 seconds zzz…
progress 5 is sleeping for 1 seconds zzz…
progress 6 is sleeping for 1 seconds zzz…
progress 7 is sleeping for 1 seconds zzz…
progress 8 is sleeping for 1 seconds zzz…
progress 9 is sleeping for 1 seconds zzz…
-e time-consuming: 10 seconds

2.并发后台执行

#!/bin/bash
Njob=10
for ((i=0; i<$Njob; i++)); do
    echo  "progress $i is sleeping for 3 seconds zzz…"
    sleep  3 &       #循环内容放到后台执行
done
wait      #等待循环结束再执行wait后面的内容
echo -e "time-consuming: $SECONDS seconds"    #显示脚本执行耗时

执行结果

progress 0 is sleeping for 3 seconds zzz…
progress 1 is sleeping for 3 seconds zzz…
progress 2 is sleeping for 3 seconds zzz…
progress 3 is sleeping for 3 seconds zzz…
progress 4 is sleeping for 3 seconds zzz…
progress 5 is sleeping for 3 seconds zzz…
progress 6 is sleeping for 3 seconds zzz…
progress 7 is sleeping for 3 seconds zzz…
progress 8 is sleeping for 3 seconds zzz…
progress 9 is sleeping for 3 seconds zzz…
-e time-consuming: 3 seconds

这种方式从功能上实现了使用shell脚本并行执行多个循环进程,但是它缺乏控制机制。

for设置了Njob次循环,同一时间Linux就触发Njob个进程一起执行。假设for里面执行的是scp,在没有pam_limits和cgroup限制的情况下,很有可能同一时刻过多的scp任务会耗尽系统的磁盘IO、连接数、带宽等资源,导致正常的业务受到影响。

一个应对办法是在for循环里面再嵌套一层循环,这样同一时间,系统最多只会执行内嵌循环限制值的个数的进程。不过还有一个问题,for后面的wait命令以循环中最慢的进程结束为结束(水桶效应)。如果嵌套循环中有某一个进程执行过程较慢,那么整体这一轮内嵌循环的执行时间就等于这个“慢”进程的执行时间,整体下来脚本的执行效率还是受到影响的。

分批并行的方式并发执行

#!/bin/bash
NQ=3
num=5
for ((i=0; i<$NQ; i++)); do
     for ((j=0; j<$num; j++)); do
         echo  "progress $i is sleeping for 3 seconds zzz…"
        sleep 3 &
     done
     wait
 done
#等待循环结束再执行wait后面的内容
echo -e "time-consuming: $SECONDS    seconds"    #显示脚本执行耗时

执行结果

 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
-e time-consuming: 9    seconds

3.使用模拟队列来控制进程数量

要控制后台同一时刻的进程数量,需要在原有循环的基础上增加管理机制。

一个方法是以for循环的子进程PID做为队列元素,模拟一个限定最大进程数的队列(只是一个长度固定的数组,并不是真实的队列)。队列的初始长度为0,循环每创建一个进程,就让队列长度+1。当队列长度到达设置的并发进程限制数之后,每隔一段时间检查队列,如果队列长度还是等于限制值,那么不做操作,继续轮询;如果检测到有并发进程执行结束了,那么队列长度-1,轮询检测到队列长度小于限制值后,会启动下一个待执行的进程,直至所有等待执行的并发进程全部执行完。

#!/bin/bash
Njob=15 #任务总数
Nproc=5 #最大并发进程数

function PushQue {      #将PID值追加到队列中
           Que="$Que $1"
           Nrun=$(($Nrun+1))
}

function GenQue {       #更新队列信息,先清空队列信息,然后检索生成新的队列信息
           OldQue=$Que
           Que=""; Nrun=0
           for PID in $OldQue; do
                 if [[ -d /proc/$PID ]]; then
                        PushQue $PID
                 fi
           done
}

function ChkQue {       #检查队列信息,如果有已经结束了的进程的PID,那么更新队列信息
           OldQue=$Que
           for PID in $OldQue; do
                 if [[ ! -d /proc/$PID ]];   then
                 GenQue; break
                 fi
           done
}

for ((i=1; i<=$Njob; i++)); do
           echo "progress $i is sleeping for 3 seconds zzz…"
           sleep 3 &
           PID=$!
           PushQue $PID
           while [[ $Nrun -ge $Nproc ]]; do          # 如果Nrun大于Nproc,就一直ChkQue
                 ChkQue
                 sleep 0.1
           done
done
wait
echo -e "time-consuming: $SECONDS   seconds"    #显示脚本执行耗时

执行结果

progress 1 is sleeping for 3 seconds zzz…
progress 2 is sleeping for 3 seconds zzz…
progress 3 is sleeping for 3 seconds zzz…
progress 4 is sleeping for 3 seconds zzz…
progress 5 is sleeping for 3 seconds zzz…
progress 6 is sleeping for 3 seconds zzz…
progress 7 is sleeping for 3 seconds zzz…
progress 8 is sleeping for 3 seconds zzz…
progress 9 is sleeping for 3 seconds zzz…
progress 10 is sleeping for 3 seconds zzz…
progress 11 is sleeping for 3 seconds zzz…
progress 12 is sleeping for 3 seconds zzz…
progress 13 is sleeping for 3 seconds zzz…
progress 14 is sleeping for 3 seconds zzz…
progress 15 is sleeping for 3 seconds zzz…
-e time-consuming: 3   seconds

这种使用队列模型管理进程的方式在控制了后台进程数量的情况下,还能避免个别“慢”进程影响整体耗时的问题:

4.使用fifo管道特性来控制进程数量

管道是内核中的一个单向的数据通道,同时也是一个数据队列。具有一个读取端与一个写入端,每一端对应着一个文件描述符。
命名管道即FIFO文件,通过命名管道可以在不相关的进程之间交换数据。FIFO有路径名与之相关联,以一种特殊设备文件形式存在于文件系统中。

FIFO有两种用途:

• FIFO由shell使用以便数据从一条管道线传输到另一条,为此无需创建临时文件,常见的操作cat file|grep keyword就是这种使用方式;
• FIFO用于客户进程-服务器进程程序中,已在客户进程与服务器进程之间传送数据,下面的例子将使用这种方式。

根据FIFO文件的读规则(参考http://www.cnblogs.com/yxmx/articles/1599187.html),如果有进程写打开FIFO,且当前FIFO内没有数据,对于设置了阻塞标志的读操作来说,将一直阻塞状态。

利用这一特性可以实现一个令牌机制。设置一个行数等于限定最大进程数Nproc的fifo文件,在for循环中设置创建一个进程时先read一次fifo文件,进程结束时再write一次fifo文件。如果当前子进程数达到限定最大进程数Nproc,则fifo文件为空,后续执行的并发进程被读fifo命令阻塞,循环内容被没有触发,直至有某一个并发进程执行结果并做写操作(相当于将令牌还给池子)。

需要注意的是,当并发数较大时,多个并发进程即使在使用sleep相同秒数模拟时,也会存在进程调度的顺序问题,因而并不是按启动顺序结束的,可能会后启动的进程先结束。

#!/bin/bash

Njob=15 #任务总数

Nproc=5 #最大并发进程数

mkfifo ./fifo.$$ && exec   9<>  ./fifo.$$     #通过文件描述符777访问fifo文件

for ((i=0; i<$Nproc; i++)); do  #向fifo文件先填充等于Nproc值的行数
  echo  "init time add $i" >&9
done
for ((i=0; i<$Njob; i++)); do
{
  read  -u  9             #从fifo文件读一行
  echo  "progress $i is sleeping for 3 seconds zzz…"
  sleep  3
  echo  "real time add $(($i+$Nproc))"  1>&9 #sleep完成后,向fifo文件重新写入一行
} &
done
wait
echo -e "time-consuming: $SECONDS seconds"
rm -f ./fifo.$$

执行结果

progress 0 is sleeping for 3 seconds zzz…
progress 1 is sleeping for 3 seconds zzz…
progress 2 is sleeping for 3 seconds zzz…
progress 3 is sleeping for 3 seconds zzz…
progress 4 is sleeping for 3 seconds zzz…
progress 5 is sleeping for 3 seconds zzz…
progress 6 is sleeping for 3 seconds zzz…
progress 8 is sleeping for 3 seconds zzz…
progress 12 is sleeping for 3 seconds zzz…
progress 13 is sleeping for 3 seconds zzz…
progress 9 is sleeping for 3 seconds zzz…
progress 11 is sleeping for 3 seconds zzz…
progress 14 is sleeping for 3 seconds zzz…
progress 10 is sleeping for 3 seconds zzz…
progress 7 is sleeping for 3 seconds zzz…
-e time-consuming: 10 seconds

原文地址:
Shell脚本实现并发多进程
Shell脚本并发执行

OKR 填写指南

一、OKR概述
OKR是一个目标管理工具。其中O指Objective,是团队或个人的工作目标;KR指Key Result,是一系列可以衡量的关键结果,用来判断Objective是否达成。在作业帮,OKR的制定和共享,是公司、团队和个人制定任务,对齐目标,协调和集中精力的重要手段。

二、OKR执行要点
工作目标设置应该激进,要使自己和团队感受到压力。
关键结果要容易打分衡量,不要模棱两可。
除保密事项外,OKR尽可能的公开,方便互相了解在忙些什么工作。
写好OKR,要求严格的聚焦,只写最重要的目标,不罗列堆砌。
OKR并不是绩效考核工具,但应该是自我检测工作成果的重要工具。
OKR不是一个共享的工作清单或者to-do list,而是一个管理精力,自我规划的重要工具。
没有经过沟通和对齐的OKR,相当于没有写,OKR的对齐方包括你的上级、同事和你承接需求(提出需求)的协同方。

三、设定激进、聚焦的目标

在每个OKR周期中,公司从CEO到全体员工都会制定自己的OKR,通常是三四个Objectives,每个Objective有3个左右的Key Results。这些OKR既有从上到下目标分解而来的,也有从基层收集的各种意见演化而来,所以OKR的制定是一个循环迭代和讨论的过程。

我们希望每一个人都制定激进的目标,这些目标看上去刚好处于 “这个周期完不成” 的边缘。事实证明,制定高目标,有助于我们取得优于普通水准的成果。 每个高目标都需要全情投入。所以OKR制定的另一方面,是严格地聚焦到重点方向,不贪多。但在选定的重点方向上,力求保证目标达成。

一些设定Objective的窍门:

选三五个目标即可,宁缺勿滥。太多目标,工作容易失去焦点,团队也会疲于应付。
描述最终状态,比如“上线xx功能”,“获得10%的市场份额”。
常规动作不要写进OKR,例如“继续推进”、“保持行业地位”。如果确是长期重要工作,要思考如何拆解成合理的周期OKR,而不是不经思考地写“继续做”。
团队leader是团队的大脑,更要注意排优先级和授权。

四、可衡量的Key Result

关键结果(Key Result,或简称KR)是用来评价目标是否达成的。考虑到这个功能,对关键结果最大的要求就是容易衡量,且直接支持目标的达成。

一些设定Key Result的窍门:

每个目标定3个左右KR即可,不多刻意多写。
注意,KR是用来衡量Objective的达成程度的,问一下自己“这个KR和相应的Objective有直接的支撑关系吗”。
要描述产出,而不是动作。产出是指类似于“发表一篇论文”这样的表述,而动作是指“进行研究并撰写论文”这样的表述。所以当一个KR中出现 “参与”、“分析”、“辅助”这样的词汇,或者描述过程的动词特别多时,可能就有问题了。
用客观、外部可观测和不模糊的表述。最好是你的同事也能够准确地对你的KR进行打分,这说明KR足够清晰。
五、常见错误
执行不好的OKR,不但对公司、团队和个人没有帮助,反而可能造成团队安于现状,甚至方向混淆、内耗。所以我们要避免常见的OKR执行错误,比如下面这些:

堆砌不重要的目标
OKR常常被写得很长、很多,很难用两三句阐述其中的重点。往往这样的OKR包含大量的常规工作。常规工作不是不可以写,而是要判断,这项工作需要我和团队付出额外的努力才能达成吗。因为OKR要求激进,“担心有可能完不成”,相当多的常规目标是达不到这个要求的。这里有两点需要澄清:一、OKR不是工作量的衡量工具,并且我们应当主要关注OKR对业务带来的实际效果,所以不必在OKR中堆砌工作量;二、避免堆砌,要求每个人有判断力,自觉抛弃低优先级的目标和低价值的目标,而将精力和资源放到高优先级目标上。可以问自己几个问题,“这件事不做有什么影响吗”,“这件事做了有实际的业务收益吗”。

只写一个单词
写OKR时容易偷懒,只写一个单词,比如“收入” “DAU” “xx项目”。这样的写法,既没有说明最终要达到的状态,也很难客观打分。同时在沟通和对齐的时候,其他同事也很难看懂。类似的,“其他项目”、“重点项目”也是不可取的Objective,因为项目真的重要话,应该明确说出来要达到什么目标;而归为“其他”的目标,通常可能并不重要。

给自己留余量
如果历史上所有的OKR都轻松达成,可能定目标的时候就不够有雄心。

六、沟通、对齐和进度更新
沟通和对齐有几个重要的作用。首先是在重要的方向上,配合团队和上下级形成合力,而不要往不同方向使力。其次,OKR的制定是一个迭代过程,从草稿到定稿,需要方方面面的意见输入。再次,要了解配合团队定OKR时的激进程度,判断如果配合团队的OKR只能完成0.5分,会不会影响自己的工作。另一方面,应该鼓励经常更新OKR进度,既是对自己的提醒鞭策,又是向同事同步信息的好方式。

查理·芒格:如何过上痛苦的生活?

在大学毕业典礼上,诞生过很多著名的演讲,如斯蒂夫·乔布斯 2005 年在斯坦福毕业典礼上的演讲,如 JK·罗琳 2008 年在哈佛大学毕业典礼上的演讲。大多数毕业演讲者会选择描述如何获得幸福的生活,而今天的作者查理·芒格(Charlie Thomas Munger)使用 逆向思维的原则,令人信服地从反面阐述了一名毕业生 如何才能过上痛苦的生活。至于那些宁愿继续保持无知和郁闷的读者,建议你们千万别阅读这篇讲稿。

痛苦人生的药方

在我听过的 20 次哈佛学校的毕业演讲中,哪次曾让我希望它再长些呢?这样的演讲只有约翰尼·卡森的那一次,他详述了保证痛苦人生的卡森药方。所以呢,我决定重复卡森的演讲,但以更大的规模,并加上我自己的药方。毕竟,我比卡森演讲时岁数更大,同一个年轻的有魅力的幽默家相比,我失败的次数更多,痛苦更多,痛苦的方式也更多。我显然很有资格进一步发挥卡森的主题。

那时卡森说他无法告诉毕业的同学如何才能得到幸福,但能够根据个人经验,告诉他们如何保证自己过上痛苦的生活。卡森给的确保痛苦生活的处方包括:

  1. 为了改变心情或者感觉而使用化学物质;

  2. 妒忌,以及

  3. 怨恨。

我现在还能想起来当时卡森用言之凿凿的口气说,他一次又一次地尝试了这些东西,结果每次都变得很痛苦。

要理解卡森为痛苦生活所开处方的第一味药物(使用化学物质)比较容易。我想补充几句。我年轻时最好的朋友有四个,他们非常聪明、正直和幽默,自身条件和家庭背景都很出色。其中两个早已去世,酒精是让他们早逝的一个因素;第三个人现在还醉生梦死地活着——假如那也算活着的话。

虽然易感性因人而异,我们任何人都有可能通过一个开始时难以察觉直到堕落之力强大到无法冲破的细微过程而染上恶瘾。不过呢,我活了 60 年,倒是没有见过有谁的生活因为害怕和避开这条诱惑性的毁灭之路而变得更加糟糕。

妒忌,和令人上瘾的化学物质一样,自然也能获得导致痛苦生活的大奖。早在遭到摩西戒律的谴责之前,它就已造成了许多大灾难。如果你们希望保持妒忌对痛苦生活的影响,我建议你们千万别去阅读塞缪尔·约翰逊(编者注:Samuel Johnson,1709——1784,英国作家,文学研究者和批评家)的任何传记,因为这位虔诚基督徒的生活以令人向往的方式展示了超越妒忌的可能性和好处。

就像卡森感受到的那样,怨恨对我来说也很灵验。如果你们渴望过上痛苦的生活,我找不到比它更灵的药方可以推荐给你们了。约翰逊说得好,他说生活本已艰辛得难以下咽,何必再将它塞进怨恨的苦涩果皮里呢。

对于你们之中那些想得到痛苦生活的人,我还要建议你们别去实践狄斯雷利的权宜之计,它是专为那些无法彻底戒掉怨恨老习惯的人所设计的。在成为伟大的英国首相的过程中,迪斯雷利学会了不让复仇成为行动的动机,但他也保留了某种发泄怨恨的办法,就是将那些敌人的名字写下来,放到抽屉里。然后时不时会翻看这些名字,自得其乐地记录下世界是怎样无须他插手就使他的敌人垮掉的。

本杰明·迪斯雷利(Benjamin Disraeli,1804 – 1881,英国保守党领袖,两度出任英国首相)

查理·芒格的四味药

好啦,卡森开的处方就说到这里。接下来是芒格另开的四味药。

第一,要反复无常,不要虔诚地做你正在做的事。只要养成这个习惯,你们就能够绰绰有余地抵消你们所有优点共同产生的效应,不管那种效应有多么巨大。如果你们喜欢不受信任并被排除在对人类贡献最杰出的人群之外,那么这味药物最适合你们。养成这个习惯,你们将会永远扮演寓言里那只兔子的角色,只不过跑得比你们快的不再只是一只优秀的乌龟,而是一群又一群平庸的乌龟,甚至还有些拄拐杖的平庸乌龟。

我必须警告你们,如果不服用我开出的第一味药,即使你们最初的条件并不好,你们也可能会难以过上痛苦的日子。我有个大学的室友,他以前患有严重的阅读障碍症,现在也是。但他算得上我认识的人中最可靠的。他的生活到目前为止很美满,拥有出色的太太和子女,掌管着某个数十亿美元的企业。如果你们想要避免这种传统的、主流文化的、富有成就的生活,却又坚持不懈地做到为人可靠,那么就算有其他再多的缺点,你们这个愿望恐怕也会落空。

说到「到目前为止很美满」这样一种生活,我忍不住想在这里引用克洛伊斯的话来再次强调人类生存状况那种「到目前为止」的那一面。克洛伊斯曾经是世界上最富裕的国王,后来沦为敌人的阶下囚,就在被活活烧死之前,他说:「哎呀,我现在才想起历史学家梭伦说过的那句话,『在生命没有结束之前,没有人的一生能够被称为是幸福的。』」

我为痛苦生活开出的第二味药是,尽可能从你们自身的经验获得知识,尽量别从其他人成功或失败的经验中广泛地吸取教训,不管他们是古人还是今人。这味药肯定能保证你们过上痛苦的生活,取得二流的成就。

只要看看身边发生的事情,你们就能明白拒不借鉴别人的教训所造成的后果。人类常见的灾难全都毫无创意——酒后驾车导致的身亡,鲁莽驾驶引起的残疾,无药可治的性病,加入毁形灭性的邪教的那些聪明的大学生被洗脑后变成的行尸走肉,由于重蹈前人显而易见的覆辙而导致的生意失败,还有各种形式的集体疯狂等等。你们若要寻找那条通往因为不小心、没有创意的错误而引起真正的人生麻烦的道路,我建议你们牢牢记住这句现代谚语:「人生就像悬挂式滑翔,起步没有成功就完蛋啦。」

避免广泛吸取知识的另一种做法是,别去钻研那些前辈的最好成果。这味药的功效在于让你们得到尽可能少的教育。

如果我再讲一个简短的历史故事,或许你们可以看得更清楚,从而更有效地过上与幸福无缘的生活。从前有个人,他勤奋地掌握了前人最优秀的成果,尽管开始研究分析几何的时候他的基础并不好,学得非常吃力。最终,他本人取得的成就引起了众人的瞩目,他是这样评价他自己的成果的:

「如果说我比其他人看得更远,那是因为我站在巨人的肩膀上。」

这人的骨灰如今埋在威斯敏斯特大教堂里,他的墓碑上有句异乎寻常的墓志铭:

「这里安葬着永垂不朽的艾萨克·牛顿爵士。」

我为你们的痛苦生活开出的第三味药是,当你们在人生的战场上遭遇第一、第二或者第三次严重的失败时,就请意志消沉,从此一蹶不振吧。因为即使是最幸运、最聪明的人,也会遇到许许多多的失败,这味药必定能保证你们永远地陷身在痛苦的泥沼里。请你们千万要忽略爱比克泰德(编者注:爱比克泰德出生在希拉波利斯城的奴隶家庭,而且患有终身残疾,他认为所有人都应该完全自由地掌握自己的生活,也应该与自然和谐相处。)亲自撰写的、恰如其分的墓志铭中蕴含的教训:「此处埋着爱比克泰德,一个奴隶,身体残疾,极其穷困,蒙受诸神的恩宠。」

为了让你们过上头脑混乱、痛苦不堪的日子,我所开的最后一味药是,请忽略小时候人们告诉我的那个乡下人故事。曾经有个乡下人说:「要是知道我会死在哪里就好啦,那我将永远不去那个地方。」大多数人和你们一样,嘲笑这个乡下人的无知,忽略他那朴素的智慧。如果我的经验有什么借鉴意义的话,那些热爱痛苦生活的人应该不惜任何代价避免应用这个乡下人的方法。若想获得失败,你们应该将这种乡下人的方法,也就是卡森在演讲中所用的方法,贬低得愚蠢之极、毫无用处。

「以避免失败为目标而成长」

卡森采用的研究方法是把问题反过来想。就是说要解出 X,得先研究如何才能得到非 X。伟大的代数学家雅各比用的也是卡森这种办法,众所周知,他经常重复一句话:「反过来想,总是反过来想。」雅各比知道事物的本质是这样的,许多难题只有在逆向思考的时候才能得到最好的解决。例如,当年几乎所有人都在试图修正麦克斯韦的电磁定律,以便它能够符合牛顿的三大运动定律,然而爱因斯坦却转了个 180 度大弯,修正了牛顿的定律,让其符合麦克斯韦的定律,结果他发现了相对论。

作为一个公认的传记爱好者,我认为假如查尔斯·罗伯特·达尔文是哈佛学校 1986 届毕业班的学生,他的成绩大概只能排到中等。然而现在他是科学史上的大名人。如果你们希望将来碌碌无为,那么千万不能以达尔文为榜样。

达尔文能够取得这样的成就,主要是因为他的工作方式。这种方式有悖于所有我列出的痛苦法则,而且还特别强调逆向思考:他总是致力于寻求证据来否定他已有的理论,无论他对这种理论有多么珍惜,无论这种理论是多么得之不易。与之相反,大多数人早年取得成就,然后就越来越拒绝新的、证伪性的信息,目的是让他们最初的结论能够保持完整。他们变成了菲利普·威利所评论的那类人:「他们固步自封,满足于已有的知识,永远不会去了解新的事物。」

达尔文的生平展示了乌龟如何可以在极端客观态度的帮助下跑到兔子前面去。这种态度能够帮助客观的人最后变成「蒙眼拼驴尾」游戏中惟一那个没有被遮住眼睛的玩家。

如果你们认为客观态度无足轻重,那么你们不但忽略了来自达尔文的训诲,也忽略了来自爱因斯坦的教导。爱因斯坦说他那些成功的理论来自「好奇、专注、毅力和自省」。他所说的自省,就是不停地试验与推翻他自己深爱的想法。

最后,尽可能地减少客观性,这样会帮助你减少获得世俗好处所需作出的让步以及所要承受的负担,因为客观态度并不只对伟大的物理学家和生物学家有效。它也能够帮助伯米吉地区的管道维修工更好地工作。因此,如果你们认为忠实于自己就是永远不改变你们年轻时的所有观念,那么你们不仅将会稳步地踏上通往极端无知的道路,而且还将走向事业中不愉快的经历给你带来的所有痛苦。

这次类似于说反话的演讲应该以类似于说反话的祝福来结束。这句祝语的灵感来自伊莱休·鲁特引用过的那首讲小狗去多佛的儿歌:「一步又一步,才能到多佛。」我祝福 1986 届毕业班的同学:

在座各位,愿你们在漫长的人生中日日以避免失败为目标而成长。■

节选自《穷查理宝典》(中信出版社)第四章,第一讲