first commit

This commit is contained in:
张乾 2024-10-15 21:07:49 +08:00
parent 58cbf6795b
commit 1b0c35dd30
115 changed files with 6918 additions and 21 deletions

0
README.md Normal file → Executable file
View File

88
crawl.php Normal file → Executable file
View File

@ -2,28 +2,74 @@
// Define the URL
$url = "https://learn.lianglianglee.com/";
// Send the GET request
$response = file_get_contents($url);
if ($response === FALSE) {
echo "Failed to access the URL.";
} else {
// Use regex to find the href values across multiple lines (with the 's' modifier)
preg_match_all('/<li><a href="([^"]*)">([^<]*)<\/a><\/li>/', $response, $matches);
# 1 获取文件主目录
// $response = file_get_contents($url);
// if ($response === FALSE) {
// echo "Failed to access the URL.";
// } else {
// // Use regex to find the href values across multiple lines (with the 's' modifier)
// preg_match_all('/<li><a href="([^"]*)">([^<]*)<\/a><\/li>/', $response, $matches);
// Prepare the output for the readme file
$output = "";
if (!empty($matches[1])) {
foreach ($matches[1] as $href) {
$output .= $href.PHP_EOL;
// // Prepare the output for the readme file
// $output = "";
// if (!empty($matches[1])) {
// foreach ($matches[1] as $href) {
// $output .= $href.PHP_EOL;
// }
// } else {
// $output = "No match found.\n";
// }
// // Write the results to readme.txt file
// file_put_contents("README.md", $output);
// echo "Results saved to readme.txt";
// }
# 2 生成不同目录的文件夹
// Path to the README.md file
$readmePath = 'README.md';
// Read the file into an array of lines
$lines = file($readmePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Remove spaces from each line
$folderName = str_replace(' ', '', $line);
$folderName = "/Users/01397713/Documents/github/learn-tech".$folderName;
$curlUrl = $url. urlencode($line);
$response = file_get_contents($curlUrl);
mkdir($folderName, 0777, true);
preg_match_all('/<a class="menu-item" id="([^"]*)" href="([^"]*)">([^<]*)<\/a>/', $response, $matches);
if (isset($matches[1])) {
$fileNameList = $matches[1];
$urlList = $matches[2];
foreach($fileNameList as $key => $name) {
$fileUlr = $url . $urlList[$key];
$fileContents = file_get_contents($fileUlr);
preg_match_all('/<div class="book-post">(.*?)<div id="prePage" style="float: left">/s', $fileContents, $divMatchs);
$a = '<meta charset="UTF-8">'.$divMatchs[1][0];
$doc = new DOMDocument();
libxml_use_internal_errors(true); // To handle any invalid HTML
$doc->loadHtml($a);
libxml_clear_errors();
$text = $doc->textContent;
$fileName = str_replace(' ', '', $name);
file_put_contents($folderName . '/'. $fileName, $text);
echo $fileName;
echo PHP_EOL;
sleep(2);
// preg_match_all('/<p>([^<]*)<\/p>/', $fileContents, $fileMatches);
}
} else {
$output = "No match found.\n";
}
// Write the results to readme.txt file
file_put_contents("README.md", $output);
echo "Results saved to readme.txt";
}
?>
}

View File

@ -0,0 +1,92 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 程序员解决的问题,大多不是程序问题
你好!我是郑晔,一个程序员。
很多人都说,程序员很辛苦,与这个角色联系在一起的词儿,通常是忙碌、加班、熬夜等。
作为程序员,我们将其看作一个值得全情投入的职业,希望能够把精力放在设计算法、改进设计、优化系统这些具有创造性与成就感的本职工作上。
但现实情况却是,许多人因为一些“意外”,陷入了无休止的忙碌,比如:
你辛辛苦苦写的代码还没上线,产品经理就告诉你需求变了;
你拼命加班只因错估了工作量,自己造的“孽”,含着泪也要搞定;
你累死累活做出来的东西和要求不符,只能从头再来;
你大面积地修改代码只是因为设计糟糕,无法适应新的需求变化;
……
诸如此类,不胜枚举。我们很辛苦,但耗费我们大量时间和精力去应付的工作,并不是技术工作,反而是这些看似很“不值当”的事儿。
为什么会这样?
软件行业里有一本名著叫《人月神话》其中提到两个非常重要的概念本质复杂度Essential Complexity和偶然复杂度Accident Complexity
简单来说,本质复杂度就是解决一个问题时,无论怎么做都必须要做的事,而偶然复杂度是因为选用的做事方法不当,而导致要多做的事。
比如你要做一个网站,网站的内容是你无论如何都要写的,这就是“本质复杂度”。而如果今天你还在用汇编写一个网站,效率是不可能高起来的,因为你选错了工具。这类选错方法或工具而引发的问题就是“偶然复杂度”。
作为一个在软件行业奋斗了近二十年的程序员,我深刻意识到一个遗憾的事实:大部分程序员忙碌解决的问题,都不是程序问题,而是由偶然复杂度导致的问题。
换句话说,只要选择了正确的做事方法,减少偶然复杂度带来的工作量,软件开发是可以有条不紊进行的。
如何减少偶然复杂度引发的问题,让软件开发工作有序、高效地进行,这正是我希望通过这个专栏帮你解决的问题。
许多人工作做事主要依靠直觉,在这个科学越发昌明的时代,我们清楚地看到,人类的直觉常常是错的,就像古人凭直觉认为大地是平的一样。
软件开发也不例外,如果你不曾在做软件这件事上有过学习和思考,形成一套高效的工作方法,只是凭直觉行事,在真实世界中往往会举步维艰。
幸运的是总会有不同的人在不同的方向上探索不同的做法一旦通过真实世界的验证就会沉淀出可供行业直接应用的最佳实践Best Practice
在软件行业中,这样能够提升工作效率的最佳实践已经有很多,但是,学习掌握这些最佳实践是有难度的,其根源就在于,很难找到这些实践彼此间的内在联系。
直觉大多是错误的,最佳实践又多而琐碎,所以在这个专栏中,我会尝试给你提供一个思考框架,帮你在遇到问题时梳理自己真正要做的事情。围绕着这个框架,我还会给你一些原则。
这些原则,是我从软件行业的诸多软件开发最佳实践中总结出来的,也是我如今在工作中所坚持的。这些原则就是一条主线,将各种最佳实践贯穿起来。
这些原则不多,总结起来就四个:
以终为始;
任务分解;
沟通反馈;
自动化。
也许看到这四个原则的名字,你会不以为然,这些说法你在很多地方都看到过,但我想与你分享的内容可能与你想的并不完全一致。
比如:你以为的“终”可能不是终,因为你只是站在自己的角度;你以为自己做了任务分解,在我看来,可能还不够,因为我希望你能够做到微操作;你以为的沟通反馈就是说话聊天,我想告诉你很多技术实践的存在也是为了沟通反馈;你以为自动化就是写代码,我会告诉你,有时候不写代码而解决问题,可能才是一个好方案。
在我看来,想要将精力聚焦在本质复杂度上,提高工作效率,摆脱直觉的束缚,只要掌握上面的四个原则就可以了。
或许你此时会问,这些原则很难吧?其实并不难,在探讨这个专栏的内容时,我的编辑作为软件开发的局外人,经常发出感叹:“这事真的就这么简单吗?这不就是正常做事应该有的逻辑吗?”
是的,就是这样简单,但大多数人没有这样做,因为这些原则在实际工作中很可能是反直觉的。只要打破思维误区,你的整个人都会变得不一样。
下面是整个专栏的目录,我希望能帮助你回答,或者厘清一些开发过程中,曾经遇到,又未曾深入的问题。
当我们详谈这些原则时,我会给你讲述一些最佳实践,让你看到这些原则是如何应用于不同的实践中的。希望我对这些实践的理解成为你的知识地图,让你拥有继续探索的方向。
我做这个专栏的原则是“授人以鱼,不如授人以渔”。我希望你很好地理解这些原则,掌握高效工作的方法。至于最佳实践,你可以自行决定,是直接采纳还是曲线救国更为合适。
介绍一下我自己,我是郑晔,目前在火币网担任首席架构师,写过代码、带过团队、做过咨询,创过业,还维护着一个拿过 Oracle Duke 选择奖的开源项目 Moco至今仍然在编程一线写着代码。
很长时间里,我一直对如何做好软件充满了好奇,了解过各种技术以及开发方法。做咨询的经历让我有机会见识到不同公司面临的问题;带团队的时候,我也看到很多小兄弟因为不会工作,虽然很努力却收效甚微;而我自己菜鸟时期的笨拙依然是历历在目。
在我看来,所有做软件的人能力都很强,这些问题都只是因为不会工作造成的,但更可怕的是,许多人深陷泥潭而不自知。
在这些年的工作里,我一遍又一遍给别人讲如何工作,逐渐总结出一套自己的工作原则,如今呈现在你面前的就是我这些年思考的总结。
我不指望所有人都能从这个专栏受益,我只想把这个专栏写给那些愿意成长的人。我只是来做一次信息分享,分享一些思考,分享一些做法,希望可以将你从常见的思维误区中带出来。
也许在这个专栏的最后,你发现自己并不认同我的原则,却能够用自己的原则来与我探讨,那么,恭喜你,因为那是最美妙的事情!

View File

@ -0,0 +1,150 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 10x程序员是如何思考的
你好,我是郑晔。
在开篇词中我们提到,程序员在工作中遇到的很多问题,大多不是程序问题,辛苦而低效的工作,多数是由偶然复杂度导致的。那这个由于偶然复杂度造成的差距会有多大呢?
1975年弗雷德里克·布鲁克斯Frederick Brooks出版了软件行业的名著《人月神话》他给出了一个统计结果优秀程序员的开发效率是普通程序员的10倍。40多年过去了这个数字得到了行业的普遍认同。
成为10x程序员是很多程序员的追求。但工作产出并不只是由写代码的效率决定的一些不恰当工作方法很大程度上影响着你的产出。
在接下来的这段时间里,我希望通过这个专栏和你一起探讨,作为一个程序员,该如何更高效地工作,怎样才能把时间和精力尽可能地放在处理本质复杂度的事情上,减少在偶然复杂度上的消耗。
作为整个课程第一讲,我就从我常用的一个思考框架开始。
一个思考框架
我曾经组织过针对应届毕业生的培训,第一堂课我往往亲自操刀,其中有一个头脑风暴的环节“畅想未来”,我会让大家思考三个问题:
我现在是个什么水平?
我想达到一个什么水平?
我将怎样到达那个目标?
大家会围绕着这三个问题,从各种角度展开讨论。这是一个有趣的练习,你会发现大家“最擅长”的是回答第一个问题:我现在处于什么水平?和有经验的人相比,他们大多自认为比较“菜”。但对于后两个问题的讨论,却可以切实看出人和人之间处理问题的能力差异。
有人通过之前的资料搜集,已经对自己的未来有了一个打算。比如想成为一个研发大牛,或者想做一个开源软件等,也就是说,对于第二个问题,他有明确的答案。
而有的人则是一脸茫然,他很可能根本没有考虑过这个问题。而从题目本身来看,目标相对清晰的同学,才会进入到第三个问题,而茫然的同学,则完全无从下手。
那么我为什么会问这几个问题呢?我是想让大家跳出现有的思考模式,摆脱仅凭直觉“闷头做事”的习惯方式,把低着的头抬起来,看一眼未来,给自己找一个方向。
否则,如果你对未来没有定位,是茫然的,尽管你也知道要努力,但劲儿该往哪里使呢?如果使劲的方向不对,那么你越使劲儿,可能会在错误的路上跑得越远。南辕北辙的道理大家都懂,但具体到自己的工作和发展上,真正能体会并实践的却是少数。
其实,这三个问题来自一个思考框架。在给其他公司团队做咨询时,我也经常会运用到它,原来的问题是:
Where are we?(我们现在在哪?)
Where are we going?(我们要到哪儿去?)
How can we get there?(我们如何到达那里?)
这三个问题实际上是帮我们确定:
现状;
目标;
实现路径。
如果一个人能够清晰地回答出这三个问题,通常意味着他对要做的事有着清晰的认识。这个框架虽然看似简单,但却非常有效,它已经成为我工具箱里一件非常称手的思考工具。
在我的职业生涯里,与很多人讨论不同的事时,我都会用到这个思考框架的不同变体,而在这个专栏里,我也会用它来帮助回答“怎样高效工作、怎样做好软件”这件事。
四个思考原则
在实际的工作中,这个思考框架会帮助我更好地了解自己的工作。比如,当一个产品经理给我交代一个要开发的功能特性时,我通常会问他这样一些问题:
为什么要做这个特性,它会给用户带来怎样的价值?
什么样的用户会用到这个特性,他们在什么场景下使用,他们又会怎样使用它?
达成这个目的是否有其它手段?是不是一定要开发一个系统?
这个特性上线之后,怎么衡量它的有效性?
如果产品经理能够回答好这些问题,说明他基本上已经把这个工作想得比较清楚了,这个时候,我才会放心地去了解后续的细节。
我们用思考框架对照一下,为什么我会问这些问题。一般来说,一个新特性要开发时,现状我是知道的。所以,我更关心目标,这里“为什么要做这个特性?”就是在问目标,“给用户带来怎样的价值”是在确定这个目标的有效性。
接下来,我会关注实现路径,用户会怎么用,是否有其他的替代手段,我需要了解产品经理的设计是经过思考的,还是“拍着脑袋”给出的。衡量有效性,则是要保证我的工作不会被浪费。
通过这个例子,我给你展示了怎么用这个思考框架提出问题。但我估计你更想了解的是,我怎么会想到问这些问题。给出思考框架是为了让你明白为什么要提出问题,而具体问题要怎么问,就可以遵循下面这四项原则:
以终为始;
任务分解;
沟通反馈;
自动化。
这是我从思考框架延伸出来的。在这个专栏里,我会围绕这四项原则和你详细讨论。
解释一下以终为始就是在工作的一开始就确定好自己的目标。我们需要看到的是真正的目标而不是把别人交代给我们的工作当作目标。你可以看出这个原则是在帮助我们回答思考框架中Where are we going?(我们要到哪儿去?)这个问题。
任务分解是将大目标拆分成一个一个可行的执行任务工作分解得越细致我们便越能更好地掌控工作它是帮助我们回答思维框架中How can we get there?(我们如何到达那里?)的问题。
如果说前两个原则是要在动手之前做的分析,那后面两个原则就是在通往目标的道路上,为我们保驾护航,因为在实际工作中,我们少不了与人和机器打交道。
沟通反馈是为了疏通与其他人交互的渠道。一方面,我们保证信息能够传达出去,减少因为理解偏差造成的工作疏漏;另一方面,也要保证我们能够准确接收外部信息,以免因为自我感觉良好,阻碍了进步。
自动化就是将繁琐的工作通过自动化的方式交给机器执行,这是我们程序员本职工作的一部分,我们擅长的是为其他人打造自动化的服务,但自己的工作却应用得不够,这也是我们工作中最值得优化的部分。
这四个原则互相配合,形成了一个对事情的衡量标准。总体上可以保证我的工作是有效的,在明确目标和完成目标的过程中,都可以尽量减少偶然复杂度。
怎么把这四个原则用在工作中呢?我们回过头来看一下前面的场景,产品经理把要做的功能特性摆在我面前。站在以终为始的角度,我需要了解真正的目标是什么,所以,我会关心为什么要做这个特性。为了保证目标是有效的,我会关心它给用户带来的价值。
有了任务分解的视角,我需要将一个大的目标进行拆解,如果我要达成这个目标,整体解决方案是远远不够的,我需要把任务分解成一个一个小的部分。所以,我会关心一个一个具体的使用场景。
一方面,我会了解到更多的细节,另一方面,当时间紧迫的时候,我会和产品经理来谈谈究竟优先实现哪个场景。
为什么要学会沟通反馈?因为我需要明确,自己是否真正理解了产品经理提交的需求。所以,我要不断地问问题,确保自己的理解和产品经理交代的内容一致。
另外,我也需要保证我的产品做出来确实能够达到目标。所以,我会关心它上线后的衡量手段。因为我知道,这个行业里有太多代码上线后,从来没有运行过。
自动化的角度很有意思,我们做的方案通常是一个自动化方案,但我们需要了解这个方案没有自动化之前是怎么做的。如果不自动化,用户会怎么用。所以,我会关心是不是还有其它替换方案,比如,买一个现成的服务。因为很多需求的提出,只是因为我们有了一个开发团队而已。
好,现在你已经对这四个原则在工作中的应用有了一个直观的认识。但你也会发现,我问的这些问题似乎已经“超纲”了,超过了一个普通程序员应该关注的范围。但这就是真实世界,它不像考试一样,有一个标准答案。
我们不是一个人孤独地在工作,而是与其他人在协作,想要做到高效工作,我们就要“抬起头”来,跳出写代码这件事本身。所以,我在开篇词里说,程序员解决的问题,大多不是程序问题。
可能你对这些原则的了解还没过瘾,没关系,这篇文章只是让大家清晰地了解思考框架和原则的背后逻辑。接下来,我会结合行业里的最佳实践,给你进一步讲解这些原则和具体应用。
总结时刻
大多数人工作低效是由于工作中偶然复杂度太多造成的,只要能够更多地将注意力放到本质复杂度上,减少偶然复杂度造成的消耗,我们“真实”的工作效率自然会得到大幅度提升。
而想要减少偶然复杂度的消耗,就要了解一些高效的工作方式和行业的最佳实践,而这一切是可以用统一的框架进行思考的。
运用这个思考框架,我们需要问自己一些问题:
Where are we?(我们现在在哪?)
Where are we going?(我们要到哪儿去?)
How can we get there?(我们如何到达那里?)
为了把这个框架应用在我们程序员的工作中,我给了你四个思考原则:
以终为始,确定好真实目标;
任务分解,找到实施路径;
沟通反馈,解决与人打交道出现的问题;
自动化,解决与机器打交道出现的问题。
如果今天的内容你只能记住一件事,那请记住:面对问题时,用思考框架问问自己,现状、目标和路径。
最后,我想请你思考一下,如果把这个思考框架运用在你的职业发展规划上,你会如何回答这三个问题呢?
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,121 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 持续集成:集成本身就是写代码的一个环节
你好,我是郑晔。
上一讲我们探讨了需求的“完成”,你现在知道如何去界定一个需求是否算做完了,这要看它是不是能够满足验收标准,如果没有验收标准,就要先制定验收标准。这一点,对于每一个程序员来说都至关重要。
在今天这一讲中,我们假设需求的验收标准已经制定清楚,接下来作为一个优秀的程序员,你就要撸起袖子准备开始写代码了。
不过在这里,我要问你一个问题:“是不是写完代码,工作就算完成了呢?”你或许会疑惑,难道不是这样吗?那我再问你:“代码是技术团队的交付物吗?”
你是不是发现什么不对劲了。没有人需要这堆文本,人们真正需要的是一个可运行的软件。写代码是程序员的职责,但我们更有义务交付一个可运行的软件。
交付一个可运行的软件,通常不是靠程序员个体奋战就能完成的,它是开发团队协作的结果。我们大多数人都工作在一个团队中,那我们写的代码是不是能够自然而然地就和其他人的代码配合到一起呢?显然没那么简单。
如果想将每个程序员编写的代码很好地组合在一起,我们就必须做一件事:集成。
但是集成这件事情,该谁做,该怎么做呢?我不知道你有没有思考过这个问题。在开始这个话题之前,我先给你讲个故事。
集成之“灾”
2009年我在一个大公司做咨询。对接合作的部门里有很多个小组正在共同研发一个项目。他们工作流程是先开发一个月等到开发阶段告一段落大项目经理再把各个小组最精锐成员调到一起开始集成。对他们来说集成是一件大事难度很大所以要聚集精英来做。
这个项目是用 C 语言编写的,所以,集成的第一步就是编译链接。大家把各个小组写好的程序模块编译到一起,哪个模块有问题,哪个小组的精英就出手解决它。
如果第一天,所有模块能够编译链接到一起,大家就要谢天谢地了。之后才进入到一个正式“联调”的过程。
“联调”的目标,是把一个最基本的流程跑通,这样,集成才算完成。而对他们这个项目来说,“联调”阶段更像是场“灾难”。
为什么?你想想,一个大部门有若干个团队,每个团队都在为同一个项目进行代码开发,周期为一个月。这一个月期间,所有团队的程序模块汇总在一起,体量会非常庞大。那么这些内容中,出现错误需要改动的可能性也就非常大,需要改动的量也就非常大。因此他们集成“联调”所需要的时间也会非常长。
即便他们调动各组精英完成一次项目集成的时间至少也需要23天改动量稍大可能就要一周了。虽然我不知道你所处公司的现状是什么样的但大概率地说你在职业生涯中会遇到过类似的场景。那怎么去解决这个问题呢
迈向持续集成
聪明的你作为旁观者一定会想,在这个故事里,为什么他们要在开发一个月后才做集成呢?为什么不能在开发一周后,甚至是更短的时间内就集成一次?
这是一个行业中常见的痛点,所以,就会有人不断地尝试改进,最先取得的突破是“每日构建”。
1996年Steve McConnel出版了一本著作《Rapid Development》国内译作《快速软件开发》。在这本书中作者首次提出了解决集成问题的优秀实践Daily Build每日构建。通过这个名字我们便不难看出它的集成策略即每天集成一次。
这在当时的人看来,已经是“惊为天人”了。就像上面提到的例子一样,当时的人普遍存在一种错误认知:集成不是一件容易的事,需要精英参与,需要很长时间,如果每天都进行集成,这是想都不敢想的事情。
实际上,每日构建背后的逻辑很简单:既然一段时间累积下来的改动量太过巨大,那一天的时间,累积的改动量就小多了,集成的难度也会随之降低。
你会看到,对比最后做集成和每日构建,这两种不同的做法都是在处理改动量和集成时间的关系。只不过,一个是朝着“长”的方向在努力,一个则瞄准“短”的方向。最后的事实证明,“长”的成了恶性循环,“短”的成了最佳实践。
既然,我们认同了只要增加集成的频率,就可以保证在每次集成时有较少的改动量,从而降低集成难度。
那问题来了?究竟要在开发后多久才进行一次集成呢?是半天、两个小时、还是一个小时呢?倘若这个想法推演到极致,是否就变成了只要有代码提交,就去做集成?
没错,正是基于这样的想法,有人尝试着让开发和集成同时进行,诞生了一个关于集成的全新实践:持续集成。
持续集成一个关键的思维破局是,将原来分成两个阶段的开发与集成合二为一了,也就是一边开发一边集成。
持续集成这个想法固然好,但是不是需要有专人负责盯着大家的工作,只要有人提交了代码,这个负责人就要去集成呢?显然,这在真实工作中是行不通的。
既然是程序员的想法,程序员解决问题的方案自然就是自动化这个过程。于是,有人编写了一个脚本,定期去源码服务器上拉代码,出现程序更新时,就自动完成构建。
后来,人们发现这段脚本与任何具体项目都是无关的。于是,把它进一步整理并发布出来,逐步迭代发展成为今天广为人知的持续集成服务器。
在2000年时“软件行业最会总结的人” Martin Fowler 发布了一篇重量级文章“Continuous Integration”。
之后一年,由 Martin Fowler 所在的 ThoughtWorks 公司发布了市面上第一款持续集成服务器 CruiseControl。CruiseControl 可谓是持续集成服务器的鼻祖,后来市面上的服务器基本都是在它的基础上改良而来的。
Martin Fowler 的重磅文章和首款持续集成服务器的问世,让软件行业对持续集成进行了更为深入的探讨,人们对于持续集成的认知程度一路走高,持续集成服务器成为了开发团队在集成阶段最得心应手的工具。围绕着持续集成的一系列行为准则逐渐成型。
以至于发展到2006年Martin Fowler 不得不重写了“Continuous Integration”这篇文章。之后人们更是以持续集成为基础进一步拓展出持续交付的概念。
人类对工具是有偏爱的,持续集成服务器的发布,将持续集成从一项小众实践逐步发展成为今天行业的“事实”标准。
“地面上”的持续集成
然而,即便持续集成已经发展多年,至今整个行业在对它的应用上,却并未达到同步的状态。有趣的是,有一部分公司虽然还无法实现持续集成,但是因为持续集成服务器的出现,反而可以做到每日构建。
这不难理解,每日构建的概念虽然早早就提出来了,但在那个时期,行业里真正践行每日构建的公司并不多,其根本原因就在于,每日构建最初都是一些指导原则,缺乏工具的支持。而每日构建和持续集成最根本的区别在于构建时机,而这只是持续集成服务器的一个配置选项而已。
当然,行业内有一部分公司已经可以将持续集成运用得得心应手,而也有相当大的一部分人还在为集成而痛苦不堪,比如我前面提到的咨询项目。
这个项目是我在2009年时参与的。也就是说此时距离 Martin Fowler 最初写下“Continuous Integration”已经过去了9年甚至距离这篇文章的更新版发布也已经过去了3年更不要说距离 McConnell 提出“每日构建”已经13年。
即便以当时的时间坐标系来看这个项目的集成实践水平至少落后行业10年以上。没错他们甚至连每日构建都还差很远。
时至今日,持续集成早就是成熟得不能再成熟的实践了。然而,据我所知,许多公司依然处于集成要依赖于“英雄”的蛮荒阶段。
虽然我们在同一个时代写代码做开发,但在技术实践层面,不同的团队却仿佛生活在不同的年代。这也是我们要学习的原因。
也许,目前国内对于持续集成的实践水平还处于较为原始的状态,这是个坏消息。但好消息是,我们可以通过更多的学习,对集成有足够的了解,从而一步到位地进入到最先进的状态中。
无需停留在以精英为核心的集成时代,也可以完全不理会每日构建,我希望你拥有这个时代的集成观,直接开始持续集成。
如果有了持续集成的集成观,我们该怎么看待开发这件事呢?开发和集成就不再是两个独立的过程,而是合二为一成为一体。
基于这样的理解,我们就不能再说代码写完了,就差集成了,因为这不叫开发的完成。一个好的做法是尽早把代码和已有代码集成到一起,而不应该等着所有代码都开发完了,再去做提交。
怎样尽早呢?你需要懂得任务分解,这是我们在之后的“任务分解”主题下会讲到的内容。
总结时刻
在软件开发中,编写代码是很重要的一环,但程序员的交付物并不应该是代码,而是一个可工作的软件。当我们在一个团队中工作的时候,把不同人的代码放在一起,使之成为一个可工作软件的过程就是集成。
在很长一段时间内,集成都是软件行业的难题,改动量和集成时间互相影响。幸运的是,不同的人在不同的方向尝试着改变,结果,同时加大改动量和集成时间的人陷入了泥潭,而调小这两个参数的人看到了曙光。
每日构建作为早期的一种“最佳实践”被提了出来,但因为它基本上都是原则,没有得到广泛的应用。当人们进一步“调小”参数后,诞生了一个更极致的实践:持续集成,也就是每次提交代码都进行集成。
真正让持续集成成为行业最佳实践的是Martin Fowler 的文章以及持续集成服务器。持续集成的思维让我们认识到,开发和集成可以合二为一。我们应该把开发的完成定义为代码已经集成起来,而站在个体的角度,我们应该尽早提交自己的代码,早点开始集成。
如果今天的内容你只能记住一件事,那请记住:尽早提交代码去集成。
最后,我想请你分享一下,在实际工作中,你遇到过哪些由集成带来的困扰?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,117 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 精益创业:产品经理不靠谱,你该怎么办?
你好,我是郑晔。
前面谈到验收标准时,我们说的实际上是确定性需求,也就是说,我们已经知道了这个需求要怎么做,就差把它做出来了。而有时候,我们面对的需求却是不确定的,比如,产品经理有了一个新想法,那我们该如何应对呢?
今天,我们从 IT 行业一个极为经典的话题开始:程序员如何面对产品经理。我先给你讲一件发生在我身边的事。
有一次,我们一大群人在一个大会议室里做一个产品设计评审,来自产品团队和技术团队的很多人都参与到这个评审中。一个产品经理正对着自己的设计稿,给大家讲解一个新的产品特性。
这个公司准备将自己的服务变成了一个云服务,允许第三方厂商申请,这个产品经理给大家讲解的就是第三方厂商自行申报开通服务的流程。听完前面基本情况的介绍,我举手问了几个问题。
我:这个服务会有多少人用?-
产品经理:这是给第三方厂商的人用的。-
我:我问的是,这个服务会有多少人用。-
产品经理:每个第三方厂商的申请人都会用。-
我:好,那你有预期会有多少第三方厂商申请呢?-
产品经理:呃,这个……我们没仔细想过。-
我:那现在给第三方厂商开通服务的具体流程是什么。-
产品经理:第三方厂商申请,然后,我们这边开通。-
我:好,这个过程中,现在的难点在哪里?这个审批过程能让我们的工作简化下来吗?-
产品经理:……-
我:那我来告诉你,现在开通第三方厂商服务,最困难的部分是后续开通的部分,有需要配置服务信息的,有需要配置网络信息的。目前,这个部分还没有很好的自动化,前面审批的部分能够自动化,对整个环节优化的影响微乎其微。
我的问题问完了,开发团队的人似乎明白了什么,纷纷表示赞同我的观点。这个审批流程本身的产品设计并不是问题,但我们的时间和资源是有限的,关键在于,要不要在这个时间节点做这个事。准确地说,这是优先级的问题。
此刻,作为开发团队一员的你,或许会有种快感,把产品经理怼回去,简直大快人心。好吧,作为一个正经的专栏,我们并不打算激化产品经理和开发团队的矛盾,而是要探讨如何做事情才是合理的。
之所以我们能很好地回绝了产品经理不恰当的需求,是因为我们问了一些好问题,但更重要的是,我们为什么能问出这些问题。
产品经理是个新职业
在做进一步讨论之前我们必须认清一个可悲的现状IT 行业中大多数人的专业程度是不够的。
IT 行业是一个快速发展的行业,这个行业里有无数的机会,相对于其它行业来说,薪资水平也要高一些,这就驱使大量的人涌入到这个行业。
也因为这是一个快速发展的行业很多职位都是新近才涌现出来的比如在2010年之前很少有专职的前端工程师之前的工程师往往要前后端通吃。
产品经理便是随着创业浪潮才风起云涌的职位。既然这是个“新”职位往往是没有什么行业标准可言的。所以你会看到很多行业乱象很多人想进入IT行业一看程序员需要会写代码觉得门槛高那就从产品经理开始吧这些人对产品经理岗位职责的理解是告诉程序员做什么。
这和郭德纲口中外行人“如何认识相声”是一个道理,以为会说话就能说相声,殊不知,这是个门槛极高的行业。产品经理也一样,没有良好的逻辑性,怎么可能在这个行业中有好的发展。
如果你遇到的产品经理能给出一个自洽的逻辑,那么恭喜你,你遇到了还算不错的产品经理。多说一句,这个行业中专业度不够的程序员也有很多,人数比产品经理还多,道理很简单,因为程序员的数量比产品经理的数量多。
这么说并不是为了黑哪个职位,而是要告诉大家,我们必须要有自己的独立思考,多问几个为什么,尽可能减少掉到“坑”里之后再求救的次数。
回到前面的主题,我们该怎么与产品经理交流呢?答案还在这个部分的主题上,以终为始。我们是要做产品,那就需要倒着思考,这个产品会给谁用,在什么场景下怎么用呢?
这个问题在 IT 行业诞生之初并不是一个显学,因为最初的 IT 行业多是为企业服务的。企业开发的一个特点是,有人有特定的需求。在这种情况下,开发团队只要把需求分析清楚就可以动手做了,在这个阶段,团队中的一个关键角色是业务分析师。即便开发出来的软件并不那么好用,企业中强行推动,最终用户也就用了。
后来,面向个人的应用开始出现。在 PC 时代和早期的互联网时代,软件开发还基本围绕着专业用户的需求,大部分软件只要能解决问题,大家还是会想办法用起来的。
但是随着互联网深入人心,软件开始向各个领域蔓延。越来越多的人进入到 IT 行业,不同的人开始在各个方向上进行尝试。这时候,软件开发的主流由面向确定性问题,逐渐变成了面向不确定性问题。
IT 行业是这样一个有趣的行业,一旦一个问题变成通用问题,就有人尝试总结各种最佳实践,一旦最佳实践积累多了,就会形成一套新的方法论。敏捷开发的方法论就是如此诞生的,这次也不例外。
精益创业
最早成型的面向不确定性创造新事物的方法论是精益创业Lean Startup它是 Eric Ries 最早总结出来的。他在很多地方分享他的理念不断提炼最终在2011年写成一本同名的书《精益创业》。
看到精益创业这个名字大多数人会优先注意到“创业Startup”这个词。虽然这个名字里有“创业”二字但它并不是指导人们创业挣大钱的方法论。正如前面所说它要解决的是面向不确定性创造新事物。
只不过,创业领域是不确定性最强而且又需要创造新事物的一个领域,而只要是面向不确定性在解决问题,精益创业都是一个值得借鉴的方法论。比如,打造一个新的产品。
精益创业里的“精益”Lean是另外一个有趣的词。精益这个词来自精益生产这是由丰田公司的大野耐一和新乡重夫发展出来的一套理论。
这个理论让人们开始理解价值创造与浪费之间的关系。创造价值是每个人都能理解的,但减少浪费却是很多人忽略的。所以,把这几个理念结合起来,精益创业就是在尽可能少浪费的前提下,面向不确定性创造新事物。
那精益创业到底说的是什么呢?其实很简单。我们不是要面向不确定性创造新事物吗?既然是不确定的,那你唯一能做的事情就是“试”。
怎么试呢试就要有试的方法。精益创业的方法论里提出“开发build-测量measure-认知learn”这样一个反馈循环。就是说当你有了一个新的想法idea就把想法开发成产品code投入市场然后收集数据data获取反馈看看前面的想法是不是靠谱。
得到的结果无非是两种好想法继续加强不靠谱的想法丢掉算了。不管是哪种结果你都会产生新的想法再进入到下一个循环里。在这个反馈循环中你所获得的认知是最重要的因为它是经过验证的。在精益创业中这也是一个很重要的概念经过验证的认知Validated Learning
既然是试,既然是不确定这个想法的有效性,最好的办法就是以最低的成本试,达成同样一个目标,尽可能少做事。精益创业提出一个非常重要的概念,最小可行产品,也就是许多人口中的 MVPMinimum Viable Product。简言之少花钱多办事。
许多软件团队都会陷入一个非常典型的误区,不管什么需求都想做出来看看,殊不知,把软件完整地做出来是最大的浪费。
你为什么要学习精益创业?
或许你会问,我就是一个程序员,也不打算创业,学习精益创业对我来说有什么用呢?答案在于,精益创业提供给我们的是一个做产品的思考框架,我们能够接触到的大多数产品都可以放在这个框架内思考。
有了框架结构,我们的生活就简单了,当产品经理要做一个新产品或是产品的一个新特性,我们就可以用精益创业的这几个概念来检验一下产品经理是否想清楚了。
比如,你要做这个产品特性,你要验证的东西是什么呢?他要验证的目标是否有数据可以度量呢?要解决的这个问题是不是当前最重要的事情,是否还有其他更重要的问题呢?
如果上面的问题都得到肯定的答复,那么验证这个目标是否有更简单的解决方案,是不是一定要通过开发一个产品特性来实现呢?
有了这个基础,回到前面的案例中,我对产品经理提的问题,其实就是在确定这件事要不要做。事实上,他们当时是用一个表单工具在收集用户信息,也就是说,这件事有一个可用的替代方案。鉴于当时还有很多其它需求要完成。我建议把这个需求延后考虑。
总结时刻
程序员与产品经理的关系是 IT 行业一个经典的话题。许多程序员都会倾向于不问为什么就接受来自产品经理的需求,然后暗自憋气。
实际上,产品经理是一个新兴职业,即便在 IT 这个新兴行业来看,也算是新兴的。因为从前的 IT 行业更多的是面向确定性的问题,所以,需要更多的是分析。只有当面向不确定性工作时,产品经理才成为一个行业普遍存在的职业。所以,在当下,产品经理并不是一个有很好行业标准的职位。
比较早成型的面向不确定创造新事物的方法论是精益创业它提出了“开发build-测量measure-认知learn”这样一个反馈循环和最小可行产品的概念。
当产品经理让我们做一个新的产品特性时,我们可以从精益创业这个实践上得到启发,向产品经理们问一些问题,帮助我们确定产品经理提出的需求确实是经过严格思考的。
如果今天的内容你只记住一件事,那请记住:默认所有需求都不做,直到弄清楚为什么要做这件事。
最后,我想请你回想一下,你和产品经理日常是怎样做交流的呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 解决了很多技术问题,为什么你依然在“坑”里?
你好,我是郑晔。
在前面的内容中,我给你介绍了几个体现“以终为始”原则的实践,包括怎样界定工作是否完成的 DoD、怎样判定需求是否完成的验收标准、还有怎样验证产品经理给出的产品特性是否合理的精益创业理念。
了解了这些内容,可能你会想:我为什么要关心这些啊?我是程序员啊!难道我不应该安安静静地写程序吗?为什么要操心其他人的工作做得好坏?如果我管了那么多事,我还是不是一个程序员,到底哪里才是我的“终”呢?
今天这一讲,我们就来聊聊这个让许多人困惑的问题。因为只有要跳出程序员的角色看问题,工作才会变得更加高效。
“独善其身”不是好事
在需要与人协作的今天,独善其身可不一定是好的做法。我先给你讲一个发生在我身边的故事。
有一次,我的团队要开发一个数据服务层,准备作为一个基础设施提供给核心业务系统。开发没多久,一个团队成员和我说,他的工作进展不顺利,卡在了一个重要问题上,他想不明白该如何在多个实例之间分配 ID。
我听完之后,有些疑惑,为什么要考虑这个和功能无关的问题呢?他解释说,因为我们的系统需要保证消息的连续性,所以他设计了消息 ID这样下游系统就可以通过消息 ID 识别出是否有消息丢失。
这是没错的,但我奇怪的是,他为什么要在多个实例之间协调呢?他给出的理由是,这么做,是出于考虑应对将来有多实例并发场景的出现。然而事实是,我们当下的需求应对的是单实例的情况。
我了解情况之后,马上跟他说清楚这一点,让他先把第一步做出来。这个同事还是有些担心未来如何做扩展。我告诉他,别纠结,先把第一步做出来,等后面真的有需求,我们再考虑。同事欣然答应了。
其实,这个同事的技术能力非常强,如果我不拦着他,他或许真能实现出一个完美的技术方案,但正如他自己所纠结的那样,这个方案可能要花掉他很长时间。但这真的是我们想要的吗?以现阶段的目标来看,根本没有这样的需求。
我们一直在强调“以终为始”。所谓“终”,其实就是我们的做事目标。虽然大家工作在一起,朝着一个共同的大目标前进,但真的到了一个具体的问题上,每个人看到的目标却不尽相同。
我之所以能把同事从一个纠结的状态中拉出来,是因为我看到的是需求,而他看到的是一个要解决的技术问题。所以,我们俩在对目标的理解上是有根本差异的。
你也许会认为,我和同事之所有这样的差异,是角色上的差异,我在项目里承担的角色要重一些,而且我的工作时间比同事要长一些。但不知道你有没有想过,不同角色的差异到底在哪里呢?
角色的差异
作为一个在职场工作的人,每个人都有一颗渴望得到认可的心,希望自己在职业的阶梯上步步高升。假如今天就让你往上走一个台阶,比如,你原来在项目里打杂,现在成为项目的主力,或者,你已经对项目细节驾轻就熟,即将委任你为项目负责人。你是否能胜任呢?
你需要补充的东西是什么?换句话说,你和你职业台阶中的上一级那个人,差异到底是什么?
也许你会说,他比我来的时间长,或者说,他每天的主要工作就是开会。如果真的是这样,那是不是只要你凑足这个条件,就可以到达他的位置呢?显然不是。
不同角色工作上真正的差异是上下文的不同。
这是什么意思呢?以前面的问题为例,你在项目里打杂,你只能关注到一个具体的任务,而项目主力心目中是整个系统。虽然写的代码都一样,但你看到的是树木,人家看到的是森林,他更能从全局思考。
同样,项目负责人的工作,虽然包括在项目组内的协调,但还有一部分工作是跨项目组的,他需要考虑你们项目组与其他组的互动。所以,他工作的上下文是在各组之间,包括技术和产品等方面。
再上升一个层面,部门负责人要协调内部各个组,同时要考虑部门之间的协调。而公司负责人考虑的上下文甚至要跳脱公司内部,进入到行业层面。
你可能会问,好了,我知道不同角色的上下文有差异了,但这对我意味着什么呢?
我们先从工作角度看。回到前面我分享的那个故事,你可能注意到了,我并不是靠技术能力解决了问题,而是凭借对需求的理解把这个问题绕过去了。
之所以我能这样做,原因就在于我是在一个更大的上下文里工作。类似的故事在我的职业生涯中发生过无数次,许多令程序员愁眉不展的问题,换个角度可能都不是问题。
技术是一把利刃,程序员相信技术可以改变世界,但并不是所有问题都要用技术解决。有这样一种说法,手里有了锤子,眼里都是钉子。花大力气去解决一个可能并不是问题的问题,常常是很多程序员的盲区。
之所以称之为盲区,是因为很多人根本看不见它,而看不见的原因就在于上下文的缺失,也就是说,你只在程序员的维度看问题。
多问几个为什么,交流一下是不是可以换个做法,许多困惑可能就烟消云散了。而能想到问这样的问题,前提就是要跳出程序员角色思维,扩大自己工作的上下文。
虽然我不是项目主力,但不妨碍我去更深入地了解系统全貌;虽然我不是项目负责人,但不妨碍我去了解系统与其他组的接口;同样,虽然我不是项目经理,但我可以去了解一下项目经理是怎样管理项目的;虽然我不是产品经理,但了解一个产品的设计方法对我来说也是有帮助的。
当你对软件开发的全生命周期都有了认识之后,你看到的就不再是一个点了,而是一条线。与别人讨论问题的时候,你就会有更多的底气,与那些只在一个点上思考的人相比,你就拥有了降维攻击的能力。
现在你知道为什么你的工作总能让老板挑出毛病了吧!没错,工作的上下文不同,看到的维度差异很大。单一维度的思考,在多维度思考者的眼里几乎就是漏洞百出的。
当扩大了自己工作的上下文时,我们的目标就不再局限于一个单点,而是会站在更高的维度去思考,解决问题还有没有更简单的方案。许多在低一级难以解决的问题,放到更大的上下文里,根本就不是问题。
我的职业生涯中经常遇到这样的情况,在一个特定的产品设计下,我总觉得设计的技术方案有些不优雅的地方,而只要产品设计微调一下,技术方案一下子就会得到大幅度提升。在这种情况下,我会先去问产品经理,是否可以这样调整。只要不是至关重要的地方,产品经理通常会答应我的要求。
在更大的上下文工作
扩展自己工作的上下文,目光不再局限于自己的一亩三分地,还可以为自己的职业发展做好布局。在这个方面,我给你分享一个不太成功的案例,就是我自己的故事。
我是属于愚钝型的程序员,工作最初的几年,一直把自己限定在程序员的上下文里,最喜欢的事就是安安静静地写代码,把一个系统运作机理弄清楚会让我兴奋很长一段时间。
我的转变始于一次机缘巧合,当时有一个咨询项目,负责这个项目的同事家里有些事,需要一个人来顶班,公司就把我派去了。
到了咨询项目中,我自己习惯的节奏完全乱掉了,因为那不是让代码正常运作就可以解决的问题,更重要的是与人打交道。
有很长一段时间,我一直处于很煎熬的状态,感谢客户没有把我从这个项目赶出来,让我有了“浴火重生”的机会。
为了让自己从这种煎熬的状态中摆脱出来,我必须从代码中走出来,尽量扩大自己思考的边界。经过一段时间的调整,我发现与人打交道也没那么难,我也能更好地理解一个项目运作的逻辑,因为项目运作本质上就是不同人之间的协作。
突破了自己只愿意思考技术的限制,世界一下子宽阔了许多。所以,后来才有机会更多地走到客户现场,看到更多公司的项目运作。虽然我工作过的公司数量并不多,但我却见过很多公司是如何工作的。
再后来,我有机会参与一个新的分公司建设工作中,这让我有了从公司层面进行思考的角度。对于员工招聘和培养,形成了自己一套独立的思考。
这些思考在我创业的过程中,帮我建立了一支很不错的团队。而创业的过程中,我又有了更多机会,去面对其他公司的商务人员,从而建立起一个更大的上下文,把思考从公司内部向外拓展了一些。
回过头来看自己的生涯时,我发现,因为不愿意拓展自己的上下文,我其实错过了很多职业发展的机会。所幸我还有机会突破自己,让自己走出来,虽然走的速度不如理想中快,但至少一直在前进,而不是原地打转。这也是我告诫你一定要不断扩大自己工作上下文的原因。
机会总是垂青那些有准备的人,尤其在公司规模不大的时候,总有一些跳跃式的发展机会。
我见过有人几年之内从程序员做到公司中国区负责人,只是因为起初公司规模不大,而他特别热心公司的很多事情,跳出了固定角色的思维。所以,当公司不断发展,需要有人站出来的时候,虽然没有人是完全合格的,但正是他的热心,让他有了更多的维度,才有机会站到了前排。
当然,随着公司规模越来越大,这种幅度极大的跳跃是不大可能的。江湖上流传着一个华为的故事,一个新员工给任正非写了封万言书,大谈公司发展,任正非回复:“此人如果有精神病,建议送医院治疗,如果没病,建议辞退。”
因为一旦公司规模大了,你很难了解更大的上下文,很多关于公司的事情,你甚至需要从新闻里才知道。
本质上,一个人能在自己的工作范围内多看到两三级都是有可能的。在公司规模不大时,从基层到老板没有太多层级,跳跃就显得很明显,而公司一大,层级一多,从低到顶的跳跃就不太可能了,但跨越级别跳跃是可能的。
所以我希望你跳出程序员思维,这不仅仅是为了工作能够更高效,也是希望你有更好的发展机会。
总结时刻
程序员总喜欢用技术去解决一切问题,但很多令人寝食难安的问题其实根本不是问题。之所以找不出更简单的解决方案,很多时候原因在于程序员被自己的思考局限住了。
不同角色工作真正的差异在于上下文的差异。在一个局部上下文难以解决的问题,换到另外一个上下文甚至是可以不解决的。所以说无论单点有多努力也只是局部优化,很难达到最优的效果。
想把工作做好,就需要不断扩大自己工作的上下文,多了解一下别人的工作逻辑是什么样的,认识软件开发的全生命周期。
扩大自己的上下文,除了能对自己当前的工作效率提高有帮助,对自己的职业生涯也是有好处的。随着你看到的世界越来越宽广,得到的机会也就越来越多。
如果今天的内容你只记住一件事,那请记住:扩大自己工作的上下文,别把自己局限在一个“程序员”的角色上。
最后,我想请你分享一下,在你的工作中,有哪些因为你扩大了工作上下文而解决的问题呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 为什么说做事之前要先进行推演?
你好,我是郑晔。
经过前面的学习,想必你已经对“以终为始”这个原则有了自己的理解。你知道接到一个任务后,要做的不是立即埋头苦干,而是要学会思考,找出真正的目标。那目标明确之后,我们是不是就可以马上开始执行了呢?
先不着急给出你的答案,今天的内容从一个技术任务开始。
一个技术任务
你现在在一家发展还不错的公司工作。随着业务的不断发展,原来采用的关系型数据库越发无法满足快速的变化。于是,项目负责人派你去做个技术选型,把一部分业务迁移到更合适的存储方式上。
经过认真的调研和思考,你给负责人提出了自己的建议,“我们选择 MongoDB。”出于对你的信任负责人无条件地同意了你的建议你获得了很大的成就感。
在你的喜悦尚未消退时,负责人进一步对你委以重任,让你来出个替代计划。替代计划?你有些不相信自己的耳朵,嘴里嘟囔着:“把现在存到数据库的内容写到 MongoDB 不就成了,我就一个表一个表地替换。难道我还要把哪天替换哪个表列出来吗?”
刚刚还对你欣赏有加的负责人,脸色一下子沉了下来。“只有表改写吗?”他问你。你一脸懵地看着他,心里想,“不然呢?”
“上线计划呢?”负责人问。
“我还一行代码都没写呢?”你很无辜地看着负责人。
“我知道你没写代码,我们就假设代码已经写好了,看看上线是怎样一个过程。”
“不是发新版本就好了吗?”你还是不知道负责人到底想说什么。
“你能确定新版代码一定是对的吗?”
虽然你已经叱咤编程很多年,但作为老江湖,一听这话反而是有些怯的。“不能。”你痛快地承认了。
“一旦出错,我们就回滚到上一个版本不就成了。”常规的处理手段你还是有的。
“但数据已经写到了不同的存储里面,查询会受到影响,对不对?”负责人一针见血。
“如果这个阶段采用两个数据存储双写的方案,新代码即便出问题,旧存储的代码是正常,我们还有机会回滚。”你一下子就给出了一个解决方案,咱最不怕出问题了。
“对。”负责人认同了你的做法,一副没看错人的神情。“让你出上线方案,就是为了多想想细节。”
你终于明白了负责人的良苦用心,也就不再大意。很快,你就给出了一份更详尽的上线方案。
你把这个方案拿给负责人看,信心满满,觉得自己够小心,一步一步做,没有任何问题。但负责人看了看你的上线计划,眉头逐渐锁了起来,你知道负责人还是不满意,但不知道还差在哪里?
“原有的数据怎么办?”负责人又问了一个问题。你一下子意识到,确实是问题。“没有原有数据,一旦查询涉及到原有数据,查询的结果一定是错的。所以,还应该有一个原有数据的迁移任务。”你尴尬地笑了笑。
负责人微笑着看着你。“好吧,从我的角度看差不多了,你可以再仔细想想。然后,排一个开发任务出来吧!”
你当然不会辜负负责人的信任,很快排出了开发任务。
看着排出的任务,你忽然困惑了。最开始只是想写个读写新库的组件,怎么就多出这么些任务。此外,你还很纳闷为什么负责人总是能找到这么多问题。
一次个人回顾
你想起之前的工作里有过类似的场景,那个负责人也是让你独立安排任务。通常,你最初得到的也是一个简单的答案,从当时的心境上看,你是很有成就感的。
只是后来的故事就不那么美妙了,上线时常常出现各种问题,你和其他同事们手忙脚乱地处理各种异常。当时顶着巨大压力解决问题的场景,你依然记忆犹新。解决完问题离开公司时,天空已经泛起鱼肚白。
而似乎自从加入了现在的公司,这种手忙脚乱的场景少了很多。你开始仔细回想现在这个负责人在工作中的种种。从给大家机会的角度来看,这个负责人确实不错,他总会让一个人独立承担一项任务。只不过,他会要求大家先将任务分解的结果给他看。
拿到组里任何一个人的开发列表之后,他都会问一大堆问题,而且大多数情况下,他都会问到让人哑口无言。说句心里话,每次被他追问心里是挺不舒服的,就像今天这样。
本来在你看来挺简单的一件事,经过他的一系列追问,变成了一个长长的工作列表,要做的事一下子就变多了。毕竟谁不愿意少做点活呢!
不过,你不得不承认的一点是,加入这个公司后,做事更从容了。你知道无论做的事是什么,那些基本的部分是一样的,差别体现在事前忙,还是事后忙,而现在这家公司属于事前忙。于是,你开始把前一家公司上线时所忙碌的内容,和现在负责人每次问的问题放在一起做对比。
这样一梳理,你才发现,原来负责人问的问题,其实都是与上线相关的问题。包括这次的问题也是,上线出问题怎么办,线上数据怎么处理等等。
你突然意识到一个关键问题,其实负责人每次问的问题都是类似的,无论是你还是其他人,他都会关心上线过程是什么样,给出一个上线计划。即便我们还一行代码都没有,他依然会让我们假设如果一切就绪,应该怎样一步一步地做。
你终于明白了,之前的项目之所以手忙脚乱,因为那时候只想了功能实现,却从来没考虑过上线,而且问题基本上都是出在上线过程中的。你想到了上次参加一个社区活动,其中的一个大牛提到了一个说法:“最后一公里”。
想到这,你赶紧上网搜了一下“最后一公里”,这个说法指的是完成一件事,在最后也是最关键的步骤。你才意识到,“最后一公里”这个说法已经被应用在很多领域了,负责人就是站在“最后一公里”的角度来看要发生的事情。
嗯,你学会了一招,以后你也可以站在“最后一公里”去发现问题了,加上你已经具备的推演能力,给出一个更令人满意的任务列表似乎更容易一些。
把这个问题想清楚了,你重新整理了自己的思路,列出了一个自己的问题解决计划。
先从结果的角度入手,看看最终上线要考虑哪些因素。
推演出一个可以一步一步执行的上线方案,用前面考虑到的因素作为衡量指标。
根据推演出来的上线方案,总结要做的任务。
不过,更令你兴奋的是,你拥有了一个看问题的新角度,让自己可以再上一个台阶,向着资深软件工程师的级别又迈进了一步。
通往结果之路
好了,这个小故事告一段落。作为我们专栏的用户,你可能已经知道了这个故事要表达的内容依旧是“以终为始”。关于“以终为始”,我们前面讲的内容一直是看到结果,结果是重要的。然而,通向结果的路径才是更重要的。
这个世界不乏有理想的人,大多数人都能看到一个宏大的未来,但这个世界上,真正取得与这些理想相配成绩的人却少之又少,大部分人都是泯然众生的。
宏大理想是一个目标,而走向目标是需要一步一个脚印地向前走的。唐僧的目标是求取真经,但他依然用了十几年时间才来到大雷音寺。唐僧西天取经有一个极大的优势,他达成目标的路径是清晰的,从长安出发,向着西天一路前行就好。
对比我们的工作,多数情况下,即便目标清晰,路径却是模糊的。所以,不同的人有不同的处理方式。有些人是走到哪算哪,然后再看;有些人则是先推演一下路径,看看能走到什么程度。
在我们做软件的过程中,这两种路径所带来的差异,已经在前面的小故事里体现出来了。一种是前期其乐融融,后期手忙脚乱;一种是前面思前想后,后面四平八稳。我个人是推崇后一种做法的。
或许你已经发现了这就是我们在“以终为始”主题的开篇中提到的第一次创造或者智力上的创造。如果不记得了不妨回顾一下《02 | 以终为始:如何让你的努力不白费?》。
实际上,早就有人在熟练运用这种思想了。在军事上,人们将其称为沙盘推演,或沙盘模拟。军队通过沙盘模拟军事双方的对战过程,发现战略战术上存在的问题。这一思想也被商界借鉴过来,用来培训各级管理者。
这个思想并不难理解,我们可以很容易地将它运用在工作中的很多方面。比如:
在做一个产品之前,先来推演一下这个产品如何推广,通过什么途径推广给什么样的人;
在做技术改进之前,先来考虑一下上线是怎样一个过程,为可能出现的问题准备预案;
在设计一个产品特性之前,先来考虑数据由谁提供,完整的流程是什么样的。
最后这个例子也是软件开发中常遇到的,为数不少的产品经理在设计产品时,只考虑到用户界面是怎样交互的,全然不理会数据从何而来,造成的结果是:累死累活做出来的东西,完全跑不通,因为没有数据源。
很多时候,我们欠缺的只是在开始动手之前做一遍推演,所以,我们常常要靠自己的小聪明忙不迭地应对可能发生的一切。
希望通过今天的分享,能让你打破手忙脚乱的工作循环,让自己的工作变得更加从容。
总结时刻
即便已经确定了自己的工作目标,我们依然要在具体动手之前,把实施步骤推演一番,完成一次头脑中的创造,也就是第一次创造或智力上的创造。这种思想在军事上称之为沙盘推演,在很多领域都有广泛地应用。
在软件开发过程中,我们就假设软件已经就绪,看就绪之后,要做哪些事情,比如,如何上线、如何推广等等,这样的推演过程会帮我们发现前期准备的不足之处,进一步丰富我们的工作计划。为了不让我们总在“最后一公里”摔跟头,前期的推演是不可或缺的,也是想让团队进入有条不紊状态的前提。
如果今天的内容你只记住一件事,那请记住:在动手做一件事之前,先推演一番。
最后,我想请你思考一下,如果把你在做的事情推演一番,你会发现哪些可以改进的地方呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 测试也是程序员的事吗?
你好,我是郑晔。
在“任务分解”这个模块,我准备从一个让我真正深刻理解了任务分解的主题开始,这个主题就是“测试”。
这是一个让程序员又爱有恨的主题,爱测试,因为它能让项目的质量有保证;恨测试,因为测试不好写。而实际上,很多人之所以写不好测试,主要是因为他不懂任务分解。
在上一个模块,我们提到了一些最佳实践,但都是从“以终为始”这个角度进行讲解的。这次,我准备换个讲法,用五讲的篇幅,完整地讲一下“开发者测试”,让你和我一起,重新认识这个你可能忽视的主题。
准备好了吗?我们先从让很多人疑惑的话题开始:程序员该写测试吗?
谁要做测试?
你是一个程序员,你当然知道为什么要测试,因为是我们开发的软件,我们得尽可能地保证它是对的,毕竟最基本的职业素养是要有的。
但测试工作应该谁来做,这是一个很有趣的话题。很多人凭直觉想到的答案是,测试不就该是测试人员的事吗,这还用问?
测试人员应该做测试,这是没错的,但是测试只是测试人员的事吗?
事实上,作为程序员,你多半已经做了很多测试工作。比如,在提交代码之前,你肯定会把代码跑一遍,保证提交的基本功能是正确的,这就是最基本的测试。但通常,你并不把它当成测试,所以,你的直觉里面,测试是测试人员的事。
但我依然要强调,测试应该是程序员工作的一部分,为什么这么说呢?
我们不妨想想,测试人员能测的是什么?没错,他们只能站在系统外部做功能特性的测试。而一个软件是由它内部诸多模块组成的,测试人员只从外部保障正确性,所能达到的效果是有限的。
打个比方,你做一台机器,每个零部件都不保证正确性,却要让最后的结果正确,这实在是一个可笑的要求,但这却真实地发生在软件开发的过程中。
在软件开发中有一个重要的概念:软件变更成本,它会随着时间和开发阶段逐步增加。也就是说我们要尽可能早地发现问题,修正问题,这样所消耗掉的成本才是最低的。
上一个模块讲“以终为始”,就是在强调尽早发现问题。能从需求上解决的问题,就不要到开发阶段。同样,在开发阶段能解决的问题,就不要留到测试阶段。
你可以想一下,是你在代码中发现错误改代码容易,还是测试了报了 bug你再定位找问题方便。
更理想的情况是,质量保证是贯穿在软件开发全过程中,从需求开始的每一个环节,都将“测试”纳入考量,每个角色交付自己的工作成果时,都多问一句,你怎么保证交付物的质量。
需求人员要确定验收标准开发人员则要交出自己的开发者测试。这是一个来自于精益原则的重要思想内建质量Build Quality In
所以,对于每个程序员来说,只有在开发阶段把代码和测试都写好,才有资格说,自己交付的是高质量的代码。
自动化测试
不同于传统测试人员只通过手工的方式进行验证,程序员这个群体做测试有个天然的优势:会写代码,这个优势可以让我们把测试自动化。
早期测试代码,最简单的方式是另外写一个程序入口,我初入职场的时候,也曾经这么做过,毕竟这是一种符合直觉的做法。不过,既然程序员有写测试的需求,如此反复出现的东西,就会有更好的自动化方案。于是开始测试框架出现了。
最早的测试框架起源是 Smalltalk。这是一门早期的面向对象程序设计语言它有很多拥趸很多今天流行的编程概念就来自于 Smalltalk测试框架便是其中之一。
真正让测试框架广泛流行起来,要归功于 Kent Beck 和 Erich Gamma。Kent Beck 是极限编程的创始人,在软件工程领域大名鼎鼎,而 Erich Gamma 则是著名的《设计模式》一书的作者,很多人熟悉的 Visual Studio Code 也有他的重大贡献。
有一次,二人一起从苏黎世飞往亚特兰大参加 OOPLSAObject-Oriented Programming, Systems, Languages & Applications大会在航班上两个人结对编程写出了JUnit。从这个名字你便不难看出它的目标是打造一个单元测试框架。
顺便说一下,如果你知道 Kent Beck 是个狂热的 Smalltalk 粉丝,写过 SUnit 测试框架,就不难理解这两个人为什么能在一次航班上就完成这样的力作。
JUnit 之后,测试框架的概念逐渐开始流行起来。如今的“程序世界”,测试框架已经成为行业标配,每个程序设计语言都有自己的测试框架,甚至不止一种,一些语言甚至把它放到了标准库里,行业里也用 XUnit 统称这些测试框架。
这种测试框架最大的价值,是把自动化测试作为一种最佳实践引入到开发过程中,使得测试动作可以通过标准化的手段固定下来。
测试模型:蛋卷与金字塔
在前面的讨论里,我们把测试分为人工测试和自动化测试。即便我们只关注自动化测试,也可以按照不同的层次进行划分:将测试分成关注最小程序模块的单元测试、将多个模块组合在一起的集成测试,将整个系统组合在一起的系统测试。
有人喜欢把验收测试也放到这个分类里。为了简化讨论,我们暂时忽略验收测试。
随之而来的一个问题是,我们应该写多少不同层次的测试呢?理论上固然是越多越好了,但实际上,做任何事都是有成本的,所以,人们必须有所取舍。根据不同测试的配比,也就有了不同的测试模型。
有一种直觉的做法是,既然越高层的测试覆盖面越广,那就多写高层测试,比如系统测试。
当然,有些情景高层的测试不容易覆盖到的,所以,还要有一些底层的测试,比如单元测试。在这种情况下,底层的测试只是作为高层测试的补充,而主力就是高层测试。这样就会形成下面这样一种测试模型:冰淇淋蛋卷。
听说过冰淇淋蛋卷测试模型的人并不多,它是一种费时费力的模型,要准备高层测试实在是太麻烦了。
之所以要在这里提及它,是因为虽然这个概念很多人没听说过,但是有不少团队的测试实际采用的就是这样一种模型,这也是很多团队觉得测试很麻烦却不明就里的原因。
接下来,要说说另一种测试模型,也是行业里的最佳实践:测试金字塔。
Mike Cohn 在自己的著作《Succeeding with Agile》提出了测试金字塔但大多数人都是通过 Martin Fowler 的文章知道的这个概念。
从图中我们不难看出,它几乎是冰淇淋蛋卷的反转,测试金字塔的重点就是越底层的测试应该写得越多。
想要理解测试金字塔成为行业最佳实践的缘由,我们需要理解不同层次测试的差异。越是底层的测试,牵扯到相关内容越少,而高层测试则涉及面更广。
比如单元测试,它的关注点只有一个单元,而没有其它任何东西。所以,只要一个单元写好了,测试就是可以通过的;而集成测试则要把好几个单元组装到一起才能测试,测试通过的前提条件是,所有这些单元都写好了,这个周期就明显比单元测试要长;系统测试则要把整个系统的各个模块都连在一起,各种数据都准备好,才可能通过。
这个模块的主题是“任务分解”,我必须强调一点:小事反馈周期短,而大事反馈周期长。小事容易做好,而大事难度则大得多。所以,以这个标准来看,底层的测试才更容易写好。
另外,因为涉及到的模块过多,任何一个模块做了调整,都有可能破坏高层测试,所以,高层测试通常是相对比较脆弱的。
此外,在实际的工作中,有些高层测试会牵扯到外部系统,这样一来,复杂度又在不断地提升。
人们会本能地都会倾向于少做复杂的东西,所以,人们肯定不会倾向于多写高层测试,其结果必然是,高层测试的测试量不会太多,测试覆盖率无论如何都上不来。而且,一旦测试失败,因为牵扯的内容太多,定位起来也是非常麻烦的。
而反过来,将底层测试定义为测试主体,因为牵扯的内容少,更容易写,才有可能让团队得到更多的测试,而且一旦出现问题,也会更容易发现。
所以,虽然冰淇淋蛋卷更符合直觉,但测试金字塔才是行业的最佳实践。
当测试金字塔遇到持续集成
测试金字塔是一个重要实践的基础,它就是持续集成。当测试数量达到一定规模,测试运行的时间就会很长,我们可能无法在本地环境一次性运行所有测试。一般我们会选择在本地运行所有单元测试和集成测试,而把系统测试放在持续集成服务器上执行。
这个时候,底层测试的数量就成了关键,按照测试金字塔模型,底层测试数量会很多,测试可以覆盖主要的场景;而按照冰淇淋蛋卷模型,底层测试的数量则有限。
作为提交代码的防护网,测试数量多寡决定着得到反馈的早晚。所以,金字塔模型与持续集成天然就有着很好的配合。
需要特别注意的是,不是用单元测试框架写的测试就是单元测试。很多人用单元测试框架写的是集成测试或是系统测试。单元测试框架只是一个自动化测试的工具而已,并不是用来定义测试类型的。
在实际工作中,区分不同测试有很多种做法,比如,将不同的测试放到不同的目录下,或是给不同类型的测试一个统一的命名规范。
区分不同类型测试主要目的,主要是在不同的场景下,运行不同类型的测试。就像前面提到的做法是,在本地运行单元测试和集成测试,在持续集成服务器上运行系统测试。
总结时刻
测试是软件开发重要的组成部分,测试应该是软件开发团队中所有人的事,而不仅仅是测试人员的事。因为软件变更成本会随着时间和开发阶段逐步增加,能在早期解决的问题,就不要将它延后至下一个阶段。
在测试问题上,程序员有着天生的优势,会写代码,于是,程序员拥有了一个突出的强项,自动化测试。写测试应该是程序员工作完成的重要组成部分。
随着人们对于测试理解的加深,各种各样的测试都出现了,也开始有了测试的分类:单元测试、集成测试、系统测试等等。越在底层测试,成本越低,执行越快;越在高层测试,成本越高,执行越慢。
人的时间和精力是有限的,所以,人们开始思考不同的测试如何组合。在这个方面的最佳实践称之为测试金字塔,它强调的重点是,越底层的测试应该写得越多。只有按照测试金字塔的方式写测试,持续集成才能更好地发挥作用。
如果今天的内容你只能记住一件事,那请记住:多写单元测试。
最后,我想请你分享一下,你的团队在写测试上遇到哪些困难呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 先写测试,就是测试驱动开发吗?
你好,我是郑晔。
在上一讲中,我向你说明了为什么程序员应该写测试,今天我准备与你讨论一下程序员应该在什么阶段写测试。
或许你会说,写测试不就是先写代码,然后写测试吗?没错,这是一个符合直觉的答案。但是,这个行业里确实有人探索了一些不同的做法。接下来,我们就将进入不那么直觉的部分。
既然自动化测试是程序员应该做的事,那是不是可以做得更极致一些,在写代码之前就把测试先写好呢?
有人确实这么做了于是形成了一种先写测试后写代码的实践这个实践的名字是什么呢它就是测试先行开发Test First Development
我知道当我问出这个问题的时候一个名字已经在很多人的脑海里呼之欲出了那就是测试驱动开发Test Driven Development也就是大名鼎鼎的 TDDTDD 正是我们今天内容的重点。
在很多人看来TDD 就是先写测试后写代码。在此我必须澄清一下,这个理解是错的。先写测试,后写代码的实践指的是测试先行开发,而非测试驱动开发。
下一个问题随之而来,测试驱动开发到底是什么呢?测试驱动开发和测试先行开发只差了一个词:驱动。只有理解了什么是驱动,才能理解了测试驱动开发。要理解驱动,先来看看这两种做法的差异。
测试驱动开发
学习 TDD 的第一步是要记住TDD的节奏“红-绿-重构”。
红,表示写了一个新的测试,测试还没有通过的状态;绿,表示写了功能代码,测试通过的状态;而重构,就是再完成基本功能之后,调整代码的过程。
这里说到的“红和绿”,源自单元测试框架,测试不过的时候展示为红色,通过则是绿色。这在单元测试框架形成之初便已经约定俗成,各个不同语言的后代也将它继承了下来。
我们前面说过,让单元测试框架流行起来的是 JUnit它的作者之一是 Kent Beck。同样也是 Kent Beck 将 TDD 从一个小众圈子带到了大众视野。
考虑到 Kent Beck 是单元测试框架和 TDD 共同的贡献者,你就不难理解为什么 TDD 的节奏叫“红-绿-重构”了。
测试先行开发和测试驱动开发在第一步和第二步是一样的先写测试然后写代码完成功能。二者的差别在于测试驱动开发并没有就此打住它还有一个更重要的环节重构refactoring
也就是说,在功能完成而且测试跑通之后,我们还会再次回到代码上,处理一下代码上写得不好的地方,或是新增代码与旧有代码的重复。因为我们第二步“绿”的关注点,只在于让测试通过。
测试先行开发和测试驱动开发的差异就在重构上。
很多人通过了测试就认为大功告成其实这是忽略了新增代码可能带来的“坏味道Code Smell”。
如果你真的理解重构,你就知道,它就是一个消除代码坏味道的过程。一旦你有了测试,你就可以大胆地重构了,因为任何修改错误,测试会替你捕获到。
在测试驱动开发中,重构与测试是相辅相成的:没有测试,你只能是提心吊胆地重构;没有重构,代码的混乱程度是逐步增加的,测试也会变得越来越不好写。
因为重构和测试的互相配合,它会驱动着你把代码写得越来越好。这是对“驱动”一词最粗浅的理解。
测试驱动设计
接下来,我们再来进一步理解“驱动”:由测试驱动代码的编写。
许多人抗拒测试有两个主要原因:第一,测试需要“额外”的工作量。这里我特意把额外加上引号,因为,你也许本能上认为,测试是额外的工作,但实际上,测试也应该是程序员工作的一部分,这在上一篇文章中我已经讲过。
第二,很多人会觉得代码太多不好测。之所以这些人认为代码不好测,其中暗含了一个假设:代码已经写好了,然后,再写测试来测它。
如果我们把思路反过来,我有一个测试,怎么写代码能通过它。一旦你先思考测试,设计思路就完全变了:我的代码怎么写才是能测试的,也就是说,我们要编写具有可测试性的代码。用这个角度,测试是不是就变得简单了呢?
这么说还是有些抽象我们举个写代码中最常见的问题static 方法。
很多人写代码的时候喜欢使用 static 方法,因为用着省事,随便在哪段代码里面,直接引用这个 static 方法就可以。可是一旦当你写测试的时候你就会发现一个问题如果你的代码里直接调用一个static 方法,这段代码几乎是没法测的。尤其是这个 static 方法里面有一些业务逻辑,根据不同业务场景返回各种值。为什么会这样?
我们想想,常见的测试手法应该是什么样的?如果我们在做的是单元测试,那测试的目标应该就是一个单元,在这个面向对象作为基础设施流行的时代,这个单元大多是一个类。测试一个类,尤其是一个业务类,一般会涉及到一些与之交互的类。
比如,常见的 REST 服务三层架构中,资源层要访问服务层,而在服务层要访问数据层。编写服务层代码时,因为要依赖数据层。所以,测试服务层通常的做法是,做一个假的数据层对象,这样即便数据层对象还没有编写,依然能够把服务层写完测好。
在之前的“蛮荒时代”,我们通常会写一个假的类,模拟被依赖那个类,因为它是假的,我们会让它返回固定的值,使用这样的类创建出来的对象,我们一般称之为 Stub 对象。
这种“造假”的方案之所以可行,一个关键点在于,这个假对象和原有对象应该有相同的接口,遵循同样的契约。从设计上讲,这叫符合 Liskov 替换法则。这不是我们今天讨论的重点,就不进一步展开了。
因为这种“造假”的方案实在很常见,所以,有人做了框架支持它,就是常用的 Mock 框架。使用 Mock 对象,我们可以模拟出被依赖对象的各种行为,返回不同的值,抛出异常等等。
它之所以没有用原来 Stub 这个名字,是因为这样的 Mock 对象往往有一个更强大的能力:验证这个 Mock 对象在方法调用过程中的使用情况,比如调用了几次。
我们回到 static 的讨论上,你会发现 Mock 对象的做法面对 static 时行不通了。因为它跳出了对象体系static 方法是没法继承的,也就是说,没法用一系列面向对象的手法处理它。你没有办法使用 Mock 对象,也就不好设置对应的方法返回值。
要想让这个方法返回相应的值,你必须打开这个 static 方法,了解它的实现细节,精心地按照里面的路径,小心翼翼地设置对应的参数,才有可能让它给出一个你预期的结果。
更糟糕的是,因为这个方法是别人维护的,有一天他心血来潮修改了其中的实现,你小心翼翼设置的参数就崩溃了。而要重新进行设置的话,你只能把代码重读一遍。
如此一来,你的工作就退回到原始的状态。更重要的是,它并不是你应该关注的重点,这也不会增加你的 KPI。显然你跑偏了。
讨论到这里你已经知道了 static 方法对测试而言,并不友好。所以,如果你要想让你的代码更可测,一个好的解决方案是尽量不写 static 方法。
这就是“从测试看待代码,而引起的代码设计转变”的一个典型例子。
关于 static 方法我再补充几点。static 方法从本质上说是一种全局方法static 变量就是一种全局变量。我们都知道,全局方法也好,全局变量也罢,都是我们要在程序中努力消除的。一旦放任 static 的使用,就会出现和全局变量类似的效果,你的程序崩溃了,因为别人在另外的地方修改了代码,代码变得脆弱无比。
static 是一个方便但邪恶的东西。所以,要限制它的使用。除非你的 static 方法是不涉及任何状态而且行为简单,比如,判断字符串是否为空。否则,不要写 static 方法。你看出来了,这样的 static 方法更适合做库函数。所以,我们日常写应用时,能不用尽量不用。
前面关于 static 方法是否可以 Mock 的讨论有些绝对,市面上确实有某些框架是可以 Mock static方法的但我不建议使用这种特性因为它不是一种普遍适用的解决方案只是某些特定语言特定框架才有。
更重要的是,正如前面所说,它会在设计上将你引到一条不归路上。
如果你在自己的代码遇到第三方的 static 方法怎么办,很简单,将第三方代码包装一下,让你的业务代码面对的都是你自己的封装就好了。
以我对大多数人编程习惯的认知,上面这个说法是违反许多人编程直觉的,但如果你从代码是否可测的角度分析,你就会得到这样的结论。
先测试后写代码的方式,会让你看待代码的角度完全改变,甚至要调整你的设计,才能够更好地去测试。所以,很多懂 TDD 的人会把 TDD 解释为测试驱动设计Test Driven Design
还有一个典型的场景从测试考虑会改变的设计那就是依赖注入Dependency Injection
不过,因为 Spring 这类 DI 容器的流行,现在的代码大多都写成了符合依赖注入风格的代码。原始的做法是直接 new 一个对象,这是符合直觉的做法。但是,你也可以根据上面的思路,自己推演一下,从 new 一个对象到依赖注入的转变。
有了编写可测试代码的思路,即便你不做 TDD依然对你改善软件设计有着至关重要的作用。所以写代码之前请先想想怎么测。
即便我做了调整,是不是所有的代码就都能测试了呢?不尽然。从我个人的经验上看,不能测试的代码往往是与第三方相关的代码,比如访问数据库的代码,或是访问第三方服务之类的。但不能测试的代码已经非常有限了。我们将它们隔离在一个小角落就好了。
至此,我们已经从理念上讲了怎样做好 TDD。有的人可能已经跃跃欲试了但更多的人会用自己所谓的“经验”告诉你TDD 并不是那么好做的。
怎么做好 TDD 呢?下一讲,我会给你继续讲解,而且,我们“任务分解大戏”这个时候才开始真正拉开大幕!
总结时刻
一些优秀的程序员不仅仅在写测试,还在探索写测试的实践。有人尝试着先写测试,于是,有了一种实践叫测试先行开发。还有人更进一步,一边写测试,一边调整代码,这叫做测试驱动开发,也就是 TDD。
从步骤上看关键差别就在TDD 在测试通过之后,要回到代码上,消除代码的坏味道。
测试驱动开发已经是行业中的优秀实践,学习测试驱动开发的第一步是,记住测试驱动开发的节奏:红——绿——重构。把测试放在前面,还带来了视角的转变,要编写可测的代码,为此,我们甚至需要调整设计,所以,有人也把 TDD 称为测试驱动设计。
如果今天的内容你只能记住一件事,那请记住:我们应该编写可测的代码。
最后,我想请你分享一下,你对测试驱动开发的理解是怎样的呢?学习过这篇内容之后,你又发现了哪些与你之前理解不尽相同的地方呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 大师级程序员的工作秘笈
你好,我是郑晔。
前面我和大家分享了 TDD 的来龙去脉,那些尚未将 TDD 烂熟于胸的同学会分为两个派别。一派是摩拳擦掌准备动手实践一番另一派是早就自我修炼过但实践之路不通。所以市面上经常会听到有人说TDD 不实用。
但是 TDD 真的不实用吗?
和任何一门技能一样TDD 也是需要练习的。更重要的是,你需要打通 TDD 的“任督二脉”,而这关键正是我们这个模块的主题:任务分解。而且,在今天的内容中,我还将带你领略大师级程序员的工作风范。让我们开始吧!
TDD从何而来
要学最原汁原味的 TDD ,莫过于从源头学起。
从前 TDD 只在小圈子里流行,真正让它在行业里广为人知的是 Kent Beck 那本知名的软件工程之作《解析极限编程》Extreme Programming Explained。这是一本重要的作品它介绍了一种软件开发方法极限编程。
当年他写作之时,许多人都在努力探寻瀑布开发方法之外的软件工程方法,除了极限编程,还有特征驱动开发、水晶开发方法等等,正是这些开发方法的探索,才有了后面敏捷方法的诞生。
极限编程对于行业最大的贡献在于,它引入了大量的实践,比如,前面提到过的持续集成、这里提到的 TDD还有诸如结对编程、现场客户等等。
极限编程之所以叫“极限”,它背后的理念就是把好的实践推向极限。
前面提到持续集成时,我们已经介绍过这个理念,如果集成是好的,我们就尽早集成,推向极限每一次修改都集成,这就是持续集成。
如果开发者测试是好的,我们就尽早测试,推向极限就是先写测试,再根据测试调整代码,这就是测试驱动开发。
如果代码评审是好的,我们就多做评审,推向极限就是随时随地地代码评审,这就是结对编程。
如果客户交流是好的,我们就和客户多交流,推向极限就是客户与开发团队时时刻刻在一起,这就是现场客户。这种极限思维是一种很好的思考问题方式,推荐你也在工作中尝试使用一下。
虽然 TDD 只是《解析极限编程》介绍的诸多实践的一种,它却是与开发人员关系最为密切的一个实践。
随着 TDD 逐渐流行开来,人们对如何做 TDD 也越来越感兴趣于是Kent Beck 又专门为 TDD 写了一本书,叫《测试驱动开发》。
大师级程序员的秘笈
《测试驱动开发》这本书很有意思。如果你只是为了了解 TDD这本书可能很无聊。Kent Beck 在第一部分只是在写一个功能,写完一段又写一段。
这本书我看过两遍,第一遍觉得平淡无奇,这种代码我也能写。第二遍看懂他的思路时,我几乎是震惊的感觉,因为它完全是在展示 Kent Beck 的工作方式。这也是我把 TDD 放到这个部分来讲的重要原因Kent Beck 在做的就是任务分解。任务分解,也是这本书的真正价值所在。
当时,我已经工作了很多年,自以为自己在写代码上已经很专业了。看懂 Kent Beck 的思路,我才知道,与他相比,我还不够专业。
Kent Beck 是怎么做的呢每当遇到一件要做的事Kent Beck 总会先把它分解成几个小任务,记在一个清单上,然后,才是动手写测试、写代码、重构这样一个小循环。等一个循环完成了,他会划掉已经做完的任务,开始下一个。
一旦在解决问题的过程中遇到任何新的问题,他会把这个要解决的问题记录在清单上,保证问题不会丢失,然后,继续回到自己正在处理的任务上。当他把一个个任务完成的时候,问题就解决完了。
你或许会纳闷,这有什么特别的吗?你不妨回答这样一个问题,你多长时间能够提交一次代码?如果你的答案超过半天,对不起,你的做法步子一定是太大了。你之所以不能小步提交,一定是牵扯了太多相关的部分。
Kent Beck 的做法清晰而有节奏,每个任务完成之后,代码都是可以提交的。看上去很简单,但这是大多数程序员做不到的。
只有把任务分解到很小,才有可能做到小步提交。你能把任务分解到很小,其实是证明你已经想清楚了。而大多数程序员之所以开发效率低,很多时候是没想清楚就动手了。
我在 ThoughtWorks 工作时,每个人都会有个 Sponsor类似于工厂里师傅带徒弟的关系。我当时的 Sponsor 是 ThoughtWorks 现任的 CEO 郭晓,他也是写代码出身的。有一次,他给我讲了他和 Wiki 的发明者 Ward Cunningham 一起结对编程的场景。
Ward 每天拿到一个需求,他并不急于写代码,而是和郭晓一起做任务分解,分解到每个任务都很清晰了,才开始动手做。接下来就简单了,一个任务一个任务完成就好了。
当时,郭晓虽然觉得工作节奏很紧张,但思路则是非常清晰的。有时,他也很奇怪,因为在开始工作之前,他会觉得那个问题非常难以解决。结果一路分解下来,每一步都是清晰的,也没遇到什么困难就完成了。
之所以这里要和你讲 Ward Cunningham 的故事,因为他就是当年和 Kent Beck 在同一个小圈子里一起探讨进步的人,所以,在解决问题的思路上,二人如出一辙。
为什么任务分解对于 TDD 如此重要呢?因为只有当任务拆解得足够小了,你才能知道怎么写测试。
很多人看了一些 TDD 的练习觉得很简单,但自己动起手来却不知道如何下手。中间就是缺了任务分解的环节。
任务分解是个好习惯,但想要掌握好它,大量的练习是必须的。我自己也着实花不少时间进行练习,每接到一个任务,我都会先做任务分解,想着怎么把它拆成一步一步可以完成的小任务,之后再动手解决。
微操作
随着我在任务分解上练习的增多,我越发理解任务分解的关键在于:小。
小到什么程度呢?有时甚至可以小到你可能认为这件事不值得成为一件独立的事。比如升级一个依赖的版本,做一次变量改名。
这样做的好处是什么呢?它保证了我可以随时停下来。
我曾在一本书里读到过关于著名高尔夫球手“老虎”伍兹的故事。高尔夫球手在打球的时候,可能会受到一些外界干扰。一般情况下还好,如果他已经开始挥杆,这时候受到了干扰,一般选手肯定是继续把杆挥下去,但通常的结果是打得不理想。
而伍兹遇到这种情况,他会停下来,重新做挥杆的动作,保证了每一杆动作的标准。
伍兹能停下来,固然是经过了大量的练习,但还有一个关键在于,对于别人而言,挥杆击球是一个动作,必须一气呵成。而对伍兹来说,这个动作是由若干小动作组成的,他只不过是刚好完成了某个小动作,而没有做下一个小动作而已。
换句话说,大家同样都是完成一个原子操作,只不过,伍兹的原子操作比其他人的原子操作小得多。
同样,我们写程序的时候,都不喜欢被打扰,因为一旦被打扰,接续上状态需要很长一段时间,毕竟,我们可不像操作系统那么容易进行上下文切换。
但如果任务足够小,完成一个任务,我们选择可以进入到下一个任务,也可以停下来。这样,即便被打扰,我们也可以很快收尾一个任务,不至于被影响太多。
其实,这种极其微小的原子操作在其他一些领域也有着自己的应用。有一种实践叫微习惯,以常见的健身为例,很多人难以坚持,主要是人们一想到健身,就会想到汗如雨下的健身场景,想想就放弃了。
但如果你一次只做一个俯卧撑呢?对大多数人来说,这就不是很难的一件事,那就先做一个。做完了一个如果你还想做,就接着做,不想做就不做了。
一个俯卧撑你会说这也叫健身一个俯卧撑确实是一个很小的动作重要的是一个俯卧撑是你可以坚持完成的如果每天做10个恐怕这都是大多数人做不到的。我们知道养成一个习惯最难的是坚持。如果你有了一个微习惯坚持就不难了。
我曾经在 github 上连续提交代码1000天这是什么概念差不多三年的时间里每天我都能够坚持写代码提交代码这还不算工作上写的代码。
对于大多数人来说,这是不可思议的。但我坚持做到了,不是因为我有多了不起,而是我养成了自己的微习惯。
这个连续提交的基础就是我自己在练习任务分解时不断地尝试把一件事拆细这样我每天都至少能保证完成一小步。当然如果有时间了我也会多写一点。正是通过这样的方法我坚持了1000天也熟练掌握了任务分解的技巧。
一个经过分解后的任务,需要关注的内容是有限的,我们就可以针对着这个任务,把方方面面的细节想得更加清晰。很多人写代码之所以漏洞百出,一个重要的原因就是因为任务粒度太大。
我们作为一个普通人,能考虑问题的规模是有限的,也就很难方方面面都考虑仔细。
微操作与分支模型
经过这种练习之后,任务分解也就成了我的本能,不再局限于写程序上。我遇到任何需要解决的问题,脑子里的第一反应一定是,它可以怎么一步一步地完成,确定好分解之后,解决问题就是一步一步做了。
如果不能很好地分解,那说明我还没想清楚,还需要更多信息,或者需要找到更好的解决方案。
一旦你懂得了把任务分解的重要性,甚至通过训练能达到微操作的水准,你就很容易理解一些因为步子太大带来的问题。举一个在开发中常见的问题,代码开发的分支策略。
关于分支策略,行业里有很多不同的做法。有的团队是大家都在一个分支上写代码,有的是每个人拉出一个分支,写完了代码再合并回去。你有没有想过为什么会出现这种差异呢?
行业中的最佳实践是,基于主分支的模型。大家都在同一个分支上进行开发,毕竟拉分支是一个麻烦事,虽然 git 的出现极大地降低了拉分支的成本。
但为什么还有人要拉出一个分支进行开发呢?多半的原因是他写的代码太多了,改动量太大,很难很快地合到开发的主分支上来。
那下一个问题就来了,为什么他会写那么多代码,没错,答案就是步子太大了。
如果你懂得任务分解,每一个分解出来的任务要改动的代码都不会太多,影响都在一个可控的范围内,代码都可以很快地合并到开发的主分支上,也就没有必要拉分支了。
在我的实际工作中,我带的团队基本上都会采用基于主分支的策略。只有在做一些实验的时候,才会拉出一个开发分支来,但它并不是常态。
总结时刻
TDD 在很多人眼中是不实用的,一来他们并不理解测试“驱动”开发的含义,但更重要的是,他们很少会做任务分解。而任务分解是做好 TDD 的关键点。只有把任务分解到可以测试的地步,才能够有针对性地写测试。
同样听到任务分解这个说法,不同的人理解依然是不一样的。我把任务分解的结果定义成微操作,它远比大多数人理解得小。我们能将任务分解到多小,就决定了我们原子操作的粒度是多大。软件开发中的许多问题正是由于粒度太大造成的,比如,分支策略。
如果今天的内容你只能记住一件事,那请记住:将任务拆小,越小越好。
最后,我想请你分享一下,你身边是否有一些由于任务分解得不够小带来的问题。欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,113 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 如何用最小的代价做产品?
你好,我是郑晔。
前面我们讲了开发任务的分解和需求管理的分解,这些都是针对“已经确定好要做的事情”的分解策略,今天我们再上一个台阶,聊聊面对那些不确定的产品功能该如何分解。
产品经理的想法层出不穷,但是,如果我们一味闷着头实现产品经理的想法,无论你有多大的开发团队都是不够用的。我们要学会用最小的代价做产品。
谈到产品这个话题,在“精益创业:产品经理不靠谱,你该怎么办?”这篇文章中,我给你分享了精益创业的理念,任何的想法都要放到真实世界中检验。
我们的直觉当然是把所有的东西都实现了再去检验,但是世界不会停下来等着我们。事实也一次又一次教育我们,“憋大招”的瀑布式软件开发已经成为不合时宜的“老古董”。那我们的理想怎么实现呢?唯有分解。
我们前面提到精益创业就是通过不断地尝试在真实世界中验证产品想法其中一个重要的实践是最小可行产品Minimum Viable ProductMVP我们这次就把这个实践展开讨论一下。
什么叫最小可行产品?就是“刚刚好”满足客户需求的产品。客户需求好理解,怎么算“刚刚好”呢?其中的关键在于理解“最小”和“可行”。
最小的代价
先说“最小”。这里的“最小”,指的是最小的代价。怎么叫最小的代价,就是能不做的事情就不做,能简化的事情就简化。
首先,我们必须清楚一件事,我们要做的是验证一个想法的可行性,甚至不是为了开发一个软件,开发软件只是一种验证手段。
很多程序员都会有一个认识上的误区,容易把解决方案当成问题。我们开发软件的目的是为了解决问题,如果不写软件就把问题解决了,岂不是更好。
我先讲一个自己的经历,帮你理解一下什么叫“最小”。有一次,有一个朋友找我帮忙,他手头有一些制造业的客户,想做一个物联网相关的项目,帮助这些客户改造设备,实现物联网功能。
该怎么着手呢?把软件写好,给客户试用吗?这样时间太长,成本太高。那么,我们是怎么做的呢?
第一步,我们要验证这样一个想法是否可行。我们做了一个产品文档,就好像我们已经有了这个产品一样,让负责销售的同事拿着这个文档给客户讲讲,看看客户对这个想法的反映。
在这个过程中,我们验证了基本的想法,已有设备进行物联网化改造的需求存在,客户看到了这样的一个东西,各种各样的想法和要求就会冒出来。
此外,我们还获得了一个额外的收获,我们知道了客户对于这样一个产品能够接受的价格区间,这可以帮助团队给产品进行适当的定价。
验证了方向上的想法,我们开始进入到具体的产品设计阶段。这个阶段我们想验证的是,我们给出的产品设计用户是否可以接受。于是,我们决定把这个产品的交互做出来。
得益于原型工具的快速发展,我们用一个原型工具做出了相对完整的用户界面,而且把各种交互流都做出来了。在用户看来,这几乎就是完整的软件了。
他们甚至可以在自己的设备上体验一下这个产品用起来是什么感觉的。一旦上手用起来,他们就会抛出各种细节的问题:如果这样就好了,如果能做到这个就太棒了。当然,他们也会说,这个东西我不需要。
这个时候,我们就可以知道,我们在产品上的假设哪些是好的,哪些是不流畅的。团队拿到这些反馈,就可以再调整产品设计,然后,再给到用户去测试,如此反复进行。有的时候,产品会在一天之内改好几个版本。
经过多轮测试下来,团队有了一大堆的用户反馈,而且是来自真实用户的反馈。接下来,就是整理这些用户反馈,决定哪些可以真正的开发出来,这时候,团队才真正进入到开发阶段。
不知道你注意到了没有,迄今为止,这个团队验证了一大堆的想法,而代码却是一行都没有写,所有花费的工作量都是有针对性的验证。
我们经常听到一个段子,叫“就差一个程序员了”。这说的是,一个创业者把前期的准备都做好,就差程序员把产品开发出来了。
按照 MVP 的思想,这个创业者做的就是对的,前提是他真的把前期准备都做好了。
开发软件是一件成本很高的事情。如果只是验证想法,无论是创业方向,还是产品设计,我们可以找到各种各样的手段,不用写代码。
即便我们不是在做一个新产品,我们依然可以运用这个“最小代价”的理念在日常工作中做事。比如,怎么来衡量产品经理的产品设计是不是好的。我会问,这个功能不做,用户会怎么样?有没有什么替代方案等等。以此来帮助产品经理想清楚自己的产品设计是否真的有价值。
可行的路径
说完了”最小”,我们再来看”可行”。可行是要找到一条路径,给用户一个完整的体验。做程序员出身的人,对软件系统的认识总是一个模块一个模块的,相对比较弱的方面是缺少一个完整的图景。
但从产品可行的角度,我们需要转换一下思路,不是一个模块做得有多完整,而一条用户路径是否通畅。
我再给你分享一个我当年做 P2P 项目经历,这里的 P2P 指的是个人对个人的互联网借贷平台。
这是一个从头开始的项目,项目方和所有的项目方一样,希望昨天这个项目就上线了,如果不能,那就尽快上线一个版本。他们给我们一个时间线,第一个上线的版本是一个月之后。
摆在我们面前的问题是,无论如何,在一个“一穷二白”的基础上,要在一个月内完成一个完整的借贷平台是不太可能的。
时间有限,我们只能做最基本的东西,许多运营上的想法,比如,发红包代金券之类的,第一期一律不做。即便如此,我们仍然认为完成完整的借贷循环是不现实的。
于是,我们就开始从需求完整性的角度动脑筋。这是一个借贷系统,其最基本的模型是:贷款方贷款之后,一次性拿到所有的钱,然后用等额本息的方式每个月还款,最后一个月剩多少钱一次性全还了。
我们在这个模型中找到了一个关键点,每个月还款。换句话说,第一笔贷款发生之后,最早的一笔还款是发生在一个月之后的。
于是,我们做了一个决定,第一个版本只包含贷款能力。是的,这个版本只能贷款,不能还款。因为用户一个月之内不会用到这个功能,你从页面上,完全看不出这样的能力缺失,因为一个月内,根本没有任何用户有可还的款项。
因为缩减了项目规模,我们在预期的一个月内完成所有开发,成功地把项目送上了线。第一批早期用户就开始了使用。从用户的视角看,这是一个功能完整的项目,虽然简单了点,但它是完整的。
当然,我们把还款排到了下一期。按照我们两周一迭代的节奏,在第一期上线两周之后,我们就会上线还款功能,届时贷款方将拥有一个真正的还款功能。
不过,这个还款功能只是每期的等额本息还款,最后的一次性还剩余所有贷款的功能,我们依然是不支持的。因为根据需求设计,最后一次还款最早发生在一年之后。
在我们把基本的功能全部送上线之后,这个系统就是一个真正的、完整的借贷平台了。但是,相对于其他提供相同能力的平台而言,这个系统依然还是很简单。比如,常见的运营功能、短期借贷计划,这个平台都没有。
但我们有了基础,接下来,就是在基础上叠加,而且随着项目方自己团队的构建,我们拥有了够大的团队,可以同时做几个大需求了。
就这样几个月之后我们就逐步上线了一个功能相对完整的P2P平台。在这个过程中我们每个阶段都会上线新功能从用户可见的角度他看到的始终是一个完整的平台其中的变化只有站在内部实现者的角度才能看得清楚。
和大家分享这个例子,主要是想破除大家对于一个“完整”系统概念的认识。当时间有限时,我们需要学会找到一条可行的路径,在完整用户体验和完整系统之间,找到一个平衡。
站在开发团队的角度,我们怎样把 MVP 理念运用在自己的工作中呢?当产品经理有一大堆要实现的功能时,我们就可以根据 MVP 理念,从这些产品功能中找出一条最小的可行路径,重新安排一个合理的开发计划。
总结时刻
产品同样需要分解目前在探索产品的不确定性上的最佳实践是精益创业而精益创业就包含了将庞大的产品分而治之的方式最小可行产品Minimum Viable ProductMVP。最小可行产品就是“刚刚好”满足客户需求的产品。
想要在实践中运用好最小可行产品的理念,就是要用最小的代价找到一条可行的路径。最小的代价就是能不做的事就不做,能简化的事情就简化。
程序员通常愿意用自己的代码解决问题,而写代码通常是代价非常高的解决方案,它应该成为最后的产品解决方案。
可行的路径,是一条完整的用户体验路径,至少在用户眼中是这样的。我们常常会想给客户一个完整的系统,但在时间有限的情况下,我们必须学会分解。
如果今天的内容你只能记住一件事,那请记住:做好产品开发,最可行的方式是采用 MVP。
最后,我想请你分享一下,你遇到或听说过采用 MVP 或类似方法解决问题的案例吗?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,129 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 为什么世界和你的理解不一样?
你好,我是郑晔。
从今天起,我们要开启一个新的模块:沟通反馈。
如果看到沟通反馈几个字,你就以为我打算在这里教一些谈话技巧,那你还真的想错了。
在这个模块里,我打算与你讨论的主题是,生活在真实世界中。沟通反馈和生活在真实世界这两个话题是怎么联系到一起的呢?请听我慢慢道来。
《大富翁》里的沙隆巴斯有句口头禅:人生不如意的事,十有八九!但是不知道你有没有想过这样的一个问题,为什么人生如此不如意?如果这是一篇鸡汤文,我应该告诉你世事艰辛。但我要说的是,真实的原因往往是因为你想得太美好,用我们做软件的例子来看一下:
在我们的愿望中,做出来的产品应该一举成名,现实却是惨淡经营;
在我们的愿望中,产品经理给出的需求应该是清晰明了的,现实却是模模糊糊;
在我们的愿望中,写出来的代码,应该是快捷无错的,维护也很容易,现实却是 Bug 百出,越修改,修改的时间就越长;
在我们的愿望中,你给我布置任务,我应该迅速地理解到关键,现实却是做出来的与你的目标根本就是天差地别;
……
为什么会这样?欢迎来到真实世界,真实世界不是以美好愿望驱动的,它有着自己的运行规律。虽然我们都生活在同一个世界中,但每个人理解世界的方式确实是千差万别。
我们努力地学习各种知识,为的就是更好地理解这个世界的运作方式,而沟通反馈,就是我们与真实世界互动的最好方式。
你也许会好奇,为什么我们对世界的理解会出现偏差?接下来,让我们一起用一个信息论的视角看一下。
一个信息论视角的解释
1948年克劳德·香农Claude Elwood Shannon在《贝尔系统技术学报》Bell System Technical Journal上发表了一篇论文《通信的数学原理》A Mathematical Theory of Communication这是现代信息论的开端。我们程序员熟知的通信、数据压缩、密码学、自然语言处理等诸多领域都有信息论的身影。
我们这里要借鉴的是香农信息论中的一个通信模型,如下图所示:
这个图中包含了几个要素:
信源Information Source它负责产生信息Message
发送器Transmitter它会对信息进行某些操作也就是对信息编码产生信号Signal
信道Channel它是信号传送的媒介。
接收器Receiver它是对信号执行发送器的逆操作解码信号提取出信息。
信宿Destination它负责接收信息。
当然图中还有一个因素叫做噪声Noise指的是削弱信号的东西。不过它并不是我们这里讨论的重点我们暂时忽略它。
我们用一个实际工作中的例子来理解一下这个过程。假设你的项目经理来给你布置一项工作,在这里,项目经理就是一个信源。他的想法就是他的消息,他要把这件事告诉你,先要在大脑中做一次编码,转换成语言表达出来。他说出来的这段话就是信号。
比如,这个信号是“完成一个需求”。这段话通过信道,也就是空气传播到你耳朵里,接收到这段话之后,你会按照自己对这段话的理解进行解码,作为信宿的你,形成了自己的想法,这就是你接到的消息,整个过程就完成了。
我们来看一下,理解偏差是怎么产生的。
项目经理给你传输的信号是“完成一个需求”,在项目经理脑子中,这个信号的原始信息可能是这样的:编写完成这个功能所需的代码,然后为这段代码写好自动化测试,再将它与现有系统集成好,通过测试人员的验证。
而在学习这个专栏之前,你从“完成一个需求”这个信号中解码出来的信息却是:把功能代码写完。这样,问题就出现了。即便这里忽略了噪声的干扰,当编码和解码不是一个版本的时候,无论如何,项目经理的信息都很难准确地传达到你这里。
这就是人们往往对世界产生误解的原因。
信息的传达要经过编码和解码两个过程,无论是编码出现问题,还是解码出现问题,都会造成信息的不准确。
一方面,有些人表达不清楚,一件简单的事,他说了半天,你依然是云里雾里。这就相当于,信源发出的信息经过编码得到的信号已经不准确了。
另一方面,就像听一些技术演讲,人家说得很清楚,但因为自己没有相关背景,依然无法得知人家表达的信息。这就相当于信号虽然准确,但我们没有对应的解码装置,信号无法转成有效信息。
再有就是像前面这个例子,收发双方编解码器不配套,同样的信号得到的信息截然不同,信息传达的目的也不能很好地完成。
有了理论做基础,我们就容易理解世界为什么总和我的理解不一样,这就是编解码的过程出了问题。因为每个人经历见识的差异,造成了各自编解码器的差异。世界是同一个世界,每个人看到的却是千姿百态。
如果想在这个真实的世界中生活得更幸福一些,我们能做点什么呢?那就是改善我们的编解码器。怎么改善自己的编解码器呢?这就是“沟通反馈”这个模块要讨论的内容。
改善编解码
站在改善编解码效果的角度,我们要考虑哪些问题呢?
首先,我们要考虑一下编码器的效果。换句话说,当我们想把信息传达给别人的时候,我们得把信息编码成一个有效的信号,至少要保证在我们这里信息不丢失。
我举个例子,有一次,我在客户现场做咨询,客户的一个程序员给我介绍他们的系统,他讲了二十分钟,我还是听得一头雾水。于是,我打断他,花了五分钟用我的语言给他讲了一遍,然后问他:“你想说的是不是这个意思?”他猛劲点头:“就是这样的。”
为什么会这样呢?究其原因就是,他上来就在给我讲实现细节,完全没有任何铺垫。
要知道,我是来了解情况的,所以,我的背景知识肯定是不足的,凭空理解这些细节是非常困难的一件事。从沟通的角度上看,这么做浪费了大量的时间,因为在过程中,我要不断地让他给我补充这些缺失的背景。这几乎是很多程序员讲东西的通病:讲东西直奔细节。
我在面试中也经常遇到过类似的情况,一些候选人上来就给我讲技术细节,我对他做过的系统一无所知,所以,我只好打断他,让他先把背景给我介绍一下。
同样,很多人抱怨别人不能理解自己,其实,首先应该想的问题是,自己到底有没有把话说清楚。这就是编码器出现问题的情况。
其次,我们还要考虑一下解码器的效果,也就是说,当一个信号呈现在我们面前时,作为接收者,我们是否能够有效地解码信息。
著名作家王小波曾经讲过一个花剌子模信使的故事,说的是中亚古国花剌子模有一个奇怪的风俗,凡是给君王带来好消息的信使,就会得到提升,给君王带来坏消息的人则会被送去喂老虎。如此一来,谁还敢把坏消息带给君王呢?但问题是,君王不听坏消息,坏消息就不存在了吗?
这就相当于解码器出了问题,过滤掉了很多真实的信息。但真实世界就是真实世界,它不会按照人们的美好愿望运行。
再举一个我们身边的例子,能做程序员的人,大多是很聪明的人, 当几个人一起讨论问题时,别人往往刚开了个头,他就认为自己已经理解了别人的想法,然后开始表达自己的观点。信息都不全,何谈解码。所以,开发团队的讨论中常常出现一个人高谈阔论,却离题万里的情况。
我们要想让自己更好地工作生活,就必须接纳真实世界的反馈,而接纳真实世界的反馈,一是需要我们打开自己的接收器,把信号接纳进来,让反馈进来,这是解码的前提;二是扩展见识,提升自己解码器的效果,更好地理解别人要表达的内容到底是什么。
说了编码器和解码器可能出现的问题,我们再来看另外一个可能造成影响的问题:编解码器算法,也就是怎么协调沟通双方更有效地进行沟通。
既然前面已经说了算法不够好会影响到信息的传递,那接下来的问题就是怎样找到一个好的算法。其实,我们从始至终在讲的各种最佳实践就是一个个好的算法,帮助我们改善沟通的效果。
还是回到前面提到“完成一个需求”的例子,我们在“以终为始”模块已经讲过了,通过制定“完成的定义”就可以帮助改善这个过程。这就相当于,沟通的双方都有了一个编解码手册。
当“完成一个需求”这样的信号发出时,作为接收方,你的解码动作就变成了,先要查一下手册里,关于“完成一个需求”的标准动作都有哪些。于是,你就不会对事情做那么简单的估计了。
在“沟通反馈”这个模块下,我还会给你介绍各种“算法”,也就是最佳实践,帮你在工作中提高“信息”传递的效率。
回到我们这部分主题上,沟通反馈就是改善编码、解码以及算法的方式。无论是“发送”得更清楚,还是“接收”得更明白,抑或是通过各种协调算法,都是为了让通信的双方做好准备。
总结时刻
人生不如意之事,十有八九,之所以很多人有如此多的不如意,很大原因在于我们对真实世界有着很多不切实际的幻想,美好的愿望并不能驱动这个世界,在软件开发中也是如此。虽然人和人生活在一个世界中,但对世界的理解却是千差万别的。
我们借用了信息论的一个通信模型解释为什么每个人看到的世界会有如此大的差异,其核心就在于,人和人拥有不同的编解码器。想要在这个真实世界中生活得更幸福一些,需要我们不断地改善自己的编解码器。
改善编解码,需要从几个角度着手,分别是:编码器,让信息能输出更准确;解码器,减少信号过滤,改善解码能力;还有编解码算法,也就是各种来自行业的“最佳实践”,协调沟通的双方。
如果今天的内容你只能记住一件事,那请记住:通过沟通反馈,不断升级自己的编解码能力。
最后,我想请你回想一下,你在工作中遇到过哪些因为沟通反馈不畅造成的问题呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 你的代码为谁而写?
你好,我是郑晔。
关于“沟通反馈”的话题,我准备从代码开始讲起,毕竟我们程序员是靠代码与机器进行沟通的。
写代码是每个程序员的职责,程序员们都知道要把代码写好。但究竟什么叫写好呢?每个人的理解却是各有差异。
编写可维护的代码
初涉编程的程序员可能觉得能把功能实现出来的代码,就是好代码,这个阶段主要是基本功的学习,需要掌握的是各种算法、数据结构、典型的处理手法、常用的框架等等。
经过一段时间工作,日常工作所需的大多数代码,在你看来都是不在话下的。尤其像搜索和问答网站蓬勃发展之后,你甚至不需要像我初入职场时那样,记住很多常见的代码模式,现在往往是随手一搜,答案就有了。
再往后,更有追求的程序员会知道,仅仅实现功能是不够的,还需要写出可维护的代码。于是,这样的程序员就会找一些经典的书来看。
我在这方面的学习是从一本叫做《程序设计实践》The Practice of Programming的书开始的这本书的作者是 Brian Kernighan 和 Rob Pike这两个人都出身于大名鼎鼎的贝尔实验室参与过 Unix 的开发。
写出可维护的代码并不难,它同样有方法可循。今天,我们用写代码中最简单的一件事,深入剖析怎样才能写出可维护的代码,这件事就是命名。
命名难题
计算机科学中只有两大难题:缓存失效和命名。-
—— Phil Karlton
这是行业里流传的一个经典说法,无论是哪本写代码风格的书,都会把命名放在靠前的位置。
估计你开始写程序不久,就会有人告诉你不要用 a、b、c 做变量名,因为它没有意义;步入职场,就会有人扔给你一份编程规范,告诉你这是必须遵循的。
不管怎样,你知道命名是很重要的,但在你心目中,合格的命名是什么样的呢?
想必你知道命名要遵循编码规范比如Java 风格的 camelCase常量命名要用全大写。
但是,这类代码规范给出的要求,大多是格式上的要求。在我看来,这只是底线,不应该成为程序员的追求,因为现在很多编码规范的要求,都可以用静态检查工具进行扫描了。
我们的讨论要从名字的意义说起。作为程序员,我们大多数人理解为什么要避免起无意义的名字,但对于什么样的名字是有意义的,每个人的理解却是不同的。
名字起得是否够好,一个简单的评判标准是,拿着代码给人讲,你需要额外解释多少东西。
比如,我们在代码评审中会看到类似这样的场景:
评审者:这个叫 map 的变量是做什么用的?-
程序员:它是用来存放账户信息的,它的键值是账户 ID值就是对应的账户信息。-
评审者:那为什么不直接命名成 accounts
你知道评审者给出的这个建议是什么意思吗?如果不能一下子意识到,遇到类似的问题,你可能会和这个程序员一样委屈:这个变量本来就是一个 map我把它命名成 map 怎么了?
变量的命名,实际上牵扯到一个重要问题,代码到底是给谁写的?
代码为谁而写?
任何人都能写出计算机能够理解的代码,只有好程序员才能写出人能够理解的代码。-
—— Martin Fowler
代码固然是程序员与机器沟通的重要途径,但是,机器是直白的,你写的代码必须是符合某种规则的,这一点已经由编译器保证了,不符合规则的代码,你想运行,门都没有。
所以,只要你的代码是符合语言规则的,机器一定认。要让机器认,这并不难,你写得再奇怪它都认。行业里甚至有专门的混乱代码比赛。比如,著名的 IOCCCThe International Obfuscated C Code Contest国际 C 语言混乱代码大赛)。
但是,我们写代码的目的是与人沟通,因为我们要在一个团队里与人协同工作。
与人沟通,就要用与人沟通的方式和语言写代码。人和机器不同,人需要理解的不仅是语言规则,还需要将业务背景融入其中,因为人的目的不是执行代码,而是要理解,甚至扩展和维护这段代码。
人要负责将业务问题和机器执行连接起来,缺少了业务背景是不可能写出好代码的。
我们在“为什么世界和你理解的不一样”这篇内容中就讲过,沟通的时候,输出时的编码器很重要,它是保证了信息输出准确性的关键。
很多程序员习惯的方式是用计算机的语言进行表达,就像前面这个例子里面的 map这是一种数据结构的名字是面向计算机的而评审者给出的建议把变量名改成 accounts这是一个业务的名字。
虽然只是一个简单的名字修改,但从理解上,这是一步巨大的跨越,缩短了其他人理解这段代码所需填补的鸿沟,工作效率自然会得到提高。
用业务语言编程
写代码的时候,尽可能用业务语言,会让你转换一个思路。前面还只是一个简单的例子,我们再来看一个。
我们用最常用的电商下单过程来说,凭直觉我们会构建一个订单类 Order。什么东西会放在这个类里呢
首先,商品信息应该在这个类里面,这听上去很合理。然后,既然是电商的订单,可能要送货,所以,应该有送货的信息,没问题吧。再来,买东西要支付,我们会选择一些支付方式,所以,还应该有支付信息。
就这样,你会发现这个订单类里面的信息会越来越多:会员信息可能也要加进去,折扣信息也可能会加入。
你是一个要维护这段代码的人,这个类会越来越庞大,每个修改都要到你这里来,不知不觉中,你就陷入了一个疲于奔命的状态。
如果只是站在让代码运行的角度,这几乎是一个无法解决的问题。我们只是觉得别扭,但没有好的解决方案,没办法,改就改呗!
但如果我们有了看业务的视角,我们会问一个问题,这些信息都放在“订单”是合理的吗?
我们可以与业务人员交流,询问这些信息到底在什么场景下使用。这时候你就会发现,商品信息主要的用途是下单环节,送货信息是在物流环节,而支付信息则用在支付环节。
有了这样的信息,你会知道一件事,虽然我们在用一个“订单”的概念,但实际上,在不同的场景下,用到信息是不同的。
所以,更好地做法是,把这个“订单”的概念拆分了,也就有了:交易订单、物流订单和支付订单。我们原来陷入的困境,就是因为我们没有业务知识,只能笼统地用订单去涵盖各种场景。
如果你在一个电商平台工作,这几个概念你可能并不陌生,但实际上,类似的错误我们在很多代码里都可以看到。
再举个例子,在很多系统里,大家特别喜欢一个叫“用户”的概念,也把很多信息塞到了“用户”里。但实际上,在不同的场景下,它也应该是不同的东西:比如,在项目管理软件中,它应该是项目管理员和项目成员,在借贷的场景下,它应该是借款方和贷款方等等。
要想把这些概念很好地区分出来,你得对业务语言有理解,为了不让自己“分裂”,最好的办法就是把这些概念在代码中体现出来,给出一个好的名字。这就要求你最好和业务人员使用同样的语言。
如果了解领域驱动设计Domain Driven DesignDDD你可能已经分辨出来了我在这里说的实际上就是领域驱动设计。把不同的概念分解出来这其实是限界上下文Bounded Context的作用而在代码里尽可能使用业务语言这是通用语言Ubiquitous Language的作用。
所以,一个好的命名需要你对业务知识有一个深入的理解,遗憾的是,这并不是程序员的强项,需要我们额外地学习,但这也是我们想写好代码的前提。现在,你已经理解了,取个好名字,并不是一件容易的事。
总结时刻
代码是程序员与机器沟通的桥梁,写好代码是每个程序员的追求,一个专业程序员,追求的不仅是实现功能,还要追求代码可维护。如果你想详细学习如何写好代码,我推荐你去读 Robert Martin 的《代码整洁之道》Clean Code这本书几乎覆盖了把代码写好的方方面面。
命名,是写程序中最基础,也是一个程序员从业余走向专业的门槛。我以命名为基础,给你解释了写好代码的提升路径。最初的层次是编写可以运行的代码,然后是编写符合代码规范的代码。
对于命名,最粗浅的理解是不要起无意义的名字,遵循编码规范。但名字起得是否够好,主要看是否还需要额外的解释。很多程序员起名字习惯于采用面向实现的名字,比如,采用数据结构的名字。
再进一步提升,编写代码是要写出人可以理解的代码。因为代码更重要的作用是人和人沟通的桥梁,起一个降低其他人理解门槛的名字才是好名字。
实际上,我们很多没写好的程序有一些原因就是名字起错,把一些概念混淆在一起了。想起好名字,就要学会用业务语言写代码,需要尽可能多地学习业务知识,把业务领域的名字用在代码中。
如果今天的内容你只能记住一件事,那请记住:用业务的语言写代码。
最后,我想请你思考一下,想要写好代码,还有哪些因素是你特别看重的?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 轻量级沟通:你总是在开会吗?
你好,我是郑晔。
今天我们来探讨一个很多程序员日常工作中,经常碰到却会带来困扰的话题:开会。
头疼的开会
有一次,我听到两个程序员在聊天。一个资深程序员说:“还是晚上好,我可以一门心思写代码”,另一个年轻程序员不解地问:“你白天也可以写啊。”
资深程序员很无奈,“我倒是这样想,可是白天参加那么多会,哪有工夫啊!我的代码就只能加班写了。”
这段对话听上去让人有点心酸,但这种现象,确确实实广泛存在于程序员的日常工作中,尤其是你经验丰富又在一个大组织中工作,这几乎成了你的宿命。在这些程序员的认知中,开会太多影响了他们写代码。
你以为我想讨伐开会吗?并不是,开会本身并没有错,因为开会的本意是将大家组织起来解决问题。但请你回想一下,你参加的会议有多少解决了问题呢?
开会是为了解决问题,但真实情况却是开了会又没有解决多少问题,这真是一个奇特的矛盾。
回想一下,你参加过的会议里面,有没有效果特别好的呢?在我职业生涯中,凡是效果特别好的会议,基本上都是用来做信息同步的。比如,领导宣布一个事情,这种会议几乎不会浪费时间。宣布消息,大家收到消息,结束。
那效果不好的会议是什么样呢?几乎都是那些讨论会,你一言我一语,每个会几乎无一例外,都有几个擅长打岔的,这个会基本上都会跑偏,时间就会这样一分一秒地流逝了。
我给你举个例子,我之前参加过一个上线计划的评审会,这个团队的负责人要把相关利益方都召集起来,其中包括上下游可能会受影响的团队、测试、运维等等,一个不大的会议室里挤满了人。
这个负责人刚开始讲方案没几分钟,下游团队的负责人就站出来问:“这个方案为什么要这么做?我担心会对我们系统造成影响。”讲方案的人只好停下来解释。结果是越解释,细节越多,双方你来我往,一个方案评审会,就转变成一个技术讨论会了。
测试和运维的同事本来是想来听技术方案,以便为后续的工作做准备的。看着双方的讨论,一脸无奈,因为他们知道,方案没确定好,所有的事情还是下回再说吧!
怎么样?是不是很熟悉的感觉。为什么会这样?因为他们选错了沟通方式。
开会是一种重量级的沟通,几乎是我们日常工作中最重的。它有很强的仪式感,所以,大家相对来说会很重视。而且会议通常会牵扯到很多人,尤其是与这个事情相关度不那么高的人。
你可以想一下,有多少次开会,你是在精力集中的?如果你是高度集中的,那恭喜你,你是高效地参与其中。但更多时候,你可能神游天外,因为讨论的内容可能与你关系不大,或者你已经听不懂了,你坐在那里的唯一原因是,主持人还没宣布会议结束。
用开会这种重量级的方式讨论问题,就好比杀鸡用了牛刀,这是不恰当的。那该怎么解决这个问题呢?很简单,杀鸡用鸡刀。
轻量级沟通
实际上,真正在会议上能够积极参与讨论的人并不会觉得会议是浪费时间,因为高度参与其中,人是进入到心流状态的,时间流逝很快。觉得浪费时间的,往往是没有参与其中的人。
换句话说,会议之所以给人留下如此不堪的印象,一个重要的原因是,真正参与讨论的人并不多。所以,我们换个角度思考一下,只要把这些真正参与讨论的人拉到一起讨论不就好了?
所以,改善会议的第一个行动项是,减少参与讨论的人数。
有人会说,我这个讨论有好几个议题,每个议题要不同的人参与,那你要做的是,分别找这几个人专门讨论,而不是把大家放到一起。
不知道你发现没有,在讨论行动项的时候,我用的是“讨论”,而没有提到“会议”两个字。我之前说过了,会议是一种重量级的沟通方式。所以,我们会倾向于选择一种轻量级的沟通方式,比如面对面沟通,这样一来,每个人的压力就会小很多。
相比于会议的形式,面对面沟通因为注意力有限,参与的人数不可能太多。也因为参与的人数相对少一些,每个人的投入也会更多一些。
所以,我们的第二个行动项是,如果你要讨论,找人面对面沟通。
一旦理解了这些改进方式,我们就可以改进自己的行为方式。如果有一个问题需要讨论,我要做的是,分别找到相关人针对关心的主题进行讨论,然后,我把讨论的结果汇总再去征求大家意见。如果大家达成一致了,我才会选择开会。
这个时候,开会的目的不再是讨论,而是信息同步:我准备这么干了,相关各方已经同意了,知会大家一下,结束。
站立会议
我前面说过了,开会并非都是不好的,一些信息同步的会还是有必要的。
举个例子有一种实践叫站会Standup。很多公司都在实践它站会甚至成为每天的开工仪式。一般的做法是早上大家来上班了先开一个站会让大家同步一下昨天的工作然后开始今天的工作。
有的人一听到站会这个形式就会皱起眉头。如果是这样,多半是你的团队“站”错了。
你知道这个会为什么是“站”会吗因为按照一般人的习惯站的时间不会太长因为站的时间长累啊所以如果站会超过10分钟你的站会一定是错的。
也许你会说,这点时间恐怕不够给我们站会吧?因为每个人都有一大堆要说的。请问,你觉得其他人说那么多,你关心吗?现实是,一旦一个人说多了,跟你关系又不大,你就开始思维发散了。
所以,在总长固定的情况下,每个人发言的时间一定是有限的。在有限的时间内,你能说什么呢?我建议你只说三件事:
我昨天做了什么?
我今天打算做什么?
我在过程中遇到了什么问题,需要请求帮助。
“做了什么” ,是为了与其他人同步进展,看事情是否在计划上。一旦偏离计划,请主动把它提出,这样,项目经理可以过问,因为这会涉及到是否要调整项目计划;
“要做什么” ,是同步你接下来的工作安排。如果涉及到与其他人协作,也就是告诉大家,让他们有个配合的心理准备;
“问题和求助”, 就是与其他人的协作,表示:我遇到不懂的问题,你们有信息的话,可以给我提供一下。
这三件事都是与别人相关的,几句话快速说完,结束。因为这些事情与别人相关,所以,大家的注意力可以相对集中一些。
你或许会问,如果我的问题很复杂,需要讨论该怎么办。对不起,那是另外一件事,你可以在站会结束之后,找相关人去讨论,不要在这个会上浪费大家时间。在站会上,你只要在问题和求助中告诉大家,你有一个问题,需要相关人讨论,结束。
为了让大家保持注意力集中,我的一些团队还用过发言令牌的方式。比如,找一个毛绒玩具,谁拿到“令牌”谁发言,然后,随机地扔给一个人,一旦这个人走神,大家一下子就能发现了。
一些有趣的方式、短暂的时间,以及与所有人相关的事情,因为满足了这三点,所以普遍来说,这种站会效果还可以。
关于站会,有一个典型的错误是,有些团队把站会开成了汇报会。项目负责人指定一个个轮流发言,说的人都向负责人在汇报工作,其他人自然就容易走神了,因为事情与己无关。
还有一点你可能会有疑问,我所在的团队比较大,一个人几句话时间也会很长。
当团队很大时更应该做的是把团队拆分了因为你不太可能与20个人紧密地工作在一起。沃顿商学院曾经做过一项研究5-12个人是一个恰当的团队规模每个人在其中都能发挥自己的重要作用。
总结时刻
开会是很多程序员的困扰,太多的会议甚至会影响到你工作的进展。开会的本意是为了解决问题,但实际上,大多数会议并不能很好地解决问题。因为会议是一种重量级的沟通方式,很多人参加会议时,并不能很好地参与其中。
如果你想用会议的形式与别人讨论问题,最好放弃这种打算,面对面的沟通是最好的方式。因为面对面沟通很轻,人数相对少,每个人参与度就会高很多。基于这种改进,我们可以把大部分会议都改成信息同步的会,效率就会得到提高。
我还给你介绍了一种特殊的会议:站会。之所以采用站会的方式,就是要控制时间。在站会上每个人说什么,我给了你一个建议的格式:
我昨天做了什么?
我今天打算做什么?
我在过程中遇到了什么问题,需要请求帮助。
如果你经常组织别人开会,请你想一下,是不是自己没有利用好开会这件事;如果你经常被别人组织开会,不妨把这篇文章转发给他,让他别总是开会“讨论”问题。
如果今天的内容你只能记住一件事,那请记住:多面对面沟通,少开会。
最后,我想请你思考一下,你在工作中,还遇到过哪些因为开会带来的问题呢?欢迎留言与我写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,107 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 作为程序员,你也应该聆听用户声音
你好,我是郑晔。
在前面的专栏内容中,我们讨论过几次与产品经理的交流:你应该问问产品经理为什么要做这个产品特性,要用 MVP最小可行产品的角度衡量当前做的产品特性是不是一个好的选择。
但还有一个问题可能困扰着我们:怎么判断产品经理说的产品特性是不是用户真的需要的呢?
很多时候,产品经理让你实现一个产品特性,你感觉这么做好像不太对,却又说不出哪不对,想提出自己的看法,却不知道从哪下手。之所以会遇到这样的问题,一个重要的原因就是,你少了一个维度:用户视角,你需要来自真实世界的反馈。
吃自家的狗粮
产品经理无论要做什么,他都必须有一个立足的根基:为用户服务。所以,如果你了解了用户怎么想,你就有资本判断产品经理给出的需求,是否真的是用户需要的了。
而作为一个程序员,欠缺用户视角,在与产品经理的交流中,你是不可能有机会的,因为他很容易用一句话就把你打败:“这就是用户需求。”
很多程序员只希望安安静静地写好代码,但事实上,对于大多数人来说,安安静静是不太可能写好代码的,只有不断扩大自己的工作范围,才可能对准“靶子”。
今天我们讨论的角度,就是要你把工作范围扩大,由听产品经理的话,扩大成倾听用户的声音。
作为程序员你应该听说过一个说法“Eat your own dog food”吃自家的狗粮。这个说法有几个不同的来源都是说卖狗粮的公司真的用了自家的狗粮。
从1988年开始这个说法开始在 IT 行业流行的微软的保罗·马瑞兹Paul Maritz写了一封“Eating our dog food”的邮件提到要“提高自家产品在内部使用的比例。”从此这个说法在微软迅速传播开来。
如今,自己公司用自己的产品几乎成了全行业的共识。抛开一些大公司用这个说法做广告的因素,不断使用自家的产品,会让你多出一个用户的视角。
在挑毛病找问题这件事上,人是不需要训练的,哪里用着不舒服,你一下子就能感受到。所以,不断地使用自家产品,你自己就是产品的用户,这会促使你不断去思考怎么改进产品,再与产品经理讨论时,你就自然而然地拥有了更多的维度。
比如,前面在讨论 MVP 时,我曾经讲过一个我做 P2P 产品的经历。在这个项目中,我就作为用户在上面进行了一些操作。当自己作为用户使用时,就发现了一些令人不爽的地方。
比如,一开始设计的代金券只能一次性使用,如果代金券金额比较大,又没那么多本金,只能使用代金券的一部分,就会让人有种“代金券浪费了”的感觉。
于是,我就提出是不是可以把代金券多次使用。很快,产品就改进了设计。这种改进很细微,如果你不是用户,只从逻辑推演的角度是很难看到这种差异的。
当你吃不到狗粮时
不过不是每家公司的产品都那么“好吃”。“吃自家狗粮”的策略对于那些拥有“to C”产品的公司来说相对是比较有效的。但有时候你做的产品你根本没有机会用到。
我曾经与很多海外客户合作过,我做的很多产品,自己根本没有机会使用。比如,我做过五星级酒店的审计平台。除了能对界面上的内容稍微有点感觉之外,对于使用场景,我是完全不知道的。
如果没有机会用到自己的产品,我们该怎么办呢?我们能做的就是尽可能找机会,去到真实场景里,看看用户是如何使用我们软件的。
比如,做那个酒店审计平台时,我就和客户一起到了一家五星级酒店,看着他们怎样一条一条地按照审计项核查,然后把审计结果登记在我们的平台上。
那些曾经只在写程序时见到的名词,这回就活生生地呈现在我眼前了。后来再面对代码时,我看到就不再是一个死板的程序了,我和产品经理的讨论也就更加扎实了。
有的团队在这方面有比较好的意识,会主动创造一些机会,让开发团队成员有更多机会与用户接触。
比如,让开发团队到客服团队轮岗。接接电话,听听用户的抱怨,甚至是谩骂。你会觉得心情非常不好,但当你静下来的时候,你就会意识到自己的软件有哪些问题,如果软件做得不好,影响会有多大。
这时,你也就能理解,为什么有的时候,很多业务人员会对开发团队大发雷霆了,因为他们是直接面对用户“炮火”的人。
我们为什么要不断地了解用户的使用情况呢?因为用户的声音是来自真实世界的反馈。不去聆听用户声音,很容易让人自我感觉良好。还记得在 “为什么世界和你的理解不一样” 中,我们提到的那个只接收好消息的花剌子模国国王的例子吗?
我们要做一个有价值的产品,这个“价值”,不是对产品经理有价值,而是要对用户有价值。华为总裁任正非就曾经说过,“让听得见炮声的人来做决策。”
我们做什么产品,本质上不是由产品经理决定的,而是由用户决定的。只有听见“炮声”,站在一线,我们才更有资格判断产品经理给出的需求是否真的是用户所需。
当产品还没有用户时
如果你的团队做的是一个新的产品,还没有真正的用户,那又该怎么办呢?你可以尝试一下“用户测试”的方法。
之前我做过一个海外客户的项目。因为项目处于启动阶段,我被派到了客户现场。刚到那边,客户就兴高采烈地告诉我,他们要做一个用户测试,让我一起参加。当时,我还有点不知所措,因为我们的项目还没有开始开发,一个什么都没有的项目就做用户测试了?是的,他们只做了几个页面,就开始测试了。
站在今天的角度,我前面已经给你讲过了精益创业和 MVP你现在理解起来就会容易很多。是的他们就是要通过最小的代价获取用户反馈。
他们是怎么做测试的呢?首先是一些准备工作,找几个普通用户,这些人各有特点,能够代表不同类型的人群。准备了一台摄像机,作为记录设备,拍摄用户测试的全过程。还准备了一些表格,把自己关注的问题罗列上去。
然后,就是具体的用户测试了。他们为用户介绍了这个测试的目的、流程等一些基本信息。然后,请用户执行几个任务。
在这个过程中,测试者会适时地让用户描述一下当时的感受,如果用户遇到任何问题,他们会适当介入,询问出现的问题,并提供适当的帮助。
最后,让用户为自己使用的这个产品进行打分,做一番评价。测试者的主要工作是观察和记录用户的反应,寻找对用户使用造成影响的部分。做用户测试的目的就是看用户会怎样用这个网站,这样的网站设计会对用户的使用有什么影响。
当天测试结束之后,大家一起整理了得到的用户反馈,重新讨论那些给用户体验造成一定影响的设计,然后调整一版,再来做一次用户测试。
对我来说,那是一个难忘的下午,我第一次这么近距离地感受用户。他们的关注点,他们的使用方式都和我曾经的假设有很多不同。后面再来设计这个系统时,我便有了更多的发言权,因为产品经理有的角度,我作为开发人员也有。
最后,我还想说一个程序员常见的问题:和产品经理没有“共同语言。”
因为他们说的通常是业务语言而我们程序员的口中基本上是计算机语言。这是两个领域的东西很难互通。前面在讨论代码的时候我提到要用业务的语言写代码实际上这种做法就是领域驱动设计中的通用语言Ubiquitous Language
所谓通用语言,不只是我们写代码要用到,而是要让所有人说一套语言,而这个语言应该来自业务,来自大家一起构建出的领域模型。
这样大家在交流的时候,才可能消除歧义。所以,如果你想让项目顺利进行,先邀请产品经理一起坐下来,确定你们的通用语言。
总结时刻
今天我们讨论了一个重要的话题:倾听用户声音。这是开发团队普遍欠缺的一种能力,更准确地说,是忽略的一种能力。所以,“吃自家的狗粮”这种听上去本来是理所当然的事情,才被反复强调,成为 IT 行业的经典。
在今天这一讲,我给你介绍了“了解用户需求”的不同做法,但其归根结底就是一句话,想办法接近用户。
无论是自己做用户,还是找机会接触已有用户,亦或是没有用户创造用户。只有多多听取来自真实用户的声音,我们才不致于盲目自信或是偏颇地相信产品经理。谁离用户近,谁就有发言权,无论你的角色是什么。
如果今天的内容你只能记住一件事,那请记住:多走近用户。
最后,我想请你思考一下,在你的实际工作中,有哪些因为走近客户而发现的问题,或者因为没有走近客户造成的困扰呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
27 尽早暴露问题: 为什么被指责的总是你?
你好,我是郑晔。
今天我准备讨论一个经常会让很多程序员郁闷的事情,为什么你已经工作得很辛苦了,但依然会被指责。在讨论这个问题之前,我们先来讲一个小故事。
程序员小李这天接到了一个新的任务。系统要做性能提升,原先所有的订单都要下到数据库里,由于后来有很多订单都撤了,反复操作数据库,对真正成交过程的性能造成了影响。所以,技术负责人老赵决定把订单先放到缓存里。
这就会牵扯到一个技术选型的问题,于是,老赵找了几个可以用作缓存的中间件。为了给大家一个交代,老赵决定让小李给这几个中间件做一个测试,给出测试结果,让大家一起评估。小李高兴了,做这种技术任务最开心,可以玩新东西了。
老赵问他:“多长时间可以搞定?”
小李说:“一个星期吧!”
老赵也很爽快,“一个星期就一个星期。不过,我得提个要求,不能是纯测中间件,得带着业务跑。”
“没问题。”小李一口答应下来。老赵怕小李做多了,还特意嘱咐他,只测最简单的下单撤单环节就好。
等真的开始动手做了,小李发现,带着业务跑没那么容易,因为原来的代码耦合度太高,想把新的中间件加进去,要先把下单和撤单环节隔离开来。而这两个操作遍布在很多地方,需要先做一些调整。
于是,小李只好开始不分白天黑夜地干起来。随着工作的深入,小李越发觉得这个活是个无底洞,因为时间已经过半了,他的代码还没调整完。
这时,老赵来问他工作进展,他满面愁容地说,估计干不完了。老赵很震惊,“不就是测试几个中间件吗?”
小李也一脸委屈,“我们为啥要带着业务跑啊?我这几天的时间都在调整代码,以便能够把中间件的调用加进去。”
老赵也很疑惑,“你为啥要这么做?”
“你不是说要带着业务跑吗?”
“我是说要带着业务跑啊!但你可以自己写一个下单撤单的程序,主要过程保持一致就好了。”小李很无奈,心里暗骂,你咋不早说呢?
是啊!你咋不早说呢?不过,我想说不是老赵,而是小李。
谁知道有问题?
我们来分析一下问题出在哪。在这个故事里,小李和老赵也算有“以终为始”的思维,在一开始就确定了一个目标,做一个新中间件测试,要带着业务跑。
小李可以说是很清楚目标的,但在做的过程中,小李发现了问题,原有代码很复杂,改造的工作量很大,工作可能没法按时完成。
到此为止,所有的做法都没有错。但接下来,发现问题的小李选择了继续埋头苦干,直到老赵来询问,无奈的小李才把问题暴露出来。
在老赵看来,这并不是大事,调整一下方案就好了。但是小李心生怨气,在他看来,老赵明明有简单方案,为啥不早说,害得自己浪费了这么多时间。
但反过来,站在老赵的角度,他是怎么想的呢?“我的要求是带着业务跑,最理想的方案当然是和系统在一起,你要是能搞定,这肯定是最好的;既然你搞不定,退而求其次,自己写一个隔离出来的方案,我也能接受。”
你看出来问题在哪了吗?老赵的选择没有任何问题,问题就出在,小李发现自己可能搞不定任务的时候,他的选择是继续闷头做,而不是把问题暴露出来,寻求帮助。
作为一个程序员,克服技术难题是我们工作的一个重要组成部分,所以,一旦有困难我们会下意识地把自己投入进去。但这真的是最好的做法吗?并不是,不是所有的问题,都是值得解决的技术难题。
在工作中遇到问题,这简直是一件正常得不能再正常的事儿了,即便我们讲了各种各样的工作原则,也不可避免会在工作中遇到问题。
既然是你遇到的问题,你肯定是第一个知道问题发生的人,如果你不把问题暴露出来,别人想帮你也是有心无力的。
如果老赵不过问,结果会怎么样?必然是小李一条路跑到黑。然后,时间到了,任务没完成。
更关键的是,通常项目计划是一环套一环的,小李这边的失败,项目的后续部分都会受到影响,项目整体延期几乎是必然的。这种让人措手不及的情况,是很多项目负责人最害怕见到的。
所以,虽然单从小李的角度看,这只是个人工作习惯的事,但实际上,处于关键节点的人可能会带来项目的风险。而小李的问题被提前发现,调整的空间则会大很多。
遇到问题,最好的解决方案是尽早把问题暴露出来。其实,这个道理你并不陌生,因为你在写程序的时候,可能已经用到了。
Fail Fast
写程序有一个重要的原则叫 Fail Fast这是什么意思呢就是如果遇到问题尽早报错。
举个例子我做了一个查询服务可以让你根据月份查询一些信息一年有12个月查询参数就是从1到12。
问题来了,参数校验应该在哪做呢?如果什么都不做,这个查询参数就会穿透系统,传到你的数据库上。
如果传入的参数是合法的当然没有任何问题这个查询会返回一个正常的结果。但如果这个参数是无意义的比如传一个“13”那这个查询依然会传到数据库上。
事实上,很多不经心的系统就是这么做的,一旦系统出了什么状况,你很难判断问题的根源。
在这个极度简化的例子里,你可以一眼看出问题出在输入参数上,一旦系统稍具规模,请求来自不同的地方,这些请求最终都汇集到数据库上,识别来源的难度就会大幅度增加。尤其是系统并发起来,很难从日志中找出这个请求的来源。
你可能会说“为了方便服务对不同数据来源进行识别可以给每个请求加上一个唯一的请求ID吧
系统就是这么变复杂的我经常调侃这种解决方案就是没有困难创造困难也要上。当然即便以后真的加上请求ID理由也不是现在这个。
其实,要解决这个问题,做法很简单。稍微有经验的人都知道,参数校验应该放在入口的位置上,不合法的请求就不让它往后走了。这种把可能预见的失败拦在外面的做法就是 Fail Fast有问题不可怕让失败尽早到来。
上面这个例子很简单,我再给你举一个例子。如果配置文件缺少了一个重要参数,比如,缺少了数据库最大连接数,你打算怎么处理?很多人会选择给一个缺省值,这就不是 Fail Fast 的做法。既然是重要参数,少了就报错,这才叫 Fail Fast。
其实Fail Fast 也有一些反直觉的味道,很多人以构建健壮系统为由,兼容了很多奇怪的问题,而不是把它暴露出来。反而会把系统中的 Bug 隐藏起来。
我们都知道,靠 debug 来定位问题是最为费时费力的一种做法。所以,别怕系统有问题,有问题就早点报出来。
顺便说一下在前面这个例子里透传参数还有几个额外的问题。一是会给数据库带来额外的压力如果有人用无意义查询作为一种攻击手段它会压垮你的数据库。再有一点也是安全问题一些SQL攻击利用的就是这种无脑透传。
克服心理障碍
对我们来说,在程序中尽早暴露问题是很容易接受的。但在工作中暴露自己的问题,却是很大的挑战,因为这里还面临着一个心理问题:会不会让别人觉得自己不行。
说实话,这种担心是多余的。因为每个人的能力是强是弱,大家看得清清楚楚。只有你能把问题解决了大家才会高看你,而把问题遮盖住,并不能改善你在别人心目中的形象。
既然是问题,藏是藏不住的,就像最开始那个故事里的小李,即便他试图隐藏问题,但最后他还是不可能完成的,问题还是会出来,到那时,别人对他的评价,只会更加糟糕。
比起尽早暴露问题,还有更进一步的工作方式,那就是把自己的工作透明化,让别人尽可能多地了解自己的工作进展,了解自己的想法。
如果能做到这一点,其他人在遇到与你工作相关的事情,都会给你提供信息,帮助你把工作做得更好。当然,这种做法对人的心理挑战,比尽早暴露问题更大。
从专栏开始到现在,我们讲了这么多原则和实践,其实,大多数都是在告诉你,有事先做。
一方面,这是从软件变更成本的角度在考虑;另一方面,也是在从与人打交道的角度在考虑。
越往前做,给人留下的空间和余地越大,调整的机会也就越充足。而在最后一刻出现问题的成本实在太高,大到让人无法负担。
总结时刻
我们今天讨论了一个重要的工作原则,把事情往前做,尽早暴露问题。我们前面讲的很多内容说的都是这个原则,比如,要先确定结果,要在事前做推演等等。越早发现问题,解决的成本就越低,不仅仅是解决问题本身的成本,更多的是对团队整体计划的影响。
一方面,事前我们要通过“以终为始”和“任务分解”早点发现问题;另一方面,在做事过程中,一旦在有限时间内搞不定,尽早让其他人知道。
这个原则在写程序中的体现就是 Fail Fast很多程序员因为没有坚持这个原则不断妥协造成了程序越来越复杂团队就陷入了无尽的泥潭。
原则很简单,真正的挑战在于克服自己的心理障碍。很多人都会下意识地隐瞒问题,但请相信你的队友,大家都是聪明人,问题是藏不住的。
如果今天的内容你只记住一件事,那请记住:事情往前做,有问题尽早暴露。
最后,我想请你回想一下,如果遵循了这样的工作原则,你之前犯过的哪些错误是可以规避掉的呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,117 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
32 持续交付:有持续集成就够了吗?
你好,我是郑晔。
在前面两讲,我给你讲了开发过程的自动化,将我们的程序打成发布包;然后讲了部署过程的自动化,通过各种工具将发布包部署起来。
有了这些基础,我们就可以考虑在每次开发完之后,将程序打包部署到环境中。开发完就自动打包,然后自动部署,听起来很像持续集成是不是?
关于持续集成,我在专栏里已经讲过两次,分别讨论了“为什么要做持续集成”和“怎么做好持续集成”。但持续集成的讨论只停留在开发环节。
有了前面两讲的准备,我们就可以把这个过程再进一步延伸。聪明的你或许已经听出来了,这次我要讲的主题是持续交付。
持续交付
让持续交付这个概念广为人知的是一本书Jez Humble 和 Dave Farley 的《持续交付》Continuous Delivery
前面讲持续集成的发展历史时,我提到了 CruiseControl它是持续集成服务器的鼻祖。因为持续集成的不断发展2007年我的老东家 ThoughtWorks 公司有意以 CruiseControl 为基础提供企业级服务于是成立了一个团队打造一个更好的持续集成服务器Jez Humble 就是在这个团队中工作的。
同样在这个团队工作的还有一个人,乔梁,他是《持续交付》这本书的中文版译者,而且在这本书出版近十年后,他自己写了《持续交付 2.0》,把自己多年来关于持续交付的新理解整理了进去。
那么,什么叫更好的持续集成服务器呢?当时我的理解很浅薄,只是希望它有更好的界面,更快的构建速度,而 Jez Humble 他们对于这个产品的构想远远超过了我当时的想象,他们将生产环境也纳入了考量。
什么是持续交付?简言之,它就是一种让软件随时处于可以部署到生产环境的能力。从一个打好的发布包到部署到生产环境可用,这中间还差了什么呢?那就是验证发布包,部署到环境中。
验证发布包,你或许会想,这不是测试的事吗?这不是已经在持续集成阶段完成的吗?不尽然。在持续集成阶段验证的包,往往缺少了环境的支持。
因为持续集成的环境往往是单机的,主要强调功能验证,而一些与生产环境相关的测试往往是欠缺的。所以,这里就引出了持续交付中一个需要关注的点:环境。
一般来说,在构建持续交付的基础设施时,会有下面几个不同的环境。
持续集成环境,持续集成是持续交付的前提,这个过程主要是执行基本的检查,打出一个可以发布的包。
测试环境Test这个环境往往是单机的主要负责功能验证这里运行的测试基本上都是验收测试级别的而一般把单元测试和集成测试等执行比较快的测试放到持续集成环境中执行。
预生产环境Staging这个环境通常与生产环境配置是相同的比如负载均衡集群之类的都要有只是机器数量上会少一些主要负责验证部署环境比如可以用来发现由多机并发带来的一些问题。
生产环境Production这就是真实的线上环境了。
你也看出来了每个环境的作用是有差异的所以通常不会将所有的验证放在一起执行而是要分阶段的去执行一个阶段不通过是不能进入下一阶段的这种按照不同阶段组织构建的方式称之为构建流水线Build Pipeline
一旦通过了各种验证,就会到构建流水线的最后一个阶段,生产发布。通常来说,生产发布这个过程不是自动化的。我们说,持续交付的关注点在于,让软件具备随时可以发布的能力,但并不等于它要立刻上线,所以,最后这一下,还要由人来决定,到底是不是要上线。
如果把由人决定的是否上线变成自动化的,就成了另外一个实践:持续部署。但通常人们都会比较谨慎,最后这一下还是由人拍板比较稳妥,所以,持续交付是现在的主流。
至此,我们讨论了持续交付的第一个方面,验证发布包。接下来,我们再来看看另外一个重要部分:部署。
DevOps
早期人们做部署都是自己编写 Shell 脚本完成的但在上一讲中我提到的一些工具比如Chef、Puppet、Ansible 等等大幅度地简化了部署脚本的编写。这些工具在业界的兴起与一个概念息息相关DevOps。
DevOps 是一种软件交付的理念和方法目的是增强软件的可靠性。从名字便不难发现DevOps 是将开发Development和运维Operations组合在了一起。
在传统的 IT 公司中,开发和运维往往是井水不犯河水的两个职位,甚至是两个不同的部门,由此带来了很多问题,比如,开发人员修改了配置,但没有通知运维,造成了新代码不能运行。
DevOps 提倡的就是将二者融合起来打破壁垒。2009年Flickr 做了一个分享《每天部署10次》整个行业受到了极大的冲击从此 DevOps 运动风起云涌。DevOps 给这个行业带来的理念冲击是很大的,想要做好 DevOps需要在文化、流程和工具等诸多方面不断改善。
但对我们程序员的日常工作来说最直接的影响是体现在各种工具上。Chef、Puppet、Ansible 这些工具基本上都是在那之后,兴起或广为人知的。
在上一讲中,我给你讲了这些配置管理工具在运维体系中的角色,它们相当于提供了一个框架。但对于行业来说,这些工具给行业带来了部署的规范。
从前写 Shell 的方式,那就是各村有各村的高招。你在 A 公司学会的东西,到 B 公司是没法用的,甚至在很多人的印象中,部署这件事就应该属于某个特定的场景,换台机器脚本都要重新写过。这种形势就如同 Spring 出现之前,几乎所有的公司都在写自己的框架一样。
Spring 的出现打破这一切,让你的 Java 技能从归属于一个公司变成了行业通用。同样运维体系中这些配置工具也起到了这样的作用。它们甚至带来了一个新的理念基础设施即代码Infrastructure as code将计算机的管理与配置变成了代码。
一旦成了代码,就可以到处运行,可以版本管理,那种强烈依赖于“英雄”的机器配置工作终于可以平民化了。这在从前是想都不敢想的事。
这些工具采用的都是声明式接口,从 Shell 那种描述怎么做,到描述做什么,抽象程度上了一个台阶,让开发者或系统管理员从琐碎的细节中脱身,把更多的注意力用于思考应该把机器配置成什么样子。
如果这些配置管理工具还需要有一台具体的机器去部署,放在持续交付中,也只能扮演一个部署环境的次要角色,那 Docker 的出现则彻底地改变最终交付物。
我在上一讲说过Docker 相当于是一台机器。Docker 非常好的一点是,它是一台可以用代码描述的机器,在 Docker 配置文件中描述的就是我们预期中那台机器的样子,然后,生成镜像,部署到具体的机器上。
既然是要描述机器的样子,我们就可以在 Docker 的配置文件中使用前面提到的配置工具,如此一来,我们的配置工作就简单了。那既然我们在讨论持续交付,还可以通过配置工具将我们的发布包也部署到最终的镜像中。这样一来,最终生成的镜像就是包含了我们自己应用的镜像。
你或许已经知道我要说什么了,结合着这些工具,我们的生成产物就由一个发布包变成了一个 Docker 镜像。
Docker 在开发中扮演的角色,是一个构建在我们应用与具体机器之间的中间层。对应用而言,它就是机器,但对机器而言,它只是一个可以部署的镜像,统一了各种应用千奇百怪的部署差异,让部署本身变得更简单了。
到这里,我给你介绍了持续交付中最基础的东西,让你有了一个基本的框架理解持续交付。当然,如果你关注这个领域,就会发现,它早已超出了一个实践的层面,有更多组织、文化的内容。
Jez Humble 写《持续交付》时就已经想到如此完整的一个体系,受限于当时的环境,书中介绍的自动化还比较宽泛,不像今天有更加完善的工具支撑。
只可惜,虽然当时他对持续交付的理解已经到达如此高度,他所在的团队也做出了一个颇具先锋气质的持续交付工具,但是受限于产品推广策略,这个工具并没有成为主流,即便后来开源了。(如果你想了解一下这个工具,可以点击链接去查看)
总结时刻
总结一下今天的内容。我们延续了前两讲的内容,在准备好发布包和部署的基础设施之后,我们顺着持续集成的思路,将部署过程也加了进来,这就是持续交付。
持续交付,是一种让软件随时处于可以部署到生产环境的能力。让软件具备部署到生产环境的能力,这里面有两个关键点:验证发布包和部署。
验证发布包不仅是功能上的验证还包括与环境结合在一起的验证。所以通常会用几个不同的环境验证每一个环境都是一个单独的阶段一个阶段不通过是不能进入下一阶段的这种按照不同阶段组织构建的方式称之为构建流水线Build Pipeline
与部署相关的一个重要概念是 DevOps也就是将开发和运维结合起来。DevOps 包含了很多方面对程序员最直接的影响是各种工具的发展这些工具推动着另一个理念的发展基础设施即代码Infrastructure as code 。有赖于这些工具的发展,今天定义交付,就不再是一个发布包,而是一个可以部署的镜像。
如果今天的内容你只能记住一件事,那请记住:将部署纳入开发的考量。
最后,我想请你分享一下,你对持续交付的理解是什么样的呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,159 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
33 如何做好验收测试?
你好,我是郑晔。
经过前面三讲的讲解,相信你对一个项目自动化应该是什么样子有了一个相对完整的认识:程序员写好程序,用构建脚本执行检查,提交代码,在服务器上打出一个发布镜像,部署到各个环境进行检查,检查好了,随时可以发布上线。
我们在前面的内容中只说了该检查,但怎么检查呢?这就轮到测试发挥作用了。
在“任务分解”的模块,我给你完整地介绍了一下开发者测试的概念,但在那个部分讲解的测试基本上还停留在单元测试和集成测试的范畴。对于整个应用该怎么测,我们并没有仔细讨论。
今天我们就来说说应用测试的话题:验收测试。
验收测试
验收测试Acceptance Testing是确认应用是否满足设计规范的测试。这种测试往往是站在用户的角度看整个应用能否满足业务需求。
从名字上来看,验收应该是业务人员的事,但业务人员能做的最多只是验收,测试是他们无论如何也不太可能做仔细的。
所以,验收测试这件事,往往还是由技术团队自己完成,而且在很多公司,这就是测试人员的事。
时至今日,很多测试团队都拥有自动化的能力。所以,自动化验收测试自然是重点考虑对象。今天,我们的重点就是怎么做好自动化的验收测试。
其实,验收测试应该是人们最早想到的自动化测试,早在单元测试还不流行的年代,人们就开始了对自动化验收测试的探索。有不少团队甚至还构建了自己的框架,只不过,这种框架不是我们今天理解的测试框架,而是针对着一个应用的测试框架。
比如,我曾经见过有人为通信软件构建的一套完整的测试框架,甚至构建了属于自己的语言,测试人员的工作就是用这种特定的语言,对系统进行设置、运行,看它是否满足自己的预期。
相对来说,他们的这种做法已经非常成熟了。但更多团队的现实情况是,自己把对应用的访问做一个简单的封装,然后,写测试就是编写代码调用这个封装。
让验收测试从各自为战的混乱中逐渐有了体系的是行为驱动开发Behavior Driven Development这个概念的诞生也就是很多人知道的 BDD。
行为驱动开发
行为驱动开发中的行为指的是业务行为。BDD 希望促进业务人员与开发团队之间的协作,换句话说,如果你想做 BDD就应该用业务语言进行描述。
这与我们传统上理解的系统测试有着很大的差别,传统意义上的系统测试是站在开发团队的角度,所以,更多的是在描述系统与外部系统之间的交互,用的都是计算机的术语。
而 BDD 则让我们换了一个视角,用业务语言做系统测试,所以,它是一个更高级别的抽象。
BDD 是2003年由 Dan North 提出了来的。Dan North 不仅仅提出了概念,为了践行他的想法,他还创造了第一个 BDD 的框架JBehave。后来又改写出基于 Ruby 的版本 RBehave这个项目后来被并到 RSpec 中。
今天最流行的 BDD 框架应该是 Cucumber它的作者就是 RSpec 的作者之一 Aslak Hellesøy。
Cucunber 从最开始的 Ruby BDD 框架发展成今天支持很多不同程序设计语言的 BDD 测试框架,比如,常见的 Java、JavaScript、PHP 等等。
BDD 框架给我们最直观的感受就是它给我们提供的一套语言体系,供我们描述应用的行为,下面是一个例子,它描述了一个交易场景,应用需要根据交易结果判定是否要发出警告。你可以感受一下:
Scenario: trader is not alerted below threshold
Given a stock of symbol STK1 and a threshold of 10.0
When the stock is traded at 5.0
Then the alert status should be OFF
Scenario: trader is alerted above threshold
Given a stock of symbol STK1 and a threshold of 10.0
When the stock is traded at 11.0
Then the alert status should be ON
我们在这里的关注点是这个例子的样子首先是描述格式“Given…When…Then”这个结构对应着这个测试用例中的执行步骤。Given 表示的一个假设前提When 表示具体的操作Then 则对应着这个用例要验证的结果。
还记得我们讲过的测试结构吗前置准备、执行、断言和清理这刚好与“Given…When…Then”做一个对应Given 对应前置条件When 对应执行Then 则对应着断言。至于清理,它会做一些资源释放,属于实现层面的内容,在业务层面上意义不大。
了解了格式,我们还要关心一下内容。你会看到这里描述的行为都是站在业务的角度进行叙述的,而且 Given、When、Then 都是独立的,可以自由组合。也就是说,一旦基础框架搭好了,我们就可以用这些组成块来编写新的测试用例,甚至可以不需要技术人员参与。
不过,这些内容都是站在业务角度的描述,没有任何实现的内容,那实现的内容放在哪呢?
我们还需要定义一个胶水层,把测试用例与实现联系起来的胶水层,在 Cucumber 的术语里称之为步骤定义Step Definition。这里我也给出了一个例子你可以参考一下
public class TraderSteps implements En {
private Stock stock;
public TraderSteps() {
Given("^a stock of symbol {string} and a threshold of {double}", (String symbol, double threshold) -> {
stock = new Stock(symbol, threshold);
});
When("^the stock is traded at {double}$", (double price) -> {
stock.tradeAt(price);
});
Then("the alert status should be {string}", (String status) -> {
assertThat(stock.getStatus().name()).isEqualTo(status);
})
}
}
写好验收测试用例
有了对 BDD 框架的基本了解,接下来的问题就是,怎么用好 BDD 框架。我们举个简单的例子,如果我们要写一个登录的测试用例,你会怎么写呢?
有一种写法是这样的为了方便叙述我把它转成了中文描述的格式Cucumber 本身是支持本地化的,你可以使用自己熟悉的语言编写用例:
假定 张三是一个注册用户,其用户名密码分别是 zhangsan 和 zspassword
当 在用户名输入框里输入 zhangsan在密码输入框里输入 zspassword
并且 点击登录
那么 张三将登录成功
这个用例怎么样呢或许你会说这个用例挺好的。如果你这么想说明你是站在程序员的视角。我在前面已经说过了BDD 需要站在业务的角度,而这个例子完全是站在实现的角度。
如果登录方式有所调整,用户输完用户名密码自动登录,不需要点击,那这个用例是不是需要改呢?下面我换了一种方式描述,你再感受一下:
假定 张三是一个注册用户,其用户名密码是分别是 zhangsan 和 zspassword
当 用户以用户名 zhangsan 和密码 zspassword 登录
那么 张三将登录成功
这是一个站在业务视角的描述,除非做业务的调整,不用用户名密码登录了,否则,这个用例不需要改变,即便实现的具体方式调整了,需要改变的也是具体的步骤定义。
所以,想写好 BDD 的测试用例,关键点在用业务视角描述。
编写验收测试用例的步骤定义时,还有一个人们经常忽略的点:业务测试的模型。很多人的第一直觉是,一个测试要啥模型?还记得我们讲好测试应该具备的属性吗?其中一点就是 Professional专业性。想要写好测试同写好代码是一样的一个好的模型是不可或缺的。
这方面一个可以参考的例子是,做 Web 测试常用的一个模型Page Object。它把对页面的访问封装了起来即便你在写的是步骤定义你也不应该在代码中直接操作 HTML 元素,而是应该访问不同的页面对象。
以前面的登录为例,我们可能会定义这样的页面对象:
public class LoginPage {
public boolean login(String name, String password) {
...
}
}
如此一来,在步骤定义中,你就不必关心具体怎么定位到输入框,会让代码的抽象程度得到提升。
当然,这只是一个参考,面对你自己的应用时,你要考虑构建自己的业务测试模型。
总结时刻
今天我和你分享了自动化验收测试的话题。验收测试Acceptance Testing是确认应用是否满足设计规范的测试。验收测试是技术交付必经的环节只不过各个团队实践水平有所差异有的靠人工有的用简单自动化一些做得比较好的团队才有完善的自动化。
自动化验收测试也是一个逐步发展的过程,从最开始的各自为战,到后来逐渐形成了一个完整的自动化验收测试的体系。
今天我以行为驱动开发Behavior Driven DevelopmentBDD为核心给你介绍了一种自动化验收测试的方式。这个在2003年由 Dan North 提出的概念已经成为了一套比较完善的体系,尤其是一些 BDD 框架的发展,让人们可以自己的项目中实践 BDD。
我以 Cucumber 为样例,给你介绍了 BDD 验收用例的编写方式你知道“Given…When…Then”的基本格式也知道了要编写步骤定义Step Definition将测试用例与实现连接起来。
我还给你介绍了编写 BDD 测试用例的最佳实践:用业务的视角描述测试用例。在编写步骤定义时,还要考虑设计自己的业务测试模型。
其实,验收测试的方法不止 BDD 一种像实例化需求Specification by ExampleSbE也是一种常见的方法。验收测试框架也不止 BDD 框架一类,像 Concordion 这样的工具甚至可以让你把一个验收用例写成一个完整的参考文档。
如果你有兴趣,可以深入地去了解。无论哪种做法,都是为了缩短业务人员与开发团队之间的距离,让开发变得更加高效。
如果今天的内容你只能记住一件事,那请记住:将验收测试自动化。
最后,我想请你分享一下,你的团队是怎么做验收测试的呢?欢迎在留言区分享你的做法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,136 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
38 新入职一家公司,怎么快速进入工作状态?
你好,我是郑晔。
经过前面几个模块的学习,我们分别领略了各个原则在不同场景下的应用,相信你对于这些原则的理解也上了一个台阶。但实际工作并不会清晰地告诉你,到底该运用哪个原则来解决问题。
所以,在接下来的三讲中,我挑选了程序员职业生涯中三个非常经典的场景,与你一起看看怎么在实际的工作中运用好已经学习到的这些原则。
在综合运用这个模块的第一讲,我们就来谈谈,当你加入一家新公司时,应该怎么做。
IT 行业快速发展,无数的机会涌现了出来,程序员频繁流动是这个行业的一个典型特征。频繁换工作,无论是对公司,还是对个人都是成本很高的一件事。所以,在加入一个新公司时,怎么让自己快速融入,尽快发挥价值,是摆在我们面前的一个重要问题。
以行业标准来看,我换工作的速度是很低的,但因为之前工作的原因,我需要到不同的公司与不同的人合作,每到一个新公司,工作的内容就是全新的,就如同换了一个新工作一般。因为合作周期有限,我不可能像普通员工入职新公司一样,花几个月时间慢慢熟悉,只能在尽可能短的时间内,快速上手,而且还要提出自己的新想法。
那我是怎么做的呢?其实,我就是运用这个专栏里提到的各种方法解决这个问题。下面我就来分享一下具体的做法。
运用思考框架
还记得专栏之初我提出的思考框架吗?我们要问三个问题:
Where are we?(我们现在在哪?)
Where are we going?(我们要到哪儿去?)
How can we get there?(我们如何到达那里?)
先来看第一个问题,如果刚刚加入一家公司,哪怕我们不是一脸懵,也只是对公司业务有一个简单地了解,这是我们的现状。
第二个问题来看看我们的目标。一般来说,我们都是打算在新公司大展身手,但这个答案太宽泛了,我们还需要进一步细化。在这个公司长远的发展是以后的事,我们还是把第一步的目标制定成能够达到上手工作的程度,比如,能够解决日常的项目问题。
那接下来,我们就需要回答第三个问题了,怎么才能够达到这个目标呢?我们需要做一个分解。
你可以回想一下过往的工作经验,要在一个项目上工作起来,先要了解什么呢?很多人的第一反应是技术,我是程序员嘛,当然就是技术优先了。估计大多数人进到项目里,都是一头奔进代码里,然后,从各种细节研究起来。技术肯定是你要了解的,但它不应该是第一位的。
技术解决的是“怎么做”的问题,而我们第一个应该了解的问题是“做什么”。一个软件到底在做什么,能够回答这个问题的就是业务。所以,我们排在第一优先级的事情应该是业务。
了解业务和技术都只是让你扮演好你个人的角色,但我们通常都是在一个团队内工作的,所以,还要解决好与其他人协作的问题,这就需要我们了解团队本身是如何运作的。
好,我们已经将大目标做了一个分解,得到了三个小目标:
业务;
技术;
团队运作。
从大图景入手
接下来,我们来针对每一个目标,进一步看看需要了解哪些内容。
业务
首先是业务。这是程序员入手新项目时最容易忽略的点。在这个专栏中,我在不同的模块中都说到了知识结构的重要性,没有结构的知识是零散的。所以,不管做任何项目,都要先从大图景入手。只有了解了大图景,各种知识才能各归其位。
对于一个普通的程序员来说,业务就是这个大图景。
如果你了解了业务,你自己就可以推演出基本的代码结构。但反过来,如果让你看了代码,从中推演出业务,那几乎是不可能的。
事实上,每次了解到一个业务,我都会在脑子中过一下,如果是我做这个业务,我会怎么做。这样一来,我就会先在整体上有一个预判,后面再对应到实际的代码上,就不会那么陌生了。
要了解业务,我一般都会请人给我讲一下,这个业务是做什么的,解决什么样的问题,具体的业务流程是什么样子的,等等。
在初期的了解中,我并不会试图弄懂所有的细节,因为我的目标只是建立起一个基本的框架,有了这个初步的了解,后续再有问题,我就知道该从哪里问起了。
理论上,了解业务是每个程序员都该做的事,但事实上,这也常常是出问题的地方。在请别人给我讲解业务的过程中,我发现,很多人是分不清业务和技术的,经常把二者混在一起讲。如果你跟着他的思路走,很容易就会陷入到对细节的讨论中。
所以,了解业务时,一定要打起精神,告诉自己,这个阶段,我要了解的只是业务,千万别给我讲技术。
技术
了解完业务就该到技术了。这是程序员最喜欢的话题。但即便是了解技术也要有个顺序所以我们先从宏观内容开始。第一个问题就是这个系统的技术栈Java、JavaScript 还是.NET这样我就可以对用到的工具和框架有个大致的预期。
接下来是系统的业务架构,这个系统包含了哪些模块,与哪些外部系统有交互等等。最好能够有一张或几张图将架构展现出来。现实情况是,不少项目并没有现成的图,那就大家一起讨论,在白板上一起画一张出来,之后再来慢慢整理。
有了一个初步的体系,接下来,就可以稍微深入一些。
我会选择从外向内的顺序了解起。首先是外部,这里的外部包括两个部分:
这个系统对外提供哪些接口,这对应着系统提供的能力;
这个系统需要集成哪些外部系统,对应着它需要哪些支持。
一旦涉及到与外部打交道,就涉及到外部接口是什么样子的,比如,是用 REST 接口还是 RPCRemote Procedure Call远程方法调用 调用,抑或是通过 MQMessage queue消息队列传递消息。
不要简单地认为所有接口都是你熟悉的,总有一些项目会采用不常见的方式,比如,我曾见过有系统用 FTP 做接口的。
所有这些都相当于信息承载方式,再进一步就是了解具体的信息是什么格式,也就是协议。
今天常见的协议是 JSON 格式或者是基于某个开源项目的二进制编码比如Protocol Buffers、Thrift 等等。一些有年头的系统可能会采用那时候流行的协议比如XML有一些系统则采用自己特定领域的协议比如通信领域有大量3GPP 定义的协议。
一般来说,从外部接口这件事就能看出一个项目所处的年代,至少是技术负责人对技术理解的年代。
了解完外部,就该了解内部了。了解内部系统也要从业务入手,对应起来就是,这个系统由哪些模块组成,每个模块承担怎样的职责。如果系统已经是微服务,每个服务就应该是一个独立的模块。
通常这也是一个发现问题的点,很多系统的模块划分常常是职责不清的,因此会产生严重的依赖问题。在前面的内容中,我多次提到限界上下文,用限界上下文的视角衡量这些模块,通常会发现问题,这些问题可以成为后续工作改进的出发点。
业务之后是技术,对应着我需要了解分层。前面说过,分层结构反映着系统的抽象。我希望了解一个模块内部分了多少个层,每个层的职责是什么。了解了这些对系统的设计,也就对系统有了一个整体的认识。
设计之后,就到了动手的环节,但还不到写代码的时候。我会先从构建脚本开始,了解项目的常用命令。我预期从版本控制里得到的是一个可以构建成功的脚本,如果不是这样,我就知道哪里需要改进了。
最后才是代码,比如,代码的目录结构、配置文件的位置、模块在源码上的体现等等,这是程序员最熟悉的东西,我就不多说了。作为初步的接触,了解基本的东西就够了,代码是我们后期会投入大量精力的地方,不用太着急。
团队运作
最后,我们还要了解一下团队运作。同样从外部开始,这个团队有哪些外部接口,比如,需求是从哪来的,产品最终会由谁使用,团队需要向谁汇报。如果有外部客户,日常沟通是怎么安排的。
再来就是内部的活动,一方面是定期的活动,比如,站会、回顾会议、周会,这些不同活动的时间安排是怎样的;另一方面是团队的日常活动,比如,是否有每天的代码评审、是否有内部的分享机制等等。
通过了解这些内容,基本上可以大致判断出一个团队的专业程度,也可以知道自己需要帮助的时候,可以找谁帮忙,为自己更好地融入团队打下基础。
你也许会问,了解这么多东西需要很长时间吧?其实不然,因为只需要从整体上有认知,如果有人很清楚团队现状的话,你可以去请教,也许一天就够了,这也是我往往能够快速上手的原因。接下来,就该卷起袖子干活了!
总结时刻
我给你介绍了怎么把前面学到的知识运用在了解一个项目上,按照业务、技术和团队运作三个方面去了解。
大多数程序员习惯的工作方式,往往是从细节入手,很难建立起一个完整的图景,常常是“只见树木不见森林”,而我的方式则是从大到小、由外而内,将要了解的内容层层分解,有了大图景之后,很容易知道自己做的事情到底在整体上处于什么样的位置。我把上面的内容总结了成一份供你参考。
附赠一点小技巧:使用“行话”。在交流的过程中,学习一点”行话“。这会让人觉得你懂行,让你很快得到信任,尽早融入团队。
如果今天的内容你只能记住一件事,那请记住:了解一个项目,从大图景开始。
最后,我想请你分享一下,你在入职一个新公司遇到过哪些困难呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,122 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
39 面对遗留系统,你应该这样做
你好,我是郑晔。
在上一讲中,结合着“新入职一家公司”的场景,我给你讲了如何在具体情况下应用我们前面学到的知识。这一讲,我们再来选择一个典型的实际工作场景,将所学综合应用起来。这个场景就是面对遗留系统。
在《34 | 你的代码是怎么变混乱的?》中,我给你讲了代码是会随着时间腐化的,无论是有意,还是无意。即便是最理想的场景,代码设计得很好,维护得也很精心,但随着技术的不断升级进步,系统也需要逐步升级换代。
比如,我们一直认为电信是一个独特的领域,与 IT 技术是完全独立的,学好 CTCommunication Technology通信技术就可以高枕无忧了。但随着 IT 技术的不断发展,今天的电信领域也开始打破壁垒,拥抱 IT 技术,提出了 ICT 的概念Information and Communications Technology信息通信技术
所以,无论怎样,系统不断升级改造是不可避免的事。问题是,你连自己三个月前写的代码都不愿意维护,那当面对庞杂的遗留系统时,你又该何去何从呢?
很多人的第一直觉是,我把系统重写一下就好了。不经思考的重写,就像买彩票一样,运气好才能写好,但大多数人没有这么好运气的,我们不能总指望买彩票中大奖改变生活。那有什么稍微靠谱的一点的路呢?
分清现象与根因
面对庞大的遗留系统,我们可以再次回到思考框架上寻找思路。
Where are we?(我们现在在哪?)
Where are we going?(我们要到哪儿去?)
How can we get there?(我们如何到达那里?)
第一个问题,面对遗留系统,我们的现状是什么呢?
我在这个专栏前面的部分,基本上讨论的都是怎么回答目标和实现路径的问题。而对于“现状”,我们关心的比较少。因为大多数情况下,现状都是很明显的,但这一次不一样。也许你会说,有什么不一样,不就是遗留系统,烂代码,赶紧改吧。但请稍等!
请问,遗留系统和烂代码到底是不是问题呢?其实并不是,它们只是现象,不是根因。
在动手改动之前,我们需要先分析一下,找到问题的根因。比如,实现一个直觉上需要两天的需求,要做两周或更长时间,根因是代码耦合太严重,改动影响的地方太多;再比如,性能优化遇到瓶颈,怎么改延迟都降不下来,根因是架构设计有问题,等等。
所以最好先让团队坐到一起让大家一起来回答第一个问题现状到底是什么样的。还记得我在《25 | 开发中的问题一再出现,应该怎么办?》中提到的复盘吗?这就是一种很好的手段,让团队共同确认现状是什么样子的,找到根因。
为什么一定要先做这个分析,直接重写不就好了?因为如果不进行根因分析,你很难确定问题到底出在哪,更关键的是,你无法判断重写是不是真的能解决问题。
如果是架构问题,你只进行模型的调整是解决不了问题的。同样,如果是模型不清楚,你再优化架构也是浪费时间。所以,我们必须要找到问题的根源,防止自己重新走上老路。
确定方案
假定你和团队分析好了遗留系统存在问题的根因,顺利地回答了第一个问题。接下来,我们来回答第二个问题:目标是什么。对于遗留系统而言,这个问题反而是最好回答的:重写某些代码。
你可能会问,为什么不是重构而是重写呢?以我对大部分企业的了解,如果重构能够解决的问题,他们要么不把它当作问题,要么早就改好了,不会让它成为问题。所以我们的目标大概率而言,就是要重写某些代码。
但是,在继续讨论之前,我强烈建议你,先尝试重构你的代码,尽可能在已有代码上做小步调整,不要走到大规模改造的路上,因为重构的成本是最低的。
我们真正的关注点在于第三个问题:怎么做?我们需要将目标分解一下。
要重写一个模块,这时你需要思考,怎么才能保证我们重写的代码和原来的代码功能上是一致的。对于这个问题,唯一靠谱的答案是测试。对两个系统运行同样的测试,如果返回的结果是一样的,我们就认为它们的功能是一样的。
不管你之前对测试是什么看法,这个时候,你都会无比希望自己已经有了大量的测试。如果没,你最好是先给这个模块补测试。因为只有当你构建起测试防护网了,后续的修改才算是走在坚实的道路上。
说到遗留代码和测试我推荐一本经典的书Michael Feathers 的《修改代码的艺术》Working Effectively with Legacy Code从它的英文名中你就不难发现它就是一本关于遗留代码的书。如果你打算处理遗留代码也建议你读读这本书。
在2007年我就给这本书写了一篇书评我将它评价为“这是一本关于如何编写测试的书”它会教你如何给真实的代码写测试。
这本书对于遗留系统的定义在我脑中留下了深刻印象:遗留代码就是没有测试的代码。这个定义简直就是振聋发聩。按照这个标准,很多团队写出来的就是遗留代码,换言之,自己写代码就是在伤害自己。
有了测试防护网,下一个问题就是怎么去替换遗留系统,答案是分成小块,逐步替换。你看到了,这又是任务分解思想在发挥作用。
我在《36 | 为什么总有人觉得5万块钱可以做一个淘宝》中提到淘宝将系统改造成 Java 系统的升级过程,就是将业务分成若干的小模块,每次只升级一个模块,老模块只维护,不增加新功能,新功能只在新模块开发,新老模块共用数据库。新功能上线,则关闭老模块对应功能,所有功能替换完毕,则老模块下线。
这个道理是普遍适用的,差别只是体现在模块的大小上。如果你的“小模块”是一个系统,那就部署新老两套系统,在前面的流量入口做控制,逐步把流量从老系统转到新系统上去;如果“小模块”只在代码层面,那就要有一段分发的代码,根据参数将流程转到不同的代码上去,然后,根据开发的进展,逐步减少对老代码的调用,一直到完全不依赖于老代码。
这里还有一个小的建议按照分模块的做法将新代码放到新模块里按照新的标准去写新的代码比如测试覆盖率要达到100%,然后,让调用入口的地方依赖于这个新的模块。
最后,有了测试,有了替换方案,但还有一个关键问题,新代码要怎么写?
要回答这个问题,我们必须回到一开始的地方,我们为什么要做这次调整。因为这个系统已经不堪重负了,那我们新做的修改是不是一定能解决这个问题呢?答案是不好说。
很多程序员都会认为别人给留下的代码是烂摊子,但真有一个机会让你重写代码,你怎么保证不把摊子弄烂?这是很多人没有仔细思考过的问题。
如果你不去想这个问题,即便今天你重写了这段代码,明天你又会怨恨写这段代码的人没把这段代码写好,只不过,这个被抱怨的人是你自己而已。
要想代码腐化的速度不那么快,一定要在软件设计上多下功夫。一方面,建立好领域模型,另一方面,寻找行业对于系统构建的最新理解。
关于领域模型的价值,我在专栏前面已经提到过不少次了。有不少行业已经形成了自己在领域模型上的最佳实践,比如,电商领域,你可以作为参考,这样可以节省很多探索的成本。
我们稍微展开说说后面一点,“寻找行业中的最新理解”。简言之,我们需要知道现在行业已经发展到什么水平了。
比如说,今天做一个大访问量的系统,我们要用缓存系统,要用 CDN而不是把所有流量都直接转给数据库。而这么做的前提是内存成本已经大幅度降低缓存系统才成为了标准配置。拜 REST 所赐,行业对于 HTTP 的理解已经大踏步地向前迈进CDN 才有了巨大的进步空间。
而今天的缓存系统已经不再是简单的大 Map有一些实现得比较好的缓存系统可以支持很多不同的数据结构甚至支持复杂的查询。从某种程度上讲它们已经变成了一个性能更好的“数据库”。
有了这些理解,做技术选型时,你就可以根据自己系统的特点,选择适合的技术,而不是以昨天的技术解决今天的问题,造成的结果就是,代码写出来就是过时的。
前面这个例子用到的是技术选型,关于“最新理解”还有一个角度是,行业对于最佳实践的理解。
其实在这个专栏里,我讲的内容很多都是各种“最佳实践”,比如,要写测试,要有持续集成,要有自动化等等,这些内容看似很简单,但如果你不做,结果就是团队很容易重新陷入泥潭,继续苦苦挣扎。
既然选择重写代码,至少新的代码应该按照“最佳实践”来做,才能够尽可能减缓代码腐化的速度。
总之,改造遗留系统,一个关键点就是,不要回到老路上。
总结时刻
我们把前面学到的各种知识运用到了“改造遗留系统”上。只要产品还在发展,系统改造就是不可避免的。改造遗留系统,前提条件是要弄清楚现状,知道系统为什么要改造,是架构有问题,还是领域模型混乱,只有知道根因,才可能有的放矢地进行改造。
改造遗留系统,我给你几个建议:
构建测试防护网,保证新老模块功能一致;
分成小块,逐步替换;
构建好领域模型;
寻找行业中关于系统构建的最新理解。
如果今天的内容你只能记住一件事,那请记住:小步改造遗留系统,不要回到老路上。
最后,我想请你分享一下,你有哪些改造遗留系统的经验呢?欢迎在留言区分享你的做法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
划重点 关于“以终为始”你要记住的9句话
你好,我是郑晔。
“以终为始”这个主题模块已经全部更新完毕,相信通过对各种实践的深入讲解,你已经对“以终为始”这个原则有了更为全面和透彻的理解。
为了帮助你更好地回顾和复习,我为每个主题模块增设了“划重点”的加餐内容。现在,我就带你一起梳理一下“以终为始”主题的核心要点。
重点复习
在这个模块中,我们学习到了一些行业最佳实践。
DoD确定好完成的定义减少团队内部的理解不一致。
用户故事,细化出有价值的需求。
持续集成,通过尽早集成,减少改动量,降低集成的难度。
精益创业,减少过度开发不确定性产品带来的浪费。
迭代0在项目开始之前做好一些基础准备。
还学习到一些重要的思维转变。
任何事物都要经过两次创造一次是在头脑中的创造也就是智力上的或者第一次创造Mental/First Creation然后才是付诸实践也就是实际的构建或第二次创造Physical/Second Creation
在更大的上下文内发现自己的“终”。
通过推演,找到通往“终”的路径。
用可度量的“数字”定义自己的“终”。
实战指南
在每一篇文章的结尾,我们还将全篇内容浓缩为“一句话”的实战指南,希望你可以迅速上手,把“以终为始”的原则运用在实际工作之中,我们一起来回顾一下这些实战指南。
遇到事情,倒着想。-
——《02 | 以终为始:如何让你的努力不白费?》
在做任何事之前,先定义完成的标准。-
——《03 | DoD的价值你完成了工作为什么他们还不满意
在做任何需求或任务之前,先定好验收标准。-
——《04 | 接到需求任务,你要先做那件事?》
尽早提交代码去集成。-
——《05 | 持续集成:集成本身就是写代码的一个环节》
默认所有需求都不做,直到弄清楚为什么要做这件事。-
——《 06 | 精益创业:产品经理不靠谱,你该怎么办?》
扩大自己工作的上下文,别把自己局限在一个“程序员”的角色上。-
——《07 | 解决了很多问题,为什么你依然在“坑”里?》
在动手做一件事之前,先推演一番。-
——《08 | 为什么说做事之前要先进行推演?》
问一下自己,我的工作是不是可以用数字衡量。-
——《09 | 你的工作可以用数字衡量吗?》
设计你的迭代0清单给自己的项目做体检。-
——《10 | 启动开发之前,你应该准备什么?》
额外收获
在这个部分的最后,针对大家在学习过程中的热门问题,我也进行了回答,希望你懂得:
作为程序员,你可以管理你的上级;
拿老板说事的产品经理,你可以到老板面前澄清;
喜欢无脑抄袭的产品经理,让他回去先想清楚到底抄的是什么;
分清楚需求和技术,产品经理和开发团队各自做好各自的事。-
——《答疑解惑 | 如何管理你的上司?》
留言精选
同学们的留言很踊跃,也很有价值。精彩的留言本身就是对文章内容的补充与丰富,在此我挑出一些优秀的留言与你分享。
在讲高效工作的思考框架时,张维元 同学提到:
思考框架是道,原则是演化下的术,我们从 A → B有无穷无尽的路径最有效的唯有那条直线。本质上各个维度、原则不限于作者提到的四项原则都是帮助我们更好地定位 A 在哪里B 在哪里,那条直线在哪里。
对于以终为始的原则WTF 同学提到:
“以终为始”,最常见的一个实践就是计划倒排了。先定时间,然后看功能是不是做不过来得砍掉一些,人力是不是不够需要补充一些,提前预知规避风险。
对于用户故事的验收标准liu 同学提到:
程序员的核心职责是如何实现产品功能,怎么实现功能;前提是理解产品功能,需要实现哪些功能。有些项目经理,产品经理与程序员角色混淆。你同他谈功能,他同你谈技术实现,你同他谈技术,他同你谈产品(需要实现哪些功能)。
大家对沙盘推演的话题很感兴趣。其中,西西弗与卡夫卡 同学提到:
推演可以发现达成目标会涉及到哪些部门、哪些利益相关者,需要哪些资源,以及他们需要何时怎样的配合。
ZackZeng 同学也针对这个话题留言:
项目上线之前一般都会有一个launch plan, 数据库迁移这种项目不去考虑上线回滚我认为是设计上的缺失。我们公司的launch plan一般是写成一步一步的checklist, 在上线之前会做同伴审查。
Scott 同学也提到:
我觉得领导说先跑通再说和事前推演是不矛盾的很多时候我们需要一个poc来证明这个项目是可行的这其实也是事前推演的一部分。上线要事无巨细的检查推演和快速跑通poc不矛盾当然现实世界是大家就急着把poc当正式产品上线了这是无数个悲剧故事的序章。
休息一下马上回来 同学对推演过程进行了很好地补充:
上线前,哪些机器什么配置,应该有一个预期,甚至提前准备好。
adang 同学也分享了他在工作中的感悟:
想清楚了才能写清楚,这是我在编程工作非常认可的一句话,并且我也认为它是区分合格与不合格开发工程师的重要区别。软件开发过程中,最常见的例子就是拿到需求后不管三七二十一,上来就开始撸代码,但最后往往返工不断,质量问题层出不穷,而且加班没完没了,这里面一个根本原因就是没有系统地想清楚,但很多人都觉得前期澄清需求、分析设计是浪费时间,只有编码才是真正的创造价值,这就是差距。
在讲到工作要尽量用数字衡量时,西西弗与卡夫卡 同学提到:
比如开发常常关注的是产品经理提的功能有没有实现实际上也应该了解做出来的有多少人使用每个页面有多少人使用。此外看开发是否努力勤奋不要光听他说而是要看看他提交git有多频繁、提交的时间段、代码量有多少。代码质量可以用bug数/代码量来衡量。当然,这些量化未必科学,甚至会被误用,但总胜过凭印象拍脑袋的判断。
大彬 同学也提到:
上周我把一个方案进行推迟了让同事去搜集某项指标的数据没数据一切方案都是空谈。AB测试留言量阅读量转发量一切数据都是下一步决策和改进的基础。
篇幅限制,就为大家分享这么多,感谢同学们的精彩留言。留言区还有很多同学提出了各种问题,其实都可以用任务分解的方式去解决。不着急,我们下一个主题的内容就是“任务分解”。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,172 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
划重点 关于“任务分解”,你要重点掌握哪些事?
你好,我是郑晔,恭喜你,又完成了一个模块的学习。
在这个模块中,我主要讲解的是“任务分解”这个知易行难的工作原则。普通人与高手之间的差异,很大程度上取决于任务分解的粒度大小。但真正理解并应用好“任务分解”的原则并不容易,希望你能勤于练习,将知识内化成为你的能力。
重点复习
在这个模块中,我们学习到了一些最佳实践:
测试金字塔-
-- 行业中测试组合的最佳实践。-
-- 多写单元测试是关键。
测试驱动开发-
-- 测试驱动开发的节奏是:红——绿——重构,重构是测试驱动开发区别于测试先行的关键。-
-- 有人把测试驱动开发理解成测试驱动设计,它给行业带来的思维改变是,编写可测的代码。
艾森豪威尔矩阵Eisenhower Matrix-
-- 将事情按照重要和紧急进行划分。-
-- 重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情,可以委托别人做。不重要不紧急的事情,尽量少做。
最小可行产品-
-- “刚刚好”满足客户需求的产品。-
-- 在实践中,要用最小的代价找到一条可行的路径。
另外,我还提到了一些可以直接在工作中应用的做法和评判标准:
尽量不写 static 方法;
主分支开发模型是一种更好的开发分支模型;
好的用户故事应该符合 INVEST 原则;
估算是一个加深对需求理解的过程,好的估算是以任务分解为基础的;
好的测试应该符合 A-TRIP。
我也带你学习了一些重要的思想,帮你更好地改善自己的开发工作:
分而治之,是人类解决问题的基本手段;
软件变更成本,它会随着时间和开发阶段逐步增加;
测试框架把自动化测试作为一种最佳实践引入到开发过程中,使得测试动作可以通过标准化的手段固定下来;
极限编程之所以叫“极限”,它背后的理念就是把好的实践推向极限;
大师级程序员的工作秘笈是任务分解,分解到可以进行的微操作;
按照完整实现一个需求的顺序安排开发任务。
实战指南
在“任务分解”的板块,我也将每篇内容浓缩为“一句话”的实战指南,现在一起回顾一下。
动手做一个工作之前,请先对它进行任务分解。-
—— 《11 | 向埃隆·马斯克学习任务分解》
多写单元测试。-
——《12 | 测试也是程序员的事吗?》
我们应该编写可测的代码。-
——《13 | 先写测试,就是测试驱动开发吗?》
将任务拆小,越小越好。-
——《14 | 大师级程序员的工作秘笈》
按照完整实现一个需求的顺序去安排分解出来的任务。-
——《15 | 一起练习:手把手带你拆任务》
要想写好测试,就要写简单的测试。-
——《16 | 为什么你的测试不够好?》
想要管理好需求,先把需求拆小。-
——《17 | 程序员也可以“砍”需求吗?》
尽量做最重要的事。-
——《18 | 需求管理:太多人给你安排任务,怎么办?》
做好产品开发,最可行的方式是采用 MVP。-
——《19 | 如何用最小的代价做产品?》
额外收获
在这个部分的最后,针对大家在学习过程中的热门问题,我也进行了回答,希望你懂得:
对不了解技术的任务,先要去了解技术,然后再做任务分解;
通过一次技术 Spike ,学习新技术;
丢弃掉在 Spike 过程中开发的原型代码;
分清目标与现状,用目标作为方向,指导现状的改变;
多个功能并行开发可以考虑使用 Feature Toggle
在遗留系统上做改造可以考虑使用 Branch by Abstraction 。
——《答疑解惑 | 如何分解一个你不了解的技术任务?》
留言精选
在“任务分解”的模块中,有很多同学非常用心,将自己的学习心得和工作中的经验进行了分享,在此我挑选了一些同学的留言,与你一起学习。
在讲大师级程序员的工作秘笈时,西西弗与卡夫卡 同学提到:
最近在做战略拆解,都是一样的道理。战略飘在空中遥不可及,要落地就必须拆解。比如说达成目标有哪几个方面可以努力,各方面都需要做哪些事,这是路径。这些路径里哪些优先级最高,需要配置哪些组织资源。心里有数之后就是制订计划时间表。
另外,西西弗与卡夫卡 同学还为Spike给出了一个很生动的解释
“技术Spike”可以翻译成“技术撩”就是撩妹的那个撩。试探下有戏就继续撩不动就算或者放一段时间再说。
针对分解的粒度问题,大彬 同学也分享了自己的心得:
我会的任务分解不仅可执行粒度还很细。比如说我要修复一个rpc接口的bug。我会列出每个代码的修改点要修改的测试要增加的测试合并到哪个分支修改rpc文档文档中有哪些点要修改。-
每一步都非常容易执行看起来没多少必要但在我当前的工作环境特别有用1事前思考不会造成遗漏2任务实施过程中经常被打断比如测试有疑问和你讨论、主管找你谈事、紧急会议来了这种“硬中断”完全打破了节奏而任务列表让我清楚知道当前做了多少该从哪一步继续。
对于单元测试,树根 同学提到:
我的想法可以在复杂度高,重要核心的模块先开始写单元测试。特别是公用、底层的,因为这些靠功能测试很难覆盖。-
单元测试难以推行主要是没有碰到质量的痛点,通常都依靠测试工程师来保证质量。我们之前就遇到过质量崩塌,倒逼着我们去做,以保证质量。
树根 同学还分享了自己的任务分解实践心得:
刚改了编程习惯先在notion写出思路、需要用到的知识点api等写出各个小任务然后对应写出关键代码段。最后真正敲代码就花了10来分钟。-
重新开始看极客时间就看到这篇,实践过来读,很认同。-
我特别佩服国外的工程师写的代码代码块很小非常清晰易读。特别记得之前参加infoq会议听socketio作者的分享看他现场撸码思路、代码结构都非常顺畅和清晰。
关于TDD的具体应用 萧 同学提到了遇到的问题:
不久前第一次接触TDD时为它的思想而惊叹感觉它能极大的提升编码效率编码后期的大量重构还能保障代码质量。后面自己在写代码的时候也注意使用它的思想但说实话理解是一回事用起来就不是那么回事了很多的东西还不是太熟练前期说实话比较耗时间有些拖进度。-
由于也毕业不久经验上有些欠缺还不太熟练有些测试还不知道怎么写。现在写多了一点感受到的是代码质量上的提高bug比起以前少了需求变更下改动也不伤筋动骨了但还是有许多感觉做的不够好的地方。看了这篇文章补充了对TDD的认知感受到如果和任务分解结合起来TDD会有更好的效果期待后面的文章
关于“任务分解”的执行问题,如明如月 同学分享了感悟:
对任务分解的体会非常深刻刚入职的时候任务评估不准。现在想想主要是两个原因1需求梳理的不清晰还没清楚地搞明白需求就动手写代码导致返工和一些“意想不到”的情况。2任务分解做的不好没有将任务分解成非常清晰地可执行的单元导致有些时候无从下手而且任务时间评估不准确。
在讲到为什么很多人的测试不够好这个问题时, 毅 同学提到:
本节课我有以下几点体会:-
1从开发者的视角看编码和测试是不分家的是可以通过重构形成良性生态圈的类似之前课程中的反馈模型和红绿重构模型-
2A-TRIP是个很好的总结和行动指南在今后工作中应一以贯之把工作做到扎实有成效-
3对文中提到的数据库依赖的问题我也说说自己的浅见。我觉得在测试代码中尽量避免与数据库打交道测试更关注领域与业务往往爆雷更多的是resource和service模型的变化往往牵动着表结构的变化与其两头兼顾不如多聚焦模型。-
我常用的做法是用例配合若干小文件(数据忠实于模型),保证库操作临门一脚前所有环节都是正确的,同时方便适应变化。一旦出现异常,也比较容易定位是否是数据库操作引发的问题。 (此点基于,我在工作中发现,项目型程序员大多是先急于把表结构定义出来,好像不这么做,写代码就不踏实)
针对需求的管理问题WL 同学提到的点也非常关键:
程序员也应该更积极主动一些, 最好能推动事情发展, 当这件事情由你推动时,主动权就在你的手里了。
感谢同学们的精彩留言。在下一个模块中,我将为大家分享“沟通反馈”这个原则的具体应用。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,147 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
答疑解惑 如何分解一个你不了解的技术任务?
你好,我是郑晔。
在“任务分解”这个模块,我以测试为核心,讲解了任务分解这个原则,同时也给你介绍了一些最佳实践,帮助你更好地理解任务分解的重要性,以及应该怎样分解任务。
同学们对任务分解这个原则大多是表示认同的,但就一些具体应用的场景,还是提出了自己的问题。
在今天的答疑中,我选择了几个非常典型的问题来进行深入讨论。
问题1面对不了解的技术我该如何分解任务
pyhhou 同学提到
很想听听老师的意见,就是在一个自己不熟悉的,充满未知的项目中该怎么更好地进行任务分解?-
——《11 | 向埃隆·马斯克学习任务分解》
shniu 同学提到
想请问一下老师,面对探索型的需求,调研型的需求如何做任务分解呢?-
——《15 | 一起练习:手把手带你分解任务》
这是一个很好的问题。在这个模块讨论开发中的任务分解时我说的都是确定了解的某项技术比如数据库、REST 服务等等,因为这是开发中最常见的场景,也是最基础的能力,连熟悉的技术都做不好分解,就别说不熟悉的技术了。
那如果不了解这项技术呢?答案很简单,先把它变成你熟悉的技术。一旦变成了你熟悉的技术,你就可以应用在这个模块中学到的,面对确定性技术的分解方案。
我知道,这个答案你并不满意。其实,你真正的问题是,怎么把它变成你熟悉的技术。
我的答案是,做一次技术 Spike。这里之所以用英文是因为我没有找到一个特别合适的词来翻译。Spike 这个词的原意是轻轻地刺,有人把它翻译成调研,我觉得是有些重了。
Spike 强调的重点在于快速地试,和调研的意思不太一样。既然是快速地试,就要在一定的时间内完成,比如,五人天,也就是一个人一周的时间,再多就不叫 Spike 了。一些简单的技术,用一天时间做 Spike 就差不多了。
这里强调的重点在于,要做一次技术 Spike。Spike 的作用就在于消除不确定性,让项目经理知道这里要用到一项全团队没有人懂的技术,需要花时间弄清楚。
项目经理比你更担心不确定性,你清楚地把问题呈现在他面前,项目经理是可以理解的,他更害怕的是,做到一半你突然告诉他,项目进度要延期。
把事情做在前面,尽早暴露问题,正是我们要在下一个模块要讨论的一个主题。
好,那么接下来的问题变成了:怎么做技术 Spike 呢?
这里,我假设你已经通过各种渠道,无论是新闻网站,还是技术 blog又或是上级的安排对要用的技术有了一些感性的认识至少你已经知道这项技术是干什么的了。
接下来,我们要进入到技术 Spike 的任务分解。
首先,快速地完成教程上的例子。稍微像样点的技术都会有一个教程,跟着教程走一遍,最多也就是半天的时间。之所以要快速地完成教程上的例子,是为了让你有一个直观的认识,这时候,你对这项技术的认识就会超过新闻网站的报道。
其次,我们要确定两件事:这项技术在项目中应用场景和我们的关注点。
技术最终是要应用到项目中的,本着“以终为始”的原则,我们就应该奔着结果做,整个的 Spike 都应该围绕着最终的目标做。
很多程序员见到新技术都容易很兴奋,会把所有的文档通读一遍。如果是技术学习,这种做法无可厚非,但我们的目标是做 Spike快速地试没有那么多时间必须一切围绕结果来。
项目中的场景有无数,我们需要选择最重要的一个场景,而针对着这项最重要的场景,我们还要从这项技术无数功能中选取最需要的几个,而不是“满天撒网”。
再有是我们要找准关注点,比如,采用新的缓存中间件是为了提高性能,那关注点就是性能,采用新的消息队列是为了提升吞吐,那关注点就是吞吐。我们选用一项新技术总是有自己的一些假设,但这些假设真的成立吗?这是我们需要验证的。
无论是场景,还是关注点,我们要在前面先想清楚,其目的就是为了防止发散。当时间有限时,我们只能做最重要的事,这也是我在专栏中不断强调的。
确定好场景和关注点,接下来,我们要开发出一个验证我们想法的原型了。这个原型主要目的就是快速地验证我们对这项技术的理解是否能够满足我们的假设。开发一个只有主线能力的原型,对大部分程序员来说并不难,这里就不赘述了。
当你把想法全部验证完毕,这项技术就已经由一项不熟悉的技术变成了熟悉的技术。我们前面的问题也就迎刃而解了。这时候,你就可以决定,对于这项技术,是采纳还是放弃了。
但是,我这里还有一点要提醒,当你确定要使用这项技术时,请丢弃掉你的原型代码。
你或许会说,我辛辛苦苦写了几天的代码就这么丢了?是的,因为它是原型,你需要为你的项目重新设计。
如果顺着原型接着做,你可能不会去设计,代码中会存在着大量对这项技术直接依赖的代码,这是值得警惕的,所有第三方技术都是值得隔离的。这是我们会在“自动化”模块讨论的内容。
问题2项目时间紧该怎么办
在这个模块里,我花了大量的篇幅在讲测试,很多同学虽然认同测试的价值,却提出了开发中普遍存在的一些情况。
玄源 同学提到
很多时候项目时间很紧经常会提测后再补测试或者直接code review测试就不写了。-
——《12 | 测试也是程序员的事吗?》
这是一个非常典型的问题,我在之前做咨询的时候,经常会遇到很多团队说,项目时间紧,所以,他们没有时间做测试。
这里面有一个非常经典误区:混淆了目标与现状。目标是应该怎么做,现状是我们正在怎么做。我们都知道现状是什么样的,问题是,你对现状满意吗?如果每个人都对现状是满意的,就不会有人探索更好的做法。
假设现在不忙了,你知道该怎么改进吗?
遗憾的是,很多人根本回答不了这个问题,因为忙是一种借口,一种不去思考改进的借口。
我之所以要开这个专栏,就是为了与大家探讨行业中一些好的做法。
回到这个具体问题上,我们在专栏开始就在讲以终为始,首先要有一个目标,专栏中介绍的各种实践都可以成为你设置目标的参考。有了这个目标再来考虑,如何结合我们工作的现状来谈改进。
接下来我们以测试为例讨论一下具体的改进过程。用我们专栏最初讲过的思考框架看一下假如我们的现状是团队之前没什么自动化测试而我们的目标是业务代码100%测试覆盖。如果要达成这个目标,我们需要做一个任务分解。
这时你会发现,分解的过程主要需要解决两方面的问题,一个是与人的沟通,另一方面是自动化的过程。
与人的沟通,就是要与团队达成共识。关于这点,你可以尝试将专栏里讲到的各种最佳实践以及其背后的逻辑,与团队进行沟通,也可以把专栏文章分享给他们。
再来,我们考虑一下自动化的改进,因为我们的现状是没什么测试,所以,不能强求一步到位,只能逐步改进。下面我给出了一个具体的改进过程:
把测试覆盖率检查加入到工程里,得到现有的测试覆盖率。
将测试覆盖率加入持续集成,设定当前测试覆盖率为初始值。测试覆盖率不达标,不许提交代码。
每周将测试覆盖率调高比如5%或10%直到测试覆盖率达到100%。
这样,我们就找到了一条由现状通往目标的路径,接下来,就是一步一步地具体实施了,由团队成员逐步为已有代码补充测试。
问题3多个功能同时开发怎么办
妮可 同学提到
公司经常存在有两个需求同时开发的情况。请问老师所在的团队如何解决单分支上线不同步的情况呢?-
——《14 | 大师级程序员的工作秘笈》
在主分支开发模型中有一些常见的解决多功能并行开发的方法其中Feature Toggle 是最常用的一个,也就是通过开关,决定哪个功能是对外可用的。
关于这一点Y024 同学也补充了一些信息。
Feature toggle功能开关分享两篇文章-
1. Feature Toggles (aka Feature Flags)-
2.使用功能开关更好地实现持续部署
不过如果用户故事划分得当你可以很快完成一个完整的业务需求。实际上Feature Toggle 只是一个非常临时的存在。但如果你在一个遗留系统上工作一个功能要跨越很长的周期Feature Toggle 才显得很有用。
额外补充一个与主分支开发模型相关的常用技术,如果你想对遗留系统做改造,传统的做法是,拉出一个分支。
如果在一个分支上怎么做呢?可以考虑采用 Branch by Abstraction简言之再动手改造之前先提取出来一个抽象把原先的实现变成这个抽象的一个实现然后改造的过程就是提供这个抽象的一个新实现。这种做法对设计能力有一定要求所以对很多团队来说这是一个挑战。
好,今天的答疑就到这里,请你回想一下,你在工作中是否也遇到过类似的问题呢?你又是怎么解决的呢?欢迎大家在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,145 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
答疑解惑 如何在实际工作中推行新观念?
你好,我是郑晔。
在整个专栏的最后一个大模块”综合运用”中,我们把前面学到的各种原则和知识穿插在一起应用在了不同的场景中。在这个模块的答疑中,我们也综合汇总一次,把整个专栏中出现的一些有趣却还没有来得及讨论的问题放在一起。
问题1想要推行 DDD阻力很大怎么办
段启超 同学提到
想在公司内推行DDD阻力真的很大首先是很多人对DDD没概念需要一定的学习成本二是团队间相互隔离沟通成本很高起码的通用语言都很难达成。-
——《37 | 先做好DDD再谈微服务吧那只是一种部署形式》
段启超同学提到的这个问题是一个非常典型的问题,而且,这个问题并不仅仅局限于 DDD。你在一个地方看到了一些好东西技术、实践或是想法然后想把它运用在自己的项目中希望项目越做越好越来越顺利。但在实际情况中想在一个组织内推广一些不一样的东西都会面临层层阻力。
我在《40 | 我们应该如何保持竞争力?》中提到了一个学习模型,你只要在学习区不断地练习新技能,很快就可以超越同侪。其中的原因是,大部分人只习惯待在舒适区,在舒适区的人能力上的进步非常有限。也因为在舒适区实在太舒适了,走出舒适区会让人产生焦虑,所以,人的内心是惧怕改变的。
你有良好的愿望,驱动你自己去改变是一件可控的事,有愿意和你一起改变的人是一件幸运的事,但你指望所有人一下子和你走上同一条道路,这是一件几乎不可能的事,即便你是很高层的领导,让所有人与你保持一致也不现实。
我曾经在一个大公司做过敏捷咨询,这还是由他们顶层领导推动的敏捷转型,但依然是困难重重。那些习惯于待在自己舒适区的人总会找到各种神奇的理由告诉你,他们的情况有多么特殊,这些最佳实践在他们那里是不适用的。
我们放弃了吗?并没有。我们的做法是,找一个团队来做试点。
换句话说,我们找到了几个愿意改变的人,把这些最佳实践应用在他们的项目上。在这种情况下,大家的目标是一致的,就是希望让这个项目得到改善。所以,大家自然会想尽一切办法,克服遇到的困难。比如,我们当时的切入点是持续集成。
他们的代码都在老旧的 ClearCase 上,每个人修改文件要先去竞争文件锁,特别不利于小步提交,所以,我们推动着将 ClearCase 改成了稍微进步一点的 Subversion。好吧你能听出来这是一个有些年头的故事。
代码是用 C 语言编写的,在他们的代码规模下,编译时间会很长。于是,我们决定搭建一个分布式构建系统,这需要有很多台电脑。不过,他们的硬件是严格管控的,申请电脑是很困难的,虽然花了很大的力气,但最终我们做到了。
以往团队都是几天甚至几周才提交一次代码,我们先将代码提交的要求限定在每人每天至少提交一次,为此,我们专门坐下来与团队成员一起分解任务,将他们理解的大任务拆分成一个一个的小任务。
……
想做事,只需要一个理由就够了,不想做,理由有一万个。劝那些不想改变的人改变是异常耗时而且收效甚微。最好的办法是,找到愿意和你一起改变的人,做一件具体的事。
我们并没有劝说谁去听从我们的想法,只是在一个一个地解决问题。我们花了很长时间,最终建立起了持续集成,看到大屏幕上的绿色标识,我颇为感动。原本只需要一两天搭建的持续集成,在一个复杂组织中,它要花费那么长时间,这也是我从未经历过的。
当我们把这件事做成之后,其他团队看到了效果,开始纷纷效仿。于是,原本复杂的各种规定也开始纷纷松绑,比如,他们再也不需要为申请电脑发愁了。至于之前质疑我们的人,因为看到了成效,他们的关注点就成了怎么把事能做成。
后来我听说,他们在组织内部专门建立了一个持续集成中心,为各个团队提供了公共的构建资源,提升了整体的效率。
Linus Torvalds 曾经说过“Talk is cheap. Show me the code. ”讲道理很容易,但也难以让人真正的信服。同样,做事很难,但成果摆在那里,让人不得不信服。
在英文中对这种行为有一个说法叫“Lead by Example”通常用来形容团队领导以身作则的行事风格。当你寻求改变时无论你的角色是什么你都需要扮演好领导者的角色“Lead by Example”送给你
问题2测试怎么写
andyXH 同学提到
目前对于 TDD 还是处于理解状态不知道如何真正的在项目工程中使用。因为项目工程往往还有很多其他调用如rpc数据库服务第三方服务不知道在这个过程如何处理。期待老师在之后文章中讲解。-
——《13 | 先写测试,就是测试驱动开发吗?》
梦倚栏杆 同学提到
从数据库或者第三方api查询类内容需要写测试吗这种测试怎么写呢如果不需要写会发现大量展示类系统不需要写测试了感觉怪怪的。-
——《16 | 为什么你的测试不够好?》
闷骚程序员 同学提到
假设我要测试的函数是一个关于tcp的网络发送函数我想问一下老师在写类似这样功能的单元测试是怎么实现的-
——《39 | 面对遗留系统,你应该这样做》
TimFruit 同学提到
问个问题一般web服务依赖数据库这部分如何做好单元测试如果去掉数据库很难测试相应的sql语句。-
——《39 | 面对遗留系统,你应该这样做》
大家看到了,这是一类非常典型的问题。一般来说,如果写的测试是一些业务逻辑的测试,大多数人还知道怎么测,一旦涉及到外部系统、数据库,很多人就不知道该怎么办了。
我们先来回答一个问题,你要测外部系统的什么?
你当然会说,我的整个系统都依赖于外部系统,没有了它,我的系统根本运行不起来,不能完成工作啊!但是,我的问题是你要测的是什么?
我知道很多人一想到外部系统,第一反应是:“我的整段代码都是依赖于外部系统的,因为外部系统不好测,所以,我这段代码都没法测了。”如果你是这样想的,说明你的代码将对外部系统的依赖在业务代码中散播开了,这是一种严重的耦合。外部系统对你来说,应该只是一个接口。
我在《13 | 先写测试,就是测试驱动开发吗?》中说过,想写好测试,先要站在可测试的角度思考。假设我同意你关于外部系统不好测的观点,那应该做的是尽量把能测的部分测好。将对外部系统的依赖控制在一个小的范围内。
一个好的做法就是设计一个接口让业务代码依赖于这个接口而第三方依赖都放在这个接口的一个具体实现中。我在《34 | 你的代码是怎么变混乱的?》中提到了 SOLID 原则,这种做法就是 接口隔离原则ISP的体现。
如果你能够站在系统集成的角度思考这个部分就是系统与系统之间的集成点。我在《37 | 先做好DDD再谈微服务吧那只是一种部署形式》提到了 DDD。在 DDD 的战略设计中有一个概念叫上下文映射图Context Map在不同上下文中集成最常见的一种模式是防腐层Anti-Corruption LayerACL
很多系统在实现时就是缺少了防腐层,造成的结果就是系统耦合极其严重。因为外部服务的任何修改都会造成自己的代码跟着大幅度变动,更极端的情况是,我见过一个网关系统在自己的业务逻辑中直接依赖于第三方服务传过来的 JSON 对象,造成内存资源的极大浪费,网关本身极其不稳定。
至此,你知道了,如果有任何外部系统,都要设计防腐层,用接口做隔离。这样,才能保证你的业务代码是可测的。如果外部系统真的不好测,这种做法将大幅度降低不可测的比例,尽可能提高测试覆盖率。
我们前面的假设是,外部系统不好测,但真的不好测吗?
作为 Moco 这个模拟服务器的作者,我肯定是不会同意这个说法。如果你的系统依赖的外部系统是最常见的 REST 服务,那 Moco 就是给这种场景准备的。我给你看一个最简单的例子,这是 Moco 中最简单的用法:
@Test
public void should_response_as_expected() throws Exception {
HttpServer server = httpServer(12306);
server.response("foo");
running(server, new Runnable() {
@Override
public void run() throws IOException {
Content content = Request.Get("http://localhost:12306").execute().returnContent();
assertThat(content.asString(), is("foo"));
}
});
}
在这个例子里,你设置外部服务的行为,让它按照你的需求返回特定的内容,然后,运行你的服务去访问这个外部服务,它和你访问真实服务效果是一样的。而且,通过 Moco你还可以模拟出一些真实服务不可能给你做出的效果比如连接超时。
这里给出的是一个用 Java 编写的例子。如果你采用的是其他语言,也可以使用 Moco 的 Standalone 模式,用 JSON 配置出一个模拟服务器。
对于数据库的测试,如果你采用的是 Spring Framework它就提供了一套完整的方案比如你可以在运行测试时插入一些数据然后在测试执行完毕之后回滚回去保证测试的可重复性。
事实上,它对测试的支持已经非常强大了,远不止于数据库。如果你采用的是 Spring Boot对测试的支持就更加完整了但基础还是 Spring Framework 提供的。如果用到真实的数据库,最好是一套独立的本地数据库,保证环境的可控。
对于外部服务的测试,简言之,能模拟的就模拟,能本地的就本地。如果你的服务没有现成的工具支持,也许就是一个打造新工具的好时机。
总结一下。关于外部系统的测试,你可以先通过接口隔离开来,然后,通过模拟服务或本地可控的方式进行测试。
好,今天的答疑就到这里,你对这些问题有什么看法呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,150 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
答疑解惑 如何管理你的上级?
你好,我是郑晔。
在这个模块里,我围绕着“以终为始”这个原则为你进行了详细地讲解,还给你介绍了应用“以终为始”原则的一些行业最佳实践。
同学们的留言特别踊跃,很多同学表示有收获的同时,也提出了大量的问题,大家比较关心怎样将这些实践在自己的实际工作中落地,部分问题我已经在留言中回复了。在今天的答疑环节中,我挑选了一些非常典型的问题来更详细地回答一下。
问题1领导要求的无力反驳怎么办
achenbj 同学提到
讲得很好,感觉落地还需努力。我们就是领导给了功能,跟你说下要做啥,就那么做就行。没有了。-
—— 04 | 接到需求任务,你要先做哪件事?
Alexdown 同学提到
考虑到地位的不对等以及我的“人设”已经定型了,实施起来有点难度。-
—— 02 | 以终为始:如何让你的努力不白费?
这类问题很经典,很多同学留言提到,我就不一一列举了。
在我的职业生涯中,无数次听到不同的人有过同样的抱怨。我最初也觉得这是一个无解的问题,直到后来我读到了一本书。
管理大师彼得·德鲁克有一本经典著作《卓有成效的管理者》,虽然标题上带着管理者几个字,但在我看来,这是一本告诉我们如何工作的书,每个人都可以读一下。
当年我读这本书时,其中的一个观点让我很受震撼:如何管理上级。
什么?上级也可以管理?这对于我们这些习惯了接受上级指挥的人来说,观念上的转变几乎是天翻地覆一般。
在很多人看来,自己累死累活,只是因为自己的笨蛋上级,没有很好地处理好他该处理好的事情,还把“锅”扣到了自己的头上。
不过,在德鲁克看来,上级也是人,一样有着长处和短处。我们应该发挥其长处,减少其短处带来的不良影响。管理上级,也就是要发挥上级的长处,不能唯命是从,应该从正确的事情入手,以上级能够接受的方式向其提出建议。
具体到我们的日常工作中该怎么管理上级呢?我给你一些小建议。
我们要敢于管理上级,对上级不合理的要求说“不”,这是一个思想上的转变。很多人受到传统官本位思想的影响,对上级的服从达到了不健康的程度。勇于改变,是有效管理上级的前提条件。如果不从思想上转变,我接下来的建议都是没有价值的。
那具体要从哪些方面着手呢?
第一,管理上级的预期。
上级问你:“一个产品特性,你多长时间能做完?两天?一天行不行?”你想了想,如果不写测试,确实能够省下不少时间,于是,你决定答应上级的要求。是的,大部分人就是这么妥协的。
妥协很容易,但再往回扳就不容易了。下次,他还会再进一步压缩:“半天能不能搞定?两小时行不行?”人的欲望是无限的,所以,就不要让上级有错误的预期。
如果是我,我会告诉上级,这个压缩会影响到什么。比如,要想做这个调整,你需要放弃的内容是什么;或者,我可以给出一个快速上线的临时方案,但接下来的几天,我需要调整,让代码回到一个正常的状态中。所以,你就不要给我安排新工作了。
这个过程,相当于我把自己看到的问题暴露给上级,让他选择。他有更多的上下文,他会平衡该做的事情。
第二,帮助上级丰富知识。
不是每个上级都是经验丰富的知道所有事情。比如有些成长得比较快的负责人自己甚至都还没来得及了解软件开发全生命周期。在IT这个快速发展的行业里这是非常可能出现的情况。所以在某些局部你比他了解得多是非常有可能的。
在那些他做得不够好的领域,他肯定有许多烦恼。比如,盲目给需求的产品经理,可能也会影响到他对需求的判断。
这个时候,你就不妨把自己知道的内容找个机会给他讲讲。一个简单的方式是,把我专栏的内容发给他,和他一起探讨怎么做是合理的。然后,大家一起协同,改进工作方式。因为你是在帮他解决问题,他会更愿意接受。
第三,说出你的想法。
如果你什么都不做,上级会按照他自己的理解安排工作。比如,小李擅长处理消息队列,那消息队列的活都给他。
如果你有自己的想法和打算,不妨提出来,主动承担一些职责。比如,你接下来打算多学点消息队列,那就大大方方地告诉上级,下次有相关的活,考虑一下自己,上级再安排工作的时候,他就会多想想。这其实就是我们熟悉的一个最简单的道理:会哭的孩子有奶吃。
如果经过你的种种努力,发现你的上级真的是完全没法影响,只能以令人无语的方式行事,那你需要仔细考虑一下与他合作的前景了。
不过,更可能出现的场景是,你还没去尝试改变就放弃了,将全部责任都归结于上级的问题。如果你是这种思考问题的逻辑,不论到哪个公司,结果都不会比现在更好。
问题2产品经理总拿老板说事怎么办
此方彼方Francis 同学提到
很多时候产品要做这需求的理由就一个:老板要的!-
——01 | 10x程序员是如何思考的
西西弗与卡夫卡 同学提到
有的产品经理会使出必杀技——这是老板的需求-
——06 | 精益创业:产品经理不靠谱,你该怎么办?
用老板来“甩锅”,这在软件行业中特别常见。
实际上,老板要求的是方向,不是产品特性。大老板不会安排那么细的细节。所以,一个产品经理该做的事就是把老板给的方向,变成一个个可以实现的产品特性,他要分析其中的合理与不合理。
不合理的部分应该是他和老板去沟通的,而不是让开发团队来实现。
在真实世界中,更有可能的情形是,产品经理“拿着鸡毛当令箭”,老板说的是试一下,到他这里就变成了必须完成。他不敢对老板提问,就只能压迫下游了。
这种情况,你就不妨和产品经理一起去见老板。我们在《解决了很多技术问题,为什么你依然在”坑“里?》这篇文章中提到,要扩大自己工作的上下文,这种做法也可以帮助你解决问题,在自己上下文中解决不了的问题,就放到更大的上下文中去解决。
问题3别人能做的我们也要做
Xunqf 同学提到
当你和产品经理理论的时候,他往往会拿出来一个现有的产品给你看:“人家怎么就能做到人家能做到说明技术上是可行的做吧。”时间久了你会发现他的需求全是抄的的别的APP然后就觉得别人能做到的我们也一定能做。-
——《06 | 精益创业:产品经理不靠谱,你该怎么办?》
你会发现,在这个问题里,提到了两个与产品经理交流可能出现的典型问题:一个是竞争对手有的产品,我们也要有;另一个是人家能做到的,说明技术上可行,我们也能开发。
我带你来分别看下,这两种说法你该如何应对。
第一,竞争对手有的产品,我们也要有。
没有哪个企业是靠纯粹抄袭成功的。我知道,你想说腾讯。腾讯当年做 QQ从形式上看是和 ICQ 极其相似的甚至名字都是极为相似的OICQ。但腾讯却做了自己的微创新它将信息保存到了服务器端而 ICQ 是保存在客户端的。
正是有了这样看似微小的创新,让当时大部分家里没有电脑的普通用户,可以在网吧里不同的电脑上继续自己的网络社交,适应了时代发展的需要。腾讯“抄”得好的东西,都是有自己微创新的,包括如今的微信。
“抄”不是问题,问题是无脑地抄。
所以,如果你的产品经理只想无脑抄袭,本质上,他就是在偷懒,没干好他该干的活。竞争对手有这个特性,他为什么要做?他做这个特性与它其他特性是怎么配合的?我们做这个特性,在我们的产品里怎样发挥价值?作为产品经理,你必须给我讲清楚这些。
即便我们最终的结果是,做的与竞争对手一模一样,经过思考之后的“抄袭”也是一件价值更大的事。
第二:人家能做到,说明技术上是可行的。
关于这一点,我不得不说,产品经理说得对。别人能做到,说明技术上肯定是可行的。
不过,我们必须分清楚两件事:需求和技术。
要做什么是需求,怎么做是技术。与产品经理要确认的是,这个需求是不是合理,该不该做。技术上能否实现,这是开发团队要考虑的事情,并不是产品经理说事的理由。
还有一种情况是,需求确实合理,但技术实现的成本极高,所需花费的时间很长。在这种情况下,你和产品经理之间很难互相说服。
解决方案是,将问题升级,放到更大的上下文中,让上一层的领导来决定,此时此刻,在现有的资源约束下,是否要按照这种方式做。同时,你最好再提供一个可选的替换方案,这样领导才能更好做选择。
还有一些同学问了很好的问题。比如,程序员充当了太多角色,很困惑。这个问题我在专栏中已经回答了,在《 接到需求任务,你要先做哪件事?》中,我们说,每次扮演好一个角色。在《 解决了很多技术问题,为什么你依然在“坑”里?》中,我们提到程序员应该多了解不同的角色。
还有人问,计划赶不上变化快,怎么办?简单回答就是靠任务分解,因为这个话题涉及到“任务分解”这个模块的内容,等这个主题讲完之后,如果大家还有疑惑,我们再来详细讨论。
好,今天的答疑就到这里,请你回想一下,你在你工作中是否也遇到过类似的问题呢?你又是怎么解决的呢?欢迎在留言区写下你的想法。我会从中筛选出典型的问题,与大家进行互动交流。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,75 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词:了解面试“潜规则”,从海选中脱颖而出
你好,欢迎解锁由拉勾提供的面试指导课程,我是你的职场导师堃哥。目前担任拉勾网的招聘总监职务,具有近 10 年的人力资源—招聘方向的工作经验,曾经就职于美团、新东方等公司。
原本读经济学的我无意中接触到了欧洲猎头顾问的工作流程,深深被他们的专业分析能力、对企业以及候选人熟练的把控能力所吸引,他们可以帮助一家面临困难的企业寻找到合适的候选人,同时也可以帮助那些处于职业迷茫期的候选人寻找到实现自我价值的工作方向。
因此毕业后我就回国踏入了国内的猎头行业。2010 年左右,国内猎头行业刚刚起步,所谓的猎头顾问还只是一个电话销售的职责,单纯、机械化地询问候选人是否考虑新机会。虽然和我预期的工作内容不太一样,但经过一年的电话沟通,训练了我可以通过候选人的介绍而发现他工作中和变化工作中的问题点。
后来为了进一步提升自己,进入了互联网公司。期间经历了互联网快速增长的过程、大批量的人员面试以及多样本的磨练,帮助我可以通过「快速提问、逻辑的沟通」来判读一位候选人的真实求职目的和个人性格方面的问题,帮助企业招聘的岗位从最初的助理岗到公司高管岗,从团队里的单个支撑岗位到整个新业务的建设。经过多年的磨练从最初的招聘小白,到帮助公司发现目前遇到的问题然后通过人才引入或者内部人才培养的方式使公司走出困境的招聘专家。
招聘市场的“551 定律”
你知道招聘市场的“551 定律”吗?
551 定律:每一层筛选环节都会有百分之十的折损率。一个岗位从接收简历到发下 Offer 至少要筛选 500 份左右的简历、面试 50 人左右、只有 5 人左右通过面试,最终也只有 1 位候选人可以顺利入职。
对于企业内的招聘人员平均每天至少在一个岗位上要收到几百份简历,至少要面试十几个候选人,每周至少会发出 4~5 个 Offer一周内至少同时会招聘 5~7 个不同的岗位。因此,可以看出,招聘人员的工作强度是非常巨大的,面试者需要把控好每个环节的节奏并表现优秀,才能得到最好的结果。
所以说你的简历是否对自己的工作内容和项目经历描述清晰你的面试表达是否可以直击面试官的问题要点你的Offer沟通是表现的完美无缺都对你能否赢得心仪的岗位至关重要。
面试的那些事
1.简历已被阅读却迟迟没有回应
相信大部分人都会有这样的疑惑:每个求职的早上都会迫不及待的打开手机查看是否收到昨天投递简历的回复,发现都显示为“您的简历已经被阅读”的状态,带着兴奋的心情等待着心仪企业的面试邀请电话,上午过去了没有接到电话,午饭时间过去了没有接到电话,下班了仍然没有接到电话,第二天、第三天……一直没有收到任何的信息,一度以为自己的手机是否坏掉了或者怀疑招聘人员是不是忘记了拨打电话…
2.面试很顺利却迟迟没有收到录用通知
相信你也会有这样的困惑:好不容易收到面试电话,兴奋的不能自已,提前和公司请了假,穿上非常体面的衣服,吃一个元气满满的早餐,做好充分的准备去面试。面试时费尽心力地展示自己,把从事过的工作内容和听说过的项目经验全部展示在面试官面前,这时的你侃侃而谈,表现得非常自信且积极正能量。但是,不知是否留意过面试官有时候针会对一个问题进行深入的提问或者偶尔出现锁眉的动作?
3.面试了很多公司,难道能力已被透支
相信你有过这样的经历:面试了很久,也面试过 N 家公司,最后一份录用通知书也没有收到,此时是否开始对自己的工作能力表示深深的怀疑,真的是自己不够努力吗?之前的工作能力真的那么水吗?有没有想过也许就是与招聘人员沟通时的语气,与部门负责人的一次错误的意见表达等,导致公司对你的看法完全改变了,进而错过了接受心仪公司录用通知的机会。
这时候的你是不是感觉面试的道路上充满了坎坷,对自己的能力也产生了深深的质疑?
其实求职之路并没有想象的那么复杂,接下来我会从一个面试官的角度来告诉你在简历上需要注意的细节点以及哪些内容是必须要提到的;在面对面试官时如何张弛有度地展示自己,在沟通中如何让面试官感觉到你就是那个公司一直在寻找的人选。
专栏设计
本专栏将解决以下问题
首先,将讲解如何设计简历,使其具有竞争力。比如,什么样的简历是招聘人员最喜欢的,怎么样的板书清晰明了、逻辑清晰,对于自己想要面试的职位如何更好的突出自己简历的优势,哪些工作项目是为自己增分,而哪些项目即使再努力描述也只会减分等。
其次,将从投递简历的时间点和投递方式开始分析,通过大数据分析来告诉你企业招聘人员的工作作息,这样有针对性的投简历更能快速地让面试官看到。
继而,在面试过程中怎么介绍自己会显得更有逻辑、更受面试官的欢迎。面试的哪些动作是说明他对你的经历很有兴趣需要多多的展示自己,哪些动作是对你的经历有所怀疑,可以换种思路介绍。
再次,是否会想过:你对企业真的有价值吗?真的是企业在找的那个人吗?知道自己的价值后才可以更好的为自己争取符合自己能力的薪资。专栏里会简单介绍行业的薪酬情况,以帮助你更好的明确自己如何找到定位,以及当接到多个录用通知的时候,如何清晰地判断哪个最有利于自己发展。
最后,经过九九八十一难,终于收到 Offer 了。可是,怎么和公司谈离职的时候还可以得到好的离职背调评价呢?社保、公积金等福利怎么做好两家公司之间的交接工作?专栏的最后会和你分享如何优雅的分手前司。
你将获得
这个专栏适合所有在求职过程中遇到问题的读者,不论你是一位刚刚踏出校园的职场菜鸟,还是已经工作多年的职场老手,只要在求职过程中遇到过以上问题,都会为你解答。
通过这个专栏的学习,将有以下收获:
可以写出一份直接抓住面试官眼球的简历;
在设计简历时突出自己的优点、项目经验,以及个人特长怎么写才是最合适的;
面试中怎么回答一些常规问题更能获得赞许;
如何选出一份可以让自己职业发展最好的工作,为自己的未来做一个完美的规划。
课程寄语
认真看完这个专栏后,相信你通过对自己的重新塑造,在事业上能够有一个崭新和充满希望的旅程~

View File

@ -0,0 +1,165 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 设计一份吸引面试官的简历
你好,我是你的面试课老师杨宇堃,欢迎进入第 01 课时的内容“设计一份吸引面试官的简历”。
1.简历的外形
如果你刚入职场,是不是企图想通过“花里胡哨”的简历模版去引起面试官的注意?比如,套用网站上很多色彩斑斓的简历模版。
其实是徒劳无功的,色彩艳丽的简历模版确实可以给人耳目一新的感觉。但这种简历模版很容易扰乱面试官的思路,从而很难注意到简历里的核心内容;而且也很难要求每位面试官的审美都能和你有共识,万一选择了一款让面试官感觉很土的简历模版,从而会错失一次很好的面试机会。
因此,简洁干净的纯色底简历模版可能是最好的选择。面试官也会把更多的注意力放在简历内容上,从而可以更好地判断你的经历是否与工作内容相符。
例如,下面是几个“花里胡哨”的简历外形截图:
可以看到上面的 3 个简历外形,明显有两个问题点:
背景过于丰富,很容易扰乱招聘人员;
很多无用信息的展示,重点不够突出。
下面是一个比较清晰的简历模版,可供参考:
这份简历比较好的点有:
整体简历的排版清晰;
文字描述段落清晰和明确;
重点比较突出。
2.简历的结构与逻辑
在设计简历时可以分为几个部分进行阐述:个人信息、教育情况、工作经历、项目经历、自我评价、其他信息。
1个人信息
很多小伙伴在构思自己个人信息的时候,很喜欢把自己完全展示出来,如身高、体重、性别、民族、婚姻情况、政治面貌、家庭住址、籍贯等,统统都写在简历上。
但有没有思考过:这些信息是面试官所需要的吗?当然,特殊岗位可能需要提供身高、体重、籍贯、民族或者政治面貌。但是在如今这个互联网公司占据了很大就业市场的情形下,并没有太多的互联网公司在乎这些比较表面的信息,面试官往往更在乎的是姓名有没有写完整、电话号码和邮箱地址是否填写正确等,只有这些信息完整,面试官才可以快速地联系到你。其次面试官也会留意你的求职意向是否与应聘的职位相符,以及期望薪资是否在应聘岗位的范围内等。
上图的简历过于繁琐,罗列的很多个人信息不是 HR 所需要的,而且很难快速地找到个人的联系方式和邮件信息,这样会给招聘人员带来很大的困扰。简单的突出自己的关键信息既可以让招聘人员轻松的获取,也能让招聘人员认为这是一个逻辑清晰的候选人。
2教育情况
首先,建议求职的小伙伴在教育情况的部分只需要体现出正规被认证的学习经历即可,比如本科、研究生、博士生等的经历。很多小伙伴会将自己的高中,甚至初中的学习经历都要写上,也许你的初中、高中学校在家乡非常有名,但是对于企业来说很难判定和考究。因为学信网可查的学校只有大学院校,所以只需要填写高中毕业后的教育经历就好。
其次,在填写学校经历的时候只需要突出就学时间(入学时间—毕业时间)、学校名称、专业名称、学历(统招或非统招)即可。对于企业方可以很容易通过就学专业和学校判定出来你所学的内容,而不太需要你做过多的说明和解释。经常看到一些刚刚踏入职场的小伙伴在自己学校和专业信息下面会长篇大论地介绍自己的专业以及学习的科目,往往是非常徒劳无功的表现。
最后,部分小伙伴也会分享在校期间获得的所有证书和参与的活动,相信很多面试官在这里的停留不会超过 5 秒,所以,即使展示自己学习有多好也不会吸引太多的注意。倒不如在这里保持一个整洁干净的排版,将自己获得的证书或者参与自己擅长的活动信息挑一些重要的放在最后的其他信息中,这样也能给简历加分。如果觉得这里是否有些单调,可以放一些真正具有含金量的奖项。
3工作经历
工作经历往往是企业方非常在乎的一部分,这一块也是招聘方停留时间最长的地方。在写工作经历的时候一定要清晰标注这几个内容:公司名称、工作时间(开始时间—结束时间)、职位名称、工作内容、业绩成果。
a.公司名称
目前很多互联网公司的对外名称和他们的注册名称完全不同,所以在书写公司注册名称的时候建议在后面也标注一下公司的对外名称,也是帮助面试官判定你上一家所在的公司行业和领域,因为很多互联网公司的注册名称普及度并不是很高。
b.职位名称
公司名称后面建议标注所在公司的职位名称,职位名称可以写上职位的层级 + 具体做的事情,如招聘专员、招聘主管、招聘经理、招聘总监。往往面试官会根据岗位层级看你简历内容负责的事情是否与层级相符,进而来判定你的能力。目前大多数公司内部都有岗位职级,可以在职位名称后面标注上自己目前的职级,以方便面试官清晰的判定。
c.工作时间
建议具体到年、月,如 2016.07 ~ 2019.03,这样的书写方式,面试官可以很清晰的了解你的工作年限。
提示:在写工作时间的时候一定要注意每段工作时间之间的结合期是否连贯。如果没有连贯,且中间相隔的时间周期比较长,最好增加一个说明,为什么会有那么长的空白期、这段时间在做什么等。如果连续几段经历都出现这样的断档,需要慎重思考一下是不是要改变已有的工作习惯。
如果有很多段经历,建议优先写最近的经历,然后采用倒叙的顺序来写比较合适,因为大部分企业方习惯从最近的一份工作去了解。
d.工作内容
接下来我们聊一下简历中最重要的一个部分,也就是工作内容的描述。
我知道很多小伙伴在写职位描述的时候,习惯的做法是借鉴与自己相同岗位的职位 JD甚至有的小伙伴直接把大公司的职位 JD 粘贴到自己的简历中,表示自己做过相关的工作。
但有没有仔细阅读过这些岗位描述?往往企业在发布岗位描述的时候希望招的都是按照最全面的要求去撰写的,但是候选人的能力很少具备全部的要求,如果复制粘贴,很容易被 HR 怀疑简历的真实性。所以最好的做法是在设计简历之前,先认真的思考一下自己上一段的工作内容是什么,然后把这些工作内容按照重要程度依次精简描述出来,写到简历中。
当然,也有小伙伴说:我不太擅长文字的书写怎么办?没关系,如果你已经清晰知道自己的日常工作是什么了,也可以借鉴企业招聘职位中的描述。
假设你是一位销售助理,日常的工作为:
A.拜访客户与客户进行销售产品的沟通
B.收集客户的资料
C.日常的文档整理、合同的归档
对于 HR 来说A 和 B 的内容更为重要,所以放在前两条来展现自己日常工作的重要性,两件或多件内容中间要做好分段,描述完一件工作内容后记得另起一段来描述下一个工作职责。
如果有多份工作经历,且工作内容都比较相似的话,最好有一个递进的关系,每一段突出一个工作重点。
来看下下面的一个简历截图,能看出里面的问题吗?
有以下几个问题点:
工作时间倒序,并非 HR 的日常阅读习惯;
两段工作经历之间留有 1 年时间的空档期,很容易让 HR 表示怀疑;
工作描述过于简单,几乎没有突出自己工作的亮点。
e.业绩成果
如果工作中有过一些公司认可的奖状或者某一期的绩效非常优异的话,可以在“工作内容描述”后面增加一栏“业绩成果的展示”,但是这一栏的内容不易过多,把最重要或者很有价值的公司奖项或绩效按照重要程度精简出 1 ~ 3 条即可,如「公司年度优秀员工奖」、「上季度绩效为 A」等。
4项目经历
项目经历其实和工作内容描述表现的形式类似,只不过项目经历不用把每段都写在简历里,只需要选出一些自己作为主要参与人或者由自己负责的项目添加在简历里就好了。
项目经历的展示也需要体现出项目名称、项目时间、项目中担任的职务、项目职责和项目业绩。如果补充项目经历的介绍,则会给简历加分,也能更容易打动面试官。
5自我评价
不要小看自我评价哦,这一部分是上述简历整体的一个总结,大部分面试官很希望从自我评价中整体了解候选人的情况。
所以,建议从两个方面整体的评价自己:
首先先对自己过去的工作内容或者过去的学习经历做个总结
然后对自己的个性以及工作态度做一个工作展示
比如下面的「个人优势」其思路比较清晰:
上面的自我评价是一个可参考的模板:候选人不但突出了自己的优势,而且还清晰地展示了自己的学习主动性和学习能力,进而会给招聘人员留下很好的印象。
6其他信息
这里如果真的写不出来可以忽略当然如果有一些非常值得或者有帮助的奖项亦或培训内容也可以在这里展示比如某某专栏的发表、CPA 的认证、司法考试的证书等。

View File

@ -0,0 +1,57 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 读懂职位 JD精准投递简历
你好,我是你的面试课老师杨宇堃,欢迎进入第 02 课时的内容“读懂职位 JD精准投递简历”。
1.清晰的了解用人部门的招聘要求
上一讲是关于如何设计简历,相信大部分小伙伴可以很轻松的掌握,但这只是投递简历前的一个开始,明确工作方向才是真正的关键点。
很多小伙伴在投递简历的时候,很少仔细阅读招聘公司的招聘简章,只是投递出去了,甚至一个岗位投递多次或者一个公司投递多个岗位,尤其在刚刚毕业的小伙伴身上表现非常明显。可能大部分人会认为多投递几次会被面试官看中的几率更高,但往往是相反的,如果你自己的工作内容与职位的内容完全没有关联,只会被 HR 淘汰掉,然后放进简历库,而且目前很多公司在简历库里都会对投递来不合适的简历做备注,下次使用简历的时候面试官也会查看上次的评价。
Tips如果不想在心仪的公司里面留下污点建议谨慎地投递简历。
需要注意,投递的内容除了职位描述以外,还需要留意公司对人才的工作年限要求和工作的职级。例如,经常看到一些刚刚工作 1 年左右的小伙伴,直接投递公司需要 8 年以上要求的总监岗位。这样会给 HR 留下非常不好的印象,会认为这份简历的候选人对自己的定位非常不清晰,甚至有时候会觉得比较浮躁。
第一印象一旦形成是非常难改变的,所以建议各位小伙伴在投递简历时一定要做好两个准备:
要对投递的公司做好充分的了解,清晰地知道自己投递公司的业务是否和自己的发展方向一致;
投递的岗位是否与你之前的工作内容相符合。
一份满足以上条件的简历才会获得用人公司的邀请。
下面是一个某公司“产品经理”的招聘要求:
通过上面的职位描述,有几点可以注意一下:
在职位描述中,不断提到需要具有“独立负责…”、“负责具体…”等字样,说明这个岗位需要你能独立完成一些项目。如果你还是一个需要别人带着干活的话,最好不要盲目投递哦。
职位要求年限,这是一个要求有工作经验的岗位,比如需要具备 3 ~ 5 年的工作经验。往往 HR 希望求职者最好有 5 年左右的工作经验,具有 3 年工作经验是最低的要求。如果你的工作经验低于 3 年的话,就不要考虑尝试了,即使投递了也会被刷掉。
任职要求中的第4点说明这份工作的强度非常大成长性也很高。如果你是一个不希望太大工作强度或者想找一份轻松工作的小伙伴也要好好的思考一下哦。
建议:在投递简历的时候一定要注意 HR 在职位描述中的用词和一些细节,这样才能更准确地投递到心仪的岗位,进而获得一份满意的录用通知书。
2.最合理的助攻来自自己内心的力量
好的简历内容并不是通过简单地编写就可以实现了,一定是通过每天的努力工作和不断地反思才实现的。所以,即使简历构思再完美、逻辑再缜密,如果不是自己亲身经历和努力付出过的项目经验都会很容易的被发现漏洞。
一定要认真的对待自己的工作,每次的工作变动也要对自己的职业规划做好充分思考,这样才会拥有一份完美的简历和一份完整的职业规划。
可能很多刚刚毕业的小伙伴会问:“我没有工作经历,也不知道自己做什么。如果我不去尝试怎么可能知道自己适合什么呢?”其实很多小伙伴在大学读的专业也就已经明确了自己可以从事的方向,当然如果你认为自己读的专业不是你喜欢的,也许可以选择管培生的岗位,用 1 年的时间去体会各个岗位也许会对你有所帮助。
而对于工作 2 ~ 5 年的小伙伴,相信你们已经在一个岗位上至少工作了 2 年以上,如果这个时候还在反复,此时需要慎重的思考一下自己的规划了。

View File

@ -0,0 +1,39 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 把握投递简历的黄金时间段
你好,我是你的面试课老师杨宇堃,欢迎进入第 03 课时的内容“把握投递简历的黄金时间段”。
大家从事不同种类的工作,每天也在不断地制定自己的工作时间表。每个月总结的时候会发现有些事情总是在一个固定的时间去做,也可能在这个时间段发起同一件事情的几率非常的大,而且不止自己这样做,做同样工作的小伙伴亦如此。这就是工作种类作息时间的安排,招聘人员也一样,他们也有固定看简历和电话沟通的时间段。如果抓住这个“黄金投递点”,就等于抓住了招聘人员的视线,进而获得更多关注的可能性会更大。
HR 工作作息时间表
每个公司特别是互联网公司都有大量的招聘需求,而面对这么多的需求,公司 HR 是如何应对的?每天的工作作息时间是否有规律可循呢?
做为资深 HR 的我虽然每天都会合理安排工作时间和计划,但我自己的数据太单薄了,所以拜托拉勾网的研发小伙伴帮我调取了相关数据,其结果和我日常的工作安排非常的相似。由此可知,公司 HR 的工作节奏也都是差不多的。
下面通过曲线图的形式来展示拉勾网 HR 的工作作息表:
由上图可知,每天 HR 最活跃的时间段为上午 11 ~ 12 点、下午 4 点 ~ 5 点。也就是说在这两个时间段里,我们的招聘小伙伴在疯狂的筛选简历,即在招聘平台上筛选来自不同候选人的简历。
如果面试者投递简历的时间为上午的 10 ~ 11 点 或者下午的 3 ~ 4 点,那么简历有可能会被优先处理。相信你也有过体验:一天当中,上午的工作心情以及认真度普遍是最高的,也就是说投递的简历是最容易被招聘人员筛选出来的。
候选人投递时间表
上面分析了 HR 最活跃的时间段,那求职者是不是也有个投递简历的高峰期?如何错开高峰期呢?我也请研发小伙伴调取了拉勾网投递简历的数据。
下面通过曲线图的形式来展示候选人投递简历的时间表:
由上图可知,候选人投递简历的高峰期是在上午 11 点和下午 4 点这两个时间段,也就是说和 HR 筛选简历的时间段完全重合。相信你也有过类似的体验:当专心做某一件事情的时候,肯定不会注意到投递来的新简历,这就是为什么简历石沉大海的原因。
通过上面两个数据的分析,相信你也应该知道了 HR 筛选简历的时间段,由此可知,投递简历的“黄金时间段”在上午的 10 ~ 11 点 或者下午的 3 ~ 4 点。因此,从现在起,调整投递简历的时间吧,在更好的时间段将自己的简历呈现到 HR 的面前。

View File

@ -0,0 +1,80 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 做好充分的准备去面试
你好,我是你的面试课老师杨宇堃,欢迎进入第 04 课时的内容“做好充分的准备去面试”。
日常 HR 的工作流程解析
可能你会问HR 不就是每天守在电脑前等着投简历,然后第一时间去筛选简历吗?为什么还会有“投递黄金时间段”这样的说法?不应该是随时投递简历都可以吗?有这样疑惑的你就太误解 HR 的工作了接收简历只是他们手上工作的一部分。HR 每天还需要处理其他很多的工作,就像很多不同岗位的小伙伴同时负责很多工作一样。
首先,电话沟通是 HR 核心工作内容的一部分电话沟通并不是简单的通知候选人何时何地来面试。可是大部分人反馈说我每次接到电话就是通知面试并没有说其他的。其实在电话沟通之前HR 就已经仔细阅读过简历内容了,有疑问的地方会标记出来,在电话沟通过程中与你确认具体的问题点在哪里。
所以,电话沟通分为两种:一种是电话预约;另外一种是电话确认。
电话预约已经很清晰了,就是确认面试的时间。
电话确认一般发生在 HR 不确认你对招聘的岗位是否有兴趣或者简历存在比较大的问题的情况下。也就是说在接到电话沟通时不要表现的太过随意,因为你在电话另外一端说的任何一句话 HR 都会有一个潜意识的判断,这个判断很有可能会影响到你的面试环节。
如何在电话沟通时表现良好呢?
一定要对投递的公司有所了解,也正如前面说过的,很多人都是随便投递简历,最终也不记得都投递了哪些公司,所以在电话沟通时,招聘人员很不喜欢听到“请问你们公司是做什么的?”、“这个岗位的职位 JD 可以发给我看看吗?”、……,类似的声音,他们会认为你都没有认真对待这个岗位。
电话沟通时语气非常冷漠,明显表现出非常不高兴的情绪。其实 HR 在电话另外一端可以很清晰地听出对方的性格和表现。例如,有的人声音很消极,给 HR 的感觉是这个人对工作没有热情;有的人声音非常强硬,很容易给 HR 对方不是很好相处的印象。
因此,一定要记住自己投递过的公司和岗位信息,给 HR 一种你已经做好准备的感觉;然后掌握沟通的技巧,清晰地表达自己的想法和观点,语气尽量亲切一些,这样 HR 应该很愿意和你沟通,还能留下不错的印象。
其次,面试安排其实也会占据 HR 的很多工作时间。这个环节和电话沟通一样,你的表现也很容易被 HR 作出初步的判断。如何在面试安排时表现良好呢?
要有时间观念,尽量提前 5 ~ 10 分钟到达面试地点。很多招聘企业非常喜欢遵守时间的人,而且 HR 每天都会安排很多场面试,可能每个候选人的面试时间都是相连的,如果迟到很有可能会影响面试官的时间安排。如果有特殊原因,最好提前 0.5 ~ 1 个小时通知 HR争取能调整面试的时间。
要保持个人卫生,相信不论到任何场合,个人卫生非常差的话很难受到欢迎。虽然不需要你穿着有多奢华,只要保持衣着整洁、干净,会给 HR 留下不错的印象。
保持礼貌,中国有句古话“礼多人不怪”,虽然 HR 只是帮你协调面试,但也起到了你是否可以入职的关键因素。所以保持礼貌和谦虚的心态和行为,可以得到不少的加分。
当然,除了上面提到的工作内容,很多 HR 还会负责面试、发放 Offer 以及办理入职相关的手续,这些内容将在后面详细讲解。
其他投递渠道分析
目前 HR 收取简历的主要渠道有招聘网站、猎头、内推、校园招聘这几类。接下来说说这些渠道都适合什么样的求职者,去帮助你准确地判断通过哪种渠道可以更好的找到工作以及心仪的公司。
1.招聘网站
不用过多介绍,目前很多求职者都在使用,比如拉勾网,其是一家专注垂直互联网领域的公司,如果有研发、产品、运营等互联网方向的岗位需求,可以到该网站上发布岗位或寻找人才。
我在面试的时候经常有人问:常年累月挂着招聘是真的有招人的需求吗?企业究竟是真的想要招人,还是只挂在网上做做样子?
你可能经常也会有这样的疑惑其实很多企业挂出去的岗位目前都是在持续招聘的可能招聘的紧急度并没有那么高也有可能是一个储备的岗位HR 会先将岗位挂出去,对投递来的简历会仔细审阅,直到选出最合适的人选才会安排面试。
2.猎头
猎头渠道对候选人的要求可能会更高,一般是针对高级管理者或者行业专家使用的渠道。如果你只有 1 ~ 3 年的工作经验,可能现在还用不到这个渠道。
此时的你是否有过这样的疑惑:看岗位是合适的,为什么最后得到的反馈就是不适合?
这个时候就要审视一下自己的简历正如第01讲所分析的是不是真的将自己的优势都凸显出来了招聘岗位所说的内容是不是都在简历中有所体现工作年限真的都留意到了吗自己的项目经验是不是也都体现出来了
3.内推
内部推荐是一个非常好的渠道,成功率非常高。如果你身边有不错关系的同学或者朋友在心仪的公司,请他内推给 HR可能反馈非常快不过面试结果还要看个人的努力了。
那为什么同一家公司自己投简历就没后续了,猎头推荐或内推就有面试机会呢?
其实猎头的工作并没有那么神奇,每位猎头顾问在推荐你简历的时候都会与你进行沟通,在沟通的过程中会提炼出你简历里面没有展现的优势,然后猎头会对你的简历进行修改,以突出工作中的重点,以便让 HR 清晰地捕捉到想要的信息。所以说将自己的简历写出价值是有多么重要。
4.校园招聘
校园招聘相信都经历过,这个渠道主要是针对应届毕业生的,每年都会在 3 月或者 10 月份进行。如果你已经离开校园了,也就没有办法参与这个渠道的招聘了。
那互联网企业招聘现在更看重是学历?还是本身具备的技能?
可能有的人比较忐忑:我学历背景并不是很好,企业在招聘的时候更看重学历还是看中我本身的技能呢?当然很多人会告诉你学历是非常重要的,同时企业也会注意你的学习能力。如果你在后期努力进修了更好的学历,其实可以充分的展现学习主动性,也会很获得企业的欢迎。当然如果你的项目经验非常完善,在过去的工作经历中负责过比较好的项目,那完全可以掩盖你学历上的差距。
希望通过对以上渠道的分析,能准确地使用它并获得邀约的机会和面试的流程。

View File

@ -0,0 +1,132 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 把握面试时的关键点
你好,我是你的面试课老师杨宇堃,欢迎进入第 05 课时的内容“把握面试时的关键点”。
按照前面几讲内容的提示终于接到心仪公司的面试邀请了心情很激动也伴随着一些小小的忐忑内心OS该如何应对面试呢。下面我将解读从进入公司到面试结束后的这段时间该如何表现自己如何抓住面试官的心也就握住了这次工作机会的核心关键点。
面试前的准备工作
先说说面试前的准备吧。常规的准备相信你一定知道,比如制作一份吸引 HR 的简历、穿一身体面的衣服、整理一下自己的发型等。简历相关的准备前面已经详细讲过,这里就不多介绍了。
下面说说穿着相关的准备,很多小伙伴认为面试时的穿着并不是很重要,面试官肯定更看重个人魅力和知识的储备。当然这么说是没错的,但如果你和面试官首次见面,在还没有开始正式聊天之前,他是无法感知你的个人魅力或者知识储备的。
假如第一次见面就看到邋遢的外表或者奇怪的着装,面试官会怎么给你贴标签呢?首先他一定会认为你并不尊重这次面试,给他造成一种没有礼貌的印象;然后就是被你身上的味道熏倒无法和你多交流;最后根本来不及了解你的个人魅力和知识储备就草草地结束了这次面试。相信这个结果一定不是你想碰到的吧?所以,干净得体的着装是面试非常重要的一个环节。
面试官也会通过你的着装去判断你的性格,以及判断与公司的文化、团队的气氛是否匹配。这时可能你会问:我也没有进入到这家公司和团队,该如何判断面试当天穿什么衣服才符合这个公司的文化或者符合这个团队的气氛呢?当然,我们没有办法做到“把面试官的感受照顾到很细”的层面。
但是不同的穿着一定会表现出你的性格,有些表现出来的性格可能不会被大众所接受的,希望可以回避一下。下面简单说说几种可以表现性格的穿着:
喜欢穿简单朴素衣服的人,往往给人的印象是性格比较沉着稳重、为人比较真诚和随和,无论是在工作或学习上,还是在生活中,会给人一种勤奋好学、诚实肯干的感觉;
喜欢穿样式繁杂、颜色多样、花里胡哨的衣服的人,多是虚荣心比较强,爱表现自己而且又是乐于炫耀的人,会给人一种性格有些飞扬跋扈的感觉;
喜欢穿浅色衣服的人,性格比较活泼好动,十分健谈,会给人一种喜欢交朋友的感觉;
喜欢穿深色衣服的人,性格比较稳重,显得城府很深,会给人一种比较沉默,做人做事深谋远虑的感觉。
如果你希望在面试中表现的不是那么具有攻击力或者给人比较亲和、稳重性格的话,建议穿简单、朴素、纯色的衣服,会显得整个人比较清爽,且比较容易亲近,相信面试官也愿意和你多聊几句。当然不仅穿着干净,而且一定要注意个人卫生,最好不要让自己身上的体味过重或者使用太重味道的香水。化妆时,不建议浓妆艳抹,自然的淡妆让自己看起来很精神就可以。
如何全面的介绍自己
接下来就是面试的过程了,首先面试官会说:“请简单介绍一下自己。”
面试官有两个目的1希望通过你的简单描述可以和简历上的经历做校对2通过简单地介绍来看看你的逻辑和总结能力如何。所以自我介绍也是非常重要的一个环节好的自我介绍一定要做到以下几点。
面试时的自我介绍
一定要把握住时间。面试时的自我介绍一般控制35分钟最合适尽量不要超过10分钟。时间过短说明你根本没有清晰的介绍自己这时面试官很难了解你到底做了什么时间过长可能很多内容不是面试官需要的信息这时大部分的面试官会主动打断你从而留下了不太好的印象。
那如何把握好时间呢建议在介绍时包含以下几个部分就好1情况介绍包括教育经历2工作经验的介绍3介绍最有价值的经历。这样的一个自我介绍应该可以很好的控制在5分钟左右了既可以让面试官清晰的了解你的情况也能表现出你的优势。
面试过程中需突出的几个点
在面试过程中一定要突出以下几个点:做过什么、有哪些工作业绩、优势是什么,这样可以很好的突出自己。
做过什么:介绍自己,把自己曾经做过的事情说清楚,每段工作对应时间节点的公司名称、担任职务、工作内容等,尤其是对最近两份工作做过的事情要重点说说,较早之前的工作经验,或者学习的经验可以一带而过,要把握“重点突出”的原则。
有哪些工作业绩把自己在不同阶段做成的有代表性的项目经验介绍清楚但是一定要注意1 应与应聘岗位需要的能力相关的业绩多介绍不相关的一笔带过或不介绍因为面试官关注的是对用人单位有用的业绩2要注意介绍你个人的业绩而不是团队业绩要把自己最精彩的一两段业绩加以重点呈现。当然也要做好充足的准备可以迎接面试官的提问。
突出自己的优势:注意介绍自己的优势一定要与应聘的岗位密切相关,主要是围绕自己专业特长来介绍。除专业特长以外的特长,特别突出可以介绍,但要点到为止。
举个例子你好我是某某2018年3月加入XXX公司担任产品经理一职主要负责公司核心产品的规划和设计工作在这段期间我独立完成过XX项目的产品跟进和上线的工作将产品的数据提升了30%业绩突出获得了公司的认可。在项目中我通过学习和与外部专家的沟通获许了XXX新策略的信息并积极尝试达成了我的目标。
每段工作的离职原因
在面试的过程中一定要突出自己职业规划的逻辑性,也就是说需要让面试官感受到你的每次工作变动都是为了个人成长以及有规划的进行变动。所以在表述的时候最好可以清晰地说出你在每段工作中的收获和成长点,当然如果在陈述这些内容时可以体现出你的个人思考,就更是画龙点睛了。
如何回答面试中的问题
相信你经常会碰到面试官问以下的问题,这些问题也是面试官给你的一些考验,如果更好地回答这些问题可能会成为你入职心仪公司的敲门砖。
你为什么选择我们公司?
这个问题相信不少小伙伴遇到过,可能你的原因是随便投递、公司离自己住的地方近、工资给的高、公司不加班、公司有各种补助等。如果这些答案出现在你的面试回答中,那 HR 会重新考虑是否要录用你了。
所以在回答这个问题时需要有一些准备:
可以先描述一下自己的能力与岗位要求的契合度,表现出在公司提供的岗位上有机会可以一展所长;
说出几个被企业所吸引的优点,这些优点能为以后的工作带来什么好处;
自己的职业发展与公司前景作出总结。
相信这些回答可以很容易抓住面试官的心,不过前期也是需要你对这家企业,以及所招聘的岗位做了一定的功课。
你为什么从上家公司离职?
也许你在前公司受到了委屈、也许前公司人事关系复杂所以离职,但无论前公司有多么的糟糕,都千万不能在面试时说出来。因为你在上家公司离职的原因,会使面试官联想到你会不会因为在新公司受到委屈而轻易离职?再者,面试官其实并不关心你为什么要离职,所以面试时只需要给在场所有的人一个都可以接受的答案就可以了。
例如,可以这样回答:为了更好的发展,所以选择离职。切记在回答这个问题的时候,不能贬低前公司、不要损害前领导的形象。
你的优点和缺点是什么?
相信很多小伙伴对这个问题都很头疼,自己的优点说的太多会让面试官感觉过于自大,可在面试的过程中又有谁愿意说自己的缺点呢?下面列举几个简单的方向,希望可以帮助你解决这个尴尬的困境。
优点:可以结合过往的工作经历和工作业绩等讲述一下自己的优势。例如,我曾经参加过某某项目,相信我的这个工作经验可以很好的帮助到公司解决什么方面的问题等。当然也可以通过一些例子说明自己的人品或性格方面的优势,哪家企业可以拒绝一位性格和能力都很好的候选人呢?
缺点:金无足赤、人无完人,要勇敢的面对自己的缺点,可以向面试官说明,你针对自己的缺点做了哪些改变,以此来说明你正在积极地改变自己去成为更优秀的人。
未来 3 年或 5 年,你的职业规划是什么?
当面试官问到这个问题时,是希望看到你的自我学习力和未来牵引你的职业动力是什么。对职业规划不清晰的人,很难获得成功,也不会在一个岗位上待很久,所以也不是公司最合适的人选。
当被问到你的职业规划是什么的时候,此时可以设定一个短期就能实现的规划和一个未来希望实现的目标。
例如,我希望可以在未来的 1 ~ 2 年内,梳理和参与到几个完整的项目中,从中学习和看到整个项目进度是什么样的,从而提升自己的工作能力和项目经验。在未来的 3 ~ 5 年内我希望可以独立承担项目,做一个可以让大家都能使用并且体验良好的产品出来。
这样的回答,在短期规划上会让面试官认为你是一个脚踏实地,希望可以通过学习而成长的人,而且也在积极的改变自己;在长期规划上也能让面试官感受到你对这份工作的热情,具有很强的成就动机。
在选工作中更看重的是什么?
很多小伙伴反馈,这个问题很难回答,其实也能想到面试官肯定更看重你的是个人成长和发展空间。当然也许你的内心想的是涨薪或者培训,虽然薪资是一定的,但是如果让面试官认为你是一个物质的人,并没有长久的培养空间,那面试的结果就可想而知了。
你还有什么问题吗?
这是面试结束前的最后一个问题,也可以认为是个形式问题或走个流程,此时可根据前面面试过程中的表现程度来适当的提问,比如公司福利、上下班时间、团队氛围、个人岗位发展等,但尽量不要问从网上就能查到公司信息的问题。

View File

@ -0,0 +1,35 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 判断公司背景,做出合理选择
你好,我是你的面试课老师杨宇堃,欢迎进入第 09 课时的内容“判断公司背景,做出合理选择”。
前面的几讲我们讨论了面试时需要做哪些准备,以及分析了如何投递心仪的公司。在投递简历时除了心仪的大厂以外,也可以考虑投递一些不知名的公司,但如何判断这些不知名的公司是否靠谱,或者说如何知道哪家公司才是最适合自己的呢?接下来我将和你一起探讨。
有一些小伙伴因为没有提前了解公司,只是单纯急切地想拥有一份工作,最终可能导致入职后不久会再次选择离职。所以,判断一家公司是否合适,对自己的职业规划才是一种非常负责的态度。那下面我们就好好聊一聊,判断公司背景的几个方法。
通过网站上展示的相关信息来判断
互联网对于大家来说已经不是那么陌生了,所以很多企业的信息都可以在互联网上获得。我们可以通过这家公司的官网去了解它的基础业务和公司发展信息;也可以通过拉勾招聘网站上的企业界面介绍页去了解这家公司的基础信息。如果还不放心的话,还可以到“天眼查”或者“企查查”官网上查一下这家公司是否是一家正常运营的公司。
当查到这些信息后该如何判断公司的背景呢?
1公司的主营业务。可以查看目前这个主营业务是不是你希望从事的是行业内的蓝海还是一个比较传统的业务。比如可以通过传播媒体的报道来了解或者如果一家公司在一年内迅速扩张那这家公司必然是蓝海企业。
2公司的融资情况。可以通过了解公司的融资轮次以及投资的金融机构来验证是否是一个成熟的公司因为优秀的金融机构对公司进行投资会非常谨慎。
3公司的人员规模和办公地点。这些也可以判断一家公司的情况假如公司人员比较多则说明业务可能更为稳定一些办公地点在比较正规的办公大厦里也代表了一种稳定的因素。当然不是说好的办公地点和公司人员数量多就是最好的公司也有不少几个人的初创公司也是很值得考虑的。
4公司的创办时间。除了以上因素以外肯定要考虑公司的创办时间可能刚刚开始创办的公司不具备那么好的条件。如果你比较喜欢初创业的公司那么可以通过公司经营的业务和投资机构的名气去判断但是如果是一个创办时间比较久的公司可能就要考虑得稍微多一些了。
5收集创始人在网上的演讲稿。从他们的演讲稿中获得一些信息通过对这个人的看法对比是否和你期望的企业形式一致也可以判断出他是否具有行业的眼光但是尽量避免跳入公关稿的坑里去。
当然不排除一些经营很好,但是企业规模和融资情况一般的公司,这个也需要通过其他方面的信息去判断。
通过来自内部人员的信息判断
以上是通过拉勾招聘网站上的公司介绍页以及企业官网来判断公司的背景,但只是了解这个公司的表面信息,想了解这家公司真实的具体情况和管理风格,通过以下的方式可能会更清晰一些。
1如果这家公司中有熟悉的朋友或前同事或者学长学姐的话就比较方便了可以问一下他们关于公司内部的团队、部门、上下班时间、福利等情况甚至可以问一下团队领导的风格是怎样的。
2如果你没有这样的朋友关系也可以通过职言或者论坛的讨论去看看这个公司内部的员工是怎么评价公司的。
面试时通过面试官的言行举止判断
在面试的过程中,还可以通过与面试官的交流去判断这家公司的情况,如果面试官在面试过程中表现的比较有条理、沟通比较有素质,则可以说明这家公司还是不错的选择;也可以通过对公司环境的观察来判断这家公司的情况。当然你也可以看看拉勾网企业界面的面试评价区,去了解一下其他面试者对这家公司的评价。
相信通过以上几种方法,可以比较清晰地了解一家公司的情况,也可以帮助你更好的去判断这家公司是否值得去。

View File

@ -0,0 +1,48 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 了解行业薪资,清晰找准定位
你好,我是你的面试课老师杨宇堃,欢迎进入第 10 课时的内容“了解行业薪资,清晰找准定位”。
清晰找准自己的定位
相信面试到这里你已经在谈薪资了,此时的你是否会有这样的疑惑:我所期望的薪资是否能给到呢?在这里我建议你一定要对自己有清晰的定位,比如可根据你的工作年限、工作经验以及对市场行情等全方位的了解后,才能拿到合理的薪资,也会让企业认为你物有所值。
那如何才能争取比较合适的薪资呢?很多小伙伴会根据身边的朋友来判断自己的薪资是否合理。但是很多人没有考虑到,大家的学历不同、做过的项目不同、所应聘的公司也不同,那么薪资水平也很可能会有较大的差距,所以一定要明确自己的情况是怎么样的。
1如果你是应届生
如果你是一个刚刚毕业的小伙伴,如果从事的是基础的岗位一般薪资基本在 4 ~ 8K但如果选择做程序员假如学校背景还不错的话薪资可在 10 ~ 15 K。
不过不用太在意薪资这一块毕竟找一个有前景的工作会更重要建议对自己有一个短期1~2 年)的职业规划,相信在不久的将来,薪资也会翻倍的。
2如果你有工作经验
如果你已经是一个在专业领域工作多年的候选人,行业经验也非常丰富,相信丰富的经验可以为你创造比较高的收入,你可以比对行业的知名公司职级的薪资结构去判断自己的薪资情况。
3如果你有项目经验
当然也可能有小伙伴会问,如果我前一家公司的薪资高于市场行情,换一家公司是否需要继续要求增加薪资,还是考虑降薪?
这个问题我认为可以根据你的项目经验来考虑。如果你的项目经验是行业非常急需而且比较难得的,同时你又做得比较突出,你要求一个合理涨幅,很多公司也是愿意的。但是如果你的工作表现一般,只是一个负责副线项目的人,我认为对你来说很难争取到新的提成,所以不如脚踏实地地去做一个比较稳定的项目,为自己多积累一些相关的经验,也为后面的涨薪做铺垫。
一些行业的薪酬报告
下图是某热门岗位的行业薪酬报告:
图中的一些信息只是一些简单的分析,可以看出 2019 年整个互联网行业的薪资增长并不是很高,相比 2018 年下降了很多。这说明很多公司并没有那么多涨薪的预算,所以在选择公司的时候如果要求过高,很难获得心仪公司的 Offer。
其他岗位的薪资报告,建议你到拉勾招聘网站上搜索多家企业发布的同一职位的薪资范围,通过对比来判断该职位的薪资涨幅,但一般企业为了吸引人才,会将薪资范围提高,所以要酌情考虑。
比如下面是高科技与互联网行业的职位薪资情况:
上面的薪资情况,只是工程师的薪酬范围报告,仅供参考。

View File

@ -0,0 +1,55 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 目标明确,阐明沟通
你好,我是你的面试课老师杨宇堃,欢迎进入第 11 课时的内容“目标明确,阐明沟通”。
前面的两讲内容和你分享了如何明确自己所希望从事的职位和工作性质,也帮你梳理了如何更好地判断自己的价值产出在哪里。明确目标后,接下来聊聊如何更好地沟通才能拿下心仪的 Offer。
下面我们就说说在谈 Offer 时都需要做哪些准备。
知道自己想要什么
在开始谈薪资之前,需要明确自己到底想要什么,希望在这次的工作变动中有什么收获,比如想在团队氛围很和谐的公司里工作、希望积累更多的项目经验、还是仅仅为了涨薪等。
当你明确自己想要的是什么同时清晰的表达出来以后HR 会根据你的需求去匹配这个职位是否可以给到你所期望的东西,或者你也可以直接询问 HR 来判断是否能得到你想要的。
很多小伙伴会说,如果我确实不知道自己这次换工作想要得到什么,该怎么办?
你可以参考在之前的工作过程中,是不是有让自己感觉不舒服或者有挫败感的时候,同时想想是什么原因造成的,然后把它们整理出来写在纸上,标出来哪些是你希望可以得到改善和需要得到成长的。
通过这样的方式,再去想想在面试的过程中或者在和面试官沟通的过程中,这家企业是否可以给到你想要的东西。
明确自己的优势
明确了自己想要的也要知道你能给企业创造出什么样的价值这样才可以在薪资上做更有利的争取。面试结束后HR 已经对你的表现做了公正的评价,相信这些评价一定都是你对企业有价值的地方。
那怎么评估自己优势的价值呢?
首先要明确,你的优势是软性的优势还是硬性的优势:
软性的优势是指性格方面(比如性格好、踏实),相信很多人都具备,所以这个优势的价值可能就没有那么大的竞争力;
硬性的优势是指之前的工作经历给到的优势(比如项目经历、专业经历),这样的优势相比软性优势要更有竞争力。
其次如果你的硬性优势又是比较稀缺的项目经验或者专业经历的话,那就更有竞争力了。
不要敌化与 HR 的关系
在谈 Offer 的时候最终肯定会落在谈薪资的问题上你是不是也会有这样的感觉明明招聘网站上写的很高HR 却说给不了这么高?
因为招聘网站上显示的薪资范围,为了吸引用户会稍微提高一点薪酬水平。但是主要确定你薪资的并不是网站上的薪酬范围,而是你的真实能力。因为 HR 会根据你的真实能力去判断你在什么薪酬档位上,然后给出你合适的薪资。所以并不是 HR 不愿意给你高薪,要判断自己是否具有拿到高薪的能力。
如果 HR 问你期望的薪资是多少,该怎么回答呢?
如果你不清楚企业的薪酬结构,可以考虑给 HR 说一个年薪的的范围。但是建议提出的薪资涨幅不要超过你之前薪资的 20%,当然如果你特别优秀或者岗位是非常紧俏的岗位可以考虑多要一些涨幅,但是也不要太离谱。
因此,不管怎么谈薪资,建议你都要明确心态,尽量不要把 HR 当作自己的敌人,因为 HR 也有指标,也希望优秀的候选人可以顺利接受 Offer。
把 HR 当做朋友,先以平和的心态和他确认一下自己的各种疑惑或者不清晰的信息,然后再清晰地表达出期望得到什么,或者也可以让 HR 说一下他们可以给到你的都有什么,然后给彼此一些时间考虑一下(最好不要超过一周,不然会错失这个机会),相信这样的沟通方式大家都比较愉快。
坦诚地沟通
前面经历了多轮的面试和沟通后,最后在沟通 Offer 的时候同样要表现出你的真诚,不要在最后的关键时刻给到 HR 比较滑头的感觉, 不然结果会前功尽弃。
相信你学完这一课后可以拿到满意的 Offer同样也可以顺利的入职。

View File

@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 SQL vs NoSQL一次搞清楚五花八门的“SQL”
这一讲我们开始讨论有关 SQL 及其变种的前世今生,以及它与分布式数据库之间的纷繁复杂关系。
21 世纪的开发者往往要面对一种窘境:需在众多的数据库中艰难地做出选择。他们其实也想如老一辈技术人一样闭着眼睛去选择 Oracle 或者 DB2因为它们曾经被证明是“不会出错”的选择即无论选择哪款数据库都不会丢工作。
而时至今日,时代变了,我们如果不了解各种数据库内部的机理,即使选择大厂的成熟产品也有可能掉进“坑”里。因此,选择合适的数据库就成了日常工作中一项必备技能。
当然数据库的分类有各种各样的维度,在过去的 20 年中有一种分类法被广泛采用SQL关系型数据库VS NoSQL其他类型数据库。随着时间的推移又出现了一些新物种如 NewSQL、DistributedSQL 等。从它们的名字上看,这些数据库都与 SQL 产生了羁绊,那么 SQL 在其中承担了什么角色呢?
这里先抛出结论SQL 是所有数据库的“核心”,即使它们声称对 SQL 说“No”。怎么理解呢现在让我们沿着数据库发展的脉络来解释并逐步验证这个观点。
SQL 的黄金年代
先抛出一个简单的定义SQL 数据库就是一种支持 SQL 的数据库,它是一种用于查询和处理关系数据库中“数据”的特定领域语言。关系数据库中的“关系”是指由 IBM 研究人员 E.F. Codd 在 20 世纪 70 年代初设计的数据管理的“关系模型”,并在 System R 及后续许多数据库系统中得到了普及。
那么 SQL 与关系型数据库有哪些优缺点呢?
先来谈谈优点:由于 Schema模式的预定义数据库获得存储相对紧凑从而导致其性能较为优异之后就是经典的 ACID 给业务带来了可控性,而基于标准化 SQL 的数据访问模式给企业级应用带来了更多的红利,因为“标准即是生产力”。
它的缺点是:对前期设计要求高,因为后期修改 Schema 往往需要停机,没有考虑分布式场景,在扩展性和可用性层面缺乏支持;而分布式是 21 世纪应用必备的技能,请你留意此处,这就是区分新老数据库的重要切入点。
自 20 世纪 70 年代末以来SQL 和关系型数据库一直是行业标准。大多数流行的“企业”系统都是 System R 的直接后代,继承了 SQL 作为其查询语言。SQL 的意义是提供了一套结构化数据的访问标准,它是脱离特定厂商束缚的客观标准,虽然不同数据库都会对标准 SQL 进行扩充和改造,但是最为常用的部分还是与最初设计保持一致。
随着 SQL 的发展,它被广泛使用在各种商业、开源数据库中。长期的生产实践与其本身优秀的设计产生了美妙的化学作用,从而生发出如下两个现象。
1. 群众基础优秀
由于 SQL 被广泛地使用,于是形成了一类可以熟练使用该技术的人群,该人群数量众多,其中不仅包含了研发技术人员,一些其他行业人员,如财务、物流和数据分析等,都以掌握 SQL 作为从业必备技能。所以说 SQL 的群众基础相当深厚。
2. 应用生态丰富
SQL 客观上并不利于程序开发,这是由于应用系统需要编写大量的原始代码与 SQL 系统进行交互,从而形成了一个客观上存在的数据访问层,该层增加了系统复杂度,对维护造成了负面的影响。
针对这个问题,应用系统内往往会引入抽象层来屏蔽其数据访问层的复杂度,从而使业务研发人员能够更轻松地使用 SQL 类数据库。从数据访问框架、ORM到数据库中间件一大波该类技术组件频频进入人们的视野从而构建出了极为丰富的生态。
以上两点相互作用,共同打造了 SQL 与关系型数据库的黄金年代。在其巅峰时期,几乎所有类型的应用都需要与数据库打交道,甚至有人戏称这是“面向数据库编程”。但随着互联网的悄然崛起,情况慢慢地发生了变化。
NoSQL 破土而出
NoSQL 数据库大概是在 2009 年被开发出来的,是一种非关系型数据库。它专注于分布式场景下数据存储与查询,不需要预先定义 Schema一般不支持连接且易于扩展。开发人员通常被允许频繁地在线更改 Schema从而更容易地实现业务需求。
NoSQL 数据库因具有庞大的数据存储需求,常被用于大数据和 C 端互联网应用。例如Twitter、Facebook、阿里和腾讯这样的公司每天都利用其收集几十甚至上百 TB 的用户数据。
那么 NoSQL 数据库与 SQL 数据库的区别表现在哪呢?如下表所示。
表 NoSQL 数据库与 SQL 数据库的区别
NoSQL 除了不是 SQL 外,另外一个广泛的解释是 Not Only SQL。其背后暗含我们没有 SQL但是有一项比 SQL 要吸引人的东西,那就是——分布式。
在 NoSQL 出现之前的商业数据库,多节点部署的难度很大且费用高昂,甚至需要使用专用的硬件。虽然理论上规模应该足够大,但其实不然。而后出现的 NoSQL大部分在设计层面天然考虑了使用廉价硬件进行系统扩容同时由于其放弃了 ACID性能才没有随着系统规模的扩大而衰减。
当然 NoSQL 的缺点也比较明显:由于缺乏 ACID应用时需要非常小心地处理数据一致性问题同时由于其数据模型往往只针对特定场景一般不能使用一种 NoSQL 数据库来完成整个应用的构建,导致设计层面的复杂和维护的困难。
当我们审视 NoSQL 数据库时会发现一个有趣的事实它们之间最大的共同点其实是没有任何共同点而“No”就成为它们的最大公约数。从而我有理由怀疑NoSQL 本质上是一个为了做宣传而创造的概念——它将一种新鲜的事物打造为一个反传统、反权威的形象,从而达到宣传的目的。
由此NoSQL 的概念大于其内涵,虽然招致了很多批评的声音,但其意义却也是重大的。我认为主要体现在以下 3 个方面。
第一,打破了固有思维
有许多应用其实使用数据库是非常烦琐的,比如互联网场景下的社交应用,它要处理大量非结构化场景。该场景其实可以使用关系型数据库实现,但却需要设计高扩展性的应用来支撑该场景,同时需要有丰富经验的 DBA 来配合,基于这两点才能使系统稳定运行。
使用 MongoDB 可以很好地解决场景问题,简化研发,在一定数量级的访问下,可以实现平滑的系统扩展,减少运维压力。这给当年资金有限的互联网公司,特别是创业公司带来了新的选择。同时也能看到,并不是任何系统都要面向关系型数据库、面向 SQL。可以说 NoSQL 一举打破了整个行业的桎梏,让技术回归人性,回归了本心。
第二,打破了大公司的垄断
当时整个行业都在诸如 Oracle、IBM 等大型数据库服务商的控制之下大部分商业场景都可以看到它们的身影。而新兴互联网行业以解决实际问题出发心中的束缚少步子可以迈得大。通过反复试错和迭代NoSQL 门类中有多种数据库得到了验证,从而在真实的商业场景中发挥了作用。
这种趋势在一定程度上打破了垄断,使行业生机勃勃,更加倒逼大型数据库服务商加快了前进的脚步,从而获得一个多赢的结果。
第三,将分布式引入到数据库中
从那之后,分布式数据库的概念开始流行,甚至整个技术圈都无法回避“分布式数据”这一理念,进而催生出我们后续要介绍的 NewSQL 等类型。
NoSQL 作为一个宣传手段,向我们揭示了那一代创新数据存储的窘境:它们其实与 SQL 相去甚远但不得不与其发生深刻的关系。从而证明了我一开始给出的论断NoSQL 数据库们的唯一核心与共同点其实就是 SQL。
但近十年来,随着 NoSQL 的发展,其中部分数据库已经摆脱了 SQL 的阴影,如 Elasticsearch、Redis 等。谈到它们的时候,人们往往不会将其与 NoSQL 概念联系起来,显然,它们已经得到了时间的认可,最终为自己正名。
NewSQL 的进击
人们常常批评 NoSQL“为了倒掉洗澡水却把婴儿一起冲进了下水道”Throwing the baby out with the bathwater。SQL 类数据库应用如此广泛,为了分布式特性就需要抛弃 SQL 显得非常得不偿失。
因此一些组织开始构建基于 SQL 的分布式数据库,从表面看它们都支持 SQL但是根据实现方式其发展出了两种路线NewSQL 和 Distributed SQL。这一讲我先介绍前者。
NewSQL 是基于 NoSQL 模式构建的分布式数据库,它通常采用现有的 SQL 类关系型数据库为底层存储或自研引擎并在此之上加入分布式系统从而对终端用户屏蔽了分布式管理的细节。Citus 和 Vitess 就是此种类型的两个著名案例,在后面的第四个模块中,我会具体介绍。
此外,一些数据库中间件,如 MyCAT、Apache ShardingShpere由于其完全暴露了底层的关系型数据库因此不能将它们称为 NewSQL 数据库,不过可以作为此种模式的另类代表。
大概在 2010 年年初的时候人们尝试构建此类数据库。而后451 ResEArch 的 Matthew Aslett 于 2011 年创造了“NewSQL”这个术语用于对这些新的“可扩展” SQL 数据库进行定义。
NewSQL 数据库一般有两种。
第一种是在一个个独立运行的 SQL 数据库实例之上提供了一个自动数据分片管理层。例如Vitess 使用了 MySQL而 Citus 使用 PostgreSQL。由于每个独立实例仍然是单机关系型数据库因此一些关键特性无法得到完美支持如本地故障转移 / 修复,以及跨越分片的分布式事务等。更糟糕的是,甚至一些单机数据库的功能也无法进行使用,如 Vitess 只能支持子查询的一个“子集”。
第二种包括 NuoDB、VoltDB 和 Clustrix 等,它们构建了新的分布式存储引擎,虽然仍有或多或少的功能阉割,但可以给用户一个完整的 SQL 数据库体验。
NewSQL 数据库最初构建的目的是解决分布式场景下,写入 SQL 数据库所面临的挑战。它可以使用多个传统单机 SQL 数据库作为其存储节点,在此基础上构建起可扩展的分布式数据库。在它产生的年代,云技术还处于起步阶段,因此这类 NewSQL 得到了一定程度的发展。但是,随着多可用区、多区域和多云的云部署成为现代应用程序的标准,这些数据库也开始力不从心起来。
与此同时,像 Google Spanner 和 TiDB 这样的 Distributed SQL 数据库的崛起NewSQL 数据库的地位就受到了进一步挑战。因为后者是被设计利用云组价的特性,并适应在不可靠基础设施中稳定运行的“云原生”数据库。
可以看到 NewSQL 回归了以 SQL 为核心的状态,这次回归展示了 SQL 的魅力,即可以穿越数十年时光。但这次革命是不彻底的,我们可以看到传统单机数据库的身影,还有对 SQL 功能的阉割。而革命者本身也往往来自应用领域而不是专业数据库机构。所以NewSQL 更像是用户侧的狂欢,它可以解决一类问题,但并不完备,需要小心地评估和使用。
Distributed SQL 的崛起
上面我也提到过 Distributed SQL 数据库,此种使用的是特殊的底层存储引擎,来构建水平可伸缩的数据库。它在 NewSQL 的功能基础上往往提供的是“地理分布”功能用户可以跨可用区、区域甚至在全球范围内分布数据。CockroachDB、Google的Spanner、OceanBase 和 PingCAP 的 TiDB 就是很好的例子,这些引擎通常比 NewSQL 的目标更高。
但需要强调的是NoSQL 和 NewSQL 是建立在一个假设上,即构建一个完备功能的分布式数据库代价是高昂的,需要进行某种妥协。而商用 Distributed SQL 数据库的目标恰恰是要以合理的成本构建这样一种数据库,可以看到它们的理念是针锋相对的。
相比于典型的 NewSQL一个 Distributed SQL 数据库看起来更像一个完整的解决方案。它的功能一般包括可扩展性、数据一致性、高可用性、地理级分布和 SQL 支持,它们并非一些工具的组合。一个合格的 Distributed SQL 数据库应该不需要额外工具的支持,就可以实现上述功能。
此外,由于 Distributed SQL 天然适合与云计算相结合,因此一些云原生数据库也可以归为此门类,如 AWS 的 Aurora 等。不论是云还是非云数据库Distributed SQL 几乎都是商业数据库,而 NewSQL 由于其工具的本质,其中开源类型的数据库占有不小的比重。
这一方面反映了 Distributed SQL 非常有潜力且具有商业价值,同时也从一个侧面说明了它才是黄金年代 SQL 关系型数据库最为正统的传承者。
新一代的 SQL 已经冉冉升起,它来自旧时代。但除了 SQL 这一个面孔外,其内部依然发生了翻天覆地的改变。不过这正是 SQL 的魅力:穿越时光,依然为数据库的核心,也是数据库经典理论为数不多的遗产。
总结
这一讲到这里就告一段落了,我们一起回顾了数据库与 SQL 的前世今生,了解了当今分布式数据库与 SQL 之间的关系,搞清楚了这些纷繁复杂的 SQL 蕴含的意义。
SQL 是在 20 世纪 70 年代被关系型数据库所引入,在随后的几十年里一直被看作是数据库标准的查询接口,从而形成了深厚的群众基础。而后 2000 年左右出现的 NoSQL 潮流,本质上与 SQL 没有实际联系,但讽刺的是,它们不得不依靠 SQL 这个“对手”来定义自身的价值,从而使我们感叹 SQL 那顽强的生命力。又随着近十年 NewSQL 和 Distributed SQL 的发展SQL 回归本源,从旧时代的霸主摇身变为新时代的先锋。
SQL 在这漫长的时间内当然不是一成不变的,甚至可以说当今 SQL 已经与最早版本天差地别。但其核心理念未有异化,所以我们还是称其为 SQL 而不是给它新的名字。
那么通过这一讲的回顾,我们确信,任何成功的数据库都需要与 SQL 产生天然联系,而 SQL 美妙的设计,也将帮助新一代的分布式数据库乘风破浪。
教学相长
学习完这一讲的内容我希望你思考这样一个问题MySQL 8.0 引入的 InnoDB Cluster 应该被归类到哪种类型的分布式数据库呢?

View File

@ -0,0 +1,211 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 数据分片:如何存储超大规模的数据?
前两讲我们介绍了分布式数据库,以及各种 SQL 的发展脉络,那么从这一讲开始,我们就正式进入分布式数据库核心原理的学习。
随着互联网时代,特别是移动互联网的到来,形形色色的企业都在将自己的系统平台快速升级迭代,以此作为向互联网转型的一部分。
在此背景下,这类应用平台所依赖的数据库系统就需要支持突然增加的巨量交易数据,但是在这种情况下单体的数据库往往会很快过载,而用于扩展数据库最常见的技术手段就是“数据分片”。
因此这一讲,我将为你介绍什么是分片,以及如何将其用于扩展数据库。同时,我还会回顾常见分片架构的优缺点,以使用 TiDB 为例,和你探讨如何在分布式数据库中实现分片。
数据分片概论
分片是将大数据表分解为较小的表(称为分片)的过程,这些分片分布在多个数据库集群节点上。分片本质上可以被看作传统数据库中的分区表,是一种水平扩展手段。每个分片上包含原有总数据集的一个子集,从而可以将总负载分散在各个分区之上。
数据分片的方式一般有两种。
水平分片:在不同的数据库节点中存储同一表的不同行。
垂直分片:在不同的数据库节点中存储表不同的表列。
如下图所示,水平和垂直这两个概念来自原关系型数据库表模式的可视化直观视图。
图 1 可视化直观视图
分片理念其实来源于经济学的边际收益理论:如果投资持续增加,但收益的增幅开始下降时,被称为边际收益递减状态。而刚好要开始下降的那个点被称为边际平衡点。
该理论应用在数据库计算能力上往往被表述为:如果数据库处理能力遇到瓶颈,最简单的方式是持续提高系统性能,如更换更强劲的 CPU、更大内存等这种模式被称为垂直扩展。当持续增加资源以提升数据库能力时垂直扩展有其自身的限制最终达到边际平衡收益开始递减。
而此时,对表进行水平分片意味着可以引入更多的计算能力处理数据与交易。从而,将边际递减扭转为边际递增状态。同时,通过持续地平衡所有节点上的处理负载和数据量,分片模式还可以获得 1+1>2 的效果,即集群平均处理能力大于单节点处理能力。
这样就使得规模较小、价格便宜的服务器组成的水平扩展集群,可能比维护一台大型商用数据库服务器更具成本效益。这也是第一讲中“去 IOE 运动”的核心技术背景。
除了解决扩展难题,分片还可以缓解计划外停机,大大降低系统 RTO目标恢复时间。即使在计划内的停机期如果没有分片的加持数据库整体上还是处于不可访问状态的这就无法满足业务上对 SLO目标服务级别的要求。
如果分片可以如我们所希望的那样正常工作,它就可以确保系统的高可用。即使数据库集群部分节点发生故障,只要其他节点在其中运行,数据库整体仍可对外提供服务。当然,这还需要复制与一致性服务的保证,我们会在之后课时中进一步探讨。
总而言之,分片可以增加数据库集群的总容量并加快处理速度,同时可以使用比垂直扩展更低的成本提供更高的可用性。
分片算法
分片算法一般指代水平分片所需要的算法。经过多年的演化,其已经在大型系统中得到了广泛的实践。下面我将介绍两种最常见的水平分片算法,并简要介绍一些其他的分片算法优化思路。
哈希分片
哈希分片,首先需要获取分片键,然后根据特定的哈希算法计算它的哈希值,最后使用哈希值确定数据应被放置在哪个分片中。数据库一般对所有数据使用统一的哈希算法(例如 ketama以促成哈希函数在服务器之间均匀地分配数据从而降低了数据不均衡所带来的热点风险。通过这种方法数据不太可能放在同一分片上从而使数据被随机分散开。
这种算法非常适合随机读写的场景,能够很好地分散系统负载,但弊端是不利于范围扫描查询操作。下图是这一算法的工作原理。
图 2 哈希分片
范围分片
范围分片根据数据值或键空间的范围对数据进行划分,相邻的分片键更有可能落入相同的分片上。每行数据不像哈希分片那样需要进行转换,实际上它们只是简单地被分类到不同的分片上。下图是范围分片的工作原理。
图 3 范围分片
范围分片需要选择合适的分片键,这些分片键需要尽量不包含重复数值,也就是其候选数值尽可能地离散。同时数据不要单调递增或递减,否则,数据不能很好地在集群中离散,从而造成热点。
范围分片非常适合进行范围查找,但是其随机读写性能偏弱。
融合算法
这时我们应该意识到,以上介绍的哈希和范围的分片算法并不是非此即彼,二选一的。相反,我们可以灵活地组合它们。
例如,我们可以建立一个多级分片策略,该策略在最上层使用哈希算法,而在每个基于哈希的分片单元中,数据将按顺序存储。
这个算法相对比较简单且灵活,下面我们再说一个地理位置算法。
地理位置算法
该算法一般用于 NewSQL 数据库,提供全球范围内分布数据的能力。
在基于地理位置的分片算法中,数据被映射到特定的分片,而这些分片又被映射到特定区域以及这些区域中的节点。
然后在给定区域内,使用哈希或范围分片对数据进行分片。例如,在美国、中国和日本的 3 个区域中运行的集群可以依靠 User 表的 Country_Code 列将特定用户User所在的数据行映射到符合位置就近规则的区域中。
那么以上就是几种典型的分片算法,下面我们接着讨论如何将分片算法应用到实际的场景中。
手动分片 vs 自动分片
手动分片,顾名思义,就是设置静态规则来将数据根据分片算法分散到数据库节点。这一般是由于用户使用的数据库不支持自动的分片,如 MySQL、Oracle 等。这个问题可以在应用层面上做数据分片来解决,也可以使用简单的数据库中间件或 Proxy 来设置静态的分片规则来解决。
手动分片的缺点是数据分布不均匀。数据分布不均可能导致数据库负载极其不平衡,从而使其中一些节点过载,而另一些节点访问量较少。
因此,最好避免在部分节点上存储过多数据,否则会造成这些节点成为访问热点,进而导致其运行速度降低,甚至使服务器崩溃。此外,当整体数据集过小时,也会导致这个问题,因为集群中只有部分节点才有数据。
这在开发和测试环境中是可以接受的,但在生产环境中是不可以接受的。因为数据分布不均,热点以及将数据存储在太少的分片上,都会导致数据库集群内的节点计算资源耗尽,造成系统不稳定。
但如果精心设计,且数据分布变化不大,采用手动分片也是一个较为简单、维护成本低廉的方案。
而使用自动分片意味着计算节点与分片算法可以相互配合,从而使数据库进行弹性伸缩。
使用基于范围的分片很容易实现自动分片:只需拆分或合并每个分片。
假设现在有一个范围为 [1100的分片我们想要将它分裂为两个范围先选择 50 作为切分点;然后将该区域分为 [150和 [50100之后将两个区域移动到两台不同的数据库节点中从而使系统负载达到平衡。
基于范围的分片可能会带来读取和写入热点,我们可以通过拆分和移动分片消除这些热点。
而使用基于哈希的分片的系统实现自动分片代价很高昂。我们现在使用上面图 1 中的例子来说明。
当前系统有 4 个节点,然后添加一个新的数据库节点。在哈希函数中,“ n”从 4 更改为 5这会导致较大的系统抖动。尽管你可以使用像 Ketama 这样的一致性哈希算法来尽可能减少系统抖动,但数据迁移与再平衡操作还是必须要有的。
这是因为在应用哈希函数后,数据是随机分布的,并且调整散列算法肯定会更改大多数数据的分布情况。
自动分片是分布式数据库的主流功能,所有主要的分布式数据库,甚至数据库中间件都在尝试自动分片。下面我将结合几个案例来说明。
分片算法案例
数据分片是数据库中间件的核心功能,且该领域开源项目较多。我这里以 Apache ShardingShpere 的分片内容为例,向你介绍分片算法的相关实践案例。
分片键生成
ShardingShpere 首先提供了分布式的主键生成,这是生成分片键的关键。由于分布式数据库内一般由多个数据库节点参与,因此基于数据库实例的主键生成并不适合分布式场景。
常用的算法有 UUID 和 Snowfalke 两种无状态生成算法。
UUID 是最简单的方式,但是生成效率不高,且数据离散度一般。因此目前生产环境中会采用后一种算法。下图就是用该算法生成的分片键的结构。
图 4 分片键结构
其中有效部分有三个。
时间戳:算法类似 UNIX 时间的表示形式,它是从一个特定时间开始到当前时间点之间的毫秒数,本案例中该算法可以使用近 70 年。
工作节点 ID保证每个独立工作的数据库节点不会产生重复的数据。
访问序列:在同一个进程、同一个毫秒内,保证产生的 ID 不重复。
灵活的分片算法
为了保证分片计算的灵活性ShardingShpere 提供了标准分片算法和一些工具,帮助用户实现个性化算法。
PreciseShardingAlgorithm 配合哈希函数使用可以实现哈希分片。RangeShardingAlogrithm 可以实现范围分片。
使用 ComplexShardingStrategy 可以使用多个分片键来实现融合分片算法。
有的时候,数据表的分片模式不是完全一致。对于一些特别的分片模式,可以使用 HintShardingStrategy 在运行态制定特殊的路由规则,而不必使用统一的分片配置。
如果用户希望实现诸如地理位置算法等特殊的分片算法,可以自定义分片策略。使用 inline 表达式或 Java 代码进行编写,前者基于配置不需要编译,适合简单的个性化分片计算;后者可以实现更加复杂的计算,但需要编译打包的过程。
用户通过以上多种分片工具,可以灵活和统一地制定数据库分片策略。
自动分片
ShardingShpere 提供了 Sharding-Scale 来支持数据库节点弹性伸缩,该功能就是其对自动分片的支持。下图是自动分片功能展示图,可以看到经过 Sharding-Scale 的特性伸缩,原有的两个数据库扩充为三个。
图 5 自动分片功能展示
自动分片包含下图所示的四个过程。
图 6 自动分片过程
从图 6 中可以看到通过该工作量ShardingShpere 可以支持复杂的基于哈希的自动分片。同时我们也应该看到,没有专业和自动化的弹性扩缩容工具,想要实现自动化分片是非常困难的。
以上就是分片算法的实际案例,使用的是经典的水平分片模式。而目前水平和垂直分片有进一步合并的趋势,下面要介绍的 TiDB 正代表着这种融合趋势。
垂直与水平分片融合案例
TiDB 就是一个垂直与水平分片融合的典型案例,同时该方案也是 HATP 融合方案。
其中水平扩展依赖于底层的 TiKV如下图所示。
图 7 TiKV
TiKV 使用范围分片的模式,数据被分配到 Region 组里面。一个分组保持三个副本这保证了高可用性相关内容会在“05 | 一致性与 CAP 模型:为什么需要分布式一致性?”中详细介绍)。当 Region 变大后,会被拆分,新分裂的 Region 也会产生多个副本。
TiDB 的水平扩展依赖于 TiFlash如下图所示。
图 8 TiFlash
从图 8 中可以看到 TiFlash 是 TiKV 的列扩展插件,数据异步从 TiKV 里面复制到 TiFlash而后进行列转换其中要使用 MVCC 技术来保证数据的一致性。
上文所述的 Region 会增加一个新的异步副本,而后该副本进行了数据切分,并以列模式组合到 TiFlash 中,从而达到了水平和垂直扩展在同一个数据库的融合。这是两种数据库引擎的融合。
以上的融合为 TiDB 带来的益处主要体现在查询层面特别对特定列做聚合查询的效率很高。TiDB 可以很智能地切换以上两种分片引擎,从而达到最优的查询效率。
总结
这一讲到这里就告一段落了。先是详细介绍了分片的原理,以及多种常用的分片技术;而后分析了手动分片与自动分片的区别,要知道数据分片的未来是属于自动分片的。
最后,我通过两个著名的开源项目介绍了分片技术是如何应用到分布式数据库中的。其中 TiDB 所展示的 HATP 融合两个分片模式的技术路线,可以被看作是未来分片模式发展的趋势。
教学相长
这里给你留一个课后思考题。
设计一个复杂分片算法,可以在一段时间内扩展节点不必迁移数据,同时保证不产生热点。

View File

@ -0,0 +1,258 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 数据复制:如何保证数据在分布式场景下的高可用?
我们上一讲介绍了分片技术,它主要的目的是提高数据容量和性能。这一讲,我们将介绍分布式数据库另外一个重要根基:复制。
复制的主要目的是在几个不同的数据库节点上保留相同数据的副本,从而提供一种数据冗余。这份冗余的数据可以提高数据查询性能,而更重要的是保证数据库的可用性。
本讲主要介绍两种复制模式:单主复制与多主复制,并通过 MySQL 复制技术的演化来进行相应的展示。
现在让我们开始学习单主复制,其中不仅介绍了该技术本身,也涉及了一些复制领域的话题,如复制延迟、高可用和复制方式等。
单主复制
单主复制,也称主从复制。写入主节点的数据都需要复制到从节点,即存储数据库副本的节点。当客户要写入数据库时,他们必须将请求发送给主节点,而后主节点将这些数据转换为复制日志或修改数据流发送给其所有从节点。从使用者的角度来看,从节点都是只读的。下图就是经典的主从复制架构。
这种模式是最早发展起来的复制模式,不仅被广泛应用在传统数据库中,如 PostgreSQL、MySQL、Oracle、SQL Server它也被广泛应用在一些分布式数据库中如 MongoDB、RethinkDB 和 Redis 等。
那么接下来,我们就从复制同步模式、复制延迟、复制与高可用性以及复制方式几个方面来具体说说这个概念。
复制同步模式
复制是一个非常耗费时间而且很难预测完成情况的操作。虽然其受影响的因素众多,但一个复制操作是同步发生还是异步发生,被认为是极为重要的影响因素,可以从以下三点来分析。
同步复制:如果由于从库已崩溃,存在网络故障或其他原因而没有响应,则主库也无法写入该数据。
半同步复制:其中部分从库进行同步复制,而其他从库进行异步复制。也就是,如果其中一个从库同步确认,主库可以写入该数据。
异步复制:不管从库的复制情况如何,主库可以写入该数据。而此时,如果主库失效,那么还未同步到从库的数据就会丢失。
可以看到不同的同步模式是在性能和一致性上做平衡,三种模式对应不同场景,并没有好坏差异。用户需要根据自己的业务场景来设置不同的同步模式。
复制延迟
如果我们想提高数据库的查询能力,最简便的方式是向数据库集群内添加足够多的从节点。这些从节点都是只读节点,故查询请求可以很好地在这些节点分散开。
但是如果使用同步复制,每次写入都需要同步所有从节点,会造成一部分从节点已经有数据,但是主节点还没写入数据。而异步复制的问题是从节点的数据可能不是最新的。
以上这些问题被称为“复制延迟”,在一般的材料中,我们会听到诸如“写后读”“读单增”等名词来解决复制延迟。但是这些概念其实是数据一致性模型的范畴。我将会在下一讲中深入介绍它们。
复制与高可用性
高可用High availablity是一个 IT 术语,指系统无中断地执行其功能的能力。系统中的任何节点都可能由于各种出其不意的故障而造成计划外停机;同时为了要维护系统,我们也需要一些计划内的停机。采用主从模式的数据库,可以防止单一节点挂起导致的可用性降低的问题。
系统可用程度一般使用小数点后面多个 9 的形式,如下表所示。
可用性
年故障时间
99.9999%
32秒
99.999%
5分15秒
99.99%
52分34秒
99.9%
8小时46分
99%
3天15小时36分
一般的生产系统都会至少有两个 9 的保证,追求三个 9。想要做到 4 个 9 是非常最具有挑战的。
在主从模式下,为了支撑高可用,就需要进行故障处理。我这里总结了两种可能的故障及其处理方案。
从节点故障。由于每个节点都复制了从主库那里收到的数据更改日志,因此它知道在发生故障之前已处理的最后一个事务,由此可以凭借此信息从主节点或其他从节点那里恢复自己的数据。
主节点故障。在这种情况下,需要在从节点中选择一个成为新的主节点,此过程称为故障转移,可以手动或自动触发。其典型过程为:第一步根据超时时间确定主节点离线;第二步选择新的主节点,这里注意新的主节点通常应该与旧的主节点数据最为接近;第三步是重置系统,让它成为新的主节点。
复制方式
为了灵活并高效地复制数据,下面我介绍几种常用的复制方式。
1. 基于语句的复制
主库记录它所执行的每个写请求(一般以 SQL 语句形式保存),每个从库解析并执行该语句,就像从客户端收到该语句一样。但这种复制会有一些潜在问题,如语句使用了获取当前时间的函数,复制后会在不同数据节点上产生不同的值。
另外如自增列、触发器、存储过程和函数都可能在复制后产生意想不到的问题。但可以通过预处理规避这些问题。使用该复制方式的分布式数据库有 VoltDB、Calvin。
2. 日志WAL同步
WAL 是一组字节序列,其中包含对数据库的所有写操作。它的内容是一组低级操作,如向磁盘的某个页面的某个数据块写入一段二进制数据,主库通过网络将这样的数据发送给从库。
这种方法避免了上面提到的语句中部分操作复制后产生的一些副作用但要求主从的数据库引擎完全一致最好版本也要一致。如果要升级从库版本那么就需要计划外停机。PostgreSQL 和 Oracle 中使用了此方法。
3. 行复制
它由一系列记录组成,这些记录描述了以行的粒度对数据库表进行的写操作。它与特定存储引擎解耦,并且第三方应用可以很容易解析其数据格式。
4. ETL 工具
该功能一般是最灵活的方式。用户可以根据自己的业务来设计复制的范围和机制,同时在复制过程中还可以进行如过滤、转换和压缩等操作。但性能一般较低,故适合处理子数据集的场景。
关于单主复制就介绍到这里,下面我们再来说说多主复制。
多主复制
也称为主主复制。数据库集群内存在多个对等的主节点,它们可以同时接受写入。每个主节点同时充当主节点的从节点。
多主节点的架构模式最早来源于 DistributedSQL 这一类多数据中心,跨地域的分布式数据库。在这样的物理空间相距甚远,有多个数据中心参与的集群中,每个数据中心内都有一个主节点。而在每个数据中心的内部,却是采用常规的单主复制模式。
这么设计该类系统的目的在于以下几点。
获得更好的写入性能:使数据可以就近写入。
数据中心级别的高可用:每个数据中心可以独立于其他数据中心继续运行。
更好的数据访问性能:用户可以访问到距离他最近的数据中心。
但是,此方法的最大缺点是,存在一种可能性,即两个不同的主节点同时修改相同的数据。这其实是非常危险的操作,应尽可能避免。这就需要下一讲要介绍的一致性模型,配合冲突解决机制来规避。
还有一种情况是处理客户端离线操作的一致性问题。为了提高性能,数据库客户端往往会缓存一定的写入操作,而后批量发送给服务端。这种情况非常类似于大家使用协作办公文档工具的场景。在这种情况下,每个客户端都可以被看作是具有主节点属性的本地数据库,并且多个客户端之间存在一种异步的多主节点复制的过程。这就需要数据库可以协调写操作,并处理可能的数据冲突。
典型的多主复制产品有 MySQL 的 Tungsten Replicator、PostgreSQL 的 BDR 和 Oracle 的 GoldenGate。
目前,大部分 NewSQL、DistributedSQL 的分布式数据库都支持多主复制,但是大部分是用 Paxos 或 Raft 等协议来构建复制组,保证写入线性一致或顺序一致性;同时传统数据库如 MySQL 的 MGR 方案也是使用类似的方式,可以看到该方案是多主复制的发展方向。关于一致性协议的内容我们将在后续课程中详细介绍。
历史的发展潮流是从单主复制向多主复制演变的,以上我们抽象地总结了复制的发展模式和需要关注的技术点。下面我将通过 MySQL 高可用技术的发展路径,向你直观地展示数据库复制技术的发展脉络。
MySQL 复制技术的发展
MySQL 由于其单机机能的限制很早就发展了数据复制技术以提高性能。同时依赖该技术MySQL 可用性也得到了长足的发展。
截止到现在,该技术经历了四代的发展。第一代为传统复制,使用 MHAMaster High Available架构第二代是基于 GTID 的复制,即 GTID+Binlog server 的模式第三代为增强半同步复制GTID+增强半同步复制;第四代为 MySQL 原生高可用,即 MySQL InnoDB Cluster。
数据库的复制技术需要考虑两个因素:数据一致 RPO 和业务连续性 RTO。所以就像前面的内容所强调的复制与一致性是一对如影随形的概念本讲内容聚焦于复制但是会提到关于一致性相关的概念。
下面我就从第一代复制技术开始说起。
MHA 复制控制
下图是 MHA 架构图。
MHA 作为第一代复制架构,有如下适用场景:
MySQL 的版本≤5.5,这一点说明它很古老;
只用于异步复制且一主多从环境;
基于传统复制的高可用。
MHA 尽最大能力做数据补偿,但并不保证一定可以成功;它也尽最大努力在实现 RPO有 RTO 概念支持。可以看到它只是一个辅助工具,本身的架构与机制对 RPO 和 RTO 没有任何保障。
那么由此可知,它会存在如下几个问题:
它的 GTID 模型强依赖 binlog server但是对于 5.7 后的 binlog 却不能识别,同时对并行复制支持不好;
服务 IP 切换依赖自行编写的脚本,也可以与 DNS 结合,其运维效果取决于运维人员的经验;
运维上需要做 SSH 信任、切换判断等人工操作,总体上处于“刀耕火种”的状态,自动化程度较低,维护难度高;
现在项目基本无维护。
从上述问题中可以看到MHA 作为第一代复制架构,功能相对原始,但已经为复制技术的发展开辟了道路,特别是对 GTID 和 binlog 的应用。但如果不是维护比较古老的 MySQL 集群,目前已经不推荐采用它了。
半同步复制
这是第二代复制技术,它与第一代技术的差别表现在以下几点。
binlog 使用半同步,而第一代是异步同步。它保障了数据安全,一般至少要同步两个节点,保证数据的 RPO。
同时保留异步复制,保障了复制性能。并通过监控复制的延迟,保证了 RTO。
引入配置中心,如 consul。对外提供健康的 MySQL 服务。
这一代开始需要支持跨 IDC 复制。需要引入监控 Monitor配合 consul 注册中心。多个 IDC 中 Monitor 组成分布式监控,把健康的 MySQL 注册到 consul 中,同时将从库复制延迟情况也同步到 consul 中。
下图就是带有 consul 注册中心与监控模块的半同步复制架构图。
第二代复制技术也有自身的一些缺陷。
存在幻读的情况。当事务同步到从库但没有 ACK 时,主库发生宕机;此时主库没有该事务,而从库有。
MySQL 5.6 本身半同步 ACK 确认在 dump_thread 中dump_thread 存在 IO 瓶颈问题。
基于此,第三代复制技术诞生。
增强半同步复制
这一代需要 MySQL 是 5.7 以后的版本。有一些典型的框架来支持该技术,如 MySQL Replication Manager、GitHub-orchestrator 和国内青云开源的 Xenon 等。
这一代复制技术采用的是增强半同步。首先主从的复制都是用独立的线程来运行;其次主库采用 binlog group commit也就是组提交来提供数据库的写入性能而从库采用并行复制它是基于事务的通过数据参数调整线程数量来提高性能。这样主库可以并行从库也可以并行。
这一代技术体系强依赖于增强半同步,利用半同步保证 RPO对于 RTO则取决于复制延迟。
下面我们用 Xenon 来举例说明,请看下图(图片来自官网)。
从图中可以看到。每个节点上都有一个独立的 agent这些 agent 利用 raft 构建一致性集群,利用 GTID 做索引选举主节点;而后主节点对外提供写服务,从节点提供读服务。
当主节点发生故障后agent 会通过 ping 发现该故障。由于 GTID 和增强半同步的加持,从节点与主节点数据是一致的,因此很容易将从节点提升为主节点。
第三代技术也有自身的缺点,如增强半同步中存在幽灵事务。这是由于数据写入 binlog 后,主库掉电。由于故障恢复流程需要从 binlog 中恢复,那么这份数据就在主库。但是如果它没有被同步到从库,就会造成从库不能切换为主库,只能去尝试恢复原崩溃的主库。
MySQL 组复制
组复制是 MySQL 提供的新一代高可用技术的重要组成。其搭配 MySQL Router 或 Proxy可以实现原生的高可用。
从这一代开始MySQL 支持多主复制,同时保留单主复制的功能。其单主高可用的原理与第三代技术类似,这里我们不做过多分析了。
现在说一下它的多主模式,原理是使用 MySQL Router 作为数据路由层,来控制读写分离。而后组内部使用 Paxos 算法构建一致性写入。
它与第三代复制技术中使用的一致性算法的作用不同。三代中我们只使用该算法来进行选主操作,数据的写入并不包含在其中;而组复制的多主技术需要 Paxos 算法深度参与,并去决定每一次数据的写入,解决写入冲突。
组复制有如下几个优点。
高可用分片:数据库节点动态添加和移除。分片实现写扩展,每个分片是一个复制组。可以结合上一讲中对于 TiDB 的介绍,原理类似。
自动化故障检测与容错:如果一个节点无法响应,组内大多数成员认为该节点已不正常,则自动隔离。
方案完整:前面介绍的方案都需要 MySQL 去搭配一系列第三方解决方案;而组复制是原生的完整方案,不需要第三方组件接入。
当然,组复制同样也有一些限制。主要集中在需要使用较新的特性,一些功能在多组复制中不支持,还有运维人员经验缺乏等。
相信随着 MySQL 的发展,将会有越来越多的系统迁移到组复制中,多主模式也会逐步去替代单主模式。
总结
这一讲内容就介绍到这里了。我们深入介绍了复制技术在分布式数据库中的作用;探讨了单主和多主两种复制技术;而后通过 MySQL 复制技术的发展路径来介绍了复制技术的应用案例。
如我在上面所描述的,复制往往需要与一致性放在一起讨论。本讲聚焦于复制,下一讲我们将详细探讨一致性问题,包括 CAP 理论与一致性模型,并带你研究它与复制的结合。
教学相长
这里给你留一个思考题:我们常听到一种叫作“无主复制”的技术,它与我们这一讲介绍的两种复制技术有什么异同?

View File

@ -0,0 +1,230 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 一致性与 CAP 模型:为什么需要分布式一致性?
上一讲我们讨论了复制的相关内容,其中有部分知识点提到了“一致性”的概念。那么这一讲我们就来聊聊 CAP 理论和一致性的相关内容。我将重点聚焦于一致性模型,因为它是复制一致性和分布式事务的理论基础。
在开始课程之前,我们先讨论一下:分布式数据库,乃至于一般的分布式系统所谈论的一致性到底是什么?
一致性是高可用的必备条件
在现实世界中,分布式数据库的节点并不总是处于活动状态且相互能够通信的。但是,以上这些故障不应该影响数据库的可用性。换言之,从用户的角度来看,整个系统必须像没有遭到任何故障一样继续运行。系统高可用性是分布式数据库一个极其重要的特性,甚至在软件工程中,我们始终致力于实现高可用性,并尽量减少停机时间。
为了使系统高度可用,系统需要被设计成允许一个或多个节点的崩溃或不可访问。为此,我们需要引入如上一讲所说的复制技术,其核心就是使用多个冗余的副本来提高系统的可用性。但是,一旦添加了这些副本,我们将面临使多个数据副本保持同步的问题,并且遭遇故障后如何恢复系统的问题。
这就是 MySQL 复制发展历程所引入的 RPO 概念,也就是系统不仅仅要可用,而且数据还需要一致。所以高可用必须要尽可能满足业务连续性和数据一致性这两个指标。
而我们马上要介绍的 CAP 理论会告诉我们还有第三个因素——网络分区会对可用性产生影响。它会告诉我们可用性和一致性在网络分区下是不能同时满足的。
CAP 理论与注意事项
首先,可用性是用于衡量系统能成功处理每个请求并作出响应的能力。可用性的定义是用户可以感知到的系统整体响应情况。但在实践中,我们希望组成系统的各个组件都可以保持可用性。
其次,我们希望每个操作都保持一致性。一致性在此定义为原子一致性或线性化一致性。线性一致可以理解为:分布式系统内,对所有相同副本上的操作历史可以被看作一个日志,且它们在日志中操作的顺序都是相同的。线性化简化了系统可能状态的计算过程,并使分布式系统看起来像在单台计算机上运行一样。
最后,我们希望在容忍网络分区的同时实现一致性和可用性。网络是十分不稳定的,它经常会分为多个互相独立的子网络。在这些子网中,节点间无法相互通信。在这些被分区的节点之间发送的某些消息,将无法到达它的目的地。
那么总结一下可用性要求任何无故障的节点都可以提供服务而一致性要求结果需要线性一致。埃里克·布鲁尔Eric Brewer提出的 CAP 理论讨论了一致性、可用性和分区容错之间的抉择。
其中提到了,异步系统是无法满足可用性要求的,并且在存在网络分区的情况下,我们无法实现同时保证可用性和一致性的系统。不过我们可以构建出,在尽最大努力保证可用性的同时,也保证强一致性的系统;或者在尽最大努力保证一致性的同时,也保证可用性的系统。
这里提到的“最大努力”意味着如果一切正常系统可以提供该特性的保证但是在网络分区的情况下允许削弱和违反这个保证。换句话说CAP 描述了一种组合性选择,也就是要有取舍。从 CAP 理论的定义,我们可以拥有以下几种系统。
CP 系统:一致且容忍分区的系统。更倾向于减少服务时间,而不是将不一致的数据提供出去。一些面向交易场景构建的 NewSQL 数据库倾向于这种策略,如 TiDB、阿里云 PolarDB、AWS Aurora 等。但是它们会生成自己的 A也就是可用性很高。
AP 系统可用且具有分区容忍性的系统。它放宽了一致性要求并允许在请求期间提供可能不一致的值。一般是列式存储NoSQL 数据库会倾向于 AP如 Apache Cassandra。但是它们会通过不同级别的一致性模式调整来提供高一致性方案。
CP 系统的场景实现思路是需要引入共识算法,需要大多数节点参与进来,才能保证一致性。如果要始终保持一致,那么在网络分区的情况下,部分节点可能不可用。
而 AP 系统只要一个副本就能启动,数据库会始终接受写入和读取服务。它可能最终会丢失数据或产生不一致的结果。这里可以使用客户端模式或 Session 模型,来提供一致性的解决方案。
使用 CAP 理论时需要注意一些限制条件。
CAP 讨论的是网络分区,而不是节点崩溃或任何其他类型的故障。这意味着网络分区后的节点都可能接受请求,从而产生不一致的现象。但是崩溃的节点将完全不受响应,不会产生上述的不一致问题。也就是说,分区后的节点并不是都会面临不一致的问题。而与之相对的,网络分区并不能包含真实场景中的所有故障。
CAP 意味着即使所有节点都在运行中我们也可能会遇到一致性问题这是因为它们之间存在连接性问题。CAP 理论常常用三角形表示,就好像我们可以任意匹配三个参数一样。然而,尽管我们可以调整可用性和一致性,但分区容忍性是我们无法实际放弃的。
如果我们选择了 CA 而放弃了 P那么当发生分区现象时为了保证 C系统需要禁止写入。也就是当有写入请求时系统不可用。这与 A 冲突了,因为 A 要求系统是可用的。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
如下图所示,其实 CA 类系统是不存在的,这里你需要特别注意。
图 1 CAP 理论
CAP 中的可用性也不同于上述的高可用性CAP 定义对请求的延迟没有任何限制。此外,与 CAP 相反,数据库的高可用性并不需要每个在线节点都可以提供服务。
CAP 里面的 C 代表线性一致,除了它以外,还有其他的一致模式,我们现在来具体介绍一下。
一致性模型
一致性模型是分布式系统的经典内容,也是入门分布式数据库的重要知识点。但很少有人知道,其实一致性模型来源于单机理论中的共享内存。
从用户的角度看,分布式数据库就像具有共享存储的单机数据库一样,节点间的通信和消息传递被隐藏到了数据库内部,这会使用户产生“分布式数据库是一种共享内存”的错觉。一个支持读取和写入操作的单个存储单元通常称为寄存器,我们可以把代表分布式数据库的共享存储看作是一组这样的寄存器。
每个读写寄存器的操作被抽象为“调用”和“完成”两个动作。如果“调用”发生后,但在“完成”之前该操作崩溃了,我们将操作定义为失败。如果一个操作的调用和完成事件都在另一个操作被调用之前发生,我们说这个操作在另一个操作之前,并且这两个操作是顺序的;否则,我们说它们是并发的。
如下图所示a是顺序操作b和 c是并发操作。
图 2 顺序操作&并发操作
多个读取或写入操作可以同时访问一个寄存器。对寄存器的读写操作不是瞬间完成的,需要一些时间,即调用和完成两个动作之间的时间。由不同进程执行的并发读/写操作不是串行的,根据寄存器在操作重叠时的行为,它们的顺序可能不同,并且可能产生不同的结果。
当我们讨论数据库一致性时,可以从两个维度来区别。
滞后性。它是数据改变的时刻与其副本接收到数据的时刻。这是上一讲所介绍的复制延迟场景一般被归类为“客户端一致性”范畴。我们将在“15 | 再谈一致性:除了 CAP 之外的一致性模型还有哪些”中进一步讨论。
顺序性。讨论的是各种操作在系统所有副本上执行的顺序状态。这是本讲一致性模型所讨论的重点。
现在我们对顺序性再做进一步的探讨。
当面对一系列读写操作时,作为人类,我们对它们的执行顺序是有一个主观判断的。甚至,对于一个单机数据而言,这些操作的顺序也是可以确定的。但是,在分布式系统中做出这种判断就不是那么容易了,因为很难知道什么时候确切地发生了什么,并且很难在整个集群中立刻同步这些操作。
为了推理操作顺序并指出真正的结果,我们必须定义一致性模型来保障顺序性。
我们怎么来理解模型中“保障”的含义呢?它是将一致性模型视为用户与数据库之间的一种约定,每个数据库副本如何做才能满足这种顺序保障?并且用户在读取和写入数据时期望得到什么?也就是说,即使数据是被并发读取和写入的,用户也可以获得某种可预测的结果。
需要注意,我们将要讨论单一对象和单一操作一致性模型,但现实的数据库事务是多步操作的,我们将在下面“事务与一致性”部分进一步讨论。
下面我按照顺序性的保障由强到弱来介绍一致性模型。
严格一致性
严格的一致性类似于不存在复制过程:任何节点的任何写入都可立即用于所有节点的后续读取。它涉及全局时钟的概念,如果任何节点在时刻 T1 处写入新数据 A则所有节点在 T2 时刻T2 满足 T2>T1都应该读到新写入的 A。
不幸的是,这只是理论模型,现实中无法实现。因为各种物理限制使分布式数据不可能一瞬间去同步这种变化。
线性一致性
线性一致性是最严格的且可实现的单对象单操作一致性模型。在这种模型下,写入的值在调用和完成之间的某个时间点可以被其他节点读取出来。且所有节点读到数据都是原子的,即不会读到数据转换的过程和中间未完成的状态。
线性一致需要满足的是,新写入的数据一旦被读取出来,那么所有后续的读操作应该能读取到这个数据。也就是说,一旦一个读取操作读到了一个值,那么后续所有读取操作都会读到这个数值或至少是“最近”的一个值。
上面的定义来自早期的论文,我将里面的关键点提炼一下,如下所示。
需要有全局时钟,来实现所谓的“最近”。因为没有全局一致的时间,两个独立进程没有相同的“最近”概念。
任何一次读取都能读到这个“最近”的值。
下面我通过一个例子来说明线性一致性。
现在有三个节点,其中一个共享变量 x 执行写操作,而第三个节点会读取到如下数值。
第一个读操作可以返回 1、2 或空(初始值,两个写操作之前的状态),因为两个写操作仍在进行中;第一次读取可以在两次写入之前,第一次写入与第二次写入之间,以及两次写入之后。
由于第一次写操作已完成,但第二次写操作尚未完成,因此第二次读操作只能返回 1 和 2。
第三次读只能返回 2因为第二次写是在第一次写之后进行的。
下图正是现象一致性的直观展示。
图 3 线性一致性
线性一致性的代价是很高昂的,甚至 CPU 都不会使用线性一致性。有并发编程经验的朋友一定知道 CAS 操作,该操作可以实现操作的线性化,是高性能并发编程的关键,它就是通过编程手段来模拟线性一致。
一个比较常见的误区是,使用一致性算法可以实现线性一致,如 Paxos 和 Raft 等。但实际是不行的,以 Raft 为例,算法只是保证了复制 Log 的线性一致,而没有描述 Log 是如何写入最终的状态机的,这就暗含状态机本身不是线性一致的。
这里推荐你阅读 TiKV 关于线性一致的实现细节,由于线性一致性价比不高,这里就不进行赘述了,我们接下来说说顺序一致性和因果一致性。
顺序一致性
由于线性一致的代价高昂,因此人们想到,既然全局时钟导致严格一致性很难实现,那么顺序一致性就是放弃了全局时钟的约束,改为分布式逻辑时钟实现。顺序一致性是指所有的进程以相同的顺序看到所有的修改。读操作未必能及时得到此前其他进程对同一数据的写更新,但是每个进程读到的该数据的不同值的顺序是一致的。
下图展示了 P1、P2 写入两个值后P3 和 P4 是如何读取的。以真实的时间衡量1 应该是在 2 之前被写入但是在顺序一致性下1 是可以被排在 2 之后的。同时,尽管 P3 已经读取值 1P4 仍然可以读取 2。但是需要注意的是这两种组合1->2 和 2 ->1P3 和 P4 从它们中选择一个并保持一致。下图正是展示了它们读取顺序的一种可能2->1。
图 4 顺序一致性
我们使用下图来进一步区分线性一致和顺序一致。
图 5 区分线性一致和顺序一致
其中,图 a 满足了顺序一致性但是不满足线性一致性。原因在于从全局时钟的观点来看P2 进程对变量 x 的读操作在 P1 进程对变量 x 的写操作之后,然而读出来的却是旧的数据。但是这个图却是满足顺序一致性,因为两个进程 P1 和 P2 的一致性并没有冲突。
图 b 满足线性一致性,因为每个读操作都读到了该变量的最新写的结果,同时两个进程看到的操作顺序与全局时钟的顺序一样。
图 c 不满足顺序一致性,因为从进程 P1 的角度看,它对变量 y 的读操作返回了结果 0。那么就是说P1 进程的对变量 y 的读操作在 P2 进程对变量 y 的写操作之前x 变量也如此。因此这个顺序不满足顺序一致性。
在实践中,你就可以使用上文提到的一致性算法来实现顺序一致。这些算法可以保证操作在每个节点都是按照一样的顺序被执行的,所以它们能保证顺序一致。
如 Google Megastore 这类系统都是使用 Paxos 算法实现了顺序一致性。也就是说在 Megastore 内部,如果有一个数据更新,所有节点都会同步更新,且操作在各个节点上执行顺序是一致的。
因果一致性
相比于顺序一致性,因果一致性的要求会低一些:它仅要求有因果关系的操作顺序是一致的,没有因果关系的操作顺序是随机的。
因果相关的要求有如下几点。
本地顺序:本进程中,事件执行的顺序即为本地因果顺序。
异地顺序:如果读操作返回的是写操作的值,那么该写操作在顺序上一定在读操作之前。
闭包传递:和时钟向量里面定义的一样,如果 a->b、b->c那么肯定也有 a->c。
那么,为什么需要因果关系,以及没有因果关系的写法如何传播?下图中,进程 P1 和 P2 进行的写操作没有因果关系,也就是最终一致性。这些操作的结果可能会在不同时间,以乱序方式传播到读取端。进程 P3 在看到 2 之前将看到值 1而 P4 将先看到 2然后看到 1。
图 6 因果一致性
而下图显示进程 P1 和 P2 进行因果相关的写操作并按其逻辑顺序传播到 P3 和 P4。因果写入除了写入数据外还需要附加一个逻辑时钟用这个时钟保证两个写入是有因果关系的。这可以防止我们遇到上面那张图所示的情况。你可以在两个图中比较一下 P3 和 P4 的历史记录。
图 7 逻辑时钟
而实现这个逻辑时钟的一种主要方式就是向量时钟。向量时钟算法利用了向量这种数据结构,将全局各个进程的逻辑时间戳广播给所有进程,每个进程发送事件时都会将当前进程已知的所有进程时间写入到一个向量中,而后进行传播。
因果一致性典型案例就是 COPS 系统,它是基于 causal+一致性模型的 KV 数据库。它定义了 dependencies操作了实现因果一致性。这对业务实现分布式数据因果关系很有帮助。另外在亚马逊 Dynamo 基于向量时钟,也实现了因果一致性。
事务隔离级别与一致性模型
现在我们谈论了一致性模型,但是它与数据库领域之中的事务有什么区别呢?我先说结论:有关系但又没有关系。
怎么理解呢?我先来论证它们之间的无关性。
ACID 和 CAP 中的“C”是都是一致性但是它们的内涵完全不同。其中 ADI 都是数据库提供的能力保障,但是 C一致性却不是它是业务层面的一种逻辑约束。
以转账这个最为经典的例子而言,甲有 100 元 RMB乙有 0 元 RMB现在甲要转给乙 30 元。那么转账前后,甲有 70乙有 30合起来还是 100。显然这只是业务层规定的逻辑约束而已。
而对于 CAP 这里的 C 上文已经有了明确说明,即线性一致性。它表示副本读取数据的即时性,也就是对“何时”能读到“正确”的数据的保证。越是即时,说明系统整体上读取数据是一致的。
那么它们之间的联系如何呢?其实就是事务的隔离性与一致模型有关联。
如果把上面线性一致的例子看作多个并行事务,你会发现它们是没有隔离性的。因为在开始和完成之间任意一点都会读取到这份数据,原因是一致性模型关心的是单一操作,而事务是由一组操作组成的。
现在我们看另外一个例子,这是展示事务缺乏一致性后所导致的问题。
图 8 事务与一致性
其中三个事务满足隔离性。可以看到 T2 读取到了 T1 入的值。但是这个系统缺乏一致性保障,造成 T3 可以读取到早于 T2 读取值之前的值,这就会造成应用的潜在 Bug。
那现在给出结论:事务隔离是描述并行事务之间的行为,而一致性是描述非并行事务之间的行为。其实广义的事务隔离应该是经典隔离理论与一致性模型的一种混合。
比如我们会在一些文献中看到如“one-copy serializability”“strong snapshot isolation”等等。前者其实是 serializability 隔离级别加上顺序一致,后者是 snapshot 隔离级别加上线性一致。
所以对分布式数据库来说,原始的隔离级别并没有舍弃,而是引入了一致性模型后,扩宽数据库隔离级别的内涵。
总结
本讲内容较长,不过已经精炼很多了。我们从高可用性入手,介绍了 CAP 理论对于分布式模型评估的影响;而后重点介绍了一致性模型,这是本讲的核心,用来帮助你评估分布式数据库的特性。
最后我介绍了事务隔离级别与一致性模型之间的区别与联系,帮助你认清分布式数据库下的事务隔离级别的概念。

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 日志型存储:为什么选择它作为底层存储?
在上一讲中,我们学习了存储引擎的逻辑概念与架构。这些概念和架构都是总结了数个存储引擎的特点后,勾勒出的高度抽象的形象。目的是帮助你对数据库存储引擎,特别是分布式数据库存储引擎有一个总体认识,从而建立起一个知识体系。
但是只有高度抽象的内容而没有具体的案例对于理解相关概念是远远不够的。这一讲我将以经典日志合并树LSM 树)——这个典型的日志型存储引擎为切入点,为你直观展示存储引擎的设计特点;同时我会解释为什么此类存储引擎特别适合于分布式数据库。
那么,我们首先开始介绍 LSM 树的结构特点。
LSM 树的结构
LSM 树存储引擎的结构暗含在它的名字内。LS 代表日志结构,说明它是以日志形式来存储数据的,那么日志有什么特点呢?如果你对财务记账有些了解的话,会知道会计在删除一笔记录时,是不会直接拿着橡皮擦去擦掉这个记录的,而是会写一笔与原金额相等的冲抵操作。这就是典型的日志型存储的模式。
日志型存储的特点是对写入非常友好,不像 B 树等结构需要进行随机写,日志存储可以进行顺序性写。因为我们常用的 HDD 磁盘是有旋转机构的,写入延迟主要发生在磁盘旋转与读写臂的移动上。如果数据可以顺序写入,可以大大加快这种磁盘机构的写入速度。
而 M 则暗含这个结构会存在合并操作,形成最终的可读取结构。这样读取操作就不用去查找对于该记录的所有更改了,从而加快了读取速度。同时将多个记录合并为一个最终结果,也节省了存储空间。虽然合并操作有诸多优点,但是它也不是没有代价的,那就是会消耗一定的计算量和存储空间。
现在让我们开始详细介绍 LSM 树的结构。
LSM 树包含内存驻留单元和磁盘驻留单元。首先数据会写入内存的一个缓冲中,而后再写到磁盘上的不可变文件中。
内存驻留单元一般被称为 MemTable内存表是一个可变结构。它被作为一个数据暂存的缓冲使用同时对外提供读取服务。当其中的数据量到达一个阈值后数据会被批量写入磁盘中的不可变文件内。
我们看到它最主要的作用是将写入磁盘的数据进行排序同时批量写入数据可以提高写入的效率。但是数据库一旦崩溃内存中的数据会消失这个时候就需要引入“07 | 概要:什么是存储引擎,为什么需要了解它”中提到的提交日志来进行日志回放,从而恢复内存中的数据了。但前提是,数据写入内存之前,要首先被记录在提交日志中。
磁盘驻留单元,也就是数据文件,是在内存缓冲刷盘时生成的。且这些数据文件是不可变的,只能提供读取服务。而相对的,内存表同时提供读写两个服务。
关于 LSM 树的结构,一般有双树结构和多树结构两种。前者一般是一个理论说明,目前没有一个实际的存储引擎是使用这种结构的。所以我简单说一下双树概念,它有助于你去理解多树结构。
双树中的两棵树分别指:内存驻留单元和磁盘驻留单元中分别有一棵树,你可以想象它们都是 B 树结构的。刷盘的时候,内存数据与磁盘上部分数据进行合并,而后写到磁盘这棵大树中的某个节点下面。成功后,合并前的内存数据与磁盘数据会被移除。
可以看到双树操作是比较简单明了的,而且可以作为一种 B 树类的索引结构而存在。但实际上几乎没有存储引擎去使用它,主要原因是它的合并操作是同步的,也就是刷盘的时候要同步进行合并。而刷盘本身是个相对频繁的操作,这样会造成写放大,也就是会影响写入效率且会占用非常大的磁盘空间。
多树结构是在双树的基础上提出的,内存数据刷盘时不进行合并操作,而是完全把内存数据写入到单独的文件中。那这个时候另外的问题就出现了:随着刷盘的持续进行,磁盘上的文件会快速增加。这时,读取操作就需要在很多文件中去寻找记录,这样读取数据的效率会直线下降。
为了解决这个问题此种结构会引入合并操作Compaction。该操作是异步执行的它从这众多文件中选择一部分出来读取里面的内容而后进行合并最后写入一个新文件中而后老文件就被删除掉了。如下图所示这就是典型的多树结构合并操作。而这种结构也是本讲介绍的主要结构。
最后,我再为你详细介绍一下刷盘的流程。
首先定义几种角色,如下表所示。
数据首先写入当前内存表,当数据量到达阈值后,当前数据表把自身状态转换为刷盘中,并停止接受写入请求。此时会新建另一个内存表来接受写请求。刷盘完成后,由于数据在磁盘上,除了废弃内存表的数据外,还对提交日志进行截取操作。而后将新数据表设置为可以读取状态。
在合并操作开始时,将被合并的表设置为合并中状态,此时它们还可以接受读取操作。完成合并后,原表作废,新表开始启用提供读取服务。
以上就是经典的 LSM 树的结构和一些操作细节。下面我们开始介绍如何对其进行查询、更新和删除等操作。
查询、更新与删除操作
查询操作本身并没有 LSM 树的特色操作。由于目标数据可能在内存表或多个数据表中,故需要对多个数据源的结果数据进行归并操作。其中使用了排序归并操作,原因也非常简单,因为不论是内存表还是数据表,其中的数据都已经完成了排序。排序归并算法广泛应用在多种数据库中,如 Oracle、MySQL等等。另外数据库中间 Apache ShardingShpere 在处理多数据源 order by 时,也使用了这个方法。感兴趣的话你可以自行研究,这里我就不占用过多篇幅了。
而查询另外一个问题是处理同一份数据不同版本的情况,虽然合并操作可以解决部分问题,但合并前的数据还需要通过查询机制来解决。我刚介绍过 LSM 树中对数据的修改和删除本质上都是增加一条记录,因此数据表和内存表中,一份数据会有多条记录,这个时候查询就需要进行冲突处理。一般一份数据的概念是它们具有相同的 key而往往不同的版本会有时间戳根据这个时间戳可以建立写入顺序这类似于向量时钟的概念。故查询中我们很容易判断哪条数据是最新数据。
更新和删除操作本质上是插入数据,然后根据上面提到的冲突处理机制和合并操作来获取最终数据。更新操作是比较简明的,插入新数据就好了。但是删除操作时插入的是什么呢?
一般插入的是特殊的值被称作墓碑Tombstone。它是一个特殊的值用来表示记录被删除。如果要产出一个范围内数据呢Apache Cassandra 的处理方法是引入范围墓碑Range Tombstone
比如有从 k0 到 k9 的 9 条数据,在 k3 处设置开始删除点(包含 k3在 k7 处设置结束删除点(不包含 k7那么 k3 到 k6 这四条数据就被删除了。此时查询就会查不到 k4 到 k6即使它们上面没有设置墓碑。
以上我们介绍了 LSM 树中最基本的操作,下面我再为你介绍一种非常特殊的操作,那就是合并操作。
合并操作
合并操作是用来维护 LSM 树的结构的,以保证其可以正常运行。需要强调的一点是,我们这里说的合并操作针对的是 LSM 树的结构里面提到的多树结构。在多树结构中,磁盘中表的数量随着刷盘动作的持续进行,而变得越来越多。合并操作正是让它们减少的一种手段。
合并操作会根据一定规则,从磁盘的数据文件中选择若干文件进行合并,而后将新文件写入磁盘,成功后会删除老数据。在整个操作的过程中,对内存的消耗是完全可控的。这是由于每个数据文件都是经过排序的,如上一讲提到的查询规则一样,我们依然可以通过排序归并来合并多个文件中的数据。这种合并每次只会加载部分数据,也就是每个文件头部的数据,进入内存进行合并操作。从而很好地控制了合并操作对内存资源的消耗。
在整个合并的过程中,老的数据表依然可以对外提供读取服务,这说明老数据依然在磁盘中。这就要求磁盘要留有一定的额外空间来容纳生成中的新数据表。同时合并操作可以并行执行,但是一般情况下它们操作的数据不会重合,以免引发竞争问题。合并操作既可以将多个数据文件合并成一个,也可以将一个数据文件拆分成多个。
常见的合并策略有 Size-Tiered Compaction 和 Leveled Compaction。
Size-Tiered Compaction
下图就是这种策略的合并过程。
其中数据表按照大小进行合并较小的数据表逐步合并为较大的数据表。第一层保存的是系统内最小的数据表它们是刚刚从内存表中刷新出来的。合并过程就是将低层较小的数据表合并为高层较大的数据表的过程。Apache Cassandra 使用过这种合并策略。
该策略的优点是比较简单,容易实现。但是它的空间放大性很差,合并时层级越高该问题越严重。比如有两个 5GB 的文件需要合并,那么磁盘至少要保留 10GB 的空间来完成这次操作,可想而知此种容量压力是巨大的,必然会造成系统不稳定。
那么有没有什么策略能缓解空间放大呢?答案就是 Leveled Compaction。
Leveled Compaction
如名称所示,该策略是将数据表进行分层,按照编号排成 L0 到 Ln 这样的多层结构。L0 层是从内存表刷盘产生的数据表,该层数据表中间的 key 是可以相交的L1 层及以上的数据,将 Size-Tiered Compaction 中原本的大数据表拆开,成为多个 key 互不相交的小数据表,每层都有一个最大数据量阈值,当到达该值时,就出发合并操作。每层的阈值是按照指数排布的,例如 RocksDB 文档中介绍了一种排布L1 是 300MB、L2 是 3GB、L3 是 30GB、L4 为 300GB。
该策略如下图所示。
上图概要性地展示了从 L1 层开始,每个小数据表的容量都是相同的,且数据量阈值是按 10 倍增长。即 L1 最多可以有 10 个数据表L2 最多可以有 100 个,以此类推。
随着数据表不断写入L1 的数据量会超过阈值。这时就会选择 L1 中的至少一个数据表,将其数据合并到 L2 层与其 key 有交集的那些文件中,并从 L1 中删除这些数据。
仍然以上图为例,一个 L1 层数据表的 key 区间大致能够对应到 10 个 L2 层的数据表,所以一次合并会影响 11 个文件。该次合并完成后L2 的数据量又有可能超过阈值,进而触发 L2 到 L3 的合并,如此往复。
可见Leveled Compaction 与 Size-Tiered Compaction 相比,每次合并时不必再选取一层内所有的数据,并且每层中数据表的 key 区间都是不相交的,重复 key 减少了,所以很大程度上缓解了空间放大的问题。
当然在实际应用中会组合两种策略,比如经典的 RocksDB 会在 L0 合并到 L1 时,使用 Size-Tiered Compaction而从 L1 开始,则是采用经典的 Leveled Compaction。这其中原因是 L0 的数据表之间肯定会存在相同的 key。
以上介绍了 LSM 树中经典的合并问题,那么在合并过程中常常面临各种困境,比如上文提到的空间放大问题。下面我为你介绍 RUM 假说,来详细分析此类问题。
RUM 假说
开始介绍这个假说之前,你要先明确几个“放大”概念。
读放大。它来源于在读取时需要在多个文件中获取数据并解决数据冲突问题,如查询操作中所示的,读取的目标越多,对读取操作的影响越大,而合并操作可以有效缓解读放大问题。
写放大。对于 LSM 树来说,写放大来源于持续的合并操作,特别是 Leveled Compaction可以造成多层连续进行合并操作这样会让写放大问题呈几何倍增长。
空间放大。这是我在说合并的时候提到过的概念,是指相同 key 的数据被放置了多份,这是在合并操作中所产生的。尤其是 Size-Tiered Compaction 会有严重的空间放大问题。
那么我们可以同时解决以上三种问题吗?根据 RUM 的假说,答案是不能。
该假说总结了数据库系统优化的三个关键参数读取开销Read、更新开销Update和内存开销Memory也就是 RUM。对应到上面三种放大可以理解为 R 对应读放大、U 对应写放大,而 M 对应空间放大Memory 可以理解为广义的存储,而不仅仅指代内存)。
该假说表明,为了优化上述两项的开销必然带来第三项开销的上涨,可谓鱼与熊掌不可兼得。而 LSM 树是用牺牲读取性能来尽可能换取写入性能和空间利用率,上面我已经详细阐明其写入高效的原理,此处不做过多说明。
而有的同学会发现,合并操作会带来空间放大的问题,理论上应该会浪费空间。但是 LSM 树由于其不可变性可以引入块压缩来优化空间占用使用且内存不需要做预留B 树需要额外预留内存来进行树更新操作),从而使其可以很好地优化空间。
你应该知道RUM 所描述的内容过于简单,一些重要指标如延迟、维护性等没有涵盖其中,但是它可以作为我们工具箱里面的一个短小精干的扳手,来快速分析和掌握一个存储引擎的特点。
总结
至此我们学习了一个典型的面向分布式数据库所使用的存储引擎。从其特点可以看到它高速写入的特性对分布式数据库而言是有非常大吸引力的同时其KV 结构更是分片所喜欢的一种数据格式,非常适合基于此构建分布式数据库。所以诸如 Apache Cassandra、ClickHouse 和 TiDB 等分布式数据库都选用 LSM 树或类似结构的存储引擎来构建分布式数据库。

View File

@ -0,0 +1,142 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 事务处理与恢复(上):数据库崩溃后如何保证数据不丢失?
上一讲我们探讨了一个典型的面向分布式数据库所使用的存储引擎——LSM 树。那么这一讲,我将为你介绍存储引擎的精华部分,也就是事务管理。首先我将从使用者角度,介绍事务的特性,也就是 ACID而后简要说明存储引擎是通过什么组件来支持这些特性的。
为了保持这些特性,事务管理器需要考虑各种可能的问题与故障,如数据库进程崩溃、磁盘故障等。在面临各种故障时,如何保证 ACID 特性,我会在“数据库恢复”部分为你介绍。
由于这部分内容较多,我分成了上下两讲来向你讲述。下一讲我会接着介绍数据库的隔离级别与并发控制,它们是数据库提供给应用开发人员的礼物,可以让其非常轻易地实现并发数据的一致性。
以上就是这部分内容的学习脉络,现在让我们从事务的概述说起。
事务概述
事务管理是数据库中存储引擎的一个相当独立并且重要的组件,它可以保证对数据库的一系列操作看起来就像只有一步操作一样。这大大简化了面向数据库的应用的开发,特别是在高并发场景下,其意义更为重要。
一个典型的案例就是转账操作:从甲处转 100 元给乙。现实生活中,这个操作是原子的,因为纸币是不可复制的。但是在计算机系统内,这个操作实际上是由两个操作组成:甲账户减 100、乙账户加 100。两个操作就会面临风险比如在转账的同时丁又从甲处转走 100此时甲给乙的 100 未扣除),而如果此时账户内钱不够,这两笔操作中的一笔可能会失败;又比如,两个操作过程中数据库崩溃,重启后发现甲的账户已经没了 100而乙账户还没有增加或者相反。
为了解决上面类似的问题,人们在数据库特别是存储引擎层面提出了事务的概念。下面我来说说事务的经典特性 ACID。
ACID
A原子性
原子性保证了事务内的所有操作是不可分割的也就是它们要么全部成功要么全部失败不存在部分成功的情况。成功的标志是在事务的最后会有提交Commit操作它成功后会被认为整个事务成功。而失败会分成两种情况一种是执行回滚Rollback操作另一种就是数据库进程崩溃退出。
原子性是数据库提供给使用者的保证,是为了模拟现实原子操作,如上文提到的转账。在现实生活中,一些看似不可分割的操作转换为计算机操作却并不是单一操作。而原子性就是对现实生活中原子操作的保证。
C一致性
一致性其实是受用户与数据库共同控制的,而不只是数据库提供的一个服务。它首先是一个业务层面的约束,比如开篇中的例子,甲向乙转 100 元。业务应用首先要保证在甲账户扣款 100 元,而且在乙账户增加 100 元,这个操作所带来的一致性与数据库是无关的。而数据库是通过 AID 来保证这两个正确的动作可以得到最终正确的结果。
这里的一致性与模块一中的分布式一致性有本质区别想了解详细对比的同学请移步到“05 | 一致性与 CAP 模型:为什么需要分布式一致性”,这里就不进行赘述了。
I隔离性
事务的一个伟大之处是能处理并发操作,也就是不同的事务在运行的时候可以互相不干扰,就像没有别的事务发生一样。做并发编程的同学会对此深有体会,处理并发操作需要的精力与经验与处理串行操作完全不在一个等级上。而隔离性恰好能将实际上并发的操作,转化为从使用者角度看却是串行的,从而大大降低使用难度。
当然在实际案例中,以上描述的强并发控制性能会偏低。一般数据库会定义多种的隔离级别来提供不同等级的并发处理能力,也就是一个事务在较低隔离级别下很可能被其他事务看见。详细内容我会在“隔离级别”部分进行说明。
D持久性
持久性比较简单,就是事务一旦被提交,那么它对数据库的修改就可以保留下来。这里要注意这个“保存下来”不仅仅意味着别的事务能查询到,更重要的是在数据库面临系统故障、进程崩溃等问题时,提交的数据在数据库恢复后,依然可以完整地读取出来。
以上就是事务的四种重要的特性,那么事务在存储引擎内部有哪些组件来满足上面的特性呢?我接下来要为你介绍的就是一个典型的事务管理组件。
事务管理器
事务主要由事务管理器来控制它负责协调、调度和跟踪事务状态和每个执行步骤。当然这与分布式事务两阶段提交2PC中的事务管理器是不同的关于分布式事务的内容我将在下一个模块详细介绍。
页缓存
关于事务管理器首先要提到的就是页缓存Page Cache或者缓冲池Buffer Pool它是磁盘和存储引擎其他组件的一个中间层。数据首先被写入到缓存里而后同步到数据磁盘上。它一般对于其他组件特别是对于写入来说是透明的写入组件以为是将数据写入磁盘实际上是写入了缓存中。这个时候如果系统出现故障数据就会有丢失的风险故需要本讲后面“如何恢复事务”要介绍的手段来解决这个问题。
缓存首先解决了内存与磁盘之间的速度差,同时可以在不改变算法的情况下优化数据库的性能。但是,内存毕竟有限,不可能将磁盘中的所有数据进行缓存。这时候就需要进行刷盘来释放缓存,刷盘操作一般是异步周期性执行的,这样做的好处是不会阻塞正常的写入和读取。
刷盘时需要注意,脏页(被修改的页缓存)如果被其他对象引用,那么刷盘后不能马上释放空间,需要等到它没有引用的时候再从缓存中释放。刷盘操作同时需要与提交日志检查点进行配合,从而保证 D也就是持久性。
当缓存到达一定阈值后,就不得不将有些旧的值从缓存中移除。这个时候就需要缓存淘汰算法来帮忙释放空间。这里有 FIFO、LRU、表盘Clock和 LFU 等算法,感兴趣的话你可以根据这几个关键词自行学习。
最后存在部分数据我们希望它们一直在缓存中且不受淘汰算法的影响这时候我们可以把它们“锁”Pinned在缓存里。比如 B 树的高节点,它们一般数据量不大,且每次查询都需要访问。还有一些经常访问的元数据也会长期保存在缓存中。
日志管理器
其次是日志管理器,它保存了一组数据的历史操作记录。缓存内的数据没有刷入磁盘前,系统就崩溃了,通过回放日志,缓存中的数据可以恢复出来。另外,在回滚场景,这些日志可以将修改前的数据恢复出来。
锁管理器
最后要介绍的就是非常重要的锁管理器,它保证了事务访问共享资源时不会打破这些资源的完整性约束。同时,它也可以保证事务可以串行执行。关于锁的内容我会在后面详细说明。
以上就是事务管理的主要组件,下面我将从数据库恢复事务的角度介绍日志管理相关内容。
数据库如何恢复事务
数据库系统是由一系列硬件和软件组成的复杂生态系统,其中每个组件都有产生各种稳定性问题的可能,且将它们组合为数据库系统后,这种可能被进一步放大了。而数据库的设计者必须为这种潜在的稳定性问题给出自己的解决方案,并使数据库作出某种“承诺”。
提交日志,即 CommitLog 或 WALWrite-Ahead Log就是应对此种问题的有效手段。这种日志记录了数据库的所有操作并使用追加Append模式记录在磁盘的日志文件中。
上文中我们知道数据库的写操作首先是写入了缓存,而后再刷入磁盘中。但是在刷盘之前,其实这些数据已经以日志的形式保存在了磁盘的提交日志里面。当数据没有刷入磁盘而仅仅驻留在缓存时,这些日志可以保证数据的持久性。也就是,一旦数据库遭遇故障,可以从日志中恢复出来数据。
那么提交日志具有哪些特性来保障这些功能呢?下面来看一下日志的特性。
提交日志的特性
首先,提交日志非常类似于上一讲介绍的 LSM 树的磁盘文件特性,都是顺序写入且不可变。其益处也是相同的,顺序写保障了写入的高性能,不可变保证了读取可以安全地从前到后读取里面的数据。
提交日志一般都会被分配一个序列号作为唯一键,这个序号不是一个自增数字,就是一个时间戳。此外,每条日志都非常小,有些数据库会将它们进行缓存而后批量写入磁盘。这就导致,默写情况下日志不能完全恢复数据库,这是对于性能的考虑,大部分数据库会给出不同的参数来描述日志缓存刷盘的行为,用户可在性能与恢复数据完整性上作出平衡。
而事务在提交的时候,一定要保证其日志已经写入提交日志中。也就是事务内容完全写入日志是事务完成的一个非常重要的标志。
日志在理论上可以无限增长但实际上没有意义。因为一旦数据从缓存中被刷入磁盘该操作之前的日志就没有意义了此时日志就可以被截断Trim从而释放空间。而这个被截断的点我们一般称为检查点。检查点之前的页缓存中的脏页需要被完全刷入磁盘中。
日志在实现的时候,一般是由一组文件组成。日志在文件中顺序循环写入,如果一个文件中的数据都是检查点之前的旧数据,那么新日志就可以覆盖它们,从而避免新建文件的问题。同时,将不同文件放入不同磁盘,以提高日志系统的可用性。
物理日志 Redo Log 与逻辑日志 Undo Log
事务对数据的修改其实是一种状态的改变,比如将 3 改为 5。这里我们将 3 称为前镜像before-image而 5 称为后镜像after-image。我们可以得到如下公式
前镜像+redo log=后镜像
后镜像+undo log=前镜像
redo log 存储了页面和数据变化的所有历史记录,我们称它为物理日志。而 undo log 需要一个原始状态,同时包含相对这个状态的操作,所以又被称为逻辑日志。我们使用 redo 和 undo 就可以将数据向前或向后进行转换,这其实就是事务操作算法的基础。
Steal 与 Force 策略
redo 和 undo 有两种写入策略steal 和 force。
steal 策略是说允许将事务中未提交的缓存数据写入数据库,而 no-steal 则是不能。可以看到如果是 steal 模式,说明数据从后镜像转变为前镜像了,这就需要 undo log 配合,将被覆盖的数据写入 undo log以备事务回滚的时候恢复数据从而可以恢复到前镜像状态。
force 策略是说事务提交的时候,需要将所有操作进行刷盘,而 no-force 则不需要。可以看到如果是 no-force数据在磁盘上还是前镜像状态。这就需要 redo log 来配合,以备在系统出现故障后,从 redo log 里面恢复缓存中的数据,从而能转变为后镜像状态。
从上可知,当代数据库存储引擎大部分都有 undo log 和 redo log那么它们就是 steal/no-force 策略的数据库。
下面再来说一个算法。
ARIES 数据恢复算法
这个算法全称为 Algorithm for Recovery and Isolation Exploiting Semantics。
该算法同时使用 undo log 和 redo log 来完成数据库故障崩溃后的恢复工作,其处理流程分为如下三个步骤。
首先数据库重新启动后,进入分析模式。检查崩溃时数据库的脏页情况,用来识别需要从 redo 的什么位置开始恢复数据。同时搜集 undo 的信息去回滚未完成的事务。
进入执行 redo 的阶段。该过程通过 redo log 的回放,将在页缓存中但是没有持久化到磁盘的数据恢复出来。这里注意,除了恢复了已提交的数据,一部分未提交的数据也恢复出来了。
进入执行 undo 的阶段。这个阶段会回滚所有在上一阶段被恢复的未提交事务。为了防止该阶段执行时数据库再次崩溃,存储引擎会记录下已执行的 undo 操作,防止它们重复被执行。
ARIES 算法虽然被提出多年,但其概念和执行过程依然在现代存储引擎中扮演着重要作用。
以上我们讲解了数据库如何恢复数据,保持一致性状态。它对应着 AIDC 如前文所示,是一种约束,一般不认为是数据库提供的功能)中的 AD。同时我们也要知道以提交日志为代表的数据库恢复技术在没有事务概念的数据库中也扮演着重要的作用因为页缓存是无处不在的解决主存掉电丢失数据的问题是提交日志的主要功能。
总结
那么这一讲就介绍到这了。我们从 ACID 原理出发,介绍了管理事务的众多组件,而后重点介绍了如何保证数据的持久性,这主要通过提交日志来实现。其余有关隔离性的概念,我将会在下一讲接着介绍。

View File

@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 错误侦测:如何保证分布式系统稳定?
经过上一讲的学习,相信你已经了解了分布式数据库领域中,分布式系统部分所重点解决的问题,即围绕失败模型来设计算法、解决各种稳定性问题。
解决问题的前提是发现问题,所以这一讲我们来说说如何发现系统内的错误,这是之后要介绍的算法们所依赖的前置条件。比如上一讲提到的共识算法,如果没有失败侦测手段,我们是无法解决拜占庭将军问题的,也就是会陷入 FLP 假说所描述的境地中,从而无法实现一个可用的共识算法。这里同时要指明,失败不仅仅是节点崩溃,而主要从其他节点看,该节点无法响应、延迟增大,从而降低系统整体的可用性。
这一讲,我将从影响侦测算法表现的几组特性出发,为评估这些算法给出可观标准;而后从你我耳熟能详的心跳算法开始介绍,逐步探讨几种其改良变种;最后介绍大型分布式数据库,特别是无主数据库常用的 Gossip 方案。
现在让我们从影响算法表现的因素开始说起。
影响算法的因素
失败可能发生在节点之间的连接,比如丢包或者延迟增大;也可能发生在节点进程本身,比如节点崩溃或者处理缓慢。我们其实很难区分节点到底是处理慢,还是完全无法处理请求。所以所有的侦测算法需要在这两个状态中平衡,比如发现节点无法响应后,一般会在特定的延迟时间后再去侦测,从而更准确地判断节点到底处于哪种状态。
基于以上原因,我们需要通过一系列的指标来衡量算法的特性。首先是任何算法都需要遵守一组特性:活跃性与安全性,它们是算法的必要条件。
活跃性指的是任何失败的消息都能被安全地处理,也就是如果一个节点失败了而无法响应正常的请求,它一定会被算法检测出来,而不会产生遗漏。
安全性则相反,算法不产生任何异常的消息,以至于使得正常的节点被判定为异常节点,从而将它标记为失败。也就是一个节点失败了,它是真正失败了,而不是如上文所述的只是暂时性的缓慢。
还有一个必要条件就是算法的完成性。完成性被表述为算法要在预计的时间内得到结果,也就是它最终会产生一个符合活跃性和安全性的检测结果,而不会无限制地停留在某个状态,从而得不到任何结果。这其实也是任何分布式算法需要实现的特性。
上面介绍的三个特性都是失败检测的必要条件。而下面我将介绍的这一对概念,可以根据使用场景的不同在它们之间进行取舍。
首先要介绍的就是算法执行效率,效率表现为算法能多快地获取失败检测的结果。其次就是准确性,它表示获取的结果的精确程度,这个精确程度就是上文所述的对于活跃性与安全性的实现程度。不精准的算法要么表现为不能将已经失败的节点检测出来,要么就是将并没有失败的节点标记为失败。
效率和准确被认为是不可兼得的,如果我们想提高算法的执行效率,那么必然会带来准确性的降低,反之亦然。故在设计失败侦测算法时,要对这两个特性进行权衡,针对不同的场景提出不同的取舍标准。
基于以上的标准,让我开始介绍最常用的失败检测算法——心跳检测法,及其多样的变种。
心跳检测法
心跳检测法使用非常广泛,最主要的原因是它非常简单且直观。我们可以直接将它理解为一个随身心率检测仪,一旦该仪器检测不到心跳,就会报警。
心跳检测有许多实现手段,这里我会介绍基于超时和不基于超时的检测法,以及为了提高检测精准度的间接检测法。
基于超时
基于超时的心跳检测法一般包括两种方法。
发送一个 ping 包到远程节点,如果该节点可以在规定的时间内返回正确的响应,我们认为它就是在线节点;否则,就会将它标记为失败。
一个节点向周围节点以一个固定的频率发送特定的数据包(称为心跳包),周围节点根据接收的频率判断该节点的健康状态。如果超出规定时间,未收到数据包,则认为该节点已经离线。
可以看到这两种方法虽然实现细节不同,但都包含了一个所谓“规定时间”的概念,那就是超时机制。我们现在以第一种模式来详细介绍这种算法,请看下面这张图片。
图 1 模拟两个连续心跳访问
上面的图模拟了两个连续心跳访问,节点 1 发送 ping 包,在规定的时间内节点 2 返回了 pong 包。从而节点 1 判断节点 2 是存活的。但在现实场景中经常会发生图 2 所示的情况。
图 2 现实场景下的心跳访问
可以看到节点 1 发送 ping 后,节点没有在规定时间内返回 pong此时节点 1 又发送了另外的 ping。此种情况表明节点 2 存在延迟情况。偶尔的延迟在分布式场景中是极其常见的,故基于超时的心跳检测算法需要设置一个超时总数阈值。当超时次数超过该阈值后,才判断远程节点是离线状态,从而避免偶尔产生的延迟影响算法的准确性。
由上面的描述可知,基于超时的心跳检测法会为了调高算法的准确度,从而牺牲算法的效率。那有没有什么办法能改善算法的效率呢?下面我就要介绍一种不基于超时的心跳检测算法。
不基于超时
不基于超时的心跳检测算法是基于异步系统理论的。它保存一个全局节点的心跳列表,上面记录了每一个节点的心跳状态,从而可以直观地看到系统中节点的健康度。由此可知,该算法除了可以提高检测的效率外,还可以非常容易地获得所有节点的健康状态。那么这个全局列表是如何生成的呢?下图展示了该列表在节点之间的流转过程。
图 3 全局列表在节点之间的流转过程
由图可知,该算法需要生成一个节点间的主要路径,该路径就是数据流在节点间最常经过的一条路径,该路径同时要包含集群内的所有节点。如上图所示,这条路径就是从节点 1 经过节点 2最后到达节点 3。
算法开始的时候,节点首先将自己记录到表格中,然后将表格发送给节点 2节点 2 首先将表格中的节点 1 的计数器加 1然后将自己记录在表格中而后发送给节点 3节点 3 如节点 2 一样,将其中的所有节点计数器加 1再把自己记录进去。一旦节点 3 发现所有节点全部被记录了,就停止这个表格的传播。
在一个真实的环境中,节点不是如例子中那样是线性排布的,而很可能是一个节点会与许多节点连接。这个算法的一个优点是,即使两个节点连接偶尔不通,只要这个远程节点可以至少被一个节点访问,它就有机会被记录在列表中。
这个算法是不基于超时设计的,故可以很快获取集群内的失败节点。并可以知道节点的健康度是由哪些节点给出的判断。但是它同时存在需要压制异常计算节点的问题,这些异常记录的计数器会将一个正常的节点标记为异常,从而使算法的精准度下降。
那么有没有方法能提高对于单一节点的判断呢?现在我就来介绍一种间接的检测方法。
间接检测
间接检测法可以有效提高算法的稳定性。它是将整个网络进行分组,我们不需要知道网络中所有节点的健康度,而只需要在子网中选取部分节点,它们会告知其相邻节点的健康状态。
图 4 间接检测法
如图所示,节点 1 无法直接去判断节点 2 是否存活,这个时候它转而询问其相邻节点 3。由节点 3 去询问节点 2 的健康情况,最后将此信息由节点 3 返回给节点 1。
这种算法的好处是不需要将心跳检测进行广播,而是通过有限的网络连接,就可以检测到集群中各个分组内的健康情况,从而得知整个集群的健康情况。此种方法由于使用了组内的多个节点进行检测,其算法的准确度相比于一个节点去检测提高了很多。同时我们可以并行进行检测,算法的收敛速度也是很快的。因此可以说,间接检测法在准确度和效率上取得了比较好的平衡。
但是在大规模分布式数据库中,心跳检测法会面临效率上的挑战,那么何种算法比较好处理这种挑战呢?下面我要为你介绍 Gossip 协议检测法。
Gossip 协议检测
除了心跳检测外,在大型分布式数据库中一个比较常用的检测方案就是 Gossip 协议检测法。Gossip 的原理是每个节点都检测与它相邻的节点,从而可以非常迅速地发现系统内的异常节点。
算法的细节是每个节点都有一份全局节点列表,从中选择一些节点进行检测。如果成功就增加成功计数器,同时记录最近一次的检测时间;而后该节点把自己的检测列表的周期性同步给邻居节点,邻居节点获得这份列表后会与自己本地的列表进行合并;最终系统内所有节点都会知道整个系统的健康状态。
如果某些节点没有进行正确响应那么它们就会被标记为失败从而进行后续的处理。这里注意要设置合适的阈值来防止将正常的节点标记为错误。Gossip 算法广泛应用在无主的分布式系统中,比较著名的 Cassandra 就是采用了这种检测手法。
我们会发现这种检测方法吸收了上文提到的间接检测方法的一些优势。每个节点是否应该被认为失败是由多个节点判断的结果推导出的并不是由单一节点做出的判断这大大提高了系统的稳定性。但是此种检测方法会极大增加系统内消息数量故选择合适的数据包成为优化该模式的关键。这个问题我会在“17 | 数据可靠传播:反熵理论如何帮助数据库可靠工作”中详细介绍 Gossip 协议时给出答案。
Cassandra 作为 Gossip 检测法的主要案例,它同时还使用了另外一种方式去评价节点是否失败,那就是 φ 值检测法。
φ 值检测
以上提到的大部分检测方法都是使用二元数值来表示检测的结果,也就是一个节点不是健康的就是失败了,非黑即白。而 φ 值检测法引入了一个变量,它是一个数值,用来评价节点失败的可能性。现在我们来看看这个数值是如何计算的。
首先,我们需要生成一个检测消息到达的时间窗口,这个窗口保存着最近到的检测消息的延迟情况。根据这个窗口内的数值,我们使用一定的算法来“预测”未来消息的延迟。当消息实际到达时,我们用真实值与预测值来计算这个 φ 值。
其次,给 φ 设置一个阈值,一旦它超过这个阈值,我们就可以将节点设置为失败。这种检测模式可以根据实际情况动态调整阈值,故可以动态优化检测方案。同时,如果配合 Gossip 检测法,可以保证窗口内的数据更加有代表性,而不会由于个别节点的异常而影响 φ 值的计算。故这种评估检测法与 Gossip 检测具有某种天然的联系。
从以上算法的细节出发,我们很容易设计出该算法所需的多个组件。
延迟搜集器:搜集节点的延迟情况,用来构建延迟窗口。
分析器:根据搜集数据计算 φ 值,并根据阈值判断节点是否失败。
结果执行器:一旦节点被标记为失败,后续处理流程由结果执行器去触发。
你可以发现,这种检测模式将一个二元判断变为了一个连续值判断,也就是将一个开关变成了一个进度条。这种模式其实广泛应用在状态判断领域,比如 APM 领域中的 Apdex 指标,它也是将应用健康度抽象为一个评分,从而更细粒度地判断应用性能。我们看到,虽然这类算法有点复杂,但可以更加有效地判断系统的状态。
总结
这一讲内容比较简单、易理解,但是却非常重要且应用广泛。作为大部分分布式算法的基础,之后我要介绍的所有算法都包含今天所说的失败检测环节。
这一讲的算法都是在准确性与效率上直接进行平衡的。有些会使用点对点的心跳模式,有些会使用 Gossip 和消息广播模式,有些会使用单一的指标判断,而有些则使用估算的连续变换的数值……它们有各自的优缺点,但都是在以上两种特点之间去平衡的。当然简单性也被用作衡量算法实用程度的一个指标,这符合 UNIX 哲学,简单往往是应对复杂最佳的利器。
大部分分布式数据库都是主从模式,故一般由主节点进行失败检测,这样做的好处是能够有效控制集群内的消息数量,下一讲我会为你介绍如何在集群中选择领导节点。

View File

@ -0,0 +1,225 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 领导选举:如何在分布式系统内安全地协调操作?
这一讲我们来聊聊如何在分布式数据库,乃至一般性的分布式系统内同步数据。
不知道你是否发现这样一种事实:同步数据是一种代价非常高昂的操作,如果同步过程中需要所有参与的节点互相进行操作,那么其通信开销会非常巨大。
如下图所示,随着参与节点的增加,其通信成本逐步提高,最终一定会导致数据在集群内不一致。尤其在超大型和地理空间上分散的集群网络中,此现象会进一步被放大。
为了减少同步通信开销和参与节点的数量,一些算法引入了“领导者”(有时称为协调者),负责协调分布式系统内的数据同步。
领导选举
通常,分布式系统中所有节点都是平等的关系,任何节点都可以承担领导角色。节点一旦成为领导,一般在相当长的时间内会一直承担领导的角色,但这不是一个永久性的角色。原因也比较容易想到:节点会崩溃,从而不能履行领导职责。
现实生活中,如果你的领导由于个人变故无法履职,组织内会重新选择一个人来替代他。同样,在领导节点崩溃之后,任何其他节点都可以开始新一轮的选举。如果当选,就承担领导责任,并继续从前一个领导节点退出的位置开始工作。
领导节点起到协调整个集群的作用,其一般职责包括:
控制广播消息的总顺序;
收集并保存全局状态;
接收消息,并在节点之间传播和同步它们;
进行系统重置,一般是在发生故障后、初始化期间,或重要系统状态更新时操作。
集群并不会经常进行领导选举流程,一般会在如下两个场景中触发选举:
在初始化时触发选举,称为首次选举领导;
当前一位领导者崩溃或无法通信时。
选举算法中的关键属性
当集群进入选举流程后其中的节点会应用选举算法来进行领导选举而这些选举算法一般包含两个属性“安全性”Safety和“活跃性”Liveness。它们是两个非常重要且比较基础的属性最早由莱斯利·兰伯特 L.Lamport——分布式计算的开创者提出。
在解释这两个属性的含义之前,我们先想象一下工作生活中是如何选举领导的?领导通常来源于一组候选人,选举规则需包含如下两点。
选举必须产生一个领导。如果有两个领导,那么下属应该听从他们中谁的指示呢?领导选举本来是解决协调问题的,而多个领导不仅没有解决这个问题,反而带来了更大问题。
选举必须有结果。较为理想的状态是:领导选举需要在大家可以接受的时间内有结果。如果领导长时间没有被选举出来,那么必然造成该组织无法开展正常的工作。因为没人来协调和安排工作,整个组织内部会变得混乱无序。
以上两个规则正好对应到算法的两个属性上。
其中第一个规则对应了算法的“安全性”Safety它保证一次最多只有一个领导者同时完全消除“脑裂”Split Brain情况的可能性集群被分成两个以上部分并产生多个互相不知道对方存在的领导节点。然而在实践中许多领导人选举算法违反了这个属性。下面在介绍“脑裂”的时候会详细讲解如何解决这个问题。
第二个规则代表了选举算法的“活跃性”Liveness它保证了在绝大多数时候集群内都会有一个领导者选举最终会完成并产生这个领导即系统不应无限期地处于选举状态。
满足了以上两个属性的算法,我们才称其为有效的领导选举算法。
领导选举与分布式锁
领导选举和分布式锁在算法层面有很高的重合性,前者选择一个节点作为领导,而后者则是作为锁持有者,因此很多研发人员经常将二者混为一谈。那么现在,让我们比较一下领导者选举和分布式锁的区别。
分布式锁是保证在并发环境中,一些互斥的资源,比如事务、共享变量等,同一时间内只能有一个节点进行操作。它也需要满足上文提到的安全性和活跃性,即排他锁每次只能分配给一个节点,同时该节点不会无限期持有锁。
从理论上看,虽然它们有很多相似之处,但也有比较明显的区别。如果一个节点持有排他锁,那么对于其他节点来说,不需要知道谁现在持有这个锁,只要满足锁最终将被释放,允许其他人获得它,这就是前文所说的“活跃性”。
与此相反,选举过程完全不是这样,集群中的节点必须要知道目前系统中谁是领导节点,因为集群中其他节点需要感知领导节点的活性,从而判断是否需要进入到选举流程中。因此,新当选的领导人必须将自己的角色告知给它的下属。
另一个差异是:如果分布式锁算法对特定的节点或节点组有偏好,也就是非公平锁,它最终会导致一些非优先节点永远都获得不了共享资源,这与“活跃性”是矛盾的。但与其相反,我们一般希望领导节点尽可能长时间地担任领导角色,直到它停止或崩溃,因为“老”领导者更受大家的欢迎。
解决单点问题
在分布式系统中,具有稳定性的领导者有助于减小远程节点的状态同步消耗,减少交换消息的数量;同时一些操作可以在单一的领导节点内部进行,避免在集群内进行同步操作。在采用领导机制的系统中,一个潜在的问题是由于领导者是单节点,故其可能成为性能瓶颈。
为了克服这一点,许多系统将数据划分为不相交的独立副本集,每个副本集都有自己的领导者,而不是只有一个全局领导者,使用这种方法的系统代表是 Spanner将在第 17 讲“分布式事务”中介绍)。由于每个领导节点都有失败的可能,因此必须检测、报告,当发生此种情况时,一个系统必须选出另一个领导人来取代失败的领导人。
上面整体介绍了领导选举的使用场景和算法特点,那么领导选举是怎样操作的呢?
典型的算法包括Bully 算法、ZABZookeeper Atomic Broadcast、Multi-Paxos 和 RAFT 等。但是除了 Bully 算法外,其他算法都使用自己独特的方法来同时解决领导者选举、故障检测和解决竞争领导者节点之间的冲突。所以它们的内涵要远远大于领导选举这个范畴,限于篇幅问题,我将会在下一讲详细介绍。
基于以上的原因,下面我将使用 Bully 算法及其改进算法来举例说明典型的领导选举流程。Bully 算法简单且容易进行收敛,可以很好地满足“活跃性”;同时在无网络分区的情况下,也能很好地满足“安全性”。
经典领导选举算法Bully 算法
这是最常用的一种领导选举算法,它使用节点 ID的大小来选举新领导者。在所有活跃的节点中选取节点 ID 最大或者最小的节点为主节点。
以下采用“ID 越大优先级越高”的逻辑来解释算法:
每个节点都会获得分配给它的唯一 ID。在选举期间ID 最大的节点成为领导者。因为 ID 最大的节点“逼迫”其他节点接受它成为领导者,它也被称为君主制领导人选举:类似于各国王室中的领导人继承顺位,由顺位最高的皇室成员来继承皇位。如果某个节点意识到系统中没有领导者,则开始选举,或者先前的领导者已经停止响应请求。
算法包含 4 个步骤:
集群中每个活着的节点查找比自己 ID 大的节点,如果不存在则向其他节点发送 Victory 消息,表明自己为领导节点;
如果存在比自己 ID 大的节点,则向这些节点发送 Election 消息,并等待响应;
如果在给定的时间内,没有收到这些节点回复的消息,则自己成为领导节点,并向比自己 ID 小的节点发送 Victory 消息;
节点收到比自己 ID 小的节点发送的 Election 消息,则回复 Alive 消息。
上图举例说明了 Bully 领导者选举算法,其中:
节点3 注意到先前的领导者 6 已经崩溃,并且通过向比自己 ID 更大的节点发送选举消息来开始新的选举;
4 和 5 以 Alive 响应,因为它们的 ID 比 3 更大;
3 通知在这一轮中作出响应的最大 ID 节点是5
5 被选为新领导人,它广播选举信息,通知排名较低的节点选举结果。
这种算法的一个明显问题是:它违反了“安全性”原则(即一次最多只能选出一位领导人)。在存在网络分区的情况下,在节点被分成两个或多个独立工作的子集的情况下,每个子集选举其领导者。
该算法的另一个问题是:它对 ID较大的节点有强烈的偏好但是如果它们不稳定会严重威胁选举的稳定性并可能导致不稳定节点永久性地连任。即不稳定的高排名节点提出自己作为领导者不久之后失败但是在新一轮选举中又赢得选举然后再次失败选举过程就会如此重复而不能结束。这种情况可以通过监控节点的存活性指标并在选举期间根据这些指标来评价节点的活性从而解决该问题。
Bully 算法的改进
Bully 算法虽然经典,但由于其相对简单,在实际应用中往往不能得到良好的效果。因此在分布式数据库中,我们会看到如下所述的多种演进版本来解决真实环境中的一些问题,但需要注意的是,其核心依然是经典的 Bully 算法。
改进一:故障转移节点列表
有许多版本的 Bully 算法的变种,用来改善它在各种场景下的表现。例如,我们可以使用多个备选节点作为在发生领导节点崩溃后的故障转移目标,从而缩短重选时间。每个当选的领导者都提供一个故障转移节点列表。当集群中的节点检测到领导者异常时,它通过向该领导节点提供的候选列表中排名最高的候选人发送信息,开始新一轮选举。如果其中一位候选人当选,它就会成为新的领导人,而无须经历完整的选举。
如果已经检测到领导者故障的进程本身是列表中排名最高的进程,它可以立即通知其他节点自己就是新的领导者。
上图显示了采用这种优化方式的过程,其中:
6是具有指定候选列表 {54} 的领导者它崩溃退出3 注意到该故障并与列表中具有最高等级的备选节点5 联系;
5 响应 3表示它是 Alive 的,从而防止 3 与备选列表中的其他节点联系;
5 通知其他节点它是新的领导者。
因此,如果备选列表中,第一个节点是活跃的,我们在选举期间需要的步骤就会更少。
改进二:节点分角色
另一种算法试图通过将节点分成候选和普通两个子集来降低消息数量,其中只有一个候选节点可以最终成为领导者。普通节点联系候选节点、从它们之中选择优先级最高的节点作为领导者,然后将选举结果通知其余节点。
为了解决并发选举的问题,该算法引入了一个随机的启动延迟,从而使不同节点产生了不同的启动时间,最终导致其中一个节点在其他节点之前发起了选举。该延迟时间通常大于消息在节点间往返时间。具有较高优先级的节点具有较低的延迟,较低优先级节点延迟往往很大。
上图显示了选举过程的步骤,其中:
节点4 来自普通的集合,它发现了崩溃的领导者 6于是通过联系候选集合中的所有剩余节点来开始新一轮选举
候选节点响应并告知 4 它们仍然活着;
4通知所有节点新的领导者是 2。
该算法减小了领导选举中参与节点的数量,从而加快了在大型集群中该算法收敛的速度。
改进三:邀请算法
邀请算法允许节点“邀请”其他进程加入它们的组,而不是进行组间优先级排序。该算法允许定义多个领导者,从而形成每个组都有其自己的领导者的局面。每个节点开始时都是一个新组的领导者,其中唯一的成员就是该节点本身。
组领导者联系不属于它们组内的其他节点,并邀请它们加入该组。如果受邀节点本身是领导者,则合并两个组;否则,受邀节点回复它所在组的组长 ID允许两个组长直接取得联系并合并组这样大大减少了合并的操作步骤。
上图显示了邀请算法的执行步骤,其中:
四个节点形成四个独立组每个节点都是所在组的领导1 邀请 2 加入其组3 邀请 4 加入其组;
2 加入节点 1的组并且 4 加入节点3的组1 为第一组组长,联系人另一组组长 3剩余组成员在本例中为 4个获知了新的组长 1
合并两个组,并且 1 成为扩展组的领导者。
由于组被合并,不管是发起合并的组长成为新的领导,还是另一个组长成为新的领导。为了将合并组所需的消息数量保持在最小,一般选择具有较大 ID 的组长的领导者成为新组的领导者,这样,只有来自较小 ID 组的节点需要更新领导者。
与所讨论的其他算法类似,该算法采用“分而治之”的方法来收敛领导选举。邀请算法允许创建节点组并合并它们,而不必从头开始触发新的选举,这样就减少了完成选举所需的消息数量。
改进四:环形算法
在环形算法中,系统中的所有节点形成环,并且每个节点都知道该环形拓扑结构,了解其前后邻居。当节点检测到领导者失败时,它开始新的选举,选举消息在整个环中转发,方式为:每个节点联系它的后继节点(环中离它最近的下一节点)。如果该节点不可用,则跳过该节点,并尝试联系环中其后的节点,直到最终它们中的一个有回应。
节点联系它们的兄弟节点,收集所有活跃的节点从而形成可用的节点集。在将该节点集传递到下一个节点之前,该节点将自己添加到集合中。
该算法通过完全遍历该环来进行。当消息返回到开始选举的节点时,从活跃集合中选择排名最高的节点作为领导者。
如上图所示,你可以看到这样一个遍历的例子:
先前的领导 6失败了环中每个节点都从自己的角度保存了一份当前环的拓扑结构
以 3 为例说明查找新领导的流程3 通过开始遍历来发起选举轮次在每一步中节点都按照既定路线进行遍历操作5 不能到 6所以跳过直接到 1
由于 5 是具有最高等级的节点3 发起另一轮消息,分发关于新领导者的信息。
该算法的一个优化方法是每个节点只发布它认为排名最高的节点,而不是一组活跃的节点,以节省空间:因为 Max 最大值函数是遵循交换率的,也就是知道一个最大值就足够了。当算法返回到已经开始选举的节点时,最后就得到了 ID 最大的节点。
另外由于环可以被拆分为两个或更多个部分,每个部分就会选举自己的领导者,这种算法也不具备“安全性”。
如前所述,要使具有领导的系统正常运行,我们需要知道当前领导的状态。因此,为了系统整体的稳定性,领导者必须保证是一直活跃的,并且能够履行其职责。为了检测领导是否崩溃,可以使用我上一讲介绍过的故障检测算法。
解决选举算法中的脑裂问题
我们需要注意到,在本讲中讨论的所有算法都容易出现脑裂的问题,即最终可能会在独立的两个子网中出现两个领导者,而这两个领导并不知道对方的存在。
为了避免脑裂问题,我们一般需要引入法定人数来选举领导。比如 Elasticsearch 选举集群领导就使用Bully 算法结合最小法定人数来解决脑裂问题。
如上图所示,目前有 2 个网络、5 个节点假定最小法定人数是3。A 目前作为集群的领导A、B 在一个网络C、D 和 E 在另外一个网络,两个网络被连接在一起。
当这个连接失败后A、B 还能连接彼此,但与 C、D 和 E 失去了联系。同样, C、D 和 E 也能知道彼此但无法连接到A 和B。
此时C、D 和 E 无法连接原有的领导 A。同时它们三个满足最小法定人数3故开始进行新一轮的选举。假设 C 被选举为新的领导,这三个节点就可以正常进行工作了。
而在另外一个网络中,虽然 A 是曾经的领导,但是这个网络内节点数量是 2小于最小法定人数。故 A 会主动放弃其领导角色,从而导致该网络中的节点被标记为不可用,从而拒绝提供服务。这样就有效地避免了脑裂带来的问题。
总结
领导选举是分布式系统中的一个重要课题,这是因为使用固定的领导者非常有助于减少协调开销并提高系统的性能。选举过程可能成本很高,但由于不是很频繁,因此不会对整个系统性能产生严重影响。单一的领导者可能成为瓶颈,但我们可以通过对数据进行分区并使用每个分区的领导者来解决这个问题,或对不同的操作使用不同的领导者。
许多共识算法包括Multi-Paxos 和 RAFT一般都有选举的过程。但是共识算法的内涵相比于单纯的选举算法更为丰富所以我在“19 | 共识算法:一次性说清楚 Paxos、Raft 等算法的区别”中会专门来和你讨论。
能力越强,责任越大。领导节点虽然解决了系统内数据同步的问题,但由于其承担重大责任,一旦发生问题将会产生严重的影响。故一个稳定高效的选举算法是领导模式的关键。
领导者的状态可能在节点不知道的情况下发生变化,所以集群内节点需要及时了解领导节点是否仍然活跃。为了实现这一点,我们需要将领导选举与故障检测结合起来。例如,稳定领导者选举算法使用具有独特的稳定领导者和基于超时的故障检测的轮次,以保证领导者可以被重新选举,从而保留它的领导地位。前提是只要它不会崩溃,并且可以访问。

View File

@ -0,0 +1,148 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 再谈一致性:除了 CAP 之外的一致性模型还有哪些?
在“05 | 一致性与 CAP 模型:为什么需要分布式一致性”中,我们讨论了分布式数据库重要的概念——一致性模型。由于篇幅的限制,我在该部分只谈到了几种数据端(服务端)的强一致模型。那么这一讲,我们将接着讨论剩下的一致性模型,包括客户端(会话)一致性、最终一致性,等等。
现在我就和你一起,把一致性模型的知识体系补充完整。
完整的一致性模型
完整的一致性模型如下图所示。
图中不同的颜色代表了可用性的程度,下面我来具体说说。
粉色代表网络分区后完全不可用。也就是 CP 类的数据库。
黄色代表严格可用。当客户端一直访问同一个数据库节点,那么遭遇网络分区时,在该一致性下依然是可用的。它在数据端或服务端,被认为是 AP 数据库;而从客户端的角度被认为是 CP 数据库。
蓝色代表完全可用。可以看到其中全都是客户端一致性,所以它们一般被认为是 AP 数据库。
我们看到图中从上到下代表一致性程度在降低。我在 05 讲中介绍的是前面三种一致性,现在要介绍剩下的几种,它们都是客户端一致性。
客户端一致性
客户端一致性是站在一个客户端的角度来观察系统的一致性。我们之前是从“顺序性”维度来研究一致性的,因为它们关注的是多个节点间的数据一致性问题。而如果只从一个客户端出发,我们只需要研究“延迟性”。
分布式数据库中,一个节点很可能同时连接到多个副本中,复制的延迟性会造成它从不同副本读取数据是不一致的。而客户端一致性就是为了定义并解决这个问题而存在的,这其中包含了写跟随读、管道随机访问存储、读到已写入、单增读和单增写。
写跟随读Writes Follow Reads
WFR 的另一个名字是回话因果session causal。可以看到它与因果一致的区别是它只针对一个客户端。故你可以对比记忆它是对于一个客户端如果一次读取到了写入的值 V1那么这次读取之后写入了 V2。从其他节点看写入顺序一定是 V1、V2。
WFR 的延迟性问题可以描述为:当写入 V1 时,是允许复制延迟的。但一旦 V1 被读取,就需要认为所有副本中 V1 已经被写入了,从而保证从副本写入 V2 时的正确性。
管道随机访问存储PRAM/FIFO
管道随机访问存储的名字来源于共享内存访问模型。像 05 讲中我们提到的那样,分布式系统借用了并发内存访问一致性的概念来解释自己的问题。后来,大家觉得这个名字很怪,故改用 FIFO也就是先进先出来命名分布式系统中类似的一致性。
它被描述为从一个节点发起的写入操作,其他节点与该节点的执行顺序是一致的。它与顺序一致性最大的区别是,后者是要求所有节点写入都是有一个固定顺序的;而 PRAM 只要求一个节点自己的操作有顺序,不同节点可以没有顺序。
PRAM 可以拆解为以下三种一致性。
读到已写入Read Your Write一个节点写入数据后在该节点或其他节点上是能读取到这个数据的。
单增读Monotonic Read它强调一个值被读取出来那么后续任何读取都会读到该值或该值之后的值。
单增写Monotonic Write如果从一个节点写入两个值它们的执行顺序是 V1、V2。那么从任何节点观察它们的执行顺序都应该是 V1、V2。
同时满足 RYW、MR 和 MW 的一致性就是 PRAM。PRAM 的实现方式一般是客户端一直连接同一个节点,因为读写同一个节点,故不存在延迟性的问题。
我们可以将 PRAM 与 WFR 进行组合,从而获得更强的因果一致。也就是一个客户端连接同一个节点,同时保持回话因果一致,就能得到一个普通的因果一致。这种模式与 05 讲中介绍的是不一样的,这次我们是采用模型递推的模式去构建一致性,目的是方便模型记忆。但这并不代表因果一致一定是这种模型化的构建方式;相反,在 05 讲中介绍的时间戳模式更为普遍。
我们刚才说到PRAM 是严格可用的,并不是完全可用,如果要完全可用一般可以牺牲 RYW只保留 MR 和 MW。这种场景适合写入和读取由不同的客户端发起的场景。
至此,我们已经将所有的强一致模型介绍了一遍。掌握上面那个图,你就掌握了完整的一致性模型。下面我要为你介绍最弱的一致性模型,也就是最终一致。
最终一致性
最终一致性是非常著名的概念。随着互联网和大型分布式系统的发展,这一概念被广泛地传播。它被表述为副本之间的数据复制完全是异步的,如果数据停止修改,那么副本之间最终会完全一致。而这个最终可能是数毫秒到数天,乃至数月,甚至是“永远”。
最终一致性具有最高的并发度,因为数据写入与读取完全不考虑别的约束条件。如果并发写入修改同一份数据,一般采用之前提到的一些并发冲突解决手段来处理,比如最后写入成功或向量时钟等。
但是最终一致性在分布式数据库中是完全不可用的。它至少会造成各种偏序skew现象比如写入数据后读取不出来或者一会儿能读取出来一会儿又读取不出来。因为数据库系统是底层核心系统许多应用都构建在它上面此种不稳定表现在分布式数据库设计中是很难被接受的。故我们经常采用可调节的最终一致性来实现 AP 类的分布式数据库。
可调节一致性
一般的分布式系统的写入和读取都是针对一个节点,而可调节一致性针对最终一致性的缺点,提出我们可以同时读取多个节点。现在我们引入可调节一致性设计的三个变量。
副本数量 N是分布式集群中总的节点数量也就是副本总量。
最少并发写入数量 W当一份数据同步写入该数量的节点后我们认为该数据是写入成功的。
最少并发读取数量 R当读取数据时至少读取该数量的节点比较后选择最终一份最新的数据。如此我们才认为一次读取操作是成功的。
当分布式系统的并发读写数量满足下面的公式:
W + R > N
这时我们认为该系统的并发度可以满足总是能读取到最新的数据。因为你可以发现,写入节点与读取的节点之间肯定存在重合,所以每次读取都会发现最新写入的一个节点。
一个常见的例子是 N=3、W=2 和 R=2。这个时候系统容忍一个节点失效。正常情况下三个节点都能提供读写服务如果其中一个节点失效读写的最少节点数量依然可以满足。在三个节点同时工作的情况下最新数据至少在两个节点中所以从三个里面任意读取两个其中至少一个节点存在最新的数据。
你可能注意到,我上文用了很多“最少”这种描述。这说明在实际中实现这种分布式数据库时,可以在写入时同时写入三个节点。但此时只要其中两个节点返回成功,我们就认为写入成功了。读取也同样,我们可以同时发起三个读取,但只需要获取最快返回的两个结果即可。
那么有的人会问,为什么不每次写入或读取全部节点呢?我的回答是也可以的,比如对于写入负载较高的场景可以选择 W=1、R=N相反对于读取负载高的场景可以选择 W=N、R=1。你不难发现这两种模式分别就是上文讨论的强一致性前者是客户端一致性后者是数据一致性同步复制。故可调节一致性同时涵盖了弱一致性到强一致性的范围。
如何选择 W 和 R 呢?增加 W 和 R 会提高可用性,但是延迟会升高,也就是并发度降低;反之亦然。一个常用的方式是 Quorums 方法,它是集群中的大多数节点。比如一个集群有 3 个节点Quorums 就是 2。这个概念在分布式数据库中会反复提及比如领导选举、共识等都会涉及。
对于可调节一致性,如果我们的 W 和 R 都为 Quorums那么当系统中失败节点的数量小于 Quorums 时,都可以正常读写数据。该方法是一种在并发读与可用性之间取得最佳平衡的办法。因为 W 和 R 比 Quorums 小,就不满足 W+R>N而大于 Quorums 只降低了并发度,可用性是不变的。因为 W 和 R 越大,可失败的节点数量越小。
但是使用 Quorums 法有一个经典的注意事项,那就是节点数量应为奇数,否则就无法形成多数的 Quorums 节点。
Witness 副本
我在上文介绍了利用 Quorums 方法来提高读取的可用性。也就是写入的时候写入多个副本,读取的时候也读取多个副本,只要这两个副本有交集,就可以保证一致性。虽然写入的时候没有写入全部副本,但是一般需要通过复制的方式将数据复制到所有副本上。比如有 9 个节点Quorums 是 5即使一开始写入了 5 个节点,最终 9 个节点上都会有这一份数据。这其实增加了对于磁盘的消耗,但是对于可用性没有实质的提高。
我们可以引入 Witeness 副本来改善上面这种情况,将集群中的节点分为复制节点与 Witness 节点。复制节点保存真实数据,但 Witeness 节点在正常情况下不保存数据。但是当集群中的可用节点数量降低的时候,我们可以将一部分 Witeness 节点暂时转换为可以存储数据的节点。当集群内节点恢复后,我们又可以将它们再次转换为 Witeness 节点,并释放上面存储的数据。
那么需要使用多少个 Witeness 副本来满足一致性呢?假设我们现在有 r 个复制副本和 w 个 Witeness 副本。那么总副本数量为 r+w需要满足下面两个规则
读写操作必须要有 Quorums 数量的节点,也就是 (r+w)/2+1 个节点参与;
在条件 1 给出的节点中,至少有一个节点是复制节点。
只要满足这两条规则,就可以保证 Witeness 节点的加入是满足一致性要求的。
现在分布式数据库广泛使用 Witeness 节点来提高数据存储的利用率,如 Apache Cassandra、Spanner 和 TiDB 等。但是它们的使用方法不尽相同,感兴趣的话你可以自己深入研究。
CRDT 算法
上文我们探讨了最终一致性方案,除了使用可调节手段来保持一致性外。我们可以使用 Conflict-Free Replicated Data TypeCRDT来解决最终一致的数据冲突问题。
CAP 理论提出者 Eric Brewer 撰文回顾 CAP 时也提到C 和 A 并不是完全互斥,建议大家使用 CRDT 来保障一致性。自此各种分布式系统和应用均开始尝试 CRDT微软的 CosmosDB 也使用 CRDT 作为多活一致性的解决方案,而众多云厂商也使用 CRDT 来制定 Redis 的多活一致性方案。
由于目前 CRDT 算法仍然处于高速发展的阶段,为了方便你理解,我这里选取携程网内部 Redis 集群一致性方案,它的技术选型相对实用。如果你对 CRDT 有兴趣,可以进一步研究,这里就不对诸如 PN-Counter、G-Set 等做进一步说明了。
由于 Redis 最常用的处理手段是设置字符串数据,故需要使用 CRDT 中的 register 进行处理。携程团队选择了经典的 LWW Regsiter也就是最后写入胜利的冲突处理方案。
这种方案,最重要的是数据上需要携带时间戳。我们用下图来说明它的流程。
从图中可以看到,每个节点的数据是一个二元组,分别是 value 和 timestamp。可以看到节点间合并数据是根据 timestamp也就是谁的 timestamp 大,合并的结果就以哪个值为准。使用 LWW Register 可以保证高并发下合并结果最终一致。
而删除时,就需要另外一种算法了。那就是 Observed-Remove SETOR Set其主要的目的是解决一般算法无法删除后重新增加该值的情况。
它相较于 LWW-Register 会复杂一些,除了时间戳以外,还需要给每个值标记一个唯一的 tag。比如上图中 P1 设置13实际需要设置1α3而后如果删除 1集合就为空再添加 1 时标签就需要与一开始不同5。这样就保证步骤 2 中的删除操作不会影响步骤 3 中的增加操作。因为它们虽然数值相同,但是标签不同,所以都是唯一的。
以上就是 Redis 跨 IDC 异步同步的核心技术方案,当然细节还是有很多的,有兴趣的话你可以自行学习。
总结
到这里,我们已经学习了分布式数据库中关于一致性问题的全部内容。这部分你要理解一致性模型图,从而可以系统地掌握数据端一致性与客户端一致性;同时结合 CAP 理论体会不同一致性所对应的可用性。
最终一致性一般应用在跨数据中心、跨区域节点这种无主同步的场景,使用可调节的一致性和 CRDT 算法可以保证同步的一致性。
学习一致性部分后,我们就可以评估各种分布式数据库文档中的一致性概念了,从而理解它们背后的设计理念。在本模块的最后一讲,我会举例说明一些分布式数据库一致性方面的背后逻辑。
欢迎你和我一起思考,祝你每天能强大一点。下一讲我们将探讨数据是如何可靠进行传输的,希望准时与你相见。

View File

@ -0,0 +1,112 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 知识串讲:如何取得性能和可扩展性的平衡?
这一讲我们来总结一下模块三。经过这个模块的学习,相信你已经对分布式数据库中分布式系统部分常见的技术有了深刻的理解。这是一节知识串讲的课,目的是帮助你将所学的内容串接起来;同时,我会通过若干的案例带你看看这些知识是如何应用到分布式数据库之中的。
知识总结
在本模块一开始我就提出了分布式系统最需要关心的问题失败。失败可以说是分布式系统中无处不在的一种现象它不仅来源于网络抖动带来的连接问题同时分布式的节点本身的稳定性也会影响系统整体的稳定状态。在“13 | 概要:分布式系统都要解决哪些问题”中我们就定义了失败模型,它们分别是:崩溃失败、遗漏失败以及任意失败。
其中分布式数据库一般研究前两种失败,因为任意失败是假设节点伪造数据,这对基于安全网络而构建的分布式数据库来说是一种不常见的情况。失败模型从上到下失败的原因越来越难以预知,同时处理的难度也越来越大。本模块介绍的绝大部分技术与算法,都是处理第一种失败场景的,而共识算法主要解决第二种失败场景。
而后我们介绍了针对失败的探测手段,其中基于心跳的算法广泛地应用在分布式数据库之中。同时对于无主节点的对等分布式数据库,基于 Gossip 算法的失败检测被用于该场景的分布式数据库,如 Apache Cassandra。
在介绍了失败检测之后,我提到了主从模式的分布式系统。具有主节点的分布式数据库相对于完全对等的分布式数据库来说,具有性能高、状态易于预测等优点。在该部分,我们重点介绍了领导选举的方案,选举算法中我介绍了 Bully 算法及其多种变种,基本涵盖了主要的领导选举手段。共识算法部分如 ZAB、Raft 等协议的领导选举方案中都可以看到 Bully 算法的影子。
在介绍完领导选举以后我们接着扩展了模块一中关于复制与一致性的内容。补充说明了客户端一致性与最终一致性。至此关于一致性所有的内容在我们的课程体系中都已经介绍完整。在“16 | 再谈一致性:除了 CAP 之外的一致性模型还有哪些”中我引入了完整的一致性模型树,帮助你建立起一致性的完整知识体系。客户端一致性与最终一致性往往同时使用,非常适合 AP 类数据库,这类数据库以 AWS 的 DynamoDB 为代表,是一类无主节点、跨区域、可全球部署的分布式数据库,且一般为 NoSQL 数据库。除了 Dynamo 外,如 Apache Cassandra、Azue Cosmos 也是此类数据库典型的代表。
由于最终一致会导致数据库允许节点之前数据的暂时不一致,根据熵增理论,此种不一致会随着时间逐渐扩大,最终导致最终一致类数据库完全不可用。基于此种原因,我们引入了反熵的概念。反熵手段分为前台与后台两类,前者包括读修复、暗示传递队列等;后者有 Merkle 树和位图版本向量。而在“14 | 错误侦测:如何保证分布式系统稳定”提到的 Gossip 协议除了可以检测系统中的异常情况外其最重要的功能是保证消息在整个分布式系统内部可靠地传递从而实现了反熵的目的。Gossip 协议非常适用于大规模的分布式数据库,如 Apache Cassandra、Redis 等都使用该技术。
分布式事务是本模块的重点内容。我用了两讲来介绍它,首先说的是最典型的原子提交事务,分别为两阶段和三阶段提交,其中三阶段提交虽然对两阶段出现的问题进行了改进,但由于其性能较低,且存在脑裂的问题,故在现实场景中很少使用。而后我们介绍了快照隔离与序列化的快照隔离,它们是现代分布式数据库最重要的隔离级别,为实现无锁高性能的分布式事务提供了支持。而 Percolator 事务模型就是基于此种隔离级别的一种高效乐观无锁的事务方案。目前 TiDB 使用该方案来实现其乐观事务。
分布式事务一直是分布式数据库领域的理论创新热点。我在本模块中对比介绍了两种创新的事务模型 Spanner 和 Calvin前者使用 PaxosGroup 结合 TrueTime在平衡性方面取得了瞩目的成绩而后者在高竞争事务的吞吐量上给我们留下了深刻的印象。同时它们之间的理论争论为我们更好地认识其优缺点指明了方向可以说它们之间的论战对整个分布式事务理论的发展是非常有意义的同时我们也期待未来分布式事务理论能否产生出更加优秀的解决方案。
在本模块的最后一讲也就是“20 | 共识算法:一次性说清楚 Paxos、Raft 等算法的区别”,我们介绍了分布式系统理论的集大成者——分布式共识算法。共识算法可以说是失败模型、失败侦测、领导选举和一致性的合体。它通过单一的算法体系,实现了以上描述的分布式系统中的多种功能。从而为构建分布式数据库提供了强有力的帮助,如分布式事务和数据复制领域中,我们会发现许多方案都使用了共识算法。目前在分布式数据库中,最为常见的共识算法是 Raft它是在 Multi-Paxos 基础上经过一定简化得到的,其易于实现、快速恢复等特点对分布式数据库的维护者而言是非常有吸引力的。
至此我们介绍完了本模块的全部内容。下面我将通过一些具体的案例来把所学知识应用在实际中。在案例部分我选择了三个比较典型的数据库TiDB、阿里的 PolarDB-X 和 Apache Cassandra。它们是目前分布式数据库比较典型的代表让我们来看看它们是如何使用本模块知识点的。
TiDB使用乐观事务打造悲观事务
在分布式事务那一讲,我提到 TiDB 的乐观事务使用了 Google 的 Percolator 模式,同时 TiDB 也对该模式进行了改进。可以说一提到 Percolator 模式事务的数据库,国内外都绕不过 TiDB。
TiDB 在整体架构上基本参考了 Google Spanner 和 F1 的设计,分两层为 TiDB 和 TiKV。 TiDB 对应的是 Google F1是一层无状态的 SQL Layer兼容绝大多数 MySQL 语法,对外暴露 MySQL 网络协议,负责解析用户的 SQL 语句,生成分布式的 Query Plan翻译成底层 Key Value 操作发送给 TiKV。TiKV 是真正的存储数据的地方,对应的是 Google Spanner是一个分布式 Key Value 数据库,支持弹性水平扩展,自动地灾难恢复和故障转移,以及 ACID 跨行事务。下面的图展示了 TiDB 的架构。
对于事务部分TiDB 实现悲观事务的方式是非常简洁的。其团队在仔细研究了 Percolator 的模型后发现,其实只要将在客户端调用 Commit 时候进行两阶段提交这个行为稍微改造一下,将第一阶段上锁和等锁提前到事务中执行 DML 的过程中,就可以简单高效地支持悲观事务场景。
TiDB 的悲观锁实现的原理是,在一个事务执行 DMLUPDATE/DELETE的过程中TiDB 不仅会将需要修改的行在本地缓存,同时还会对这些行直接上悲观锁,这里的悲观锁的格式和乐观事务中的锁几乎一致,但是锁的内容是空的,只是一个占位符,等到 Commit 的时候,直接将这些悲观锁改写成标准的 Percolator 模型的锁,后续流程和原来保持一致即可。
这个方案在很大程度上兼容了原有的事务实现,其扩展性、高可用和灵活性都有保证。同时该方案尽最大可能复用了原有 Percolator 的乐观事务方案,减少了事务模型整体的复杂度。
以上就是 TiDB 如何使用 Percolator 模型及其变种同时实现了乐观事务与悲观事务。下面我来介绍一下阿里的 PolarDB-X 是如何利用共识算法打造异地多活分布式数据库的。
PolarDB-X使用 Paxos 打造异地多活体系
阿里随着业务的高速增长“异地多活”成了其应用的新标准。基于这样的业务背景驱动PolarDB-X 早期基于单机 MySQL 实现了一致性能力,配合 TDDL 分库分表的模式部分解决了业务诉求,该模块被称为 X-Paxos。随着技术的不断发展和演进以及面向云的时代的全面普及PolarDB-X 2.0 中融合了分布式 SQL 引擎和基于 X-Paxos 的数据库存储技术,提供了全新的云原生分布式数据库。
X-Paxos 的算法基于具有领导节点的 Multi-Paxos 来实现。就像我在共识那一讲介绍的一样,这是被大量工程实践证明是最高效的一种 Paxos 算法。
在基础算法之上结合阿里是业务场景以及高性能和生态的需求X-Paxos 做了很多的创新性的功能和性能的优化,使其相对于基础的 Multi-Paxos功能变得更加丰富性能也有明显的提升。这里我介绍 X-Paxos 的几个优化点。
有主选举
X-Paxos 在标准 Multi-Paxos 的基础上,支持在线添加/删除多种角色的节点,支持在线快速将领导节点转移到其他节点。这样的在线运维能力,将极大地方便分布式节点的有计划性的运维工作,从而降低业务恢复时间。
可用区位置感知
阿里目前多地架构会有中心机房的诉求,比如,应用因其部署的特点,往往要求在未发生城市级容灾的情况下,仅在中心写入数据库,数据库的领导节点在正常情况下只在中心地域;同时又要求在发生城市级容灾的时候(同一个城市的多个机房全部不可用),可以完全不丢失任何数据的情况下,将主节点切换到非中心城市。
节点功能裁剪
Paxos 算法中每个节点都包含了 Proposer/Accepter/Learner 三种功能每一个节点都是全功能节点。但是某些情况下集群并不需要所有节点都拥有全部的功能。X-Paxos 使用了如下的一些裁剪手段:
裁剪其中一些节点的状态机,只保留日志(无数据的纯日志节点,但是在同步中作为 Quroum 计算),此时需要裁剪掉协议中的 Proposer 功能,保留 Accepter 和 Learner 功能;
一些节点只是订阅/消费协议产生的日志流,而不作为集群的成员,此时可以裁剪掉协议的 Proposer/Accepter 功能,只保留 Learner 功能。
以上裁剪手段的组合,可以提高集群利用率、节约成本,同时得到了比较灵活的功能组合。
这就是 PolarDB-X 使用共识算法的一系列尝试,最后让我们看看 Apache Cassandra 是如何实现可调节一致性的。
Apache Cassandra可调节一致性
Apache Cassandra 提供了可调节一致性,允许开发者在数据一致性和可用性之间做出权衡,并且这种灵活性由客户端来管理。一致性可以是全局的,也可以针对单个读取和写入操作进行调整。例如在更新重要数据时,需要高度的一致性。 对于不太关键的应用或服务,可以放宽一致性以实现更好的性能。
Cassandra 的可调节一致性如我在本模块一致性那一讲介绍的一样,分为写一致性与读一致性。
写一致性
写一致性声明了需要写入多少个节点才算一次成功的写入。Cassandra 的写一致性是可以在强一致到弱一致之间进行调整的。我总结了下面的表格来为你说明。
我们可以看到 ANY 级别实际上对应了最终一致性。Cassandra 使用了反熵那一讲提到的暗示切换技术来保障写入的数据的可靠,也就是写入节点一旦失败,数据会暂存在暗示切换队列中,等到节点恢复后数据可以被还原出来。
读一致性
对于读操作,一致性级别指定了返回数据之前必须有多少个副本节点响应这个读查询。这里同样给你整理了一个表格。
Cassandra 在读取的时候使用了读修复来修复副本上的过期数据,该修复过程是一个后台线程,故不会阻塞读取。
以上就是 Apache Cassandra 实现可调节一致性的一些细节。AWS 的 DynamoDB、Azure 的 CosmosDB 都有类似的可调节一致性供用户进行选择。你可以比照 Cassandra 的模式和这些数据库的文档进行学习。
总结
分布式系统是分布式数据库的核心,它与存储引擎相互配合共同实现了完整的分布式数据库的功能。存储引擎一般影响数据库的写入,也就是数据库的性能,它决定了数据库到底能多快地处理数据。同时,分布式系统处理节点直接的通信,也就是数据库的扩展性,它决定了数据库的扩展能力和数据容量。故两者需要相互配合,同时在设计使用数据库时,可以在它们之间进行一定的取舍,从而达到高效利用各种资源的目的。
分布式系统这个模块,我们介绍了分布式数据库经常使用的知识点,特别是事务、一致性和共识是本模块的核心,希望你能好好掌握。
下一模块我们就进入实际案例中,我将分门别类地为你介绍市面中典型的分布式数据库。利用课程中的知识来分析它们,从而更好地帮助你去使用它们。
感谢学习,我们下一讲再见。

View File

@ -0,0 +1,126 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 发展与局限:传统数据库在分布式领域的探索
从这一讲开始,我们进入实践(扩展)模块,目的是帮助你更了解现代分布式数据库,并且我会把之前学习的理论知识应用到实际案例中。
这个模块的讲解思路如下。
传统数据库分布式:传统数据库,如 Oracle、MySQL 和 PostgreSQL 没有一刻放弃在分布式领域的探索。我会介绍分布式技术如何赋能传统数据库,以及它们的局限性。
数据库中间件:虽然中间件严格来说并不包含在数据库领域内,但它是很多用户首次接触分布式数据库的切入点,故在该领域有着不可代替的作用。我会介绍数据库中间件的功能,特别是处理事务的方式,它与模块三中介绍的分布式事务还是有差别的。
当代分布式数据库:该部分重点介绍目前大家能接触到的 NewSQL、DistributedSQL 类型的数据库。重点关注它们在异地多活、容灾等方面的实践。
其他类型数据库与数据库选型:查缺补漏为你介绍其他类型的分布式数据库,扩宽你的视野。最后结合金融、电信和电商等场景,为你介绍这些行业是如何选择分布式数据库的。
以上就是本模块整体的讲解流程,那么现在让我们进入第一个问题的学习,来看看传统数据库如何进行分布式化改造。
传统数据库分布式化
我在模块一中介绍过,业务应用系统可以按照交易类型分为 OLTP 场景和 OLAP 场景两大类。OLTP 是面向交易的处理过程,单笔交易的数据量小,但是要在很短的时间内给出结果,典型场景包括购物、转账等;而 OLAP 场景通常是基于大数据集的运算,典型场景包括生成各种报表等。
OLTP 与 OLAP 两种场景有很大的差异,虽然传统数据库在其早期是将两者融合在一起的。但是随着它们向分布式,特别是 Sharding分片领域转型OLAP 类型的数据逐步被抛弃,它们将所有的精力集中在了 OLTP 上。
OLTP 场景通常有三个特点:
写多读少,而且读操作的复杂度较低,一般不涉及大数据集的汇总计算;
低延时,用户对于延时的容忍度较低,通常在 500 毫秒以内,稍微放大一些也就是秒级,超过 5 秒的延时通常是无法接受的;
高并发,并发量随着业务量而增长,没有理论上限。
传统数据库,比如 MySQL 和 Oracle 这样的关系型数据库就是服务于 OLTP 场景的,但我们一般认为它们并不是分布式数据库。这是为什么呢?因为这些数据库传统都是单节点的,而我们说的分布式数据库都是多节点的。
传统关系型数据库是单机模式的也就是主要负载运行在一台机器上。这样数据库的并发处理能力与单机的资源配置是线性相关的所以并发处理能力的上限也就受限于单机配置的上限。这种依靠提升单机资源配置来扩展性能的方式被称为垂直扩展Scale Up。我们之前介绍过垂直扩展是瓶颈的因为物理机单机配置上限的提升是相对缓慢的。这意味着在一定时期内依赖垂直扩展的数据库总会存在性能的天花板。
那么传统数据库的单机模式可以变为分布式吗?答案是可以的。这些传统数据库在维持关系型数据库特性不变的基础上,可以通过水平扩展,也就是 Sharding 模式,增加机器数量、提供远高于单体数据库的并发量。这个并发量几乎不受单机性能限制,我们将这个级别的并发量称为“高并发”。这里说的“高并发”并没有一个具体的数字与之对应。不过,我可以给出一个经验值,这个“高并发”应该至少大于一万 TPS。
在 Sharding 之外,还需要引入可靠的复制技术,从而提高系统整体的可用度,这在金融级的容灾场景中非常重要。这些理念都是我在模块一中就强调过的,分片与同步才是分布式数据库的核心。
那么介绍完了传统数据库如何改造为分布式数据库的基本理念,现在让我们看看它们是如何具体操作的吧。
商业产品
我在“01 | 导论:什么是分布式数据库?聊聊它的前世今生”介绍过,商业数据库如 Oracle 通过底层存储的分布式达到数据分散的目的。其实这类数据库一直没有放弃对分布式领域的探索。现在我介绍一下 Oracle Sharding。
Oracle 数据库从 12.2 版本开始引入 Sharding 特性,集成了 NoSQL 和成熟的关系型数据库的优势到如今已经过多个版本迭代成为一整套成熟的分布式关系型数据库解决方案。Oracle Sharding 可以让用户将数据分布和复制到一组 Oracle 数据库集群中集群中的数据库只需要网络连接不需要共享软件和硬件。Oracle Sharding 可以为应用提供线性扩展能力和完全容错能力。
Oracle Sharding 主要包括下面这些组件。
Sharded databaseSDB逻辑上 SDB 是一个数据库,但是物理上 SDB 包括多个物理独立的数据库SDB 类似一个数据库池pool数据库池中包括多个数据库Shard
ShardsSDB 包括多个物理独立的数据库,每一个数据库都称为 shard每个 shard 数据库位于不同的服务器。这些 Shard 被部署在独立的机器上,每个 shard 数据库中保存表的不同数据集,但是每个 Shard 中都有相同的列,也就是说这些 Shard 是按行进行分片的。
Shard catalog是一个 Oracle 数据库,用于集中存储管理 SDB 配置信息,是 SDB 的核心。SDB 配置变化,比如添加/删除 shard 等,都记录在 Shard catalog。如果应用查询多个 shard 中的数据,那么由 Shard catalog 统一协调分配。Shard catalog 需要进行 HA也就是高可用部署。因为里面的数据非常重要一旦丢失会造成整个数据库不可用。
Shard directorsGlobal Data ServiceGDS实现对 Sharding 的集中部署和管理。GSM 是 GDS 的核心组件GSM 作为 Shard director。GSM 类似监听器,将客户端对 SDB 的请求路由到对应的 shard负载均衡客户端的访问。
Oracle Sharding 优点如下。
线性扩展:因为每个 shard 是一个独立的数据库,通过增加新的 Shard 节点,来线性扩展性能,自动 rebalance 数据。
失败隔离:由于 Shard 是一种 shared-nothing 技术,每个 shard 使用独立的硬件,因此一个 shard 节点出现故障,只会影响到这个 shard 存放的数据,而不会影响到其他 shard。
按照地理位置分布数据:可以选择根据地理位置不同,将数据存储在不同的 shard。
除了以上的优点,其缺点也非常明显。
用户设计复杂:不同于传统的 RAC 模式Sharding 需要用户对表进行严格设计,从而才能发挥该模式扩展性与可用性方面的优势。同时,对于老系统迁移,这往往意味着要修改现有代码。
跨分片性能低:跨分片事务,聚合查询的性能很低。一般比单分片低 10%。
最后一个缺点就是商业数据库的老问题,性价比低。这个我在后面会进一步阐述。
那么商业方案看起来很好,但是如果你更喜欢开源的解决方案,下面我会介绍开源传统数据库对这个问题的思考。
开源定制
单体开源数据要向分布式数据库演进,就要解决写入性能不足的问题。
最简单直接的办法就是分库分表。分库分表方案就是在多个单体数据库之前增加代理节点,本质上是增加了 SQL 路由功能。这样,代理节点首先解析客户端请求,再根据数据的分布情况,将请求转发到对应的单体数据库。代理节点分为“客户端 + 单体数据库”和“中间件 + 单体数据库”两个模式。
客户端组件 + 单体数据库通过独立的逻辑层建立数据分片和路由规则,实现单体数据库的初步管理,使应用能够对接多个单体数据库,实现并发、存储能力的扩展。其作为应用系统的一部分,对业务侵入比较深。这种客户端组件的典型产品是 Apache ShardingShpere 的 JDBC 客户端模式,下图就是该模式的架构图。
Apache ShardingShpere 的 JDBC 客户端模式架构图
代理中间件 + 单体数据库以独立中间件的方式,管理数据规则和路由规则,以独立进程存在,与业务应用层和单体数据库相隔离,减少了对应用的影响。随着代理中间件的发展,还会衍生出部分分布式事务处理能力。这种中间件的典型产品是 MyCat、Apache ShardingShpere 的 Proxy 模式。
Apache ShardingShpere 的 Proxy 模式架构图
代理节点需要实现三个主要功能,它们分别是客户端接入、简单的查询处理器和进程管理中的访问控制。另外,分库分表方案还有一个重要的功能,那就是分片信息管理,分片信息就是数据分布情况。不过考虑分片信息也存在多副本的一致性的问题,大多数情况下它会独立出来。显然,如果把每一次的事务写入都限制在一个单体数据库内,业务场景就会很受局限。
因此,跨库事务成为必不可少的功能,但是单体数据库是不感知这个事情的,所以我们就要在代理节点增加分布式事务组件。同时,简单的分库分表不能满足全局性的查询需求,因为每个数据节点只能看到一部分数据,有些查询运算是无法处理的,比如排序、多表关联等。所以,代理节点要增强查询计算能力,支持跨多个单体数据库的查询。更多相关内容我会在下一讲介绍。
这时离分布式数据库还差重要的一步,那就是逻辑时钟。我们在分布式系统模块已经介绍了逻辑时钟的意义,它是实现数据一致性的必要条件。加上这最后一块拼图,这类分布式数据库区别于单体数据库的功能也就介绍完整了,它们是分片、分布式事务、跨节点查询和逻辑时钟。
这类数据库一般以 MySQL 或 PostgreSQL 为基础进行开发。MySQL 类的解决方案有 TDSQL、Vitess 和具有 JDTX 的 ShardingShpere。PGXCPostgreSQL-XC的本意是指一种以 PostgreSQL 为内核的开源分布式数据库。因为 PostgreSQL 的开放软件版权协议,很多厂商在 PGXC 上二次开发,推出自己的产品。不过,这些改动都没有变更主体架构风格,所以我把这类产品统称为 PGXC 风格,其中包括 TBase、GuassDB 和 AntDB 等。
以上我们讨论了开源领域中传统数据库在分布式领域中的尝试。但是,此类方案是有一些局限的,看看都有哪些。
局限
目前传统数据库在分布式领域内的探索,我们可以总结为“商业靠实力而开源靠合作”,它们分别打开了自己的一片天地。但是,它们长久的技术积累不仅带来了功能的丰富,同时一些局限也是其无法克服的。
性价比。以 Oracle Sharding 为代表的商业解决方案,虽然功能很完善,同时能满足多种场景,对传统 Oracle 用户有极强的吸引力。但是其费用与收益其实是不成正比的,其对分片事务支持有限,同时跨分片查询性能很低。这些重要功能的缺失与其高昂的售价相比是极不相称的。故商业的 Sharding 方案一直没有成为主流。
事务。由于传统数据库都需要复用原有的存储节点,故事务方案大多都是我们介绍过的两阶段提交这类原子提交协议。学习过模块三中分布式事务的同学都清楚,传统两阶段在性能和规模上都有很大的限制,必须采用新的事务模式才能突破这层天花板。而传统数据库的底层被锁死,很难在这个领域有更好的表现。
OLAP。传统数据库在转为分布式之前能很好地支持 OLAP。但其 Sharding 后,该过程变得越来越困难。同时随着大数据技术的崛起,它们有主动放弃该领域的趋势。而新一代的 HTAP 架构无一例外都是 NewSQL 和云原生数据库的天下,这个领域是从传统数据库发展而来的分布式数据库无法企及的。
以上我们谈的传统数据库在分布式领域的局限其实总结为一点就是,它们的底层存储引擎限制了其上层分布式功能的拓展。只有如 NewSQL 类数据库一般,使用创新的存储引擎,才能在整体上打造出功能与性能匹配的现代分布式数据库。但是,此类数据库由于发展多年,在稳定性、维护性上有不可动摇的优势,即使存在一些局限性,但其对单机版本的用户依然有很强的吸引力。
总结
这一讲,我们介绍了传统单机数据库向分布式数据库的转型尝试,它们一般经过分片、复制、分布式事务和物理时钟等过程的改造,从而打造以单体数据库为数据节点的分布式数据库。
同时我们也讨论了此类数据库的天花板,因此应该从底层去构建分布式数据库,就像 NewSQL 类数据库,才是分布式数据库发展的正途。

View File

@ -0,0 +1,157 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 数据库中间件:传统数据库向分布式数据库的过渡
上一讲我们讨论了传统单机数据库向分布式数据库的转型尝试。今天这一讲,我们就来聊聊传统数据库构造为分布式数据库的帮手,同时也是分布式数据库演化的重要一环:数据库中间件。这里说的中间件一般是具有分片功能的数据库中间层。
关系型数据库本身比较容易成为系统性能瓶颈,单机存储容量、连接数、处理能力等都很有限,数据库本身的“有状态性”导致了它并不像 Web 和应用服务器那么容易扩展。在互联网行业海量数据和高并发访问的考验下,应用服务技术人员提出了分片技术(或称为 Sharding、分库分表。同时流行的分布式系统数据库特别是我们上一讲介绍的从传统数据库过渡而来的分布式数据库本身都友好地支持 Sharding其原理和思想都是大同小异的。
成功的数据库中间件除了支持分片外,还需要全局唯一主键、跨分片查询、分布式事务等功能的支持,才能在分片场景下保障数据是可用的。下面我就为你一一介绍这些技术。
全局唯一主键
在单机数据库中,我们往往直接使用数据库自增特性来生成主键 ID这样确实比较简单。而在分库分表的环境中数据分布在不同的分片上不能再借助数据库自增长特性直接生成否则会造成不同分片上的数据表主键重复。
下面我简单介绍下使用和了解过的几种 ID 生成算法:
Twitter 的 Snowflake又名“雪花算法”
UUID/GUID一般应用程序和数据库均支持
MongoDB ObjectID类似 UUID 的方式)
其中Twitter 的 Snowflake 算法是我近几年在分布式系统项目中使用最多的,未发现重复或并发的问题。该算法生成的是 64 位唯一 ID由 41 位的 timestamp + 10 位自定义的机器码 + 13 位累加计数器组成。我在“03 | 数据分片:如何存储超大规模的数据”中介绍过 ShardingShpere 实现 Snowflake 的细节,你可以再回顾一下。
那么解决了全局唯一主键,我们就可以对数据进行分片了。下面为你介绍常用的分片策略。
分片策略
我介绍过的分片模式有:范围分片和哈希分片。
当需要使用分片字段进行范围查找时,范围分片可以快速定位分片进行高效查询,大多数情况下可以有效避免跨分片查询的问题。后期如果想对整个分片集群扩容时,只需要添加节点即可,无须对其他分片的数据进行迁移。
但是,范围分片也有可能存在数据热点的问题,有些节点可能会被频繁查询,压力较大,热数据节点就成了整个集群的瓶颈。而有些节点可能存的是历史数据,很少需要被查询到。
哈希分片我们采用 Hash 函数取模的方式进行分片拆分。哈希分片的数据相对比较均匀,不容易出现热点和并发访问的瓶颈。
但是,后期分片集群扩容起来需要迁移旧的数据。使用一致性 Hash 算法能够很大程度地避免这个问题,所以很多中间件的分片集群都会采用一致性 Hash 算法。离散分片也很容易面临跨分片查询的复杂问题。
很少有项目会在初期就开始考虑分片设计的,一般都是在业务高速发展面临性能和存储的瓶颈时才会提前准备。因此,不可避免地就需要考虑历史数据迁移的问题。一般做法就是通过程序先读出历史数据,然后按照指定的分片规则再将数据写入到各个分片节点中。我们介绍过 ShardingShpere 的弹性伸缩正是解决这个问题的有力武器。
此外,我们需要根据当前的数据量和 QPS 等进行容量规划,综合成本因素,推算出大概需要多少分片(一般建议单个分片上的单表数据量不要超过 1000W
数据分散到不同的数据库、不同的数据表上,此时如果查询跨越多个分片,必然会带来一些麻烦。下面我将介绍几种针对分片查询不同的策略。
跨分片查询
中间件跨分片查询,本质上讲原本由数据库承担的数据聚合过程转变到了中间件层。而下面介绍的几种方案,其原理都来源于存储引擎层面。
分页查询
一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。
在分布式的场景中将“LIMIT 1000000010”改写为“LIMIT 010000010”才能保证其数据的正确性。为什么这样呢你可以仔细想想。结果就是此种模式会将大量无用数据加载到内存中从而给内存带来极大的压力。一般解决的手段是避免使用 LIMIT 关键字,而是直接用如下的模式。
SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id;
而在翻页时,通过记录上一页最后一条数据的位置,从而减少数据的加载量。
聚合函数
在使用 Max、Min、Sum、Count 和 Avg 之类的函数进行统计和计算的时候,需要先在每个分片数据源上执行相应的函数处理,然后再将各个结果集进行二次处理,最终再将处理结果返回。这里要注意 Avg 函数的实现比较特殊,需要借助 Sum 和 Count 两个函数的实现逻辑进行配合。
跨分片 Join
Join 是关系型数据库中最常用的特性但是在分片集群中Join 也变得非常复杂,我们应该尽量避免跨分片的 Join查询这种场景比上面的跨分片分页更加复杂而且对性能的影响很大
通常有以下两种方式来对其进行优化。
全局表。全局表的基本思想就是把一些类似数据字典又可能会产生 Join 查询的表信息放到各分片中,从而避免跨分片的 Join。
ER 分片。在关系型数据库中,表之间往往存在一些关联的关系。如果我们可以先确定好关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能很好地避免跨分片 Join 问题。在一对多关系的情况下,我们通常会选择按照数据较多的那一方进行拆分。
以上就是分布式中间件实现跨分片查询的一些细节。下面我要为你介绍的是中间件面临的最大的挑战——分布式事务。
分布式事务
此处的分布式事务与上一讲的传统数据库发展而来的分布式数据库面临的困难是类似的。那就是,中间件只能与数据库节点进行交互,而无法影响底层数据结构。从而只能从比较高的层次去解决问题,所以下面要介绍的众多方案都有各自的缺点。
客户端一阶段
这是通过客户端发起的一种事务方案,它去掉了两阶段中的 Prepare 过程。典型的实现为:在一个业务线程中,遍历所有的数据库连接,依次做 Commit 或者 Rollback。这种方案对数据库有一种假设那就是底层数据库事务是做“前向检测”模块二事务也就是 SQL 执行阶段就可以发现冲突。在客户端进行 Commit 时失败的概率是非常低的从而可以推断事务整体失败概率很低。阅文集团早期采用该方案SLA 可达两个 9。
这种方案相比下面介绍的其他方案来说,性能损耗低,但在事务提交的执行过程中,若出现网络故障、数据库宕机等预期之外的异常现象,将会造成数据不一致,且无法进行回滚。
XA 两阶段
二阶段提交是 XA 的标准实现。让我们复习一下两阶段提交。它将分布式事务的提交拆分为两个阶段Prepare 和 Commit/Rollback。
开启 XA 全局事务后,所有子事务会按照本地默认的隔离级别锁定资源,并记录 undo 和 redo 日志,然后由 TM 发起 Prepare 投票询问所有的子事务是否可以进行提交。当所有子事务反馈的结果为“Yes”时TM 再发起 Commit若其中任何一个子事务反馈的结果为“No”TM 则发起 Rollback如果在 Prepare 阶段的反馈结果为 Yes而 Commit 的过程中出现宕机等异常时,则在节点服务重启后,可根据 XA Recover 再次进行 Commit 补偿,以保证数据的一致性。
2PC 模型中,在 Prepare 阶段需要等待所有参与子事务的反馈,因此可能造成数据库资源锁定时间过长,不适合并发高以及子事务生命周期较长的业务场景。
ShardingSphere 支持基于 XA 的强一致性事务解决方案,可以通过 SPI 注入不同的第三方组件作为事务管理器实现 XA 协议,如 Atomikos。
最大努力送达
最大努力送达,是针对客户端一阶段的一种补偿策略。它采用事务表记录所有的事务操作 SQL如果子事务提交成功将会删除事务日志如果执行失败则会按照配置的重试次数尝试再次提交即最大努力地进行提交尽量保证数据的一致性。这里可以根据不同的业务场景平衡 C 和 A采用同步重试或异步重试。这与 TiDB 实现 Percolator 事务中重试的思路有相似之处。
这种策略的优点是无锁定资源时间,性能损耗小。缺点是尝试多次提交失败后,无法回滚,它仅适用于事务最终一定能够成功的业务场景。因此最大努力送达是通过对事务回滚功能上的妥协,来换取性能的提升。
TCC
TCC 模型是把锁的粒度完全交给业务处理,它需要每个子事务业务都实现 Try-Confirm/Cancel 接口。
Try尝试执行业务。完成所有业务检查并预留必需业务资源。
Confirm
确认执行业务。真正执行业务,不做任何业务检查。只使用 Try 阶段预留的业务资源。Confirm 操作满足幂等性。
Cancel取消执行业务。释放 Try 阶段预留的业务资源。Cancel 操作满足幂等性。
这三个阶段都会按本地事务的方式执行,不同于 XA 的 PrepareTCC 无须将 XA 投票期间的所有资源挂起,因此极大地提高了吞吐量。但是它的缺点是需要实现 Cancel 操作,这不仅给实现带来了很多麻烦,同时有一些操作是无法 Cancel 的。
Saga
Saga 起源于 1987 年 Hector & Kenneth 发表的论文《Sagas》。
Saga 模型把一个分布式事务拆分为多个本地事务每个本地事务都有相应的执行模块和补偿模块TCC 中的 Confirm 和 Cancel。当 Saga 事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终的一致性。
它与 TCC 的差别是Saga 是以数据库事务维度进行操作的,而 TCC 是以服务维度操作的。
当每个 Saga 子事务“T1T2Tn”都有对应的补偿定义“C1C2Cn-1”那么 Saga 系统可以保证子事务序列“T1T2Tn”得以完成最佳情况或者序列“T1T2TjCjC2C1”得以完成也就是取消了所有的事务操作。
由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性,当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁、应用层面预先冻结资源。
Saga 支持向前和向后恢复。
向后恢复:如果任一子事务失败,补偿所有已完成的事务。
向前恢复:假设每个子事务最终都会成功,重试失败的事务。
显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务最终总会成功,或补偿事务难以定义或不可能,向前恢复会更符合你的需求。理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机、网络可能会失败,甚至数据中心也可能会停电,这时需要提供故障恢复后回退的机制,比如人工干预。
总的来说TCC 是以应用服务的层次进行分布式事务的处理,而 XA、Bed、Saga 则是以数据库为层次进行分布式处理,故中间件一般倾向于采用后者来实现更细粒度的控制。
Apache ShardingShpere 的分布式事务变迁
ShardingShpere 在 3.0 之前实现了客户端一阶段(弱 XA最大努力送达和 TCC。其中最大努力送达需要配合调度任务异步的执行。而弱 XA 作为默认的实现模式,此种组合是实用性与实现难度之间的平衡,但是在分布式失败模型描述的场景下会产生不一致的问题。
在 3.0 后,团队梳理了事务模型。实现了 XA 两阶段和 Saga。这两种事务都是面向数据库层面的同时有完整的理论支撑更加符合现代分布式数据库的设计风格。同时事务模块也如其他模块一样支持 SPI也就是可以实现第三方的事务模型。而京东 JDTX 事务引擎就是通过 SPI 集成到 ShardingShpere 的。下一讲我会介绍 JDTX 的相关内容。
总结
这一讲我们探讨了实现数据库中间件的几种技术,包括全局唯一主键、分片策略和跨分片查询。其中最重要的就是分布式事务。
不同于分布式数据库中间件的分布式事务多了很多应用服务的特色比如客户单一阶段、TCC。它们更偏向于服务层面从而揭示了中间件大部分是由应用研发或应用架构团队开发迭代的产物。而随着中间件的发展它们不可避免地向分布式数据演进如阿里云的 DRDS 和 PolarDB-X 就是由中间件 TDDL 演化而成。
数据库中间件是一个过渡产品,随着近几年技术的发展,越来越多原生 NewSQL 出现在我们面前。下一讲我就为你介绍几种典型的 NewSQL 数据库,看看它们都具备怎样的特点。

View File

@ -0,0 +1,160 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 现状解读:分布式数据库的最新发展情况
你好,恭喜你坚持到了课程的最后一讲。
上一讲,我们探讨了实现数据库中间件的几种技术,包括全局唯一主键、分片策略和跨分片查询,其中最重要的就是分布式事务,希望你可以掌握它。这一讲作为收尾,我将为你介绍 NewSQL 数据库。
首先我试着去定义 NewSQL它是一类现代的关系型数据库同时它又具备 NoSQL 的扩展能力。其擅长在 OLTP 场景下提供高性能的读写服务同时可以保障事务隔离性和数据一致性。我们可以简单理解为NewSQL 要将 2000 年左右发展而来的 NoSQL 所代表的扩展性与 20 世纪 70 年代发展的关系模型 SQL 和 ACID 事务进行结合,从而获得一个高并发关系型的分布式数据库。
如果我们使用 NewSQL 数据库,可以使用熟悉的 SQL 来与数据库进行交互。SQL 的优势我在模块一中已经有了深入的介绍。使用 SQL 使得原有基于 SQL 的应用不需要改造(或进行微量改造)就可以直接从传统关系型数据库切换到 NewSQL 数据库。而与之相对NoSQL 数据库一般使用 SQL 变种语言或者定制的 API那么用户切换到 NoSQL 数据库将会面临比较高的代价。
对于 NewSQL 的定义和适用范围一直存在争议。有人认为 Vertica、Greenplum 等面向 OLAP 且具有分布式特点的数据库也应该归到 NewSQL 里面。但是,业界更加广泛接受的 NewSQL 标准包括:
执行短的读写事务,也就是不能出现阻塞的事务操作;
使用索引去查询一部分数据集,不存在加载数据表中的全部数据进行分析;
采用 Sharded-Nothing 架构;
无锁的高并发事务。
根据以上这些特点,我总结为:一个 NewSQL 数据库是采用创新架构,透明支持 Sharding具有高并事务的 SQL 关系型数据库。
请注意 DistributedSQL 是一类特殊的 NewSQL它们可以进行全球部署。
下面就按照我给出的定义中的关键点来向你详细介绍 NewSQL 数据库。
创新的架构
使用创新的数据库架构是 NewSQL 数据库非常引人注目的特性。这种新架构一般不会依靠任何遗留的代码这与我在“22 | 发展与局限:传统数据库在分布式领域的探索”中介绍的依赖传统数据库作为计算存储节点非常不同。我们以 TiDB 这个典型的 NewSQL 数据库为例。
可以看到其中的创新点有以下几个。
存储引擎没有使用传统数据库。而使用的是新型基于 LSM 的 KV 分布式存储引擎,有些数据库使用了完全内存形式的存储引擎,比如 NuoDB。
Sharded-Nothing 架构。底层存储到上层工作负载都是独立部署的。
高性能并发事务。TiDB 实现了基于 Percolator 算法的高性能乐观事务。
透明分片。TiDB 实现了自动的范围分片,可以弹性地增减节点。
基于复制技术的自动容灾。使用 Raft 算法实现高可用的数据复制,自动进行故障转移。
可以说NewSQL 与传统关系型数据库之间的交集在于 SQL 与 ACID 事务,从而保证用户的使用习惯得以延续。
以上描述的创新点我在前两个模块都有详细的说明。这里不知道你是否注意到一个问题与使用传统数据库构建分布式数据库相比NewSQL 最为明显的差异来自存储引擎。特别是以 Spanner 为首的一众 NewSQL 数据库,如 YugaByte DB、CockroachDB 和 TiDB 都是使用 LSM 树作为存储引擎,并且都是 KV 结构。如此选择的原理是什么呢?
我在介绍 LSM 的时候提到其可以高性能地写入与读取但是牺牲了空间。可能你就据此得出结论NewSQL 数据库面对 OLTP 场景,希望得到吞吐量的提升,故选择 LSM 树存储引擎,而不选择 B 树类的存储引擎。但是,我也介绍过有很多方法可以改进 B 树的吞吐量。所以这一点并不是关键点。
我曾经也会困惑于这个问题,经过大量研究,并与项目组人员交流。从而得到了一个“结论”:开源的 NewSQL 选择的并不是 LSM 树,而是 RocksDB。选择 RocksDB 难道不是因为它是 LSM 结构的?答案是否定的。大部分以 RocksDB 为存储引擎的开源 NewSQL 数据库看中的是 RocksDB 的性能与功能。可以有一个合理的推论,如果有一款在性能和功能上碾压 RocksDB 的 B 树存储引擎,那么当代开源 NewSQL 数据库的存储引擎版图又会是另一番景象了。
你不用诧异,这种发展趋势其实代表了 IT 技术的一种实用特性。从 TCP/IP 协议的普及,到 Java 企业领域 Spring 替代 EJB都体现了这种实用性。那就是真正胜利的技术是一定有实用价值的这种实用价值要胜过任何完美的理论。这也启发我们在观察一个分布式数据库时不要着急给它分类因为今天我要介绍的评判 NewSQL 的标准,是基于现有数据库特性的总结,并不能代表未来的发展。我们要学会掌握每种数据库核心特性。
到这里,我还要提一个比较特别的数据库,那就是 OceanBase。OceanBase 的读写跟传统数据库有很大的一点不同就是OceanBase 的写并不是直接在数据块上修改而是新开辟一块增量内存用于存放数据的变化。同一笔记录多次变化后增量块会以链表形式组织在一起这些增量修改会一直在内存里不落盘。OceanBase 读则是要把最早读入内存的数据块加上后续相关的增量块内容合并读出。这种特点其实与 LSM 树的内存表和数据表有类似之处,只是 OceanBase 在更高维度上。
总结完架构上的创新,下面我将介绍 NewSQL 中关于分片的管理。
透明的 Sharding
我们上文介绍过数据库中间件是如何进行 Sharding 的,那就是使用逻辑的分片键来进行分片计算,将不同的行写入到不同的目标数据库。其中,需要用户深度地参与。比如,需要用户去指定哪个键为分片键,逻辑表与物理表的映射规则,必要的时候还需要进行 SQL 改造,等等。故中间件模式的 Sharding 我们一般称作显示 Sharding与之相对的就是 NewSQL 数据库提供的透明 Sharding。
透明 Sharding 顾名思义,用户不需要去指定任何的规则,数据就能分散到整个集群中,并自动做了备份处理。那么 NewSQL 是怎么确定分片键的呢?我们以 TiDB 为例。
一个表中的数据是按照 Region 来进行 Sharding 的。每个 Region 有多个副本,从而保障了数据的高可用。不同的表将会有不同的 Region而不是如传统分库那样每个库里的表都是相同的。 那么一个表下,每一行数据存储在哪个 Region 下是如何确定的呢?
首先要确定的是行是怎么映射到 KV 结构的: key 由 table_id 表 id+rowid 主键组成。如:
t[table_id]_r[row_id]
而 value 保存的就是一整行的数据。那么我们就使用这个 key 来计算这行数据应该落在哪个 Region 里面。
TiDB 是使用范围策略来划分数据。有索引情况下,负责索引的 Region 会根据索引字段范围来划分。基于 key 经过一个转换,将会得出一个数字,然后按范围划分多个区间,每个区间由一个 Region 管理。范围分片的好处我们之前介绍过,就是存储平衡和访问压力的平衡。其原因是,范围分片有机会做更好的动态调度,只有动态了,才能实时自动适应各种动态的场景。
虽然透明 Sharding 为用户带来了使用的便利。但聪明的你可以注意到了,这种隐式的分片方案相比上一讲介绍的方案,从功能上讲是存在缺失的。最明显的是它缺少跨分片 Join 的功能。可以想到,此种 NewSQL 与具有 ER 分片的数据库中间件在性能上是存在差异的。针对此种情况TiDB 引入了 Placement Rules 来制定将相关联的数据表放在同一个 KV 存储中,从而达到 ER 分片的效果。
当然,随着 NewSQL 的逐步发展,哈希分片也逐步被引入 NewSQL 中。我们知道范围分片有热点问题虽然可以通过动态拆分合并分片来缓解但终究不是从根本上解决该问题。特别是对于具有强顺序的数据比如时间序列该问题就会变得很突出。而哈希分片就是应对该问题的有效手段。基于这个原因Cockroach DB 引入哈希分片索引来实现针对序列化数据的扩展能力。
我们小结一下,透明的 Sharding 针对的往往是主键,一般会选择行 ID 作为主键。而如果要实现功能完善的 Sharding一些用户参与的配置操作还是有必要的。
解决了 Sharding 问题,让我们看看 SQL 层面需要解决的问题吧。
分布式的 SQL
NewSQL 数据库相对于 NoSQL 最强大的优势还是对 SQL 的支持。我曾经在模块一中与你讨论过SQL 是成功的分布式数据库一个必要的功能。SQL 的重要性我们已经讨论过了,但实现 SQL 一直是被认为很困难的,究其原因主要来源于以下两个方面。
SQL 的非标准性。虽然我们有 SQL99 这种事实标准,但是在工业界,没有一种流行的数据库完全使用标准去构建 SQL。每家都有自己的方言、自己独有的特性故我们看到的 NewSQL 数据库大部分都会按照已经存在的数据库方言去实现 SQL 语义,比如 TiDB 实现了 MySQL 的语法,而 CockroachDB 实现了 PostgreSQL。
声明式语言。SQL 语言是一种更高级的语言,比我们熟悉的 Java、Go 等都要高级。主要体现为它表述了希望得到什么结果,而没有指示数据库怎么做。这就引出了所谓的执行计划优化等概念,为实现高效的查询引擎带来了挑战。
以上是传统数据库实现 SQL 的挑战,而对于分布式数据库来说还需要将数据分散带来的问题考虑进去,同时在查询优化方面要将网络延迟作为一个重要因素来考量。
对于 NewSQL 而言,如果使用了上面所描述的创新架构,特别是底层使用 KV 存储,那么就需要考虑数据与索引如何映射到底层 KV 上。我在前面已经说明了数据是如何映射到 KV 上的,那就是 key 由 table_id 表 id+rowid 主键组成value 存放行数据:
t[table_id]_r[row_id] => row_data
而对于索引,我们就要区分唯一索引和非唯一索引。
对于唯一索引,我们将 table_id、index_id 和 index_value 组成 keyvalue 为上面的 row_id。
t[table_id]_i[index_id][index_value] => row_id
对于非唯一索引,就需要将 row_id 放到 key 中,而 value 是空的。
t[table_id]_i[index_id][index_value]_r[row_id] => null
底层映射问题解决后,就需要进入执行层面了。这里面牵扯大量的技术细节,我不会深入其中,而是为你展示一些 NewSQL 数据库会面临的挑战。
首先是正确性问题。是的,要实现正确的 SQL 语法本身就是挑战。因为 SQL 可以接受用户自定义表和索引,查询使用名字,而执行使用的是 ID这之间的映射关系会给正确性带来挑战。而其中最不好处理的就是 Null不仅仅是 NewSQL传统数据库在处理 Null 的时候也经过了长时间的“折磨”。这也是为什么一个 SQL 类数据库需要经过长时间的迭代才能达到稳定状态的原因。
然后就是性能了。性能问题不仅仅是数据库开发人员DBA 和最终用户也对它非常关注。如果你经常与数据库打交道,一定对 SQL 优化有一定了解。这背后的原因其实就是 SQL 声明式语言导致的。
大部分 NewSQL 数据库都需要实现传统数据库的优化手段,这一般分为两类:基于规则和基于代价。前者是根据 SQL 语义与数据库特点进行的静态分析也称为逻辑优化常见有列裁剪、子查询优化为连接、谓词下推、TopN 下推,等等;而基于代价的优化,需要数据库实时产生统计信息,根据这些信息来决定 SQL 是否查询索引、执行的先后顺序,等等。
而具有分布式数据库特色的优化包括并行执行、流式聚合,基于代价的优化需要考虑网络因素等。故实现分布式数据库的 SQL 优化要更为复杂。
从上文可以看到NewSQL 的数据库实现 SQL 层所面临的挑战要远远大于传统数据库。但由于 SQL 执行与优化技术经过多年的积累,现在已经形成了完整的体系。故新一代 NewSQL 数据库可以在此基础上去构建,它们站在巨人的肩膀上,实现完整的 SQL 功能将会事半功倍。
介绍完了 NewSQL 数据库在 SQL 领域的探索,最后我们来介绍其高性能事务的特点。
高性能事务
NewSQL 的事务是其能够得到广泛应用的关键。在上一个模块中我们讨论的 Spanner 与 Calvin就是典型的创新事务之争。这一讲我将总结几种 NewSQL 数据库处理事务的常见模式,并结合一定的案例来为你说明。
首先的一种分类方式就是集中化事务管理与去中心化事务管理。前者的优势是很容易实现事务的隔离,特别是实现序列化隔离,代价就是其吞吐量往往偏低;而后者适合构建高并发事务,但是需要逻辑时钟来进行授时服务,并保证操作竞争资源的正确性。
以上是比较常规的认识,但是我们介绍过 Calvin 事务模型。它其实是一种集中化的事务处理模式,但却在高竞争环境下具有非常明显的吞吐量的优势。其关键就是通过重新调度事务的执行,消除了竞争,从而提高了吞吐量。采用该模式的除了 Calvin 外,还有 VoltDB。其原理和 Calvin 类似,感兴趣的话你可以自行学习。
而采用去中心化事务的方案一般需要结合MVCC、2PC 和 PaxosGroup。将它们结合可以实现序列化的快照隔离并保证执行过程中各个组件的高可用。采用 PaxosGroup 除了提供高可用性外,一个重要的功能就是将数据进行分区,从而降低获取锁的竞争,达到提高并发事务效率的目的。这种模式首先被 Spanner 所引入,故我们一般称其为 Spanner 类事务模型。
而上一讲我们介绍了京东为 ShardingShpere 打造的 JDTX 又是另一番景象,数据库中间件由于无法操作数据库底层,所以事务方案就被锁死。而 JDTX 采用类似 OceanBase 的模式,首先在数据库节点之外构建了一个独立的 MVCC 引擎,查询最新的数据需要结合数据库节点与 MVCC 引擎中修改的记录,从而获得最新数据。而 MVCC 中的数据会异步落盘从而保证数据被释放。JDTX 的模式打破了中间件无法实现高性能事务模型的诅咒,为我们打开了思路。
可以看到,目前 NewSQL 的并发事务处理技术往往使用多种经过广泛验证的方案。可以将它比喻为当代的航空母舰,虽然每个部件都没有创新点,但是将它们结合起来却实现了前人无法企及的成就。
总结
这一讲,我介绍了 NewSQL 数据库的定义,并为你详细分析了一个 NewSQL 数据库的关键点,即以下四个。
创新的架构:分布式系统与存储引擎都需要创新。
透明的 Sharding自动的控制解放用户贴合云原生。
分布式 SQL打造商业可用分布式数据库的关键。
高性能事务NewSQL 创新基地,是区别不同种类 NewSQL 的关键。
至此,我们课程的主要内容就已经全部介绍完了。
NewSQL 和具有全球部署能力的 DistributedSQL 是当代分布式数据库的发展方向,可以说我们介绍过的所有知识都是围绕在 NewSQL 体系内的。接下来在加餐环节,我会为你介绍其他种类的分布式数据库,以帮助你拓展思路。

View File

@ -0,0 +1,113 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
加餐1 概念解析云原生、HTAP、图与内存数据库
我们课程的主要内容已经介绍完了,经过 24 讲的学习,相信你已经掌握了现代分布式数据库的核心内容。前面我所说的分布式数据库主要针对的是 NewSQL 数据库。而这一篇加餐,我将向你介绍一些与分布式数据库相关的名词和其背后的含义。学习完本讲后,当你再看见一款数据库时,就能知道其背后的所代表的意义了。
我首先介绍与 NewSQL 数据库直接相关的云原生数据库;其次会介绍目前非常火热的 HTAP 融合数据库概念;数据库的模式除了关系型之外,还有多种其他类型,我将以图数据库为例,带你领略非关系型数据库面对典型问题所具备的优势;最后为你介绍内存型数据库。
云原生数据库
云原生数据库从名字看是从云原生概念发展而来的。而云原生一般包含两个概念:一个是应用以服务化或云化的方式提供给用户;另一个就是应用可以依托云原生架构在本地部署与 Cloud 之间自由切换。
第一种概念是目前广泛介绍的。此类云原生数据库是在云基础架构之上进行构建的,数据库组件在云基础设施之上构建、部署和分发。这种云原生属性是它相比于其他类型数据库最大的特点。作为一种云平台,云原生数据库以 PaaS平台即服务Platform-as-a-Service的形式进行分发也经常被称作 DBaaS数据库即服务DataBase-as-a-Service。用户可以将该平台用于多种目的例如存储、管理和提取数据。
使用此类云原生数据库一般会获得这样几个好处。
快速恢复
也就是数据库在无须事先通知的情况下,即时处理崩溃或启动进程的能力。尽管现在有先进的技术,但是像磁盘故障、网络隔离故障,以及虚拟机异常等,仍然不可避免。对于传统数据库,这些故障非常棘手。因为用单个机器运行整个数据库,即便一个很小的问题都可能影响所有功能。
而云原生数据库的设计具有显著的快速恢复能力,也就是说可以立即重启或重新调度数据库工作负载。实际上,易处置性已从单个虚拟机扩展到了整个数据中心。随着我们的环境持续朝着更加稳定的方向发展,云原生数据库将发展到对此类故障无感知的状态。
安全性
DBaaS 运行在高度监控和安全的环境里,受到反恶意软件、反病毒软件和防火墙的保护。除了全天候监控和定期的软件升级以外,云环境还提供了额外的安全性。相反,传统数据库容易遭受数据丢失和被不受限制的访问。基于服务提供商通过即时快照副本提供的数据能力,用户可以达成很小的 RPO 和 RTO。
弹性扩展性
能够在运行时进行按需扩展的能力是任何企业成长的先决条件。因为这种能力让企业可以专注于追求商业目标,而不用担心存储空间大小的限制。传统数据库将所有文件和资源都存储在同一主机中,而云原生数据库则不同,它不仅允许你以不同的方式存储,而且不受各种存储故障的影响。
节省成本
建立一个数据中心是一项独立而完备的工程,需要大量的硬件投资,还需要能可靠管理和维护数据中心的训练有素的运维人员。此外,持续地运维会给你的财务带来相当大的压力。而使用云原生的 DBaaS 平台,你可以用较低的前期成本,获得一个可扩展的数据库,这可以让你腾出双手,实现更优化的资源分配。
以上描述的云原生数据库一般都是采用全新架构的 NewSQL 数据库。最典型的代表就是亚马逊的 Aurora。它使用了日志存储实现了 InnoDB 的功能,从而实现了分布式条件下的 MySQL。目前各个主要云厂商的 RDS 数据库都有这方面的优势,如阿里云 PolarX等等。
而第二种云原生数据库理论上可以部署在企业内部私有云和云服务商公有云而许多企业尝试使用混合模式来进行部署。面向这种场景的云原生数据库并不会自己维护基础设施而是使自己的产品能运行在多种云平台上ClearDB 就是这类数据库典型代表。这种跨云部署提高了数据库整体的稳定性,并可以改善服务全球应用的数据响应能力。
当然,云原生数据库并不仅限于关系型,目前我们发现 Redis、MongoDB 和 Elasticsearch 等数据库都在各个主要云厂商提供的服务中占有重要的地位。可以说广义上的云原生数据库含义是非常宽泛的。
以上带你了解了云原生数据库在交付侧带来的创新,下面我来介绍 HTAP 融合数据库在数据模型上有哪些新意。
HTAP
介绍 HTAP 之前,让我们回顾一下 OLTP 和 OLAP 的概念。我之前已经介绍过OLTP 是面向交易的处理过程,单笔交易的数据量很小,但是要在很短的时间内给出结果;而 OLAP 场景通常是基于大数据集的运算。它们两个在大数据时代就已经分裂为两条发展路线了。
但是 OLAP 的数据往往来自 OLTP 系统,两者一般通过 ETL 进行衔接。为了提升 OLAP 的性能,我们需要在 ETL 过程中进行大量的预计算,包括数据结构的调整和业务逻辑处理。这样的好处是可以控制 OLAP 的访问延迟,提升用户体验。但是,因为要避免抽取数据对 OLTP 系统造成影响,所以必须在系统访问的低峰期才能启动 ETL 过程,例如对于电商系统,一般都是午夜进行。
这样一来, OLAP 与 OLTP 的数据延迟通常就在一天左右,大家习惯把这种时效性表述为 N+1。其中N 就是指 OLTP 系统产生数据的日期N+1 是 OLAP 中数据可用的日期,两者间隔为 1 天。你可能已经发现了,这个体系的主要问题就是 OLAP 系统的数据时效性24 个小时的延迟太长了。是的,进入大数据时代后,商业决策更加注重数据的支撑,而且数据分析也不断向一线操作渗透,这都要求 OLAP 系统更快速地反映业务的变化。
此时就出现了将两种融合的 HTAP。HTAPHybrid Transaction/Analytical Processing就是混合事务分析处理它最早出现在 2014 年 Gartner 的一份报告中。Gartner 用 HTAP 来描述一种新型数据库,它打破了 OLTP 和 OLAP 之间的隔阂在一个数据库系统中同时支持事务型数据库场景和分析型数据库场景。这个构想非常美妙HTAP 可以省去烦琐的 ETL 操作,避免批量处理造成的滞后,更快地对最新数据进行分析。
由于数据产生的源头在 OLTP 系统,所以 HTAP 概念很快成为 OLTP 数据库,尤其是 NewSQL 风格的分布式数据库和云原生数据库。通过实现 HTAP它们试图向 OLAP 领域进军。这里面比较典型代表是 TiDBTiDB 4.0 加上 TiFlash 这个架构能够符合 HTAP 的架构模式。
TiDB 是计算和存储分离的架构底层的存储是多副本机制可以把其中一些副本转换成列式存储的副本。OLAP 的请求可以直接打到列式的副本上,也就是 TiFlash 的副本来提供高性能列式的分析服务,做到了同一份数据既可以做实时的交易又做实时的分析,这是 TiDB 在架构层面的巨大创新和突破。
以上就是 HTAP 数据库的演化逻辑和典型代表。下面我们继续拓展知识的边界,看看多种模式的分布式数据库。
内存数据库
传统的数据库通常是采用基于磁盘的设计,原因在于早期数据库管理系统设计时受到了硬件资源如单 CPU、单核、可用内存小等条件的限制把整个数据库放到内存里是不现实的只能放在磁盘上。可以说内存数据库是在存储引擎层面上进行全新架构的 NewSQL 数据库。
伴随着技术的发展,内存已经越来越便宜,容量也越来越大。单台计算机的内存可以配置到几百 GB 甚至 TB 级别。对于一个数据库应用来说,这样的内存配置已经足够将所有的业务数据加载到内存中进行使用。通常来讲,结构化数据的规模并不会特别大,例如一个银行 10 年到 20 年的交易数据加在一起可能只有几十 TB。这样规模的结构化数据如果放在基于磁盘的 DBMS 中,在面对大规模 SQL 查询和交易处理时,受限于磁盘的 I/O 性能,很多时候数据库系统会成为整个应用系统的性能瓶颈。
如果我们为数据库服务器配置足够大的内存,是否仍然可以采用原来的架构,通过把所有的结构化数据加载到内存缓冲区中,就可以解决数据库系统的性能问题呢?这种方式虽然能够在一定程度上提高数据库系统的性能,但在日志机制和更新数据落盘等方面仍然受限于磁盘的读写速度,远没有发挥出大内存系统的优势。内存数据库管理系统和传统基于磁盘的数据库管理系统在架构设计和内存使用方式上还是有着明显的区别。
一个典型的内存数据库需要从下面几个方面进行优化。
去掉写缓冲:传统的缓冲区机制在内存数据库中并不适用,锁和数据不需要再分两个地方存储,但仍然需要并发控制,需要采用与传统基于锁的悲观并发控制不同的并发控制策略。
尽量减少运行时的开销:磁盘 I/O 不再是瓶颈,新的瓶颈在于计算性能和功能调用等方面,需要提高运行时性能。
可扩展的高性能索引构建:虽然内存数据库不从磁盘读数据,但日志依然要写进磁盘,需要考虑日志写速度跟不上的问题。可以减少写日志的内容,例如把 undo 信息去掉,只写 redo 信息;只写数据但不写索引更新。如果数据库系统崩溃,从磁盘上加载数据后,可以采用并发的方式重新建立索引。只要基础表在,索引就可以重建,在内存中重建索引的速度也比较快。
综上可以看到,构建内存数据库并不是简单地将磁盘去掉,而是需要从多个方面去优化,才能发挥出内存数据库的优势。而图数据库则是通过修改数据模型来实现高性能的,下面让我们看看其具体的功能。
图数据库
图数据库graph database是一个使用图结构进行语义查询的数据库它使用节点、边和属性来表示和存储数据。该系统的关键概念是图它直接将存储中的数据项与数据节点和节点间表示关系的边的集合相关联。这些关系允许直接将存储区中的数据连接在一起并且在许多情况下可以通过一个操作进行检索。图数据库将数据之间的关系作为优先级。查询图数据库中的关系很快因为它们永久存储在数据库本身中。可以使用图数据库直观地显示关系使其对于高度互联的数据非常有用。
如果内存数据库是在底层存储上进行的创新,那么图数据库就是在数据模型上进行了改造。构造图数据库时,底层存储既可以使用 LSM 树等 KV 存储,也可以使用上一讲介绍的内存存储,当然一些图数据库也可以使用自创的存储结构。
由于图数据所依赖的算法本质上没有考虑分布式场景,故在分布式系统处理层面形成了两个流派。
以节点为中心
这是传统分布式理论作用在图数据库中的一种形式。此类数据库以节点为中心,使相邻的节点就近交换数据。图算法采用了比较直接的模式,其好处是并发程度较好,但是效率很低。适合于批量并行执行简单的图计算。典型代表是就是 Apache Spark。故此种数据库又被称为图计算引擎。
以算法为中心
这是针对图计算算法特别设计的数据库。其底层数据格式满足了算法的特性,从而可以使用较低的资源处理较多的图数据。但是此类数据库是很难进行大范围扩展的。其典型代表有 Neo4j。
当然我们一般可以同时使用上面两种处理模式。使用第一种来进行大规模数据的预处理和数据集成工作;使用第二种模式来进行图算法实现和与图应用的对接工作。
总之,图数据库领域目前方兴未艾,其在社交网络、反欺诈和人工智能等领域都有非常大的潜力。特别是针对最近的新冠疫情,有很多团队利用图数据库分析流行病学,从而使人们看到了该领域数据库的优势。
图数据库与文档型数据库、时间序列数据库等都是面向特殊数据模型的数据库。此类数据库之所以能越来越火热,除了它们所在领域被重视以外,最重要的底层原因还是存储引擎和分布式系统理论、工具的日趋成熟。可以预见,今后一些热门领域会相继产出具有领域特色的数据库。
总结
本讲作为加餐的知识补充,为你介绍了多种分布式数据库。其中云原生与 HTAP 都是 NewSQL 数据库的发展分支。云原生是从交付领域入手,提供给用户一个开箱即用的数据库。而 HTAP 拓展了 NewSQL 的边界,引入了 OLAP从而抢占了部分传统大数据和数据分析领域的市场。
而内存数据库是针对数据库底层的创新除了内存数据库外2021 年随着 S3 存储带宽的增加和其单位存储价格持续走低,越来越多的数据库开始支持对象存储。今后,随着越来越多的创新硬件的加入,我们还可能看到更多软硬结合方案数据库的涌现。
最后我介绍了图数据库,它作为特殊领域数据库在领域内取得成就,是通用关系型数据无法企及的。而随着存储引擎和分布式理论的发展,此类数据库将会越来越多。
如此多的分布式数据库真是让人眼花缭乱,那么我们该如何去选择呢?下一节加餐我会结合几个典型领域,来给你一些指引。

View File

@ -0,0 +1,90 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
加餐2 数据库选型:我们该用什么分布式数据库?
经过 24 讲的基础知识学习以及上一讲加餐,我已经向你介绍了分布式数据库方方面面的知识。这些知识,我觉得大概会在三个方面帮到你,分别是数据库研发、架构思维提升和产品选型。特别是 NewSQL 类数据库相关的知识,对于你认识面向交易的场景很有帮助。
我先后在电信与电商行业有十几年的积累,我想,你也非常希望知道在这些主流场景中分布式数据库目前的应用状况。这一讲我就要为你介绍银行、电信和电商领域内分布式数据库的使用状况。
首先我要从我的老本行电商行业开始。
电商:从中间件到 NewSQL
分布式数据库,特别是分布式中间这一概念就是从电商,尤其是阿里巴巴集团催生出来的。阿里集团也是最早涉及该领域的企业。这里我们以其 B2B 平台部产生的 Cobar 为例介绍。
2008 年,阿里巴巴 B2B 成立了平台技术部,为各个业务部门的产品提供底层的基础平台。这些平台涵盖 Web 框架、消息通信、分布式服务、数据库中间件等多个领域的产品。它们有的源于各个产品线在长期开发过程中沉淀出来的公共框架和系统,有的源于对现有产品和运维过程中新需求的发现。
数据库相关的平台就是其中之一,主要解决以下三方面的问题:
为海量前台数据提供高性能、大容量、高可用性的访问;
为数据变更的消费提供准实时的保障;
高效的异地数据同步。
如下面的架构图所示,应用层通过 Cobar 访问数据库。
其对数据库的访问分为读操作select和写操作update、insert和delete。写操作会在数据库上产生变更记录MySQL 的变更记录叫 binlogOracle 的变更记录叫 redolog。Erosa 产品解析这些变更记录,并以统一的格式缓存至 Eromanga 中后者负责管理变更数据的生产者、Erosa 和消费者之间的关系,负责跨机房数据库同步的 Otter 是这些变更数据的消费者之一。
Cobar 可谓 OLTP 分布式数据库解决方案的先驱,至今其中的思想还可以从现在的中间件,甚至 NewSQL 数据库中看到。但在阿里集团服役三年后,由于人员变动而逐步停止维护。这个时候 MyCAT 开源社区接过了该项目的衣钵,在其上增加了诸多功能并进行 bug 修改,最终使其在多个行业中占用自己的位置。
但是就像我曾经介绍的那样,中间件产品并不是真正的分布式数据库,它有自己的局限。比如 SQL 支持、查询性能、分布式事务、运维能力,等等,都有不可逾越的天花板。而有一些中间件产品幸运地得以继续进阶,最终演化为 NewSQL甚至是云原生产品。阿里云的 PolarDB 就是这种类型的代表,它的前身是阿里云的分库分表中间件产品 DRDS而 DRDS 来源于淘宝系的 TDDL 中间件。
PolarDB 相比于传统的中间件差别是采用了共享存储架构。率先采用这种架构的恰好也是一家电商到云计算的巨头:亚马逊。而这个数据库产品就是 Aurora。
Aurora 采用了这样一种架构。它将分片的分界线下移到事务及索引系统的下层。这个时候由于存储引擎保留了完整的事务系统,已经不是无状态的,通常会保留单独的节点来处理服务。这样存储引擎主要保留了计算相关逻辑,而底层存储负责了存储相关的像 redo、刷脏以及故障恢复。因此这种结构也就是我们常说的计算存储分离架构也被称为共享盘架构。
PolarDB 在 Aurora 的基础上采用了另外一条路径。RDMA 的出现和普及大大加快了网络间的网络传输速率PolarDB 认为未来网络的速度会接近总线速度,也就是瓶颈不再是网络,而是软件栈。因此 PolarDB 采用新硬件结合 Bypass Kernel 的方式来实现高效的共享盘实现,进而支撑高效的数据库服务。由于 PolarDB 的分片层次更低,从而做到更好的生态兼容,这也就是为什么 PolarDB 能够很快做到社区版本的全覆盖。副本件 PoalrDB 采用了 ParalleRaft 来允许一定范围内的乱序确认、乱序 Commit 以及乱序 Apply。但是 PolarDB 由于修改了 MySQL 的源码和数据格式,故不能与 MySQL 混合部署,它更适合作为云原生被部署在云端提供 DBaaS 服务。
以 Spanner 为代表的“无共享”类型的 NewSQL 由于有较高的分片层次,可以获得接近传统分库分表的扩展性,因此容易在 TPCC 这样的场景下取得好成绩,但其需要做业务改造也是一个大的限制。以 Aurora 及 PolarDB 为代表的“共享存储”的 NewSQL 则更倾向于良好的生态兼容,几乎为零的业务改造,来交换了一定程度的可扩展性。
可以说从 Cobar 到现在的 PolarDB我们看到了分布式数据库在电商与云计算领域的成长。其他典型代表如京东数科的 ShardingShpere 也有类似的发展脉络。可以说电商领域,乃至互联网行业,都进入了以自主知识产权研发的模式,对技术的掌控力较高,从而打造了良好的生态环境。
以上介绍完了电商这个比较技术激进的行业,下面再来看一些传统行业的表现。
电信:去 IOE
电信作为重要的 IT 应用行业,过去 20 年一直引领着技术发展的潮流。但电信却又是关系到国计民生的重要行业,故技术路线相比于电商、互联网来说更为保守。其中 Oracle 数据库常年“霸占”着该领域,而分布式计算场景被众多子系统所承担着。以中国联通为例,在 2010 年前后,联通集团的各个省系统就有数百个之多,它们之间的数据是通过 ETL 工具进行同步,也就是从应用层保证了数据一致性。故从当时的情况看,其并没有对分布式数据库有很强的需求。
但在 2012 年前后,也就是距今大概 10 年前。中国联通开始尝试引入阿里集团的技术,其中上文提到的 Cobar 就在其中。为什么三大运营商中联通首先发力呢?原因是联通当年刚完成对老网通的合并,急需将移动网业务与固网业务进行整合。其次是联通集团总部倾向于打造全国集中系统,而这就需要阿里集团提供帮助。
根据当年参与该项目的人回忆,现场近千人一起参与,众多厂商系统工作,最终在距离承诺上线日期没几天的时候才完成了主要功能的验证。按现在的技术评估标准,当年这个项目并不是十分成功。但是这个项目把阿里的“去 IOE”理念深深地写入了电信领域内所有参与者的基因里面从运营商到供应商所有人都觉得分布式数据库的解决方案是未来的趋势。
而后 Cobar 衍生的 MyCAT 在联通与电信多个产品线开始落地,而各个供应商也开始模仿 Cobar 制作了自己的中间件产品。可以说正是上面描述的背景使数据库中间件模式逐步在电信领域内被广泛接受其底色就是去“IOE”。
2016 年之后,电信行业没有放弃演化的步伐,各个主要供应商开始尝试去构建 NewSQL 数据库,其中特别是以 PGXC 架构为核心的 NewSQL 数据库引人瞩目。PGXCPostgreSQL-XC的本意是指一种以 PostgreSQL 为内核的开源分布式数据库。因为 PostgreSQL 的影响力和开放的软件版权协议(类似 BSD很多厂商在 PGXC 上二次开发,推出自己的产品。不过,这些改动都没有变更主体架构风格,所以我把这类产品统称为 PGXC 风格,如亚信的 AntDB、人大金仓的 KingbaseDB 都是这类数据库的典型代表。此类数据库开始在行业内部快速落地。
近些年,电信行业内部也逐步接触了最具创新性的 NewSQL但是此类数据库选择范围很小。电信运营商更倾向于与国内厂商合作如 TiDB 和 OceanBase 已经有在三大运营商和铁搭公司上线的案例。不过,我们可以发现,目前新一代 NewSQL 接管的系统不是属于创新领域,就是属于边缘业务,还未触及核心系统。但是我们可以认为,未来将会有更多的场景使用到创新性的 NewSQL 数据库。
最后再来说说银行系统。
银行:稳中前进
银行与电信是非常类似的行业但是银行的策略会更保守。银行并没有在内部推动轰轰烈烈的去“IOE”运动故其在 OLTP 领域一直使用传统数据库,如 Oracle 和 DB2。
一直到近 5 年开始,银行的 IT 架构才发生重大调整。比如行业标杆的工商银行的架构改造在 2018 年大规模落地,而调研和试点工作则在更早的 20162017 年。这个时点上,商用 NewSQL 数据库刚刚推出不久,而金融场景的种种严苛要求,注定了银行不会做第一个吃螃蟹的人,那么这种可能性就被排除了。同样,另一种 PGXC 风格的分布式数据库也正待破茧而出,反而是它的前身,“分布式中间件 + 开源单体数据库”的组合更加普及。
后来的结果是工行选择了爱可生开源的 DBLE + MySQL 的组合,选择 MySQL 是因为它的普及程度足够高;而选择 DBLE 则因为它是在 MyCat 的基础上研发,号称“增强版 MyCat”由于 MyCat 已经有较多的应用案例,所以这一点给 DBLE 带来不少加分。这一模式在工行内部推行得很好,最终使 MySQL 集群规模达到上千节点。虽然表面看起来还是非常保守,因为同期的电信行业已经开始推进 PGXC 架构落地了。但是工行,乃至真个银行行业,也走上了“去 IOE”的道路可以想到整个行业也是朝着 NewSQL 数据库的方向前进的。而且目前工商银行已经宣布与 OceanBase 进行合作,这预示着行业中 NewSQL 化的浪潮即将来临。
相对于 OLTP 技术应用上的平淡,工行在 OLAP 方面的技术创新则令人瞩目。基本是在同期,工行联合华为成功研发了 GaussDB 200并在生产环境中投入使用。这款数据库对标了 Teradata 和 Greenplum 等国外 OLAP 数据库。在工行案例的加持下,目前不少银行计划或者正在使用这款产品替换 Teradata 数据库。
总结
这篇加餐我为你总结了几个重点行业使用分布式数据库,特别是 OLTP 类数据库的情况。我们可以从中看到一些共同点:
发展脉络都是单机数据库、中间件到 NewSQL甚至电商领域开始做云计算
各个行业依据自己的特点进行发展,虽然基本都经过了这些阶段,但是它们之间是有技术滞后性的;
先发展的行业带动了其他行业,特别是电商领域的技术被其他行业引用,达到了协同发展的效果;
国产数据库崛起,近年采购的全新架构的 NewSQL 数据库,我们都可以看到国产厂家的身影,一方面得力于国有电信、银行等企业的政策支持,另一方面国产数据库的进步也是大家有目共睹的。
这三个具有典型性的行业,为我们勾画来整个分布式数据库在中国的发展。希望你能从它们的发展轨迹中汲取养分,从而能使用分布式数据库为自己的工作、学习助力。

View File

@ -0,0 +1,68 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇寄语:缓存,你真的用对了吗?
你好,我是你的缓存老师陈波,可能大家对我的网名 fishermen 会更熟悉。
我是资深老码农一枚,经历了新浪微博从起步到当前月活数亿用户的大型互联网系统的技术演进过程,现任新浪微博技术专家。我于 2008 年加入新浪,最初从事新浪 IM 的后端研发。2009 年之后开始微博 Feed 平台系统的的研发及架构工作深度参与最初若干个版本几乎所有业务的开发和架构改进2013 年后开始从事微博平台基础架构相关的研发工作。目前主要从事微博 Feed 平台的基础设施、缓存中间件、分布式存储等的研发及架构优化工作。
那么,我们为什么要学习缓存呢?有必要学习缓存吗?
随着互联网从门户/搜索时代进入移动社交时代,互联网产品也从满足用户单向浏览的需求,发展为满足用户个性信息获取及社交的需求。这就要求产品做到以用户和关系为基础,对海量数据进行实时分析计算。也就意味着,用户的每次请求,服务后端都要查询用户的个人信息、社交关系图谱,以及关系图谱涉及到的大量关联信息。还要将这些信息进行聚合、过滤、筛选和排序,最终响应给用户。如果这些信息全部从 DB 中加载,将会是一个无法忍受的漫长等待过程。
而缓存的使用,是提升系统性能、改善用户体验的唯一解决之道。
以新浪微博为例,作为移动互联网时代的一个开拓者和重量级社交分享平台,自 2009 年上线后,用户数量和微博数量都从 0 开启并高速增长,到 2019 年,日活跃用户已超 2亿每日新发 Feed 12亿每日访问量百亿级历史数据高达千亿级。同时在微博的日常服务中核心接口可用性要达到 99.99%,响应时间在 1060ms 以内,核心单个业务的数据访问量高达百万级 QPS。
所有这些数据都是靠良好的架构和不断改进的缓存体系来支撑的。
其实,作为互联网公司,只要有直接面对用户的业务,要想持续确保系统的访问性能和可用性,都需要使用缓存。因此,缓存也是后端工程师面试中一个非常重要的考察点,面试官通常会通过应聘者对缓存相关知识的理解深入程度,来判断其开发经验和学习能力。可以说,对缓存的掌握程度,在某种意义上决定了后端开发者的职业高度。
想学好缓存,需要掌握哪些知识呢?
可以看一下这张“缓存知识点全景图”。
首先要熟练掌握缓存的基础知识了解缓存常用的分类、读写模式熟悉缓存的七大经典问题及解决应对之策同时要从缓存组件的访问协议、Client 入手,熟练掌握如何访问各种缓存组件,如 Memcached、Redis、Pika 等。
其次,要尽可能深入理解缓存组件的实现方案、设计原理,了解缓存的各种特性、优势和不足,这样在缓存数据与预期不一致时,能够快速定位并解决问题。
再次还要多了解线上大中型系统是如何对缓存进行架构设计的。线上系统业务功能丰富多变跨域部署环境复杂而且热点频发用户习惯迥异。因此缓存系统在设计之初就要尽量进行良好设计规划好如何进行Hash及分布、如何保障数据的一致性、如何进行扩容和缩容。当然缓存体系也需要伴随业务发展持续演进这就需要对缓存体系进行持续的状态监控、异常报警、故障演练以确保在故障发生时能及时进行人肉或自动化运维处理并根据线上状况不断进行优化和改进。
最后,了解缓存在各种场景下的最佳实践,理解这些最佳实践背后的 Tradeoff做到知其然知其所以然以便在实际工作中能举一反三把知识和经验更好的应用到工作实践中来。
如何高效学习缓存呢?你能学到什么?
对于缓存网上学习资料很多但过于零散和重复想要系统地学习还是需要通过阅读缓存相关的书籍、论文和缓存源码或是学习一些来自实战总结的网络课程。但前面几种形式目前都需要花费较多时间。为了学员既系统又快速地获得所需知识拉勾教育推出了“300 分钟学会”系列技术课,其中“缓存“课由我来讲。
在这 300 分钟里,我将结合自己在微博平台的缓存架构经验,用 10 课时来分享:
如何更好地引入和使用缓存,自系统设计之初,就把缓存设计的关键点对号入座。
如何规避并解决缓存设计中的七大经典问题。
从协议、使用技巧、网络模型、核心数据结构、存储架构、数据处理模型、优化及改进方案等多角度全方位深入剖析互联网企业大量使用的Memcached、Redis等开源缓存组件。
教你如何利用它们构建一个分布式缓存服务体系。
最后,我将结合诸如秒杀、海量计数、微博 Feed 聚合等经典业务场景,分析如何构建相应的高可用、高性能、易扩展的缓存架构体系。
通过本课程,你可以:
系统地学习缓存之设计架构的关键知识点;
学会如何更好地使用 Memcached、Redis 等缓存组件;
对这些缓存组件的内部架构、设计原理有一个较为深入的了解,真正做到知其然更知其所以然;
学会如何根据业务需要对缓存组件进行二次开发;
搞懂如何构建一个大型的分布式缓存服务系统;
了解在当前多种热门场景下缓存服务的最佳实践;
现学现用,针对互联网大中型系统,构建出一个更好的缓存架构体系,在大幅提升系统吞吐和响应性能的同时,达到高可用、高扩展,从而可以更从容地应对海量并发请求和极端热点事件。

View File

@ -0,0 +1,66 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 业务数据访问性能太低怎么办?
你好我是你的缓存老师陈波欢迎进入第1课时“缓存的原理”。这节课主要讲缓存的基本思想、缓存的优点、缓存的代价三个部分。
缓存的定义
先来看下缓存的定义。
缓存最初的含义,是指用于加速 CPU 数据交换的 RAM即随机存取存储器通常这种存储器使用更昂贵但快速的静态 RAMSRAM技术用以对 DRAM进 行加速。这是一个狭义缓存的定义。
而广义缓存的定义则更宽泛,任何可以用于数据高速交换的存储介质都是缓存,可以是硬件也可以是软件。
缓存存在的意义就是通过开辟一个新的数据交换缓冲区,来解决原始数据获取代价太大的问题,让数据得到更快的访问。本课主要聚焦于广义缓存,特别是互联网产品大量使用的各种缓存组件和技术。
缓存原理
缓存的基本思想
缓存构建的基本思想是利用时间局限性原理,通过空间换时间来达到加速数据获取的目的,同时由于缓存空间的成本较高,在实际设计架构中还要考虑访问延迟和成本的权衡问题。这里面有 3 个关键点。
一是时间局限性原理,即被获取过一次的数据在未来会被多次引用,比如一条微博被一个人感兴趣并阅读后,它大概率还会被更多人阅读,当然如果变成热门微博后,会被数以百万/千万计算的更多用户查看。
二是以空间换时间,因为原始数据获取太慢,所以我们开辟一块高速独立空间,提供高效访问,来达到数据获取加速的目的。
三是性能成本 Tradeoff构建系统时希望系统的访问性能越高越好访问延迟越低小越好。但维持相同数据规模的存储及访问性能越高延迟越小成本也会越高所以在系统架构设计时你需要在系统性能和开发运行成本之间做取舍。比如左边这张图相同成本的容量SSD 硬盘容量会比内存大 1030 倍以上,但读写延迟却高 50100 倍。
缓存的优势
缓存的优势主要有以下几点:
提升访问性能
降低网络拥堵
减轻服务负载
增强可扩展性
通过前面的介绍,我们已经知道缓存存储原始数据,可以大幅提升访问性能。不过在实际业务场景中,缓存中存储的往往是需要频繁访问的中间数据甚至最终结果,这些数据相比 DB 中的原始数据小很多,这样就可以减少网络流量,降低网络拥堵,同时由于减少了解析和计算,调用方和存储服务的负载也可以大幅降低。缓存的读写性能很高,预热快,在数据访问存在性能瓶颈或遇到突发流量,系统读写压力大增时,可以快速部署上线,同时在流量稳定后,也可以随时下线,从而使系统的可扩展性大大增强。
缓存的代价
然而不幸的是,任何事情都有两面性,缓存也不例外,我们在享受缓存带来一系列好处的同时,也注定需要付出一定的代价。
首先,服务系统中引入缓存,会增加系统的复杂度。
其次,由于缓存相比原始 DB 存储的成本更高,所以系统部署及运行的费用也会更高。
最后,由于一份数据同时存在缓存和 DB 中,甚至缓存内部也会有多个数据副本,多份数据就会存在一致性问题,同时缓存体系本身也会存在可用性问题和分区的问题。这就需要我们加强对缓存原理、缓存组件以及优秀缓存体系实践的理解,从系统架构之初就对缓存进行良好设计,降低缓存引入的副作用,让缓存体系成为服务系统高效稳定运行的强力基石。
一般来讲,服务系统的全量原始数据存储在 DB 中(如 MySQL、HBase 等),所有数据的读写都可以通过 DB 操作来获取。但 DB 读写性能低、延迟高,如 MySQL 单实例的读写 QPS 通常只有千级别30006000读写平均耗时 10100ms 级别,如果一个用户请求需要查 20 个不同的数据来聚合,仅仅 DB 请求就需要数百毫秒甚至数秒。而 cache 的读写性能正好可以弥补 DB 的不足,比如 Memcached 的读写 QPS 可以达到 10100万 级别,读写平均耗时在 1ms 以下,结合并发访问技术,单个请求即便查上百条数据,也可以轻松应对。
但 cache 容量小,只能存储部分访问频繁的热数据,同时,同一份数据可能同时存在 cache 和 DB如果处理不当就会出现数据不一致的问题。所以服务系统在处理业务请求时需要对 cache 的读写方式进行适当设计,既要保证数据高效返回,又要尽量避免数据不一致等各种问题。
好了,第 1 课时的内容到这里就全部结束了,我们一起来做一个简单的回顾。首先,这一课时,你先了解了缓存的定义以及基本思想。然后,又学习了缓存的优点和代价。

View File

@ -0,0 +1,76 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 如何根据业务来选择缓存模式和组件?
你好,我是你的缓存老师陈波,欢迎进入第 2 课时“缓存的读写模式及分类”。这一课时我们主要学习缓存的读写模式以及缓存的分类。
缓存读写模式
如下图,业务系统读写缓存有 3 种模式:
Cache Aside旁路缓存
Read/Write Through读写穿透
Write Behind Caching异步缓存写入
Cache Aside
如上图所示Cache Aside 模式中,业务应用方对于写,是更新 DB 后,直接将 key 从 cache 中删除,然后由 DB 驱动缓存数据的更新;而对于读,是先读 cache如果 cache 没有,则读 DB同时将从 DB 中读取的数据回写到 cache。
这种模式的特点是,业务端处理所有数据访问细节,同时利用 Lazy 计算的思想,更新 DB 后,直接删除 cache 并通过 DB 更新,确保数据以 DB 结果为准,则可以大幅降低 cache 和 DB 中数据不一致的概率。
如果没有专门的存储服务,同时是对数据一致性要求比较高的业务,或者是缓存数据更新比较复杂的业务,这些情况都比较适合使用 Cache Aside 模式。如微博发展初期,不少业务采用这种模式,这些缓存数据需要通过多个原始数据进行计算后设置。在部分数据变更后,直接删除缓存。同时,使用一个 Trigger 组件,实时读取 DB 的变更日志然后重新计算并更新缓存。如果读缓存的时候Trigger 还没写入 cache则由调用方自行到 DB 加载计算并写入 cache。
Read/Write Through
如上图,对于 Cache Aside 模式,业务应用需要同时维护 cache 和 DB 两个数据存储方,过于繁琐,于是就有了 Read/Write Through 模式。在这种模式下,业务应用只关注一个存储服务即可,业务方的读写 cache 和 DB 的操作,都由存储服务代理。存储服务收到业务应用的写请求时,会首先查 cache如果数据在 cache 中不存在,则只更新 DB如果数据在 cache 中存在,则先更新 cache然后更新 DB。而存储服务收到读请求时如果命中 cache 直接返回,否则先从 DB 加载,回种到 cache 后返回响应。
这种模式的特点是,存储服务封装了所有的数据处理细节,业务应用端代码只用关注业务逻辑本身,系统的隔离性更佳。另外,进行写操作时,如果 cache 中没有数据则不更新,有缓存数据才更新,内存效率更高。
微博 Feed 的 Outbox Vector即用户最新微博列表就采用这种模式。一些粉丝较少且不活跃的用户发表微博后Vector 服务会首先查询 Vector Cache如果 cache 中没有该用户的 Outbox 记录,则不写该用户的 cache 数据,直接更新 DB 后就返回,只有 cache 中存在才会通过 CAS 指令进行更新。
Write Behind Caching
Write Behind Caching 模式与 Read/Write Through 模式类似,也由数据存储服务来管理 cache 和 DB 的读写。不同点是数据更新时Read/write Through 是同步更新 cache 和 DB而 Write Behind Caching 则是只更新缓存,不直接更新 DB而是改为异步批量的方式来更新 DB。该模式的特点是数据存储的写性能最高非常适合一些变更特别频繁的业务特别是可以合并写请求的业务比如对一些计数业务一条 Feed 被点赞 1万 次,如果更新 1万 次 DB 代价很大,而合并成一次请求直接加 1万则是一个非常轻量的操作。但这种模型有个显著的缺点即数据的一致性变差甚至在一些极端场景下可能会丢失数据。比如系统 Crash、机器宕机时如果有数据还没保存到 DB则会存在丢失的风险。所以这种读写模式适合变更频率特别高但对一致性要求不太高的业务这样写操作可以异步批量写入 DB减小 DB 压力。
讲到这里,缓存的三种读写模式讲完了,你可以看到三种模式各有优劣,不存在最佳模式。实际上,我们也不可能设计出一个最佳的完美模式出来,如同前面讲到的空间换时间、访问延迟换低成本一样,高性能和强一致性从来都是有冲突的,系统设计从来就是取舍,随处需要 trade-off。这个思想会贯穿整个 cache 课程,这也许是我们学习这个课程的另外一个收获,即如何根据业务场景,更好的做 trade-off从而设计出更好的服务系统。
缓存分类及常用缓存介绍
前面介绍了缓存的基本思想、优势、代价以及读写模式,接下来一起看下互联网企业常用的缓存有哪些分类。
按宿主层次分类
按宿主层次分类的话,缓存一般可以分为本地 Cache、进程间 Cache 和远程 Cache。
本地 Cache 是指业务进程内的缓存,这类缓存由于在业务系统进程内,所以读写性能超高且无任何网络开销,但不足是会随着业务系统重启而丢失。
进程间 Cache 是本机独立运行的缓存,这类缓存读写性能较高,不会随着业务系统重启丢数据,并且可以大幅减少网络开销,但不足是业务系统和缓存都在相同宿主机,运维复杂,且存在资源竞争。
远程 Cache 是指跨机器部署的缓存,这类缓存因为独立设备部署,容量大且易扩展,在互联网企业使用最广泛。不过远程缓存需要跨机访问,在高读写压力下,带宽容易成为瓶颈。
本地 Cache 的缓存组件有 Ehcache、Guava Cache 等,开发者自己也可以用 Map、Set 等轻松构建一个自己专用的本地 Cache。进程间 Cache 和远程 Cache 的缓存组件相同,只是部署位置的差异罢了,这类缓存组件有 Memcached、Redis、Pika 等。
按存储介质分类
还有一种常见的分类方式是按存储介质来分,这样可以分为内存型缓存和持久化型缓存。
内存型缓存将数据存储在内存,读写性能很高,但缓存系统重启或 Crash 后,内存数据会丢失。
持久化型缓存将数据存储到 SSD/Fusion-IO 硬盘中,相同成本下,这种缓存的容量会比内存型缓存大 1 个数量级以上,而且数据会持久化落地,重启不丢失,但读写性能相对低 12 个数量级。Memcached 是典型的内存型缓存,而 Pika 以及其他基于 RocksDB 开发的缓存组件等则属于持久化型缓存。

Some files were not shown because too many files have changed in this diff Show More