100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
From 077554a3b23e06d7294c7b43254d62451f65bb22 Mon Sep 17 00:00:00 2001
From: ramsayleung Archive
-
2023 33
五月 5
三月 6
2022 8
2021 13
2020 9
2019 16
2018 9
2017 30
三月 10
听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户, 而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
diff --git a/zh/categories/swift/index.html b/zh/categories/swift/index.html
index 5c59e241..b5f934ef 100644
--- a/zh/categories/swift/index.html
+++ b/zh/categories/swift/index.html
@@ -1,6 +1,6 @@
swift | 自由庄园
swift
-
100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
四月 9, 2024 · 5 分钟 · 2289 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
+100 Days of SwiftUI
1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副...
四月 9, 2024 · 5 分钟 · 2360 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
Powered by
Hugo &
PaperMod
\ No newline at end of file
diff --git a/zh/categories/swift/index.xml b/zh/categories/swift/index.xml
index 3fd82cc4..721be2c3 100644
--- a/zh/categories/swift/index.xml
+++ b/zh/categories/swift/index.xml
@@ -19,10 +19,10 @@
Tue, 09 Apr 2024 19:04:00 -0700
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软
+ 1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副
1 缘起
-我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
-先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
+我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
+先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
-并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
-经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
-但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
+在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
+经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
+但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
-Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
-按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
+Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
+按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
-又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
+又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
@@ -64,7 +64,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户, 而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
diff --git a/zh/categories/swiftui/index.html b/zh/categories/swiftui/index.html
index 2b5afe9e..9f40a262 100644
--- a/zh/categories/swiftui/index.html
+++ b/zh/categories/swiftui/index.html
@@ -1,6 +1,6 @@
swiftui | 自由庄园
swiftui
-
100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
四月 9, 2024 · 5 分钟 · 2289 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
+100 Days of SwiftUI
1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副...
四月 9, 2024 · 5 分钟 · 2360 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
Powered by
Hugo &
PaperMod
\ No newline at end of file
diff --git a/zh/categories/swiftui/index.xml b/zh/categories/swiftui/index.xml
index 42957182..af49a988 100644
--- a/zh/categories/swiftui/index.xml
+++ b/zh/categories/swiftui/index.xml
@@ -19,10 +19,10 @@
Tue, 09 Apr 2024 19:04:00 -0700
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软
+ 1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副
1 缘起
-我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
-先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
+我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
+先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
-并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
-经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
-但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
+在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
+经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
+但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
-Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
-按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
+Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
+按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
-又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
+又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
@@ -64,7 +64,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户, 而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
diff --git a/zh/index.html b/zh/index.html
index 39d0689c..d9d12dc8 100644
--- a/zh/index.html
+++ b/zh/index.html
@@ -1,5 +1,5 @@
自由庄园
-Hi there 👋
There is Ramsay’ personal blog, where he writes about life, software enginering, distributed system and thought.
– Ramsay Leung100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
四月 9, 2024 · 5 分钟 · 2289 字 · Ramsay Leung 三月的樱花
2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放...
三月 24, 2024 · 1 分钟 · 145 字 · Ramsay Leung 简明写作指南
1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英...
三月 15, 2024 · 9 分钟 · 4453 字 · Ramsay Leung 金榜题名之后
1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取...
二月 25, 2024 · 7 分钟 · 3470 字 · Ramsay Leung 登陆加拿大一年后的体会
1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾...
二月 25, 2024 · 11 分钟 · 5142 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
+Hi there 👋
There is Ramsay’ personal blog, where he writes about life, software enginering, distributed system and thought.
– Ramsay Leung100 Days of SwiftUI
1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副...
四月 9, 2024 · 5 分钟 · 2360 字 · Ramsay Leung 三月的樱花
2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放...
三月 24, 2024 · 1 分钟 · 145 字 · Ramsay Leung 简明写作指南
1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英...
三月 15, 2024 · 9 分钟 · 4453 字 · Ramsay Leung 金榜题名之后
1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取...
二月 25, 2024 · 7 分钟 · 3470 字 · Ramsay Leung 登陆加拿大一年后的体会
1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾...
二月 25, 2024 · 11 分钟 · 5142 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
Powered by
Hugo &
PaperMod
\ No newline at end of file
diff --git a/zh/index.json b/zh/index.json
index f0ed9803..f525318d 100644
--- a/zh/index.json
+++ b/zh/index.json
@@ -1 +1 @@
-[{"content":"1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。 先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI. 每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了. 今天是结课之日,我通过了结课的考试,总分100分,考了91分,喜提课程证书一枚. 并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费). 经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是) 但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈 2 Swift 初体验 Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。 按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。 因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling, Result, Generic, Enumerations, Protocol 这些概念都和Rust的大同小异。 又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅. 让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension 功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能. 例如,以下的代码就可以扩展内置的 Double 类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换: extension Double { var km: Double { return self * 1_000.0 } var m: Double { return self } var cm: Double { return self / 100.0 } var mm: Double { return self / 1_000.0 } var ft: Double { return self / 3.28084 } } let oneInch = 25.4.mm print(\u0026#34;One inch is \\(oneInch) meters\u0026#34;) // Prints \u0026#34;One inch is 0.0254 meters\u0026#34; let threeFeet = 3.ft print(\u0026#34;Three feet is \\(threeFeet) meters\u0026#34;) // Prints \u0026#34;Three feet is 0.914399970739201 meters\u0026#34; 总体而言, Swift是一门吸收了众多PL理论的现代编程语言 3 SwiftUI SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack, HStack, ZStack 就可以实现X轴,Y轴,和Z轴方向的布局 例如下面这个就是通过 ZStack 几行代码实现的叠加效果: let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple] var body: some View { ZStack { ForEach(0..\u0026lt;colors.count) { Rectangle() .fill(colors[$0]) .frame(width: 100, height: 100) .offset(x: CGFloat($0) * 10.0, y: CGFloat($0) * 10.0) } } } 除了声明式语法之外,SwiftUI让人赏心悦目的就是动画。好的动画在App里面绝对能起到画龙点睛的作用,而SwiftUI的内置动画已经非常强大了,下面就是使用内置动画实现的动画效果: struct ContentView: View { @State private var dragAmount = CGSize.zero @State private var enable = false let letters = \u0026#34;Hello, World\u0026#34; var body: some View{ HStack(spacing: 0) { ForEach(0..\u0026lt;letters.count, id: \\.self) { index in Text(String(letters[letters.index(letters.startIndex, offsetBy: index)])) .padding(5) .font(.title) .background(enable ? .green : .blue) .offset(dragAmount) .animation(.linear.delay(Double(index) / 20), value: dragAmount) } }.gesture( DragGesture() .onChanged { dragAmount = $0.translation } .onEnded { _ in dragAmount = CGSize.zero enable.toggle() } ) } } struct HeartBeatView: View { @State private var animationAmount = 1.0 var body: some View { Button(\u0026#34;SOS\u0026#34;){ } .padding(50) .background(.red) .foregroundColor(.white) .clipShape(Circle()) .overlay( Circle() .stroke(.red) .scaleEffect(animationAmount) .opacity(2 - animationAmount) .animation(.easeOut(duration: 1) .repeatForever(autoreverses: false), value: animationAmount) ) .onAppear { animationAmount = 2 } } } 而Xcode 15新增的预览功能也很好用,可以让开发者不需要启动iPhone模拟器就能预览页面效果,节省了非常多的等待时间。 4 问题 听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。 SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData 甚至只支持IOS17, 更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。 不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI: 对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户, 而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。 因此SwiftUI就陷入了一个尴尬的境地,东西做得好,但是不会有人用; 没有人自然就不用有人分享,宣传这门技术,自然就导致相关的学习资料非常匮乏, 进一步加深了初学者的学习难度; 开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。 又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦. 另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage 框架,顾名思义是用来作图片处理. 但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如: func applyProcess(){ guard let outputImage = currentFilter.outputImage else {return} guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return} let uiImage = UIImage(cgImage: cgImage) processedImage = Image(uiImage: uiImage) } func loadImage() { Task{ guard let imageData = try await selectedItem?.loadTransferable(type: Data.self) else {return} guard let inputImage = UIImage(data: imageData) else {return} let beginImage = CIImage(image: inputImage) currentFilter.setValue(beginImage, forKey: kCIInputImageKey) applyProcess() } } 把 CoreImage 框架的 CIImage 转成 CoreGraphics 框架的 CGImage, 然后再把 CGImage 转换成 UIKit 框架 UIImage, 然后再转换回SwiftUI 内置的 Image 类型, 可谓是相当麻烦了. 但是对比SwiftUI只支持高版本的问题,Objective-C和Swift的互操作问题也只能算是恶心,但是起码有解决方法,对于前者,开发者是完全没法自行解决. 5 总结 过了一把野生IOS开发的瘾,但是除非是苹果愿意让SwiftUI支持低版本的IOS, 不然我是不会继续使用SwiftUI来开发IOS了,受众太有限了,也没有太大的前景。 想要支持低版本的IOS,就只能走UIKit和Objective-C这条历史老路,我对此着实是望而生畏,有空还是学习点其他有趣的东西。 https://docs.swift.org/swift-book/documentation/the-swift-programming-language/guidedtour/ \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.hackingwithswift.com/100/swiftui \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/ramsayleung?tab=repositories\u0026amp;q=\u0026amp;type=\u0026amp;language=swift\u0026amp;sort= \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://en.wikipedia.org/wiki/Chris_Lattner \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/","summary":"1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软","title":"100 Days of SwiftUI"},{"content":"2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放。 在春日的夕阳下,漫步在樱花树下,微风吹过,樱花落下, 着实有「落英缤纷」的感觉 看着眼前的樱花,想着她冬日的美貌,脑海浮起诗句: 昨日雪如花,今日花如雪 ","permalink":"https://ramsayleung.github.io/zh/post/2024/%E4%B8%89%E6%9C%88%E7%9A%84%E6%A8%B1%E8%8A%B1/","summary":"2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放","title":"三月的樱花"},{"content":"1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英语,而不是像在学校那样「学」好英语。 在《口语篇》中,李笑来提到,比口语更重要的是思考能力,英文说不出口的原因,可能是脑子里面没有什么思考沉淀的东西可以说的,并藉此推荐了三本关于文风(Style)的必读书籍,其中一本就叫:A Plain English Handbook(简明英文写作指南)2。 这本书竟然是美国的证券交易委员会(Securities and Exchange Commission)1998年编著的, 旨在指导投资机构和金融机构创建更清晰,更易理解的披露文件。 可能是投资机构写的东西,普通投资者根本看不懂,逼得证券交易委员会都要下场指导投资机构写作了。 这本小册子只有83页,内容却很详实,读完之后,觉得其中的许多技巧不只适用于英文写作, 因此就结合读后感和个人心得,分享下简明写作的心得。 2 为什么写作 以我自己的工作为例,日常开发新项目,需要撰写文档来向同事和经理介绍项目背景, 动机和具体的实现细节,以寻求支持并推动项目的进展;技术交流时,需要撰写文档分享你的成果和经验;在晋升时,需要撰写文档,给自己找数据点来说服经理,为什么要给我晋升。 需要让别人「看见」我的时候,写文章就是一种非常好的手段,默默无闻的老黄牛,是很难被人看到的,酒香也怕巷子深。 你可能会认为自己工作用不到文档,但是你总归是要向同事或者上司阐述自己的观点, 无论是述职,晋升,演讲,甚至口头汇报,用文档作腹稿, 理清脉络,做到胸有成竹。 关于写作的动机和好处,我之前写过一篇文章专门来聊:《闲聊写作的好处》,这里就不赘述了。 3 如何写 如果把文章比作一个人的话,那么文风就是人的血肉和皮囊,文章的结构就是人的骨架,只有当骨架先立了起来,才能在其上涂血肉,张皮囊。 A Plain English Handbook 主要介绍的是改善文风的技巧,那么如何立起文章的骨架呢?我推荐的是黄金圈法则和金字塔原理。 3.1 结构 3.1.1 黄金圈法则 所谓的黄金圈法则,概括来说,就是思考问题的三个层面,分别是: Why: 最内层, 为什么,做一件事的原因或者目的,也就是我们为什么做这样的事情,战略层面。 How: 中间层, 怎么做,我们如何实现我们想要做的事情, 战术层面的事 What: 最外层,事情的表象,我们具体做的每一件具体的事,执行层面的事 比如我要推动一个新项目,需要写项目文档,或者口头向老板阐述项目,如果不用黄金圈法则,可能会是这样表述的: What: 我要做一个XX的服务,会有哪些新功能 How: 我是通过什么业界领先的XX技术实现的,用到了什么组件. 如果以黄金圈法则来重写项目文档,那么文章的结构应该是如何的: Why: 我们为什么要做这个项目?这个项目能给我们带来什么好处,不做会有什么损失? How: 大方向上应该怎么实施,大概会用到什么组件,架构如何? What: 具体的底层实现是什么,每个组件是怎么实现的? 大方向没有定好,再走下去也只是南辕北辙,于事无补。 其实这篇文章也使用了黄金圈法则,开篇就介绍为什么要写作,后续再介绍如何写作。 3.1.2 金字塔原理 关于金字塔原理,我觉得冯唐的《老聃的金字塔原理》一文3已经解释得非常清晰明了了: 用一句话说,金字塔原则就是,任何事情都可以归纳出一个中心论点,而此中心论点可由三至七个论据支持,这些一级论据本身也可以是个论点,被二级的三至七个论据支持,如此延伸,状如金字塔。 如果用金字塔原理来分析「为什么小王是个好对象」的论点,那么论据就可以拆分成: 家境殷实 年入百万 有车有房无贷 父母养老无忧 前景光明 名校毕业 年纪轻轻身居中层 领导赏识 相处融洽 提供情绪价值 共同话题多 情绪稳定 颜值高 皮肤白里透红 五官端正 身材修长 穿衣显瘦,脱衣有肉 基因好,后代获得先发优势 写作时,每个一级论据就是一个大的篇章,每个二级论据就是篇章下的章节,三级论据就是章节里面的小节, 依此类推,并给予最底层的论据适当的文字描述。 如果还不够的话,还可以继续向下拆分论据。 有了黄金圈法则和金字塔原理,就很容易把一篇文章的结构给搭起来。 3.2 文风 3.2.1 明确你的观众 明确你的观众,是确保你写的文档能让人理解的最重要步骤。 不同的读者,有不同的背景,对你要传递的信息是有不同的理解难度。 以我自己为例,因为我在公众号写的文章大多都是与编程技术无关的, 那么吸引到的读者自然也不会是编程从业者,所以我在公众号里面写技术文章,基本不会有什么读者阅读。 兼之微信这个阅读平台本身的局限,大多数情况下只能是在手机上阅读, 读者无法投入大量时间「沉浸式」地主动阅读,可能是快速下拉翻页读完了。 而图表和代码在手机屏幕上展示效果不佳,就进一步影响阅读体验了。 如果我把写满技术名词和代码的文档给一个完全没有技术背景的读者来阅读, 即使我的文档写得妙笔生花,对他也没有任何信息可言。 因此我基本不会在公众号写技术文章,技术文章都放在更适合在电脑阅读的博客上。 这里还有个小技巧,就是在明确你的观众的时候,可以设定到一个具体的人, 例如是你的女友/男友,你的同事,或者是你的经理。 具体的人比抽象的概念更深入人心。 3.2.2 言简意赅 能用简洁明了的段落表达全的信息,就不要长篇大论。 读者是有心智负担的,文章的内容越长,读者的负担越重,就越有可能在还没读完的情况下将文章关闭。 另外一方面,如果要传递的信息量是固定的,你的文章内容越长,你文章的信息密度越低,通俗点来说,就是干货越少。 所以我对「万字长文,讲透xxx」,「爆肝x天,四万字长文带你解读xxx」之类的文章不感冒, 文字多也不能说明干货多,爆不爆肝和干货含量也没有逻辑关联。 现在写作不按字数算稿酬,不需要搞「文字灌水」。 《唐宋八家丛话》中有一说: 欧阳修在翰林院时,常常与同院他人出游。 一次,见有匹飞驰的马踩死了一只狗。 欧阳修说:“请你们尝试描述一下这事。” 一人说:“有犬卧于通衢,逸马蹄而杀之。” 另一人说:“有马逸于街衢,卧犬遭之而毙。” 欧阳修笑说:“像你们这样修史,一万卷也写不完。”那二人说:“那你说呢?” 欧阳修道:“逸马杀犬于道。” 那二人相互笑了起来。 3.2.3 少说黑话 少说黑话和行话,例如「组合拳」,「赋能」,「抓手」之类的,字我都认识,合起来就不明白是什么意思。 如果你明确了你的观众,你可能就会意识到你的观众大概率无法理解这些话语。 多用具体的,意思明确的词,会更让读者更容易理解。能用简单的话解释清楚一个复杂的概念,就说明你对这个概念的认识越到位。 黑话用多了,也是一种「文字腐败」。 3.2.4 控制段落长度 上文提到,读者是有心智负担,负担越大,他们就越有可能在阅读你文章时「半途而弃」。 而一大片文字密密麻麻糊在一个段落,就会进一步加重他们的心智负担。 我一般推荐80到150个字一个段落,这样看上去不至于太吓到读者。 3.2.4.1 使用空行 段落与段落之间,用 空行 分隔. 空行是个非常简单,但是却异常有效的技巧,既可以拆分段落,控制单个段落的长度,也可以表达不同段落逻辑上存在并列或者递进的关系,便于读者理解。 同样的文字,使用空行分隔段落的前后对比如下: 3.2.4.2 使用列表 另外一个控制段落长度的有用技巧,是使用列表(bullet list),可以表达列表中的每个点都是并列关系。 例如,分析跑步的好处: 跑步不仅可以增强免疫系统,帮助抵御病毒和细菌,降低患病风险; 还有助于心肺功能,降低心脏病和中风的风险; 更可以减脂、增强肌肉,改善体形; 甚至促进肠道蠕动,有助于消化。 如果换成列表,那么跑步的好处就是: 提高免疫力:增强免疫系统,帮助抵御病毒和细菌,降低患病风险 改善心血管健康:有助于心肺功能,降低心脏病和中风的风险 塑造好身材:可以减脂、增强肌肉,改善体形 促进消化:促进肠道蠕动,有助于消化 如果还有优点补充,只需要继续增加列表就好了。 3.2.5 一图/表胜千言 人的大脑对图片远比文字和声音敏感。 图片比文字来说,更容易被大脑接受,大脑储存图片信息也不需要进行过多的转译,而文字进入大脑之后,还需要用“想象力”处理成画面进行记忆,这也就是为什么带生活实例的文字会比概念化的文字更容易让人记住,因为前者更容易让你想象具体的画面。 通俗地讲,就是一图胜千言。 所以要减少读者的心智负担,那么就应该多使用图表,因为它能更直观地传递更多信息。 以国家统计局发布的2月份居民消费价格分类同比涨跌幅为例,文字描述如下: 其中,教育文化娱乐、其他用品及服务、衣着价格分别上涨3.9%、3.0%和1.6%,医疗保健、生活用品及服务、居住价格分别上涨1.5%、0.5%和0.2%;交通通信价格下降0.4%。 图表如下: 关于如何画图,我之前也写过一篇文章来分享心得:《我的画图流:画图工具与技巧分享》 或者换成表格: 类别 涨跌幅 教育文化娱乐 + 3.9% 其他用品及服务 + 3.0% 衣着 + 1.6% 医疗保健 + 1.5% 生活用品及服务 + 0.5% 居住 + 0.2% 食品烟酒 - 0.1% 交通通信 - 0.4% 4 如何写好 4.1 多读多思 杜甫说,读书破万卷,下笔如有神。诗圣的意思是如果你通读过万卷书,就好像ChatGPT一样,思如泉涌,下笔如有神 陆游又说,纸上得来终觉浅,绝知此事要躬行。陆放翁的意思是,你在书上的读到的东西,也只是信息,如果没有实践过,终究不能成为技能的。 总结两位老人家的话,也就是说,多读书是写好文章的必要条件,多读书不一定能写好文章,但是不多读书呢,就一定写不出好文章。 毕竟写出来的东西,不会凭空而来,还是你脑子思考好的结果。 4.2 多写多改 与金庸并称香港四大才子的倪匡,一生写了300多部小说、400多部电影剧本。 他曾经自嘲没有谋生本能,所以看见人家写自己也写,自称是全世界写汉字最多和最快的人,自入文坛已写作三十年, 一个星期写足七天,每天写数万字。他在创作最高峰曾同时写作十二本科幻小说。 当被问及有何建议赠给写作的人,倪匡说只有一个「写」字,只有多写才能得到更多的灵感。 苏轼的《东坡志林》写到,有人问欧阳修怎么写文章,他回答说: 无他术,唯勤读书而多为之,自工。 世人患作文字少,又懒读书,每一篇出,即求过人,如此少有至者。 疵病不必待人指摘,多做自能见之。” 世人文字写得少,又懒读书,写一篇文章就希望可以超越别人,像这样是难有写得好的人。 书读多了,落笔为文,文章写多了,自然就写好了。 写作的「捷径」就是老老实实多读书、多思考、勤写作。 多练多写多修改。 只有产量提上来了,工艺才会成熟,良品率才会提高。 5 总结 写作并非是雄关漫道,遥不可及。 大家都写过作文,写作也只是一种交流方式,无非是把脑海中的想法以文字的形式付诸于纸上,比聊天更加正式而已。 千里之行始于足下,万巻之文始于笔下,开始写就好了。 5.1 历史文章推荐 旅加经历 这些年走过的路:从广州到温哥华 加拿大之初体验 加拿大考驾照的经历 登陆加拿大一年后的体会 历史与思考 为什么梦想买不起,故乡回不去(和谐版本, 原版本4) 润向何方:不完全肉身翻墙指北 皇帝与官僚:「上面」与「下面」 闲聊写作的好处 金榜题名之后 工具与分享 我的写作流 我的画图流:画图工具与技巧分享 我的搜索流:高效搜索经验分享 最好的学习方式:费曼学习法(Feynman Technique) 系统思考:既见树木,又见森林 两个鲜为人知的Gmail地址技巧 职场与思考 那些年,我从微信支付学到的东西 https://github.com/xiaolai/everyone-can-use-english/blob/main/book/README.md \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.sec.gov/pdf/handbook.pdf \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://zhuanlan.zhihu.com/p/196733201 \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttp://ramsayleung.github.io/zh/post/2023/%E7%BD%AE%E8%BA%AB%E4%BA%8B%E5%86%85/ \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://ramsayleung.github.io/zh/post/2024/%E7%AE%80%E6%98%8E%E5%86%99%E4%BD%9C%E6%8C%87%E5%8D%97/","summary":"1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英","title":"简明写作指南"},{"content":"1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取士以来,通过读书来改变自己乃至整个家族的命运,就已成为中国社会妇孺皆知的常识,”读书改变命运“。 古有《劝学诗》云:“男儿若遂平生志,六经勤向窗前读”;今有高悬在高中教室的励志标语:“辛苦三年,幸福一生”。 但是,进入好的大学,是否就意味着毕业时令人羡慕的工作呢?为什么社会出身劣势的学生即使进入了最好的大学,却仍然在毕业出路与生涯前景劣势明显? 对于寒窗苦读,从中国甘肃考入复旦大学的作者来说,她也有同样的疑问。 因此在研究生期间,就将学生家庭背景与教育结果之间的关联作为研究课题,并将研究生论文润色,扩展成书。 在读别人的书时,我总会想起自己的事,好比用别人的料理方式,来烹饪自己的食材,看能煮出什么样的【佳肴】。 虽然我未曾【金榜题名】,但也未妨我来分享下自己的所思所想。 2 迷宫的游戏规则 以前高中老师在我们向其抱怨各种学业压力和严苟的规章制度之后,总是会语重心长地解释一番,然后在对话的结尾补上一句,”上到大学你们就轻松啦“。 不同于高中只有成绩的单一的显式考核标准,只有一个方向和出口,大学更像是一个被精心布局的迷宫:并不存在一条”主干道“或者标准的走法,每一天的过法都有许多可能,每个人在路口处需要不断地做选择,每一条小路(社团,绩点,实习)都各有乾坤。 而大学生作为玩家,需要在小路之中穿行探索,一边选择自己路线,一边在路途收集有价值的筹码(成绩,经历,奖项等)。 当他们到达迷宫出口时,他们需要将口袋的筹码拿出来,用它们来兑换成下一个旅程的入场券。 只是,对于不同社会出身的探索者来说,这个迷宫的神秘程度是不同的。有人对里面的布局相当了解,有人半知半解,而有人只能通过道听途说略知一二。 尤其关键的是:并不是所有人都很清楚当中的游戏规则,譬如迷宫的尽头究竟有哪几个出口,而每个出口处有用的筹码又有什么不同。 而不同等级的大学,能提供的有价值的筹码数量和价值又是截然不同的。 为了区分在两种截然不同的驱动力下探索迷宫的玩家,作者将两者定义为: 目标掌控模式:了解大学及劳动力市场中的规则,因此能有意识地树立生涯目标,并且通过管理自身的行动来趋近目标。 直觉依赖模式:在陌生的大学场域中难以自我定位,从而无意识地陷入无目标状态,主要倚靠直觉和旧有习惯来组织大学生活。 家境优越的学子,在父母的指导下,可以更早和清晰地认识到大学的游戏规则,进而策略性地计划大学生活;而寒门学子,因为眼界和自身经历的局限性, 并不了解大学的规则,又会发现高中沉淀的学习方式在大学并不完全适用,难免陷入迷茫。 优势阶层的父母基于对迷宫的洞悉,为孩子织就一张“安全网”,帮助孩子认清形势,定位自身,树立目标,顺利通关;而弱势学生则没有这张安全网,只得独自在这个陌生领域无助摸索前进,父母能提供的建议越来越少,往哪走全靠自己或对或错的直觉和过去的习惯。 但因为作者只调研了名校的优等生,如果把排名后50%的学子也纳入调研范围,可能会发现还有一种【躺平放纵模式】,以高中老师口中【轻松】的方式度过了大学生涯。 3 非名校生的大学 近年来,红遍网络的“985废物”和“小镇做题家”的自嘲俯拾即是: “从一个连电影院都没有的小县城,考到了全河北最好的高中,六年之后要来到国际大都市上海了,要来到倾尽我家所有小积蓄,才能勉强付个首付的上海了。我这才发现,光考了好大学也是没有用的。” \u0026ndash; 《我上了985,211,才发现自己一无所有》 如果考上985,211的“小镇做题家”都如此自嘲一无所有,那些没有考上211的小镇考生,又该如何自处呢? 当聊起大学和学历话题的时候,我时常和名校毕业的高中好友开玩笑,我不是“小镇做题家”,虽然我和你们一样来自小镇,但是我连题都做不好。 3.1 直觉依赖者的探索之路 如果按照书中作者的分类标准,我属于来自农村的,第一代大学生(父母都不是大学生)的直觉依赖模式学子。 是那种最需要靠教育改变命运的学生,并不真的知道该在大学里如何做才能改变命运。 对于升学,出国,就业这三个出路,我一开始就把【出国】这个选项给排除了,小镇来的我,家里并没有足够的财力支撑我出国,何况我的学校也不容易申请到好学校(现在看来,这个理由略为牵强)。 升学读研也不是我的目标,我对走学术路线没有兴趣,读完研也终究要工作,计算机相关行业,研究生学历与本科学历相比,并没有跃迁式差距。 所以我一开始就选定了【就业】的路线,我也希望可以早点赚钱为父母分担。 因为没有人指导,又不清楚迷宫的规则,我并不清楚要如何做才能取得【就业】的优势,或者做好就业的准备。 大一开始时,还是按照高中的【勤学苦读】模式,晚上没有课的时候去课室学习,解高数的练习题,再预习专业课。 这样的学习模式持续了两个月,觉得相当别扭和迷茫,看到周围宿舍的大神们已经开始编写代码,甚至其中还有中学就开始自学编程的大神,我解高数题又有什么用呢。 恰好赶上学校的社团招新,我就加入一个校级社团的【网络部】,希望可以借机学习到计算机相关的知识,学习怎么修电脑或者装操作系统。 社团生活的确让我学习到基础的电脑维修知识,更重要的是,让我认识到同系的学长们,对于我想学习修电脑的兴趣,他们纠偏道,单纯的硬件组装其实没有太大的潜力,还是做软件开发更有前途。 在学长们的指导下,我开始到慕课网和W3School学习Html,CSS,Javascript这三剑客,开始学习前端开发,而后我又了解【前端开发】,【后端开发】,【移动端开发】的职业路径。 浅尝【前端开发】之后,发现自己对此并没有太多兴趣,又开始尝试【后端开发】的路线。 因为慕课网有把【后端开发】的课程列出来,我就知道【后端开发】需要学习什么样的技术,如Java/C++,数据库,计算机网络,我就可以有的放矢地去豆瓣上搜索对应的高分书籍来自学。 我的迷宫有了【地图】,我自然是要加速前行,我在大二的时候,就用豆瓣上列出来的经典教科书,把相关的必修专业课给自学完了,效果虽不如老师亲授,但终究有所获。 就如我在《那些年走过的路,从广州到温哥华》介绍过,而后就走上做项目和实习之路。 对于【直觉依赖模式】的学子来说,我觉得了解迷宫游戏规则最好的方式是和同系的学长学姐交流,因为他们曾处在和你同样的境况,又就读于相同的院校和专业,并且他们先于我们两到三年去探索这个迷宫,所以他们能提供最切实可行,并且可复制的路径。 3.2 答案并不唯一 正如作者所言,这个迷宫会有不同的出口,在大学过的每一天会将学子领入不同的小路。 二十岁的我可能希望可以从自己愿景的出口走出来,三十岁的我却有了不一样的看法,能找到自己的迷宫出口固然可贵,但是探索迷宫的过程也相当珍贵,大可不必那么功利,迷茫也是人生常态。 【就业】,【升学】,【考研】只是社会的评判标准,做个有趣的人也未尝不可。 4 大学之后 在大学的时候,受学长们的影响,觉得能去BAT(Baidu,Alibaba,Tencent)的大厂,就算是个很好的出路了。 所以当时一心把去大厂当作毕业的目标,当拿到个A厂Offer之后,就没有面试其他公司,自然也没有offer来compete。 但是我从来没有想过,拿到Offer,入职之后,又该如何? 再追求升职加薪,面向晋升编程? 以前是把高考结束,考上大学当作是马拉松的终点,上大学之后又把拿Offer当作迷宫的出口? 如果人生是个没有出口的迷宫呢。 4.1 迷宫并没有出口 作者说大学是个有多个出口的迷宫,如果把迷宫的概念延伸出来,每个人的人生从大学之后就都是迷宫了(如果不上大学,那么出路就更早分化),高中之后,就不再是以成绩作为唯一的标准。 当标准都不一样,就更没有办法衡量什么是【最好】的路,只有最适合自己的路。 享受探索迷宫的过程也可成为乐趣。 4.2 路要怎么走 过年前和发小喝茶聊天,聊到了他为人夫,为人父的事情,他提到当初他在产房等待女儿出生的时候,护士和他聊天的事: 护士问,他是否是95后,他说是。 护士说,95后现在都不想生孩子了,觉得压力很大。 我:那你有同样的感觉么? 发小:有阿,也觉得压力很大。 我笑道:那你还生? 发小笑回:世界上只有一种英雄主义,那就是认清生活的真想之后,继续热爱生活。 在两个人的笑声中,听到发小说出罗曼罗兰的名句,觉得发小已经活出这句话的精髓了。 写在最后,鸡汤一下: 路要怎么走,每个人都会有自己的选择,但了解迷宫的规则的【学子】,路可以走得更加自在。 5 参考 《金榜题名之后:大学生出路分化之迷》 《我上了985,211,才发现自己一无所有》 ","permalink":"https://ramsayleung.github.io/zh/post/2024/%E9%87%91%E6%A6%9C%E9%A2%98%E5%90%8D%E4%B9%8B%E5%90%8E/","summary":"1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取","title":"金榜题名之后"},{"content":"1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾里看花,再分享自己在加拿大的亲身经历 本文算是《那些年走过的路,从广州到温哥华》和《温哥华的初体验》的后续。 2 Work Life Balance 自我工作以来,基本就是在体验995的工作节奏,我曾经无数次【幻想】过,如果我能每天5点下班,我的生活会是怎么样的? 我会有接近6-7个小时的空闲时间,我会把这些时间用来干什么呢? 当我不曾拥有时,我总是在不断地想象。 但当我真的可以5点,甚至4点多下班的时候,我并没有我自己想象的那般激动,欣喜若狂,而是当作理所当然,很平淡地接受。 毕竟我所在的BC省,法定工作时间都只是7.5个小时,我朝九晚五,甚至有点高于本地平均水平,尤其是我在北美著名的【血汗工厂】打工,需要Oncall,甚至比本地公司还要卷,所以我开始觉得朝九晚五工作时间有点长。 人阿,就是贪婪,总是会得寸进尺,得陇望蜀。 我5点下班之后,我可以【奢侈】地花一个小时去做晚餐,然后吃完晚餐和舍友一起看个把小时的电视,一边撸着猫一边吐槽今天的工作内容;或者在天气放晴的时候,和舍友在附近的公园饭后溜圈,再考虑下明天要学习哪个视频,做点什么新菜。 饭后到睡前的时间,花一到两小时,学习一下新的技术,Swift或者Ruby on Rails,或者读读新书,又或者和家人亲友视频聊个天,互诉衷肠。 原来那些失去的,用于加班的时间,重新获得后,也只是把它还给生活本身。 3 英语 未落地加拿大时,最最忧心的问题就是自己的英语不过关,无法正常地与人沟通交流,也无法正常工作。 毕竟我此前没有考过雅思,也没有在纯英文的环境中生活过,不知道自己英语水平如何。 落地之后,强迫自己开口对话,虽然难免会出现词不达意和【执笔忘字】的情况,但终究是敢开口说话了, 难免会遇到不认识的词不标准的发音,但是快速纠错之后,情况就慢慢在好转,脸皮厚一些就好了。 后来还花了两周时间准备了雅思考试,顺便测试下自己的英语能力,然后考了个7分,好像还行。 刚开始产品经理们开会,他们都是美国人,是真能扯,语速也真的快,好像高中时候的英语听力一样,只看到两个人不停地在张嘴说话,大脑一片空白。 到后面熟悉公司的黑话之后,情况也在慢慢变好,也听懂他们在说什么了,的确也是在扯。 从以前非常紧张与同事1:1开会,当现在已经能主动和同事拉会1:1,我可以感觉到自己的听力和口语能力也在不断地提高。 说到底,外语也不是什么特别的秘技,也只是种用进废退技能而已。 4 惊喜 人言洛阳花似锦,偏我来时不逢春。 想来入行前,都是听说互联网公司的各种红利,但是当然真正来到这个行业时,才发现自己啥红利都没有吃上,来了都是当人矿的,起了个大早,赶了个晚集,还碰上各种【奇遇】。 想我22年中面试的时候,那时还在北美【大放水】,通货膨胀的期间,各种大包满天飞,我却因为 international hire的原因,只赶上个low ball 包,因为我此前已经遇过很多次,已经可以泰然处之。 但是到23年初,受美国加息降通涨的影响,Meta和Twitter开了个坏头,北美的互联网都开始裁员,我司也不例外,不仅是裁员,连发出去的Offer开始撤回或者延期,然后我的Offer 也被影响了,原来面的组岗位被取消了。 我当时的心情不算是五雷轰顶,也算得上是晴天霹雳。 还好找到新的组接收,然后岗位被搬到另外一个新组,无论如何,先干着吧,不至于还没入职就失业,起码干的事情是新的,一切都是从0开始。 5 什么TM的叫惊喜 在新组,我是组里的第一个SDE(软件工程师),之前的两位组员都是DE(数据工程师),manager也是刚升任成经理的,甚至我入职时,他的 title都还没有变成 manager。 马上我发现,组里是新人,新组,老代码,人是新的,但是代码却是历史代码,我们需要去维护这些历史代码,但是没有人能解释其中的逻辑为什么要这么写? 紧接着,我发现,代码主体都是SQL,项目的逻辑隐藏在数以万行计的SQL代码中,因为SQL的抽象程度高,就更难以理解业务逻辑了。 4月底,在我入职不到3周,我就被安排成为一个新项目的 owner,然后被告知要在半年后的 Re:Invent发布,当时我甚至不知道什么是 Re:Invent. 后来才被告知,Re:Invent之于我司,犹如【WWDC】之于Apple,【微信公开课】之于微信,都是用来发布新产品的全球大会。 我当时心想,老板还真的看得起我嘛。 本着【能力越大,责任越大】的自嘲心理,我就这样接手了这个项目,成为了Owner。 和我的直属manager,总监以及产品沟通之后发现,他们似乎只要求要做这样一个产品,但是这个产品是什么, 应该怎么做,都是完全没有概念,也没有文档。 在我的认知中,一个项目从提出到上线的完整生命周期应该包括以下的部分: 某位领导或者产品经理提出新产品的想法 完善 use case, 细化想法 产品的各个利益方(stakeholders),或者叫涉众达成共识,领导层面获批 产品出需求文档,明确要做什么,具体的业务规则是什么 技术评估需求可行性 技术出设计文档 技术根据设计方案给出排期 技术开发需求,自测,内部上线 产品及涉众验收产品 内测及公测上线 然而,我现在拿到手的只是一个模糊的需求概念和上线的日期,没有详细的需求文档,口头描述了大概要做什么。 我只能不停地追着产品经理和manager问他们我要做什么,能否先给我个需求文档,对于需求文档,产品经理也不会直说没有, 只是会说解释一通后,让你意会到没有,我只能当练习英语听力。 最后我被告知,先把senior data engineer写的一大段SQL转成服务代码,把End-to-End的结果跑出来再说。 我就不懂,既然SQL都能跑了,还要我写个服务来跑SQL呢? 咨询了一番,我还是没得出个所以然,最后只能是按照这段SQL来写设计文档,并根据设计方案开发服务。 心里第一次浮起疑问:【贵司的做事方式就是这样的么?它是怎么做到这种规模的?】 7月初,美国转来了一位L6的 senior SDE还有一位L5的SDE也加入到项目里面,以缓解资源不足的问题。 加入后不久,这两位工程师也问起了需求文档的事,得到的回复也是言不及没有需求文档,意含没有需求文档。 没有需求文档实在是没法干活,最后是我们三个技术开发溱一起,每个人把自己对需求的理解一人一句写下来,也算是人生新经历了。 7月底,服务End-to-End 跑通,将结果呈给总监与产品经理,然后总监和产品经理反馈这不是他们要的,要求修改需求。 8月,根据修改后的需求重新设计服务,分成三个模块,三个工程师每人负责一个模块。 总监和产品经理再修改需求,并要求开发进行建模,但是新需求的模型不具备可行性,产品经理无法给出具体的业务规则,最后开发无法建模,导致新需求被搁置。 9月,主力产品经理突然宣布离职,此时离Re:Invent 不到两个月。 10月初,开发按照变更后的需求完成服务开发,然后发现服务使用的源数据全部是脏数据,服务结果不可用,团队已有使用该数据源的服务也是错的,开发紧急调研,再切换到新数据源。 11月初,所有服务组件万事具备,只待Re:Invent东风,然后被产品经理告知,我们的项目不能发布,因为没有在领导层面获得批准。 所以让开发紧赶慢赶,干了半年的大项目,连审批都还没有通过。 开发项目期间不停地浮起疑问,【按照这种做事方式,这家公司是怎么做到这种规模的?】 但做人不能半途而废,过河抽桥,所以即使心中百般疑问和不解,我依旧是尽心尽力把这个项目做完。 在做完这个项目之后,我就谋求转组了,这样的做事方式着实不是我的风格,我主观认为也非长久之计。 1月,GM(老板的老板的老板)离职。 2月,总监也离开了这个部门。 6 裁员阴云 自从2022年起,中美的互联网行业都笼罩在裁员的阴云之下,只是两者背后的原因各种不同。 朋友们在我登陆加拿大之后也和跟我吐槽国内环境变差,红利期已过,我只是个臭写代码的,也分析不出其背后的原因。 但是我知道的,大洋彼岸的北美大厂也在持续裁员,首当其冲的就是Google等大厂, 在人们2024年不要再有裁员的期待中,1月Google就以裁员来开年,真是【合家富贵】。 疫情时期的【大放水】,导致大厂们都用大包疯狂扩张,为了抑制通胀而采取的加息措施让企业们紧缩信贷, 压减成本,而人力成本在互联网大厂中可谓是占大头,然后在Meta和Twitter的带头下,开始挥刀裁员。 开始时,各大厂裁起员来还有些扭捏,裁完人公司高层还会写信安慰员工,说就裁这一波,高层还会出来道歉背锅。 然而裁到现在,已经变得明目张胆,和肆无忌惮,像Google这种, 都宣布2024年会持续裁员,还有其他大厂,就没有正式宣布裁员和什么时候结束裁员,就这么裁着。 毕竟在缺乏增长点情况下,裁员能缩减开支,让财报好看。 至于打工人们的看法,从来就不会有人在乎的。 所以「工作」也回归到它本身的意义上,这也只是份工作而已,It\u0026rsquo;s just a job,不要赋予工作过多的意义。 「得益于」裁员,我现在对工作的看法已经很佛系了,以前那种拼命卷,想拿到好绩效证明自己的想法已经不复存在了,也难怪朋友会说我现在心态变好了。 7 万税之国 虽然在来加拿大之前听说过加拿大的税非常高和多,但是只有从我的钱包把钱拿走,才能切实体会到什么叫【万税之国】。 除了薪资收入30+%的个人所得税外,还有日常消费12%的消费税,15%的酒税, 以及超过50%的资本所得税(比如银行存款利息,基金,股票收益等等),各种五花八门,名目繁多的税种。 虽然知道【死亡和缴税无可避免】,但是死亡是一次性的,缴税却是持续性的。 更何况,交税后的许多社会福利却是和收入挂钩的,你的收入越低,能享受到的福利就越多,而富人又有非常多的避税手段。 像 daycare, 牛奶金,低保这些,都是和每个人的收入挂钩,低就有,高就没有。 所以说下来,而低收入者可以少交税,却问政府要钱要福利;富人又可以避税,只有老实打工的中产是被收割的,福利又少。 难怪人们总说,加拿大适合躺平吃福利,不适合来挣钱,带资来加拿大的可以靠吃政府福利过得非常滋润。 8 医疗 加拿大的医疗体系是吃全民大锅饭,免费医疗。 免费的饭一般都不会很好吃,也不会很容易吃到。 加拿大的医疗体系我还没有机会亲身体验过,但是舍友有过几次的问诊纪录,原来抽个血化验排队等个两-三个小时着实是件很稀松平常的事。 9 此处并非天堂 世界上不存在天堂,所以如果抱着前往天堂的愿景来加拿大,难免会失望,加拿大也有自身的问题。 疫情期间为了保消费实行的【大放水】政策导致持续的高通涨, 高企的物价,为了抑制通涨而实行的加息政策而导致高企的利率,7-8%的房贷车贷利率。 飙升的房价,虽然待过深圳的我觉得温哥华房价还赶不上深圳, 但是对比温哥华本地的中位数收入,温哥华的房价已经远远高于居民的中位数收入,一般人都负担不起了。 以房租举例,我现在与舍友合租,房租以人民币计价,大概是我之前在深圳的四倍。 增加的移民人口与减少的工作机会,各种【苛捐杂税】让带资过来的移民和本地的金主都不需要创办企业, 资本没法流动起来,自然不能创造就业岗位,随着移民人数的增加,以及激进的难民接收政策,就进一步加剧【僧多粥少】的问题。 而加息导致的企业的信贷紧缩,也抑制企业扩张,甚至导致企业缩减规模,进行裁员,又推进了失业的严重程度。 而政府对失业人数增多的应对措施竟然是【头痛治脚】地增加失业保险的缴纳基数,而非想办法重启经济活力,进一步扩大就业市场。 持续增多的各种税收与各种层出不穷的问题,也难怪认识的加拿大人都对现在的政府相当不满。 10 好山好水好寂寞 温哥华的自然风光的确很美,依山望海。 经历过加拿大的冬天之后,我能理解为什么当地人在夏天都一股脑地出去玩,因为夏天不玩,冬天真的没得玩。 温哥华的冬天,只有雪和雨,阴冷潮湿,早上八点半日出,下午3点半日落,日照时间也只有7-8个小时。 冬天除了滑雪和滑冰外,基本没有太多其他种类的户外活动。 而温哥华的夏天要到接近7月份才来临,那时候大家可以露营,划船,登山。 很多店铺晚上6-7点就会关门,邮递员周末也不会送信,更不会有广州那种深夜大排档的盛况。 可能是因为温哥华相对国内人少,各种活动和玩法也没有国内花,也难怪有人评价其为【好山好水好寂寞】 11 好脏好乱好热闹 回到国内时的第一感受是,好多人,真的好久没有看到过这么多人。 得益于国内相对廉价的人力以及世界工厂的地位,以致于国内相对加拿大拥有价格更便宜,品质更好的产品与服务。 即使是深夜,到处也是人头攒动,可以很轻易地朋友玩通宵,到处都是人气和烟火气。 所以总会有朋友问我,【后面你会回国么?】 我只能说,未来的事无法计划,我也没有一个确切的答案。 当初想要出来只是某些契机因缘际会的结果,未来的事谁也不知道,只能拭目以待。 ","permalink":"https://ramsayleung.github.io/zh/post/2024/%E7%99%BB%E9%99%86%E5%8A%A0%E6%8B%BF%E5%A4%A7%E4%B8%80%E5%B9%B4%E7%9A%84%E4%BD%93%E4%BC%9A/","summary":"1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾","title":"登陆加拿大一年后的体会"},{"content":"1 前言 前几天在讨论刷题工作,分享职场经历(也就是水群)的群里,有位「老好人」senior 工程师向大家倾诉他的职场烦恼。 他作为项目的后端负责人,把后端的活都又快又好地干完了,然后前端毫无进度,大家看起来还没有什么责任心和紧迫感。 如果项目不做成,原有的客户都会决定放弃公司的产品,转投其他公司。 所以这位老哥就会忍不住在干好自己的活的同时,去前端帮帮忙. 然后过了一段时间之后,不干活的前端们开始疯狂向后端推卸责任,明里暗里,阴阳怪气指责后端问题,导致项目被block住. 群友们纷纷给自己的建议,有建议和经理1:1 把事情捅出来的;有建议下次提前和经理同步进度,暴露风险的。 此时,老道的群主也出来给出自己的建议: 找到自己工作的亮点,要把亮点展示给老板们,然后离这群不干活的人远点,有啥要求就去捧杀下他们,说他们能力强,进度喜人,自己就不过去拖后腿了 通过提管理手段来暴露他们的问题,而不是直接去和经理说谁谁不好,口说无凭的; 代码质量差就多组织代码review, 没进度就每周或每两周review 进度, 没产出就建议review 代码量和设计文档. 如果没人听自己的建议,那就放慢点节奏,自己找点事消磨下时间,总不能指望没人重视自己的项目,还为它夙兴夜寐 用群主的原话来总结: 银行被抢了,是冲上去和歹徒搏斗,牺牲了还要为是不是工伤扯皮?还是顺便把自己和领导亏空的那部分也算在损失里,提拔一下? 群主的话可谓是振聋发聩。 当然,企业老板听了可能会不高兴,觉得给了员工工资,即使公司搞996,搞降本增效,裁员减薪,员工也应该献出自己的心脏。 因为我总是读些乱七八糟的东西,所以我思绪比较容易飞扬,总是会联想到一些不搭边的东西。 群主的话让我联想到了《西游记》,如果以职场视角来打开《西游记》,就会发现完全不一样的内容,也解了我的心头之惑。 2 胡说西游记 年少时读《西游记》,读到的只是神魔鬼怪,孙悟空如何大战各路觊觎唐僧肉的妖魔鬼怪,一路通关刷副本护送师父西天取经的故事。 现在想来,如果《西游记》只是日本少年热血漫画的内容类似,又是如何被位列中国的四大名著之一的呢? 有人说西游记的仙魔志怪只是皮,骨是人性,所以才能经久不衰,流传数百年。 那么人性又体现在哪呢? 2.1 困惑 先来看下我曾经困惑的地方。 每次唐僧师徒落难,孙悟空营救未果,都向去天上的神仙,或者去地府的阎王救助,众人总是口称「大圣爷」, 恭敬相待,殷勤相从,基本是有求必应。 年少时觉得合情合理,因为孙悟空是「齐天大圣」,所谓众神都小心应付着。 出来打工之后,才意识到这些个情节非常不合理: 按理说天上的众神都位列仙班,都是「体制内」有头有脸的人物, 哪天来了个石头蹦出来的破落户,连个「编制」都没有,敢自封「齐天大圣」, 还把我们的机关单位大闹了一场,让我们在领导面前丢光了脸面,连大领导「玉帝」都被牵连了。 现在他有事要来求我们帮忙, 谁有空谁帮,反正我手上的事情是非常紧急的,会影响到万千生民的,不比你救个师父重要? 你自己不是很能么,自己去救嘛,找我们干嘛? 有人可能会觉得我以「小人」之心揣度「神仙」之腹,人家都是「神仙」了,还会计较这些嘛? 肯定是能修炼到喜、怒、哀、惧、爱、恶、欲七情皆去的境界。 只是《西游记》中有明确描述,「神仙」并非是没有七情六欲的。 在《西游记》的结尾,唐僧师徒历尽艰辛,终于到了灵山见佛祖,得偿所愿要把真经取回去,却遇到佛祖座下弟子阿傩,迦叶索贿。 他们索贿不成,甚至只给了唐僧师徒无字经让他们带回去。 「佛」犹如此,「神」何以堪? 2.2 涂生死簿 在孙悟空在菩提祖师学到学到出神入化的神通和七十二变的本领后, 为了改变自己以及猴子猴孙们都逃避不了生老病死的命运,去地府把生死簿中,把猴属之类,但有名者,一概勾之: 悟空道:“我也不记寿数几何,且只消了名字便罢。取笔过来。” 那判官慌忙捧笔,饱掭浓墨。 悟空拿过簿子,把猴属之类,但有名者,一概勾之。捽下簿子道:“了帐,了帐,今番不伏你管了。”一路棒,打出幽冥界。 那十王不敢相近,都去翠云宫,同拜地藏王菩萨,商量启表,奏闻上天,不在话下。 相当于孙悟空把地府这个机构最重要的账本都给改没了,阎王这个主管相当于严重渎职啊,负有重大管理责任! 那么为什么每次孙悟空来找阎王,阎王都毕恭毕敬的呢? 以前觉得是阎王怕了孙悟空,所以因惧生敬。 现在联想到群主的话,意识到阎王不但不会因失职而沮丧,并记恨孙悟空,反而会乐得鼻涕泡都出来。 孙悟空就是「抢劫银行」的劫匪阿,自己有多少亏空和前科, 有什么亲戚好友免除死期的,有什么用钱换阳寿的事,全部都可以算到孙悟空头上。 有审计机构过来审查,我都可以说是被孙悟空涂掉的。 有这么个冤大头,阎王自然开心得不行,可能阎王他父亲都没有对他这么好,在工作上这么关照他。 2.3 纵天马 在孙悟空在东海夺取了「定海神针」作「如意金箍棒」,大闹森罗殿,私改生死簿后,玉帝大为震惊,然后听从建议, 招安了孙悟空,封为「弼马温」. 「弼马温」就是御马监正堂管事,手下还有监丞、监副、典簿、力士等大小官员不少人,管辖天马千匹, 所以是个管后勤的职位。 原来孙悟空以为这是个「一品」大官,最后却在旁人口中知晓,这个是个未入流的小官,不禁大怒,把公案推倒,在天河放走了所有的天马。 想来天马虽然名为天马,也不是以云雾为食的,想必也要有后勤草料供应,还有各种马具,马廊等配套设施。 现在孙悟空把天马都放了,那么属下的亏空,上下游的贪墨,又可以把锅栽在孙悟空头上了。 虽然我们中饱私囊,1000匹天马的财政拨款,贪了900匹,实际只有100匹,但是你孙悟空把马都放了,我们就咬死说马就是有1000匹的。 好比粮仓主管离任,对上官不满,放火把粮仓烧了,不管里面是陈米,沙土还是精米,下属一概都说是精米,那都是主管的责任了。 2.4 关系 唐僧,转世前为金蝉子,如来佛祖的二弟子,因犯错被贬,转世成为唐僧, 要通过取真经,重新成佛,回来佛祖身边。 也就是说,取经队伍里面,带头那个是如来董事长的亲信秘书,和董事长关系密切。 而取经也变成了一件在大领导「玉帝」CEO和如来董事长那挂上号的,核心KPI项目了. 那么当孙悟空来求助的时候,也就变成了一件可以在CEO和董事长那里展示自己手段的事了。 既能露脸,也能赚credit. 如果能帮上忙,那么就可以在领导面前露把脸,展示下自己的本领; 如果帮不上忙,也没什么大碍,你看孙悟空他业务能力多强,他都求助,肯定不是啥容易啃的骨头,我处理不成也很正常。 反正有事没事去老板面前刷刷脸总是好的,你没看到,领导叫踢球,群里一群人响应么? 你觉得他们是想踢球呢,还是想和领导踢球呢? 3 总结 经典之所以能经得起时间的洗礼,历久弥新,大抵是因为总有人能拿书里的事,说自己的话吧。 3.1 历史文章精选 旅加经历 这些年走过的路:从广州到温哥华 加拿大之初体验 加拿大考驾照的经历 历史与思考 为什么梦想买不起,故乡回不去(和谐版本, 原版本1) 润向何方:不完全肉身翻墙指北 皇帝与官僚:「上面」与「下面」 《君主论》:所谓「帝王心术」 工具与分享 我的写作流 我的画图流:画图工具与技巧分享 我的搜索流:高效搜索经验分享 最好的学习方式:费曼学习法(Feynman Technique) 系统思考:既见树木,又见森林 两个鲜为人知的Gmail地址技巧 职场与思考 那些年,我从微信支付学到的东西 : http://ramsayleung.github.io/zh/post/2023/%E7%BD%AE%E8%BA%AB%E4%BA%8B%E5%86%85/ \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%BB%8E%E8%A5%BF%E6%B8%B8%E8%AE%B0%E7%9C%8B%E8%81%8C%E5%9C%BA%E6%84%9F%E6%82%9F/","summary":"1 前言 前几天在讨论刷题工作,分享职场经历(也就是水群)的群里,有位「老好人」senior 工程师向大家倾诉他的职场烦恼。 他作为项目的后端负责人","title":"胡说:从西游记看职场感悟"},{"content":"1 前言 1.1 「老司机」 在高考之后,未上大学的那段时间,我就在父母的建议之下去考了驾照。 虽然持有驾照已经7-8年的时间,期间甚至驾照过期,都换过一次证件了。 所以,以驾龄论,可谓是「老司机」了,但以里程论,还是「新手」,不足一万公里。 毕竟在国内上班,可以搭乘公共交通工具,地铁,小电驴或者共享单车都可以组合使用,车不算是必需品。 尤其是在还没有买房的情况下,找地方停车都是个问题。 1.2 北美的车 但来到北美之后,发现车的确是必需品。 去美国玩的时候,发现美国真的是大农村,地广人稀,生活设施遍布在不同的地方,买个菜都要去几公里之外的超市,没有车真的是寸步难行。 相比之下,我所处的温哥华还算是方便,还有地铁和对应的接驳巴士,能满足基本的生活需要,但也仅限于生活需要。 例如想去个附近的山登高游玩,地图显示距离17公里,开车30分钟,公交2小时。 兼之冬天将近,如果下雪的话,再用两条腿,可以活动的范围就相当有限。 诸事安顿下来之后,车的优先级就提上来了;而考虑车的前提条件,就是先有驾照 1.3 驾考难度 因为中国与加拿大没有驾照互认协议,所以中国的驾照不能直接换成加拿大驾照,所以就能人工去考。 前面提到,北美的车是生活必需品,兼之从不同的好友的经历来看,考驾照似乎并不难,何况我又不是不会开车。 按照朋友们的描述,在美国,能把车开出去,再安全开回来,就能过了。 后面发现,的确如此,但是仅适用于美国,加拿大(或者说温哥华)除外。 温哥华20世纪初建城的时候,汽车还不是普遍,设计的路主要是以马车通行为主,所以老城区的路基本都很狭窄。 等到20世纪40-50年代,汽车大量普及的时候,最初的城市规划设计师就给出了面向汽车道路的城市规划, 但没有被政府接受,重新规划的蓝图就没有实现,所以温哥华城区的路都相当「紧凑」。 像BC 99这条去温哥华市中心,每天上下班必塞的高速公路,双向只有中间的一条黄线分隔,车头偏一下就被对面车道的来车给撞了: 虽然路规划得不怎么样,但是政府还是希望交通尽量高效运行,减少阻塞, 而其中的一个手段就是提高司机准入的门槛,让司机在理想条件,尽量以最高限速速度通行。 以至于刚从美国转过来的同事说,怎么感觉温哥华的人开车,都那么匆忙呢(rush). 用舍友的话来说就是, 「驾照考试难度,一般与马路杀手数量成正比;马路杀手多的地方,一般都考试难度都比较高。温哥华路况不好,容易培养马路杀手」 导致的结果就是,温哥华的驾照考试难度相对较高。 但考试难度这个东西很难量化的,每个人驾驶经验也不一样,但以我知道的例子,一位朋友,考了4次才通过,花费了4个月;还有同事的舍友,也考了4次。 2 回炉再造 虽然我在国内也开过车,但是鉴于温哥华的考试难度,兼之想一次通过考试,所以就找了位朋友推荐的教练来重新学车。 要考试通过,主要是做好这三部分的内容: 道路意识 驾驶技术 考点的熟悉程度 我本以为是我要关注的主要是第3点,没想到第3点优先级是最低的。 前面几次课程,我都是在修正国内的驾驶习惯,比如驾车时,开在最右车道时,习惯偏向左边,而不是开在车道中间。这个叫 lane position(车道位置)问题 教练说,这个很正常,因为国内经常会有行人或者自行车从非机动车道,开到了机动车道上,为了不碰到他们,就习惯向左边开一些。 还有右转的时候,习惯向外再带一些再转弯,因为又担心转弯的时候会有行人或者障碍物,所以预留出空间,这个叫 steering wheel position(转弯位置)问题。 诸如此类的问题,都是会扣分,甚至会挂掉的. 道路意识方面,教练首先纠正我的就是速度问题,限速50km/h的意思,不是说开50 km/h 以下都可以,而是说在理想情况下,要开到50 km/h, 太慢就会阻塞交通了。 比如限速50,开到40以下可以就扣分了,开到35,可能就挂了,当有车在你后面排队,就要注意了; 超速也不行,超过55可能就会挂掉。(当然,只是考试,平时超速,只要不超太多,一般不会有警察抓你的) 还有道路优先权的意识,在没有红绿灯的情况下,多车交互,什么时候,谁应该先走;对公交,行人的礼让意识。 对于消防车和救护车的紧急车辆出现在路上,双向两车道的车都要停下来, 以便紧急车辆快速通过,所以就会有路上就听到警笛声,路上所有的车都就近停下来的场景。 加拿大的基础设施真的不行,路灯都是20-30年前的款式,让我非常不习惯的是,在大多数情况,直行和左转灯是合并在一起的。 意味着当你要左转,灯变绿的时候,不一定就能转,对面直行车道的车有更高的优先权,你需要把车探出去,等待对面的直行车通行完,并且在自己这边的绿灯变黄前走掉,需要对交通变化快速反应。 因为这种种限制,就对司机的驾驶技术有较高要求,具体体现就是要快速过弯。 而温哥华的交通要求司机快速通行,路上交通状况变化很快,如果转弯太慢,灯可能就变, 行人或者其他方向的车就马上要来了,就比较容易出现危险的状况,就要求在可以通行的情况下,快速过弯。 北美这边的过弯标准是,右转绿灯,减速,shoulder check, 平稳快速转弯,加速,过程要流畅一气呵成,不能有颠簸,滞涩的感觉,让乘客紧张。 所谓的 shoulder check 就是转弯的时候,不能只看镜子,要把头转90度,转过肩膀,观察转向的盲区,避免撞上行人或者单车,这也是国内没有的要求。 所以快速转弯就要求手,脚,头协调并用。 就这样,每周一次,练了10次,花费了近三个月来练习。 3 考试 考试内容分为笔试和路试,笔试就是针对交通规则的做题,问题不大,通过之后就会拿到实习驾照,在有19岁以上的驾照持有者陪同下就能开车了。 路试就是载着考官,按照考官的指示,开25-30分钟的车。 笔试与路试都与驾校无关,也不需要有驾校这样的机构介入,考试用车可以是任意符合规定的车,比如轿车,SUV, 越野车,或者是皮卡,只要功能正常即可。 在考点区域,按照随机考官的指示开车,考察内容大概包括红绿灯右转,左转,随机临时停车, 随机地点2分钟内完成侧方停车,考官观察考生的表现,并进行打分。 视考点不同,还可能需要开一段高速,考察如何加速并入,高速超车与换道,及离开高速等等。(考官估计也会紧张,要和连驾照都没有「马路杀手」上高速) 因为练的时间比较长,兼之在考试之前去美国有过一次公路之旅,开了有600-700公里,所以一次就通过了。 考官评语: A1 Missed right turn shoulder check/lane change Good speed control shoulder check 部分还是被扣分了,做得不标准,习惯着实很难一下子养成。 这样就通过驾照考试,「又」成为一名「马路杀手」了。 整个考试的费用: 笔试: 31加元(约160人民币) 路试: 50加元(约270人民币) 驾照制作及邮寄费用: 31加元(约160人民币). 4 对比 因为在中国和加拿大都考过驾照,所以很自然地会对比两个国家的驾照考试。 加拿大考试的最大感觉就是真的要求你考完之后,是可以马上上路开车的,毕竟考试都需要在马路上真枪实弹开车的,甚至上高速。 但是你是怎么学的,他是不管的,所以也难怪北美很多人都是父母教开车,然后开自己家的车去考试。 相比之下,中国的驾照考试是相对比较「教条」和「形式化」的,以大部分人花费最多时间的科目二而言,主要是考察倒车入库,侧方停车,直角转弯,半坡起步等技能,而其中花费最多时间的是倒车入库,要求一次就能倒进去。 科目三就是加减档位,加减速,路边停车等等,我当初考试的时候,科目三还有长途,我需要和教练还有同车的考生,驱车到另外一个城市。 但是考试要求与实际开车的诉求并不契合,没有见过谁开车主要是处理倒车入库的,何况是要求一次就能停进去。 为什么要考这么机械的内容,就不能以实际开车的内容来考么? 比如让考生开个20-30分钟,然后让考官评判开得是否符合标准。 回忆起发生在我自己身上的一件事,我意识到,在当前环境下, 这个是很难落实的。 4.1 科目三经历 当时考科目三的前,我就只练习了一次,驾校教练把我们拉到一条未启用的高速公路上,给我们讲怎么起步,加减档,超车,路边停车, 向我们介绍,考试的时候,会有个考官坐在副驾驶上,然后对我们进行打分。 让我们每人开一段路,第二天就约考试了。 考试前,另外一位驾校教练,也是驾校老板单独叫我出了办公室,对我说了一番话,但原话已经记不大清了: 教练:你准备了么? 我:准备什么?考试内容么? 教练:红包阿 我:什么红包? 教练:给考官的红包 当时高中毕业的我,还不懂这人情世故,陷入了短暂的沉默。教练并没有给我太多思考的时间,继续了对话(这次的原话我倒是还记得很清楚) 教练:这红包不是给我的 教练:每个人都给考官准备了红包 教练:同车考试的人里,是有人要挂的 读懂潜台词的我意识到,相当于我被陷于了囚徒困境了,如果别人给了我没给,挂的就是我。 那我也只能给红包了,但教练的话却没有就此停下来。 教练:你知道给多少么? 我:\u0026hellip; 教练:给个500吧。 我高中毕业的时候,移动支付还不是非常普及,所以还是现金支付为主。 我当时报驾校的费用是3300元,我记得很清楚,是因为我父亲带我去镇上的驾校,从他的裤袋里面,抽出了一捆现金,放到桌上给驾校老板。 没想到,给个红包就要驾校费用的1/6. 驾校老板不为自己谋利益,还这么热心地帮考官张罗,真的难得的好人。 4.2 实用与教条 换个角度想,可能开过车的人都能意识到,耗费最多时间的科目二真的对驾驶技术的提高不大,那为什么不能使用更实用的考察方式。 因为科目二这样教条,机械的考察方式,可以通过仪器进行量化考察,仪器在大部分情况下,作假难度会高很多。 但如果使用实用的考察方式,只能使用人来主观考察,如何避免以权谋私呢?避免出现在我身上的事,发生在其他人身上。 考生能否检举这样的行为呢? 如何避免考生得到保护,不受到来自驾校和考官的打压呢? 我只知道,在加拿大,驾照考试是不需要驾校的,你有选择的自由和权利,但是考试本身不与驾校挂钩。 考试费用也是固定的。 如果有考官向我提这样的要求,我可以去驾考机构投诉,他有大概率会丢失工作。 而我既有权力选择更换考官,也可以自由更换考点。 5 后话 虽然花费3个月重新考个驾照相当费时间,但是期间的过程还是很有收获的,我也思考过规则背后的设计思路。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E6%8B%BF%E5%A4%A7%E8%80%83%E9%A9%BE%E7%85%A7%E7%9A%84%E7%BB%8F%E5%8E%86/","summary":"1 前言 1.1 「老司机」 在高考之后,未上大学的那段时间,我就在父母的建议之下去考了驾照。 虽然持有驾照已经7-8年的时间,期间甚至驾照过期,都换过一","title":"加拿大考驾照的经历"},{"content":"1 背景 我习惯使用浏览器匿名模式来打开 Youtube 视频,避免 Youtube 的推荐算法给我总是推荐同一类的视频。 但有个问题: 匿名模式下,Youtube的播放器是默认自动播放的。 虽然我可以登录 Google 账号并关闭自动播放,但是每次我使用匿名模式来浏览,关闭窗口之后,所有的操作记录都会被清除了,自动播放设置也不会被保存。 所以我使用 TamperMonkey 给 Youtube写了一个关闭自动播放的脚本,打开 Youtube 播放器时,把自动播放按钮给关闭掉,避免我使用浏览器的匿名窗口打开 Youtube 之后,Youtube自动播放导致一直有声音。 脚本逻辑很简单: // ==UserScript== // @name Disable YouTube Autoplay // @namespace http://tampermonkey.net/ // @version 0.1 // @description Automatically turn off YouTube autoplay // @author ramsayliang // @match https://*.youtube.com/* // @grant none // @icon https://www.youtube.com/favicon.ico // ==/UserScript== (function() { \u0026#39;use strict\u0026#39;; // Function to disable autoplay function disableAutoplay() { console.log(\u0026#34;disableAutoplay\u0026#34;); const autoplayToggle = document.querySelector(\u0026#39;div[class=\u0026#34;ytp-autonav-toggle-button\u0026#34;]\u0026#39;); if (autoplayToggle \u0026amp;\u0026amp; autoplayToggle.getAttribute(\u0026#39;aria-checked\u0026#39;) === \u0026#39;true\u0026#39;) { autoplayToggle.click(); console.log(\u0026#34;Disable YouTube Autoplay\u0026#34;); } } console.log(\u0026#34;Before loading\u0026#34;); // Run the function when the page loads window.addEventListener(\u0026#39;load\u0026#39;, () =\u0026gt; { // Wait for a short delay to ensure the page fully loads // setTimeout(disableAutoplay, 8000); console.log(\u0026#34;Loading page..\u0026#34;); disableAutoplay(); }, false); })(); 2 问题 但是我发现这个脚本时灵不灵,甚至有一天晚上,睡着之后被自动播放的声音吵醒了。 我就在找能稳定复现这个问题的场景,花费半个小时,终于能稳定复现问题了。 打开 Chrome 的匿名模式 打开 Youtube 首页, 地址是: youtube.com, 脚本运行. 随意点击一个视频,进行播放:https://www.youtube.com/watch?v=-pKGaxoVhok ,脚本就不会运行了。 如果我在视频播放页刷新,脚本又会重新运行。 但分析了1个小时,都没有找到原因,我甚至怀疑是 Tampermonkey 有Bug(虽然主观感觉这个可能性较小) 3 分析 结合 Tampermonkey 的表现,我觉得可能是 Tampermonkey 的执行机制有问题,可能是判断 youtube.com 和 youtube.com/watch?v=xxxx 是同一个页面,就不会运行两次。 在Stackoverflow上搜索了一下,发现果然如此: https://stackoverflow.com/questions/65017670/tampermonkey-match-not-working-when-visit-target-link-through-redirection 原来这个是feature, 不是bug. 对于 Single Page Application, Tampermonkey 无法判断页面的 DOM 是否发生变化,是否访问到新的页面了,所以不会重复执行。 分析下来,之前脚本能直接生效的原因是, 我在Chrome正常模式下打开 Youtube 首页, 右键对想要看的视频,点击\u0026quot;Open link in incognito window\u0026quot;, 因为页面是首次打开,所以就能正常运行。 但当通过Chrome 匿名模式打开 Youtube 首页,然后再点击视频播放,无法运行脚本。 因为在打开首页的时候,脚本已经运行过了,当点击跳转到指定的视频时,且 Youtube 是个 Single Page Application, 对脚本来说,页面就没有发生过变化,所以不会再运行。 如果手动刷新,页面重新加载,脚本就又会被加载。 4 解决方案 最后通过 Stackoverflow 的建议,增加一个对 DOM 事件变化的监听来解决: // https://stackoverflow.com/questions/2844565/is-there-a-javascript-jquery-dom-change-listener/39508954#39508954 // Detect the url change for programmatic navigation let lastUrl = location.href; new MutationObserver(() =\u0026gt; { const url = location.href; if (url !== lastUrl) { lastUrl = url; onUrlChange(); } }).observe(document, {subtree: true, childList: true}); // callback when url change function onUrlChange() { console.log(\u0026#39;URL changed!\u0026#39;, location.href); if(isYouTubeVideoURL(location.href)){ disableAutoplay(); } } 这样就可以正常运行了。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/tampermonkey_userscript_not_invokved_in_spa/","summary":"1 背景 我习惯使用浏览器匿名模式来打开 Youtube 视频,避免 Youtube 的推荐算法给我总是推荐同一类的视频。 但有个问题: 匿名模式下,Youtube的播放器是默认自","title":"TamperMonkey userscript在 Single Page Application 跳转链接后不运行问题分析"},{"content":"1 引子 年少的时候,很喜欢看历史书,倒不是因为「以史为鉴,可以明得失」发人深省的思考,单纯是因为手头没有太多打发时间的活动(那时还没有太多的电脑游戏和视频),而少年人最多的就是时间。 历史书里面有光怪陆离,跌宕起伏的故事,那时的我自然很容易被吸引进去。 上中学的时候,特别喜欢看三国,喜欢武将决斗,谋士运筹的故事。 再后来,随着网络小说的兴起,兼之三国的历史已经烂熟于心,没有太多的新意,就开始看各种的架空,穿越三国的网络小说。 比如:《重生三国之xx》,《xx三国》等等,后来又因为《明朝那些事儿》的爆红,开始看明史。 工作之后,出于历史的兴趣,就开始摆脱通史类的故事书,阅读一些偏学术类的历史著作。 只是在截止到这个时候,阅读的都是中国历朝历代的历史,毕竟外国人的历史有什么好读的,地名和人名这么难记。 大概在三年多前,阅读一本英语学习书籍(《Word Power Made Easy》,这也是本神书,后面我一定要好好聊聊)的时候, 该书中提到,古希腊和古罗马是现代欧美文明的精神来源,相关的传统,法律或者是宗教, 都可以从这两个古代文明找到参照物,约70%的英语单词就是衍生自古希腊语或者罗马人用的拉丁语。 何况古罗马历经共和国及帝国时代,历经1300年不倒。 对比之下,中国的朝代大多是三百年,为什么古罗马就能走出这样的历史周期律,延续千年呢? 巅峰时期,甚至将地中海变成罗马人「自己的内海」,成为一个横跨欧亚非的帝国。 想必自有其独到之处。 吃了那么久的中国菜,去尝试下西餐也不错嘛,说不定还能吃出新意呢。 2 罗马建国 公元前八世纪,传说罗马第一任国王罗穆路斯和双胞胎弟弟瑞摩斯出生后即被装到一个桶里,和唐僧一样,被投到河里遗弃。 木桶顺流而下,婴儿在桶里大声哭闹,引来了附近正在徘徊的一匹母狼。 这匹母狼没有把这两个婴儿当作午餐,反而将乳头塞进了两个婴儿的嘴里,把他们从死亡线上拉了回来。 婴儿由母狼抚养长大的故事显然过于玄幻,所以母狼在喂抱两个幼儿之后就离开了,是一个羊倌发现他们并把他们带回家了。 所以在罗马,都有各种各样母狼喂养罗穆路斯和瑞摩斯的塑像: 甚至意甲球队罗马(就是现在穆里尼奥执教的那支球队)的队徽也是母狼喂养罗穆路斯和瑞摩斯图: 在罗穆路斯18岁的时候,他和3000名追随他的拉丁人,以他自己的名字(Romulus),在台伯河下游平原的七座小山丘上建立了罗马国(Roma,或者叫罗马城): 为什么3000人就能建国?,这个没有什么硬性标准的嘛,只要实力够,自然可以自封为王。 所以也难怪曹操会说,设使天下无有孤,不知当有几人称王,几人称帝(串台了) 3 国政 罗穆路斯建国之后,作为开国之王,却没有独揽大权,他把国政分成三个机构,分别是国王,元老院和市民大会。 国王作为宗教祭祀,军事和政治的最高领导人,由市民大会投票选举产生。 罗穆路斯认为,如果没有市民大会选举,自封的王不具有执政基础。 (只能说东西方的执政想法真的不一样,尧舜禹禅让,都被儒家夸了两千年。而罗马的权力竟然不是世袭的) 罗穆路斯召集100位部族长老,设立元老院,他们的职责是为国王提忠告和建议,所以不需要通过市民大会的选举。 元老院的拉丁语是:Senātus,也是众多英语单词的词根,比如年长者(senior), 或者美国的参议院(senate), 都是元老院的衍生词。 市民大会由全体罗马市民组成,它的任务是选出以国王为首的各级政府官员。 (政府官员究竟不是国王或者上级官员指派的,无知的我又吃了一惊。) 市民大会没有制定政策的权力,但是对国王听取元老院的建议后制定出来的政策有赞成或反对的表决权。 此外,在对外关系上,是战是和,也必须征得他们的同意才可实施。 当时的罗马国规模还不大,所以还能由全体罗马市民组成市民大会,不知道市民人数多了之后,市民大会又会如何发展呢? 4 总结 原来十八世纪法国启蒙思想家孟德斯鸠的三权分立理念,在公元前八世纪的罗马城就能找到雏形。 这顿西餐,看来是越来越有趣了。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%9B%BD%E7%8E%8B%E6%98%AF%E9%80%89%E4%B8%BE%E5%87%BA%E6%9D%A5%E7%9A%84/","summary":"1 引子 年少的时候,很喜欢看历史书,倒不是因为「以史为鉴,可以明得失」发人深省的思考,单纯是因为手头没有太多打发时间的活动(那时还没有太多的电","title":"闲话罗马:国王是选举出来的"},{"content":"1 前言 前段时间流行一种关于程序员效率的说法,叫「10x程序员」,即一个好的程序员的工作效率是普通程序员的10倍。 但是,在编程界,有这么一群人,他们的工作效率,可以说是百倍,甚至千倍于普通程序员; 更令人叹服的是,他们创造了普通程序员即使百倍努力也无法写出的作品。 使用「程序员」这个职业来称呼他们,未免流于平凡,无法展现出他们竖立起的丰碑;而使用「计算机科学家」,又未免过于学术,不接地气; 那么,就回到最初,用「黑客(hacker)」这个称谓来称呼他们吧。 2 关于黑客 可能大家对「黑客 (hacker)」的印象多来自于电影,比如《黑客帝国》,就是那种在电脑面前,使用各种看不懂的工具入侵别人电脑的人。 但是这种看法大多是对于「黑客」的误解,称之为「骇客(cracker)」可能更加合适,即恶意入侵他人电脑的人。 hacker 一词又是从 hacking 衍生而来的,将 hacking 翻译成成中文网络语中的「整,搞,开干」可能会更贴切, 而最初的「黑客」指的就是一群富有创造力和兴趣的爱好者,只是比较具有代表性的是在计算机领域。 国外有个很有名的科技相关的聚合网站,叫做「Hacker News」, 其中的「Hacker」, 也是沿用黑客最初的含义。 既然提到黑客,那么有一个无法绕过去的人物,那就是今天的主角,黑客文化的领军人物:Richard Stallman 3 UNIX 3.1 分时系统 相信今天的我们,对操作系统这个概念不会陌生,在电脑上有 Windows 10, Windows 11, Windows 7 或者苹果的 MacOS操作系统,在手机上有 Android 和 IOS操作系统。 所谓的操作系统,即是一套管理硬件,发挥硬件性能的软件,避免应用程序直接和硬件打交道,省去普通程序员大量的开发成本和心智。 与今天直接在手机操作系统上,一边聊微信,一边放音乐不同,远古时候(二十世纪六十年代)的操作系统只支持批处理模式: 即用户同时提交多个任务,任务1运行完才能运行任务2,相当于你只能把音乐听完,然后关掉音乐软件,然后才能打开微信,发送聊天消息。 (请忽略远古时代还没有微信这个问题) 你可能会想,这也太挫了吧。 没错,当时的计算机科学家也这么认为的。 因此1964年,通用电气和麻省理工大学就打算合作开发一个多任务操作系统,支持多个用户,运行多个任务,名为 MULTICS 后来,AT\u0026amp;T公司的贝尔实验室也加入到这个操作系统的研发中,但是项目目标过于庞大,特性太多,性能又很低, AT\u0026amp;T见项目前景不妙,就把资源都撤了,退出了这个项目。 3.2 玩游戏玩出来的UNIX 贝尔实验室的一位工程师,名叫Ken Thompson, 刚加入 MULTICS 项目不久,公司就准备退出了,但是通用公司为了项目而准备的机器 GE-645 就还保留在贝尔实验室,Ken 就打算用这些机器写个太空旅行的游戏。 然而,Ken 写出来的游戏跑得很慢,每次运行还要75美刀,更难受的是,GE-645 这批机器,不久后就被搬回去通用公司了。 所以Ken 只好在实验室角落找了几台没人用的PDP-7, 在同事 Dennis Ritchie 的帮助下,再重写了一次游戏。 这次的游戏开发经历,加上之前的 MULTICS 项目经验,让Ken 开始研究如何使用 PDP-7 开发一个分时多任务操作系统。 然后他花费了一年的时间,和 Dennis 一起,在PDP-7上开发了一个分时多任务系统,名为UnICS,这就是第一版的 UNIX。 因为PDP-7的性能不佳,最多支持两个用户, Ken 和 Dennis 又把第一版的 UNIX迁移到 PDP-11上,为了方便迁移,还顺便发明了一门编程语言,名为 C语言,并将UnICS 改名为 UNIX. (这两位也是神) 影响后世无数操作系统的 UNIX 操作系统就此诞生,并迅速风靡各大研究机构,政府机关,企业与大学,成为70-80年代,操作系统事实上的标准 3.3 商业版本与闭源 原来的软件只是买硬件时的赠品,到七十年代未,人们开始发现,原来软件也可以卖钱,很快,制作与销售商业软件成为一门热门生意。 最开始的UNIX 版本是开放源代码供使用者的,也就是使用者不但可以安装 UNIX 系统,还可以阅读,并修改UNIX 系统的源代码。 但是贝尔实验室的母公司 AT\u0026amp;T毕竟是商业公司,把自己的源代码授权出去,后面还怎么赚钱呢? 所以在20世纪80年代相继发布的UNIX 商业版本,只发行二进制,不再包含源代码。 对于黑客来说,就是你能看到这个操作系统是怎么跑的,但是你再也无法知道他是怎么实现的了。 4 RMS Richard Matthew Stallman, 1953年出生于纽约的一个犹太家庭, 1974年毕业于哈佛大学,1975年在 MIT 攻读博士,后来退学在 MIT AI 实验室写代码。 他的名字首字母为 RMS, 早期在黑客社区混的时候,以 RMS为用户名,所以大家都叫他 RMS(后面就以RMS来称呼他了). 当时的「黑客文化」崇尚开放,分享与交流,认为分享才能促进社会进步,在这样的文化熏陶下,RMS 自然对闭源软件痛恨不已。 1980年,还在 MIT AI 实验室工作的时候,因为激光打印机和大部分工作人员都不在同一层楼,总是跑上跑下去查看打印结果和进度就很麻烦。 RMS 就给实验室的激光打印机写了一个程序: 可以在打印任务完成时,发消息通知用户;或者当打印任务卡住的时候,也发消息通知用户; 然而,因为最新版本的打印机源码不再开放,RMS写的程序就无法再适配,让他相当恼火。 以小见大,整个软件行业都在发生变化,甚至连UNIX 这样的基石软件都开始不再开放源代码授权,RMS感觉,他要站出来做些什么了。 5 GNU 5.1 荜路蓝缕 在1983年, RMS 宣布了GNU 操作系统计划,计划开发出一个兼容 Unix的源码开放的操作系统,让 Unix用户可以无缝切换到 GNU 操作系统上. GNU 就是 \u0026ldquo;GNU is Not Unix\u0026quot;的缩写(那开头的GNU又是什么意思呢? 按照程序员的行话来说,这个叫递归) 经过十多年的发展,Unix 已经成为操作系统事实上的标准,重新开发一个新的操作系统几近天方夜谭。 想象一下,有人跟你说要开发一个 Android 操作系统,用来替换掉 Google 的Android 系统,这工作量和难度可想而知,这就是现实中的想要移山的愚公,大战风车的堂吉诃德。 但是 RMS 并未被眼前的困难所吓退,而是一步一步,从0开始构建他心中的类Unix操作系统. 1984年, RMS 开发并发布GNU Emacs 这个著名的文本编辑器, 方便程序员进行代码开发; 1986年, RMS 开发并发布GNU Debugger(gdb) 调试器, 方便程序员来调试程序; Emacs + gdb 就是他那个时代的IDE 1987年, RMS 开发并发布GNU Compiler Collection(gcc) 编译器套件; 所谓的编译器,即将人写的代码,转换成机器可以运行的二进制代码。 开发出一个这样的软件就足以在计算机史上留名,RMS 在这3年间,还一口气开发出了3个,这样的技术水平和生产效率,只能让人叹服,影响力堪比盗火的普罗米修斯。 何况这些软件至今仍在迭代,被无数程序员所依赖,所使用。 比如微信的所有后台代码,都是使用GCC 编译出来的,也就是你现在也在间接使用着 RMS 当初编写的软件。 近40年过去了,市面上被广泛使用的C/C++编译器就只有三个: 微软家的 MSVC, 苹果支持开发的 Clang, 还有GNU 项目的 GCC. 除此之外,GNU 项目还开发了许多的基础设施,如GNU make, GNU grep, bash,以及志在替换掉 PS的 GIMP 等等. 除了基础设施外,GNU项目还希望类似通过美国宪法保证言论自由一样,通过法律和版权,确保软件开放源代码。 因此, 在1989年, RMS 发布了 GNU General Public License(GPL)授权, 主要内容是: 用户可以自由使用,复制,修改GPL软件, 派生的软件也必须使用GPL, 不能转换成闭源软件. 从法律层面保证了GPL软件不会被有心人直接拿去闭源赚钱。 此外, RMS 还将自由软件发展成社会运动,将软件开发这个程序员小圈子的活动,成为扩展到整个社会的思想运动。 在人类没有进化到「无私」的精神境界前(可能永远都达不到),通过GPL法律条款来保证「自由」的权利, 不得不说是一种创举,从而让世界上每个人都有机会享受到软件发展所带来的好处。 5.2 开花结果 时间来到90年代, 经过近10年的耕耘, 在基础组件和配套设施相继完善之后,GNU 项目终于来到最关键的节点,开发出可以替换Unix 系统的内核(kernel). 如果电脑硬件来比喻操作系统的话,就是内存,硬盘,主板,显示器,电源全部都就绪,就差最后的CPU, 画龙最后的点睛. 但是GNU 的内核 Hurd 却迟迟未能发布, 而天下可谓苦闭源 Unix 久矣。 在1991年, 一个叫Linus的芬兰学生在社区上发布了他自己的业余项目:一个类Unix 的操作系统内核。 他把GNU 项目的相关组件(bash和gcc)移植到这个系统,也能正常运行起来了, 这个系统就是Linux(完整的名称应该是 GNU/Linux) 自此, GNU 项目的最后一块拼图完整了, 十年磨一剑, GNU的基础组件加 Linus 的Linux内核, 一个志在替换 Unix 的操作系统终于完成了, 这就是 GNU/Linux. 苦Unix久矣的社区的开发者云集而来,为 GNU/Linux 添砖加瓦, 让GNU/Linux 成为今天的参天大树(连微软家的服务器也在运行 Linux) \u0026mdash; 只见新人笑,哪闻旧人哭. 有点离谱的是, GNU Hurd 已经开发超过30年了,还没有发布1.0(稳定可用版本). 更离谱的是,最近还有更新: 2023年6月份,还发布了2023年 版本更新: 6 轶事 6.1 教主 为什么称RMS 为教主呢? 因为RMS 创建了 Emacs 这个神的编辑器,自其诞生以来,与编辑器之神 Vi/Vim 的圣战就从未停息。 使用Emacs 的程序员与使用Vi/Vim 的程序员,一直在争论,究竟哪个才是更好的编辑器? 既然 RMS 是Emacs 的创始人,自然被使用 Emacs的人尊称为「教主」。 而这场争论已经持续近四十年,依旧没有分出胜负。 像 Google 这样浓眉大眼的家伙,还在不时地给这场战争拱火: 6.2 教主与教主 乔布斯被「果迷」尊称为「教主」,大家可能不知道的是,这两位「教主」曾经有过一场交锋。 1993年, 当时乔布斯还在 NeXT公司, 买下了 Objective-C 语言来开发应用程序(后来的IOS用的也是 Objective-C), 使用的编译器也是 GCC. NeXT 修改了 GCC的源码,以便增加对 Objective-C 的支持,而GCC 使用的又是GPL 授权,而根据GPL 的授权,任何对GPL软件的修改,也必须要开放源代码。 所以乔布斯就问RMS, 他能否把 GCC 拆分成两部分,一部分是原来GCC, 继续开放源代码;另外一部分是增加 Objective-C 的GCC 编译器前端,闭源收费商用。 RMS 回复,当然是不可以。我估计老爷子心想,防的就是你这种人。 乔布斯只好将 Objective-C 编译器的前端也以GPL 授权开放出源代码。 \u0026mdash; 若干年后,苹果计划开发自己的编译器,因为设计以及授权的原因,在谋求与 GCC的合作未果后,转而支持 LLVM 的clang, 那也是后话了. 6.3 中国芯 根据 RMS 自述, 他之前用的一直是中国科学院设计的龙芯处理器的龙梦电脑, 虽然这台电脑的性能,显示尺寸(只有9英寸)都无法让RMS 满意,但是这台电脑的是完全自由的,包括硬件, bios, 软件: What hardware do you use? I am using a Lemote Yeelong, a netbook with a Loongson chip and a 9-inch display. This is my only computer, and I use it all the time. I chose it because I can run it with 100% free software even at the BIOS level. 在性能和自由之间,他一如既往地选择了「自由」 根据RMS 官网的描述, 他不用intel 或者 amd 的芯片,是因为他们都有后门: Reasons not to use Intel Don\u0026rsquo;t use Intel processors newer than Core2, because they have the \u0026ldquo;management engine\u0026rdquo; back door. Recent AMD processors have a similar problem, but we do not yet have an article about it. 不过,据闻他的龙梦电脑被偷了之后,他也就换到 ThinkPad 上了: As of 2022 I use a Thinkpad x200 computer, which has a free initialization program (Libreboot) and a free operating system (Trisquel GNU/Linux). 6.4 抠脚 菜的抠脚就听说过,强得抠脚又是什么呢? 因为他真的抠脚(字面意思),还吃回去了。 7 总结 他是天才黑客,是自由软件的精神领袖,是知行合一的孤勇者,更是个凡人堆里的理想主义者. 当然,还是我大 Emacs 神教的教主. 8 参考 https://en.wikipedia.org/wiki/GNU_Compiler_Collection https://en.wikipedia.org/wiki/Richard_Stallman https://en.wikipedia.org/wiki/GNU_Emacs https://en.wikipedia.org/wiki/GNU_Debugger https://stallman.org/ https://usesthis.com/interviews/richard.stallman/ ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%BC%BA%E5%BE%97%E6%8A%A0%E8%84%9A%E7%9A%84%E6%95%99%E4%B8%BBrms/","summary":"1 前言 前段时间流行一种关于程序员效率的说法,叫「10x程序员」,即一个好的程序员的工作效率是普通程序员的10倍。 但是,在编程界,有这么一群人","title":"黑客列传:强得抠脚的教主RMS"},{"content":"1 关于 Demo 昨天下班路上,和朋友闲聊的时候,想起了当年大学时候看过的《李开复自传》的一个故事。 当年李开复在卡内基梅隆大学的研究方向是语音识别,即如何将人说的声音,转变成计算机可以识别的文字内容。 他的语音识别的研究成果还被《商业周刊》评选为「1988年最重要科学创新奖」。 但是令我印象深刻的并非是语音识别的成果,而是他导师教他的,如何向世人展示他的成果的市场营销手段: 1988 年 4 月,我受邀到纽约参加一年一度的世界语音学术会议,发表学术论 文。赴会的一个月前,我的导师瑞迪教授又给我上了一课,但是不是学术方面,而 是市场方面的。 他对我说:“学术演讲的 30 分钟,你只要讲 25 分钟就行了,最后 5 分钟你拿 一个话筒传给观众,让他们自己试试,这个系统是不是真的。” 我说:“但是,会场噪音很大,一定会打折扣,达不到 96%成功率,而且那么多日本 学者,他们的口音我的系统可没听过。” 老师说:“实际上你的识别率是 90%还是 96%,没有什么差别。我们这么做的 目的,不是要监测你的识别率,而是要造成一个效果,让每个学者终生都会记得, 第一次接触不指定语者系统就是在纽约,在李开复的演讲上。” 在学术结果和演示效果的交互相映之下,李开复的研究成果撼动了整个学术领域,认为他的研究成果,建立起了人机沟通的桥梁。 纵然演示者的PPT美轮美奂,演讲舌灿莲花,带来的冲击,远不如用户亲身体验来得强烈。 2 MVP与及早反馈 无论是所谓的敏捷和精益迭代开发,都强调快速试错,快速反馈,开发最小可用的产品(minimial viable product, MVP)。 所谓的快速试错,及早反馈,就是把产品原型做出来,然后让用户进行体验,收集用户反馈,再根据用户的评价,进行后续的优化和调整。 这样的理念无缝是非常有价值的,可以避免花了好几年,大量人力物力,做了一个过时或者不受市场青睐的产品。 而其中的「用户」,并不一定指的是最终使用你产品的「用户」,你的产品经理,组长,总监都是你的用户。 他们才是能决定你的产品方向的人,所以在做完产品原型之后,应该尽快让他们尝试产品原型,可以及早得到反馈和修改建议。 在展示 Demo 的时候,也应该由他们亲身去尝试产品,观察他们作为新用户的使用习惯; 以此得到的反馈和惊喜,也会比工程师亲身演示来得更真实和贴切。 此外,正如《动物庄园》里面说「所有动物生而平等,但有些动物比其他动物更平等」。 每个用户的反馈和建议都应该被平等对待的,只是他们的意见比普通用户的更平等。 而从管理者的角度来说,管理者对员工抱有高信任度的终究在少数,即使每位员工起早贪黑地干活,写周报,日报;开晨会,周会汇报进度; 管理者难免会有疑问,项目什么时候才能做完,他们是否有在认真干活? 可能会有管理者跳出来说,「哪有这样的想法?」。 但事实就是许多的管理措施,都体现出这种不信任。 而提供MVP供管理者体验就是不断地告知管理者项目的进度: 项目正在从蓝图,变为现实。 3 后话 但,尽早反馈,快速试错,从来都不应该成为加班的借口或理由。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%BA%A7%E5%93%81%E7%9A%84%E5%BF%AB%E9%80%9F%E8%AF%95%E9%94%99%E4%B8%8E%E5%8F%8A%E6%97%A9%E5%8F%8D%E9%A6%88/","summary":"1 关于 Demo 昨天下班路上,和朋友闲聊的时候,想起了当年大学时候看过的《李开复自传》的一个故事。 当年李开复在卡内基梅隆大学的研究方向是语音识别,即","title":"产品的快速试错与及早反馈"},{"content":"1 前言 之前写《软件工程师的软技能指北》系列的时候,就有个挺想聊的话题的,就是写作。 其实不只是对软件工程师而言,我觉得对于所有人而言,都应该尝试下写作。 所以今天就来闲聊下写作的好处。 2 提升表达能力 社会上,或者是网络上,都会有对软件工程师(俗称码农)的刻板印象:加班多,情商低,表达能力不行,不修边幅。 国内国外,基本如此。 我去办信用卡的时候,负责帮我办卡的银行工作人员就两次问我,你们是否就只需要一天对着电脑,敲键盘就可以了? 虽说这只是刻板印象,但是的确切中了部分要点(起码对于我个人而言)。 以表达能力为例,我理解的表达能力好,就是能简洁明了,逻辑清晰地把一件复杂的事情描述清楚。 逻辑太跳跃,或者思路不流畅,就很容易让人听得云里雾里。 而表达能力本身又非常重要,无论与家人沟通,同事合作或者晋升答辩,良好的表达能力都能事半功倍。 而写作就要求你把自己脑海中以网状交织的知识,以结构清晰的方式,呈现给读者,做到「娓娓道来」。 在这个过程中,你的文字表达能力能得到提升,口头表达能力也会得到提升。 因为两者是相通的,都要求头脑对需要表达的内容具有层次性和条理性,只是最终的输出手段有差别。 3 加深理解 在写作时,我总有种奇怪的感觉「这个东西我懂,但是我写不出来」。 其内在原因是,对于该领域的内容,「我懂,但不是完全理解,无法做到信手拈来。」 因为要给写一篇让人能读懂的文章,势必要从基本的概念开始讲起,然后层层递进, 如果你对该领域的知识体系理解不到位,就会出现卡壳,写不出来的情况。 写作过程就促使你回头重新学习,弥补薄弱之处,进而加深对整个体系的理解。 所以写作本身就是在践行最好的学习方法:《费曼学习法》。 只是从给小朋友讲解,变成了写作,向所有读者分享。 4 促进内容传播 对比常见的沟通(如微信聊天,面对面交流等)和信息交流方式(音频,视频),文章拥有更好的传播优势。 如果你是面对面与人交流,或者微信聊天,你的交流方式是点对点的,只限于对面的人,你无法将信息广播给其他人; 而文章传播是点对面的,文章可以被复制,粘贴以及转发,自然拥有更广的受众。 又因为面对面交流,或者微信聊天是点对点的,所以你回答A的问题,可能也会被B问到,但是你却无法「复用」你的答案; 而文章是可被复用的,如果A和B看完文章,疑问自消。 而对比音频,视频等多媒体内容,文章的传播成本更低,可以直接被转发; 此外文章的阅读成本也比音频,视频更低,你可以检读,跳读,搜索文章内容,而视频只能从头看完,才能知道其究竟介绍了什么内容。 5 建立影响力 无论是有意还是无意,当你写的文章被阅读,被传播之后,你就在建立影响力。 如果需要建立影响力,可以通过演讲,制作B站或Youtube视频,或者写作来实现。所以会有这样的话: 如果你是个外向的人,你就去演讲和拍视频。 如果你是个内向的人,你就去写作。 但从传播学的角度来说,演讲和视频的传播优势都不如文章。 而建立个人影响力,都可能会对你的事业和心理健康起到促进作用。 从事业的角度来分析,建立影响力可以建立个人品牌,积累个人的影响力,助力职业发展和提升。 更多的人知道你,你才会有更多的机会,毕竟有人的地方才会有机会。 从自我实现的角度来分析,你的影响力越大,你的读者越多,你传播的知识可以影响和帮助到读者就越多,你就越能满足心理学家马斯洛所说的「自我实现」需求。 6 碰撞交流的火花 写文章本质就是在分享观点,当文章被传播,有了读者之后,自然会有人对你的观点持赞同态度,有人持保留意见。 读者就有可能向你阐述他们自己的想法: 当你有一个苹果,我也有一个苹果,我们交换了苹果,也只有一个苹果; 但当你有一个想法,我也有一个想法之后,我们交换想法,我们就有了两个想法。 要做到闻过则喜非常难,但是不同的观点就相当于一面镜子,可以让我们审视自己原来的观点是否合理。 他人的观点也给我们提供了换位思考的机会,从他人的观点切入,了解别人是如何思考的,避免「同温层效应」,只听到自己想听到的观点。 7 记录思考与成长 所谓「雁过留声」,又所谓「雁去无痕」。 横向对比,每个人都是独立的个体;纵向对比,每个人在不同的时期又会有不同的思考和感悟。 中学时候,语文老师总是会鼓励大家写日记,或者周记,说可以提高自己的作文水平。 所以我当时「轻信」老师的建议,尝试写了近1年的日记和周记,希望可以借此提高下自己的作文成绩。 但是即使我写了一年的笔记,也没有见语文老师多给我的作文一些分,感觉用处着实不大,然后就放弃了。 前段时间,在家里的柜子发现我这些用稚嫩笔迹写下的日记,翻看着有些泛黄的,写着的各种生活小事或者心情的纸张,忍不住笑了起来。 又或者翻开自己大二大三写的博客,记录着自己当初学习的一些笔记,稚嫩的思考,都会有种翻看旧照片的感觉。 写作,大概就相当于是用笔触作胶卷,给当下自己的思考和感悟拍下一幅幅「游客照」,以待日后再聆听昔日「雁行」时所留下过的声音。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E9%97%B2%E8%81%8A%E5%86%99%E4%BD%9C/","summary":"1 前言 之前写《软件工程师的软技能指北》系列的时候,就有个挺想聊的话题的,就是写作。 其实不只是对软件工程师而言,我觉得对于所有人而言,都应该尝","title":"闲聊写作的好处"},{"content":"1 前言 人并非全知全能,工作和生活难免会有各种的疑问,有问题自然可以询问有经验的同事或朋友。 但为了避免一有问题就去问人,给别人造成困扰,更推荐的就是: 自己先搜索,然后再去问人(Do a search before you ask a question) 当然,如果你不想打扰他人,直接问ChatGPT也未尝不可,只是答案的准确性不一定有保证。 如何高效地搜索,缩小搜索的范围,如何快速地检索到答案呢? 那么我来分享一下自己的个人经验: 2 Google Search 虽然我认为「搜索并不仅限于使用搜索引擎」,但是「搜索引擎」却是搜索并不可少的一部分。 虽然搜索引擎有很多,但是我基本只用 Google;如果没法使用 Google, 那么推荐使用Bing, 反正百度不在我的推荐之列. Google 搜索的界面很简单,只有一个搜索框,用户只需要把想要搜索的内容输入进去并回车即可。 比如搜索:「cpp modules」,返回了 7,320,000条结果。 搜索结果太多,我想对搜索内容进行筛选,google 就提供了相当多的搜索指令(search operator) 2.1 时间 cpp modules是c++20 才新增的特性,如果我想按时间搜索下相关的内容,可以使用 :before, :after 指令,后面跟着一个日期: cpp modules :before 2020 可以看到搜索结果变成了185,000条,并且返回的搜索结果都是在 2020 年以前的纪录,这个在查看历史新闻时特别有用,比如看历史合订本。 2.2 站点 如果你只想搜索某个站点,但是这个站点没有提供搜索功能(比如学校或者公司官网),或者搜索质量不够好,那么就可以加上 site: 的关键词, 要求 Google 只返回某个网站的检索结果: 比如我想看下 jetbrains家的IDE 对 c++ 20 Modules的支持程度: cpp modules site:jetbrains.com 又或者,我搜索网站的时候,想把某个网站排除掉, 比如使用中文搜索编程相关关键词的时候,经常会被CSDN 的垃圾内容污染,那么就可以使用 -, 来排除掉某些内容. cpp modules -microsoft 原来排名第二的 Miscrosft 就被过滤掉了. 2.3 社交媒体 如果你想在社交媒体上搜索某个关键词,那么可以使用 @ 后跟社交媒体的名字来进行搜索,例如 \u0026ldquo;cpp modules @twitter\u0026rdquo; 或者 \u0026ldquo;cpp modules @reddit\u0026rdquo;, 可以把 @ 理解成是 :site 指令的简化版本. 只是社交媒体(social media)的定义比较含糊, Google没有给出具体的说明,但是比较有名的社交媒体都是支持的. cpp modules @reddit cpp modules @zhihu 2.4 文件类型 可以通过 filetype 来指定想要搜索的文件类型,比如想搜索 pdf 相关的内容: cpp modules filetype:pdf 这个在知道书名,想要搜索电子书的时候特别有用. 2.5 关键字匹配 Google 支持若干个关键字匹配的指令: 双引号: \u0026ldquo;cpp modules\u0026rdquo;, 精确匹配,只匹配包含\u0026quot;cpp modules\u0026quot;的内容 \u0026#34;cpp modules\u0026#34; 搜索结果变成 3530 条纪录了. 星号: \u0026ldquo;* modules\u0026rdquo;, 通配符,所有包含 \u0026ldquo;modules\u0026quot;的内容都会被检索出来。个人觉得用处不大,只会让搜索结果膨胀. OR: \u0026ldquo;cpp or module\u0026rdquo;, 匹配包含 \u0026ldquo;cpp\u0026rdquo; 或者\u0026quot;module\u0026rdquo; 的内容, or 可以使用竖线代替 | 个人觉得用处不大,也只会让搜索结果膨胀 AND: \u0026ldquo;cpp and module\u0026rdquo;, 匹配包含 \u0026ldquo;cpp\u0026rdquo; 与\u0026quot;module\u0026quot; 的内容, and 可以使用与符号代替 \u0026amp; 3 Custome Search 前面提到「搜索并不仅限于使用搜索引擎」,是因为有很多内容,搜索引擎检索不到。 比如在公司内网的信息,Google 再强大,也不可能会检索得到的,因为不公开。 这个时候就可以借助浏览器的 Custom Search能力(Chrome 叫 Site Search, Firefox叫 Keyword Search)。 举个例子,我的老东家用的是代码搜索工具是 OpenGrok, 可以搜索整个事业群的代码,支持多种语言,可以搜索代码的定义,引用,历史记录等。 (下文以同样使用 OpenGrok 部署的开源项目 LibreOffice 的代码为例子) 因为在日常开发的时候,遇到陌生的函数名或者枚举定义,就需要看下他们的定义与实现,看下有没有问题: 比如想看下 contains 这个函数的实现: 或者想看下 Intersection 这个函数的引用,看下其他人是怎么用这个函数的,我也顺便抄下。 一般的步骤是: 打开或切换到浏览器(Chrome/Firefox) 打开内网网站链接, 在例子中就是 https://opengrok.libreoffice.org 点击 Definition 或者 Symbol 输入或者粘贴想要查询的内容,比如 contains 一套流程下来,大概需要30-40秒,不能说很慢吧,但是起码算不上快。 但是如果使用 Custom Search, 大概可以缩短至 7-8秒, 并且适用于绝对大部分的网站. 首先把查询函数引用的url 复制下来, 观察: https://opengrok.libreoffice.org/search?full=\u0026amp;defs=\u0026amp;refs=Intersection\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=refs\u0026amp;searchall=true\u0026amp;si=refs refs 后面跟着的就是需要查询的内容, 即 Intersection, 将 Intersection 替换成 %s : https://opengrok.libreoffice.org/search?full=\u0026amp;defs=\u0026amp;refs=%s\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=refs\u0026amp;searchall=true\u0026amp;si=refs 3.1 Chrome/Chromium Site Search 打开Chrome/Chromium -\u0026gt; 点击设置(Setting) -\u0026gt; 点击搜索引擎(Search Engine) -\u0026gt; Manage search engines and site search -\u0026gt; Site search [Add] Search Engine: OpenGrok Code Search Find Reference(取个有意义的名字) Keyword: csr URL: https://opengrok.libreoffice.org/search?full=\u0026amp;defs=\u0026amp;refs=%s\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=refs\u0026amp;searchall=true\u0026amp;si=refs 然后,在Chrome 的浏览器地址,输入 csr, 空格, 再搜索 Intersection, 回车。就可以直接在Chrome 地址栏里面搜索指定网页的代码. 而搜索代码定义,URL 如下: https://opengrok.libreoffice.org/search?full=\u0026amp;defs=Intersection\u0026amp;refs=\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=defs\u0026amp;searchall=true\u0026amp;si=defs 只需要将 defs 后面的内容修改成 %s, 再建一个新的site search, 名为 Opengrok Code Search Find Definition, keyword 为 csd, 就可以快速搜索代码定义. 如果想要搜索其他网站,比如公司内网: https://search.xxoa.com/query=Foobar, 只需要把查询内容修改为 %s, 再新建个Site Search 即可。 在老东家,搜索错误码,或者是搜索内网上的文章,我都是这么干的;所以到新东家之后,我也是这么搞的。 3.2 Firefox Firefox 也提供类似的功能,叫 Keyword Search, 添加起来甚至更方便: 打开想要搜索的网站 在搜索框点击鼠标右键,然后会看到一个「Add a Keyword for this Search\u0026hellip;」 修改名字与 keyword 然后,在 Firefox 的浏览器地址,输入 csd, 空格, 再搜索 Intersection, 回车。就可以直接在 Firefox 地址栏里面搜索指定网页的代码. 如果没有右键时没有找到 「Add a Keyword for this Search\u0026hellip;」的选项,也可以使用添加书签的方式,手动添加一个 keyword search: 4 Alfred Web Search 如果使用的是 Mac OS, 那么通过Alfred 插件的 Web Search功能,甚至可以不用手动切换到浏览器,直接就可以进行搜索,可以把搜索流的耗时进一步缩短到1-3秒。 Alfred -\u0026gt; Preference -\u0026gt; Web Search -\u0026gt; Add custome Search 除了要将 %s 换成 {query} 之外, 其他添加的步骤与 Site Search 一致: 录制 Gif 只花了1.5 秒. 5 总结 Perl语言之父Larry Wall 有句广为人知的名言:「程序员要有三大美德:急躁,懒惰,自大」。 急躁意味着不愿意花时间等待缓慢的程序,会想办法优化程序; 自大意味着不愿让人指谪,对自身要求强,要写出高质量的代码; 懒惰意味着不想花精心做重复无用的事情,会想办法自动化,让电脑帮忙处理。 \u0026ldquo;We will encourage you to develop the three great virtues of a programmer: laziness, impatience, and hubris.\u0026rdquo; \u0026ndash; LarryWall 而我对搜索流的优化,就是在培养「急躁」与「懒惰」的美德。 6 延伸阅读 我的各种「流」: 我的写作流:写作工具与平台分享 我的画图流:画图工具与技巧分享 7 参考 Mozilla Support: How to search IMDB, Wikipedia and more from the address bar Google Document: Refine web searches Google Document: Do an Advanced Search on Google ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%88%91%E7%9A%84%E6%90%9C%E7%B4%A2%E6%B5%81/","summary":"1 前言 人并非全知全能,工作和生活难免会有各种的疑问,有问题自然可以询问有经验的同事或朋友。 但为了避免一有问题就去问人,给别人造成困扰,更推荐","title":"我的搜索流:高效搜索经验分享"},{"content":"1 前言 分享两个鲜为人知,但是却相当有用的 Gmail 地址技巧。「鲜为人知」并非是标题党,而是引用Gmail 博客原话: I recently discovered some little-known ways to use your Gmail address that can give you greater control over your inbox and save you some time and headache. 2 技巧 假设你的Gmail 地址是 xiaoming@gmail.com: 2.1 加号 你可以将在用户名后面增加一个加号 +, 并在加号后面增加任意数量的字符,比如 xiaoming+happy@gmail.com, xiaoming+upset@gmail.com, Gmail 都会把这些地址当作成 xiaoming@gmail.com, 发送到你的地址邮箱中。 2.2 点号 你也可以在地址的任意地方插入任意数量的点号: ., 比如 x.i.a..o.ming@gmail.com, xiao...mi..ng@gmail.com, Gmail 都会把点号忽略掉,解析成 xiaoming@gmail.com 3 用途 技巧比较简单,寥寥数语就说完了,好像也没有什么大不了,有什么用处么? 这个就要发挥想象力了。 3.1 用途一:重复注册用户 这个主要是针对能使用邮箱注册的网站,可能大多数是国外网站。 如果网站的邮箱地址校验正则写得不好,允许加号和点号,不知道Gmail的这两个规则,那么 xiaoming+user1@gmail.com, xiaoming+user2@gmail.com, xi..aoming@gmail.com 就会被认为是三个不同的邮箱地址,就可以重复注册。 在薅羊毛等需要重复注册用户的场景就比较有用了。 3.2 用途二:溯源 个人邮箱难免会收到一些奇怪的邮件,例如:猎头的招聘邮件,钓鱼邮件等等。 收到这些邮件的第一反应肯定是把邮件删掉,之后就会思考,究竟是哪里泄漏了个人邮箱。 而通过 Gmail 加号的技巧,我就可以做到垃圾邮件溯源. 首先,在注册每个网站的时候,都给他们加上一个tag, 例如注册Twitter, 那就用 xiaoming+twitter@gmail.com, 如果注册Github, 那就用 xiaoming+github@gmail.com, 依此类推。 只要有垃圾邮件,我就能通过加号的后缀,知道是哪个浓眉大眼的网站把我的信息给泄漏出去了。 比如下面这个垃圾邮件,我就知道它是通过爬虫爬取我Github 公开邮件群发的. 我就可以选择不公开 Github 邮箱,来避免后续收到类似的邮件。 4 参考 Google Gmail Blog: 2 hidden ways to get more from your Gmail address ","permalink":"https://ramsayleung.github.io/zh/post/2023/gmail%E5%9C%B0%E5%9D%80%E7%9A%84%E9%9A%90%E8%97%8F%E6%8A%80%E5%B7%A7/","summary":"1 前言 分享两个鲜为人知,但是却相当有用的 Gmail 地址技巧。「鲜为人知」并非是标题党,而是引用Gmail 博客原话: I recently discovered some little-known ways to use your Gmail address that can give you greater control","title":"两个鲜为人知的Gmail地址技巧"},{"content":"1 前言 学习一门语言和学习手艺,过程差不多,没有太多的捷径可走,除了练习,还是练习。 无论是以前,还是现在,去公司上班,都需要接近一个小时的时间通勤。 为了不浪费通勤的一小时,我大多会在路上收听英文播客来练习英语听力。 2 工具 以前是坐班车上班,经常是听着听着英语听力就睡着了,毕竟播客的对话有深有浅,听不懂就容易睡着,英语练习就变成班车补觉。 虽然各种英语学习心得都强调多听的重要性,但是架不住着实听不懂,Podcasts App又没有办法展示字幕,你只知道你听不懂这个单词,但是却不知道这个单词究竟是什么? 不会的内容就不会有机会改善。 最近接触到一个很优秀的 Podcasts APP, 名为 Snipd, 可以通过AI自动把播客内容翻译成字幕。 说来有趣,这个Podcasts 软件的产品初衷并不是为了英语学习,而是类似视频截图,将播客的精彩瞬间和金句分享出来。 但是声音是很难以视觉化的方式来进行分享,转发的,所以他们就直接将当前播放进度前后80秒的内容以字幕形式呈现。 如果想要记录生词,可以直接点击创建「Create snip」,将句子保存下来,相当于保存了生词的上下文。 对于字幕生成,我现在发现,Snipd是采用离线缓存+在线生成的方式的: 如果是热门播客,可能就有用户已经提交了生成字幕请求,其他用户直接点开播客就可以直接展示; 对于冷门播客,需要我点击生成字幕,等待个10分钟,他们后台生成完成后会再通知我。 使用这个App还有一个附带的好处:可以收听非常多的海外播客。 因为中国什么都会有特供版本,播客也不例外。 如果使用的是国区的 Apple Id, 那么使用Iphone 自带的Podcasts App, 有非常多优秀的海外播客都无法搜索到(毕竟「收听敌对电台」) 而这个Snipd App可以搜索到非常多的海外播客,而大部分的英文播客都是海外播客。 3 播客 推荐几个我经常收听的英文播客: 3.1 Healthy hacker 网站链接:https://www.healthyhacker.com/ 一个从苹果天才吧电脑维修员工,成长为Github 工程师的小哥Chris Hunt主持的播客,我个人的最爱,主要是分享一些 Chris 自己觉得有趣的东西。 Chris 声音热情洋溢,可惜播客在2019年之后就没有更新了。 从天才吧员工成长为Github 工程师的那一期: 《11: Growing as a programmer》 3.2 THE CHANGELOG 网站链接:https://changelog.com/podcast 主要是分享软件工程,极客和行业创新,也有不少大咖上过播客,比如: Ruby On Rail之父 DHH, Sqlite 作者 D. Richard Hipp, Ruby之父,以及K\u0026amp;R 中的K( Brian Kernighan) . 3.3 Daily Easy English Expression 网站地址: https://dailyeasyenglish.libsyn.com/ 一个美国老师每期分享的地道英语词句的表达,每期只有几分钟。因为主持人是专业的英语外教,所以语速较慢,难度较低,非常好懂。 我在好几年前就在Youtube关注这个老师的口语教程,叫做 Daily English Dictation, 深入浅出,娓娓道来。 B 站上也有搬运Youtube的教程:每日英语听写 Daily English Dictation 1-400 翻开2020年的笔记,当时一天学习一课 Daily English Dictation,我学习到142课然后就放弃了。 3.4 THE HANSELMINUTES PODCAST 网站链接:https://www.hanselminutes.com/ 微软的 Scott Hanselman 主持的播客,类似技术杂谈,在英文技术类播客中也非常有名,他的角色类似个布道师。 3.5 Lex Fridman Podcast 网站:https://www.hanselminutes.com/ Lex Fridman 是俄裔计算机科学家,在MIT任职,他说话的方式很真诚,口音很好听. 他的访谈对象通常都非常大牌,比如是 Facebook 创始人 Mark Zuckerberg, 特斯拉的Elon Musk, 还有计算机的殿堂大神Donald Knuth等等. 只是他的访谈一般都很长,2-3个小时,我一般需要用一周的通勤时间来听完一期节目。 3.6 BBC 6 Minute English 网站: https://www.bbc.co.uk/learningenglish/english/features/6-minute-english BBC 主持的英语学习播客,顾名思义,每期6分钟,都是纯正的英音,女主持的英音尤其悦耳。 每期都截取一小道报道或者对话,然后学习一些新词,以练带学。 4 总结 突然意识到,收听播客和小时候通过收音机收听各种电台节目,如「评书讲古」似乎是异曲同工。 虽然媒介在改变,但是对好内容的需求却是一直不变的。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%8B%B1%E8%AF%AD%E5%90%AC%E5%8A%9B%E5%AD%A6%E4%B9%A0%E5%B7%A5%E5%85%B7%E5%88%86%E4%BA%AB/","summary":"1 前言 学习一门语言和学习手艺,过程差不多,没有太多的捷径可走,除了练习,还是练习。 无论是以前,还是现在,去公司上班,都需要接近一个小时的时间","title":"英语听力学习工具分享"},{"content":"1 问题 最近整理了桌上乱糟糟的线,把原来使用aux 线连接的蓝牙音响换成通过蓝牙连接。 然后就发现一个问题,只要音响没有发出声音超过30分钟,蓝牙音响就会断开连接,并且自动关机,即使蓝牙音响连接着电源。 一番搜索之后,就在知乎上发现了这个问题:求问如何避免蓝牙音箱自动关机? 但里面提到的解决方案,大多只适用于特定平台,例如Windows 或者Macos, 没有提到 Linux 上的解决方案。 每过半个小时手动打开蓝牙音响再连接的方式,实在是太蠢了。 2 灵感 但是知乎问题里面的部分回答给了我灵感,让我们想起国内某些APP 为了保活,避免被系统kill 掉,在后台播放无声音频的操作。 我可以以低音量循环播放一段音频,以实现保活的作用: mpg123 -f 1000 ~/music/listen_to_the_sea.mp3 --loop -1 mpg123 是mp3 播放命令行, -f 1000 参数的含义是:100%的音量是32768, 1000 约等于是1000/32768 = 3% 的音量, -loop -1 就是指无限循环播放。 man mpg123 ... -f factor, --scale factor Change scale factor (default: 32768). --loop times for looping track(s) a certain number of times, \u0026lt; 0 means infinite loop (not with --random!). 3 优化 这样就实现了一个可用的版本,只是还要依赖一个 mp3 文件,肯定还有优化的空间。 一番调研之后发现, play/sox 命令可以播放指定频率和时长的声音,可以播放20 hz以下的声音,这个频率下的声音人耳是听不到的: play -q -n synth 10 sin 20 -q: 不显示播放进度条 -n synth 10 播放10秒的音频 sin 20 频率为20 hz(如果听到了,可以设置成更低) 执行命令之后,可以使用 pavucontrol 命令查看声音输出,应该是类似这样的效果: 4 定时执行 一直开着个terminal 窗口运行命令有点麻烦,这种重复性的工作,就可以交给 crontab, 让它每分钟执行一次,每次播放10秒。 * * * * * play -q -n synth 10 sin 20 但实际运行,发现声音不能如预期那样播放。一番搜索之后,发现 StackExchange 上有个答案提到需要 export 个环境变量,所以最好创建个脚本 play_beep.sh: #!/bin/bash export XDG_RUNTIME_DIR=/run/user/1000 play -q -n synth 10 sin 10 echo $(date) # 打印日期,主要是为了方便排查 然后再安装一个 crontab 任务: * * * * * /usr/bin/sh /home/ramsay/code/shell/play_beep.sh \u0026gt;\u0026gt; /tmp/beep.log 2\u0026gt;\u0026amp;1 经过验证,一天都没有断开过蓝牙,自动关机了。 5 参考 求问如何避免蓝牙音箱自动关机? Can I use cron to chime at top of hour like a grandfather clock? ","permalink":"https://ramsayleung.github.io/zh/post/2023/linux%E4%B8%8B%E5%A6%82%E4%BD%95%E9%81%BF%E5%85%8D%E8%93%9D%E7%89%99%E9%9F%B3%E7%AE%B1%E8%87%AA%E5%8A%A8%E5%85%B3%E6%9C%BA/","summary":"1 问题 最近整理了桌上乱糟糟的线,把原来使用aux 线连接的蓝牙音响换成通过蓝牙连接。 然后就发现一个问题,只要音响没有发出声音超过30分钟,蓝牙","title":"Linux下如何避免蓝牙音箱自动关机"},{"content":"1 前情提要 软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 2 前言 让我静静,我只想写代码 化用《我的团长我的团》里面,孟烦了父亲的一句话: 为何诺大的公司,放不下一张能安静写代码的书桌? 在我此前的固有认知里,所谓的软件工程师就应该安安静静地写代码,但为何我总是求而不得呢? 但事实是,在软件开发的大部分时间里,我们都是在与「人」交流,而非与「计算机」交流。 即使我们编写代码,首先也是让「人」去理解,其次才是让机器来执行,否则直接写二进制代码即可。 而在程序优化中,有一条金科玉律:「针对热点代码进行优化」,因为那是性价比最高的优化策略。 既然软件开发中,大部分时间都是与人交流,那么如果能提高与人交流的效率,那么我们的开发效率也会相应地大幅提高。 3 原则 3.1 尊重 与人相处时,最重要的概念之一(可能没有之一),就是尊重他人,每个人心底都是渴望被尊重的。 所谓的尊重体现在各种的细节里面,例如: 尊重他人的观点和言论,留意倾听,眼神放在对方身上,不随意打断别人。 尊重他人的时间,不迟到。 尊重他人的成果和工作,引用时注意作者与链接等。 尊重别人的空间,不在工位附近大声开会,尽量找个会议室。 被尊重是每个人最基本的需要,也是很容易忽略的地方。 我自己也会在心急时,直接把别人的话打断掉,所以自己在这方面还有很大的改善空间。 3.2 不随意批评 因为国内普遍存在的各种上下级等级关系和官本位思想。 遇到阻碍或者问题时,很容易通过「批评」来推动和开展工作,甚至很容易出现所谓的「PUA」话术: 其实,我对你是有一些失望的。当初给你定级px,是高于你面试时的水平的。 我是希望进来后,你能够拼一把,快速成长起来的。 px这个层级,不是把事情做好就可以的 你的产出,和同层级比,是有些单薄的,马上要到年底了,加把劲儿。 什么,这个事情排期要2周,1周就可以了,没有多少工作的。 我自己也亲耳听过类似的话,心情着实是难受。 事实上,如果真的把「尊重」这个基本原则考虑在内,鼓励与赞扬是比批评更有用的工具。 我现在的 manager 是个白人,他就很喜欢夸人,我私下喊他做「夸夸群群主」。 我和组员刚来的时候,可能他担心我们不适宜,或者是不干活,我们做了一些工作之后,总是在换着法子在夸我们: Thanks you for help to our team, your work makes a great difference. You are doing a great job, I am impressed by the way you tackled the problems. You will be successful in Amazon, I am pretty confident about that. 虽然知道老板目的还是想让我们干活,但是被人夸的感觉肯定比被人用鞭子抽打的感觉要好。 见贤思其焉,所以我也学老板多夸人。 有一次和舍友去一家韩餐餐馆吃饭,炸鸡很好吃,其他菜也不错。 上完菜后,韩国小姐姐过来问我们还有需要,我就说,「all foods are delicious, especially the fried chinken」。 小姐姐开心得拍起了小手。 身为中国人,可能从小被教育要内敛和矜持,但我们大可不必太高冷,不要吝啬自己的溢美之词。 3.3 换位思考 高效沟通的另外一个要点就是换位思考,从别人的角度,而不是自己的角度来思考问题。 在沟通对话中,什么对于他们来说是重要的?他们想要的是什么? 最好的方案是一个共赢的方案,可以把多方的诉求都包括在内。 一个非常有效的技巧就是,在开始你自己的观点,先重复一次别人的观点,这样就给对方一个明确的信号,我是真的考虑过你的观点的。 举个例子,前段时间发了一个 Amazon Canada 招聘的文章,有朋友闻讯而来,给我发简历,让我内推到系统中。 只是他没有预料到的是,发送完简历后,马上就收到一封笔试邮件,要求在一周内完成笔试。 朋友觉得时间太紧,没有准备好,于是邮件告知我准备放弃。 我思索片刻之后,决定与recruiter 沟通下,询问能否推迟笔试截止时间。 因为对于朋友而言,他的诉求肯定是有充足的时间来准备; 而对于recruiter 而言,她们办这个event ,也是希望有尽量多的候选人参加,有尽量多的候选人通过。 所以推迟笔试时间,以便朋友参与笔试,是一个符合多方诉求的方案,最后recruiter 的回复也是可以推迟时间: 3.4 充分的上下文 高效沟通和决策的前提是,提供足够的,充分的信息。 也就是说,在你提问题或者沟通的时候,把问题的上下文信息给提供清楚。 以前经常会遇到的一种情形是,在企业微信被人拉到一个群里,然后被@, 「xx哥,帮忙看下这个问题」。 我也很想帮忙,但是我连问题是什么都不清楚,我是没有办法解决的。 一个群几十上百条信息,我是没有精力去逐条翻聊天纪录的。 然后,很快就会有人打电话过来,让我解决这个xx问题。 如果想要我快速解决问题的话,麻烦首先要给出定义,问题是什么?然后再给出问题的上下文,这样我才能方便排查问题。 但这还不是最佳的咨询姿势,我推崇的咨询方式是所谓的 STAR 方法或者叫「Search before Asking」。 4 STAR 方法:高效提问 所谓的STAR method, 是四个单词的首字母缩写,分别是: Situation(场景), Task(任务), Action(行动),Result(结果)。即: situation: 描述问题的背景,这个问题是什么,以及你为什么需要做这个事情 task: 你具体的任务是什么,你需要做什么 action: 你做了什么事情?你的行动是什么. result: 结果如何,你得出的结论是什么? 前面提到过,尊重是与人交流的基本原则, 尊重自然包括尊重别人的时间,不做伸手党。 在咨询别人问题的时候,不仅要把问题说清楚,还需要把自己的调查和排查结果告诉别人,即所谓的「search before asking」,这样给人的印象是我尝试自己来解决,但解决无果才来请教你。 既表现出对别人能力的尊重,也显示出自己是经过调查才发问的,避免询问一些低级,Google 就能找到答案的问题。 没有人喜欢伸手党,你直接拿个问题,不经自己思考去询问别人,这就不是交流沟通,是「空手套方案」了。 别人没有这样的义务来给你提供解决方案。 所以我向别人求助,无论是企业微信,邮件,还是当面求教,流程一般是: 我现在尝试解决xx问题,我要去解决这个问题的原因是yyy 我尝试了解法1, 解法2,都无法解决,这是我的日志 尝试这几种解法都不能解决问题,不能你能否根据你的经验,给我提供点思路呢?或者是我漏了什么关键步骤么? 或者是. 我现在尝试解决xx问题,我要去解决这个问题的原因是yyy 我尝试了方案a xxxx, 得出的结果是xx, 然后我再尝试了方案b xxx, 得出的结果是xxx. 我个人感觉这两种方案各有优劣,分别是xxx, 我倾向方案a, 原因是xxx. 想请教下,你的看法是觉得哪个方案更优,或者你有什么建议么? 提供足够的信息和选项给别人做选择题,让不是提供个空白问卷让别人做主观题。 毕竟大多数人都喜欢做选择题,省时省力。 \u0026mdash; \u0026lt;2023-05-29 一\u0026gt; 关于如何提问,《How to ask questions the smart way》一文已经把要点给掰碎讲清楚了,推荐阅读。 5 云雨伞: 有效提建议 来源于日本知名咨询师大石哲之的著作《靠谱:顶尖咨询师教你的工作基本功》,简单有效,原理概括起来就一句话: 天上出现乌云,眼看要下雨,带上伞比较好。 其中的「云」代表通过观察得到的客观事实;「快要下雨」,是从客观事实得出的分析;「带上伞」这个是根据分析给出的建议。 这就所谓的「云雨伞」模型的来源,运用「云雨伞」模型提建议,有理有据有方案,能让对方更愿意接受。 青史留名,给老板提建议(画饼)的名篇《隆中对》,也运用了「云雨伞」模型: 亮答曰:“自董卓以来,豪杰并起,跨州连郡者不可胜数。 \u0026hellip; 荆州北据汉、沔,利尽南海,东连吴会,西通巴、蜀,(描述「云」,表达事实) 此用武之国,而其主不能守,此殆天所以资将军,(推测「雨」,即分析利弊) 将军岂有意乎?(带上「伞」,即提出建议) 益州险塞,沃野千里,天府之土,高祖因之以成帝业。(描述「云」,表达事实) 刘璋暗弱,张鲁在北,民殷国富而不知存恤,智能之士思得明君。 将军既帝室之胄,信义著于四海,总揽英雄,思贤如渴,若跨有荆、益,保其岩阻,西和诸戎,南抚夷越,外结好孙权,内修政理; (推测「雨」,即分析利弊) 天下有变,则命一上将将荆州之军以向宛、洛,将军身率益州之众出于秦川,百姓孰敢不箪食壶浆以迎将军者乎? 诚如是,则霸业可成,汉室可兴矣。(带上「伞」,即提出建议) 这一番结合「云雨伞」的建议(画饼),让老板直呼,「孤之有孔明,犹鱼之有水也」 6 总结 所谓的「Skill = knowledge + practice」,知道一项知识,如果不运用,没有办法修炼成技能的。 毕竟「纸上得来终觉浅,绝知此事要躬行」。 要多实践才能提高沟通能力。 推荐几本读过的,关于沟通,心理学与咨询的好书,推荐度由高至低: 《非暴力沟通》, 豆瓣评分:8.7(当然,评分只是一个参考项) 《社会性动物》,豆瓣评分:9.0 《靠谱》:豆瓣评分:7.6 《QBQ!問題背後的問題》,豆瓣评分:7.4 软技能系列的下一篇是: 软件工程师的软技能指北(四):简历篇 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E8%BD%AF%E6%8A%80%E8%83%BD%E6%8C%87%E5%8C%97_%E9%AB%98%E6%95%88%E4%BA%A4%E6%B5%81%E7%AF%873/","summary":"1 前情提要 软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 2 前言 让我静静,我只想写代码 化用《我的团长我的团》里面,孟","title":"软件工程师的软技能指北(三):高效交流篇"},{"content":"1 主业 Ramsay 是位软件工程师,以写程序为业.\n推崇开源与动手精神,使用Emacs 与Linux 多年,喜欢动手折腾。 写过C++, Java, Python, Javascript/Typescript这四种语言的生产代码,喜欢Rust 与 Ruby,常用EmacsLisp 为自己的工作流写小工具。 2 写作 从16年开始在博客上写博文,期间经历过多次迁移,但大多时候,博客的读者都只有我自己。 博客还有一个对应的英文博客,名为「In pursuit of Simplicity」,主要是记录一些英文写作与感悟的平台。 Simple is Beautiful 非纯技术文章大多会同步发布到公众号「宫孙说」上: 3 开源 在 Github 上有若干个开源项目,但目前主要在维护RSpotify 这个使用Rust 语言编写的 Spotify SDK。 在Stackoverflow 上也会帮忙解答其他开发者的疑问,反馈社区。 4 交流 Ramsay 喜读书,电影,历史;亦爱生活,料理;闲暇时常涂鸦写作,抒发感想。 绿蚁新醅酒,红泥小火炉。 晚来天欲雪,能饮一杯无? 望与君,共举杯。 5 友链 AsyncX: 🌌 Per Aspera Ad Astra ","permalink":"https://ramsayleung.github.io/zh/about_me_zh/","summary":"1 主业 Ramsay 是位软件工程师,以写程序为业. 推崇开源与动手精神,使用Emacs 与Linux 多年,喜欢动手折腾。 写过C++, Java, Python, Javascript","title":"关于我"},{"content":"1 前情提要 软件工程师的软技能指北(一):总览篇 2 前言 打工是不可能打工的,这辈子都不可能打工的。 3 心态转变 很多软件工程师容易把自己定义成「写代码的」,或者是「码农」,就是以写软件为生的人。 也只愿意接受写代码相关的任务,什么文档,设计,需求分析,是一概不想理的,我就是一把唆。 也有工程师觉得,反正我把事情做好也只有这么点工资,摆烂收入也不一定会下降,那不如就躺平,反正我的收入是固定。 也不能说毫无道理,只是把自己定位成「需求翻译机」,着实和「流水线的工人」区别不大。 随着自动化技术的进步,「流水线工人」很容易就被机器人所取代,它们只要能源充足,就可以24小时不停地产出 但是踏实干活的工程师,也难免容易有与以上类似的疑惑。 那不如换个思路: 把你的工作当成是你自己的生意(business),那你眼中的一切都会变得截然不同。 3.1 客户 既然是生意,自然要找对目标客户。 如果把工作当成生意,那么你的客户就是你的雇主,虽然你的客户大多数情况下只有一个。 但是很多的公司,都是靠给某一个大客户供货而做大做强的。 3.1.1 客户(customer)与用户(user) 谈起business, 我就想聊一下个人对于客户与用户的浅薄见解。 归纳起来,就是两点: 商业公司总是客户第一 用户不等于客户 简而言之,用户是使用某项服务或者产品的人,而客户是为某项服务或者产品付费的人。 举个例子,经常有人说,微信不注重用户体验,微信不倾听用户的声音,微信有着地球上第二傲慢的产品经理团队(第一可能是苹果)。 就我在微信的开发经历而言,的确如此。 没有见过哪些产品经理提的需求是来自于改善用户体验的, 腾讯内网上都是挂着各种反馈微信用户体验的帖子,最后都是以「这个问题,楼主可以私聊我们讨论」结束的。 因为,对于微信而言,微信用户只是使用微信这个软件的人,而不是为微信付费的人,不是微信收入的来源。 对于微信支付而言,客户是各种接入微信支付的商户,因为每笔交易,他们要交约等于交易金额 0.0021%或者更多的手续费,属于躺着赚钱的模式; 对于微信朋友圈,公众号而言,客户是各种广告主; 在微信用户面前,微信就是个爹,教育你们怎么使用微信;但是在微信客户面前,比如美团,快手这些微信支付的大客户,微信就是孙子。 要做什么需求,产品经理根本没有办法推;要什么时候上线,就什么时候上线,即使不合理,也只能回来压榨工程师的时间。 毕竟客户说了,你们不做我们就切到支付宝去。 所以微信用户本质上只是微信收入来源的耗材和燃料,反正用户离不开微信这口灶,产品经理为什么还要听燃料的心声呢。 当然,背后的商业逻辑是这样,用户体验又是另外一回事了。 3.2 产品 既然是生意,那么自然要有可以营利的产品或服务,对于大部分工程师而言,他们能提供的产品,就是生产软件的服务。 那和「写代码的」也没有什么差别嘛? 稍安勿躁,这只是第一步嘛。 如果我们提供的生产软件的服务是生意的话,那么要想营利,产生更大的利润,就需要我们考虑一个问题: 如何大家都是生产软件的生意,你的产品又如何从同质化严重的同行中脱颖而出。 3.3 竞争优势 搞低价倾销(加班巻死他们)? 这也是个可行但不能持久的法子: 毕竟你搞低价倾销,即使把生意都抢到,你产能有限,客户的单不一定都能接过来; 另外低价倾销,只会把市场搞坏,降低了利润空间,只会让客户单方面受益 强中自有强中手,一山还有一山高,万一遇到比你还能搞低价倾销的同行,那不是哑巴吃黄莲,有苦说不出嘛。 所以最优解应该是你提供更优质的服务,将优质服务作为自己的竞争优势。 既然要提供优势的服务,就需要 学会与客户沟通交流,先明确客户的需求, 然后分析需求,明确这服务是否客户想要的, 再动工建设,保证最终成品贴近客户的诉求。 或者是成为某个领域的专家,提供差异化的服务。 所谓人无我有,人有我优。 看到这里,有朋友可能会质疑:即使我做了这么多,做得这么好,但是工资(产品的售价)还是不涨阿,那还有什么意思? 如果把这个当作自己的生意,提供优质服务之后,自然是需要和客户重新谈合同的嘛(加薪)。 如果谈不拢,那就换家客户就好了,反正我只要产品够好,自然不缺客户,我还可以拿现有的供货合同和未来的客户谈。 生意是自己,服务做优质之后,最终受益的还是自己(当然,需要些时间和策略) 3.4 大厂光环 所谓的大厂光环,和偶像光环类似,就觉得个人会因为进去某个公司,把平台优势当作自己的成就,从而骄傲了起来。 坦白讲,以前我也有大厂光环,在自己去了某家大厂之后。 走路的时候,头抬得更高了,背挺得更直了,以便于胸前的工牌更加醒目。 如果把自己的职业生涯比作一门生意后,我想我应该不会再为与某个客户合作而沾沾自喜,毕竟客户的商业成就,与我关系不大。 客户可以有很多个,没有必要为别人的成就而自得不已。 最近新读到一首诗,唐代孟郊的《劝学》: 击石乃有火,不击元无烟。 人学始知道,不学非自然。 万事须己运,他得非我贤。 青春须早为,岂能长少年。 万事须己运,他得非我贤。 4 十项全能 既然是要做生意,那么只会写代码,注定是不可行。 毕竟没有见过哪家成功的商业公司,只在车间生产产品即可,不需要一系列配套的商业运作流程: 营销与广告,打造个人品牌,写博客或者做Up主 持续学习,没有什么生意是一成不变,就能从爷爷辈做到孙子辈的 如何提升个人效率,以更少的投入获取更多的产出 如何理财,管理你生意的营收与支出 如何健身,管理你自己的身材 旋转720度,落地无水花 诸如此类,这些技能要求也就变成理所当然。 5 总结 把工作当作生意的思路转变,只是第一步。 套用《霸王别姬》的一句台词: 今儿个是破题儿 文章还在后头呢 客户或公司是一个抽象的概念,实际也是由形形色色的人组成。 与客户合作,实际是与各种人打交道,如何高效沟通和交流就是一个非常有用的技能。 但对于曾经社恐的我来说,跨出第一步却是非常艰难。 所以软技能系统的下一篇是: 软件工程师的软技能指北(三):高效交流篇 对于后续的篇章,呼应上文,我有了大概腹稿,分别是: 软件工程师的软技能指北(四):简历篇 软件工程师的软技能指北(五):面试篇 软件工程师的软技能指北(六):谈薪篇 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E8%BD%AF%E6%8A%80%E8%83%BD%E6%8C%87%E5%8C%97_%E4%BA%8B%E4%B8%9A%E7%AF%872/","summary":"1 前情提要 软件工程师的软技能指北(一):总览篇 2 前言 打工是不可能打工的,这辈子都不可能打工的。 3 心态转变 很多软件工程师容易把自己定义成「写代","title":"软件工程师的软技能指北(二):事业篇"},{"content":"1 目录 为了方便阅读,把本系列的文章的目录整理如下:\n软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 软件工程师的软技能指北(三):高效交流篇 2 背景 上大学时,曾经看过一本书《软技能,代码之外的生存指南》,主要介绍程序员要想取得成功的职业生涯,所必需的软技能。\n所谓的软技能,是区别与使用C++或Java 编写业务逻辑和单元测试代码,使用Docker 部署等「硬技能」,而是注重职场,心态,身体与理财等各方面提升的「软技能」。\n大学时候的我还沉迷于编写各种很cool 的代码,学各种编程语言,觉得此书不过是面向程序员的「鸡汤」一本。\n2.1 再读 工作第三年时,换了一份工作,对职业充满了迷茫与困惑,然后再读了一次这本书,得出的结论是书挺不错的, 但是不符合中国程序员的国情。\n比如理财方面,建议程序员在低房价时买房,我也想买,但是在深圳只能是望楼兴叹;\n健身方面,下班后多运动,自己做饭,控制饮食以增肌减脂,着实没有时间自己做饭。\n但里面的思路和哲学很有参考意义。\n2.2 三读 最近突然想起了这本书,搜索之后发现,英文原版在2020能出了第二版,而中文版本也在2022年翻译出版了;\n因为书再版了,所以又重读了一次这本书,英文书名会更正常一些:Soft Skills: The Software Developer\u0026rsquo;s Life Manual\n得出的结论还是与之前一致:「书中的思路和哲学很有参考意义,做法却不一定适用」。\n但区别在于,我这次打算把我自己的做法与行动也总结下来,「用他的旧瓶子,装我的新酒」。\n就成功的标准而言,我的职业生涯还远谈不上成功,甚至可以说还有很多失败之处,这也算是我自己反思的心得。\n毕竟也不只有别人优秀的经历,我这种潦倒的经历也是可以参考的。\n见贤思齐焉,见不贤而内自省也。\n这让我想起了Netflix 的高分纪录片《我心永随桑德兰》,别人拍纪录片是记录成功,这部豆瓣9.2分的片子却是在记录失败:\n英超保级劲旅桑德兰,在2017年终于花光自己在英超的保级运气,从顶级联赛英超跌入二级联赛英冠。\n然后俱乐部高层想拍一部纪录片,记录自己奋发图强,卧薪尝胆杀回英超的经历,以此吸引投资者,最后却是二连跳, 跌入到三级联赛英甲的神剧情。\n把我不开心的事说出来,拿出来给大家一起开心下嘛。\n3 软技能 就软技能而言,绝不止「软件工程师」这个职业需要这个技能,而是绝大部分职业都需要的。\n因为绝大部分的工作都是与人打交道,而软技能就是如果更高效与人沟通的关键技能。\n如果有「软件工程师」认为自己只要写代码就可以了(我曾经就是这么认为的);或者觉得,写代码才是最有趣的部分(我现在也是这么认为的),我不想理这么多事情;\n那么你的可替代性就非常高,当代码生成工具足够成熟之后,你就可以被裁掉了。\n之前办信用卡时候,我说的职业是Software Engineer,银行客户经理问我, 你们是不是只要埋头对着电脑敲键盘即可,不需要和人说话的?\n(我心想,你是美剧看多了吧)\n而事实恰恰与直觉相反,软件工程师大部分时间都在与人打交道:\n拿到需求时,需要分析需求的可行性,与产品经理扯皮,理清模糊之处 撰写设计文档,和组员及老板介绍方案,比较方案优劣,选择最优解 与上下游团队扯皮,求下游团队帮忙干活,给上游团队表演太极 和老板画饼,只要再给我些时间,定然能做得成绩斐然。 哪项不是与人打交道呢?项项都是我缺乏的技能阿,我就只会接活,干活,然后再接再干。\n老黄牛听到我这境况,估计都得叫我一声兄弟;流水线见我这际遇,也会直呼一声「内行」\n所以「软技能」真的是不可或缺。\n或许是我给银行客户经理的答案不合他意,我信用卡申请被拒了;\n或许我「软技能」再强大一些就能通过了.\n4 主旨 原书把主旨分成七部分,分别是:\n事业(Career): 像经营企业一样,打磨自己的职业生涯 自我营销(Marketing Yourself): 通过高质量文章和视频,打造自己的个人品牌 学习(Learning): 终身学习,自我学习 生产力(Productivity):提高生产力的方式 理财(Finanacial):如何复利,创造被动收入 健身(Fitness):健康,有型的体魄 心态(Mindset):培养积极的心态,To be a better man(woman). 我就不会按照作者的主旨来阐述我自己的想法,毕竟「他瓶装我酒」,想怎么「装」就是我自己的选择了。\n感触多些的主旨,就拆分开多几篇文章,没有太多感慨的部分,可能就选择性省略了。\n5 总结 所以要「软硬都抓,内外兼修」,才能成长为一个优秀的软件工程师。\n只会写代码的软件工程师,真的是注定吃亏,酒香也怕巷子深。 这是多次吃亏之后得出的经验总结。\n其他的职业与岗位也大抵如此。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%80%BB%E8%A7%88%E7%AF%87/","summary":"1 目录 为了方便阅读,把本系列的文章的目录整理如下: 软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 软件工程师的软技能","title":"软件工程师的软技能指北(一):总览篇"},{"content":"1 Amazon Canada国内专场招聘 刚刚有群里消息灵通的小伙伴分享,Amazon Canada(主要Base 在Vancouver )会在国内的深圳,上海及周边(杭州,苏州)和香港有专场的招聘会。\n最近有比较多的同学和朋友咨询加拿大工作的事情,所以就把招聘Event 分享出来。\n本次招聘的主要对象是L5的工程师,L4和L6也有少数的HC.\n国内专场招聘的链接:\nhttps://amazon.jobs/en/jobs/2361958/hong-kong-event-sde-amazon-stores\n二维码:\n可以微信或者邮箱找我帮忙内推, 我可以帮忙递简历和咨询进度,我邮箱是 cmFtc2F5bGV1bmcrYW16bl9oaXJlX2V2ZW50QGdtYWlsLmNvbQo= (base64 decode),希望能做个摆渡人吧。\n1.1 面试流程 与招聘的 recruiter 沟通过,应该还不会有 in person 的面试,还是线上的virtual 面试,给面试候选人发一个会议链接,然后进行线上面试。\n所以招聘写着HongKong Event, 也并不需要去香港面试。\n这次主要是面向SDE(Software Development Engineer), 职位大多是L5.\n面试时间大概是6月底,所以大概有2个月的时间来准备。\n以我个人经验,L5面试流程大概是:\nOnline Assessment(OA), 做一到两道算法题,难度大概是Leetcode Medium - Hard, 多刷题库,有机会试上原题。 Phone Screen(Phone Interview), 视OA 结果而定,根据OA 的答题结果,系统会给出建议是否需要Phone Screen, 如果答得比较好,可能就不需要。我当时也没有这一轮 4轮onsite, 一天搞完,包括: 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + 一题算法题,Leetcode Medium 水平 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + 一题算法题,Leetcode Medium 水平 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + System Design(SD) 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + Object Oriented Design(OOD) 拿 Offer 2 Q\u0026amp;A 在微信上和朋友分享之后,有比较多的朋友咨询问题,我就尽量总结下Q\u0026amp;A\n2.1 面试是全英文的么? 如无意外,是的。毕竟去了温哥华工作,也不可能只说中文。\n2.2 我英语口语不行,可以试试么? 其实,只要能沟通就会了,不是雅思考试,不需要4个7,能让面试官听懂就可以了。\n即使去尝试下,最多也只是付出时间成本,也损失不了什么。\n小马过河,你也不知道你是松鼠还是黄牛。\n2.3 面试通过后公司帮办工签么? 是的,公司会指派对应的律所来帮忙申请工签,只需要按律所要求提供材料即可。\n时间大概需要4-6个月,视情况而定。\n配偶和子女也可以一并办理签证。\n2.4 面试通过后多久入职? 一般大概需要半年,主要是办工签,等IRCC 审批。\n2.5 Amazon不是刚裁员,怎么又招人了? well, I don\u0026rsquo;t know,我也想知道。\n2.6 Amazon的面试难么? 这个嘛,只能说见仁见智。\n只是相对来说,Amazon 是北美大厂FANNG 里面的地板难度,主要是多刷题,就有机会遇到原題。\n多刷面经,亚麻的面试套路还是相对固定的。\n2.7 Behavioral Qusetion 与Leadership Principle 究竟是什么? BQ(Behavioral Qusetion),即行为面试问题,Amazon 面试官会通过BQ考察你的性格特征和职场软技能,判断你与企业的文化、价值观等原则是否匹配,最终决定是否录用你。\n而判断标准就是Amazon 的17条「价值观」,即 Leadership Principle, 例如:\nCustomer Obsession Ownership Invent and Simplify \u0026hellip; 具体可见官网说明:https://www.amazon.jobs/content/en/our-workplace/leadership-principles\n常见的问题如:\nTell me a time when you had a deal with a very difficult customer Tell me about a time when you took on something significant outside your area of responsibility Tell me about something you deliver above standards Tell me about a time when you couldn\u0026rsquo;t meet your deadline Tell me about xxx 相当于命题作文,需要结合你自己的简历内容,提前准备小故事\n2.8 招聘的业务部门是哪些? 据recruiter 所说,这次是个专场的招聘,相当于是放到一个大池子里面,通过之后再做Team Match, 所以她们也不知道具体的业务团队是哪些?\n2.9 我要刷多少题Leetcode 呢? well, 这个也很难回答,毕竟每个人能力不一样,但一般来说,300题 + Amazon 的题库应该可以应付了。\n2.10 我是写xxx 语言的,可以去面试么? 就我所知,无论你是写 Javascript/Java/C++/C/Python/Ruby/Assembly 还是Lisp,\n只要你能通过面试即可,使用什么语言都没关系。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/amazon_canada_hiring_event/","summary":"1 Amazon Canada国内专场招聘 刚刚有群里消息灵通的小伙伴分享,Amazon Canada(主要Base 在Vancouver )会在国内的深圳,上海","title":"做个摆渡人:Amazon Canada的国内专场招聘"},{"content":"1 起 落地加拿大已经大半个月了,也开始工作了,心情也从期待,紧张,忐忑,彷徨,兴奋到现在逐渐平静下来。\n来到一个陌生的国家,使用不一样的货币,说不一样的语言,过不一样的生活和工作习惯。一切如初生婴儿一般,需要重新学习和适应。\n现在就来分享下我与加拿大之初体验。\n2 衣 出发前,在广东的天气都快接近30度,在家的我已经把拖鞋,短裤都已经穿起来了。\n虽然我所在的城市已经是加拿大的最南端,但我落地加拿大之后的第一感觉就是:冷。\n看了下手机上的天气预报,温度只有5度,还刮起了风。\n落地之后,我的穿着从短裤变成了棉衣,只是我穿着行李箱拿出来的棉衣都觉得冷,怎么看到街上的行人只穿了个薄外套或者夹克,都丝毫没有寒意。\n天气太冷,以至于我都无法出门跑步了。\n除去冷之外,温哥华的另外一个特点就是多雨,所以温哥华(Vancouver)也被称为是雨哥华(Raincouver),之前还遇到下冰雹:\n后来,我发现,无论大太阳还是下雨,温哥华本地人都不打伞。因此,我得出一个结论,他们的衣服是防水的。\n那么问题就来了,他们的裤子防水么?\n2.1 地理位置 家人开始总会很疑惑,为什么我说我去的城市是温哥华(Vancouver), 他们看到我的定位叫列治文/列士满(Richmond), 我不会去错地方,进了传销窝吧。\n所谓的温哥华一般指的是大温哥华地区(Greater Vancouver), 或温哥华都会区(Metro Vancouver), 是指温哥华市和周围的卫星城组成的都会区,约等于粤港澳大湾区,但是不同城市之间的距离就小很多了,大体与广州和佛山类似。\n对这些大温城市的「刻板」印象:\nVancouver(温哥华):真-温哥华,大温地区唯一的「城里(downtown)」, CBD,潮牌夜店与流浪汉所在地。 North Vancouver(北温):风景好,本地人居多。 West Vancouver(西温): 巨富聚居,豪宅云集之地,俞敏洪家就在温西。 Burnaby(巴拿比):地理位置处于大温中心,有BC省最大的购物中心,新城区。 Surrey(索里):印度族裔聚居之地,治安较差。 Richmond(列治文):华人聚居之地,目测50%以上都是华裔。白人在这里,算是「外国人」,被华裔移民戏称为「老家」。 3 食 因为我有一个挑剔的广东胃,来之前,我还担心来到加拿大之后不适应,只能委屈我的胃消化薯条,炸鸡,汉堡了。\n没有想到 Richmond 被誉为北美最多中国美食的城市(另外一个可能是多伦多)。\n3.1 酒楼 这里遍地写满繁体中文的饭店和酒楼,再上服务员的粤语招待,让我有种身处香港的错觉。\n而饭菜品尝下来,这里的店的水准和味道,可谓吊打深圳(深圳能有什么好吃,就随处可见的椰子鸡),估摸能比得上广州。当然,只是味道,价格要贵很多。\nRichmond 饭菜虽美味,但北美有个我至今也难以适应的风俗:给小费。\n加拿大的小费还是税后的,一般要给个15%. 只能默默地为国内的服务业从业者哀叹下,国内人工真廉价。\n3.2 做饭 酒楼饭店虽多,但总不能天天下馆子,所以就需要拿出我多年的「煮饭公」经验了。\n而住处1km内,就有华人超市「大统华」和香港风格的菜市场,我平时在国内用到的酱料和厨具,在 Richmond 的菜市场都能买到,甚至包括盐焗鸡粉和磨刀石,着实有点离谱。\n因为有这样的便利条件,我就能吃到有家味道的饭菜:\n回公司办公时,因为公司在温哥华市中心,附近真的只有汉堡和炸鸡,就一定要自己带饭了。\n不过,公司办公楼外的风景着实很不错:\n4 住 4.1 选择 新到一个城市,最重要就是要找到住处,有个可落脚的窝。\n之前做过攻略,预期是住在 Richmond 或者Burnaby,为了实地考察究竟哪里更宜居,我通过酒店和Airbnb 民宿,把Vancouver 市中心, Richmond, Burnaby都住了两天,得出的结论是:\nVancouver downtown: 又贵,又小,流浪汉又多,附近又没有便利的生活设施 Burnaby: 环境虽好,多公寓,但是附近没有便利的生活设施,如果没有车,购物,买菜都不方便。 Richmond: 好吃的多,生活设施方便,但距离市中心稍远,通勤时间略长。 权衡利弊之后,最后毫无悬念地选择住在了 Richmond.\n4.2 租售比 租房时,从网站上浏览温哥华租金历史的变化趋势,看到这两年的租金翻了一倍。\n一个两室的房子,如果靠近天车站和超市,基本都要3000加元,通货膨胀压力太大,真的吃不消。\n从深圳来的人,难免会对房价感兴趣,就看了一下住处附近的房价。\n温哥华西靠大海,北朝雪山,身处加国最南边,气温宜人,风景好又宜居,所以温哥华的房价一直高居加国前茅。兼之这两年房价又猛涨,已经到了让普通人咋舌的地步了。\n只不过加币兑换人民币,汇率是1:5,如果温哥华的房价以人民币换算过来,还是比不上深圳。\n不知道这是否值得深圳人民骄傲呢。\n4.3 租房条件 和国内租房,房子基础家居齐全,能够「拎包入住」的情况不同,北美租房的「标配」是只有房屋一间,家居,床垫等都需要自行采购。\n有家居配置的反而是「高配」,幸运的是,我和舍友找到这样的房子恰好是「高配」,房东也是位很nice 的女士。\n此外,与国内租房只需要签租房合同,按时交租不同,在加拿大租房还需要额外提供相当多的信息:\ncredit history, 房东用来审核租房是否有不良的信用纪录,毕竟这里是个信用社会。而我初来乍到,信用卡都还没有,自然就没有credit history, 所以就需要交半个月的押金。 工作offer 和收入证明,证明是有稳定收入的。 护照原件。 验资,提供存款纪录,证明可支付三个月的房租 额外提供更多的信息,也换来了相应的法律保障。\n法律规定了房东一年的租金涨价幅度不能超过通货膨胀的水平,如果租房选择继续次年续租,租金最高涨幅2%。就不会出现腾讯员工尊享「12折」租房优惠的情况。\n如果房东想要给房子大幅涨租金,唯一的方法应该是等租房合同到期,选择不续租,然后加租金再次出租。\n5 行 就公共交通设施的便利程度,与广州或深圳相比,温哥华真的算是「大农村」,或者整个北美都是大农村。\n与广州密布全城,甚至跨城的地铁相比,温哥华只有短短的几条地上铁线路(这里的地上铁,称为天车(skytrain)),搭乘公共交通出行相当不便,所以买车是必然的选择。\n不过想来也合理,毕竟加拿大地广人稀,如果要建设公共交通设施,覆盖面小了,受惠人群有限;覆盖面大,成本也大限上升,但人流量的上限也就在这里,投入产出比太低。\n像广州地铁的「死亡3号线」这种情况,是不可能在加国出现的,毕竟这里的自行车都能扛到天车里面去,可见人流量之稀疏。\n但因为我还没有加国的驾照,要通勤方便的话,只能选择住在天车站附近,通过11号车和天车换乘。\n5.1 高速 因为需要采购生活必备品,没有车着实不便,我们就租了个车,由有北美生活经验的舍友来开车,开到Burnaby 去采购。\n开着开着,导航就说顺着高速走,我很奇怪,问舍友,我们上高速了么?我怎么没有看到收费站。\n舍友科普到,这里的高速是没有收费站的,并且超速是不能使用摄像头抓拍的,是不算的。\n如果要抓超速,只能由警察现场拿测速仪「人赃俱获」,并开罚单。所以这里的高速限速90km/h, 但是大家都开得很快。\n话音刚落,就看到路边有位警察拿测速仪如雕塑般在太阳底下一动不动,现场教学,吓了我一跳,这位警察真的是辛苦了。\n我脑海里浮现的,就是《逃学威龙2》里面,星爷去当交通警察,拿着相机拍照的画面。难怪星爷要蹲点测速了:\n顺便感叹一下,北美的地是真的多,宜家在 Burnaby 的店,开得像个城堡一样大:\n5.2 过马路 我预想到会遇到很多状况,但是我没有预想到,过马路我都要学。\n加国马路上的绿灯亮的时候,相同方向斑马线的灯却不一定会绿起来。\n以致于我等了5分钟的灯,从路上的红灯等到绿灯,再转回红灯,都没有看到斑马线的灯绿起来,我还在想,这路口的灯是坏了嘛?\n直到后来有位行人加入等待的行列,按了信号灯上的一个装置,信号灯过了一会才绿了起来。\n个人猜想,是因为加国地广人稀,并不一定有那么多的人要过马路,为了提高车的通行速度,如果没有行人显式标识要过马路,就不需要提醒司机,有人要过马路,再提醒司机。\n6 钱 6.1 支付方式 在国内,已经习惯了不带钱包,只带手机出门。\n但在加国,除去部分华人超市与市场,基本看不到支付宝和微信支付这两家国内常见三方支付的身影,只能使用现金或信用卡,部分商家还支持Apple Pay\n刚开始时,就试过延续国内的习惯,出门吃饭没有带钱包,只能到店里后,又走回去拿钱包的情况。\n作为之前在微信支付的打工人,很自然地会去思考,为什么像支付宝/微信支付这样的三方支付没有在加国流行起来。\n观察下来,发现加国的POS机刷卡服务非常发达,无论是普通的快餐店,还是城里的商场,无一例外,都支持刷储蓄卡或者信用卡。\n消费者只需要拿卡贴近一下机器,滴一下,就能完成付款,体验比微信支付扫码还方便;如果是消费金额较大,就需要输入密码进行支付。\n除去pos刷卡体系完善外,像加拿大这样的北美国家,是建立在信用卡体系上的信用社会。前面提到的credit history 会和你的信用卡记录挂钩,也就是说,信用卡消费在加国不仅是一种支付方式,也是一种建立信用的途径。\n而信用卡,在国内,还只是一种超前消费的支付方式。\n对于有消费力,不需要透支的消费者而言,使用信用卡和储蓄卡并没有差别,信用卡积分也没有用处。\n而国内因为此前金融业发展落后于北美,pos 刷卡远没有北美普及,而通过微信支付和支付宝,相当于直接跳过了pos 机普及的过程,直接信息化。\n所以说微信支付和支付宝的发展是契合了中国的现状的弯道超车,但国外发达的pos 刷卡体系,也没有引进微信支付这样的三方支付方式的诉求,也难怪微信支付和支付宝在北美出海不顺利。\n6.2 货币 一个比较有趣的事情就是,加拿大现在还在使用硬币,硬币金额由大到小分别是:2元,1元,50分,25分,10分,5分。\n而除了2元外,其他的硬币都没有写数字,像我这样开始不熟悉硬币的话,要盯着硬币看很久,才能找到5 cents, 或者 25 cents的标识。\n硬币一般头像面是女王的头像,背面是动物。\n为什么还会有10分,5分的硬币呢,因为还有$1.89这样的标价,给现金的时候,就会给回你10分。\n只是让人相当不习惯的是,5分的硬币竟然是比10分大的。\n6.3 物价 温哥华的物价是真的高,据说2022年,加国的通货膨胀达到了8%, 而物价上涨的幅度可能还不止于此,让我这个之前挣人民币的感觉压力山大。\n和朋友提到物价这个事之后,他就说,你后面挣的是加元,那感觉就会好的啦。\n即使挣的是加元,感觉压力也大嘛。\n真的和深圳一样,「哪里挣钱哪里花,一分别想带回家」\n6.4 税 据说加拿大的福利很好,但是羊毛出自羊身上,福利都是从公民身上收税收过来的,毕竟政府不会产生价值。\n所以无论吃饭还是购物,小票上都可以看到加的税,购物需要交12%的税,分别是5%的联邦商品与消费税(federal Goods and Services Tax (GST)), 以及7% 的BC省销售税(Provincial Sales Tax (PST));吃饭要交5% 的税,可谓是雁过拔毛了。\n问题就来了,为什么在国内消费,消费者感觉不到加的税呢,是否不需要加税呢?\n正如我之前说的,「羊毛出自羊身上」,哪个国家都不例外。\n宜家在国内的增值税大概是17%, 只是国内大部分商家的票据,应有关部门要求,都不会把税率打印出来。具体的税率,要开增值发票的时候才会展示出来。\n只要不让羊知道羊毛出自羊身上就好。\n7 网 坦言之,加拿大的手机网络是真的又贵又垃圾,25G流量的套餐,需要50加元,而信号又一言难尽。\n在住处房间里面,即使是使用号称信号最强的Rogers 家的卡,也只有一格信号,在车库,就只能打紧急电话,信号完全没有了。\n在刚搬进去,还没有装wifi 的时候,每天都是戒网治疗,而戒断反应又相当剧烈。微信文字有时能发出来,有时发不出来;晚上发不出去,早上能发出去。\n要和家人或妹子语音聊天,只能到阳台外面去,温哥华的室外还只有不到10度,真的是一边聊天,一边在发抖;即使阳光信号也不稳定,有时延迟严重,说话靠喊才能听到。\n我在想,即使我去贵州的深山里面,信号还是满格的;即使20年前也不需要如此通信吧。\n移动,联通,电信,我应该向你们道个歉。\n与同行一对比,你们都打出了苹果vs诺基亚的表现了。\n8 天 总说国外的月亮更圆,晚上我盯着天空的月亮看,并不觉得更圆,但是脑海总是涌现苏轼的名句:\n但愿人长久,千里共婵娟\n可能要十万里才能共婵娟了。\n国外的月不见得更圆,但天着实更蓝:\n9 地 加拿大人与动物相处得很友好,路边随处可见各种动物与飞鸟。\n天上与窗外飞过的海鸥(看来以后吃薯条要当心了):\n车库溜进来的松鼠,看起来像大号的老鼠\n街上的兔子\n10 乱 因为加拿大可以合法吸大麻,所以在温哥华市区中心,有很多人吸大麻。\n初时,我还不知道那股刺鼻的味道是什么,直到舍友科普,我才知道,原来那是大麻。\n在大麻味道飘进鼻腔时,也能理解为什么加拿大被称为「加麻大」 .\n正如朋友所言,在国内吸二手烟,在这里吸二手麻。\n另外,Vancouver 市中心也真的多流浪汉,让农村来的我震惊不已,不过舍友说这比对美国,真的算小巫见大巫。\n有流浪汉的地方,治安自然不会太好;而downtown 商铺加装的铁闸门,也似乎在印证我的想法。\n之前,温哥华市长也来了一波「清理低端人口」的措施,不知道从哪里学来的。但是,这终究是治标不治本的手段,又能把人赶到哪里去呢。\n如果有工作,有住处,可能大部分人都不想当嬉皮士。\n只能说,没事多待在村里,离市中心远点来了。\n11 花 来到之后才知道,温哥华有非常多的樱花,可谓随处可见。\n4月的时候,樱花烂漫盛放,还有日本的樱花赏在温哥华公园举行。\n人站在樱花树下,着实有种误入「樱花源」,「落英缤纷」的感觉:\nblooming\n12 语 在落地之前一直担心语言不够用,落地之后,意识到我的语言是「够用」又不「够用」。\n「够用」是指日常生活,购物,工作交流基本都能应付过来,无障碍。\n而「不够用」是指难免会出现「词不达意」,或者「欲语还休」的情况,说明表达能力还不够用;\n此外,软件工程师的工程并不只是限于写代码,那是「硬技能」,还需要有对应的「软技能」,而沟通就是非常重要的「软技能」。\n如果技术生涯还想继续向上走,免不了要与更多人,更多团队交流,要扯皮,要对线,单纯地say yes 和 no 都不够用。\n语言可以说决定了技术生涯的天花板,所以现在每天还会去抽时间学习英语。\n现在可以说是「沉浸式学习」,有更多地学以致用的机会,相信坚持下来,效果会更佳。\n13 终 大半个月的时候,可以说看到了,体验到非常多不一样的事物与人文,但一切终究还是停留在走马观花,蜻蜓点水的程度。\n有朋友问我,这是我想要的么?\n我只能说,还不确定,初到时,人容易被好奇和新鲜被迷惑,这最终只能由时间来赋予,由时间来洗尽铅华。\n但有些理所当然的权利,在失而复得之后,却不会引起人的注意力,因为那是我们本就拥有的。\n去地铁站坐车,再也不需要大包小包过安检。\n打开网页,访问Google, 再也无需使用代理。\n过了许久之后,我才突然意识到,无论线上或线下,出行都不需要被「安检」了。\n关于这篇文章的一个小彩蛋:\n以「衣食住行」开篇,但写着写着,干脆把子标题都用一个字代替好了,因为这样看起来很有趣,就成这个样子了。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E6%8B%BF%E5%A4%A7%E4%B9%8B%E5%88%9D%E4%BD%93%E9%AA%8C/","summary":"1 起 落地加拿大已经大半个月了,也开始工作了,心情也从期待,紧张,忐忑,彷徨,兴奋到现在逐渐平静下来。 来到一个陌生的国家,使用不一样的货币,说","title":"加拿大之初体验"},{"content":"1 前言 从微信支付离职,我能带走什么?文档,代码,设计方案还是微信支付的漏洞?\n如果我带走这些资产,那我现在就在深圳的看守所里面吃着公家饭了。\n既然这些资产不能带走,那么我能带走什么?\n如果沉下心思考,就会发现,这些资产价值并不大,对于工程师而言,也没有领导想象中的那么重要,除非我们试图将代码放在黑市售卖。\n对于业务开发而言,也可能是同样的道理。业务开发每天对着业务需求做CRUD,可能会羡慕开发底层组件的工程师,可以学习并提升技术水平,而自己技术水平还是在原地打转,能学习到的东西随着时间的推移,越来越少。\n王安石的《游褒禅山记》有这样的感叹:\n夫夷以近,则遊者众;险以远,则至者少;而世之奇伟瑰怪非常之观,常在于险远,而人之所罕至焉;故非有志者,不能至也。\n所谓的「险以远」,并不特指深奥难懂的底层组件技术,也指思考的深度;\n如果多去思考技术和业务,挖掘背后的本质,我们也可以看到许多「世之奇伟瑰怪非常之观」\n1.1 鱼与渔 文档,代码,设计都是针对特定问题的解决方案,如果离职到新公司之后,我们遇到的问题肯定不会完全一样,或者手头可用的工具不一样,那么这些资产的价值就会打折扣。\n更何况这些资产都是「一次性的」,用完即止;是属于「授人以鱼不如授人以渔」中的「鱼」;是「生产线」上的「成品」,而我对能生产「成品」的「生产线」更感兴趣。\n二战结束以后,美国把1600多名德国科学家、工程师、技术人员带到美国,包括沃纳.冯.布劳恩和他的V-2火箭研究团队;\n而苏联凭借地理位置靠近德国占领了一些重要的工厂,比如著名的德国光学巨头卡尔蔡司公司,苏联几乎搬空了该公司的设备,把1万多台设备中的9000多台都搬到了苏联。\n有人对现成的「鱼」感兴趣,也有人对未来的「渔」感兴趣,我属于后者。\n2 思路 既然选择「渔」,那么,要怎么挑选适合的「渔」来丰富自己的「渔库」呢?\n两千多年前的老师孔子就已经给出自己的答案:\n见贤思齐焉,见不贤而内自省也\n见到那些优秀的实践和思路,就学下来;对于有弊端的实践,就要分析弊端形成的原因,再想办法避免和改进,别人掉进去的坑,我们就不要进去凑热闹了。\n3 贤 3.1 模式化 1994年,4个博士合著了一本书,书中对常见的设计问题进行了分类,归纳与总结,并且针对每一类问题,给出可重用的解决方案。他们将这些可以复用的解决方案,称之为设计模式(design pattern)。\n这本书也成为软件工程和面向对象设计经久不衰的经典。\n这本书即是《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software),这四位博士也被称为Gang of Four (GoF)\nA design pattern is the re-usable form of a solution to a design problem.\n那么什么是模式呢?\n按照另外一本经典名著《面向模式的软件架构卷一》的定义:\n当专家求解一个问题时,他们一般不会发明一种和已有解决方案完全不同的方案来处理这个问题。他们往往想起已解决过的相似的问题,并重用其解法的精华来解决新问题。\n在微信支付研发理念中,程序设计和开发,很多问题都是类似,或者是重复出现的。\n针对此类重复问题,直接复制代码来解决,是下下策。\n对代码进行抽象,复用代码来解决重复问题,也是下策。因为使用公共库会导致代码之间无法隔离,并且把逻辑隐藏在公共库,会导致无法分析代码的调用关系。\n微信支付研发理念推崇的上策是对问题进行抽象,归纳出这类问题的通用解法,即模式;更进一步的是,为模式定义对应的代码模板,直接生成代码。\n即使不生成代码,也可以将模式实现成对应的组件或库,方便直接调用。\n具体例子如:\n微信支付就总结常见的分布式事务场景,设计和开发了分布式事务编排中间件。通过在画板编排事务资源,即可生成对应的代码模板,开发者只需要在指定的地方编写个性化代码即可。\n针对常见的领域服务,抽象了基于状态机和事件驱动的模型,设计了领域服务的代码生成组件。可通过绘制状态机UML图,直接生成接口代码,由开发者填充实现。\n以上算是技术组件的模式化,对业务开发而言,还有对业务的模式化。\n比如对扣款模式进行抽象,扣款时开启事务,进行风控校验,创建(或不创建)业务单,查询支付方式,轮询支付方式进行扣款,异常关单等。\n当时组里的大神龙哥,就是对已有的扣款模式进行了抽象,基于面向对象,设计成同步扣款框架,定义了以上的接口,由业务进行继承和扩展。再使用同步扣款框架对已有的3个类似但不完全一致的代扣扣款业务进行了重构,把扣款模式都统一了。\n3.2 复盘 没有人能保证自己写的代码绝对不会出错,当错误与问题不期而至的时候,我们能做的就是将「错误」的效益最大化,即从「错误」中吸引教训,做到「不二过」。\n复盘,就是在「错误」中吸引教训,做到「不二过」的手段。Amazon 也有类似的概念与机制,称为 Correctness Of Error(COE)\n我们一直说「失败是成功之母」,但根据生物学常识,只有「成功才是成功之母」,或者说「小步的成功才是大步成功之母」,别人踩过的坑,我们就不要进去了。\n复盘的一般步骤:\n回顾目标 故障影响 时间精确到分钟(甚至秒级别)的过程回顾。比如是新需求写出一级故障的bug, 就从拿到需求,设计方案,开发,部署上线,流量灰度,问题告警,处理手段,到故障排除,每个时间点操作都写下来。 分析问题原因,挖掘导致故障的表面原因与根本原因 总结针对问题的改进措施。 落实改进措施 通过这样的复盘过程,确保同样的问题不会再次出现。\n这样的工作方式和理念,无论是对个人还是组织,才同样适用。\n3.3 持续学习 微信支付一直在推广全栈工程师,认为只从自己做的事情来思考问题,容易导致盲维和短板,看待问题的眼光容易受限。\n此外,根据《人月神话》的理念,工程师之间的沟通成本,会随着人数的增加,呈指数水平上涨。而成为全栈工程师,可以一个人处理完需求,沟通成本就下降到0,极大地提交工作效率。\n微信支付的全栈工程师定义是前端工程师 + 服务端工程师 + 数据开发工程师。\n当然,某一端的开发工程师,不会某天突然自己变成全栈工程师,这些都是需要持续学习的,人总是需要不断提升自己的。\n不能人为能给自己设限,把自己定义成「前端工程师」,「后端工程师」,或者「数据工程师」,应该是「工程师」。\n3.4 需求分析 每个工程师都需要做需求,与正确地做需求相比,做正确的需求显然更重要。\n如何确保做正确的需求呢?\n微信支付选择的方法论是:需求分析与业务建模,脱胎自UML专家潘家宇的著作《软件方法》。大概的流程是:\n寻找老大(需要满足谁的诉求) 寻找业务用例(业务执行者做什么事情,比如QQ音乐用户购买QQ音乐会员,就是一个业务用例) 根据业务用例,寻找系统用例。(例如商户发起扣款是一个系统用例;扣款成功回调通知商户也是一个系统用例) 将需求的业务规则,总结归纳成系统用例的规则。 当然,业务用例和系统用例这套东西,可能只有微信支付用。但找准客户,帮客户解决真正的痛点,创造真正的价值,这个是有普适性的。\n做需求时,可以多问这两个问题:\n谁是我们的客户。 我们在帮他们解决什么问题。 3.5 “云雨伞” “云雨伞”这个概念来自内部的一份PPT,讲述的是如何更好地向别人提出建议,内容大概是:\n屋外乌云密布,儿子要出门,妈妈对儿子说,马上要下雨,淋雨容易生病,把伞带上吧。\n“云雨伞”的步骤就是:\n指出现状:乌云密布,马上要下雨 导致的问题与影响:淋雨容易生病 提出措施和建议:把伞带上。 通过这样的表述方式,会比「把伞带上」这样直接命令的话,更容易让人接受。\n当然,如果阅读过《非暴力沟通》,会发现“云雨伞”的表述,其实是《非暴力沟通》总结的有效沟通方式的简化版本:\n清楚地表达观察结果 表达感受 说出是什么需求和原因导致了这样的感受 具体的请求 当然,总是强调「云雨伞」的做法,把问题归咎到提问者身上,我是不赞同的。\n领导经常说,提问题的时候,要把自己的解决方案也提出来,没有人喜欢听吐槽。\n话虽如此,但是我想起之间还在蚂蚁时,一位P10工程师的文章,《没有答案,也可以提问题》。\n提问题是为了帮助组织发现问题,如果不能吐槽的话,很多问题也不会被发现,自然也得不到解决,毕竟也没有人喜欢帮别人的问题提解决方案。\n\u0026lt;2023-05-20 六\u0026gt;\n针对如何高效交流,我写了一篇自己的心得文章:软件工程师的软技能指北(三):高效交流篇\n3.6 一致性 领导总说,软件工程的本质就是管理和控制复杂度,而一致性就是减少复杂度的有力工具。所谓的一致性,可以理解成统一的流程,统一的组件等等\n在这种理念的驱动下,微信支付内部使用统一的编程语言,统一的工具库,统一的存储组件(使用别的存储需要特殊审批和说明),统一的数据访问组件,使用统一的研发流程。\n保证每个研发工程师,即使调到微信支付的其他团队,也是使用同样的工具,即插即用,和车床生产的螺丝一样。\n开始我对这样的理念是持支持态度的,但到AWS以后,我的想法发生了动摇。\n因为我发现AWS的工具真的是琳琅满目,应有尽有,而Amazon也并未对使用什么样的组件作要求。\n反正AWS对各种组件的支持都很好,所以业务团队可以自行选择适合自身业务的任意组件,能完成需求就好。\n所以我现在不确定,通过追求一致性来降低复杂度这样的做法是否合理。\n3.7 设计优于实现 从2020年初起,微信支付内部的需求都需要先写设计文档,Leader 评审通过才能开发。\n设计时有个非常关键的点,就是列出所有能想到的可行方法,而后比较各个方案的优劣,再作出取舍,选择最终方案。\n软件工程没有银弹,系统/软件设计就是不断地在做取舍,当然,人生也是。\n设计才是最重要的,而编码和实现都是简单的,因为这只是水到渠成的事(我也不是说可以不用重视代码质量,毕竟这是吃饭的手艺)\n我个人觉得,对于业务开发(或者对于软件工程师)而言,不要过多花时间关注在编码上,而应该是花时间思考需求和问题,找到好的设计上。\n良好设计带来的红利,是要多于良好编码带来的红利的。\n如果把编码比作战术,设计就是战略,不要让战术的勤奋,掩盖了战略上的懒惰。\n编码算是建筑的外墙和玻璃,而设计就是承重墙和地基,毕竟换皮容易换根难。\n微信支付对于业务代码的态度是,能生成就尽量生成,就不要人写了,要多花些时间在设计上。\n4 总结 拿走「代码,文档」终究是术,学走「思想和理念」才是道。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%BB%8E%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E7%A6%BB%E7%BA%BF_%E6%88%91%E5%B8%A6%E8%B5%B0%E4%BA%86%E4%BB%80%E4%B9%88/","summary":"1 前言 从微信支付离职,我能带走什么?文档,代码,设计方案还是微信支付的漏洞? 如果我带走这些资产,那我现在就在深圳的看守所里面吃着公家饭了。 既","title":"那些年,我从微信支付学到的东西"},{"content":"1 前言 因为之前我发文总结了一些打工心得,提到我最终选择了去加拿大,有比较多朋友对此比较感兴趣(或者有疑问):为什么选择的是加拿大?而不是xx国。\n我写了这篇文章来总结下个人的分析和见解,以下纯属个人见解,每个人应该结合自身实际情况具体分析。若有疑问,建议进一步咨询中介。\n如无兴趣,博君一笑\n2 基本认知 2.1 为什么要出国 要想清楚为什么要出国?\n世界上没有天堂,跟你说存在天堂的,只会是骗你的。\n想清楚为什么而出国,才能建立好认知,这个事关你能否在国外能坚持下来。\n一切都要像婴儿一样,重新开始学习。\n切忌为了出国而出国,我就是想出去。\n冲动的情绪就像一阵风,来得快,去得也快,在挫折面前很容易就演变成沮丧;只有深思熟虑后的决定,才能经得起考验。\n2.2 小马过河 和世界上大多事情一样,出国这事也是「小马过河」。\n光听别人说,自己不去实践和调查,总是无法过这条「河」的。你听到的案例可能是来自「松鼠」或「老牛」,但你要清楚自己的定位,你可能是「马」。\n因为出国的途径和排列组合着实非常多,你能通过的,我不一定能通过,反之亦然。\n而你需要做的,就是在诸多的排列组合中,找出对你而言,成本最能接受,可行性最高的组合。\n2.3 身份 无论是想要去外国工作,旅游还是生活,「身份」是一切的前提。\n「身份」可以理解成别国给你发的准入许可证,如果没有「身份」,你就无法在这个国家定居或工作,除非你愿意当「黑户」。\n而签证就可以理解成是短期(十年以内)的准入证明,比如留学签证,旅游签证,工作签证。如果两个国家关系友好,那么两个国家公民在对方国家短暂(少于半年)的逗留,有可能就不需要签证,比如日本公民2023年可以访问190个国家或地区不需要签证,台湾有近145个免密国家或地区。\n绿卡, permanent resident(PR), 即永久居民。以前美国permanent resident的卡是绿色的所以叫绿卡,后面变成一种对PR的统称了。如果你拿了某国绿卡之后,你就可以永久逗留在某个国家了,但此时你还是原来国家的国籍。\n入籍,更换国籍,从法律上正式成为x国人,拥有x国护照。\n3 常见途径 3.1 读书 去某个国家读书,工作,然后申请绿卡定居下来,是大部分人出国定居的途径,也相对而言为稳妥的途径。\n难度:中等 成本:较高 风险:较低 成本包括时间成本和金钱成本。\n时间成本,如果你是读master, 你需要花费2-3年来完成学业;读书期间,需要学费和生活费,这个就是金钱成本。如果你本来已经在工作,后面去读书,那么在此期间损失的收入也算是金钱成本的一部分。\n一般而言,如果留学生毕业后能找到支持工作签证的工作,那么就可以拿到工作签证,继续留在这个国家,然后排期等绿卡。\n当然,那只是一般而言。如果你要去美国这样的热门国家读书,找到工作之后也不是直接给你发工作签证H1B的,因为僧多粥少,需要抽签。\n近些年来,H1B 中签率逐年下降,现在大概在20%, 理工科学生毕业后最多能参加3次H1B抽签。\n3.2 工作 如果你已经在工作,不想花成本读书,那么直接申请某个国家的工作,也是一个可行的路径:\n难度:较高 成本:低 风险:低 但是你就需要研究你能否胜任某个国家的工作,并且雇主能否帮你解决工作签证问题。\n像清洁,外卖这些体力劳动,大部分成年人都能胜任,但是他们的雇主大多无能力(意愿)帮你解决工作签证问题。\n另外,也需要考虑你的心仪国家的签证体系,是否对国外务工者足够友好。\n以美国举例,除非是杰出人才,不然想直接从外国去美国打工,基本没戏。杰出人才的标准大概是博士学历,发了一堆的顶会论文,有客观指标和数据来证明你足够「杰出」。\n相对而言,加拿大,日本,新加坡,欧洲国家基本都可以申请工作签证,但各有各的门槛。\n比如加拿大,从国外直接招人,需要先申请LMIA(Labour Market Impact Assessment),相当繁琐和复杂,就是说明为什么这个人要从国外招,不优先考虑我们加拿大国内的劳动力,避免过多的外来劳工冲击本地劳动力市场,当时律所帮忙,整LMIA都花了2-3个月。\n新加坡还有个 EP工签,满足一定的薪水条件即可;日本也同理,程序员能面上日本的公司,基本能申请到签证。\n这个途径主要就和心仪国家以及是自身能力相关,基本没有什么成本,风险也低。\n3.2.1 内部转岗 这个算是求职的分支途径,对于跨国大公司,可能在世界各地都有分部。那自然就有人会想,我能否先面试到中国的分部公司,然后再内部转岗到心仪的国家所在的部门呢。\n难度:中等 成本:低 风险:低 心理压力:max 真的是个小机灵鬼。\n但是这个主要是和公司策略以及是目标国家签证体制相关。\n再以大家关注的美国为例,这种内部转岗到美国需要的签证是L1 签证,分为L1A 和L1B.\nL1A是发给高管的,有效期七年;L1B是发给普通打工人的,有效期五年。在座的可能都还是打工人,所以我们就来看下L1B。\n那L1B和H1B的差别是什么呢?H1B 可以跳槽,L1B不能跳槽。\n也就是在你拿L1B 签证期间,需要一直为这家公司打工,如果中途被裁,那就只能回国了。\n因为L1B这样被人拿捏,所以L1B 一般都是拿low ball,就不要想着拿高薪大包了。\n当然L1B 也可以排队绿卡和抽H1B,只是看看留学生H1B 的中签率,就能想象到没有L1B 抽H1B 中签率了。\n所以L1 签证需要在较长时间里,承受非常大的心理压力。\n3.3 结婚 通过和公民或者绿卡持有者结婚获得移民资格,路径非常简单,成功率与个体强相关。\n难度:因人而异 成本:低 风险:很低 不过多展开\n3.4 投资 某些国家,可以通过投资一定的钱,获得工作签证或绿卡。因为我没有这样的实力,所以完全没有了解过。\n难度:因人而异 成本:高 风险:低 3.5 曲线/非正当途径 了解到的,不建议途径:\n政治庇护 「走线」,非法入境,然后黑下来; 曲线途径,在心仪国家产子,「父凭子贵」。\n无论去哪,都是为了更好地生活,不要为了润而润。\n4 国家分析 4.1 常见选择 妈妈常和我说,「人往高处走」。对于我而言,既然是出国是为了更好地生活,那选择自然是发达国家。\n美国,加拿大,日本,澳大利亚,英国,德国,荷兰,法国,新西兰,新加坡等等。\n4.2 见解与分析 4.2.1 美国 难怪很多人都想去美国,毕竟我们的教科书上也说,美国是世界上唯一的超级大国。\n美国工作机会多,工作薪资高,税收较低(相对于列表中的其他国家),挣到钱才能更好地生活。\n对于计算机相关行业从业者来说,美国就是最好的工作地。\n也因为持有这种想法的人非常多,导致去美国的难度较高,而常见的出国途径也只有这几种,详见前文分析。\n4.2.2 欧陆国家:德国,荷兰,英国,法国 德国,荷兰和英国都是欧洲大陆的国家,因此可以把他们都放在同一类型里面。\n欧陆国家的特点就是生活非常非常躺,会有各种的福利和假期,如果不想卷,在欧陆国家生活会是一个很不错的选择。\n就计算机行业而言,欧陆国家算不温不火,美国的企业也在欧洲设有分部。\n但是天底没有免费的午餐,这些福利都是来自于纳税人的税收,福利越多,税收自然越重(反过来却不一定成立,某些国家税收非常重,但是基本无福利)\n并且,这种普遍吃大锅饭的氛围,也不卷,也就导致欧陆的薪资不高(相对美国而言)。\n年薪10万欧元已经是比较高的薪资,但可能要交1/3 - 1/2的税。\n还有一个问题,就是如果想申请这些欧陆国家的绿卡,除了英国外,基本都需要学习第二门外语,德语,荷兰语,法语等等。\n并且,华人在欧陆的数量也不多。\n个人主观感觉,英国工作岗位没有那么多,德国和荷兰比较缺IT的劳动力,我在Linkedin 更新简历后,有比较多的德国和荷兰的recruiter 和猎头找过来。\n做高频交易的:\n4.2.3 日本 虽然因为历史和文化的原因,很多朋友情感上对日本持否定态度,但无可否认的是,日本是地理位置距离中国最近的几个老牌发达国家。\n日本可能是对程序员而言,最容易来的发达国家之一(可能没有之一),对学历要求低(大专以上),对年龄也没有要求。\n签证比五眼等国家的好拿,而且对人的要求也很低,并不需要你的日语有多么溜,只要能正常交流,把工作做出来,来日本还是很容易的。\n对于二次元爱好者来说,日本来谓是圣地。\n虽说身处东亚的日本也卷,但那是相对西方发达国家而言的。\n前段时间看到个新闻,说因为日本的低生育率,政府都要严格禁止企业加班了,对于习惯了996的中国程序员而言,日本可以说是很佛系了。\n日本很多公司实行的是终生雇佣制,也就是意味着,公司很难开除你。\n同是黄种人,在外貌上与日本人几无差异,生活习惯也类似,当然除非口语能练习得与日本人一样好,不然开口就有差别了。\n距离中国的距离也近,从东京飞到中国的最南边香港,也只需要4个小时。\n但平心而论,日本的IT业并不发达,甚至可以说比较落后。\n在日本最top 的薪资应该是日本Google,5年以上的工程师大概能开出2000 千万日元的薪资;次top的就是日本亚麻,Indeed,PayPay 5年以上的工程师大概能开出1000-1500千万日元的薪资,所以日本的薪资在国内是没有竞争力的。\n日经中文网有这样一条新闻,细看下来非常能反应现状:\n但因为长年累积下来的卷文化,东亚三国的生育率都逐年下降,未来社会可能缺乏活力。\n在日本,想要拿PR,需要在日本居住10年,期间不能有任何犯罪记录,不能有失信行为,要遵守公序良俗做一个守法的移民。\n但通过高度人才签证,理论上最快一年就能拿到永久。\n高度人才签证是2017年推出的新政策,一定程度上说明了日本人才的紧缺,该签证采取的是打分制。\n在日本工作生活三年或者一年即可申请,其中70分-79分者原则上是3年,80分以上则只需要理论上的一年就可以拿到永驻资格。\n我参照打分表,给自己估了一下分,可以去到80分以上。\n4.2.4 新加坡 新加坡有非常多国内公司的分部或者总部,比如Shopee, 字节跳动,Tiktok;也有非常多跨国公司的亚太总部放在新加坡,也有非常多聚居的华人,不会有陌生和疏离之感。\n所以对于很多人来说,新加坡是出国的首选,无论是内部转岗或者是直接申请新加坡的公司;部分新加坡公司甚至可以使用中文来面试。\n此外,根据美国-新加坡自由贸易协定,新加坡的公民(绿卡持有者不行)可以申请H1B1工作签证去美国工作。\n但对我来说,新加坡这个选项,很快被我排除掉了,原因如下:\n新加坡国土面积太小,俗称坡县。国土面积小,可容纳公民少,缺乏战略纵深,容易受地缘政治影响。近些年因为大量移民进入,物价与房租飞涨,说明不堪重负了。 新加坡也很卷,因为大量中国公司和移民的涌入,导致新加坡也卷了起来。如果选择继续卷,何必出国再卷呢。 新加坡的PR不好拿,理论上新加坡的EP工签两年内就可以申请绿卡,但是据说绿卡很玄学。 对我而言,新加坡是个面积缩小,难度强化版本的海外深圳。\n4.2.5 澳大利亚,新西兰 澳大利亚和新西兰合并在一起了,都在南半球。\nIT行业比较一般,本土公司是Atlassian,Canva,国际公司在澳大利亚都有分部,如Google, Amazon 这些,高级工程师大概能给到15-20W澳元,工资对比国内没有明显优势。\n环境优美,工作也不卷,对新移民友好。\n因为身处在南半球岛国上,即使因为地缘政治,出现战争也难涉及这两个岛国。\n4.2.6 加拿大 加拿大有非常多的华人,华人社区非常多,在温哥华的Richmond 地区,街上商铺的招牌有许多使用的都是中英双语,听着街上的粤语,甚至有种在香港的感觉。\nIT业还可以,大部分的知名美国公司在加拿大有分部,例如Google, Meta, Amazon, Microsoft 等等。\n拿到工签落地之后,考出符合要求的语言成绩,就可以申请绿卡。\n根据北美自由贸易协定,美国给予加拿大和墨西哥公民的非移民工作签证(TN签证)。\n所以入籍加拿大之后,可以申请TN签证南下美国打工。\n加拿大可能是对新移民最友好和宽松的国家之一。\n加拿大的Express Entry 项目,支持在加拿大境外申请加拿大的绿卡,会根据你的经历,学历,语言成绩进行打分,入池排队,分数高的就可以直接获得加拿大的绿卡。\n但加拿大也有许多不足之处,冷,税收高,工资低。\n另外,加拿大三面环大洋,南面是盟国美国,所以除非是外星人入侵,不然战争是没有可能波及加拿大的。\n4.2.7 香港,台湾 香港IT行业就业机会较少,互联网很少,大多是交易或者投行公司。\n香港也不是个适居的地方,物价高,房价尤其高,和新加坡一样。\n随着中国经济的发展以及《国安法》的实施,香港和深圳的差距进一步缩小。\n通过优才计划,要7年才能拿到香港居民身份证。\n对于非广东人来说,香港的官方语言粤语一样算外语。\n台湾很好,经济发达,免费医疗和教育,同根同源,都不需要适应期。\n但中国大陆公民没有身份可以去台湾。\n签证是邦交两国之间的准入身份,台湾与中国大陆肯定不会是邦交国关系。\n4.2.8 总结 个人向:\n5 申请国外工作流程 如何将一头大象放去冰箱:\n打开冰箱门 将大象放进去 关闭冰箱门 程序员如何申请国外的工作:\n在Leetcode (非大陆版本)上面刷题,基本所有的公司都需要解算法题。这个就是游戏规则,你喜欢或者不喜欢,规则都不会改变 学习并准备 System Design 的知识 使用 Linkedin (非大陆版本),将个人信息更新成英文,撰写英文简历,选择心仪国家和公司进行投递;或者等猎头和recruiter 找上门。 在一亩三分地(https://www.1point3acres.com/) 上查看面经 面试 拿 Offer 具体每一步要如何展开,每个人都会不一样,无法一概而论。\n6 总结 种一棵树最好的时间是十年前,其次是现在。\n无论是去哪个国家,学会外语是第一要务,这个决定了你能否通过别国公司的面试,以及能否正常地在外国生活。\n无论你的外语水平什么样,无论是什么语言,英语也罢,日语也罢,现在开始学习都不会迟,因为它决定你的职业上限。\n凡事预则立,不预则废。无论要做什么,都需要提前准备。出国也罢,在国内也罢,都需要事先做好准备。\n自学能力,无论什么时候,都需要学习,固步自封不会有任何的改变。在一个新的环境里,你的知识储备随时都可能不够用。\n信息检索与分析能力,很多解决方案和知识就在哪里,如果不会检索和分析,你就一直待在井里,观着天。\n不会有人随时,免费,耐心地给你解答问题的,Google 和 ChatGPT 除外。在询问别人前,自己先找下答案。\n勇气,人类的赞歌就是勇气的赞歌,没有勇气,想法就永远不会变成现实。\n7 参考 润学:如何寻找适合自己的方案 润学:日本攻略 日本IT人才短缺,收入低於平均工資 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%B6%A6%E5%90%91%E4%BD%95%E6%96%B9_%E4%B8%8D%E5%AE%8C%E5%85%A8%E8%82%89%E8%BA%AB%E7%BF%BB%E5%A2%99%E6%8C%87%E5%8C%97/","summary":"1 前言 因为之前我发文总结了一些打工心得,提到我最终选择了去加拿大,有比较多朋友对此比较感兴趣(或者有疑问):为什么选择的是加拿大?而不是xx","title":"润向何方:不完全肉身翻墙指北"},{"content":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一段旅途;\n失大于得,那就说明这段时间是混过去了,唯有过后空叹,无可奈何花落去。\n但无论是失大于得,还是得大于失,已经过去的就注定成为历史,无法挽回,人终究只能是向前看。\n谨以本文,纪念我至今的打工生涯。\n2 广州 2.1 自学 虽然当初没有去到我想去的大学,但我也不想虚度大学四年时光。\n我对自己的大学生涯有很高的要求,我上的学校可能并不如其他人,但是我大学要学会的东西并不能比其他人差。\n大一计算机导论的老师对我们说,国内很多教材编写得可能并没有那么好,最好还是看国外的经典教材。\n因此,我把教材大多换成了国外的教材,并通过配套的网课进行自学\n在大二的时候,把计算机相关的课程,例如计算机网络,数据库,数据结构,算法,C语言与Java语言等都自学完了。\n2.2 大二第一份 Offer 并开始和同学组队写程序,我负责用Java写后端,参加各种比赛。\n当时和同学模仿「超级课程表」这个APP,写了一个我们学校版本的仿制品,称为「眸知(MooApp)」.\n因为我们学校最有标识的神兽是:动物科学院放养的一群黄牛,而黄牛叫声类似Moo,因此就起了这样的一个名字(粤语发音就类似:无知,就什么也不知道)\n做完「眸知」这个App之后,就开始一鱼多吃,把它投到所有能参加的比赛,并在其中一个叫「穗港IT应用系统开发大赛」获得了三等奖。\n颁奖典礼上邀请了加州州立大学的教授刘颖作程序开发经验分享,在分享过程中,刘颖先生提了一些问题作为互动,大多问题我都举手作答。\n会后,刘颖教授与我交流,询问我是否有意愿去他在深圳的创业公司(6滴科技有限公司)实习,怀揣着忐忑的心情,我表达了同意。\n就这样,我在大二暑假,在无需面试的情况,我拿到了人生的第一份实习Offer。(Offer来得太容易,当时同学还担心我去深圳进传销窝了)\n实习期间,我主要是参与电商系统DevOps 功能的开发,主要是通过Docker 来做CI/CD。\n但现在回想,当时并没有什么产出,大部分时间都在学习各种文档和概念(此前对Docker, CI, CD等根本没有认知),在老刘的指导下编写 Shell脚本。\n可能是我学习态度还算勤勉,工作尚且认真,在实习期结束,我也拿到了公司的 Return Offer。\n暑假结束,大三开始了。我用实习工资,给自己交了大三的学费。\n2.3 大三实习 Offer 大三要上的主要是专业选修课,大三下学期,我选的课是「面向对象分析与设计」,并且我此前已经自学过这门课了。\n就尝试和授课老师曾玲(我们同学口中的「老奶奶」,是个专业水平非常高,并且人非常好的老师)商量,我已经自学完这门课了,可否申请不来上课,按照提交作业和考试,我继续去深圳实习。\n令人惊讶的是,老奶奶竟然答应了我这个要求,于是我又回去了深圳实习,过上了边打工边上课的生活。\n(再回首,现在会觉得当初自己没有好好学习,光顾着打工,却不知未来打工之路漫长无期;但是没法用现在的标准去要求过去的自己,大二时家里生了变故,我希望自己能解决学费和生活费,我需要收入。)\n这次参与的是电商平台的后端开发,我们当时的代码都是开源项目,所以还能在Github 上找到源码\n虽然我拿到了Return Offer, 但我感觉当时在做的业务并没有太多前景,无论是电商平台还是跨境电商,都已经是一片红海;\n开发流程和开发工具也相对简陋,并且我想去BAT大厂见识下,于是我开始准备大三暑假的实习。\n当时很天真,因为我只会Java, BAT 里面只有Alibaba 是用 Java 的,其余两家用的是 C++,所以我就只投了阿里。\n而在选择事业群的时候,我也不知道阿里云,菜鸟是干什么的,所以就投了我用过的淘宝和支付宝。\n简历被支付宝的面试官捞起了,凭着这些年的实习打工和各种开发工具,开发框架折腾经验,通过了三轮面试,顺利拿到了支付宝的实习Offer,而要去的部门是芝麻信用。\n当时自我感觉还不错,毕竟我知道这个部门。\n3 杭州 3.1 芝麻信用 当时支付宝的总部还在黄龙时代,距离新总部Z空间建成还有1年多的时间。\n在新人培训结束的那个午后,我戴着实习生的工牌,迎着风走出公司大门。\n回头看去,那个印着支付宝的Logo的大楼赫然立在身后,我面带微笑,感觉前景充满了希望。\n但我当时还没有考虑Return Offer的事,还以为一切都是水到渠成的。\n在实习期的两个月里面,一边学习蚂蚁的各种中间件和Sofa框架,一边跟着导师尝试做需求。但却没有做出成果,以此证明自己有留用能力的紧迫感。\n每天都是开心地过着,甚至导师还会用他的内网权限,带着年轻的我,一起看内网阿里味的相亲帖子。\n直到留用面试的到来,其他的实习生同学都在紧张地准备,还拉上同事帮忙模拟面。\n与密锣紧鼓准备的其他同学相比,我却还在自我感觉良好。因为没有找其他同事模拟面试,我甚至都不知道终面面试会问什么。\n以现在的眼光来讲,终面面试,我发挥得是一塌糊涂,完全是答非所问,也没有结合业务和自己实习做的事情。所以,我理所当然地没有拿到Return Offer.\n而更为糟糕的是,因为我是迷之自信,以为自己可以拿到Return Offer, 就没有去准备秋招面试。\n所以到实习结束,我是0 Offer在手,并且秋招已经结束,即使拿着在蚂蚁金服的实习经验,投递的简历也石沉大海。\n我就这样回了广州,时间又来到了大四。\n4 广州 我在懊悔和自责中继续投递着简历,但是依旧音讯全无。\n我甚至去参加了腾讯的霸面,但是在酒店枯坐了一下午,也没有等来任何的面试机会。\n我不禁焦急了起来,然后开始临时抱佛脚地学习起了C++.\n这个时候,师兄帮我找了个机会,把我的简历发到了他在UC的部门群里面,由此我获得了一次面试实习的机会,表现达标的话,可以留用。\n我相当珍惜这次的机会,在面试前做了很多准备。\n这次只有一轮面试,面试官是中心的总监,在紧张和不安中,我基本回答上了面试官的问题,面试官让我回去等消息。\n不久后,我收到了HR的电话,通知我面试通过了,但是现在秋招已经结束了,已经没有实习生的HC了。\n但他们给我提供了一个选项,以合作伙伴(即外包)的身份入职工作半年,再视表现决定是否录用。\n虽然知道我可能被白嫖,但是我别无选择,只能努力向前,争取留用。\n4.1 UC 我就在UC 开始了自己的外包(实习生)之旅,邮箱也从之前的 gongsun@alipay.com 变成了 wb-lzr345319@alibaba-inc.com, 但平心而论,团队Leader 和导师真的是在用心指导和培养我。\n因为有了在芝麻信用的翻车之鉴,我也格外珍惜这次的机会,所以花了很多心思和精力学习业务和参与开发。\n与之前在芝麻只写运营系统代码不同,我这次写的代码是真的运行在生产系统上。\n当时在UC 做的是业务存储系统,提供一个通用的存储模型供其他业务使用。\n使用方定义模型,直接把数据存储在我们的系统里面,我们提供通用的数据访问接口,有点类似内部的PAAS 平台。\n有趣的是,当时的团队是使用 HBase 来做实时系统的存储的,并使用Mysql 作备份,通过消息队列在两套存储系统之间作数据同步,然后再从 Mysql 拉取数据,写入到 ElasticSearch, 通过ES 提供各种个性化的查询接口。\n使用 Hbase 主要是看上了它的水平扩展能力,非常易于扩展。\n但 Hbase 此前主要是配合Hadoop 作离线计算,UC内部没有其他团队有类似的实践,所以有很多问题需要解决。\n比如FGC降低系统吞吐量,负载均衡切换到其他的节点,在大流量情况下造成雪崩,把所有节点打挂,导致系统不可用的问题,就需要针对GC 参数作调优,减少GC Stop The World 的影响。\n我从这个系统中学习到非常多系统设计和热点调优的知识,上手实操又加深了我对这些开源中间件的认知, 初窥了系统设计的门道。\n虽说此前我连 Hadoop 和 ElasticSearch 是什么都不知道。\n半年之期很快就到了,因为之前的翻车状况太过惨烈,这次的留用面试,我提前两周就开始准备PPT,并和导师总结实习结果和收获。\n凭借半年的实习期的表现,以及最后的面试发挥,我通过留用面试,我把这个Offer 拿到手了。\n但是,我没有选择这个Offer。\n4.2 再话蚂蚁金服 在UC 工作4个多月后,我在v2ex 上面看到一个帖子,说蚂蚁这个团队秋招还有名额,欢迎应届生投递。\n因为在UC 留用的事情不确定,本着多个Offer 多份保障的心思,我尝试着投递了简历,很快就收到面试电话邀约。\n还是熟悉的三轮面试,但是可以深切感受到,校招的三轮面试,难度要远大于实习的面试。\n终面时,面试官(我的未来二级主管)甚至问到数据库怎么水平扩容,要怎么分库分表,事务如何保障等等。\n如果没有在UC 实习的这段经历,我可能真的没法办法回答得上,这些都不是看看面经就能回答上的问题。我感觉也超出了校招对应届生的要求,就这样被「拷问」了接近一个半小时,远超出正常的面试时间。\n经过这三轮面试之后,我终于收到了第一份校招 Offer,兜兜转转,又拿到了蚂蚁金服的Offer 了。可谓「山穷水尽疑无路,柳暗花明又一村」:\n4.3 抉择 所以,我拿到了两份校招Offer,两份来自「阿里」的 Offer 。\n最后,在权衡发展前景,技术成长和个人成长等因素,我选择了蚂蚁金服的Offer, 就这样,我以不一样的途径,又回到了蚂蚁这个最开始的地方。\n这次去的部门是网商银行。\n如果从现在的眼光来看,实习时没有在芝麻信用留用,不见得是件坏事。\n因为芝麻信用用户虽然多,但是没有找到业务发展的突破点和营收方向。在集团层面,已经连续几次被打业绩差了。(阿里人熟知的3.25)\n5 杭州 5.1 近卫军 2018年,一群来自天南海北的应届生来到了蚂蚁金服,公司开展了为期1个月的脱产培训,名为「青年近卫军」培训,我也认识了一群好朋友。\n在这一个月的培训里面,上午来自不同部门的专家对我们进行组件和技术的培训,下午我们组队开发项目 mini-alipay 项目,然后为了赶进展,开始体验到传说中的996的工作节奏。\n最后我们成功完成了自己的一个mini-alipay 的Android App, 并且凭借这个项目,收获到一篮子的奖项。\n我很自然地会以为,我「重生」的蚂蚁之旅,也会是这样顺利。\n5.2 客户域 我当时任职的团队是网商银行的客户域,负责处理网商银行所有的用户与商户信息,算是基础团队。\n客户域非常值得称道的是,使用的是蚂蚁集团内部总结的金融数据模型「飞马模型」进行重构的,对数据模型进行了清晰的划分,可以称之为标杆。\n又因为客户域属于整个网商银行的底层服务,被非常多的服务所依赖,所以系统设计和空灾就要做得非常扎实,我也因此受益匪浅。\n在客户域待了八个月后,有一天导师来和我们说,客户域的业务要移交给北京的团队;虽然知道阿里的文化有「拥抱变化」,只是未曾想,变化来得如此之快。\n5.3 聚合收单 客户域的业务移交后,原团队的同事因为没有业务可干,分别被分流到其他团队。我来到了聚合收单团队;\n所谓的聚合收单,即所谓的四方支付,在微信支付和支付宝支付外,再增加一层代理商的角色,为直连商户或服务商接入微信支付和支付宝。\n那商户不能自己接入微信支付和支付宝么?当然可以,聚合支付只是可以帮你同时接入这两家。\n这也是这个业务的问题所在,只能作为通道存在,不具有任何的门槛和粘性,商户可以随时切走。\n其兴也勃焉 其亡也忽焉。\n聚合收单巅峰时,曾代理微信支付10%的交易量;但在微信支付发现这种代理行为,并进行打击之后,业务量急据萎缩,聚合收单团队又面临解散。\n在聚合收单待了10个月之后,我又无事可干了。\n5.4 金融网络 这一次,我和老板详谈,希望可以到个稳定的团队,可以踏实地工作。而老板手下能满足我要求的就是另外一个团队:金融网络。\n对于网商银行,或者支付宝,微信支付等三方支付而言,必须要和其他的银行打交道,通过指令进行扣款/扣款。\n因此就需要与每个银行进行对接,这个就是金融网络团队的工作。\n或许会有人问,不能接入一个统一的代理中继,这样就不需要几百个银行,每个都对接一次了。这个中继是存在的,就是网联。\n但是多一个中继,就需要多一份成本,人家又不可能给你白干,需要收手续费的。所以为了降低成本,也需要分别对接不同的银行。\n网商银行的金融网络是从支付宝fork 过来的,不同的是,支付宝有100多号人的团队维护,网商银行的金融网络团队,加上我也不过8个人。\n金融网络维护的系统,庞大,灵活且复杂。很多功能,复杂到都没有人能说清它是怎么工作的,也没有文档或者资料,一切都靠口口相传。\n因为金融网络复杂又重要,被整个网商银行所依赖,就导致金融网络很容易出故障。\n在这样的环境里面,我又坚持了半年,感觉着实看不到什么前景和机会。\n频繁的业务变更,两年时间,经历了3个团队,兼之晋升和绩效的问题,导致我心生去意。\n5.5 加班与学习 杭州是996之都,而阿里可以说是996的发源地。因此,在蚂蚁金服,想正常上下班基本是种奢望。\n我很敬仰的一位博主随想君对996工作制的认知是,996工作制只不过「劫贫济富」的缩影。\n996工作制对工程师职业生涯的影响非常不利,主要是:\n压缩了员工的业余时间,因此减少了员工的自学时间,你更加没有时间去自学,去提升自己的能力;如果能力得不到提升,你在人力市场中的「议价能力/谈判筹码」也就得不到提升;然后只能继续接受这种变态的工作时间,这是个恶性循环。身陷其中,并越来越无法自拔 消耗了员工的自控力,也就减少了自学的「动力」:如果你的工作不是你的兴趣所在,长时间加班之后,回到家里,你很难再有动力去学习其它新技能。 对健康的负面影响 对家庭的负面影响 如何走出996的怪圈呢?关键在于时间与坚持。\n每天挤出的时间不需要很多,哪怕半小时到一小时,足矣。这里的关键在于「坚持」。\n如果你能坚持每天挤出“半小时到一小时”用来自学,大约1到2年时间,就会有效果——你的能力就会有提升\n提升自己的能力,是摆脱这个怪圈的第一步。\n蚂蚁的工作强度虽然大,但只是995,又因为我住在公司旁边,所以省去了通勤的时间,不玩游戏,又省下不少时间。\n每天晚上回去,花一个小时看书和学习;周末和近卫军的小伙伴韬然一起去学习半天到一天,然后另外一天去踢球。\n就这样,我每年大概看完了20本书,专业书看得比较慢,花了1年多的时间,学习了C++,算是入了门。\n英语是不能放下的,听,说,读,写;除了说的机会不大,读和写都尽量保持着,使用英文进行搜索,阅读英文文章;使用英语回复Github 和Stackoverflow 的问题。\n在2020年的时候,又开始自学日语。学好语言,机会总会多些的。\n5.6 面试 在决定离开之后,又开始了面试之路。\n5.6.1 腾讯 因为好朋友在腾讯是做计费和结算系统的,然后就把我简历推给到他们部门,就这样开始了腾讯的面试之旅。\n一面很顺利,在马路边一边散步一边电话面试,问题都不难。\n二面总监面也还不错,问到的系统设计问题以及取舍,组件选型等问题我都能回答上来。\n本来面试就差不多结束了,从电话那头,总监听起来也还挺满意的,最后问了我一个问题,我是怎么看待加班的。我就把我的观点和对996的看法如实告知了总监,感觉电话那头的面试官陷入了沉默,面试就这样结束了。\n然后,我二面就挂了,我不知道是否因为我太坦诚。或者我应该说不排斥加班,就能结束这个话题了。\n5.6.2 微软 因为不想加班,所以就尝试外企,就找微软的朋友内推了简历,便有了人生第一次的外企面试经历。\n外企基本不考察项目经历和计算机原理(即所谓的八股文),基本只看解算法题,而我当时在leetcode 上也就解决了不到200道题。\n当时令我惊讶的是无法约上他们面试官的时候,我希望是可以中午面试,HR反馈员工中午休息,不面试。我打算是5点半之后面试,HR反馈大家下班了,不会进行面试。外企都这么早下班的么?这是我们这种996打工人无法想象的事情。\n我都打算是请假面试了,最后是微软的面试官进行妥协,回家之后来面试我。面试时候,我甚至可以听到面试官孩子在旁边玩耍的笑声。\n面试官问了3道算法题,我只做出来了一题半,那半题是使用暴力解法解出来的,时间复杂度基本没法看。\n剩下的时间就和面试官相互沉默与尴尬,即使面试官给我提示,我也没有思路做出来。\n解题的确是需要训练的,这一面自然是面试失败了。\n当时可以说是相当沮丧。\n5.6.3 微信支付 这样又过去了一个多月。\n好朋友给我推荐了微信支付的岗位,说是有个师兄在学校的群里发的。\n我就尝试投了一下简历,这连串的面试失败让我对自己没有什么信心,何况这还是微信支付。\n微信支付一面的面试官面试内容比较有广度,从工程实践,面向对象设计,设计模式问到了分布式系统算法。\n最后的20分钟又上一道算法题,我解出来之后,又追问我怎么证明我是对的。我只能当场写几个test case 来断言一下,只能说我的解法能覆盖到这些case。\n然后一面就通过了。\n新奇的是,在通过一面之后,一面面试官询问我是否愿意做一道笔试题。其实这个也不算征询我的意见,如果想继续面试的话,笔试题只能做。\n只是这道笔试题,需要两周的时间才能完成,也就是我拿到了一个完整的需求,要求2周内完成:依照微信客户端,实现微信支付委托代扣服务列表和服务详情查询。\n面试的时候说语言不限,现在又要求我使用C++ 和grpc 完成,说考察我的学习能力,还好我都学过。\n但我就没见过这种面试要求,可能微信支付比较牛吧,我只能这么安慰自己。\n就唯有白天和晚上上班,下班后加班到凌晨来做这个笔试题。\n花了两周时间,撰写了设计文档,使用C++17写完了这个需求,并附上完整的测试case,得到的反馈是还不错。\n就这样,推进到第三面(如果笔试题算二面的话)三面面试官问题都非常有深度,但都是从浅入深,针对我给出的答案进行发问,没有实际的工程经验和思考,只靠面经是无法水过去的。\n后来就是面委面,不过因为我的级别不到高级工程师(9级及以上),所以只是微信支付内部的面委。\n因为我的C++ 不够扎实,担心面试官问我C++, 面试前又恶补了一波;\n万万没想到,面试官都是在问我Java,还有相当宽泛的问题,HTTPS是怎么实现的?\n我都不知道这是否是压力面试,我回答什么,对面都不给反馈,就这么听着,让我觉得面试体验非常差,但最终都过了。\n然后就到了HR面,不是说后面还有面试么?为什么要先来HR面?\nHR面通过后,来到GM面,即所谓的总经理面,面试前,被要求用一周时间,针对笔试题,做一个述职PPT,并给了我述职大纲。\n这都是些什么面试要求,还要画PPT?\n只能按照要求,晚上回去埋头写PPT。GM面使用30分钟给GM讲完PPT,回答了几个面试官的问题,然后就结束了;后面就被通知通过了。\n我还以为GM面是走个过场,后来才知道有非常多的面试者GM 面被GM问得体无完肤,因为 GM 想要既会做的,又会说的。\n只说不做的假把式和只做不说的傻把式都不要。\n就这样又到了HR面,怎么要面试两次HR,比阿里的HR面还要多。\n这样就通过了所有轮次的面试,收到了微信支付的Offer。\n面试要求和面试花样比别家多,待遇却不比别家高。\n但最后还是选择了微信支付的Offer, 毕竟这是微信,想去看下。\n就这样,在2020年,我回到了广东,去了深圳。\n6 深圳 之前听人说,深圳是一座只适合的打工的城市。\n来了之后发现,的确如此。\n6.1 微信支付 6.1.1 业务 在微信支付的人才会意识到,微信和微信支付更像是两个截然不同的公司,微信支付与腾讯的财付通关系反而要比微信本身更密切。\n我所在的团队在微信支付做的是委托代扣业务,在微信支付内部,与收银台,付款码并称基础支付,虽然现在已经很少用这个称呼了。\n委托代扣业务常见的业务场景就是免密支付和自动续费:\n如乘坐滴滴或者骑行共享自行车,在行程结束后,商家自动扣款,这就是免密支付;每个月腾讯视频,QQ音乐自动扣月费,那就是自动续费。\n所谓的委托代扣,即是用户委托商户发起扣款,建立委托关系后,商户可以在用户无需输入密码验证身份的情况下,发起扣款。\n所以委托代扣的业务流程分成两步:\n用户和商户建立委托关系,称为「签约」。这是一次性动作,只需要授权一次。 商户请求微信支付,对用户发起「扣款」。 我之前还在腾讯内网写了一篇文章来介绍委托代扣的业务场景,可惜我自己已经看不到了。\n委托代扣每天有海量的交易请求,即使在整个微信支付也是排得上号的(不然怎么会叫基础支付),而微信支付对系统可用性的要求是99.999%, 也就是意味着全年的不可用时长不能超过5分钟。\n在一个海量交易系统,需要实现5个9的可用性,难度可以说是非常高,因此需要做的事情非常多。\n与之前在蚂蚁团队动荡的经历不同,直到我离开微信支付,我都一直在委托代扣团队工作。所以我能从中学习到非常多关于如何构建高可用分布式系统的经验和知识\n6.1.2 加班与学习 无论在哪个大厂,加班也是绕不开的话题。\n微信支付也不例外,微信是有名的卷厂。\n据我观察,广州总部的工作节奏大概是11115,因为他们下班得晚,所以上班得晚,而我所在的微信支付稍好,大概是995, 1095.\n在我的认知中,我是很排斥996这种工作制,而正如前文所说的那样,个人要摆脱996这种工作制,只有合理利用时间,坚持学习。\n而健康的体魄又是实现任何想法的前提,所以身体和头脑,都需要锻炼。\n因为我租住的房子,地铁和公交都不便利,因此乘坐公司的班车上下班就是我的最佳选择。\n虽说腾讯标榜弹性工作制,但是却有很多潜规则。\n例如班车在9点前,把员工送回到公司上班,就是其中一条。\n另外一条就是,不同小区,对应上班的班车只有一趟,下班班车有多趟。因为公司「期望」员工在固定时间前回公司上班,可以加班到不同的时间点下班。\n因为班车要9点到公司,就要求班车必须较早出发,即8:14分出发,因此我每天必须7:50起床赶班车。\n为了早起赶班车,我又必须在晚上23:30前睡觉,不然起不来。\n因此,我每天的时间安排基本被固定下来了,再结合我自己的学习和运动计划,就变成了一个时间表:\n7:50:起床 8:14:乘坐班车 8:14 - 9:00:在车上阅读电子书或听英文Podcast(推荐几个Podcast:个人最爱 Healthy hacker, The Changelog, Let\u0026rsquo;s Master English) 9:00 - 9:15/9:20:早餐 9:30 - 12:00:工作 12:00 - 14:00 午休时间:健身1小时,半小时洗澡+吃午饭 14:00 - 18:00 工作 18:00 - 18:40 晚饭 18:40 - 20:10/40 工作 20:10 - 20:40 下班班车 21:00 - 23:00 学习半小时日语或英语,阅读1小时书或维护开源项目或和妹子聊天或看视频,洗漱 23:30 - 7: 50 睡觉 这样的时间表,从2020到2023,持续了近三年。\n6.1.3 魔幻2022 2022年是魔幻的一年。\n在疫情层面,深圳在农历新年之后,就开始了长达一个月的封城,并拉开了持续一整年的核酸大戏的序幕。\n在公司层面,腾讯从2022年开始,就宣布了降本增效的大政方针,用通俗的话讲,就是裁员降薪。从年初每天刷屏的毕业论文(被裁员同事写的感想),到年中宣布绩效与晋升改革,缩减高绩效名额,增加低绩效名额,晋升与涨薪脱钩,晋升机会从一年两次缩减为一年一次等等。\n在个人层面,2022年是厚积薄发的一年。\n我站在智哥的基础上,花了近4个月,把负债沉重的祖传签约链路给重构了,并梳理清楚了签约链路的业务规则,沉淀成文档。\n花了1年多的时间,从0搭建了代扣的数据仓库。\n花了1个多月时间,从0重新搭建了一套类似委托代扣签约的免密收银台签约链路。\n在腾讯KM平台输出了十多篇文章,有超过5篇入选/获得双月度的腾讯知识奖,1篇获得年度腾讯知识奖,影响力超过了99%的同事。\n一边是个人能力和认知的进,一边是公司待遇和前景的退,还有疫情的前途未卜,难免令人心生迷茫,不知前路在何方。\n6.1.4 骆驼身上的稻草 如果一直给骆驼加稻草,可能会看到骆驼最终倒下,却不知道一把稻草里面,哪根是最后一根让骆驼倒下的稻草。\n同组刚结婚购房的小伙伴,因为降本增效的政策,被毕业了。\n2022年的两次的绩效考核,我的业绩都是 outstanding, 总评都只是 good, 而绩效又直接与收入回报挂钩。\n2022年6月,本来我已经满了晋升高级工程师的停留时限要求,但是公司的一纸改革,直接把这次年中的晋升机会抹掉。\n某天清晨,当我如往常一样准备穿衣上班,却突然发现小区因为疫情被毫无征兆地封控三天。网上的蔬菜食物早被抢购一空,冰箱冷藏层找到的,数周前购买的冰鲜鸡腿,才让我得以饱食。\n封控的第四天凌晨4点,舍友因为肾结石发作,敲响了我的房门,我唯有先向居委会申请通行证,才被允许出门看急诊。直到2个小时之后,我们才走出了小区门。\n如果舍友的病在封控期结束的早一天发作,我都不知道要如何才能出得了这道每天进出的门。\n好朋友4年T10的晋升速度,与我可能6年还停留在T8的差距; 深圳高企的房价以及我增长缓慢的收入。\n微信支付,在各种压力之下,变得越发地像一个工厂,而每个开发者,都只是流水线上的工人。\n或许,我可以尝试去其他国家,去看下那个不一样的世界。\n6.1.5 面试 自从公司明里暗里宣布裁员开始,我就开始重新在 Leetcode 上面刷题,坚持每天一题,我不喜欢被动应对。\n有了尝试去其他国家的想法之后,我就在Linkedin 上面更新了英文简历和自己的简介,然后就有不同的国家的recruiter找上我。\n排掉哪些我不想去的国家(比如坡县),排掉某些我不感兴趣的公司(某跳动,某Tiktok),排掉哪些我不感兴趣的职位,我约了两家来自不同国家的公司面试。\n一个是来自的日本的 paypay, 是日本最大的三方支付公司,模仿的是支付宝。比较吸引我的点是:\n他们的公司75%都是外国人。 允许在日本任何地方远程办公,如果愿意在东京办公,有额外的补贴。我面试时视频见过的3个面试官+ recruiter,就没有一个是在公司环境办公的 较高的薪资,日本的IT公司薪资普遍不高,但paypay 给的薪资,能比得上0.75个日本Google 较新的技术栈,他们用的Java版本是JDK17,存储竟然用的是 TIDB. 因为之前一直在学日语,所以刚开始时,还尝试用日语和这家公司recruiter 打招呼,类似《大家的日语》第一课:\n我: はじめまして、わたしわ梁です\nHR: はじめまして、よろしくお願いします\n我: よろしくお願いします\n当然,后面我就切换回英文了,毕竟我的日语口语还不支持我完成面试。paypay 是一轮笔试加四轮面试\n另外一家就是AWS,base 在Canada,毕竟我没有身份可以去美帝。\nAWS也是一轮笔试加四轮面试,笔试还是很有难度的,一道大概leetcode medium + 一道leetcode hard+ 原题,那道 hard+ 的题,如果不是刷过原题,我是解不出来的。\n因为面试的是SDE2,所以这四轮面试是:\n解算法题 + 2个 LP 问题,算法题判断多叉树是否存在指定路径,如果存在,返回该路径。 解算法题 + 2个 LP 问题,算法题是Top K freqent element 问题 System Design + 2个 LP 问题,设计一个日志系统。 Object-Oriented Designa + 2个 LP 问题, 根据需求,用面向对象设计类,算是算法题与面向对象的结合版本。 所谓的LP 问题,指的是 Leadership Principles, 就是 Amazon 的企业文化里面有16条Leadership Principles, 他们会针对这些准则,让你给合个人经历,讲你自己的故事。\n例如,告诉我一个你没有在deadline 前完全项目的经历?主要是看你如何介绍背景,阐述问题,你的行动,最后的结果。即所谓的STAR: Situation, Task, Action, Result. 通过你的经历和应对,判断你是否是个合格的候选人。\n我花了一个月的时间,写了20多个故事的英文底稿,基本覆盖了这16条principles, 把这些故事双面打印出来,用了大概11页纸。\n最后面试都通过了,我选择了这个温哥华的 Offer。\n6.1.6 离开 在每两周至少至少交付一个需求的前提下,我写的生产代码,没有出过一次故障,我没有写过一次复盘,我写过的最大的bug 就是读写文件时,没有对指针判空,导致文件不存在时,服务coredump。\n曹操在评注《孙子兵法》时,有一句批注,「善战者无赫赫之功」。善于指挥的人,没有跌宕起伏的故事,没有赫赫有名的战功。\n我喜欢四平八稳,而不是狼烟四起,再四处救火,不是「扶大厦于将倾」,方显「英雄本色」。\n我更倾向于设计(尽量)不会倾的大厦,所以就没有什么存在感,故而平平无奇,会被认为,换谁来都可以。\n就这样,到了樱花盛开的季节,也到了离开的季节。\n3月,我离开了微信支付。\n坐上了前往温哥华的班机。\n7 温哥华 对于习惯了只有夏季的广东人来说,温哥华的春天比广东的冬天还冷。\n但新的开始,总是伴随着与众不同。\n那温哥华的冬天是怎么的呢?只能等到冬天来了才知道。\n我的未来会是怎么样的呢?也只有未来来了才知道。\n8 后话 好友总问我,你每天这样的忙碌,还给自己的时间表排得这么满,不觉得累的么?你是怎么坚持的?\n我希望可以追上期望中的自己,每次想到,我这样的坚持可以让我摆脱这样生活,我的动力就涌出来了。\n所谓知人者智,自知者明,我只是个没有天赋,也没有资源的普通人,想要追上期望中的自己,坚持就是我最大的天赋。\n中学时有篇文章是帝师宋濂讲自己早年求学经历,勉励学子马生专心治学的《送东阳马生序》\n余幼时即嗜学。家贫,无从致书以观,每假借于藏书之家,手自笔录,计日以还。\n天大寒,砚冰坚,手指不可屈伸,弗之怠。录毕,走送之,不敢稍逾约。\n以是人多以书假余,余因得遍观群书。\n既加冠,益慕圣贤之道 。\n又患无硕师名人与游,尝趋百里外,从乡之先达执经叩问。\n先达德隆望尊,门人弟子填其室,未尝稍降辞色。\n余立侍左右,援疑质理,俯身倾耳以请;或遇其叱咄,色愈恭,礼愈至,不敢出一言以复;俟其欣悦,则又请焉。\n故余虽愚,卒获有所闻。\n\u0026hellip;\n我自己的经历和成就,自知无法与宋濂先生相比。但十数年后,再读宋先生的《送东阳马生序》,却有了不一样的感悟。\n故余虽愚,卒获有所闻。\n我虽然普通,但是坚持还是有收获了。\n9 延伸阅读 《为什么梦想买不起,故乡回不去》 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%BF%99%E4%BA%9B%E5%B9%B4%E8%B5%B0%E8%BF%87%E7%9A%84%E8%B7%AF_%E4%BB%8E%E5%B9%BF%E5%B7%9E%E5%88%B0%E6%B8%A9%E5%93%A5%E5%8D%8E/","summary":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一","title":"这些年走过的路:从广州到温哥华"},{"content":"1 糖葫芦 2 答案 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E7%8F%AD%E8%B4%B9%E4%B8%8E%E5%B9%B4%E7%BB%88%E5%A5%96/","summary":"1 糖葫芦 2 答案","title":"加班费与年终奖"},{"content":"1 前言 因为ChatGPT 的爆红,最近基于在 ChatGPT 的工具如雨后春笋般冒出来,在 Twitter 上,基本每周都可以看到开发者发布基于 ChatGPT 的新应用(这些人不用上班的么?)。\n而使用过好多的 ChatGPT 应用后,最惊艳的是 @yetone 开发的是 openai-translator 这款应用,支持「翻译」,「润色」,「总结」三种功能。\n1.1 开发历程 因为在Twitter 上关注了 @yetone, 所以能从推文看到@yetone 的开发历程:\n最开始 yetone 是为 Bob 开发了基于 ChatGPT-api 的 openai-translator 插件,广受Bob用户的好评。后面yetone 码力全开,又乘胜追击,为 Bob 开发了基于ChatGPT-api 的润色和语法纠错 openai-polisher 插件,完美替代了 Grammarly.\n因为这两个插件的出色表现,很多非Bob 用户和 Mac 用户也希望可以尝鲜,因此 yetone 就徇众要求, 「糊」(yetone的原话)了一个浏览器插件,这就是 openai-translator ,而后就一发不可收拾了。\n因其出色的表现,在Github 和 Hacker News 上爆火了。\n然后 yetone 又将openai-translator 浏览器插件进行打包,做成跨平台的桌面端应用。\n2 效果 测试文章来自Github Blog:Raising the bar for software security: GitHub 2FA begins\n2.1 翻译(translate) 翻译的用户体验,对比我之前一直在使用的沙拉划词(ext-saladict):\n沙拉划词:\nopenai-translator:\n翻译的效果(最核心的功能)\n测试文章片段:\nWe want enrolling your GitHub account in 2FA to be as easy as possible, using methods that are reliable and secure so you always have access to your account (and no one else does!). To prepare for this program we’ve been busy enhancing that experience. Here are a few of the highlights:\n有道翻译与Google翻译:\n我们希望在2FA中注册您的GitHub帐户尽可能简单,使用可靠和安全的方法,以便您始终可以访问您的帐户(没有其他人可以!)。为了准备这个节目,我们一直在忙着提高这种体验。以下是其中的一些亮点\n我们希望使用可靠且安全的方法在2FA中注册您的GitHub帐户尽可能容易,因此您始终可以访问您的帐户(而且没有其他人可以!)。为了准备该计划,我们一直在忙于增强这种体验。以下是一些亮点:\nDeepL翻译:\n我们希望为您的GitHub账户注册2FA时尽可能简单,使用可靠和安全的方法,这样您就可以始终访问您的账户(而没有其他人可以访问!)。为了准备这个项目,我们一直在忙着增强这种体验。以下是其中的几个亮点。\nopenai-translator 翻译:\n我们希望让您的GitHub账户启用双重身份验证变得尽可能简单,使用可靠和安全的方法,以便您始终可以访问自己的账户(而别人则不能!)。为了准备这个计划,我们一直在不断改进用户体验。以下是其中的亮点:\n2.2 润色(polish) 语法纠错及词句润色的效果:\n测试文章片段:\nWe want enrolling your GitHub account in 2FA to be as easy as possible, using methods that are reliable and secure so you always have access to your account (and no one else does!). To prepare for this program we’ve been busy enhancing that experience. Here are a few of the highlights:\n对比我之前一直使用的 Language Tool:\n估值 100 亿刀的 Grammarly:\nDeepL 家新出的基于AI的写作助手DeepL Write:\nopenai-translator:\n因为openai-translator没有给出润色前后的比对,我们可以通过 diff 工具查看下:\n2.3 总结(summarize) 这个应该是openai-translator 特有的功能,可针对长文给出总结与概要:\n原文片段:\nOver the course of the next year, we’ll be reaching out to groups of developers and administrators, starting with smaller groups on March 13, to notify them of their 2FA enrollment requirement. This gradual rollout will let us make sure developers are able to successfully onboard, and make adjustments as needed before we scale to larger groups as the year progresses.\nIf your account is selected for enrollment, you will be notified via email and see a banner on GitHub.com, asking you to enroll. You’ll have 45 days to configure 2FA on your account—before that date nothing will change about using GitHub except for the reminders. We’ll let you know when your enablement deadline is getting close, and once it has passed you will be required to enable 2FA the first time you access GitHub.com. You’ll have the ability to snooze this notification for up to a week, but after that your ability to access your account will be limited. Don’t worry: this snooze period only starts once you’ve signed in after the deadline, so if you’re on vacation or out of office, you’ll still get that one week period to set up 2FA when you’re back at your desk.\nSo, what if you’re not in an early enrollment group but you want to get started? Click here and follow a few easy steps to enroll in 2FA.\nopenai-translator 总结后的片段:\nGitHub will gradually roll out 2FA enrollment requirements to developers and administrators over the next year, starting with smaller groups on March 13.\nThose selected for enrollment will be notified via email and a banner on GitHub.com, with 45 days to configure 2FA before being required to enable it upon accessing the site. A snooze period of up to one week is available after the deadline has passed.\nAnyone can enroll in 2FA by following a few easy steps provided by GitHub.\n3 使用方式 所谓 action is louder than words, 不看广告看疗效。\n评价一个产品好不好用,只有用过才知道。体验openai-translator 最简单快捷的方式就是使用浏览器插件,免于安装桌面应用。\n而你所需要的只是一个chatgpt 账号+ 一个浏览器插件:\n1.注册安装 openai, 此处省去指引,有非常多的教程。\n2.获取 openai api key,并复制此key\n3.打开 Chrome web store, 搜索 OpenAi Translator, 并点击安装\n4.点击搜件,粘贴刚刚复制的api-key:\n5.划词,并点击 openai-translator 图标进行体验。\n4 总结 ChatGPT 向我们展示了 GPT 模型的伟大之处。但模型虽强,阳春白雪,终究是离普通用户太远。\n是无数个像yetone 这样的开发者,用产品展示给用户看,GPT 模型是如何的伟大。\n向 yetone 致敬。\n5 参考 bob-plugin-openai-polisher bob-plugin-openai-translator openai-translator @yetone ","permalink":"https://ramsayleung.github.io/zh/post/2023/openai-translator/","summary":"1 前言 因为ChatGPT 的爆红,最近基于在 ChatGPT 的工具如雨后春笋般冒出来,在 Twitter 上,基本每周都可以看到开发者发布基于 ChatGPT 的新应用(这些人不用上班的么","title":"OpenAI-translator: 基于ChatGPT的划词翻译及润色应用"},{"content":"1 技巧 对于使用org-mode 格式的文本,例如Emacs官方 tree-sitter 的使用教程\n在线阅读不是很易读,相当于人脑解析 org-mode. 我的个人习惯是使用 eww 浏览器来阅读:\n复制网页链接 使用 eww 打开链接 major-mode 切换到 org-mode, 就可以愉快地使用 Emacs 来阅读 org-mode 文本. ","permalink":"https://ramsayleung.github.io/zh/post/2023/emacs%E6%8A%80%E5%B7%A7%E5%88%86%E4%BA%AB_%E4%BD%BF%E7%94%A8eww%E6%89%93%E5%BC%80%E5%9C%A8%E7%BA%BForg-mode%E6%96%87%E6%A1%A3/","summary":"1 技巧 对于使用org-mode 格式的文本,例如Emacs官方 tree-sitter 的使用教程 在线阅读不是很易读,相当于人脑解析 org-mode. 我的个人习惯是使用 eww 浏览器来阅读","title":"Emacs技巧分享: 使用eww打开在线org-mode文档"},{"content":"1 技巧 分享一下平时使用 dired-mode 批量修改文件名的技巧:\nC-x C-f 指定的文件目录,进入 dired-mode C-x C-q dired-toggle-read-only: Edit Dired buffer with Wdired. 批量修改,手段有 使用 query-replace 批量修改文件名 使用evil的多行编辑模式 使用 rectangle-command: C-x r t string-rectangle C-c C-c 提交修改或 C-c C-k 放弃修改 Figure 1: 使用 rectangle-command 进行批量修改\nFigure 2: 使用 evil的多行编辑模式进行批量修改\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%8A%80%E5%B7%A7%E5%88%86%E4%BA%AB_dired%E6%89%B9%E9%87%8F%E4%BF%AE%E6%94%B9%E6%96%87%E4%BB%B6%E5%90%8D/","summary":"1 技巧 分享一下平时使用 dired-mode 批量修改文件名的技巧: C-x C-f 指定的文件目录,进入 dired-mode C-x C-q dired-toggle-read-only: Edit Dired buffer with Wdired. 批量修改,手段有 使用 query-replace 批量修改文件名 使用evil的多","title":"Emacs 技巧分享:dired-mode 批量修改文件名"},{"content":"1 非必要不加班 2 八小时工作制 3 后话 如有雷同,可能在同一家公司打工。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E7%8F%AD%E7%94%B3%E8%AF%B7/","summary":"1 非必要不加班 2 八小时工作制 3 后话 如有雷同,可能在同一家公司打工。","title":"加班申请"},{"content":"1 弹性打卡 2 鄙人张麻子 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%BC%B9%E6%80%A7%E6%89%93%E5%8D%A1/","summary":"1 弹性打卡 2 鄙人张麻子","title":"弹性打卡"},{"content":"1 灵感来源 周末的时候,和好朋友出去玩,聊到了自媒体,就提起我这个新开的公众号。朋友就提了问题,能否写些能让人看得懂的内容。\n因为我之前都写博客为主,一篇长博客几千字,主要涉及计算机和历史相关的内容,涉及到具体问题和算法,就会比较深入。\n毕竟深度和广度无法兼顾,比如我写的《深入浅出Count-Min Sketch算法》可能大部分读者既不关注,也不知道是个什么东西。\n微信公众号这个阅读载体就注定无法阅读知识密度比较高的内容,毕竟拿起手机可能只是想阅读一些下饭的内容, 你写个长篇大论,人家还不一定有意愿看完。\n经典的传媒学著作《娱乐至死》就有这样的观点: 大众化的媒介必定也是娱乐化的\n还有一个原因是,我之前写的博文大多都无法直接发表在公众号上,比如《为什么梦想买不起,故乡回不去》, 我删改了8次才过审。\n还有另外一篇历史著作《天朝的崩溃》的读后思考,发表之后直接被删除。 内容不过从兵力,武器,政治制度,科技层面分析为什么鸦片战争中失败的是清军,而非来袭的英军。\n所谓知人者智,自知者明。搞清楚自己的定位很重要。所以我决定换种方式来阐述自己的想法,通过xkcd 风格的漫画来展现自己的想法。\nxkcd 是国外有名的网络漫画网站,主要与科技相关,风格就是火柴人简笔画,但充满哲理,回味无穷。\n所以我决定东施效颦,也用xkcd 风格的简笔画来表达。\n2 上菜 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%B3%84%E5%AF%86/","summary":"1 灵感来源 周末的时候,和好朋友出去玩,聊到了自媒体,就提起我这个新开的公众号。朋友就提了问题,能否写些能让人看得懂的内容。 因为我之前都写博客","title":"泄密"},{"content":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%; 月活用户4.89亿。Spotify成为第一家订阅用户数突破2亿的音乐流服务。\n而我突然意识到,我那个使用Rust, 为Spotify开发,挂在Sptofiy官网的library:RSpotify,已经维护有五年了:\n一件用爱发电的开源项目要坚持维护五年,也是有很多话可以说的。\n2 起源 还记得大三暑假时,也就是2017年,当时找好了实习,并且拿到了Return Offer. 在拿到Offer之后,实习还没有离职,就想学习一门新的编程语言。\n因为之前用的是都是Java,Python之类的带GC的编程语言,就想学习点硬核,偏底层的编程语言。 本来是想学C++,结果在知乎看了一圈之后,大家都说C++要没落了,推荐学习Rust。(然后我现在靠写C++混饭吃)\n搜索了Rust的信息,发现它性能媲美C/C++, 又不需要手动管理内存,还连续几年荣膺Stackoverflow的 Most Loved Programming Language, 就它了。就开始了一边实习一边摸鱼学习了Rust的旅程。\n因为大学前三年把学分都已经修满了,所以大四一整个学年都不需要上课了,就有时间折腾。\n在学了2-3个月之后,就想拿Rust来写些项目。 但因为我只会写Web应用,又没有想到能写什么,到时就用Rust写了个博客,并将博客从原来的Github Pages迁移到自建的博客上。\n很臭屁地在 V2ex 和 Reddit 分享用Rust重写博客的经历,V2ex 一群人问我为什么不用PHP/xxx语言写,Reddit社区就友好很多。 (然后过了5年之后,服务器欠费,又把自建博客迁移回Github Pages。当然,那是后话了。)\n在花了2-3个月写完博客之后,觉得自己入门Rust,就想写个开源项目,感受下与其他开发者协作的场景。\n当时看到个网易云音乐命令行版本的播放器 musicbox, 当时我在用的是Spotify,就希望可以为Spotify写个类似的播放器。\n虽说Spotify API是对外开放,但直接使用HttpClient来请求HTTP API有点太祼,所以就希望使用先封装个library,方便后续的Rust应用直接调用,就不需要自己操心Http请求了。\n这就是RSpotify这个库的来源。\n这次,我就只在 Reddit和博客 上分享使用Rust来写library 的经历了。\n3 演进 3.1 野蛮生长阶段 刚开始写RSpotify的时候,对于如何设计一个易用,友好的library 完全没有头绪,毕竟设计好用的类库需要相当的经验沉淀。\n对于没有设计思路的我而言,当时能想来的解决方案是去Spotify官方列出来的library看下,哪个语言的library看得懂,star又多,就把这个library 翻译到Rust上。\n就把目光瞄准到Python版本的 spotipy 上。\n在2018-01-08 提交了第一个commit, 经过一个多月的日夜施工,终于在2018-02-18 完成了所有的API接口开发,发布了 0.1版本\n虽然这是我这个学生写的第一个Rust库,但是开源项目需要的标准配置,我还是都加上了:\n自动化流水线,Travis(当时Github Action还没有出现) 齐全的文档说明 完整的单元测试用例 使用示例 README说明与License 为了吸引其他开发者来协作开发,所有的资料都是英文的。\n不过从Rust 包托管网站 crates.io 的数据可以看到,0.1版本只有300+的下载量,几乎没有什么人在用。\n3.2 async 阶段 时间来到2019年,对于Rust社区来说,最激动人心的应该是Rust 1.39版本,将正式包含 async/await 特性,自那天起,Rust正式支持异步编程。\n自此之后,Rust社区在做的事情,就是把已有Rust代码疯狂升级到async await,RSpotify虽迟,但也赶上了这波潮流。\n当时RSpotify 请求Spotify的API使用的HTTP库是 reqwest,在 reqwest 支持异步模式之后,开发者 Alexander就提了一个超大的PR,把所有已有的api全部修改成async, 我就乐见其成,就把这个PR合并了。\n有社区的同学抱怨说异步模式的代码不好使用,他对性能没有什么要求,能否保留同步模式的接口调用。\n后来为了兼顾同步模式和异步模式这两种调用方式,Alexander 又提了一个超大超大的PR,把现有的异步模式代码复制一份,然后把async 关键字去掉。\n从此以后,RSpotify就需要同时维护两份几乎相同的代码,每次新增,修改,删除都需要确保同时变更两份代码。 着实痛苦不堪,但我也没有思考出更优解。\n这时候,后来和我共同维护RSpotify 的开发者 Mario 出现了。\n3.3 maybe_async 阶段 当时RSpotify最大的问题在于有两份几乎一样,但是使用同步调用和异步调用模式的代码。\n而异步调用的代码,返回参数都是一个 Future\u0026lt;T\u0026gt; ,将真正的响应结果封装在一个 Future 结构里面。\n所以当时Mario 提出的第一个解决思路,是将对异步代码进行封装,使用同步调用的runtime调用异步函数,然后再把响应结果返回回去:\n// 异步代码 async fn original() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { reqwest::get(\u0026#34;https://www.rust-lang.org\u0026#34;) .await? .text() .await } lazy_static! { // Mutex to have mutable access and Arc so that it\u0026#39;s thread-safe. static ref RT: Arc\u0026lt;Mutex\u0026lt;runtime::Runtime\u0026gt;\u0026gt; = Arc::new(Mutex::new(runtime::Builder::new() .basic_scheduler() .enable_all() .build() .unwrap())); } // 同步版本代码 fn with_block_on() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { RT.lock().unwrap().block_on(async move { original().await }) } 再通过Rust macro来为每个async 函数生成一个block_on 版本的函数。\n但实际上,发现编写macro太复杂,并且这种方案不够灵活,实现起来也相当复杂。\n然后Mario 又调研出一种新方案,通过 maybe_async 这个库在同步和异步模式之间切换。 默认是异步模式,但可以通过 features = [\u0026quot;is_sync\u0026quot;] 编译选项来切换到同步模式,maybe_async 就会把所有的async/await 关键字给去掉。\n这个方案简单,可读性高,易于扩展,也不需要维护复杂的 macro 代码。\n这也是我们最终采取的方案,重构之后,把 blocking 目录的近万行复制粘贴而来的代码删除掉,就非常爽。\n3.4 二次重构 阶段 前面提到,RSpotify最开始是直接翻译spotipy的代码。 因为Python是弱类型,而Rust是强类型,直接翻译,难免会有不少代码,写法没有纯正的Rust味道。\n社区的Kestrer同学就在一个issue里面,给RSpotify提了近90条优化建议,指出了RSpotify中设计的各种问题,包括强类型运用不当,使用过多的原始类型,函数出入参设计不够优雅,授权流程设计不够易用等等。\n这么多的优化建议,可以看出Kestrer真的花了很多时间来阅读和改善RSpotify的代码,盛情难却(可见原来的代码是多烂, 有非常多激发社区同学参与改进的空间).\n别人指出问题,就要好好优化。\n所以我和Mario就分别对每个接口返回的数据模型,数据模型与Json间转换的序列化方式,授权流程作了改进。 并对RSpotify这个library作了拆分,按照功能,拆分成 model, http, macros 三个单独的library。\n期间提了大概20多个PR,花了超过一年的时间,才处理完Kestrer 提的所有建议。\n3.5 pre-release 阶级 在开源社区里面,有一个约定俗成的规范: 当一个library 发布1.0 之后,就代表这个库已经处于稳定状态,不会再出现大量breaking change 的情况了。(py2, py3不在此约束内)\n而经过4年的开发,RSpotify 已经步入一个相对稳定的开发状态,没有太多的breaking change 或重构了,开始为发布正式的1.0release 版本作准备。\n当功能与架构相对稳定后,近一年时间,我和Mario就开始优化RSpotify的易用性,比如\n添加更多,针对不同场景的 examples; 尽可能地去掉 unsafe 代码; 为返回列表的API提供同步及异步版本的Iterator支持; 保持向前兼容的情况下,尽量使已有接口更加Rust化; 添加更多的自动化检查,如检查代码中文档的链接是否404; 性能优化,减少不必要的内存分配 目前版本已经去到了 0.11.6, 功能也相对稳定, 预计不久后就会正式发布1.0版本。\n4 感悟 4.1 开源协作 截至到2023-02-09,RSpotify一共有1673次commit, 但我和Mario都只贡献了1/3的commit,剩下的commit都是社区的其他开发者提交的。\n从RSpotify的演进历程也可以看出,我只是从0开发了最初版本的RSpotify,后面都是随着Rust的演进,有不同的开发者帮忙优化与迭代,我做的事情就从单纯的creator, developer 变成maintainer, reviewer,负责review其他开发者的PR。\n可以说,如果没有其他开发者的贡献与协作,RSpotify不会演进成现在的样子。\n如何吸引更多的开发者加入,让他们乐于为项目作贡献,我个人的见解是:\n所有文档,注释,commit message, issue, CHANGELOG等材料,都只使用英文。 标准的开源协作流程; issue, PR, CHANGELOG 都提供标准模板 要添加新特性,修改已有功能的时候,新建issue讨论动机与可行性 每个PR都需要一个Peer Reviewer review后才能合并 每次发新版本,都需要在 CHANGELOG 注明大的特性变更,以及breaking change 文档,示例,开发指引,测试case完备,降低新开发者参与的成本。 be nice,态度友好,针对issue,PR都尽量回复,理性,友善讨论。 开源协作的一个感受就是,在Github讨论问题的时候,可能突然有位大佬也加入群聊。\n比如和Mario讨论, 增加更多更严格cargo clippy 的rule,以便让编译器帮我们发现更多潜在问题时,cargo clippy 的maintainer 也加入讨论,就什么rule 更合适,给出自己的建议。\n4.2 收获 我用C++已经混了三年的饭吃了,但还只能看到C++的门槛,没法说入了C++的门。\n同理,虽然距离我学习Rust已经过去6年了,我依然感觉我还不会Rust,都是编译器教我写代码。\n在Review别人代码的过程中,我也学习到非常多「地道」和高级的Rust用法,项目维护的经验.\n想到的点:\n使用Rust的macro来减少copy-paste的代码(但复杂的 macro,基本不具备可读性。) 使用serde 自定义序列化函数; 以workspace 模式管理多个crates; 编写 async/await 的异步代码; 使用标准库的Trait, 风格契合标准库; 结合thiserror 和anyhow 处理异常; 通过自动化和模式化,减少项目维护的成本(能用机器做的,就不要用人做)。 规范的开发流程,包括commit message, issue, PR, CHANGELOG, release note 等等 期间把收获与心得写了两篇文章:\nThe lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable 4.3 关于开源 维护这个项目5年之后,对于「开源」有了些不一样的理解。\n在1970 年代,Richard Stallman发起自由软件运动,旨在推广用户有使用,复制,研究,修改和分发软件的社会运动。 自由软件运动人士认为自由软件的精神应该贯彻到所有软件。\n在90年代,又兴起了开源软件运动,则计算机软件的源代码是可以公开,随意获取的。 (自由软件与开源软件不是同一个概念,自由软件定义更为严格)\n在那个崇尚黑客精神的年代,开源是「目的」,是为了贯彻自由的精神。\n以前听到某某公司内部有好用的工具,组件,框架时,总会问一句,为什么他们不像Google一样把它们开源出来。\n现在的想法可能是,为什么要开源出来,价值和收益是什么?\n开源一个项目的目的可能是:\n我做了个很有用,很有趣的东西,就想分享出来。但是我个人人力有限,大家一起来帮忙做大做好。(Linux, Ruby On Rails等) 我们做了个好东西,我们要抢占市场。我们就开源,搞人海战术,让竞品淹没在人民群众的汪洋大海中,让我们的东西成为事实的标准。(Android,Chromium, Kubernetes, Vscode) 就想开源让你们见识下大佬是怎么样子的。 个人理解,开源是「手段」,而非「目的」\n对于商业公司而言,如果没有收益,为什么要把花钱雇的人写的内部组件开源出来呢?总不成是为了在B站上博取小朋友的称赞吧。\n而公司内部的组件,往往是与业务共生,高度适配的,藕断丝连,没有那么容易开源的。\n商业公司,只谈收益与预期,如果名声能卖钱,估计也会拿来换取利润。\n更重要的是,开源并不是简单把代码公开出来。\n软件和生物一样,是有生命的,需要长期维护的,而不是一个commit把所有代码一把push 到Github就完事了,或者然后过了几年又push一把,更新几百个文件。\n开源是一个技术与管理结合的决定,需要把开发模式都切换到开源社区,决策过程与动机要对社区可见。\n不仅让人能从代码中读懂功能是「什么」,也要从动机讨论中知道「为什么」要这么改。\n4.4 些许成果 在Github上,收获了495个star,被1108个仓库及18个package 所依赖,而其中Alexander的 spotify-tui 就是我期望做的终端版本的Spotify。\n开源的好处就是,在开发好基础设施之后,自然就会有其他有相同想法的同学,把应用开发出来。\ncrates.io 的统计,总计被下载23w次,当然包括很多CI的重复下载。\n对于有Rust,Spotify,Library等诸多定语的RSpotify来说,目标受众本来就不多,能有现在这样的用户量已远超我最初了预期了。\n5 总结 虽然我未曾从这个项目上获得到一分物质上的回报,但在创建这个项目的时候,我可能不会想到,我能维护它长达五年。\n天上的云,飘来又飘走;开源的项目,挖坑又弃坑。\n视线望不到下一个五年,唯有且行且看。\n6 参考 Spotify is first music streaming service to surpass 200M paid subscribers RSpotify The lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable spotify-tui ","permalink":"https://ramsayleung.github.io/zh/post/2023/rspotify_%E4%B8%80%E4%B8%AA%E7%94%A8%E7%88%B1%E5%8F%91%E7%94%B5%E4%BA%94%E5%B9%B4%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","summary":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%","title":"RSpotify: 一个用爱发电五年的开源项目"},{"content":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%; 月活用户4.89亿。Spotify成为第一家订阅用户数突破2亿的音乐流服务。\n而我突然意识到,我那个使用Rust, 为Spotify开发,挂在Sptofiy官网的library:RSpotify,已经维护有五年了:\n一件用爱发电的开源项目要坚持维护五年,也是有很多话可以说的。\n2 起源 还记得大三暑假时,也就是2017年,当时找好了实习,并且拿到了Return Offer. 在拿到Offer之后,实习还没有离职,就想学习一门新的编程语言。\n因为之前用的是都是Java,Python之类的带GC的编程语言,就想学习点硬核,偏底层的编程语言。 本来是想学C++,结果在知乎看了一圈之后,大家都说C++要没落了,推荐学习Rust。(然后我现在靠写C++混饭吃)\n搜索了Rust的信息,发现它性能媲美C/C++, 又不需要手动管理内存,还连续几年荣膺Stackoverflow的 Most Loved Programming Language, 就它了。就开始了一边实习一边摸鱼学习了Rust的旅程。\n因为大学前三年把学分都已经修满了,所以大四一整个学年都不需要上课了,就有时间折腾。\n在学了2-3个月之后,就想拿Rust来写些项目。 但因为我只会写Web应用,又没有想到能写什么,到时就用Rust写了个博客,并将博客从原来的Github Pages迁移到自建的博客上。\n很臭屁地在 V2ex 和 Reddit 分享用Rust重写博客的经历,V2ex 一群人问我为什么不用PHP/xxx语言写,Reddit社区就友好很多。 (然后过了5年之后,服务器欠费,又把自建博客迁移回Github Pages。当然,那是后话了。)\n在花了2-3个月写完博客之后,觉得自己入门Rust,就想写个开源项目,感受下与其他开发者协作的场景。\n当时看到个网易云音乐命令行版本的播放器 musicbox, 当时我在用的是Spotify,就希望可以为Spotify写个类似的播放器。\n虽说Spotify API是对外开放,但直接使用HttpClient来请求HTTP API有点太祼,所以就希望使用先封装个library,方便后续的Rust应用直接调用,就不需要自己操心Http请求了。\n这就是RSpotify这个库的来源。\n这次,我就只在 Reddit和博客 上分享使用Rust来写library 的经历了。\n3 演进 3.1 野蛮生长阶段 刚开始写RSpotify的时候,对于如何设计一个易用,友好的library 完全没有头绪,毕竟设计好用的类库需要相当的经验沉淀。\n对于没有设计思路的我而言,当时能想来的解决方案是去Spotify官方列出来的library看下,哪个语言的library看得懂,star又多,就把这个library 翻译到Rust上。\n就把目光瞄准到Python版本的 spotipy 上。\n在2018-01-08 提交了第一个commit, 经过一个多月的日夜施工,终于在2018-02-18 完成了所有的API接口开发,发布了 0.1版本\n虽然这是我这个学生写的第一个Rust库,但是开源项目需要的标准配置,我还是都加上了:\n自动化流水线,Travis(当时Github Action还没有出现) 齐全的文档说明 完整的单元测试用例 使用示例 README说明与License 为了吸引其他开发者来协作开发,所有的资料都是英文的。\n不过从Rust 包托管网站 crates.io 的数据可以看到,0.1版本只有300+的下载量,几乎没有什么人在用。\n3.2 async 阶段 时间来到2019年,对于Rust社区来说,最激动人心的应该是Rust 1.39版本,将正式包含 async/await 特性,自那天起,Rust正式支持异步编程。\n自此之后,Rust社区在做的事情,就是把已有Rust代码疯狂升级到async await,RSpotify虽迟,但也赶上了这波潮流。\n当时RSpotify 请求Spotify的API使用的HTTP库是 reqwest,在 reqwest 支持异步模式之后,开发者 Alexander就提了一个超大的PR,把所有已有的api全部修改成async, 我就乐见其成,就把这个PR合并了。\n有社区的同学抱怨说异步模式的代码不好使用,他对性能没有什么要求,能否保留同步模式的接口调用。\n后来为了兼顾同步模式和异步模式这两种调用方式,Alexander 又提了一个超大超大的PR,把现有的异步模式代码复制一份,然后把async 关键字去掉。\n从此以后,RSpotify就需要同时维护两份几乎相同的代码,每次新增,修改,删除都需要确保同时变更两份代码。 着实痛苦不堪,但我也没有思考出更优解。\n这时候,后来和我共同维护RSpotify 的开发者 Mario 出现了。\n3.3 maybe_async 阶段 当时RSpotify最大的问题在于有两份几乎一样,但是使用同步调用和异步调用模式的代码。\n而异步调用的代码,返回参数都是一个 Future\u0026lt;T\u0026gt; ,将真正的响应结果封装在一个 Future 结构里面。\n所以当时Mario 提出的第一个解决思路,是将对异步代码进行封装,使用同步调用的runtime调用异步函数,然后再把响应结果返回回去:\n// 异步代码 async fn original() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { reqwest::get(\u0026#34;https://www.rust-lang.org\u0026#34;) .await? .text() .await } lazy_static! { // Mutex to have mutable access and Arc so that it\u0026#39;s thread-safe. static ref RT: Arc\u0026lt;Mutex\u0026lt;runtime::Runtime\u0026gt;\u0026gt; = Arc::new(Mutex::new(runtime::Builder::new() .basic_scheduler() .enable_all() .build() .unwrap())); } // 同步版本代码 fn with_block_on() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { RT.lock().unwrap().block_on(async move { original().await }) } 再通过Rust macro来为每个async 函数生成一个block_on 版本的函数。\n但实际上,发现编写macro太复杂,并且这种方案不够灵活,实现起来也相当复杂。\n然后Mario 又调研出一种新方案,通过 maybe_async 这个库在同步和异步模式之间切换。 默认是异步模式,但可以通过 features = [\u0026quot;is_sync\u0026quot;] 编译选项来切换到同步模式,maybe_async 就会把所有的async/await 关键字给去掉。\n这个方案简单,可读性高,易于扩展,也不需要维护复杂的 macro 代码。\n这也是我们最终采取的方案,重构之后,把 blocking 目录的近万行复制粘贴而来的代码删除掉,就非常爽。\n3.4 二次重构 阶段 前面提到,RSpotify最开始是直接翻译spotipy的代码。 因为Python是弱类型,而Rust是强类型,直接翻译,难免会有不少代码,写法没有纯正的Rust味道。\n社区的Kestrer同学就在一个issue里面,给RSpotify提了近90条优化建议,指出了RSpotify中设计的各种问题,包括强类型运用不当,使用过多的原始类型,函数出入参设计不够优雅,授权流程设计不够易用等等。\n这么多的优化建议,可以看出Kestrer真的花了很多时间来阅读和改善RSpotify的代码,盛情难却(可见原来的代码是多烂, 有非常多激发社区同学参与改进的空间).\n别人指出问题,就要好好优化。\n所以我和Mario就分别对每个接口返回的数据模型,数据模型与Json间转换的序列化方式,授权流程作了改进。 并对RSpotify这个library作了拆分,按照功能,拆分成 model, http, macros 三个单独的library。\n期间提了大概20多个PR,花了超过一年的时间,才处理完Kestrer 提的所有建议。\n3.5 pre-release 阶级 在开源社区里面,有一个约定俗成的规范: 当一个library 发布1.0 之后,就代表这个库已经处于稳定状态,不会再出现大量breaking change 的情况了。(py2, py3不在此约束内)\n而经过4年的开发,RSpotify 已经步入一个相对稳定的开发状态,没有太多的breaking change 或重构了,开始为发布正式的1.0release 版本作准备。\n当功能与架构相对稳定后,近一年时间,我和Mario就开始优化RSpotify的易用性,比如\n添加更多,针对不同场景的 examples; 尽可能地去掉 unsafe 代码; 为返回列表的API提供同步及异步版本的Iterator支持; 保持向前兼容的情况下,尽量使已有接口更加Rust化; 添加更多的自动化检查,如检查代码中文档的链接是否404; 性能优化,减少不必要的内存分配 目前版本已经去到了 0.11.6, 功能也相对稳定, 预计不久后就会正式发布1.0版本。\n4 感悟 4.1 开源协作 截至到2023-02-09,RSpotify一共有1673次commit, 但我和Mario都只贡献了1/3的commit,剩下的commit都是社区的其他开发者提交的。\n从RSpotify的演进历程也可以看出,我只是从0开发了最初版本的RSpotify,后面都是随着Rust的演进,有不同的开发者帮忙优化与迭代,我做的事情就从单纯的creator, developer 变成maintainer, reviewer,负责review其他开发者的PR。\n可以说,如果没有其他开发者的贡献与协作,RSpotify不会演进成现在的样子。\n如何吸引更多的开发者加入,让他们乐于为项目作贡献,我个人的见解是:\n所有文档,注释,commit message, issue, CHANGELOG等材料,都只使用英文。 标准的开源协作流程; issue, PR, CHANGELOG 都提供标准模板 要添加新特性,修改已有功能的时候,新建issue讨论动机与可行性 每个PR都需要一个Peer Reviewer review后才能合并 每次发新版本,都需要在 CHANGELOG 注明大的特性变更,以及breaking change 文档,示例,开发指引,测试case完备,降低新开发者参与的成本。 be nice,态度友好,针对issue,PR都尽量回复,理性,友善讨论。 开源协作的一个感受就是,在Github讨论问题的时候,可能突然有位大佬也加入群聊。\n比如和Mario讨论, 增加更多更严格cargo clippy 的rule,以便让编译器帮我们发现更多潜在问题时,cargo clippy 的maintainer 也加入讨论,就什么rule 更合适,给出自己的建议。\n4.2 收获 我用C++已经混了三年的饭吃了,但还只能看到C++的门槛,没法说入了C++的门。\n同理,虽然距离我学习Rust已经过去6年了,我依然感觉我还不会Rust,都是编译器教我写代码。\n在Review别人代码的过程中,我也学习到非常多「地道」和高级的Rust用法,项目维护的经验.\n想到的点:\n使用Rust的macro来减少copy-paste的代码(但复杂的 macro,基本不具备可读性。) 使用serde 自定义序列化函数; 以workspace 模式管理多个crates; 编写 async/await 的异步代码; 使用标准库的Trait, 风格契合标准库; 结合thiserror 和anyhow 处理异常; 通过自动化和模式化,减少项目维护的成本(能用机器做的,就不要用人做)。 规范的开发流程,包括commit message, issue, PR, CHANGELOG, release note 等等 期间把收获与心得写了两篇文章:\nThe lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable 4.3 关于开源 维护这个项目5年之后,对于「开源」有了些不一样的理解。\n在1970 年代,Richard Stallman发起自由软件运动,旨在推广用户有使用,复制,研究,修改和分发软件的社会运动。 自由软件运动人士认为自由软件的精神应该贯彻到所有软件。\n在90年代,又兴起了开源软件运动,则计算机软件的源代码是可以公开,随意获取的。 (自由软件与开源软件不是同一个概念,自由软件定义更为严格)\n在那个崇尚黑客精神的年代,开源是「目的」,是为了贯彻自由的精神。\n以前听到某某公司内部有好用的工具,组件,框架时,总会问一句,为什么他们不像Google一样把它们开源出来。\n现在的想法可能是,为什么要开源出来,价值和收益是什么?\n开源一个项目的目的可能是:\n我做了个很有用,很有趣的东西,就想分享出来。但是我个人人力有限,大家一起来帮忙做大做好。(Linux, Ruby On Rails等) 我们做了个好东西,我们要抢占市场。我们就开源,搞人海战术,让竞品淹没在人民群众的汪洋大海中,让我们的东西成为事实的标准。(Android,Chromium, Kubernetes, Vscode) 就想开源让你们见识下大佬是怎么样子的。 个人理解,开源是「手段」,而非「目的」\n对于商业公司而言,如果没有收益,为什么要把花钱雇的人写的内部组件开源出来呢?总不成是为了在B站上博取小朋友的称赞吧。\n而公司内部的组件,往往是与业务共生,高度适配的,藕断丝连,没有那么容易开源的。\n商业公司,只谈收益与预期,如果名声能卖钱,估计也会拿来换取利润。\n更重要的是,开源并不是简单把代码公开出来。\n软件和生物一样,是有生命的,需要长期维护的,而不是一个commit把所有代码一把push 到Github就完事了,或者然后过了几年又push一把,更新几百个文件。\n开源是一个技术与管理结合的决定,需要把开发模式都切换到开源社区,决策过程与动机要对社区可见。\n不仅让人能从代码中读懂功能是「什么」,也要从动机讨论中知道「为什么」要这么改。\n4.4 些许成果 在Github上,收获了495个star,被1108个仓库及18个package 所依赖,而其中Alexander的 spotify-tui 就是我期望做的终端版本的Spotify。\n开源的好处就是,在开发好基础设施之后,自然就会有其他有相同想法的同学,把应用开发出来。\ncrates.io 的统计,总计被下载23w次,当然包括很多CI的重复下载。\n对于有Rust,Spotify,Library等诸多定语的RSpotify来说,目标受众本来就不多,能有现在这样的用户量已远超我最初了预期了。\n5 总结 虽然我未曾从这个项目上获得到一分物质上的回报,但在创建这个项目的时候,我可能不会想到,我能维护它长达五年。\n天上的云,飘来又飘走;开源的项目,挖坑又弃坑。\n视线望不到下一个五年,唯有且行且看。\n6 参考 Spotify is first music streaming service to surpass 200M paid subscribers RSpotify The lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable spotify-tui ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%B8%80%E4%B8%AA%E7%94%A8%E7%88%B1%E5%8F%91%E7%94%B5%E4%BA%94%E5%B9%B4%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","summary":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%","title":"一个用爱发电五年的开源项目"},{"content":"1 前言 在公众号开通10天之际,我已经在公众号写了10来篇文章了,也基本熟悉了公众号的写作,发文以及与用户的互动流程。\n对比个人博客,公众号多了很多额外的限制和规则。 所谓人在屋檐下,不得不低头,选择在公众号这个平台创作,自然只能接受各种合理和不合理的规则。\n在了解清楚规则是「什么」之后,我开始思考「为什么」会有这些规则?许多问题的本质,就藏在诸多的「为什么」后面。\n所以就来分享下我个人对公众号产品规则的思考与见解,仅为一家之言。\n2 审核 我的文章《为什么梦想买不想,故乡回不去》大概有7到8次发布失败,原因都是「审核不通过」。 虽说「自由」是创作的基础,但是既然选择公众号,只能接受就被审核的命运。\n但这里让我抓狂的是,公众号不会像个编辑一样告诉我哪里审核不通过,什么地方需要修改, 只有一句含糊的「此内容因违规无法查看,请前往草稿箱修改后重试」:\n我只能重读文章,把那些我认为「不利于团结」的话逐条删除,然后重新发布,审核不通过,如此循环。\n思考下来,公众号平台不告诉作者哪里审核不通过的原因可能是:\n让作者自我审查,所谓「刑不可知 则威不可测」: 如果告知作者哪里有问题,也就是让作者知道高压线在哪里,作者就会肆无忌惮地在高压线下起舞。 哪天「上面」觉得作者舞姿过于妖娆,那么公众号平台就会一起受罚。\n所以就只告诉作者有高压线,但是这条高压线却是移动且隐形的,只有被电到才知道线在何方,让作者心中时刻紧绷着一根弦。\n指明修改点还可能会让「不利于团结」的内容扩散。 假如一个电影解说公众号,写了一篇解读诺兰蝙蝠侠的文章审核不通过,作者原来以为只是血腥场面审核不通过,没想到公众号却标明审核不通过的是「小丑」。\n作者只会好奇,为什么「小丑」一词会被敏感,就会了解原因。 根据传播学原理,只会导致原来需要被和谐的和「小丑」相关的内容以另类的形式传播起来。\n3 图片素材 把在外部写好的文章复制到公众号编辑器的时候,所有的图片都要上传到公众号的图片素材库上,无法直接使用图片的链接。\n很多程序员(比如我)喜欢把博客搭建在Github Pages上,然后把图片都上传到Github 仓库,然后把博客内容复制到公众号时,就会出现图片获取失败的问题。\n原因无非是伟大的防火墙把Github给墙了,导致微信的服务器无法拉取到Github 的图片。\n问题就来了,为什么公众号要大费周章把图片拉取并保存起来,直接使用图片链接来访问不好么?\n个人理解,主要是出于「用户体验」与「安全」两方面考虑:\n如果让用户通过图片链接直接访问外部的图片服务器,图片链接可能失效,也就是图片无法再访问,就出现「图裂」了的情况,极大地影响用户体验。\n也有可能是,图片服务器能访问,但是访问速度非常慢,用户加载图片速度非常慢,用户只会抱怨微信的公众号怎么这么卡,图片都加载不出来。 用户可不管是外部服务器,还是公众号服务器,反正都是微信卡,骂人只会挑最显眼的来骂。\n另外一种可能是图片链接原来是一张正常的图片,待文章阅读量到10W+之后,就换成一张敏感图片,那么就会防不胜防。\n从系统安全的角度来说,到外部服务器拉取文件,保存并展现给用户,是一个危险的操作,这样的操作应该越少越好,微信这样的APP注定会是各种攻击者的目标。\n所以一次性,把图片拉取到公众号内部的服务器,就是最稳妥的方法\n4 限制外链 在公众号文章里面,是无法插入外部网站链接的,只能引用公众号文章,或者公众号文章链接。\n让我这种习惯在文章末尾写上一大堆引用和参考链接的人无所适从,毕竟不注明引用,就有剽窃之嫌。\n这样的做法,背后的暖心原因大概是:\n避免为外部网站和App引流,避免把用户从公众号的生态,引流到抖音号,小红书号上,避免从平台沦为引流工具,「肉要都烂在锅里」,内容只能在微信生态内流传 安全因素:避免公众号作者把各种诈骗,钓鱼链接放到公众号里面,让用户在公众号上受害,避免成为黑产的温床,保证微信的口碑。 毕竟用户受骗骂人的时候,才不会骂公众号作者,只会骂最显眼的那个,就是公众号平台,乃至微信。 话虽如此,但无法引用外部链接的做法,让我这种崇尚开放Web精神的人,相当难受。\n5 群发次数 对文章,公众号有所谓的「群发」和「发布」之分。\n「群发」是指把文章推送给所有的关注者,并且会出现在作者公众号的主页。而「发布」只是将文章发布出来,不会推送,也不会出现在主页,但是可以被链接到。\n「群发」是限制次数的,一天只能「群发」一次,但是「发布」却是无限制的。\n「群发」的次数限制成一次,我理解是保证用户体验避免过多的公众号消息推送,给用户造成困扰,让公众号成为商户引流推广的工具。毕竟根据墨菲定律,可能会被滥用的规则,就一定要被滥用。\n另外一个是对作者的潜在约束,让作者好好珍惜「群发」的机会,尽量写出好文章。按照《影响力》里提到的「稀缺」原理,机会越少见, 价值似乎越高。\n同样的原理,还运用到「账号详情」信息的修改,限制修改次数,就能让用户审慎修改,审慎着,审慎着,可能就不会修改了。\n对于系统而言,修改1次与修改100次成本几乎无差别,只是通过产品规则,来作限定,人为制造「稀缺」。\n6 限制修改 公众号文章一经「发布」或「群发」,就无法大幅修改内容,只能更正最多20个错别字,这个规则让许多作者大为诟病,认为限制了其修改文章的权利。\n那么为什么会有这条规则呢?\n我个人揣摩,认为是公众号产品认为文章因为是类似报纸书刊等出版物,落笔无悔,一旦写出去的文章,就无法修改(错别字除外)。\n因为公众号是和微信这个聊天工具紧密结合在一起的,如果分享,转发的过程中,文章的内容发生多次变更,就会出现「罗生门」的情况,就是没有人知道文章内容最初的观点究竟是什么。\n那些事后诸葛亮就会跑出来,说自己是事前诸葛亮。 比如那些鼓吹俄罗斯2小时攻陷基辅的军事爱好者们,虽然一年过去了,但是在他们看来,还没有到2小时。\n另外一个就是对「作者」的约束,当你知道你写出去的东西无法修改,你应付审慎对待,文理顺畅,前后呼应的情况才能发布,迫使作者尽量产出高质量的文章。\n7 总结 电视剧《雍正王朝》中,为描绘年羹尧身为大将军,生活奢华,介绍其有一道菜,名为小炒肉。\n虽为小炒肉,但这道菜做法十分复杂,就是在取肉之前,要找数名壮汉对肥猪展开暴力围殴。\n棍棒之下,猪吃痛嚎叫,血管也因此爆裂,猪血会聚集至脊梁附近,由此,厨师才会下刀,将猪杀死后去其尾骨附近的精肉。 据称,此法制作出来的猪肉,因为有鲜血的浸透,吃起来会极为鲜嫩可口,味道不逊色于各类山珍海味。\n所谓的「产品规则」,背后都是「人性」,都是对作者与读者心理的揣摩和把玩。 公众号对作者施加诸多的限制与规则,无非是像壮汉殴猪,希望最终可以取出精肉,以飨读者。\n难怪文章不好看,因为作者是头猪。难怪文章好看,因为作者是头挨过打的猪。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%80%9D%E8%80%83_%E5%85%AC%E4%BC%97%E5%8F%B7%E8%83%8C%E5%90%8E%E7%9A%84%E4%BA%A7%E5%93%81%E9%80%BB%E8%BE%91/","summary":"1 前言 在公众号开通10天之际,我已经在公众号写了10来篇文章了,也基本熟悉了公众号的写作,发文以及与用户的互动流程。 对比个人博客,公众号多了","title":"思考:公众号背后的产品逻辑"},{"content":"1 前言 古人云:「一图胜千言」。 一幅合适的图片可以清晰地向读者表达我们的意图,又因为我们人脑的作用机制,阅读一张图片所耗费的脑力要远少于一段文字,故而我们对图片更加深刻。\n古人又云,「工欲善其事,必先利其器」,那么我就来分享一下我使用得顺手的画图工具与画图技巧。\n2 excalidraw excalidraw 是我最常用的画图工具,是一款开源的手绘画风的画板工具,图形风格是简洁而精美,一经使用,便爱不释手。\n非常适合构建原型或阐述想法\n我见证它在Github上的star数从10k涨至现在的40k,表明众多用户对它的喜爱。\nexcalidraw提供了基本的图形,如矩形,图形,菱形,文本,箭头等,稍经组合,就可以绘制很精美,简洁的图画。\n2.1 涂鸦之作 Hadoop 词频计算:\n数据治理:\n数据未分层:\n数据分层:\n因为excalidraw 相当的灵活,甚至系统循环图我都是使用它来绘制的:\n系统循环图:\n2.2 素材库 如果基本的图形无法满足诉求的话,excalidraw 还提供了在线library,供设计师把他们的图形,图标分享给其他用户。例如系统架构图,AWS组件图,UML图,手绘人物图等等,应有尽有,不一而足。\n素材库:\n(商户系统的头像就是引用自 library)\n2.3 在线协作 excalidraw 还支持端对端加密的在线协作,只需要将一个链接发送给协议方,就能实现画图在线协作:\nhttps://excalidraw.com/#room=91bd46ae3aa84dff9d20,pfLqgEoY1c2ioq8LmGwsFA 在远程会议,需要多方画图协作沟通的时候非常有用。\n2.4 技巧分享 excalidraw 画曲线的技巧\n按住Control/Command, 然后双击线条,进入曲线编辑模式 然后拖动线条,使用Control/Command + D 在末尾增加一个端点,或者使用删除键删除一个端点(留意excalidraw 工具栏下方的操作提示) 绘制曲线:\n我在拙作《我的写作流》中提到过,我倾向「本地化」+ 「文本化」 + 「版本管理」 + 「云同步」的知识管理文案,对于图片管理,我也是类似的倾向。\n因为图片是二进制流,无法做版本管理,所以我一般会把excalidraw 文件保存到本地,保存成xxx.excalidraw 的文件,实际是Json 文本;然后再导出成png, svg 等各种形式的图片文件。\n如果需要修改图片或者复制,剪切,只需要导入xxx.excalidraw,修改保存成新的excalidraw 文件,即可以实现「版本管理」\n原来excalidraw 有个限制,就是一次只能编辑一个excalidraw 文件,经@qisdreamyan 提醒,Vscode的excalidraw 插件支持直接在Vscode 里面编辑excalidraw 文件,那么就可以同时编辑多个文件啦。\n目前excalidraw 美中不足的一点就是,不支持手绘风格的非拉丁文字体,如中文,日文字体等,很早之前就有issue在谈论了,目前还没有什么进展。\n3 graphviz 我主要是用graphviz 来绘制复杂的关系图,timeline图。 它系出名门,出自大名鼎鼎的的AT\u0026amp;T实验室,类似微软出的「Visio」,但两者有个本质的差别。\n就是「Visio」是手动的,需要绘图者指定点线之间的布局,而graphviz 是自动布局的,只要将告知graphviz点与线的关系,graphviz 就能实现「自动布局」。\n如果是绘制简单的布局的图表,「自动布局」与「手动布局」差别不大。\n但如果是绘制复杂的图画,「手动布局」不仅繁琐,还不美观,而「自动布局」都能帮我们轻松搞定,为我们节省非常多的精力。\n不看广告,看疗效,来看下我使用graphviz 画出的图:\n土地财政时间线:\n西方哲学史演进历程:\nGraphviz 官方示例库:\nUnix 家谱:\n数据结构:\n更多更复杂的示例,可见官方的gallery\n3.1 快速入门 graphviz 使用所谓的「dot 语言(language)」这种标记语言来描述图形,然后再由命令行生成图片。\n程序员们可以把这个理解成,从源码编译到可执行文件。\n3.1.1 有向图 (digraph)与无向图 (graph) dot语言支持两种图形,分别是有向图 (digraph)与无向图(graph).\n定义一个无向图\ngraph mygraph { 1 -- 2 -- 3; 2 -- 4; } // graph 标识来定义一个无向图 定义一个有向图:\ndigraph mydigraph { 1 -\u0026gt; 2 -\u0026gt; 3; 2 -\u0026gt; 4; } // digraph 标识来定义一个无向图 命名规范与C家族的编程语言类似:图形关系定义在花括号{} 中;每条语句以 ; 结尾; // 表示单行注释, /**/表示多行注释\n3.1.2 节点(node) mydigraph 是图形名,1, 2 是节点名(node), 两个节点构成一条边(edge)。在图的定义中,相同的名称就代表同一个节点。\n当dot 编译器遇到一个新的名称,就认为是新的节点\n3.1.3 属性(property) 属性可以设置在节点和边上,通过「方括号 []」来定义属性,属性之间用英文逗号分隔。\n属性的定义采用如下的格式:\n属性名 = 属性值 常见的属性有:\nlabel: 标题 color: 颜色 style: 样式 shape: 形状 strict graph { // 设置节点属性 1 [shape=box]; 3 [shape=triangle]; // 设置边属性 1 -- 2 [color=blue]; 1 -- 3 [style=dotted]; } 属性还可以作用于图(graph)上,常用的属性包括:\nlabel:标题 bgcolor:颜色 fontname:字体名称(【不】影响节点和连线) fontsize:字体大小(【不】影响节点和连线) fontcolor:字体颜色(【不】影响节点和连线) center:是否居中绘制 digraph graph_attr { graph[bgcolor=\u0026#34;yellow\u0026#34; label=\u0026#34;标题\u0026#34; fontsize=24 fontcolor=\u0026#34;green\u0026#34;]; 1 -\u0026gt; 2; 1 -\u0026gt; 3; } 更多的属性可见官网:Attributes\n3.1.4 子图(subgraph) subgraph 的作用主要有 3 个:\n表示图的结构,对节点和边进行分组 提供一个单独的上下文设置属性(类似操作系统里面不同的线程,有不同的线程变量) 针对特定引擎使用特殊的布局。比如下面的例子,如果 subgraph 的名字以 cluster 开头,所有属于这个子图的节点会用一个矩形和其他节点分开。 digraph graphname{ a -\u0026gt; {b c}; c -\u0026gt; e; b -\u0026gt; d; subgraph cluster_bc { bgcolor=red; b; c; } subgraph cluster_de { label=\u0026#34;Block\u0026#34; d; e; } } 3.1.5 图布局(layout) 默认情况下图是从上到下布局的(rankdir \u0026quot;TB\u0026quot;),通过设置 rankdir\u0026ldquo;LR\u0026rdquo; 可以让图从左到右布局。\n默认布局(From top to bottom)\ndigraph { rankdir=\u0026#34;TB\u0026#34; a -\u0026gt; b -\u0026gt; c; } From Left to right:\ndigraph { rankdir=\u0026#34;LR\u0026#34; a -\u0026gt; b -\u0026gt; c; } 该属性只针对图(graph)生效.\n3.1.6 示例 再回头看下,「土地财政时间线」这图的源代码:\ndigraph 土地财政时间线 { size=\u0026#34;7,8\u0026#34;; node [fontsize=24, shape = plaintext]; 1976 -\u0026gt; 1985; 1985 -\u0026gt; 1994; 1994 -\u0026gt; 1998; 1998 -\u0026gt; 1999; 1999 -\u0026gt; 2000; 2000 -\u0026gt; 2001; 2001 -\u0026gt; 2002; 2002 -\u0026gt; 2008; 2008 -\u0026gt; 2009; 2009 -\u0026gt; 2014; 2014 -\u0026gt; 2015; node [fontsize=20, shape = box]; { rank=same; 1976 \u0026#34;改革开放\u0026#34;; } { rank=same; 1985 \u0026#34;财政包干\u0026#34;; } { rank=same; 1994 \u0026#34;分税制改革\u0026#34;; } { rank=same; 1998 \u0026#34;住房商品化改革\u0026#34; \u0026#34;《中华人民共和国土地管理法》实施\u0026#34;; } { rank=same; 1999 \u0026#34;土地财政兴起\u0026#34;; } { rank=same; 2000 \u0026#34;工业化\u0026#34; \u0026#34;城市化\u0026#34; \u0026#34;土地金融\u0026#34;; } { rank=same; 2001 \u0026#34;房价\u0026#34;; } { rank=same; 2002 \u0026#34;所得税改革\u0026#34;; } { rank=same; 2008 \u0026#34;金融危机\u0026#34; \u0026#34;四万亿刺激\u0026#34;; } { rank=same; 2009 \u0026#34;房地产与基建投资激增\u0026#34;; } { rank=same; 2014 \u0026#34;产能积压\u0026#34; \u0026#34;库存过剩\u0026#34;; } { rank=same; 2015 \u0026#34;棚改货币化\u0026#34; \u0026#34;涨价去库存\u0026#34; ; } \u0026#34;改革开放\u0026#34; -\u0026gt; \u0026#34;财政包干\u0026#34;; \u0026#34;财政包干\u0026#34; -\u0026gt; \u0026#34;分税制改革\u0026#34;[label=\u0026#34;地方政府收入下降\u0026#34;]; \u0026#34;分税制改革\u0026#34; -\u0026gt; \u0026#34;所得税改革\u0026#34;[label=\u0026#34;地方政府占比下降\u0026#34;]; \u0026#34;所得税改革\u0026#34; -\u0026gt; \u0026#34;土地金融\u0026#34;[label=\u0026#34;促进\u0026#34;] \u0026#34;分税制改革\u0026#34; -\u0026gt; \u0026#34;土地财政兴起\u0026#34;[label=\u0026#34;推动\u0026#34;] \u0026#34;土地财政兴起\u0026#34; -\u0026gt; \u0026#34;工业化\u0026#34;; \u0026#34;土地财政兴起\u0026#34; -\u0026gt; \u0026#34;城市化\u0026#34;; \u0026#34;土地金融\u0026#34; -\u0026gt; \u0026#34;城市化\u0026#34;[label=\u0026#34;促进\u0026#34; color =\u0026#34;red\u0026#34;]; \u0026#34;土地金融\u0026#34; -\u0026gt; \u0026#34;房价\u0026#34;[label =\u0026#34;推高\u0026#34;] \u0026#34;城市化\u0026#34; -\u0026gt; \u0026#34;房价\u0026#34;[label =\u0026#34;推高\u0026#34;] \u0026#34;土地财政兴起\u0026#34; -\u0026gt; \u0026#34;土地金融\u0026#34;[label=\u0026#34;地方政府收入增加\u0026#34;]; \u0026#34;土地金融\u0026#34; -\u0026gt; \u0026#34;工业化\u0026#34;[label=\u0026#34;促进\u0026#34; color =\u0026#34;red\u0026#34;]; \u0026#34;住房商品化改革\u0026#34; -\u0026gt; \u0026#34;土地财政兴起\u0026#34;[label=\u0026#34;停止福利分房\u0026#34;]; \u0026#34;《中华人民共和国土地管理法》实施\u0026#34; -\u0026gt; \u0026#34;土地财政兴起\u0026#34;[label=\u0026#34;限制农业用地非农用途\u0026#34;]; \u0026#34;金融危机\u0026#34; -\u0026gt; \u0026#34;四万亿刺激\u0026#34;; \u0026#34;四万亿刺激\u0026#34; -\u0026gt; \u0026#34;房地产与基建投资激增\u0026#34;[label=\u0026#34;宽松货币政策\u0026#34;] \u0026#34;房地产与基建投资激增\u0026#34; -\u0026gt; \u0026#34;产能积压\u0026#34;; \u0026#34;房地产与基建投资激增\u0026#34; -\u0026gt; \u0026#34;库存过剩\u0026#34;; \u0026#34;房地产与基建投资激增\u0026#34; -\u0026gt; \u0026#34;土地金融\u0026#34;[label=\u0026#34;强化\u0026#34;]; \u0026#34;产能积压\u0026#34; -\u0026gt; \u0026#34;涨价去库存\u0026#34;; \u0026#34;库存过剩\u0026#34; -\u0026gt; \u0026#34;涨价去库存\u0026#34;; \u0026#34;棚改货币化\u0026#34; -\u0026gt; \u0026#34;房价起飞\u0026#34;; \u0026#34;涨价去库存\u0026#34; -\u0026gt; \u0026#34;房价起飞\u0026#34;; \u0026#34;房价\u0026#34; -\u0026gt; \u0026#34;房价起飞\u0026#34;[label =\u0026#34;逐年上涨\u0026#34;]; } 3.1.7 编辑器支持 如果是Emacs 用户,可以使用graphviz-dot-mode 来编辑并预览生成的图片,效果如下:\n虽然我是重度Emacs 用户,但是在Emacs上实时预览图片效果并不好。\nEmacs对查看图片功能支持不够强大,无法通过鼠标放大缩小,并实时预览图片。\n如果需要实时预览graphviz 生成的图片,我个人更加推荐使用Vscode + graphviz 插件 :\n4 plantuml 身为程序员,免不了撰写各种设计方案,绘制各种序列图,类图,活动图,状态机图等等各种UML图。\n而plantuml 就是这样一个绘图组件,支持绘制各种程序开发需要用到的图。\nplantuml 依赖的底层组件就有前文提到的graphviz,所以plantuml的语法也类似graphviz, 通过自定义的标记语言,来描述不同图形之间的关系,「自动布局」并绘制。\n学过UML规范的同学应该都知道这些图应该怎么画,我就拿几个常见的图来举个例子。\n4.1 时序图 plantuml 提供不同的组件供时序图使用。不同的组件有不同的形状,默认情况下,组件的声明顺序就是他们的展示顺序。\n使用-\u0026gt; 来表示在两个组件/参与者(participant) 之间传递消息,\u0026lt;-- 表示回包信息。\n@startuml participant Participant as Foo actor Actor as Foo1 boundary Boundary as Foo2 control Control as Foo3 entity Entity as Foo4 database Database as Foo5 collections Collections as Foo6 queue Queue as Foo7 Foo -\u0026gt; Foo1 : To actor Foo -\u0026gt; Foo2 : To boundary Foo -\u0026gt; Foo3 : To control Foo -\u0026gt; Foo4 : To entity Foo -\u0026gt; Foo5 : To database Foo -\u0026gt; Foo6 : To collections Foo -\u0026gt; Foo7: To queue Foo \u0026lt;-- Foo7: Response from queue @enduml 时序图的更多用法可见官网文档:Sequence-Diagram\n4.2 活动图 @startuml start repeat :Test something; if (Something went wrong?) then (no) #palegreen:OK; break endif -\u0026gt;NOK; :Alert \u0026#34;Error with long text\u0026#34;; repeat while (Something went wrong with long text?) is (yes) not (no) -\u0026gt;//merged step//; :Alert \u0026#34;Success\u0026#34;; stop @enduml 4.3 编辑器 如果需要实时预览,个人推荐Vscode + plantuml插件来绘制plantuml 图,所见即所得,实时预览,并提供代码补全:\n5 matplotlib 这就是个绘图库了,主要是用来绘制各种图表,比如折线图,饼图,直方图等,通常是配合数据分析使用,还支持xkcd 风格。\n之前在上MIT 6.00网课的时候,John Guttag教授出了一个概率统计题,一个醉汉每次向四个方向中任意一个方向走一步,500步后,醉汉是离原点越来越近呢,还是越来越远?\n下面是Python代码实现,模拟醉汉行为:\n# 运行下面代码前,确保依赖已安装 # pip3 install matplotlib --user import math, random import matplotlib.pyplot as plt plt.xkcd() class Location(object): def __init__(self, x, y): self.x = float(x) self.y = float(y) def move(self, xc, yc): return Location(self.x + float(xc), self.y + float(yc)) def getCoords(self): return self.x, self.y def getDist(self, other): ox, oy = other.getCoords() xDist = self.x - ox yDist = self.y - oy return math.sqrt(xDist**2 + yDist **2) class CompassPt(object): possibles = (\u0026#39;N\u0026#39;, \u0026#39;S\u0026#39;, \u0026#39;E\u0026#39;, \u0026#39;W\u0026#39;) def __init__(self, pt): if pt in self.possibles: self.pt = pt else: raise ValueError(\u0026#34;in CompassPt.__init__\u0026#34;) def move(self, dist): if self.pt == \u0026#34;N\u0026#34;: return (0, dist) elif self.pt == \u0026#34;S\u0026#34;: return (0, -dist) elif self.pt == \u0026#34;E\u0026#34;: return (dist, 0) elif self.pt == \u0026#34;W\u0026#34;: return (-dist, 0) else: raise ValueError(\u0026#34;in CompassPt.move\u0026#34;) class Field(object): def __init__(self, drunk, loc): self.drunk = drunk self.loc = loc def move(self, cp, dist): oldLoc = self.loc xc, yc = cp.move(dist) self.loc = oldLoc.move(xc,yc) def getLoc(self): return self.loc def getDrunk(self): return self.drunk class Drunk(object): def __init__(self, name): self.name = name def move(self, field, time = 1): if field.getDrunk() != self: raise ValueError(\u0026#34;Drunk.move called with drunk not in field\u0026#34;) for i in range(time): pt = CompassPt(random.choice(CompassPt.possibles)) field.move(pt, 1) def performTrial(time,f ): start = f.getLoc() distances = [0.0] for t in range(1, time+1): f.getDrunk().move(f) newLoc = f.getLoc() distance = newLoc.getDist(start) distances.append(distance) return distances def firstTest(): drunk= Drunk(\u0026#34;Homser Simpson\u0026#34;) for i in range(5): f = Field(drunk, Location(0, 0)) distances = performTrial(500, f) plt.plot(distances) plt.title(\u0026#34;Homer\u0026#39;s random Walk\u0026#34;) plt.xlabel(\u0026#34;Time\u0026#34;) plt.ylabel(\u0026#34;Distance from origin\u0026#34;) fname = \u0026#34;images/mit6.00/simulation_random_walk_trail1.png\u0026#34; plt.savefig(fname) return fname return firstTest() 模拟5次,生成出来的xkcd风格的图表:\n5.1 再话org-mode 在《我的写作流》里面,我有提到过,我使用Emacs + org-mode 来编写文章,对比markdown 或者其他的标记语言,org-mode 有一个巨大的优势,就是org-mode 借助内置的org-babel 组件,可以直接运行代码。\n在markdown 里面,下面的代码块的用处仅仅是语法高亮:\n```python print(\u0026#34;helloworld\u0026#34;) ``` 但在 org-mode, 下面的代码块是可运行的,我只要在Emacs中按下C-c C-c,就会运行代码,并输出helloword。\n#+begin_src python print(\u0026#34;helloworld\u0026#34;) #+end_src 看起来作用不大,但是和 graphviz, plantuml, matplotlib 结合,就会产生无穷的威力:只要我把绘图源码写好,然后再按下 C-c C-c,就能自动生成图片,并自动插入到当前这篇文章中(当然,如果代码写错了,是编译生成不出图片的)。\n根本不需要手动编译,生成图片,然后再把图片以markdown格式手动插入: ![图片](链接) 。\n上面的概率统计模拟图也是这样生成出来的,写好Python 代码,然后按下 C-c C-c\n一切都浑然天成。\n6 那些年,我使用过的绘图工具 都是曾经使用过,现在也基本弃用的工具:\nWord:最开始时也不知道什么画图工具,就使用Word 来画图。 PPT:写技术方案基本不用了,画PPT做分享和述职,就还只能继续使用。 drawio: 功能丰富,但图形有种说不出的丑,并且绘制起来不顺手 processon: 在线绘图服务,免费版本有绘画张数限制(不记得是10张还是15张);对于Saas服务而言,数据不属于用户。公司倒闭或限制用户,就有丢失数据风险。 图表与文章一样,都是资产。\n对于这样的重要资产,我还是倾向于「本地化」+ 「文本化」+ 「版本管理」+ 「云同步」的方案,保证图表既易于修改,又无丢失风险。\n7 结语 金庸笔下的「独孤求败」的用剑之道:「四十岁后,不滞于物,草木竹石均可为剑。自此精修,渐进于无剑胜有剑之境」\nexcalidraw, graphviz 也好, plantuml, org-mode 也罢,只是「器」,都只是用来表达想法与智慧的工具。\n所谓「飞花摘叶皆可伤人,草木竹石均可为剑」,真正的大牛,即使不使用画图工具,寥寥数语就会把一个复杂的概念解释得清楚明了。\n厚积而薄发,选择合适的「剑」很重要,但「内功」的修炼同样重要。\n8 参考 Graphviz Documentation Graphviz 入门指南 【自动】绘图工具 Graphviz ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%88%91%E7%9A%84%E7%94%BB%E5%9B%BE%E6%B5%81/","summary":"1 前言 古人云:「一图胜千言」。 一幅合适的图片可以清晰地向读者表达我们的意图,又因为我们人脑的作用机制,阅读一张图片所耗费的脑力要远少于一段文","title":"我的画图流:画图工具与技巧分享"},{"content":"1 前言 年后在家略为空闲,就写了篇思考与感悟:\n「上面的意思是好的,就是下面的人执行出了问题」\n「下面的人也没办法,毕竟这是上面的规定」\n那事情没做好,究竟是谁的原因?\n「上面」与「下面」的关系与内在逻辑究竟是怎么样的呢?\n让我们翻开中国漫长的历史,一探皇帝与官僚之间的关系.\n2 牧民之道 皇帝是国家的最高统治者,号为「天子」,代表上天来统治臣民,具有至高无上的权力。\n虽然想当皇帝的人有很多个,但皇帝这个岗位实际只能有一个人在职.\n「普天之下,莫非王土,率土之滨,莫非王臣」,这么多的「王臣」与「王土」,皇帝一个人自然是管理不过来,就需要三公九卿等文武百官。\n封建官僚拿着皇帝给予的权力和薪水,逐级管理着小农。\n最底层是万千小农,他们对帝国纳税,用自己的血汗钱养活帝王与封建官僚。\n3 利益之差 封建官僚是一种“压力单向传导机制”,压力只能逐级向下传导,封建官僚不但不会分散压力,而且会为了自身利益扩大这种压力。这样,压力传导到最后的小农便会呈几何级数扩张。\n疫情期间,各地都出现「层层加码」的现象,这也是一种压力单向传导机制作用的现象。\n对封建官僚来说,假如中央要求州上缴赋税10万石,每个郡平均上缴1万石;每个州向郡下达任务时,可能会要求上缴10万石,每个县平均上缴5千石;郡向县下达任务,可能就会要求每个县上缴一万石;\n这样既能满足上级的要求,又可以截留剩余的税赋,还师出有名,可以把锅甩给上级。\n说到底对国家财富具有所有权的,是帝王,天下是一家一姓的地盘,民不聊生对他们也没有好处。\n封建官僚却完全不同,只要达到目的,管你大浪滔天,反正又不是我的天下。\n3.1 王朝周期循环 中国封建王朝轮回更替两千多年, 尽管每朝每代制度不尽相同,但是总逃脱不了所谓的王朝周期循环:\n阶段1:新朝初立,连年征战国家元气损伤,人口凋零,开国君主多是刀枪入库,马放南山,休养生息,鼓励民众生产,轻徭薄赋 阶段2: 王朝强盛,人口剧增,土地兼并,社会阶级逐步分化 阶段3: 王朝末期,土地集中在少数人手上,农民失去土地,失去工作,成为流民,揭杆而起。一切变为废墟,破而后立,直至新朝建立, 开启新一轮循环。 如果从封建官僚的角度来分析王朝周期循环的原因:\n皇权只有借助封建官僚才能统治整个帝国,但是封建官僚自身就是一个强势分利集团,他们会借助手中的权力疯狂掠夺帝国财富。\n皇权根本就无法彻底遏制这种掠夺,毕竟联系皇权和封建官僚的纽带恰恰就是掠夺财富的权力。\n封建官僚对财富的掠夺将成为帝国难以治愈的沉疴。\n强势分利集团完全不遵守财富规则,毕竟他们既是运动员,又是裁判,又怎么会遵守规则呢?\n最终,掠夺超出了帝国居民承受的极限,人们失去了土地、失去了工作、没有能力组建家庭,最终成为流民。\n3.2 官僚利益 总有人奇怪,为什么明朝皇帝崇祯不像唐玄宗,宋高宗那样,在贼兵临城时,跑路迁都南京。\n如果再翻开史料,重现当时的场景,就会发现,既有崇祯主观的因素,也有官僚在背后推波助澜。\n崇祯十七年,当时李自成已兵临北京,形势危急,右庶子李明睿劝崇祯放弃北京,尽快南迁,皇帝告诉他:“汝意与朕合,但外边诸臣不从,奈何?”\n李明睿说:“天命微密,当内断圣心,勿致噬脐之忧。”并请崇祯勿犹豫,尽快决断。\n崇祯帝一直有意迁都,崇祯对众臣说:“李明睿有疏劝朕南迁。国君死于社稷,朕将何往?又劝朕教太子先往南京,诸卿以为如何?”\n首辅陈演反对南迁,并示意兵科给事中光时亨,严厉谴责李明睿,扬言:“不杀李明睿,不足以安定民心。”其事遂不了了之\n为什么官员们把调子提得这么高呢,莫非迁都真的事不可为?\n平心而论,崇祯着实算不上一位好老板,性格存在缺陷,求治心切,生性多疑,刚愎自用,并且有不少恶劣的前科:\n「崇祯十五年, 松山、锦州失守,洪承畴降清,崇祯想和满清议和而和兵部尚书陈新甲暗中商议计划, 没想到事情泄漏,被朝臣知晓,明朝士大夫鉴于南宋的教训,皆以为与满人和谈为耻。\n崇祯就把议和责任都推到兵部尚书陈新甲, 并将其下狱斩首。」\n面对这样性格的老板,即使知道迁都是个明智之选,官僚们明面上都不会赞同这样的建议。\n如果能打退贼兵,老板如果面子挂不住,又要追究迁都的责任,那我们附和迁都建议的都可能完蛋。如果打不退贼兵,双膝朝下,跪谁不是跪,只是换个老板而言。\n所以对于官僚来说,最佳的选择就是把调子拨得高高的,君王死社稷,我们都要和陛下共存亡,死战不退。这样官员们就能立于不败之地。\n崇祯十七年,李自成入北京,崇祯皇帝自缢于煤山,首辅陈演想逃离北京,但因家产太多而未果。他主动向农民军献白银四万两。稍后,其家仆告发,说他家中地下藏银数万。农民军掘之,果见地下全是白银。\n另一民变领袖张献忠后来称帝,以陈演的女儿为皇后,陈演的儿子为翰林学士。\n想起韩国电影《辩护人》中关于爱国者的论述:\n你不是真正的爱国者\n你是让善良无罪的国家生病的蛆虫,军事政权肮脏的帮手而已\n说出真相,那才是真正的爱国\n官僚利益与皇帝利益并不一致,管你洪水滔天,还是民不聊生,反正民不是我家的,国不是我家的,钱才是我家的。\n3.3 服从性测试 对于皇帝而言,官员的能力,品格,尚在其次,皇帝最关心的是忠诚。毕竟品格越高尚,能力越出色,但是怀有二心,对皇帝的危害就越大。\n所以皇帝就派自己家奴去监视官员。但俗话说,「人心隔肚皮」,皇帝也没有办法知道官员的心思。皇帝是想到的办法,就和我们玩狼人杀一样,就是「观其言,察其行」,看你们是否表里不一。\n所以对于官员们「层层加码」的行为,就能明白其背后的逻辑:对于「上面」的命令,「下面」的执行不到位,就容易被理解是对「上面」不忠诚。\n所谓「忠诚不绝对,就是绝对不忠诚」,何况即使你100%执行到位,隔壁的同行150%执行到位。相比之下,你就变成执行不到位,就容易「内巻」起来,变成相互竞争加码。\n对于官僚而言,正确的命令要坚决执行,不正确的命令也要坚决执行。前者容易理解,后者又有什么解究呢?\n对于不正确的命令,如果官员执行到位,他不会有任何的错,因为错在「上面」。但因为「上面」不可能出错,所以就变成了大家都没有错。如果执行不到位,那么「上面」就可以认为,是执行有问题,而不是决策有问题,其罪在你。\n因此,权衡之下,官员的最佳选择就是加倍执行命令,无论对错,甚至还可以夹带点私货。\n何况,不正确的命令也是一种服从性测试。如果君王知道你在民怨沸腾的情况,还把不正确的命令也如实执行了,会认为你对君王忠心不二,简在帝心。\n不正确的命令也要执行,那小农的死活怎么办?\n权力运行自有其规律:**权力只对来源负责**.\n老爷心善,见不得穷人, 把他们赶走吧。\n4 决策权与信息权之争 皇帝拥有至高无上的权力,是帝国的主人,对帝国的政策拥有最终的决策权。\n拥有决策权,并不意味着皇帝都能在问题上作出正确的抉择,官僚们只能俯首听令。\n面对皇帝的政令,官僚们自有应对之策,皇帝拥有决策权,但做出决策的前提是有充足的信息,对问题有充分的了解。但皇帝囿于深宫之中,又如何能知天下事呢,只能听从官僚们的汇报,所以说,影响皇帝决策的信息权,掌握在官僚手上。\n官僚们可以通过隐藏,截留,篡改信息,以误导皇帝作出有利于自己的决策。\n《天朝的崩溃》就有提到,原来在鸦片战争期间,直到英军直逼天津大沽口, 道光皇帝才知道战事之靡烂,防备之严峻,而北京几乎无险可守,此前道光皇帝还一直以为清军战事占优。\n而皇帝对官僚们隐瞒信息的招数,自然是心知肚明,所以自明朝起,皇帝就开设东厂与锦衣卫,通过特务机构来打破封建官僚的信息垄断。\n受电视剧影响,我们总以为东厂首领权倾朝野,对百官予取予求。但本质上,东厂首领钦差掌印太监,只是皇帝的家权,他们的权力都来源于皇权,所以他们忠诚的对象只能是皇帝。又因为他们生理缺陷,无法生育,也无法将权力延续,自然不会对皇权产生威胁。\n即使是明熹宗权倾朝野的九千岁魏忠贤,在崇祯登基后,一纸诏书就将其赐死。\n通俗理解,在明朝以前,是官僚集团与皇帝在玩二人转;皇帝招架不过来,就拉了些信得过的家奴,和官僚一起斗地主。\n在现代社会,各种媒体资讯发达,官僚已经无法垄断信息权。\n想起英剧《是,大臣》里面的一个情节,外交部大臣竟然是通过电视来了解最新的国际时事的,既滑稽又现实。毕竟记者都跑得比较快。\n4.1 信息反馈与决策 信息权无法再被垄断,但还可以人为制造信息的茧房:\n王小波先生在《沉默的大多数》中有一篇文章,名为《花剌子模信使问题》:\n据野史记载,中亚古国花剌子模有一古怪的风俗,凡是给君王带来好消息的信使,就会得到提升,给君王带来坏消息的人则会被送去喂老虎。\n于是将帅出征在外,凡麾下将士有功,就派他们给君王送好消息,以使他们得到提升;有罪,则派去送坏消息,顺便给国王的老虎送去食物。\n虽说信使带来的消息的好坏,决定人并不是信使本身,并不妨碍君王把他们当作老虎的点心。\n正常人都会趋利避害,长此下去,君王只会听到各种花团锦簇的好消息,不会听到任何反映现实的坏消息,就这样活在了自己制造的信息茧房里。缺乏现实感,又怎能作出正确的决定的呢。\n唐太宗李世民曾说过:「以铜为鉴,可以正衣冠;以人为鉴,可以明得失;以史为鉴,可以知兴替」。\n所谓的「以人为鉴」就是指臣下能如实说清事实,指明对错。君主要有容人之量,臣下要有犯颜直谏的勇气。\n即使魏征经常把李世民怼得脑壳疼,甚至把李世民的宠物鸟都憋死了(太宗怀鹞),也没见魏征去当老虎点心。\n「以人为鉴」既要有识人之明,又要有容人之量,更要有自省之心。只愿开美颜,就只会看到美照,苏大强都觉得自己是吴彦祖。\n5 出路在何方 抗日战争胜利前夕, 1945年7月1日至5日,黄炎培等人访问延安,试图调解国共关系,化解政治危机。\n7月4日下午,毛泽东接待黄炎培等人,黄炎培说:“中国历史上的王朝都存在一个从兴起到消亡的周期率,一部历史,「政怠宦成」的也有,「人亡政息」的也有,「求荣取辱」也有, 总之没有能跳出这「周期率」。\n毛泽东欣然笑对道:“我们已经找到新路,我们能够跳出这「周期率」。这条新路,就是民主,(我们要)用民主来打破历代从艰苦创业到腐败灭亡的「周期率」,跳出这种兴亡「周期率」。”\n6 参考 《中国是部金融史》 维基百科:崇祯帝 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E7%9A%87%E5%B8%9D%E4%B8%8E%E5%AE%98%E5%83%9A/","summary":"1 前言 年后在家略为空闲,就写了篇思考与感悟: 「上面的意思是好的,就是下面的人执行出了问题」 「下面的人也没办法,毕竟这是上面的规定」 那事情没做","title":"皇帝与官僚:「上面」与「下面」"},{"content":"1 前言 自大学起,已经写过好几年的东西,写作工具和流程也反复折腾过好多回. 目前的写作流程已经很流程顺手,所以想分享一下。\n2 写作工具 我不倾向任何专用格式的写作软件(如Word), 或者笔记云服务(EverNote,xx笔记等)。\n因为前者只能使用专门的软件打开,和该软件绑定,会导致文本内容被专用的软件绑架。 可能过个10年,可能该软件不再流行,你甚至无法打开自己的文章。\n对于笔记服务,你的文章数据甚至不掌握在你手里,该服务停用,你的数据就会全部丢失。\n所以我倾向于「本地化储存」+「文本格式」 + 「易于同步」的写作方案\n最后我选择的写作工具是 Emacs + org-mode,不熟悉org-mode 的读者可能不知道 org-mode 是什么,可能理解成类似是markdown 的标记语言,但是和Emacs 结合后,易用性与扩展性比markdown 提高了一个数量级。\n这篇文章就是使用 Emacs + org-mode 写出来的:\n3 写作平台 3.1 博客 在大学时期,我使用的是 org-page + org-mode 来自建博客在 Github Pages, 写了大概一年的博客文章。\n后来因为 org-page 总是存在各种奇怪的小bug,博客样式也只有那么几种,不能满足折腾欲望很强烈的我 (这种行为和年少时喜欢在QQ空间换皮肤差不多)。\n大概是2018年,我当时学习了Rust,打算找个机会来练手,然后就使用Rust搭建了个人的博客,开源在Github上,还有100+的star,就这样,在自己的博客上又写了几年文章。\n直到2022年,因为工作太忙碌,没有太多时间来维护博客的代码以及运维博客的服务。 自建博客的初衷只是为了有个地方可以承载我写的内容,只是因为年轻,有精力折腾,就自己写代码,搭建服务。\n但总体而言,写作的流程并不顺手: 因为我一直都是使用org-mode 来写作,而我自己的博客只支持markdown 格式,这就意味着我必须每次写作完成后,需要将org-mode 转换成markdown , 然后再在博客后台发布。\n如果涉及到图片就更麻烦,需要将图片逐张上传到图床,然后再插入到org-mode中, 而博客使用的markdown 编辑器过了5年后,开发者已经不维护了,所以免不了又会有各种小问题。\n我又尝试了Emacs + org-mode + hugo 的博客方案,发现使用起来非常舒心,发布文章几乎无成本。正好碰上博客服务器到期,就干脆切换回Github Pages.\nhugo 是使用markdown 来构建网站的框架,而我使用的org-mode,所以还需要一个工具将org-mode 转换为hugo markdown,这个就是Emacs 插件ox-hugo。当然, hugo 原生也支持org-mode, 只是功能支持不完整, 不及ox-hugo.\nox-hugo 只需要将在org-mode 内容的最开始插入标记:\n#+HUGO_BASE_DIR: ~/code/org/ramsayleung.github.io #+HUGO_SECTION: post/2023 #+HUGO_CUSTOM_FRONT_MATTER: :toc true #+HUGO_AUTO_SET_LASTMOD: t #+HUGO_DRAFT: false #+DATE: [2023-01-25 Wed 14:25] #+TITLE: 我的写作流 #+HUGO_TAGS: writing #+HUGO_CATEGORIES: writing Emacs一个快捷键 C-c C-e H h 就可以将org-mode 转换成hugo 格式的markdown:\n结合Github Action,就可以自动把导出的markdown 博文部署到Github Pages,称得上是一键部署。\n不过,说出来有点心酸,我在博客写的文章,读者大多是我自己。\n3.2 公众号 在过新年的时候,闲来无事,新开了一个公众号「宫孙说」,用来「一鱼多吃」,将自己文章也转到公众号。\n因为Richard Stallman的影响,我一直是倾向于开放,自由的软件与生态,所以对于公众号这样仅限于「微信」封闭生态的平台不感冒。\n另外,关于「公众号」的看法,我是与陈皓一致的,详见他的文章《为什么我不在微信公众号上写文章》\n但不可否认的是,「公众号」是国内最完善的创作平台,有完整的分享,阅读生态。\n毕竟我不只写技术文章,还会写一些历史,人文类的文章,我希望可以分享给朋友,但Github Pages会间歇性被墙,朋友们大多是在微信阅读文章,虽然博客支持移动端,但是体验终究不及「公众号」。\n屈服于现实压力,我最终开了这个公众号。\n3.3 知乎 知乎也是国内创作氛围相对自由和蓬勃的平台,本着「一鱼多吃」的心态,我也会把文章转载到知乎上。\n3.4 KM 这是公司内部的知识交流创作平台,算是私域流量。有点难以置信的是,在这个读者数量远不如外部的平台,我写的文章有最多的阅读与收藏. 或许是因为我写的内容贴近业务,是同事比较感兴趣的。\n3.5 一鱼多吃的问题 将同一篇文章分发到多个平台,就难免会有不同平台格式不通的问题。\n上面这些平台,都是不支持 org-mode 这种相对小众的文本格式,所以 markdown 算是比较理想的中间格式,可以先把org-mode 转换成hugo markdown,再将hugo markdown 转换成markdown:\n使用 ox-hugo,可以将org-mode 转换成hugo markdown , 至于将hugo markdown 转换成markdown, 我写了个脚本 hugomd2md.sh来处理:\n除了将hugo markdown 转换成markdown,使用Github Pages 当作图床,还在最后插入一个公众号广告。\n4 写在最后 因为使用Emacs + org-mode 写文章非常顺手,所以我写下了很多的文稿,只是思绪杂乱,不方便都发布出来.\n现在正逐步将存稿往公众号平台上迁移,只是公众号有诸多限制,其中一条是一天只能群发一篇文章,只能「小刀锯大树」,慢慢来了。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%88%91%E7%9A%84%E5%86%99%E4%BD%9C%E6%B5%81/","summary":"1 前言 自大学起,已经写过好几年的东西,写作工具和流程也反复折腾过好多回. 目前的写作流程已经很流程顺手,所以想分享一下。 2 写作工具 我不倾向任何","title":"我的写作流"},{"content":"1 前言 有心仪工作的城市房价太高,而房价合适的城市没有心仪的工作。\n梦想买不起,故乡回不去。\n眼看着大城市一座座高楼拔地而起,却难觅容身之所。\n为什么房子这么贵?为什么归属感这么低?为什么非要孤身在外地闯荡,不能和父母家人在一起?\n这些问题都与地方政府推动经济发展的模式有关,《置身事内》这本书都给出了自己的解答。\n结合书中的内容和我自己的阅历,我也有了些自己的浅薄见解,我希望把作者的书,读成我自己的书。\n先把形成这个结果的历史原因与决策摆出来:\n2 改革开放 1978年12月18日中共十一届三中全会后,开始实施的一系列以经济为主的改革措施,可总结为“对内改革,对外开放”。\n这是一切故事的起点。\n3 分税制改革 办事要花钱,如果没钱,话说得再好听也难以落实。钱从哪来,从税里来。所以要真正理解政府行为,必然要了解财税\n3.1 财政包干:1985-1993 财政承包始于1980年,中央与省级财政之间对收入和支出进行包干,地方可以留下一部分增收。\n1980—1984年是财政包干体制的实验阶段,1985年以后全面推行,建立了“分灶吃饭”的财政体制.\n既然是承包,当然要根据地方实际来确定承包形式和分账比例,所以财政包干形式五花八门,各地不同。比较流行的一种是“收入递增包干”。\n以1988年的北京为例,是以1987年的财政收入为基数,设定一个固定的年收入增长率4%,超过4%的增收部分都归北京,没超过的部分则和中央五五分成。假如北京1987年收入100亿元,1988年收入110亿元,增长10%,那超过了4%增长的6亿元都归北京,其余104亿元和中央五五分成。\n广东的包干形式更简单,1988年上解中央14亿元,以后每年在此基础上递增9%,剩余的都归自己。也就是说,如果后续广东的财政收入增长高于9%, 那么能留下的钱就会越来越多(事实也是如此)\n财政承包制下,交完了中央的,剩下的都是地方自己的,因此地方有动力扩大税收来源,大力发展经济。\n3.1.1 问题:地方越富,中央越穷 税制是这样设计,随着时间的推移,却出现了中央财政越来越穷的问题。\n正常人都希望是自己手上的钱越来越好,要交的钱越少越好,地方政府也不例外。\n一方面,地方政府控制预算收入增长,避免增长过快,毕竟账面上增加得越多,要交的就越多;另外一方面,虽然地方预算内的税收收入要和中央分成,但预算外收入则可以独享,地方政府就可以通过给企业免税,再用其他手段,把应收的税款收回来,就能免于与中央分成。\n就出现了,经济发展越来越好,地方政府越来越富裕,中央却越来越穷的问题。\n翻开中国漫长的历史,就会发现,「弱干强枝」乃取祸之道,不利于政治稳定,所以税制是到了非改不可的地步了。\n3.2 分税制改革:1994 1994年的分税制改革把税收分为三类:中央税(如关税)、地方税(如营业税)、共享税(如增值税).\n分税制改革中最重要的税种是增值税,占全国税收收入的1/4。改革之前,增值税(即产品税)是最大的地方税,改革后变成共享税,中央拿走75%,留给地方25%. 企业只要开工生产,不管盈利与否都得交增值税,规模越大缴税越多。\n分税制改革,地方阻力很大。比如在财政包干制下过得很舒服的广东省,就明确表示不同意分税制。与广东的谈判能否成功,关系到改革能否顺利推行。为了这项改革的展开,朱镕基总理亲自带队,用两个多月的时间先后走了十几个省,面对面地算账,深入细致地做思想工作,最后广东还是服从了大局。\n分税制是20世纪90年代推行的根本性改革之一,也是最为成功的改革之一。中央占全国预算收入的比重从改革前的22%一跃变成55%,并长期稳定在这一水平。但分税制改革的影响深远,还远不止于此。\n有钱才能办事,而税收又关系到政府能收到多少钱。\n而分税制改革又调整了税收分配模式,直接影响到地方政府的财税收入,为地方政府后续搞「土地财政」埋下了伏笔。\n4 土地财政 4.1 缘起 1994的分税制改革并没有改变地方政府以经济建设为中心的任务,却减少了其手头可支配的财政资源。\n对于地方政府而言,钱变少了,活还是要照干,地方政府环视一圈,盘了下自己手上有什么资源可以用,最后把目光投到拥有的,最有价值的资源:**土地**。\n4.2 土地财政 所谓“土地财政”,不仅包括巨额的土地使用权转让收入,还包括与土地使用和开发有关的各种税收收入。其中大部分税收的税基是土地的价值而非面积,所以税收随着土地升值而猛增。\n这些税收分为两类,一类是直接和土地相关的税收,其收入百分之百归属地方政府; 另一类税收则和房地产开发和建筑企业有关,主要是增值税和企业所得税,要与中央分成。\n4.3 土地金融 再穷的国家也有大片土地,土地本身并不值钱,值钱的是土地之上的经济活动。\n若土地只能用来种小麦,价值便有限,可若能吸引来工商企业和人才,价值想象的空间就会被打开,笨重的土地就会展现出无与伦比的优势:它不会移动也不会消失,天然适合做抵押,做各种资本交易的压舱标的,身价自然飙升。\n地方政府就可以把与土地相关的未来收入资本化,去获取贷款和各类资金,将“土地财政”的规模成倍放大为“土地金融”\n4.3.1 城投公司 法律规定,地方政府不能从银行贷款,2015年之前也不允许发行债券,所以政府要想借钱投资,需要成立专门的公司。\n这些公司名称大多有「建设投资」和「投资开发」的字样,因此统称「城投公司」\n5 房价与债务 5.1 房价的影响因素 房价短期内受很多因素影响,但中长期主要由供求决定。无论是发达国家还是发展中国家,房屋供需都与人口结构密切相关,因为年轻人是买房主力。\n年轻人大都流入经济发达城市,但这些城市的土地供应又受政策限制,因此房屋供需矛盾突出,房价居高不下。\n\u0026ldquo;好消息\u0026quot;是因为房价高企等种种原因,中国的出生人口正逐年下降,按照「官方数据」, 2021年全年出生人口1062万人,自然增长率为0.34%. 而2022年的人口数据还没有公布,各方估计有望下降到1000万以下,甚至实现自然增长率负增长的目标。\n这就是意味着,对于等等党来说,再多等个十几年,大概率就没有年轻人和你争买房子了,就可以在梦想的地方,以梦想的价格买到想要的房子。\n5.2 政府债务与房价 随着城市化和商品房改革,土地价值飙升,政府不仅靠土地使用权转让收入支撑起了“土地财政”,还将未来的土地收益资本化,从银行和其他渠道借入了天量资金,利用“土地金融”的巨力,推动了快速的工业化和城市化。\n但同时也积累了大量债务。这套模式的关键是土地价格。\n只要不断地投资和建设能带来持续的经济增长,城市就会扩张,地价就会上涨,就可以偿还连本带利越滚越多的债务。\n可经济增速一旦放缓,地价下跌,土地出让收入减少,累积的债务就会成为沉重的负担,可能压垮融资平台甚至地方政府。\n而据中国冰川思想库研究员提供的数据,截至2022年,中国城投债(就是地方政府控制的公司借的钱)规模可能达到65万亿,中国人均负债5万元。\n我画了一张系统循环图来分析其中的因果关系:\n(系统循环图:A -\u0026gt;(+) B代表: A的增加会导致B的增加,A的减少会导致B的减少;A-\u0026gt;(-)B代表:A的增加会导致B的减少,反之亦然)\n可见,主客观上,政府和官员都不会想土地价格降下来,因为一降下来,地方政府可能被债务压垮,官员个人待遇也会大受影响。\n另外,因为政府垄断了土地,而土地的供给又影响土地出让的价格,所以政府必然会制造供给侧的短缺,以拉高土地出让的价格。\n而土地出让价格又是房价的大头,所以政府是不会想房价降下来的。\n5.3 银行信贷 2008年至2009年,为应对全球金融危机,我国迅速出台“4万亿”计划,同时不断降准降息,放宽银行信贷(也就是所谓的「大放水」),这些资金找到了基建和房地产两大载体,相关投资迅猛增加。\n虽说银行增加货币供给,增加信贷比重,但是各种实体企业总是喊「借钱难,借不到钱」,这是因为银行尤其偏爱以土地和房产为抵押物的贷款。\n先看住房按揭: 银行借给张三100万元买房,实质不是房子值100万元,而是张三值100万元,因为他未来有几十年的收入。\n但未来很长,张三有可能还不了钱,所以银行要张三先抵押房子,才肯借钱。房子是个很好的抵押物,不会消失且容易转手,只要这房子还有人愿意买,银行风险就不大。而房子具有普适性,李四要买的房子可能就是张三要卖的房子。\n若没有抵押物,张三的风险就是银行的风险,但有了抵押物,风险就由张三和银行共担。张三还要付30万元首付,相当于抵押了100万元的房子却只借到了70万元,银行的安全垫很厚。\n再来看企业贷款: 银行贷给企业家李四500万元买设备,实质也不是因为设备值钱,而是用设备生产出的产品值钱,这500万元来源于李四公司未来数年的经营收入。\n但作为抵押物,设备的专用性太强,价值远不如住房或土地,万一出事,想找到人接盘并不容易。就算有人愿意接,价格恐怕也要大打折扣,所以银行风险不小。但若李四的企业有政府担保,甚至干脆就是国企,银行风险就小多了。\n这就是加大流动性,资金也流不到实业企业中去的原因。\n这也是个马太效应,除了有政府担保外,借了钱又还不上的企业成了大爷,银行担心这家企业还不上钱,破产,坏账,也有意愿再借钱给他,借新债还旧债。\n5.4 居民债务 根据央行的统计,2008年之后的10年,我国房价急速上涨,按揭总量越来越大,居民债务负担上涨了3倍多。\n居民债务中有53%是住房贷款,24%是各类消费贷(如车贷)。这一数据可能还低估了与买房相关的债务,毕竟一些消费贷也被用来交首付买房了。\n总体看来,我国居民的债务负担不低,且仍在快速上升。最主要的原因是房价上涨。\n而居民债务居高不下,就很难抵御经济衰退,尤其是房产价格下跌所引发的经济衰退。\n低收入人群的财富几乎全部是房产,其中大部分是欠银行的按揭,负债率很高,很容易受到房价下跌的打击。\n5.5 房价下跌与债务风险 债务关系让经济各部门之间的联系变得更加紧密,任何部门出问题都可能传导到其他部门,一石激起千层浪,形成系统风险。\n银行既贷款给个人,也贷款给企业。若有人不还房贷,银行就会出现坏账,需要压缩贷款;\n得不到贷款的企业就难以维持,需要减产裁员;于是更多人失去工作,还不上房贷;银行坏账进一步增加,不得不继续压缩贷款……\n如此,恶性循环便产生了:\n如果房价下跌太多,房子的价值比要还房贷还低,而还贷圧力又超出可承受范围,债务人就有可能放弃首付,断供,造成银行坏账.\n如果各部门负债都高,那应对冲击的资源和办法就不多,风吹草动就可能引发危机, 原因有二:\n负债率高的经济中,资产价格的下跌往往迅猛。若债务太重,收入不够还本,甚至不够还息,就只能变卖资产,抛售的人多了,资产价格就会跳水 资产价格下跌会引起信贷收缩,导致资金链断裂。借债往往需要抵押物(如房产和煤矿),若抵押物价值跳水,债权人(通常是银行)坏账就会飙升,不得不大幅缩减甚至干脆中止新增信贷,导致债务人借不到钱,资金链断裂,业务难以为继。 一个部门的负债对应着另一个部门的资产。债务累积或“加杠杆”的过程,就是人与人之间商业往来增加的过程,会推动经济繁荣。而债务紧缩或“去杠杆”也就是商业活动减少的过程,会带来经济衰退。\n举例来说,若房价下跌,老百姓感觉变穷了,就会勒紧裤腰带、压缩消费。东西卖不出去,企业收入减少,就难以还债,债务负担过高的企业就会破产,银行会出现坏账,压缩贷款,哪怕好企业的日子也更紧了。\n说出来有点难以置信,就是房价下跌,最终会影响到卖皮肤氪金的游戏公司和收交易手续费的金融科技公司,甚至出现裁员的情况。\n基于以上分析,我认为未来的房价会是「跌不下,涨不上,买不起,卖不动」\n5.5.1 疫情与债务风险 如果把疫情这个超大号黑天鹅与政府的应对造成的影响再纳入分析范畴,就会变成这个样子:\n5.6 人口与债务风险 如果把人口数量这台灰犀牛纳入到分析范畴,就会变成这个样子\n(系统循环图的 -||-\u0026gt; 标识表示滞后效应,产生作用需要时间)\n6 解决办法 6.1 政府债务 任何国家的债务问题,解决方案都可以分成两个部分:一是偿还已有债务;二是遏制新增债务,改革滋生债务的政治、经济环境。\n6.1.1 偿还已有债务 如果借来的钱能用好,能变成优质资产、产生更高收入,那债务负担就不是问题。\n但如果投资失败或干脆借钱消费挥霍,那就没有新增收入,还债就得靠压缩支出:居民少吃少玩,企业裁员控费,政府削减开支。\n但甲的支出就是乙的收入,甲不花钱乙就不挣钱,乙也得压缩支出。\n大家一起勒紧裤腰带,整个经济就会收缩,大家的收入一起减少。若收入下降得比债务还快,债务负担就会不降反升。\n还债让债务人不好过,赖账让债权人不好过。所以偿债过程很痛苦,还有可能陷入经济衰退。\n相比之下,增发货币也能缓解债务负担,「似乎」还不那么痛苦,因为没有「明显」的利益受损方,实施起来阻力也小.\n但增发货币大概率会导致通货膨胀,国民手中的钱变得不值钱了,相当于全体国民为政府的债务买单\n按照我们政府对国民负责任的态度,相信最终还是会选择这条路。\n6.1.2 遏制新增债务 理解了各类债务的成因之后,也就不难理解遏制新增债务的一些基本原则:限制房价上涨,限制“土地财政”和“土地金融”,限制政府担保和国有企业过度借贷,等等。\n但困难在于,就算搞清楚了原因,也不一定就能处理好后果,因为“因”毕竟是过去的“因”,但“果”却是现在的“果”,时过境迁,很多东西都变了。\n好比一个人胡吃海塞成了大胖子,要想重获健康,少吃虽然是必须的,但简单粗暴的节食可能会出大问题,必须小心处理肥胖引起的很多并发症。\n为此,近几年政府出台了限制供给侧的政策,包括「房住不炒」,「三道红线」等等。\n只是,在疫情的第三年,在清零导致的财政压力下,这些限制又有解绑的趋势。\n6.2 居民债务 要化解居民债务风险,除了遏制房价上涨势头以外,根本的解决之道还在于提高收入,尤其是中低收入人群的收入,鼓励他们到能提供更多机会和更高收入的地方去工作。\n让地区间的经济发展和收入差距成为低收入人群谋求发展的机会,而不是变成人口流动的障碍。\n很多事情都是提出愿景容易,执行落地难:\n按照有关部门统计,2020年,民营企业从业人员占城镇就业的83%, 也就是说,要提高居民收入,就要做多做大做强民营企业,毕竟系统调优,也是针对热点,调优效果最好。\n要发展好民营企业,就需要建立好的营商环境;\n完善法律法规,不要再你法我笑;\n提高宽松自由的环境,放宽媒体限制;\n政府退出各种高利润的垄断行业,不再与民争利,不要既当裁判,又当运动员。\n关于官营商业,盐铁专营的弊端,二千年前的《盐铁论》中,贤良文学已经阐述得非常清晰了。\n尊重与保护劳动者权益,把劳动者当作「人」而非「矿」 要让人口流动起来,就要改革革户籍制度,不要把农民绑死在土地上 效率低,营收差的国企要有退出机制,政府不能无限地为其输血,兜底 要完善法律法规,就要做到依法治国。年少时,听到依法治国,「有法可依,有法必依」八个字的准则,觉得理所当然。年纪渐长,才读懂背后的含义,原来有很多「有法不依,无法可依」的情况,也难怪会出现「你法我笑」的情况。 要政府不再与民争利,退出垄断行业,就要改革已有的经济结构。 要创新,就需要提供自由宽松的环境,因为创新就和基因突变一样,是无序的,甚至开始是异端的。量变产生质变,只有有足够多的创新,才能由市场进行选择,优胜劣汰,留下市场认可的创新。产品创新和生物进化一样,都是肥沃土壤,长出百花齐放的结果,不是哪个造物主指明的方向。 开放媒体,加强对行政执行和腐败的监管。 政府瘦身,建立小政府,减少对市场的干预,也减少对财政的压力。 解法办法就在这里,但知易行难。 改革从来都不是请客吃饭,都是要下大决心,有壮士断腕的狠劲。\n这些都是结构性的问题,只能做结构性改革。\n但「深化改革」的口号,从我上小学,一直喊到我上大学,至于我们是离目标越来越近,还是越来越远,南辕北辙,摸着石头倒车。\n相信大家心里还是有杆称的。\n对于历史债务,Leader们常挂在嘴边的话是:「不重构是等死,重构是找死」,但是我们选择的路,大多是找死。\n7 总结 7.1 以古为鉴 年少时读史,觉得汉武帝刘彻战功赫赫,封狼居胥,扬我大汉雄武之风,不愧为武帝之名。\n年青时再读史,会读到许多对汉武帝的批评,说其穷兵黩武,不惜民力,无数英魂埋骨他乡,户籍几近减半。对批评甚是不解,认为凡事都会有代价。\n年长些,出外为三餐奔波谋生,再读史,就会对汉武帝产生不一样的看法:十年匈奴之战,败光了他祖父和父亲「文景之治」所积累的财富家底,政府财政全面恶化,就出台了影响中国两千年的「盐铁官营」政策,与民争利,不如此不足以应付政府的开销;后来甚至出台了告发富人瞒税,告发人可获告发金额一半的,严重败坏社会道德「告缗令」,令汉朝的中产阶级和富人阶级被一扫而空,甚至出现100倍于前代的超级通货膨胀。\n就我这种黔首看来,武帝可谓是独夫,我不想成为代价。\n但即使「雄才大略」如汉武帝,晚年面对糜烂的政局,还是下了「轮台罪己诏」,向天下臣民承认自己的错误。\n7.2 写在最后 在经济规律面前,无论多强势的强人,都只能屈服。\n而无形的经济规律,可以理解成国人所谓的「势」,理解经济规律之后,我们可以顺势而为,而非逆势而动,被时代的大势无情碾过。\n毕竟古诗有云:「时来天地皆同力 运去英雄不自由」\n8 参考 《置身事内》 中国人口十年图谱 轮台诏 65万亿的城投债 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E7%BD%AE%E8%BA%AB%E4%BA%8B%E5%86%85/","summary":"1 前言 有心仪工作的城市房价太高,而房价合适的城市没有心仪的工作。 梦想买不起,故乡回不去。 眼看着大城市一座座高楼拔地而起,却难觅容身之所。 为什","title":"为什么梦想买不起,故乡回不去"},{"content":"1 Definition In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertx v, u comes before v in the ordering.\nIt sounds pretty academic, but I am sure you are using topological sort unconsciously every single day.\n2 Application Many real world situations can be modeled as a graph with directed edges where some events must occur before others. Then a topological sort gives an order in which to perform these events, for instance:\n2.1 College class prerequisites You must take course b first if you want to take course a. For example, in your alma mater, the student must complete PHYS:1511(College Physics) or PHYS:1611(Introductory Physics I) before taking College Physics II.\nThe courses can be represented by vertices, and there is an edge from College Physics to College Physics II since PHYS:1511 must be finished before College Physics II can be enrolled.\n2.2 Job scheduling scheduling a sequence of jobs or tasks based on their dependencies. The jobs are represented by vertices, and there is an edge from x to y if job x must be completed before job y can be started.\nIn the context of a CI/CD pipeline, the relationships between jobs can be represented by directed graph(specifically speaking, by directed acyclic graph). For example, in a CI pipeline, build job should be finished before start test job and lint job.\n2.3 Program build dependencies You want to figure out in which order you should compile all the program\u0026rsquo;s dependencies so that you will never try and compile a dependency for which you haven\u0026rsquo;t first built all of its dependencies.\nA typical example is GNU Make: you specific your targets in a makefile, Make will parse makefile, and figure out which target should be built firstly. Supposing you have a makefile like this:\n# Makefile for analysis report output/figure_1.png: data/input_file_1.csv scripts/generate_histogram.py python scripts/generate_histogram.py -i data/input_file_1.csv -o output/figure_1.png output/figure_2.png: data/input_file_2.csv scripts/generate_histogram.py python scripts/generate_histogram.py -i data/input_file_2.csv -o output/figure_2.png output/report.pdf: report/report.tex output/figure_1.png output/figure_2.png cd report/ \u0026amp;\u0026amp; pdflatex report.tex \u0026amp;\u0026amp; mv report.pdf ../output/report.pdf Make will generate a DAG internally to figure out which target should be executed firstly with typological sort:\n3 Directed Acyclic Graph Back to the definition, we say that a topological ordering of a directed graph is a linear ordering of its vertices, but not all directed graphs have a topological ordering.\nA topological ordering is possible if and only if the graph has no directed cycles, that is, if it\u0026rsquo;s a directed acyclic graph(DAG).\nLet us see some examples:\nThe definition requires that only the directed acyclic graph has a topological ordering, but why? What happens if we are trying to find a topological ordering of a directed graph? Let\u0026rsquo;s take the figure 3 for an example.\nThe directed graph problem has no solution, this is the reason why directed cycle is forbidden\n4 Kahn\u0026rsquo;s Algorithm There are several algorithms for topological sorting, Kahn\u0026rsquo;s algorithm is one of them, based on breadth first search.\nThe intuition behind Kahn\u0026rsquo;s algorithm is pretty straightforward:\nTo repeatedly remove nodes without any dependencies from the graph and add them to the topological ordering\nAs nodes without dependencies are removed from the graph, the original nodes depend on the removed node should be free now.\nWe keep removing nodes without dependencies from the graph until all nodes are processed, or a cycle is detected.\nThe dependencies of one node are represented as in-degree of this node.\nLet\u0026rsquo;s take a quick example of how to find out a topological ordering of a given graph with Kahn\u0026rsquo;s algorithm.\nNow we should understand how Kahn\u0026rsquo;s algorithm works. Let\u0026rsquo;s have a look at a C++ implementation of Kahn\u0026rsquo;s algorithm:\n#include \u0026lt;deque\u0026gt; #include \u0026lt;vector\u0026gt; // Kahn\u0026#39;s algorithm // `adj` is a directed acyclic graph represented as an adjacency list. std::vector\u0026lt;int\u0026gt; findTopologicalOrder(const std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; \u0026amp;adj) { int n = adj.size(); std::vector\u0026lt;int\u0026gt; in_degree(n, 0); for (int i = 0; i \u0026lt; n; i++) { for (const auto \u0026amp;to_vertex : adj[i]) { in_degree[to_vertex]++; } } // queue contains nodes with no incoming edges std::deque\u0026lt;int\u0026gt; queue; for (int i = 0; i \u0026lt; n; i++) { if (in_degree[i] == 0) { queue.push_back(i); } } std::vector\u0026lt;int\u0026gt; order(n, 0); int index = 0; while (queue.size() \u0026gt; 0) { int cur = queue.front(); queue.pop_front(); order[index++] = cur; for (const auto \u0026amp;next : adj[cur]) { if (--in_degree[next] == 0) { queue.push_back(next); } } } // there is no cycle if (n == index) { return order; } else { // return an empty list if there is a cycle return std::vector\u0026lt;int\u0026gt;{}; } } 5 Bonus When a pregnant woman takes calcium pills, she must make sure also that her diet is rich in vitamin D, since this vitamin makes the absorption of calcium possible.\nAfter reading the demonstration of topological ordering, you (and I) too should take a certain vitamin, metaphorically speaking, to help you absorb. The vitamin D I pick for you (and myself) is two leetcode problems, which involve with the most typical use case of topological ordering \u0026ndash; college class prerequisites:\nCourse Schedule Course Schedule II 6 Reference Topological Sort | Kahn\u0026rsquo;s Algorithm | Graph Theory Directed Acyclic Graph Hands-on Tutorial on Make Topological sorting ","permalink":"https://ramsayleung.github.io/zh/post/2022/topological_sorting/","summary":"1 Definition In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertx v, u comes before v in the ordering.\nIt sounds pretty academic, but I am sure you are using topological sort unconsciously every single day.\n2 Application Many real world situations can be modeled as a graph with directed edges where some events must occur before others.","title":"Topological Sort"},{"content":"1 前言 「最好的学习方式」\n在如今打广告也需要遵守广告法的时代,用这样的标题来描述某个学习方法难免会让人觉得言过其实,不客气的朋友可能会直接说「营销味」十足。\n不看广告看疗效,「言过其实」还是「名符其实」,只有试过才知道。\n如果「尝试」也过于麻烦,那不如来看下提出该学习方法的理查德·费曼(Richard Phillips Feynman)其人。\n1.1 费曼其人 理查德·菲利普斯·费曼(Richard Phillips FeynmanA), 美国理论物理学家,以对量子力学的研究闻名于世,除此之外,他还是量子计算领域的先驱,并提出了纳米技术的概念。\n因对量子电动力学的贡献,于1965年共同获得诺贝尔物理学奖。\n费曼发展了得到广泛应用的亚原子粒子行为的图像化数学表述——费曼图(Feynman diagram)。费曼图长这个样子:\n费曼在世时是世界上最有名的科学家之一。\n1999年,在英国学术期刊《物理世界》举办的130位世界顶尖物理学家参与的票选活动中,费曼跻身十大有史以来最伟大物理学家之列\n二战期间他曾参与曼哈顿计划,协助原子弹的开发,而后在1980年代因参与调查挑战者号航天飞机灾难而为公众熟知。\n1.1.1 趣事 费曼有一辆有名的货车(The Feynman Van),他在这辆车上画满了以他名字命名的费曼图(Feynman diagram):\n虽然费曼他偶尔会开这辆车,但是大部分时间都是他的妻子(Gweneth)开。\n有一次,她妻子开这辆车出去,等待红灯时,一个识货的司机走过来问,为什么她开着一辆画满费曼图的货车,她回答到,因为我是费曼的妻子。\nAlthough Richard occasionally used the van to commute from his home in Altadena to Caltech, the van was usually driven by his wife, Gweneth.\nOne time a perplexed motorist waiting at a red light asked the unidentified woman why she was driving a van with Feynman diagrams on the side.\nHer answer: “Because my name is Gweneth Feynman.”\n另外一个小彩蛋,这辆车曾经出现在《生活大爆炸上》,看谢尔顿的翻脸速度和崇拜表情,大概就能感受到费曼有多牛。\nYour browser does not support the video tag.\n(视频来自知乎问题:费曼是一个什么样的人,答主@流川枫)\n2 理念 费曼学习法,核心理念就是:\n学习一种新事物最好的方法是,用你的话讲给别人听。\n通过向别人清楚的解说某一事物,来确认自己是否真的弄懂了这件事。\n所以说,学习最好的方式,是把你学到的东西教给别人。\n学习步骤如下:\n2.1 学习并汇总 选择一个你想要学习的领域或者课题,然后学习,再将你所学到的所有内容给记录下来。\n做笔记,摘抄,或者写下自己的理解都可以。\n2.2 用自己的话向别人解释 找一个小白或者小朋友,用你「自己的话」来复述你学到的内容。\n你可以援引自己笔记的内容,但是不要说那些专业术语,只用你自己的话来解释。\n因为用自己的话,才能说话你把学习的内容真正消化了,直接使用专业术语,只能说明你背下来了,不一定能说明你理解了。\nSimple is beautiful.\n目标是 通顺,通透 地向这个「无知」的小朋友解释你所学到的内容。\n2.3 反馈,改进 在步骤二,你可能会出现四种情况。\n2.3.1 欲语还休 情况一,算是最糟糕的情况,你可能不知道从何讲起,说你还对该领域还缺乏「系统」的理解,就需要返回步骤一重新学习。\n2.3.2 跌跌撞撞 情况二,算是最常见的情况,你能用自己话讲出来,但是讲得「磕磕碰碰」,一直会卡壳,小白也听得一知半解。\n说明你对该领域有一个的了解,但还没完全「理顺」整个体系,这里就可以针对「卡壳」的点,有针对性地回到目步骤一进行学习。\n2.3.3 深入不浅出 情况三,你能用自己的话用该领域的内容通顺地讲出来,但是小白没有完全理解,就说明你讲得不够「通透」,不够「深入浅出」。\n这个时候就可以询问小白,是哪个部分没有讲清楚,这个部分就是你盲点,拼图缺乏这块,导致没法构建完体系。\n再回去步骤一针对学习。\n2.3.4 娓娓道来 在这种情况下,你能顺滑,流畅地使用自己的话向小白解释你学到的内容,小白也能完全领会 到你所讲述的知识,那就说明你已经完全掌握了这个领域的知识。\n不要吝啬赞美的言词,大声地表扬下自己。\n2.4 总结 2.4.1 双赢 当你「通顺」,「通透」地向一个小白介绍完的你所学的知识时,你一定会有新的收获和感慨,把你新学到的知识再总结下来。\n这就是输出的收获和乐趣,所以这也是一个「双赢」的学习法,既能育人,也能育已。\n听众既能受益,输出知识的作者本身也能提高个人能力。\n如果能吸引到读者高质量的问题,那么作者在解答的过程中,又可以进一步受益和得到提升,这还是一个良性循环。\n2.4.2 换位思考 为了践行这种「以教代学」的学习方法,在向小白讲授内容的时候,为了更好地帮助他们理解,你就可能需要切换到「小白视角」,以他们的角度来思考问题。\n久而久之,你的「换位思考」和「共情」能力自然就会得到提升。\n3 输出 最好的学习方法是输出,而我这篇文章就是在用自己的话向大家介绍「费曼学习法」,而这个行为本身也是在实践「费曼学习法」。\n如果大家看完本文,能理解「费曼学习法」是什么,那也就说明我已经掌握了「费曼学习法」(否则我就讲不清楚)!\n干杯!\nPS:\n这应该算编译器自举(bootstrap)了 :)\n4 参考 The Feynman Technique: The Best Way to Learn Anything The Feynman Van 费曼是一个什么样的人 如何【系统性学习】——从“媒介形态”聊到“DIKW 模型” ","permalink":"https://ramsayleung.github.io/zh/post/2022/feynman_technique/","summary":"1 前言 「最好的学习方式」 在如今打广告也需要遵守广告法的时代,用这样的标题来描述某个学习方法难免会让人觉得言过其实,不客气的朋友可能会直接说「","title":"最好的学习方式:费曼学习法(Feynman Technique)"},{"content":"1 前言 秦人不暇自哀而后人哀之,后人哀之而不鉴之,亦使后人而复哀后人也\n每个朝代灭亡之后,就会由下一个朝代的史官编写,如元朝灭亡之后,就由明朝来撰写《元史》。\n虽然国民党政权在大陆败退之后,没有史官来专门编写《民国史》,但还是有许多资料来探究国民党失败的原因。 而《党员,党权与党争》就从组织角度来考察国民党失败的原因。\n想来,以后也会有类似的,研究共产党的学术资料。\n2 以俄为师 中国国民党承续兴中会,同盟会,国民党,中华革命党而来,期间不仅名称数度更易,其组织形式亦几度因革。 1924年国民党改组,借鉴俄国布尔什维克的组织模式,建立了一套全新的政党组织体系。\n有感于国民党党务不振,孙中山于1924年从组织技术层面学习苏俄的建党,治党方法。 实现三民主义为体,俄共组织为用,从党的组织结构,党章党纪,基层组织建制,基于党的军队建设,全面向苏俄学习,企图打造出一个组织严密,富有战斗力的新党。\n只是,国民党师法俄共的组织形式,将党建在国上,实行以党治国,一党专政。 但是,孙中山三民主义理念中的政治蓝图又是基于西方民主体制而设计的。\n国民党借鉴了两个不能同时并立的政治架构,拼装了一台不伦不类的政治机器:\n成立国民政府后一方面依照西方分权学说,成立五院(所谓的五权分立,即在西方的行政权,立法权,司法权外,再增加考试权和监察权), 另一方面又依照苏俄党治学说,设立集权的中执会,中政会。\n这种兼收并蓄,可谓错漏百出。\n3 清党分共 在1924年的国民党一大上,孙中山提出了「联俄」,「联共」,「扶助工农」的三大政策, 期望在苏俄和中共共产党的帮助下,完成反帝反封的国民革命。\n而国民党与共产党的合作关系,是被称为「党内合作」的合作关系,并不是国民党与共产党两个政党开展合作, 而是共产党员以个人身份加入国民党,成为跨党党员,开展工作。\n国共两党的党员,在实际的工作中,是呈现出「国民党在上层,共产党在下层」这样的状态。 国民党差不多专做上层的工作,而下层的民众运动,国民党员参加的少,共产党员参加的多,因此形成了一种畸形的发展。\n到1926年,国民党二大召开前后,甚至出现共产党「包办」大部分国民党党务的现象. 据中共领导人称,在国民党二大召开前后,大约有90%的国民党地方组织处于共产党员和国民党左派的领导之下。\n1924-1927年的国共关系,既是一种相互合作的关系,又是一种相互竞存的关系。\n到第一次国共合作的后期,国共合作关系已经从原来的国民党「容共」,变成共产党「容国」。 即使共产党员远少于国民党员,但因为其组织严密,工作高效,已经成为国民党党内之党。\n两党组织动作的巨大反差,国民党内部一方面为内部组织松懈而忧虑,更对共产党组织严密而恐惧, 这种忧虑和恐惧衍化为「分共」,「反共」的主张与行动,另一方面促使部分富有革命热情的国民党青年, 在强烈对比之下,加下到组织更严密的共产党去,这又加深了国民党内部的恐惧与忧虑。\n在此种忧虑与压力之下,1927年4月12日蒋介石下令武力清党分共,屠杀共产党员,就变得理所当然。 如果蒋不分共,国民党的情况也不会好转,只会被和平演变成共产党。\n4 党务不振 「四一二」清共,对共产党造成了巨大的伤害,让共产党意识到「文斗」虽强,但是还是干不过刀把子,悟出了枪杆里出政权的道理。\n很多人没有意识到的是,清共对国民党同样造成巨大的伤害. 因为共产党使用秘密党员制,因此,要分共时,并不知道哪个是共产党员,哪个是国民党员。\n蒋介石的亲信陈果夫曾经使用过一种荒谬儿戏至极的方法来辨别两党党员:让屋子里面的人相互殴打,然后他认为打着打着,共产党员就会帮助共产党员,国民党员就会帮助国民党员,就能借此分出两党党员。\n因为分不清共产党员,就免不了杀错人,把「像」共产党的党员杀完之后,就开始把有革命热情的左派国民党员和群众都当成共产党杀了,清共之旋风越刮越大,甚至国民党民都无人敢做真正的革命之事. 地主劣绅趁机诬陷减租减息等不利于现有特权阶级的国民党党员为共产党员,以除之而后快,以致人人自危。 甚至还有索婚不成,诬其为共党以杀之的可悲之事。\n此种分党手段造成的严重后果就是,国民党在国民心中的民心一落千丈,并再也回不去了。 而在基层乡镇,因此大量干实事的国民党员被杀,导致基层权力空虚,地主劣绅趁虚而入,摇身一变成为国民党员,把持基层党权。\n在蒋介石独裁及派系核相争的帝王心术之下,中上层的党员,忠于派系多过忠于党,对权力的向往大于对革命理想的践行,党统在各派系激烈的权力斗争中濒于破裂,党务亦在派系的内耗中日趋衰微。\n蒋既无决心重组党务,又不愿意放弃手中独裁的权力,失去民心,党心,国民党的党务建设有如泥牛入海,再也无法拉回来了. 自北伐后,国民党民心日失,党务难振。\n自分党至国民党败退台湾,党务再没再振之机。\n5 以军治党 自孙中山逝世,蒋介石依靠黄埔系的党军之力上位,靠军权登上国民党权力的高位。 而因为党权不振,无法通过党来治理国家,蒋就希望通过军队来治理国家,由原来的以党治军,变成以军治党。\n蒋的军治理念,首先表现为放大军人在国家社会中的作用,蒋认为,在任何时候,任何国家,军人应该是社会的主导群体。 其次,强调军队组织在国家和社会的各个领域的普适性,认为无论古今中外,要组织成一个健全的国家和社会,都是要全国军队化。\n蒋介石对军权和军治的过分迷恋,分散甚至取代了他对党治和党务组织建设的关注与考虑。 而蒋重军轻党的原因是多方面的,在与汪精卫及胡汉民的「继承人」之争中,前者都是通过党权与蒋所持的军权相争,蒋难免会有把党权作为障碍。 其次,1931-1932年之交,蒋二次下野时,反思认为国民党的各级党组织并不忠于他,而他的一生中,最为依赖的权势资源就是军队,拿着个锤子,难免看什么都像钉子。\n在蒋介石重军轻党的主导下,军权日趋膨胀,党权日趋低落,从中央到地方,军权凌驾于党政之上,党治徒有其表,军治实际取代了党治。\n而军治,既是党权不振的因,又是党权不振的果,可谓因果纠缠,恶性循环。\n6 总结 国民党依照俄共实行一党专政,而在实际运行中,其组织散漫性,又像西方议会政党,只重政见不重组织。 国民党是一个弱势独裁政党,国民党并非不想独裁,而是独裁之心有余,独裁之力不足。\n国民党和中国共产党都是以俄为师的中国学生,花开两朵,各表一枝。 全盘俄化的中国共产党赢了只学到半套功夫的国民党,而国民党后又败在年轻的民进党手下。\n蒋公一再以「亡党亡国」警示其党员,然而,事实却是,党破山河在,党亡国不亡。而后之视今,亦尤今之视昔。\n只是未想,时运弄人,两个学生尚在,引以为师的苏共竟先消散在历史的尘埃中。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%85%9A%E5%91%98_%E5%85%9A%E6%9D%83%E4%B8%8E%E5%85%9A%E4%BA%89/","summary":"1 前言 秦人不暇自哀而后人哀之,后人哀之而不鉴之,亦使后人而复哀后人也 每个朝代灭亡之后,就会由下一个朝代的史官编写,如元朝灭亡之后,就由明朝来","title":"党员,党权与党争"},{"content":"1 前言 历史教科书说,与英国的第一次鸦片战争,敲开了清朝的大门,清朝也因此和外国签订了一系列不平等条约. 而因为清朝的落伍,导致清朝不敌英国.\n教科书却未曾告诉我们,清朝落伍在什么地方,当时清朝的正确之途又在何方。\n而这些内容,《天朝的崩溃》都会告诉我,还原历史,以当时人的眼光,再去审视这场影响深远的战争。\n以史为鉴,以彼「天朝」审视此「天朝」。\n2 军事力量 尽管现代人已对战争下了数以百计的定义,但是,战争最基本的实质只是两支军事力量之间的对抗。\n鸦片战争中英武器的水平,概括来说:\n英军已处于步发展的火器时代,而清军仍处于冷热兵器混用的时代.\n2.1 武器装备 2.1.1 火器 火药和管型火器都是中国发明的,但中国一直处于前科学时期,没有形成科学理论和实验体系,使中国火器发展受到了根本性制约。\n至鸦片战争期间,清军使用的火器,主要不是中国发明的,而是仿照明代引进的「佛郞机」,「鸟铳」等西方火器样式制作。\n由此可以说,清军使用的是自制的老式的「洋枪洋炮」,就样式而言,与英军相比,整整落后二百余年。\n2.1.2 火炮 清军使用的火炮,如同其鸟枪,其原型可追溯至明代。 清军的火炮从样式上,主要是仿照西方17世纪至18世纪的加农炮. 与英军相比,清军火炮在样式及机制原理上大体相同,但两者的差别在于制造工艺引起的质量问题:\n铁质差\n工业革命使英国的冶炼技术改观,铁质大为提高,为铸造高质量的火炮提供了良好的原料。\n清朝的冶炼技术落后,炉温低,铁水无法提纯,含杂质多,铸造出来的火炮十分粗糙,气孔气泡多,容易炸膛:\n关天培称,在1835年,他在佛山制造59门新炮,试放时就炸裂10门,损坏3门。\n清军针对此问题主要采用两策: 一是加厚火炮的管壁,使清军的火炮极为笨重,数千斤巨炮,威力还不如西方小炮; 二是使用铜作为铸炮材料,但铜资源缺乏\n铸炮工艺落后\n英国已经使用铁模工艺,并使用车床对炮膛内部加工,使之更为光洁。\n清朝此时仍沿用落后的泥模工艺,铸件毛糙,又未对炮膛做深入加工,致使炮弹射出后,弹道紊乱,降低射击精度。\n英方因此科学的进步,对火药燃烧,弹道,初速度等方面做研究,使火炮设计较合理。\n炮架和瞄准器具不全或不完善\n炮架是调整火炮射击和高低夹角的器具,清军对此不甚重视,火炮没有炮架,只是固定严重限制了射击角度。\n又没有瞄准器具,只能靠经验操作,全凭感觉。\n炮弹种类少,质量差;\n英军的炮弹有实心弹,霰弹,爆破弹等,而清军只有效能最差的实心弹,且弹体粗糙,弹径小。\n2.1.3 火药 与枪炮相关联的,是火药。\n鸦片战争时期,中英火药处于同一发展阶段,皆为黑色有烟火药。\n然而,因为质量问题,使中英在火药上的差距大于前面所提到的火炮。\n这里面的关键,是科学和工业。\n1825年,歇夫列里在经过多次实验后,提出了黑色火药的最佳化学反应方程式,英国据此方程式,配制了枪用发射火药和炮用发射火药。\n除了科学带来的理论进步外,工业革命又带来了机械化的生产,通过先进的工业设备,提炼高纯度的硝和硫; 使用蒸汽装置和水压机进行火药加工,使颗粒均匀,保证优良品质。\n而火药虽起源于中国,发展却主要凭借经验,鲜有理论的层层揭示,也靠手工制作,靠舂碾,颗粒粗糙。\n2.1.4 舰船 对照中英武器装备,差距最大者,莫过于舰船。\n英国海军为当时世界之最,拥有各类舰船400余艘,其主要作战军舰仍为木制风帆动力,似与清军同类,当相较之下,有下列特点:\n用坚实木料制作,能搞风涛而涉远洋 船体下部为双层,抗沉性能好,外包铜片,防蛀防火 船上有两桅或三桅,有数十具风帆,能利用各种风向航行 军舰较大,排水量从百余吨到千余吨 安炮较多,有10至120门不等 此外,诞生于工业革命末期的蒸汽动力铁壳轮船,也开始装备海军。\n清军的海军,时称“水师”,并不以哪一国的舰队为作战对象,其对手仅仅是海盗。\n2.2 兵力与编制 武器装备有着物化的形态,其优劣易于察觉,因此不同的人们都得出相同的结论:清朝处于兵器上的劣势。\n许多人又不约而同地指出:清朝在鸦片战争中处于兵力上的优势\n单从数字来看,这是事实。 清朝有八旗兵20万,绿营兵60万,总兵力达80万,而英国远征军,海陆军合并计算,大约7000人。\n但就作者考证,清军不是一支纯粹的国防军,而是同时兼有警察,内卫部队,国防军三种职能。\n以绿营军为例,绿营军大多是以数名,十数名,最多数百名(200名)分散在当时的市镇要冲等地。 清朝是靠武力镇压而建立起来的高度中央集权的政权,军队是其支柱,而当时清朝没有警察, 维持社会治安,保持政治秩序就成了清军最重要最大量的日常任务。\n所以理解成,总兵力80万,里面有很大一部分是警察和城管,打小贩可能在行,打英军就不大行了。\n因为清军没有常备的国防机动力量,因此抽调是鸦片战争中清军集结的唯一方法。 而鸦片战争中每个省能抽调的兵力,不足万人(四川最多是7500人),所以只能从各地,东拼西凑出兵力,兵不知将,将不知兵。\n更要命的是清军的调兵速度,鸦片战争期间,清军调兵的大概速度是,邻省约30至40天,隔一二省约50天,隔三省约70天,隔四省约90天以上。 如此缓慢的调兵速度,使清军丧失了本土作战的有利条件。\n当时英海军从南非的开普敦驶至香港约60天,从印度开来约30至40天,即使英国本土开来也不过4个月。 蒸汽机的出现,轮船的使用,又大大加速了英军的速度,从孟买到澳门,只需25天。\n也就是英军从印度调兵,可能比清政府调兵还快. 何况,英军坐船,清军靠人力,劳师远征,到达战场也没法形成战斗力。\n2.3 士兵与军官 清朝的兵役制度是一种变形的募兵制。早期的八旗是兵民合一的制度,清入关后,人丁生繁,兵额固定。 绿营兵募自固定的兵户,与民户相比,兵户出丁后可免征钱粮赋税。\n清兵收入不高,大抵可养活自己,但无法养活家人,因此兵丁大多去有第二职业. 把当兵当作固定的旱涝保收的「铁杆庄稼」,值班充差操演时上班打卡,其他时间则操持旧业。\n清军军官的来源,主要是两途:一是行伍出身;二是武科举出身。\n正如认为八股文可以治天下一样荒谬,清代武科举场内考试项目是武经七书(《孙子》,《吴子》这些). 与近代战争的要求南辕北辙,因为很多考生不识字,导致错误百出,因此武科举以外场为主,集中一项,即拉硬弓。\n清代军官的升迁,除军功外,均需考弓马技能,若不能合格,不得晋职。\n用今日的眼光观之,这种方法挑选聘出来不过是一句优秀的士兵,而不是领兵的军官。\n由此,在当时人的心目中,军官只是一介鲁莽武夫,「不学无术」成为军官的基本标志。 军人的身份为社会所鄙视。所谓无官不贪,军官也不能免俗。主要手段有:\n吃空额:这个就是人人知详的手段了。 克兵饷:传统手法 创意手段:浙江军官出售兵缺(毕竟是国企岗位);广东绿营开赌收费;福建水师就比较有创意,将战船租赁给商人贩货运米 因此,在鸦片战争中,清军在作战中往往一触即溃,大量逃亡,坚持抵抗者殊少。 在这种情况下,谈论人的因素可以改变客观上的不利条件,又似毫无基础可言:毕竟,清军已经腐败。\n3 政治与文化 3.1 定于一尊 自明朝废宰相之位后,中国王朝政府已经没有首相,皇权得到前所未有的加强,由皇帝独断朝纲。 清朝有着宰相之名的军机大臣,不过是按照老板意思,草拟旨意的秘书。\n这种政权体制下,一切的决断都由君主作出,也就一切都取决于君主的好恶与见识。\n如果是个英明无比的君上,大概能发挥该政体的长处. 但君主能成为君主,不是因为他有着治理国家的才能,只是因为他是上一代的君主的几个继承人之一。\n因此,从统计学来说,出现一个平庸之主的概率就会非常大。\n而英国当时已实行君主立宪,由首相代替国王治理国家,首相有对应选拨淘汰机制,不至于久为庸人。\n人臣诸事听命于君主,没有任何的灵活性(除非是抗旨不遵),也不想因此做事而承担相应的任何责任。 这一幕,似乎在当下,又再度重演。\n全书看下来,把权力全部集中于一个人身上,有着巨大的风险与弊端。 各级政府有处理事务的责任,但却没有自由采取措施处理事务的权利。\n事事都要先问询君主,而君主对事情发展的了解,仅限于臣下的启奏, 臣下出于种种考虑,又不会(或不能)把完全事情的全貌告知君主,导致决策最需要的信息疏漏,让君主决策时误判。\n又因为决策错误的责任不能归咎于君主,因此决策失误,人臣也很难指出,只能任由事情发展; 而臣下为了避免背上违背上意,抗旨不遵的责任,避免做多错多,往往又会听任问题发展,直到出现更大问题。\n这是严重缺乏灵活度和弹性的制度。 在黑天鹅来临时,君臣按此种方式来处理突发事件,非常容易让事情的严重性和后果扩大,直到难以收拾的地步。\n英军的来临是黑天鹅,疫情的到来也是黑天鹅。\n关于皇帝与官僚的关系分析,可以参考《皇帝与官僚:「上面」与「下面」》:\n3.2 天朝上国与蛮夷 鸦片战争以前,中华文明一直是相对独立地发展的,并以其优越性,向外输出,在东亚地区形成了以中国为中心的汉文化圈。\n长此以往,中国人习惯以居高临下的姿态,环视四方。清王朝正是在这种历史沉淀中,发展完备了「天朝」对外体制。\n在古代,依据儒家的经典,中国皇帝为「天子」,代表「天」来统治地上的一切。 皇帝直接统治的区域,相对于周边的「蛮荒」之地,为「天朝上国」。 而周边的地区的各国君主,出于种种动机,纷纷臣属于中国,向清王朝纳贡,受清王朝册封。 至于藩属国以外的国家,包括西方各国,清王朝一般皆视之为「化外蛮夷之邦」。\n3.3 奸臣与忠臣 在清政府禁止鸦片这种败坏国民的货物的贸易,销毁无数鸦片之后;英国贸易负责人向英国求援,请求派遣军队,通过武力打开中国市场。\n当时的中国元首,最高领导人道光皇帝在知道有胆敢犯上的蛮夷军队时,自然是一心剿灭。 不成想蛮夷军队,船坚炮利,连克广东城市,而又闻其是因为林则徐禁烟,受了「冤屈」, 找大皇帝作主伸冤,因英夷「情词恭顺」,于是心思由剿转向「抚」。\n而主抚停战期间,辗转听闻了臣下转述的,已经隐瞒许多实情的和约要求,又大为火光,旨意顿变,由主「抚」转向「剿」。 至此,大皇帝心意已决,「剿」,且天朝无任何败之理由。 直至,英军兵临南京城下,切断北方的粮食运输,道光妥协求和。\n令人惊讶的是,主持过战局的12个官员,几乎没有一个是如实向老板反馈实情的. 以致于到鸦片战争末期,道光老板还觉得英军只是船坚炮利,但腰硬腿直,不擅长陆战。\n每个主持的官员,战前都是和老板说,武备准备充足,定叫英夷有来无回。 但每场战争之后,老板收到的是城破师溃的消息,当然,下属不会直说,而是以各种故事来包装,美化自己,比如皆因汉奸协助云云。\n在鸦片战争中,主持战局的,除去林则徐,历史刀笔下的诸人,大多是「奸臣」,「卖国贼」。 为何会导致这种结果呢,而他们是否真的卖国呢?作者就给出了自己的解释:\n从功利主义的角度来看,这种说法首先有利于道光帝。 在皇权至上的社会,天子被说成至圣至明,不容许也不「应该」犯任何错误。 尽管皇帝握有近于无限的权力,因而对一切事件均应该负有程度不一的责任。\n但当时人们对政治的批判,,最多只能到大臣一级。 由此产生了中国传统史学,哲学中的「奸臣」模式:「奸臣」欺蒙君主,滥用职权,结党营私,致使国运败落; 一旦除去「奸臣」,圣明重开,拨月见月。不是党的政策不好,只是下面的人执行变了形,党是英明的。\n这一模式使皇帝直接免除了承担坏事的责任,至多不过是用人不周不察,而奸臣去承担责任,充当替罪羊。\n此外,按照「夷夏」的观念,这些蛮夷胆敢进犯天朝,唯一正确的方法就是来一个「大兵进剿」,杀他个片甲不留。 既然「剿夷」是唯一正确之途,时人也就合乎逻辑地推论: 战争失败的原因在于「剿夷」不力,之所以「剿夷」不力,在于有「奸臣」的破坏,能对战局产生影响, 肯定不止一个奸臣,那些战败的官员,一定没有尽忠报国。 与奸臣截然对立的,是忠臣的精忠报国。\n于是,时人把希望寄托于或阵亡(如关天培),或主战到底的官员(林则徐)。 他们的结论是:只要重用林则徐,中国就可能胜利,如果沿海彊臣均同林则徐,如果军机阁均同林则徐,中国一定会胜利。 看完全书的人会意识到,林则徐只不过是和其他人一般的清朝官员,只是开明些罢了。\n忠奸论所能得出的结论是,中国想要取得战争的胜利,只需罢免奸臣及其同党,重用林则其同志即可,不必触动中国的现状。 也就是说,只要换几个人就行,不需要做改革。\n忠奸论的最终结论是,为使忠臣得志,奸臣不生,就必须加强中国的纲纪伦常,强化中国的传统。 也就是,鸦片战争所暴露出来的,不是「天朝」的弊端,不是中华的落伍; 反而证明了中国的圣贤经典,天朝制度的正确性,坏就坏在一部分「奸臣」并没有照此办理。\n于是,中国此时的任务,不是改革旧体制,而是加强旧体制\n4 失败的价值 一个失败的民族在战后认真思过,幡然变计,是对殉国者最大的尊崇,最好的纪念。清军将士流淌的鲜血,价值就在于此。\n可是,清朝呢?它似乎仍未从「天朝」的迷梦中醒来,勇敢地进入全新的世界,而是依然如故,就像一切都没有发生。\n《天朝的崩溃》成书为20世纪末,人们说,19世纪是英国人的世纪。20世纪是美国人的世纪。21世纪呢?\n也有些黑头发黄皮肤的人宣称,21世纪是中国人的世纪。 可是,真正的要害在于中国人应以什么样的姿态进入21世纪?中国人怎么才能赢得这一称号。\n人们只有明白看清了过去,才能清晰地预见未来。 一个民族对自己历史的自我批判,正是它避免重蹈历史覆辙的坚实保证。\n而认清弊端,是修正弊端的必经之路。\n「后人哀之而不鉴之,亦使后人而复哀后人也」。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%A4%A9%E6%9C%9D%E7%9A%84%E5%B4%A9%E6%BA%83/","summary":"1 前言 历史教科书说,与英国的第一次鸦片战争,敲开了清朝的大门,清朝也因此和外国签订了一系列不平等条约. 而因为清朝的落伍,导致清朝不敌英国. 教","title":"天朝的崩溃"},{"content":"1 命运的必然 公元前219年,汉尼拨翻过阿尔卑斯山,向意大利发起进攻,指挥官科尔涅利乌斯迎战汉尼拨,被重伤。幸而从手下的新兵抢回到营地,而这名新兵,恰好是首次参加战斗的,科尔涅利乌斯的儿子,年仅17岁西庇阿。\n3年后,当罗马人集结举国之力,与汉尼拨展开坎尼会战,最终惨败, 仅有不足万人逃回了罗马。而身在岳父埃米利乌斯军团的西庇阿, 再次见证罗马军团败于汉尼拨的精妙战术之下,也又一次在汉尼拨的手下逃脱。\n公元前218年,汉尼拨从西班牙出发,翻过阿尔卑斯山,进攻意大利。为了让汉尼拨无法从西班牙获取支援与补给,并釜底抽薪,彻底把把汉尼拨关在意大利,西庇阿的父亲与叔叔,执政官科尔涅利乌斯与弟弟格奈乌斯各率领一个军团进攻西班牙。\n历时8年,科尔涅利乌斯与格奈乌斯终于夺取了迦太基统治下的西班牙三分之一的区域,但两支军团却需要去面对迦太基3支军队\n。公元前211年初夏,科尔涅利乌斯军团遭受到三支迦太基军队的围攻,兵力处于绝对劣势的罗马军队,其退路被切断,最后被一举消灭。迦太基的一支军队在打败了科尔涅利乌斯后,又袭击了不远处正在行军的格奈乌斯军团。在遭到三倍于自己的敌军进攻后,这支罗马军团与友军一样也被消灭。科尔涅利乌斯兄弟花了整整8年时间,取得的成果转眼化为云烟。\n罗马绝对不能放弃西班牙战线,为此需要一句统帅担任该战线的总指挥官。但此时,罗马已经向汉尼拨发起了正式攻势。在这种情况下,罗马没有可以动用的将领派往其他战线。\n一个年纪不大的年经人推开了元老院的大门, 在年已64岁的元老院第一人费边与59岁的“罗马之剑”马尔凯鲁斯眼里,这个24岁的年经人看上去,像个未成年人。\n他叫普布利乌斯-科尔涅利乌斯-西庇阿,他主动请缨,要求担任西班牙战线的总指挥官,代替战死在西班牙的父亲完成任务。在共和政体下的罗马,指挥一个由两个军团共2.5万名到3万名士兵所组成的作战单位的统帅,迄今为止,这类职位都是由执政官或法务官担任的。这两个官职的资格年龄都在40岁以上。\n一年后,西庇阿说服了元老院的元老,以「前法务官」之职指挥军团,即使现在他从来没有担任过法务官,时年25岁。\n2 闪耀西班牙 公元前210年,这位25岁的年经统帅,来到罗马军队在西班牙的军队,迎接他的是历经8年的艰苦战斗幸存下来的士兵,他所做的第一件事就是要消除士兵们的失败阴影。他把他们召集起来说,昨天的事情已经过去,一切明天开始。他还说,虽然自己年龄不大,但是,海神波塞冬在保佑自己,他甚至让大家相信,自己真正的父亲不是战死在西班牙的科尔涅利乌斯,而是海神波塞冬。(这些军事奇才们,为了笼住部下士兵的心,通常会绿掉自己的母亲)笃信诸神的罗马士兵,听了这番话以后,开始觉得自己一方一定能获胜。\n接着这位年经的统帅开始收集包含友邦马赛在内的所有地方的情报,包含地研,气候,原住民族的分页情况,迦太基军队所在的位置,兵力等等。\n而后,西庇阿的第一个目标就是汉尼拨在西班牙的根据地,家族经营10年的城堡,「新迦太基」卡塔赫纳。\n仅一天时间,西庇阿就攻克了敌人的根据地\u0026ndash;卡塔赫纳,而迦太基的三支军队根本来不及赶回来增援。\n通过这场战斗的胜利,这位年轻人使罗马完全恢复了两年前因其父亲和叔叔失败而失去的在西班牙的势力。\n次年,西庇阿迎战三支军队,趁着三支军队还没有合兵于一处,首先击败了汉尼拨二弟哈斯鲁鲍尔率领的军队, 消灭8000人,俘虏1.2万人。\n公元前207年,汉尼拨的幼弟马可尼与迦太基将领吉斯戈合兵一处,共7.4万士兵,与西庇阿4.8万士兵决战。最后,以汉尼拨的围歼战法,打败了迦太基的军队,仅有6000人逃离战场。\n公元前206年的冬季,西庇阿在已经到手的西班牙境内留下两个军团守卫,自己带着长期在这里坚守战斗的老兵,走海路回罗马,他已经四年未回那里了。\n3 进攻迦太基 回到罗马后,西庇阿向元老院汇报在西班牙的战况,即使他还不是议员,离30岁的年龄资格还差几个月。结束汇报后,没有要求举行凯旋仪式。他的战线无愧于凯旋将军的称号,但在共和政体的罗马,指挥一个战略单位,即两个军团的总指挥官资格必须是年龄40岁以上的执政官,前执政官,法务官或前法务官。\n西庇阿是破例作为总司令官被派往西班牙,而当时他的年龄只有25岁,即使完成西班牙霸权的现在,他也只有29岁。在这个年龄上要求元老院为他举行凯旋行为,似乎过于奢望\n对罗马将军来说,凯旋仪式是至高荣誉,西庇阿放弃了这一荣誉,取而代之,他要求元老院推荐自己为第二年即公元前205年度的执政官候选人。即使是明年,西庇阿也只有30岁,与执政官的年龄差距仍是10岁。\n结果,深受民众喜好的西庇阿,在市民大会上,以压倒多数的票数当选执政官。\n执政官是共和政体罗马的最高官职,同时也是军队的最高司令官,由市民大会选举产生。但是,这两位执政官的任地原则上通过抽签决定。但事实上,指挥一个战略单位即两个军团的所有总司令官,包括两位执政官负责的战场都是由元老院决定的。 但西庇阿请求元老院让自己负责北非战线。\n而后,西庇阿在元老院展开演讲,阐述自己的「围迦救罗」战略:\n早晚我会和汉尼拨交锋,但是,这场交锋,不是他在发动进攻的时候。我要主动出击,引他出击,让他不得不与我展开会战。战场不应该在卡拉布里亚已经毁了一半的城堡,而是在迦太基!\n因为这与现有的持久战战略相违背,也正因为持久战战略,才把汉尼拨逼到无处腾挪之地,现状的确没有必要改变,不然有让汉尼拨猛虎出笼的危险。但西庇阿的战法又的确有可取之处。最后,双方达到一个折中的意见,既不伤害元老院年长者的体面,又不违背年轻议员们的变革意愿:西庇阿的任地在西西里岛,并授予一个权力,如有必要,第二年可以进军非洲。\n只是作为执政官,他必须放弃指挥在首都完成编组的两个军团的指挥权,也就是放弃执政官应得到的指挥正规军的权利。取之代之,他有权在西西里招募志愿兵。元老院明确表示:将来西庇阿远征非洲,不是元老院认可的军事行动,一旦远征失败,责任不在元老院,而在他个人。\n公元前204年春天,西庇阿以前执政官身份,率领志愿军踏上了非洲的土地。以2.6万士兵,迎战迦太基与盟国努来米底亚联军,共9.3万人。以汉尼拨的围歼战术,再次获胜,直逼迦太基首都。\n恐慌中的迦太基政府,发出了召回汉尼拨的命令。\n4 击败汉尼拨 在《汉尼拨的遥望》一文中,曾经提到, 罗马人与汉尼拨进行过五次决战,前四次都以罗马人大败而收场,而现在终于来到了16年后的第五场会战,扎马战役。由归国的汉尼拨,迎战学习其战术的「学生」西庇阿。\n如果要列举古代5位名将,汉尼拨和西庇阿一定是其中的两位。如果要列举迄今为止历史上的10位优秀将领,他们二人无疑也会位列其中。虽然历史造就了无数优秀的武将,但是,发生在具备同等才能的人之间的会战,却少之又少。这少而又少的事情,就在扎马战场上演。\n在开战前一日,西庇阿和汉尼拨各带一队骑兵离开了各自的营地, 各带翻译,前往指定的地点. 指定的地点位于两军之间的一个小山丘上,骑兵留在山下,只有二将带着翻译继续前往。\n具有同等才华的名将交战极为罕见,在交战的前一天,这样的两个人坐在一起会谈,在历史上也是空前绝后的。\n公元前202年,秋日的阳光柔柔地照在扎马和纳拉格拉之间开阔的平原上。两军分别在这里摆下阵型。迦太基军队的总指挥是汉尼拨,战斗力为步兵4.6万人,骑兵4000人,合计5万人。罗马军队由西庇阿担任总指挥,步兵共3.4万人,骑兵6000人,共计4万人。\n扎马平原重现了14年前坎尼平原上发生的事情,只是,对象变了。\n古代屈指可数的名将,45岁的汉尼拨只有眼睁睁地看着自己的亲兵纷纷被杀,超过2万个战士在扎马遭到全歼,还有多达2万人被捕。余下的人向着10行程之外的首都迦太基逃去。汉尼拨只带着数名骑兵,逃离战场。扎马战役中,罗马方面的战死者是1500人,西庇阿完胜。西庇阿也成为唯一一个击败汉尼拨的罗马将领。\n普布利乌斯-科尔涅利乌斯-西庇阿从此被人们尊称为“阿非利加努斯”,意思是“征服非洲的人”。\n5 风流总被雨打风吹去 15年后,被尊称为阿非利加努斯,长年占据元老院“第一人”的西庇阿,被检举。质疑部分军费,在西庇阿远征马其顿王国中下落不明,要求其接受传讯与审查。甚至提起17年前,西庇阿在西西里曾经越权,前往任地外的地方攻打迦太基军队之事。\n像这种没有事实根据的检举,与其说是检举,不如说是弹劾,政敌把西庇阿赶下来的一种手段。审判的第一天,在两位检举人的轮番揭发中结束,第二天由被告为自己作辩护。\n「这让想起了《三体》里面的执剑人,罗维。放下剑后,被控谋杀罪」\n这一天,西庇阿迟到了,他带着一大群朋友和支持者来到人群簇拥的会场:\n两位护民官及罗马各位市民,今天,是我在非洲扎马与汉尼拔和迦太基军队作战,有幸取得胜利的第15个纪念日。在这样一个值得纪念的日子里,我建议让我们忘掉一切争执,大家团结一心,向诸神奉上我们的感恩之心。\n现在,我就要出发前往卡匹托尔山,向供奉在那里的、以最高神朱庇特和朱诺女神及密涅瓦女神为代表的诸神表示感谢,感谢他们给了我和参加那次战役的所有罗马市民为祖国罗马的自由和安全竭尽全力的机会。\n各位,如果你们愿意,我诚心邀请各位与我同行。希望各位和我一样对诸神心怀感激之情。因为从我17岁开始到已显老迈的现在,是罗马的各位市民给了我打破常规的机遇,让我有机会发挥了自己的才能。\n说完,没等人们开口回答,西庇阿就离开了会场,他的身后,不只有他的朋友们和支持者们。因为所有罗马人终于都想了起来,元老院议会站起了身,旁听的市民们离开了会场,连书记员都放下铁笔,跟随在西庇阿之后。会场只剩下两个检举人和西庇阿的政敌。\n历史学家李维写道:这一天,身着托加的西庇阿已经不见昔日的风采。他脑袋秃顶,身体羸弱。但是,市民们对他的敬爱之情,胜过第二次布匿战争结束,从非洲凯旋时,人们向三十几岁的年轻胜将发出喝彩声,并送上鲜花。李维还说:“这一天,成了西庇阿灿烂辉煌的最后一天。”\n第二天,西庇阿离开了罗马,在海边的别墅过起了隐居生活。4年后,西庇阿-阿非努加利斯在别墅出世,享年52岁,但他留下遗言,拒绝葬在祖祖辈辈的墓地里,原因是墓地在罗马境内:\n不知感恩的祖国,你们有何资格拥有我的遗骨\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%A4%A9%E9%80%89%E4%B9%8B%E5%AD%90%E8%A5%BF%E5%BA%87%E9%98%BF/","summary":"1 命运的必然 公元前219年,汉尼拨翻过阿尔卑斯山,向意大利发起进攻,指挥官科尔涅利乌斯迎战汉尼拨,被重伤。幸而从手下的新兵抢回到营地,而这名","title":"罗马人的故事(二):天选之子西庇阿"},{"content":"1 夫国以一人兴,以一人亡 经过三次,历经百年的布匿战争,腓尼基民族建立起来的北非强国迦太基灭亡,地中海被罗马人称为“我们的海”。\n可以说,如果没有布匿战争,即使罗马能称霸地上海,也不会只历经短短的130年。\n在三次布匿战争中,有如同漩涡中心一般的两个人,让罗马与迦太基的命运,漩向了两个不同的方向,其中一个,即为汉尼拨。\n2 蛰伏 第一次布匿战争,历时23年,因西西里岛的城邦大国锡拉库萨企图入侵邻国墨西拿而起,演变为新兴农业大国与传统海洋强国迦太基的全面战争,最终以罗马胜出,西西里岛的城邦或成为同盟国,或成为行省;迦太基全面退出西西里岛而结束。\n而后期指挥迦太基军队的总督,正是汉尼拨的父亲,哈米尔卡。哈米尔卡在第一次布匿战争的最后6年,曾经英勇奋战,但却不得不代表迦太基政府向罗马求和。\n当时还不到40岁的哈米尔卡为此备感屈辱,时刻不忘记一雪前耻; 而这样的意志,由年幼的长子,汉尼拨所继承。\n哈米尔卡在第一次布匿战争后,离开了纷争的祖国,带领家人与追随者,跨上了征服西班牙,建立新根据地之路。\n苦心经营10余年,西班牙成为迦太基新的财源,弥补失掉的西西里岛。在18岁那年,父亲在一场攻城战中去世,可即使如此,汉尼拨从未曾忘记父亲的遗志,征服罗马。\n26岁那年,在所有方面都已经成熟起来之后,汉尼拨担任了西班牙的总督。28岁,汉尼拨开始他复仇罗马的行动,从攻打罗马的同盟城市萨贡托开始。\n罗马与同盟城市关系类似互生互存,同盟城市享有与罗马市民一样的权利,除了无法像罗马市民那样享有选举权与被选举权,参与罗马的国政,而他们的义务即是在战时,为罗马提供兵力。身为盟主,罗马有保护同盟城市的义务。\n起初,罗马尝试通过外交手段来解决问题,但汉尼拨对此只是一味的拖延与搪塞。8个月后,萨贡托被攻破,全城居民被贩卖为奴,至此,罗马对迦太基宣战。\n3 出世 汉尼拨的终极目标是打败罗马,但以西西里岛为战场的第一次布匿战争已经证明,在意大利以外的地方作战,不可能战胜罗马。要战胜罗马,战场只能在罗马的土地上,在意大利境内进行。\n但意大利地形像一只靴子一样伸向地中海,东西两侧是海,南面是西西里岛,在当时,三侧防线可谓固若金汤,根本无法进入意大利本土。\n可走的路,只有从北方进入意大利。但是,那是一条未有前人尝试过的路。公元前218年,汉尼拨率领他的军队,向北横渡埃布罗河,翻过比利牛斯山脉,进入现在的法国,当时的高卢,再渡过罗纳河,横穿法国,最后,翻越阿尔卑斯山,进入意大利。\n其战略之宏大,2000年后的今天看来,依旧震撼。从西班牙出发时,汉尼拨有士兵共5.9万人,而5个月后,当他们历经风雪雨水,泥沼森林,翻越过阿尔卑斯山时,只剩下步兵2万人,骑兵6千人,一路上留下了多达3.3万人的尸骸,但他做到了前人未曾尝试的伟业。\n4 闪耀 第二次布匿战争,被罗马人称为「汉尼拨战争」,罗马人共与汉尼拨正面发生了五次会战,还有三次罗马人与其他迦太基将领的会战。\n前四次会战发生在相近的3年间。以武力闻名意大利的罗马,四次会战战果如下:\n4.1 第一次会战,提契诺战役 时间:公元前219年\n罗马指挥官:科尔涅利乌斯,兵力:两个军团,约2.1万 汉尼拨,兵力:2.3万从西班牙带来的士兵与中途加入的高卢士兵1万人,共3.3万人 战果:汉尼拨大胜,罗马指挥官被重伤。 4.2 第二次会战,特雷比亚战役 时间:公元前219年\n罗马指挥官:塞姆普罗尼乌斯,兵力4万人,其中骑兵4000人 汉尼拨,兵力3.8万人,骑兵1万人 战果:汉尼拨可忽略不计,歼灭罗马2万人,俘虏1万人,幸存者不超过1.5万人;汉尼拨大胜。 4.3 第三次会战,特拉西梅诺战役 公元前217年\n罗马指挥官,弗拉米尼,2.5万罗马士兵 汉尼拨,因为有不满罗马政策的高卢人加入,兵力涨到5万人 战果,以有备算无备,汉尼拨大胜。超过2万名罗马士兵战死,仅有2000人逃回罗马,而汉尼拨侧损失侧2000人,大多还是高卢士兵。 4.4 第四次会战,坎尼会战 时间,公元前216年\n罗马军,指挥官特雷恩蒂乌斯,兵力87200人,其中7200人是骑兵 汉尼拨,兵力50000人,其中10000人是骑兵 战果,汉尼拨大胜,战死5500人, 其中三分之二是高卢兵,罗马方战死超7万人。 4.5 战略之战 罗马统治意大利的方式,并不是用武力征服整个意大利,然后将原有民族变化奴隶,然后让罗马人移居到这些城市,以此来统治大片的土地。战败的城市,变成罗马的同盟城市,享有与罗马市民几乎一样的权利,承担相应的兵役义务。而汉尼拨认为,即使占领罗马首都,也没法彻底消灭罗马,消灭罗马的唯一方法,就是让罗马联盟分崩离析。只有这样打掉罗马的外围后才能一举占领对方的大本营\n因此,在获得4埸会战后,汉尼拨仍在坚持离间罗马与同盟城市的关系,而非直接挥军攻打罗马的首都。\n而接连在会战中输给汉尼拨,让罗马人不得不承认,在会战中,没有办法能战胜汉尼拨,因此执政官费边提出了「为了不输给汉尼拨,只要不交手的就可以」的持久战战略,主张围困汉尼拨,缩小汉尼拨的活动空间。\n就这样,汉尼拨与罗马进入了相持阶段,这一相持,就在罗马的国土上,相持了整整16年。\n5 暗淡 在汉尼拨与罗马相持及反围困的年岁里,一位年经的执政官提出了登陆汉尼拨的宗主国迦太基,开展「围迦救罗」的战略公元前204年春天,这位年轻的执政官,率领2.6万的士兵,踏上了非洲的土地。而后,迦太基在本国境内的第一次会战中,吃了败仗,尚不习惯这种事情的迦太基陷入了深深的恐慌中,恐慌中的迦太基政府接受了把汉尼拨召回国,与罗马人决一死战的提议。\n那一年,汉尼拨接到了回国的命令。现存文献中找不到任何描述他接到命令时内心波动的记录。这一年,他已经44岁,距离进入意大利,第16个年头快要过去了。\n为了父亲的遗志,为了自己的理想,这个男人在敌国领土,坚持了16年。而在这16年间,除了一次有4000名士兵在罗马军队的进攻下投降罗马以外,没有一个人,真的没有一个人,离开汉尼拨。\n汉尼拨并不是一个随和的人,更别说和士兵们打成一片,既然如此,在任何时间都不失孤傲的汉尼拨被逼进弹丸之地后,士兵们依然追随于他,空间是为什么呢?\n也许像马基雅维利说的那样,一方面可能是慑于他的威严,另一方面,对这位才能卓越却陷于困境的男人,也许有一种谅解的情感。\n一位领袖之所以优秀不是因为他具备卓越的才能,而是他能让追随者觉得自己在这个集体中必不可少。人与人之间能长期维持的关系,一定是相互依存的关系。不是相互依存的关系,很难指望会长久。\n汉尼拨接到回国命令时,就在克罗托内。克罗托内是一个港口城市,向南延伸的一个海角有一座神殿,供奉的是这一带希腊族信奉的女神赫拉。\n现在,神殿只剩下一根圆柱,在古代却是一个因漂亮外形而闻名的神殿。接到回国命令后,44岁的迦太基统帅命人在这座神殿祭坛的一面墙上,嵌入一块刻有文字的铜板。铜板记录了汉尼拨离开西班牙以后的所有战果。\n(罗马人对汉尼拨留下的这些东西一定恨之入骨,但是,直到50年后,历史学家李维看到时,这块铜板依然完好无损,不得不说,罗马人很有意思)\n汉尼拨乘船返回迦太基,船队离开克罗托内港,向迦太基驶去。\n很长一段时间,可以从船上清楚地看到矗立在海角前端的白色大神殿渐渐远去,直到消失在遥远的地平线。没有史料记载快满45岁的汉尼拔是怀着怎样的心情遥望这座神殿的。也许他根本就没有看\n而漩涡中心的另一人,名为西庇阿,即是那位年轻的执政官。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E6%B1%89%E5%B0%BC%E6%8B%A8%E7%9A%84%E9%81%A5%E6%9C%9B/","summary":"1 夫国以一人兴,以一人亡 经过三次,历经百年的布匿战争,腓尼基民族建立起来的北非强国迦太基灭亡,地中海被罗马人称为“我们的海”。 可以说,如果没","title":"罗马人的故事(二):汉尼拨的遥望"},{"content":"来自《罗马人的故事》第一册,介绍罗马的时候,总离不开希腊文明。\n希腊文明起源于公元前2000年前后的克里特岛。因为特里克岛比希腊本土更靠近当时的先进文明之国埃及。新的文明往往出自自身的周边\n1 克里特文明 克里特文明的鼎盛时期据说是在公元前1700年到前1500年前后(要用据说一词,大概是因为太久远,没有相应的史料)。\n以公元前1350年前后为界,爱琴海的主人克里特文明急速衰退,不清楚是大地震的缘故,还是因为来自希腊本土的进犯。\n总之,到了公元前1350年前后,首都克诺索斯遭到破坏,优雅而华丽的克里特文明敲响了晚钟。\n2 迈锡尼文明 曾经的周边变成了中心,在它周围又形成了新的周边。位于希腊本土南部伯罗奔尼撒半岛的迈锡尼一带成了希腊文明新的中坚力量-历史上称为迈锡尼文明。当时好像还是军人统治的国体,这些军人因荷马史诗《伊利亚特》和《奥德赛》而为后世的我们所熟知(的确令人惊讶,这样的世界名著,竟然成书于3000多年前)\n然而,以公元前1200年为界,迈锡尼文明也消失了,作为迈锡尼中坚力量的人或被杀,或被逼为奴,从而被彻底挤出历史的舞台,被北方南下的多利亚民族所消灭。在此以后,整个希腊沉寂了整整400年,公元前1200年至前800年长达400年的沉寂时期,在希腊史上称作“希腊的中世”,意思是一切归于沉寂,夹在以活跃为特点的两个时代的时期。\n3 城邦国家时代 公元前800年前后,希腊人走出他们的“中世”,进入统称为城邦国家的时代。由多利亚人建起来的斯巴达和因多利亚人入侵而出逃的阿卡亚人建立的国家雅典成为城邦国家的代表。随着城邦国家的诞生,希腊人开始了向海外的殖民运动。\n希腊人的殖民运动分两个时期进行,第一次是殖民运动是在公元前9世纪末到前8世纪初,殖民对象主要集中在小亚细亚西岸。爱琴海的意思是多岛海。\n第二次殖民运动是在第一次殖民运动之后,即过去了约半个世纪的公元前8世纪中叶前后。这一时期的殖民范围从爱琴海扩展到整个地中海。\n希腊本土的希腊人去得最多的地方是意大利南部,在海上可以与他们抗衡的,在当时只有由腓尼基人殖民而建起来的迦太基\n对于希腊人来说,公元前8世纪是向海外发展的时期,也是充实国内的时期。正是在这个时期,形成了最能有效发挥希腊人活力的城邦国家,而希腊人发明的国体\u0026ndash;城邦\u0026ndash;的代表就是雅典和斯巴达\n3.1 雅典 3.1.1 贵族政体 传说中雅典的创立者是推翻了克里特暴君米诺斯王的忒修斯,雅典初斯的政体是王政,并于公元前8世纪改为贵族政体。\n在该政体下,9们贵族出身的执政官在一年的任期内,分管内政,军事和宗教,由其他贵族组成的长老会辅佐,自由市民组成的市民大会没有发言权,形同虚设.\n3.1.2 梭伦改革 进入公元前7世纪,这种贵族政体渐渐地暴露出与雅典现状的不相适应,相对于经济基础建立在土地所有制上的贵族阶级,依靠工商业强大起来的新兴阶段开始抬头,但是却空有经济实力却无法参与国政。开始了反抗贵族之路。\n这些自由市民取得的第一个胜利是公元前620年的法律条文化,贵族阶级也因此失去了司法权,无法像在法律不成文的时代那样随心所欲。\n但是这并不能消除“自由市民”的一不满,于是,梭伦登场了,公元前594年,开始了历史著名的“梭伦改革”。\n梭伦自己既不属于新兴的工商业阶级,也不是出身重债缠身的自耕农阶级,而是拥有大片土地,在雅典举足轻重的名门望族之后。\n梭伦首先制定了拯救被重债缠身的自耕农的政策并使之法制化。为此,农民的债务被大幅度地削减。\n同时,他还废除了因无法偿还政务而被迫为奴的旧制度,彻底废除了在古代被认为理所当然地以人身偿还债务的制度,这是古代社会第一个尊重人权的例子\n梭伦自身似乎是个温和的自由主义者,他拒绝了激进派市民的提案,即没收私有土地为国有,然后将土地重新进行平等分配的提案,对此梭伦写道:\n我们给了市民们适当的名誉。我们不剥夺他们已有的权利,但也不再新加任何权利。\n梭伦改革的最大着眼点是政治改革:他首先开展了人口改革,并以调查结果为依据,制定了个人权利与其所拥有的不动产成正比的政策。如此一来,参与国政的权利不再受出身左右。\n梭伦根据财产的多少,将雅典市民分成四个等级,根据收入,从高到低依次为第一等级,第二等级,第三等级及无产市民构成的第四等级。\n各等级业务:\n第一等级,第二等级:义务服骑兵兵役,自备军备,军装与马匹 第三等级:义务服重装步兵兵役,自备军备,军装 第四等级:义务提供轻装步兵或舰队成员 各等级义务:\n政府要职由第一,第二等级的市民担任 行政官僚由第三等级担任 第四等级只有选举权 3.1.3 克利斯梯尼改革 开始投资动产的雅典市民,迟早会对以不动产为基础的政体心怀不满,只是没人敢于正面挑战梭伦的权威。在梭伦死后,庇西特拉图作为独裁者登上了历史的舞台。政体的变迁可以从教科书上学到,但是判断一种政体的好坏,有时和教科书不一样,在庇西特拉图独裁的20年间,不仅给雅典带来和平与秩序,还带来了经济上空前繁荣。\n公元前510年,庇西特拉斯的儿子的独裁统治被克利斯梯尼推翻,面对矛盾重重的雅典,克里斯梯尼开始了他的改革。借用亚里士多德的话,他“将体制改革得更加民主”\n行政改革:将雅典的领地分成三大区,每个大区划分20个小区,各小区根据人口再分若干“居民区”。而后, 这种“居民区”成为雅典的行政基础。此项改革被认为是历史上第一个因单纯行政上的目的而将国土进行分割的例子 政治体制改革:克利斯梯尼的改革出现的政体叫民主政体 强化市民大会权力:20岁以上的所有雅典市民有权参加市民大会,实行一人一票(直接民主)。成为国家最高权力机构,决定与外国缔结和约等外交及政府首脑选举等内政事务 保留梭伦改革的四个等级,但划分标准从原来的农业收入,变成无行业区别的收入 五百人会议:由30岁以上的雅典市民组成,负责处理日常政务。(即官僚或政府公职人员) 国家战略官:由任期一年的10人组成政府官员,重新命名为“内阁” 陶片放逐法:市民可以将自己希望放逐的人名字写在陶片上,在市民大会上投票,每年只要过半数,就有权把市民认为权威和权力将会威胁雅典的市民逐出国外10年。放逐不会损害该市民的名誉,即使遭到放逐,当事人也不会觉得羞愧,他不会失去市民权,财产不会被没收,只是被逐出雅典。显然是为了防止某人独裁(但是也会被用来对付政敌) 由此,诞生了世界史上第一个由普通市民直接参与国政的政体。在雅典,无论多么无知,只要是市民,他的权利都受到绝对的尊重。但是不具备市民权这一形式的国籍的人则完全没有参政权。关于成为市民的条件,可以参考罗马人的故事第一册提到的市民条件\n苏格拉底说过,祖国的法律即使不好也要遵守,为此他拒绝了逃亡国外的劝告,而被处以死刑。同时哲学家的亚里士多德则不愿殉法,早早溜之大吉。对于雅典市民来说,雅典是他的祖国,对于出生地不在雅典的亚里士多德,他没有义务为雅典的法律牺牲自己。\n但是,和市民缴纳同样的税金,非但没有被选举权,甚至连选举权也得不到承认的国家,在当今世界也不少见(比说要求人民感恩政府的某国)\n3.1.4 伯利克里时代 伯里克利从进入雅典政界的那一年起,在长达30年的时间里,几乎年年当选“国宝战略官”,并且大部分时间被选为议长。同时代的历史学家修昔底德告诉我们,伯里克利曾经说过这样一段话:\n我们雅典人无须羡慕任何其他国家的政体。我们的政体不是模仿他国得来的。我们的制度要成为别人的模范。我们的政体之所以称为民主政体,是因为政权在多数公民手中,而非少数人手中。\n在这一政体下,每个人在法律上都是平等的;担负公职的人能够得到的荣誉,不是因其出身,而在于他的努力和贡献。任何人,只要他能够对国家有所贡献,绝对不会因贫穷而默默无闻。\n我们的日常生活和政治生活一样,享有充分的自由。雅典市民享有的自由程度之高,甚至连怀疑、妒忌都是自由的。……尽管如此,我们可以享受各种娱乐,丰富我们的精神世界,忘却日间劳作的辛苦。每年在规定的日子里,举行各种比赛和祭祀,不忘让我们的居所变得更加舒适。……\n在教育制度上,我们的竞争对手(隐指斯巴达人)从孩提时代起,即加以最严格的训练,使其成为勇敢的人,而在我们的国家里,对孩子的教育没有他们那样严酷。但是,当危机来临时,我们表现出来的勇气不在他们之下。\n我们不学习他们通过非人的残酷训练来应对考验,我们用每个人所具备的能力,即决断力,来应对考验。我们的勇气不是产生于法律的要求,而是源于每个雅典市民在日常生活中各自的行为准则。……\n我们爱美,但我们有度;我们尊重智慧,但绝不迷恋于此;我们追求财富,但我们只会尽可能地利用它,而不以此炫耀。\n在雅典,贫穷不可耻,可耻的是不为脱离贫穷而努力。\n我们尊重个人利益,却是为了更加关心公共利益。这是因为在追求个人利益为目的的事业中表现出的能力,同样可以服务于公共事业。\n在雅典,一个不关心政治的人,我们不会认为他爱好和平,我们认为他不具有市民的资格。\n时隔2500年后,作为一句x国人,读起这段话,不免心生感慨: 2500年前雅典人的权利与追求,我们到现在都还没有实现,甚至越走越远\n与伯里克利同时代,留下《伯罗奔尼撒战争史》的历史学家修昔底德对伯里克利的雅典所作的评价是:\n表面上看实行的是民主政体,实际上是一个人统治的国家\n3.2 斯巴达 公元前1200年前后,多利亚民族挥军南下,征服了土著居民后,建立了城邦国家\u0026ndash;斯巴达。\n征服者的子孙,构成现有统治阶级的斯巴达人,他们是自由市民及其家人,共约1万人。服兵役是这些血统纯正的斯巴达人的唯一工作,参与国政的权利也只有这些人享有\n有市民权的斯巴达人并非一成年便有权出席市民大会,行使自己的一票权。他们必须等到30岁才能享有这些权利。市民大会由30岁以上的斯巴达人组成,险些之外,还有长老会议。\n长老会议:共有议会28人,年龄都在60岁以上,由市民大会选出,任期为终身制 国王:人数为2位,由两家名门望族世袭,同时执政。即二头政治 公元前7世纪后半中,来库古进行的改革进一步稳固了这一体制,使得斯巴达的风格更加激进,与梭伦的改革决定了雅典的风格一样,来库古决定了斯巴达的风格\n3.2.1 来库古改革 来库古的改革使斯巴达更加彻底成为一个军事大国,斯巴达人的日常生活更是以军务为至高目的。\n孩子一出生,就要经过长老们的检查。经过检查,判断一个孩子是否能健康、平安地长大,被认为不够健壮的孩子当即会被抛弃或沦为奴隶。\n被认为有希望成为强壮战士的婴儿由父母抚养到6岁,一到7岁,便要离开父母开始集体生活。他们与同龄人一同生活,按照以培养合格战士为目的的严密计划接受教育。\n到了20岁,斯巴达人就开始服兵役,一直到60岁退役。30岁之前有义务过集体生活,即使结了婚,晚上也必须回到兵营。无论是少年的宿舍还是战士的兵营,都没有相应的建筑物,他们都要生活在帐篷里。这样做的目的是为了使斯巴达人可以忍受恶劣的环境。\n在过了30岁才被认为是独立市民的斯巴达人,可以和妻儿一起在有墙有屋顶的室内生活,也只有独立市民才能享有这种权利。\n由于斯巴达人一切都服从于军事目的,所以其军事力量之强大令人惊畏。尽管军队数量很少,但是其威名甚至远震波斯。在希腊,一提起精锐部队,指的就是斯巴达的步兵军团。\n但是,斯巴达除了战士什么都不产。哲学、科学、文学、历史、建筑和雕刻,没有留下任何一样东西。非要说留下了什么东西,那就是一个词——“斯巴达式的”。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%B8%8C%E8%85%8A%E6%96%87%E6%98%8E/","summary":"来自《罗马人的故事》第一册,介绍罗马的时候,总离不开希腊文明。 希腊文明起源于公元前2000年前后的克里特岛。因为特里克岛比希腊本土更靠近当时","title":"罗马人的故事(一):希腊文明"},{"content":"1 前言 以史为鉴,可以知兴替。而罗马,作为西方文明的来源之一,有着漫长辉煌无比的历史,其影响直至今天仍可见。因此,我开了个新坑,阅读盐野阿姨的《罗马人的故事》。当然,这只能算是历史的科普书,其学术价值约等于当年明月的《明朝那些事》,就很适合我这样的非专业人士阅读。\n根据传说, 罗马于公元前753年建国, 根据史实, 罗马于公元前270年完成了意大利半岛的统一\n2 王政时代 2.1 建国之王罗穆路斯 18岁的罗穆路斯, 带领3000名拉丁人, 定都罗马于台伯河东岸. 将国政分成三个机构, 分别是国玉, 元老院和市民大会, 并由这三方共同治理罗马. 就这一分权的举措, 就与东方帝国走上了不同的道路.\n国王: 作为宗教祭祀, 军事和政治的最高领导人, 国王由市民大会投票选举产生 元老院: 由贵族长老组成, 其职责是向国王提出忠告与建议 市民大会: 由全体罗马市民组成, 它的任务是选出以国王为首的各级政府官员, 市民大会没有制定政策的权力, 但是对国王制定的政策有赞成或反对的表决权. 此外, 对外关系上, 是战是和, 也必须说征得他们的同意才可实施 2.2 第二代国王努马 在适当的时候, 把适当的人放在适当的位置上施展才华的事例, 在各民族走向兴盛的历程中比比皆是. 而领导者政德不修, 让整个国家与民族走向衰頹, 也正在上演.\n罗穆路斯死后就任的国王是努马, 历史学习李维在\u0026lt;罗马史\u0026gt;中关于努马的功绩是这样描述的:\n就任王位后的努马, 试图对依靠武力和战争打下建国基础的罗马进行立法和习俗的改革.\n这里所谓的立法改革, 不是要制定全新的法律, 而是要建立秩序, 要让当时逞强好胜的罗马人懂得做人的礼法. 在了解自身力量的局限性的同时, 让他们懂得要对超越自身极限心存畏惧.\n努马认为除了为防御而战之外, 这一时期的罗马不需要战争. 他集中力量发展农业和畜牧业. 目的是在战争取得服务后, 即使不对失败者进行掠夺也能做到自给自足.(在2700年前, 有这样长远的眼光, 着实令人敬佩)\n努马还对罗马市民进行了职业分工, 让每个人归属于有独立保护神的团体.\n努马为了使人们的日常生活变得有序, 还进行了历法改革, 根据月亮的盈亏, 把一年分成12个月, 规定总天数为355天(而650年后, 恺撒才重新修正历法, 把一年总天数定为365天, 也就是今天依旧沿用的历法)\n因为罗马是多神教, 努马还对这些神进行了整顿, 设立了等级制度, 让大家懂得尊重诸神的重要性(有信仰的确让人心存敬畏). 像犹太教, 基督教即是一神教, 即只允许有信仰一个神.\n一神教和多神教的区别不只在于纯粹的神的数量, 还在于是否认同他人信奉的神. 认可他人的神, 意味着认可他人的存在.\n希腊历史学家狄厄尼索斯在其著作\u0026lt;古罗马史\u0026gt;中说过这样的一句话:\n使罗马强大起来的要因在于他们对宗教的见解之中\n对罗马人来说, 宗教不是指导原理, 它只是精神寄托. 因为有宗教信仰, 人性不再受到禁锢. 想到在某个政党的统治下, 连信仰自由这样的自由都失去了.\n和罗马人一样从不向神祈求纠正人类伦理道德的希腊人转而在哲学中探索真理. 因此:\n向宗教寻求纠正人类行为准则的是犹太人\n向哲学寻求纠正人类行为准则的是希腊人\n向法律寻求纠正人类行为准则的是罗马人\n2.3 第三代国王托里斯-奥斯蒂吕斯 继努马之后, 登上王位的是托里斯-奥斯蒂吕斯. 他是拉丁系罗马人, 和罗穆路斯一样, 是个崇尚对外进攻的男人.\n托里斯攻占了拉丁民族的发祥地, 阿鲁巴, 并将居民强行迁居罗马, 但给予他们罗马市民的身份, 吸纳他们成功罗马国民, 进一步壮大罗马.\n托里斯率领罗马军队一次又一次出征, 取得比罗穆路斯还辉煌的军事战绩, 他的统治历时32年, 不过根据历史学家李维的说法, 他是死于雷劈.\n2.4 第四代国王安库斯-马尔西乌斯 安库斯是努马的外孙, 成为国王后统治长达25年, 其中免不了挥军与罗马周围的部族战斗. 除了战斗, 他还完成了几件大事:\n在台伯河架起了第一座桥梁, 上的是把位于西岸的贾尼科洛山和集中在东岸的七个山丘联系起来 他征服了位于台伯河河口的奥斯提亚, 为此罗马终于得以和地中海直接连通. 并在奥斯提亚周边的海滩发展制盐业, 为罗马人提供了不是流通货币的货币( 2.5 第五代国王塔克文-普里斯库斯 塔克文, 一个来自伊特鲁里亚的移民, 在国王死后, 他毛遂自荐要竞选罗马国王, 大概是开展选举活动的第一个罗马人.\n成为罗马第五代国王的塔克文显示出了他超强的领导能力. 在37年的统治期间, 不仅使罗马的势力范围得到进一步的扩张, 而且罗马内部也发生了巨大的改变, 同时市民的生活水平也得到极大的提升, 罗马一跃成为名副其实的罗马城邦.\n他即位后做的第一件事情是增加元老院的人数, 自从罗穆路斯设立元老院以来, 人数一直维持在100人, 而塔克文将它增加到200人, 以此塞进自己的忠实支持者, 以稳固自己的权力\n塔克文与前任国王们一样, 继续征战四方, 但战斗结束后, 他没有让战败者移民罗马, 而是给他们市民权, 继而同化他们.\n他还开发罗马人居住的七个山丘之间的湿地, 挖掘下水渠, 构建大规模的下水道网络. 塔克文领导下的排水开垦事业不仅增加了可用土地资源, 而且也为罗马形成一体, 促进各民族之间的交流, 直到了不可磨灭的作用.\n而后, 塔克文还在最高的卡匹托尔山丘建起神殿, 专门供奉罗马诸神中的最高神朱庇特神, 其他诸神也都各基所.\n2.6 第六代国王塞尔维乌斯-图里乌斯 在当时先王的排水开垦事业和朱庇特神殿建造工程完成之后, 作为先王女婿的国王塞尔维乌斯-图里乌斯的之急就是保卫全罗马的城墙建设.\n这座城墙在经过了2500年后的今天依然叫\u0026quot;塞尔维乌斯城墙\u0026quot;, 在现代罗马, 随处可见其断壁残垣.\n在塞尔维乌斯成就的功绩中, 最重要的莫过于军队体制的改革. 他所进行的这一改革不仅涉及税制改革, 而且涉及选举制度的改革.\n作为一个国民, 他所承担的义务一是缴纳税金, 二是保家卫国. 在古代, 很多国家都以服兵役的形式来抵直接税, 罗马如此, 希腊如此. 只有做到这一点的, 他才是独立的市民. 作为独立的市民, 自然会有相应的权利. 市民的权利就是投票权. 所以军队体制等于税制, 也等于选举制, 这一等式成立, 并且天衣无缝.\n而2500年后, 仍然有某些国家, 只有缴纳税金的义务, 却没有选举的权利.\n另外, 罗马选举制实行的不是一人一票制, 而是按军团的最小单位, 每百人队一票. 百人队中的100个人首先要在内部进行讨论, 形成的统一意见就体现在这一票上. 其实, 它相当于小的选举区制.\n看起来是否很熟悉, 个人感觉, 美国的选举人制度就是来借鉴自罗马的百人一票, 总统选举, 在某个州赢得50%的票, 就可以赢得全部选举人票.\n2.7 最后一位国王: \u0026ldquo;傲慢者塔克文\u0026rdquo; 当塞尔维乌斯执政44年之后, 野心勃勃的塔克文的孙子反叛国王, 与妻子, 即国王的女儿一起谋杀了国王. 并在国内实行独裁统治, 从来不向元老院征求任何意见或建议, 也从业不问市民大会同意与否, 因此市民在背后称其为\u0026quot;傲慢者塔克文\u0026quot;\n在国内实行独裁统治地专制君主\u0026quot;傲慢者塔克文\u0026quot;在军事方面却表现出卓越的才能. 在与周边部族的战斗中, 罗马几乎都是常胜军.\n在一个人强大的时候丑闻不会招惹你, 而一旦显出疲态, 丑闻将毫不留情地击垮你. 即使丑闻与你无关, 但是作为有效武器, 它的作用不可小觑.\n国王有一个儿子叫塞克斯图斯, 看上了亲戚科拉提努斯的妻子琉克蕾西娅, 欲火中烧的年轻人乘琉克蕾西娅的丈夫不在家的夜里, 来到女人的家中, 用短剑相威胁, 占有了女人的身体.\n当天夜里, 琉克蕾西娅就给在罗马的父亲和正在出征的丈夫分别送去一封信, 令其速归. 坐在床上沉浸在悲愤之中的琉克蕾西娅向赶来的父亲与丈夫及朋友说完事情的经过, 就拿出短刀刺向自己的胸膛, 她呼吸艰难地要在场所有男人发誓为她报仇后, 就永远地闭上了眼睛.\n琉克蕾西娅的遗体被送到罗马, 放置在古罗马广场的演讲台上, 面对这一惨状, 人们纷纷指责国王和他一家的蛮横与傲慢. 因此有丈夫的朋友布鲁特斯向市民作出演讲, 历数国王的罪行, 提议将国王和他的家人逐出罗马. 市民纷纷云从.\n\u0026ldquo;傲慢者塔克文\u0026quot;的统治持续了25年, 随着第七代国王塔克文的统治结束, 罗马的王政时代也宣告结束. 时间是公元前509年. 从罗穆路斯于公元前753建国到这一年, 罗马已经走过了244年.\n3 共和时代 随后的罗马进入了共和政体, 迎来了执政官统治的时代. 和从前一样, 执政官也由市民大会选举产生, 任期由终身改为短短的一年, 还有, 原来由一位国王统治改由两位执政官共同治理.\n在战时, 可以任命独裁官, 任期为6个月, 所有人需听从独裁官命令(包括执政官)\n3.1 路奇乌斯-尤尼乌斯-布鲁特斯 巧妙利用丑闻推翻王政的最大功臣是路奇乌斯-尤尼乌斯-布鲁特斯. 他是随后延续500年的共和制罗马的创始人.\n路奇乌斯-尤尼乌斯-布鲁特斯是历史上难得一见的, 兼具先见之明和行动力的人. 因为他的母亲是被逐出罗马的国王塔克文的姐妹, 所以他和国王是舅甥关系. \u0026ldquo;布鲁特斯\u0026quot;这个姓不是他的原姓, 而是他的外号, 意思为\u0026quot;傻瓜\u0026rdquo;. 据说他在专横跋扈的塔克文时代, 一直隐忍着被蔑称为\u0026quot;傻瓜\u0026rdquo;, 结果, 这个外号就成了他的姓氏.\n他认为罗马已经长大, 完全可以废除效率很高却只受国王个人意志左右的王政制度. 他在与被放逐国王企图夺回罗马的一战中, 与国王之子, 表兄阿隆斯激战对决, 双双殞命.\n3.2 \u0026ldquo;亲民者\u0026quot;瓦莱里乌斯 共和政体的创立者布鲁特斯的壮烈牺牲让罗马人悲痛不已. 然而他们的眼泪未干, 就开始猜疑了幸存的执政官瓦莱里乌斯. 认为瓦莱里乌斯凯旋时所乘战车为四匹白马, 过于高调, 有炫耀王者风范的意思, 再者瓦莱里乌斯家里富有, 位置居市中心, 建筑气派, 像国王的居所.\n瓦莱里乌斯为避流言, 连夜拆除自家的房子, 并于便宜地段建了简陋的房屋, 并向众人自由进出, 以示一心为公.\n瓦莱里乌斯而后制定法律, 改善国政:\n制定有关国库的法律, 在王政时代, 国库由国王掌管, 现在则交由财务官管理. 作为政治军事最高权力者的执政不干预国家财政一法赢得了市民们的喝彩. 制定了诉讼的法律: 凡是享有罗马市民权的人, 对法务官作出的判决有权向市民提起诉讼. 有点难以想象, 这是2500年前的法律, 着实体现出对人的权利的尊重. 它的制定, 为后世罗马留下了极其重要的法的概念 过度在乎舆论而制定的一条法律: \u0026ldquo;凡是觊觎王位之人, 无论是谁, 其生命和财产将为诸神所有\u0026rdquo;. 也就是说, 即使杀了人, 只要有证据证明被杀的人对王位有所企图, 就可以赦免杀人者. 证明企图这个本身就相当模糊, 就相当于在法律上开了个免除罪罚的口子. 在公元前509年至前503年的6年间, \u0026ldquo;亲民者\u0026quot;瓦莱里乌斯共当选了四届执政官, 因此期间实施的政策可以认为基本出自这位\u0026quot;亲民者\u0026rdquo;:\n盐收归国有: 过去奥斯提亚盐田出产的盐是由个人经营, \u0026ldquo;亲民者\u0026quot;经营权收了回来, 改由政府经营. 他试图通过这一改变, 来恢复因伊特鲁里亚人外流而日渐下滑的罗马经济. 当时罗马还没有流通货币, 盐在交易外国商品中充当了货币的角色. 相当于货币国有化.\n如果改革仅此而已, 那么只能使用高价盐交易商品的商人对通商的兴趣就会大大减弱, 对恢复经济于事无补. 于是, \u0026ldquo;亲民间\u0026quot;降低了向他们征收的间接税, 因此还吸引了一些本不从商的人也开始纷纷从商(通过税收调整经济政策, 2500年前执政者都知道使用的手段, 某些国家, 就只会在经济下行的时候, 还加税)\n\u0026ldquo;亲民者\u0026quot;非常欢迎外国人移民罗马, 在罗马邻近部族中, 有人说同属拉丁民族之间, 拥有相同语言和相同诸神的拉丁人之间相互争斗毫无意义. (可见, 罗马人是相当开放的, 并没有以血统论身份, 而是真的做到, 来了就是罗马人)\n公元前503年, 罗马改为共和制已经6年了, 这一年, \u0026ldquo;亲民者\u0026quot;撒手人寰, 离世人而去, 此时的瓦莱里乌斯已经散尽万贯家财, 边丧葬费都拿不出来, 是每个罗马人自发捐款, 为\u0026quot;亲民者\u0026quot;举行了葬礼. 和布鲁特斯死时一样, 罗马女人像为父亲离世那样, 服丧一年\n罗马共和政体由布鲁特斯播下种子, 又在\u0026quot;亲民者\u0026quot;的施政中深深扎下了根. 在这两人之后的罗马, 再也没有出现过试图复辟王政的人.\n3.3 贵族与平民的对立 在进行共和政体的罗马, 在其后的80年, 一直到公元前367年, 始终处于摇摆不定的不稳定状态, 贵族和平民之争一直没有得到有效的遏制. 造成这种情况的原因可以列举如下几个:\n归咎于农牧民族的罗马人自古以来的保守性格. 罗马人本能地厌恶改革, 即使璚非改革不可的时候, 进展也很缓慢. 一旦改革成功, 不会轻易改变. 罗马贵族抗争的态度非常强硬, 并且, 罗马的贵族阶级拥有强大的力量, 足以和平民阶级一决高下. 尽管罗马平民强烈要求少数人统治的政体下的机会均等, 但是他们并没有要求改变少数人统治的政体, 即寡头政治. 尽管他们要求授予自己的代表以统治的权力, 但是他们并没有要求让平民阶级的所有人都参与政权. 王政时期的罗马:\ngraph 王政{ rankdir=LR; 国王 -- 市民大会; 元老院 -- 市民大会; 元老院 -- 国王; } 共和政体的罗马:\ngraph{ 执政官 -- 市民大会; 元老院 -- 市民大会; } 国王是终身制, 由市民大会选举产生, 经元老出家人确认同意, 一位国王只要在王位上坐上30-40年, 势必与元老院的关系变得很松散, 权力的独立性也会很高.\n因为元老院的职责只剩下向国王提建议和劝告. 与此相反, 所有罗马市民都可以参加市民大会, 因为有权对国王行使的政治策略和军事行动投票赞成或反对. 因此, 国王政体的权力构造呈三足鼎立, 是非常稳定的.\n进入共和政体的罗马, 权力构造发生了变化, 由两个执政官同时执政取代了以前的国王, 尽管可以多次当选, 但每次任期都是一年, 而年年选择两位执政官的, 就是各派势力首脑组成的团体\u0026ndash;元老院. 于是, 执政官和元老院之间的距离自然是逐年缩短, 渐渐地, 三足中的两足出现重叠, 直至合二为一.\n共和政体诞生之初的十几年里, 罗马不得不举国一致共同对外, 但是与此同时, 罗马的平民阶级也认识到自己的力量, 他们意识到, 没有他们的参战, 无何止的战斗既不能取胜也无法坚持下去.\n几乎年年不断的战事使他们不得不长时间离开他们工作的农田, 牧场, 施工现场或商店, 直接导致平民阶级的经济状况越来越差. 另一方面, 贵族阶级有大片的农田, 牧场作后盾, 即使不劳动, 经济也不至于很快衰退.\n对抗越演越烈, 平民甚至还有在参加战斗期间, 家里财产因负债被出售或没收的风险, 因此民愤激昂, 外敌来犯时, 再没有人响应执政官的号召, 一致对敌, 平民们固守在埃斯奎里山和阿文庭山,拒不出来, 这是罗马历史最早的罢工运动. 而后罗马市民又进行了第二次罢工运动\n最终的结果是, 与贵族谈判后, 决定设立一个专门以保护平民阶级利益和权利为上的的职位, 这个职位叫护民官, 就任职位的必须是平民阶级出身. (这就是最早的, 用脚投票的结果, 权利不是别人施舍来的, 是自己争取来的). 护民官有权对执政官作出的决定行使否认权, 但限制时, 战时不得行使.\n罗马军常年去外敌作战, 虽说不是无敌之师, 但基本都是罗马军占优势, 而问题恰出于此. 通常罗马在取得战斗胜利后, 不会把对方部族置于彻底的统治之下, 他们通常会接收战败方的部分\u0026quot;所有地\u0026rdquo;, 把其中一半作为同盟国赢得的份额, 另一半留作自己的\u0026quot;公有地\u0026quot;出租给罗马市民.\n围绕公有地的出租份额比例, 再次引起贵族与平民的对立, 平民阶级认为仅有地的出租公配偏向贵族阶级, 而贵族阶级则以尊重私有财产的法律为挡箭牌, 抵制平等分配. 并且, 肥沃的土地分配给贵族, 自己只能得到贫瘠的土地, 平民阶级反应强烈.\n另外, 平民阶级提出要求法律的成文化: 法律只要还停留在口头约定上, 在执行时, 就容易偏向所有法律制定权势贵族阶级. 因此, 要求法律成文合情合理. 成文的法律谁都能看到, 执行起来就可以做到客观公允(所谓法不可知, 则威不可测, 这样的道理2000年前的罗马人都认识到了, 而在某些国家, 法律就制定得非常模糊, 方便政府解释, 造成群体普遍违法, 应政府需要, 选择性执法. 法律变成政府抓人的大网, 而某些发言人还能公言说出, 不要拿法律当挡箭牌这样的话.)\n而后, 贵族应平民要求, 编写成文法\u0026lt;十二表法\u0026gt;, 又名十二铜表法, 因为它是一项一项刻在铜板上的12条法律. 此前, 平民与贵族是不允许通婚的, 在\u0026lt;十二铜表法\u0026gt;出台4年后, 一项允许贵族与平民通婚的法律出台了, 这一法律的出台, 对平民阶级的人才培养直到了积极的促进作用. 因为在教育制度不健全的那个时代, 出身和门第就是接受教育的标志.\n尽管如此, 公元前449年至前367年80多年里, 罗马一直处于探索过程之中, 尝试尝试废除二人的执政官, 代之以六人的军事指挥官(头太多也不行吧), 可能是考虑到把两个人行使的权力分散到六个人的手上, 结果却, 每当需要统一指挥时, 不得不一次次地任命独裁官.\n公元前396年, 经过10年的漫长战争, 罗马终于成功攻取了伊特鲁里亚非常强大的城市维爱, 为此罗马, 举国同庆, 而战斗一结束, 平民与贵族又展开了斗争, 平民建议在刚刚攻取的维爱设立第二个首都, 距离罗马20公里, 地位等同于罗马, 看来他们是和贵族玩累了, 就打算自己另开地图.\n即使独裁官强烈反对, 但是还是有一半的人离开, 罗马去了维爱. 此时, 他们所不知道的是, 善战民族凯尔特人从北方, 向罗马攻来了.\n3.4 凯尔特人的入侵 公元前390年7月18日, 罗马军队在台伯河上游迎战来敌, 结果大败而回, 凯尔特人开进了毫无防御的罗马, 罗马城门大开, 沦陷, 并被蹂躪了7个月. 罗马与凯尔特人和谈, 兼之凯尔特人不习惯城里生活, 便拿着300公斤的金块, 解除了对罗马7个月的占领, 离开了.\n而后, 罗马的拉丁同盟见罗马被凯尔特人打败, 并分崩离析, 甚至转脸成为罗马的敌人, 试图乘机消灭罗马.\n罗马就此走上了重建与应对围攻之路, 建国360年, 共和政体实行100年后的罗马, 不得不从头来过.\n希腊历史学家波利比乌斯认为公元前390年凯尔特人的入侵, 是罗马开始走向强大的第一步. 不小心跌入谷底后, 唯一办法就是爬上来. 尽管罗马人在公元前390年一度跌入谷底, 但是, 罗马人终究是罗马人, 尽管速度缓慢, 他们还是一步一步地爬了上来.\n重建罗马, 按照英国学者的研究, 公元前390年后的罗马人必须解决的问题, 按罗马人排列的顺序如下:\n注重防卫的同时, 重建被毁的罗马 与叛离的旧同盟各部族作战, 以此确保边境安全 消除贵族与平民的对立, 实现社会安定和舆论统一, 而这必然意味着政治改革. 3.5 政治改革 到了公元前4世纪前半叶的罗马, 已经具备实施根本性改革的一切内外条件了.\n公元前367年, 罗马史上划时代的法律\u0026lt;李锡尼法\u0026gt;得以实施, 在这部法中, 首先废除了六人军事指挥官政体, 恢复二人执政官制度, 明确今后罗马将实行寡头政制, 即少数人的领导体制.\n其次, 规定共和政府的所有要职向平民出身的人开放(不得不说这是一个非常高明的决定, 以前平民要求的是两个执政官, 平民占一个名额, 现在全面开放正如他们所希望, 而平民出身的李锡尼制定了\u0026lt;李锡尼法\u0026gt;, 贵族为这一想法的法制化投了赞成票, 他们选择不以阶级分配要职, 而是全面开放.\n如果按贵族和平民分配官职, 首先有悖机会均等, 即才能不足, 可能仅仅因为其出身平民即可成为执政官, 另外虽然以废除差别为目的分配官职, 却反而会出现强化差别的结果,两派一直处于敌对状态. 现在采取机会平等, 而非结果平等, 即可能出现两个平民出身的执政官, 也可能出现两个贵族出身的执政官)\n在\u0026lt;李锡尼法\u0026gt;实施若干年后, 又出台了一部法, 此法规定, 凡是担任过重要公职的人, 不论贵族还是平民, 都有权取得元老院议席, 即使是以保护平民阶级为己任的护民官, 在离任后也可以成为元老院议员.\n(我认为由那些具备丰富经验和出类拔萃的能力, 但不需要经过选举的人们组成的机构是共和政体下不可或缺的机构, 正因为他们远离选举, 所以他们可以从长远视角去制定一贯的政策, 即避免为迎合民意, 作出损害公众利益的事, 防止民粹, 美国的参议院就是参考了元老院)\n顺便跑个题, 编程随想君在介绍美国选举制度的就是, 就有提及参议院:\n下面大致列举参众两院的差别:\n参议员任期(6年)是众议员任期(2年)的三倍(甚至超过总统的4年任期) 参议员是由州议会选出(这点在20世纪初出现变化,下面会聊到),而众议员是由选区的选民直接选出 参议员的任职资格比众议员更高 有些职能是参议院可以干而众议院干不了的,比如: 4.1 总统提出的重要人事任命(比如最高法院大法官),须由参议院审批才生效 4.2 总统批准的国与国之间的条约,须由参议院审批才生效 众议员选举是对应到人口数量的(每N个选民划定一个选区, 每个选区选出一个众议员), 因此众议院更像选民的[传声筒], 能迅速反馈选区的民意, 但这种机制容易被民意裹挟.\n为了体现制衡,国会的【任何法案】要参众两院都批准才能成为法律。而且两院投票通过的法案文本必须是【完全一样】的。\n由于参议院的性质,使得它比较稳重。关于这点,作为“美国国父”之一的华盛顿打了个比方(大意是):\u0026ldquo;把热咖啡从众议院这个杯子倒入参议院这个杯子,使之冷却一下\u0026rdquo;。\n他的意思是:有了参议院的制衡,可以防止众议院一时头脑发热而让某个不恰当的法案获得通过。\n跑题之再跑题, 这种把主题相同内容的书籍相互关联的阅读方式, 被称为主题阅读(来自如何阅读一本书的这本书)\n从此, 罗马不再是贵族政体, 而是变成名副其实的寡头政体国家, 所谓贵族政体是由贵族出身的少数人统治绝大多数人的政体, 而寡头政体在少数人统治多数人的这一点上与贵族政体相同, 但是, 对少数人的血统没有要求. (寡头政体也是分情况的, 罗马这种由市民大会选举出来的执政官执政的方式是少数人统治的寡头政体, 像俄罗斯这种普京一人转的, 也是寡头政体, 但是他是上了不会下来. 而80年代某国的老人政治, 一群老家伙在最高领导人之上继续执政的方式, 也是寡头政体, 他们也不是选举出来的)\n3.6 罗马政体 以前在教科书上, 学到的\u0026quot;真理\u0026quot;是: 经济基础决定上层建筑(即政治体系), 现在的认识是, 政治体系是一切的基础, 涉及国民的生活形式, 经济也自然是受政治的影响.\ndigraph G{ subgraph K{ 法务官[label =\u0026#34;法务官 1人-\u0026gt;2人-\u0026gt;16人\u0026#34;]; 财务检查官[label = \u0026#34;财务检查官 2人-\u0026gt;40人\u0026#34;]; 财务官[label=\u0026#34;财务官 2人\u0026#34;]; 按察官[label=\u0026#34;按察官 4人\u0026#34;]; 元老院议员[label=\u0026#34;元老院议员 300人\u0026#34;]; 执政官[label=\u0026#34;执政官 2人\u0026#34;]; 独裁官[label=\u0026#34;独裁官 1人\u0026#34;]; 骑兵长官[label=\u0026#34;骑兵长官 实质上的副官 1人\u0026#34;]; 市民大会[label=\u0026#34;市民大会 全体市民\u0026#34;] 市民大会-\u0026gt;法务官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;财务检查官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;财务官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;按察官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;元老院议员[label = \u0026#34;选出\u0026#34;]; 元老院议员-\u0026gt;执政官[label=\u0026#34;承认劝告\u0026#34;]; 执政官-\u0026gt;独裁官[label=\u0026#34;指名\u0026#34;]; 独裁官-\u0026gt;骑兵长官[label=\u0026#34;任命\u0026#34;]; } subgraph H{ 平民大会[label=\u0026#34;平民大会(仅限平民)\u0026#34;]; 护民官[label=\u0026#34;护民官2人-\u0026gt;10人\u0026#34;]; 平民大会 -\u0026gt; 护民官[label=\u0026#34;选出\u0026#34;]; } } 3.6.1 执政官 执政官取代王政时代的国王, 是共和政体下的罗马的最高官职, 由市民大会选举产生, 经元老院批准后就职. 其过程与国王一样, 但国王是终身制, 而执政官的任期只有短短的一年, 执政官允许再选, 年龄下限为40岁.\n两位执政官相互有权对对方的想法或做法使否决权, 一项政策, 只要两位执政官都不同意, 就不能付诸实施.\n执政官是内政的最高领导人, 同时在战场担任指挥重任, 军务是执政官最重要的工作.\n一旦执政官各持己见, 互不妥协, 他们就会任命独裁者, 一统指挥权.\n3.6.2 独裁官(Dictator) 它是当国家处于非常时期时任命的一个官职, 意思是临时独裁执政官, 与其他官职通过选举产生不同, 独裁官由两位执政官中的一人指定即可. 独裁官除了无权决定政体之外, 在任何问题上享有绝对的决定权, 对于独裁官的作出决定, 任何人无权反对, 独裁官的任期很短, 为6个月, 人数当然只有1人.\n独裁官有权任命\u0026quot;骑兵长官\u0026rdquo;, 相当于副官, 两位执政官在任命独裁官的同时必须授受独裁官的命令.\n3.6.3 法务官(Praetor) 法务官任期为一年, 最初由一个人担任, 后来成倍增加, 最后达到16人. 既然翻译成了法务官, 负责的自然是司法事务, 最初, 当执政官上前线时, 法务官负责管理后方, 后来慢慢变身为司法责任人.\n身处该官职的人很多时候也要上战场, 要求法务官年龄在40岁以上, 基于一旦需要就可以代替执政官指挥军队的考虑.\n此外, 执政官不在时, 法务官要担任议长, 召集在首都罗马举行的市民大会\n3.6.4 财务检察官(Quaestor) 财务检察官的人数最初是2个人, 到了共和政体末期增加40个人, 任期一年, 年龄要求30岁以上.\n账务检察官的任务, 有一项是负责前线的财务, 这是很重要的工作. 例如军费是否过于浪费等.\n3.6.5 财务官(Censor) 这一官职最初是为人口调查而设, 因此在共和政体的初期, 不是每年, 而是每五年进行一次人口调查时才会选举, 任期一年半以上. 财务官人数是两人, 年龄要求不明.\n在罗马进行人口调查, 不是调查总人口, 而是调查户主们的财政状况, 对于未如实申报财政状况的人, 无论是贵族还是其他什么人, 财务官都有权告发. 之所以说他们权力很大, 原因就在于此.\n险些之外, 从国有土地的使用国库的收支, 都由他们进行严格t监督, 同时他们还有权决定公路及上下道建设的开支, 所以, 说他们是国家财政的责任人未尝不可. 掌握金钱收支的人就是掌握权力的人.\n3.6.6 按察官(Aedilis) 在所有官职中, 只有这一官职从设立之初就明确了当选者所属的出身阶级\u0026ndash;贵族和平民平分秋色, 各选两个人, 大概是担任这一官职的人需要直接且经常接触市民的缘故. 任期一年, 年龄要求在30岁以上. 年轻人也有机会担任的官职.\n按察官的任务:\n是策划组织庆祝和祭祀活动, 其中包括举办运动会的工作 是与公安警察相关的工作 保障粮食的供给, 由于农村地区自给自足, 所以按察官负责的是首都罗马的粮食供给 负责道路维修, 交通整顿和上下水道管理 对各种违法行为作出罚款金额的决定以作为惩戒 对市场进行严格监管以保障市场的公平运行. 如此看来, 该官职似乎是一个权威不高, 涉及领域繁多, 事务忙碌且报酬微薄的官职. 但实际并非如此, 由于这一官职所负责的领域大多和民众的生活息息相关, 因此就争取民众支持而言, 是一个非常理想的职位.\n3.6.7 护民官(Tribunus Plebis) 护民官是代表平民阶级的一个官职, 因此, 非平民阶级出身的人无缘这个官职, 由平民大会上选举产生, 平民大会只有平民阶级出身的人才有权参加, 护民官的任期为一年, 对年龄没有限制.\n护民官最主要的任务当然是保护平民的权利, 因此, 他们有权对政府所作出的决定行使否认权, 但在战时不得使用这种权力.\n因利害冲突, 护民官有可能遭到某些行事过激的贵族的暗算, 所以, 为了防止此类事件发生, 护民官享有人身不可侵犯的特别权利, 这一权利边执政官都不享有.\n护民官的人数最下2个人, 后来不断增加, 最后达到了10人之多\n3.6.8 元老院(Senatus) 在罗马, 作为国家最高决策机构, 市民大会一直存在. 除了护民官, 以执政官为首的政府要职全部由该\u0026quot;国会\u0026quot;选举产生\n只有元老院议员不需要经过选举, 但是这绝不意味着只要过了30岁就可以自动得到议席, 正因为如此, 它还是世袭的. 只有经过相当严格的甄别, 确认其见识, 责任心, 能力和经验都符合的人才允许进入元老院. 当然, 出身名门望族的人, 相对多一些优势.\n其实, 元老院充分发挥了心脏的作用, 现在, 它的功绩已经成为西洋史的常识. 作为上院的名称, 美国, 法国, 意大利和加拿大, 都愿意留下\u0026quot;senatus\u0026quot;的称谓. 就像历史学家波得比乌斯指出的那样, 罗马在健全的元老院开始发挥作用以后, 彻底摆脱了迷惘, 勇敢地走上了繁荣之路, 无疑人人都会对此心生羡慕.\n3.7 政治建筑的杰作 善于从失败中学习, 并在此基础上挣脱已有观念的束缚, 提高自己, 然后再重新站起来, 这就是罗马人的性格.\n这不是说失败是好事, 失败没有什么好或不好, 失败只是失败. 重要的是如何从失败中站起来, 也就是怎样对待失败.\n进入公元前4世纪后半叶, 改变了对外关系形态后的罗马人, 没有改变公元前8世纪罗穆路斯以来的罗马人的一个特点, 即同化失败者. \u0026lt;列传\u0026gt;作者普鲁塔克认为罗马强大的首要原因就是他们这种性格\n3.8 市民权 3.8.1 权利 不论动产还是不动产, 保证一切私有财产. 允许私有财产自由买卖 享有选举权和被选举权, 有参与国政的权利 有依法接受审判的权利. 同时, 在罗马, 被判死刑后, 有权向市民大会提出诉讼, 也就是上诉权. 事实上, 有罗马市民权的人极少被执行死刑 有证据证明是有独立, 自由身份的成熟男子 3.8.2 业务 首先,有义务服务军务, 17岁至45岁为现役, 46岁至60岁为预备役. 这项义务替代了市民另一项义务, 即纳税的业务. 以间接税为主的古代税制中, 直接税用兵役相抵的情况很普遍, 因此, 军税又叫\u0026quot;血税\u0026rdquo;\n法律并未规定市民不得缴纳税款以逃避军务, 只是, 对于罗马人来说, 这样太可耻. 在罗马, 以经济行为纳税的只有不享有市民权而不承担军备义务的非市民以及经济富裕但没有孩子的女人.\n3.8.3 条件 成为市民的条件:\n在雅典, 要取得雅典市民权必须父母双方都是雅典人, 即使在鼎盛期也是如此, 斯巴达也一样. 但在罗马则不同, 只要生活在罗马就可以取得市民权, 而且这一情形延续了相当长的时间.\n对于市民权, 希腊人和罗马人的区别也体现在奴隶的处境上.\n在希腊, 奴隶终身为奴是普遍现象, 但是罗马的奴隶有路可选, 奴隶主为了回报奴隶长年的无偿奉献会还奴隶以自由, 或者奴隶能够用自己积攒起来的钱赎回自由(奴隶竟然还可以有自己的财产).\n获得自由的奴隶叫解放奴隶, 他们的子孙可以取得罗马譏权, 至于有了市民权后, 能否在社会上出人头地, 就看个人的才能和运气了.\n希腊哲学家亚里士多德把奴隶和家畜作了比较, 并写下这样的一句话:\n在有用方面, 两者几乎没有区别. 奴隶和家畜用他们的肉体为我们人类所用, 这一方面是一样的.\n比亚里士多德早200年的罗马第六代国王塞尔维乌斯-图里乌斯说过这样的话, 尽管有传言说他本人是奴隶出身:\n奴隶和自由民的不同不是先天造成的, 而是生来遭遇的命运不同而已.\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E7%BD%97%E9%A9%AC%E4%B8%8D%E6%98%AF%E4%B8%80%E5%A4%A9%E5%BB%BA%E6%88%90%E7%9A%84/","summary":"1 前言 以史为鉴,可以知兴替。而罗马,作为西方文明的来源之一,有着漫长辉煌无比的历史,其影响直至今天仍可见。因此,我开了个新坑,阅读盐野阿姨的","title":"罗马人的故事(一):罗马不是一天建成的"},{"content":"1 前言 如果鲁迅先生都叫不醒的人,可以是真的叫不醒了。从前我对鲁迅先生的印象,就停留在语文课本上的《社戏》, 玩梗的《闰土》和《孔乙已》,和其他出现在教科书上的作者无异。\n甚至因为官方的推荐,产生了反感之心。毕竟年青时认为,文人大都有风骨,受朝廷推崇之人,怕是无甚风骨,心中印象自然不佳。而后,年岁渐长,读到了不同的声音,诸如编程随想君的《面对共产党——民国人文大师的众生相》一文中提到了饱受争议的鲁迅。毛先生对鲁迅的点评:\n周海婴在《鲁迅与我七十年》一书中提及了一段往事。 1957年反右的时候,罗稷南当着老毛的面提了一个问题:要是今天鲁迅还活着,他可能会怎样?毛腊肉沉思片刻,回答说:\u0026ldquo;要么是关在牢里还要写,要么他识大体不做声。\u0026rdquo;\n而鲁迅自我的点评:\n在《鲁迅纪念集》第1辑第68页,记录了鲁迅向李霁野复述了一段他跟冯雪峰的对话,时间是1936年4月。\n鲁迅:你们来时,我要逃亡,因为首先要杀的,恐怕是我。 冯雪峰则连忙摇头摆手应之:那弗会,那弗会!\n想来,鲁迅也并非郭沫若之流, 是写得出《毛主席是我爷爷》这样的颂歌的人。\n2 振聋发聩 如果鲁迅不弃医从文,可能民国会多一个分析人体病理的名医,会少一个鞭辟国民精神的执戈披甲之士。\n2.1 铁屋子 “假如一间铁屋子,是绝无窗户而万难破毁的,里面有许多熟睡的人们,不久都要闷死了,然而是从昏睡入死灭,并不感到就死的悲哀。现在你大嚷起来,惊起了较为清醒的几个人,使这不幸的少数者来受无可挽救的临终的苦楚,你倒以为对得起他们么?”\n“然而几个人既然起来,你不能说决没有毁坏这铁屋的希望。”\n是的,我虽然自有我们的确信,然而说到希望,却是不能抹杀的,因为希望是在于将来,决不能以我之必无的证明,来折服了他之所谓可有。\n人总是要怀有希望的,纵使四周昏暗无光,纵使你挺身而出会受到众人的背弃。清醒总是痛苦的,更痛苦的是面对困境无能为力,而鲁迅决心做出改变,由他而始,怀抱希望。\n2.2 吃人 凡事总须研究,才会明白。古来时常吃人,我也还记得,可是不甚清楚。我翻开历史一查,这历史没有年代,歪歪斜斜的每叶上都写着“仁道义德”几个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是“吃人”!\n古人书上全是仁义,心里全是吃人。现在的人是嘴上全是主义,心里全是生意。看来今人是不如古人的,真是一代不如一代。\n2.3 你也配姓赵 那是赵太爷的儿子进了秀才的时候,锣声镗镗的报到村里来,阿Q正喝了两碗黄酒,便手舞足蹈的说,这于他也很光采,因为他和赵太爷原来是本家,细细的排起来他还比秀才长三辈呢。其时几个旁听人倒也肃然的有些起敬了。\n那知道第二天,地保便叫阿Q到赵太爷家里去;\n太爷一见,满脸溅朱,喝道:“阿Q!你这浑小子!你说我是你的本家么?”\n阿Q不开口。赵太爷愈看愈生气了,抢进几步说:“你敢胡说!我怎么会有你这样的本家?你姓赵么?”\n阿Q不开口,想往后退了。\n赵太爷跳过去,给了他一个嘴巴。\n“你怎么会姓赵!——你那里配姓赵!”\n你也配姓赵,韭菜还操着镰刀的心,你哪里配姓赵呢。\n2.4 闰土 这来的便是闰土。虽然我一见便知道是闰土,但又不是我这记忆上的闰土了。他身材增加了一倍;先前的紫色的圆脸,已经变作灰黄,而且加上了很深的皱纹;眼睛也像他父亲一样,周围都肿得通红,这我知道,在海边种地的人,终日吹着海风,大抵是这样的。他头上是一顶破毡帽,身上只一件极薄的棉衣,浑身瑟索着;手里提着一个纸包和一支长烟管,那手也不是我所记得的红活圆实的手,却又粗又笨而且开裂,像是松树皮了。\n我这时很兴奋,但不知道怎么说才好,只是说:“阿!闰土哥,——你来了?……”\n我接着便有许多话,想要连珠一般涌出:角鸡,跳鱼儿,贝壳,猹,……但又总觉得被什么挡着似的,单在脑里面回旋,吐不出口外去。\n他站住了,脸上现出欢喜和凄凉的神情;动着嘴唇,却没有作声。他的态度终于恭敬起来了,分明的叫道:“老爷!……”\n我似乎打了一个寒噤;我就知道,我们之间已经隔了一层可悲的厚障壁了。我也说不出话。\n年青时读课文的时候,还未能体会到与童年好友重逢时的喜悦碰撞两人身份差异, 导致友谊不再的悲凉。想必那里鲁迅在十二月冰井里打的寒噤。这种感觉大概和,你和初恋情人偶遇,相谈甚欢,然后有个男孩在旁边叫,妈妈,我们回家吧,差不大多。\n3 总结 不知道鲁迅活到今天,会写出怎么样的作品。时代的土壤之于作家,就有如阳光之于鲜花,想来文学也该有时势造英雄之说。如果鲁迅还活到今天,肯定会写出比《呐喊》精妙百倍的文章,令后人拜服。\n我建议鲁迅先生将其文集取名为《捂嘴》\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%91%90%E5%96%8A/","summary":"1 前言 如果鲁迅先生都叫不醒的人,可以是真的叫不醒了。从前我对鲁迅先生的印象,就停留在语文课本上的《社戏》, 玩梗的《闰土》和《孔乙已》,和其他","title":"呐喊"},{"content":"1 Design principle 在谈论四个设计的基本准则前, 作者强调了关于命名的重要性.\n作者举了一个例子, 在圣诞节, 他收到一本书介绍树木的分类, 他注意到一种叫Joshua tree的树, 造型奇特. 他想, 如果我看过, 我肯定会记得, 毕竟形状特别. 当他走出家门时, 发现社区80%的院子都有这种树, 但他此前从未注意到. 一旦你可以叫出它的名字, 你就发现它随处可见\nOnce you can name something, you\u0026rsquo;re conscious of it. You have power over it. You\u0026rsquo;re in control. You own it.\n深有体会. 树尤如此, 设计准则亦如是.\nGood design is as easy as:\nLearn the basic principles: They\u0026rsquo;re simpler than you might think Recognize when you\u0026rsquo;re not using them: Put it into words - name the problem Apply the principles: Be amazed 1.1 Proximity The principle of Proximity states: Group related item together. 有关联的元素, 位置上让它们更接近, 以表示它们有关联的一个群组而不是若干个无关联散落的元素\n当然, 有关联才放在一起, 没关联就不要硬挤过来. 留下视觉距离让读者可以判别出他们的关系. 这也和生活经验吻合, 可以从两人的物理距离判别出他们的关系\n如下例子:\nThe idea of proximity doesn\u0026rsquo;t mean that everything is closer together; it means elements that are ntellectually connected, those that have some sort of communication relationship, should also be visually connected.\nIt\u0026rsquo;s all about space. The principle of Proximity helps you focus on space and what it can do for communication\n1.2 Alignment The Principle of Alighment states: Nothing should be placed on the page arbitrarily. Every item should have a visual connection with something else on the page. The principle of alignment forces you to be conscious \u0026ndash; no longer can you just throw things on the page and see where they stick\n通过对齐, 可以让元素之间产生联系, 使杂乱的设计变得有条理, 通过布局来展现关联. 如下图分析\n又或者是:\n通常来说, 左对齐或者右对齐会比居中对齐有更强烈的视觉效果, 因为居中对齐两边不对齐, 就会让我有种未对齐的感觉\n对于页面, 可分析其页面元素的对齐, 然后修正成统一的对齐方式. 而下图的书页, 左右都对齐了, 还增加了缩进和行分隔, 看起来就清晰多了.\nI am giving you a number of rules here, and it\u0026rsquo;s true that rules are made to be broken. But remember the Rule about Breaking Rules: You must know what the rule is before you can break it\n1.3 Repetition The Principle of Pepetition States: Repeat some aspect of the design throught the entire piece. The repetitive element may be a bold font, a thick rule(line), a certain bullet, design element, color, format, spatial relationships, etc. It can be anything that a reader will visually recognize\n重复是一致性的一种实现, 但重复并不止于一致性, 它还是一种统一设计中各个元素的有力手段. 还是熟悉的名片:\n1.4 Contrast The Principle of Contrast states: Contrast Various elements of the piece to draw a reader\u0026rsquo;s eye itno the page. If two items are not exactly the same, then make them different. Really different\n对比有很多手法, 诸如大与小, 复古与新潮, 强与弱, 明与暗, 粗糙与细滑, 水平与垂直等等.\n需要注意的是, 如果两个元素有区分, 但本质无差别, 那就不是 contrast, 而是 conflict.\nThere is one more general guiding principle of Design(and of Life): Don\u0026rsquo;t be wimp\n突然意识到, 本书的PDF 版本的排版和字段, 图片也是相当舒服的\n2 Design with Type 接下来大部分内容都关于Type, 关于印刷, 关于字体种类, 不是很感兴趣, 所以就草草涉猎过.\n","permalink":"https://ramsayleung.github.io/zh/post/2021/the_nondesigners_design_book/","summary":"1 Design principle 在谈论四个设计的基本准则前, 作者强调了关于命名的重要性. 作者举了一个例子, 在圣诞节, 他收到一本书介绍树木的分类, 他注意到一种叫Josh","title":"The Non-designer's design book"},{"content":"1 前言 阿德勒与弗洛伊德, 荣格并称为\u0026quot;心理学三大巨头\u0026quot;, \u0026lt;被讨厌的勇气\u0026gt;这本书主要是以对话的形式来讲述阿德勒的心理学. 类似苏格拉底的对话, 认为\u0026quot;任何人都可以随时获得幸福\u0026quot;, 并给出了\u0026quot;自我接纳\u0026quot;, \u0026ldquo;他者信赖\u0026quot;和\u0026quot;他者贡献\u0026quot;三个手段\n你认为\u0026quot;人是可以改变\u0026quot;的么?\n2 目的论 弗洛伊德所创建的心理学主张原因论, 即现在的不幸是由过去的原因造成的, 现在的我(结果)是由过去的事情(原因)所决定, 最常见的原因是童年不幸. 虽说这个被现代心理学家证实是过于片面, 但是对于编故事还是很有用, 比如\u0026lt;穆赫兰道\u0026gt;, \u0026lt;致命ID\u0026gt;等电影灵感都来自弗洛伊德的原因论.\n而阿德勒心理学主张目的论, 考虑的不是过去的\u0026quot;原因\u0026rdquo;, 而是现在的\u0026quot;目的\u0026quot;. 阿德勒说, 决定我们自己的不是\u0026quot;经验本身\u0026quot;而是\u0026quot;赋予经验的意义\u0026quot;.\n按照书中的例子, 青年的一个朋友一直宅在家不愿出门, 一出门就会全身不舒服. 按照原因论, 即他过去受过某种创伤, 所以他无法走出家门; 而按照目的论, 即是他不想走出家门, 然后再为自己找不能出门的原因.\n按照阿德勒的观点, 任何经历本身并不是成功或者失败的原因. 我们并非因为自身经历中的刺激\u0026ndash;所谓的心理创作-而痛苦, 事实上我们会从经历中发现符合我们目的的因素. 决定我们自身的不是过去的经历, 而是我们自己赋予经历的意义\n经历本身不会决定什么. 我们给过去的经历赋予了什么样的意义, 这直接决定了我们的生活. 人生不是由别人赋予的, 而是由自己选择的, 是自己选择自己如何生活.\n3 选择生活 重要的不是被给予什么, 而是如何去利用被给予的东西. 因此按照书中的观点, 你的不幸, 皆是自己\u0026quot;选择\u0026quot;的:\n但是, 现在的你之所以不幸正是因为你亲手选择了\u0026quot;不幸\u0026quot;, 而不是因为生来就不幸.\n我个人觉得这样的观点是过分一元论, 过分强调主观意识对客观世界的影响.\n因为生活都是你自己选择的, 即使人们有各种不满, 但还是认为保持现状更加轻松, 更能安心. 是人们常常下定决心\u0026quot;不改变\u0026quot;\n4 一切烦恼的来源 \u0026ldquo;一切烦恼都是人际关系的烦恼\u0026rdquo;(这个是否也算过分绝对呢?)这个是阿德勒心理学的一个基本概念. 如果这个世界没有人际关系, 如果这个宇宙没有他人只有自己, 那么一切烦恼也都将消失\n自卑感来自主观的臆造, 例如的身高的自卑是比对出来的, 没有人关注你的身高.(BBS的相亲帖子都是有身高限定的). 我们无法改变客观事实, 但可以任意改变主观解释.\n自卑情结是把自己的自卑感当作某种借口使用的状态, 例如, 我找不到女朋友是因为不够高; 将原本没有任何因果关系的事情解释成似乎有重大因果关系.\n我正好看过亲密关系, 实验证明, 身高对吸引力的确有非常大的影响\n健全的自卑感不是来自与别人的比较, 而是来自与\u0026quot;理想的自己\u0026quot;的比较, 人生不是与他人的比赛.\n在独自成行的活动中, 人生的确不是与他人的比赛, 但部分活动的确是与他人的比赛. 比如求偶\n情绪波动时易发怒, 但发怒只是一种表达方式, 怒气终归是为了达成目的的一种手段和工具; 那么既然发怒是交流的一种形态, 不使用发怒这种方式也可以交流的.\n5 自由是不再寻求认可 阿德勒心理学否定寻求他人的认可. 其实, 我们\u0026quot;并不是为了满足别人的期待而活着\u0026quot;的. 倘若自己都不为自己活出自己的人生, 那还有谁会为自己而活呢?\n5.1 课题分离 课题是你我在做的事情. 面对课题, 首先要思考\u0026quot;这是谁的课题\u0026quot;, 把自己的课题与别人的课题分离开.\n例如书中的例子, 学习是孩子的课题, 无论父母多么关心孩子, 都不应干涉孩子的课题. 基本上, 一切人际关系矛盾都起因于对别人的课题妄加干涉或者自己的课题被别人妄加干涉\n5.2 放开烦恼 人为什么在意别人的视线呢? 阿德勒心理学给出的答案非常简单, 那就是因为还不会进行课题分离. 把原本是别人的课题当作自己的课题.\n伸伸手即可触及, 但又不踏入对方领域, 保持这种适度距离非常重要.\n对认可的追求, 扼杀了自由, 自由就是被别人讨厌. 不畏惧被人讨厌是勇往直前, 不随波逐流而是激流勇进, 这才是对人而言的自由.\n拼命寻求认可反而是以自我为中心\n在只关心\u0026quot;我\u0026quot;这个意义上来讲, 是以自我为中心. 你正因为不想被他人认为自己不好, 所以才在意他人的视线. 这不是对他人的关心, 而是对自己的执著.\n6 追求幸福 把对自己的执著(self interest)转换成对他人的关心(social interest), 建立起共同体感觉. 这需要以下三点做好:\n自我接纳 他者信赖 他者贡献 6.1 自我接纳 自我接纳是指假如做不到就诚实地接受这个\u0026quot;做不到的自己\u0026quot;, 然后尽量朝着能够做到的方向去努力, 不对自己撒谎.\n对得了60分的自己说\u0026quot;这次只是运气不好, 真正的自己能得100分\u0026quot;, 这是自欺欺人; 与此同时, 在诚实地接受60分的自己的基础上努力思考\u0026quot;如何才能接近100分\u0026quot;, 这就是自己接纳\n上帝, 请赐予我平静, 去接受我无法改变的; 给予能手, 去改变我能改变的; 赐我智慧, 分辨这两者的区别 \u0026ndash;尼布尔的祈祷文\n6.2 他者信赖 在相信他人的时候不附加任何条件. 即使没有足以构成信用的客观依据也依然相信, 不考虑抵押之类的事情, 无条件的相信. 这就是信赖.\n如果你认为无条件地相信他人可能会被背叛, 阿德勒心理学认为决定背不背叛的不是你, 那么他人的课题, 你只需要考虑\u0026quot;我该怎么做\u0026quot;.\n(按照这个观点, 如果没有建立起信赖的条件, 是你没有足够信赖对方, 正因为有这种忧虑, 以致于与任何人都无法建立起深厚的关系. 我觉得是过于理想化, 建立稳定关系的过程本身就像刺猬相互取暖, 从慢慢接近开始的).\n6.3 他者贡献 对他人寄予信赖就是把他人当作伙伴, 正因为是伙伴, 所以才能信赖, 如果还是伙伴, 也就做不到信赖. 对作为伙伴的他人给予影响, 作出贡献, 这就是他者贡献\n人只有在能够感觉到“我对别人有用”的时候才能体会到自己的价值。但是,这种贡献也可以通过看不见的形式实现。只要有“对别人有用”的主观感觉,即“贡献感”就可以。\n并且,书中还得出了这样的结论:幸福就是“贡献感”\n7 此时此刻 人生是连续的刹那, 根本不存在过去和未来, 我们的人生只存在于刹那之中. 人生像是在每一个瞬间不停旋转起舞的连续的刹那, 并且, 蓦然四顾时常常会惊觉: 已经来到这里了么?\n请你想象一下自己站在剧场舞台上的样子. 此时, 如果整个会场都开着灯, 那就可以看到山顶位的观众席. 但是, 如果强烈的聚光灯打向自己, 那就边最前排都看不见.\n我们的人生也完全一样. 正因为把模糊而微弱的光人生整体, 所以才能够看到过去和未来; 不, 是感觉能够看到. 如果把强烈的聚光灯对准\u0026quot;此时此刻\u0026quot;, 那就会既看不到过去也看不到未来.\n我自己无论怎样回顾之前的人生也无法解释自己为什么会走到\u0026quot;此时此刻\u0026quot;\n只要不迷失他者贡献这个引导之星就可以, 只要朝着这个方向前进就可以获得幸福.\n让我们度过各自的夜晚, 然后迎来新的早晨吧.\n8 总结 读完《亲密关系》再读《被讨厌的勇气》, 会觉得书中的结论得出没有经过对应的实验论证, 实验结果可信度存疑.\n但是本书的确提出了很多有趣的观点, 接受的人就会觉得胜读十年书, 不接受的人就会觉得又是鸡汤一碗.\n我对书中的部分观点表示认可, 比如自由, 此时此刻的观点; 对于一切烦恼来自人际关系的论点觉得过于绝对, 目的论的观点让人印象深刻, 但过分强调主观意识, 忽视客观世界.\n重要的不是被给予了什么, 而是如何去利用被给予的东西\n这观点我是非常赞同的, 但是也要关注, 你到底被给予了什么. 过分强调主观意识, 会让人盲目乐观. 既要抬头看天, 也要低头踏地.\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E8%A2%AB%E8%AE%A8%E5%8E%8C%E7%9A%84%E5%8B%87%E6%B0%94/","summary":"1 前言 阿德勒与弗洛伊德, 荣格并称为\u0026quot;心理学三大巨头\u0026quot;, \u0026lt;被讨厌的勇气\u0026gt;这本书主要是以对话的形式来讲述阿德勒的","title":"被讨厌的勇气"},{"content":"1 Preface I have been maintained a legacy distributed timer for months for my employer, then some important pay business are leveraging on it, with 1 billion tasks handled every day and 20k tasks added per second at most.\nEven though it\u0026rsquo;s old and full of black magic code, but it also also have insighted and well-designed code. Based on this old, running timer, I summarize and extract as this article, and it wont include any running code(perhaps pseudocode, and a lot of figures, as an adage says: A picture is worth a thousand words).\nif you are curious about the reason(I personally suggest to watch the TV series Silicon Valley, Richard has gave us a good example and answer)\n2 Design 2.1 Algorithm There are several algorithms in the world to implement timer, such as Red-Black Tree, Min-Heap and timer wheel. The most efficient and used algorithm is timer wheel algorithm, and it\u0026rsquo;s the algorithm we focus on.\nAs for timing wheel based timer, it can be modelled as two internal operations: per-tick bookkeeping and expiry processing.\nPer-tick bookkeeping: happens on every \u0026rsquo;tick\u0026rsquo; of the timer clock. If the unit of granularity for setting timers is T units of time (e.g. 1 second), then per-tick bookkeeping will happen every T units of time. It checks whether any outstanding timers have expired, and if so it removes them and invokes expiry processing. Expiry processing: is responsible for invoked the user-supplied callback (or other user requested action, depending on your model). 2.1.1 Simple Timing Wheels The simple timing wheel keeps a large timing wheel, the below timing wheel has 8 slots, and each slot is holding the task which is going to be expired. Supposing every slot presentes one second(one tick as a second), then the current slot is slot 1, if we want to add a task needed to be triggered 2s later, then this task will be inserted into slot 3.\nper-tick bookkeeping: O(1) What happen if we want to add a task needed to be launched 20s later, the answer is we have no way to do so since there are only 8 slots. So if we have a large period of timer task, we have to maintain a large timing wheel with tons of slots, which requires exponential amount of memory.\n2.1.2 Hashed Timing Wheel Hashed Timing Wheel is an improved simple timing wheel. As we mentioned before, it will consume large resources if timer period is comparatively large. Instead of using one slot per time unit, we could use a form of hashing instead. Construct a circular buffer with a fixed number of slots(such as 8 slots). If current slot is 0, we want store 3s later task, we could insert into slot 3, then if we want bookkeep 9s-later task, we could insert into slot 1(9 % 8 = 1)\nper-tick bookkeeping: O(1) - O(N) It\u0026rsquo;s a tradeoff strategy, We trade space with time.\n2.1.3 Hierarchical Timing Wheels Since simple timing wheels and hashed timing wheel come with drawback of time efficiency or space efficiency. Back to 1987, after studying a number of different approaches for the efficient management of timers, Varghese and Lauck posted a paper to introduce Hierarchical Timing Wheels\nJust make a long story short, I won\u0026rsquo;t dive deep into hierarchical timing wheels, you could easily understand it by a real life reference: the old water meter\nthe firse level wheel(seconds wheel) rotates one loop, triggering the second level(minutes wheel) ticks one slot, same for the third level(hour wheel). Therefore, we present a day(60*60*24 seconds) with 60+60+24 slots. If we want to present a month, we only need to a four level wheel(month wheel) with 30 slots.\nper-tick bookkeeping: O(1) 2.2 Per-tick bookkeeping After introducing timing wheel algorithm, let\u0026rsquo;s go back to the topic about designing a reliable distributed timer, it\u0026rsquo;s essential to decide how to store timer task. Taking implementation complexity and time, space trade off, we choose the Hashed Timing Wheel algorithm.\nThere are several internal components developed by my employer, one of them is named TableKV, a high-availability(99.999% ~ 99.9999%) NoSql service. TableKV supports 10m buckets(the terminology is table) at most, every table comes with full ACID properties of transactions support. You could simply replace TableKV with Redis as it provides the similar bucket functionality.\n2.2.1 Insert task into slot We are going to implement Hashed Timing Wheel algorithm with TableKV, supposing there are 10m buckets, and current time is 2021:08:05 11:17:33 +08=(the UNIX timestamp is =1628176653), there is a timer task which is going to be triggered 10s later with start_time = 1628176653 + 10 (or 100000010s later, start_time = 1628176653 + 10 + 100000000), these tasks both will be stored into bucket start_time % 100000000 = 28176663\n2.2.2 Pull task out from slot As clock tick-tacking to 2021:08:05 11:17:43 +08(1628176663), we need to pull tasks out from slot by calculating the bucket number: current_timestamp(1628176663) % 100000000 = 28176663. After locating the bucket number, we find all tasks in bucket 28176663 with start_time \u0026lt; current_timestamp=, then we get all expected expiry tasks.\n2.3 Global clock and lock As we mentioned before, when the clock tick-tacks to current_time, we fetch all expiry tasks. When our service is running on a distributed system, it\u0026rsquo;s universal that we will have multiple hosts(physical machines or dockers), with multiple current_times on its machine. There is no guarantee that all clocks of multiple hosts synchronized by the same Network Time Server, then all clocks might be subtly different. Which current_time is correct?\nIn order to get the correct time, it\u0026rsquo;s necessary to maintain a monotonic global clock(Of course, it\u0026rsquo;s not the only way to go, there are several ways to handle time and order). Since everything we care about clock is Unix timestamp, we could maintain a global system clock represented by Unix timestamp. All machines request the global clock every second to get the current time, fetching the expiry tasks later.\nWell, are we done? Not yet, a new issue breaks into our design: if all machines can fetch the expiry tasks, these tasks will be processed more than one time, which will cause essential problems. We also need a mutex lock to guarantee only one machine can fetch the expiry task. You can implement both global clock and mutex lock by a magnificent strategy: an Optimistic lock\nAll machines fetch global timestamp(timestamp A) with version All machines increase timestamp(timestamp B) and update version(optimistic locking), only one machine will success because of optimistic locking. Then the machine acquired mutex is authorized to fetch expiry tasks with timestamp A, the other machines failed to acquire mutex is suspended to wait for 1 seconds. Loop back to step 1 with timestamp B. We could encapsulate the role who keep acquiring lock and fetch expiry data as an individual component named scheduler.\n2.4 Expiry processing Expiry processing is responsible for invoked the user-supplied callback or other user requested action. In distributed computing, it\u0026rsquo;s common to execute a procedure by RPC(Remote Procedure Call). In our case, A RPC request is executed when timer task is expiry, from timer service to callback service. Thus, the caller(user) needs to explicitly tell the timer, which service should I execute with what kind of parameters data while the timer task is triggered.\nWe could pack and serialize this meta information and parameters data into binary data, and send it to the timer. When pulling data out from slot, the timer could reconstruct Request/Response/Client type and set it with user-defined data, the next step is a piece of cake, just executing it without saying.\nPerhaps there are many expiry tasks needed to triggered, in order to handle as many tasks as possible, you could create a thread pool, process pool, coroutine pool to execute RPC concurrently.\n2.5 Decoupling Supposing the callback service needs tons of operation, it takes a hundred of millisecond. Even though you have created a thread/process/coroutine pool to handle the timer task, it will inevitably hang, resulting in the decrease of throughout.\nAs for this heavyweight processing case, Message Queue is a great answer. Message queues can significantly simplify coding of decoupled services, while improving performance, reliability and scalability. It\u0026rsquo;s common to combine message queues with Pub/Sub messaging design pattern, timer could publish task data as message, and timer subscribes the same topic of message, using message queue as a buffer. Then in subscriber, the RPC client executes to request for callback service.\nAfter introducing message queue, we could outline the state machine of timer task:\nThanks to message queue, we are able to buffer, to retry or to batch work, and to smooth spiky workloads\n2.6 High availability guarantee 2.6.1 Missed expiry tasks A missed expiry of tasks may occur because of the scheduler process being shutdown or being crashed, or because of other unknown problems. One important job is how to locate these missed tasks and re-execute them. Since we are using global `current_timestamp` to fetch expiry data, we could have another scheduler to use `delay_10min_timestamp` to fetch missed expiry data.\nIn order to look for a needle in a haystack, we need to set a range(delay_10min - current time), and then to batch find cross buckets. After finding these missed tasks, the timer publishes them as a message to message queue. For other open source distributed timer projects like Quartz, which provides an instruction to handle missed(misfire) tasks: Misfire instructions\nIf your NoSql component doesn\u0026rsquo;t support find-cross-buckets feature, you could also find every bucket in the range one by one.\n2.6.2 Callback service error Since the distributed systems are shared-nothing systems, they communicate via message passing through a network(asynchronously or synchronously), but the network is unreliable. When invoking the user-supplied callback, the RPC request might fail if the network is cut off for a while or the callback service is temporarily down.\nRetries are a technique that helps us deal with transient errors, i.e. errors that are temporary and are likely to disappear soon. Retries help us achieve resiliency by allowing the system to send a request repeatedly until it gets an explicit response(success or fail). By leveraging message queue, you obtain the ability for retrying for free. In the meanwhile, the timer could handle the user-requested retries: It\u0026rsquo;s not the proper time to execute callback service, retry it later.\n3 Conclusion After a long way, we are finally here. The final full architecture would look like this:\nThe whole process:\nAdding a timer task, with specified meta info and task info Inserting task into bucket by hashed timing wheel algorithm(With task_state set to pending) Fetch_current scheduler tries to acquire lock and get global current time The Acquired lock scheduler fetches expiry tasks Return the expected data. \u0026amp; 7. Publishing task data as message to MQ with thread pool; And then set task_state to delivered Message subscriber pulls message from MQ Sending RPC request to callback service(set task_state to success or fail) Retry(If necessary) Wish you have fun and profit\n4 Reference Paper: Hashed and Hierarchical Timing Wheels: Efficient Data Structures for Implementing a Timer Facility Hashed and Hierarchical Timing Wheels ","permalink":"https://ramsayleung.github.io/zh/post/2021/how_to_design_a_reliable_distributed_timer/","summary":"1 Preface I have been maintained a legacy distributed timer for months for my employer, then some important pay business are leveraging on it, with 1 billion tasks handled every day and 20k tasks added per second at most.\nEven though it\u0026rsquo;s old and full of black magic code, but it also also have insighted and well-designed code. Based on this old, running timer, I summarize and extract as this article, and it wont include any running code(perhaps pseudocode, and a lot of figures, as an adage says: A picture is worth a thousand words).","title":"How To Design A Reliable Distributed Timer"},{"content":"1 Distributed Systems for fun and profit source: http://book.mixu.net/distsys/\n2 1. Basic 2.1 Basic concept 本章介绍了分布式系统的基本概念, 例如 scalablity, perfomance, latency, availability\n关于 latent, 这里给出了一个很cool的描述:\nFor example, imagine that you are infected with an airbone virus that turns people into zombies. The laten period is the time between when you became infected, and when you turn into a zombie. That\u0026rsquo;s latency: the time during which something that has already happed is concealed from view.\n关于 availability, 计算公式是:\nAvailability = uptime / (uptime + downtime)\n2.2 Failt tolerance 设计一个可靠的分布式系统, 相当程度上是设计一个fault tolerance 系统, failure 一词 在此章出现了很多次. But without knowing every single aspect of the system, the best we can do is design for fault tolerance.\nFault tolerance: ability of a system to behave in a well-defined manner once faults occur. Fault tolerance boils down to this: define what faults you expect and then design a system or an algorithm that is tolerant of them. You can\u0026rsquo;t tolerate faults you haven\u0026rsquo;t considered.\n限制分布式系统的主要是两个物理因素:\n节点数(你想要更多的存储空间, 更强的计算能力, 自然需要更多的节点)\n节点间的距离(信息传输, 光速是上限)\n从设计系统的角度来考虑这两个限制:\n节点数越多, 出错(failure)的概率就越高(降低可用性, 增加了管理成本) 节点数越多, 节点之间的通信就越多(限制节点数与性能之间的线性增长) 距离越大, 节点通信的延迟就大(性能下降) 2.3 Abstraction and model 因为真实世界有很多与问题域无关的干扰因素, 为了排队这些干扰, 我们引入了Abstraction和Model的概念.\nAbstraction: it make things more manageable by removing real-world aspects that are not relevant to solving a problem.\nModel: it describes the key properties of a distributed system in a precise manner.\n基于不同维度, 可以总结出不同的 Model:\nSystem model(asynchronous/synchronous) Failure model(crash-fail, partitions, Byzantine) Consistency model(strong, eventual) 2.4 Partition and replicate 数据在不同节点之间如何存储是个非常关键的问题, 目前有两种经典的数据存储技术, 分片(partitioning)与冗余(replication):\npartitioning: data set can be split over multiple nodes to allow for more parallel processing.\nreplication: data set can be copied or cached on different nodes to reduce the distance between the client and the server and for greater fault tolerence.\npartitioning: 相当每个节点存储一部分数据, 所有节点的数据汇总起来就是该系统存储的总数据. 但是某个节点挂了, 该节点的数据就丢了\nreplication: 不同节点都存储同一份数据, 这样就可以减少读取不同数据的开销, 以及避免某个节点挂了, 导致部分数据不可用的情况. 但是需要更多的存储空间且不同节点之间数据的同步又是个大问题, 可以说是按下葫芦浮起瓢\nTo replication! The cause of, and the solution to all of life\u0026rsquo;s problems - Homer J Simpson(我很喜欢这句话)\n3 2. Up and down the level of abstraction 3.1 Abstraction 何谓抽象, 有过编程经验的我们自然不会陌生. 抽象, 即去除真实世界与问题域无关的干扰, 专注于问题本身, 使解决方案可被广泛采用.\nAbstractions make the world manaeable: simpler problem statements - free of reality - are much more analytically tractable and provided that we did not ignore anything essential, the solutions are widely acclicable.\n3.2 A system model 分布式系统的关键属性是分布式, 或者说, 程序运行在分布式环境:\n并发运行在独立节点\n通过网络通信, 可能出现某种不确定性或消息丢包\n没有共享内容或共享锁\n上面的特定会带来诸多的影响:\n每个节点都并发运行程序\n本地为先: 每个节点都可以快速访问他们的本地状态, 而所有关于全局状态的信息都有可能是过时的\n节点可能挂掉, 并从故障中恢复回来\n消息可能延迟或丢失(不同于节点故障, 通常很难区分节点故障或网络故障)\n节点间的时钟可能不同步(本地时间与全局时间不一定对应, 且很难观察到异常)\n通过定义一个模型(model)来标识实现一个分布式系统需要交互的环境与机制:\na set of assumptions about the environment and facilities on which a distributed system is implemented\nA robust system model is one that makes the weakest assumptions: any algorithm written for such a system is very tolerant of different environments, since it makes very few and very weak assumptions.\n模型需要越少的假设条件, 可以适应的环境就越多. 等价交换, fair enough.\n3.2.1 Nodes in our system model 节点是作计算与存储的主机(物理或虚拟主机), 它们有如下属性:\n可以运行程序\n可以存储数据到volatile memory(例如内存)或stable state(日志或磁盘)\n拥有时钟(可以准的或者是不准的)\n有很多的故障模型(failure models) 描述了节点挂掉(fail)的方式, 实际中, 大部分的系统都假设是个crash-recovery failure model, 即节点可能挂掉, 但是能从某个状态中恢复回来.\nA crash-recovery failure model: that is, nodes can only fail by crashing, and can(possibly) recover after crashing at some later point.\n3.2.2 Communication links in our system model communication links 不知道应该怎么翻译, 通讯链路? 不译也罢\ncommunication links 用于沟通不同的节点, 允许信息在双向流动. 部分算法假设网络是可靠的: 消息永不丢失并且永不延迟. 虽说这样假设有些许道理, 但是通常我们都是假设网络是不可靠, 因此消息可能丢失或者延迟.\n节点故障 vs 网络分区故障: 3.2.3 Timing/ordering assumptions 分布式系统在物理上分布在不同的位置, 这就会无可避免地会带来一个问题, 如果节点之间的距离不同, 那么节点之间的消息将会以不同的时间点, 甚至不同的顺序到达.\n有两个主要的时序模型:\nSynchronous system model: Process execute in lock-step; there is a known upper bound on message transimission delay; each process has an accurate clock.(这里的 known upper bound 指的是就是传输层的最大等待时间, 超过即由传输层进行重试. 当然, 这样的假设不大现实, 所以这样的模型实际应用比较少) Asynchronous system model: No timing assumptions - e.g. processes execute at indenpendent rates; there is no bound on message transmission delay; useful clocks doesn\u0026rsquo;t exist. 3.2.4 The consensus problem 共识问题(consensus problem) 是商业分布式系统关注的核心问题之一, 所谓的共识问题, 即若干个计算机或节点就某个值达到共识, 更正式的说法是:\nAgreement: Every corrent process must agree on the same value(不搞多数人的暴政, 只搞全体的暴政) Integrity: Every corrent process decides at most one value, and if it decides some value, then it must have been proposed by some process Termination: All processess eventually reach a decision.(不达到共识, 今天你就出不了这个门) Validity: If all corrent processes propose the same value V, then all correct processes decide V.(咱也不能赖皮) 3.3 The FLP impossibility result FLP 不可能原理(FLP impossibility result, 以作者的首字母命名, Fischer, Lynch and Patterson): 在网络可靠, 但节点失效(即便只有一个)的最小化异步模型系统中, 不可能存在一个可以解决一致性问题的确定性共性算法.\nThere does not exist a (deterministic) algorithm for the consensus problem in an asynchronous system subject to failures, even if message can never be lost, at most one process may fail, and it can only fail by carshing(stoppping executing).\nFLP impossibility result 告诉我们: 不要浪费时间, 去试图为异步分布式系统设计面向任意场景的共识算法(别忙了, 白费力气的).\nFLP impossibility result 定义了一个最坏情况, 在允许节点失效的情况下, 异步系统无法确保共识在有限时间内完成, 包括Paxos, Raft等算法也都存在无法达成共识的极端情况, 只是在工程实践中这种情况出现的概率很小.\n3.4 The CAP theorem 非常有名的CAP 定理, 即无法同时达到C,A,P三个属性, 这三个属性是:\nConsistency: all nodes see the same data at the same time.(准确来说, 应该是Strong Consistency)\nAvailability: node failures do not prevent survivors from continuing to operate.\nPartition tolerance: the system continues to operate despite message loss due to network and/or node failure.\n最多只能有两个属性被满足, 如下图:\n同时满足三个属性情况是无法实现的, 即中间交集处. 而满足两个属性的系统模型有如下三个:\nCA(consistency + availability): 弱化分区, 保证一致性和可用性, 也变成单机程序, 个人认为Oracle就是其中典范\nCP(consistency + partition tolerance): 弱化可用性, 可能出现无法提供可用结果的情形, 允许少数节点不可用. 典型算法就是Paxos\nAP(availability + partition tolerance): 弱化一致性, 节点之间可能失去联系, 导致全局数据不一致. 典型例子就是诸多的NoSql\nCA 和CP 模型都提供强一致的模型, 唯一的差别是, CA系统不允许任何节点故障, 因为CA系统无法区别节点故障和网络故障, 为了避免状态不一致, 只能停写; 而对于 2f+1 个节点的CP系统, 允许 f 个节点故障, 是因为其能通过 single-copy consistency 机制, 能保证状态能达到最终一致, 避免出现状态不一致, 从而支持部分节点不可用\n因此, 选择了网络分区, 就需要在高可用和强一致性之间作取舍, 而系统设计即是在基于不同的场景, 作出不同的取舍.\n同样, 强一致性和高性能也存在矛盾, 要保证强一致性, 自然需要节点之间通信达到共识, 这自然会拉高延迟, 这也要系统设计者作出取舍.\n3.5 Consistency model 何谓一致性模型呢:\nConsistency model: a contract between programmer and system, wherein the system guarantees that if the programmer follows some specific rules, the results of operations on the data will be predictable.\n一致性模型可以区分成两类: 强和弱一致性模型\n强一致性模型: Linearizable consistency Sequential consistency 弱(非强)一致性模型 Client-centric consistency models Causal consistenc: strongest model available Eventual consistency models 3.5.1 Strong consistency model Lineariable consistency 和 Sequential consistency 两个模型很相似, 关键的区别是: Lineraiable consistency 要求操作生效的顺序与操作的实际顺序相等; 而Sequential consistency允许对操作进行重新排序, 只要在每个节点上观察到的顺序保持一致.\n只要实现了强一致性模型, 模型保证将单机程序扩展到分布式集群时, 程序不会出现任何问题(程序本身的bug就另当别论), 而使用其他非一致性模型的扩展程序时, 就可能会出现问题.\n3.5.2 Client-centric consistency model 以客户为中心的一致性(Client-centric consistency model)模型是指以某种方式涉及客户或会话概念的一致性模型.\n例如, 以客户为中心的一致性模型可以保证客户永远不会看到一个数据项的旧版本. 这通常是通过在客户端库中建立额外的缓存来实现的.\n感觉这种模型见得不是很多.\n3.5.3 Eventual consistency 最终一致性(Eventual consistency)模型: 如果你停止更新值, 那么间隔某段时间后, 所有的节点都会看到同样的值. 那么某段时间是多少呢? 如果不能给该值给定个严格下限, 这个模型就和\u0026quot;人最终总会死\u0026quot;模型一样, 用处并不大.\n4 3. Time and Order 时序(Time and order)在分布式系统中非常重要, 那么为什么它这么重要呢? 前方提到分布式系统的目标就是可以像在单台机器上解决问题那样, 在多台机器解决同样的问题.\n单台机器运行模型是: 单个程序, 单进程, 单内存空间, 运行在单个CPU上, 而操作系统把多个CPU可能运行多个程序,共享内存的情况给抽象掉, 每个操作就好像人们通过一道门一样, 有预先定义好的前者与后者.\n现在中, 分布式系统运行在多个节点上, 可能同时拥有多个CPU和待运行操作. 如果还想像单台机器那样, 定义一个全序关系(total order), 要不就需要一个精确的时钟, 每个操作一个时间戳, 通过时间戳推算出执行顺序; 要不就需要额外的通信, 指定对应的序号.\n但维护精确的时钟困难且不可靠, 额外的通信成本高.\n4.1 Total and partial order 全序关系(total order)和偏序关系(partial order), 两个数学概念, 作者的解释有点简略, 当初上数学课又没有好好听课, 所以相关的概念不是很理解, 知乎上面有个挺详尽的解释:\n假设A是一个集合 {1,2,3} ;R是集合A上的关系,例如{\u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;1,2\u0026gt;,\u0026lt;1,3\u0026gt;,\u0026lt;2,3\u0026gt;}\n自反性:任取一个A中的元素x,如果都有\u0026lt;x,x\u0026gt;在R中,那么R是自反的.\n对称性:任取一个A中的元素x,y,如果\u0026lt;x,y\u0026gt; 在关系R上,那么\u0026lt;y,x\u0026gt; 也在关系R上,那么R是对称的.\n反对称性:任取一个A中的元素x,y(x!=y),如果\u0026lt;x,y\u0026gt; 在关系R上,那么\u0026lt;y,x\u0026gt; 不在关系R上,那么R是反对称的.\n传递性:任取一个A中的元素x,y,z,如果\u0026lt;x,y\u0026gt;,\u0026lt;y,z\u0026gt; 在关系R上,那么 \u0026lt;x,z\u0026gt; 也在关系R上,那么R是对称的.\n偏序: 设R是非空集合A上的关系,如果R是自反的,反对称的,和传递的,则称R是A上的偏序关系.\n全序:如果R是A上的偏序关系,那么对于任意的A集合上的 x,y,都有 x \u0026lt;= y,或者 y \u0026lt;= x,二者必居其一,那么则称R是A上的全序关系.\n所以可以看到,全序也是一种偏序. 偏序究竟在说啥,关键在于反对称性上,就是说,\u0026lt;x,y\u0026gt; 在关系R上,那么 \u0026lt;y,x\u0026gt; 不在关系R上,那我问你,\u0026lt;y,x\u0026gt; 关系是啥,就是大家都不知道.\n所以说偏序就在于你的集合A={1,2,3,4},有一些元素的关系根据R你是得不出的. 那么既然你不知道这个\u0026lt;y,x\u0026gt;,那么全序关系上,就多加一个条件,都有 x \u0026lt;= y,或者 y \u0026lt;= x,二者必居其一,这样你总知道了吧.\n偏序举例:假设有 A={1,2,3,4},假设R是集合A上的关系:{\u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;4,4\u0026gt;,\u0026lt;1,2\u0026gt;,\u0026lt;1,4\u0026gt;,\u0026lt;2,4\u0026gt;,\u0026lt;3,4\u0026gt;},\n那么:\n自反性:可以看到 \u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;4,4\u0026gt; 都在R中,满足. 反对称性:由于 \u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;4,4\u0026gt; 不属于 x !=y ,所以不考虑这4种,对于 \u0026lt;1,2\u0026gt;,有 \u0026lt;2,1\u0026gt; 不在R中;对于\u0026lt;2,4\u0026gt; 有\u0026lt;4,2\u0026gt;不在R中;对于\u0026lt;3,4\u0026gt; 有\u0026lt;4,3\u0026gt; 不在 R中,满足. 传递性:\u0026lt;1,1\u0026gt;\u0026lt;1,2\u0026gt;在R中,并且\u0026lt;1,2\u0026gt;在R中;\u0026lt;1,1\u0026gt;\u0026lt;1,4\u0026gt;在R中,并且\u0026lt;1,4\u0026gt;在R中;\u0026lt;2,2\u0026gt;\u0026lt;2,4\u0026gt;在R中,并且\u0026lt;2,4\u0026gt;在R中;\u0026lt;3,3\u0026gt;\u0026lt;3,4\u0026gt;在R中,并且\u0026lt;3,4\u0026gt;在R中;等等其他,满足. 所以说R是偏序关系.\n全序举例:\n假设有 A={a,b,c},假设R是集合A上的关系:{\u0026lt;a,a\u0026gt;,\u0026lt;b,b\u0026gt;,\u0026lt;c,c\u0026gt;,\u0026lt;a,b\u0026gt;,\u0026lt;a,c\u0026gt;,\u0026lt;b,c\u0026gt;}和上述一样,可以证明具有自反性,反对称性,传递性,所以是偏序的.\n又因为有 \u0026lt;a,b\u0026gt;,\u0026lt;a,c\u0026gt;,\u0026lt;b,c\u0026gt;, 也就是说两两关系都有了,所以满足对于任意的A集合上的 x,y,都有 x \u0026lt;= y,或者 y \u0026lt;= x,二者必居其一,所以说是全序关系.\n目前还不是很清楚有什么用处.\n4.2 What\u0026rsquo;s time Time is a source of order. 时间可以解析成以下三种形式:\nOrder: 当提到时间是序列的来源时, 我们是在说: 我们可以给无序事件指派时间戳, 以此排序 我们可以使用时间戳来指定操作或者消息分发的顺序 我们可以通过时间来判断某个时间是否在另外一个事件前发生. Interpretation: 时间戳所代表的时间可以解析成秒, 分, 时, 日等人类可读的标识. Duration: 代表真实世界流逝的时间. 计算机世界的算法通常不关系时间的绝对值或人类可读的标识, 但duration 可被用于作某些重要的判断, 例如某个节点是延迟高还是挂了. 4.3 Does time process at the same rate everywhere 在任何地方, 时间流逝的速度是一样的么? 对于这个问题, 有三个不同的答案(即使是在物理世界, 答案也是, 不一样. 相对论说的):\n\u0026ldquo;Global clock\u0026rdquo;: yes \u0026ldquo;Local clock\u0026rdquo;: no, but \u0026ldquo;No clock\u0026rdquo;: no 4.3.1 Time with a \u0026ldquo;Global-clock\u0026rdquo; assumption 全局时钟(Global clock)模型假设有一个完美的全局时钟, 并且所有节点都与这个时钟通信, 且每个节点都精确同步, 且没有时间漂移:\n但是实际不存在这样的时钟, 任何的时钟都会有一定的精度损失, 并且时钟很难避免出现问题: 有人误修改了时钟; 或者有台过时的机器加入集群, 或者时钟因硬件故障而出现漂移.\n不过, 工程实践中的确有程序使用这样的模型:\nFacebook的Cassandra: 假设时钟是同步的, 因为它使用时间戳来处理写冲突, 以最新的时间为准 Google的Spanner: 使用TrueTime API, 保证时间同步的条件下, 又消除了时间漂移的最坏情况. 4.3.2 Time with a \u0026ldquo;Local-clock\u0026rdquo; assumption 每台机器有自己的时钟, 但是没有全局时钟, 这意味着你不能使用时间戳来比较两台不同机器操作的顺序.\n此模型更接近于真实世界, 它指定一个偏序关系: 节点内是有序的, 但是无法仅通过时间戳来进行跨节点排序.\n4.3.3 Time with a \u0026ldquo;No-clock\u0026rdquo; assumption 顾名思义, 无时钟模型不使用时间来排序, 而是使用另外的方式, 例如逻辑时间.\n这个模型也是偏序的: 事件在某个节点, 可以只通过计数器来进行排序, 但是跨系统排序需要额外的信息交换\n当时钟不再使用, 跨节点排序的最大精度上限就由通信延迟决定了. 逻辑时间, 最有名的就是Lamport clock, Lamport就是Paxos之父.\n4.4 Vector clocks(time of causal order) 物理时钟通过计数数和通信来跨分布式系统决定事件顺序, 而 Lamport clock 和 Vector clock是物理时钟的代替品. 使用Lamport clock的时候, 每个进程通过以下规则来维护一个计数器:\n每当一个进程起作用(does work)时, 递增计时器 每当一个进程发送一条消息时, 附带上此计时器 当收到一条消息的时候, 更新计时器的值为 max(local_counter, received_counter) + 1 Lamport clock是偏序关系, 当 timestamp(a) \u0026lt; timestamp(b), 意味着:\na 可能比 b 先发生 a 可能无法与 b 进行比较 已知的时钟一致性条件: 如果一个事件A比事件B先发生, 那么事件A的逻辑时钟也会先于事件B. 如果 a 和 b 来自相同的因果史, 即两个时间戳值都是由同一个进程产生; 或者 b 是 a 发送的消息的响应, 那么 a 先于 b 发生.\n由此可见, 如果两个系统没有交集, 那么它们的时钟值将无法比较. 不过, 这里还有个关键的属性, 某个节点而言, 如果它以 ts(a) 来发送消息, 并以 ts(b) 收取此条消息的响应, 那么 ts(b) \u0026gt; ts(a)\nVector clock是Lamport clock 的扩展, 对于有 N 个节点的分布式系统, 会维护一个size = N的数组 [t1, t2, ....], 对应记录每个节点的计数. 计数值的更新规则:\n每当一个进程起作用(does work)时, 递增数组对应该进程的计数值 每当一个进程发送一条消息, 附带上整个计数器数组 每当收到一条消息: 更新数组中的每个元素 max(local, received) 递增数组中当前节点对应的计数值 4.5 Failure detector(time for cutoff) 假设程序运行在某个节点A上, 它与节点B通信异常了, 怎么判断是网络延迟还是节点B挂了呢? 如果过了相当一段时间后, 我们可以判断节点B挂了, 那么相当一段时间又是多长呢?\nChandra 提出了failure detector这个概念来解决这个问题, failure detector包括两个属性, 完整性(completeness)与精确性(acuracy).\nStrong completeness: Every crashed process is eventually suspected by every correct process Weak completeness: Every crashed process is eventually suspected by some correct process Strong accuracy: No correct process is suspected ever Weak accuracy: Some correct process is never suspected. 完整性比准确性更容易实现, 避免错付没有问题的进程是很难的, 除非可以假设消息延迟的上限, 但是这样的假设只能在同步模型中实现, 所以failure detector在同步模型中可以达到相当的高精度. 而对于没有延迟上限的系统(异步模型), failure detector能做到的极限就是最终准确.\n下面的图阐述了系统模型与问题可解决性之间的关系:\n理想情况下, 我们希望failure detector可以根据网络条件动态调整检测阈值, 以此避免硬编码阈值, 例如TCP的动态超时计算算法那样. Cassandra 使用的是 accrual failure detector, 输出结果是一个故障的可疑度(0到1之间值), 而不是简单的 挂/没挂, 以此为应用根据可疑度自行决断, 提供更高的灵活度.\n4.6 Time, order and performance 使用全序关系也是可能的, 但是为了协调全局顺序, 会付出高昂的性能代价.\n如果你对时间_顺序_同步性要求没有那么高, 你可以获得相当的性能提升. 那么, 什么时候需要顺序来保证正确性呢? 后面提到 CALM定理 会为你提供答案.\n说到底, 又是取舍的话题, 下面的情景只存在电影中:\n5 4. Replication: Preventing Divergence 复制(replication)是分布式系统需要考虑的诸多问题中的其中一项, 但是却非常关键, 并与选主(leader election), 失败检测(failure detection), 共识(consensus)和原子广播(atomic broadcast)等问题息息相关.\n现在让我们先来看复制长什么样, 假设有某个数据库, 存储初始状态的数据, 客户端请求修改数据状态:\n复制的模式可以划分成以下几个步骤:\n(Request) 客户端发送请求到服务端 (Sync) 同步流程开始处理 (Response) 响应返回到客户端 (Async) 异步流程开始处理 由此可见, 复制模式可以划分为同步复制(Synchronous replication)与异步复制(Asynchronous replicatin)\n5.1 Synchronous replication 同步复制模式(也被称为active/eager/push/pessimistic replication):\n我们可以看到三个不同的阶段:\n首先,客户端发送请求. 接下来,处理此前提到的同步复制流程, 即客户端被阻塞至服务端响应. S1与S2, S3通信, 并等待, 直到收到所有服务器的响应 向客户端发送响应, 告知其结果(成功或失败) 可以观察到, 这是一种 N 对 N 的模式: 在返回响应前, 该请求必须被每个节点所确认, 任何一个节点都不允许丢数据, 否则系统就无法成功向所有节点写入数据, 就无法继续提供写入服务, 可能只允许提供只读服务. 此外, 该系统的响应速度取决于最慢那台服务器的响应速度.\n虽说同步模式性能有所欠缺, 但是能提供强持久性保证(strong durability guarantees): 只要向客户端返回成功, 就能保证所有节点写入成功, 要丢失这次的更新数据, 就需要所有节点都丢失.\n5.2 Asynchronous replication 异步模式(也被称为 passive/pull/lazy replication):\n在收到客户端的请求后, Master 节点(S1)立即返回了一个响应, 然后在若干时间后, 执行异步复制: Master 节点以某种模式与其他节点通信, 通知他们更新数据副本. 细节就取决具体的算法.\n这是一个 1 对 N 的模型, 立即返回一个响应, 然后在稍后的某个时间进行复制. 从性能的角度解析, 异步模式可以快速响应, 客户端无需阻塞等待.\n但只能提供弱持久性保证(weak durability guarantees), 只有一个节点持有更新数据, 如果这个节点挂了, 又还没有复制到其他节点, 数据就永久丢失了, 所以给客户端响应成功, 也只能表示, 有概率复制成功.\n5.3 An overview of major replication approaches 在讨论了两种基本的复制模式:同步复制和异步复制之后,我们来看看主要的复制算法.\n有很多对复制算法进行分类的指标, 作者认为第二个指标是(继同步与异步之后):\n避免数据不一致的复制算法 承受数据不一致风险的复制算法 第一类算法的特征是它们\u0026quot;看起来像个单机系统\u0026quot;, 尤其是出现故障的时候, 算法确保系统中只有一个复本是可用的(active), 此外, 该系统保证副本数据总是一致的(consensus problem).\n不同复制算法需要交换的消息数:\n1 * n 条消息(asynchronoous primary/backup) 2 * n 条消息(synchronous primary/backup) 4 * n 条消息(2-phase commit, Multi-Paxos) 8 * n 条消息(3-phase commit, Paxos with repeated leader election) 不同的复制算法有不同的特点, 以下是改编自Google 的Ryan Barret 的一张图:\n上图中的一致性、延迟、吞吐量、数据丢失和故障转移的特点,实际上可以追溯到两种不同的复制模式:\n同步复制(在响应前等待)和异步复制(立即响应).\n当你等待时,你会得到更差的性能,但有更强的保证.\n5.4 Primary/backup replication 主从复制, 可能是最常用的复制算法, 也是最基本的复制算法. 所有更新操作都是在主机进行, 然后复制数据到备机. 有两种变体:\nsynchronous primary/backup replication asynchronous primary/backup replication 前者需要两条消息(update + ack), 而后者只需要一条消息(update). 主_从复制非常常见, MySQL 复制使用的就是主_从复制, MySQL 支持三种模式复制:\n同步: 客户端请求, 先写入主机, 然后同步到所有备机, 成功后响应客户端, 在此之间, 阻塞客户端(性能最差) 异步: 客户端请求, 先写入主机, 然后响应客户端, 再同步备机(同步备机前主机挂, 则丢失数据) 半同步: 客户端请求, 先写入主机, 再同步到备机, 响应客户端, 然后再同步到其他备机(可靠性与性能的折衷) 但是即使是半同步模式, 也会存在问题:\n主机收到一个更新请求,并将其同机给备机 备机同步成功并ACK主机 主机在向客户端响应请挂了 客户端认为是提交失败, 但实际备机写入成功; 如果备机升主, 数据就会有问题.\n所以可见, 主/从复制模式提供的一致性保证, 只能说明是\u0026quot;尽力而为\u0026quot;. 为了避免异常导致一致性保证被违反, 我们需要增加多一轮的消息传递, 就来到了段提供协议(2PC)\n5.5 Two phase commit (2PC) 下图说明两阶段提交(2PC)的消息流:\n[ Coordinator ] -\u0026gt; OK to commit? [ Peers ] \u0026lt;- Yes / No [ Coordinator ] -\u0026gt; Commit / Rollback [ Peers ] \u0026lt;- ACK 2PC流程如下:\n在一阶段(投票), 协调者将更新请求发送给所有的参与者, 每个参与者处理请求并决定是commit 还是 rollback. 在二阶段(决定), 协调者决定结果并通知每个参与者. 2PC容易出现阻塞,因为单个节点的故障(参与者或协调者)会阻塞进度,直到该节点恢复; 并且因为它是 N 对 N, 所以在最慢的节点确认前不能写入. 因此2PC的性能不理想也是情理之中.\n回想之前的CAP理论, 2PC就属于是CA, 它弱化了分区容忍性,所以2PC解决的故障模型不包括网络分区, 当出现网络分区的时候, 2PC就懵了, 不知所措, 只能等待网络分区合并.\n2PC在性能和容错性之间取得了很好的平衡,这就是为什么它在关系型数据库中一直很受欢迎.\n然而,较新的系统通常使用分区容忍的共识算法(partition tolerant consensus algorithms),因为这样的算法可以提供从临时网络分区中的自动恢复,以及更优雅地处理节点间延迟的增加.\n5.6 Partition tolerant consensus algorithms 提起分区容忍的共识算法, 最有名的就是Paxos算法, 但是Lamport大神把他的论文写小说, 让大家苦不堪言, 正因为Paxos出了名的难理解, 所以出现了Raft. 但Goole Chubby的作者Mike Burrow认为:\nThere is only one consensus protocol, and that\u0026rsquo;s Paxos – all other approaches are just broken versions of Paxos. (世界上只有一种共识协议,就是Paxos,其他所有共识算法都是Paxos 的残缺版本)\n首先让我们看下分区容忍算法的特点:\n5.6.1 Network partition 网络分区是指一个或几个节点的网络链接失效. 节点本身继续保持活跃,它们甚至可以接收来自网络分区一侧的客户端的请求.\n网络分区很棘手的,因为在网络分区期间,不可能区分一个节点是挂了呢还是出现网络分区导致请求不可达. 如果发生了网络分区,但没有节点发生故障,那么系统就被分成两个分区. 如下图:\n2个节点时, 节点故障vs 网络分区:\n3个节点时, 节点故障vs 网络分区:\n分裂成两个网络分区时, 共识算法必须要处理这种对称情况, 只允许一个网络分区保持活跃.\n5.6.2 Majority decisions 出现网络分区时, 是怎么保证只有一个分区是活跃的呢? 答案就来自于日常生活: 投票. 只要只要 N 个节点中的 (N/2+1) 投票同意更新, 系统就更新成功, 可以继续运行, 少数派就可以自己独处, 直到分区合并.\n因此, 使用共识算法的系统的节点数都是奇数(3,5或7), 避免出现无法投票成功的情况. 例如,如果节点数为3,那么系统可以容忍一个节点故障;有5个节点,系统容忍两个节点故障.\n除去网络分区, 基于多数人投票的共识算法还可以容忍抖动或故障导致的反对票, 并以此提供系统健壮性.\n5.6.3 Roles 构建一个系统有两种途径:所有节点可以有相同的责任,或者节点可以有单独的、不同的角色.\n而共识算法通常选择为每个节点设置不同的角色, Paxos中的提议者与投票者, 对应Raft中的领导与从众, 也就是领袖与民众. 拥有领袖是一种优势, 可以让系统更有效率.\n角色在系统正常运行过程是固定的, 但这并不意味着领导不会出现意外, 或者可以无限期当领导.\n5.6.4 Epochs 在Paxos和Raft算法, 正常运行的每个一个时间被称为一个纪元(有魔戒的味道了. 在Raft中被称为期限), 在每个纪元期间,只有一个节点是指定的领导者.\n选举成功后,同一个领导者会协调到纪元结束(任期到了就要换人, 不能賴着不走.). 如上图所示(来自Raft的论文),一些选举可能会失败,导致纪元立即结束.\n5.6.5 Leader changes 所有节点开始时都是民众;一个节点在开始时被选为领袖. 在正常运行期间,领袖与民众通过心跳包保持通信,民众可以检测领导是否挂了或被分割到另一个网络分区.\n当一个节点检测到领导变得没有反应时(或者在最初的情况下,不存在领导者),它就会切换到一个中间状态(在Raft中称为 \u0026ldquo;候选\u0026rdquo;),在那里它将术语/周期值增加1,启动竞选程序并竞争成为新领导(王侯将相, 宁有种乎).\n为了当选为领导,一个节点必须获得多数票. 分配选票的一种方法是以先来先得的方式分配选票;这样一来,最终会选出一个领导者.\n5.6.6 Normal operation 在正常操作中,所有的提议都要经过领导节点.\n当客户端提交一个提案(如更新操作), 领导会联络所有节点. 如果不存在竞争性的提议, 领导者就会提出该值. 如果大多数民众接受该值, 那么该值就被认为被接受了. 一旦一个提案被接受, 其值就不能改变, 关于此, Lamport的表述是:\nP2: If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v.\n为了实现这个属性, 领导提案前必须先询问民众, 编号最高的提案与值是哪个, 如果领导发现现在已经有一个提案在执行, 那么就必须先把这个提案执行完, 而不是提出自己的提案.\n即:\nP2c. For any v and n, if a proposal with value v and number n is issued [by a leader], then there is a set S consisting of a majority of acceptors [followers] such that either (a) no acceptor in S has accepted any proposal numbered less than n, or (b) v is the value of the highest-numbered proposal among all proposals numbered less than n accepted by the followers in S.\n这是Paxos算法的核心, 为了确保领导在咨询民众编号最高的提案与值时, 没有竞争提案出现,领导要求民众不能接受比当前编号低的提案. 所以把Lamport的话组合起来, 使用Paxos算法要达到一个共识, 需要两轮的沟通:\n[ Proposer ] -\u0026gt; Prepare(n) [ Followers ] \u0026lt;- Promise(n; previous proposal number and previous value if accepted a proposal in the past) [ Proposer ] -\u0026gt; AcceptRequest(n, own value or the value [ Followers ] associated with the highest proposal number reported by the followers) \u0026lt;- Accepted(n, value) 第一轮, 带上最近的提案与值, 就是为了避免在第一轮问询与第二轮更新之间, 接受一个新更新请求, 就工程实践的角度来看, 此处的previous value with the highest proposal number 相当于是一个乐观锁, 避免被更少的值所更新; 假如中间插入的更新请求成功, highest proposal number 就不是第一轮问询持有的值, 更新就会失败, 避免被覆写.\n5.7 Paxos, Raft, ZAB 有名的分区容忍性共识(Partition tolerant consensus algorithms)算法:\nPaxos: 以希腊的Paxos岛命名, 最重要的分区容忍性共识算法之一(甚至可以把之一去掉), 以难实现难理解著称(但Lamport说Paxos 很简单, 受不了一群人整天问他Paxos算法, 就写了篇 Paxos Make Simple的论文, 人与神的差距), 被用在谷歌的许多系统中, 例如Chubby, BigTable, Spanner等等 ZAB: the Zookeeper Atomic Broadcast protocol, 被用在Apache Zookeeper中, Zookeeper 可以算是Chubby的开源版本 Raft: 对Paxos算法的简化和改进, 被认为更易于学习与理解. 5.8 Replication methods with strong consistency 以下是支持强一致性复制算法的一些特征:\nPrimary/Backup 单一, 静态 Master 节点(主机) Slave 节点(备机)不参与执行操作 复制延迟无上限 不支持分区 手动/临时故障切换,不容错,\u0026ldquo;热备份\u0026rdquo; 2PC 一致表决: commit or rollback(to be or not to be) 静态 Master 节点 不支持分区, 对尾部延迟(tail latency)敏感 Paxos 多数人投票 动态 Master 节点 可应对N/2 - 1个节点的故障 对尾部延迟(tail latency)不敏感 6 5. Replication: Accepting Diveragence 笔记待续\n","permalink":"https://ramsayleung.github.io/zh/post/2021/distributed_system_for_fun_and_profit/","summary":"1 Distributed Systems for fun and profit source: http://book.mixu.net/distsys/ 2 1. Basic 2.1 Basic concept 本章介绍了分布式系统的基本概念, 例如 scalablity, perfomance, latency, availability 关于 latent, 这里给出了一个很cool的描述: For example, imagine that you are infected with an airbone virus that turns people","title":"(笔记)Distributed Systems for fun and profit"},{"content":"1 前言 领导平日开会时,总是对我们说,你们思考问题角度太单一与片面,没有想清楚问题背后的逻辑与作用因子。\n所以领导总是会不厌其烦地给我们推荐《系统思考》这本书:\n初读觉得平平无奇,后来开始使用《系统思考》的系统循环图来分析问题。 既能帮助自己理解清问题的脉络,又能清晰地向他人阐述自己的思路,可谓利器一件。\n2 概念 所谓的系统思考, 即在真实世界中,问题不是简单且孤立的,而是相互联系,交织影响的。\n所以处理真实世界中复杂问题的最佳方式就是用整体的观点观察周围的事物。\n只有拓宽视野,才能避免“竖井”式思绪和组织“近视”问题,做到既见树木,又见森林。\n所谓的系统,是由一群相互连接的实体构成的整体。\n系统具有两个关键特性, 即自组织和涌现。\n自组织:在没有外力的干涉下,动态系统仍能展示出某种的稳定结构, 即自行车的运动,鸟群的盘旋等 涌现:存在一股将给定系统与周围环境联系起来的能量流. 自组织系统与周围环境的能量交换,构成\u0026quot;开放系统\u0026quot;(有点玄幻) 3 系统循环图 系统循环图是进行系统思考的主要工具\n3.1 连接 所有的系统循环图都具有如下基本形式:\n“原因”处于连接箭头的起点,而“结果”处在箭头的尾部。\nS型(+)连接: 原因的增长而导致结果也增长的连接 O型(-)连接: 原因的增长导致了结果的下降的连接 系统循环图中的每个连接必须是S型连接或者O型连接两者中的一种\u0026ndash;不会有其他可能性\n3.2 回路 代表因果链的回路最终连接自己身上,整个回路没有起点,没有终点,每项事物都最终和其他事物产生联系,这样的回路就被称为反馈回路\n3.2.1 增加回路 随着环的每次旋转而不断得到加强,这个情况被称为正反馈,与此对应的系统循环图被称为正反馈回路或增强回路\n相同的算法, 不同的输入导致不同的输出; 因此对于增加回路, 如果输入为正, 则为良性循环; 否则则为恶性循环。\n3.2.2 调节回路 寻求达到某个特定的目标, 减少目标与现实差距的回路,被称为调节回路\n3.2.3 分辨增加回路与调节回路 对于任何连续闭合回路, 如果有偶数个O型连接,该回路为增加回路;否则为调节回路。\n3.3 绘制系统循环图黄金法则 了解问题的边界 从有趣的地方开始 询问“它将驱动什么”以及“它的驱动力是什么” 不要陷入混乱 不要使用动词,请使用名词 不要使用类似于“在xxxx方面增长/降低”这样的词 不要害怕从未出现过的项目 随着进展及时确定连接类型 坚持就是胜利,持续前进吧 好图表必须反映实况 不要爱上你的图表 没有“已经完成”的图表 4 实操 使用系统循环图来分析「置身事内」一书中的政府土地财政与债务风险的问题:\n5 后续 有用的东西就到这里了,剩下的都是例子和实践。\n讲的东西可能很有用,但是用简单的话就能讲清楚,最后写成了一本书,有点灌水了。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E7%B3%BB%E7%BB%9F%E6%80%9D%E8%80%83/","summary":"1 前言 领导平日开会时,总是对我们说,你们思考问题角度太单一与片面,没有想清楚问题背后的逻辑与作用因子。 所以领导总是会不厌其烦地给我们推荐《","title":"系统思考"},{"content":"1 前言 因为此前读了彼德.海斯勒的《寻路中国》,对中国的社会内在结构与驱动力产生了兴趣.\n当然, 我是无法像彼得那样子, 驾车环游中国来作田野调查. 既然无法亲身躬行, 那么只有从纸上得来了. 以此契机, 阅读了费孝通先生的名作《乡土中国》\n2 社会结构 阅读完《乡土中国》之后,有种拨开迷雾, 豁然开朗的感觉, 解答了困扰我许久的问题。因为最近在阅读和学习《系统思考》,于是便使用系统循环图总结了费先生在书中的解析。\n2.1 农业为本 从基层上看去, 中国社会是乡土性的, 而我们的民族自古而来, 就在拖泥带水下田讨生活的, 长江与黄河流域全是农业区. 这样说来, 我们确是和泥土分不开了, 从土里长出过光荣的历史, 自然也会受到土的束缚, 现在很有些飞不上天的样子.\n农业和游牧或工业不同, 它是直接取资于土地的, 游牧的人可以逐水草而居, 飘忽不定; 做工业的人可以择地而居, 迁移无碍. 而种地的人却搬不动地, 长在土里的庄稼行动不得。\n当然, 乡村人口是不可能一直固定的, 因为人口一直在增加, 一块地只要经过几代的繁殖, 人口就到了饱和点; 过剩的人口自得宣泄出外, 负起锄头去另辟新地。可是老根不常动。这是宣泄出外的人,像是从老树上被风吹出去的种子,找到土地的生存,又形成一个小小的家庭殖民地,找不到土地的也就在各式各样的命运下被淘汰,或是发迹。\n《国富论》中提到一个社会越是发展,社会分工就越细,但农业除外。耕种活动中的分工程度很低,至多是男女间有一些分工,例如女人插秧,男人锄地。这种合作与其说是为了增加效率,不如说是因为在某一时间男人忙不过来,家里人来帮忙。\n既然耕种不需要精细的分工,那为什么却会聚集起各种大大小小的村子呢?\n中国农民聚村而居的原因大致有以下几点:\n每家所耕的面积小,所谓小农经营,所以聚在一起住,住宅和农场不会距离过分远 需要水利的地方,有合作的需要,在一起住,合作起来比较方便 为了安全,人多了容易保卫 土地平等继承的原则下,兄弟分别继承祖上的遗业,使人口在一地方一代一代地积起来,成为相当大的村落。 2.2 熟人社会 生活上被土地被囿住的乡民,他们平素所接触的是生而与俱的人物,正像我们的父母兄弟一般,并不是由于我们选择得来的关系,而是无须选择,甚至先我而在的一个生活环境。\n熟悉是从时间里,多方面,经常的接触中所发生的亲密的感觉,这感觉是无数次小摩擦里练习出来的结果。\n而文字发生之初是“结绳记事”,需要结绳来记事是为了在空间和时间中人和人的接触发生了阻碍,我们不能当面讲话,才需要找一些东西来代话。 而熟悉的环境中,我们通过言语,动作,表情就可以表达自己的感想和想法,自然无需退而求其次去使用文字,也难怪文字此前在乡村用处有限,以致于多数人都是不识文字的“文盲”。\n现代社会是个陌生人组成的社会,各人不知道各人的底细,所以得讲明白; 还要怕口说无凭,要立字为据,这样才发生了法律。 在乡土社会中法律是无从发生的。“这不是见外了么?”乡土社会里从熟悉得到信任。\n2.3 差序格局 在《乡土中国》中,费孝通先生提出了一个差序格局的概念,用以描述中国社会和西方社会结构的差异。\n西洋的社会有些像我们在田里捆柴,几根稻草束成一把,几把束成一扎,几扎束成一捆,几捆束成一挑。每一根柴在整个挑里都属于一定的捆,扎,把。我们可以称之为团体格局。而中国的社会结构和西洋的格局是不相同的,我们的格局不是捆一捆扎清楚的柴,而是好像把一块石头丢在水面上所发生的一圈圈推出去的波纹。每个人都是他社会影响所推出去的圈子的中心。被圈子的波纹所推及的就发生联系。此为所谓的差序格局。\n我们的社会中最重要的亲属关系就是这种丢石头形成同心圆波纹的性质。亲属关系是根据生育和婚姻事实所发生的社会关系。从生育和婚姻所结成的网络,可以一直推出去包括无穷的的人,过去的,现在的和未来的人物。这个网络像个蜘蛛的网,有一个中心,就是自己。每个人都有这么一个以亲属关系布出去的网,但是没有一个网所罩住的人是相同的。\n穷在闹市无人问,富在深山有人知。中国传统结构中的差序格局具有这样伸缩能力。在乡下,家庭可以很小,而一到有钱的地主和官僚阶层,可以大到像个小国。中国人也特别对世态炎凉有感触,正因为这富于伸缩的社会圈子会因中心势力的变化而大小。\n2.4 私德与法律 社会结构格局的差别引起了不同的道德观念。道德观念是在社会里生活的人自觉应当遵守社会行为规范的信念。\n在西方的“团体格局”中,道德的基本观众建筑在团体和个人的关系上。团体是一束人和人的关系,是一个控制和个人行为的力量,是一种组成分子生活所依赖的对象,是先于任何个人而又不能脱离个人的共同意志。 在“团体格局”的社会中才发生笼罩万有的神的观念。团体对个人的关系的象征在社对信徒的关系中,是有个赏罚的裁判者,是个公正的维持者,是个万能的保护者。\n如果要了解西洋的“团体格局”社会中的道德体系,决不能离开他们的宗教观念。宗教的虔诚和信赖不但是他们道德观念的来源,而且还是支持行为规范的力量,是团体的象征。\n在象征着团体的神的观念下,有着两个重要的派生观念:每个个人在神前的平等;一是神对每个个人的公道。上帝在冥冥之中,象征着团体无形的实在;但是在执行团体的意志时,还得有人来代理。“代理者”Minister是团体格局的社会中一个基本的概念。执行上帝意志的牧师是Minister, 执行团体权力的官吏也是Minister, 都是“代理者”,而不是神或团体的本身。\n这上帝和牧师,国家和政府的分别是不容混淆。\n神对每个个人是公道,是一视同仁的,是爱的;如何代理者违反了这些“不证自明的真理”,代理者就失去了代理的资格。团体格局的道德体系中于是发生了权利的观念。人对人得互相尊重权利,团体对个人也必须保障这些个人的权利,防止团体代理人滥用权力,于是产生了宪法。\n而在以自己为中心的社会关系网络中,最主要的自然是\u0026quot;克己复礼\u0026quot;, \u0026ldquo;壹是皆以修身为本\u0026rdquo;\u0026ndash;这是差序格局中道德体系的出发点。一个差序格局的社会,是由无数私人关系搭成的网络。这网络的每一个结都附着一种道德要素,因之,传统的道德里不另找出一个笼统的道德观念,所有的价值标准也不能超脱于差序人伦而存在。\n2.5 家族与感情 在西洋,家庭是团体性的社群,这一点我在上面已经说明有严格的团体界限。因为这缘故,这个社群能经营的事务很少,主要的是生育儿女。但在中国乡土社会中,家并没有严格的团体界限,可以沿依需要,沿亲属差序向外扩大。中国的家扩大的路线是单系的,就是只包括父系的这一方面。\nFigure 1: 家族关系\n中国的家是一个事业组织,家的大小是依着事业的大小而决定的。一切事业都不能脱离效率的考虑。求效率就得讲纪律;纪律排斥私情的宽容。在中国的家庭里有家法,在夫妇间得相敬,女子有着三从四德的标准,亲子间讲究负责与服从。\n因为社群不限于夫妻,功能不限于生育,难怪两性间的矜持和保留,不肯像西洋人一般的在表面上流露。 所谓感情相当于普通所谓激动,动了情,甚至说动了火。感情的激动改变了原有的关系,即如果要维持固定的社会关系,就得避免感情的激动。稳定社会关系的力量,不是感情,而是了解。\n如此的社会,如此的家庭关系,每个人都只是完成必备工作的「工具」,人性,作为人的诉求着实不在考虑之内。 所谓了解,是接受着同一的意义体系。同样的刺激会引起同样的反应。乡土社会是靠亲密和长期的共同生活来配合各个人的相互行为,社会的联系是长成的,是熟习的,到某种程度使人感觉到是自动的。\n恋爱是一项探险,是对未知的摸索。这和友谊不同,友谊是可以停止在某种程度上的了解,恋爱却是不停止的,是追求。这种企图并不以实用为目的,是生活经验的创造,也可以说是生命意义的创造,但不是经济的生产,不是个事业。\n社会秩序范围着个性,为了秩序的维持,一切足以引起破坏秩序的要素都被遏制着。男女之间的鸿沟从此筑下。乡土社会是个男女有别的社会,也是个安稳的社会。\n2.6 伦理与教化 礼是社会公认合式的行为规范。合于礼的就是说这些行为是做得对的,对是合式的意思。\n如果单从行为规范一点说,本和法律无异,法律也是一种行为规范。礼和法不相同的地方是维持规范的力量。法律是靠国家的权力来推行的。“国家”是指政治的权力,在现代国家没有形成前,部落也是政治权力。而礼却不需要这有形的权力机构来维持。维持礼这种规范的是传统。\n传统是社会所累积的经验。行为规范的目的是在配合人们的行为以完成社会的任务,社会的任务是在满足社会中各分子的生活需要。人们要满足需要必须相互合作,并且采取有效技术,向环境获取资源。\n这套方法并不是由每个人自行设计,或临时聚集了若干人加以规划的。人们有学习的能力,上一代所试验出来有效的结果,可以教给下一代。这样一代一代地累积出一套帮助人们生活的方法。\n从每个人说,在他出生之前,已经有人替他准备下怎样去应付人生道上所可能发生的问题了。他只要“学而时习之”就可以享受满足需要的愉快了。\n乡土社会是安土重迁的,生于斯、长于斯、死于斯的社会。不但是人口流动很小,而且人们所取给资源的土地也很少变动。在这种不分秦汉,代代如是的环境里,个人不但可以信任自己的经验,而且同样可以信任若祖若父的经验\n在都市社会中一个人不明白法律,要去请教别人,并不是件可耻之事。事实上,普通人在都市里居住,求生活,很难知道有关生活、职业的种种法律。法律成了专门知识。不知道法律的人却又不能在法律之外生活。\n但是在乡土社会的礼治秩序中做人,如果不知道“礼”,就成了撒野,没有规矩,简直是个道德问题,不是个好人\n所谓礼治就是对传统规则的服膺。生活各方面,人和人的关系,都有着一定的规则。行为者对于这些规则从小就熟习,不问理由而认为是当然的。长期的教育已把外在的规则化成了内在的习惯。维持礼俗的力量不在身外的权力,而是在身内的良心。\n每个人知礼是责任,社会假定每个人是知礼的,至少社会有责任要使每个人知礼。所以“子不教”成了“父之过”。这也是乡土社会中通行“连坐”的根据。儿子做了坏事情,父亲得受刑罚,甚至教师也不能辞其咎,教得认真,子弟不会有坏的行为。打官司也成了一种可羞之事,表示教化不够。\n3 总结 虽说《乡土中国》写作时间已经过去了80年,中国也在城填化的方向大踏步前进,但是费先生的书仍然让我更加清晰地了解中国,了解中国人的人情世故,婚姻,传统背后深层的动机。\n说到底,“土气”才是最中国的地方。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E4%B9%A1%E5%9C%9F%E4%B8%AD%E5%9B%BD/","summary":"1 前言 因为此前读了彼德.海斯勒的《寻路中国》,对中国的社会内在结构与驱动力产生了兴趣. 当然, 我是无法像彼得那样子, 驾车环游中国来作田野调查.","title":"乡土中国"},{"content":"1 前言 1.1 关于君主论 国内的电视剧总会提到所谓的「帝王心术」,「帝王心术」究竟是什么? 马基雅维利就从他的视角,向世人剖析,什么是帝王心术,一个优秀的君主应该如何言行处事。\n《君主论》是文艺复兴时期,意大利城邦国家佛罗伦萨共和国的执政官马基雅维利的著作,他的主张有违常人道德观念: 「如有必要,君主是应该使用不道德的手段去实现目标(例如荣誉和生存)」。\n被誉为「一本毁誉参半的奇书,一直被奉为欧洲历代君主的案头之书」\n一八一五年滑铁卢战役后,普鲁士士兵在拿破仑的座驾中发现了一份写满批注的、以《君主论》为主体的《马基雅维利著作集》。\n难怪这本书会被后世诸多政治家所批评,因为马基雅维利把那些「只能做,不能说」的事情,都清楚地写了出来,让世人更好地了解君主行为背后的逻辑。连「马基雅维利主义」都成为了「权术」的代名词。\n从某个角度而言,马基雅维利就像是普罗米修斯,把政治家们的火种盗走,送给了人类。\n1.2 动机 攀附下古人, 既然马基雅维利不是君主可以写出《君主论》, 那么我这个既不是君主的人, 也未曾豪言「彼可取而代之」,立志成为君主的普通人, 也来感受下《君主论》背后的权术.\n我既不想吃猪肉,也不想看猪跑,只是想领会一下,他们是怎么赶猪的。\n正如那些描绘风景的人一样, 为了考察山岳的性质和高地的高度, 就置身到平原, 而为了考察平原便必须高踞顶峰. 同样的道理,\n要真正认识人民本质的人, 需要站在君主的位置上, 而真正认识君主本质的人则需要站在人民的位置上.\n2 道德与政治 马基雅维利认为君主治国不一定恪守道德. 传统所谓的正义, 自由, 宽厚, 信仰,虔诚等美德没有自身的价值, 因为\u0026quot;人们实际上怎样生活与人们应当怎样生活之间有很大距离\u0026quot;\n作为君主, 如果只是善良就会灭亡; 一个君主必须狐狸般的狡猾, 狮子般的凶狠\n对于守信义之类的美德, 君主的正确态度是: 在守信有好处时应当守信, 否则不要守信. 君主有时候必须不讲信义, \u0026ldquo;但是必须把这种品格掩饰好,必须习惯于冒充善者、口是心非的伪君子\u0026rdquo;\n做君主的并没必要条条具备上述的品质(各种传统美德), 但是非常有必要显得好像有这些品质.\n也难怪马基雅维利会被人恨之入骨, 他就像皇帝的新衣里面的小孩, 把人们都共知的但不敢说出的话说了出来, 以个人的私德来要求政治人物, 难免过于可笑;\n毕竟政治从来都只论利弊, 不论道德.\n2.1 苏德互不侵犯条约 谁成想过,在爆发20世纪最为惨烈,最为致命,双方战损近3000千万人的战争「苏德战争」的前两年, 苏联才和德国签订了《苏德互不侵犯条约》:\n希特勒计划在1939年9月1日攻击波兰,因此指示外长里宾特洛甫在8月23日前往苏联,指示其接受苏联的所有条件, 以避免入侵波兰时,又要面对法国和苏联同时介入。\n最后,双方在8月23日签订苏德互不侵犯条约,希特勒和斯大林更协议瓜分波兰。\n在协议签定背后的博弈与斗争:\n俄国十月革命后,由于意识形态及苏联的领土扩张主义等原因,西方国家与苏联的矛盾激化了。 而纳粹德国也反对共产主义,纳粹德国的崛起对苏联夺取波兰领土形成阻碍。\n希特勒一方面谋划入侵波兰,将东普鲁士和德国本土连接,一方面又加紧准备向西方侵略扩张。\n斯大林认为英法不愿和德国开战,故此放弃与英法共同抗纳粹德国,改为与纳粹德国保持表面上的友好关系, 既可以从中谋取利益,又可争取时间及空间应对德国的军事行动,更可趁机侵占芬兰和波罗的海三国。\n苏联先在1939年8月和纳粹德国订立苏德商业协议,为德国提供生产武器的物资, 而德国则向苏联提供机器及车辆等技术产品,增进苏德关系。\n纳粹德国密谋吞并波兰时,苏德展开秘密谈判,苏联决定参与入侵行动, 苏联认为得到波兰东部可达成领土向西扩张,又可构建面对德国的缓冲地带。 另一方面,希特勒为了达成闪电战军事效果,避免过早与苏联发生冲突,故也愿意与苏联签订非战条约\n这两位「元首」虽不是君主,但却有与君主一样的道德观,就是「没有道德」。\n即使明知道将来会成为死敌,但为了当下的利益,也可以携手结成同盟。 既然为了利益,死敌能成为同盟;那为了利益,刀剑相加,自然再合理不过。\n3 人性 马基雅维利对人性有清醒的认识:\n人类本性总是忘恩负义, 变化多端, 弄虚作假, 怯懦软弱, 生性贪婪的\n当你对他们有利用价值的时候, 他们可以为你奉献牺牲; 当你失去利用价值时, 他们会毫不犹豫地抛弃你.\n对人们最好是加以安抚, 要不然就必须消灭掉. 这是因为人们如果受到了轻微的侵害, 仍有能力进行报复; 但是对于沉重的伤害, 他们就无能为力了.\n因此, 当我们对一个人进行侵害时, 应该彻底, 不留后患, 不给他任何报复的机会.\n唐高祖武德九年,秦王李世民在玄武门发动兵变,亲手射死他的哥哥、太子李建成,弟弟李元吉也死于这场兵变。 为斩草除根,他把李建成和李元吉的子女全数杀死。\n假如任何人相信一个大人物因为给予新的恩惠就忘却旧日的损害, 他只能是自欺欺人.\n越王卧薪尝胆,以及邓小平三起三落。\n3.1 渭水之盟 唐高祖武德九年(626年),玄武门之变,李世民杀死当时太子李建成和弟弟李元吉,不久李渊禅位,李世民继受皇位。\n八月,东突厥伺机入侵,攻至距首都长安仅40里的泾阳(今陕西咸阳泾阳县)。\n此时唐朝政局不稳,唐太宗李世民被迫设疑兵之计,亲率高士廉、房玄龄等6骑在渭水隔河与颉利可汗对话,又赠予金帛财物,并与之结盟.\n与异族于首都结城下之盟,不可谓不是奇耻大辱,尤其是对李世民这样的英武之主而言,故而又称渭水之辱。\n而后贞观元年,唐太宗励精图治,并且挑拨颉利、突利二可汗和突厥与铁勒诸部的关系,由是内外离怨,诸部多叛。\n贞观四年,唐军灭东突厥,颉利可汗被押往长安。唐太宗在他面前列举其罪,然后免其一死,安置他在唐长安城。\n颉利在长安“郁郁不得志,与其家人或相对悲歌而泣”。贞观八年,颉利亡于长安。\n4 为君之道 以史为鉴, 总结出来的为君之道, 以此巩固君权:\n避免蔑视与憎恨: 尊重臣民的财产及其妇女, 一言九鼎, 努力使自己的行为表现得伟大, 英勇, 严肃, 庄重, 坚韧不拔 支配权力: 君主必须把承担责任的事情让他人办理, 而把施恩的事情交由自己掌管(施恩我做, 背锅你来; 类似韩非子所说的二柄) 巩固基本盘: 平民, 贵族, 军队, 人人有不同的诉求, 如果都能满足自然最好; 否则先优先满足基本盘, 即统治基础. 展现才能: 成就伟大的事业, 作出卓越的范例, 展示自身的能力, 作出非凡之事. 任用贤才: 任用有才之人, 促进国家和城市繁荣, 物尽其用, 人尽其才. 远离谄媚之人: 提拔敢言之人, 尽量避免被谄媚的谎言所蒙蔽, 听别人的建议, 自己做明智的决定, 没有判断力的君主注定走向灭亡. 君主的核心职责是让合适的人,做合适的事,选贤举能,毕竟贤明如诸葛孔明者,也无法事事躬亲。\n如若事事都由君主处理,精力精悍如朱元璋或者雍正,一年无休都无法把国事处理过来。\n何况集权于一身者,必然集怨于一身,毕竟你的权力只能从别人手上抢过来,这个集权过程本身就会树敌无数。 何况集权于一身,事情搞砸了,也没有人可以来背锅,所以不要把那么多「小组长」的职务挂身上。\n为君主分忧可不止为君主做事,还要包括为君主背锅。\n4.1 崇祯与满清议和 崇祯十五年,崇祯帝密召兵部尚书陈新甲主持与满清议和,由马绍愉北上洽谈。\n一日,马绍愉从边关发回议和条件的密函,陈新甲置于案上,其家童误以为是《塘报》,交给各省驻京办事处传抄,事起泄露,群臣哗然。\n明朝士大夫鉴于南宋的教训,皆以为与满人和谈为耻。\n新甲不引罪,反自诩其功,言称此皆陛下旨意。崇祯更加愤怒。(不帮忙背锅,还甩锅给皇帝了,也难怪崇祯会那么生气)\n崇祯十五年七月二十九日将陈新甲下狱,后以失陷城寨为罪名而斩首。(恼羞成怒了)\n陈新甲既死,明朝丧失最后一次议和的机会。\n5 总结 君主论的核心是, 如何建设强大的国家, 并为达目的, 不择手段; 可为崇高理想, 行卑劣之事.\n如果马基雅维利生在中国,估计会与韩非子引为知己,毕竟马基雅维利的主张与商鞅与韩非子的法家思想,有相当多的相似之处。\n虽说如此, 但我个人觉得, 人生而为人, 还是应该有值得坚守之事, 权谋手段纵使能奏一时之效, 恐怕也无法万世通行.\n最后还是应该回到仁爱道德这条老路上, 毕竟仁者无敌嘛.(儒家的想法)\n但多读点书, 看下权谋之术终究无大错. 好比即使我不去忽悠人, 也可以学下忽悠的本领, 防止被人忽悠.\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%90%9B%E4%B8%BB%E8%AE%BA/","summary":"1 前言 1.1 关于君主论 国内的电视剧总会提到所谓的「帝王心术」,「帝王心术」究竟是什么? 马基雅维利就从他的视角,向世人剖析,什么是帝王心术,一个优","title":"《君主论》:所谓「帝王心术」"},{"content":"Iterate through pagination in the Rest API\n1 Preface About 4 months ago, icewind1991 created an exciting PR that adding Stream/Iterator based versions of methods with paginated results, which makes enpoints in Rspotify more much ergonomic to use, and Mario completed this PR.\nIn order to know what this PR brought to us, we have to go back to the orignal story, the paginated results in Spotify\u0026rsquo;s Rest API.\n2 Orignal Story Taking the artist_albums as example, it gets Spotify catalog information about an artist\u0026rsquo;s albums.\nThe HTTP response body for this endpoint contains an array of simplified album object wrapped in a paging object and use limit field to control the number of album objects to return and offset field to set the index of the first album to return.\nSo designed endpoint in Rspotify looks like this:\n/// Paging object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#object-pagingobject) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Page\u0026lt;T\u0026gt; { pub href: String, pub items: Vec\u0026lt;T\u0026gt;, pub limit: u32, pub next: Option\u0026lt;String\u0026gt;, pub offset: u32, pub previous: Option\u0026lt;String\u0026gt;, pub total: u32, } /// Get Spotify catalog information about an artist\u0026#39;s albums. /// /// Parameters: /// - artist_id - the artist ID, URI or URL /// - album_type - \u0026#39;album\u0026#39;, \u0026#39;single\u0026#39;, \u0026#39;appears_on\u0026#39;, \u0026#39;compilation\u0026#39; /// - market - limit the response to one particular country. /// - limit - the number of albums to return /// - offset - the index of the first album to return /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) pub fn artist_albums\u0026lt;\u0026#39;a\u0026gt;( \u0026amp;\u0026#39;a self, artist_id: \u0026amp;\u0026#39;a ArtistId, album_type: Option\u0026lt;\u0026amp;\u0026#39;a AlbumType\u0026gt;, market: Option\u0026lt;\u0026amp;\u0026#39;a Market\u0026gt;, ) -\u0026gt; ClientResult\u0026lt;Page\u0026lt;SimplifiedAlbum\u0026gt;\u0026gt;; Supposing that you fetched the first page of an artist\u0026rsquo;s ablums, then you would to get the data of the next page, you have to parse a URL:\n{ \u0026#34;next\u0026#34;: \u0026#34;https://api.spotify.com/v1/browse/categories?offset=2\u0026amp;limit=20\u0026#34; } You have to parse the URL and extract limit and offset parameters, and recall the artist_albums endpoint with setting limit to 20 and offset to 2.\nWe have to manually fetch the data again and again until all datas have been consumed. It is not elegant, but works.\n3 Iterator Story Since we have the basic knowledge about the background, let\u0026rsquo;s jump to the iterator version of pagination endpoints.\nFirst of all, the iterator pattern allows us to perform some tasks on a sequence of items in turn. An iterator is responsible for the logic of itreating over each item and determining when the sequence has finished.\nIf you want to know about about Iterator, Jon Gjengset has covered a brilliant tutorial to demonstrate Iterators in Rust.\nAll iterators implement a trait named Iterator that is defined in the standard library. The definition of the trait looks like this:\npub trait Iterator { type Item; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt;; // methods with default implementations elided } By implementing the Iterator trait on our own types, we could have iterators that do anything we want. Then working mechanism we want to iterate over paginated result will look like this:\nNow let\u0026rsquo;s dive deep into the code, we need to implement Iterator for our own types, the pseudocode looks like:\nimpl\u0026lt;T\u0026gt; Iterator for PageIterator\u0026lt;Request\u0026gt; { type Item = ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt; { match call endpoints with offset and limit { Ok(page) if page.items.is_empty() =\u0026gt; { we are done here None } Ok(page) =\u0026gt; { offset += page.items.len() as u32; Some(Ok(page)) } Err(e) =\u0026gt; Some(Err(e)), } } } In order to iterate paginated result from different endpoints, we need a generic type to represent different endpoints. The Fn trait comes to our mind, the function pointer that points to code, not data.\nThen the next version of pseudocode looks like:\nimpl\u0026lt;T, Request\u0026gt; Iterator for PageIterator\u0026lt;Request\u0026gt; where Request: Fn(u32, u32) -\u0026gt; ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;, { type Item = ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt; { match (function_pointer)(offset and limit) { Ok(page) if page.items.is_empty() =\u0026gt; { we are done here None } Ok(page) =\u0026gt; { offset += page.items.len() as u32; Some(Ok(page)) } Err(e) =\u0026gt; Some(Err(e)), } } } Now, our iterator story has iterated to the end, the next item is that current full version code is here, check it if you are interested in :)\n4 Stream Story Are we done? Not yet. Let\u0026rsquo;s move our eyes to stream story.\nThe stream story is mostly similar with iterator story, except that iterator is synchronous, stream is asynchronous.\nThe Stream trait can yield multiple values before completing, similiar to the Iterator trait.\ntrait Stream { /// The type of the value yielded by the stream. type Item; /// Attempt to resolve the next item in the stream. /// Returns `Poll::Pending` if not ready, `Poll::Ready(Some(x))` if a value /// is ready, and `Poll::Ready(None)` if the stream has completed. fn poll_next(self: Pin\u0026lt;\u0026amp;mut Self\u0026gt;, cx: \u0026amp;mut Context\u0026lt;\u0026#39;_\u0026gt;) -\u0026gt; Poll\u0026lt;Option\u0026lt;Self::Item\u0026gt;\u0026gt;; } Since we have already known the iterator, let make the stream story short. We leverage the async-stream for using macro as Syntactic sugar to avoid clumsy type declaration and notation.\nWe use stream! macro to generate an anonymous type implementing the Stream trait, and the Item associated type is the type of the values yielded from the stream, which is ClientResult\u0026lt;T\u0026gt; in this case.\nThe stream full version is shorter and clearer:\n/// This is used to handle paginated requests automatically. pub fn paginate\u0026lt;T, Fut, Request\u0026gt;( req: Request, page_size: u32, ) -\u0026gt; impl Stream\u0026lt;Item = ClientResult\u0026lt;T\u0026gt;\u0026gt; where T: Unpin, Fut: Future\u0026lt;Output = ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;\u0026gt;, Request: Fn(u32, u32) -\u0026gt; Fut, { use async_stream::stream; let mut offset = 0; stream! { loop { let page = req(page_size, offset).await?; offset += page.items.len() as u32; for item in page.items { yield Ok(item); } if page.next.is_none() { break; } } } } 5 Appendix Whew! It took more than I expected. Since iterators is the Rust features inspired by functional programming language ideas, which contributes to Rust\u0026rsquo;s capability to clearly express high-level ideas at low-level performance.\nIt\u0026rsquo;s good to leverage iterators wherever possible, now we can be thrilled to say that all endpoints don\u0026rsquo;t need to manuallly loop over anymore, they are all iterable and rusty.\nThanks Mario and icewind1991 again for their works :)\n","permalink":"https://ramsayleung.github.io/zh/post/2021/iterate_through_pagination_api/","summary":"Iterate through pagination in the Rest API\n1 Preface About 4 months ago, icewind1991 created an exciting PR that adding Stream/Iterator based versions of methods with paginated results, which makes enpoints in Rspotify more much ergonomic to use, and Mario completed this PR.\nIn order to know what this PR brought to us, we have to go back to the orignal story, the paginated results in Spotify\u0026rsquo;s Rest API.\n2 Orignal Story Taking the artist_albums as example, it gets Spotify catalog information about an artist\u0026rsquo;s albums.","title":"Let's make everything iterable"},{"content":"1 前言 这是我读的第二本Peter Hessler(何伟)的书, 第一本是在大学期间, 经朋友推荐读起的, 再次拾起他的书, 便是这本.\n一如前作, 他的文笔还是那般地平静隽永, 充满各种不期而至的幽默, 引人入胜, 手不释卷.\n2 长城, 乡村, 工厂 本书的主题是用汽车来感受中国的变迁, 记录下他眼中的中国工业革命之路.\n第一部分, 追寻长城的殘亘断砖, 游历中国大半个北方, 见识在现代化的进程下, 各种历史与村庄的消亡.\n第二部分, 在北京附近的农村定居, 并融入当地村民的生活, 见证农村的日益变化.\n第三部分, 是现代化进程的主力, 工厂的变迁之旅, 着眼于浙江的工厂, 以此来感受中国那如同19世纪工业革命的洪流.\n把目光放在每一个个体上, 记录着这个国家的一个个小人物, 如何成为时代中的历史人物.\n\u0026lt;2022-02-26 Sat\u0026gt;\n看到徐州丰县为了避免记者进入八孩妈所在的村子,把整个村子用铁皮围了起来,由此想起了长城,想起了Beyond《长城》里面的歌词:\n围着老去的国度,围着事实的真相。\n3 熟悉与陌生 在涪陵的生活经历, 让他抹去了中国与外国的差别, 用中国通这样的词语来形容他, 甚至会显得苍白, 他对中国社会的观察和理解, 甚至超越了许多身处其中的中国人.\n读此书的时候, 有种非常奇妙的感觉, 时常在熟悉与陌生之间, 在现实与魔幻之间不停地交替, 他写出了那种说不清, 道不明的微妙之处. 当局者迷, 旁观者清, 在一团迷雾之中理不清的头绪, 原来在旁人看来却异常清晰.\n尤其令我感触良多的是, 写出了中国人心照不宣的潜规则, 写出了对生活的反思, 对世界的思考. 尤其是关于房价与地产的观察和思考, 可以说具有相当的洞察力.\n3.1 房地产 各地都在基建, 大兴土木, 招商引资, 但是资金从何而来呢?\n沿海地区的各大城市的一半的财政收入来源于地产, 每个城市的市长相当于企业的CEO, 想的是在5年任期内多搞钱, 玩法就是从农村买来土地, 然后修建对应的开发区, 拉高地价, 再转手以城市的土地价格卖出, 一进一出, 获利丰厚.\n对于农民来说, 只有土地的使用权, 没有所有权, 村集体也是这般, 所以当政府以城市化进程征收土地时, 农民并没有反对的余地, 而征地补偿款是以农村土地的价格补偿的.\n所以说当卖地是政府的财政收大头, 还指望政府帮忙控制房价, 客观事实上就如同缘木求鱼.\n更何况有金融的系统性风险, 房产一旦降下来, 当房子的价值甚至少于要还的房贷, 只会导致贷款坏账, 进而引发银行业的蝴蝶效应.\n3.2 政治与官僚 作为一个在中国长期居住的外国人, 少不了和各种政府官员打交道, 因此对人民公仆们有相当深的认识:\n税务局尤其重要-如果没把这些干部们弄高兴,他们会让你的企业完全垮掉。 「知道在中国是怎么回事吧:偷税漏税,」高老板说道。他这么说的意思,是指如果工厂要跟着大家做那种低报营业收入的勾当,先得跟那些干部们把关系搞好\n\u0026ldquo;我们还没有做到这一步,但稍后,我们肯定要请税务局的官员们出去吃吃饭,\u0026ldquo;他说。我问他,这些宴席上要不要送礼,他摇了摇头。\u0026ldquo;饭桌上是不送礼的,\u0026ldquo;他跟我讲,\u0026ldquo;那些事情都是单独的。要送礼,得到他们家里去。\u0026rdquo;\n位干部都不太注重着装,不过,他们还是高昂着头,其中一个拿出丽水市国税局的身份卡晃了一下。他姓刘,穿着蓝色牛仔裤和橘黄色T恤衫。 他蓄着平头,这样的发型在中国一般意味着麻烦来了。在中国,那是恃强凌弱者的经典发型,我只要看见这样的平头,心就会不由自主地往下沉\n在工业城镇,人们对于当地政府的态度大多漠不关心。很多人抱怨当地政府官员贪污腐化,可说起这些事情的时候用的尽是些非常抽象的话语,因为他们跟领导干部很少有正面接触\n等到人们真正向政府求助时,那通常是走投无路的标志\n实际上,石帆的所有人都在抱怨搬迁这件事,其中有几个人怒气很大,他们已经准备正式上访。他们的目标,是要找到政府里面某个级别更高的官员。\n跟许许多多中国人一样,他们对当局有一种根深蒂固的信任,觉得贪腐只是地方一级官员的毛病。他们去了省城杭州,在各个专门设置的办公室里排队等候,以期引起某个官员的关注\n中国人即对公共事务, 政治缺少关注, 但日常又始终被政治空气所萦绕。你不来关心政治, 政治也会来关心你.\n中国人应该主动关心政治,政治不只是头上的一片飘渺的云,政治与你的工作,税收,教育,养老,医疗,防疫和各种民生问题息息相关。\n如果每个公民是计算机里面的独立进程,政治就是运行每个进程的底层操作系统,与每个进程「生死悠关」。\n4 写在最后 读何伟的书, 你不会有外国人写中国人的感觉, 不会有那种被故意夸大或者美化的故事, 你读到的, 只是一个温柔的旁观者, 用他的笔触, 记录他所看到的事, 每一个同时代的中国人都可能经历过的事而言.\n\u0026lt;2021-07-03 Sat\u0026gt; 在Twitter上看到了消息,据说因为被学生举报,兼之中美关系恶化,美国作家何伟未获四川大学续聘,学期结束后他不得不与家人离开中国返美,希望今后能再回到中国。\n唉,一声叹息。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%AF%BB%E8%B7%AF%E4%B8%AD%E5%9B%BD/","summary":"1 前言 这是我读的第二本Peter Hessler(何伟)的书, 第一本是在大学期间, 经朋友推荐读起的, 再次拾起他的书, 便是这本. 一如前作, 他的文","title":"《寻路中国》:一个中国通眼中的中国"},{"content":"1 前言 在今天的大众媒体和图书市场上, 到处充斥着关于潜能提升, 心理操控, 占卜星座, 催眠读心等伪心理学的主题.\n而这本书就是想拔除迷雾, 去伪存真, 告诉读者什么才是真正的心理学.\n2 可证伪性 科学家们提到「可解的问题」时, 通常指的是「可校验的理论」.\n「可校验的理论」的定义在科学上是非常明确的: 这个理论是有可能被证伪的, 如果一个理论不可证伪, 并且和自然界的真实事件没有关联, 那么它就是无用的.\n不可证伪的例子:\n18世纪的医生本杰明.拉什(benjamin Rush), 在面对袭击而来的黄热病的时候, 使用的是放血疗法(用手术刀或水蛭吸血的方式离开身体, 顺便插个历史故事, 美国国父华盛顿就是死于这种疗法), 他为许多病人实施了这种疗法, 当他自己被感染时, 也是如法炮制.\n拉什坚信自己的理论是正确的, 拉什认为如果病人好转, 就被作为 放血疗法有效的证据, 如果病人死掉, 就被拉什解释为病人已经病入膏肓, 无药可救. 如此一来, 拉什的放血疗法就是不可证伪的, 立于不败之地. 可证伪性,让我想起了卡尔·波普尔对对马克思理论的批判,认为其总是预设了立场与动机甚至预设结论来判断资本主义必将被共产主义取代,是不可证伪的伪科学的教条(波普尔给出专业的批判)。资本主义必将被共产主义取代,那为什么到现在还没有发生?」,辩护者如是说:「那是资本家采取诸如提高工人福利等手段,延缓了资本主义被推翻的过程」。\n按照这样的说法,马克思主义的预言没有成真,并不是不成真,而是时间还没有到,是因为资本家有这样那样的措施,总之存在各种原因,就存在马克思主义是错的这一种原因。\n马克思主义和占星术也有相同的奥秘嘛。好的理论能够做出具体的预测, 具有高度的可证伪性. 相比于一个不精确的预测, 一个具体的预测如果得到证实, 会为产生这个预测的理论提供更大的支持. 事前预测, 而不是事后諸葛亮解释.\n3 个案与见证 个案研究和见证叙述在心理学(以及其他科学)研究的早期阶段是有用的, 但在研究的后期, 当对理论进行检验的之时, 个案研究就毫无用处. 因为个案是不可重复的, 可能是实验过程导致的偏差. 「反对者很喜欢用个例来反驳,如指责某国的国民普遍被压迫,某国新闻发言人的反驳逻辑竟然是,为什么我没有感受到压迫?」另外安慰剂效应和鲜活性问题也说明个案的不可靠\n安慰剂效应: 无论治疗是否有效, 人们都会报告某种疗法曾经对他们有所帮助. 也就是你什么都没有做, 只要别人对你说, 你接受了治疗, 你都会有一定概率变好. 正如《绿野仙踪》中, 仙女并没有真的给铁皮人一个心脏, 没有给稻草人一个大脑, 也没有给狮子勇气, 但是他们都感觉更好了.\n鲜活性问题: 当面临问题解决或决策情境的时候, 人们会从记忆中提取与当前情境有关的信息. 因此, 人们倾向于利用更容易获得的, 能够用来解决问题或做出决策的信息. 例如, 有10w人在美团上推荐了一个店, 但是你一个朋友说这家真难吃, 你很可能会决定不再去这家店, 即使有10w推荐过.\n4 相关与因果 两个变量之间仅仅存在相关, 并不能保证一个变量变化就会导致另一个的变化, 也就是相关并不意味关系.\n正如文中的例子, 据观察, 家庭中家用电器越多的人, 使用避孕工具就会越多. 那能否得出这样的结论? 烤箱导致人们使用避孕工具.\n根据常识, 答案自然是不. 我们会认识到, 这两个变更可能有相关, 但不是因果关系, 这两个变量可能通过其他变量联系起来.\n我们知道, 教育水平和避孕工具使用和社会经济地位都有关系, 经济水平高的家庭会拥有更多的家用电器.\n两个变量除了不一定会有因果关系外, 还可能存在不同的方向, 即我们假设A的变化引起B的变化, A-\u0026gt;B, 但实际可能是相反的作用方向, B-\u0026gt;A\n5 操纵与控制 科学家最有力的武器就是实验, 而实验的核心就是操纵, 控制, 比较, 即控制变量来比较. 在实验中, 研究者要对被假设为原因的变量进行操纵, 通过实验控制和随机分配来保持对其他所有变量不变, 然后观察这个假设变量是否会产生影响.\n80年前, 有一匹名叫聪明汉斯的马, 似乎知道如何算术, 无论给汉斯出加法, 减法, 乘法, 汉斯都能用它的蹄子敲出答案, 人们都惊呆了, 认为这匹马会思考, 具有数学能力.\n一位名为芬斯特的心理学家解开了谜团, 他发现这匹马对视觉线索极其敏感, 它能察觉人类头部的细微动作, 于是他设计了一个方法来测试马的能力:\n就是让不知道答案的提问者向这匹马提问, 或者让提问者在马的视线范围以外呈现问题, 而在这些情况下, 汉斯就失去了它的\u0026quot;数学能力\u0026quot;\n原来, 汉斯是一个非常细心的人类行为的观察者, 当它正在敲出答案的时候, 它会观察训练员或者出题者的头部.\n当汉斯接近答案的时, 训练员会下意识地稍微歪一下他的头, 然后汉斯就会停下来.\n可见, 从\u0026quot;马能敲出正确答案\u0026quot;就得出\u0026quot;马具有数学能力\u0026quot;的结论是不符合逻辑的.\n对照和实验, 就能识破大部分伪科学的骗局.\n6 聚合性证据 关于科学的典型误解: 公众误认为, 某一科学研究领域中的所有问题都能通过某个关键实验得到解决, 或者某个重要灵感成就了理论的进步, 并彻底颠覆了先前众多研究者累积的全部知识(可能是好莱坞的电影看多了)\n但实际的科学是蹒跚而曲折的, 渐进地进行整合. 正如没有哪个实验可以一捶定音的, 但是每一个实验至少都能帮助我们排除一些可能的解释, 并让我们在接近真理的道路上向前迈进.\n所谓的聚合性证据, 就是来自多个方面, 多个角度的证据, 联系起来, 交叉印证某个理论的正确性, 新的理论不仅能解释新的科学数据, 也必须能解释已有的数据\n延伸:\n频率-效力效应: 一个陌生但看似有理的论断, 不管是真是假, 只要经过不断地重复, 就会增加人们对它的相信程度\n例子:\n有名的\u0026quot;曾参杀人案\u0026quot; 法西斯宣传部部长戈林的名言, 一个谎言, 被重复一万次之后, 也会成为真理. 7 概率推理 大多数学科, 包括心理学所得出的都是概率式的结论, 大多数情况下会发生, 但并非任何情况下都会发生. 例如, 吸烟危害健康, 但不是说吸烟你就一定死, 但是吸烟的人的死亡率是会比不吸烟的人高.\n\u0026ldquo;某某人\u0026quot;统计学: 用特例来否定统计学概率. 你说吸烟死亡率更高, 你看街口的张三, 从12岁吸烟, 每天三包烟, 现在87岁了, 还不是好好的么.\n当我们面对和过去持有的观察相矛盾, 同时又是强有力的证据时, 无所不在的\u0026quot;某某人\u0026quot;总是会立刻跳出来否定这些统计规律\n赌徒谬误: 即倾向于将过去事件和未来事件之间联系起来, 而实际上两者是独立的. 连开了十局\u0026quot;大\u0026quot;之后, 下一局开\u0026quot;大\u0026quot;的概率被认为会高于50%; 连续生了两个女儿之后, 第三个孩子会被认为更有可能是儿子.\n再谈鲜活性问题: 当人们遇到具体的, 具有鲜活性的证据时, 就把概率信息抛到一边了. 他们没有考虑到, 较大的样本能够提供对于总体数值更为精确的估计\n虽然科学的结论并非是100%准确, 但根据心理学研究及理论所做出的预测仍然是有用的.\n8 偶然性 人们很难认识到, 行为事件结果的变化中有一部分是由偶然因素造成的, 也就是说, 行为的变化有一部分是随机因素作的结果.\n科学的预测应该是概率性的, 是对总体趋势的概率性预测. 在解释人类行为的原因方面, 统计预测(基于群体统计趋势的预测)远远优于临床预测(基于个人经验预测)\n当人们相信两类事件在通常情况下应该一起发生时, 就会认为自己频繁地看到了同时发生的现象, 甚至当这两类事件的同时出现是随机的, 并不比任何其他两个事件同时发生的频率更高时也是如此.\n人们总是倾向于看到自己想看到的事情. 而试图去解释偶然事件的倾向可能源于我们深切地渴望相信自己是可以控制这些事件的\n控制错觉: 人们错误地相信他们的参与行为能够决定随机事件. 常见的就是打麻将的时候, 拿到麻将之后, 不会直接看, 而是搓着麻将, 小心翼翼地开牌, 但是这样并不会改变什么.\n9 总结 虽然, 通篇介绍的都是心理学, 译标题也是对伪心理学说不, 但实际的阐述适用于所有科学的方法论. 科学, 是一种思考和观察事物以便深入理解其运行机制的方法.\n科学进步的方式是:\n提出理论解释世界中的特定现象, 根据这些理论做出预测, 实证地检验这些假设, 基于检验的结果对理论进行修正(通常次序为:理论\u0026mdash;预测\u0026mdash;检验\u0026mdash;修正)\n译名为\u0026quot;对伪心理学说不\u0026rdquo;, 实际是\u0026quot;对伪科学说不\u0026quot;\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%AF%B9%E4%BC%AA%E5%BF%83%E7%90%86%E5%AD%A6%E8%AF%B4%E4%B8%8D/","summary":"1 前言 在今天的大众媒体和图书市场上, 到处充斥着关于潜能提升, 心理操控, 占卜星座, 催眠读心等伪心理学的主题. 而这本书就是想拔除迷雾, 去伪存真,","title":"对伪心理学(科学)说不"},{"content":"1 前言 非暴力沟通, 通过体会言语背后的情感, 进而体察他人的内心, 与他人建立情感上的联系. 通俗地说, 就是个心理学家教你, 应该如何说话.\n2 非暴力沟通模型 非暴力沟通模型:\n非暴力沟通指导我们转变谈话和聆听的方式。我们不再条件反射式地反应,而是去明了自己的观察、感受和愿望,有意识地使用语言。\n非暴力沟通的精髓在于对观察、感受、需要、请求 四个要素的觉察,而不在于使用什么字眼进行交流。\n首先,留意发生的事情。我们此刻观察到什么?不管是否喜欢,只是说出人们所做的事情。要点是,清楚地表达观察的结果,而不判断或评估。\n接着,表达感受,例如受伤、害怕、愤怒等。\n然后,说出哪些需要导致那样的感受。\n一旦诚实地表达自己后,提出第四个要素-具体的请求。\n这一要素明确告知他人,我们期待采取何种行动,来满足我们。\n举例:一位母亲对处在青春期的儿子:“小明,看到咖啡桌下的两只袜子(观察),我不太高兴(表达感受),因为我看重整洁(需要),你是否愿意将袜子拿到房间或放进洗衣机?(明确地请求)\n抽象模型:\n留意发生的事情, 客观地表达观察结果 表达我的感受 说明导致感觉的原因 提出请求 3 用心倾听 倾听的第一步, 是留意他人的感受而不是说教. 而体会他人的感觉和需要, 但遭遇他人的痛苦时, 我们常常急于提建议, 安慰或表达我们的态度和感受.\n以下的行为会妨碍我们体会他人的处境:\n建议: \u0026ldquo;我想你应该\u0026hellip;\u0026hellip;\u0026rdquo;\u0026quot; 比较: \u0026ldquo;这算不了什么. 你听听我的经历\u0026hellip;\u0026hellip;\u0026rdquo; 说教: \u0026ldquo;如果你这样做\u0026hellip;\u0026hellip;你将会得到很大的好处.\u0026rdquo; 安慰: \u0026ldquo;这不是你的错;你已经尽最大努力了.\u0026rdquo; 回忆: \u0026ldquo;这让我想起\u0026hellip;\u0026hellip;\u0026rdquo; 否定: \u0026ldquo;高兴一点. 不要这么难过.\u0026rdquo; 同情: \u0026ldquo;哦,你这可怜的人\u0026hellip;\u0026hellip;\u0026rdquo; 询问: \u0026ldquo;这种情况是什么时候开始的?\u0026rdquo; 辩解: \u0026ldquo;我原想早点打电话给你,但昨晚\u0026hellip;\u0026hellip;\u0026rdquo; 纠正: \u0026ldquo;事情的经过不是那样的.\u0026rdquo; 在倾听他人的观察, 感受, 需要和请求之后, 我们可以主动表达我们的理解.\n如果我们已经领会了他们的意思, 我们的反馈将帮助他们意识到这一点. 而非暴力沟通建议我们使用疑问句来给予他人反馈.\n而在给他人反馈时, 我们语气十分重要. 一个人在听别人谈自己的感受和需要时, 将会留意其中是否包含着批评和嘲讽.\n如果我们的语气很肯定, 仿佛在宣布他们的内心世界, 那么, 通常不会有好的反应.\n4 区分观察和评论 不区分观察和评论, 人们将倾向于听到批评.\n社会心理学中有一条阿伦森第一定律:人们在解释令人讨厌的行为时,倾向于给作恶者贴上标签,由此而将这个人从\u0026quot;我们这些好人\u0026quot;中排除。因此,我们需要的是描述观察而不是对他人的行为进行评论,从以下的例子中可以看出观察与评论的差别:\n使用的语言没有体现出评论人对其评论负有责任: 评论:你太大方了。观察:当我看到你把吃饭的钱都给了别人,我认为你太大方了。\n把对他人思想、情感或愿望对推测当作唯一的可能: 评论:她无法完成工作。观察:我不认为她能完成工作。\n把预测当作事实: 评论:如果你饮食不均衡,你的健康就会出问题。观察:如果你饮食不均衡,我就会担心你的健康会出问题。\n缺乏依据: 评论:米奇花钱大手大脚。 观察:米奇上周买书花了一千元。\n评价他人时,把评论当作事实: 评论:欧文是个差劲的前锋观察:在过去五场比赛中,欧文没有进一个球\n使用形容词和副词时,把评论当作事实: 评论:索菲长得很丑观察:索菲对我没有什么吸引力\n不带评论的观察是人类智力的最高形式.\n同理, 需要区分事实与观点.\n5 体会和表达感受 示弱有助于解决冲突.\n区分感受和自我评论.\n听到不中听的话的四种选择:\n责备自己 指责他人 体会自己的感受和需要 体会他人的感受和需要 表达自己的诉求和需要, 如果不表达, 他人就对你的需要一无所知.\n6 请求帮助 提出具体的请求, 你的请求越明确, 就越可能得到快速和正面的回应. 提出的要求越含糊, 就越难实现. 明确谈话的目的. 我们在说话的时, 并不知道自己想要什么. 表面上是在与人谈话, 实际上是自说自话, 进而导致我们的谈话对象不知道如何回应. 请求反馈. 如果无法确定对方是否明白, 我们可能就需要得到反馈, \u0026ldquo;我的意思清楚了吗?\u0026rdquo; 了解他人的反应. 对方此时的感受 对应此时的想法 对方是否接受我们的请求 参加集体讨论时, 说清楚我们希望得到怎样的反馈, 是至关重要的. 区分请求和命令 7 沟通常识与技巧 在一个生气的人面前, 永远不要用「不过」,「可是」,「但是」之类的词语 - 如果我们致力于满足他人及自己健康成长的需要, 那么, 即使艰难的工作也不乏乐趣. 反之, 如果我们的行为是出于义务, 职责, 恐惧, 内疚或羞愧, 那么, 即使有意思的事情也会变得枯燥无味.\n用\u0026quot;我选择做\u0026hellip;是因为我想要\u0026hellip;\u0026ldquo;来代替 \u0026ldquo;不得不\u0026rdquo;\n生气时, 用\u0026quot;我生气是因为我需要..\u0026ldquo;来取代\u0026quot;我生气是因为他们\u0026hellip;\u0026rdquo;\n表达愤怒的四个步骤是:\n停下来,除了呼吸,什么都别做 想一想是什么想法使我们生气了 体会自己的需要 表达感受和尚未满足的需要 表达感激的方式:\n对方做了什么事情使我们的生活得到了改善 我们有哪些需要得到了满足 我们的心情是怎样的. 容易引起纷争的沟通方式\n对他人作出价值观与道德评判, 而他人的评价实际反映了我们的需要和价值观 比较也是评判的一种形式.(你和完美男人的身材比较, 你和莫扎特12岁时的成就比较) 回避责任(有些事不得不做; 你让我伤透了心; 主动承担责任的表述: 我选择xx, 因为我想xx) 强人所难 人们越是习惯于评定是非, 他们也就越倾向于追随权威, 来获得正确和错误的标准. 一旦专注于自身的感受, 我们就不再是好奴隶和好属下.\n其实, 在我们责备和批评的背后, 间接隐含着我们的期望没有得到满足的心情.\n8 总结 我看完如来神掌的秘笈,不代表我就学会了如来神掌。看过的是信息,学到的是知识,用上的才是技能。\n人是动物,不是机器,情绪总是有的,希望我可以籍此在急躁时控制住我的情绪,提高我的表达与共情能力,更好地与人沟通。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E9%9D%9E%E6%9A%B4%E5%8A%9B%E6%B2%9F%E9%80%9A/","summary":"1 前言 非暴力沟通, 通过体会言语背后的情感, 进而体察他人的内心, 与他人建立情感上的联系. 通俗地说, 就是个心理学家教你, 应该如何说话. 2 非暴力沟","title":"非暴力沟通"},{"content":"1 前言 为什么我们对容易被人上当受骗,为什么有些人总能说服别人?\n如何对「对他人施加影响(忽悠)」, 达到让人顺从的效果?\n这本书高屋建瓴地总结了的种种伎俩,从心理学的角度来分析,总结了6种心理武器, 揭示游说高手们如何使用这些手段,让我们普通人就范。\n如果想学习如何在交际或商业活动中说服别人,这书可谓必读之作。如果不想忽悠人, 也可以学下怎么反忽悠, 不成为《卖拐》的范厨师。\n信息安全也有类似的概念,认为人是系统的脆弱之处。 因此可以针对「人」来实施「入侵」,即所谓的社会工程学(当然,也可以通俗地理解成忽悠)\n2 心理学原理 2.1 对比原理 基于先前所发生事件的性质,相同的东西会显得极为不同, 要是第二样东西跟第一样东西有着相当的不同,那么,我们往往会认为两者的区别比实际上更大。 这样一来,如果我们先搬一种轻的东西,再拿一件重的东西,我们会觉得第二件东西比实际上更沉;而要是我们一开始直接就搬这件重东西,反倒不会觉得有这么沉\n先买了昂贵的东西, 再拿出便宜的东西, 就觉得会更便宜, 更有购买的欲望.\n聚会时,要是我们先跟一个非常帅气的人聊天, 接着插进来一个相貌平平的家伙, 我们会觉得第二个人简直没劲透了-而他其实没有那么索然寡味啦.\n2.2 光环效应 一个人的正面特征就能主导他人看待此人的眼光.(而颜值魅力基本就是这样的特征)\n我们会自动给长得好看的人添加一些正面特点, 比如有才华, 善良, 诚实和聪明等. 而且我们在作出这些判断的时候并没有意识到颜值在其中发挥的作用.\n2.3 自动反应 很多时候,我们在对某人或某事做判断的时候,并没有用上所有可用的相关信息。\n相反,我们只用到了所有信息里最具代表性的一条(颜值, 身材, 衣着等等最外显的特征)\n3 六种武器 3.1 互惠 3.1.1 原理 有债必还:\n要是人家给了我们什么好处,我们应当尽量回报. 正是因为有了互惠体系,人类才成为人类, 由于我们的祖先学会了在「有债必还的信誉网」里分享食物和技巧,我们才变成了人(没有学会分享的人,大概率在进化中被淘汰了)\n强加恩惠\n其他人,不管有多奇怪、讨厌、不受欢迎,只要先给我们点小恩小惠,就能提高我们照着其要求做的概率.\n一个人靠着硬塞给我们一些好处,就能触发我们的亏欠感\n毕竟不能拿了好处不干事。\n不对等交换\n原理要求,某一种行为需要以与其类似的行为加以回报。\n人家施恩于你,你必以恩情报之,不理不睬是不行的,以怨报德更加不可以。\n但这里面也有着很大的灵活性,别人最初给予的小小恩惠,能够让当事人产生亏欠感,最终回报以大得多的恩惠\n为什么最初的小小善意往往刺激人们回报以大得多的恩惠?原因在于\n亏欠感让人觉得很不舒服 违背互惠原理,接受而不试图回报他人善举的人,是不受社会群体欢迎的 互惠式让步\n我们已经看到,这一规则造成的后果之一是,面对接受的善意,我们感到有义务要偿还;而这一规则带来的另一后果则是,倘若有人对我们让了步,我们便觉得有义务也退让一步\n互惠原理通过两条途径来实现相互让步。 头一条很明显:它迫使接受了对方让步的人以同样的方式回应; 第二条尽管不那么明显,但更为关键:由于接受了让步的人有回报的义务,人们就乐意率先让步,从而启动有益的交换过程\n鲁讯先生说 \u0026ldquo;中国人的性情总是喜欢调和、折中的,譬如你说,这屋子太暗,说在这里开一个天窗,大家一定是不允许的。但如果你主张拆掉屋顶,他们就会来调和,愿意开天窗了\u0026rdquo;\n可能不只是因为中国人温和, 而是互惠原理在起作用.\n3.1.2 套路 \u0026ldquo;先予后取\u0026rdquo;: 先强给路人送礼物, 即使他们不喜欢礼物, 然后再募捐。 \u0026ldquo;赠送免费样品\u0026rdquo;: 向潜在客户送上少量的相关产品,看看他们是否喜欢; 实际是作为一份礼物, 暗中却把礼物天然具备的亏欠感给释放了出来。 \u0026ldquo;拒绝-后撤\u0026rdquo;(中国人所说的,以退为进): 先提一个稍过分的要求, 被拒绝后, 再提出原本的要求.(看过《亮剑》电视剧的朋友可能会发现,李云龙对上级提要求时,经常使这招) 3.1.3 如何拒绝 以直报直: 倘若别人的提议我们确实赞同,那就不妨接受它;倘若这一提议别有所图,那我们就置之不理. 真诚的礼物是礼物, 需要回报; 有目的的礼物只是销售手法, 不是礼物, 可以礼貌道谢后, 把对方送出门. 3.2 承诺和一致 3.2.1 原理 人人都有一种言行一致(同时也显得言行一致)的愿望\n一旦我们作出了一个选择,或采取了某种立场,我们立刻就会碰到来自内心和外部的压力,迫使我们按照承诺说的那样去做。 在这样的压力之下,我们会想方设法地以行动证明自己先前的决定是正确的\n言出必行\n依照人们的普遍感觉,言行不一是一种不可取的人格特征。 信仰、言语和行为前后不一的人,会被看成是脑筋混乱、表里不一,甚至精神有毛病的。\n另一方面,言行高度一致大多跟个性坚强、智力出众挂钩,它是逻辑性、稳定性和诚实感的核心\n因此: 一开始就拒绝,比最后反悔要容易\n欺骗自己\n显然, 一旦作出艰难的选择, 人就很乐意相信自己选对.\n事实上,我们所有人都会一次次地欺骗自己,以便在作出选择之后,坚信自己做得没错\n公开承诺\n公开承诺往往具有持久的效力, 每当一个人当众选择了一种立场,他便会产生维持它的动机,因为这样才能显得前后一致\n额外的努力\n为一个承诺付出的努力越多,它对承诺者的影响也就越大. 费尽周折才得到某样东西的人,比轻轻轻松就得到的人,对这件东西往往更为珍视\n他人的感观\n周围的人认为我们什么样,对我们的自我认知起着十分重要的决定作用.\n一旦主动作出了承诺,自我形象就要承受来自内外两方面的一致性压力。 一方面,是人们内心里有压力要把自我形象调整得与行为一致;另一方面,外部还存在一种更为鬼祟的压力,人们会按照他人对自己的感知来调整形象\n内心的抉择\n只有当我们认为外界不存在强大的压力时,我们才会为自己的行为发自内心地负起责任.\n此认识在教育孩子上有重要意义: 对于我们希望孩子真心相信的事情,绝不能靠贿赂或威胁让他们去做,贿赂和威胁的压力只会让孩子暂时顺从我们的愿望\n3.2.2 套路 \u0026ldquo;挖坑\u0026rdquo;: 先叫你作出承诺(也即选择立场,公开表明观点), 然后再提出与你承诺相关的要求 \u0026ldquo;登门槛\u0026rdquo;: 以小请求开始, 积小成大, 最终要人答应更大请求. (国人常见的话术,「来了都来了」,「大过年的」,「他还是个孩子」) \u0026ldquo;抛低球\u0026rdquo;: 先给人一个甜头,诱使人作出有利的购买决定. 而后, 等决定作好了, 交易却还没最终拍板, 卖方巧妙地取消了最初的甜头, 交易敲定之后,买方又不好反悔。 \u0026ldquo;恭維\u0026rdquo;: 先夸别人大方, 乐善好施; 然后过段时间再去募捐. 挖坑示例:\n电话募捐时, 先问问你的近况和身体. 当你客套回复, 还不错的时候, 表明事事顺利,募捐员逼你资助那些过得不咋样的人.\n人要是刚刚才说了自己感觉挺好或者过得不错,哪怕这么说不过是出于社交时的客套,马上就作出一副小气样会显得很尴尬\n3.2.3 如何拒绝 紧随内心: 死脑筋地保持一致愚不可及, 尽管保持一致一般而言是好的,甚至十分关键,我们也必须避免愚蠢的死脑筋. 明牌: 只需要一语道破他们在利用承诺和一致原理, 承认自身有不足, 就可以光明正大地拒绝 3.3 社会认同 3.3.1 原理 在判断何为正确时, 我们会根据别人的意见行事, 而我们对社会认同的反应方式完全是无意识的, 条件反射式. 要想把人说服, 我们提供任何证据的效果都比不上别人的行动.\n多元无知效应\n现场有大量其他旁观者在场时, 旁观者对紧急情况伸出援手的可能性最低. 原因:\n周围有其他可以帮忙的人, 单个人要承担的责任就减少 很多时候, 紧急情况乍看起来并不会显得十分紧急. 每个人都得出判断:既然没人在乎,那就应该没什么问题 多元无知效应似乎在陌生人里显得最为突出:\n因为我们喜欢在公众面前表现得优雅又成熟,又因为我们不熟悉陌生人的反应,所以,置身一群素不相识的人里面,我们有可能无法流露出关切的表情,也无法正确地解读他人关切的表情\n有样学样\n我们会根据他人的行为来判断自己怎么做才合适,尤其是在我们觉得这些人跟自己相似的时候。\n3.3.2 套路 \u0026ldquo;罐头笑声\u0026rdquo;:电视台播放情景喜剧时, 在\u0026quot;观众应该笑\u0026quot;的地方插入笑声录音。 \u0026ldquo;托儿\u0026rdquo;: 在捐款_表演, 安排几个托儿, 到特定时间时, 这些托儿就上台捐款_热烈鼓掌或者热泪盈眶 \u0026ldquo;模仿教育\u0026rdquo;: 想让小朋友学习某个技能/习惯某样焦虑的物品, 给他看同龄人是怎么做的.(内卷教育) 3.3.3 如何拒绝 学习识别: 面对明显是伪造的社认同会证据,我们只要多保持一点警惕感,就能很好地保护自己了 3.4 喜好 3.4.1 原理 我们大多数人总是更容易答应自己认识和喜欢的人所提出的要求.\n3.4.2 喜欢别人的理由 外表魅力: 人人都喜欢看得好看的人, 颜值高有令人低估的巨大优势; 结合光环效应, 颜值高的人犯罪在概率上会比颜值低的人少判几年 相似性: 我们喜欢与自己相似的人, 不管相似之处是在观点, 个性, 背景还是生活方式上. 恭維: 千穿万穿, 马屁不穿 接触与合作: 熟悉会影响人的喜好 条件反射与关联: 糟糕的消息会让报信人也染上不祥, 人总是自然而然地讨厌带来坏消息的人, 哪怕报信人跟坏消息一点关系也没有. 3.4.3 套路 \u0026ldquo;代言\u0026rdquo;: 汽车广告里总站着一堆漂亮的女模特?广告商希望她们把自己积极的特性-漂亮, 性感投射到汽车身上; 运动员代言, 明星代言, 奥运会赞助, 为了让观众把自己的产品跟当前的文化热潮关联起来, 把产品和运动员关联起来.(条件反射与关联) \u0026ldquo;午宴术\u0026rdquo;: 就餐期间接触到的人或事物更为喜爱, 把接触到的事物和美好关联起来.(条件反射与关联) \u0026ldquo;粉丝效应\u0026rdquo;: 将自身与偶像或球队关联起来, 展示积极的联系,隐藏消极的联系,努力让旁观者觉得我们更高大,更值得喜欢.(条件与反射关联) 粉丝效应补充:\n倘若我们觉得自己看起来不怎样,那么我们就很有可能使用这一效应。\n每当我们的公众形象受损,我们就会产生强烈的欲望,宣扬自己跟其他成功者的关系,借此恢复自身形象。 同时,我们还会小心避免暴露自己与失败者之间的关系\n但在我们以个人成就为傲的时候,我们不会沾别人的光。 只有当我们在公在私的威望都很低的时候,我们才会想借助他人成功来恢复自我形象\n对于自我意识太差的人, 他们内心深处的个人价值感过低,没办法靠推动或实现自身成就来追求荣誉,只能靠着吹嘘自己与他人成就的关系来找回尊严,\n他们的成就并不来自本身\n光环效应 + 一致性原理 + 粉丝效应:\n基于光环效应, 偶像的某个[显著的]局部特征(如颜值)的看法被盲目扩大化, 变成对此人整体的看法. 又因为一致性原理的存在(当你正面评价某对象(包括人或事物)时,你在潜意识里会排斥该对象的负面评价;反之亦然;)\n当有人提出偶像的不足时, 即所谓的黑点的时候, 粉丝很自然地愤怒起来, 因为影响了偶像在他们心目中的形象, 自然地与提出问题的人对线起来, 进而涉及到对应的偶像, 无可避免地battle起来;\n最后因为粉丝效应的存在, 很自然把偶像的成功当作自己的成功, 竭力维护偶像, 做出什么事情也都不奇怪了.\n3.4.4 如何拒绝 反思我们是不是觉得自己超乎寻常地迅速、热烈地喜欢上了对方 区分请求人和请求本身 3.5 权威 3.5.1 原理 我们从小被教育服从权威, 而服从权威, 总是能给我们带来一些实际的好处. 部分是因为权威(老师, 家长)更有智慧, 部分是因为权威(老板, 法官)手里攥着对我们的奖惩.\n《圣经旧约》用充满恭敬的行文讲述了上帝权威的故事:只因上帝有了吩咐(哪怕没有半点解释),亚伯拉罕就愿意把利剑插入自己小儿子的心脏。 通过这个故事,我们知道判断一个行为正确与否,跟它有没有意义、有没有危害、公不公正、符不符合通常的道德标准没有关系,只要它来自更高权威的命令,那就是对的。\n3.5.2 象征权威的符号 在没有真正权威的情况下, 象征权威的符号也能十分有效地触发我们的顺从态度. 象征权威的符号:\n头衔: 头衔是最难也最容易得到的权威象征 衣着: 人靠衣装, 佛靠金装 身份标志(珠宝, 豪车): 珠宝, 豪车承载着地位和身份的光环 3.5.3 套路 \u0026ldquo;假扮权威\u0026rdquo;: 广告商利用我们对医生的尊重, 找演员假扮医生, 宣传他们的产品(利用了权威原理带来的影响力,却根本不曾拿出一个真正的权威,光是看起来像权威就足够了) \u0026ldquo;违反自身利益来赢取信任\u0026rdquo;: 销售员为客户争取低价, 与老板吵得不可开交(让客户以为销售员站在己方, 实际争取来的是销售员的心理价位); 还有就是审讯时的红脸和黑脸. 3.5.4 如何拒绝 提高对权威力量的警惕性:\n权威的资格(如, 这个是否是真正的医生) 权威的资格是否跟眼前的主题相关(如, 是真的医生和买这个商品有没有联系) 事出反常则必有妖:\n思考: 陌生人是否真的愿意为了我们, 牺牲个人利益.\n3.6 稀缺 3.6.1 原理: 机会越少见, 价值似乎越高; 对失去某种东西的恐惧,似乎要比对获得同一物品的渴望,更能激发人们的行动力.\n逆反心理:\n机会越来越少的话,我们的自由也会随之丧失。而我们又痛恨失去本来拥有的自由.\n保住既得利益的愿望, 是心理逆反理论的核心.\n每当有东西获取起来比以前难, 我们拥有它的自由受了限制, 我们就越发地想要得到它.\n(罗密欧·蒙特鸠与朱丽叶·凯普莱特是莎士比亚笔下的悲剧人物,两人相爱,两个家族却是世仇。 为了反抗父母拆散他们的企图,他们双双自杀殉情,用这种最极端的悲剧方式来声张自由意志。如果父母不反对, 听凭这对青年男女自由恋爱,他们的浓情蜜意说不定只是初恋时短暂的冲动罢了)\n2022年,哈萨克斯坦一则呼吁年轻人积极参加总统选举投票和其他各类政治议题的广告《他们说:别来投票》,广告内容就是一桌子政客劝年轻人不要来投票,这个国家的未来由他们决定就好。通过逆反心理来劝年轻人来参加投票。\n从充裕到稀缺\n较之一贯短缺, 从充裕变成短缺的物品, 人们的反应更应积极(自由这种东西, 给一点又拿走, 比完全不给更危险)\n而因社会需求而变成稀缺, 会让人变成更加渴望(参与竞争稀缺资源的感觉,有着强大的刺激性; 渴望拥有一件众人争抢的东西,几乎是出于本能的身体反应)\n3.6.2 套路 \u0026ldquo;数量有限\u0026rdquo;:告诉顾客,某种商品供不应求,不见得随时都有 \u0026ldquo;最后期限\u0026rdquo;: 告诉顾客, 这是获得产品的最后机会, 过期不候 \u0026ldquo;通过封杀来传播\u0026rdquo;: 基于逆反心理, 越被限制的信息, 人们越有兴趣知道.(比如,电影海报说,仅限18岁以上观众,电影票会更易用售光) \u0026ldquo;独家信息\u0026rdquo;: 要是我们觉得没法从别处获取某条信息, 我们就会认为它更具说服力 \u0026ldquo;制造竞争\u0026rdquo;: 只有一样商品, 但是卖家找来了多个买家 3.6.3 如何拒绝 一旦在顺从环境下体验到高涨的情绪,我们就可以提醒自己:说不定有人在玩弄稀缺手法,必须谨慎行事。 喜悦并非来自对稀缺商品的体验,而来自对它的占有. 反思, 我们是真的需要某样商品, 还是单纯想要占有它. 4 总结 总结了这么多招式,不知道诸位不看「菜谱」看「兵法」的「范厨师」学会没有,又是否能运用自如呢?\n所谓「忽悠人之心不可有,防忽悠人之心不可无」,多读几本心理学的书,可以让家里少几件贵价且无用的商品,何乐而不为呢。\n觉得意犹未尽的,推荐去看下社会心理学名作《社会性动物》,进阶之作。\n或者可以看下「范厨师」被忽悠的经典小品:《卖拐》\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%BD%B1%E5%93%8D%E5%8A%9B/","summary":"1 前言 为什么我们对容易被人上当受骗,为什么有些人总能说服别人? 如何对「对他人施加影响(忽悠)」, 达到让人顺从的效果? 这本书高屋建瓴地总结了的","title":"影响力"},{"content":"1 前言 如果生活没有泪水, 欢乐还有意义么?\n2 新世界 在这个未来的美丽新世界\n不再有家庭, 不再有父亲 母亲, 这两个词甚至成为极端下流的名词,\n不再有胎生, 不再有家境的差异, 所有人都在瓶子中诞生, 被预设了各种条件, 各种限制;\n不再有异议, 在睡眠中就被灌输了各种关于集体, 社交, 性, 幸福的观念. \u0026ldquo;人人彼此相属\u0026rdquo;, 类似这样的话语, 在你还是孩童时期, 每晚都会在你耳边重复;\n不再会有宗教, 科学, 文学, 音乐, 艺术, 鲜花和大自然, 一切都被限定接触;\n不再会有婚姻, 不再需要对某人忠贞, 连只交一个男朋友都会成为异类, 滥交会成为一种常态;\n不再会有衰老和病痛, 人人都如年青人一般, 只会在某天突然死亡;\n不再有痛苦和烦恼, 连毒品都被全社会推崇, 一片解千愁.\n不再有阶级和人与人的差别, 你的种姓等级, 身高, 样貌, 智力都在培养瓶中被预设好了.\n这样的新世界, 你期待么?\n就如苏格拉底的疑问一样: 快乐的猪和痛苦的人, 你想成为哪个?\n3 反乌托邦 作为反乌托邦的三部曲之一, 《美丽新世界》总是会被拿来和另外一部伟大作品《1984》作比较, 但这是两部完全不同的作品.\n我在看《1984》时候, 感觉是深深的恐惧, 是斯大林式和希特勒式统治方式, 是中国人熟悉的统治方式.\n付诸于谎言和恐惧来支配人民, 但是在恐惧背后, 还是有残存的希望的, 大家还是能意识到老大哥的存在的, 也怀抱着「我们终将在没有黑暗的地方相见」的信念.\n但是看《美丽新世界》, 只有无尽的绝望; 非暴力控制, 基因改造, 无处不在的服从性训练;\n人已非人, 不在有思考的权利和能力, 已经无力对此一切作出反抗.\n4 你所追求之事 美丽新世界不是把人类想要的一切都为人类奉上了么? 只是作为等价交换, 人类献祭了自己作为的灵魂. 如果这样的东西不是你想要的? 你想要的究竟是什么呢?\n我想要回自由, 我想要回泪水, 我想要回痛苦, 我想要回上帝, 即使我不相信上帝, 我想要回诗歌, 我想要回真正的危险, 最重要的是我想要回选择的权利, 我想重新成为人.\n「要是每一次暴风雨之后, 都有这样和煦的阳光, 那么尽管让狂风肆意地吹, 把死亡都吹醒了吧」, 没有暴风雨, 只有阳光的日子, 有何意义呢?\n5 写到最后 「你的1984终将过去,我的美丽新世界总会到来」 \u0026ndash; 赫胥黎\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E7%BE%8E%E4%B8%BD%E6%96%B0%E4%B8%96%E7%95%8C/","summary":"1 前言 如果生活没有泪水, 欢乐还有意义么? 2 新世界 在这个未来的美丽新世界 不再有家庭, 不再有父亲 母亲, 这两个词甚至成为极端下流的名词, 不再有胎生","title":"美丽新世界"},{"content":"1 前言 在肺炎肆虐的2020年, 阅读 这本书想来会别有一番滋味, 因为会有一种身临其境的奇妙之感, 这也是我在2020年结束前想要阅读这本书的初衷.\n\u0026lt;2022-02-26 六\u0026gt;\n没想到,疫情肆虐了两年多,仍未散去如鼠疫那般散去,还越演越烈。\n2 故事的展开 先来介绍下故事中的主要人物:\n里厄大夫: 抗争疫病的主心骨, 为人坚定果敢, 沉稳可靠. 塔鲁: 疫城奥兰城的短暂居客, 体魄健壮, 为人宽厚, 组建志愿者队伍救助患者, 里厄大夫的得力助手 朗贝尔:巴黎一家大报馆的年轻记者, 因出差奥兰而滞留疫城. 疫情开始时把自己视同局外人, 千方百计想要脱身回巴黎与恋人团聚, 后受到里厄大夫和塔鲁等人的精神感召, 加入了志愿队 帕纳卢:神父, 擅长讲道, 声音洪亮, 充满激情; 后加入志愿队 科塔尔:边缘人物,因有案底随时可能被捕而过度紧张, 曾上吊自杀被救; 鼠疫爆发封城后他如鱼得水,因走私而阔绰,还想帮助朗贝尔私自出城 北非小城奥兰, 在毫无症兆的情况, 大批老鼠呕血而亡, 而后鼠疫尾随而来.\n开始, 政府对此是否真正出现鼠疫表示疑虑, 然后在里厄大夫等少数医生的力证下, 政府才真正承认出现了鼠疫, 而后封关断航, 封锁疫城, 为避免鼠疫通过书信传播, 书信也被迫中断, 与世隔绝疫城内外的人们联系的唯一途径, 仅剩余电报的廖廖数语.\n看来,政府都大差不差.\n疫情开始后, 居民以为鼠疫不过个过路的不速之客, 示威一番之后便是离去, 自然是舞照跳, 歌照唱.\n而后鼠疫这个瘟神开始露出獠牙, 肆意开始吞噬着一条条鮮活的生命. 在死神滴血的鐮刀面前, 人们开始惊恐, 在宗教面前, 在各种主面前寻求救赎; 也有自认为局外人的人群试图寻求各种门路逃离此座疫城, 离开本与\u0026quot;他们无关\u0026quot;的瘟神.\n然而死神的意志又岂会因凡人的乞求而转换呢, 患病人数日渐增多, 接纳病人的场所从医院扩展到改建的学校, 里厄大夫及他的同事, 即使竭尽全力, 也没有丝毫减缓死神收割病人的速度, 患病的人还在不断增加; 而面对患病的亲人, 人们从惊恐, 震惊, 不舍到默然接受;\n在前方一片黑暗的情况, 塔鲁组建志愿队, 自愿协助医生救助病人, 坚持做应该做的事情, 承担失去生命的风险, 甚至不知道此举是否会对局势产生一丝改变, 后来朗贝尔和帕纲卢神父也在里厄大夫和塔鲁的感召下, 加入了志愿队救助患者.\n就这样, 从初春到深秋, 鼠疫一直笼罩在奥兰海滨小城上空, 这个鼠疫的瘟神, 他像撒旦那样漂亮, 像疫病本身那样闪光, 就停在人们的屋顶上方,右手执红色猎矛,抬起有他的头那么高,左手指着哪家的房舍.\n其兴也勃焉, 其亡也忽焉. 鼠疫的离去, 就像他的到来一样毫无预兆, 就这样, 在肆虐近一年后, 鼠疫就节节衰退,而后一蹶不振, 在鼠疫就此离去前, 在病魔似乎受严寒、灯火和人群的驱赶, 逃出本城黑暗幽深的洞穴之时, 却向为疫病耗尽力量的塔鲁的身躯发起最后攻击, 并就此夺走了他的生命.\n这是最后一次失败,而这次失败终结了战争,将和平本身变成一种永难治愈的伤痛\n在塔鲁染病离去后, 紧随而来的是里厄医生在外治疗的妻子病逝的消息.\n3 苦难是一面照妖镜 经典的作品, 都是能反馈人性和人生的作品, 毕竟时代在变, 人也在变, 但有些东西不会随时代洪流而有多少变化, 人性如此, 对待人生的态度亦如此.\n关于人性, 虽然书名是, 故事写的也是鼠疫, 但鼠疫不过是一个舞台, 一面镜子, 照出疫病肆虐下的形形色色的人性, 照出那些隐藏在笑谈下的真面目.\n关于人生, 加缪借书中主角的经历, 给出了三种不同的态度:\n第一种, 颓废, 堕落, 无所事事, 像科塔尔那样选择自杀来逃避一切, 最后在疫情结束后陷入疯狂;\n第二种, 笃信宗教, 相信人类是有罪的, 应当受到上帝的惩罚, 却因为目睹法官的幼子临终前痛苦不堪的样子, 而对人类本身有罪, 需要上帝救赎的信条产生怀疑, 而后怀疑信仰, 最后疑似感染鼠疫而亡;\n第三种, 即是与命运奋战到底, 一次次被打倒, 又一次次重新站起来, 直至像塔鲁那样子与鼠疫斗争到最后一口气, 获得寻觅已久的安宁.\n你自己, 在这个世界上没有任何意义, 鼠疫中的所有角色死亡后都对这个世界没有任何影响, 但是你可以选择活得精彩,\n这真是个残酷又真实的哲理, 又像极了人生.\n4 神父的讲道 神父有雄辩之名, 里厄也评价神父: \u0026ldquo;他讲道好, 做得更好\u0026rdquo;, 而神父第一次讲道, 关于人类本身是有罪的讲道, 当时看得我心潮澎湃, 只看文字都觉得自己罪孽深重,\n也难怪在现场看live的听众会在神父面前全部下跪忏悔, 来称赏下加缪的文笔:\n帕纳卢神父中等身材,但是很敦实。他两只大手抓住木栏,俯依在讲坛前沿,只能看到他那厚实的黑色形体,顶着满面红光的脸颊,戴着一副钢丝边眼镜。他的嗓音洪亮,充满激情,能传出去很远,一上来就抛出一句激烈的话,铿锵有力地抨击全体听众:\u0026ldquo;弟兄们,你们在受苦受难。弟兄们,你们这是咎由自取。\u0026ldquo;全场一阵骚动,一直波及广场上的人。\n他接下来说的话,从逻辑上看,似乎同他这句悲愤的开场白并无紧密关系。可是他的演说越往下听,我们的同胞才越明白,神父演说的方法巧妙,仿佛猛然一击,和盘托出他这场讲道的主题。\n果然,帕纳卢抛出了这句话,紧接着就引述《出埃及记》中有关埃及发生鼠疫的段落,并且说道:\u0026ldquo;这种灾难在历史上头一次出现,就是要打击上帝的敌人。\n法老违抗天意,于是鼠疫就迫使他屈膝。有史以来,上帝降以灾难,让那些狂妄者和盲目者都匍匐在他的脚下。\u0026rdquo;\n外面的雨更狂了,在急雨噼啪敲窗的声音而突显的绝对肃静中,神父讲出最后这句话,声音极其响亮,有几名听众略微犹豫一下,便不由自主地滑下座椅,跪到跪凳上。\n其他一些人以为应当效仿,结果陆陆续续,不大工夫全场听众都跪下了,寂静中只听见几张椅子的吱嘎声响。这时,帕纳卢神父又挺起身子,深吸一口气,调门越来越高,继续说道:\u0026ldquo;如果说今天,鼠疫降临到你们头上,就是因为反思的时刻到了。\n义人自不必恐惧,而恶人却理应颤抖。世界好似无比巨大的麦场,灾难如同连枷,无情地击打人类这片麦子,直到麦粒脱离麦秸。麦秸要多于麦粒,被召去的人也要多于上帝的选民,而这场灾难并不是上帝的初衷。\n这个世界同邪恶妥协时间太久了,这个世界依赖上天的宽容时间也太久了。只要痛悔一下,就可以为所欲为。要表示痛悔,人人都觉得游刃有余。时候一到,肯定就会有悔恨的感觉。不过,在那之前,最简便的做法就是放任自流,余下的事就交由仁慈的上帝去处理了。要知道,这种状况不能持续下去了。上帝那张慈悲的面孔,太久太久俯视这座城市的居民,等得厌倦了,他那永恒的希望化为失望,已经移开了目光。\n我们失去了上帝的光明,就这样长期陷入鼠疫的黑暗啦!\u0026rdquo;\n大堂里有人像急躁的马那样,打了一声鼻息。\n神父停顿了一下,放低声调接着说道:\u0026quot;《圣徒传》[插图]上能看到这样一段话:在亨伯特国王[插图]统治伦巴第[插图]的时期,意大利遭受鼠疫的大浩劫,幸免于难者少得可怜,仅仅够埋葬死者了。\n鼠疫肆虐最凶的地方,当属罗马和帕维亚。一个善良的天使显形了,他命令恶神手持狩猎的长矛,去敲击各家各户,每家挨几下敲击,就要抬出多少死人。\u0026rdquo;\n帕纳卢说到此处,伸出两只短粗的手臂,指着教堂前广场的方向,仿佛让人透过摇曳的雨幕看什么东西,他用力朗声说道:\u0026ldquo;弟兄们,如今在我们街道上奔跑的,是同样的死亡的追猎。你们瞧啊,这个鼠疫的瘟神,他像撒旦那样漂亮,像疫病本身那样闪光,就停在你们的屋顶上方,右手执红色猎矛,抬起有他的头那么高,左手指着你们哪家的房舍。\n此时此刻,他的手指也许正指向您家的房门,长矛击打着房门的木板;此时此刻,鼠疫瘟神走进您的家,坐到您的房间里,等待您回去。瘟神守在那里,耐心等待,十分专注,就像人世的秩序那样胸有成竹。\n他那只手要朝你们伸去,世间任何力量,即使人类的科学,你们要记清,即使人类的科学也无济于事,无法使你们免遭打击。你们将在血淋淋的痛苦的打麦场上,被打得血肉横飞,最终连同麦秸一起被抛弃。\u0026rdquo;\n神父讲到此处,越发展现这场灾难的悲惨景象。他又提起那根在城池上空盘旋的长矛,随意打击,落下又起来时血淋淋的,总之将鲜血和痛苦散布开来,\u0026ldquo;以便播种,准备收获真理\u0026rdquo;。\n这一和谐复合长句讲完之后,帕纳卢神父停了一下,他的头发披散在前额上,浑身颤抖,而双手又将这颤动传给讲台。\n接着,他的声音低沉下来,但以责备的口吻说道:\u0026ldquo;是的,反思的时刻到了。你们原以为,只要星期天来拜拜天主就够了,其余的日子就可以任性妄为了。你们还曾想,随便跪拜跪拜,就足以救赎你们罪恶的放肆行为。\n然而,上帝可不是这样不冷不热的。这种若即若离的关系,不足以赢得上帝的无限慈爱。他希望看到你们的时间更长些,这才是他爱你们的方式,老实说,这也是唯一爱的方式。这就是为什么,上帝等你们不来,实在厌倦了,就让灾难来光顾你们,正如有史以来,灾难光顾了所有罪恶深重的城市那样。\n现在你们懂得了什么是罪孽,正如古代该隐[插图]及其儿子们、大洪水之前的人们、所多玛和蛾摩拉[插图]两城的居民、法老和约伯,以及所有受到天谴的人,无不懂得了什么是罪孽。自从封城的那一天起,你们就跟灾难一起被关在城墙之内,你们也就跟所有上述那些人一样,换了一副新眼光看待人和事物了。\n现在,你们终于懂得了,必须归到根本上来。\u0026rdquo;\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E9%BC%A0%E7%96%AB/","summary":"1 前言 在肺炎肆虐的2020年, 阅读 这本书想来会别有一番滋味, 因为会有一种身临其境的奇妙之感, 这也是我在2020年结束前想要阅读这本书的初衷.","title":"鼠疫"},{"content":"The lesson learned from refactoring rspotify\n1 Preface Recently, I and Mario are working on refactoring rspotify, trying to improve performance, documentation, error-handling, data model and reduce compile time, to make it easier to use. (For those who has never heard about rspotify, it is a Spotify HTTP SDK implemented in Rust).\nI am partly focusing on polishing the data model, based on the issue created by Koxiaet.\nSince rspotify is API client for Spotify, it has to handle the request and response from Spotify HTTP API.\nGenerally speaking, the data model is something about how to structure the response data, and used Serde to parse JSON response from HTTP API to Rust struct, and I have learnt a lot Serde tricks from refactoring.\n2 Serde Lesson 2.1 Deserialize JSON map to Vec based on its value. An actions object which contains a disallows object, allows to update the user interface based on which playback actions are available within the current context.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;disallows\u0026#34;: { \u0026#34;resuming\u0026#34;: true } ... } The original model representing actions was:\n#[derive(Clone, Debug, Serialize, PartialEq, Eq)] pub struct Actions { pub disallows: HashMap\u0026lt;DisallowKey, bool\u0026gt; } #[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, Hash, ToString)] #[serde(rename_all = \u0026#34;snake_case\u0026#34;)] #[strum(serialize_all = \u0026#34;snake_case\u0026#34;)] pub enum DisallowKey { InterruptingPlayback, Pausing, Resuming, ... } And Koxiaet gave great advice about how to polish Actions:\nActions::disallows can be replaced with a Vec\u0026lt;DisallowKey\u0026gt; or HashSet\u0026lt;DisallowKey\u0026gt; by removing all entires whose value is false, which will result in a simpler API.\nTo be honest, I was not that familiar with Serde before, after digging in its official documentation for a while, it seems there is now a built-in way to convert JSON map to Vec\u0026lt;T\u0026gt; base on map\u0026rsquo;s value.\nAfter reading the Custom serialization from documentation, there was a simple solution came to my mind, so I wrote my first customized deserialize function.\nI created a dumb Actions struct inside the deserialize function, and converted HashMap to Vec by filtering its value.\n#[derive(Clone, Debug, Serialize, PartialEq, Eq)] pub struct Actions { pub disallows: Vec\u0026lt;DisallowKey\u0026gt;, } impl\u0026lt;\u0026#39;de\u0026gt; Deserialize\u0026lt;\u0026#39;de\u0026gt; for Actions { fn deserialize\u0026lt;D\u0026gt;(deserializer: D) -\u0026gt; Result\u0026lt;Self, D::Error\u0026gt; where D: Deserializer\u0026lt;\u0026#39;de\u0026gt;, { #[derive(Deserialize)] struct OriginalActions { pub disallows: HashMap\u0026lt;DisallowKey, bool\u0026gt;, } let orignal_actions = OriginalActions::deserialize(deserializer)?; Ok(Actions { disallows: orignal_actions .disallows .into_iter() .filter(|(_, value)| *value) .map(|(key, _)| key) .collect(), }) } } The types should be familiar if you\u0026rsquo;ve used Serde before.\nIf you\u0026rsquo;re not used to Rust then the function signature will likely look a little strange. What it\u0026rsquo;s trying to tell is that d will be something that implements Serde\u0026rsquo;s Deserializer trait, and that any references to memory will live for the 'de lifetime.\n2.2 Deserialize Unix milliseconds timestamp to Datetime A currently playing object which contains information about currently playing item, and the timestamp field is an integer, representing the Unix millisecond timestamp when data was fetched.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;timestamp\u0026#34;: 1490252122574, \u0026#34;progress_ms\u0026#34;: 44272, \u0026#34;is_playing\u0026#34;: true, \u0026#34;currently_playing_type\u0026#34;: \u0026#34;track\u0026#34;, \u0026#34;actions\u0026#34;: { \u0026#34;disallows\u0026#34;: { \u0026#34;resuming\u0026#34;: true } } ... } The original model was:\n/// Currently playing object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/player/get-the-users-currently-playing-track/) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CurrentlyPlayingContext { pub timestamp: u64, pub progress_ms: Option\u0026lt;u32\u0026gt;, pub is_playing: bool, pub item: Option\u0026lt;PlayingItem\u0026gt;, pub currently_playing_type: CurrentlyPlayingType, pub actions: Actions, } As before, Koxiaet made a great point about timestamp and =progress_ms=(I will talk about it later):\nCurrentlyPlayingContext::timestamp should be a chrono::DateTime\u0026lt;Utc\u0026gt;, which could be easier to use.\nThe polished struct looks like:\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CurrentlyPlayingContext { pub context: Option\u0026lt;Context\u0026gt;, #[serde( deserialize_with = \u0026#34;from_millisecond_timestamp\u0026#34;, serialize_with = \u0026#34;to_millisecond_timestamp\u0026#34; )] pub timestamp: DateTime\u0026lt;Utc\u0026gt;, pub progress_ms: Option\u0026lt;u32\u0026gt;, pub is_playing: bool, pub item: Option\u0026lt;PlayingItem\u0026gt;, pub currently_playing_type: CurrentlyPlayingType, pub actions: Actions, } Using the deserialize_with attribute tells Serde to use custom deserialization code for the timestamp field. The from_millisecond_timestamp code is:\n/// Deserialize Unix millisecond timestamp to `DateTime\u0026lt;Utc\u0026gt;` pub(in crate) fn from_millisecond_timestamp\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;DateTime\u0026lt;Utc\u0026gt;, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { d.deserialize_u64(DateTimeVisitor) } The code calls d.deserialize_u64 passing in a struct. The passed in struct implements Serde\u0026rsquo;s Visitor, and look like:\n// Vistor to help deserialize unix millisecond timestamp to `chrono::DateTime` struct DateTimeVisitor; impl\u0026lt;\u0026#39;de\u0026gt; de::Visitor\u0026lt;\u0026#39;de\u0026gt; for DateTimeVisitor { type Value = DateTime\u0026lt;Utc\u0026gt;; fn expecting(\u0026amp;self, formatter: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!( formatter, \u0026#34;an unix millisecond timestamp represents DataTime\u0026lt;UTC\u0026gt;\u0026#34; ) } fn visit_u64\u0026lt;E\u0026gt;(self, v: u64) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { ... } } The struct DateTimeVisitor doesn\u0026rsquo;t have any fields, it just a type implemented the custom visitor which delegates to parse the u64.\nSince there is no way to construct DataTime directly from Unix millisecond timestamp, I have to figure out how to handle the construction. And it turns out that there is a way to construct DateTime from seconds and nanoseconds:\nuse chrono::{DateTime, TimeZone, NaiveDateTime, Utc}; let dt = DateTime::\u0026lt;Utc\u0026gt;::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc); Thus, what I need to do is just convert millisecond to second and nanosecond:\nfn visit_u64\u0026lt;E\u0026gt;(self, v: u64) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { let second = (v - v % 1000) / 1000; let nanosecond = ((v % 1000) * 1000000) as u32; // The maximum value of i64 is large enough to hold millisecond, so it would be safe to convert it i64 let dt = DateTime::\u0026lt;Utc\u0026gt;::from_utc( NaiveDateTime::from_timestamp(second as i64, nanosecond), Utc, ); Ok(dt) } The to_millisecond_timestamp function is similar to from_millisecond_timestamp, but it\u0026rsquo;s eaiser to implement, check this PR for more detail.\n2.3 Deserialize milliseconds to Duration The simplified episode object contains the simplified episode information, and the duration_ms field is an integer, which represents the episode length in milliseconds.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;audio_preview_url\u0026#34; : \u0026#34;https://p.scdn.co/mp3-preview/83bc7f2d40e850582a4ca118b33c256358de06ff\u0026#34;, \u0026#34;description\u0026#34; : \u0026#34;Följ med Tobias Svanelid till Sveriges äldsta tegelkyrka\u0026#34; \u0026#34;duration_ms\u0026#34; : 2685023, \u0026#34;explicit\u0026#34; : false, ... } The original model was\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct SimplifiedEpisode { pub audio_preview_url: Option\u0026lt;String\u0026gt;, pub description: String, pub duration_ms: u32, ... } As before without saying, Koxiaet pointed out that\nSimplifiedEpisode::duration_ms should be replaced with a duration of type Duration, since a built-in Duration type works better than primitive type.\nSince I have worked with Serde\u0026rsquo;s custome deserialization, it\u0026rsquo;s not a hard job for me any more. I easily figure out how to deserialize u64 to Duration:\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct SimplifiedEpisode { pub audio_preview_url: Option\u0026lt;String\u0026gt;, pub description: String, #[serde( deserialize_with = \u0026#34;from_duration_ms\u0026#34;, serialize_with = \u0026#34;to_duration_ms\u0026#34;, rename = \u0026#34;duration_ms\u0026#34; )] pub duration: Duration, ... } /// Vistor to help deserialize duration represented as millisecond to `std::time::Duration` struct DurationVisitor; impl\u0026lt;\u0026#39;de\u0026gt; de::Visitor\u0026lt;\u0026#39;de\u0026gt; for DurationVisitor { type Value = Duration; fn expecting(\u0026amp;self, formatter: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!(formatter, \u0026#34;a milliseconds represents std::time::Duration\u0026#34;) } fn visit_u64\u0026lt;E\u0026gt;(self, v: u64) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { Ok(Duration::from_millis(v)) } } /// Deserialize `std::time::Duration` from millisecond(represented as u64) pub(in crate) fn from_duration_ms\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;Duration, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { d.deserialize_u64(DurationVisitor) } Now, the life is easier than before.\n2.4 Deserialize milliseconds to Option Let\u0026rsquo;s go back to CurrentlyPlayingContext model, since we have replaced millisecond (represents as u32) with Duration, it makes sense to replace all millisecond fields to Duration.\nBut hold on, it seems progress_ms field is a bit different.\nThe progress_ms field is either not present or a millisecond, the u32 handles the milliseconds, as its value might not be present in the response, it\u0026rsquo;s an Option\u0026lt;u32\u0026gt;, so it won\u0026rsquo;t work with from_duration_ms.\nThus, it\u0026rsquo;s necessary to figure out how to handle the Option type, and the answer is in the documentation, the deserialize_option function:\nHint that the Deserialize type is expecting an optional value.\nThis allows deserializers that encode an optional value as a nullable value to convert the null value into None and a regular value into Some(value).\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CurrentlyPlayingContext { pub context: Option\u0026lt;Context\u0026gt;, #[serde( deserialize_with = \u0026#34;from_millisecond_timestamp\u0026#34;, serialize_with = \u0026#34;to_millisecond_timestamp\u0026#34; )] pub timestamp: DateTime\u0026lt;Utc\u0026gt;, #[serde(default)] #[serde( deserialize_with = \u0026#34;from_option_duration_ms\u0026#34;, serialize_with = \u0026#34;to_option_duration_ms\u0026#34;, rename = \u0026#34;progress_ms\u0026#34; )] pub progress: Option\u0026lt;Duration\u0026gt;, } /// Deserialize `Option\u0026lt;std::time::Duration\u0026gt;` from millisecond(represented as u64) pub(in crate) fn from_option_duration_ms\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;Option\u0026lt;Duration\u0026gt;, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { d.deserialize_option(OptionDurationVisitor) } As before, the OptionDurationVisitor is an empty struct implemented Visitor trait, but key point is in order to work with deserialize_option, the OptionDurationVisitor has to implement the visit_none and visit_some method:\nimpl\u0026lt;\u0026#39;de\u0026gt; de::Visitor\u0026lt;\u0026#39;de\u0026gt; for OptionDurationVisitor { type Value = Option\u0026lt;Duration\u0026gt;; fn expecting(\u0026amp;self, formatter: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!( formatter, \u0026#34;a optional milliseconds represents std::time::Duration\u0026#34; ) } fn visit_none\u0026lt;E\u0026gt;(self) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { Ok(None) } fn visit_some\u0026lt;D\u0026gt;(self, deserializer: D) -\u0026gt; Result\u0026lt;Self::Value, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { Ok(Some(deserializer.deserialize_u64(DurationVisitor)?)) } } The visit_none method return Ok(None) so the progress value in the struct will be None, and the visit_some delegates the parsing logic to DurationVisitor via the deserialize_u64 call, so deserializing Some(u64) works like the u64.\n2.5 Deserialize enum from number An AudioAnalysisSection model contains a mode field, which indicates the modality(major or minor) of a track, the type of scle from which its melodic content is derived. This field will contain a 0 for minor, a 1 for major, or a -1 for no result.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;mode\u0026#34;: 0, \u0026#34;mode_confidence\u0026#34;: 0.414, ... } The original struct representing AudioAnalysisSection was like this, since mode field was stored into a f32=(=f8 was a better choice for this case):\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct AudioAnalysisSection { ... pub mode: f32, pub mode_confidence: f32, ... } Koxiaet made a great point about mode field:\nAudioAnalysisSection::mode and AudioFeatures::mode are f32=s but should be =Option\u0026lt;Mode\u0026gt;=s where =enum Mode { Major, Minor } as it is more useful.\nIn this case, we don\u0026rsquo;t need the Opiton type and in order to deserialize enum from number, we firstly need to define a C-like enum:\npub enum Modality { #[serde(rename = \u0026#34;0\u0026#34;)] Minor = 0, #[serde(rename = \u0026#34;1\u0026#34;)] Major = 1, #[serde(rename = \u0026#34;1\u0026#34;)] NoResult = -1, } pub struct AudioAnalysisSection { ... pub mode: Modality, pub mode_confidence: f32, ... } And then, what\u0026rsquo;s the next step? It seems serde doesn\u0026rsquo;t allow C-like enums to be formatted as integers rather that strings in JSON natively:\nworking version: { ... \u0026#34;mode\u0026#34;: \u0026#34;0\u0026#34;, \u0026#34;mode_confidence\u0026#34;: 0.414, ... } failed version: { ... \u0026#34;mode\u0026#34;: 0, \u0026#34;mode_confidence\u0026#34;: 0.414, ... } Then the failed version is exactly what we want. I know that the serde\u0026rsquo;s official documentation has a solution for this case, the serde_repr crate provides alternative derive macros that derive the same Serialize and Deserialize traits but delegate to the underlying representation of a C-like enum.\nSince we are trying to reduce the compiled time of rspotify, so we are cautious about introducing new dependencies. So a custom-made serialize function would be a better choice, it just needs to match the number, and convert to a related enum value.\n/// Deserialize/Serialize `Modality` to integer(0, 1, -1). pub(in crate) mod modality { use super::enums::Modality; use serde::{de, Deserialize, Serializer}; pub fn deserialize\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;Modality, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { let v = i8::deserialize(d)?; match v { 0 =\u0026gt; Ok(Modality::Minor), 1 =\u0026gt; Ok(Modality::Major), -1 =\u0026gt; Ok(Modality::NoResult), _ =\u0026gt; Err(de::Error::invalid_value( de::Unexpected::Signed(v.into()), \u0026amp;\u0026#34;valid value: 0, 1, -1\u0026#34;, )), } } pub fn serialize\u0026lt;S\u0026gt;(x: \u0026amp;Modality, s: S) -\u0026gt; Result\u0026lt;S::Ok, S::Error\u0026gt; where S: Serializer, { match x { Modality::Minor =\u0026gt; s.serialize_i8(0), Modality::Major =\u0026gt; s.serialize_i8(1), Modality::NoResult =\u0026gt; s.serialize_i8(-1), } } } 3 Move into module Update:\n2021-01-15\nfrom(to)_millisecond_timestamp have been moved into its module millisecond_timestamp and rename them to deserialize \u0026amp; serialize from(to)_duration_ms have been moved into its module duration_ms and rename them to deserialize \u0026amp; serialize from(to)_option_duration_ms have been moved into its module option_duration_ms and rename them to deserialize \u0026amp; serialize 4 Summary To be honest, it\u0026rsquo;s the first time I have needed some customized works, which took me some time to understand how does Serde works. Finally, all investments paid off, it works great now.\nSerde is such an awesome deserialize/serialize framework which I have learnt a lot of from and still have a lot of to learn from.\n5 Reference Deserializing optional datetimes with serde PR: Keep polishing the models PR: Refactor model PR: Deserialize enum from number ","permalink":"https://ramsayleung.github.io/zh/post/2020/serde_lesson/","summary":"The lesson learned from refactoring rspotify\n1 Preface Recently, I and Mario are working on refactoring rspotify, trying to improve performance, documentation, error-handling, data model and reduce compile time, to make it easier to use. (For those who has never heard about rspotify, it is a Spotify HTTP SDK implemented in Rust).\nI am partly focusing on polishing the data model, based on the issue created by Koxiaet.\nSince rspotify is API client for Spotify, it has to handle the request and response from Spotify HTTP API.","title":"Serde Tricks"},{"content":"局中人的思考\n1 前言 最近深圳的特殊工时制度搞得甚嚣尘上, 兼之有位与互联网行业完全无交集的朋友咨询我, 为什么你们程序员要996呢?\n有感于此, 写下自己这个局中人的理解和看法. 下面的内容大部分来源于2020年年后, 解答某国外新闻系同学关于国内996工作制度的疑问.\n平时我很少写时政人文类的文章, 一方面是这类话题容易引发口水仗, 另外一方面是因为我的学识不足以阐述清楚问题的根源, 只是本次的话题, 我也算是利益相关者, 所以就发表下个人感想.\n2 (昔日)现状 言归正传, 我本科毕业之后来了蚂蚁金服(aka, 支付宝/Alipay)工作, 至今已经将近两年. 日常的工作时间是 9.am - 10/11/12.pm, 什么时候下班取决于工作量的多少, 以及是否需要发布迭代.\n虽然没有明确要周未需要工作, 但是在周未, Leader 时常也会用钉钉/电话和你沟通, 并没有这是周未, 不应该再工作的觉悟.\n至于 9.am 这个时间点, 是新任CEO强制要求的, 要求 9.am 到公司, 9:15.am 晨会(站会), 美其名为敏捷开发. 在我入职的时候, 弹性上下班的工作制度也是会执行的, 即如果你晚上工作到比较晚(11.pm), 你早上可以晚点来(10.am 后), 现在是你 12.am 之后下班, 明天还要 9.am 到公司晨会.\n想起个比较心酸的有趣事:\n曾经我们组员在会议室闭关(也就一群人被关在会议室里面封闭开发), 公司邮件说事业群下个月要开年会, 因为我们的年会不是年年开, 时有时无, 并不知道为什么, 然后有个同事说了下, 不是要在年会宣布996吧?\n(杭州一家叫有赞的企业刚刚在年会宣布996) 众人大笑; 然后另外一位同事略显无奈地接了句, \u0026ldquo;如果能9点下班, 那就好咯\u0026rdquo;, 众人笑声更甚.\n3 见解 为什么我们工作要这么累, 连9点下班也是一种奢望; 为什么我们要996, 不能 work life balance呢? 关于这样的问题, 我也曾思考过, 下面是我不成熟的见解:\n3.1 革新与底层技术 从革新与底层技术方面来说, 我们没有经历过工业革命, 没有以技术去推动社会生产力进步的传统, 这三十年的发展很大一部分是全球化与人口红利的结果, 即秦晖教授所言的低人权优势. 同理, 中国互联网只有业务模式的创新, 并没有基础技术的革新与壁垒, 所谓的新四大发明便是如此;\n因为没有技术壁垒, 你做的东西, 别人也容易仿制, 所以只能和别人比速度, 通过快速迭代来抢占市场, 难免就出现拼命加班的情况.\n3.2 企业 而从企业的角度来说, 以这个号称996始祖之一的公司举例, 他们的目的就是要不择手段地实现利益最大化, 员工利益的保障只能靠资本家良心发现了, 而资本家只是资本的人格化, 资本是不论对错, 只谈利弊的.\n3.3 公司文化 此外, 某司从公司创始人到公司文化, 都喜欢洗脑, 洗脑纲领即所谓的价值观, 最近还出了新的价值观, 号为新六脉神剑.\n公司还抽了两天下午, 全员脱产学习, 场景让我梦回大学思修毛概课堂. 其中有一条为, 客户第一, 员工第二, 股东第三. 有同事质疑, 既然公司把员工第二写到价值观, 为何员工还要996, 我们做的是体力劳动还是脑力劳动. 公司HR答复: 这是你们自己的选择? 同事追问, 为何对公司有益的事, 公司要强力推行, 为何要公司让步, 造益员工的时候, 又要让员工自行选择, 是否双标呢? 公司HR: 此事容后再议.\nHR 和老板们在公司内部群解答新六脉神剑, 总能在让员工纷纷点赞, 双击666, 不知道是反串黑还是幸存者偏见了.\n3.4 政府及立法角度 从政府及立法角度来说, 到了这一步, 员工的权利只能由政府来保障, 需要对企业作限制, 然而我们的政府对这种创造大量GDP的企业, 只会当作爸爸, 又怎会处罚呢? 你见过在南山区法院打赢腾讯的么? 在西湖区打赢支付宝的么?\n而我们又没有投票权, 政府要加税就加税, 要监控你就监控你, 要发红头文件就发, 要保大企业就保大企业, 我们又能怎样? 政府不鼓励大企业实行996就不错了, 还处罚他们?(好吧, 一语成谶)\n目前的执政党是靠工人运行起家的, 靠组建工会的方式来对抗无底线的加班也不实际, 能否成立工会都是个问题.\n3.5 员工自身 还有员工自身的原因, 或出于对物质的更高追求, 或出于生存的压力, 身边自然不会少自愿加班的人, 他们大多有家有口, 步入中年, 而他们作为三/四口之家的唯一劳动力, 想要在杭州安家, 想有自己的房子, 需要负出自己的时间, 精力与健康.\n也因为这样的人, 使996得而蔚然成风, 但为何买一套自己的房子需要付出如此大的代价, 引申出来又是一个复杂的问题. 关于房地产的分析, 可以参考下这两篇文章:\n帮你分析中国的房地产市场 2010年的房地产调控,我们收获了什么?写在房价暴涨前 4 更新 2020-10-14:\n大半年的时间过去了, 针对这个问题, 我个人有了些新的感想. 除去上述因素, 我们这些从业者还很容易陷入到一个剧场效应的怪圈中:\n4.1 剧场效应 想象此刻你正在影院看电影, 如果所有人都坐着看其实很轻松而且看的很清楚, 但如果有人选择站起来, 那他会获得更好的视野, 代价就是劳累.\n不过接踵而至的问题是其他人的体验就会变差, 为了应对这个局面那你也「不得不」站起来.\n最后的结果就是除了第一排以外的所有人都站着看完了电影, 十分劳累不说视野未必比都坐着好.\n第一排的就是坐在食物链顶部的资本家, 开始的时候, 给高于其他人报酬, 以利诱之, 让你加入到996的行列, 当你身在其中的时候, 以你为榜样裹挟其他人入场.\n当所有人都变成996的时候, 自然不可能再给你高于其他人报酬。 最后的结果就是所有人都在原来的薪酬水准上, 付出更多的时间和精力, 甚至得到更低的时薪.\n这就涉及到另外一个问题, 是否给钱就可以996加班, 只要钱给够就可以为所欲为? 有感于某电商企业月工双休的传闻.\n其实不是的, 持「有钱就可以996加班」这样的观点的人, 很容易变成影院第一个站起来的人, 成为破窗效应里面第一块被打破的窗户, 某种程度上说, 你正在卖力地帮着你的顾主压榨未来的自己:\n她那时还太年轻,不知道命运所赠送的礼物,早已在暗中标好了价格 \u0026ndash; 《断头王后》\n996加班是违法的, 即使修改劳动法也改变不了这个事实, 钱给够就可以996加班, 后面会演变成钱给够就可以007加班, 最后会变成摩登时代里面的工人, 我们都变成人肉干电池.\n这也是法律禁止器官买卖的原因, 即使双方同意, 不然很容易演变成强者对弱者的剥削.\n5 解法 那996加班就没有解法么? 我们就避免不了人肉干电池的命运么?\n其实解法还是有的, 在有人站起来的时候, 如果有工作人员把他呵斥, 让他坐下去, 否则就赶他出去, 相信他会老实下来, 但是工作人员又不看电影, 为何要为你们操心, 反正你们已经买票了;\n不过, 如果有多个电影院可以选, 情况就会有所不同了.\n另外一个解法就是, 在有人站起来的时候, 前面和后面的观众都集体把他呵斥下去, 不然饮料, 食物都往他头上招呼.\n只是人要主动站出来, 为自己争取权益, 并不容易.\n但须知, 世界上没有从天而降的英雄, 只有挺身而出的凡人.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/996%E6%88%90%E5%9B%A0/","summary":"局中人的思考 1 前言 最近深圳的特殊工时制度搞得甚嚣尘上, 兼之有位与互联网行业完全无交集的朋友咨询我, 为什么你们程序员要996呢? 有感于此, 写下","title":"为什么我们要996"},{"content":"1 前言 尾调用消除(tail call elimination, TCE)是函数式编程的重要概念, 有时也被称为尾调用优化(tail call optimization, TCO), 作用是将尾递归函数转化成循环, 避免创建许多栈帧, 减少开销.\n遗憾的是, Java不支持TCE, 所以本文主要是介绍, 如何使用java8特性, 基于堆来实现尾递归优化.\n一个有趣的事,这篇文章是我在阿里ATA上发的最后一篇文章。发在内网的第二天,也就是我的last day,有位P8的同事在钉钉上夸我文章写得好,只回复了一句,还未来得及多交流几句,我的离职流程就走完,钉钉被强制下线了,甚至没看到这位同事的回复。\n2 尾调用与尾递归 想要了解尾递归优化, 首先要了解下什么是尾调用.\n尾调用的概念非常简单, 一言以蔽之, 指函数的最后一步是调用另一个函数. 以斐波那契数列为例:\npublic int fac(int n) { if (n \u0026lt; 2) { return 1; } return n * fac(n - 1); } 虽说上面的函数看起来像是尾调用函数, 但实际上它只是普通的递归函数, 因为它最后一步不是调用函数, 它只是作了加法计算, 上面的逻辑等同于:\npublic int fac(int n){ if(n \u0026lt; 2){ return 1; } int accumulator = fac(n - 1); return n * accumulator; } 既然调用 fac(n-1)函数的目的是为了获取累加值, 那么我们自然将累加值抽出来, 然后把上面的斐波那契数列函数改成尾调用函数呢:\npublic int fac(int n) { return facTailCall(1, n); } public int facTailCall(int accumulator, int n) { if (n \u0026lt; 2) { return accumulator; } return facTailCall(n * accumulator, n - 1); } 函数调用自身, 称为递归函数. 如果尾调用函数自身, 就称为尾递归函数. 那尾递归函数有什么用呢? 仅仅是将斐波那契数列的累加值抽了出来么?\n要回答这个问题, 让我们先把目光投回到递归版本的斐波那契数列, 当调用 fac(6)时发生了什么事情:\n6 * fac(5) 6 * (5 * fac(4)) 6 * (5 * (4 * fac(3))) // N次展开之后 6 * (5 * (4 * (3 * (2 * (1 * 1))))) // \u0026lt;= 最终的展开 // 到这里为止, 程序做的仅仅还只是展开而已, 并没有运算真正运运算, 接下来才是运算 6 * (5 * (4 * (3 * (2 * 1)))) 6 * (5 * (4 * (3 * 2))) 6 * (5 * (4 * 6)) // N次调用之后 720 // \u0026lt;= 最终的结果 fac(10000) // =\u0026gt; java.lang.StackOverflowError 从上面的例子可以看出, 普通递归的问题在于展开的时候会需要非常大的空间, 这些空间指的就是函数调用的栈帧, 每一次递归的调用都需要创建新的栈帧, 递归调用有对应的深度限制, 这个限制就是栈的大小.\n默认栈空间从32kb到1024kb不等, 具体取决于Java版本和所用的系统, 对于64位的java8程序而言, 递归的最大次数约为8000.\n我们也没法通过增加栈的大小来增加递归的次数, 栈的大小相当于是一个全局配置, 所有的线程都会使用相同的栈, 增加栈的大小只是浪费资源而言.\n那有没有方法可以避免上述的 StackOverflowError 呢? 那当然是有的, 答案就是上文提到的尾递归.\n让我们来观察下尾递归版本的斐波那契数列, 看看调用 facTailCall(1, 6) 会发生什么事情?\nfacTailCall(1, 6) // 1 是 fac(0) 的值 facTailCall(6, 5) facTailCall(30, 4) facTailCall(120, 3) facTailCall(360, 2) facTailCall(720, 1) 720 // \u0026lt;= 最终的结果 facTailCall(1, 15000) // java.lang.StackOverflowError 与上方的普通递归函数相比, 尾递归函数在展开的过程中计算并且缓存了结果, 使得并不会像普通递归函数那样展开出非常庞大的中间结果, 但是尾递归函数还是递归函数, 如果不作尾递归优化(TCO), 依然会出现 StackOverflowError.\n所谓的尾递归优化, 可以简单理解成将尾递归函数优化成循环; 在函数式编程中, 是鼓励大家使用递归, 而不是循环来解决问题.\n这是因为循环会引入变量, 而变量是函数式编程中被视为洪水猛兽一样的存在.\n但如果递归调用的深度比较大, 栈帧会开辟很多, 一来是浪费空间, 二来性能也必然会下降(有很多读写内存操作);\n相反, 如果使用循环, 则只在一个函数栈空间里, 不会开辟更多的空间, 所以使用循环, 性能要好于递归.\n所以在函数式编程语言中, 如Scheme, Haskell, Scala, 尾递归优化是标配, 所以不会出现 StackOverflowError\n(define (fact x) (define (fact-tail x accum) (if (= x 0) accum (fact-tail (- x 1) (* x accum)))) (fact-tail x 1)) (fact 1000000), ;;; 返回一个很大很大的数, 使用的空间与(fact 3)相当 遗憾的是, Java并不支持尾递归优化.\n3 基于堆的尾递归 尾递归优化的一大用处是维持常数级空间, 保证不会爆栈.\n既然爆栈的原因是栈空间不足, 又无法扩大栈的空间, 那么只能把函数存在其他地方, 比如堆(heap). 使用堆来抽象递归, 那么需要做的事情如下:\n表示一个函数的调用 把函数调用存储在栈式结构中, 直到条件终止 以后进先出(LIFO)的顺序调用函数 为此我们可以定义一个名为TailCall的抽象类, 它有两个子类: 其一表示挂起一个函数以再次调用该函数对下一步求值, 如下, 先暂停f()的调用, 先调用出g()的结果, 再对f()进行求值, 此子类名为Suspend:\ndef f(): return g() + 1 而一个函数的调用可以通过java8引入的Supplier\u0026lt;T\u0026gt;类来表示, 以此来存储函数, T为TailCall, 表示下一个递归调用.\n这样一来, 就可以通过每个尾调用引用下一个调用的方式来构造一个隐式链表, 完成栈式数据结构存储的要求.\n另一个子类表示返回一个调用, 它应该返回结果, 不会持有到一个TailCall的引用, 因为已经没有下一个TailCall了, 所以其名为Return.\n其外, 还需要几个额外的抽象方法: 返回一个调用, 返回结果, 以及判断是否判断TailCall是Suspend还是Result, 接口及子类实现如下:\n/** * @author Ramsay/Ramsayleung@gmail.com * Create on 7/5/20 */ public abstract class TailCall\u0026lt;T\u0026gt; { public abstract TailCall\u0026lt;T\u0026gt; resume(); public abstract T eval(); public abstract boolean isSuspend(); public static class Return\u0026lt;T\u0026gt; extends TailCall\u0026lt;T\u0026gt; { private final T t; private Return(T t) { this.t = t; } @Override public TailCall\u0026lt;T\u0026gt; resume() { throw new IllegalStateException(\u0026#34;Return has no more TailCall\u0026#34;); } @Override public T eval() { return t; } @Override public boolean isSuspend() { return false; } } public static class Suppend\u0026lt;T\u0026gt; extends TailCall\u0026lt;T\u0026gt; { private final Supplier\u0026lt;TailCall\u0026lt;T\u0026gt;\u0026gt; resume; private Suppend(Supplier\u0026lt;TailCall\u0026lt;T\u0026gt;\u0026gt; resume) { this.resume = resume; } @Override public TailCall\u0026lt;T\u0026gt; resume() { return resume.get(); } @Override public T eval() { TailCall\u0026lt;T\u0026gt; tailCall = this; while (tailCall.isSuspend()) { tailCall = tailCall.resume(); } return tailCall.eval(); } @Override public boolean isSuspend() { return true; } } public static \u0026lt;T\u0026gt; Return\u0026lt;T\u0026gt; tReturn(T t){ return new Return\u0026lt;\u0026gt;(t); } public static \u0026lt;T\u0026gt; Suppend\u0026lt;T\u0026gt; suppend(Supplier\u0026lt;TailCall\u0026lt;T\u0026gt;\u0026gt; supplier){ return new Suppend\u0026lt;\u0026gt;(supplier); } } Return并没有实现resume方法, 只是简单地抛出了异常, 因为前文提到过, Return表示最后一个调用, 没有下一个调用了, 自然无法实现resume方法;\n同理, 只要不是最后一个调用, 就没法实现eval()方法, 因为最后的一个调用才能返回结果.\n那为啥Suspend还实现了eval方法呢? 主要是不让用户感知函数调用并返回结果的逻辑, 将其内敛到Suspend内. 现在让我们来看看效果:\n/** * @author Ramsay/Ramsayleung@gmail.com * Create on 7/5/20 */ public class TailCallTest { /** * 尾递归版本斐波那契数列 */ public int fac(int accumulator, int n) { return facTailCall(accumulator, n).eval(); } public TailCall\u0026lt;Integer\u0026gt; facTailCall(int accumulator, int n) { if (n \u0026lt; 2) { return TailCall.tReturn(accumulator); } return TailCall.suppend(() -\u0026gt; facTailCall(accumulator * n, n - 1)); } /** * 递归版本的两数相加 */ public int addRecur(int x, int y) { return y == 0 ? x : addRecur(++x, --y); } /** * 尾递归优化版本的两数相加 */ public int addTCO(int x, int y) { return addTailCall(x, y).eval(); } public TailCall\u0026lt;Integer\u0026gt; addTailCall(int x, int y) { int _x_plus_one = x + 1; int _y_minus_one = y - 1; return y == 0 ? TailCall.tReturn(x) : TailCall.suppend(() -\u0026gt; addTailCall(_x_plus_one, _y_minus_one)); } @Test public void addTest() { addRecur(10, 10); // =\u0026gt; 20 addRecur(10, 10000); // StackoverFlowError addTCO(3, 100000); // =\u0026gt; 100003 } @Test public void test() { fac(1, 6); // =\u0026gt; 720 fac(1, 600000); // 数字过大溢出, 返回0, 且没有出现 StackOverflowError } } 4 总结 至此, 我们通过java8的lambda, Supplier接口实现了基于堆的尾递归优化, 虽说没有优化成常数空间, 但终归解决了递归过深时, 栈空间不足导致 StackOverflowError的问题.\n而按照Stackoverflow问题的说法, java不支持尾调用的原因如下:\nIn jdk classes there are a number of security sensitive methods that rely on counting stack frames between jdk library code and calling code to figure out who\u0026rsquo;s calling them.\n后续java版本也暂无支持尾递归优化的计划, 无奈摊手.jpg\n5 参考 https://en.wikipedia.org/wiki/Tail_call Functional Programming in Java NightHacking with Venkat Subramaniam Designing tail recursion using java 8 ","permalink":"https://ramsayleung.github.io/zh/post/2020/java%E5%AE%9E%E7%8E%B0%E5%B0%BE%E9%80%92%E5%BD%92%E4%BC%98%E5%8C%96/","summary":"1 前言 尾调用消除(tail call elimination, TCE)是函数式编程的重要概念, 有时也被称为尾调用优化(tail call optimization, TCO), 作用是将尾递归函数转化成循环, 避免创建许","title":"java8基于堆实现尾递归优化"},{"content":"1 导语 呆在那里, 还是走开, 结果一样. \u0026ndash; 加缪《局外人》(又译作, 异乡人)\n2 妈妈走了 \u0026ldquo;今天, 妈妈死了, 也许是昨天\u0026rdquo;. 某天, 养老院来电通知男主默尔索妈妈去世了, 他前去奔丧, 却在守灵时抽烟, 喝咖啡, 跟人闲聊, 昏昏欲睡, 记不起母亲的岁数, 拒绝看母亲入入殓前的最后一面, 甚至未曾因母亲的离开而有一丝悲伤, 为能睡足12小时而高兴, 母亲入土第二天, 与女友游泳, 看喜剧, 发生关系.\n开头便用妈妈去世这件事刻画出默尔索的性格, 对周围一切事物的疏离与冷漠, 对世俗规则与戒条的忽视.\n3 身处局外, 看破世俗 3.1 关于爱 默尔索的女友玛莉想知道他是否爱自己, 他说\u0026quot;如果一定要说的话, 我大概是不爱的\u0026quot;;\n关于是否想和自己结婚, 他说\u0026quot;怎么都行\u0026quot;, \u0026ldquo;如果她想, 我们可以结婚\u0026rdquo;, 在他看来, 人们常常挂在嘴边的爱并不能说明什么, 这种关于爱的问答就好像一个语言游戏, 其实没有什么意义, 也不能证明什么, 只不过问答双方依旧乐此不疲, 但默尔索已经看透这些事情, 只是他拒绝参加这个语言游戏.\n3.2 关于异乡人 局外人的另一译名为《异乡人》, 后面了解到, 对比巴黎, 默尔索所处的殖民地为异乡, 人们以巴黎为荣, 而巴黎又代表了世俗的看法, 养老院的门房想让默尔索知道, 他是个巴黎人, 很怀念巴黎的生活;\n默尔索的女友玛莉很乐意去巴黎; 马颂的太太有巴黎口音; 在默尔索杀人案(和一起弑父案)开庭期间, 有巴黎派来的记者; 老板想在巴黎开个新的办事处; 当玛莉问他对巴黎的看法时, 他说: \u0026ldquo;那里满脏的, 到处都是鸽子和阴暗的庭院, 而且人的肤色很苍白\u0026rdquo;.\n阿尔及利亚之于巴黎是为异乡, 默尔索之于世俗规则是为异乡人, 或者, 这便是异乡人这个译名的来由.\n说到底, 默尔索不想因为这些世俗观点而改变自己的生活方式, 不想做出一丝改变, 他以一种迟钝的态度应对着这个世界.\n4 罪行与罪人 预审法官就对如何审理默尔索杀人一案指出了方向: 本案关注的还是罪行, 而是犯罪的人.\n他对默尔索说, \u0026ldquo;我真正感兴趣的, 是您本人\u0026rdquo;. 默尔索杀了人, 然而法庭却一直在讨论\u0026quot;他在母亲的葬礼上有没有哭\u0026quot;, 因为他在母亲的丧礼不哭, 所以他有可能成为下一起案子的杀人犯, 由此推断出, 他的坏是他的本质, 甚至论证成了确凿的杀人动机.\n在我们的社会里, 一个人在母亲的葬礼上没有哭, 他就会有被判死刑的危险.\n5 丧与醒悟 默尔索作为局外人的领悟, 让其萦绕着一种无所谓的丧感.\n\u0026ldquo;对于我真正感兴趣的事我也许没有绝对的把握, 他对于我感兴趣的事情我是有绝对的把握的\u0026rdquo;, 神父找他聊的事正好是他不感兴趣的事, 所以他坦然走向断头台. \u0026ldquo;什么样的生活都差不多, 人们永远无法改变生活\u0026rdquo;, 他觉得这一切都不重要, 所以他拒绝了老板让他去巴黎工作的建议. \u0026ldquo;人生在世, 永远也不该演戏作假\u0026rdquo;, 正是这样的人生准则最后导致了他庭审被判斩首. 在生命的最后时刻, 我也不知道默尔索是觉悟到失去后才懂得珍惜的道理, 被关进监狱才想起自由的宝贵, 要被斩首才领会到生命的价值;\n还是说在生命将要走到尽头的时候, 才理解了母亲在养老院找了个新\u0026quot;男友\u0026quot;的原因.\n\u0026ldquo;从我遥远的未来, 一股暗潮穿越尚未到来的光阴冲击着我, 流过至今我所度过的荒谬人生, 洗清了过去那些不真实的岁月里人们为我呈现的假象\u0026rdquo;\n6 写在最后 人生在世, 终究是没法活成默尔索的样子, 或者说活成默尔索的样子, 对于身边的人来说, 是一种折磨.\n人是一种群居动物, 过分的冷漠与疏离, 只会让群体离你越来越远, 自以为是的冷漠, 终究还是会走向悲剧.\n有感于最近发生的诸事, 有感于我不正确的为人处事的方式.\n最好的方式是, 看清游戏的本质, 并且以此赢得游戏, 说出那些无所谓的话, 于人于已无半点用处.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E5%B1%80%E5%A4%96%E4%BA%BA/","summary":"1 导语 呆在那里, 还是走开, 结果一样. \u0026ndash; 加缪《局外人》(又译作, 异乡人) 2 妈妈走了 \u0026ldquo;今天, 妈妈死了, 也许是昨天\u0026rdquo;. 某天","title":"局外人"},{"content":"1 背景介绍 笔者目前在蚂蚁金服-网商银行做后端开发, 因为在组内毕业时间最短(2年), 所以经常会被Leader当成免费的HR去找校招简历, 所以见过不少的简历(\u0026gt;100份), 把收到简历之后, 有时会给简历打分, 然后再给到老板.\n因为见过不少的简历, 发现有些学历,经历优秀的同学, 因为没有好好写简历而被埋没, 也见过通过简历, 放大自身优点的同学. 所以在这里, 以前人的姿态, 斗胆谈一个应届生如何写好一份简历的技巧, 也希望给各位同学带来一点帮忙.\n2 自我介绍 自我介绍, 无需赘言, 就是把你的个人信息简明介绍完, 包括教育经历, 专业, 邮箱, 电话, Github地址(如果有优秀项目的话, 如果只是注册了个账号, 还是不要放上去).\n需要注意的是, 关于是否放照片这一点, 个人倾向于不放, 作为技术开发, 放不放都无所谓, 放了容易分散注意力. 如果要放照片, 照片请做到简洁, 得体.\n3 实习经历/项目经历 对于开发岗, 实习经历和项目经历是重要的栏目, 也是面试官期待看到的栏目, 因为应届生没有工作经历, 所以就只好写实习经历和项目经历, 对于实习经历/项目经历, 按时间升序或者降序排列, 不要太乱, 个人推荐的格式:\nxx 公司/xx 项目, 时间: 2020.03-2020.xx 1. 项目背景一句话、 2. 自己在项目里负责的工作 3. 用到的技能/思考的过程或者难点攻克的过程 4. 项目的结果或者我的成绩 总而言之, 参考STAR法则. 需要避免的一些问题:\n技术无关的事情少写, 更不要写一些大家都知道的事情. 在项目中负责”代码的编写, 用例的测试, 以及相关文档的校对/编辑”, 总结来说, 你写代码了, 但是做了啥呢? 没体现. 避免流水账, 希望可以简洁明了, 突出重点, 使用STAR法则, 参见如何使用STAR法则写自己的简历啊 避免写和你面试岗位不相关的内容, 我去当家教了, 我把它写到简历里, 但是你面试的是技术岗位, 不是老师. 4 个人技能 将个人技能按照熟悉程度降序排列, 通过项目和技能介绍, 给面试官留下一种”喜欢学习新事物, 喜欢挑战, 喜欢折腾, 有geek精神”. 列下需要注意的点:\n避免主观内容, 比如吃苦耐劳, 善于学习这些; 招聘面试很重要的一点是筛选出符合有相关专业/潜力的同学, 这些都是通过客观条件体现的, 比如你的项目, 竞赛, 论文等, 尝试通过能力和项目来证明, 而不是自己主观评价. 程序开发是技术活, 对于应届生而言, 讲究的是 Talk is cheap, Show me your work. 尝试提供事实支撑; 如”熟悉Spring框架”的表述, 肯定不如”了解Spring框架, 读过部分代码, 包括容器依赖注入, 控制反转, 总结相关的设计模式”等. 不要写一些和技术无关的技能, 如”会PS, 有驾照”这类. 四六级, 雅思/托福, 日语N1/N2这些语言技能可以加上 5 顶级期刊论文/Acm竞赛 这些都是重要加分项, 如果有的话, 就把期刊论文和Acm竞赛的获奖经历, 列出来, 提高面试官的期望值, 按奖项/论文的含金量降序排列, 如果没有的话, 就跳过.\n6 其他亮点 大部分的同学可能都没有Github 1w+的star, 没有为Linux Kernel/Netty/Redis/Mysql这些项目贡献过代码 ,没发过顶级期刊的论文, 就觉得自惭形愧, 一无是处.\n我觉得并非如此, 我觉得折腾过Vim/Emacs, 熟悉使用Zsh+Tmux+Git, 熟悉Linux(关于熟悉的标准, 参见下文), 也是亮点;\n并非要做到最好, 才叫有亮点; 也并非产出对应的结果才讲亮点, 对于学生而言, 探索/折腾的过程同样重要; 此外, 没有哪个专家不是从菜鸟开始起步的; 接下来我会列举下我认为亮点的地方:\n参与开源项目, 有一定的star/follower, 比如我到现在都在维护Rust的一个开源库, 也写过700+star的爬虫. 有自己的blog, 很多新的技术就可以在blog实践, 也有地方可以沉淀自己的思考, 包括遇到的问题及其排查思路与过程, 记录有趣的事情等等, 但如果都是搬运的文章就算了. 研究过开源技术, 如我自己折腾过常用的Linux发行版本, 个人开发日常使用Linux, 使用Emacs超过5年, 自己编写Shell脚本管理电脑, 在17年开始学习Rust等等. 阅读相关项目源码, 有相应的总结/思考. 如Jdk/JUC源码, Spring源码, Tomcat源码, Netty源码, 记录在自己blog上. 了解/使用多种语言, Java/C++/C/Python/Go/Rust/Sql/Shell, 这个就不一一列举了. 总而言之, 自己的思考_动手折腾_新鲜事物的探索, 都可以像亮点.\n7 个人评价/兴趣爱好 公司招聘是选择有能力, 并且合适的同学, 并不是相亲, 所以老板并不关心你的兴趣爱好和个人评价; 在面试中, 你应该是由面试官评价, 自我评价并没有什么用处, 写上去还占空间.\n8 细节 需要明确的一点, 在面试官面试你之前, 你的简历就是你最大的推销手段, 你的简历代表着你在和其他上百名的竞争者做着竞争.\n因此你的简历每多打磨一分, 你的在众多简历中脱颖而出的机会就多了一分, 所以简历需要精心打磨, 那么很多细节就应该注意, 说下我看到的细节点:\n文档格式: 简历的文件类型最好用pdf, 很多技术开发用的是Mac, 如果用的是word, 可能遇到各种问题, 排版也可能会乱掉, 对于pdf而言就不存在这样的问题, 速度也足够行. 简历模板: 可以的话, 请不要用 word 套模板, 要套模板就用latex, 不用调格式, 例如: https://github.com/billryan/resume 对于伸手党同学, 注册这个网站, 把你的简历内容替换掉模板即可: https://www.overleaf.com/project/5e6c67ac54a3190001a2fed7 如果这样还不会的话, 那就\u0026hellip; 简历篇幅: 应届生的简历最好一页写完, 如果一页没写完, 第二页只多了一点内容, 就会显得很难受. 简历命名: 发送简历给面试官, 或者简历收集同学的时候, 请不要用”个人简历_我的简历”这类的名字, 谁知道”个人_我”指的是谁, 推荐命名: 学校_学历_姓名_求职意愿.pdf 如: xx大学_硕士_宫xx_后端开发.pdf 技术熟悉程度: 精通, 熟悉, 了解; 这些用词请注意, 按我的理解, \u0026ldquo;了解\u0026quot;要起码用这个技术自己做过一点东西, 平时关心相关的新闻和前沿进展; \u0026ldquo;熟悉\u0026quot;则是平时经常用到这个技术, 或者曾经在很长一段时间内以它为主做过开发;\u0026ldquo;精通\u0026quot;则起码要能把它从头到尾理解得非常透彻才能算是. 如果你是了解, 然后简历说是精通, 面试官对你的期望会拔高, 然后发现你是了解, 那心理就会有落差. 举例 ,我精通git, 然而只会git add/git commit/git push, 连git bisect都没听过, 那就\u0026hellip; 参与程度; 参与, 负责; 请注意用词, 参与系统开发表现对某个功能模块清楚, 负责表示所有设计考虑, 技术实现都清楚. 和你面试工作相关的东西不要写; 如我是学生会干部, 这个没啥用, 我们要的不是干部, 而是有相关专业技能的人才. 9 总结 总而言之, 写好简历可以做到扬长避短, 最大限度突出亮点的作用, 如果你觉得实在绞尽脑汁都没有什么可以写的话, 或者你应该重新去做些个人项目, 积累经验再来投递.\n说了这么多, 因为拿份示例出来了, 因为我已经工作2年, 已经找不到找实习当时的简历了, 所以拿了基友的简历过来, 基友拿到了AWS的offer, 他的实习/学术项目已经足够丰富, 其他内容就做了取舍(已获得基友授权, 基于本人要求, 去掉个人信息):\nFigure 1: 8ZxIzt.jpg\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E5%BA%94%E5%B1%8A%E7%94%9F%E5%A6%82%E4%BD%95%E5%86%99%E5%A5%BD%E6%8A%80%E6%9C%AF%E7%AE%80%E5%8E%86/","summary":"1 背景介绍 笔者目前在蚂蚁金服-网商银行做后端开发, 因为在组内毕业时间最短(2年), 所以经常会被Leader当成免费的HR去找校招简历, 所以见","title":"应届生如何写好技术简历"},{"content":"1 Preface Today, I am exited to introduce you the v0.9 release I have been continued to work on it for the past few weeks that adds async/await support now!\n2 The road to async/await What is rspotify: \u0026gt; For those who has never heared about rspotify before, rspotify is a Spotify web Api wrapper implemented in Rust.\nWith async/await\u0026rsquo;s forthcoming stabilization and reqwest adds async/await support now, I think it\u0026rsquo;s time to let rspotify leverage power from async/await. To be honest, I was not familiar with async/await before, because of my Java background from where I just get used to multiple thread and sync stuff(Yes, I know Java has future either).\nAfter reading some good learning resources, such as Async book, Zero-cost Async IO, I started to step into the world of async/await. async/await is a way to write functions that can \u0026ldquo;pause\u0026rdquo;, return control to the runtime, ant then pick up from where they left off.\nI think perhaps the most important part of async/await is runtime, which defines how to schedule the functions.\nNow, by leveraging the async/await power of reqwest, rspotify could send HTTP request and handle response asynchronously.\nFuthermore, not only do I refactor the old blocking endpoint functions to async/await version, but also keep the old blocking endpoint functions with a new additional feature blocking, then other developers could choose API to their taste.\n3 Overview album example:\nuse rspotify::client::Spotify; use rspotify::oauth2::SpotifyClientCredentials; #[tokio::main] async fn main() { // Set client_id and client_secret in .env file or // export CLIENT_ID=\u0026#34;your client_id\u0026#34; // export CLIENT_SECRET=\u0026#34;secret\u0026#34; let client_credential = SpotifyClientCredentials::default().build(); // Or set client_id and client_secret explictly // let client_credential = SpotifyClientCredentials::default() // .client_id(\u0026#34;this-is-my-client-id\u0026#34;) // .client_secret(\u0026#34;this-is-my-client-secret\u0026#34;) // .build(); let spotify = Spotify::default() .client_credentials_manager(client_credential) .build(); let birdy_uri = \u0026#34;spotify:album:0sNOF9WDwhWunNAHPD3Baj\u0026#34;; let albums = spotify.album(birdy_uri).await; println!(\u0026#34;{:?}\u0026#34;, albums); } Just change the default API to async, and moving the previous synchronous API to blocking module.\nNotes that I think the v0.9 release of rspotify is going to be a huge break change because of the support for async/await, which definitely breaks backward compatibility.\nSo I decide to make an other break change into the next release, just refactoring the project structure to shorten the import path:\nbefore:\nuse rspotify::spotify::client::Spotify; use rspotify::spotify::oauth2::SpotifyClientCredentials; after:\nuse rspotify::client::Spotify; use rspotify::oauth2::SpotifyClientCredentials; the spotify module is unnecessary and inelegant, so I just remove it.\n4 Conclusion rspotify v0.9 is now available! There is documentation, examples and an issue tracker!\nPlease provide any feedback, as I would love to improve this library any way I can! Thanks @Alexander so much for actively participate in the refactor work for support async/await.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/async_await_for_rspotify/","summary":"1 Preface Today, I am exited to introduce you the v0.9 release I have been continued to work on it for the past few weeks that adds async/await support now!\n2 The road to async/await What is rspotify: \u0026gt; For those who has never heared about rspotify before, rspotify is a Spotify web Api wrapper implemented in Rust.\nWith async/await\u0026rsquo;s forthcoming stabilization and reqwest adds async/await support now, I think it\u0026rsquo;s time to let rspotify leverage power from async/await.","title":"rspotify has come to async/await"},{"content":"1 前言 \u0026lt;枪炮, 病菌与钢铁\u0026gt;以一个新几内亚政治家耶利的问题展开,\u0026ldquo;为什么你们白人制造了那么多的货物并将它们运到新几内亚来, 而我们黑人却几乎没有属于我们自己的货物呢?\u0026rdquo;, 由此引出此书后续的的核心主题:\n为什么这个世界的财富与权力的分配会是现在这个样子的, 来自欧亚大陆的民族, 尤其是仍然生活在欧洲与东亚的民族, 以及移居到北美的民族, 控制着世界的财富与权力.\n其他民族, 包括大多数非洲人, 已经摆脱了欧洲人的殖民统治, 但在财富与权力方面仍然远远落在后面. 简而言之, 为什么在不同的大陆上人类以如此不同的速度发展呢?\n2 粮食生产与大陆轴线 Jared 提出了四组差异, 并用大量篇幅介绍这四组差异如何让欧亚大陆成为政治, 经济与军事的强者, 而不是现代人类的发祥地非洲, 不是美洲或者澳大利亚.\n2.1 可驯化动植物品种的差异 第一组差异是各大陆在可以用作驯化的起始特种的野生动植物品种方面的差异. 这是因为, 粮食生产之所以具有决定性的意义, 在于它能积累剩余粮食以养活不从事粮食生产 的专门人材, 同时也在于它能形成众多的人口, 从而甚至在发展出任何技术与政治优势之前, 仅仅凭借人多就可以拥有军事上的优势.\n由于这两个原因, 从小小的不成熟的酋长管辖地阶段向经济复杂的, 社会分层次的, 政治上集中的社会发展的各个阶段, 都是以粮食生产为基础的.\n就可驯化的生物物种而言, 欧亚大陆最为得天独厚, 非洲次之, 美洲又次之, 而澳大利亚最下. 令人惊讶的是, 原来美洲驯化的动物只有羊驼, 还只是小部分地方有, 而马是由欧洲殖民者带到美洲的, 部分印第安部落成为驯马的高手又是后来的事了, 看来我原来一直都被美国西部片给误导了\n2.2 影响传播和迁移速度的差异 第二组因素就是那些影响传播和迁移速度的因素, 而这种速度在大陆与大陆之间差异很大.\n在欧亚大陆速度最快, 这是由于它是的东西向主轴线和它的相对而言不太大的生态与地理障碍, 对于作物和牲畜的传播来说, 这个道理是最简单不过, 因为这种传播大大依赖于气候, 因而也就是大大依赖于纬度.\n同样的道理也适用于技术的发明, 如果不用对特定的环境加以改变就能使这些发明得到最充分的利用的话. 传播的速度在非洲就比较缓慢, 而在美洲就尤其缓慢, 这是由于这两个大陆的南北向主轴线生地理与生态障碍.\n2.3 影响大陆内部传播的差异 与影响大陆内部传播的这些因素相关的, 是第三组影响大陆之间传播的因素, 大陆与大陆之间传播的难易程度是不同的, 因为某些大陆比另一些大陆更为孤立.\n在过去的6000年, 传播最容易的是从欧亚大陆到非洲撒哈拉沙漠以南地区, 非洲大部分牲畜就是通过这种传播得到的.\n但东西半球的传播, 则没有对美洲的复杂社会作出任何贡献, 这些社会会在低纬度与欧亚大陆隔着宽阔的海洋, 而在高纬度又在地形和适合狩猎采集生活的气候之间又与欧亚大陆相去甚远.\n对于原始的澳大利亚来说, 由于印度尼西亚群岛的一道道水上障碍把它同欧亚大陆隔开, 欧亚大陆对它的唯一的得到证明的贡献就是澳洲野狗.\n2.4 面积与人口总数方面的差异 第四组也是最后一组因素是各大陆之间在面积和人口总数方面的差异.\n更大的面积或更多的人口意味着更多的潜在发明者, 更多的互相竞争的社会, 更多的可以采用的发明创造\u0026ndash;以及更大的采用和保有发明创造的压力, 因为任何社会如果不这样做就往往会被竞争对手所淘汰.\n3 中国的落后原因分析 文中除了阐述了为什么这个世界的财富与权力分配是现在这个样子之外, 还尝试解答一个问题: 既然新月沃地和中国最早产生粮食中心, 那么为什么现在先进的是欧洲, 而不是新月沃地与中国呢?\n对于新月沃地的解答是环境破坏导致新月沃地在后来成为了沙漠与半沙漠, 不再具有粮食生产中心的后续优势, 继而被欧洲取代, 我不了解阿拉伯历史, 因此不置可否.\n3.1 统一与分裂 而对于中国的分析则是提出了一个分裂有益的观点: 对于欧洲而言, 割裂的地理环境使其无法成为大一统的国家, 而中国则是因为历史, 地理的原因, 从秦始皇之后就是一个统一的国家, 即使有过分裂, 但也没有形成持续分裂的国家. 而这种大一统是有害的, 适合的分裂反而有益.\nJared 举出的例子是, 当初郑和下西洋时, 他的宝船代表着当时世界最先进的造船技术, 而却因为政治斗争失败, 明王朝停止派遣舰队远航并拆掉船坞, 禁止后续远洋航行.\n而欧洲的哥伦布曾请求葡萄牙国王派船让他向西航行探险, 他的请求被国王拒绝, 于是他就求助于梅迪纳-塞多尼亚公爵, 也遭到拒绝, 接着他又去请求梅迪纳-塞利伯爵, 依然遭到拒绝, 最后他求助于西班牙国王与王后, 他们拒绝了他的第一次请求, 但后来在他再次提出请求时总算同意.\n如果欧洲在这头3个统治者中任何一个的统治下统一起来, 它对欧洲殖民也许一开始就失败了. Jared 认为, 正是由于欧洲是分裂的, 哥伦布才成功地于第五次在几百个王公贵族中说服一个来赞助他的航海事业.\n虽说 Jared 提出了一个新奇的观点, 但我对这个的观点并不认同, 虽然我并不能像你回答耶利的问题那样, 总结出种种的因由, 但我觉得你的观点并不正确.\n像你而言, 中国人在1405年就已经率领28000船员到达非洲东海岸, 但是中国人并没有对他们建立殖民统治, 只是对这些非洲国家或者部落进行访问; 而达伽马和哥伦布到非洲与美洲的一个地方就要殖民一个地方, 中国人与欧洲人的差异在何处? 导致这种差异的原因是什么?\n3.2 地理决定论 Jared 他现在是手上拿着一个锤子(地理决定论), 看着什么都觉得是钉子.\n我觉得原因在于政治制度与文化传统的原因导致中国在明朝之后落后于欧洲的, 中国的统一王朝封建制度在初期是先进的制度, 而到后期逐步成为一种阻碍, 权力过于集中于一人之手(而平庸的君主总是比贤明的君主稀少).\n另外一个重要的因素是儒家思想, 作为王朝思想统治的手段, 目的只是制定一系列的标准与守则, 让人忠于王朝统治者, 类似中世纪的宗教, 有差异的是中国并没有出现文艺复兴, 而在南宋进一步加强.\n但是这些观点只是我一个工科男没有证明根据的臆测, 或许有一天, Jared 或者其他人可以给出一个让我信服的观点.\n4 总结 许久没有看过历史相关的著作, 虽说我自诩是个历史的爱好者.\nJared 这本书的确解答了我许多关于世界历史与现状的疑问, 甚至配得上人类简史这个名号, 但是此书也引起了我关于自己国家的思考与疑问, 为什么中国会变得落后于人, 原因何在?\n最后, 谨以书中的一句话结束此文, 也送给欧洲人民, 与其他大洲人民共勉:\n环境改变了, 过去是第一并不能保证将来也是第一.\n所谓萧瑟秋风今又是, 换了人间.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E6%9E%AA%E7%82%AE_%E7%97%85%E8%8F%8C%E4%B8%8E%E9%92%A2%E9%93%81/","summary":"1 前言 \u0026lt;枪炮, 病菌与钢铁\u0026gt;以一个新几内亚政治家耶利的问题展开,\u0026ldquo;为什么你们白人制造了那么多的货物并将它们运到新几内亚","title":"枪炮, 病菌与钢铁"},{"content":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一段旅途;\n失大于得则证明这段时间虚度罢了, 却无法重来. 本文便是对过去一年得与失的总结.\n无可奈何花落去,似曾相识燕归来.\n2 工作 我所在的项目组做的是对B端的聚合收单业务, 有蚂蚁的big title 背书, 服务一堆的服务商, 但业务主导一切, 一切以业务为中心, 技术并没有话语权.\n而后业务突遭变故, 业务接近停滞. 都要以为要重新准备简历了, 要拥抱变化了. 然后后面业务重新复活, 继续挣扎, 在生与死之间反复横跳, 为了复活做各种奇形怪状的需求, 也未见有起色.\n在整个由死重生的过程中对于收单业务有了重新的认识, 对了公司也有了新的认识, 对于自己的地位与作用也有新的认识.\n说到底, 我自己只是个工具人, 对于完全业务化的系统, 技术的作用着实毫不起眼, 充斥着无力感. 因此工作上免不了彷徨与迷茫.\n另外一方面, 因为这样的业务状况, 我也如自己预想中那般, 绩效拿了3.5, 无晋升提名. 在花呗的室友, 同一天入职, 类似的绩效, 晋升了. 可见, 选择着实比努力更重要点.\n3 读书 打算用读书冲淡工作变故而来的彷徨感, 兼之对于自身的不满与现实的疑惑, 寄望于通过多读书充实自己和从书中得到解答, 因此今年读了不少的书, 基本每本书都写了笔记与感悟:\n读完的书:\n追风筝的人 双城记 浮生六记 围城 沉默的大多数 苏菲的世界 月亮与六便士 netty实战 java并发编程实战 Effective C++ 在读的书\nUnix网络编程(读了1/3) 枪炮, 病菌与钢铁 除此之外, 还看了各种文章, 关于电影, 财经, 政治以及历史.\n总结下来基本是每个月读完一本书, 虽说与去年20本的目标还有差距, 但这年来读的书, 着实解答了我不少疑问.\n例如我现在为什么会996(实际上9105或者9115)? 原因可以说是多方面的:\n从革新与底层技术方面来说, 我们没有经历过工业革命, 没有以技术去推动社会生产力进步的传统, 这三十年的发展很大一部分是全球化与人口红利的结果.\n同理, 中国互联网只有业务模式的创新, 并没有基础技术的革新与壁垒, 所谓的新四大发明便是如此; 因为没有技术壁垒, 你做的东西, 别人也容易仿制, 所以只能和别人比速度, 难免就出现拼命加班的情况.\n而从企业的角度来说, 以这个号称996发源地的公司举例, 他们的目的就是要不择手段地实现利益最大化, 员工利益的保障只能靠资本家良心发现了, 而资本家只是资本的人格化, 资本是不论对错, 只谈利弊的.\n从政府及立法角度来说, 到了这一步, 员工的权利只能由政府来保障, 需要对企业作限制, 然而我们的政府对这种创造大量GDP的企业, 只会当作爸爸, 又怎会处罚呢? 你见过在南山区法院打赢腾讯的么? 在西湖区打赢支付宝的么?\n而我们又没有投票权, 政府要加税就加税, 要保大企业就保大企业, 我们又能怎样? 政府不鼓励大企业实行996就不错了, 还处罚他们?\n当然, 还有自身的原因, 身边自然不会少自愿加班的人, 而他们作为三口之家的唯一劳动力, 想要在杭州安家, 想有自己的房子, 需要负出自己的时间, 精力与健康, 也因为这样的人, 使996得而蔚然成风, 但为何买一套自己的房子需要付出如此大的代价, 引申出来又是一个复杂的问题, 可以参考下这两篇文章\n每周转载 天涯kkndme 神贴聊房价 诸如此类的感悟, 是我在今年读书后, 对心中疑惑的思考. 读书的作用就如小恶魔 Tyrion Lannister所说的那般, 好脑筋需要书本, 就如同宝剑需要磨刀石.\n4 其他 对于杭州有了新的认识, 借用下别人对杭州的评价:\n马路平整, 四季分明, 冬暖夏凉, 房价便宜, 美食多样, 工资够用, 一天工作 8 小时, 地铁发达, 大公司多, 小公司都很专业\n只需将上面的内容反转一下, 就可以知道杭州的实况. 去掉古代文人墨客诗文的滤镜, 杭州也就是这样罢了.\n逐渐明白, 技术并不是万能, 甚至用处并不是那么大.\n明白自己的无知和渺小, 很多事情并不能用技术来解决, 如工作遇到的变故等, 过于沉迷技术会形成一个误区, 以为什么都能用技术解决(当然, 也不能以此为借心放弃自己).\n明白了每项技术都用其存在的意义及背景, 如什么场景都用javascript自然不行, 但在浏览器场景不用javascript, 自然也是不行的.\n并技术没有对错, 争论哪个技术最好, 哪个编程语言更佳, 脱离场景毫无意义, 文人相轻又能解决什么问题呢?\n开始学习其他技术无关的知识与技能; 重新练习口琴; 开始有意识地控制体重;\n开始按照无器械健身的相关指南锻炼; 每周基本都有去运动, 游泳或者踢球;\n开始补经典的番; 想学日语, 想去日本看看; 想去加拿大看看, 心生去意;\n5 展望 把去年的展望搬过来, 机智如我:\n了解分布式, 高可用的知识,争取通过实战掌握; 读完《netty in action》; 通过许家纯大大的教程,自己实现一个Rpc 框架;读sofa-bolt, sofa-rpc 和 Netty 的源码 成为一个掌握金融知识的计算机从业人员 读完20本书 结束单身狗的生活 借一句诗勉励自己:\n沉舟侧畔千帆过,病树前头万木春.\n","permalink":"https://ramsayleung.github.io/zh/post/2019/2019%E6%80%BB%E7%BB%93/","summary":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一","title":"2019年总结: 人生如逆旅, 我亦是行人"},{"content":"1 前言 JDK 提供了各种功能强大的工具类, 宛如装备齐全的军火库, 而容器就是其中一项内置的利器, 提供了包括诸多常用的数据结构, 下图对 JDK 已有容器进行了概括:\nFigure 1: JDK 容器\n不过, 虽然 JDK 的容器类已经五花八门, 琳琅满目, 但是某些很有用的容器类 JDK 依然欠缺, 而 Guava 恰如其分地填补了这些空缺, 开发了 JDK 所欠缺的容器类, \u0026ldquo;造福大众\u0026rdquo;.\n此外, 虽然引入了新的容器类, 但 Guava 实现了 JDK 的 Collection 接口, 保证 Guava 的容器类能够与 JDK 的容器类”和谐共处”, 避免不必要的”纷争”.\n2 Multiset 假设你是个书店的老板, 你想统计下书店里不同书籍的存货量, 你可能写下这样的实现:\nMap\u0026lt;String, Integer\u0026gt; counts = new HashMap\u0026lt;String, Integer\u0026gt;(); for (String book : bookNames) { Integer count = counts.get(book); if (count == null) { counts.put(book, 1); } else { counts.put(book, count + 1); } } 嗯, 我现在想改需求, 我想知道书店里共有多少本书? 怎么办呢? 把 counts 的 value 都加起来?\n对于这样的要求, Guava 提供了一个更好的解决方案: 一个新类型容器 Multiset , 它支持新增多个相同类型的元素并统计. 维基百科给出的关于 Multiset 的解释:\n这是个成员可以出现多次的集合(Set), 也被称为背包(bag)\n大名鼎鼎的《算法/Algorithm》也给出过 bag 的解释和实现.\n需要注意的是, multisets 的成员是无序的, {a,a,b} 和 {a,b,a} 这两个集合在 multisets 看来是相等.\n我们可以从两个角度来分析 multisets :\nmultisets 就好像一个ArrayList\u0026lt;E\u0026gt;, 只不过是无序的. 当把它当作ArrayList\u0026lt;E\u0026gt;时:\n调用add(E)函数, 增加给定元素的出现次数 调用iterator()函数, 获取一个 multisets 的迭代器, 用来迭代每个元素 调用size()函数, 获取所有元素出现次数之和 multisets 就好象一个Map\u0026lt;E, Integer\u0026gt;, 包含元素和对应的数量, 只不过数量只能为正数. 当把它当作Map\u0026lt;E, Integer\u0026gt;的时候:\n调用count(Object)函数获取某个特定元素的出现次数. 调用entrySet()函数返回一个Set\u0026lt;Multiset.Entry\u0026lt;E\u0026gt;\u0026gt;, 大概类似一个 Map 返回 entrySet . 调用 elementSet 函数返回一个Set\u0026lt;E\u0026gt;对象, 返回所有的元素(去掉重复的元素) 2.1 Multiset 的例子 粗略介绍完 Multiset 之后, 现在就让我们用它重新实现原来的需求:\n@Test public void testMultiset() { final String POTTER = \u0026#34;Potter\u0026#34;; Multiset\u0026lt;String\u0026gt; bookstore = HashMultiset.create(); bookstore.add(POTTER); bookstore.add(POTTER); bookstore.add(\u0026#34;四体\u0026#34;); bookstore.add(\u0026#34;五体\u0026#34;); Assert.assertTrue(bookstore.contains(POTTER)); Assert.assertEquals(2, bookstore.count(POTTER)); Assert.assertEquals(4, bookstore.size()); bookstore.remove(POTTER); Assert.assertTrue(bookstore.contains(POTTER)); Assert.assertEquals(1, bookstore.count(POTTER)); } multisets 完美满足了我们的需求.\n2.2 Multiset 并不是一个 Map 需要注意的是, Multiset 虽然与Map\u0026lt;E, Integer\u0026gt;类似, 但 Multiset 并不是一个Map\u0026lt;E, Integer\u0026gt;, 请不要混淆它们两个.\n最大的差别是, Multiset 实现了 Collection 接口, 完全遵守 Collection 接口需要满足的协议, 而 Map 和 Collection 是完全不同的接口, 这点需要牢记于心. 还有其他的差别, 诸如:\nMultiset\u0026lt;E\u0026gt;出现的次数只能是正数, 没有任何元素的出现次数会是负数的, 出现次数为 0 的元素会被认为不存在, 这样的元素是不会出现在elementSet()和entrySet()的返回结果中的. 而Map\u0026lt;E, Integer\u0026gt;肯定不会有这样的限制. multiset.size()返回所有元素出现次数之和, 如果想要知道有多少个不重复的元素, 可以使用elementSet().size(), 例如{a,a,b}, elementSet.size()返回结果是 2, multiset.size()返回结果是 3. multiset.iterator()用于迭代每个出现的元素, 所以迭代次数和multiset.size()的值一样的. Multiset\u0026lt;E\u0026gt;支持增加元素, 删减元素, 或者通过 setCount 函数直接设置元素的出现次数, setCount(a, 0)的意思等于将删除所有的 a 元素. multiset.count(elem): 如果元素 elem 不存在, 那么返回值总是 0. 而 Map 对于不存在的元素, 返回的是 null . 2.3 Multiset 实现 鉴于 Multiset 只是个接口, Guava 提供许多的接口实现, 大致可以与 Java 中的容器对应上:\nMap MultiSet 支持 null 元素 HashMap HashMultiset Yes TreeMap TreeMultiset Yes LinkedHashMap LinkedHashMultiset Yes ConcurrentHashMap ConcurrentHashMultiset No ImmutableMap ImmutableMultiset No 3 Multimap 又来假设, 你是个班主任, 刚刚考完试, 你想记录下班里所有同学的成绩, 你可能写下这样的实现:\nInteger studentScore = 60; String studentName = \u0026#34;Alan\u0026#34;; // 每个学生的成绩单 Map\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt; studentScoresMap = new HashMap\u0026lt;\u0026gt;(); // 如果 Alan 还没记录各科成绩的列表, 就新建一个列表 List\u0026lt;Integer\u0026gt; studentScores = studentScoresMap.get(studentName); if (studentScores == null) { studentScores = new ArrayList\u0026lt;\u0026gt;(); studentScoresMap.put(studentName, studentScores); } // 然后将某个科目的成绩加进去 studentScores.add(studentScore); 一个学生考试要考多个科目, 自然就会有多个学科成绩, 也就出现了一个 key 需要对应多个 value 的情况. 使用Map\u0026lt;K, List\u0026lt;V\u0026gt;\u0026gt;或者Map\u0026lt;K, Set\u0026lt;V\u0026gt;\u0026gt;这样的方式构建 key-values 自然可以, 只不过显得不甚优雅.\n为此, Guava 提供了新的容器类型来应对一个 key 对应多个 values 的场景: Multimap . 同样的, 我们也可以从两个角度来理解 Multimap :\n一个 key 对应一个 value , 同样的 key 可以存在多个: a -\u0026gt; 1 a -\u0026gt; 2 a -\u0026gt; 4 b -\u0026gt; 3 c -\u0026gt; 5 或者一个 key 对应一个列表的 value : a -\u0026gt; [1, 2, 4] b -\u0026gt; [3] c -\u0026gt; [5] 通常来说, 最好以第一种方式来理解 Multimap 接口, 不过你也可以以第二种方式来获取数据: asMap()函数, 返回一个 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; 对象.\n需要注意的是, 不存在 1 个 key 对应 0 个 value 的情况, 不会有空的值列表这样的说法, 要不一个 key 对应至少一个 value , 要不就是这个 key 不存在于这个 Multimap .\n一般来说, 我们不会直接使用 Multimap 接口, 使用的是它的子接口; Multimap 接口提供了两个子接口: ListMultimap 和 SetMultimap , 大致类似于 Map\u0026lt;K, List\u0026lt;V\u0026gt;\u0026gt;和 Map\u0026lt;K, Set\u0026lt;V\u0026gt;\u0026gt;.\n3.1 Multimap 的例子 现在让我们用 Multimap 重新实现一次学生不同科目的成绩单:\nString alan = \u0026#34;Alan\u0026#34;; String turing = \u0026#34;Turing\u0026#34;; // 创建一个 ListMultimap ListMultimap\u0026lt;String, Integer\u0026gt; studentScoresMap = MultimapBuilder.hashKeys().arrayListValues().build(); studentScoresMap.put(alan, 95); studentScoresMap.put(alan, 88); studentScoresMap.put(turing, 100); Assert.assertEquals(3, studentScoresMap.size()); List\u0026lt;Integer\u0026gt; alanScores = studentScoresMap.get(alan); Assert.assertEquals(2, alanScores.size()); alanScores.clear(); Assert.assertEquals(0, studentScoresMap.get(alan).size()); 3.1.1 构造 细心的同学可能会发现, 上面创建 ListMultimap 的方式不是直接调用实现类的.create()函数, 而是使用 MultimapBuilder .\n并不是 Multimap 的实现没有提供.create()方法, 是通过 MultimapBuilder 创建 Multimap 实现会更加便利一点, 使用hashKeys()函数创建的就是一个 HashMap , 使用treeKeys()函数创建的就是一个 TreeMap .\n3.1.2 修改 Multimap.get(key)返回的就是特定 key 关联的集合, 对于一个 ListMultimap , 返回的就是一个 List ; 对于一个 SetMultimap , 返回的就是一个 Set .\n实际返回的是集合的引用, 所以对这个返回集合的操作, 将直接反馈在 Multimap 实例上. 如上面的例子所示, 把学生 alan 返回列表的数据清空, 在 ListMultimap 的数据也相应地被清空了.\n3.2 视图 所谓的视图(Views), 我理解就是看待事物的方式和角度, 称为视图(或者视角\u0026rsquo;perspective\u0026rsquo;).\nMultimap 提供了若干个有用的视图:\nasMap 把 Multimap\u0026lt;K,V\u0026gt; 看作一个 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; , 返回一个的 map 对象支持 remove 操作, 但不支持 put 和 putAll 操作.\n值得关注的是: 当对应的 key 不存在的时候, multiMap 返回的是一个新构造的, 为空的集合, 如果你想在对应的 key 不存在的时候返回空指针(就好像 HashMap 那样), 你可以通过 asMap().get(key) 实现这样的效果\nstudentScoresMap.asMap().remove(alan); // 抛出 UnsupportedOperationException 异常 studentScoresMap.asMap().put(\u0026#34;key\u0026#34;, Lists.newArrayList()); // 抛出 UnsupportedOperationException 异常, 除非 anotherScores 是个空的 Map studentScoresMap.asMap().putAll(anotherScores); // 返回空的集合 Collection\u0026lt;Integer\u0026gt; Elons = studentScoresMap.get(\u0026#34;Elon\u0026#34;); // 返回空指针 studentScoresMap.asMap().get(\u0026#34;Elon\u0026#34;); entries 把 Multimap 内所有的记录(entry)看作 Collection\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt;, 如前文的 studentScoresMap.entries() 返回的就是: [{\u0026quot;Alan\u0026quot;: 95}, {\u0026quot;Alan\u0026quot;: 88}, {\u0026quot;Turing\u0026quot;: 100}]. keySets 把 Multimap 内所有的不重复的 key 看作一个 Set . 如前文的 studentScoresMap.keySets() 返回的就是: Set([\u0026quot;Alan\u0026quot;,\u0026quot;Turing\u0026quot;]). keys 把 Multimap 内所有的 key 看作一个前文提到的 Multiset , 可以从这个 Multiset 删除元素, 但不能新增元素, 如前文的 studentScoresMap.keys() 返回的就是: Multiset([\u0026quot;Alan\u0026quot;,\u0026quot;Alan\u0026quot;, \u0026quot;Turing\u0026quot;]). values() 把 Multimap 内所有的 value 看作一个集合, 相当于把所有 key 对应的 value 集合串联起来, 如前文的 studentScoresMap.values() 返回的就是: [95, 88, 100] 3.3 Multimap 也不是一个 Map 严格来说, 即使 Multimap 名字中带有 map, 甚至 map 可能用来实现 Multimap , 但一个 Multimap\u0026lt;K,V\u0026gt; 终究不是一个 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt;. 它们之间的差异包括:\nMultimap.get(key) 返回的对象总是不为空指针的, 即使查询的 key 不存在, 返回的是个空的集合. 而 Map.get(key) 查询的 key 不存在, 返回的就是空指针. 前文提到过, 如果想要让 Multimap 在查询 key 不存在的时候返回空指针, 可以使用 Multimap.asMap().get(key). Multimap.containsKey(key) 在 values 集合为空的时候就会返回 false, 例如 studentScoresMap.putAll(\u0026quot;elon\u0026quot;, Lists.newArrayList()); Assert.assertFalse(studentScoresMap.containsKey(\u0026quot;elon\u0026quot;)), 但对于 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; 而言, 返回的就会是 true, 因为 value 不为 null. Multimap.size() 返回的是所有记录的总数的, 即把所有的 value 的数量累加起来, 而 Map\u0026lt;K, Colleciton\u0026lt;V\u0026gt;\u0026gt; 返回的就是 key 对应的数量. 3.4 实现 Multimap 提供了若干个不同类型的实现, 你可以使用对应的实现来取代原来 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; 的地方:\n实现 key 表现得类似\u0026hellip; value 表现得类似\u0026hellip; ArrayListMultimap HashMap ArrayList HashMultimap HashMap HashSet LinkedListMultimap LinkedHashMap LinkedList LinkedHashMultimap LinkedHashMap LinkedHashSet TreeMultimap TreeMap TreeSet ImmutableListMultimap ImmutableMap ImmutableList ImmutableSetMultimap ImmutableMap ImmutableSet 上述的实现, 除了不可变的实现之外, 其他都支持 null key 与 null value. 并非所有的实现底层用的都是 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt;, 有好几个实现出于性能的考虑, 实现了自定义的 hash 表.\nMultimap 还支持自定义 value 的集合形式, 如 List 形式或者 Set 形式, 详情可见 Multimaps.newMultimap(Map, Supplier\u0026lt;Collection\u0026gt;)\n4 BiMap 继续假设, 你是个班主任, 你有个学生名字与学号的名单, 你有时会通过名字查询对应学号, 有时又会根据学号反查询学生名字, 通常来说, 你会这么实现这个名单:\nMap\u0026lt;String, String\u0026gt; nameToId = Maps.newHashMap(); Map\u0026lt;String, String\u0026gt; idToName = Maps.newHashMap(); nameToId.put(\u0026#34;Linus\u0026#34;, \u0026#34;0001\u0026#34;); idToName.put(\u0026#34;0001\u0026#34;, \u0026#34;Linus\u0026#34;); // 如果 0001 这个学号, 或者 Linus 这个名字已经存在了, 会发生什么事情呢? // 会出现很微妙的 bug, 为了避免出现这种情况, 你需要手动维护这种限制 不得不说, 通过两个 Map 和实现 value 反查 key 的传统做法并不优雅, 即增加了心理负担, 又容易出 bug.\n幸运的是, Guava 有一个名为 BiMap 类库, 提供了通过 value 也反查 key 的特性. 一个BiMap\u0026lt;K,V\u0026gt;是一个Map\u0026lt;K,V\u0026gt;, 提供了如下功能:\n允许通过 inverse() 函数调转 key-value, 从 Map\u0026lt;K,V\u0026gt; 变成 Map\u0026lt;V,K\u0026gt; 保证所有的 value 都是唯一的, values() 函数返回一个包含所有 value 的 Set 如果 value 已经存在, 那么 BiMap.put(key,value) 会抛出一个 IllegalArgumentException 异常, 如果想强制删除掉原来的 value , 并插入一对新的 key-value, 可以使用 Bimap.forcePut(key,value) 4.1 BiMap 例子 让我们用 BiMap 来重新实现学生名字和学号的名单:\nBiMap\u0026lt;String, String\u0026gt; userId = HashBiMap.create(); userId.put(\u0026#34;Linus\u0026#34;, \u0026#34;0001\u0026#34;); String user = userId.get(\u0026#34;Linus\u0026#34;); // 反向查询, 通过学号查询名字. String idForUser = userId.inverse().get(\u0026#34;0001\u0026#34;); // 抛出异常: java.lang.IllegalArgumentException: value already present: 0001 userId.put(\u0026#34;RMS\u0026#34;, \u0026#34;0001\u0026#34;); 4.2 BiMap 实现 key-value map 实现 value-key map 实现 对应的 BiMap HashMap HashMap HashBiMap ImmutableMap ImmutableMap ImmutableBiMap EnumMap EnumMap EnumBiMap EnumMap HashMap EnumHashBiMap 5 Table 假设还是个班主任, 现在你需要制作一个包含学号, 姓名与成绩的名单, 然后可以通过姓名或者学号进搜索, 你会怎么实现呢? 什么? 用 excel? 你好幽默啊!\n// key: 学号, value: {姓名: 成绩} Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; studentScores = Maps.newHashMap(); Map\u0026lt;String, Integer\u0026gt; linus = Maps.newHashMap(); linus.put(\u0026#34;Linus\u0026#34;, 99); studentScores.put(\u0026#34;0001\u0026#34;, linus); // 通过学号获取成绩 Assert.assertEquals(1, studentScores.get(\u0026#34;0001\u0026#34;).size()); for (Map.Entry\u0026lt;String, Integer\u0026gt; element : studentScores.get(\u0026#34;0001\u0026#34;).entrySet()) { String name = element.getKey(); Integer scores = element.getValue(); } // 通过姓名获取成绩 for (Map.Entry\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; element : studentScores.entrySet()) { String id = element.getKey(); Map\u0026lt;String, Integer\u0026gt; nameScores = element.getValue(); if (nameScores.containsKey(\u0026#34;Linus\u0026#34;)) { Integer score = nameScores.get(\u0026#34;Linus\u0026#34;); } } 不得不说, 用 Map\u0026lt;R, Map\u0026lt;C, V\u0026gt;\u0026gt; 的形式来实现多 key 搜索非常难受, 算法效率变为 O(n), 线性时间复杂度, 不但不优雅, 还容易出错, 如果我是班主任, 我就辞职了, 给我个 excel 不行么?\nexcel 是没有的了, 但是 Guava 提供了一个类 excel 的多 key 存储/搜索的容器: Table, 它支持以行和列维度搜索.\n5.1 Table 例子 让我们用 Table 重新实现一次可根据姓名与学号进行搜索的成绩单:\n// 从左到右各列分别是: 学号, 姓名, 成绩 Table\u0026lt;String, String, Integer\u0026gt; idNameScoreTranscript = HashBasedTable.create(); idNameScoreTranscript.put(\u0026#34;0001\u0026#34;, \u0026#34;Linus\u0026#34;, 99); idNameScoreTranscript.put(\u0026#34;0002\u0026#34;, \u0026#34;Aaron\u0026#34;, 100); idNameScoreTranscript.put(\u0026#34;0001\u0026#34;, \u0026#34;RMS\u0026#34;, 98); idNameScoreTranscript.put(\u0026#34;0004\u0026#34;, \u0026#34;RMS\u0026#34;, 97); /// 返回结果: /// Linus: 99 /// RMS: 98 idNameScoreTranscript.row(\u0026#34;0001\u0026#34;); /// 返回结果: /// 0001: 98 /// 0004: 97 idNameScoreTranscript.column(\u0026#34;RMS\u0026#34;); 正常来说, 不会有两个学号一样的学生, 只是为了展示 Table 用法而这样造数据. row, column 函数可能让人比较迷惑, 这两个函数是怎么搜索的?\n其实很简单, row 是以第一个 key 来搜索, 而 column 以第二个 key 来搜索, 如图:\nFigure 2: row: 以第一个 key 来搜索\nFigure 3: column: 以第二个 key 来搜索\n5.2 Table 视图 一往常, Table 也提供了若干个视图:\nrowMap(), 把 Table\u0026lt;R, C, V\u0026gt; 看作一个 Map\u0026lt;R, Map\u0026lt;C, V\u0026gt;\u0026gt;, 同样的, rowKeySet()返回一个Set\u0026lt;R\u0026gt;. row(r) 返回一个非空的 Map\u0026lt;C, V\u0026gt; 的引用, 对返回的 Map 的修改也会反馈给持有该引用 Table. 类似地, column(c) 返回一个非空的 Map\u0026lt;R, V\u0026gt; 的引用, 对返回的 Map 的修改也会反馈给持有该引用 Table. cellSet() 把Table\u0026lt;R, C, V\u0026gt;看作一个 Table.Cell\u0026lt;R, C, V\u0026gt;, Cell 与 Map.Entry 十分类似, 只不过它有两个 key, 形式是 (r,c)=v, 而 Map.Entry 是 key = value. 5.3 Table 实现 Table 依旧提供了若干个实现, 列表如下:\nTable\u0026lt;R, C, V\u0026gt; 类似的 Map\u0026lt;R, Map\u0026lt;C, V\u0026gt;\u0026gt; HashBasedTable HashMap\u0026lt;R, HashMap\u0026lt;C, V\u0026gt;\u0026gt; TreeBasedTable TreeMap\u0026lt;R, TreeMap\u0026lt;C, V\u0026gt;\u0026gt; ImmutableTable ImmutableMap\u0026lt;R, ImmutableMap\u0026lt;C, V\u0026gt;\u0026gt; ArrayTable ImmutableMap\u0026lt;R, ImmutableMap\u0026lt;C, V\u0026gt;\u0026gt;, 特别的一个 6 ClassToInstanceMap 目前, 我们介绍过的 Map, 无论是原生 Jdk 的 Map, 抑或是 Guava 的 Map, key 都是同一个类型的.\n这是因为 Map 的签名是 Map\u0026lt;K,V\u0026gt;, 实例的时候, 只能实例成某具体一个类型的参数. 所谓凡事都有例外, 有没有支持 key 是不同类型的 map 呢? 自然是有的, Guava 的 ClassToInstanceMap 就可以支持多个类型的 key.\n为什么它可以实现多个类型的 key 呢? 因为 ClassToInstanceMap 的签名声明为 Map\u0026lt;Class\u0026lt;? extends B\u0026gt;, B\u0026gt;, 通过传入不同类型的 Class 对象, 实现类型不同的 =key=(如果你要说, 即使传入不同类型的 Class 对象, 它只有一个 Class, 没有实现多个不同类型的 key 阿! 你也可以这样理解, well, 咬文嚼字就没有什么意义了)\n6.1 ClassToInstanceMap 例子 ClassToInstanceMap\u0026lt;Number\u0026gt; numberDefault = MutableClassToInstanceMap.create(); numberDefault.put(Integer.class, 10); numberDefault.put(Long.class, 20L); // 编译失败 //numberDefault.put(String.class, \u0026#34;string\u0026#34;); Assert.assertEquals(Long.valueOf(20L), numberDefault.getInstance(Long.class)); 如果查看源码, 可以发现, ClassToInstanceMap\u0026lt;B\u0026gt; 只有一个类型参数 B:\npublic interface ClassToInstanceMap\u0026lt;B\u0026gt; extends Map\u0026lt;Class\u0026lt;? extends B\u0026gt;, B\u0026gt; 很明显的, 类型 B 限制了 key 与 value 的类型。\n对于 value 的限制, 就和常规的 map 一样; 而对于 key 而言, 泛型实例化时的参数类型只能是 B, 或者是 B 的子类, 例如: ClassToInstanceMap\u0026lt;Number\u0026gt;, 那么这个 map 的 key 类型必须是 Number 或 Number 的子类, 而传入的 Integer 和 Long 都是 Number 子类, 因此能编译通过。\n如果传入的是 String, 不符合声明, 编译就报错了.\n需要注意的是, 和 Map\u0026lt;Class, Object\u0026gt; 一样, 一个 ClassToInstanceMap 可以包含着是原始类型的 value, 而原始类型与它对应的包装类型并不是同一种类型, 不要混淆了哦\n6.2 ClassToInstanceMap 实现 ClassToInstanceMap 提供了两个实现:\nClassToInstanceMap 类似的 Map MutableClassToInstanceMap Map\u0026lt;Class, Object\u0026gt; ImmutableClassToInstanceMap ImmutableMap\u0026lt;Class,Object\u0026gt; 7 RangeSet 目前为止, 我们介绍过的新类型容器都是常见的 Map/Set/Table, 现在我们就来介绍一个表示区间的容器: RangeSet. 一个 RangeSet, 表示一个包含无连接的, 不为空的区间的集合, 例如包含一个整数区间的 RangeSet: {[1,5], [7,9)}.\n在 RangeSet 中, 区间是由类 Range 来表示的, 当把一个区间加入到一个可变的 RangeSet 时, 任何有交集的区间都会被合并, 为空的区间就会被忽略, 例如将区间 [3,5] 加入到已有的 RangeSet {[2,4]}, 就会被合并成 {[2,5]}, 这个也符合我们日常的生活经验.\n7.1 RangeSet 例子 让我们现在来看一下 RangeSet 的两个例子, 一个是整数的 RangeSet\nRangeSet\u0026lt;Integer\u0026gt; rangeSet = TreeRangeSet.create(); // {[2,4]} rangeSet.add(Range.closed(2, 4)); // {[2,5]} rangeSet.add(Range.closed(3, 5)); // {[1,10]} rangeSet.add(Range.closed(1, 10)); // 无连接上的区间: {[1,10], [11,15)} rangeSet.add(Range.closedOpen(11, 15)); // 连接上的区间; {[1,10], [11,20)} rangeSet.add(Range.closedOpen(15, 20)); // 空区间, 被忽略; {[1,10],[11,20)} rangeSet.add(Range.openClosed(0, 0)); // 分割区间 [1,10]; {[1,5],[10,10],[11,20)} rangeSet.remove(Range.open(5, 10)); 在上面的例子中, [2,4] 和 [3,5] 这两个区间有交集, 所以它们被自动合并到一起了, 而对于区间 [1,10] 和 [11,15), 10 相邻的整数就是 11, 但两个区间也没有合并起来, 因为它们没有相交, 如果想要他们合并起来, 可以手动调用 Range.canonical(DiscreteDomain), 即:\n// {[1,10]} rangeSet.add(Range.closed(1, 10).canonical(DiscreteDomain.integers())); // 连接上的区间: {[1,15)} rangeSet.add(Range.closedOpen(11, 15)); 另外一个例子是日期的 RangeSet:\nRangeSet\u0026lt;LocalDate\u0026gt; rangeSet = TreeRangeSet.create(); // {[2019-10-10, 2019-12-25]} rangeSet.add(Range.closed(LocalDate.parse(\u0026#34;2019-10-10\u0026#34;), LocalDate.parse(\u0026#34;2019-12-25\u0026#34;))); // {[2019-10-10, 2019-12-30)} rangeSet.add(Range.closedOpen(LocalDate.parse(\u0026#34;2019-12-24\u0026#34;), LocalDate.parse(\u0026#34;2019-12-30\u0026#34;))); Assert.assertTrue(rangeSet.contains(LocalDate.parse(\u0026#34;2019-10-20\u0026#34;))); // {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)} rangeSet.remove(Range.closed(LocalDate.parse(\u0026#34;2019-10-20\u0026#34;), LocalDate.parse(\u0026#34;2019-10-30\u0026#34;))); Assert.assertFalse(rangeSet.contains(LocalDate.parse(\u0026#34;2019-10-20\u0026#34;))); // [2019-11-10,2019-11-25] 在 `{[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}`的区间包围内 Assert.assertTrue(rangeSet.encloses(Range.closed(LocalDate.parse(\u0026#34;2019-11-11\u0026#34;), LocalDate.parse(\u0026#34;2019-11-20\u0026#34;)))); Assert.assertEquals(Range.closedOpen(LocalDate.parse(\u0026#34;2019-10-10\u0026#34;), LocalDate.parse(\u0026#34;2019-10-20\u0026#34;)), rangeSet.rangeContaining(LocalDate.parse(\u0026#34;2019-10-19\u0026#34;))); // {[2019-10-10, 2019-12-30)} Range\u0026lt;LocalDate\u0026gt; span = rangeSet.span(); Assert.assertEquals(LocalDate.parse(\u0026#34;2019-10-10\u0026#34;), span.lowerEndpoint()); Assert.assertEquals(LocalDate.parse(\u0026#34;2019-12-30\u0026#34;), span.upperEndpoint()); RangeSet 提供了若干个查询函数, 用法在上面的代码已经展示了, 查询函数列表:\ncontains(C): RangeSet 最基础的查询操作, 判断任意的元素是否在 RangeSet 内. rangeContaining(C): 与 contains(C) 类似, 判断任意的元素是否在 RangeSet 内, 如果在的话返回一个对应的区间, 否则返回空指针. 如上代码, 有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 而元素 2019-10-19 在区间 [2019-10-10, 2019-10-20) 内, 因此 rangeContaining(C) 函数返回的就是 [2019-10-10, 2019-10-20). encloses(Range\u0026lt;C\u0026gt;): 判断任意的区间是否在 RangeSet 的包围中. span: 返回一个最小区间, 包含 RangeSet 中的所有区间, 如有: RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, span 函数返回的区间就是 {[2019-10-10, 2019-12-30)}. 7.2 RangeSet 视图 依照惯例, RangeSet 也提供了若干个视图:\ncomplement(): 返回某个 RangeSet 的补集, 返回结果也是个 RangeSet, 如有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 那它的补集就是: RangeSet: {(-∞,2019-10-10), [2019-10-20,2019-10-30], [2019-12-30,+∞)}, 分别是三个区间: 负无穷到 2019-10-10, 2019-10-20 到 2019-10-30, 以及 2019-12-30 到正无穷. subRangeSet(Range\u0026lt;C\u0026gt;): 返回某个 RangeSet 相交的子区间, 如有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 取子区间 [2019-11-10,2019-11-20], 那么返回结果就是 {[2019-11-10, 2019-11-20]}; 如果取子区间 [2019-10-15, 2019-11-20], 那么返回结果就是 {[2019-10-10, 2019-10-20), (2019-10-30, 2019-11-20]} asRanges(): 把 RangeSet 当作一个 Set\u0026lt;Range\u0026lt;C\u0026gt;\u0026gt;, 如有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 返回结果就是: Set({[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}) 7.3 RangeSet 实现 RangeSet 提供了两个实现:\nRangeSet 类似的 Set\u0026lt;Range\u0026gt; TreeRangeSet TreeSet\u0026lt;Range\u0026gt; ImmutableRangeSet ImmutableSet\u0026lt;Range\u0026gt; 8 RangeMap 既然能以区间集作为容器, 那么能否把区间当作 Map 的 key 呢? 答案是当然可以, Guava 就提供了一个这样的容器: RangeMap.\n需要注意的是, 不像 RangeSet 那样, 相邻或者相交的区间不能连接起来的, 即使毗邻的区间映射的是同一个 value.\n9 RangeMap 例子 RangeMap\u0026lt;Integer, String\u0026gt; rangeMap = TreeRangeMap.create(); // {[1,10] =\u0026gt; \u0026#34;foo\u0026#34;} rangeMap.put(Range.closed(1, 10), \u0026#34;foo\u0026#34;); // {[1, 3] =\u0026gt; \u0026#34;foo\u0026#34;, (3, 6) =\u0026gt; \u0026#34;bar\u0026#34;, [6, 10] =\u0026gt; \u0026#34;foo\u0026#34;} rangeMap.put(Range.open(3, 6), \u0026#34;bar\u0026#34;); // {[1, 3] =\u0026gt; \u0026#34;foo\u0026#34;, (3, 6) =\u0026gt; \u0026#34;bar\u0026#34;, [6, 10] =\u0026gt; \u0026#34;foo\u0026#34;, (10,20) =\u0026gt; \u0026#34;foo\u0026#34;} rangeMap.put(Range.open(10, 20), \u0026#34;foo\u0026#34;); // {[1, 3] =\u0026gt; \u0026#34;foo\u0026#34;, (3, 5) =\u0026gt; \u0026#34;bar\u0026#34;, (11, 20) =\u0026gt; \u0026#34;foo\u0026#34; rangeMap.remove(Range.closed(5, 11)); Assert.assertSame(\u0026#34;foo\u0026#34;, rangeMap.get(3)); Range\u0026lt;Integer\u0026gt; span = rangeMap.span(); Assert.assertEquals(span.lowerEndpoint(), Integer.valueOf(1)); Assert.assertEquals(span.upperEndpoint(), Integer.valueOf(20)); // {[12, 15]} =\u0026gt; \u0026#34;foo\u0026#34; RangeMap\u0026lt;Integer, String\u0026gt; subRangeMap = rangeMap.subRangeMap(Range.closed(12, 15)); Assert.assertEquals(subRangeMap.span().lowerEndpoint(), Integer.valueOf(12)); Assert.assertEquals(subRangeMap.span().upperEndpoint(), Integer.valueOf(15)); Assert.assertEquals(\u0026#34;foo\u0026#34;, subRangeMap.get(12)); RangeMap 提供的查询函数不多, 满打满算也只有 get(K) 和 span 这两个函数.\n9.1 RangeMap 视图 RangeMap 提供的视图也不多, 只有两个:\nasMapOfRanges(), 把 RangeMap 看作一个 Map\u0026lt;Range\u0026lt;K\u0026gt;, V\u0026gt;, 可以用来遍历 RangeMap subRangeMap(Range\u0026lt;K\u0026gt;), 返回某个 RangeMap 相关区间的子区间以及对应的 value, 如有 RangeMap: {[1, 3] =\u0026gt; \u0026quot;foo\u0026quot;, (3, 5) =\u0026gt; \u0026quot;bar\u0026quot;, (11, 20) =\u0026gt; \u0026quot;foo\u0026quot;, 取子区间 [12,15], 返回结果就是 {[12, 15]} =\u0026gt; \u0026quot;foo\u0026quot;; 如果取子区间[4,12], 返回结果就是: {[4,5) =\u0026gt; bar, (11, 12] =\u0026gt; foo} 9.2 RangeMap 实现 RangeMap 提供了两个实现:\nRangeMap 类似的 Map\u0026lt;Range, V\u0026gt; TreeRangeMap TreeMap\u0026lt;Range, V\u0026gt; ImmutableRangeMap ImmutableMap\u0026lt;Range\u0026lt;K, V\u0026gt;\u0026gt; 10 总结 介绍完新类型的容器之后, 希望大家对这些新类型容器熟悉起来, 应对需求来也能得心应手 :)\n","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E6%96%B0%E7%B1%BB%E5%9E%8B%E5%AE%B9%E5%99%A8/","summary":"1 前言 JDK 提供了各种功能强大的工具类, 宛如装备齐全的军火库, 而容器就是其中一项内置的利器, 提供了包括诸多常用的数据结构, 下图对 JDK 已有容器进行了","title":"guava 探究系列之五:新类型容器"},{"content":" 一本披着悬疑小说外衣的西方哲学史.\n1 前言 关于哲学, 一般人第一反应应该是那三个有名的哲学问题, 灵魂三问(配上相应的表情包的食用更佳):\n我是谁? 我来自哪里? 我在做什么?\n我像其他人一样, 对于哲学的概念就只有上面三个问题, 以及高中政治里面提到的各种\u0026quot;正确的\u0026quot;, \u0026ldquo;错误的\u0026quot;哲学概念.\n记得在大一的时候, 有诸多空闲时间, 当时就想找本科普读物, 了解哲学的概念.\n毕竟,计算机科学的尽头是物理, 物理的尽头是数学, 数学的尽头是哲学, 哲学的尽头是神学.(第一句是我自己加的), 正本清源一番也不错, 只是没有预料到的是, 这本书要4年之后才会看完.\n2 讲故事的高超技巧 个人觉得《苏菲的世界》 最让人称道的是说故事的方式, 全书前半部分的内容如果拍成电影, 那么应该是部惊悚电影.\n经常会出现艾伯特和苏菲聊着聊着哲学的时候, 出现一些不可思议的事情, 例如艾伯特的狗汉密士会开口说话, 诸如此类的事情.\n其次还有贯穿全书前半部分的疑团, 席德是谁? 她和苏菲有何关系? 她的少校父亲又是谁? 为何他会被艾伯特隐晦地称为上帝?\n相信其他初读此书的读者会像我一样, 落入到层层迷雾之中.\n而到全书过半, 到\u0026quot;柏客来\u0026quot;一章, 所有的谜团才被一一揭晓: 原来前面那么长篇幅描写的哲学家艾伯特和中学生苏菲都是虚构的, 只是席德的生日礼物中的角色.\n读到这一章的时候, 着实有种拍案叫绝的感觉, 为作者的构思所深深折服. 这让我想起了楚门的世界, 相信苏菲会和楚门有相同的感触; 原来自己的世界, 只是别人精心设计的剧本, 原来一切都是虚无, 如梦幻泡影.\n同时, 一个想法不可抵制地浮上水面: 如果苏菲只是席德讲义夹的一个人物, 席德也不过是手上的一本书的虚拟的中学生, 而我又会是谁操纵的人偶呢?\n这个想法有点可怕, 现在想来, 如果同样内容的故事, 如果以线性的述事方式进行平铺直述, 相信感染力会大打折扣, 而让书中人物和读者相互交错, 相互影响的述事方式, 想来也是作者的\u0026quot;诡计\u0026quot;之一, 让读者像哲学家一样思考自己存在的意义.\n3 哲学时间线 《苏菲的世界》以苏菲和艾伯特的经历为骨架, 串连起整个西方哲学史, 而粘连在骨架上的血肉就是一个个的哲学家和哲学理论, 把冰冷的哲学概念变成鲜活的体会, 寓教于乐中. 鉴于里面的内容繁多, 我也不对每个哲学家的理论感兴趣, 所以就把近三千年的重要哲学流派做成时间线, 耗费了近一下午的工夫才画完:\nFigure 1: 西方哲学史\n其中红色箭头指影响和传承, 黑色箭头指的是时间线顺序, 例如柏拉图生活的时期在苏格拉底之后, 其哲学理论又受到苏格拉底的学说的影响. 所以有一红一黑两个箭头从苏格拉底指向柏拉图.\n4 我看哲学 目前读过两本哲学书, 一本是《苏菲的世界》, 另外一本是高中政治教材《哲学生活》.\n看完《苏菲的世界》之后, 才发现, 哲学不会有明确的正确/错误之分, 不会有人轻易地给一种哲学下定论, 哲学是一种看待问题的方式, 指导我们如何寻求问题的答案.\n一个不持怀疑态度, 轻易对各种问题下定论的人, 既武断, 也殊为不智. 纵观整个西方哲学史, 我打算来谈谈我感兴趣的哲学家和哲学理论.\n4.1 圣奥古斯丁和圣多玛斯 引起我注意的是两位中世纪的哲学家(也可以称为神学家), 但是我感兴趣的不是他们的哲学, 而是他们阐述自己理论的手段和方式.\n众所周知, 中世纪也被称为黑暗时代, 在近千年的时间, 教会控制了所有的教育和思想, 神学大行其道, 上帝几近成为唯一的真理, 古希腊哲学毫无疑问地被基督教会钳制, 这个时候推行柏拉图和亚里斯多德哲学思想的难度可想而知.\n而圣奥古斯丁和圣多玛斯能成为中世纪最著名的哲学家, 自然有其独到之处, 而他们都做着一件类似的事情, 将古希腊的哲学思想进行包装, 进行基督教化, 为其披上宗教的外衣, 用基督教的\u0026quot;新瓶\u0026quot;装上古希腊的旧酒. 这种巧妙的传道方式不得不让人赞赏, 实乃大智慧.\n4.2 黑格尔 黑格尔用自己的智慧对历史和真理做了概述性总结, 就真理而言, 真理是主观的,他不承认在人类的理性之外有任何\u0026quot;真理\u0026quot;存在。他说,所有的知识都是人类的知识。思想(或理性)的历史就像河流。人的思考方式乃是受到宛如河水般向前推进的传统思潮与当时的物质条件的影响。\n因此你永远无法宣称任何一种思想永远是对的。只不过就你当时所置身之处而言,\n这种思想可能是正确的.\n老实说, 《苏菲的世界》全书提到不同时期, 不同类型的哲学, 但是很多哲学的观点我到现在都没有明白其意思, 也有很多看懂后, 并不赞同的观点, 而黑格尔的观点正好是我能理解, 又为之赞同的观点, 这是一种与时俱进, 闪烁着人类智慧光芒的见解.\n思想也不再会简单地被评判成正确/错误, 一切都需要与时代结合. 举例来说, 牛顿的三大定律在他的时代, 甚至之后的几百年都是正确的, 只是忽略了特定的情况, 而爱因斯坦的相对论把这个短板给弥补上了, 我觉得以黑格尔的历史观解释, 加上时代的限制, 则牛顿和爱因斯坦的观点也都是正确的.\n4.3 马克思 建国30年的时间都是以马克思主义哲学来建设的, 效果怎么样, 历史已有定论. 此外《苏菲的世界》的中文版(萧宝森译)中(就是我看的这版), 部分内容被中国政府(文化产业部)要求删除,如马克思部分结尾处的32个段落. 所以, 不言自明咯.\n5 名句 你觉得自己好像刚从一个梦幻中醒来. 我是谁? 你问道.\n我有时会有这种奇怪的想法, 很多时候是在读书或者写字, 突然对这些汉字感到陌生, 这些是什么? 为什么我在看这些图案(文字)?\n遗憾的是, 当我们成长时, 不仅习惯了有地心引力这回事, 同时也很快地习惯了世上的一切. 我们在成长的过程中,似乎失去了对世界的好奇心.\n事实的确如此. 因为我们都长大了.\n由于种种理由.大多数人都忙于日常生活的琐事,因此他们对于这世界的好奇心都受到压抑.\n像我现在这样天天加班,压抑的又何止好奇心呢?\n每一个生物都是理型世界中永恒范式的不完美复制品 \u0026ndash; 柏拉图\n而\u0026quot;真理\u0026quot;就是这个过程, 因为在这个历史的过程之外, 没有外在的标准可以判定什么是最真, 最合理的.\n你不能从古代, 中世纪, 文艺复兴时期或启蒙运动时期挑出一些思想, 然后说它们是对的, 或是错的. 同样的, 你也不能说柏拉图是错的, 亚理士多德是对的, 或者说休姆是错的, 而康德或谢林是对的, 这样的思考方式是反历史的.\n6 总结 最后, 借用我看到的一个比喻, 《苏菲的世界》就像一本哲学的请帖, 如果对请帖上说到的菜感兴趣的话, 就可以去找地方\u0026quot;试吃\u0026rdquo;, 如果你对《苏菲的世界》上提到的某道菜感兴趣, 想去试吃的话, 说明《苏菲的世界》, 这份请帖写得非常合格呢.\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E8%8B%8F%E8%8F%B2%E7%9A%84%E4%B8%96%E7%95%8C/","summary":"一本披着悬疑小说外衣的西方哲学史. 1 前言 关于哲学, 一般人第一反应应该是那三个有名的哲学问题, 灵魂三问(配上相应的表情包的食用更佳): 我是谁?","title":"小说体西方哲学史: 苏菲的世界"},{"content":"1 前言 以前没有读过王小波作品时候, 经常能看到王小波身上的各种标签, 中国第一代程序员, 现代伟大作家诸如此类.\n但是没有读过王小波的作品, 对于他的印象终究是道听途说, 所以抽空把王小波《沉默的大多数》这部作品看完.\n以前以为这作品批判的是大多数无主见, 只会冷眼旁观的麻木的芸芸众生. 然而真正读完这部作品之后, 才会发现, 根本不是这样的.\n2 韩寒《青春》 记得我在大学时候读过韩寒的一本杂文集, 名为《青春》, 当时记得是在二手书摊里低价买回来的, 当时只是想看一下, 这位颇具争议的\u0026quot;名作家\u0026quot;的作品究竟如何.\n后来看下去才发现, 原来\u0026quot;杂文集\u0026quot;是博客文章的集合, 韩寒的很多文章看起来都有意针砭时弊, 但是却总给人一种流于表面的感觉, 无处着力, 并没有对种种社会现象的背后深层原因进行分析.\n像他那些批评政府的博文, 阅读起来感觉很爽. 但是看完之后并没有对各种社会问题的背后原因有多少了解, 感觉就是避重就轻.\n3 历史与真相 《沉默的大多数》读下去之后, 也发现这部作品也是王小波各种专栏作品的集合, 但是不同于《青春》, 《沉默的大多数》确实给了我一种杂文的感觉, 毕竟我对杂文的印象还停留在鲁迅的文章中.\n与鲁迅先生的作品类似, 《沉默的大多数》也对诸多的社会现象进行了批评和讽刺, 只是王小波的行文风格不如鲁迅先生如刀枪那般硬朗, 王小波更多的是幽默, 更为内敛的讽刺. 例如王小波提到的优越感的时候, 说到:\n有些作品. 有些人能欣赏, 有些人则看不懂, 这就是说, 有些人的幸福能力较为优越. 这个优越最招人嫉妒.\n消除这种优越感的方法之一就是给聪明人头上一闷棍, 把他打笨. 但打轻了不管用, 打重了会把脑子打出来, 这又不是我们的本意. 另一种方法则是: 一旦聪明人和傻人发生了争执, 我们总说傻人有理, 久而久之, 聪明人也会变傻. 这种法子现在正用着呢.\n对, 这种方法现在也正用着呢. 诸如此类睿智又充满机锋的句子在文中比比皆是.\n作为历史的亲历者, 又作为历史的见证者, 像王小波这样坦白敢言的作者的确不多, 在说到他们上山下乡的时候, 明确说到这是一个大坏事, 好事就是好事, 坏事就是坏事, 不需要文过饰非, 这的确是难得的品格. 毕竟当全部人都在说1+1=3的时候, 你说出1+1=2是多么的难能可贵.\n毕竟大部分的君王都是中亚古国花剌子模的君王那般, 给君王带来好消息的信使, 就会得到提升, 给君王带来坏消息的人则会被送去喂老虎, 人都会趋利避害, 就没人敢说真话了, 现在都没人敢说真话了, 说真话的那个人都被海葬了.\n突然明白书名《沉默的大多数》的意思, 原来你不是在批判沉默的大多数, 而是你在写坚持做那少数人的历程, 正如你所说, 这是一个熵减的过程.\n4 国学 天不生仲尼, 万古长如夜. 高中的时候, 时常拿这个当作作文的点睛之笔, 即使和作文的主题不搭, 也可以拿来用, 无须顾忌.\n对于国学, 古人也是这般使用. 关于王小波对于孔子和孟子的评价, 我是持赞同态度的, 对于孔子强调的礼, 王小波认为和\u0026quot;文化革命\u0026quot;里搞的那些仪式差不多.\n什么早请求晚汇报, 他都经历过. 我也觉得, 这些礼对于人幸福的提高, 学识的增长没有什么帮助, 为的是区别阶级和权利, 无权者向有权者行礼, 这才是核心.\n这就好像我司用工牌带子的颜色来区别阶级一样, 本质都是权力和地位. 至于孟子, 王小波觉得孟子甚偏执, 表面上体面, 其实心底有股邪火, 比方说,他提到墨子,杨朱, \u0026ldquo;无君无父, 是禽兽也\u0026rdquo;.\n不同意见, 就觉得别人是禽兽, 这样的学术胸怀不甚宽广. 我最喜欢的是王小波关于这么多读书人把四书五经研究了两千年的比喻:\n二战时期, 有一位美国将军深入敌后, 不幸被敌人堵在了地窖里, 敌人在头上翻箱倒柜, 他的一位随行人员却咳嗽起来 . 将军给了随从一块口香糖让他嚼, 以此压制咳嗽, 但是该随从嚼了一会儿, 又伸手来要, 理由是: 这一块太没味道. 将军说: 没味道不奇怪, 我给你之前已经嚼了两个钟头了| 我举这个例子是要说明, 四书五经再好, 也不能几千年地念; 正如口香糖再好吃, 也不能抱着人地嚼\u0026hellip;\n过去钻石四书五经, 现在钻研《红楼梦》. 我承认, 我们晚生一辈在这方面差得很远, 但也未尝不是一件好事. 四书五经也好, 《红楼梦》也罢, 本来只是几本书, 却硬要把整个大千世界都塞在其中. 我相信世界不会因此得益, 而是因此受害.\n要把大千世界硬塞进的书何止是《红楼梦》, 还有某些主义书籍, 某些思想书籍. 以前的人以以前的四书五经治国, 现在的人以现在的四书五经治国, 历史并没有发生改变, 只是轮回了.\n5 尊严 《沉默的大多数》一书最后的篇幅都是用来讲述尊严的, 在我们的国家里, 言必称集体, 话必及党国, 平常很少提到个人, 中学政治书也曾经提到, 当个人利益与集体利益发生冲突时,诚信守则要求我们站在集体利益一边.\n而在王小波看来, 在国外人们对时事做出价值评判的时, 总是从两个独立的方面来进行: 一个方面是国家或者社尊严, 这像是时事的经线; 另一个方面是个人尊严, 这像时事的纬线.\n回到国内, 一条纬线就像是没有, 连尊严这个字眼也感到陌生了.\n这不禁让我想起美国梦和中国梦, 人民网甚至发文对比过美国梦和中国梦, 循例作了一番批判. 列出七大点, 例如中国梦是国家的富强, 美国梦是个人的富裕; 中国梦的目的是民族振兴,美国梦的目的是个人成功等等.\n给我的感觉是中国梦以集体为荣, 个人为耻一样, 然后国家不是一个虚无飘渺的实体, 是由诸多的基本单位组成的, 而这些基本单位就是由一个家庭和个体组成的, 如果个人不实现富裕, 国家的富强从何而来, 莫非是空中花园, 凭空而来?\n但国情如此, 由不得我们多言. 然而王小波早已懂得如何将个人尊严加塞于国家和政府尊严之中:\n作为一个知识分子, 我发现自己曾有一种特别的虚伪之处, 虽然一句话说不清, 但可以举些例子来说明. 假如我看到火车上特别挤, 就感慨一声道: 这种事居然发生中华人民共和国的土地上! 假如我看到厕所特脏, 又长叹一声: 唉! 北京市这是怎么搞的嘛! 我的确觉得国家和政府的尊严受到损失, 并为此焦虑着. 当然, 我自己也想要点个人尊严, 但以个人名义提出就过于直露. 不够体面\u0026ndash;言必称天下, 不以个人面目出现, 是知识分子的尊严所在.\n因为尊严是属于个人的, 不可压缩的空间, 这块空间要靠自己来捍卫\u0026ndash;捍卫指的意思是敢争, 敢打官司, 敢动手(勇斗歹徒). 我觉得人还是有点尊严的好, 假如个人连个待的地方都没有, 就无法为人做事, 更不要说做别人的典范.\n原来我们没有尊严的原因都是自找的, 并没有所谓的天赋人权, 有的只有自己争取而来的权利和尊严.\n6 总结 读完《沉默的大多数》之后, 对于王小波有了初步的认识, 他是个特立独行的人, 是个敢说话的人, 也是个有着时代印记的普通人,\n文章有种言有尽而意无穷的滋味. 更多的感受恐怕要看完他的代表作《黄金时代》之后才好继续评价, 就先这样了.\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E6%B2%89%E9%BB%98%E7%9A%84%E5%A4%A7%E5%A4%9A%E6%95%B0/","summary":"1 前言 以前没有读过王小波作品时候, 经常能看到王小波身上的各种标签, 中国第一代程序员, 现代伟大作家诸如此类. 但是没有读过王小波的作品, 对于他的","title":"沉默的大多数"},{"content":"1 前言 先此声明, 个人倾向于将Collection翻译成容器, 将Set翻译成集合.\n已经许久没有更新Guava研读系列的文章, 今天要介绍的是Guava的不可变容器.\n2 关于不可变对象 不可变的对象有许多的优点, 如下:\n线程安全, 可以在多线程之间使用也不用担心有竞争条件的风险 可以放心地用于不被信任的第三方类库 不用考虑支持可变性, 无需额外的空间和时间消耗. 可用作常量使用 使用对象的不可变拷贝是一项良好的编程防御策略, 为此, Guava提供了许多简单易用的, 实现了标准库Collection接口的不可变容器, 当然也包括实现了他们自家Collection接口的不可变容器.\n虽然通过JDK的静态方法Collection.unmodifiableXXX可以使用内置不可变容器, 但是在Guava团队的同学看来, 它们有若干的不足(又到了喜闻乐见的黑JDK的环节):\n笨重; 使用起来很笨重, 不够赏心悦目和优雅. 不安全; 上述静态方法返回的容器只有在没有对象持有原来容器的情况下才是真正不可变的. 例如, 当想要通过可变Map=ids=来生成一个不可变Map的时候,=Collections.unmodifiableMap(ids)=, 如果有多个对象持有ids时, 静态方法返回的对象就不是真正的不可变. 具体的分析可以参考StackOverFlow关于unmodifiableMap和ImmutableMap的讨论 低效; 静态方法生成的不可变容器和可变容器有着同样的性能开销, 包括并发修改, 动态扩容等(对于真正的不可变容器而言, 这些都是不会出现的操作) 综上所述, 如果你不想修改某个容器, 或者你想把某个容器当作不可变常量, 把这个容器变成一个不可变容器是一个很好的手段(使用Guava的不可变容器).\n此外, 在之前的文章中, 我阐述过Guava对于空指针的态度是尽量不要使用空指针, Guava的类库对于空指针都是快速失败的, Guava的不可变容器也是不例外的, 是拒绝接受空指针的.\n3 代码实例 前面详细介绍了不可变容器, 现在是时候来看一下Guava不可变容器的代码例子:\npublic static final ImmutableSet\u0026lt;String\u0026gt; COLOR_NAMES = ImmutableSet.of( \u0026#34;red\u0026#34;, \u0026#34;orange\u0026#34;, \u0026#34;purple\u0026#34;); class Foo { final ImmutableSet\u0026lt;Bar\u0026gt; bars; Foo(Set\u0026lt;Bar\u0026gt; bars) { this.bars = ImmutableSet.copyOf(bars); // defensive copy! } } 前文提到的, Collections.unmodifiableXXX(mutableXXX), Collections方法不能提供真正的不可变容器, 除非没有对象持有可变对象mutableXXX的引用\n那么Guava的不可变容器又是否是真正的不可变呢? 以ImmutableSet为例, 发现所有可以修改ImmutableSet对象的操作函数, 包括add, remove, addAll, removeAll等函数都被重载, 然后标注成@Deprecated, 重载函数的内容就是抛出UnsupportedOperationException异常, 所以不可能修改ImmutableSet对象的内容:\n/** * Guaranteed to throw an exception and leave the collection unmodified. * * @throws UnsupportedOperationException always * @deprecated Unsupported operation. */ @Deprecated @Override public final boolean add(E e) { throw new UnsupportedOperationException(); } 至于持有mutableXXX对象引用, 修改mutableXXX对象内容导致不可变内容发生改变的情况也不会发生:\n@Test public void testImmutable() { Set\u0026lt;String\u0026gt; colors = Sets.newHashSet(); colors.add(\u0026#34;blue\u0026#34;); Set\u0026lt;String\u0026gt; modifiableSet = Collections.unmodifiableSet(colors); Set\u0026lt;String\u0026gt; unmodifiableSet = Collections.unmodifiableSet(new HashSet\u0026lt;\u0026gt;(colors)); final ImmutableSet\u0026lt;String\u0026gt; COLOR_NAMES = ImmutableSet.copyOf(colors); colors.add(\u0026#34;yellow\u0026#34;); // 不会修改不可变集合的值 Assert.assertFalse(COLOR_NAMES.contains(\u0026#34;yellow\u0026#34;)); // 修改引用导致集合值发生修改 Assert.assertTrue(modifiableSet.contains(\u0026#34;yellow\u0026#34;)); // 因为没有对象持有new HashSet\u0026lt;\u0026gt;(colors)的引用, 所以unmodifiableSet是不可变集合, 不能修改 Assert.assertFalse(unmodifiableSet.contains(\u0026#34;yellow\u0026#34;)); Assert.assertTrue(colors.contains(\u0026#34;yellow\u0026#34;)); } 查看ImmutableSet.copyOf(Set\u0026lt;T\u0026gt;)函数的源码, 发现不可变集合的实现逻辑和在构造函数新建对象实现对象引用拷贝的逻辑一致, 即和Collections.unmodifiableSet(new HashSet\u0026lt;\u0026gt;(colors))的逻辑一样的:\npublic static \u0026lt;E\u0026gt; ImmutableSet\u0026lt;E\u0026gt; copyOf(Collection\u0026lt;? extends E\u0026gt; elements) { /* * TODO(lowasser): consider checking for ImmutableAsList here * TODO(lowasser): consider checking for Multiset here */ if (elements instanceof ImmutableSet \u0026amp;\u0026amp; !(elements instanceof ImmutableSortedSet)) { @SuppressWarnings(\u0026#34;unchecked\u0026#34;) // all supported methods are covariant // 新建对象, 拷贝对象引用 ImmutableSet\u0026lt;E\u0026gt; set = (ImmutableSet\u0026lt;E\u0026gt;) elements; if (!set.isPartialView()) { return set; } } else if (elements instanceof EnumSet) { return copyOfEnumSet((EnumSet) elements); } Object[] array = elements.toArray(); return construct(array.length, array); } 4 具体细节 下面我们来讨论一下各种不可变容器的具体使用细节.\n4.1 构造不可变容器 关于如何构造一个不可变容器, Guava提供的手段是多种多样的:\n使用copyOf静态方法, 例如ImmutableSet.copyOf(set), 这种构造方法与JDK不可变容器的构造方式类似Collections.unmodifiableXXX(mutableXXX) 使用of静态方法, 例如ImmutableSet.of(\u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;, \u0026quot;c\u0026quot;)或者ImmutableMap.of(\u0026quot;a\u0026quot;, 1, \u0026quot;b\u0026quot;, 2), 前文已经介绍过, 在此就不赘言 使用Builder构造不可变容器, 例如: public static final ImmutableSet\u0026lt;Color\u0026gt; GOOGLE_COLORS = ImmutableSet.\u0026lt;Color\u0026gt;builder() .addAll(WEBSAFE_COLORS) .add(new Color(0, 191, 255)) .build(); 不过某些不可变容器的builder方法废弃了, 如ImmutableSortedSet的builder方法就被替换成了naturalOrder.\n此外, 对于有序容器(sorted collections)而言, 容器内的元素的顺序是按照构造时元素的插入顺序排列的, 例如如下代码\nfinal ImmutableSet\u0026lt;String\u0026gt; alphaTable = ImmutableSet.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;, \u0026#34;b\u0026#34;); alphaTable.forEach(System.out::println); // 结果为 a b c d 4.2 asList函数 所有的不可变容器都提供了一个asList方法来返回一个不可变列表ImmutableList, 所以即使你把数据存在一个不可变有序集合ImmutableSortedSet, 你也可以通过下标索引获取最小的元素或者第n小的元素, 如:\nfinal ImmutableSet\u0026lt;Integer\u0026gt; numberSet = ImmutableSortedSet.\u0026lt;Integer\u0026gt;naturalOrder() .add(2, 3, 1) .add(4, 5, 6).build(); numberSet.asList().get(0) # 结果为1 4.3 智能的copyOf函数 前文提到, 不可变容器都提供了一个copyOf方法用于从另外一个容器构造出一个不可变容器. 值得指出的是不可变容器的copyOf方法在不需要拷贝数据的时候就会尽量避免拷贝数据, 但这是什么意思呢? 假如有如下的代码:\nImmutableSet\u0026lt;String\u0026gt; foobar = ImmutableSet.of(\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;, \u0026#34;baz\u0026#34;); thingamajig(foobar); void thingamajig(Collection\u0026lt;String\u0026gt; collection) { ImmutableList\u0026lt;String\u0026gt; defensiveCopy = ImmutableList.copyOf(collection); ... } 在上面的代码调用ImmutableList.copyOf(foobar)函数的时候, 函数的内部实现不会逐个拷贝, 而会直接通过foobar.asList()函数返回一个不可变值列表, 这样实现的算法时间复杂度就是O(1), 而不是O(n), 实现性能消耗的最小化, 这也就是小标题智能指的意思.\n但是需要注意的是, 并不是所有的不可变容器之间的转换都能实现O(1)时间复杂度, 例如ImmutableSet.copyOf(ImmutableList)就只能逐个元素拷贝, 时间复杂度退化到O(n).\n5 JDK容器与Guava不可变容器 对于JDK提供的标准容器, Guava提供了相应的不可变容器实现, 对于Guava自家的容器, Guava也提供了对应的不可变容器, 具体实现对比如下:\nInterface JDK or Guava? Immutable Version Collection JDK ImmutableCollection List JDK ImmutableList Set JDK ImmutableSet SortedSet=/=NavigableSet JDK ImmutableSortedSet Map JDK ImmutableMap SortedMap JDK ImmutableSortedMap Multiset Guava ImmutableMultiset SortedMultiset Guava ImmutableSortedMultiset Multimap Guava ImmutableMultimap ListMultimap Guava ImmutableListMultimap SetMultimap Guava ImmutableSetMultimap BiMap Guava ImmutableBiMap ClassToInstanceMap Guava ImmutableClassToInstanceMap Table Guava ImmutableTable 6 总结 因为不可变容器不会在运行时改变他们的内部状态, 所以他们是线程安全和无副作用的.\n因为这些属性, 不可变容器在多线程环境就会变得特别有用, 可以安全地传递数据. 总而言之, 生活和工作或许可以多拥抱变化, 对于代码, 最好还是多保持不变地好.\n7 参考 Immutable Collections ","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%AE%B9%E5%99%A8/","summary":"1 前言 先此声明, 个人倾向于将Collection翻译成容器, 将Set翻译成集合. 已经许久没有更新Guava研读系列的文章, 今天要介绍的是Gu","title":"guava探究系列之四:不可变容器"},{"content":"1 前言 Java 是一门集大成的面向对象语言, 在Java的世界里, 一切皆对象, 而Object类就是所有对象的默认父类. Guava 提供了若干个工具方法来扩展Object类的通用能力.\n2 equals 在Java的编程世界, 比较两个对象是个很常见的操作, Object类也提供了一个equals方法来判断对象是否相等. 但是Object使用的equals方法有诸多不便, 最痛苦的是无处不在的NullPointerException, 例如:\npublic testEqueal(Object input){ this.equals(input); } 但当 this指针指向一个空对象的时候, 就会出现null.testEqueal(input)的情形, 就会抛出NPE. 为了让equals方法更易用, Guava提供了一个Objects.equal(Object a, Object b)方法来判断两个对象是否相等. 用法如下:\nObjects.equal(\u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;); // returns true Objects.equal(null, \u0026#34;a\u0026#34;); // returns false Objects.equal(\u0026#34;a\u0026#34;, null); // returns false Objects.equal(null, null); // returns true 可能是Java语言的开发者也意识到Object.equals方法的不便, 所以在JDK7的时候, 官方也提供了Objects.equals(Object a, Object b)的方法, Guava的竞品自然也没了用武之处。\n不过, 说实话, 无论是JDK的Objects.equals, Object.equal还是Guava的Object.equal(), 在日常的开发中也用的不多, 用的最多的是Apache Common库的各种Utils工具, 比较String类型用的是StringUtils.equals(), 比较容器(Collection)用的CollectionUtils.isEqualCollection(), 毕竟这些工具要更高级和完善(有趣的是, JDK的方法名是equals, Guava的方法名是equal, 下文提到的JDK的hash方法名叫hash, Guava叫hashCode).\n2.1 hashCode 在《Effective Java》的条款9中说到:\nAlways override hashCode when you override equals\n就是说在你重写equals方法的时候, 记得重写hashCode方法, 因为按照Java的约定, 如果两个对象通过调用equals方法判断是相等的话, 它们调用hashCode()方法的返回结果也是一样的.\n《Effective Java》 给出的重写建议是把一个对象的所有字段进行计算取得一个hash值, 示例代码如下:\nprivate volatile int hashCode; public class User { private String name; private int age; private String passport; @Override public int hashCode() { int result = hashCode; if(result == 0){ result = 17; // Aribtrary number. result = 31 * result + name.hashCode(); result = 31 * result + age; // 31 is an odd prime result = 31 * result + passport.hashCode(); hashCode = result; } return hashCode; } } 这样的计算方式虽然有效, 但是未免过于烦琐, 还要手动计算每个字段. 为此, Guava 提供了一个 Objects.hashCode(field1, field2, ..., fieldn的方法, 用于对所有的字段计算hash值, 用法如下:\npublic class User { public int hashCode() { return Objects.hashCode(name, age, passport); } } 看起来简洁多了。然后在Java7的时候, JDK也推出了一个Objects.hash(field1, field2,...,fieldn)的方法, 而Guava的竞品很快就被废弃了. 我都在想JDK是不是在吸收Guava的精华, 毕竟实现都一样!( ̄▽ ̄)\n3 toString 《Effective Java》的条款10说到:\nAlways override toString\n也就是说, 《Effective Java》建议所有的类都重写toString()方法. 其实toString()方法不是给程序看的, 而是给开发者自己看的.\n据说, 好看的toString()方法的输出结果可以让程序员更愉悦, 可见颜值处处都有用. 比较常见的重写toString()的方式是把所有的字段拼接输出, 只不过手动拼接有点累.\n省心的是, Intellij Idea 为开发者提供了生成toString()的快捷方式, 如下图:\nFigure 1: Idea生成toString\n如果觉得Idea生成的toString()有太多的拼接字符串, 还可以试试Guava提供的toString()工具方法: MoreObjects.toStringHelper, 具体用法如下:\n@Test public void test() { System.out.println(MoreObjects.toStringHelper(this) .add(\u0026#34;name\u0026#34;, \u0026#34;Linus\u0026#34;) .toString()); System.out.println(MoreObjects.toStringHelper(\u0026#34;TestToStringHelper\u0026#34;) .add(\u0026#34;method\u0026#34;, \u0026#34;toStringHelper\u0026#34;) .toString()); } // 结果如下: // ToStringTest{name=Linus} // TestToStringHelper{method=toStringHelper} 使用方法也是很明了, 就不过多赘述.\n4 compare/compareTo 既然前面提到了《Effective Java》, 那么基于前后呼应的原则, 最后也免不了要再引用一下《Effective Java》:\n条款12: Consider implementing Comparable\n不像前文介绍过的方法, compareTo方法并不是Object类的方法, 而是Comparable接口的方法. 这个方法和前文提到的equals方法类似, 只不过用法不一样. compareTo通常用于排序, 如下面的代码就是对实现了Comparable接口的Person对象的列表进行排序:\nclass Person implements Comparable\u0026lt;Person\u0026gt; { private String lastName; private String firstName; private int zipCode; public int compareTo(Person other) { int cmp = lastName.compareTo(other.lastName); if (cmp != 0) { return cmp; } cmp = firstName.compareTo(other.firstName); if (cmp != 0) { return cmp; } return Integer.compare(zipCode, other.zipCode); } } public class CompareTest { @Test public void testSort(){ Person[] persons = new Person[2]; persons[0] = new Person(\u0026#34;Ma\u0026#34;, \u0026#34;Jack\u0026#34;, 12345); persons[1] = new Person(\u0026#34;Ma\u0026#34;, \u0026#34;Pony\u0026#34;, 65432); Arrays.sort(persons); Arrays.stream(persons).forEach(person -\u0026gt; System.out.println(person.getFirstName())); } } 上面的代码的逻辑就是先比较Person.lastName, 如果相等再比较Person.firstName, 如果前面的条件还是相等, 就再比较Person.zipCode.\n代码的含义相当清晰, 只是有不少的模板代码, 如果能减少这些模板代码, 那就更好了. 幸运的是, Guava 提供了一个 ComparisonChain来处理这些模板逻辑, 应用ComparisonChain之后的代码如下:\npublic class ComparisonChainPerson implements Comparable\u0026lt;ComparisonChainPerson\u0026gt; { private String lastName; private String firstName; private int zipCode; @Override public int compareTo(ComparisonChainPerson that) { return ComparisonChain.start() .compare(this.lastName, that.lastName) .compare(this.firstName, that.firstName) .compare(this.zipCode, that.zipCode) .result(); } } 确实简洁了许多~~\n5 总结 到本文为止, Guava提供的基本工具类就已经介绍完了,暂时告一段落了, 接下来就要介绍Guava最常用的工具之一: 各种容器(Collections)\n","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E7%B1%BB%E6%94%B9%E5%96%84%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95/","summary":"1 前言 Java 是一门集大成的面向对象语言, 在Java的世界里, 一切皆对象, 而Object类就是所有对象的默认父类. Guava 提供了若干个工具方法来扩展Ob","title":"Guava探究系列之三:改善通用方法"},{"content":"1 前言 记得高中讀各种经典名著的时候, 曾经蜻蜓点水般翻阅过《围城》一书. 白衣苍狗, 时过景迁, 再讀《围城》, 有了不一样的感受. 正如《围城》开篇所言:\n围在城里的人想逃出来, 城外的从想冲进去, 对婚姻也罢, 职业也罢, 人生的愿望大都如此.\n这实在是至理名言. 婚姻我尚且未曾有过经验, 但是对于职业却确有体会, 记得当初大二的我, 目标只是毕业后去网易写程序, 因为网易的食堂有名, 好吃.\n只是没想到当初找实习的时候, 不报什么希望的大阿里顺利通过面试, 而所倾心的网易连面试的机会都没有, 就这样, 我来了中国互联网的龙头公司-阿里巴巴.\n来了阿里之后, 对围城的认识就更加真实有体感. 面对996的福报, 很多人想要逃出来, 而外面却还有诸多\u0026quot;入福无门\u0026quot;的同行, 着实是人生何处不围城!\n2 一个残酷但真实的故事 你方鸿渐拿着前岳父的资助出国读书, 过着富二代的生活, 却一事无成买假文凭回国. 刚回国时, 被不知真相的众人仰慕与包容, 拒绝了爱耍小聪明苏小组的示爱, 欺骗了原是真爱的唐小姐.\n生活开始磨平你的棱角, 为了生活远走内陆任教, 又因无谓的高傲放弃留下来的机会, 甚至到后来每次的谋生都需要仰仗看不上当初的朋友.\n与心机的孙柔嘉结婚, 有了一段世俗的婚姻, 且又因为无谓的倔强和尊严搞得家不成家, 一地鸡毛, 甚至连当初爱慕你的女生都对你和你的妻子不屑一顾, 你的人生一直滑落, 直至谷底.\n你觉得你的人生是座围城, 可被困在围城里的又何止你一个人, 世俗之人都囿于这围城中, 我也在进围城的路上.\n3 一本关于中国人的书 记得大学时, 在宿舍附近的图书室的角落, 曾读过一本\u0026quot;经典名作\u0026quot;\u0026ndash;《丑陋的中国人》. 书中诸多演讲内容, 除了书名, 我已基本忘却, 只记得当初看得不以为然, 觉得这位作家强把全世界人的陋习安在中国人头上, 然后一顿狠批. 这本书应该叫《丑陋的人类》.\n重讀《围城》, 为文中对中国人的描述之准确而拍案叫绝, 而后又不禁深思, 中国人竟然是这样的?\n此外, 《围城》有许多有趣的情景, 只不过我没有疏理清楚, 与其刻意地寻找, 不如偶然地发现, 姑且想到哪, 就写到哪了.\n4 语言艺术 4.1 讽刺的语言艺术 年龄看上去有二十五六, 不过新派女人的年龄好比旧式女人合婚帖上的年庚, 需要考订学家所谓外证来断定真确性, 本身是看不出来的.\n原来 70 年前,女人的保养技术已经如此了得了(围城中很多话语, 女权主义者看完想锤钱老)\n做媒和做母亲是女人的两个基本欲望. 有鸡鸭的地方, 粪多; 有年轻女人的地方, 笑多.(女人和鸡鸭类比~~) 鸿渐暗笑女人真是天生的政治家, 她们俩背后彼此诽谤, 面子上这样多情, 两个政敌在香槟酒会上碰杯的一套工夫, 怕也不过如此. 她眼睛并不顶大, 可是灵活温柔, 反衬得许多女人的大眼睛只像政治家的大话,大而无当. 还有读了好几次才明白的句子, 着实让人看得无言以对, 原来输钱是要开心的: \u0026gt; 太太不忠实, 偷人, 丈夫做了乌龟, 买彩票准中头奖,赌钱准羸. 所以,他说,男人赌钱输了, 该引以自慰\n4.2 搭讪的语言艺术 开篇时的方鸿渐, 也是长袖善舞, 让诸多女人倾心, 从归国船上的鲍小组, 到方文纨, 再到唐晓芙. 想必与鸿渐的风趣诙谐不无关系, 搭讪的技术也是一流:\n你表姐说你朋友很多, 我不配高攀, 可是很想在你的朋友里凑个数 我想去吃, 对自己没有好借口, 借你们两位的名义,自己享受一下, 你就体贴下情, 答应了罢~ 5 中国人的人情世故 细读之后, 发现《围城》写的就是中国人的种种人情世故. 如果没有足够的人生阅历, 相信很难将中国人的人情世界写的如此生动, 跃然于纸上, 难怪钱老会被评价为极通世故.\n读完《围城》, 甚至可以根据其中的内容写一部中国人行为处世教程, 教你如何做个\u0026quot;合格的中国人\u0026quot;\n5.1 陆子瀟教你如何低调装 x 他(陆子瀟)亲戚曾经写给他一封信, 这左角印\u0026quot;行政院\u0026quot;的大信封上大书着\u0026quot;陆子瀟先生\u0026quot;, 就仿佛行政院都要让他正位居中似的.\n他写给外交部那位朋友的信, 信封虽然不大, 而上面开的地址\u0026quot;外交部欧美司\u0026quot;六字,笔酣墨饱, 字字端楷, 文盲在黑夜里也该一目了然. 这一封来函, 一封去信, 轮流地在他桌上装点着.\n一个装腔作势的人(装 x 犯)的形象跃然于纸上\n5.2 陆子瀟教你如何欲擒故纵 陆子瀟想向方鸿渐打探自己爱慕的孙小姐的信息, 不直接向鸿渐了解孙小姐的信息,反而整天阴阳怪气地说鸿渐和孙小姐关系很好, 时刻关注着孙小姐和鸿渐,又不愿直说:\n子瀟又尖刻地瞧鸿渐一眼道: \u0026ldquo;我以为你们(方与孙)经常见面\u0026rdquo;\u0026hellip;. 待鸿渐与孙小姐交谈离开后, 鸿渐刚回房, 陆子瀟就进来, 说:\u0026ldquo;咦, 我以为你跟孙小姐同吃晚饭去了. 怎么没有去?\u0026rdquo;\n当鸿渐看破了陆子瀟的爱慕之情之后,愿意给陆子瀟介绍孙小姐之后, 陆子瀟又不愿承认, 又怀疑鸿渐的动机:\n子瀟猜疑地细看鸿渐道: \u0026ldquo;你不是跟她很好么?夺人所爱, 我可不来. 人弃我取, 我更不来\u0026rdquo;\n诸如此类的描述, 《围城》还有很多, 就不必一一列举了.\n6 中国人的政治 所谓有人的地方就有江湖, 有江湖的地方就有纷争. 且来看看钱老笔下国人的江湖:\n6.1 所谓的民主 鸿渐学校仿照牛津,剑桥要搞个导师制, 老师与学生要在食堂一同进餐. 而人家牛津, 剑桥饭前饭后都会用拉丁文祝福, 本着学就要学彻底, 鸿渐的学校也要效仿祝福. 李梅亭绞尽脑汁想了个\u0026quot;一粥一饭, 要思来之不易\u0026quot;, 大家哗然大笑.\n儿女成群的经济系主任自言自语:\u0026ldquo;干脆大家像我儿子一样,念\u0026rsquo;吃饭前, 不要跑; 吃饭后, 不要跳'\u0026rdquo;\n高松年直对他眨白眼, 一壁严肃地说:\u0026ldquo;我觉得在坐下吃饭以前, 由训导长领导学生静默一分钟, 想想国家抗战时期的民生问题的艰难,我们吃饱了肚子应当怎样报效国家社会, 这也是很意思的举动. 经济系主任忙说:\u0026ldquo;我愿意把主席的话作为我的提议. \u0026ldquo;.\n李梅亭附议, 高松年付表决, 全体通过.\n这不禁让我想起了公司的 outing, 就是公司组织的\u0026quot;自费旅游\u0026rdquo;. 最开始大家民主投票, 提出自己想去的地方,然后集体投票, 大家选择了几个地方投票之后,老板给了建议, 然后大家全体决议, 就去老板建议的地方, 一如书中所述.\n6.2 请客吃饭 没想到, 请客吃饭, 还有如此多的讲究:\n请吃饭好比播种子; 来的客人里有几个是吃了不还请的, 例如最高上司和低级小职员;\n有几个一定还席的, 例如地址和收入相等的同僚,\n这样, 种一頓饭可以收获几顿饭.\n6.3 比较和虚荣 汪先生得意地长叹道: \u0026ldquo;这算得什么呢!我有点东西, 这一次全丢了.\n两位没看见我南京的房子\u0026ndash;房子总算没给日本人烧掉, 里面的收藏陈设都不知下落. 幸亏我是个达观的人,否则真要伤心死呢.\u0026rdquo;\n这类的话, 他们近来不但听熟, 并且自己也说惯了. 这次在兵灾当然使许多有钱, 有房子的人流落做穷光蛋, 同时也让不知多少的穷光蛋有机会追溯自己为过去的富翁.\n7 名句摘抄 自己太不成了, 撒了谎还要讲良心, 真是大傻瓜.\n事实上, 一个人的缺点正像猴子的尾巴, 猴子蹲在地面的时候, 尾巴是看不见的, 直到他向树上爬, 就把后部供大众瞻仰, 可是这红臀长尾巴本来就有, 并非地位爬高了的新标识.\n拥挤里的孤寂, 热闹里的凄凉, 使他像许多住在这孤岛上的人, 心灵也仿佛一个无凑畔的孤岛.\n我常说, 结婚不能太冒昧, 譬如这个人家里有没有住宅,就应该打听打听. (原来 70 年前, 结婚就需要有房子了)\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%9B%B4%E5%9F%8E/","summary":"1 前言 记得高中讀各种经典名著的时候, 曾经蜻蜓点水般翻阅过《围城》一书. 白衣苍狗, 时过景迁, 再讀《围城》, 有了不一样的感受. 正如《围城》开篇所","title":"人生何处不围城"},{"content":"1 前言 根据防御式编程的要求, 在日常的开发中, 总少不了对函数的各种入参做校验, 以便保证函数能按照预期的流程执行下去.\n比如各种费率的值就没可能是负数, 如果费率出现负数, 所以数据有问题, 我们需要做的事情就是把这些有问题的数据挑出来. 自己手写这些校验函数未免过于繁琐, 所幸的是我们需要的函数已经有现成的:\nGuava 提供了一系列的静态方法用于校验函数和类的构造器是否符合预期, 并称其为前置条件(preconditions). 如果前置条件校验失败, 就会抛出一个指定的异常.\n2 前置函数特征 目前的前置校验方法有如下特征: 须需要, 下面例子中的checkArgument函数可以替换成任何一个前置条件校验函数\n这些前置方法一般接受一个布尔表达式作为入参,并判断表达是否为true, 格式如: Preconditions.checkArgument(a\u0026gt;1) // 如果表达式为false, 抛出IllegalArgumentException 除了用于判断的布尔表达式之外, 前置方法可以接受一个额外的Object作为入参, 在抛出异常的时候, 把Object.toString()作为异常信息, 如: public enum ErrorDetail { SC_NOT_FOUND(\u0026#34;404\u0026#34;, \u0026#34;Resource could not be fount\u0026#34;); // 省略部分内容 @Override public String toString() { return \u0026#34;ErrorDetail{\u0026#34; + \u0026#34;code=\u0026#39;\u0026#34; + code + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, description=\u0026#39;\u0026#34; + description + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } @Test public void testCheckArgument() { Preconditions.checkArgument(1 \u0026gt; 2, ErrorDetail.SC_NOT_FOUND); } // 结果如下: // java.lang.IllegalArgumentException: ErrorDetail{code=\u0026#39;404\u0026#39;, description=\u0026#39;Resource could not be fount\u0026#39;} Guava的前置表达式还支持类似printf函数那样的格式化输出错误信息, 只不过出于兼容性和性能的考虑, 只支持使用%s指示符格式化字符串, 不支持其他类型. 如: int i=-1; checkArgument(i \u0026gt;= 0, \u0026#34;Argument was %s but expected nonnegative\u0026#34;, i); // 结果如下: // java.lang.IllegalArgumentException: Argument was -1 but expected nonnegative 3 前置条件函数介绍 须注意的是, 下面介绍的checkArgument, checkArgument, checkState函数都有三个对应的重载函数,分别对应前文所述的三种特征, 下文不会三种函数都介绍, 只介绍标准格式的前置条件函数. 以checkArgument函数为例, 三个重载函数分别是(忽略函数体):\npublic static void checkArgument(boolean expression); public static void checkArgument(boolean expression, @Nullable Object errorMessage); public static void checkArgument(boolean expression,@Nullable String errorMessageTemplate,@Nullable Object... errorMessageArgs) 3.1 checkArgument 函数的签名如下:\npublic static void checkArgument(boolean expression); 入参是一个布尔表达式, 函数校验这个表达式是否为true, 如果为false, 抛出IllegalArgumentException. 例子如下:\n@Test public void testCheckArgument() { Preconditions.checkArgument(1 \u0026gt; 2); } 3.2 checkNotNull 这是个泛型函数, 函数签名如下:\npublic static \u0026lt;T\u0026gt; T checkNotNull(T reference); 入参是个任意类型的对象, 函数校验这个对象是否为null, 如果为空, 抛出NullPointerException, 否则直接返回该对象, 所以checkNotNull的用法就比较有趣, 可以在调用setter方法前作前置校验. 例子如下:\nPreconditionTest caller = new PreconditionTest(); caller.setErrorDetail(Preconditions.checkNotNull(ErrorDetail.SC_INTERNAL_SERVER_ERROR)); 3.3 checkState 函数签名如下:\npublic static void checkState(boolean expression); 看着这个函数, 我个人感觉很奇怪: 这个函数和checkNotNull函数功能非常相似, 实现也基本一样, 都是判断表达式是否为true, 只是抛出的异常不一样而已, 是否有必要开发这个函数. 两个函数的实现如下:\npublic static void checkArgument(boolean expression) { if (!expression) { throw new IllegalArgumentException(); } } public static void checkState(boolean expression) { if (!expression) { throw new IllegalStateException(); } } 此外, 因为这两个函数相当类似, 就不展示相应例子了.\n3.4 checkElementIndex 函数签名如下:\npublic static int checkElementIndex(int index, int size); 这个函数用于判断指定数组, 列表, 字符串的下标是否越界, index是下标, size是数组, 列表或字符串的长度, 下标的有效范围是[0,数组长度) 即 0\u0026lt;=index\u0026lt;size. 如果数组下标越界(即index=\u0026lt;0 或者 =index=\u0026gt;==size), 那么抛出IndexOutOfBoundsException异常, 否则返回数组的下标, 也就是index. 例子如下:\nPreconditions.checkElementIndex(\u0026#34;test\u0026#34;.length(), \u0026#34;test\u0026#34;.length()); // 运行结果: // 抛出异常: java.lang.IndexOutOfBoundsException: index (4) must be less than size (4) Assert.assertEquals(3, Preconditions.checkElementIndex(\u0026#34;test\u0026#34;.length() - 1, \u0026#34;test\u0026#34;.length())); // 运行结果: // 通过 4 checkPositionIndex 函数的签名如下:\npublic static int checkPositionIndex(int index, int size); 这个函数和checkElementIndex非常类似, 连Guava wiki的说明也基本一致(只有一个单词不同).\n除了一点, checkElementIndex函数的下标有效范围是[0, 数组长度), 而checkPositionIndex函数的下标有有效范围是[0, 数组长度], 即0\u0026lt;=index\u0026lt;=size. 例子如下:\nPreconditions.checkPositionIndex(\u0026#34;test\u0026#34;.length() + 1, \u0026#34;test\u0026#34;.length()); // 运行结果: // 抛出异常: java.lang.IndexOutOfBoundsException: index (5) must be less than size (4) Assert.assertEquals(4, Preconditions.checkPositionIndex(\u0026#34;test\u0026#34;.length(), \u0026#34;test\u0026#34;.length())); // 运行结果: // 通过 4.1 checkPositionIndexes 函数的签名如下:\npublic static void checkPositionIndexes(int start, int end, int size); 这个函数是用于判断[start,end]这个范围是否是个有效范围, 即[start, end] 是否在[0, size] 范围内(如果[start, end] 和[0, size]相同, 也认为在范围内), 如果不在, 则抛出IndexOutOfBoundsException异常. 例子如下:\nPreconditions.checkPositionIndexes(1, 3, 2); // 运行结果: // 抛出异常: java.lang.IndexOutOfBoundsException: end index (3) must not be greater than size (2) Preconditions.checkPositionIndexes(0, 2, 2); // 运行结果: // 校验通过 5 前置条件在实际项目的应用 前置条件在检验条件不成交的时候抛的异常类型虽说是合情合理(比如, checkArgument函数抛出IllegalArgumentException),\n但是对于业务系统来说, 你抛出个IllegalArgumentException或者NullPointerException, 接口调用方对于这个异常摸不着头脑, 虽说只是正常的数据问题, 还是很容易觉得接口提供方服务出了问题, 甚至还会被质疑技术不过硬.\n咱们又不是底层组件, 抛个NPE, 着实是不成体统. 基于各种有的没的的原因, 我们的业务系统在使用前置条件的时候进行了封装, 将前置条件抛出的异常进行了转换, 换成正常的业务异常, 提供完整的异常信息, 代码如下:\n// 封装代码: public final class AssertUtils { /** * 检查条件表达式是否为真 * * @param expression 条件表达式 * @param errDetailEnum 错误码 * @param msgTemplate 错误消息模板 * @param vars 占位符对应变量 * @throws BkmpException 条件表达式结果为假 */ public static void checkArgument(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate, Object... vars) { try { Preconditions.checkArgument(expression); } catch (IllegalArgumentException e) { throw new BkmpException(errDetailEnum, msgTemplate, vars); } } /** * 检查条件表达式是否为假 * * @param expression 条件表达式 * @param errDetailEnum 错误码 * @param msgTemplate 错误消息模板 * @param vars 占位符对应变量 * @throws BkmpException 条件表达式结果为假 */ public static void checkArgumentNotTrue(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate, Object... vars) { try { Preconditions.checkArgument(!expression); } catch (IllegalArgumentException e) { throw new BkmpException(errDetailEnum, msgTemplate, vars); } } } // 省略其他部分的封装 // 调用例子: AssertUtils.checkArgument(merchantEntity.exist(), ErrDetailEnum.DATA_NOT_EXIT, \u0026#34;商户不存在\u0026#34;); 6 Guava Precondition vs Apache Common Validate 自古文无第一, 武无第二, 文人之间的口水战总是少不了的.\n没想到这不是国人的专利, 原来国外也有文人相轻的风气: Guava wiki 在介绍完preconditions之后, 还踩了一波竞品Apache Common Validate, 认为Guava的preconditions 比Apache Common 更加清晰明了, 也更加美观,\n我个人对Apache Common Validate 了解不深, 也不好随意置喙. 除了踩竞品之外, Guava wiki 还提了两点最佳实践(best practice):\n使用前置条件校验的时候, 推荐每个校验条件单独一行, 这样即更了然, 出问题也更方便调试. 使用前置条件校验的时候, 尽量提供有用的错误信息, 这样可以更快地定位问题. 7 总结 代码大全一书有一章是关于防御式编程的, 用于提高程序的健壮性, 主要思想是子程序应该不因传入错误数据而被破坏,要保护程序免遭非法输入数据的破坏.\n而Guava的preconditions 就是实现防御式编程的有力工具呢. oh yeah!\n8 参考 PreconditionsExplained ","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E4%BC%98%E9%9B%85%E6%A0%A1%E9%AA%8C%E6%95%B0%E6%8D%AE/","summary":"1 前言 根据防御式编程的要求, 在日常的开发中, 总少不了对函数的各种入参做校验, 以便保证函数能按照预期的流程执行下去. 比如各种费率的值就没可能是","title":"Guava探究系列之二: 优雅校验数据"},{"content":"1 前言 To be, or not to be, that is the question:\n先来看看奆佬们关于空指针的看法:\nNull sucks - Doug Lea(JCP,Java并发编程实战作者, Java巨佬)\nI call it my billion-dollar mistake. - Sir C. A. R. Hoare, 空指针的发明者\n按照Guava wiki的说法, 大部分的Google代码都是不支持使用空指针(下文用null表示空指针)的,\n如接近95%的集合类都不支持使用null作为集合元素. 像Google这样的大公司明确不建议使用null自然是有其原由的, 不会无的放矢. 那具体原因是什么呢?待下文为你细细道来;\n2 空指针的问题 2.1 空指针语意隐晦不明 null的语意并不了然明确, 即当一个函数返回null, 我们并不知道null的意思是指返回结果理应为空? 还是指函数没有达到预期结果, 返回null表示失败?\n举个常见的例子, 当调用Map.get(key)获取key对应的value的时候, 返回结果为null; null是指找不到这个key对应的value? 还是说这个key对应的value本身就是null, 原来是通过Map.put(key,value)赋值的呢? null甚至可以是代表其他东西!\n老实说, 当我们获得一个null, 我们并不清楚它究竟指的是啥, 除非有对应的javadoc 进行了说明.\n2.2 空指针”暗藏杀机” null除了语意不明外, 还非常容易在不经意间挖坑坑人. 例如有下面的代码:\nprivate String testNull(String input) { if (random.nextInt() % 2 == 0) { return input; } else { return null; } } @Test public void useNull() { String foo = testNull(\u0026#34;foo#bar\u0026#34;).split(\u0026#34;#\u0026#34;)[0]; String bar = testNull(\u0026#34;foo#bar\u0026#34;).split(\u0026#34;#\u0026#34;)[1]; } 可能你会说,这样的明显有坑的代码, 程序员理所当然会注意, 并对null指针进行校验的.\n但事实并非如此, 因为null是一个特殊类型, 它可以表示一切的类型, 所以上面的代码是肯定可以编译通过的. 没有了编译器的约束, 只要使用testNull函数的时候没有查看源码, 或者源码非常复杂, 一下子理不清思路, 防御式编程落实不到, 就会忽略了null, 运行时就有可能抛出NullPointException, 导致程序crash. 这种情况真的防不胜防.\n3 Guava对于空指针的态度 因为上文提到或者隐藏但没提到的种种问题, Guava的诸多类库在设计时就不支持null.\n如果检测到null的存在, Guava的类库就会快速失败(fail fast),一般的处理策略是抛出异常. 虽说null存在种种的坑, 但null依旧是Java的一项关键特性, 因此Guava的类库也不能将null彻底拒之门外.\n此外, Guava秉承既然不能消灭null, 那就把null建设得更好用的理念, 除了提供了一些工具可以让开发者避免使用null, 还提供了可以让开发者更易于使用null的工具.\n4 Optional 在很多情况下, 程序员使用null是为了表示有些值可能存在或者不存在. 我们又可以用熟悉的Map.get(key)函数来举例, 如果规定null不能作为value值使用(但事实并非如此), 那么当这个函数返回null时就代表没有找到这个key对应的value.\n为了应对这种使用null的情况, Guava团队参考其他语言(例如Scala)应对null的实践, 开发了Optional\u0026lt;T\u0026gt;类. Optional类表示那些可能为空的值, 一个Optional类要不包含一个非空的T类型的对象引用(这种情况下, 我们称引用对象是存在的-\u0026ldquo;present\u0026rdquo;), 要不什么东西都不包含(这种情况被, 我们说引用对象是不存在的-\u0026ldquo;absent\u0026rdquo;), 除此之外, Optional不存在其他情况, 更没有可能是null.\n4.1 Java8的Optional 鉴于我对Optional类的兴趣, 我用下面这条命令找了一个Guava库Optional开发的最初提交历史:\nfind guava/ -name \u0026#34;Optional.java\u0026#34; -print | xargs -I \u0026#39;{}\u0026#39; git log --pretty=tformat:%cd-%aN-%s --date=iso |tail -n2 # 结果如下 # 2009-09-15 19:50:59 +0000-kevinb@google.com-Initial code dump: version 9.09.15 # 2009-06-18 18:11:55 +0000-(no author)-Initial directory structure. 从Guava的commit历史中, 我们可以知道Optional最开始是在2009年开始开发的, 而10年前还是Java6的时代, Java7都尚未发布.\n在那个”远古年代”, 是Guava的Optional一直引领着Java的抗击null重任, 为众多的蒙受”空指针之苦”的Java的程序员带来希望之光.\n而当时光的脚步终于来到2014年3月18号, 在这一天, Java程序员迎来了Java8, 这是自Java5发布以来最激动人心的发布. 这天之后, 尘埃落定, Optional, Stream, Lambda等诸多令人期待已久的特性终于成为Java的标准库的一部分, 而这也意味, Guava的Optional已经完成了自己的使命, 成为历史.\nGuava的Optional类与JDK的Optional功能类似, 既然JDK的Optional已成为正统, 那么下面我就不再介绍Guava的Optional=(Guava的wiki本来是有较大篇幅介绍自家的=Optional, 个人感觉已经意义不大), 转而介绍JDK的Optional=(下文通称为=Optional).\n4.2 Optional构造方式 在使用Optional之前, 首先需要了解如果构造Optional对象, 方式有如下几种:\n4.2.1 声明一个空的Optional对象 可以通过静态工厂方法Optional.empty, 创建一个空的Optional对象:\nOptional\u0026lt;T\u0026gt; optional = Optional.empty(); 4.2.2 根据一个非空值创建Optional 还可以使用静态工厂方法Optional.of, 依据一个非空值创建一个Optional对象:\nOptional\u0026lt;T\u0026gt; optional = Optional.of(objectT); 需要注意的是, 按照Optional的源码声明, 如果传入的objectT为null, 那么Optional就会立刻抛出NullPointException=(这就是快速失败-fail fast), 而还是等到访问=optional属性时才返回一个错误.\n/** * Returns an {@code Optional} with the specified present non-null value. * * @param \u0026lt;T\u0026gt; the class of the value * @param value the value to be present, which must be non-null * @return an {@code Optional} with the value present * @throws NullPointerException if value is null */ public static \u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; of(T value) { return new Optional\u0026lt;\u0026gt;(value); } 4.2.3 可接受null的Optional 最后, 使用静态工厂方法Optional.ofNullable, 我们可以创建一个允许null的Optional的对象:\nOptional\u0026lt;T\u0026gt; optional = Optional.ofNullable(objectT); 如果objectT为null, 那么得到的Optional对象就是个空对象.\n4.3 Optional的消费方式 4.3.1 Optional与Stream的邂逅 既然Optional在Oracle的文档中被定性为一个容器(container),\n那么对于一个容器, 我们关注的点无非是这个容器如何存*(对于Optional来说是构造)和如何取*这两件事而已(也就是消费). 在谈Optional的消费接口之前, 先来回顾一下Java8引进的Stream操作(关于Java8 Stream操作的说明已经汗牛充栋了, 既然珠玉在前, 我就不赘言了), 常用的Stream操作函数有如下几个:\nfilter map flatmap peek reduce 更多的函数可以参考Oracle文档 因为前文已经说过Optional是容器类, 那么按理来说, 正常容器类支持的Stream操作, Optional也支持.\n只不过在Java8的时候, Optional只支持filter,=map=和flatmap这三个Stream操作.\n可能是因为Java委员会的奆佬们也觉得Optional身为一个容器类只支持三个Stream操作有点丢人, 所以在Java9, Optional增加了一个Optional.stream()这样一个可以返回Stream对象的函数, 让Optional拥有了容器类操作Stream的所有能力, 重振了身为一个容器的荣光. Optional与Stream结合使用的示例如下:\npublic String getCarInsuranceName(Optional\u0026lt;Person\u0026gt; person) { return person.flatMap(Person::getCar) .filter(car-\u0026gt;car.getName().equals(\u0026#34;Spaceship\u0026#34;)) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse(\u0026#34;Unknown\u0026#34;); 4.3.2 默认行为及解引用Optional对象 除了使用Stream来消费Optional对象, 还可以使用解引用读取Optional实例中的变量值以及定义默认行为, 具体函数说明如下:\nget()是这些方法中最简单但又最不安全的方法. 如果变量存在, 它直接返回封闭的变量值. 否则就抛出一个NoSuchElementException异常. 所以, 除非是非常确定Optional变量一定包含值, 否则使用这个函数就相当容易踩坑. 此外, 使用这个函数和直接进行null检查差别并不大. orElse(T other) 该函数允许在Optional对象不存在的时候提供一个默认值(也是我个人最常用的使用方式之一) orElseGet(Supplier\u0026lt;? extends T\u0026gt; other)是orElse函数的延迟调用版, Supplier方法只有在Optional对象不含值的时候才执行. 如果创建默认值是件耗时操作, 那么可以使用这种方式来提升性能, 又或者某个函数仅在Optional为空的时候才调用, 也可以使用这种方式 orElseThrow(Supplier\u0026lt;? extends X\u0026gt; exceptionSupplier) 和get方法非常类似, 这两个函数都会在Optional对象为空时, 抛出异常, 但差别在于orElseThrow可以指定抛出的异常类型 ifPresent(Consumer\u0026lt;? super T\u0026gt;)和orElseGet函数类似, 可以在变量存在的时候执行传入的函数, 否则就不进行任何操作. 4.3.3 Optional 实战示例 在啰啰嗦嗦介绍了一系列Optional的概念之后, 是时候来看一下Option的实例了. 现存的Java API几乎都是通过返回一个null的方式表示所需的值的缺失, 或者由于某些原因计算无法得到所需的值.\n在上文, 我们已经给null盖棺定论了, null是有坑的, 甚至是有害的, 所以要尽量少用null. 而现存的海量Java API都已经使用null作为返回结果, 我们没可能把这些API都重构成返回一个Optional对象的, 但眼看着Optional这样一个设计更完善无法在已有的Java API中使用未免令人心有不甘.\n现实中, 可能我们无法修改这些API的签名, 但是我们却可以很轻易地用Optional对象对这些API的返回值进行封装. 现在还是用熟悉的Map举例, 假设有一个Map\u0026lt;String, Object\u0026gt;的对象, 在查询key对应的value时, 如果value不存在, 那么调用Map.get(key)就会返回一个null:\nObject value = map.get(key); 现在, 每次使用value都需要进行空指针判断, 着实是太繁琐. 为了解决这个问题, 可以使用Optional.ofNullable函数进行优化:\nMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;); String value = Optional.ofNullable(map.get(\u0026#34;foo\u0026#34;)).map(Object::toString).orElse(\u0026#34;helloworld\u0026#34;); 这样, 每次使用value都不会再有NullPointException的忧虑.\n5 结语 本文最开始只是想阐述Guava类库使用空指针和避免使用空指针的设计理念, 只是因为Guava大部分类库都是不支持null, 因此使用Guava自家的Optional类来代替null的大部分应用场景, 而Guava自家的Optional无可避免地被JDK的Optional取代,\n所以本文大部份的内容也变成对JDK的Optional的探讨. 相信下篇文章会有所改观, 总不可能Guava所有的工具类, 都有JDK对应的竞品, 如果真是这样的话, JDK应该改名为GDK :)\n6 参考 Using and avoiding Java8 in Action Oracle java doc about Optional ","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E4%BD%BF%E7%94%A8%E5%92%8C%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8%E7%A9%BA%E6%8C%87%E9%92%88/","summary":"1 前言 To be, or not to be, that is the question: 先来看看奆佬们关于空指针的看法: Null sucks - Doug Lea(JCP,Java并发编程实战作者, Java巨佬) I call it my billion-dollar mistake. - Sir C.","title":"Guava探究系列之一: 使用和避免使用空指针"},{"content":"1 前言 转眼间, 我已经工作一年了\n去年的6月28日, 我到了杭州, 入职了的一家全国闻名的金融科技公司, 开始了自己的职业生涯.\n2 关于工作 2.1 工作后 工作之后, 日常会回想起学校的生活. 工作对比学生生活, 最大的差别就是, 我再也不能随心所欲, 尤其在大学时候, 想上课就上课, 想出外旅行就出外旅行, 什么事情都不想做的时候, 还能躺在床上睡觉.\n而工作就是工作, 在公司,你需要做的就是不停地工作, 而9105已经是常态. 说好的弹性工作, 也可以通过要求早上9.15开晨会的形式来花式强制打卡, 员工除了被动接受, 也不会有其他的选择. 看来, 资本家果然是资本的人格化.\n2.2 拥抱变化 我的公司和我的国家一样, 也会有各种的价值观, 而”拥抱变化”就是其中一项. 其意思在网上已经流传甚广, 公司内部也各种解读, 总结来说就是有任何看起来不好的事情, 都可以用”拥抱变化”来概述.\n入职不到半年, 我便经历了一次”拥抱变化”: 原来的组被解散, 我被分流到新的组. 而入职一年后, 我又将要经历另外一次”拥抱变化”. 不禁感概, \u0026ldquo;拥抱变化\u0026quot;果然是我司的价值观.\n3 关于读书 入职以后, 有感自我提升的迫切性, 前后买了近两打书, 最近搬家之后, 房子没有书柜, 书全放床上, 占了三分之一的地方, 颇有种”著作等身”的感觉. 这两打书,既有计算机相关的, 也有非计算机相关的书籍,\n目前我大概读了三分之一, 也记录了相关的读书笔记心得. 目前在读的书名著是: 《苏菲的世界》,《围城》, 《枪炮、病菌与钢铁》, 几本书之间切换, 计算机的主要是 《Unix网络编程》.\n之前搬家时, 才发现我的书点了两个箱子, 把和我一起搬东西的新舍友累个半死; 虽说如此, 书仍旧会继续, 因为书中记录的知识和书的价格相比, 书的价格实在是便宜.\n4 关于生活 搬了一次家, 和朋友合租了间在7楼的房子, 现在住的地方离公司, 直线距离200M, 上班步行10分钟, 大部分时间是用在上下楼和等红灯. 因为现在住得近, 所以中午能和舍友一起自己做饭并在家午休.\n除了舍友是个素食主义者和公司把午休时间从12:00-14:00改为12:00-13:30之外, 生活上的其他事情还是挺不错的.\n入职半年之后, 可能是因为工作压力的缘故, 体重竟然比在校期间下降了5-6Kg, 而后可能是因为逐渐适应工作的强度, 体重渐渐恢复正常. 目前无肌肉, 无赘肉.\n感情生活: 为0, 按下不表\n5 其他小事 弃用Emacs, 转向Vscode, 因为工作事情繁多缘故, 没有时间与精力研究Emacs, 遂转投Vscode, 目前一切尚可, 暂无回归Emacs之意. 学习五笔, 已经能熟练使用, 本文即用五笔书写. 最开始原因是拼音重码率高(如ji,si,shi,zhi, 有太多同音字), 不容易记录文言文\u0026lt;\u0026gt;笔记, 且我使用的系统是Linux, Linux上的拼音并不好用, 于是用五一的三天假期学习五笔, 上手有难度, 但是熟悉后会比拼音好用, 熟悉费时约一个月. 勤勉一年, 绩效尚可, 无晋升提名. 6 Guava开坑序言 不甘心于碌碌无为, 每日只是搬砖, 工作一年后也没有什么值得引以为豪的地方, 所以需要决定开个大坑: 研读Guava代码, 并翻译其文档.\nGuava是Java程序员工具箱中的一把瑞士军刀, 与Apache common类库齐名, 这样有名的类库, 对于一个合格的Java程序员, 自然是不能不读. 但是我无意于单纯的翻译文章, 这样的文章已经太多了,\n我期待自己能结合实际的场景和源码解读Guava, 所谓”知其然知其所以然”. 这也是对自己的一个期许, 唯愿自己不要轻易放弃, 须知: 千淘万漉虽辛苦, 吹尽狂沙始到金.\n7 憧憬与展望 借用高适的一首诗以寄期望:\n千里黄云白日曛 北风吹雁雪纷纷 莫愁前路无知己 天下谁人不识君\n希望自己还能保持乐观~~\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%B7%A5%E4%BD%9C%E4%B8%80%E5%91%A8%E5%B9%B4%E8%AE%B0/","summary":"1 前言 转眼间, 我已经工作一年了 去年的6月28日, 我到了杭州, 入职了的一家全国闻名的金融科技公司, 开始了自己的职业生涯. 2 关于工作 2.1 工作后 工作","title":"工作一年记"},{"content":"浮生六记共有六卷,如今仅存四卷, 而\u0026quot;浮生\u0026quot;一名来自李太白的\u0026lt;春夜宴从弟桃花园序>中的名句\u0026quot;而浮生若梦,为欢几何\u0026quot;.\n主要是围绕作者沈复与夫人陈芸的闲情逸事, 如闺房之乐,诗酒之乐,游玩之乐等等.\n1 卷一 闺房记乐 卷一主要描写的是沈复与陈芸幼时相识,青年婚后为欢之事. 因为作者伉俪和读者我都是青年, 虽说相隔二百多年,也不免会有诸多相似之处,读来也会有许多有趣之事.\n卷一读起来最大的感受,以现在流行语说就是, 沈复和陈芸这俩小青年在疯狂\u0026quot;撒狗粮, 秀恩爱\u0026quot;,字里行眼,情深不禁跃然于纸上.\n卷一都是种种日常趣事,印象深刻, 读来令人忍俊不禁,我便挑几处与大家分享:\n1.1 深夜煮粥 沈复十三岁时,陈芸堂姐嫁人,沈复随母亲同去观礼,当日送嫁回城已经深夜了,沈复肚子饿了想找吃的,家中老妪给沈复做枣脯,沈复嫌太甜,然后淑姐(陈芸)把沈复悄悄带到自己的房间,拿出自己的热粥和小菜给沈复吃.\n这个时候陈芸堂兄芸衡在门外叫唤陈芸,让她快点出来. 陈芸答道,我太累了,要休息了.\n怎知玉衡推门而入, 发现我在吃粥. 就笑着对陈芸说,之前我问你还有没有粥,你说吃完了, 原来你是留给自己的丈夫呢.\n陈芸觉得非常害羞,大家都在哗笑. 我也闹小孩子脾气,和老妪先回家了.(原来两个小青年青梅竹马,早已暗生情愫)\n1.2 女着男装行庙会 离沈复家不远处,有一个名为洞庭君祠的庙,每逢神诞,热闹非凡,按照沈复自己的描述, 花光灯影,宝鼎香浮,若龙宫夜宴.\n这么热闹的场景,沈复自然回家对妻子讲述,陈芸叹息道,可惜我不是男子阿,去不了呢.\n沈复出了个主意, 你穿我的衣服,戴我的帽子,女扮男装就可以啦(这年青人真会玩).\n在庙中游玩的时候,都没有人认出陈芸是女子,旁人问沈复这是何人的时候,沈复说这是我表弟呢(真的皮).\n最后走到庙庆负责人家眷坐的地方的时候,陈芸上去攀谈,不自觉地把手放到了一位年青妇人的肩上, 然后旁边的侍女怒而起,斥骂道:\u0026ldquo;哪里来的狂徒呢,竟然在这里非礼人\u0026rdquo;.\n眼看局势就要控制不住,陈芸把帽子一脱,头发一甩(这个动作是我脑补的), 示意道, 我也是女子呢. 大家都惊呆了,而后转怒为欢(感觉陈芸真的会玩, 也率性)\n1.3 见湖生情, 与妓同饮 沈复父亲朋友病逝,沈复奉父命前去吊唁,途径太湖.\n而陈芸想要去见识一下太湖风光,就找了个借口和沈复一起乘船前去,见到太湖时直呼:\u0026ldquo;此太湖耶?今得见天地之宽, 不虚此生矣!想闺中人有终身不能见此者\u0026rdquo;(感觉有点心酸, 两百年前的女子竟可能终身不得见此景).\n归途中,沈复,陈芸与船家女素云共饮, 乘兴而来,大醉而归.\n后来沈复朋友的夫人对陈芸说,听说前几天你老公和两个妓女在船上饮酒,你知道不. 陈芸回答到,其中一个就是我呢,然后将事情始末告诉这位夫人.\n1.4 不知夭寿之机,此已伏矣 诸如此类趣事还有许多, 如陈芸为沈复纳妾之类的,而沈复和陈芸耳鬢相磨,亲如形影之情言语尚不能尽述,只觉得羡杀旁人(看两百多年前的年轻夫妻秀恩爱,感觉非常有趣)\n只是卷一回忆欢乐之趣事时,时常会有\u0026quot;不知夭寿之机,此已伏矣\u0026quot;,\u0026ldquo;真所谓乐极灾生, 亦是白头不终之兆\u0026quot;之类的话语,让人惴惴不安,知道佳人终究会陨落。\n正应了柳三变那句\u0026quot;应是良辰美景虚设,便纵有千种风情,更与何人说\u0026rdquo;, 不禁令人扼腕叹息.\n2 卷二 闲情记趣 这卷主要是作者平时闲情时的活动, 比如种花, 对对子之类.\n因为个人对这些山水盘栽, 吟诗作文这些文人墨客活动兴趣不大, 所以蜻蜓点水地阅读了这一卷. 这卷有一段名句, 出现在了中学课本上:\n余忆童稚时, 能张口对日, 明察秋毫. 见藐小微物. 必细察其纹理, 故时有物外之趣. 夏蚊成雷, 私拟作群鹤舞空. 心之所向, 则或千或百, 果然鹤也; 昂首观之, 项为之强. 又留蚊于素帐中, 徐喷以烟, 使其冲烟飞鸣, 青云白鹤观, 果如鹤唳云端, 怡然称快. 于土墙凹凸处, 花台小草丛杂处, 常蹲其身, 使与台齐, 定神细视: 以丛草为林, 以虫蚁为兽, 以土砾凸者为丘, 凹者为壑, 神游其中, 怡得自得. 记得当初上学的时候还背这篇文章, 情景还历历在目, 文章还记得大部分, 但是一切都已事是人非.\n3 卷三 坎坷记愁 这卷的内容从标题都可以看出来, 基本就是惨.\n按照作者的描述, 有如下的惨事:\n沈复父亲因为误会妻子识字, 却不愿意为沈复母亲写信, 导致父亲很生气(怒). 实际是母亲不让芸写信, 芸怕影响和母亲关系, 就没有向父亲辨解, 也阻止了沈复去辨解.(感觉沈复为自已的懦弱甩锅) 父亲想要找小老婆, 芸就为父亲寻小老婆, 被母亲知道后, 母亲也对芸不满了. 沈复弟弟启堂让嫂子作担保借钱, 然后被父亲发现了, 询问弟弟详情, 弟弟甩锅说什么都不知道. 然后父亲大怒(怒甚), 就要沈复赶妻子出门, 还骂了沈复一顿.(感觉这父亲也太激动了, 莫非是因为芸为他找小老婆的事情被大老婆知道了? 小儿子说不知道就是没有借錢?) 芸的弟弟去世了, 妈妈也去世了 芸的病情因为种种事情开始恶化. 沈复也友人借钱作担保, 然后友人就跑了, 沈复作为担保人, 理所当然地被追债. 在家的时候被上门追债, 妻子的友人华氏恰好也派人上门找芸. 沈复老爸又生气了, 以为这是憨园(妓女)的人, 把沈复又骂了一顿, 说芸不守妇道, 和妓女结交, balabala.(人家芸和憨园交往是在为你儿子找老婆啊) 经过这么多的破事之后, 芸决定去朋友华氏处养病. 跑路怕被追债的人发现, 只能未亮就走. 这时还有一对子女未曾安置好, 然后草率地把女青君嫁人作童养媳, 子逢森安置在沈复父亲家中. 到了朋友家中, 沈复想要做点事, 又没錢, 就打算去找堂姐夫追债, 一番波折之后, 只追回20两, 途中还差点没钱吃饭, 住店. 在芸身体好得差不多之后, 离开了朋友家, 去了沈复工作的地方, 华氏还送芸一个仆人, 名为阿双, 过了不久, 沈复就被裁员了. 沈复只好再去讨债, 最后讨得25两. 芸在沈复讨债回来之后, 病情急转直下, 最后说完一番遗言后, 溘然长往, 香消玉殒. 芸死后, 沈复扶棺归家, 能被弟弟忽悠去了扬州. 后来, 收到儿子的信: 父亲病重, 沈复还担心父亲还在生气, 不知道应不应该回去. 然后, 很快就收到父亲的死讯. 被弟弟排挤出家门, 没拿到一分遗产, 与儿子逢森吿别, 只身去四川. 半年后收到女儿来信, 儿子逢森去世. 卷一曾经透露,陈芸与沈复终难厮守, 但是这卷看来, 相守23年, 也堪算白头偕半老, 终究比纳兰容若与妻子卢氏相爱3年之后, 妻子因病香消玉殒来得要好.\n纳兰的悼念妻子卢氏的诸多诗词, 也不禁令人为之叹息. 沈复与陈芸相守半生, 着实不算乐极生悲. 纳兰的悼念词也真的是让人看得悲从中来:\n我是人间惆怅客, 知君何事泪纵横, 断肠声里忆平生。\n谁念西风独自凉?萧萧黄叶闭疏窗,沉思往事立残阳。 被酒莫惊春睡重,赌书消得泼茶香,当时只道是寻常。 (或者说这种事不能比惨)\n\u0026ldquo;曾经沧海难为水, 除却巫山不是云\u0026rdquo;\u0026ndash; 致陈芸.\n沈复对陈芸的确是位贴心丈夫, 但是却不是一位好丈夫, 因为他不能保护好自已的妻子, 更不能照顾好自己的子女, 逢森的早逝沈复绝对是有责任, 可真算是位\u0026quot;渣父\u0026quot;.\n4 卷四 浪游记快 顾名思义, 这卷的内容就是沈复浪浪浪, 各种游玩, 观赏.\n各种活动诸如: - 沈复15岁时, 去寻觅名妓苏小小之墓, 初时只是斗丘黄土, 在乾隆询问过之后, 苏小小之墓日渐隆重, 吊古骚人可轻易寻至.\n年幼的沈复感慨到: 余思古来烈魄忠魂湮没不传者,因不可胜数, 即传而不久者, 亦不为少; 小小一名妓耳, 自南齐至今, 尽人而知之, 此殆灵气所钟, 为湖山点缀耶? (我觉得是因为文人骚客都是男的, 喜欢名妓多于豪杰是合理的, 是生物进化的自然选择. 有李白的诗为证: 美酒樽中置千斛,载妓随波任去留)\n幼时从师在春和景明之际扫墓同游, 挖竹笋, 作羹汤, 游水洞, 不亦乐乎.\n少时与思斋先生共赴寒山登高,与知己鸿干四处闲玩, 更被人认为是风水先生, 是来找墓地的.\n凡所种种, 不一而足. 卷四中沈复游山玩水, 有时写的是山水, 有时写的还是山水, 只不过里头指的却是人世间。\n如登石镜山的时候, 有感于山中僻庵的小沙弥吃了肉馒头后拉肚子, 跟同仁说到:\u0026ldquo;作和尚者, 必居此等僻地, 终身不见不闻, 或可修养静. 若吾乡之虎丘山, 终日目所见者妖童艳妓,耳所听者弦索笙歌, 鼻所闻者佳肴美酒, 安得身如枯木, 心如死灰哉?\u0026rdquo; (我觉得嘛, 这种只是初级高僧的修炼方式, 抵抗诱惑的方式只是去个没有诱惑的地方. 真正经得起考验的高僧应该是经得住繁华, 耐得住寂寞, 所谓酒肉穿过, 佛祖心中留嘛)\n卷四中还有大段篇幅描写的是去我大广东嫖妓的事(文人墨客都这么骚的么?), 嫖妓的时候嫌弃本地妓女异服, 就喜欢扬帮妓女, 还把妓女带回住所,导致被人敲诈,要跑路, 后面嫖妓还嫖出了自豪感,觉得每个妓女都喜欢我. 原文如此说道:\n余则惟喜儿一人. 偶独往, 或小酌于平台,或清淡于寮内, 不令唱歌, 不强多饮, 温存体恤, 一艇怡然. 邻妓皆羡之. 有空闲无客者, 知余在寮, 必来相访. 合帮之妓无一不识. 每上其艇, 呼余声不绝. 余亦左顾右盼, 应接不暇,此虽挥霍万金所不能致者.(看来沈复嫖出了境界)\n后来沈复他爹不允许沈复再来广东, 沈复知道喜儿为他的离开伤心不已, 几寻短见, 沈复感慨到 \u0026ldquo;半年一觉扬帮梦, 赢得花船薄倖名\u0026rdquo;(我还是觉得他在炫耀)\n5 总结 仅存的四卷已经读完了, 最大的感受是沈复夫人陈芸的风采, 着实令人神往. 难怪林语堂先生会说芸是\u0026quot;中国文学中一个最可爱的女人\u0026quot;, 也诚非过誉.\n所以私以为卷一的闺房记乐才是全书的精华, 至于沈复的浪游记快和闲情记趣, 因为我个人对这些内容不甚感兴趣, 所以只是浮光掠影地过了一遍.\n最后, 既然是以李白的\u0026quot;而浮生如梦, 为欢几何\u0026quot;一诗开始的, 那就以这首诗结束吧:\n夫天地者万物之逆旅也;光阴者百代之过客也。 而浮生若梦,为欢几何? 古人秉烛夜游,良有以也。 况阳春召我以烟景,大块假我以文章。 会桃花之芳园,序天伦之乐事。 群季俊秀,皆为惠连;吾人咏歌,独惭康乐。 幽赏未已,高谈转清。 开琼筵以坐花,飞羽觞而醉月。 不有佳咏,何伸雅怀? 如诗不成,罚依金谷酒数。\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E6%B5%AE%E7%94%9F%E5%85%AD%E8%AE%B0/","summary":"浮生六记共有六卷,如今仅存四卷, 而\u0026quot;浮生\u0026quot;一名来自李太白的\u0026lt;春夜宴从弟桃花园序>中的名句\u0026quot;而浮生若梦,为","title":"浮生六记"},{"content":"1 前言 最近花时间,看完了狄更斯先生的名著<双城记>,除了那脍炙人口的开篇名句之外,我还看到了一些其它的内容, 待我细细述来\n2 说说作品本身的东西 老实说,在读复活篇和金线篇的时候,我是时常看到昏昏欲睡的,狄更斯先生用了 2/3的篇幅来描述马奈特医生一家和他们周遭的朋友与琐事,看着这些家常里短的描述,我是充满期待和疑惑的, 期待即将到来的转折,疑惑马奈特医生入狱的原因,期待法国大革命的上演,期待种种铺垫最后的爆发.\n只是我等待的时间未免过长,狄更斯先生的铺垫也着实太久,几次都看不下去,几欲放弃,直到看到法国大革命的爆发,书一切的暗线和铺垫才开始聚集起来, 在最后的一篇暴风雨的踪迹,将故事推向高潮. 我个人感觉最充满悬念和出人意料的地方有几处:\n卡顿先生在达尔再次入狱的时候,终于显露身影,和巴塞德这个密探博弈了起来,按照卡顿先生的说法,一副博命的牌局.\n已经稳居上风的卡顿先生说到巴塞德同伴是克莱时,自信的神色仿佛再次回到巴塞德这个密探, 这个密探甚至拿出了克莱的死亡证明说明卡顿先生说错了(当时很奇怪的是,为啥会把别人的死亡证明随身放身上),\n这时候进来的杰里先生跳起来说棺材中根本没有人,克莱是诈死,棺材中根本没有人,他就知道没有人.\n杰里先生的说明就让人想起之前有相当篇幅描述杰里先生表面是个跑腿的随从,背地是个掘墓偷尸的生意人的伏笔, 在这时候引爆之前的伏笔,不禁让人拍案叫绝\n卡顿先生在赢了巴塞德密探之后的要求,在达尔行刑前见他一面,联想起达尔和卡顿先生样貌想像的伏笔. 不看后面的篇幅,也能猜出卡顿先生的调包之计,他去为达尔去死,践行他之前对露西小姐的承诺\n最后行刑时,紧握卡顿先生手的那个姑娘,她的表妹是否是德日发夫人呢?\n行刺伯爵,把伯爵的头从伯爵府中取走的雅克,究竟是谁?\n最后终于明白,原著标题 A Tale of Two Cities 中的 Two Cities 指的是什么? 巴黎和伦敦\n其它在阅读是感叹,现在却想不起来的情节\n在看书的时候,很容易将自己代入到故事中去,把自己想像成书中的种种人物或者联想至其它人物.\n坦白说,我觉得卡顿先生死得其所,因为他前半生空有一身才华,但是却甘心堕落,成为狮子身边的胡狼,不想改变现状,但是对于卡顿先生为什么堕落却未有解释.\n卡顿先生不想成为死去没有人挂念和尊敬的人,不想孤独地死去,在失去(他也未曾争取过)露西小姐之后.\n卡顿先生就用另外一种方式让露西小姐永记自己,以牺牲自己的形式,最后,他也达成了自己的愿望, 用一种(肉体)死代替了另外一种(精神)死: 我(卡顿先生)知道,他俩(露西和达内)在对方心中深受尊重,视为神圣,可我在他们心目中,更受尊重,更为神圣.\n我并没有觉得德日发夫人的复仇有什么问题,父亲被逼害,姐姐被人侮辱并致死,姐夫被杀,哥哥被杀,向惨剧的始作俑者复仇,自然也是情理之中.\n只是最后我觉得需要谴责的只是想在向埃尔瑞蒙德家族复仇之后,祸及马奈特医生,露西小姐和小露西未免扩大仇恨,最后德日发夫人也为此付出了代价\n马奈特医生让我联想到了一位获得诺贝尔和平奖,却无缘领取荣誉 ,癌症病逝后还被海葬的作家.\n只是马奈特医生能在关押 10 多年后出狱,重新发挥影响力,而这位作家却没机会继续为国家和人民燃尽余辉.\n如果这位作家能活着出狱,并能见证国家的革命,他也会像马奈特先生那样,因为10多年的巴士底狱囹圉经历而备受尊敬.\n露西过于脸谱化,只有担忧, 关怀和爱,似乎没有其它情绪一样,只存在文学作品中的人物\n3 说说作品之外的东西 3.1 家有倔子,不败其家; 国有诤臣,不亡其国 按照作者在书中的序言所述,最开始作者是想写一个为了心爱的姑娘而甘为情敌牺牲的故事,只是到后来,脱离作者最初的设想,成为一部讽谏的作品.\n当时作者所处的年代,英国内外交困,整个欧洲风起云涌,作者忧心当初 60多年前法国大革命在英国重现,所以写下这部世界名著,表达自己担忧,劝谏并警告当时的当权者和广大的民众.\n历史总是相似的,古今中外从不缺位卑未敢忘忧国的仁人志士,狄更斯的作品让我想起了中国 100 年前,也有位先生以笔代药,针对当时世人的种种弊病下药.\n我有时候会有\u0026quot;奇思妙想\u0026quot;, 如果那位先生活在当下,会有怎样的反映,又会有怎样的待遇?如查尔斯.达内那般被革命群众投入牢狱? 还是如现在这般受人拥戴? 这真的是个奇怪的想法\n3.2 民主啊,多少罪名都是假你的名义干出来的 容我修改了书中提到的罗兰夫人的名言,自由和民主都是很容易被滥用的借口,时常蒙上不白之冤,为莫须有的罪名负责.\n书中关注革命群众的描述,非常容易让人产生联想,只不过是\u0026quot;公民/雅克\u0026quot;变成\u0026quot;同志\u0026quot;, 书中革命群众攻占巴士底监狱之后,导致的持续数年的乱象总给我一种熟悉的感觉,我似乎在什么地方见过这种场景,当然也是在书中的描述见到的.\n<亮剑>一书,原书有 2/3 的篇幅都是描述建国后的事情,其中那场从 1966 年5 月发起的,持续十年的运动,就占据一般的篇幅,<亮剑>中的红卫兵与<双城记>中的革命群众非常相似,差别仅在刀斧变成了枪炮,审判法庭变成了批斗会,失控的革命运动总是这般的相似,总是如此充满破坏力,以至于在拨乱反正之后,留下的伤口也是许久才能结痂.\n而那些失去生命的,去侍奉吉萝亭的\u0026quot;幸运儿\u0026quot;总是没有机会回来的,例如九月屠杀丧命的犯人,替代达内的卡顿,不一而足.而这些阿,都是自由(民主)你所背上的罪名啊.\n4 名句 书中那些令人过目难忘的语句:\n那是最美好的时代,那是最糟糕的时代; 那是个睿智的年月,那是个蒙味的年月;那是信心百倍的时期,那是疑虑重重的时期; 那是阳光普照的季节, 那是黑暗笼罩的季节; 那是充满希望的春天,那是让人绝望的冬天; 我们面前无所不有, 我们面前一无所有; 我们大家都在直升天堂, 我们大家都在直下地狱 耶稣说,复活在我,生命也在我. 信我的人,虽然死了,也必复活; 凡活着信我的人, 也必不死 自由啊,多少罪名都是假你的名义干出来的(我在本书知道这句名句,就姑且算该书的名句) 你替他去死么? 确如译者宋兆霖所说,狄更斯是为语言大师\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%8F%8C%E5%9F%8E%E8%AE%B0%E8%AF%BB%E5%90%8E%E6%9C%89%E6%84%9F/","summary":"1 前言 最近花时间,看完了狄更斯先生的名著<双城记>,除了那脍炙人口的开篇名句之外,我还看到了一些其它的内容, 待我细细述来 2 说说作品本身的东西","title":"双城记读后有感"},{"content":"浅谈Java公平锁与内存模型\n1 前言 春天来了,春招还会远么? 又到了春招的季节,随之而来的是各种的面试题。今天就看到组内大佬面试实习生的一道Java题目:\n编写一个程序,开启 3 个线程A,B,C,这三个线程的输出分别为 A、B、C,每个线程将自己的 输出在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如:ABCABCABC\u0026hellip;.\n2 经过 出于好奇的心态,我花了点时间来尝试解决这个问题, 主要的难点是让线程顺序地如何顺序地输出,线程之间如何交换。\n很快就按着思路写出了一个版本,用Lock 来控制线程的顺序,A,B,C线程依次启动,因为A线程先启动,所以A线程会最先拿到锁,B,C阻塞;但是A输出完字符串,释放锁,B 线程获得锁,C,A线程阻塞; 依此循环:\npublic void Test(){ private static Integer index = 0; Lock lock = new ReentrantLock(); @Test public void testLock(){ Thread threadA = work(i -\u0026gt; i % 3 == 0, () -\u0026gt; System.out.println(\u0026#34;A\u0026#34;)); Thread threadB = work(i -\u0026gt; i % 3 == 1, () -\u0026gt; System.out.println(\u0026#34;B\u0026#34;)); Thread threadC = work(i -\u0026gt; i % 3 == 2, () -\u0026gt; System.out.println(\u0026#34;C\u0026#34;)); threadA.start(); threadB.start(); threadC.start(); } private Thread work(Predicate\u0026lt;Integer\u0026gt; condition, Runnable function) { return new Thread(() -\u0026gt; { while (index \u0026lt; 30) { lock.lock(); if (condition.test(index)) { function.run(); index++; } lock.unlock(); } }); } } 输入结果如我预期那般,ABCABC交替输出,也成功输出了10次,奇怪的是A,B却多输出了一次? 为什么会多输出一次,不是应该恰好是输出30次么, 为什么会多输出一次A,B 真的百思不得其解. 所以我把index 也打印出来查看, 结果相当奇怪:\n... function.run(); System.out.println(index); .... 为什么A 会是30, B会是31, 不是有(index.intvalue\u0026lt;30) 的条件判断么, 为什么还会出现这样的数据?灵异事件? 3 解惑 灵异事件自然是不存在的,仔细分析了一番代码之后,发现了问题:\nwhile (index.intValue() \u0026lt; 30) { // 1 lock.lock(); // 2 if (condition.test(index.intValue())) { function.run(); index++; } lock.unlock(); } 将1,2行的操作做了这三件事,如下:\n线程读取index的值 比较index的值是否大于30 3. 如果小于30, 尝试获取锁 换言之,当index=29时,线程C持有锁,但是锁只能阻止线程A,线程B修改index的值,并不能阻止线程A,线程B在获取锁之前读取index的值,所以线程A读取index=29,并把值保持到线程的内部,如下图:\n当线程C执行完,还没释放锁的时候,线程A的index值为29;当线程C释放锁,线程A获取锁,进入同步块的时候,因为 Java内存模型有内存可见性的要求, 兼之Lock的实现类实现了内存可见,所以线程A的index值会变成30,\n这就解析了为什么线程A index=30的时候能跳过(index.intValue\u0026lt;30)的判断条件,因为执行这个判断条件的时候线程A index=29, 进入同步块之后变成了30:\n把问题剖析清楚之后,解决方案就呼之欲出了:\nwhile (index.intValue() \u0026lt; 30) { // 1 lock.lock(); // 2 if(index\u0026gt;=30){ continue; } if (condition.test(index.intValue())) { function.run(); index++; } lock.unlock(); } 这种解决方法不禁让我想起单例模式里面的双重校验:\npublic static Singleton getSingleton() { if (instance == null) { //Single Checked synchronized (Singleton.class) { if (instance == null) { //Double Checked instance = new Singleton(); } } } return instance ; } 只是当时并不清楚Double Checked的作用,究竟解决了什么问题?\n只是知道不加这条语句就会造成初始化多个示例,的确是需要知其然知其所以然.\n4 公平锁问题 前文说到,\n这个程序是用Lock 来控制线程的顺序,A,B,C线程依次启动,因为A线程先启动,所以A线程会最先拿到锁,B,C阻塞;\n但是A输出完字符串,释放锁,B 线程获得锁,C,A线程阻塞; 依此循环。\n粗看似乎没什么问题, 但是这里是存在着一个问题: 当线程A释放锁的时候,获取锁的是否一定是线程B, 而不是线程C, 线程C是否能够”插队”抢占锁?\n这个就涉及到了公平锁和非公平锁的定义了:\n公平锁: 线程C不能抢占,只能排队等待线程B 获取并释放锁\n非公平锁:线程C能抢占,抢到锁之后线程B只能继续等(有点惨!)\n而ReentrantLock默认恰好是非公平锁, 查看源码可知:\n/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } 因此为了规避非公平锁抢占的问题, 上述的代码在同步块增加了判断条件:\nif (condition.test(index.intValue())) { .... } 只有符合条件的线程才能进行操作,否则就是线程自旋.(但是加锁+自旋实现起来,效率不会太高效!)\n5 小结 写一条面试题的答案都写得是问题多多的,不禁令人沮丧,说明自己对Java的并发模型理解还有很大的提高。 不过在排查问题的过程中,通过实践有体感地理解了Java的内存模型,发现Java内存模型并不是那么地曲高和寡,在日常的开发中也是很常见的.\n费了一番工夫排查之后,终究是有新的收获的\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E4%B8%80%E6%9D%A1%E7%BB%8F%E5%85%B8%E9%9D%A2%E8%AF%95%E9%A2%98%E5%BC%95%E5%8F%91%E7%9A%84%E6%80%9D%E8%80%83/","summary":"浅谈Java公平锁与内存模型 1 前言 春天来了,春招还会远么? 又到了春招的季节,随之而来的是各种的面试题。今天就看到组内大佬面试实习生的一道Ja","title":"一条经典面试题的错误答案引发的思考"},{"content":"刷POJO类的变更行覆盖率\n1 反射大法好 1.1 背景 众所周知,蚂蚁对代码质量要求很高,质量红线其中一项指标就是变更行覆盖率。如果你的变更行覆盖率没有达到80%,测试同学是不会允许你上测试环境的(如果对此有所不满,测试同学就会过来捶你)。 为了提高代码质量,这项要求倒是无可厚非,变更的代码逻辑需要充分的测试;但是如果我新增了一堆的POJO类,只是为了逻辑模型,变更行也会变得非常可观。为了覆盖这些POJO类的变更,你免不了会测试一堆的Getter/Setter 方法:\nFigure 1: getter/setter\n(红色是指没有覆盖到的变更行)\n1.2 反射 如果为了变更行覆盖了,我要写上一堆的Getter/Setter 方法测试用例,测试用例也只是单纯调用一下方法,未免过于痛苦,能否偷个懒,解决覆盖率问题,也不需手写这些没啥用的测试用例.\n但是一时间没有想到解决方法,我就一边写这些没啥用的用例,一边思考,突然发现了规律:\npublic SomeType getXxxx(){} public void setXxxx(SomeType Xxx){} public SomeType getYyy(){} public void setYyyy(SomeType Yyyy){} 所有这些方法都是的前缀都是 set/get (真.废话),如果我能获取一个Pojo类所有的方法,然后循环执行所有以get/set开头的方法,不就不用手动写方法了么?\npublic class MerchantBusiModelTest { protected static final Logger LOGGER = LoggerFactory.getLogger(ModelUtils.class); /** * get类型方法的前缀 */ private static final String GET_METHOD_PREFIX = \u0026#34;get\u0026#34;; /** * set类型方法的前缀 */ private static final String SET_METHOD_PREFIX = \u0026#34;set\u0026#34;; MerchantBusiModel merchantBusiModel = new MerchantBusiModel(); @Test public void testModel() { Method[] methods = merchantBusiModel.getClass().getDeclaredMethods(); for (Method method : methods) { if (Modifier.isPublic(method.getModifiers()) \u0026amp;\u0026amp; method.getName().startsWith(GET_METHOD_PREFIX)) { Object[] parameters = new Object[method.getParameterCount()]; try { method.invoke(merchantBusiModel, parameters); LoggerUtil.warn(LOGGER, \u0026#34;调用方法, method: {}.{}\u0026#34;, merchantBusiModel.getClass().getSimpleName(), method.getName()); } catch (IllegalAccessException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, merchantBusiModel.getClass().getName(), method.getName()); } catch (InvocationTargetException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, merchantBusiModel.getClass().getName(), method.getName()); } } } } } 这样很快就把MerchantBusiModel所有的get方法执行了(set 方法也同理啦),调用结果如下:\n19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632162,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getMcc] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getOutMerchantId] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getMerchantName] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getMerhantType] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getDealType] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632257,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getAlias] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632257,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getLegalPerson] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632257,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getPrincipalCertType] 省略一大片类似的输出,省点篇幅 1.3 org.reflections.Reflections 通过反射,就很完美地解决了POJO类的变更行覆盖率问题了,反正POJO类都是Getter/Setter 方法,我的反射方法能把它们全覆盖上啦 (๑\u0026gt;◡\u0026lt;๑) .\n很快,我就遇到了另外的一个问题: 像MerchantBusiModel这样的Model类有十几二十个,难道每个Model我都需要写一个XxxModelTest的测试类么?也实在是太痛苦了,也太不优雅了(其实是我懒),能不能自动把所有的Model类扫出来,然后循环执行每个Model的Getter/Setter方法呢?\n因为这些Model都是继承一个统一的基类BaseBusiModel, 能否把这个基类的所有子类搞出来,这样就可以开心地用反射解决问题了.\n调研一番之后发现,Jdk 的反射方式不支持遍历父类所有子类的方法,这做法行不通阿!!!\n在我都几乎要放弃,要手写所有ModelTest的时候,我在StackOverFlow上面找到了 reflections 这第三方包,发现这个包非常强大(niubility), 可以获取基类的子类或者接口的实现类:\nReflections reflections = new Reflections(\u0026#34;my.project\u0026#34;); Set\u0026lt;Class\u0026lt;? extends SomeType\u0026gt;\u0026gt; subTypes = reflections.getSubTypesOf(SomeType.class); 简直了。在这”牛包”的帮助下,成功实现了扫描某个package下面所有基类的实现类的方法, 我的用例有救了:\npublic class ModelTest { protected static final Logger LOGGER = LoggerFactory.getLogger(ConvertorTest.class); private static final String PACKAGE_NAME = \u0026#34;xxx.xxx.core.service.v1.busimodel\u0026#34;; // model所有的包 @Test public void testModel() { Reflections reflections = new Reflections(PACKAGE_NAME); Set\u0026lt;Class\u0026lt;? extends BaseBusiModel\u0026gt;\u0026gt; classes = reflections.getSubTypesOf(BaseBusiModel.class); for (Class\u0026lt;? extends BaseBusiModel\u0026gt; clazz : classes) { if (Modifier.isAbstract(clazz.getModifiers())) { continue; } BaseBusiModel modelInstance = null; try { modelInstance = clazz.newInstance(); } catch (IllegalAccessException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法IllegalAccessException异常, clazz: {}\u0026#34;, e, clazz.getName()); } catch (InstantiationException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法InstantiationExceptionn异常, clazz: {}\u0026#34;, e, clazz.getName()); } ModelUtils.invokeGetAndSetMethod(modelInstance); } } } ModelUtils.invokeGetAndSetMethod(modelInstance); 这个静态方法就是上一节反射方法的完整可用版:\npublic class ModelUtils { protected static final Logger LOGGER = LoggerFactory.getLogger(ModelUtils.class); /** * get类型方法的前缀 */ private static final String GET_METHOD_PREFIX = \u0026#34;get\u0026#34;; /** * get类型方法的前缀 */ private static final String SET_METHOD_PREFIX = \u0026#34;set\u0026#34;; /** * 调用clazz 对象的所有get, set方法 * * @param clazz */ public static void invokeGetAndSetMethod(Object clazz) { invokeMethodWithPrefix(GET_METHOD_PREFIX, clazz); invokeMethodWithPrefix(SET_METHOD_PREFIX, clazz); } /** * 通过方法前缀调用方法 * * @param prefix * @param instance */ public static void invokeMethodWithPrefix(String prefix, Object instance) { Method[] methods = instance.getClass().getDeclaredMethods(); for (Method method : methods) { if (Modifier.isPublic(method.getModifiers()) \u0026amp;\u0026amp; method.getName().startsWith(prefix)) { Object[] parameters = new Object[method.getParameterCount()]; try { method.invoke(instance, parameters); } catch (IllegalAccessException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, instance.getClass().getName(), method.getName()); } catch (InvocationTargetException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, instance.getClass().getName(), method.getName()); } } } } } 1.4 总结 Reflections 包是真的强,有空要去看一下源码 懒惰是程序员的第一生产力, 这话真不是我编的,是Perl 语言之父 Larry Wall 说的 加了其他两个类似功能的反射测试类,我的变更行覆盖率暴增30% (可以看出我这次的变更主要是新增模型和工具类,这样反射才能调用规律性代码) Java大法好,Java世界那么大,还需要我好好探索. ","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%A6%82%E4%BD%95%E5%88%B7pojo%E7%B1%BB%E7%9A%84%E5%8F%98%E6%9B%B4%E8%A1%8C%E8%A6%86%E7%9B%96%E7%8E%87/","summary":"刷POJO类的变更行覆盖率 1 反射大法好 1.1 背景 众所周知,蚂蚁对代码质量要求很高,质量红线其中一项指标就是变更行覆盖率。如果你的变更行覆盖率没有","title":"How to fool the Jacoco ◜◡‾"},{"content":"1 前言 最近读完了《追风筝的人》这部书,这部书给我一种熟悉的感觉,一种时代史诗的感觉。后来终于想起,这种时代史诗的感觉《霸王别姬》也有过\n2 兄弟情 《追风筝的人》前面有很大一部分都是在描写哈桑和阿米尔的活动,两个孩童每日的游戏,只有他们两个人,一个哈扎拉男孩和一个普什图男孩,亲密无间。通过阿米尔对孩童时和哈桑游戏的会议,即向读者展示了他们的感情,也披露 昔日的阿富汗是那般的安静祥和。他们之间最激动兴奋的活动就是斗风筝:用自己的风筝割断别人风筝线,最后幸存者获胜,追到割下的风筝会成为荣耀的战利品。而阿米尔是斗风筝的好手,哈桑是追风筝的好手,真的是注定的伙伴。\n3 突变 历史上存在无数的种族冲突,有的成为历史的一部分,有的成为生活的一部分,比如美国当初的黑人和白人,比如阿富汗的哈扎拉人和普什图人;甚至还有我无法理解的教派争端,如逊尼派和什叶派的冲突。正如哈桑和阿米尔的亲密,在自诩的 \u0026ldquo;卫道者\u0026quot;眼中既是原罪。正是这样的原罪,导致哈桑在阿米尔赢得喀布尔风筝大赛之后,奋力追着阿米尔的战利品的时候,\u0026ldquo;卫道者\u0026quot;阿塞夫侵犯了哈桑,目睹一切的阿米尔选择了避让,背叛了那个说\u0026quot;为你,千千万万遍\u0026quot;的男孩。这事成为阿米尔 以后的梦魇,也成为阿米尔和哈桑破裂的导火索。对不起一个人,心存愧疚,难道最好的方式是永远不见被自己所负之人?眼不见为净?\n孩童关系发生突变的时候,阿富汗国家形势也发生了突变:苏联入侵阿富汗,开始了对阿富汗16年之久的征伐。我自己也曾查阅资料,还是无法明白为什么苏联要入侵阿富汗,对我而言只是心存疑惑的事,对阿富汗人民来说,就是苦难的来源。 喀布尔曾经的风筝,歌谣,烤羊羔,如今变成了地雷,坦克,火箭炮,阿富汗的人民的遭遇,不禁让我想起当初的日本铁蹄下中国,真的是宁为太平犬,毋为乱世人\n篇中多次提到的羔羊意象,如影片《沉默的羔羊》那样,成为主角心中的梦魇。那羔羊被杀前的眼神,那迫在眉睫的厄运,是为了某个崇高的目的。而后的阿富汗人民都成为了羔羊,或许这是真主的安排的磨练,或许这样的想法能稍稍欢慰那些饱经磨难的心,或许\u0026hellip;\n4 赎罪之路 二人关系突变之后,哈桑和父亲离开了喀布尔,阿米尔和父亲也因为国家的突变离开了阿富汗,辗转之后去了美国。阿米尔若干年后在美国求学,娶妻,立业,而后父亲离世,时间慢慢向前走,平淡而甜蜜。直到阿拉辛的一通电话,将阿米尔拉回了那个 记忆中苦难的祖国,阿拉辛的话:\n来吧,这里有再次成为好人的路\n就这样阿米尔走上了赎罪之路,为自己对哈桑曾经的懦弱赎罪,也为父亲的欺骗赎罪-哈桑是父亲的私生子,父亲背叛了阿里。而后,\u0026ldquo;为你,千千万万遍\u0026quot;的哈桑,因为自己的忠诚付出了生命的代价, 后面就是阿米尔拯救哈桑儿子索拉博,拯救曾经的自己的事情了\n5 时代史诗 以前的阿富汗,只是新闻上的一处地名,经常伴随着爆炸和恐怖袭击。而《追风筝的人》让我意识到,这也是一片有血有肉,曾经有过欢声笑语的土地,而战争带走了一切,从苏联,到塔利班,再到美国,战火无情的肆虐着那片充满苦难的土地。\n《追风筝的人》给我的那种熟悉的感觉,《霸王别姬》也曾有过,写的是程蝶衣和段小楼两个戏子的故事,说的却是整个20世纪风云变幻的中国,从清朝到民国,日本人也来了,而后战争结束,但却不代表河清海晏,毕竟塔利班也带来过和平,也被当作过英雄。\n好的作品总是有共通之处的,总是能触动人心,正如最好的战争电影总是反战的,人文关怀总是可以引起共鸣,读过《追风筝的人》虽未令我潸然泪下,但也感人至深。篇末的阿米尔为索拉博追到风筝,也追到那个期许的自己,那条再次成为好人的路,他找到了.\n6 那些直达人心的句子 为你,千千万万遍(这不只是一份诺言) 记住,阿米尔少爷,没有鬼怪,只是个好日子(没想到,阿米尔少爷成为湖里的鬼怪) 没有良心,没有美德的人不会痛苦 当罪行导致善行,那就是真正的救赎 我们生活的喀布尔是个奇怪的地方,在那儿,有些事情比真相更重要 ","permalink":"https://ramsayleung.github.io/zh/post/2019/%E8%BF%BD%E9%A3%8E%E7%AD%9D%E7%9A%84%E4%BA%BA/","summary":"1 前言 最近读完了《追风筝的人》这部书,这部书给我一种熟悉的感觉,一种时代史诗的感觉。后来终于想起,这种时代史诗的感觉《霸王别姬》也有过 2 兄弟","title":"追风筝的人"},{"content":"一晃,2018年已经过去了\n6月25日,拖着行李,从广州来了杭州\n告别了学校,从学生变成了一个社会人\n既然选择了远方, 便只顾风雨兼程 \u0026ndash; 汪国真\n1 工作 从工作上来说,我”换”了两份工作,阿里大文娱和蚂蚁金服; 阿里大文娱-UC 2017.11-2018.5 实习,然后毕业之后入职蚂蚁金服-微贷-网商银行,主要是负责客户相关的业务;工作很累,但是总归是有收获的.\n入职蚂蚁之后,感觉就是忙,很忙。从新人培训的近卫军到回归日常业务,每天都有各种各样的事情需要处理,加班已经成为了工作中”不可磨灭的一部分”了\n刚入职的时候,给自己定了目标:业务上熟悉自己客户相关的业务,熟悉领域模型,继而从客户延伸了解整个网商银行的业务,学习金融知识;技术上学习组里的高可用架构,如何实现分布式系统的高可用,学习高并发-高可用-分布式-Java/蚂蚁中间件 生态;争取一年P6\n但是大半年下来,基本都是没有达到自己的预期目标,目测升P6的目标基本也是凉了。反思没有达到预期的原因;自身原因有之,外部原因亦有之.\n10,11月这两个月,组里的同事被拉去做各种项目,之剩下包括我在内的两个开发,面对一堆需求,资源最紧张的时候,我们每个人,每个迭代需要开发3个需求,然而一个迭代开发加自测只有一周多的时间,实在是忙。\n忙导致的副作用就是累,而后下班回家只想睡觉,每天学习一个小时的目标早已抛之脑外。每天被需求推着走,没有对需求后面的意义进行思考,只是简单的需求翻译器,并不会有多少成长,兼之对需求不了解,导致需求发现变更的时候手忙脚乱.\n12月之后需求缓下来之后,就开始有时间对之前做的事做个总结,可以对之前完成需求时积下来的问题进行反刍,结合现有的模型进行理解,过程虽费时,总归有收获;现有的业务开发开始渐入佳境,然后就开始”拥抱变化”,客户的业务全部交接别的团队,客户的团队被分流到其他团队,负责别的业务。\n以前总是听说阿里的”拥抱变化”,没想到来得如此之快,这么快就有了体感。\n2 生活 2017.12.31-2018.12.31, 单身, 按下不表\n2018.6, 从UC离职之后,趁着还有些许学生时光,就和两个好友去了趟顺德,品尝一下顺德的美食,所谓食在广州, 厨出凤城,广州生活了四年,是时候去尝尝凤城(顺德)的滋味。\n4天的微游,终究是不虚此行,在蝦炳海鲜吃到了最好吃的烧鹅,每天去公寓对面的茶楼喝早茶,去了清晖园游园,也去了民信老店尝了各式甜品(感觉民信真的不咋地,贵且不说,味道还不咋地,不值特意来)\n2018.8, 团队outing去了趟庐山(基本自费),庐山果真是个避暑圣地,把穿着短袖短裤,并只带了短袖短裤的这个广东人冻成dog。\n不过无论如何,庐山还是不虚此行的\n2018.10, 害怕挂了,开始重新踢球当作运动.\n开始只有十分钟体能,全程只能散步,真的是丢人。过了两个多月,体能提升到了三十分钟,优秀\n2018.12, 毕业半年, 轻了十斤左右。看来工作真的是烧脑,占体重8%的器官,消费了超过20%的能量\n3 读书 工作之后,买了两打书,分类大概是计算机相关/非计算书籍=3/1,然而过去近四个月,也只是读了不到三分之一的书,快餐文化盛行的今天,看来很难沉下心看书(不要甩锅呢)\n计算机相关: 重温了《Effective Java》, 《java并发编程实战》, 《深入理解Java虚拟机》和《Java8 in action》《CSAPP》(没读完),新入了《C++primer》和 《UNIX环境高级编程》(没读完)\n非计算书籍大概读了《活着》,《追风筝的人》\n饭仍是要继续吃,书也是要继续读的.\n4 美食 来了杭州之后,只做过一次饭,做给舍友吃, 幸好舍友还是吃得挺开心的 :)\n而后再也没有做过。 看了三部美食纪录片聊以自慰 《人生一串》,《人间风味》,《寻味顺德》。\n再次感谢《人生一串》,为广东人洗白,看来广东人吃得真的很正常,一点也不重口。\n嗯,在杭州想念广东的味道了,想念不吃辣的味道,想家了.\n5 展望 一年P6(感觉没戏了,那就两年P6) 了解分布式, 高可用的知识,争取通过实战掌握; 读完《netty in action》, 通过许家纯大大的教程,自己实现一个Rpc 框架;读Sofa-rpc 和 Netty 的源码 成为一个掌握金融知识的计算机从业人员 读完20本书 结束单身狗的生活(估计也没戏了) ","permalink":"https://ramsayleung.github.io/zh/post/2019/2018%E6%80%BB%E7%BB%93/","summary":"一晃,2018年已经过去了 6月25日,拖着行李,从广州来了杭州 告别了学校,从学生变成了一个社会人 既然选择了远方, 便只顾风雨兼程 \u0026ndash; 汪国真 1 工作","title":"迟来的2018年总结"},{"content":"Maven 在工作中的经验以及《Maven 实战》读后感\n1 前言 蚂蚁金服的伯岩大大曾经说 Java 生态都太重量级,连Maven 都是怪兽级的构建工具,需要整整一本书来讲解. 平心而论,Maven 的确\u0008如此, 但是无论是怪兽级,还是迪迦级的工具,只要能把事情做好了就是好工具, 而 Maven 恰恰就是这样的工具\n2 配置文件 2.1 pom.xml 就好像 Unix 平台的 Make 对应的 MakeFile,Cmake对应的 CmakeFile.txt, Maven 项目的核心是 pom.xml, POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖等等,可以 pom.xml 是 Maven 一切实践的基础\n3 依赖管理 3.1 坐标 Maven 仓库中有成千上万个构件(jar,war 等文件),Maven 如何精确地找到用户所需的构件呢,用的就是坐标。说起坐标,可能第一反映是平面几何中的 x,y坐标,通过 x,y坐标来唯一确认平面中的一个点,而Maven 的坐标就是用来唯一标识一个构件。\nMaven 通过坐标为构件引入了秩序,任何一个构件都需要明确定义自己的坐标,而坐标是由以下元素组成:groupId, artifactId, version, packaging, classifier, scope, exclusions等。一个典型的Maven 坐标:\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-beans\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 坐标元素详解:\ngroupId(必填): 定义当前Maven 项目隶属的实际项目, 一般是域名的方向定义 artifactId(必填): 定义实际项目中的一个Maven 项目,推荐的做法是使用实际项目名称作为 artifactId 的前缀, 比如上例的 artifactId 是 spring-beans,使用了实际项目名 spring 作为前缀 version(必填): 定义了Maven 项目当前所处的版本,如上例版本是 1.2.6 packaging(选填): 定义了Maven 项目的打包方式。打包方式和所生成的构建的文件扩展名对应,如果上例增加了\u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt;元素,最终的文件名为spring-beans-1.2.6.jar(Maven 打包方式默认是 jar),如果是 web 构件,打包方式就是 war,生成的构件将会以.war 结尾 classifier: 用来帮助定义构建输出的一些附属构件. 附属构建和主构件对应,如上例的主构件是spring-beans-1.2.6.jar, 这个项目还会通过使用一些插件生成`=spring-beans-1.2.6-doc.jar=, spring-beans-1.2.6-source.jar, 其中包含文档和源码 exclusions: 用来排除依赖 scope: 定义了依赖范围,例如 junit 常见的scope 就是\u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt;, 表示这个依赖只对测试生效 3.2 依赖范围 上文提到,JUnit 依赖的测试范围是test,测试范围用元素scope 表示。首先需要知道,Maven 在编译项目主代码的时候需要使用一套classpath,上例在编译项目主代码的时候就会用到spring-beans,该文件以依赖的方式呗引入到classpath 中。\n其次,Maven 在执行测试时候会使用另外一套 classpath。如上文提到的 JUnit 就是以依赖的方式引入到测试使用的 classpath,需注意的是这里的依赖范围是test. 最后,项目在运行的时候,又会使用另外一套的 classpath,上例的spring-beans就是在该classpath里,而JUnit 则不需要。\n简而言之,依赖范围就是用来控制依赖与这是那种 classpath (编译classpath, 测试 classpath, 运行 classpath 的关系,Maven 有以下几种依赖范围:\ncompile: 编译依赖范围,如果没有显式指定scope, 那么compile就是默认依赖范围,使用此依赖范围的Maven 依赖,对于编译,测试,运行三种 classpath 都是有效的 test: 测试依赖范围,指定了该范围的依赖,只对测试 classpath 有效,在编译或者运行项目的时候,无法使用该依赖;典型例子就是 JUnit provided: 已提供依赖范围。使用此依赖范围的 Maven 依赖,对于编译和测试classpath 有效,但在运行时无效 runtime:运行时依赖范围。使用此依赖范围的 Maven 依赖,对于测试和运行的classpath 有效,但在编译主代码时无效 import: 导入依赖范围,该依赖范围不会对三种 classpath 产生实际的影响 system: 系统依赖方位。与 provided 依赖范围完全一致, 即只对编译和测试的classpath有效,对运行时的 classpath 无效. 但是,使用system 范围的依赖必须通过systemPath 元素显式地指定依赖文件的路径 如: \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.sql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jdbc-stdext\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;system\u0026lt;/scope\u0026gt; \u0026lt;systemPath\u0026gt;${java.home}/lib/rt.jar\u0026lt;/systemPath\u0026gt; \u0026lt;/dependency\u0026gt; 由于此类依赖不是通过Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用 上述除import 以外的各种依赖范围与三种classpath 的关系如下:\n依赖范围 scope 对于编译classpath有效 对于测试classpath 有效 对于运行时classpath有效 例子 compile Y Y Y spring-core test \u0026ndash; Y \u0026ndash; JUnit provided Y Y \u0026ndash; servlet-apt runtime \u0026ndash; Y Y JDBC 驱动实现 system Y Y \u0026ndash; 本地的,java类库以外的文件 4 仓库 上文提及了依赖管理,通过声明的方式指定所需的构件,那么是从哪里获取所需的构件的呢?答案是 Maven 仓库,Maven 仓库可以分为两类: 本地仓库和远程仓库。\n当 Maven 需要根据坐标寻找构件的时候,它首先会查找本地仓库,如果本地仓库存在该构件,则直接使用,如果本地不存在该构件,或者需要查看是否有更新的构件版本,Maven 聚会去远程仓库查找,发现需要的构件之后,下载到本地仓库在使用.\n如果本地和远程仓库都没有所需要的构件,那么 Maven 就会报错。如果需要细化远程仓库的类型,还可以分成中央仓库,私服和其他公共库。\n中央仓库:Maven 核心自带的的远程仓库,它包含了绝大部分开源的构件。在默认的配置下,当本地仓库没有 Maven 需要的构件的时候,它就会尝试从中央仓库下载。 私服:为了节省带宽和时间,可以在内网假设一个特殊的仓库服务器,用来代理所有的外部的远程仓库 Figure 1: repo\n4.1 SNAPSHOT 在Maven 的世界中,任何一个项目或者构件都必须有自己的版本,版本可能是 1.0.0, 1.0-alpha-4,2.1-SNAPSHOT 或者 2.1-20181028-11, 其中 1.0.0, 1.0-alpha-4 是稳定的发布版本,而 2.1-SNAPSHOT 或者 2.1-20181028-11 是稳定的快照版本。\nMaven 为什么要区分快照版本和发布版本呢?难道1.0.0 不能解决么?为什么需要2.0-SNAPSHOT。\n我对此 SNAPSHOT 这个特性印象非常深刻,在蚂蚁金服的新人培训中,其中就有一项是大家协作完成一个 Mini Alipay,一个 Mini Alipay 分成三个应用bkonebusiness, bkoneuser, bkoneacccount,以SOA 的架构进行拆分,应用之间相互依赖。\n在开发过程中,bkoneuser 经常需要将最新的构件共享 bkonebusiness, 以供他们进行测试和开发。\n因为bkoneuser本身也在快速迭代中,为了让bkonebusiness 用到最新的代码,我们不断地变更版本,1.0.1, 1.0.2, 1.0.3,\u0026hellip; bkoneuser 不断发版本,bkonebusiness 不断升版本,甚至有一次bkoneuser 在没有更新版本号的情况下发布了最新代码,而 bkonebusiness 已经有原来版本的 jar 包,所以就没有去远程仓库拉取最新的代码,就出问题了\u0026hellip;.\n其实 Maven 快照版本就是为了解决这种问题,防止滥用版本号和及时拉取最新代码。\nbkoneuser 只需将版本指定为1.0.1-SNAPSHOT, 然后发布到远程服务器,在发布的工程中,Maven 会自动为构件打上时间戳,比如 1.0.1-20181028.120112-13 表示 2018年10月28号的12点01分12秒的13次快照,有了时间戳,Maven 就能随时找到仓库中该构件1.0.1-SNAPSHOT版本的最新文件。\n这是,bkonebusiness对于 bkoneuser的依赖,只要构建bkonebusiness,Maven就会自动从仓库中检查 bkoneuser的罪行构建,发现有更新便进行下载。\n基于快照版本,bkonebusiness 可以完全不用考虑 bkoneuser 的构建,因为它总是拉取最新版本的 bkoneuser,这个是 Maven 的快照机制进行保证。\n如果到了 release,就要及时将 1.0.1-SNAPSHOT, 否则 bkonebusiness 在构建发布版本的时候可能拉取到最新的有问题的版本.\n4.2 仓库搜索服务 在公司开发的时候有私服,但是在开发自己项目的时候,我一般到 SnoaType Nexus 找对应的构件\n5 插件与生命周期 5.1 何为生命周期 在有关 Maven 的日常使用中,命令行的输入往往就对应了生命周期,如 mvn package 就表示执行默认的生命周期阶段 package.\nMaven 的生命周期是抽象的,其实际行为都由插件来完成,如package 阶段的任务就会有maven-jar-plugin 完成。\nMaven的生命周期就是为了对所有的构建过程进行抽象和统一,包括项目的清理,初始化,编译,测试,打包,集成测试,验证,部署等几乎所有的构建步骤。\n需要注意的是 Maven 的生命周期是抽象的,这意味着生命周期本身不作任何实际的工作,实际的任务(如编译源代码)都交由插件来完成. 每个步骤都可以绑定一个或者多个插件行为,而且Maven 为大多数构建步骤编写并绑定了默认的插件\n例如:针对编码的插件有 maven-compiler-plugin,针对测试的插件有maven-surefire-plugin 等,用户几乎不会察觉插件的存在\n5.2 三套生命周期 Maven 有用三套相互独立的生命周期,它们分别是clean, default , site. clean 生命周期的目的是清理项目,default 生命周期的目的是构件项目,而 site 生命周期的目的是建立项目站点\n5.2.1 clean 生命周期 clean 生命周期主要是清理项目,它包含三个阶段:\npre-clean: 执行一些清理前需要完成的工作 clean 清理上一次构造生成的文件 post-clean 执行一些清理后需要完成的工作 5.2.2 default 生命周期 default 生命周期奠定了真正构件时所需要执行的所有步骤,它是所有生命周期最核心的部分,其包含的阶段如下:\nvalidate initialize generate-sources process-sources 处理项目主资源文件。一般来说,是对src/main/resources 目录内的内容进行变量替换的工作后,复制到项目输出的主classpath 目录中 generate-resources process-resources compile 编译项目的主源码,一般来说,是编译 src/main/java 目录下的java 文件至项目输出的主 classpath 目录中 process-classes generate-test-sources process-test-sources 处理项目测试资源文件。一般来说,是对src/test/resources 目录的内容进行变量替换等工作后,复制到项目输出的测试classpath 目录中 generate-test-resources process-test-resources test-compile 编码项目的测试代码。一般来说,是编译 src/test/java 目录下的java 文件至项目输出的测试classpath 目录中 process-test-classes test 使用单元测试框架运行测试,测试代码不会被打包或部署 prepare-packae package 接受编译好的代码,打包或可发布的格式,如 jar pre-integration-test integration-test post-integration-test vertify install 将包安装到Maven 本地仓库,供本地其他Maven 项目使用 deploy 将最终的包复制到远程仓库,共其他开发人员和Maven 项目使用 5.2.3 site 生命周期 site 生命周期的目的是建立和发布项目站点,生命周期包含如下阶段\npre-site 执行一些在生成项目站点前需要完成的工作 site 生成项目站点文档 post-site 执行一些在生成项目站点之后需要完成的工作 site-deploy 将生成的项目站点发布到服务器上 5.2.4 命令行和生命周期 从命令行执行Maven 任务的最主要方式就是调用 Maven的生命周期阶段。需要注意的是,各个生命周期是相互独立的,而一个生命周期的阶段是有前后依赖关系的。\n下面以一些常见的Maven 命令为例,解释其执行的生命周期阶段:\nmvn clean: 该命令调用clean 生命周期的clean 阶段。实际执行的阶段为clean 生命周期的pre-clean 和clean 阶段 mvn test: 该命令调用default 生命周期的test 阶段。实际执行的阶段是 default 生命周期的 validate, initialize, 直到 test 的所有阶段。这也解释了为什么在测试的时候,项目的代码能够自动得以编译 mvn clean install: 该命令调用 clean 生命周期的clean 阶段和default 生命周期的 install 阶段。实际执行的阶段为 clean 生命周期的 pre-clean, clean 阶段,以及default 生命周期的从validate 到 install 的所有阶段。该命令结合了两个生命周期,在执行真正的项目构建之前清理项目是一个很好的实践 6 继承 如bkoneuser 的项目结构所示:\nFigure 2: bkoneuser 的项目结构\n按照 DDD(Domain Driven Design) 的驱动,bkoneuser 下有多个对应的子模块,每个模块也是一个 Maven 项目,每个模块里面可能有相同的依赖,如 SpringFramework 的 spring-core, spring-beans, spring-context 等。\n如果每个子模块都维护一份大致相同的依赖,那么就有10几份相同的依赖,这还会随着子模块的增多而变得庞大。\n如果我们工程师的嗅觉, 会发现有很多的重复依赖,面对重复应该怎么办?通过抽象来减少重复代码和配置,而 Maven 提供的抽象机制就是继承(还有聚合,只是个人觉得不如继承常用).\n在 OOP 中,工程师可以建立一种类的父子结构,然后在父类中声明一些字段供子类继承,这样就可以做到“一处声明,多处使用”, 类似地,我们需要创建 POM 的父子结构,然后在父POM 中声明一些供子 POM 继承,以实现“一处声明,多处使用”\n6.1 配置示例 parent 的配置如下:\n\u0026lt;groupId\u0026gt;com.minialipay\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;bkgponeuser-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;bkgponeaccount.common.service.facade.version\u0026gt;1.1.0.20180919\u0026lt;/bkgponeaccount.common.service.facade.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;module\u0026gt;app/core/service\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/core/model\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/biz/shared\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/biz/service-impl\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/util\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/service/facade\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/service/integration\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/dal\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/test\u0026lt;/module\u0026gt; \u0026lt;/modules\u0026gt; 需要主要的关键点是parent 的 packaging 值必须是 pom, 而不是默认的 jar, 否则则无法进行构件.\n而 modules 元素则是实现继承最核心的配置,通过在打包方式为 pom 的Maven 项目中声明任意数量的 module 来实现模块的继承, 每个 module的值都是一个当前POM 的相对目录,比如 app/core/service 就是说子模块的POM在 parent 目录的下的 app/core/service目录\n6.2 子模块配置示例 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;com.minialipay\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;bkgponeuser-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;relativePath\u0026gt;../../../pom.xml\u0026lt;/relativePath\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;bkgponeuser-core-service\u0026lt;/artifactId\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; 上述pom 中使用 parent 元素来声明父模块,parent 下元素groupid, artifactId 和 version 指定了父模块的坐标,这三个元素是必须。\n元素 relativePath 表示父模块POM的相对路径, ../../../pom.xml 指父POM的位置在三级父目录上\n6.3 可继承的POM 元素 可继承元素列表及简短说明:\ngroupId: 项目Id, 坐标的核心元素 version:项目版本, 坐标的核心元素 description: 项目的描述信息 organization: 项目的组织信息 inceptionYear: 项目的创始年份 url: 项目的url 地址 developers: 项目的开发者信息 contributors: 项目的贡献者信息 distributionManagement:项目的部署配置 issueManagement: 项目的缺陷跟踪系统信息 ciManagement: 项目的持续继承系统信息 scm: 项目的版本控制系统信息 mailingLists: 项目的邮件列表信息 properties: 自定义的Maven 属性 dependencies: 项目的依赖配置 dependencieyManagemant: 项目的依赖管理配置 repositories: 项目的仓库配置 build: 包括项目的源码目录配置,输出目录配置,插件配置,插件管理配置等 reporting: 包括项目的报告输出目录配置,报告插件配置等 6.4 dependencyManagement 依赖管理 可继承列表包含了 dependencies 元素,说明是会被继承的,这是我们就会很容易想到将这一特性应用到 bkoneuser-parent 中。子模块同时依赖 spring-beans,=spring-context=,=fastjson= 等, 因此可以将这些依赖配置放到父模块 bkoneuser-parent 中,子模块就能移除这些依赖,简化配置.\n这种做法可行,但是存在问题,我们可以确定现有的子模块都是需要 spring-beans, spring-context 这几个模块的,但是我们无法确定将来添加的子模块就一定需要这四个依赖.\n假设将来项目中要加入一个app/biz/product, 但是这个模块不需要 spring-beans, spring-context, 只需要 fastjson, 那么继承 bkoneuser 就会引入不需要的依赖,这样是非常不利于项目维护的!\nMaven 提供的 dependencyManagement 元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在 dependencyManagement 元素下的依赖声明不会引入实际的依赖,不过它能够约束 dependencies 下的依赖使用。\n例如在 bkoneuser-parent 用 dependencyManagement声明依赖:\n\u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.33\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.7\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 在 app/core/service 子模块进行引用:\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 子模块的fastjson 依赖只配置了 groupId 和 artifactId, 省去了 version , 而 junit 依赖 不仅省去了version, 连scope 都省去了。\n《Maven 实战》作者强烈推荐使用这种方式,其主要原因在与在父POM 中使用 dependencyManagement 声明依赖能够统一规范依赖的版本,当依赖版本在父POM中声明之后,子模块在使用依赖的时候就无须声明版本,也就不会发生多个子模块使用依赖版本不一致的情况\n7 依赖冲突 在Java 项目中,随着项目代码量的增长,各种问题就会接踵而至,jar 包冲突就是其中一个最常见的问题. jar 冲突常见的异常: NoSuchMethodError, NoClassDefFoundError\n7.1 成因 当Maven根据pom文件作依赖分析, 发现通过直接依赖或者间接依赖, 有多个相同groupId, artifactId, 不同 version 的依赖时, 它会根据两点原则来筛选出唯一的一个依赖, 并最终把相应的jar包放到 classpath下:\n依赖路径长度: 比如应用的pom里直接依赖了A, 而A又依赖了B, 那么B对于应用来说, 就是间接依赖, 它的依赖路径长度就是2. 长度越短, 优先级越高. 当出现不同版本的依赖时, maven优先选择依赖路径短的依赖. 依赖声明顺序: 当依赖路径长度相同时, POM 里谁的声明在上面, Maven 就选择谁. public class A { private B b =new B(); public void func_a(){ b.func_b(); } } // 来自b-1.0.jar public class B { private C c=new C(); public void func_b(){ c.func_c(); } } // 来自c-1.0.jar public class C{ public void func_xxx(){ } public void func_c(){ } } // 来自c-1.1.jar public class C{ public void func_xxx(){ } public void func_c1(){ } } // d.1.0.jar public class D{ // 来自c-1.1.jar C c = new C() public void func_d(){ c.func_xxx(); } } public class MyMain{ public static void main(String[] args){ new A().func_a() } } 应用程序里有个A类, 里面含有一个属性B, 这个B类来自 b-1.0.jar 包. A类有个 func_a() 方法, 里面会调用b类的 func_b 方法.B类含有一个属性C, 这个C类来自c-1.0.jar. B类还提供一个方法 func_b(), 里面调用C类的 func_c() 方法.\n这时, 应用程序的主POM里间接依赖了 c-1.1.jar 包, 但是这个jar里的C类中已经把 func_c() 删除了.\n这样由于B类使用的 c-1.0.jar 对于应用程序来说, 是间接依赖, 依赖路径长度是2 (A -\u0026gt; B -\u0026gt; C), 比应用程序主pom中间接依赖的 c-1.1.jar 路径(D-\u0026gt;C)长, 最后就会被maven排掉了 (也就是应用程序的 classpath 下, 最终会保留 c-1.1.jar).\n最后执行main函数时, 就会报 NoSuchMethodError, 也就是找不到C类中 func_c() 方法.\n7.2 解决方案 强制Maven 使用c-1.0.jar, 也就是将c-1.1.jar排除掉:\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.d\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;d\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;com.c\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;c\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 在 d.1.0.jar 的依赖排除 c.1.1.jar 的时候,不需要指定版本, 因为这个时候d.1.0.jar 的依赖的版本一定是 c.1.1.jar. 需要注意的是,如果 d 使用了c.1.1.jar 的 func_c1(),排掉 c.1.1.jar 是会报错的,因为满足了B类的 func_c() 就无法满足 D 类的 func_c1(), 这个就是著名的“菱形依赖问题”(diamond dependency problem)。\n不得不说,入职的时候,遇上了各种jar 包冲突的问题,排包都排出心得. 在此推荐个排包神器, Intellij Idea 的插件:maven helper, 比手动-verbose:class + mvn dependency:tree排包方便多了\n8 总结 的确,写到这里,必须再次承认 Maven 是怪兽级的 构建工具,但是同样无可否认的是,它出色的构建和依赖管理功能。写go 语言的时候,我多希望有个 Maven 可以用呢 ╥﹏╥\u0026hellip;\n","permalink":"https://ramsayleung.github.io/zh/post/2018/maven%E5%B0%8F%E8%AE%B0/","summary":"Maven 在工作中的经验以及《Maven 实战》读后感 1 前言 蚂蚁金服的伯岩大大曾经说 Java 生态都太重量级,连Maven 都是怪兽级的构建工具,需要整整一本书","title":"Maven 小记"},{"content":"纪念我即将终结的大学时光\n1 前言 转眼到了5月,广州的夏天来的特别早。学校的花已谢幕,而我在上个月已经拍完毕业照,在上星期已经完成了毕业答辨,如无意外,我大学的旅程已要走到尽头。\n而我大一入学的场景还历历在目,恍如昨日。\n2 迷茫 往事如烟,当初因高考失利(是个人都说自己失利, 这不禁让我想起了\u0026lt;\u0026gt;, 每个罪犯都自嘲说自己无罪),入学时充满不甘。\n但是心知一切已成事实,我自己无论怎样不甘和愤懑,都是于事无补的。当时的我一心想要弥补当初的失利,只是不知道如何去规划,如何去做。开始时是效仿高中时的学习方法,好好上课,努力做题,大一上学期大半个学期就是这么度过的。\n只是这样无甚成效,也没有一个准确的评判标准来衡量我的努力。后来经常向助班师兄请教,希望师兄可以帮我走出困扰,与师兄聊了很多次,师兄建议我学好专业课,多实践, 学习计算机不能像高中那样学。\n3 奋进 助班师兄的建议以及身边同学的激励(业神在大一的时候已经可以自己编写绕过游戏的程序保护的游戏外挂,可以自己和大三的师兄组建工作室赚外快)让我有了方向.\n所以从大一下学期到大三上学期,我大部份时间都是在图书馆和工作室度过,在图书馆自学计算机系的专业课,从操作系统到计算机网络,再到数据库,而后在工作室中跟着其他师兄做各种项目。\n那些年在学校的图书馆还是借阅了很多书的,虽说有些只是借了之后就还回去了,但是大部份的书还是有看的,如图:\nFigure 1: 借书\n大二寒暑假放假后,我都会留校去工作室写项目,记得大二那年的寒假,也就是2016年的春节前后,在广州下了雪(只不过山东的同学说那只能算是冰渣子),那时我骑车从宿舍去 工作室,每次都把手冻得通红。现在想来,真的感慨万分\n4 机遇 4.1 创业公司 经过大一大二的恶补之后,我那时基本可以写些简单的项目了,正好那个时候同班的聪哥想要组队去参加比赛,就带上了我。\n聪哥负责移动端,我负责后端,我们一起写了个类似超级课程表简化版的APP参加比赛,幸运地拿了校内计算机比赛的冠军,后来我们那同样的作品去参加穗港澳的一个计算机比赛,也侥幸地拿了个三等奖。\n在颁奖典礼上,我偶遇了人生中 第一个大「Boss」\u0026ndash; 老刘,老刘的title 很多,只记得其中的几个:曾在上世纪90年代任职于微软,惠普华南区总裁,现在是美国一所大学的终身副教授,国内电子科技大学的教授。当时在颁奖典礼既技术分享沙龙上,老刘问了几个问题,在场的同学应该是过于羞涩,所以只有我举手回答。\n分享之后老刘就和我交流,当时老刘在一个深圳的创业公司担任VP, 交流过后就邀请我去他的公司实习,就这样我就在大二暑假的时候拿了第一份实习Offer.\n在老刘的教导下,实习的最大收获是视野和信心,看到很多学校老师不能教给我的东西,比如开发规范,上线流程等。在老刘的鼓励下,我觉得自己并不比其他人差。\n4.2 阿里 4.2.1 蚂蚁金服 在大三下学期找实习的时候,在面蚂蚁金服的时候,幸运地通过内推面笔试,在一面二面技术面,面试官考察的问题我也恰好有了解过,就这样我就侥幸拿到名额不多的阿里实习Offer.\n在蚂蚁金服只是实习了一个暑假,具体的需求只是完成了两三个,更多地是在学习蚂蚁金服的技术体系,了解这么大用户量的公司的开发流程,如何在保证代码质量的前提下 进行开发。\n4.2.2 阿里大文娱 在蚂蚁金服实习回来之后,已经大四的我不想在宿舍虚度光阴,因为我大四已经没有任何课了,所以我在广州另外找了个实习--阿里大文娱-UC.\n是Kevin 把我招到UC 的,Kevin很看好我,把我安排到了新成立的核心组,是负责UC 国内业务的平台组,组里的目标是可以发展成可以支撑起100亿量级数据的平台,我入职的时候正是平台刚起步的时候,所以我算是见证了平台的负责。\n而在UC 的半年,是我技术成长最快的半年,Java 的GC, 平台的双活,控流,资源融断等高可用策略,分布式的存储和搜索(Hbase+ElasticSeach)到数据库的优化和基于 shardingKey 的分库分表。\n虽然我不是平台的核心开发者,但是作为参与者,我也是获益匪浅。只是4月底,实习近半年后,我拿到了UC 的Offer,只不过我最终离开了UC, 选择蚂蚁金服,我依然感谢UC 所有帮过我的同事。\n5 展望 在我参加工作前的最后一个半月,我回到了宿舍,看我自己喜欢的动漫,看我自己喜欢的书,吹奏我喜欢的乐曲,登录上我5年没玩的游戏,折腾起如智力游戏般的CPP;\n在我还是学生时,做我自己喜欢的事。峥嵘四载,今再望,诸事宛如梦中。前路茫茫,不失辛酸与希望,突然想起高中英语口试前的那句名言:生活就像海洋,只有意志坚强的人才能到达彼岸。\n","permalink":"https://ramsayleung.github.io/zh/post/2018/farewell_to_my_university_time/","summary":"纪念我即将终结的大学时光 1 前言 转眼到了5月,广州的夏天来的特别早。学校的花已谢幕,而我在上个月已经拍完毕业照,在上星期已经完成了毕业答辨,如","title":"恰同学少年"},{"content":"Socket 泄漏引起的Tomcat 宕机问题分析\n在2018年4月9号下午,收到反馈:测试集群部分接口访问有问题,请求时而正常,时而超时。\n最近的测试环境真的是问题多多,可是测试环境就是我搭建的,冏。查看日志发现87 这台 服务器的Tomcat 无法访问:\n2018-04-09 17:41:31,568 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 2018-04-09 17:41:33,168 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 1 Linux 文件句柄限制 报错看起来像是进程打开文件句柄的个数达到了linux的限制。而这种限制是分为系统层面的和用户层面的\n1.1 系统层面 系统层面的在:/proc/sys/fs/file-max里设置\ncat /proc/sys/fs/file-max 2442976 1.2 用户层面 用户层面的限制在:/etc/security/limits.conf里设定。通过ulimit -a 查看系统允许单个进程打开的最大文件数:\nulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 192059 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 65536 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 10240 cpu time (seconds, -t) unlimited max user processes (-u) 65535 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited 单个进程可以打开的最大文件数是 65536\n2 lsof 显示大量open file 按照Tomcat 给出的报错信息,登录87 这台服务器检查打开的文件数,发现打开的文件超过70000:\nlsof |wc -l 75924 然后找出打开文件数最多的进程,按文件数降序排列,左边是 open file 的数量,右边是进程ID:\nlsof -n|awk \u0026#39;{print $2}\u0026#39;| sort | uniq -c | sort -nr | head 65966 25204 5374 20179 184 27275 65 5361 61 29421 16 22177 14 19751 12 22181 12 22179 12 22178 发现 25204 这个进程打开了大量的文件,已经超过了单个进程的最大文件数限制。而这个进程就是部署的java 应用对应的进程。打开的文件句柄数量已经超过Linux 限制, Tomcat 无法创建新的socket 连接。\n3 can\u0026rsquo;t identify protocol 用 lsof 查看 java 应用打开的文件的时候,发现有非常多奇怪的输出:\njava 25204 nemo *516u sock 0,6 0t0 215137625 can\u0026#39;t identify protocol java 25204 nemo *517u sock 0,6 0t0 215137626 can\u0026#39;t identify protocol java 25204 nemo *518u sock 0,6 0t0 215137627 can\u0026#39;t identify protocol java 25204 nemo *519u sock 0,6 0t0 215137628 can\u0026#39;t identify protocol java 25204 nemo *520u sock 0,6 0t0 215137629 can\u0026#39;t identify protocol java 25204 nemo *521u sock 0,6 0t0 215137630 can\u0026#39;t identify protocol java 25204 nemo *522u sock 0,6 0t0 215137631 can\u0026#39;t identify protocol java 25204 nemo *523u sock 0,6 0t0 215137634 can\u0026#39;t identify protocol java 25204 nemo *524u sock 0,6 0t0 215137635 can\u0026#39;t identify protocol java 25204 nemo *525u sock 0,6 0t0 215137636 can\u0026#39;t identify protocol java 25204 nemo *526u sock 0,6 0t0 215137637 can\u0026#39;t identify protocol java 25204 nemo *527u sock 0,6 0t0 215137638 can\u0026#39;t identify protocol java 25204 nemo *528u sock 0,6 0t0 215137639 can\u0026#39;t identify protocol java 25204 nemo *529u sock 0,6 0t0 215137640 can\u0026#39;t identify protocol java 25204 nemo *530u sock 0,6 0t0 215137641 can\u0026#39;t identify protocol java 25204 nemo *531u sock 0,6 0t0 215137642 can\u0026#39;t identify protocol java 25204 nemo *532u sock 0,6 0t0 215137644 can\u0026#39;t identify protocol java 25204 nemo *533u sock 0,6 0t0 215137646 can\u0026#39;t identify protocol 统计之后发现, can't identify protocol 这样的文件数量非常多:\nlsof -p 25204|grep \u0026#34;can\u0026#39;t identify protocol\u0026#34;|wc -l 64214 也就是大部份打开的文件都是属于 cant' identify protocol 的文件。\n4 问题定位 Google 搜索之后发现,这个 cant' identify protocol 的东东出现的原因是因为 这些 sockets 处于 CLOSED 的状态,但是却没有真正close 掉,正处于 half-close 状态。因此,如果使用 netstat 来查看socket 状态,是不会显示这些 half-close的 socket 的:\nnetstat -nat |wc -l 881 使用 netstat 的改进版本 ss 就能发现大量处于 Closed 状态的 socket:\nss -s Total: 76052 (kernel 76254) TCP: 75924 (estab 123, closed 75524, orphaned 0, synrecv 0, timewait 173/0), ports 104 Transport Total IP IPv6 * 76254 - - RAW 0 0 0 UDP 9 6 3 TCP 116 80 36 INET 125 86 39 FRAG 0 0 0 接着查看内核的 socket 情况:\ncat /proc/net/sockstat sockets: used 75724 TCP: inuse 886 orphan 0 tw 0 alloc 72134 mem 222 UDP: inuse 5 mem 0 UDPLITE: inuse 0 RAW: inuse 0 FRAG: inuse 0 memory 0 很多的 socket 处于 alloc, 只有少量的 socket 处于 inuse. 可以确认是 java 应用出现了 socket fd 的泄漏。 但是为什么会有那么多的socket 泄漏呢?\n5 大胆假设 现在可以确定的是 java应用出现了问题,导致了socket 泄漏,让 Tomcat 无法建立新连接,最终宕机。既然导致问题出现的是 java 应用,那么就应该去检查应用日志。\n2018-04-09 17:41:31,491 - [ERROR] - from com.alibaba.druid.pool.DruidDataSource in Druid-ConnectionPool-CreateScheduler--4-thread-214 create connection error, url: jdbc:mysql://test-server-host:3306/db_name?readOnlyPropagatesToServer=false\u0026amp;rewriteBatchedStatements=true\u0026amp;failOverReadOnly=false\u0026amp;socketTimeout=6000\u0026amp;connectTimeout=20000\u0026amp;zeroDateTimeBehavior=convertToNull\u0026amp;allowMultiQueries=true\u0026amp;characterEncoding=utf-8\u0026amp;autoReconnect=true com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up. at sun.reflect.GeneratedConstructorAccessor169.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.Util.getInstance(Util.java:408) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:918) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2163) at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2088) at com.mysql.jdbc.ConnectionImpl.\u0026lt;init\u0026gt;(ConnectionImpl.java:806) at com.mysql.jdbc.JDBC4Connection.\u0026lt;init\u0026gt;(JDBC4Connection.java:47) at sun.reflect.GeneratedConstructorAccessor152.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:410) at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:328) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:148) at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:211) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:142) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1423) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1477) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.runInternal(DruidDataSource.java:1884) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.run(DruidDataSource.java:1849) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. at sun.reflect.GeneratedConstructorAccessor157.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:341) at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2251) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2104) ... 21 common frames omitted Caused by: java.net.SocketException: Too many open files at java.net.Socket.createImpl(Socket.java:460) at java.net.Socket.getImpl(Socket.java:520) at java.net.Socket.setTcpNoDelay(Socket.java:980) at com.mysql.jdbc.StandardSocketFactory.configureSocket(StandardSocketFactory.java:132) at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:203) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:300) ... 23 common frames omitted 2018-04-09 17:41:31,568 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 2018-04-09 17:41:33,168 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 2018-04-09 17:41:34,470 - [ERROR] - from com.alibaba.druid.pool.DruidDataSource in Druid-ConnectionPool-CreateScheduler--4-thread-216 create connection error, url: jdbc:mysql://test-server-url:3306/db_name?readOnlyPropagatesToServer=false\u0026amp;rewriteBatchedStatements=true\u0026amp;failOverReadOnly=false\u0026amp;socketTimeout=6000\u0026amp;connectTimeout=20000\u0026amp;zeroDateTimeBehavior=convertToNull\u0026amp;allowMultiQueries=true\u0026amp;characterEncoding=utf-8\u0026amp;autoReconnect=true com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up. at sun.reflect.GeneratedConstructorAccessor169.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.Util.getInstance(Util.java:408) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:918) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2163) at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2088) at com.mysql.jdbc.ConnectionImpl.\u0026lt;init\u0026gt;(ConnectionImpl.java:806) at com.mysql.jdbc.JDBC4Connection.\u0026lt;init\u0026gt;(JDBC4Connection.java:47) at sun.reflect.GeneratedConstructorAccessor152.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:410) at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:328) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:148) at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:211) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:142) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1423) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1477) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.runInternal(DruidDataSource.java:1884) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.run(DruidDataSource.java:1849) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. at sun.reflect.GeneratedConstructorAccessor157.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:341) at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2251) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2104) ... 23 common frames omitted Caused by: java.net.SocketException: Too many open files at java.net.Socket.createImpl(Socket.java:460) at java.net.Socket.getImpl(Socket.java:520) at java.net.Socket.setTcpNoDelay(Socket.java:980) at com.mysql.jdbc.StandardSocketFactory.configureSocket(StandardSocketFactory.java:132) at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:203) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:300) ... 25 common frames omitted 2018-04-09 17:41:34,769 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 检查日志发现,在 Tomcat 彻底挂机之前,曾经有比较大量的数据源连接池出错,无法访问 Mysql, 但是非常奇怪的是,在87 这台机器上面,是可以使用 mysql 命令行连接到测试数据库的,说明 Mysql 的连接是没有问题。\n但是数据源连接就会出错!! 真的是很奇怪,为什么连接池会报错,有没有可能是这些异常导致 socket 泄漏呢?后来,在本地运行应用,有时候会发现IDE 的控制台报错:\n2018-04-11 09:43:48,363 - [ERROR] - from com.alibaba.druid.pool.DruidDataSource in poolTaskScheduler-11 discard connection com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet successfully received from the server was 100,610 milliseconds ago. The last packet sent successfully to the server was 0 milliseconds ago. at sun.reflect.GeneratedConstructorAccessor108.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3556) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3456) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3897) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2524) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2677) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2545) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2503) at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1369) at com.alibaba.druid.filter.FilterChainImpl.statement_executeQuery(FilterChainImpl.java:2363) at com.alibaba.druid.filter.FilterAdapter.statement_executeQuery(FilterAdapter.java:2481) at com.alibaba.druid.filter.FilterEventAdapter.statement_executeQuery(FilterEventAdapter.java:302) at com.alibaba.druid.filter.FilterChainImpl.statement_executeQuery(FilterChainImpl.java:2360) at com.alibaba.druid.proxy.jdbc.StatementProxyImpl.executeQuery(StatementProxyImpl.java:211) at com.alibaba.druid.pool.DruidPooledStatement.executeQuery(DruidPooledStatement.java:138) at com.taobao.tddl.atom.jdbc.TStatementWrapper.executeQuery(TStatementWrapper.java:315) at com.taobao.tddl.group.jdbc.TGroupStatement.executeQueryOnConnection(TGroupStatement.java:549) at com.taobao.tddl.group.jdbc.TGroupStatement$4.tryOnDataSource(TGroupStatement.java:633) at com.taobao.tddl.group.jdbc.TGroupStatement$4.tryOnDataSource(TGroupStatement.java:615) at com.taobao.tddl.group.dbselector.AbstractDBSelector.tryOnDataSourceHolder(AbstractDBSelector.java:155) at com.taobao.tddl.group.dbselector.OneDBSelector.tryExecuteInternal(OneDBSelector.java:52) at com.taobao.tddl.group.dbselector.AbstractDBSelector.tryExecute(AbstractDBSelector.java:405) at com.taobao.tddl.group.dbselector.AbstractDBSelector.tryExecute(AbstractDBSelector.java:412) at com.taobao.tddl.group.jdbc.TGroupStatement.executeQuery(TGroupStatement.java:488) at com.taobao.tddl.group.jdbc.TGroupStatement.executeInternal(TGroupStatement.java:131) at com.taobao.tddl.group.jdbc.TGroupStatement.execute(TGroupStatement.java:101) at com.taobao.tddl.repo.mysql.spi.My_JdbcHandler.executeQuery(My_JdbcHandler.java:521) at com.taobao.tddl.repo.mysql.spi.My_Cursor.init(My_Cursor.java:106) at com.taobao.tddl.repo.mysql.handler.QueryMyHandler.handle(QueryMyHandler.java:89) at com.taobao.tddl.executor.AbstractGroupExecutor.executeInner(AbstractGroupExecutor.java:47) at com.taobao.tddl.executor.AbstractGroupExecutor.execByExecPlanNode(AbstractGroupExecutor.java:36) at com.taobao.tddl.executor.TopologyExecutor.execByExecPlanNode(TopologyExecutor.java:66) at com.taobao.tddl.executor.MatrixExecutor.execByExecPlanNodeByOne(MatrixExecutor.java:670) at com.taobao.tddl.executor.MatrixExecutor.execByExecPlanNode(MatrixExecutor.java:659) at com.taobao.tddl.executor.MatrixExecutor.execute(MatrixExecutor.java:137) at com.taobao.tddl.matrix.jdbc.TConnection.executeSQL(TConnection.java:241) at com.taobao.tddl.matrix.jdbc.TPreparedStatement.executeSQL(TPreparedStatement.java:64) at com.taobao.tddl.matrix.jdbc.TStatement.executeInternal(TStatement.java:133) at com.taobao.tddl.matrix.jdbc.TPreparedStatement.execute(TPreparedStatement.java:49) at sun.reflect.GeneratedMethodAccessor148.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.ibatis.logging.jdbc.PreparedStatementLogger.invoke(PreparedStatementLogger.java:59) at com.sun.proxy.$Proxy102.execute(Unknown Source) at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:63) at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79) at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63) at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325) at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83) at sun.reflect.GeneratedMethodAccessor146.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49) at fastfish.interceptor.DbLogInterceptor.intercept(DbLogInterceptor.java:49) at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61) at com.sun.proxy.$Proxy100.query(Unknown Source) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141) at sun.reflect.GeneratedMethodAccessor145.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:434) at com.sun.proxy.$Proxy87.selectList(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:231) at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:128) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:68) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53) at com.sun.proxy.$Proxy124.selectAll(Unknown Source) at fastfish.services.BusinessService.getAll(BusinessService.java:73) at fastfish.services.BusinessService.loadDB(BusinessService.java:38) at sun.reflect.GeneratedMethodAccessor190.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.runAndReset$$$capture(FutureTask.java:308) at java.util.concurrent.FutureTask.runAndReset(FutureTask.java) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost. at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3008) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3466) ... 83 common frames omitted 是数据池连接出错。但是我本地的应用确实是可以访问测试数据库的, 比较有趣的异常就是\nCaused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost. 数据还没有读完, Connection 就丢了。为什么会 lost connection 呢,可能数据库出问题,也可能是网络出了问题。\n我还能从数据库读到数据,说明数据库没问题的,兼之这个异常只是偶尔出现,所以可能就是网络出问题了。\n如此说来,是否可能是因为测试环境网络不稳定,连接池无法和 Mysql 保持连接,在丢掉 Connection 之后,连接池重新发起连接,但是因为网络不稳定又丢掉了Connection, 不断循环这个过程,导致建立的 socket 连接越 来越多,但是建立的 socket 很快就被Close 掉了,内核又没有把这些 Close 掉的 socket 资源回收掉,因此打开的 socket 文件越来越多,最后导致 Tomcat 因为打开的文件过多无法建立新的 socket 连接。\n6 小心求证 如果连接池真的不断尝试连接Mysql 的话,必定会建立很多的连接,而Mysql 是会将这些记录保存下来的,检查Mysql 的变量:\n查看Mysql 的文档关于 Connection 和 Thread_connected 的说明:\nConnections\nThe number of connection attempts (successful or not) to the MySQL server.\nThreads_connected\nThe number of currently open connections.\n也就是说,当时共有20000 多的连接请求,但是真正被 Mysql accpet 并且服务的只有 28 个连接。看来的确是因为连接池的连接导致 socket 泄漏\n6.1 更新 和运维同学沟通之后,发现丢连接的原因不是网络不稳定,而是测试集群都是虚拟机,内存 用光,导致无法建立新的连接,内核释放一部分资源之后又可以建立连接了。内存用完,我能怎么办,我也很无奈。\n7 解决方法 虽说基本确定了 socket 泄漏的源头,但是对于内核为什么无法回收已经关闭 socket 的原因依然不明确。\n最令人百思不得其解的是,部署了应用的测试服务器有两台,另外一 台服务器也有同样的连接池问题,但是却没有出现 socket 泄漏问题, 出现泄漏的只有 87 这台机器。真的令人费解. 所以最后解决方法就是撤下 87 服务器的应用,换一台服务器来部署。\n新的服务器部署应用之后虽说也有同样的数据库连接池异常,但是却没有出现 socket 泄漏,初步定位是 87这台机器的内核环境存在问题。\n8 参考 tcp-socket文件句柄泄漏/ lsof-cant-identify-protocol/ ","permalink":"https://ramsayleung.github.io/zh/post/2018/lsof_cant_identify_protocol/","summary":"Socket 泄漏引起的Tomcat 宕机问题分析 在2018年4月9号下午,收到反馈:测试集群部分接口访问有问题,请求时而正常,时而超时。 最近的测试环境真","title":"lsof can't identify protocol"},{"content":"1 背景 在2018年4月4号早上,业务方反应Hbase 读超时,无法读取当前数据。然后发现测试环境的 Hbase region server 全部宕机,已经无可用Region Server. 因为公司的机器的Ip 和Host 不便在博文展示,所以我会用:\n192.168.2.1: node-master 192.168.2.2: node1 192.168.2.3: node2 来代替\n2 Region Server 宕机原因分析 经查看日志,发现三台部署了Hbase 的服务器,分别是node-master 192.168.2.1, node1 192.168.2.2,=node2 192.168.2.3=. node1 机器在2018-03-13 14:47:55 收到了Shutdown Message, 停了Region Server. node-master这台机器在2018-03-20 10:13:07收到了Shutdown Message, 停掉了Region Server.\n也就是说在3月下旬到昨天,Hbase 一直只有一台Region Server 在运行。而在昨天,2018-04-03 23:19:35, 剩下的最后一台机器也收到了Shutdown Message, 因此把剩下的最后一台Region Server 停掉,测试 环境的Hbase 全部下线。那么,为什么这三台服务器会收到Shutdown Message 呢?\n2.1 node1 先从 node1这台机器开始分析,关于 Region Server 退出的日志显示如下:\n2018-03-13 14:47:49,665 INFO [main-SendThread(node-master:2181)] zookeeper.ClientCnxn: Unable to reconnect to ZooKeeper service, session 0x161d6c1ae910001 has expired, closing socket connection 2018-03-13 14:47:49,706 FATAL [main-EventThread] regionserver.HRegionServer: ABORTING region server node1,60020,1519732610839: regionserver:60020-0x161d6c1ae910001, quorum=node-master:2181,node1:2181,node2:2181, baseZNode=/hbase regionserver:60020-0x161d6c1ae910001 received expired from ZooKeeper, aborting org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode = Session expired at org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher.connectionEvent(ZooKeeperWatcher.java:700) at org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher.process(ZooKeeperWatcher.java:611) at org.apache.zookeeper.ClientCnxn$EventThread.processEvent(ClientCnxn.java:522) at org.apache.zookeeper.ClientCnxn$EventThread.run(ClientCnxn.java:498) 2018-03-13 14:47:49,718 FATAL [main-EventThread] regionserver.HRegionServer: RegionServer abort: loaded coprocessors are: [org.apache.hadoop.hbase.coprocessor.MultiRowMutationEndpoint] 2018-03-13 14:47:50,705 WARN [DataStreamer for file /hbase-nemo/WALs/node1,60020,1519732610839/node1%2C60020%2C1519732610839.default.1520922158622 block BP-1296874721-192.168.2.1-1519712987003:blk_1073743994_3170] hdfs.DFSClient: DataStreamer Exception org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.LeaseExpiredException): No lease on /hbase-nemo/oldWALs/node1%2C60020%2C1519732610839.default.1520922158622 (inode 18837): File is not open for writing. [Lease. Holder: DFSClient_NONMAPREDUCE_551822027_1, pendingcreates: 1] at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.checkLease(FSNamesystem.java:3612) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.getAdditionalDatanode(FSNamesystem.java:3516) at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.getAdditionalDatanode(NameNodeRpcServer.java:711) at org.apache.hadoop.hdfs.server.namenode.AuthorizationProviderProxyClientProtocol.getAdditionalDatanode(AuthorizationProviderProxyClientProtocol.java:229) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.getAdditionalDatanode(ClientNamenodeProtocolServerSideTranslatorPB.java:508) at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java) at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:617) at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1073) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2086) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2082) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.Subject.doAs(Subject.java:422) at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1693) at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2080) at org.apache.hadoop.ipc.Client.call(Client.java:1471) at org.apache.hadoop.ipc.Client.call(Client.java:1408) at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:230) at com.sun.proxy.$Proxy16.getAdditionalDatanode(Unknown Source) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.getAdditionalDatanode(ClientNamenodeProtocolTranslatorPB.java:429) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:256) at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:104) at com.sun.proxy.$Proxy17.getAdditionalDatanode(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.hbase.fs.HFileSystem$1.invoke(HFileSystem.java:279) at com.sun.proxy.$Proxy18.getAdditionalDatanode(Unknown Source) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1228) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-13 14:47:53,803 FATAL [regionserver/node1/192.168.2.2:60020] regionserver.HRegionServer: ABORTING region server node1,60020,1519732610839: org.apache.hadoop.hbase.YouAreDeadException: Server REPORT rejected; currently processing node1,60020,1519732610839 as dead server 从 14:47:49 开始, Hbase 没法和 Zookeeper 通信,连接时间超时。翻查 Zookeeper 的日志,发现Zookeeper 的日志有如下内容:\n2018-03-13 14:47:46,926 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910002 for client /192.168.2.2:51611, probably expired 2018-03-13 14:47:49,612 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910001 for client /192.168.2.2:51612, probably expired 说明Hbase 和 ZooKeeper 的通信的确去了问题。连接出问题以后,集群就会认为 这个 Hbase 的节点出了故障,宕机,然后就把这个节点当作 DeadNode, 这个节点的 RegionServer 就下线了。\n2.2 node-master 现在再来看看node-master这台机器的日志\n2018-03-20 10:12:19,986 INFO [main-SendThread(node-master:2181)] zookeeper.ClientCnxn: Unable to read additional data from server sessionid 0x361d65049260001, likely server has closed socket, closing socket connection and attempting reconnect 2018-03-20 10:12:20,841 INFO [main-SendThread(node1:2181)] zookeeper.ClientCnxn: Opening socket connection to server node1/192.168.2.2:2181. Will not attempt to authenticate using SASL (unknown error) 2018-03-20 10:12:43,747 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node1:2181)] zookeeper.ClientCnxn: Client session timed out, have not heard from server in 60019ms for sessionid 0x161d65049590000, closing socket connection and attempting reconnect 2018-03-20 10:12:44,574 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node-master:2181)] zookeeper.ClientCnxn: Opening socket connection to server node-master/192.168.2.1:2181. Will not attempt to authenticate using SASL (unknown error) 2018-03-20 10:12:44,575 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node-master:2181)] zookeeper.ClientCnxn: Socket connection established, initiating session, client: /192.168.2.1:58042, server: node-master/192.168.2.1:2181 2018-03-20 10:12:44,577 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node-master:2181)] zookeeper.ClientCnxn: Session establishment complete on server node-master/192.168.2.1:2181, sessionid = 0x161d65049590000, negotiated timeout = 90000 2018-03-20 10:12:49,625 INFO [main-SendThread(node1:2181)] zookeeper.ClientCnxn: Socket connection established, initiating session, client: /192.168.2.1:46815, server: node1/192.168.2.2:2181 2018-03-20 10:12:53,258 WARN [ResponseProcessor for block BP-1296874721-192.168.2.1-1519712987003: blk_1073747108_6286] hdfs.DFSClient: Slow ReadProcessor read fields took 70070ms (threshold=30000ms); ack: seqno: -2 reply: 0 reply: 1 downstreamAckTimeNanos: 0, targets: [DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]] 2018-03-20 10:12:53,259 WARN [ResponseProcessor for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286] hdfs.DFSClient: DFSOutputStream ResponseProcessor exception for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286 java.io.IOException: Bad response ERROR for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286 from datanode DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK] at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer$ResponseProcessor.run(DFSOutputStream.java:1002) 2018-03-20 10:12:53,259 WARN [DataStreamer for file /hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323 block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286] hdfs.DFSClient: Error Recovery for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286 in pipeline DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]: bad datanode DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK] 2018-03-20 10:12:53,264 WARN [DataStreamer for file /hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323 block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286] hdfs.DFSClient: DataStreamer Exception java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:12:53,265 WARN [sync.4] hdfs.DFSClient: Error while syncing java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:12:53,266 ERROR [sync.4] wal.FSHLog: Error syncing, request close of WAL java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:12:53,266 INFO [sync.4] wal.FSHLog: Slow sync cost: 474 ms, current pipeline: [DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]] 2018-03-20 10:13:05,816 INFO [regionserver/node-master/192.168.2.1:60020.logRoller] wal.FSHLog: Slow sync cost: 12546 ms, current pipeline: [DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]] 2018-03-20 10:13:05,817 ERROR [sync.0] wal.FSHLog: Error syncing, request close of WAL java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:13:05,817 ERROR [regionserver/node-master/192.168.2.1:60020.logRoller] wal.FSHLog: Failed close of WAL writer hdfs://node-master:19000/hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323, unflushedEntries=1 org.apache.hadoop.hbase.regionserver.wal.FailedSyncBeforeLogCloseException: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hbase.regionserver.wal.FSHLog$SafePointZigZagLatch.waitSafePoint(FSHLog.java:1615) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.replaceWriter(FSHLog.java:833) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:699) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:13:05,818 FATAL [regionserver/node-master/192.168.2.1:60020.logRoller] regionserver.HRegionServer: ABORTING region server node-master,60020,1519720160721: Failed log close in log roller org.apache.hadoop.hbase.regionserver.wal.FailedLogCloseException: hdfs://node-master:19000/hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323, unflushedEntries=1 at org.apache.hadoop.hbase.regionserver.wal.FSHLog.replaceWriter(FSHLog.java:882) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:699) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: org.apache.hadoop.hbase.regionserver.wal.FailedSyncBeforeLogCloseException: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hbase.regionserver.wal.FSHLog$SafePointZigZagLatch.waitSafePoint(FSHLog.java:1615) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.replaceWriter(FSHLog.java:833) ... 3 more Caused by: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:13:05,818 FATAL [regionserver/node-master/192.168.2.1:60020.logRoller] regionserver.HRegionServer: RegionServer abort: loaded coprocessors are: [] 2018-03-20 10:13:05,997 INFO [regionserver/node-master/192.168.2.1:60020.logRoller] regionserver.HRegionServer: Dump of metrics as JSON on abort: 从上面的日志可以看到 node-master与node1机器通信,获取node1 的响应失败,认为node1 是 bad DataNode,接着集群想要把出现问题的DataNode 下掉,却发现没有多余DataNode 来替换, 紧接着在Syncing 时出错,关闭 WAL 失败, 最后就停掉了Region Server. 比较关键的时机如下:\n2018-03-20 10:12:53,265 WARN [sync.4] hdfs.DFSClient: Error while syncing 2018-03-20 10:12:53,266 ERROR [sync.4] wal.FSHLog: Error syncing, request close of WAL 2018-03-20 10:13:05,817 ERROR [sync.0] wal.FSHLog: Error syncing, request close of WAL 2018-03-20 10:13:06,397 ERROR [regionserver/node-master/192.168.2.1:60020] regionserver.HRegionServer: Shutdown / close of WAL failed: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. 期间HDFS 同步出错,尝试关闭WAL, 失败。失败的原因是无法用健康的节点替换出了问题的节点, 应该是健康的节点数太少了。最后在多次尝试关闭WAL都因为IOException 失败之后, RegionServer 下线。只是为什么尝试关闭WAL 失败需要关闭Region Server 依然存疑。\n2.3 node2 node2 是Hbase 集群最后一台机器,当node2 倒下了,Hbase 就真的完全宕机了。\n2018-04-03 23:19:33,472 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.LogRoller: Aborting java.io.IOException: cannot get log writer at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:365) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.createWriterInstance(FSHLog.java:724) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:689) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.SafeModeException): Cannot create file/hbase-nemo/WALs/node2,60020,1519732668326/node2%2C60020%2C1519732668326.default.1522768773233. Name node is in safe mode. Resources are low on NN. Please add or free up more resources then turn off safe mode manually. NOTE: If you turn off safe mode before adding resources, the NN will immediately return to safe mode. Use \u0026#34;hdfs dfsadmin -safemode leave\u0026#34; to turn safe mode off. at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.checkNameNodeSafeMode(FSNamesystem.java:1418) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFileInt(FSNamesystem.java:2674) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFile(FSNamesystem.java:2561) at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.create(NameNodeRpcServer.java:593) at org.apache.hadoop.hdfs.server.namenode.AuthorizationProviderProxyClientProtocol.create(AuthorizationProviderProxyClientProtocol.java:111) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.create(ClientNamenodeProtocolServerSideTranslatorPB.java:393) at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java) at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:617) at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1073) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2086) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2082) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.Subject.doAs(Subject.java:422) at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1693) at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2080) at org.apache.hadoop.ipc.Client.call(Client.java:1471) at org.apache.hadoop.ipc.Client.call(Client.java:1408) at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:230) at com.sun.proxy.$Proxy16.create(Unknown Source) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.create(ClientNamenodeProtocolTranslatorPB.java:296) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:256) at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:104) at com.sun.proxy.$Proxy17.create(Unknown Source) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.hbase.fs.HFileSystem$1.invoke(HFileSystem.java:279) at com.sun.proxy.$Proxy18.create(Unknown Source) at org.apache.hadoop.hdfs.DFSOutputStream.newStreamForCreate(DFSOutputStream.java:1897) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1738) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1698) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:450) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystemLinkResolver.resolve(FileSystemLinkResolver.java:81) at org.apache.hadoop.hdfs.DistributedFileSystem.createNonRecursive(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1124) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1100) at org.apache.hadoop.hbase.regionserver.wal.ProtobufLogWriter.init(ProtobufLogWriter.java:90) at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:361) ... 4 more 2018-04-03 23:19:33,501 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.HRegionServer: ABORTING region server node2,60020,1519732668326: IOE in log roller java.io.IOException: cannot get log writer at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:365) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.createWriterInstance(FSHLog.java:724) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:689) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.SafeModeException): Cannot create file/hbase-nemo/WALs/node2,60020,1519732668326/node2%2C60020%2C1519732668326.default.1522768773233. Name node is in safe mode. Resources are low on NN. Please add or free up more resources then turn off safe mode manually. NOTE: If you turn off safe mode before adding resources, the NN will immediately return to safe mode. Use \u0026#34;hdfs dfsadmin -safemode leave\u0026#34; to turn safe mode off. at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.checkNameNodeSafeMode(FSNamesystem.java:1418) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFileInt(FSNamesystem.java:2674) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFile(FSNamesystem.java:2561) at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.create(NameNodeRpcServer.java:593) at org.apache.hadoop.hdfs.server.namenode.AuthorizationProviderProxyClientProtocol.create(AuthorizationProviderProxyClientProtocol.java:111) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.create(ClientNamenodeProtocolServerSideTranslatorPB.java:393) at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java) at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:617) at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1073) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2086) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2082) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.Subject.doAs(Subject.java:422) at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1693) at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2080) at org.apache.hadoop.ipc.Client.call(Client.java:1471) at org.apache.hadoop.ipc.Client.call(Client.java:1408) at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:230) at com.sun.proxy.$Proxy16.create(Unknown Source) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.create(ClientNamenodeProtocolTranslatorPB.java:296) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:256) at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:104) at com.sun.proxy.$Proxy17.create(Unknown Source) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.hbase.fs.HFileSystem$1.invoke(HFileSystem.java:279) at com.sun.proxy.$Proxy18.create(Unknown Source) at org.apache.hadoop.hdfs.DFSOutputStream.newStreamForCreate(DFSOutputStream.java:1897) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1738) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1698) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:450) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystemLinkResolver.resolve(FileSystemLinkResolver.java:81) at org.apache.hadoop.hdfs.DistributedFileSystem.createNonRecursive(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1124) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1100) at org.apache.hadoop.hbase.regionserver.wal.ProtobufLogWriter.init(ProtobufLogWriter.java:90) at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:361) ... 4 more 可以看到上面的日志出现IO 出现异常,无法获取 log writer:\n2018-04-03 23:19:33,472 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.LogRoller: Aborting java.io.IOException: cannot get log writer 2018-04-03 23:19:33,501 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.HRegionServer: ABORTING region server node2,60020,1519732668326: IOE in log roller java.io.IOException: cannot get log writer 而无法获取 log writer, 对日志进行写入的原因是:\nCaused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.SafeModeException): Cannot create file/hbase-nemo/WALs/node2,60020,1519732668326/node2%2C60020%2C1519732668326.default.1522768773233. Name node is in safe mode. Resources are low on NN. Please add or free up more resources then turn off safe mode manually. NOTE: If you turn off safe mode before adding resources, the NN will immediately return to safe mode. Use \u0026#34;hdfs dfsadmin -safemode leave\u0026#34; to turn safe mode off. NameNode 进入了safe-mode, 关于safe-mode 的描述: \u0026gt;During start up the NameNode loads the file system state from the fsimage and the edits log file. It then waits for DataNodes to report their blocks so that it does not prematurely start replicating the blocks though enough replicas already exist in the cluster. During this time NameNode stays in Safemode. Safemode for the NameNode is essentially a read-only mode for the HDFS cluster, where it does not allow any modifications to file system or blocks. Normally the NameNode leaves Safemode automatically after the DataNodes have reported that most file system blocks are available. If required, HDFS could be placed in Safemode explicitly using bin/hadoop dfsadmin -safemode command. NameNode front page shows whether Safemode is on or off. A more detailed description and configuration is maintained as JavaDoc for setSafeMode().\nNameNode 进入safe-mode 的原因是因为 node-master这台Master 机器的磁盘被应用日志打满了,导 致 NameNode 进入了只读的 safe-mode. 因为NameNode 进入readonly 的safe-mode 就无 法写入日志, 所以 Hbase 在出现异常之后,就开始把Hbase 的信息 dump 出来,并关闭 Region Server, 导致整个Hbase 集群宕机。\n对于node2 Region Server 下线的原因,猜测是 NameNode 服务器的磁盘用完,导致NameNode 进入read-only 的safe-mode, 又因为Hbase 存储的核心之一是WAL(write-ahead-log, 预写日志),较长时间无法写入日志,最终导致 Region Server 下线。\n3 分析小结 经过这样的一翻排查,可以得出结论,最开始 node1 因为Hbase 和 ZooKeeper 的通信出现问题, 被认为是问题节点,下线了Region Server;\n一个星期之后,node-master这台机器在同步的时候 出现问题,想要关闭WAL, 但是却因为没有充足的健康节点来替换出现问题的node1, 导致关闭 WAL 失败,也下线了Region Server. node2 这台机器因为作为 NameNode 的node-master服务器的磁盘用 完,导致NameNode 进入read-only 的safe-mode, 又因为Hbase 存储的核心之一是 WAL(write-ahead-log, 预写日志),较长时间无法写入日志,最终导致 Region Server 下线。\n4 其他 还有一个关键点是为什么Hbase 和Zookeeper 的连接超时,Zookeeper 的日志只是简单地说明:\n2018-03-13 14:47:46,926 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910002 for client /192.168.2.2:51611, probably expired 2018-03-13 14:47:49,612 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910001 for client /192.168.2.2:51612, probably expired 为什么 session 会无效,日志并没有给出说明,个人猜测可能是因为在部署了 Hbase/Zookeeper 的服务器上还部署了应用。\n应用或者是Hbase 导致的长GC 导致ZooKeeper 停顿,并且导致session 超时无效。\n5 结语 和同事交流之后,觉得以上的分析只是基于日志的猜测,可能Hbase 宕机的原因正如我所说, 或者另有原因,所以现在最关键的措施是加上对Hbase 的各种监控。\n在Hbase 宕机的时候, 参考日志和详细的监控,比如连接数,CPU 使用率,内存,集群负载情况,每个节点情况。不然再遇到一次宕机,还是只能看日志,猜原因。\n话分两头,现在的分析主要是基于Hbase 和ZooKeeper 的日志进行分析,简而言之就是捞日 志,查看信息; 捞日志,查看信息;通过工具找出日志中隐藏的关键时机,然后对时机前后发生的事情进行分析,这也是一个有趣的过程。\n只是从1G 多的日志里面找出想要的内容,也不是一个容易的过程。\n","permalink":"https://ramsayleung.github.io/zh/post/2018/hbase_crash/","summary":"1 背景 在2018年4月4号早上,业务方反应Hbase 读超时,无法读取当前数据。然后发现测试环境的 Hbase region server 全部宕机,已经无可用Region Server. 因为","title":"记一次Hbase 宕机原因分析"},{"content":"从Mysql, Hbase 迁移数据\n1 Mysql 数据迁移 搭建完之后就是数据迁移了,mysql 的数据迁移比较简单。在旧的机器用 mysqldump 把所 有的数据导出来,然后传到新的环境然后导出:\n旧环境导出: mysqldump -u root -p --all-databases \u0026gt; all_dbs.sql 新环境导入: mysql -u root -p \u0026lt; all_dbs.sql Mysql 集群在数据迁移的时候是在提供服务的,所以自然会有新数据写入,但因为是测试环 境,所以在迁移过程中可以忽略新数据写入的影响。不然这又会是一个大问题~\n2 Hbase 数据迁移 2.1 Hbase 集群复制(cluster replication) 2.1.1 配置旧集群和新集群 在新的集群,创建和旧集群一样的表结构(table schema)和列族(column family),这样新的集群就知道在接收到旧集群数据时候怎么去保存。下面是具体的步骤:\n通过以下命令启动 Hbase Shell: hbase shell 通过下面的命令获取已有的表的元数据: hbase\u0026gt; describe \u0026#34;content\u0026#34;; 输入结果如下:\nTable content is ENABLED content, {TABLE_ATTRIBUTES =\u0026gt; {coprocessor$1 =\u0026gt; \u0026#39;|org.apache.phoenix.coprocessor.ScanRegionObserver|805306366|\u0026#39;, co processor$2 =\u0026gt; \u0026#39;|org.apache.phoenix.coprocessor.UngroupedAggregateRegionObserver|805306366|\u0026#39;, coprocessor$3 =\u0026gt; \u0026#39;|or g.apache.phoenix.coprocessor.GroupedAggregateRegionObserver|805306366|\u0026#39;, coprocessor$4 =\u0026gt; \u0026#39;|org.apache.phoenix.copr ocessor.ServerCachingEndpointImpl|805306366|\u0026#39;} COLUMN FAMILIES DESCRIPTION {NAME =\u0026gt; \u0026#39;baseinfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536\u0026#39; , IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} {NAME =\u0026gt; \u0026#39;extrainfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536 \u0026#39;, IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} 2 row(s) in 0.3060 seconds 复制以下内容到编辑器,并按要求进行修改: {NAME =\u0026gt; \u0026#39;baseinfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536\u0026#39; , IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} {NAME =\u0026gt; \u0026#39;extrainfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536 \u0026#39;, IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} 将TTL =\u0026gt; 'FOREVER' with TTL 修改成 org.apache.hadoop.hbase.HConstants::FOREVER\n在列族的描述之间加上逗号,用来在新建的时候分隔列族\n去掉文本中的换汉符(\\n, \\r), 这样文本就会变成单行文本\n通过下面的语句在新的集群创建新的相同的表:\n2.1.2 CopyTable 按照Apache 官方文档的介绍,Hbase 支持两种形式的数据备份,分别是停服和不停服的。\n我选择的是不停服的形式,停服的代价太大。而不停服的数据备份有三种方案,我选择是的 CopyTable方案。\nAdd Peer\n因为 CopyTable 需要源机房和目标机房是网络连通,并且是目标集群在源集群的 peer list里面。所以要先在源集群添加 peer. 按照 Hbase cluster replication 关于添加 peer 的说明:\nadd_peer \u0026lt;ID\u0026gt; \u0026lt;CLUSTER_KEY\u0026gt; Adds a replication relationship between two clusters. + ID — a unique string, which must not contain a hyphen. + CLUSTER_KEY: composed using the following template, with appropriate place-holders: `hbase.zookeeper.quorum:hbase.zookeeper.property.clientPort:zookeeper.znode.parent` + STATE(optional): ENABLED or DISABLED, default value is ENABLED 而 hbase.zookeeper.quorum 可以在目标集群的 $HBASE_HOME/conf/hbase-site.xml 目录找到设置的值; hbase.zookeeper.property.clientPort 可以在 $HBASE_HOME/conf/hbase-site.xml指定或者是在 $ZOOKEEPER_HOME/conf/zoo.cfg通 过 clientPort 指定; zookeeper.znode.parent 默认值是 /hbase\n所以,在 Hbase Shell 运行下面的命令来添加 peer\nadd_peer \u0026#39;1\u0026#39;, \u0026#34;node-master,node1,node2:2181:/hbase\u0026#34; 2.1.3 复制表和数据 CopyTable 的命令说明如下:\n./bin/hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help /bin/hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help Usage: CopyTable [general options] [--starttime=X] [--endtime=Y] [--new.name=NEW] [--peer.adr=ADR] \u0026lt;tablename\u0026gt; 可以指定需要复制的数据的时间间隔,也可以不指定。那么默认是全部数据,以 cset_content 表为例,复制一个小时的数据:\nbin/hbase org.apache.hadoop.hbase.mapreduce.CopyTable --starttime=1265875194289 --endtime=1265878794289 --peer.adr=node-master,node1,node2:2181:/hbase --families=baseinfo,extrainfo cset_content 然后Hadoop 就会启动一个 MapReduce 的Job来运行这个 CopyTable任务。而我要复制所 有的数据,就需要把列族和表都列出来\n3 结语 折腾一波之后,终于把环境弄好。如果目标机房和源机房不同的话,也可以尝试使用 Hbase 的 Exporter 和 Importer\n","permalink":"https://ramsayleung.github.io/zh/post/2018/store_cluster_migrate2/","summary":"从Mysql, Hbase 迁移数据 1 Mysql 数据迁移 搭建完之后就是数据迁移了,mysql 的数据迁移比较简单。在旧的机器用 mysqldump 把所 有的数据导出来,然后传到新的环","title":"记存储集群的一次迁移过程(下)"},{"content":"搭建和配置 Hadoop, Zookeeper, Hbase\n1 前言 最近负责公司测试环境的迁移,主要包括 Hbase+Mysql 存储集群的迁移,消息队列,缓存组件的迁移, 而我打算说说存储集群的迁移。因为公司的机器的Ip 和Host 不便在博文展示,所以我会用:\n192.168.2.1: node-master 192.168.2.2: node1 192.168.2.3: node2 来代替公司的机器和域名。\n2 搭建新环境 2.1 Hadoop 搭建流程 2.1.1 Hadoop 集群的架构 在配置 Hadoop 的主从节点(master/slave)之前,先来了解一下 Hadoop 集群的组件作用;\nmaster 节点负责担任管理分布式文件系统以及进行相应的资源调度的角色: NameNode: 管理分布式文件系统并且感知数据块在集群的存储位置 ResourceManager: 管理 YARN 任务,并且负责在 slave 节点调度和处理 slave 节点负责担任存储真实的数据并且提供运算 YARN 任务的能力的角色: DataNode: 负责物理存储真实的数据 NodeManager: 管理在该节点 YARN 任务的具体执行。 master 和 slave 的角色不一定像上面划分得泾渭分明,比如 master 节点也可以是 dataNode,这个就看具体配置了。\n2.1.2 配置JDK Hadoop 集群需要JAVA 环境,而Linux 的发行版本一般都是默认带有JDK 的,只是OpenJDK 而不是 Oracle JDK, 如果需要修改JDK 的版本,可以自行修改,网上已经有很多安装JDK 的教程,我就不一一讲解了。\n2.1.3 修改host 因为需要不同的机器之间通信,所以需要先配置好Ip 和域名的映射。修改每台机器的 /etc/hosts 文件,加上以下内容:\n192.168.2.1: nodw-master 192.168.2.2: node1 192.168.2.3: node2 2.1.4 新建 hadoop 用户 虽说我可以用我自己的登录名来配置和运行 hadoop, 但是出于安全的考虑,还是在每个节点创建一个专门用来运行 hadoop 集群的用户比较好。\nuseradd hadoop passwd hadoop 2.1.5 SSH 免密码登录 因为在 Hadoop 集群中, node-master 节点会通过SSH连接和其他节点进行通信,所以需要为 Hadoop 集群配置免密码校验的通信。首先以 hadoop 用户身份登录到 node-master 节点,然后生成 SSH的公私钥:\nssh-keygen -b 4096 然后把公钥复制到其他的节点,如果你想要把 node-master也当作dataNote的话,就需要把公钥也复制到 master节点:\nssh-copy-id -i $HOME/.ssh/id_rsa.pub hadoop@node-master ssh-copy-id -i $HOME/.ssh/id_rsa.pub hadoop@node1 ssh-copy-id -i $HOME/.ssh/id_rsa.pub hadoop@node2 谨记:复制的是”公钥”,不是”私钥”.\n2.1.6 安装hadoop 1. 下载hadoop 安装包,以 hadoop登录node-master,采用 wget命令下载: wget http://archive.cloudera.com/cdh5/cdh/5/hadoop-2.6.0-cdh5.7.1.tar.gz\n创建一个hadoop目录,将各个组件都安装在这个目录。 mdkir ~/hadoop tar -zxvf hadoop-2.6.0-cdh5.7.1.tar.gz -C ~/hadoop 2.1.7 修改配置文件 所有修改的 hadoop配置文件都位于 ~/hadoop/etc/hadoop/ 目录\ncd ~/hadoop/etc/hadoop 设置 NameNode 位置\nvim core-site.xml: 修改成以下内容:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.defaultFS\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://node-master:19000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.trash.interval\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;10080\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.trash.checkpoint.interval\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;10080\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 设置 HDFS 路径\nvim hdfs-site.xml,修改内容为:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.replication\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;1\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/home/hadoop/hadoop/data/temp\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master:50070\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.secondary.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node1:50090\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.webhdfs.enabled\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.data.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/home/hadoop/hadoop/data/hdfs\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 将 YARN 设置成任务调度器(Job Scheduler)\ncp mapred-site.xml.template mapred-site.xml 然后修改配置,将 yarn 设置成 MapReduce 操作的默认框架: vim mapred-site.xml:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.framework.name\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;yarn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master:10020\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.webapp.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master:19888\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 配置 YARN\nvim yarn-site.xml:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.acl.enable\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;0\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.hostname\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.aux-services\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;mapreduce_shuffle\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 配置 master 节点\n因为公司的机器内存较大,兼之机器数不多,所以我就把两台机器都都当作 master: vim masters修改文件为:\nnode-master node1 配置 slave 节点\n把全部节点都当作数据几点(dataNode): vim slaves:\nnode-master node1 node2 修改 Hadoop 环境变量配置\n这个配置是否修改就视情况而定了。 vim hadoop-env.sh:\n#因为ssh的端口不是默认的22,需要重新指定 export HADOOP_SSH_OPTS=\u0026#34;-p 9922\u0026#34; #如果报错java_home找不到,可在这里重新指定 #export JAVA_HOME=/usr/java/jdk1.8.0_161/ 在其它的节点安装并且解压,不要修改配置文件 将配置文件同步到slave主机\nscp -r -P 9922 /home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc/hadoop/ node1:/home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc scp -r -P 9922 /home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc/hadoop/ node2:/home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc 修改环境变量(所有节点都要配置) 编辑 `.bash_profile`: `vim ~/.bash_profile` 加入: ```shell export JAVA_HOME=/usr/java/jdk1.8.0_161/ export JRE_HOME=/usr/java/jdk1.8.0_161/jre export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH export HADOOP_HOME=/home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1 export HBASE_HOME=/home/hadoop/hadoop/hbase-1.2.0-cdh5.7.1 export HADOOP_MAPRED_HOME=${HADOOP_HOME} export HADOOP_COMMON_HOME=${HADOOP_HOME} export HADOOP_HDFS_HOME=${HADOOP_HOME} export YARN_HOME=${HADOOP_HOME} export HADOOP_YARN_HOME=${HADOOP_HOME} export HADOOP_CONF_DIR=${HADOOP_HOME}/etc/hadoop export HDFS_CONF_DIR=${HADOOP_HOME}/etc/hadoop export YARN_CONF_DIR=${HADOOP_HOME}/etc/hadoop PATH=$PATH:$HOME/bin:$JAVA_HOME/bin:$HADOOP_HOME/sbin:$HBASE_HOME/bin:$HADOOP_HOME/bin export PATH ``` 然后加载 `~/.bash_profile`: `source ~/.bash_profile` 格式化 HDFS 就像其它的文件系统那样,在使用之前需要格式化,HDFS 这个分布式文件系统也不例外。在 `node-master`,运行: ```nil hdfs namenode -format ``` 那么,到目前为止, Hadoop 就已经安装和配置好了。 2.1.8 运行和监控 HDFS 启动HDFS\n在 node-master 的 /home/hadoop/hadoop/sbin/ 目录运行下面的命令以启动 HDFS:\n./start-dfs.sh 然后就会启动 NameNode 和 SecondaryNameNode, 然后继续启动 DataNode.\n验证HDFS\n可以在各个节点通过 jps 检查HDFS 的运行状态。比如在 node-master 运行 jps:\n12243 NameNode 2677 ResourceManager 19593 Jps 15036 DataNode node1:\n30464 DataNode 13094 Jps 28589 SecondaryNameNode 停止HDFS\n在 node-master 的 /home/hadoop/hadoop/sbin/ 目录运行下面的命令以停止HDFS:\n./stop-dfs.sh 监控HDFS\n如果你在启动 HDFS 之后,想要获取关于 HDFS 的详细信息,你可以使用 hdfs dfsadmin -report 命令, 例如在 node-master 运行 hdfs dfsadmin -resport, 输出如下:\nConfigured Capacity: 1201169780736 (1.09 TB) Present Capacity: 1129442681745 (1.03 TB) DFS Remaining: 1129442358161 (1.03 TB) DFS Used: 323584 (316 KB) DFS Used%: 0.00% Under replicated blocks: 0 Blocks with corrupt replicas: 0 Missing blocks: 0 Missing blocks (with replication factor 1): 0 ------------------------------------------------- Live datanodes (3): Name: 192.168.2.3:50010 (node2) Hostname: node2 Decommission Status : Normal Configured Capacity: 400389926912 (372.89 GB) DFS Used: 102400 (100 KB) Non DFS Used: 24759164513 (23.06 GB) DFS Remaining: 375630659999 (349.83 GB) DFS Used%: 0.00% DFS Remaining%: 93.82% Configured Cache Capacity: 0 (0 B) Cache Used: 0 (0 B) Cache Remaining: 0 (0 B) Cache Used%: 100.00% Cache Remaining%: 0.00% Xceivers: 11 Last contact: Mon Mar 05 14:05:38 CST 2018 Name: 192.168.2.2:50010 (node1) Hostname: node1 Decommission Status : Normal Configured Capacity: 400389926912 (372.89 GB) DFS Used: 77824 (76 KB) Non DFS Used: 23483977479 (21.87 GB) DFS Remaining: 376905871609 (351.02 GB) DFS Used%: 0.00% DFS Remaining%: 94.13% Configured Cache Capacity: 0 (0 B) Cache Used: 0 (0 B) Cache Remaining: 0 (0 B) Cache Used%: 100.00% Cache Remaining%: 0.00% Xceivers: 7 Last contact: Mon Mar 05 14:05:38 CST 2018 Name: 192.168.2.1:50010 (node-master) Hostname: node-master Decommission Status : Normal Configured Capacity: 400389926912 (372.89 GB) DFS Used: 143360 (140 KB) Non DFS Used: 23483956999 (21.87 GB) DFS Remaining: 376905826553 (351.02 GB) DFS Used%: 0.00% DFS Remaining%: 94.13% Configured Cache Capacity: 0 (0 B) Cache Used: 0 (0 B) Cache Remaining: 0 (0 B) Cache Used%: 100.00% Cache Remaining%: 0.00% Xceivers: 7 Last contact: Mon Mar 05 14:05:39 CST 2018 或者可以使用更加友好的 Web 管理界面,在浏览器输入: http://node-master-ip:50070, 然后你就可以看到如下的监控界面: 使用 HDFS\n既然 HDFS 可以跑起来了,现在就需要添加一点数据以测试 HDFS 了。 在HDFS 的根目录下新建一个 test 目录:\nhdfs dfs -mkdir /test 然后在本地创建一个 helloworld 文件,内容如下:\nBye world! 接着把 helloworld 文件放置到HDFS的 /test 目录下:\nhdfs dfs -put helloworld /test 最后查看文件是否存在:\nhdfs dfs -ls /test 2.2 小结 至此,如果一切顺利的话, 那么Hadoop 集群就运行起来了。因为我是需要用Hbase 作存储集群,暂不需用Yarn 作计算,所以我就没有介绍启动 Yarn 的 流程了。\n2.3 ZooKeeper 搭建流程 因为需要用 ZooKeeper 来管理集群,所以也需要安装 ZooKeeper. 而 ZooKeeper 的安装和 配置也是用 hadoop 用户进行操作的。\n2.3.1 安装zookeeper (每个节点同样的操作) 1. 下载zookeeper安装包,登录主机,采用wget命令下载: wget http://archive.cloudera.com/cdh5/cdh/5/zookeeper-3.4.5-cdh5.7.1.tar.gz\n解压安装到hadoop目录,将各个组件都安装在这个目录。 tar -zxvf zookeeper-3.4.5-cdh5.7.1.tar.gz -C ~/hadoop 2.3.2 配置 ZooKeeper 修改zoo.cfg (所有机器一样的配置): vim /home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1/conf/zoo.cfg, 配置文件内容如下:\n# The number of milliseconds of each tick tickTime=2000 maxSessionTimeout=300000 # The number of ticks that the initial # synchronization phase can take initLimit=10 # The number of ticks that can pass between # sending a request and getting an acknowledgement syncLimit=5 # the directory where the snapshot is stored. # do not use /tmp for storage, /tmp here is just # example sakes. dataDir=/home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1/data # the port at which the clients will connect clientPort=2181 # # Be sure to read the maintenance section of the # administrator guide before turning on autopurge. # # http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance # # The number of snapshots to retain in dataDir #autopurge.snapRetainCount=3 # Purge task interval in hours # Set to \u0026#34;0\u0026#34; to disable auto purge feature #autopurge.purgeInterval=1 server.1=node-master:2888:3888 server.2=node1:2888:3888 server.3=node2:2888:3888 创建myid文件 (不同主机不同数字)\ncd {data_dir} # 按照上面的配置,就应该是/home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1/data vim myid myid的数字要与zoo.cfg配置的一一对应。即要对应:\nserver.1=node-master:2888:3888 server.2=node1:2888:3888 server.3=node2:2888:3888 也就是 node-master 的 myid是1, node1的 myid是 2,依次类推。需要注意的 是,数字前后都不能有空格!\n启动 ZooKeeper\n在 node-master 的 /home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1 目录,运行以 下命令:\nsh bin/zkServer.sh start 其它相应的命令如下:\n启动ZK服务: sh bin/zkServer.sh start 查看ZK服务状态: sh bin/zkServer.sh status 停止ZK服务: sh bin/zkServer.sh stop 重启ZK服务: sh bin/zkServer.sh restart 验证 ZooKeeper\n可以通过调用 jps 或者 bin/zkCli.sh 来验证 Zookeeper 的运行情况:\n./zkCli.sh -server 192.168.2.1:2181 2.4 HBase 搭建流程 2.4.1 安装Hbase 下载hbase 安装包,登录主机,采用wget命令下载: wget http://archive.cloudera.com/cdh5/cdh/5/hbase-1.2.0-cdh5.7.1.tar.gz 2、解压安装到hadoop目录(3台主机同样操作) tar -zxvf hbase-1.2.0-cdh5.7.1.tar.gz -C ~/hadoop 2.4.2 修改hbase的配置文件(所有主机一样的配置) 修改 hbase-1.2.0-cdh5.7.1/conf 目录下的文件: 1. 修改 hbase-site.xml 内容如下:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.rootdir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://node-master:19000/hbase-${user.name}\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.master\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.cluster.distributed\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/home/hadoop/hadoop/data/hbase-${user.name}\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.zookeeper.quorum\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master, node1, node2\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 修改 hbase-env.sh: export JAVA_HOME=/usr/java/jdk1.8.0_161/ (无法识别系统的环境变量,在这里直接指定) export HBASE_MANAGES_ZK=false (关闭自带的zookeeper) export HBASE_SSH_OPTS=\u0026#34;-p 9922\u0026#34; (端口号不是默认的22,要改为9922) 修改 =regionservers=(指定regionservers的主机地址): node-master node1 node2 2.4.3 启动 Hbase 在 node-master 的 /home/hadoop/hadoop/hbase-1.2.0-cdh5.7.1/bin 目录下运行:\nstart-hbase.sh 2.4.4 监控 Hbase 可以在浏览器查看 HBase 集群的信息:\nHMaster web 管理信息:http://192.168.2.1:60010/master-status HregionServer 管理信息:http://192.168.2.1:60030/ http://192.168.2.2:60030/ http://192.168.2.3:60030/\n3 结语 整个 Hbase 集群应该搭建完了,关于Mysql 搭建的文章就太多了,我也不赘言了。至此,所有存储的组件就已经安装完毕并已启用,但是都是没有数据的,接下来我们需要做的是如何将旧的测试环境的数据迁移到新的测试环境。考虑到这篇内容已经很长了,剩下的内容我就另外写一篇博文了。\n4 参考 http://blog.csdn.net/u010824591/article/details/51174099 https://yq.aliyun.com/articles/26415 https://linode.com/docs/databases/hadoop/how-to-install-and-set-up-hadoop-cluster/ ","permalink":"https://ramsayleung.github.io/zh/post/2018/store_cluster_migrate1/","summary":"搭建和配置 Hadoop, Zookeeper, Hbase 1 前言 最近负责公司测试环境的迁移,主要包括 Hbase+Mysql 存储集群的迁移,消息队列,缓存组件的迁移, 而我打算说说存储集群的迁移。因为公司的","title":"记存储集群的一次迁移过程(上)"},{"content":"开发第一个Rust crate 的感受和踩到的坑\n最近写了人生第一个 Rust crate\u0026ndash; rspotify. 虽说并不是什么惊天地,泣鬼神的大作,但是也是我花费了近两个月实现的。\n现在就来聊聊这个开发过程的感悟和踩到的坑\n1 感悟 1.1 函数的缺省值 因为我是参考着 Python 版本的 Spotify API SDK 来写 rspotify的,Spotify 某些API 需要请求的时候附加上默认值,例如在获取一个歌手最热的10首歌的时候需要指定country.\n因为Python 的函数是有缺省参数的,所以用 python 来实现就很方便\ndef artist_top_tracks(self, artist_id, country=\u0026#39;US\u0026#39;): \u0026#34;\u0026#34;\u0026#34; Get Spotify catalog information about an artist\u0026#39;s top 10 tracks by country. Parameters: - artist_id - the artist ID, URI or URL - country - limit the response to one particular country. \u0026#34;\u0026#34;\u0026#34; trid = self._get_id(\u0026#39;artist\u0026#39;, artist_id) return self._get(\u0026#39;artists/\u0026#39; + trid + \u0026#39;/top-tracks\u0026#39;, country=country) 但是用 Rust 来实现的时候,问题就来了,因为Rust 是没有缺省参数的。而Rust 处理缺省参数的策略一般是Builder Pattern:\nstruct Part1 { points: u32, tf: f64, dt: f64 } impl Part1 { fn new() -\u0026gt; Part1 { Part1 { points: 30_u32, tf: 3_f64, dt: 0.1_f64 } } fn tf(mut self, tf: f64) -\u0026gt; Self { self.tf = tf; self } fn points(mut self, points: u32) -\u0026gt; Self { self.points = points; self } fn dt(mut self, dt: f64) -\u0026gt; Self { self.dt = dt; self } fn run(self) { // code here println!(\u0026#34;{:?}\u0026#34;, self); } } //调用函数 Part1::new().points(10_u32).run(); Part1::new().tf(7_f64).dt(15_f64).run(); 具体情况具体分析,就 rspotify 而言, Builder Pattern 并不适用,因为 rspotify 有很多函数都需要缺省参数,而不同函数的缺省值可能又不一样。\n例如,有些函数的 offset参数是 0, 而另外一些函数的 offset 参数是1. 为此,我还在 Reddit 发贴询问意见,PM_ME_WALLPAPER 建议我用Into\u0026lt;Option\u0026lt;T\u0026gt;\u0026gt;:\nfn foo\u0026lt;T: Into\u0026lt;Option\u0026lt;usize\u0026gt;\u0026gt;\u0026gt;(limit: T) { let limit = limit.into().unwrap_or(10); … } 在他的建议下,我把 artist_top_tracks() 修改成:\npub fn artist_top_tracks( \u0026amp;self, artist_id: \u0026amp;mut str, country: impl Into\u0026lt;Option\u0026lt;String\u0026gt;\u0026gt;, ) -\u0026gt; Option\u0026lt;FullTracks\u0026gt; { let mut params: HashMap\u0026lt;\u0026amp;str, String\u0026gt; = HashMap::new(); params.insert(\u0026#34;country\u0026#34;, country.into().unwrap_or(\u0026#34;US\u0026#34;.to_owned())); let trid = self.get_id(Type::Artist, artist_id); let mut url = String::from(\u0026#34;artists/\u0026#34;); url.push_str(\u0026amp;trid); url.push_str(\u0026#34;/top-tracks\u0026#34;); match self.get(\u0026amp;mut url, \u0026amp;mut params) { Some(result) =\u0026gt; { // let mut albums: Albums = ; match serde_json::from_str::\u0026lt;FullTracks\u0026gt;(\u0026amp;result) { Ok(_tracks) =\u0026gt; Some(_tracks), Err(why) =\u0026gt; { eprintln!(\u0026#34;convert albums from String to Albums failed {:?}\u0026#34;, why); None } } } None =\u0026gt; None, } } 虽说不如Python 那样优雅,但是看起来还是不错滴\n1.2 错误处理 对于一个 library 而言,错误处理是设计的重要一环。\n因为我之前只有开发应用的经验, 而开发应用的错误处理和开发类库的错误处理显然需要考虑的东西不一样,所以我还谨慎思考过这个问题。后来,我决定不处理调用Spotify API 或者其他操作导致的错误,将错误进行一次包装(wrap), 然后再返回给library 的调用者。\n最开始的时候,我是自己定义错误类型的,后来觉得过于累赘,就用上error_chain. 用上 error_chain 之后, errors.rs这个文件也非常简单:\n///The kind of spotify error. use serde_json; error_chain! { errors {} foreign_links { Json(serde_json::Error) #[doc = \u0026#34;An error happened while serializing JSON\u0026#34;]; } } 而刚刚我提到了只对错误作简单的包装,得益于 error_chain的设计,这个特性也很容易实现:\npub fn convert_result\u0026lt;\u0026#39;a, T: Deserialize\u0026lt;\u0026#39;a\u0026gt;\u0026gt;(\u0026amp;self, input: \u0026amp;\u0026#39;a str) -\u0026gt; Result\u0026lt;T\u0026gt; { let result = serde_json::from_str::\u0026lt;T\u0026gt;(input) .chain_err(|| format!(\u0026#34;convert result failed, content {:?}\u0026#34;,input))?; Ok(result) } 这个函数是将 Spotify 的响应体映射成对应的 object(例如 playlist, album 等). 如果转换过程出错了,那么就返回convert result failed, content {:?}错误信息之后,返回 serde_json 转换时出现的错误信息。\n1.3 Reddit+clippy 剩下的是在纠结定义一个函数传参的时候是传值,参数是 mutable 还是 immutable, 以及其他类似的考虑。\n或许 Effective Rust 和 More Effective Rust 出现之后,我读完就知道什么样的设计才是 best practice. 因为有诸多设计的不确定,所以在完成rspotify 90% 的代码量之后,我在 Reddit 上发贴,邀请社区的同学来 review code 以帮我完善代码。\n他们的确给了我很多建议,我也根据他们的建议修改 rspotify. 在经过人肉 code review 之后,是时候祭出 clippy 这个大杀器, clippy 就代码的编写给出了非常多的建议,比如将函数 Vec\u0026lt;String\u0026gt; 的参数类型修改成 \u0026amp;[String], 因为函数并没有使用(consume) 这个参数,所以传引用比传值更合适,类似 的建议不胜枚举。\n最后在 clippy 的建议下, 我几乎将所有的 clippy warning 都消除掉。 邀请别人经常帮你 review code 有点不实际,但是 clippy 确是不会因为帮你审查代码而感到厌烦的,真的是非常强大的工具\n2 坑 2.1 Debugger 虽说 Rust 也有Debugger\u0026ndash; gdb-rust. gdb 我以前写c 的时候用过,gdb 熟悉程度虽然谈 不上精通,但是也能熟练使用。但是用gdb-rust 调试并不是非常便利,比如在使用 Rocket 这个Web框架的时候,就很难使用gdb来调试Web程序。\n虽说 intellij-rust 这个 Intellij Idea 的插件也支 持Debugger, 但是只有配合Clion才能使用。因为 只有 Clion 才能调用 gdb, 无奈。所以在开发 rspotify 的时候,我用得都是 println!()调试大法。\n2.2 编译器Bug 战战兢兢地开发着,终于到发布到 crates.io 的大喜日子了,怎知在发布之后一直没办法看到生成的文档,本地不是一切正常么?\n后来在社区 同学的提醒下, 我才发现我踩到了 Rust 编译器的一个bug, 最后我就顺手提交了一个 issue, 虽说这个问题已经在 nightly 里面修复了。\n3 结语 前后两个月的时间,终于发布了 rspotify. 项目不大,但是也是我花费时间,精力去开发的,也得到其他同学的肯定,喔耶 :)\n","permalink":"https://ramsayleung.github.io/zh/post/2018/rspotify/","summary":"开发第一个Rust crate 的感受和踩到的坑 最近写了人生第一个 Rust crate\u0026ndash; rspotify. 虽说并不是什么惊天地,泣鬼神的大作,但是也是我花费了近两个月实现的。 现在就来聊聊","title":"rspotify– 我的第一个Rust crate"},{"content":"自定义错误和error_chain 库\n1 前言 上一篇文章聊到 Rust 的错误处理机制,以及和 Java 的简单比较,现在就来聊一下如何在 Rust 自定义错误,以及引入 error_chain这个库来优雅地进行错误处理。\n还有,少不了用 Java 来做对比咯:)\n1.1 Java 自定义异常 前文简单提到 Java 的错误和异常但是继承自一个 Throwable的父类,既然异常是继承自异常父类的,我们自定义异常的时候, 也可以模仿JDK, 继承一个异常类:\npublic class MyException extends Exception { public MyException(String message) { super(message); } } 这样就定义了属于自己的异常. 只需要继承 Exception,然后调用父类的构造方法。\n不过 对于那些复杂的项目,这样的例子未免过于简单。现在就来看一个我项目的中的一个异常类:\npublic final class MyError extends RuntimeException { /** * */ private static final long serialVersionUID = 1L; private static boolean isFillStack = true; private Integer httpStatusCode; private Integer code; private String message; private MyError(int httpStatusCode, int code, String message) { super(message, null, isFillStack, isFillStack); this.httpStatusCode = httpStatusCode; this.code = code; this.message = message; } public MyError(MyErrorCode myErrorCode, Object... messageArgs) { this(myErrorCode.getHttpStatusCode(), myErrorCode.getErrorCode(), MessageFormat.format(myErrorCode.getMessagePattern(), messageArgs)); } public static MyError throwError(MyErrorCode myErrorCode, Object... messageArgs) { throw new MyError(myErrorCode, messageArgs); } public static MyError internalServerError(String logId) { throw new MyError(MyErrorCode.INTERNAL_SERVER_ERROR, logId); } public static MyError DataError(String logId) { throw new MyError(MyErrorCode.DATA_ERROR, logId); } public static MyError BadParameterError(String logId) { throw new MyError(MyErrorCode.BAD_PARAMETER_ERROR, logId); } } 这是我去掉了多余方法和变量的简化版,但是也足以一叶知秋了。\nMyError这个异常类是 继承于 RuntimeException的,并调用了 RuntimeException的构造方法。\n因为我的项目是 WEB 服务的业务层,要处理大量的逻辑,难免会出现异常.\n比如说可能调用方调用接口 的时候,入参不符合规范,我就抛出一个经过包装的 BadParameterError 异常,对于接 口调用方,这样会比一个单纯的 400 错误要友好,其他的异常也是同理。\n1.2 Rust 自定义错误 对于习惯了 OOP 编程的同学来说,Java 的异常是很容易理解,但是回到 Rust 身上,Rust是没有父类一说的,显然,Rust 是没可能套用 Java 的自定义异常的方式的。\nRust 用的是 trait, trait就有点类似 Java 的 =interface=(只是类似,不是等同!).\n按照 Rust 的规范,Rust 允许开发者定义自己的错误,设计良好的错误应该包含以下的特性:\n使用相同的类型(type)来表示不同的错误 错误中包含对用户友好的提示(我也在上面提到的) 能便捷地与其他类型比较,例如: Good: Err(EmptyVec) Bad: Err(\u0026quot;Please use a vector with at least one element\u0026quot;.to_owned()) 包含与错误相关的信息,例如: Good: Err(BadChar(c, position)) Bad: Err(\u0026quot;+ cannot be used here\u0026quot;.to_owned()) 可以很方便地与其他错误结合 use std::error; use std::fmt; use std::num::ParseIntError; type Result\u0026lt;T\u0026gt; = std::result::Result\u0026lt;T, DoubleError\u0026gt;; #[derive(Debug, Clone)] // 自定义错误类型。 struct DoubleError; // 不同的错误需要展示的信息也不一样,这个就要视情况而定,因为 DoubleError 没有定义额外的字段来保存错误信息 // 所以就现在就简单打印错误信息 impl fmt::Display for DoubleError { fn fmt(\u0026amp;self, f: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!(f, \u0026#34;invalid first item to double\u0026#34;) } } // 实现 error::Error 这个 trait, 对DoubleError 进行包装 impl error::Error for DoubleError { fn description(\u0026amp;self) -\u0026gt; \u0026amp;str { \u0026#34;invalid first item to double\u0026#34; } fn cause(\u0026amp;self) -\u0026gt; Option\u0026lt;\u0026amp;error::Error\u0026gt; { // Generic error, underlying cause isn\u0026#39;t tracked. None } } fn double_first(vec: Vec\u0026lt;\u0026amp;str\u0026gt;) -\u0026gt; Result\u0026lt;i32\u0026gt; { vec.first() // Change the error to our new type. .ok_or(DoubleError) .and_then(|s| s.parse::\u0026lt;i32\u0026gt;() // Update to the new error type here also. .map_err(|_| DoubleError) .map(|i| 2 * i)) } fn print(result: Result\u0026lt;i32\u0026gt;) { match result { Ok(n) =\u0026gt; println!(\u0026#34;The first doubled is {}\u0026#34;, n), Err(e) =\u0026gt; println!(\u0026#34;Error: {}\u0026#34;, e), } } fn main() { let numbers = vec![\u0026#34;42\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; let empty = vec![]; let strings = vec![\u0026#34;tofu\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); } 这段代码的运行结果如下:\nThe first doubled is 84 Error: invalid first item to double Error: invalid first item to double 1.3 error_chain 虽说 Rust 自定义错误很灵活和方便,但是如果每次定义异常都需要实现 Display 和 Error, 未免过于繁琐,现在来介绍 error_chain 这个类库。\nerror_chain 是由 Rust 项目组的 leader\u0026ndash;Brian Anderson 编写的异常处理库,可以让你更舒心简单不粗 暴地定义错误。\n1.3.1 error_chain示例 以上面的 DoubleError为例,并改写 error_chain 的官方例子 以实现相同的效果,代码如下:\n// `error_chain!` 的递归深度 #![recursion_limit = \u0026#34;1024\u0026#34;] //引出 error_chain 和相应的宏 #[macro_use] extern crate error_chain; //将跟错误有关的内容放入 errors module, 其他需要用到这个错误module 的模块就通过 // use errors::* 来引入所有内容 mod errors { // Create the Error, ErrorKind, ResultExt, and Result types error_chain! { errors{Double{ description(\u0026#34;invalid first item to double\u0026#34;) display(\u0026#34;invalid first item to double\u0026#34;) }} } } use errors::*; pub type Result\u0026lt;T\u0026gt; = ::std::result::Result\u0026lt;T, ErrorKind\u0026gt;; fn main() { let numbers = vec![\u0026#34;42\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; let empty = vec![]; let strings = vec![\u0026#34;tofu\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); } fn double_first(vec: Vec\u0026lt;\u0026amp;str\u0026gt;) -\u0026gt; Result\u0026lt;i32\u0026gt; { vec.first() // Change the error to our new type. .ok_or(ErrorKind::Double) .and_then(|s| s.parse::\u0026lt;i32\u0026gt;() // Update to the new error type here also. .map_err(|_| ErrorKind::Double) .map(|i| 2 * i)) } fn print(result: Result\u0026lt;i32\u0026gt;) { match result { Ok(n) =\u0026gt; println!(\u0026#34;The first doubled is {}\u0026#34;, n), Err(e) =\u0026gt; println!(\u0026#34;Error: {}\u0026#34;, e), } } 运行这代码可以得到和上面小节同样的输出。\n1.3.2 error_chain 详解 刚刚就先目睹了一下 error_chain 的芳容了,现在是时候来解剖一下 error_chain, 这次就以 error_chain的 example来解释\n// Simple and robust error handling with error-chain! // Use this as a template for new projects. // `error_chain!` can recurse deeply #![recursion_limit = \u0026#34;1024\u0026#34;] // Import the macro. Don\u0026#39;t forget to add `error-chain` in your // `Cargo.toml`! #[macro_use] extern crate error_chain; // We\u0026#39;ll put our errors in an `errors` module, and other modules in // this crate will `use errors::*;` to get access to everything // `error_chain!` creates. mod errors { // Create the Error, ErrorKind, ResultExt, and Result types error_chain! { } } use errors::*; fn main() { if let Err(ref e) = run() { println!(\u0026#34;error: {}\u0026#34;, e); for e in e.iter().skip(1) { println!(\u0026#34;caused by: {}\u0026#34;, e); } // The backtrace is not always generated. Try to run this example // with `RUST_BACKTRACE=1`. if let Some(backtrace) = e.backtrace() { println!(\u0026#34;backtrace: {:?}\u0026#34;, backtrace); } ::std::process::exit(1); } } // Most functions will return the `Result` type, imported from the // `errors` module. It is a typedef of the standard `Result` type // for which the error type is always our own `Error`. fn run() -\u0026gt; Result\u0026lt;()\u0026gt; { use std::fs::File; // This operation will fail File::open(\u0026#34;contacts\u0026#34;) .chain_err(|| \u0026#34;unable to open contacts file\u0026#34;)?; Ok(()) } 重要的信息例子已经作了注释,现在就来看看用法。首先来看看 main 函数\nfn main() { if let Err(ref e) = run() { println!(\u0026#34;error: {}\u0026#34;, e); for e in e.iter().skip(1) { println!(\u0026#34;caused by: {}\u0026#34;, e); } // The backtrace is not always generated. Try to run this example // with `RUST_BACKTRACE=1`. if let Some(backtrace) = e.backtrace() { println!(\u0026#34;backtrace: {:?}\u0026#34;, backtrace); } ::std::process::exit(1); } } 可以看出,这个函数的大部份逻辑是进行错误处理,例如返回自定义的 Result和 Error, 然后处理这些错误。上面的处理流程显示了 error_chain 从某个错误继承而来的三样信息:最近出现的错误(即e),导致错误的调用链,原来错误的堆栈信息 (e.backtrace())\n2 小结 刚刚的例子只是 error_chain 小试了一波牛刀,如果想要了解更多关于 Rust 异常处理 的细节,就需要看看 Rust 的文档咯\n3 参考 rust by example starting with error chain 24-days-rust-error_chain/ handling errors in rust error chain error handle ","permalink":"https://ramsayleung.github.io/zh/post/2018/error_handle_in_rust_2/","summary":"自定义错误和error_chain 库 1 前言 上一篇文章聊到 Rust 的错误处理机制,以及和 Java 的简单比较,现在就来聊一下如何在 Rust 自定义错误,以及引入 er","title":"Rust的错误处理(二)"},{"content":"拉上Java 来谈谈 Rust的错误处理\n1 前言 每个语言都会有异常处理机制(没有异常处理机制的语言估计也没有人会用了),Rust 自然也不例外,所以今天我就来谈Rust 的异常处理,因为 Rust 的异常处理跟常见的语言 (Java/Python 等)的处理机制差异略大,所以打算拉个上个语言,对比着解释. 没错,这 个光荣的任务就落到了 Java 身上\n2 Java 的异常处理 在谈 Rust 的异常处理之前,为了把它们之前的差异讲清楚,先来聊一下 Java 的异常处理。\nFigure 1: Java exception hierarchy\n如上面的简易图所示, Java 的异常都是继承于 Throwable 这个分类的,而异常又是分 成不同的类型: Error, Exception; Exception 又分成 Checked Exception 和 RuntimeException.\nError 一般都是了出现严重的问题,按照JDK 注释的说法,都是不应该 try-catch的:\nAn {() Error} is a subclass of {() Throwable} that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.\n比如虚拟机挂了,或者JRE 出了问题就可能是 Error,前几天我就遇到一个JRE 的 Bug, 整个项目都挂 了:\nFigure 2: JRE fatal error\n我还顺便给 Oracle 报了个Bug :)\n至于RuntimeException 就是类似数组越界,空指针这些异常,即无法在程序编译时发现,只有在运行的时候才会出 现的问题,所以叫做运行时异常(RuntimeException).\n3 Checked Exception Java的Checked Exception, 也就是Java 要求你必须在函数的类型里面声明或处理它可能抛出的异常。比如,你的函数如果是这样:\nvoid foo(string filename) throws IOException { File file = new File(filename); BufferedReader br = new BufferedReader(new FileReader(file)); String st; while ((st = br.readLine()) != null) System.out.println(st); } } Java 要求你必须在函数头部写上 throws IOException 或者是必须用 try-catch处理这个异常,因为readline() 的方法签名是:\nString readLine(boolean ignoreLF) throws IOException { } 所以编译器要求必须要处理这个异常,否则它就不能编译。\n同理,在使用 foo()这个函数 的时候,可能会抛出 IOException 这个异常,由于编译器看到了这个声明,它会严格检 查你对 foo 函数的用法。\n在我看来,CheckedException是Java 优良的设计之一,正因 为Checked Exception的存在,会更容易编写出正确处理错误的程序,更健壮的程序\n4 Rust 的异常处理 Rust 是一个注重安全(Safety)的语言,而错误处理也是 Rust关注的要点之一。\nRust 主要是将错误划分成两种类型,分别是可恢复的错误(recoverable error) 和不可恢复错误 (unrecoverable error).\n出现可恢复的错误的原因多种多样,例如打开文件的时候,文件找不到或者没有读权限等,开发者就应该对这种可能出现的错误进行处理;\n而不可恢复的错误就可能是Bug 引起的,比如数组越界等。而其他常见的语言一般是没有没有区分 recoverable error和 unrecoverable error的. 比如 Python, 用的就是 Exception.\n而Rust 是没有 Exception, Rust 用 Result\u0026lt;T, E\u0026gt; 表示可恢复错误, 用 panic!() 来表示出现错误,并且中断程序的执行并退出(不可恢复错误)。\nResult 是Rust 标准库的枚举:\npub enum Result\u0026lt;T, E\u0026gt; { Ok(T), Err(E), } T和E都是泛型,T表示程序执行正常的时候的返回值,那E自然是程序出错时的返回 值。以标准库的打开文件的函数为例, std::io::File 的 open() 函数的签名如下:\npub fn open\u0026lt;P: AsRef\u0026lt;Path\u0026gt;\u0026gt;(path: P) -\u0026gt; io::Result\u0026lt;File\u0026gt; { OpenOptions::new().read(true).open(path.as_ref()) } 忽略这个方法的参数,只看返回值类型:io::Result\u0026lt;File\u0026gt;, 又因为有 type Result\u0026lt;T\u0026gt; Result\u0026lt;T, Error\u0026gt;;=\n这个 typedef 语句,所以返回值的完整版本时io::Result\u0026lt;File,io::Error\u0026gt;, 即调用 open 这个函数的时候,可能出现错误,出现错误时候返回一个 io::Error, 如果调用open没有问题的话,就会返回一个 File 的结构体,所以这个就类似 Java 的CheckedException,\n只要声明了函数可能出现问题,在调用函数的时候就必须处理可能出现的错误,不然编译器就不会让你通过(Rust 的编译器就像位父亲那样对开发者耳提面命), 例如:\nmatch File::open(\u0026amp;self.cache_path) { Ok(file) =\u0026gt; println!(\u0026#34;{:?}\u0026#34;,file), Err(why) =\u0026gt; { panic!(\u0026#34;couldn\u0026#39;t open {:?}\u0026#34;, why.description()) } }; 5 Java 的异常传递 在程序中,总会有一些错误需要处理,但是却不应该在错误出现的函数进行处理的情况(或者是,你很懒惰,只想应付一下编译器,不想处理出现的异常 :)\n比如你正在编写一个类 库,里面有很多的IO 操作,有IO 操作的地方就有可能出现IOException. 如果出现异常, 你不应该自己在类库把异常给 try-catch了,如果这样,使用你类库的开发者就没办法知 道程序出现了异常,异常的堆栈也丢了。\n比较合理的做法是,把IOException捕捉了,然后对 IOException 做一层包装,然后再抛给类库的调用者,例如:\npublic void doSomething() throws WrappingException{ try{ doSomethingThatCanThrowException(); } catch (SomeException e){ e.addContextInformation(\u0026#34;there is something happen in doSomething() function, `Some Exception` is raised, balabala\u0026#34;); //throw e; //throw e, or wrap it see next line. throw new WrappingException(e, more information about Some Exception, balabala); } finally { //clean up close open resources etc. } } 当然,你也可以在添加了额外的信息之后,直接把原来的异常抛出来\n6 Rust 的异常传递 刚刚谈了 Java 的异常传递,现在轮到 Rust 的异常传递了,既然Rust 没有 Exception一说,那 Rust 传递的自然也是 Result\u0026lt;T,E\u0026gt; 这个枚举类型(这里针对的是 可恢复错误,不可恢复错误出现错误的时候,会返回错误并弹出程序,自然不存在异常传递).\n先来看看 Rust 的异常传递的例子:\nuse std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -\u0026gt; Result\u0026lt;String, io::Error\u0026gt; { let f = File::open(\u0026#34;hello.txt\u0026#34;); let mut f = match f { Ok(file) =\u0026gt; file, Err(e) =\u0026gt; return Err(e), }; let mut s = String::new(); match f.read_to_string(\u0026amp;mut s) { Ok(_) =\u0026gt; Ok(s), Err(e) =\u0026gt; Err(e), } } 例子来自 Rust Book\n先来看看函数的返回值 Result\u0026lt;String,io::Error\u0026gt;, 也就是说, read_username_from_file 正确执行的时候返回是 String, 错误的时候,返回的是 io::Error. 这里的异常传递是在出现 io::Error的时候,将错误原样返回,不然就是返 回函数执行成功的结果。\n就异常传递的方式而言,Rust 和 Java 是大同小异:声明可能抛出的异常和成功时返回的结果,然后在遇到错误的时候,直接(或者包装一下)返回错误。\n6.1 ? 关键字 虽说 Rust 的异常处理很清晰,但是每次都要 match 然后返回未免太繁琐了,所以 Rust 提供了一个语法糖来显示繁琐的异常传递:用 \u0026ldquo;?\u0026rdquo; 关键字进行异常传递:\nuse std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -\u0026gt; Result\u0026lt;String, io::Error\u0026gt; { let mut f = File::open(\u0026#34;hello.txt\u0026#34;)?; let mut s = String::new(); f.read_to_string(\u0026amp;mut s)?; Ok(s) } 同样的功能,但是模板代码却减少了很多 :)\n6.2 unwrap 和 expect 虽说 Rust 的可恢复错误设计得很优雅,但是每次遇到可能出现错误得地方都要显示地进行 处理,不免让人觉得繁琐.\nRust 也考虑到这种情况了,提供了 unwrap() 和 expect()让你舒心简单粗暴地处理错误:在函数调用成功的时候返回正确的结果,在 出现错误地时候直接 panic!(),并退出程序\n6.2.1 unwrap fn main() { let f = File::open(\u0026#34;hello.txt\u0026#34;).unwrap(); } 打开 hello.txt这个文件,能打开就返回文件 f,不能打开就 panic!() 然后退出程序。\nthread \u0026#39;main\u0026#39; panicked at \u0026#39;called `Result::unwrap()` on an `Err` value: Error { repr: Os { code: 2, message: \u0026#34;No such file or directory\u0026#34; } }\u0026#39;, /stable-dist-rustc/build/src/libcore/result.rs:868 6.2.2 expect expect()和 unwrap()类似,只不过 expect()可以加上额外的信息:\nuse std::fs::File; fn main() { let f = File::open(\u0026#34;hello.txt\u0026#34;).expect(\u0026#34;Failed to open hello.txt\u0026#34;); } 出现错误的时候,除了显示应有的错误信息之外,还会显示你自定义的错误信息:\nthread \u0026#39;main\u0026#39; panicked at \u0026#39;Failed to open hello.txt: Error { repr: Os { code: 2, message: \u0026#34;No such file or directory\u0026#34; } }\u0026#39;, /stable-dist-rustc/build/src/libcore/result.rs:868 以上代码来自 Rust book\n7 结语 以上只是浅谈了 Rust 的错误处理,以及和 Java 的异常处理机制的简单比较,接下来我会 谈谈如何自定义Error以及使用 erro_chain 这个库来优雅地进行错误处理 :)\n如果想了解更多关于 Rust 异常处理的内容,可以查阅 Rust book Error handle\n8 参考 propagating exceptions Rust book IO Result ","permalink":"https://ramsayleung.github.io/zh/post/2018/error_handle_in_rust_1/","summary":"拉上Java 来谈谈 Rust的错误处理 1 前言 每个语言都会有异常处理机制(没有异常处理机制的语言估计也没有人会用了),Rust 自然也不例外,所以","title":"Rust的错误处理(一)"},{"content":"1 前言 目标: 在Eshell中像在bash/zsh中使用fzf那般搜索历史命令\n2 fzf 我的主力Shell 是Eshell, 但是平时我也会用Zsh, 而fzf 是一个非常好用的命令行工具,用了fzf搜索历史命令:\nFigure 1: fzf\n3 Eshell 我日常的操作基本都是在 Eshell 上面进行的,不过 Eshell 是没办法直接像 Bash 那样调用 fzf来查找命令历史的,所以我希望把这个功能迁移到到Eshell 上面来。\n我在 Emacs 使用的补全框架是 Ivy/Counsel,它有一个 counsel-esh-history的命令可以使用 Ivy 来搜索命令,但是没办法使用用户已经输入的内容来过滤命令,所以我就在自己折腾了一个\ncounsel-esh-history 命令。效果如下:\nFigure 2: 感觉很不错嘛 :)\n4 源代码 得益于 Ivy强大的内置函数, 功能实现起来相当便利,完整代码如下:\n(defun samray/esh-history () \u0026#34;Interactive search eshell history.\u0026#34; (interactive) (require \u0026#39;em-hist) (save-excursion (let* ((start-pos (eshell-bol)) (end-pos (point-at-eol)) (input (buffer-substring-no-properties start-pos end-pos))) (let* ((command (ivy-read \u0026#34;Command: \u0026#34; (delete-dups (when (\u0026gt; (ring-size eshell-history-ring) 0) (ring-elements eshell-history-ring))) :preselect input :action #\u0026#39;ivy-completion-in-region-action)) (cursor-move (length command))) (kill-region (+ start-pos cursor-move) (+ end-pos cursor-move)) ))) ;; move cursor to eol (end-of-line) ) 代码不是很复杂, 主要功能是获取用户输入的命令, 然后把所有的历史命令读取出来,最后使用ivy-read内置的ivy-completion-in-region-action功能, 用用户的输入的命令与历史命令进行匹配, 由用户选择最终的命令.\nivy-read是Emacs内置completing-read的函数的强化, 关于ivy-read具体用法可以参考文档ivy-read.\n5 总结 最后, 我也顺便把代码分享到 Emacs社区, 而 manateelazycat也把这段代码的功能加入到aweshell, Oh yeah !\n","permalink":"https://ramsayleung.github.io/zh/post/2017/search_eshell_history_like_fzf/","summary":"1 前言 目标: 在Eshell中像在bash/zsh中使用fzf那般搜索历史命令 2 fzf 我的主力Shell 是Eshell, 但是平时我也会用Zsh, 而","title":"Eshell实现fzf的历史命令搜索功能"},{"content":"python 与嵌入式关系数据库 sqlite3的邂逅\nSQLite 是一个非常优秀的嵌入式数据库,非常轻量,可以与 Mysql, PostgreSQL 这样的 大型数据库互补使用. 而 Python 标准库中的 sqlite3 模块实现了兼容 SQLite 的 Python DB-API 2.0接口, 因此我们可以很方 便地使用 sqlite3 模块来操作 SQLite\n1 入门 1.1 创建数据库 SQLite 数据库是存储在文件系统的单个文件上的,所以如果数据库文件不存在,那么在第一次访问这个数据库,就会创建相应的数据库文件。\nimport os import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; db_exist = os.path.exists(db_filename) conn = sqlite3.connect(db_filename) if db_exist: print(\u0026#39;Database exists\u0026#39;) else: print(\u0026#34;Database does not exist\u0026#34;) conn.close() 上面的例子会在连接数据库之前检查一下数据库文件是否存在,然后使用 connect() 函数连接数据库。\n你在执行该代码之前查看一下当前目录的话,如果不存在 sqlite3_demo.db 的话,那么跑完这段代码,你应该会看到 sqlite3_demo.db 文件的.\n这段代码本身是没有做多少事,我只是用它来阐述一下 SQLite 的原理\n1.2 创建表 那么,现在,让我们用 SQLite 来做点数据库的本份工作。先创建一张表,接下来的操作都会围绕着这张表进行。\nuser.sql:\ncreate table role( name text primary key, description text ); create table user ( id integer primary key autoincrement not null, name text, phone_number integer, birthday date, role text not null references role(name) ); 然后使用 Connection 对象的 executescript() 函数来创建表以及插入对应的数据\nimport os import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; schema_filename = \u0026#39;user.sql\u0026#39; db_exists = os.path.exists(db_filename) with sqlite3.connect(db_filename) as conn: if db_exists: print(\u0026#39;Creating schema\u0026#39;) with open(schema_filename, \u0026#39;rt\u0026#39;) as f: schema = f.read() conn.executescript(schema) print(\u0026#39;Inserting initial data\u0026#39;) conn.executescript(\u0026#34;\u0026#34;\u0026#34; insert into role (name,description) values (\u0026#39;student\u0026#39;,\u0026#39;This is a student\u0026#39;); insert into role (name,description) values (\u0026#39;teacher\u0026#39;,\u0026#39;This is a teacher\u0026#39;); insert into user (id,name,phone_number,birthday,role) values (1,\u0026#39;Samray\u0026#39;,12345678,\u0026#39;2017-11-10\u0026#39;,\u0026#39;student\u0026#39;); insert into user (id,name,phone_number,birthday,role) values (2,\u0026#39;Paul\u0026#39;,3231546,\u0026#39;2017-11-11\u0026#39;,\u0026#39;student\u0026#39;); insert into user (id,name,phone_number,birthday,role) values (3,\u0026#39;Trump\u0026#39;,13254768,\u0026#39;2017-11-12\u0026#39;,\u0026#39;teacher\u0026#39;); \u0026#34;\u0026#34;\u0026#34;) 1.3 检索数据 如果想要使用检索存储在 user 表中的数据,那么就需要从数据库连接对象 Connection 中创建一个 Cursor对象。\n而Cursor 对象负责与数据库进行交互并获取 数据\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: cursor = conn.cursor() cursor.execute(\u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=\u0026#39;student\u0026#39; \u0026#34;\u0026#34;\u0026#34;) for row in cursor.fetchall(): id, name, phone_number, birthday = row print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( id, name, phone_number, birthday)) SQLite3 数据库的查询分成两步。首先,使用 Cursor 对象的 execute() 对象执行查询语句,告诉数据库引擎我们需要什么样的数据,然后,使用 fetchall() 函数把数据集从数据库的返回结果中取出来。\n返回结果是包含着一系列 tuple 的列表,而tuple 中对应着的数据就是 select 语句指定返回的字段值。\nfetchall()函数是把所有符合 条件的结果一次性返回,如果需要的话,我们可以使用fetchone()函数返回单条记录, 或者使用fetchmany()返回固定数量的记录\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: cursor = conn.cursor() cursor.execute(\u0026#34;\u0026#34;\u0026#34; select name, description from role where name=\u0026#39;teacher\u0026#39; \u0026#34;\u0026#34;\u0026#34;) name, description = cursor.fetchone() print(\u0026#39;Role details for {} ({}) \\n\u0026#39;.format(name, description)) cursor.execute(\u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=\u0026#39;student\u0026#39; \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;/nNext 10 tasks:\u0026#39;) for row in cursor.fetchmany(10): id, name, phone_number, birthday = row print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( id, name, phone_number, birthday)) 使用 fetchmany() 函数需要注意的是,当你指定的数量超过了符合条件的全部记录的数量的时候,fetchmany()只会返回全部记录的数量。\n例如上面的代码里面,我想要 fetchmany() 返回10条记录,但是我的数据库只有2条符合条件的数据,而 fetchmany() 之后返回两条记录\n1.4 Row 对象 在先前的内容内,我已经提到,数据库返回的数据行都是以 tuple的形式返回的,所以 程序调用者必须知道查询语句字段的顺序,然后在tuple取出记录的时候把字段名和变量名一一对应上,例如 name, description = cursor.fetchone().\n查询语句中字段不多的时候或许还能记住,但是如果字段值多了起来,就很容易出现问题.\n如果可以像value=dict['key'] 那样使用键值对的形式获取数据,那样就方便很多.\n而sqlite3也有为你提供这样便利的操作,诀窍就在使用 Row 对象。sqlite3 可以把查询结果映 射到 Row 对象,然后我们就可以通过Row[字段名'] 这种方式来获取指定字段对应的值。\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(\u0026#34;\u0026#34;\u0026#34; select name, description from role where name=\u0026#39;teacher\u0026#39; \u0026#34;\u0026#34;\u0026#34;) name, description = cursor.fetchone() print(\u0026#39;Role details for {} ({}) \\n\u0026#39;.format(name, description)) cursor.execute(\u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=\u0026#39;student\u0026#39; \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;/nNext 10 tasks:\u0026#39;) for row in cursor.fetchmany(10): print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( row[\u0026#39;id\u0026#39;], row[\u0026#39;name\u0026#39;], row[\u0026#39;phone_number\u0026#39;], row[\u0026#39;birthday\u0026#39;])) 通过指定 Connection 对象的 row_factory 属性就可以控制查询结果集返回的对象。\n在上面的代码,我们使用了 Row 对象而不是 tuple 来获取数据,而程序的执行结果都是相同,但是程序的健壮性就得到了提高。\n1.5 在查询中使用变量 我们上面的代码里面的查询语句都是硬编码的,不利于扩展。如果你希望可以使用更灵活的查询语句,你可能会去用字符串拼接查询语句。\n但是这样的做法是不被提倡的,因为很容易出现安全问题,比如说 SQL 注入. 比较提倡的方式是在执行 execute() 函数的时候进行 变量替换,使用变量替换可以避免SQL注入攻击,因为那些不被信任的代码没办法被解析。\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() sql = \u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=:role_name \u0026#34;\u0026#34;\u0026#34; cursor.execute(sql, {\u0026#39;role_name\u0026#39;: \u0026#39;student\u0026#39;}) print(\u0026#39;/nNext 10 tasks:\u0026#39;) for row in cursor.fetchmany(10): print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( row[\u0026#39;id\u0026#39;], row[\u0026#39;name\u0026#39;], row[\u0026#39;phone_number\u0026#39;], row[\u0026#39;birthday\u0026#39;])) 如上面的代码所示,使用 :role_name 占位符来表示 role_name变量, 然后在执行 SQL 语句的时候把 role_name的值传到 SQL 语句里面去。\n1.6 批量插入 我们之前提到的插入都是使用 execute() 函数逐条插入的,但是 sqlite3 也是支持批 量插入的, 使用 executemany()函数就可以实现一次插入批量的数据,而函数的底层也 是对插入多条数据的循环进行了优化的,这些就无需调用者操心了。\nuser.csv\nbirthday,name,id,phone_number 2018-11-30,Torres,22,98564311 2010-08-10,Messi,12,81582236 2018-11-21,Saul,9,23564548 import csv import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; data_filename = \u0026#39;users.csv\u0026#39; SQL = \u0026#34;\u0026#34;\u0026#34; insert into user (id,name,phone_number,birthday,role) values (:id,:name,:phone_number,:birthday,\u0026#39;student\u0026#39;) \u0026#34;\u0026#34;\u0026#34; with open(data_filename, \u0026#39;rt\u0026#39;) as csv_file: csv_reader = csv.DictReader(csv_file) with sqlite3.connect(db_filename) as conn: cursor = conn.cursor() cursor.executemany(SQL, csv_reader) 我们从 csv 文件中批量导入数据,而Python 的标准库也内置了 CSV 的解析器,使用 DictReader 就是将 csv 文件解析成 {'id':22,'birthday':'2018-11-30','name':'Torres','phone_number':98564311}的形式 然后配合上面提到的命名变量,把所有数据插入到数据库。\n2 进阶 自定义数据库列类型 SQLite 的数据列原生支持整型(integer), 浮点数(floating point), 文本类型 (text), 并且由 sqlite3 转换成 Python内置的数据类型。\n例如:数据库的整型可以转 换成Python 的 int 或者是 long, 具体取决于值的大小;文本类型默认会转换成 str 类型,除非我们修改了 Connection 对象的 text_factory 属性。\n虽然 SQLite 内部支持的数据类型不多,但是得益于 sqlite3 的内置机制的支持,我们可以 定义程序自己的数据列。\nimport sqlite3 import sys db_filename = \u0026#39;sqlite3_demo.db\u0026#39; sql = \u0026#39;select id,name,birthday from user\u0026#39; def show_birthday(conn): conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(sql) row = cursor.fetchone() for col in [\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;birthday\u0026#39;]: print(\u0026#39;{:\u0026lt;8} {:\u0026lt;10} {}\u0026#39;.format(col, row[col], type(row[col]))) return print(\u0026#39;Without type detection:\u0026#39;) with sqlite3.connect(db_filename) as conn: show_birthday(conn) print(\u0026#39;\\nWith type detection:\u0026#39;) with sqlite3.connect(db_filename, detect_types=sqlite3.PARSE_DECLTYPES,) as conn: show_birthday(conn) 如上面的代码所示,如果你想在Python 数据类型和 SQLite 数据列转换的时候使用 SQLite 原本不支持的类型,你可以在调用 connect() 函数的时候,传一个 detect_types 参数进去,而 PARSE_DECLTYPES 的意思是指转换成字段声明时候的类型, 比如 birthday 声明成 datetime类型,但是没有指定成 PAESE_DECLTYPES 的时候, 转换成 str, 指定后,转换成 datetime.\n现在我们就来说说怎么定义自己的数据列类型:\n我们需要注册两个函数,一个函数把 Python 对象转换成 byte string 存储到数据 库里面去,这个函数被称为 adapter(适配器); 既然有从Python 对象转换到数据库存储 对象的函数,那么自然就有从数据库存储转换成 Python 对象的函数,这个函数被称为 converter(转换器).\n然后就需要使用 register_adapter() 函数将一个函数注册成 adapter 函数,至于register_converter()函数,也是同理可得了。\nimport pickle import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def adapter_func(obj): \u0026#34;\u0026#34;\u0026#34;Covert from python to sqlite3 representation \u0026#34;\u0026#34;\u0026#34; print(\u0026#39;adapter_func({})\\n\u0026#39;.format(obj)) return pickle.dumps(obj) def converter_func(data): \u0026#34;\u0026#34;\u0026#34;Convert from sqlite3 to python representation \u0026#34;\u0026#34;\u0026#34; print(\u0026#39;converter_func({})\u0026#39;.format(data)) return pickle.loads(data) # custom type class MyObj: def __init__(self, arg): self.arg = arg def __str__(self): return \u0026#39;MyObj({!r})\u0026#39;.format(self.arg) # Register functions sqlite3.register_adapter(MyObj, adapter_func) sqlite3.register_converter(\u0026#34;MyObj\u0026#34;, converter_func) # Create some objects to save to_save = [ (MyObj(\u0026#39;this is a value to save\u0026#39;),), (MyObj(42),) ] with sqlite3.connect(db_filename, detect_types=sqlite3.PARSE_DECLTYPES) as conn: conn.execute(\u0026#34;\u0026#34;\u0026#34; create table if not exists obj ( id integer primary key autoincrement not null, data MyObj ) \u0026#34;\u0026#34;\u0026#34;) cursor = conn.cursor() cursor.executemany(\u0026#34;insert into obj (data) values (?)\u0026#34;, to_save) # Query the database for the objects just saved cursor.execute(\u0026#34;select id, data from obj\u0026#34;) for obj_id, obj in cursor.fetchall(): print(\u0026#39;Retrieved\u0026#39;, obj_id, obj) print(\u0026#39; with type\u0026#39;, type(obj)) print() 上面的例子使用了Python 标准库的 pickle 模块,将一个 Python 对象转换成可以保存 到数据库的字符串,然后使用 pickle 把字符串转换成Python 对象。\n这就基本实现了自定义的数据类型。不过我们自己实现的这种自定义数据类型是有局限的,我们只能把整个 Python 对象当作字符串来查询,而没办法针对 Python 对象的属性进行查询,如果你感兴趣的话,你可以看看 Python ORM 框架是怎么实现这些功能的。\n2.1 事务 谈及关系型数据库,必不可少的一定是事务。对于事务的见解,网上的资料都已经浩如烟海 了,那么,就要我们直接来说一下 SQLite 事务的使用\n2.1.1 commit 对数据库的修改操作,无论是新增(insert) 还是更新 (update), 都需要调用 commit() 来保存。\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def show_role(conn): cursor = conn.cursor() cursor.execute(\u0026#39;select name, description from role\u0026#39;) for name, description in cursor.fetchall(): print(\u0026#39; \u0026#39;, name) with sqlite3.connect(db_filename) as conn1: print(\u0026#39;Before changes:\u0026#39;) show_role(conn1) # Insert in one cursor cursor1 = conn1.cursor() cursor1.execute(\u0026#34;\u0026#34;\u0026#34; insert into role (name, description) values (\u0026#39;president\u0026#39;,\u0026#39;well, this is a president\u0026#39;) \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;\\nAfter changes in conn1:\u0026#39;) show_role(conn1) # 在没有提交事务之前,使用其它的数据库连接进行查询 print(\u0026#39;\\nBefore commit:\u0026#39;) with sqlite3.connect(db_filename) as conn2: show_role(conn2) # 提交事务,然后使用另外的数据库连接进行查询 conn1.commit() print(\u0026#39;\\nAfter commit:\u0026#39;) with sqlite3.connect(db_filename) as conn3: show_role(conn3) commit() 函数的调用结果可以被使用若干个数据库连接的程序查询到,在第一个数据库连接插入了一行新的数据,另外两个数据库连接尝试读取到新插入的数据。\n当 show_role() 函数在 conn1 提交事务之前被调用,返回结果就取决于调用 show_role() 是哪个数据连接了。\n因为是通过 conn1来修改数据库,所以它可以看到修改后的数据,但是 conn2看不到。在提交事务之后(commit()) ,通过其他的数据库连接 (conn3)也可以看到修改结果了\n2.1.2 rollback 未提交的修改可以通过调用rollback() 函数全部丢弃。通常 commit() 和 rollback() 函数都是在 try-except 语句块的不同地方被调用的,例如错误异常触发, 事务回滚(rollback)\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def show_role(conn): cursor = conn.cursor() cursor.execute(\u0026#39;select name, description from role\u0026#39;) for name, description in cursor.fetchall(): print(\u0026#39; \u0026#39;, name) with sqlite3.connect(db_filename) as conn1: print(\u0026#39;Before changes:\u0026#39;) show_role(conn1) try: # Delete cursor1 = conn1.cursor() cursor1.execute(\u0026#34;\u0026#34;\u0026#34; delete from role where name=\u0026#39;president\u0026#39; \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;\\nAfter delete\u0026#39;) show_role(conn1) # 模拟接下来的操作出现了错误 raise RuntimeError(\u0026#39;This is an error\u0026#39;) except Exception as error: # 丢弃之前的修改 print(\u0026#39;Error:\u0026#39;, error) conn1.rollback() else: # 保存修改,提交事务 conn1.commit() print(\u0026#39;\\nAfter rollback:\u0026#39;) show_role(conn1) 在调用 rollback() 函数回滚事务之后,对数据库的修改都丢弃了。\n2.2 内存型数据库 正如我们先前提到的,SQLite 是文件型数据库,它通过文件系统来管理数据库。但是 SQLite 也可以把整个数据库放到内存中去。\nimport sqlite3 schema_filename = \u0026#39;user.sql\u0026#39; with sqlite3.connect(\u0026#39;:memory:\u0026#39;) as conn: conn.row_factory = sqlite3.Row print(\u0026#39;Creating schema\u0026#39;) with open(schema_filename, \u0026#39;rt\u0026#39;) as f: schema = f.read() conn.executescript(schema) print(\u0026#39;Inserting initial data\u0026#39;) conn.execute(\u0026#34;\u0026#34;\u0026#34; insert into role (name,description) values (\u0026#39;Admin\u0026#39;, \u0026#39;wow, administrator\u0026#39; ) \u0026#34;\u0026#34;\u0026#34;) data = [ (\u0026#39;Xi\u0026#39;, 119, \u0026#39;1910-10-03\u0026#39;,\u0026#39;president\u0026#39;), (\u0026#39;Jiang\u0026#39;, 110, \u0026#39;2020-10-10\u0026#39;,\u0026#39;president\u0026#39;), (\u0026#39;Mao\u0026#39;, 10086, \u0026#39;2010-10-17\u0026#39;,\u0026#39;president\u0026#39;), ] conn.executemany(\u0026#34;\u0026#34;\u0026#34; insert into user (name, phone_number, birthday,role) values (?, ?, ?,?) \u0026#34;\u0026#34;\u0026#34;, data) print(\u0026#39;Dumping:\u0026#39;) for text in conn.iterdump(): print(text) 想要把 SQLite 当作内存型数据库,只需在调用 connect() 函数的时候,使用 :memory: 参数而不是数据库文件的文件名。\n需要注意的是,每一个 connect() 函数都会打开新建一个数据库实例,所以在一个数据库连接上的修改是不会影响其它的连接的。\n而 iterdump() 函数会返回一个迭代器,输出一系列对数据库修改的 SQL.\n最后需要注意的是,使用内存型的数据库是有风险的,要切记这一点。\n2.3 在SQL 使用 Python 函数 SQLite 支持在查询的时候使用注册了的 Python函数的,这个特性就使我们在可以获取到 查询结果之前先对数据进行加工,或者调用Python 函数实现那些 纯SQL 力所不能及的功能\nimport codecs import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def encrypt(s): print(\u0026#39;Encrypting {!r}\u0026#39;.format(s)) return codecs.encode(s, \u0026#39;rot-13\u0026#39;) def decrypt(s): print(\u0026#39;Decrypting {!r}\u0026#39;.format(s)) return codecs.encode(s, \u0026#39;rot-13\u0026#39;) with sqlite3.connect(db_filename) as conn: conn.create_function(\u0026#39;encrypt\u0026#39;, 1, encrypt) conn.create_function(\u0026#39;decrypt\u0026#39;, 1, decrypt) cursor = conn.cursor() # Raw values print(\u0026#39;Original values:\u0026#39;) query = \u0026#34;select id, name from user\u0026#34; cursor.execute(query) for row in cursor.fetchall(): print(row) print(\u0026#39;\\nEncrypting...\u0026#39;) query = \u0026#34;update user set name = encrypt(name)\u0026#34; cursor.execute(query) print(\u0026#39;\\nRaw encrypted values:\u0026#39;) query = \u0026#34;select id, name from user\u0026#34; cursor.execute(query) for row in cursor.fetchall(): print(row) print(\u0026#39;\\nDecrypting in query...\u0026#39;) query = \u0026#34;select id, decrypt(name) from user\u0026#34; cursor.execute(query) for row in cursor.fetchall(): print(row) print(\u0026#39;\\nDecrypting...\u0026#39;) query = \u0026#34;update user set name = decrypt(name)\u0026#34; cursor.execute(query) 通过 create_function() 注册了两个可供 SQL 使用的函数,而 create_function() 的参数分别是定义函数的名字,函数传递的参数的个数,以及源函数\n3 总结 虽说 SQLite 只是一个嵌入式的轻量数据库,但是麻雀虽小,五脏俱全嘛。\n内置的 sqlite3 库为Python 和 SQLite 的沟通构建了一个便捷的桥梁,但是这个桥梁只是个木桥,如果你希望使用斜拉索跨海大桥的话,你就需要去了解 sqlalchemy, 那是一个功能完善的 ORM 框架 :)\n4 参考 sqlalchemy sqlite sqlite3 python3 module of week ","permalink":"https://ramsayleung.github.io/zh/post/2017/python_with_sqlite3/","summary":"python 与嵌入式关系数据库 sqlite3的邂逅 SQLite 是一个非常优秀的嵌入式数据库,非常轻量,可以与 Mysql, PostgreSQL 这样的 大型数据库互补使用. 而 Python 标准库中的 sqlite3 模块实","title":"用python 来操控 sqlite3"},{"content":"1 博客迁移 我将博客从 Github Page 迁移到现在的博客上,原来基于 Gtihub Page,使用 Emacs, org-mode 和 org-page 的博客其实也相当好用,只是某一些我想要的功能却缺失,所以我就自己花时间动手写了现在这个博客,并且将原来的博文迁移\n\u0026lt;2022-02-25 五\u0026gt;\n没想到五年后,我又从自建的Blog,又迁移回Github Page。当时org-page bug不少,支持不是很及时,虽然org-mode很好用,但最后走到自建的博客的路子上。\n使用Rust练手写的博客很不错,但是写作workflow 还不够滑顺,使用orgmode写作,导出为markdown;对于图片,只能先上传到图床,然后再插入到文章里面。\n所以现在切换回Github Page,使用org-mode和ox-hugo, 不需要修改就可以直接把文章发布成博客,又可以愉快地写文章了。\n2 为什么要写博客 虽说我写博客的时间不长,但是当9月份为止,我已写了不少博文的。那么,为什么要写博文呢?很明显,这是一个费时费精力的工作,那么为什么我还要写呢?我自己思考过这个问题,我觉得,原因有下:\n2.1 技术沉淀 在平时的工作生活中,我免不了会碰到种种问题,了解这些问题,思考解决方案并最终付诸行动,这本身就是一个学习和进步的过程。而把其中的想法感悟写成博文会更有益于我的技术沉淀\n2.2 提高组织文字的能力 对于很多工科生来说,不擅文字,不擅言语表达估计是他们撕不掉的标签之一,但是,学会恰当地表达自己的想法是一项非常重要的技能,写博客可以提高自己的语言组织能力。此外,一位前辈曾经告诫我,如果你连文字都没办法组织好,我怎么相信你可以把你的代码组织好呢?\n2.3 便于了解自我 当在编写博客的时候,你会有诸多的想法和思考,然后你会把你的思考一点一滴付诸于笔尖,你的博文越来越清晰了,你的自己的认识也会越来越清晰,你最终会了解到自己是一个什么样的人,喜欢做的事是什么,想要的又是什么?\n2.4 分享与交流 你有一个苹果,我有一个苹果,我们交换了苹果,我们还只是拥有一个苹果;但是,你有一种想法,我有一种想法,我们交换了想法,我们就有两个想法。写博客就是一种双向的交流方式,笔者介绍自己的观点,读者发表自己的评论,思想由此而激荡,甚至孕育出新的想法\n3 结语 每个人总会有不同的想法,不同的际遇,如果你想和他人分享而不知从何言起,与何人言?何不付诸博客呢?\n","permalink":"https://ramsayleung.github.io/zh/post/2017/blog/","summary":"1 博客迁移 我将博客从 Github Page 迁移到现在的博客上,原来基于 Gtihub Page,使用 Emacs, org-mode 和 org-page 的博客其实也相当好用,只是某一些我想要的功能却缺失,所以我就自己","title":"为什么要写博客"},{"content":"我最近编写了两只京东商品和评论的分布式爬虫来进行数据分析,现在就来分享一下。\n1 爬取策略 众所周知,爬虫比较难爬取的就是动态生成的网页,因为需要解析 JS, 其中比较典型的例子就是淘宝,天猫,京东,QQ 空间等。\n所以在我爬取京东网站的时候,首先需要确定的就是爬取策略。因为我想要爬取的是商品的信息以及相应的评论,并没有爬取特定的商品的需求。所以在分析京东的网页的 url 的时候, 决定使用类似全站爬取的策略。 分析如图:\n可以看出,京东不同的商品类别是对应不同的子域名的,例如 book 对应的是图书, mvd 对应的是音像, shouji 对应的是手机等。\n因为我使用的是获取 \u0026lt;a href\u0026gt; 标签里面的 url 值,然后迭代爬取的策略。所以要把爬取的 url 限定在域名为jd.com 范围内,不然就有可能会出现无限广度。\n此外,有相当多的页面是不会包含商品信息的;例如: help.jd.com, doc.jd.com 等,因此使用 jd.com 这个域名范围实在太大了,所以把所需的子域名都添加到一个 list :\njd_subdomain = [\u0026#34;jiadian\u0026#34;, \u0026#34;shouji\u0026#34;, \u0026#34;wt\u0026#34;, \u0026#34;shuma\u0026#34;, \u0026#34;diannao\u0026#34;, \u0026#34;bg\u0026#34;, \u0026#34;channel\u0026#34;, \u0026#34;jipiao\u0026#34;, \u0026#34;hotel\u0026#34;, \u0026#34;trip\u0026#34;, \u0026#34;ish\u0026#34;, \u0026#34;book\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;health\u0026#34;, \u0026#34;baby\u0026#34;, \u0026#34;toy\u0026#34;, \u0026#34;nong\u0026#34;, \u0026#34;jiu\u0026#34;, \u0026#34;fresh\u0026#34;, \u0026#34;china\u0026#34;, \u0026#34;che\u0026#34;, \u0026#34;list\u0026#34;] 2 提取数据 在确定了爬取策略之后,爬虫就可以不断地进行工作了。那么爬虫怎么知道什么时候才是商品信息的页面呢?再来分析一下京东的商品页面:\n从上面的信息可以看出,每个商品的页面都是以 item.jd.com/xxxxxxx.html 的形式存 在的;而 xxxxxxx 就是该商品的 sku-id. 所以只需对 url 进行解析,子域名为 item 即商品页面,就可以进行爬取。\n页面提取使用 Xpath 即可,也无需赘言。不过,需要注 意的是对商品而言,非常重要的价格就不是可以通过爬取 HTML 页面得到的。\n因为价格是经常变动的,所以是异步向后台请求的。对于这些异步请求的数据,打开控制台,然后刷新,就可以看到一堆的 JS 文件,然后寻找相应的请求带有 \u0026ldquo;money 或者price\u0026rdquo; 之类关 键字的 JS 文件,应该就能找到。\n如果还没办法找出来的话,Firefox 上有一个 user-agent-switcher 的扩展,然后通过这个扩展把自己的浏览器伪装成 IE6, 相信所有 花俏的 JS 都会没了, 只剩下那些不可或缺的 JS, 这样结果应该一目了然了,这么看来 IE6 还是有用滴。最终找到的URL 如下 https://p.3.cn/prices/mgets?callback=jQuery6646724\u0026amp;type=1\u0026amp;area=19_1601_3633_0.137875165\u0026amp;pdtk=9D4RIAHY317A3bZnQNapD7ip5Dg%252F6NXiIXt90Ahk0if2Yyh39PZQCuDBlhN%252FxOch3MpwWpHICu4P%250AVcgcOm11GQ%253D%253D\u0026amp;pduid=14966417675252009727775\u0026amp;pdpin=%25E5%2585%2591%25E9%2587%2591%25E8%25BE%25B0%25E6%2589%258B\u0026amp;pdbp=0\u0026amp;skuIds=J_3356012\u0026amp;ext=10000000\u0026amp;source=item-pc\n不得不说,URL 实在是太长了。\n根据经验,大部分的参数应该都是没什么用的,应该可以去掉的,所以在浏览器就一个个参数去掉,然后试试请求是否成功,如果成功,说明此参数无关重要,最后简化成: http://p.3.cn/prices/mgets?pduid={}\u0026amp;skuIds=J_{} sku_id 即商品页面的 URL中包含的数字,而 pduid 则是一随机整数而已,用 random.randint(1, 100000000) 函数解决。\n3 商品评论 商品的评论也是以 sku-id 为参数通过异步的方式进行请求的,构造请求的方法跟价格类 似,也不需过多赘述。\n只是想要吐嘈一下的是,京东的评论是只能一页页向后翻的,不能跳转。还有一点就是,即使某样商品有 10+w 条评论,最多也只是返回 100页的数据。 略坑\n4 反爬虫策略 商品的爬取策略以及提取策略都确定了,一只爬虫就基本成型了。但是一般比较大型的网站都有反爬虫措施的。所以道高一尺,魔高一丈,爬虫也要有对应的反反爬虫策略\n4.1 禁用 cookie 通过禁用 cookie, 服务器就无法根据 cookie 判断出爬虫是否访问过网站\n4.2 轮转 user-agent 一般的爬虫都会使用浏览器的 user-agent 来模拟浏览器以欺骗服务器 (当然,如果你是一只什么 user-agent都不用耿直的小爬虫,我也无话可说).\n为了提高突破反爬虫策略的成功率,可以定义多个 user-agent, 然后每次请求都随机选择 user-agent。\n4.3 伪装成搜索引擎 要说最著名的爬虫是谁?肯定是搜索引擎,它本质上也是爬虫,而且是非常强大的爬虫。\n而且这些爬虫可以光明正大地去爬取各式网站,相信各式网站也很乐意被它爬。\n那么, 现在可以通过修改 user-agent 伪装成搜索引擎,然后再结合上面的轮转 user-agent,\n伪装成各式搜索引擎:\n\u0026#39;Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)\u0026#39;, \u0026#39;DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)\u0026#39;, \u0026#39;ia_archiver (+http://www.alexa.com/site/help/webmasters; crawler@alexa.com)\u0026#39;, 4.4 代理 IP 虽说可以伪装成搜索引擎,但是因为 http 请求是建立在三次握手之上的,爬虫的 IP 还是会被记录下来的,如果同一个 IP 访问得太频繁,那基本就可以确定是一只爬虫了,然后就把它的 IP 封掉,温和一点的就会叫你输入验证码,不然就返回 403.\n对待这种情况,就需要使用代理 IP 了。\n只是代理 IP 都有不同程度的延迟,并且免费的 IP 大多不能用,所以这是不得而为之了\n5 扩展成分布式爬虫 一台机器的爬虫可能爬取一个网站可能需要 100 天,而且带宽也到达瓶颈了,那么是否可以提高爬取效率呢?\n那就用 100台机器,1天应该就能爬取完 (当然,现实并非如此美好).\n这个就涉及到分布式的爬虫的问题。而不同的分布式爬虫有不同的实现方法,而我选择了 scrapy 和 redis 整合的 scrapy-redis 来实现分布式,URL 的去重以及调度都有了相应的实现了,也无需额外的操心\n6 爬虫监控 既然爬虫从单机变成了分布式,新的问题随之而来:如何监控分布式爬虫呢?在单机的时候,最简单的监控 \u0026ndash; 直接将爬虫的日志信息输出到终端即可。\n但是对于分布式爬虫,这样的做法显然不现实。我最终选择使用 graphite 这个监控工具。\n6.1 scrapy-graphite 参考 Github上 distributed_crawler 的代码,将单机版本的 scrapy-graphite 扩展成基于分布式的 graphite 监控程序,并且实现对 python3 的支持。\n6.2 docker 但是 graphite 只是支持 python2, 并且安装过程很麻烦,我在折腾大半天后都无法安装成功,实在有点沮丧。最后想起了伟大的 docker, 并且直接找到已经打包好的image. 数行命令即解决所有的安装问题,不得不说:docker, 你值得拥有。运行截图:\n7 爬虫拆分 本来爬取商品信息的爬虫和爬取评论的爬虫都是同一只爬虫,但是后来发现,再不使用代理 IP 的情况下,爬取到 150000 条商品信息的时候,需要输入验证码。\n但是爬取商品评论的爬虫并不存在被反爬策略限制的情况。所以我将爬虫拆分成两只爬虫,即使无法爬取商品信息的时候,还可以爬取商品的评论信息。\n8 小结 在爬取一天之后,爬虫成果:\n8.1 评论 8.2 评论总结 8.3 商品信息 商品信息加上评论数约 150+w.\n9 参考及致谢 https://github.com/noplay/scrapy-graphite https://github.com/gnemoug/distribute_crawler https://github.com/hopsoft/docker-graphite-statsd 10 项目源码 https://github.com/samrayleung/jd_spider\n","permalink":"https://ramsayleung.github.io/zh/post/2017/jd_spider/","summary":"我最近编写了两只京东商品和评论的分布式爬虫来进行数据分析,现在就来分享一下。 1 爬取策略 众所周知,爬虫比较难爬取的就是动态生成的网页,因为需要","title":"从京东\"窃取\"150+万条数据"},{"content":"1 发现帅气的提示符 近日,笔者在浏览 Reddit 的时候,发现了一位 Emacs 用户把他的 Eshell 提示符修改得很帅,如图:\n本着拿来主义的想法,我就直接把这位小哥的代码添加到了我的配置文件里面:\n(setq eshell-prompt-function (lambda () (concat (propertize \u0026#34;┌─[\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (user-login-name) \u0026#39;face `(:foreground \u0026#34;red\u0026#34;)) (propertize \u0026#34;@\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (system-name) \u0026#39;face `(:foreground \u0026#34;blue\u0026#34;)) (propertize \u0026#34;]──[\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (format-time-string \u0026#34;%H:%M\u0026#34; (current-time)) \u0026#39;face `(:foreground \u0026#34;yellow\u0026#34;)) (propertize \u0026#34;]──[\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (concat (eshell/pwd)) \u0026#39;face `(:foreground \u0026#34;white\u0026#34;)) (propertize \u0026#34;]\\n\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize \u0026#34;└─\u0026gt;\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (if (= (user-uid) 0) \u0026#34; # \u0026#34; \u0026#34; $ \u0026#34;) \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) ))) 效果自然是很 sexy.\n2 与原有提示符冲突 但是我原来使用的 eshell-prompt-extra 的效果就被覆盖了。而 eshell_prompt_extra 可以提供的额外信息非常多,包括:git, python virtualenv, 以及远程登录时的主机信息,如图:\n如果用上这个 sexy 的提示符,eshell-extra-prompt 的额外的信息就不能显示,感觉好亏:(\n鱼和熊掌我都想要,似乎太贪心了?怎么办,自己去修改 eshell_prompt_extra 的源码 :).\n3 折腾源码 eshell_prompt_extra 这个包注释加上全部代码也只是 400 行,代码也写得很清晰. 其中大部份是辅助函数,而 Eshell 的提示符效果是通过两个 eshell-theme 函数来实现的。use-package 的配置:\n(use-package eshell-prompt-extras :ensure t :load-path \u0026#34;~/Code/github/eshell-prompt-extras\u0026#34; :config (progn (with-eval-after-load \u0026#34;esh-opt\u0026#34; (use-package virtualenvwrapper :ensure t) (venv-initialize-eshell) (autoload \u0026#39;epe-theme-lambda \u0026#34;eshell-prompt-extras\u0026#34;) (setq eshell-highlight-prompt nil eshell-prompt-function \u0026#39;epe-theme-lambda)) )) 而 epe-theme-lambda 的代码如下:\n(defun epe-theme-lambda () \u0026#34;A eshell-prompt lambda theme.\u0026#34; (setq eshell-prompt-regexp \u0026#34;^[^#\\nλ]*[#λ] \u0026#34;) (concat (when (epe-remote-p) (epe-colorize-with-face (concat (epe-remote-user) \u0026#34;@\u0026#34; (epe-remote-host) \u0026#34; \u0026#34;) \u0026#39;epe-remote-face)) (when epe-show-python-info (when (fboundp \u0026#39;epe-venv-p) (when (and (epe-venv-p) venv-current-name) (epe-colorize-with-face (concat \u0026#34;(\u0026#34; venv-current-name \u0026#34;) \u0026#34;) \u0026#39;epe-venv-face)))) (let ((f (cond ((eq epe-path-style \u0026#39;fish) \u0026#39;epe-fish-path) ((eq epe-path-style \u0026#39;single) \u0026#39;epe-abbrev-dir-name) ((eq epe-path-style \u0026#39;full) \u0026#39;abbreviate-file-name)))) (epe-colorize-with-face (funcall f (eshell/pwd)) \u0026#39;epe-dir-face)) (when (epe-git-p) (concat (epe-colorize-with-face \u0026#34;:\u0026#34; \u0026#39;epe-dir-face) (epe-colorize-with-face (concat (epe-git-branch) (epe-git-dirty) (epe-git-untracked) (let ((unpushed (epe-git-unpushed-number))) (unless (= unpushed 0) (concat \u0026#34;:\u0026#34; (number-to-string unpushed))))) \u0026#39;epe-git-face))) (epe-colorize-with-face \u0026#34; λ\u0026#34; \u0026#39;epe-symbol-face) (epe-colorize-with-face (if (= (user-uid) 0) \u0026#34;#\u0026#34; \u0026#34;\u0026#34;) \u0026#39;epe-sudo-symbol-face) \u0026#34; \u0026#34;)) 代码主要逻辑是调用之前定义的辅助函数,判断是否需要显示 git, python, 远程主机等信息,然后对相应的提示符进行拼接。\n而其中出现得比较频繁的 epe-colorize-with-face 就是作者定义的一个宏(macro), 用来显示字符串以及对应的 face(其实就是不同的颜色啦). 看懂了代码就好办了,现在就可以自己添加一个 Eshell 主题。\n3.1 定义所需的 face 因为我需要显示的 face(颜色), eshell-extra-prompt 并没有定义,所以就只好自己动手啦:\n(defface epe-pipeline-delimiter-face \u0026#39;((t :foreground \u0026#34;green\u0026#34;)) \u0026#34;Face for pipeline theme delimiter.\u0026#34; :group \u0026#39;epe) (defface epe-pipeline-user-face \u0026#39;((t :foreground \u0026#34;red\u0026#34;)) \u0026#34;Face for user in pipeline theme.\u0026#34; :group \u0026#39;epe) (defface epe-pipeline-host-face \u0026#39;((t :foreground \u0026#34;blue\u0026#34;)) \u0026#34;Face for host in pipeline theme.\u0026#34; :group \u0026#39;epe) (defface epe-pipeline-time-face \u0026#39;((t :foreground \u0026#34;yellow\u0026#34;)) \u0026#34;Face for time in pipeline theme.\u0026#34; :group \u0026#39;epe) 然后就是按着原有的 Eshell 提示符来组装一个新的 Eshell 主题了,然后把这个主题定义成 pipeline (其实是我自己也没想出比较新颖的名字啦):\n(defun epe-theme-pipeline () \u0026#34;A eshell-prompt theme with full path, smiliar to oh-my-zsh theme.\u0026#34; (setq eshell-prompt-regexp \u0026#34;^[^#\\nλ]* λ[#]* \u0026#34;) (concat (if (epe-remote-p) (progn (concat (epe-colorize-with-face \u0026#34;┌─[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (epe-remote-user) \u0026#39;epe-pipeline-user-face) (epe-colorize-with-face \u0026#34;@\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (epe-remote-host) \u0026#39;epe-pipeline-host-face)) ) (progn (concat (epe-colorize-with-face \u0026#34;┌─[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (user-login-name) \u0026#39;epe-pipeline-user-face) (epe-colorize-with-face \u0026#34;@\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (system-name) \u0026#39;epe-pipeline-host-face))) ) (concat (epe-colorize-with-face \u0026#34;]──[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (format-time-string \u0026#34;%H:%M\u0026#34; (current-time)) \u0026#39;epe-pipeline-time-face) (epe-colorize-with-face \u0026#34;]──[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (concat (eshell/pwd)) \u0026#39;epe-dir-face) (epe-colorize-with-face \u0026#34;]\\n\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face \u0026#34;└─\u0026gt;\u0026#34; \u0026#39;epe-pipeline-delimiter-face) ) (when epe-show-python-info (when (fboundp \u0026#39;epe-venv-p) (when (and (epe-venv-p) venv-current-name) (epe-colorize-with-face (concat \u0026#34;(\u0026#34; venv-current-name \u0026#34;) \u0026#34;) \u0026#39;epe-venv-face)))) (when (epe-git-p) (concat (epe-colorize-with-face \u0026#34;:\u0026#34; \u0026#39;epe-dir-face) (epe-colorize-with-face (concat (epe-git-branch) (epe-git-dirty) (epe-git-untracked) (let ((unpushed (epe-git-unpushed-number))) (unless (= unpushed 0) (concat \u0026#34;:\u0026#34; (number-to-string unpushed))))) \u0026#39;epe-git-face))) (epe-colorize-with-face \u0026#34; λ\u0026#34; \u0026#39;epe-symbol-face) (epe-colorize-with-face (if (= (user-uid) 0) \u0026#34;#\u0026#34; \u0026#34;\u0026#34;) \u0026#39;epe-sudo-symbol-face) \u0026#34; \u0026#34;)) 4 总结 这样一个新的 Eshell 主题就完工了,然后我给 eshell-extra-prompt 发了一个Pull Request, 最终效果如下:\nEnjoy Emacs, Enjor Tweaking :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/tweak_eshell_prompt/","summary":"1 发现帅气的提示符 近日,笔者在浏览 Reddit 的时候,发现了一位 Emacs 用户把他的 Eshell 提示符修改得很帅,如图: 本着拿来主义的想法,我就直接把这位小哥的代码添加","title":"Eshell提示符优化"},{"content":"1 前言 几天前 Goolge 在 I/O 大会上宣布了 Android 将官方支持 Kotlin, 这意味着 Android开发者可以更好地使用 Kotlin 开发 Android.\n我虽不是 Android 开发者,但是也为 Android 开发者多了一个选择而感到高兴,略显意外的是,接下来到处可以看到 \u0026ldquo;Java已死,Kotlin 当立\u0026rdquo; 之类的言论。\n一群人围在一起诉说被 Java \u0026ldquo;折磨\u0026rdquo; 的血泪史,然后为Kotlin 的到来欢欣鼓舞。我学过挺多的语言,也并不是一个 \u0026ldquo;Java 卫道士\u0026rdquo;.\n但是看到很多人都说 \u0026ldquo;Java 的语法啰嗦,每次都要编写一大段 \u0026ldquo;Setter/Getter\u0026rdquo; 这类的模板代码,还有各种的 Bean; 真的好累\u0026rdquo; 我就觉得其实很多人都是人云亦云,他们也并没有对 Java 有多少关注。\n其实 Java8 发布以后,使用 Java8 的函数式进行编码已经可以减少很多代码了;其次,一个新颖的类库也可以帮 Java 的代码进行瘦身 \u0026ndash; Lombok\n2 Lombok 2.1 简介 很多开发者都对模板代码嗤之以鼻,但是 Java 中就有很多非常类似且改动很少的样板 代码。\n这问题一方面是由于类库的设计决定,另外一方面也是 Java 的自身语言的特性。 而 Lombok 这个项目就是希望通过注解来减少模板代码。\n就注解而言,大多是各类框架用于生成代码 (典型的就是 Spring 和 Hibernate 了),而很少直接使用注解生成的代 码。\n因为如果想要直接在程序中使用注解生成的代码,就意味着在代码进行编译之前,注解就要进行相应的处理。这似乎是没可能发生的事情: 在编译代码之前使用编译后生成的代码。\n但是 Lombok 在 IDE 的配合下就真的做到在开发的时候就插入相应的代码。\n2.2 @Getter and @Setter 百闻不如一见,还是直接看例子吧。\n对于使用过 Java 的开发者而言,我相信他们最熟悉的肯定是 Java 无处不在的封装以及对应的 Getter/Setter. 现在就来看一下如何 为最常见的模板代码瘦身。\n未使用 Lombok 的代码:\nprivate boolean employed = true; private String name; public boolean isEmployed() { return employed; } public void setEmployed(final boolean employed) { this.employed = employed; } protected void setName(final String name) { this.name = name; } 并没有上面特点,还是 \u0026ldquo;旧把式\u0026rdquo;. 那么现在来看一下使用了 Lombok 的同等作用的代 码:\n@Getter @Setter private boolean employed = true; @Setter(AccessLevel.PROTECTED) private String name; 除了必要的变量定义以及 @Getter, @Setter 注解以外,没有了其他的东西的。\n但是使用 Lombok 的代码比原生的 Java 代码少了很多行,定义的类的属性越多,减少的代码数就越可观。\n而 @Getter 和 @Setter 注解的作用就是为一个类的属性生成 getter 和 setter 方法,而这些生成的方法跟我们自己编写的代码是一样的。\n2.3 @NonNull 相信每一个使用过 Java 的开发者都不会对空指针这个异常陌生吧,因为 NullPointException 导致了各种 Bug, 以至于它的发明者 Tony Hoare 都自嘲到他创 造了价值十亿的错误 (\u0026ldquo;Null Reference: The Billion Dollar Mistake\u0026rdquo;).\n因此在Java 的代码中,出于安全性的考虑,对于可能出现空指针的地方,都需要进行空指针 检查,自然无可避免地产生了很多的模板代码。\n而 Lombok 引入的 @NonNull 注解可以让需要进行空指针检查的代码 fast-fail; 这样就无需显示添加空指针检查了。\n当为类 的属性添加了 @NonNull 注解以后,在对应的 setter 函数,Lombok 也会生成对应的 空指针检查。例子:使用 Lombok 对 Family 类添加注解:\n@Getter @Setter @NonNull private List\u0026lt;Person\u0026gt; members; 对应的相同作用的原生代码:\n@NonNull private List\u0026lt;Person\u0026gt; members; public Family(@NonNull final List\u0026lt;Person\u0026gt; members) { if (members == null) throw new java.lang.NullPointerException(\u0026#34;members\u0026#34;); this.members = members; } @NonNull public List\u0026lt;Person\u0026gt; getMembers() { return members; } public void setMembers(@NonNull final List\u0026lt;Person\u0026gt; members) { if (members == null) throw new java.lang.NullPointerException(\u0026#34;members\u0026#34;); this.members = members; } 不得不感慨,使用 Lombok, 敲击键盘的次数都成指数级下降.\n2.4 @EqualsAndHashCode 因为 Java 的 Object 类存在用于比较的 equals() 以及对应的 hashCode() 方法,而很多类都经常需要重写这两个方法来实现比较操作。\n比较的操作大多是逐一比较子类的属性,而计算 hash 值的函数也基本是逐一取各个属性的 hash 值,然后与固定值相乘在相加. 这样的操作并不需要复杂算法,完成的都是重复性的 \u0026ldquo;体力活\u0026rdquo;.\n幸运的是, Lombok 也提供了相应的注解来减少这些模板代码。类级别的 @EqualsAndHashCode 注解可以为指定的属性生成 equals() 方法和 hashCode() 方法。\n默认情况下,所有非静态或者没被标注成 transient 的属性都会被 equals() 和 hashCode() 方 法包含在内。当然,你也可以使用 exclude 声明不需要被包含的属性。\n例子:使用了 @EqualAndHashCode 注解的代码\n@EqualsAndHashCode(callSuper=true,exclude={\u0026#34;address\u0026#34;,\u0026#34;city\u0026#34;,\u0026#34;state\u0026#34;,\u0026#34;zip\u0026#34;}) public class Person extends SentientBeing { enum Gender { Male, Female } @NonNull private String name; @NonNull private Gender gender; private String ssn; private String address; private String city; private String state; private String zip; } 对应的相同作用的原生代码:\npublic class Person extends SentientBeing { enum Gender { /*public static final*/ Male /* = new Gender() */, /*public static final*/ Female /* = new Gender() */; } @NonNull private String name; @NonNull private Gender gender; private String ssn; private String address; private String city; private String state; private String zip; @java.lang.Override public boolean equals(final java.lang.Object o) { if (o == this) return true; if (o == null) return false; if (o.getClass() != this.getClass()) return false; if (!super.equals(o)) return false; final Person other = (Person)o; if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false; if (this.gender == null ? other.gender != null : !this.gender.equals(other.gender)) return false; if (this.ssn == null ? other.ssn != null : !this.ssn.equals(other.ssn)) return false; return true; } @java.lang.Override public int hashCode() { final int PRIME = 31; int result = 1; result = result * PRIME + super.hashCode(); result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); result = result * PRIME + (this.gender == null ? 0 : this.gender.hashCode()); result = result * PRIME + (this.ssn == null ? 0 : this.ssn.hashCode()); return result; } } 模板代码和使用了 Lombok 的代码简洁程度而言,差距越来越大了\n2.5 @Data 下面我就来介绍一下在我项目中使用最频繁的注解 @Data.\n使用 @Data 相当于同时在类级别使用 @EqualAndHashCode 注解以及我未曾提及的 @ToString 注解 (这个应该可以从注解名字猜出注解的作用), 以及为每一个类的属性添加上 @Setter 和 @Getter 注解。\n在一个类使用 @Data, Lombok 还会为该类生成构造函数。\n例子:使用 了 @Data 注解的函数:\n@Data(staticConstructor=\u0026#34;of\u0026#34;) public class Company { private final Person founder; private String name; private List\u0026lt;Person\u0026gt; employees; } 同等作用的原生 Java 代码:\npublic class Company { private final Person founder; private String name; private List\u0026lt;Person\u0026gt; employees; private Company(final Person founder) { this.founder = founder; } public static Company of(final Person founder) { return new Company(founder); } public Person getFounder() { return founder; } public String getName() { return name; } public void setName(final String name) { this.name = name; } public List\u0026lt;Person\u0026gt; getEmployees() { return employees; } public void setEmployees(final List\u0026lt;Person\u0026gt; employees) { this.employees = employees; } @java.lang.Override public boolean equals(final java.lang.Object o) { if (o == this) { return true; } if (o == null) { return false; } if (o.getClass() != this.getClass()) { return false; } final Company other = (Company) o; if (this.founder == null ? other.founder != null : !this.founder.equals(other.founder)) { return false; } if (this.name == null ? other.name != null : !this.name.equals(other.name)) { return false; } if (this.employees == null ? other.employees != null : !this.employees.equals(other.employees)) { return false; } return true; } @java.lang.Override public int hashCode() { final int PRIME = 31; int result = 1; result = result * PRIME + (this.founder == null ? 0 : this.founder.hashCode()); result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode()); return result; } @java.lang.Override public java.lang.String toString() { return \u0026#34;Company(founder=\u0026#34; + founder + \u0026#34;, name=\u0026#34; + name + \u0026#34;,\u0026#34; + \u0026#34; employees=\u0026#34; + employees + \u0026#34;)\u0026#34;; } } 差别更加显而易见了\n3 小结 我在日常的开发中,使用的开发语言主要是 Java, 我也学习过其他的语言,所以 Java 和其他语言相比的优缺点也了然于心。\nJava 绝佳的工程性,优秀的 OOP 范式,以及大量的类库,框架 (例如 Spring \u0026ldquo;全家桶\u0026rdquo;), 以及 JIT 带来的接近 C++ 的性能,但是 Java 语法实在啰嗦,需要编写很多的模板代码,以至于经常出现将小项目写成中项目,中项目写成大项目的烦恼,更被戏称为 \u0026ldquo;搬砖\u0026rdquo;.\n现在看来,Lombok 为 Java 减少的模板代码实在算是造福 Java 开发者,让开发者在获得 Java 优势的时候,还可以尽量少地打字,可谓是来的及时。\n4 参考 https://projectlombok.org/index.html ","permalink":"https://ramsayleung.github.io/zh/post/2017/lombok/","summary":"1 前言 几天前 Goolge 在 I/O 大会上宣布了 Android 将官方支持 Kotlin, 这意味着 Android开发者可以更好地使用 Kotlin 开发 Android. 我虽不是 Android 开发者,但是也为 Android 开发者多了一个选择","title":"为Java瘦身 – Lombok"},{"content":"最近我需要为运行的分布式系统某部分模块构造系统唯一的ID, 而 ID 需要是数字的形式,并应该尽量的短。不得不说,这是一个有趣的问题\n1 若干实现策略 查阅完相关的资料,发现为分布式系统生成唯一 ID 方法挺多的,例如:\nUUID 使用一个 ticket server, 即中央的服务器,各个节点都从中央服务器取 ID Twitter 的 Snowflake 算法 Boundary 的 flake 算法 其中 UUID 生成的 ID 是字符串+数字,不适用; ticket server 的做法略麻烦,笔者 并不想为了个 ID 还要去访问中央服务器;剩下就是 Snowflake 和 flake 算法, flake 算法生成的是 128 位的 ID, 略长;所以最后笔者选择了 Snowflake 算法。\n2 Snowflake 算法实现 本来 Twitter 的算法是有相应实现的,不过后来删除了;笔者就只好自己卷起袖子自己 实现了:(\n虽说 Twitter 没有了相应的实现,但是 Snowflake 算法原理很简单,实现起来并不难.\n2.1 Snowflake 算法 Snowflake 算法生成 64 位的 ID, ID 的格式是 41 位的时间戳 + 10 位的截断的 mac 地址 + 12 位递增序列:\n\u0026#34;\u0026#34;\u0026#34;id format =\u0026gt; timestamp | machineId|sequence 41 | 10 |12 \u0026#34;\u0026#34;\u0026#34; 2.2 Snowflake 实现 2.2.1 生成时间戳 Java 内置了生成精确到毫秒的时间戳的方法,非常便利:\nlong timestamp = System.currentTimeMillis(); 2.2.2 生成递增序列 12 bits 的最大值是 2**12=4096, 所以生成递增序列也非常简单:\nprivate final long sequenceMax = 4096; //2**12 private volatile long sequence = 0L; public void generateId(){ //do something sequence = (sequence + 1) % sequenceMax; //do something } 2.2.3 获取 Mac 地址 我们通过获取当前机器的 IP 地址以获取对应的物理地址:\nprotected long getMachineId() throws GetHardwareIdFailedException { try { InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); long id; if (network == null) { id = 1; } else { byte[] mac = network.getHardwareAddress(); id = ((0x000000FF \u0026amp; (long) mac[mac.length - 1]) | (0x0000FF00 \u0026amp; (((long) mac[mac.length - 2]) \u0026lt;\u0026lt; 8))) \u0026gt;\u0026gt; 6; } return id; } catch (SocketException e) { throw new GetHardwareIdFailedException(e); } catch (UnknownHostException e) { throw new GetHardwareIdFailedException(e); } } 又因为 Mac 地址是6 个字节 (48 bits),而需要的只是 10 bit, 所以需要取最低位的 2个字节 (16 bits),然后右移 6 bits 以获取 10 个 bits 的 Mac地址\n2.3 Snowflake 完整代码 下面是 Snowflake 的 Java 实现:\npublic class IdGenerator { // id format =\u0026gt; // timestamp |datacenter | sequence // 41 |10 | 12 private final long sequenceBits = 12; private final long machineIdBits = 10L; private final long MaxMachineId = -1L ^ (-1L \u0026lt;\u0026lt; machineIdBits); private final long machineIdShift = sequenceBits; private final long timestampLeftShift = sequenceBits + machineIdBits; private static final Object lock=new Object(); private final long twepoch = 1288834974657L; private final long machineId; private final long sequenceMax = 4096; //2**12 private volatile long lastTimestamp = -1L; private volatile long sequence = 0L; private static volatile IdGenerator instance; public static IdGenerator getInstance() throws Exception { IdGenerator generator=instance; if (instance == null) { synchronized(lock){ generator=instance; if(generator==null){ generator=new IdGenerator(); instance=generator; } } } return generator; } private IdGenerator() throws Exception { machineId = getMachineId(); if (machineId \u0026gt; MaxMachineId || machineId \u0026lt; 0) { throw new Exception(\u0026#34;machineId \u0026gt; MaxMachineId\u0026#34;); } } public synchronized Long generateLongId() throws Exception { long timestamp = System.currentTimeMillis(); if (timestamp \u0026lt; lastTimestamp) { throw new Exception( \u0026#34;Clock moved backwards. Refusing to generate id for \u0026#34; + ( lastTimestamp - timestamp) + \u0026#34; milliseconds.\u0026#34;); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) % sequenceMax; if (sequence == 0) { timestamp = tillNextMillis(lastTimestamp); } } else { sequence = 0; } lastTimestamp = timestamp; Long id = ((timestamp - twepoch) \u0026lt;\u0026lt; timestampLeftShift) | (machineId \u0026lt;\u0026lt; machineIdShift) | sequence; return id; } protected long tillNextMillis(long lastTimestamp) { long timestamp = System.currentTimeMillis(); while (timestamp \u0026lt;= lastTimestamp) { timestamp = System.currentTimeMillis(); } return timestamp; } protected long getMachineId() throws GetHardwareIdFailedException { try { InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); long id; if (network == null) { id = 1; } else { byte[] mac = network.getHardwareAddress(); id = ((0x000000FF \u0026amp; (long) mac[mac.length - 1]) | (0x0000FF00 \u0026amp; (((long) mac[mac.length - 2]) \u0026lt;\u0026lt; 8))) \u0026gt;\u0026gt; 6; } return id; } catch (SocketException e) { throw new GetHardwareIdFailedException(e); } catch (UnknownHostException e) { throw new GetHardwareIdFailedException(e); } } } 正如笔者所言,算法并不难,就是分别获取时间戳, mac 地址,和递增序列号,然后移位得到 ID. 但是在具体的实现中还是有一些需要注意的细节的。\n2.3.1 线程同步 因为算法中使用到递增的序列号来生成 ID,而在实际的开发或者生产环境中很可能不止一个线程在使用 IdGenerator 这个类,如果这样就很容易出现不同线程的竞争问题,所以我使用了单例模式来生成 ID, 一方面更符合生成器的设计,另一方面因为对生成 ID的方法进行了同步,就保证了不会出现竞争问题。\n2.3.2 同一毫秒生成多个 ID 因为序列号长度是 12个 bit, 那么序列号最大值就是2**12=4096了,此外时间戳是精确到毫秒的,这就是意味着,当一毫秒内,产生超过 4096 个 ID 的时候就会出现重复的ID.\n这样的情况并不是不可能发生,所以要对此进行处理;所以在 generateId() 函数中:\nif (lastTimestamp == timestamp) { sequence = (sequence + 1) % sequenceMax; if (sequence == 0) { timestamp = tillNextMillis(lastTimestamp); } } 有以上的一段代码。当现在的时间戳与之前的时间戳一致,那么就意味着还是同一毫秒,如果序列号为 0, 就说明已经产生了 4096 个 ID了,继续产生 ID,就会出现重复 ID, 所以要等待一毫秒,这个就是 tillNextMills() 函数的作用了。\n3 小结 算法虽然简单,但是在找到 Snowflake 算法之前,笔者尝试了挺多的算法,但是都是因为不符合要求而被一一否决, 而 Snowflake 算法虽然简单,但是胜在实用。最后附上我写的 snowflake 算法的 Python 实现: Snowfloke\n","permalink":"https://ramsayleung.github.io/zh/post/2017/distributed_system_unique_id/","summary":"最近我需要为运行的分布式系统某部分模块构造系统唯一的ID, 而 ID 需要是数字的形式,并应该尽量的短。不得不说,这是一个有趣的问题 1 若干实现策略 查","title":"关于分布式系统唯一ID的探究"},{"content":"笔者近来闲来无事,又因为有需要构造全局唯一 ID 的需求,所以就去看了 UUID 这个提供稳定的系统唯一标识符的类的源码\n1 UUID variant 事实上是存在很多中 UID 的不同实现的的,但是 UUID 里面默认是使用 \u0026ldquo;加盐\u0026rdquo;(Leach-Salz)实现,但是也可以使用其他的实现。\n2 Layout of variant2(Leach-Salz) UUID 加盐的 UUID 的结构布局如下:最高位 (most significant) 的64 位长整型值由下面的的无符号位组成:\n0xFFFFFFFF00000000 time_low //时间的低位值 0x00000000FFFF0000 time_mid //时间的中位值 0x000000000000F000 version // 说明 UUID 的类型,1,2,3,4 分别代表 基于时间,基于 DEC,基于命名,和随机产生的 UUID 0x0000000000000FFF time_hi //时间的高位值 最低位 (least significant) 的 64 位长整型由以下的无符号位组成:\n0xC000000000000000 variant //说明UUID 的结构布局,并且只有在类型 2 (加盐类型), 结构布局才有效 0x3FFF000000000000 clock_seq 0x0000FFFFFFFFFFFF node 3 UUID constructor UUID 类有两个构造函数,分别是 public 和 private 修饰的构造函数\n3.1 private UUID private 类型的构造函数以一个 byte 数组为构造参数:\n/* * Private constructor which uses a byte array to construct the new UUID. */ private UUID(byte[] data) { long msb = 0; long lsb = 0; assert data.length == 16 : \u0026#34;data must be 16 bytes in length\u0026#34;; for (int i=0; i\u0026lt;8; i++) msb = (msb \u0026lt;\u0026lt; 8) | (data[i] \u0026amp; 0xff); for (int i=8; i\u0026lt;16; i++) lsb = (lsb \u0026lt;\u0026lt; 8) | (data[i] \u0026amp; 0xff); this.mostSigBits = msb; this.leastSigBits = lsb; } private 构造器完成的工作主要是通过左移位,与运算和或运算对 mostSigBit 和 leastSigBit 赋值。 private的构造函数只能在类本身被调用, 该构造器的用法会在接下来阐述。\n3.2 public UUID public 类型的构造器接受两个 long 类型的参数,即上面提到的最高位和最低位:\n/** * Constructs a new {@code UUID} using the specified data. {@code * mostSigBits} is used for the most significant 64 bits of the {@code * UUID} and {@code leastSigBits} becomes the least significant 64 bits of * the {@code UUID}. * * @param mostSigBits * The most significant bits of the {@code UUID} * * @param leastSigBits * The least significant bits of the {@code UUID} */ public UUID(long mostSigBits, long leastSigBits) { this.mostSigBits = mostSigBits; this.leastSigBits = leastSigBits; } 使用最高位和最低位的值来构造 UUID, 而最高位和最低位的赋值是在 private 的构造器里面完成的。\n4 UUID type 4.1 type 4 \u0026ndash; randomly generated UUID 现在就看看使用频率最高的 UUID 类型 \u0026ndash; 随机的 UUID 以及随机生成 UUID 的函数: randomUUID()\n/** * Static factory to retrieve a type 4 (pseudo randomly generated) UUID. * * The {@code UUID} is generated using a cryptographically strong pseudo * random number generator. * * @return A randomly generated {@code UUID} */ public static UUID randomUUID() { SecureRandom ng = Holder.numberGenerator; byte[] randomBytes = new byte[16]; ng.nextBytes(randomBytes); randomBytes[6] \u0026amp;= 0x0f; /* clear version */ randomBytes[6] |= 0x40; /* set to version 4 */ randomBytes[8] \u0026amp;= 0x3f; /* clear variant */ randomBytes[8] |= 0x80; /* set to IETF variant */ return new UUID(randomBytes); } 关于调用到的 Holder 变量的定义:\n/* * The random number generator used by this class to create random * based UUIDs. In a holder class to defer initialization until needed. */ private static class Holder { static final SecureRandom numberGenerator = new SecureRandom(); } 上面用到 java.security.SecureRandom 类来生成字节数组, SecureRandom 是被认为是达到了加密强度 (cryptographically strong) 并且因为不同的 JVM 而有不同的实现的。所以可以保证产生足够 \u0026ldquo;随机\u0026quot;的随机数以保证 UUID 的唯一性。\n然后在即将用来构造的 UUID 的字节数组重置和添加关于 UUID 的相关信息,例如版本,类型信息等,然后把处理好的字节数组传到 private 的构造器以构造 UUID。这里的randomUUID 静态方法就是通过静态工厂的方式构造 UUID.\n4.2 type 3 \u0026ndash; name-based UUID 在上面关于 UUID 结构布局的时候提到,UUID 有四种类型的实现,而类型3 就是基于命名的实现:\n/** * Static factory to retrieve a type 3 (name based) {@code UUID} based on * the specified byte array. * * @param name * A byte array to be used to construct a {@code UUID} * * @return A {@code UUID} generated from the specified array */ public static UUID nameUUIDFromBytes(byte[] name) { MessageDigest md; try { md = MessageDigest.getInstance(\u0026#34;MD5\u0026#34;); } catch (NoSuchAlgorithmException nsae) { throw new InternalError(\u0026#34;MD5 not supported\u0026#34;, nsae); } byte[] md5Bytes = md.digest(name); md5Bytes[6] \u0026amp;= 0x0f; /* clear version */ md5Bytes[6] |= 0x30; /* set to version 3 */ md5Bytes[8] \u0026amp;= 0x3f; /* clear variant */ md5Bytes[8] |= 0x80; /* set to IETF variant */ return new UUID(md5Bytes); } MessageDigest 是 JDK 提供用来计算散列值的类,使用的散列算法包括Sha-1,Sha-256 或者是 MD5 等等。\nnameUUIDFromBytes 使用 MD5 算法计算传进来的参数 name 的散列值,然后在散列值重置,添加 UUID 信息,然后再使用生成的散列值 (字节数组)传递给 private 构造器以构造 UUID.\n这里的 nameUUIDFromBytes 静态方法也是通过静态工厂的方式构造 UUID.\n4.3 type 2 \u0026ndash; DEC security 在 JDK 的 UUID 类中并未提供 基于 DEC 类型的 UUID 的实现。\n4.4 type 1 \u0026ndash; time-based UUID 与基于命名和随机生成的 UUID 都有一个静态工厂方法不一样, 基于时间的 UUID 并不存在静态工厂方法,time-based UUID 是基于一系列相关的方法的:\n4.4.1 timestamp public long timestamp() { if (version() != 1) { throw new UnsupportedOperationException(\u0026#34;Not a time-based UUID\u0026#34;); } return (mostSigBits \u0026amp; 0x0FFFL) \u0026lt;\u0026lt; 48 | ((mostSigBits \u0026gt;\u0026gt; 16) \u0026amp; 0x0FFFFL) \u0026lt;\u0026lt; 32 | mostSigBits \u0026gt;\u0026gt;\u0026gt; 32; } 60 个bit长的时间戳是由上面提到的 time_low time_mid time_hi 构造而成的。\n而时间的计算是从 UTC 时间的 1582 年 10月 15 的凌晨开始算起,结果的值域在 100-nanosecond 之间。\n但是这个时间戳的值只是对基于时间的 UUID 有效的,对于其他类型的 UUID, timestamp() 方法会抛出UnsuportedOperationException异常。\n4.4.2 clockSequence() public int clockSequence() { if (version() != 1) { throw new UnsupportedOperationException(\u0026#34;Not a time-based UUID\u0026#34;); } return (int)((leastSigBits \u0026amp; 0x3FFF000000000000L) \u0026gt;\u0026gt;\u0026gt; 48); } 14 个 bit 长的时钟序列值是从 该UUID 的时钟序列域构造出来的(clock sequence filed).\n而时钟序列域通常是用来保证基于时间的 UUID 的唯一性。跟 timestamp() 函数一样, clockSequence() 函数也只对基于时间的 UUID 有效。 对于其他类型的 UUID, 它会抛出UnsuportedOperationException异常。\n4.4.3 node() 48 个 bit 长的节点值是从该 UUID 的节点域 (node filed) 构造出来的。节点域通过保存运行 JVM 机器的局域网地址 (IEEE 802) 来保证该机器生成 UUID 的空间唯一性。\n和上述方法一样, node() 方法只对基于时间的 UUID 有效,对于其他类型的 UUID 该方法会抛出UnsuportedOperationException异常。\n对应 field 的图示\n0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time_low | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time_mid | time_hi_and_version | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |clk_seq_hi_res | clk_seq_low | node (0-1) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | node (2-5) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 5 FromString()/ToString() 5.1 toString() 以字符串的形式表示 UUID, 格式说明:\nhexDigit = \u0026#34;0\u0026#34; | \u0026#34;1\u0026#34; | \u0026#34;2\u0026#34; | \u0026#34;3\u0026#34; | \u0026#34;4\u0026#34; | \u0026#34;5\u0026#34; | \u0026#34;6\u0026#34; | \u0026#34;7\u0026#34; | \u0026#34;8\u0026#34; | \u0026#34;9\u0026#34; | \u0026#34;a\u0026#34; | \u0026#34;b\u0026#34; | \u0026#34;c\u0026#34; | \u0026#34;d\u0026#34; | \u0026#34;e\u0026#34; | \u0026#34;f\u0026#34; | \u0026#34;A\u0026#34; | \u0026#34;B\u0026#34; | \u0026#34;C\u0026#34; | \u0026#34;D\u0026#34; | \u0026#34;E\u0026#34; | \u0026#34;F\u0026#34; hexOctet = \u0026lt;hexDigit\u0026gt;\u0026lt;hexDigit\u0026gt; time_low = 4*\u0026lt;hexOctet\u0026gt; time_mid = 2*\u0026lt;hexOctet\u0026gt; time_high_and_version = 2*\u0026lt;hexOctet\u0026gt; variant_and_sequence = 2*\u0026lt;hexOctet\u0026gt; node = 6*\u0026lt;hexOctet\u0026gt; UUID = \u0026lt;time_low\u0026gt; \u0026#34;-\u0026#34; \u0026lt;time_mid\u0026gt; \u0026#34;-\u0026#34; \u0026lt;time_high_and_version\u0026gt; \u0026#34;-\u0026#34; \u0026#34;variant_and_sequence\u0026#34; \u0026#34;-\u0026#34; \u0026lt;node\u0026gt; 而关于这些不同 field 的大小,之前的内容已经有图示,需要的可以去回顾。\n/** Returns val represented by the specified number of hex digits. */ private static String digits(long val, int digits) { long hi = 1L \u0026lt;\u0026lt; (digits * 4); return Long.toHexString(hi | (val \u0026amp; (hi - 1))).substring(1); } public String toString() { return (digits(mostSigBits \u0026gt;\u0026gt; 32, 8) + \u0026#34;-\u0026#34; + digits(mostSigBits \u0026gt;\u0026gt; 16, 4) + \u0026#34;-\u0026#34; + digits(mostSigBits, 4) + \u0026#34;-\u0026#34; + digits(leastSigBits \u0026gt;\u0026gt; 48, 4) + \u0026#34;-\u0026#34; + digits(leastSigBits, 12)); } 5.2 fromString() 与 toString() 函数功能相反, fromString() 函数的作用就是将字符串形式的对象解码成 UUID 对象:\npublic static UUID fromString(String name) { String[] components = name.split(\u0026#34;-\u0026#34;); if (components.length != 5) throw new IllegalArgumentException(\u0026#34;Invalid UUID string: \u0026#34;+name); for (int i=0; i\u0026lt;5; i++) components[i] = \u0026#34;0x\u0026#34;+components[i]; long mostSigBits = Long.decode(components[0]).longValue(); mostSigBits \u0026lt;\u0026lt;= 16; mostSigBits |= Long.decode(components[1]).longValue(); mostSigBits \u0026lt;\u0026lt;= 16; mostSigBits |= Long.decode(components[2]).longValue(); long leastSigBits = Long.decode(components[3]).longValue(); leastSigBits \u0026lt;\u0026lt;= 48; leastSigBits |= Long.decode(components[4]).longValue(); return new UUID(mostSigBits, leastSigBits); } 6 使用场景 UUID 一般用来生成全局唯一标识符,那么 UUID 是否能保证唯一呢?以UUID.randomUUID() 生成的 UUID 为例,从上面的源码,除了 version 和 variant是固定值之外,另外的 14 byte 都是足够随机的.\n如果你生成的是 128 bit 长的 UUID 的话,理论上是 2的14x8=114次方才会有一次重复。这是个什么概念的呢? 即你每秒能 生成 10 亿个 UUID, 在100年以后,你就有 50%的可能性产生一个重复的 UUID了,是不是很开心呢?\n即使你使用 UUID.randomUUID.getLeastSignificant() 生成长整型的ID, 你理论上需要生成 2的56次方个 ID 后才会产生一个重复的 ID, 所以你可以放心地使用 UUID 了 :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/uuid/","summary":"笔者近来闲来无事,又因为有需要构造全局唯一 ID 的需求,所以就去看了 UUID 这个提供稳定的系统唯一标识符的类的源码 1 UUID variant 事实上是存在很多中 UID 的不同实现","title":"Java UUID 源码剖析"},{"content":"分享一下平时工作生活中编写的一些脚本片段(一直更新). 适用于 OS X 和 Linux\n1 准备工作 因为我比较多的脚本都是基于 percol 这个神器,所以需要先安装 percol, 如果 不了解 percol 的话,可以翻看一下我之前的文章 Linux/Unix Shell 二三事之神器percol .\n我一般将写好的函数 source 命令添加到 Shell. 例如脚本函数都在一个叫tool_function.sh 的文件里面,而我使用 Zsh, 则只需要在 .zshrc 添加一句语句:\nsource /path/to/tool_function.sh 如果使用 Bash, 添加到 .bashrc 即可。\n2 有趣的脚本 2.1 SSH 免密码登录 SSH 基本就是登录远程服务器的标配了,只是每次登录服务器都要输入密码,未免太麻烦了(好吧,我拥有懒惰这个美德),所以我决定配置 SSH 的免密码登录。代码如下:\nfunction config_ssh_login_key(){ if [ $# -lt 3 ];then echo \u0026#34;Usage: $(basename $0) -u user -h hostname -p port\u0026#34; kill -INT $$ fi #if public/private key doesn\u0026#39;t exist ,generate public/private key if [ -f ~/.ssh/id_rsa ];then echo \u0026#34;public/private key exists\u0026#34; else ssh-keygen -t rsa fi while getopts :u:h:p: option do case \u0026#34;$option\u0026#34; in u) user=$OPTARG;; h) hostname=$OPTARG;; p) port=$OPTARG;; *) echo \u0026#34;Unknown option:$option\u0026#34;;; esac done if [ -z \u0026#34;$port\u0026#34; ];then port=22 fi #check whether it is the first time to run this script and whether authorized_keys exists # ssh_host_and_user=\u0026#34;$1@$2\u0026#34; authorized_keys=\u0026#34;$HOME/.ssh/authorized_keys\u0026#34; read -r -s -p \u0026#34;$user@$hostname\u0026#39;s password:\u0026#34; password if sshpass -pv $password ssh -p \u0026#34;$port\u0026#34; \u0026#34;$user@$hostname\u0026#34; test -e \u0026#34;$authorized_keys\u0026#34;;then echo \u0026#34;authorized key exists\u0026#34; kill -INT $$ else sshpass -p $password ssh $user@$hostname -p $port \u0026#34;mkdir -p ~/.ssh;chmod 0700 .ssh\u0026#34; sshpass -p $password scp -P $port ~/.ssh/id_rsa.pub $user@$hostname:~/.ssh/authorized_keys # ssh-copy-id \u0026#34;$user@$hostname -p $port\u0026#34; fi } 基本做法就是生成一对公私密钥,然后把公钥发送到服务器。而脚本其他的部分就是判断密钥是否存在,修改密钥权限等工作。用法也很简单,假如你把以上脚本保存到了一个叫 config_ssh_login_key.sh 的文件:\nbash config_ssh_login_key.sh -h your-server-ip -u user -p 2222 当然,如果你按照我的前面提到的做法,用 source 命令引入脚本,你可以直接在命令行输入:\nconfig_ssh_login_key -u root -h your-server-ip 如果端口未指定,默认端口为 22\n2.2 生成若干位密钥 生成若干位的密钥是常见的需求,得益于 Linux/Unix 命令行强大的过滤器,所以只需把命令整理成脚本即可:\n# generate key function gkey(){ if [ -n \u0026#34;$1\u0026#34; ];then local length=\u0026#34;$1\u0026#34; else local length=32 fi OS_NAME=$(uname) if [ $OS_NAME = \u0026#34;Darwin\u0026#34; ]; then LC_CTYPE=C cat /dev/urandom |tr -cd \u0026#34;[:alnum:]\u0026#34;|head -c \u0026#34;$length\u0026#34;;echo else cat /dev/urandom |tr -cd \u0026#34;[:alnum:]\u0026#34;|head -c \u0026#34;$length\u0026#34;;echo fi } 用法:\ngkey 45 即生成一个45位字符的随机密钥,如果没有指定长度的话,默认是 32 位。因为 OS X和 Linux 的 tr 使用有差异,所以要处理一下\n2.3 复制命令行输出 有时可能需要复制某个命令的输出,一般的做法都是运行某个命令,用鼠标选中,然后复制。例如在生成密钥之后,需要复制到项目的配置文件。但是每次都要用鼠标,效率实在不高。这个功能其实可以脚本实现:\nOS_NAME=$(uname) function pclip() { if [ $OS_NAME = \u0026#34;CYGWIN\u0026#34; ]; then putclip \u0026#34;$@\u0026#34;; elif [ $OS_NAME = \u0026#34;Darwin\u0026#34; ]; then pbcopy \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xsel ]; then xsel -ib \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xclip ]; then xclip -selection c \u0026#34;$@\u0026#34;; else echo \u0026#34;Neither xsel or xclip is installed!\u0026#34; fi fi fi } 备注:这个脚本不是我原创,取自 陈斌 博客。\n在 Linux 运行这脚本需要先安装 xsel 或者是 xclip 命令。结合生成密钥的命令使用:\ngkey -28|pclip 这样,生成的密钥就被复制到系统上了。\n2.4 复制当前目录 有时候,我需要复制当前目录下某个文件的路径,但是无论是文件管理器,还是在Shell 中都要用鼠标选中然后复制指定文件的路径,效率不高且很不方便。所以我通过结合 percol 和上面提高的 pclip 函数改进了做法:\nfunction pwdf() { local current_dir=`pwd` local copied_file=`find $current_dir -type f -print |percol` echo -n $copied_file |pclip; } 只需在 Shell 中输入 pwdf, 然后选择需要复制的路径即可。 运行截图:\n\u0026lt;2017-05-22 Mon\u0026gt; Update\n2.5 判断 Unix 系统的版本 因为我经常需要在不同的 Unix 机器之间切换,例如工作用的 Mac OS X, 另外一台笔记本上的 Fedora, 还有一台工作站上的 Arch Linux, 以及各种发行版本的 VPS 等,在不同的发行版本或者系统之间切换,我希望我常用的工具也可以很轻易地移植到不同的发行版本上。\n但是不同的发行版本使用不同的包安装管理器,例如 OS X 上的 brew, Fedora 的 dnf, Centos 的 yum, Ubuntu 上的 apt-get 等等。如果可以通过使用脚本来实现根据不同的发行版本使用不同的包安装管理器安装软件,这样就省心很多。\n# GetOSVersion function GetOSVersion { # Figure out which vendor we are if [[ -x \u0026#34;`which sw_vers 2\u0026gt;/dev/null`\u0026#34; ]]; then # OS/X os_VENDOR=`sw_vers -productName` elif [[ -x $(which lsb_release 2\u0026gt;/dev/null) ]]; then os_VENDOR=$(lsb_release -i -s) if [[ \u0026#34;Debian,Ubuntu,LinuxMint\u0026#34; =~ $os_VENDOR ]]; then os_PACKAGE=\u0026#34;deb\u0026#34; elif [[ \u0026#34;SUSE LINUX\u0026#34; =~ $os_VENDOR ]]; then lsb_release -d -s | grep -q openSUSE if [[ $? -eq 0 ]]; then os_VENDOR=\u0026#34;openSUSE\u0026#34; fi elif [[ $os_VENDOR == \u0026#34;openSUSE project\u0026#34; ]]; then os_VENDOR=\u0026#34;openSUSE\u0026#34; elif [[ $os_VENDOR =~ Red.*Hat ]]; then os_VENDOR=\u0026#34;Red Hat\u0026#34; fi os_CODENAME=$(lsb_release -c -s) elif [[ -r /etc/redhat-release ]]; then # Red Hat Enterprise Linux Server release 5.5 (Tikanga) # Red Hat Enterprise Linux Server release 7.0 Beta (Maipo) # CentOS release 5.5 (Final) # CentOS Linux release 6.0 (Final) # Fedora release 16 (Verne) # XenServer release 6.2.0-70446c (xenenterprise) # Oracle Linux release 7 os_CODENAME=\u0026#34;\u0026#34; for r in \u0026#34;Red Hat\u0026#34; CentOS Fedora XenServer; do os_VENDOR=$r done if [ \u0026#34;$os_VENDOR\u0026#34; = \u0026#34;Red Hat\u0026#34; ] \u0026amp;\u0026amp; [[ -r /etc/oracle-release ]]; then os_VENDOR=OracleLinux fi elif [[ -r /etc/SuSE-release ]]; then for r in openSUSE \u0026#34;SUSE Linux\u0026#34;; do if [[ \u0026#34;$r\u0026#34; = \u0026#34;SUSE Linux\u0026#34; ]]; then os_VENDOR=\u0026#34;SUSE LINUX\u0026#34; else os_VENDOR=$r fi os_VENDOR=\u0026#34;\u0026#34; done # If lsb_release is not installed, we should be able to detect Debian OS elif [[ -f /etc/debian_version ]] \u0026amp;\u0026amp; [[ $(cat /proc/version) =~ \u0026#34;Debian\u0026#34; ]]; then os_VENDOR=\u0026#34;Debian\u0026#34; fi export os_VENDOR } 2.6 根据不同的发行版本安装软件 刚刚上面的脚本是为了准确判断出所有的 *nix 系统的,但是方便起见,也可以直接使用uname 命令\nif [ \u0026#34;$(uname)\u0026#34; == \u0026#34;Darwin\u0026#34; ]; then # Do something under Mac OS X platform echo \u0026#34;This is mac os\u0026#34; # check if brew exists type brew\u0026gt;/dev/null 2\u0026gt;\u0026amp;1 || { echo \u0026gt;\u0026amp;2 \u0026#34; require brew but it\u0026#39;s not installed. Aborting.\u0026#34;; exit 1; } echo \u0026#34;install htop\u0026#34; brew install htop echo \u0026#34;install ag\u0026#34; brew install ag echo \u0026#34;install httpie\u0026#34; brew install httpie echo \u0026#34;install fasd\u0026#34; brew install fasd echo \u0026#34;install tree\u0026#34; brew install tree echo \u0026#34;install shellcheck\u0026#34; brew install shellcheck echo \u0026#34;install guile\u0026#34; brew install guile echo \u0026#34;install proxychains-ng\u0026#34; brew install proxychains-ng echo \u0026#34;install pandoc\u0026#34; brew install pandoc echo \u0026#34;install markdown\u0026#34; brew install markdown echo \u0026#34;install cloc\u0026#34; brew install cloc elif [ \u0026#34;$(expr substr $(uname -s) 1 5)\u0026#34; == \u0026#34;Linux\u0026#34; ]; then # Do something under GNU/Linux platform GetOSVersion if [ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Ubuntu\u0026#34; ] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Debian\u0026#34; ]] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;LinuxMint\u0026#34; ]]; then # install htop sudo apt-get install htop -y # install httpie sudo apt-get install httpie -y # install ag sudo apt-get install silversearcher-ag -y # install zeal sudo apt-get install zeal -y # install ncdu sudo apt-get install ncdu -y # install i3 sudo apt-get install i3 -y # install emacs (i could die without it) sudo apt-get install emacs -y # install vim sudo apt-get install vim -y # install tree sudo apt-get install tree -y # install shellcheck sudo apt-get install shellcheck -y # install guile (scheme compiler) sudo apt-get install guile -y # install source code pro font [ -d /usr/share/fonts/opentype ] || sudo mkdir /usr/share/fonts/opentype sudo git clone https://github.com/adobe-fonts/source-code-pro.git /usr/share/fonts/opentype/scp sudo fc-cache -f -v # install proxychains-ng sudo apt-get install proxychains-ng -y # install pandoc sudo apt-get install pandoc -y sudo apt-get install markdown -y sudo apt-get install cloc -y elif [ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Fedora\u0026#34; ] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;CentOS\u0026#34; ]] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Korora\u0026#34; ]]; then # install ag sudo yum install -y the_silver_searcher # install zeal sudo yum install -y zeal # install httpie sudo yum install -y httpie # install htop sudo yum install -y htop # install ncdu sudo yum install -y ncdu # install vim sudo yum install -y vim # install emacs sudo yum install -y emacs # install i3 sudo yum install -y i3 # install tree sudo yum install -y tree # install shellcheck sudo yum install ShellCheck -y # install guile sudo yum install guile -y # install source code pro font sudo yum install adobe-source-code-pro-fonts -y # install proxychains-ng sudo yum install proxychains-ng -y sudo yum install pandoc -y sudo yum install markdown -y # count line and space in code sudo yum install cloc -y elif [ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Arch\u0026#34; ] ; then # install ag sudo pacman -S -y the_silver_searcher # install zeal sudo pacman -S -y zeal # install httpie sudo pacman -S -y httpie # install htop sudo pacman -S -y htop # install ncdu sudo pacman -S -y ncdu # install vim sudo pacman -S -y vim # install emacs sudo pacman -S -y emacs # install i3 sudo pacman -S -y i3 # install tree sudo pacman -S -y tree # install shellcheck sudo pacman -S ShellCheck -y # install guile sudo pacman -S guile -y # install source-code-pro font sudo pacman -S adobe-source-code-pro-fonts -y # install proxychains-ng sudo pacman -S proxychains-ng -y sudo pacman -S pandoc -y sudo pacman -S markdown -y sudo pacman -S ripgrep -y sudo pacman -S cloc -y fi elif [ \u0026#34;$(expr substr $(uname -s) 1 10)\u0026#34; == \u0026#34;MINGW32_NT\u0026#34; ]; then # Do something under 32 bits Windows NT platform echo \u0026#34;This is 32-bit windows\u0026#34; elif [ \u0026#34;$(expr substr $(uname -s) 1 10)\u0026#34; == \u0026#34;MINGW64_NT\u0026#34; ]; then # Do something under 64 bits Windows NT platform echo \u0026#34;this is 64-bit windows\u0026#34; fi 2.7 加密目录 每个人都会有需要只属于自己的东西,保护这些东西最好的办法就是对其进行加密:\n2.7.1 加密 使用 tar 和 openssl 对目录进行加密,先使用 tar 归档当前文件,然后使用 aes256 算法进行加密:\ntar -czf - * | openssl enc -e -aes256 -out encrypted.tar.gz 2.7.2 解密 把加密后的归档文件解密到当前命令:\nopenssl enc -d -aes256 -in encrypted.tar.gz| tar xz -C $(pwd) ","permalink":"https://ramsayleung.github.io/zh/post/2017/share_shell_script/","summary":"分享一下平时工作生活中编写的一些脚本片段(一直更新). 适用于 OS X 和 Linux 1 准备工作 因为我比较多的脚本都是基于 percol 这个神器,所以需要先安装 percol, 如果 不了","title":"脚本分享"},{"content":"因为需要编写 RESTful api 测试的缘故,重拾了 Spock 这个适用于 Groovy/Java 的测试 框架,顺便把以前写的一篇旧文整理了一下,权当重温。\n1 关于 Spock Spock 是一个适用于 Java(Groovy) 的一个优雅并且全面的测试框架, 说 Spock 全面,是 因为 Spock 集成了现有的 Java 测试库;至于为什么赞美 Spock 优雅,阅读完全文你就会 有体会的了\n因为基于 Groovy, 使得 Spock 可以更容易地写出表达能力更强的测试用例。又因为它内置 了 Junit Runner, 所以 Spock 兼容大部分的 IDE,测试工具,和持续集成服务器。接下来 就介绍一下 Spock 的特性\n2 Spock 特性 内置支持 mocking stubbing,可以很容易地模拟复杂的类的行为 Spock 实现了 BDD 范式(behavior-driven development) 与现有的 Build 工具集成,可以用来测试后端代码,Web 页面等等 兼容性强,内置 Junit Runner, 可以像运行 Junit 那样运行 Spock,甚至可以在同一个项 目里面同时使用两种测试框架 取长补短,吸收了现有框架的优点,并加以改进 Spock 代码风格简短,易读,表达性强,扩展性强,还有更清晰显示 bug 3 为什么是 Spock Spock 似乎有很多不错的特性,但是为什么有 Junit 这个那么强大的测试框架, 还要去 使用 Spock 呢? 甚至可以用 Spock 来代替 Junit 呢? 下面就用一些简单的例子来诠释 一下Spock 的强大. 以一个简单的加法为例:\nJunit 的测试用例\nSpock 的测试用例\n是否觉得耳目一新呢? 因为 Spock 支持以类人类语言的形式来定义方法名, 所以对比 Junit 的测试用例, 你会发现 Spock 的测试用例, 只需函数名, 就可以清晰了解这个测 试的用途\n接下来, 再写一个乘法的类, 然后人为地加入一个 Bug, 再看看 Junit 和 Spock 的表现\n如果测试 fail, 会出现什么情况呢?\n显而易见,Junit 只是显示了结果不等,却没办法究竟判断是加法还是乘法出现了 bug, 但是 Spock 就很清晰地给出了答案。不难看出 Spock 的语法更加简洁, 优雅; 此外, 得 益于 Spock 独特的命名方式,只需查看函数名字便可以了解测试用例的目的,无需额外 的注释。而这只是 Spock 和 Junit 的一部分差异,其他的差异,接下来会继续说明。\n4 Spock 语法 4.1 Specification class MyFirstSpecification extend Specification{ //fields //fixture methods //feature methods //helper methods } Specification 是指一个继承于 spock.lang.Specification 的一个 Groovy 类. 而 Specification 的名字一般是跟系统或者业务逻辑有关的组合词,例如之前的AdderSpec\n4.2 Fields 实例化一个类\ndef obj = new ClassUnderSpecification() def coll = new Collaborator() 4.3 Feature Methods Feature Methods 指具体的测试用例方法\ndef \u0026#34;pushing an element on the stack\u0026#34;() { // blocks go here } 4.4 Fixture Methods def setup() {} // run before every feature method def cleanup() {} // run after every feature method def setupSpec() {} // run before the first feature method def cleanupSpec() {} // run after the last feature method 关于 Fixture Methods 的作用,笔者引用一下官方文档的一段话\nFixture methods are responsible for setting up and cleaning up the environment in which feature methods are run. Usually it’s a good idea to use a fresh fixture for every feature method, which is what the setup() and cleanup() methods are for. All fixture methods are optional.\n简而言之, Fixture methodr 是进行初始化或者收尾工作的。为了更好地理解 Spock 的特性,可以用 Spock 和 Junit 进行比较,(图截自官网)\n以上就是 Spock 的基本用法, 也只能说是中规中矩,难言惊艳。那么,接下来介绍的 就是 Spock killer 级别的特性了\n4.5 Blocks 关于 Blocks 的用法, 这里引用官网的一段话\nSpock has built-in support for implementing each of the conceptual phases of a feature method. To this end, feature methods are structured into so-called blocks. Blocks start with a label, and extend to the beginning of the next block, or the end of the method. There are six kinds of blocks: setup, when, then, expect, cleanup, and where blocks\n简而言之, 这些内置的功能强大的 blocks, 就是帮助开发者编写单元测试的语法糖\n下面就了解一下不同 Block 的功能\n4.5.1 The given: block given: 应该包含所有的初始化条件或者初始化类,例如你可以把要测试的类的实例化放在 given. 总而言之, given 就是放置所有单元测试开始前的准备工作的地方\n4.5.2 The setup: block setup: 笔者个人理解功能跟 given 很相似,所以初始化的时候可以二选一(笔者 个人推荐用 given,因为这样更符合 BDD 范式)\n4.5.3 The when: blcok when: 是 Spock 测试中最重要的一部分,这里放置的就是你要测试的代码,和你如 何测试的用例,这里的测试代码应该尽可能地短。有经验的 Spock 用户可以直接看 when: block 就了解测试流程了\n4.5.4 The then: block then: block 包含隐式的断言, 补充一下,Spock 是没有 assert 这个断言函数的, Spock 使用的是 assertion, 笔者个人理解成这是一种隐式的断言。概括来说, then 就是放置你预期测试结果的地方。\n现在已经把 given-when-then 粗略地解释了一下, 现在就通过代码阐述具体的用法. 首先确定一下需求; 假设现在要测试一个通过网站来销售电脑的电商平台, 如下图 (图 截自 java_test_with_spock 一书)\n然通过模拟用户添加商品到购物车, 以展示 Spock 的用法\npublic class Product{ private String name; private int price; private int weight; } public class Basket{ public void addProduct(Product product){ addProduct(product,1) } public void addProduct(Product product,int times){ //some code about business } public int getCurrentWeight(){ // } public int getProductTypesCount(){ // } } 然后编写 Spock 的测试用例\ndef \u0026#34;A basket with one product has equal weight\u0026#34;(){ given: \u0026#34;an empty basket and a Tv\u0026#34; Product tv=new Product(name:\u0026#34;bravia\u0026#34;,price:1200,weight:18) Basket basket=new Basket() when:\u0026#34;user wants to buy the TV\u0026#34; basket.addProduct(tv) then:\u0026#34;basket weight is equal to the TV\u0026#34; basket.currentWeight==tv.weight } 现在对 Spock 有一个初步的认识了。也可以使用 given-when-then 这 \u0026ldquo;三板斧\u0026rdquo; 来写 一些逻辑不是非常复杂的测试用例了。\n4.5.5 The and: block and: 它的用法有点像语法糖,它自己本身是没有什么功能,它只是拿来扩展其他的 功能的. 用上面的例子来解释一下用法:\ndef \u0026#34;A basket with one product has equal weight\u0026#34;(){ given: \u0026#34;an empty basket \u0026#34; Basket basket=new Basket() and: \u0026#34;several products\u0026#34; Product tv=new Product(name:\u0026#34;bravia\u0026#34;,price:1200,weight:18) Product camera=new Product(name:\u0026#34;panasonic\u0026#34;,price:350,weight:2) Product hifi=new Product(name:\u0026#34;jvc\u0026#34;,price:600,weight:5) when:\u0026#34;user wants to buy the TV abd the camera and the hifi\u0026#34; basket.addProduct(tv) basket.addProduct(camera) basket.addProduct(hifi) then:\u0026#34;basket weight is equal to all product weight\u0026#34; basket.currentWeight==(tv.weight+camera.weight+hifi.weight) } 从上面的代码可以看出,given 和 and 都用来进行类初始化,只是根据 Basket 和 Product 类型进行了细分。如下图\n使用 and block 可以代码结构更简洁优雅. 此外, 如果 and 是紧跟在 when 后 面, 那么 and 就据有和 when block 一样的功能,依此类推\n4.5.6 The expect: block expect 是一个很强大的特性,它用很多种用法,最常用的用法就是把 given-when-then 都结合起来\ndef \u0026#34;An empty basket has no weight\u0026#34;(){ expect:\u0026#34;zero weight when nothing is added\u0026#34; new Basket().currentWeight==0 } 或者是以下这种形式\ndef \u0026#34;An empty basket has no weight(alternative)\u0026#34;(){ given:\u0026#34;an empty basket\u0026#34; Basket basket=new Basket() expect:\u0026#34;that the weight is 0\u0026#34; basket.currentWeight==0 } 又或者用 expect 提前进行条件判断\ndef \u0026#34;A basket with two products weights as their sum (precondition)\u0026#34;() { given: \u0026#34;an empty basket, a TV and a camera\u0026#34; Product tv = new Product(name:\u0026#34;bravia\u0026#34;,price:1200,weight:18) Product camera = new Product(name:\u0026#34;panasonic\u0026#34;,price:350,weight:2) Basket basket = new Basket() expect:\u0026#34;that nothing should be inside\u0026#34; basket.currentWeight == 0 basket.productTypesCount == 0 /* expect: block performs intermediate assertions*/ when: \u0026#34;user wants to buy the TV and the camera\u0026#34; basket.addProduct tv basket.addProduct camera then: \u0026#34;basket weight is equal to both camera and tv\u0026#34; basket.currentWeight == (tv.weight + camera.weight) /* then: block examines the final result*/ } 上面那个例子是在添加产品之前检查初始化条件,这种情况下,能更容易看出是哪里测试 fail\n4.5.7 The clean: block clean 就相当于在所有的测试结束以后执行的操作,例如,如果在测试中新建了 IO 流, 就可以在 clean 里面关闭 IO 流,那样就可以保证代码的正确性了\n4.6 Spock killer future 确定需求:(例子来自 Java_test_with_spock 一书),假设有一个核反应堆,这个反应 堆的系统组成:\n多个烟雾感应器(输入)\n3 个辐射感应器(输入)\n现在的压力值(输入\n报警器(输出)\n疏散命令(输出)\n通知操作员关闭反应堆(输出) 系统如图\n系统相关设定:\n如果压力值超过 150,报警器报警\n如果 2 个或者更多的烟雾感应器被触发,那么报警器报警,通知操作员关闭反应堆\n如果辐射值超过 100,警报器报警,通知操作员关闭反应堆,并马上疏散人群\n输入输出对应关系\n现在,假如用 Junit 来写测试用例\n@RunWith(Parameterized.class) public class NuclearReactorTest { private final int triggeredFireSensors; private final List\u0026lt;Float\u0026gt; radiationDataReadings; private final int pressure; private final boolean expectedAlarmStatus; private final boolean expectedShutdownCommand; private final int expectedMinutesToEvacuate; public NuclearReactorTest(int pressure, int triggeredFireSensors, List\u0026lt;Float\u0026gt; radiationDataReadings, boolean expectedAlarmStatus, boolean expectedShutdownCommand, int expectedMinutesToEvacuate) { this.triggeredFireSensors = triggeredFireSensors; this.radiationDataReadings = radiationDataReadings; this.pressure = pressure; this.expectedAlarmStatus = expectedAlarmStatus; this.expectedShutdownCommand = expectedShutdownCommand; this.expectedMinutesToEvacuate = expectedMinutesToEvacuate; } @Test public void nuclearReactorScenario() { NuclearReactorMonitor nuclearReactorMonitor = new NuclearReactorMonitor(); nuclearReactorMonitor.feedFireSensorData(triggeredFireSensors); nuclearReactorMonitor.feedRadiationSensorData(radiationDataReadings); nuclearReactorMonitor.feedPressureInBar(pressure); NuclearReactorStatus status = nuclearReactorMonitor.getCurrentStatus(); assertEquals(\u0026#34;Expected no alarm\u0026#34;, expectedAlarmStatus, status.isAlarmActive()); assertEquals(\u0026#34;No notifications\u0026#34;, expectedShutdownCommand, status.isShutDownNeeded()); assertEquals(\u0026#34;No notifications\u0026#34;, expectedMinutesToEvacuate, status.getEvacuationMinutes()); } @Parameters public static Collection\u0026lt;Object[]\u0026gt; data() { return Arrays .asList(new Object[][] { { 150, 0, new ArrayList\u0026lt;Float\u0026gt;(), false, false, -1 }, { 150, 1, new ArrayList\u0026lt;Float\u0026gt;(), true, false, -1 }, { 150, 3, new ArrayList\u0026lt;Float\u0026gt;(), true, true, -1 }, { 150, 0, Arrays.asList(110.4f, 0.3f, 0.0f), true, true, 1 }, { 150, 0, Arrays.asList(45.3f, 10.3f, 47.7f), false, false, -1 }, { 155, 0, Arrays.asList(0.0f, 0.0f, 0.0f), true, false, -1 }, { 170, 0, Arrays.asList(0.0f, 0.0f, 0.0f), true, true, 3 }, { 180, 0, Arrays.asList(110.4f, 0.3f, 0.0f), true, true, 1 }, { 500, 0, Arrays.asList(110.4f, 300f, 0.0f), true, true, 1 }, { 30, 0, Arrays.asList(110.4f, 1000f, 0.0f), true, true, 1 }, { 155, 4, Arrays.asList(0.0f, 0.0f, 0.0f), true, true, -1 }, { 170, 1, Arrays.asList(45.3f, 10.3f, 47.7f), true, true, 3 }, }); } 各种输入输出数据以及 getter setter 耦合在一起,代码变得难读起来. 此外,除了可 读性, 还有更严重的问题,假如需求要增加一个输入或者增加一个输出呢, 就只能改 变数据结构, 这样的代码真的难以维护。不知道 Spock 的表现又如何呢?\nclass NuclearReactorSpec extends spock.lang.Specification{ def \u0026#34;Complete test of all nuclear scenarios\u0026#34;() { given: \u0026#34;a nuclear reactor and sensor data\u0026#34; NuclearReactorMonitor nuclearReactorMonitor =new NuclearReactorMonitor() when: \u0026#34;we examine the sensor data\u0026#34; nuclearReactorMonitor.feedFireSensorData(fireSensors) nuclearReactorMonitor.feedRadiationSensorData(radiation) nuclearReactorMonitor.feedPressureInBar(pressure) NuclearReactorStatus status = nuclearReactorMonitor.getCurrentStatus() then: \u0026#34;we act according to safety requirements\u0026#34; status.alarmActive == alarm status.shutDownNeeded == shutDown status.evacuationMinutes == evacuation where: \u0026#34;possible nuclear incidents are:\u0026#34; pressure | fireSensors | radiation || alarm | shutDown | evacuation 150 | 0 | [] || false | false | -1 150 | 1 | [] || true | false | -1 150 | 3 | [] || true | true | -1 150 | 0 | [110.4f ,0.3f, 0.0f] || true | true | 1 150 | 0 | [45.3f ,10.3f, 47.7f] || false | false | -1 155 | 0 | [0.0f ,0.0f, 0.0f] || true | false | -1 170 | 0 | [0.0f ,0.0f, 0.0f] || true | true | 3 180 | 0 | [110.4f ,0.3f, 0.0f] || true | true | 1 500 | 0 | [110.4f ,300f, 0.0f] || true | true | 1 30 | 0 | [110.4f ,1000f, 0.0f] || true | true | 1 155 | 4 | [0.0f ,0.0f, 0.0f] || true | true | -1 170 | 1 | [45.3f ,10.3f, 47.7f] || true | true | 3 } } 除了上面提及的 given-when-then 范式外,还多了一个之前没见过的 where block。现 在就来认识一下 Spock 的 killer 特性. 可以看到 Spock 的输入输出参数都保存在类 似表格的数据结构,其实这是 Spock 的 Parameterized tests,而在 || 符号左边的 是输入,右边的输出,每一列开始都是该参数的属性名,这样就可以很便捷地在 then 判断输出结果是否符合预期结果. 而数据添加或者减少输入参数或者输出结果的操作, 只需在 where block 里面对应地添加或者减少具体的参数,整个操作一目了然. 参数 的新增或者移除也很容易地实现\n5 结语 笔者在项目中正是使用 Spock 编写测试, 或许对比 Junit, Spock 在流行度方面还难而 望其项背, 但是综合多方考虑,Spock 真的值得一试,兼之 Groovy 语言的语法加成,就 有一种在使用脚本编写 Java 的感觉 (好吧,笔者知道 Groovy 就是基于 jvm 的脚本), 无需再为 Java 啰嗦的语法而烦恼。此外 Spock还有很多很强大的功能,例如内置的 Mocking Stubbing (Junit 需要第三方库支持), 还有支持企业级应用,Spring, Spring boot, 和 Restful service 测试等。更多的用法,就要查阅官方文档了\n6 参考 Java Testing with Spock Spock Framework Reference Documentation ","permalink":"https://ramsayleung.github.io/zh/post/2017/spock/","summary":"因为需要编写 RESTful api 测试的缘故,重拾了 Spock 这个适用于 Groovy/Java 的测试 框架,顺便把以前写的一篇旧文整理了一下,权当重温。 1 关于 Spock Spock 是一个适用于 Java(Groovy) 的一个优雅并","title":"Spock 一个优雅的Groovy/Java测试框架"},{"content":"1 重要性 笔者最近都在负责项目中关于日志的部分,因为跟日志打交道比较多,所以有一些关于日 志感受和技巧想要分享一下。\n笔者认为对于各种程序和应用,日志都是非常重要的,因为程序在部属到服务器之后,开发者是没办法像在本地开发那样可以充分了解程序发生的状况,而使用日志可以让开发者了解运行中的程序的状态,即使出现了错误,或者是系统挂了,也可以从日志中分析原因。\n所以换句话说,日志的重要程度甚至可以称得上是不可或缺。接下来,笔者将会以 Python 中的 logging 模块为例阐述日志。\n2 关于日志 2.1 使用 print 函数输出? 日志是为了输出程序的运行状态,那么可否使用 print 函数进行 logging 的工作呢?\n我并不建议把 print() 函数当作日志使用 (当然,如果你一定要这么用,我也拦不住);不建议使用 print 进行logging 原因有:\n无法在不修改源代码的情况下,控制日志的输出 日志信息可能跟程序输出的有用数据混杂,导致输出的数据不可读或者非常难读 print 无法将日志信息输出到除标准输出以外的目标 (例如文件,socket,SMTP 服务器等) 无法根据错误信息的等级进行动态输出,因为 print 函数的作用只是输出信息 可能对于非常简单的小程序,开发者可以使用 print 进行日志输出,但是对于比较大型的程序,系统内置的 logging 类库或许是更好的选择\n2.2 日志需要记录的是什么 Python 的日志类库 logging 可以让开发者根据不同场景使用不同的日志等级以输出 不同的日志信息。\n而日志需要记录的最基本的信息又是什么呢?要想回答这个问题,先和我一起回顾一下日志的功能:记录程序的状态,为程序的开发和调试提供便利!\n所谓方便调试,需要记录的必然包括可以帮助更快定位到错误的有用信息:\nLogger 的名字 (比较常用的做法都是 __name__,即当前文件的信息) 具体日期 (这个可以帮助确定出错的具体场景) 方法名 源代码行数 异常的 traceback 信息 这只是最基本的信息,具体还要根据场景添加其它有用信息;比如对于分布式的程序,肯定还要记录其它节点的名字,IP 等有用信息。\n3 Logging 的正确姿势 3.1 使用 Python 的 logging 模块 我认为,使用 Python 的标准日志库是比较好的实践,因为标准库已经提供了开箱即用的特性,无需重复造轮子。Python 的 logging 模块也很容易上手,举个小例子:\nimport logging logging.basicConfig(level=logging.DEBUG) # define a logger logger = logging.getLogger(__name__) #Info level msg logger.info(\u0026#39;Info level message\u0026#39;) #Debug level msg logger.debug(\u0026#39;Debug level message\u0026#39;) #Warning level msg logger.info(\u0026#39;Warning level message\u0026#39;) 日志输出如下:\nINFO:__main__: Info level message DEBUG:__main__: Debug level message WARN:__main__: Warning level message 3.2 记录异常信息 日志一个非常重要的作用就是调试,所以记录出现异常的地方是有必要,并且需要记录栈的调用信息。例如:\ntry: open(\u0026#39;file_not_exist.txt\u0026#39;, \u0026#39;wt\u0026#39;) except Exception, e: logger.error(\u0026#39;Failed to write a file\u0026#39;,exc_info=True) 通过将 exc_info 设置成 True, 栈的调用信息就会记录到日志里面。而也可以使用 logger.exception(message,*args) 方法,它等同于 logger.error(msg,exc_info=True,*args) 方法。\n3.3 使用日志文件轮转控制器 (rotating file handler) 如果使用日志文件控制器 (FileHandler), 不断地运行程序,就会产生越来越多的日志 信息或者是日志文件。\n为了控制日志文件的数量,可以使用 RotatingFileHandler 自 动新建新的日志文件,并且保留旧的日志文件,当产生一定数量的日志文件之后,就会 自动删除掉最旧的日志文件。例如:\nhandler = logging.handlers.RotatingFileHandler( LOG_FILENAME, maxBytes=20, backupCount=5, ) my_logger.addHandler(handler) 就是日志文件大小超过20个字节 (当然,真实情况不会那么小的阀值),就创建一个新的日志文件,把原来的日志文件,例如叫 example.log 重命名为 example.log.1,然后新建的日志文件就会被命名为_example.log_, 一直到产生了6个日志文件,即 example.log.5, 继续记录日志,最开始的第一个日志就会被删除。\n3.4 使用日志服务器 对于那些分布式的应用,或者部署多台服务器上有不同日志的程序而言,逐个服务器或者节点查看日志实在太可怕了. 这个时候,就可以设置一个日志服务器,把重要的日志信息发送到日志服务器,你就在日志服务器上监控各个节点的日志状态了。\nlogging-cookbook 的例子:\n客户端或者节点:\nimport logging import logging.handlers rootLogger = logging.getLogger(\u0026#39;\u0026#39;) rootLogger.setLevel(logging.DEBUG) socketHandler = logging.handlers.SocketHandler(\u0026#39;localhost\u0026#39;, logging.handlers.DEFAULT_TCP_LOGGING_PORT) # don\u0026#39;t bother with a formatter, since a socket handler sends the event as # an unformatted pickle rootLogger.addHandler(socketHandler) # Now, we can log to the root logger, or any other logger. First the root... logging.info(\u0026#39;Jackdaws love my big sphinx of quartz.\u0026#39;) # Now, define a couple of other loggers which might represent areas in your # application: logger1 = logging.getLogger(\u0026#39;myapp.area1\u0026#39;) logger2 = logging.getLogger(\u0026#39;myapp.area2\u0026#39;) logger1.debug(\u0026#39;Quick zephyrs blow, vexing daft Jim.\u0026#39;) logger1.info(\u0026#39;How quickly daft jumping zebras vex.\u0026#39;) logger2.warning(\u0026#39;Jail zesty vixen who grabbed pay from quack.\u0026#39;) logger2.error(\u0026#39;The five boxing wizards jump quickly.\u0026#39;) 日志服务器:\nimport logging import logging.handlers import pickle import socketserver import struct class LogRecordStreamHandler(socketserver.StreamRequestHandler): \u0026#34;\u0026#34;\u0026#34;Handler for a streaming logging request. This basically logs the record using whatever logging policy is configured locally. \u0026#34;\u0026#34;\u0026#34; def handle(self): \u0026#34;\u0026#34;\u0026#34; Handle multiple requests - each expected to be a 4-byte length, followed by the LogRecord in pickle format. Logs the record according to whatever policy is configured locally. \u0026#34;\u0026#34;\u0026#34; while True: chunk = self.connection.recv(4) if len(chunk) \u0026lt; 4: break slen = struct.unpack(\u0026#39;\u0026gt;L\u0026#39;, chunk)[0] chunk = self.connection.recv(slen) while len(chunk) \u0026lt; slen: chunk = chunk + self.connection.recv(slen - len(chunk)) obj = self.unPickle(chunk) record = logging.makeLogRecord(obj) self.handleLogRecord(record) def unPickle(self, data): return pickle.loads(data) def handleLogRecord(self, record): # if a name is specified, we use the named logger rather than the one # implied by the record. if self.server.logname is not None: name = self.server.logname else: name = record.name logger = logging.getLogger(name) # N.B. EVERY record gets logged. This is because Logger.handle # is normally called AFTER logger-level filtering. If you want # to do filtering, do it at the client end to save wasting # cycles and network bandwidth! logger.handle(record) class LogRecordSocketReceiver(socketserver.ThreadingTCPServer): \u0026#34;\u0026#34;\u0026#34; Simple TCP socket-based logging receiver suitable for testing. \u0026#34;\u0026#34;\u0026#34; allow_reuse_address = True def __init__(self, host=\u0026#39;localhost\u0026#39;, port=logging.handlers.DEFAULT_TCP_LOGGING_PORT, handler=LogRecordStreamHandler): socketserver.ThreadingTCPServer.__init__(self, (host, port), handler) self.abort = 0 self.timeout = 1 self.logname = None def serve_until_stopped(self): import select abort = 0 while not abort: rd, wr, ex = select.select([self.socket.fileno()], [], [], self.timeout) if rd: self.handle_request() abort = self.abort def main(): logging.basicConfig( format=\u0026#39;%(relativeCreated)5d %(name)-15s %(levelname)-8s %(message)s\u0026#39;) tcpserver = LogRecordSocketReceiver() print(\u0026#39;About to start TCP server...\u0026#39;) tcpserver.serve_until_stopped() if __name__ == \u0026#39;__main__\u0026#39;: main() 通过给 logger 添加一个SocketHandler 就可以把日志事件发送到服务器端\n3.5 使用配置文件 虽然开发者可以使用 Python 代码来配置日志系统,但是这样是很不灵活的,每次修改日志等级还需要去改动代码。\n而使用配置文件无疑是一个更好的选择,例如 json 或者是 yaml 文件,这样就可以在 json/yaml 文件中加载日志配置了。以 Django 项目的配置文件为例,我改成了 json 格式:\n{ \u0026#34;version\u0026#34;: 1, \u0026#34;disable_existing_loggers\u0026#34;: True, \u0026#34;formatters\u0026#34;: { \u0026#34;verbose\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s\u0026#34; }, \u0026#34;simple\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;%(levelname)s %(message)s\u0026#34; }, }, \u0026#34;filters\u0026#34;: { \u0026#34;special\u0026#34;: { \u0026#34;()\u0026#34;: \u0026#34;project.logging.SpecialFilter\u0026#34;, \u0026#34;foo\u0026#34;: \u0026#34;bar\u0026#34;, } }, \u0026#34;handlers\u0026#34;: { \u0026#34;null\u0026#34;: { \u0026#34;level\u0026#34;: \u0026#34;DEBUG\u0026#34;, \u0026#34;class\u0026#34;: \u0026#34;django.utils.log.NullHandler\u0026#34;, }, \u0026#34;console\u0026#34;: { \u0026#34;level\u0026#34;: \u0026#34;DEBUG\u0026#34;, \u0026#34;class\u0026#34;: \u0026#34;logging.StreamHandler\u0026#34;, \u0026#34;formatter\u0026#34;: \u0026#34;simple\u0026#34; }, \u0026#34;mail_admins\u0026#34;: { \u0026#34;level\u0026#34;: \u0026#34;ERROR\u0026#34;, \u0026#34;class\u0026#34;: \u0026#34;django.utils.log.AdminEmailHandler\u0026#34;, \u0026#34;filters\u0026#34;: \u0026#34;special\u0026#34; } }, \u0026#34;loggers\u0026#34;: { \u0026#34;django\u0026#34;: { \u0026#34;handlers\u0026#34;: \u0026#34;null\u0026#34;, \u0026#34;propagate\u0026#34;: true, \u0026#34;level\u0026#34;: \u0026#34;INFO\u0026#34;, }, \u0026#34;django.request\u0026#34;: { \u0026#34;handlers\u0026#34;: [\u0026#34;mail_admins\u0026#34;], \u0026#34;level\u0026#34;: \u0026#34;ERROR\u0026#34;, \u0026#34;propagate\u0026#34;: false, }, \u0026#34;myproject.custom\u0026#34;: { \u0026#34;handlers\u0026#34;: [\u0026#34;console\u0026#34;, \u0026#34;mail_admins\u0026#34;], \u0026#34;level\u0026#34;: \u0026#34;INFO\u0026#34;, \u0026#34;filters\u0026#34;: [\u0026#34;special\u0026#34;] } } } 以及加载 json 文件到日志配置中:\nimport json import logging.config def setup_logging(): \u0026#34;\u0026#34;\u0026#34; Setup logging configuration \u0026#34;\u0026#34;\u0026#34; with open(\u0026#39;logging_configuration.json\u0026#39;, \u0026#39;rt\u0026#39;) as f: config = json.load(f) logging.config.dictConfig(config) 使用 json 还有一个好处是标准库已经内置了 json 模块,无需像 yaml 那样需要安装额外的模块,不过我更推崇 yaml, 因为清晰之余,还可以少打很多字 :)\n3.6 对于不同的代码,使用不同的日志等级 因为一个项目不同代码要求不一样,也无需把每一个实现细节都记录在日志,只需要根 据不同的实现,使用不同的日志等级,例如使用 Debug 记录系统启动,处理业务逻辑 请求的信息,使用 Error, 记录系统的出错信息,可以结合堆栈分析原因,等等。\n此外,Logger 实例可以被配置成基于名字的树状结构。 每一个部件都定义了一个基础的名字,对应的模块被设置成子节点。而 root logger 没有名字。如图:\n就配置 logging 而言,我认为树状结构是非常有用的,因为无需为每一个 logger 都设置handler. 如果一个 logger 没有 handler 的话,它就会让父节点来处理。所以 对于对于大部份的应用而言,只需配置 root logger, 而所有的信息都会发送到同一个 地方\n而树状结构可以对应用的不同部分使用不同的日志等级,不同的 handler, 不同的formatter, 以更好地控制日志信息\n3.7 使用结构化日志 虽然大部份的日志信息对于人类都是可读的,但是对于程序而言,就很难进行解析了。\n这个时候,为了方便程序进行解析,我建议使用结构化格式的日志,这样就不再需要各种复杂的正则表达式来解析日志了。得益于内置的 json 模块,使用 json 就可以很简单地生成的利于程序解析结构化日志,以 logging cookbook 中的例子说明:\nimport json import logging class StructuredMessage(object): def __init__(self, message, **kwargs): self.message = message self.kwargs = kwargs def __str__(self): return \u0026#39;%s \u0026gt;\u0026gt;\u0026gt; %s\u0026#39; % (self.message, json.dumps(self.kwargs)) _ = StructuredMessage # optional, to improve readability logging.basicConfig(level=logging.INFO, format=\u0026#39;%(message)s\u0026#39;) logging.info(_(\u0026#39;message 1\u0026#39;, foo=\u0026#39;bar\u0026#39;, bar=\u0026#39;baz\u0026#39;, num=123, fnum=123.456)) 日志输出结果如下:\nmessage 1 \u0026gt;\u0026gt;\u0026gt; {\u0026#34;fnum\u0026#34;: 123.456, \u0026#34;num\u0026#34;: 123, \u0026#34;bar\u0026#34;: \u0026#34;baz\u0026#34;, \u0026#34;foo\u0026#34;: \u0026#34;bar\u0026#34;} 3.8 参考 https://logmatic.io/blog/python-logging-with-json-steroids/ https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/ https://docs.python.org/3/howto/logging-cookbook.html https://pymotw.com/3/logging/index.html 3.9 小结 虽然这次的日志阐述是以 Python 的日志模块举例,但是绝大部分的语言都内置或者是有第三方的日志支持,所以我分享的技巧还是可以应用到其他的语言的。\n这些都是我在日常项目中的一点体会,与诸君共赏罢。Enjoy :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/logging/","summary":"1 重要性 笔者最近都在负责项目中关于日志的部分,因为跟日志打交道比较多,所以有一些关于日 志感受和技巧想要分享一下。 笔者认为对于各种程序和应用,","title":"你所不可或缺的 – logging"},{"content":"笔者最近思考如何编写高效的爬虫; 而在编写高效爬虫的时候,有一个必需解决的问题就是: url 的去重,即如何判别 url 是否已经被爬取,如果被爬取,那就不要重复爬取。\n一般如果需要爬取的网站不是非常庞大的话,使用Python 内置的 set 就可以实现去重了,但是使用 set 内存利用率不高,此外对于那些不像Python 那样用 hash 实现的 set 而言,时间复杂度是 log(N),实在难说高效。\n1 Bloom Filter 那么如何实现高效的去重呢? 笔者查阅资料之后得知:使用布隆过滤器 (Bloom Filter).\n布隆过滤器可以用于快速检索一个元素是否在一个集合中。布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数(Hash函数)。\n而一般的判断一个元素是否在一个集合里面的做法是:用需要判断的元素和集合中的元素进行比较,一般的数据结构,例如链表,树,都是这么实现的。\n缺点是:随着集合元素的增多,需要比较的元素也增多,检索速度就越来越慢。\n而使用布隆过滤器判重可以实现常数级的时间复杂度(检索时间不随元素增长而增加).那么布隆过滤器又是怎样实现的呢\n1.1 布隆过滤器实现原理 一个Bloom Filter是基于一个m位的位向量(Bit Vector),这些位向量的初始值为0, 并且有一系列的 hash 函数,hash 函数值域为1-m.在下面例子中,是15位的位向量,初始值为0以空白表示,为1以颜色填充\n现在有两个简单的 hash 函数:fnv,murmur.现在我输入一个字符串 \u0026ldquo;whatever\u0026rdquo; ,然后分别使用两个 hash 函数对 \u0026ldquo;whatever\u0026rdquo; 进行散列计算并且映射到上面的位向量。\n可知,使用 fnv 函数计算出的 hash 值是11,使用 murmur 函数计算出的 hash 值是4. 然后映射到位向量上:\n如果下一次,笔者要判断 whatever 是否在字符串中,只需使用 fnv 和 murmur 两个 hash 函数对 whatever 进行散列值计算,然后与位向量做 \u0026ldquo;与运算\u0026rdquo;,如果结果为0, 那么说明 whatever 是不在集合中的,因为同样的元素使用同一个 hash 函数产生的值每次都是相同的,不相同就说明不是同一个元素。\n但是如果 \u0026ldquo;与运算\u0026rdquo; 的结果为1,是否可以说明 whatever 就在集合中呢?其实上是不能100% 确定的,因为 hash 函数存在散列冲突现象 (即两个散列值相同,但两个输入值是不同的), 所以布隆过滤器只能说\u0026quot;我可以说这个元素我在集合中是看见过滴,只是我有一定的不确定性\u0026quot;.\n当你在分配的内存足够大之后,不确定性会变得很小很小。\n你可以看到布隆过滤器可以有效利用内存实现常数级的判重任务,但是鱼和熊掌不可得兼,付出的代价就是一定的误判 (机率很小),所以本质上,布隆过滤器是 \u0026ldquo;概率数据结构 (probabilistic data structure)\u0026rdquo;.\n这个就是布隆过滤器的基本原理。当然,位向量不会只是15位,hash函数也不会仅是两个简单的函数. 这只是简化枝节,为了清晰解述原理而已。\n2 Python BloomFilter 算法都是为了实际问题服务的,又回到爬虫这个话题上。在了解布隆过滤器原理之后,可以很容易地实现自己的布隆过滤器,但是想要实现一个高效健壮的布隆过滤器就需要比较多的功夫了,因为需要考虑的问题略多。\n幸好,得益Python 强大的社区,已经有Python BloomFilter 的库。一个文档中的简单例子:\nfrom pybloomfilter import BloomFilter bf = BloomFilter(10000000, 0.01, \u0026#39;filter.bloom\u0026#39;) with open(\u0026#34;/usr/share/dict/words\u0026#34;) as f: for word in f: bf.add(word.rstrip()) print \u0026#39;apple\u0026#39; in bf 结果为 True\n3 总结 原理就说得差不多了,要想对布隆过滤器有更深的认识,还需要更多的实战。多写,多思考。 Enjoy Python,Enjoy Crawler :)\n4 参考 https://llimllib.github.io/bloomfilter-tutorial/ https://en.wikipedia.org/wiki/Bloom_filter ","permalink":"https://ramsayleung.github.io/zh/post/2017/bloom_filter/","summary":"笔者最近思考如何编写高效的爬虫; 而在编写高效爬虫的时候,有一个必需解决的问题就是: url 的去重,即如何判别 url 是否已经被爬取,如果被爬取,那就不要","title":"爬虫高效去重之布隆过滤器"},{"content":"不久前,Apple 的文件系统 (Apple File System) 新推出,然后各方便一起挤身向前对APFS \u0026ldquo;评头品足\u0026rdquo;,我是不了解 APFS ,所以也没有什么发言权嘛,不过话分两头;\n对Linux的文件系统,我还是有了解过的,所以可以聊聊Linux 的文件系统;与Windows 和Apple 两家商业公司不同,Linux 是开源的,因此,只要你有足够的时间和能力,你就可以自己写一个文件系统,这个也是Linux 文件系统众多的主要原因。\n那么有众多的Linux 文件系统,它们的差异,优缺点又是什么呢?\n1 File System 在开始 \u0026ldquo;大话\u0026rdquo; 各种文件系统的时候,我先想谈谈什么是文件系统。\n一般用户平时都会有上百G 的数据,那么多的数据,应该怎么保存呢?\n不知道怎么回答,可以先类比一下,想象一下你有很多书,你会怎么放置你的书籍呢?直接丢在客厅中间?或者是按书内容分类 放到书架上?如果分类的话,是按怎么划分呢?你买回来的书架一次可以放置多少书籍呢?怎么才能更快地找到你要找的书呢?\n其实文件系统处理,检索文件和你放置,查找书籍是类似的!此外,在Windows 下常见到的格式化,其实就是在不同的文件系统之间切换,那么为什么数据会丢失呢?\n你可以类比成你家里装修,想换新的书架,但是如果不把原来书架上的书籍搬出准备装修的屋子,装修时肯定会损坏书籍嘛!\n2 Journaling 在比较文件系统之前,先聊聊文件系统中的日志 (Journaling), 而有些文件系统是有日志功能的,而有些是没有日志功能的;\n为了了解它们之间的差异,就需要先了解一下什么是日志。日志的出现本质而言都是为了更好地保存数据。\n假设你正在向磁盘里写入文件,突然间断电了,但是你的文件是没有完成写入磁盘的,而你的电脑是不知道是否已经完成写入,所以下次重新来电启动电脑的时候,是没有“人”告知电脑是否要重新写数据的?这样数据就丢失了。\n能不能在文件还没有完成写回磁盘又中途出现错误的时候,告诉系统你的 目标文件还有数据没有写回磁盘,你要记得完成这项工作阿?\n当然可以,这就是文件系统中日志的功能,在日志的协助下,你的电脑会在日志中记着,“我要把某某文件写入到磁盘”,如果顺利完成写入磁盘工作,日志的这项记录就会被删除,如果中途出现异常,例 如断电了,重新启动的时候,你的系统就会发现,我要写入某某文件的工作还没完成,它就会继续未了的事业,这样就可以保障数据不会丢失。\n(而你日常工作生活中,数据的丢失是因为你没有保存数据,即没有把数据写入到磁盘). 如图:\n因为写入日志需要额外的工作,所以需要额外的资源,但是这样的消耗是相当值得地。\n3 Comparison 下面对比一下 各种文件系统的特性\nFigure 1: 图来自archwiki\n如果你想了解你的Linux 内核所支持的文件系统,你可以\ncat /proc/filesystems 现在就来比较一下各种常见的Linux文件系统\n3.1 Ext Ext 是指\u0026quot;Extended file system(扩展文件系统)\u0026quot;,应该算是Linux 文件系统里面的老大爷了,它是从经典的 Minix 的文件系统衍生过来的,为Linux 专门设计的,但是它缺乏很多重要的特性,比如上面提到的日志功能,所以大部份的Linux 发行版本都是不支持 Ext 了\n3.2 Ext2 Ext2 也是不支持日志的,但是它是第一个支持扩展文件属性和2T 容量的Linux 文件系统,但是正由于Ext2 不支持日志功能,它可以更少地写磁盘,所以它适合像USB 这种闪存,但是Ext2 无法被Windows 识别的,所以它的闪存功能更多地被FAT32和exFAT所代替,换言之,Ext2 用的也不多\n3.3 Ext3 Ext3 就是Ext2 带有日志功能的扩展,并且Ext3 也向后兼容Ext2,所以你在Ext2 和Ext3 之间切换也是不需要重新格式化滴,但是最常用的还不是Ext3,而是Ext4 :)\n3.4 Ext4 Ext4 也是向后兼容 Ext3 和Ext2 的,所以你是可以在Ext4,Ext3,Ext2 之间切换而无需格式化文件系统。\nExt4 包含很多新的特性,例如支持存储更大的文件,支持延迟分配以 改进对闪存的支持,还能有效地减少文件的碎片化,提高利用效率。\n显而易见,Ext4 是最先进的 Ext 系列的文件系统,也是大部分Linux 发行版本的默认文件系统\n3.5 ZFS ZFS 最初是给Sun 的Solaris 设计的,Sun 被收购后,现在是属于Oracle 的,ZFS 支持 大量非常先进的特性,比如说 快照 (snapshot),动态存储 (dynamic disk striping), 驱动池等 (drive pool);\n此外ZFS 文件的每个文件都是有校验和的,所以通过检查校验和,就能确定文件的完整性。但是,虽说ZFS 非常强大,却因为ZFS license 的缘故, ZFS 无法添加到 Linux 内核的。如果你非常想要尝试ZFS 的话,你可以自行添加对Linux 发行版本上面添加ZFS 的支持\n3.6 BrtFS Brtfs 是由Oracle 设计的一个支持写时复制 (copy on write) 的现代文件系统;\n而Btrfs 的意思是 B 树文件系统 (B-Tree File System),它支持大量非常先进的特性,例如 动态inode 分配,数据校验和,有效的增量备份,驱动池,最大支持 2^64 byte 容量即16 Eib 大的文件。\nBtrFS 是被设计成取代Ext 系列的文件系统的,只不过因为现在的BtrFS 还没有足够成熟,所以还没有大公司在生产环境使用 BtrFS,但是BtrFS 的未来可期\n3.7 JFS JFS 是IBM 为IBM 自家的AIX 操作系统设计的日志文件系统 (Journaled File System), 后来迁移到了Linux 系统上 (HP-UX 也有一个叫做JFS 的文件系统).\n在AIX 系统上是存在过两代的JFS文件系统的,分别是 JFS1和JFS2,而Linux 上就只有JFS2了(Linux 上的JFS都是指JFS2)。\nJFS 无论在处理大文件还是小文件都有非常不错的表现,并且CPU 占用也是比较低的;JFS 也是支持非常多的特性的,例如 B+ 树,动态Inode 分配,并发IO等。\nJFS 也是一个设计优秀的文件系统并且支持大部分的Linux 发行版本,但是因为它最初是为AIX 设计,所以在处于生产环境上的Linux服务器的测试就不如Ext :(.\n3.8 ReiserFS ReiserFs 是第一个被引进Linux 标准内核的日志文件系统(在内核版本为2.4.1的时候引进),也是Linux 文件系统的一次飞跃,那时的ReiserFS 包含了很多Ext 没有的新特性。\n虽说 ReiserFS 在Linux 有一个非常华丽的开头,但是后来ReiserFS 的开发就陷入了停滞,因为ReiserFS 的核心开发者Hans Reiser(ReiserFS 名字的来由)因为谋杀妻子而被收监 :(。\n而后来的ReiserFS 也没有出现在主要的Linux 内核版本里面,虽说ReiserFS是非常好的文件系统,但是它前景如何,我们也只能拭目以待了\n3.9 XFS XFS 最初是Silicon Graphics 为SGI IRX 操作系统设计的64位高性能文件系统,在2001 年迁移到了Linux.\n得益于XFS 基于 allocation groups 的设计,XFS 拥有非常优秀的并行IO 能力,并支持延迟分配 (delayed allocation) 以改进文件碎片化,在某种程度上,XFS 和Ext 有一定的相似;\n此外,虽说XFS 是高性能的文件系统,但是那只是针对大文件而言的,对于小文件XFS 就有点力所不能及 (当然,这是相对而言).\n所以如果是需要 经常处理大文件的服务器,XFS 会是一个很好的选择\n4 小结 如果将Linux 的文件系统进行分类的话,还可以分成 FUSE (Filesystem in Userspace, 让用户在没有权限的情况下,创建自己的文件系统), Stackable file System (先进的多层次统一文件系统), Read-only file systems (只读文件系统), Clustered file systems (集群文件系统)等等。\nLinux 的文件系统还有很多,每一种都有自己的特点;但是如果你想问:\u0026ldquo;Linux 最好的文件系统是什么?\u0026quot;;这个问题就跟 \u0026ldquo;最好的Linux 发行版本是什么?\u0026rdquo;, \u0026ldquo;最好的文本编辑器是什么?\u0026rdquo; 一样,是没有标准答案,一千个人都有一千个哈姆雷特了,你的哈姆雷特是什么样子,只有你自己清楚。\n不同的文件系统对应不同的场景,只有针对特定场景的最优解决方案!如果还不知道怎么选择,那就选择Ext4 吧,无法做出选 择时,默认的就是最好 :)\n5 参考 https://en.wikipedia.org/wiki/File_system https://btrfs.wiki.kernel.org/index.php/Main_Page https://en.wikipedia.org/wiki/JFS_(file_system) https://en.wikipedia.org/wiki/ReiserFS https://www.howtogeek.com/howto/33552/htg-explains-which-linux-file-system-should-you-choose/ https://en.wikipedia.org/wiki/ZFS https://en.wikipedia.org/wiki/XFS https://wiki.archlinux.org/index.php/file_systems ","permalink":"https://ramsayleung.github.io/zh/post/2017/linux_file_system/","summary":"不久前,Apple 的文件系统 (Apple File System) 新推出,然后各方便一起挤身向前对APFS \u0026ldquo;评头品足\u0026rdquo;,我是不了解 APFS ,所以也没有什么","title":"大话Linux文件系统"},{"content":"笔者最近一直在思考,关于工具,关于折腾,关于其中的付出与收获\n1 乐趣 1.1 Linux 回顾笔者大学,从大一开始就是一个不停折腾的过程,在其他的同学还在用Windows玩游戏的时候,笔者已经把系统换成Linux了.\n记得最开始装的第一个发行版本是 Kali Linux一个黑客和安全专家使用的发行版本,上面有不计其数的渗透工具;毕竟每一个学 计算机的孩子心中都是有个 hacker dream的嘛,笔者也不例外:)。\n只是笔者最开始并没有能力去使用Kali Linux; 甚至连基本的命令都完全不了解;笔者相当沮丧,因为 hacker并不是想象中的那么容易的. 笔者后来就把自己的系统重装,装了个 Ubuntu, 买 了一本《鸟哥的Linux私房菜》,一边学,一边用,就这样进了Linux的坑了。\n《鸟哥的私房菜》大概看了两年,翻过好几次了,后来也看了《服务器篇》,前后共看了近十本Linux的书籍吧,整个大学大概在自己电脑上前后装了10种的发行版本吧\n1.2 Vim/Emacs 《鸟哥Linux私房菜》一书中,鸟哥强推Vim, 其他的Linux论坛也对 Vim 推崇备至,笔者 很自然就随大流去学习Vim了,开始的时候,真的非常不习惯,编辑个文本还要分那么多 的模式,真的是反人类,连个单词都不能输入.\n后来,好不容易输完数据之后,又不知道怎么保存 (Ctrl-S? 想多了), 然后直接关闭,重新打开又有什么提示说是否恢复数据。 觉得为何有这样异类难用的编辑器, 真不知道为什么那么多人推崇。\n但当笔者坚持这种煎熬半个月以后,就发现其他的编辑器都非常低效,没错,就是非常低效,又要鼠标, 又要键盘,不断地切换,效率实在太低了。\n就这样,笔者糊里糊涂就进入了Vim的阵营,直到遇到 Vim实用技巧 这本神书,它跟你讲述了如何实现 Vim \u0026ldquo;Edit Text at the speed of thought\u0026rdquo; 的理念,的确是神书. 自然,笔者对Vim就更 \u0026ldquo;坚贞不渝\u0026rdquo; 了;\n直到有一天,在浏览Linux/Unix历史的时候掀开了 Editor War(Vim与Emacs之战)一章, 那些 Emacser 竟敢宣称 Emacs 比 Vim 好用,笔者对此并不服气,不相信有比Vim强的编辑器,这可是编辑器之神阿,而笔者是一个很实在的人,没用过 Emacs 是不会随便发 言的,所以就跑去折腾Emacs ,打算折腾回来再跟 Emacser 论道,结果嘛,笔者就 \u0026ldquo;叛 逃\u0026rdquo; 到了 Emacs 了 :)。\n作为一个曾经的 Vim 粉丝,笔者就抛开 \u0026ldquo;宗教因素\u0026rdquo; 比较一下 Vim 跟 Emacs:\nVim的 modal edit 是最好的,真的难有敌手,所以这也是为什么在各种的 IDE/editor 都有 Vim 插件的原因; 但 Emacs 的扩展性也是无可匹敌 (毕竟是伪 装成编辑器的操作系统,只缺一个好用的编辑器),又因为 Emacs lisp这种真正的编程 语言(对比之下 viml真的很弱)的存在, Emacs 就有了无限可能,这也是 Emacs 上面有非常多高质量的插件的原因之一,其中最典型的例子就是 Org-mode ,无愧神器之名,笔者现在的博文也是在 Emacs 里面利用 Org-mode 编写,然后发布的。 而至于选择神之编辑器还是编辑器之神,那就是信仰的抉择了。笔者选择了在 Emacs 里面使用 Vim 的编 辑模式 Evil :)\n1.3 Misc 除了折腾编辑器之外,笔者还折腾了各种的命令行,Shell 脚本,还有 Firefox, Chrome浏览器。当初那些 Windows 用户一直说 Linux 的桌面丑,笔者就去了折腾各种 的桌面环境 (window manager)这种折腾可不是 Windows 上面的切换壁纸哦,后来把桌 面折腾得非常炫,以至同学看到笔者的电脑就说我装了黑苹果,然而事实并非如此。\n如果你也好奇那些炫酷的 Linux/Unix 桌面,可以查看 https://reddit.com/r/unixporn 上面有各种 Linuxer/Unixer 分享的炫酷桌面\n2 投入产出比 2.1 值得否? 笔者的大学基本都是在学习并折腾各种的工具或者技术,并且乐在其中.\n但是有一天当笔 者又在跟朋友推荐 Vim/Emacs, 或许是笔者喋喋不休实在太多次了,朋友回了笔者一句 \u0026quot; notepad++, sublime text 不一样可以写代码,你为什么还要花那么多时间去折腾这些东西呢,你写脚本都可以直接用IDE,为什么还要自己折腾呢,把时间花到其他地方不更好么?\u0026quot;.\n笔者难以反驳,笔者之前一直是玩得很开心,从未曾考虑过这个问题,所以那个时候开始询问自己,这是否值得,自己是否要把时间用到其他地方?\n在之后的一段 时间,笔者都难掩沮丧,因为觉得自己浪费了很多的时间来完成一些无用功!\n2.2 长期投资 但是最终笔者还是解答了自己的疑问! 笔者之前付出是绝对值得的,先不说笔者在其中获得的乐趣,乐趣是无价的嘛 :)\n笔者在折腾的过程中也学到很多新的东西: 为了用好我配置的 Emacs, 笔者使用 Emacs 写了很多不同的脚本,这种感觉就好像,侠士为了展示手中利刃之威力,苦练武艺; 而在折腾 Emacs lisp 的过程中,也学习很多函数式编程的思想,甚至掌握了一门新的语言 \u0026ndash; elisp, 虽说它的语法很奇怪。\n其实笔者的付出是长期投资,学会了 Vim 的 moral edit, 也可以在其他 IDE使用嘛,这并不矛盾的,无鼠标操作是非常高效的,也是所谓的 modern editor 无法比拟的。\n最重要的是,在折腾过程中所培养的解决困难的动手能力,也是可以受益终生的,笔者知道如何去google,如何去查找文档,如何去提问; 而且在不停的折腾过程中,你对某样技术的理解是单纯的理论学习无法比拟的;\n在大学的操作系统课,笔者基本是没听老师讲解课程的,因为老师讲的,笔者基本都知道,甚至实践过。\n3 工具集 在经历大学的折腾后,笔者现在很多的工具集都基本确定下来了;这些也是对笔者而言, 最高效的工具集\n3.1 编辑器 Emacs 神之编辑器,主力编辑器 个人配置 Vim 编辑器之神,一般在服务器改改配置的时候用 3.2 浏览器 Chrome 不常用,特定情况下使用 Firefox 日常浏览器,笔者也折腾过非常久,所以即使 Chrome 很强,笔者只为 Firefox 倾心 3.3 FireFox扩展 因为 FireFox 对插件的限制相对宽松,所以社区开发出了非常多非常强的插件,笔者就 列举一下自己使用的扩展集吧\nbitwarden -免费的密码管理器,比LastPass强 Bluhell Firewall-轻量级的广告拦截器,和隐私保护 Clear Cache -更方便清除缓存 FalshGot -下载扩展器,配合axel或者aria2使用更佳 FoxyProxy -类似Chrome SwitchOmega,但是略有不如,配合Shadowsocks翻墙,必备 Ghostery -隐私保护 Greasemonkey -用户自定义插件管理器,神器 HttpRequester -类似Chrome Postman,发送Http请求 HTTPtoHTTPS -尽可能使用Https,提高安全性 KeySnail -把Firefox快捷键设置为Emacs快捷键,无鼠标操作,你也可以为该插件编 写插件.神器,这个是我无法切换回Chrome的原因 Octotree -以树状目录来浏览Github代码,非常方便 uBlock Origin -广告blocker,低资源要求,感觉比Adblock plus好用 User Agent Switcher -切换User Agent,写爬虫时非常有用 Xpath checker -直接获取Dom节点的Xpath,配合Lxml解析网页非常高效 Firebug -神器,但是已经停止开发了。 3.4 桌面 i3wm, 在折腾过炫酷的 KDE, Gnome, xfce, 而笔者最后选择的是 i3这个平铺桌面,可 以实现无鼠标操作,非常轻量。\n3.5 命令行 3.5.1 Shell zsh -配合oh-my-zsh,可以非常高效,但是使用频率不高 Eshell -与Emacs集成,是笔者的主力Shell,不过某些Eshell不支持的操作,只好在 zsh完成 3.5.2 过滤器 ag grep的加强版,速度快 ripgrep 最快的命令搜索工具 percol 过滤文本,神器 fasd 目录跳转,文件查找,高效 3.5.3 misc httpie http客户端,发送http请求 htop top的改进版,信息更详细 glances 一个好用的系统监控工具 ncdu Linux最好用的磁盘分析工具 git Linus又一神作 其它就是常用的内置命令了\n3.6 影音 VLC Linux最好用的播放器 网易云音乐 国产良心音乐软件 musicbox 网易云音乐的社区命令行版本 3.7 其它 Fcitx -中文输入 VirtualBox -开源虚拟机 Shadowsocks 翻墙必备 Zeal 类似Mac 上的Dash,查看各种文档 Intellij Idea Java IDE(写Java 我是不会使用Emacs 的:) ) Datagrip SQL IDE 使用最频繁的就是 I3+Firefox+Emacs,实现无鼠标操作,因为使用鼠标太慢了,效率太 低。笔者也不是一个疯子,所以只会用Emacs 做力所能及的事情,煮咖啡就算了。\n4 结语 如果让笔者的大学重来一遍,估计笔者还是会这样折腾,因为自己动手的感觉还是很美好,充满成就感,这也是玩游戏所不能给予我的感觉,毕竟 hacker 不是想出来的嘛,是做出来的。\n更新 2017-4-21\n附上一篇关于折腾的文章 (需翻墙) The importance of ZheTeng\nEnjoy tweaking;Enjoy Linux :) ","permalink":"https://ramsayleung.github.io/zh/post/2017/about_tool_about_tweak/","summary":"笔者最近一直在思考,关于工具,关于折腾,关于其中的付出与收获 1 乐趣 1.1 Linux 回顾笔者大学,从大一开始就是一个不停折腾的过程,在其他的同学还在用Wi","title":"关于工具,关于折腾"},{"content":"近两日,闲来无事,就写了些端口扫描器,重温TCP/IP协议栈的部分原理。\n1 端口扫描器 所谓的端口扫描器,其实是用来检测目标服务器有哪些端口开放所使用的工具,一般是管理员用来进行安全加固,检测是否有无意开放的端口;或者是恶意攻击的人员在进行攻击前的准备工作。\n所以综述上下,端口扫描器是用来确定目标机器 (本地机器或者远程机器)的特定服务的可用性\n2 端口扫描原理 上面提到过,端口扫描器是用来确定目标机器的服务的可用性的;那么具体是怎么确定的呢?如果还没有答案的话,可以换个角度来思考这个问题。\n假如你想确定邻居家的妹子是否在家,你会怎么办?这不简单么,问一下不就清楚了么?对阿,对于服务器的端口也可以适用这样的方法嘛。端口扫描的原理都是“问一下”,只是问的方法不一样而已,就好像你是决定直接过去敲邻居门,还是打电话过去一样,殊途同归,方法是没有对错的之分,差异只是方法的优劣。\n2.1 TCP连接扫描 这是最简单的一种方法,一般被称为连接扫描,即利用 socket 对目标机器进行连接尝试,如果能够成功建立三次握手连接,那就说明你用 socket 连接的端口是开放的;然后你就可以断开连接,扫描下一个目标端口了 (如果不断开连接,这就是一种 DDOS攻击了).\n只不过TCP连接扫描不是很常用,不仅是因为容易被发现,而且你的IP地址也可能会被目标地址记录下来的(对于攻击者来说,隐藏身份是很重要的)\n2.1.1 代码解析: def scan(self, args): host, port = args try: # Create a TCP socket and try to connect # AF_INET for ipv4,AF_INET6 for ipv6 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) sock.close() return host, port, True except (socket.timeout, socket.error): return host, port, False 因为原理很简单,所以核心代码也是很简洁的,只是建立 socket 然后进行连接,如果连接不上,就很大几率说明端口是关闭的 (并不是绝对的,例如socket超时的异常可能就是因为网络异常,不一定是目标机器的缘故)\n2.2 SYN扫描 再回顾一下TCP的三次握手:\n2.2.1 TCP三次握手 TCP建立连接时,首先客户端和服务器处于close状态。 然后客户端发送SYN同步位,此时客户端处于SYN-SEND状态,服务器处于lISTEN状态,当服务器收到SYN以后,向客户端发送同步位SYN和确认码ACK,然后服务器变为SYN-RCVD,客户端收到服务器发来的SYN和ACK 后,客户端的状态变成ESTABLISHED(已建立连接), 客户端再向服务器发送ACK确认码,服务器接收到以后也变成ESTABLISHED。然后服务器客户端开始数据传输 如图:\nFigure 1: 图来源于Google\n2.2.2 SYN扫描原理 SYN+ACK\n那么现在再回到SYN扫描上来.如果在发送第一次握手的 SYN flag 时,目标机器回复了SYN+ACK,这不就说明笔者发送的TCP包中的目标端口是开放的么!如果不开放,服务器就不会期待第三次握手了,也不会给笔者发送 SYN+ACK 了;如图:\n图来自 http://resources.infosecinstitute.com/port-scanning-using-scapy/\nRST\n如果第二次握手的时候,目标机器回复的不是 SYN+ACK, 而是 RST, 就说明TCP包中的目标端口在目标机器上是关闭的;如图\n图来自 http://resources.infosecinstitute.com/port-scanning-using-scapy/\nFiltered\n上面提及了目标端口的开放和关闭两种状态,那么,还有没有其他状态呢?什么,还有其他状态?\n如果就SYN扫描而言,就还有 filtered被过滤之一说,如果还有加上其他扫描技术, 就还有其他状态了。\n回到SYN扫描,当返回的不是服务器想建立第二次握手的包,而是ICMP的包就有可能被过滤,例如响应信息是ICMP错误信息类型3代码3(无法到达目标:端口不可达)这里出现的端口不可达,可能就是被防火墙过滤了,如果是类型3代码13(无法到达目标:通信被管理员禁止),那也是被过滤了。\n更多信息就要查询ICMP的官方文档 了\n2.2.3 代码解释 def scan(self, args): dst_ip, dst_port = args src_port = RandShort() answered, unanswered = sr(IP(dst=dst_ip) / TCP(sport=src_port, dport=dst_port, flags=\u0026#34;S\u0026#34;), timeout=self.timeout, verbose=False) for packet in unanswered: return packet.dst, packet.dport, \u0026#34;Filtered\u0026#34; for (send, recv) in answered: if(recv.haslayer(TCP)): flags = recv.getlayer(TCP).sprintf(\u0026#34;%\u0026#34;) if(flags == \u0026#34;SA\u0026#34;): # set RST to server in case of ddos attack send_rst = sr(IP(dst=dst_ip) / TCP(sport=src_port, dport=dst_port, flags=\u0026#34;R\u0026#34;), timeout=self.timeout, verbose=True) return dst_ip, dst_port, \u0026#34;Open\u0026#34; elif (flags == \u0026#34;RA\u0026#34; or flags == \u0026#34;R\u0026#34;): return dst_ip, dst_port, \u0026#34;Closed\u0026#34; elif(recv.haslayer(ICMP)): icmp_type = recv.getlayer(ICMP).type icmp_code = recv.getlayer(ICMP).code if(icmp_type == ICMP_TYPE_DESTINATION_UNREACHABLE and icmp_code in ICMP_CODE): return dst_ip, dst_port, \u0026#34;Filtered\u0026#34; else: return dst_ip, dst_port, \u0026#34;CHECK\u0026#34; 核心代码很简单,就是发送建立连接的握手请求,然后根据不同的返回结果判断不同的状态。\n如果端口确定是开放,那就发送 R flag给目标机器结束握手 (如果不结束握手的话,那就是DDOS,这也是DDOS最常用的手段); 因为这次不是使用操作系统原生的 socket, 而是自行构造发送 IP数据包,所以需要使用一个很强大的构造 操作各种数据包的工具 \u0026ndash; scapy\n(顺便说一下,如果在Windows下安装 scapy,需要非常多的步骤,如果是Unix/Linux,只需几行命令:) )\n3 后话 简单的扫描器就已经完成了,加上多线程的功能提高性能。\n很想吐嘈一下,真的对Python 的多线程恨铁不成钢,只好换成多进程;也给 Python2 Python3 API的改变折腾得够呛,不禁让笔者怀念起Java:(\n其实正如笔者开头所言的,你确定隔壁家妹子是否在家的方法有很多,你扫描端口的方法也有很多:例如 XMAS scan(TCP圣诞树扫描), FIN scan,Null scan, ACK scan, Window scan, UDP scan等。\n当然你如果不想针对各种扫描都写一个扫描器,你可以使用 nmap 这个地球最强大的扫描器 (没有之一). 在Python也已经有与nmap整合的强大的包 python-nmap\n扫描器完整代码地址 https://github.com/samrayleung/PortScanner\n参考\nhttp://resources.infosecinstitute.com/port-scanning-using-scapy/ ","permalink":"https://ramsayleung.github.io/zh/post/2017/port_scanner/","summary":"近两日,闲来无事,就写了些端口扫描器,重温TCP/IP协议栈的部分原理。 1 端口扫描器 所谓的端口扫描器,其实是用来检测目标服务器有哪些端口开放","title":"Python多线程端口扫描器"},{"content":"文本三剑客之 Grep\ngrep - print lines matching a pattern\n今天我想聊聊 grep 这个命令;据说,有Unix/Linux 的地方就会有 grep, 这个可能是安装得最广泛的命令之一;那么 grep 是用来干什么的呢?\ngrep 其实是用来在文件中搜索特定内容或者模式的工具(配合正则表达式“食用”,味道更佳 :))现在就来一起看看grep 的用法\n1 基本用法 1.1 基础用法 现在假设有一个简单的文本文件(双城记开头)tinytale.txt,内容如下\nit was the best of times it was the worst of times it was the age of wisdom it was age of foolishness it was the epoch of belief it was the epoch of incredulity it was the season of light it was the season of darkness IT WAS THE SPRING OF HOPE IT WAS THE WINTER OF DESPAIRE\n现在开始介绍 grep 的基本用法: grep 的基本用法很简单的,假设我想要搜索单词 darkness\ngrep darkness /tmp/tinytale.txt 输出如下:\nit was the season of light it was the season of darkness 1.2 结合正则表达式 默认情况下, grep 是开启正则表达式的模式的,所以你可以直接在文件搜索中使用 正则表达式。现在在文件中搜索以字母 e 开头后接三个字符,然后以 h 结尾的单词:\ngrep \u0026#34;e...h\u0026#34; /tmp/tinytale.txt 输出如下:\nit was the epoch of belief it was the epoch of incredulity 可以看到,正则表达式匹配了 epoch 这个单词。正则表达式的威力无与伦比的,把 grep和正则表达式结合起来可以更好地发挥 grep 这个工具的潜力;而本文主要是介绍 grep, 更多有关正则表达式的用法不细讲了\n1.3 统计出现的次数 有时,如果你需要统计某种模式或者某个单词出现的个数,你会发现 grep 非常有用;\n要实现该功能,只需给 grep 添加 -c 参数;例如统计单词 the 出现的个数:\ngrep -c the /tmp/tinytale.txt 结果输出如下:\n4 文本中包含4个 the\n1.4 忽略大小写 前面提到, grep 默认是使用正则表达式来搜索文件的,所以 grep 是区分大小写的;\n如果你想修改 grep 的默认行为来忽略大小写,你可以添加 -i 参数\ngrep -i the /tmp/tinytale.txt 输出结果如下:\nit was the best of times it was the worst of times it was the age of wisdom it was age of foolishness it was the epoch of belief it was the epoch of incredulity it was the season of light it was the season of darkness IT WAS THE SPRING OF HOPE IT WAS THE WINTER OF DESPAIRE 可以发现 THE 也是可以被 grep 搜索到的;但是如果没有添加 -i ,你只会看到4行输出。\n当然你可以在正则表达式里面添加忽略大小写的模式,只是直接添加 -i 会简单很多。\n2 搜索多个文件 上面搜索的都只是单个文件,而 grep 可以让你同时搜索多个文件;现在就来看看怎么搜索多个文件吧。\n下面两种写法结果都是一样的,但是我个人推崇第一种,因为可以输入更少一些内容 :)\ngrep belief /tmp/{tinytale.txt,tale.txt} grep belief /tmp/tinytale.txt /tmp/tale.txt 输出结果如下:\ntinytale.txt:it was the epoch of belief it was the epoch of incredulity tale.txt:it was the epoch of belief it was the epoch of incredulity tale.txt:pains of by rearing her in the belief that her father was dead tale.txt:this was no passive belief but an active weapon which they flashed tale.txt:belief in solomon deducting a mere trifle for this slight mistake tale.txt:you will bear testimony to what i have said and to your belief in it tale.txt:herself into the show of a belief that they would soon be reunited 可以看到, grep 把匹配到单词的那一行内容和对应的文件都显示出来了,你就可以很方便地看到搜索结果,并知道匹配单词的来源。\n如果你也像我这样,不想输入那么多的内容,你可以使用正则表达式匹配所有的文本文件,如下:\ngrep belief /tmp/*.txt 输出结果也会跟上面一致 (假设你 tmp 目录下只有两个文本文件); 我告诉grep 搜索**/tmp** 下所有的 .txt 文件。\n2.1 递归搜索 你也可以使用 grep 递归搜索目录;你只需在指定目录后,添加 -R , grep 就会 递归搜索指定目录的所有子目录。我已经把当前目录切换到 /tmp:\ngrep -R \u0026#34;belief\u0026#34; . 输出结果如下:\n./tale.txt:it was the epoch of belief it was the epoch of incredulity ./tale.txt:pains of by rearing her in the belief that her father was dead ./tale.txt:this was no passive belief but an active weapon which they flashed ./tale.txt:belief in solomon deducting a mere trifle for this slight mistake ./tale.txt:you will bear testimony to what i have said and to your belief in it ./tale.txt:herself into the show of a belief that they would soon be reunited ./tinytale.txt:it was the epoch of belief it was the epoch of incredulity 结果展示了一系列在当前目录和子目录匹配 belief 的文件。此外你也可以排除掉某些你 不需要搜索的文件,例如有一个 foo.xml 的文件,里面也可能会有 belief 这个单词, 但是你就是不想搜索这个文件,或者全部的 .xml 文件,你可以这么玩:\ngrep -R --exclude=\u0026#34;*.xml\u0026#34; \u0026#34;belief\u0026#34; . 3 在标准输入搜索 grep 也是过滤器,所以 grep 自然而然具有处理标准输入输出的能力了;处理其他命令的输出结果也是 grep 非常常用的场景之一。假设你现在的 vim 突然卡顿,挂了:),你想要 kill 掉 vim 的进程,你可以:\nps -e|grep vim 结果输出如下:\nsamray 21939 1 0 19:42 ? 00:00:00 gvim 其中第一条记录就是你想要搜索的进程了,你运行 kill 21939 就可以杀掉 vim 的进程了;因为我系统的是图型化界面的 vim, 所以是 gvim.\n正如我之前的文章提到的那样,单纯的过滤器的用处似乎不大,但是如果结合起来就会威力无穷至于,如何结合,就需要慢慢探索了。\n4 反向搜索 现在执行的搜索都是匹配搜索,即将匹配的内容显示出来,而 grep 还有反向搜索的功能 (invert Searches)就是将不包含有指定模式的内容显示出来。\n该功能在用来修改有很多注释的配置文件时特别有用;例如常用的服务器软件 nginx 的配置文件是默认是含有很多注释的,如下\n#user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 1024; # multi_accept on; } http { ## # Basic Settings ## sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # server_tokens off; # server_names_hash_bucket_size 64; # server_name_in_redirect off; include /etc/nginx/mime.types; default_type application/octet-stream; ## # SSL Settings ## ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; ## # Logging Settings ## log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; $status $bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#34;$http_user_agent\u0026#34; \u0026#34;$gzip_ratio\u0026#34;\u0026#39;; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; ## # Gzip Settings ## gzip on; gzip_disable \u0026#34;msie6\u0026#34;; # gzip_vary on; # gzip_proxied any; # gzip_comp_level 6; # gzip_buffers 16 8k; # gzip_http_version 1.1; # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; ignore_invalid_headers on; client_header_timeout 240; client_body_timeout 240; send_timeout 240; client_max_body_size 100m; proxy_buffer_size 128k; proxy_buffers 8 128k; upstream tomcat_server{ server 127.0.0.1:8080 fail_timeout=0; } upstream gunicorn_server{ server 127.0.0.1:5000 fail_timeout=0; } server{ server_name 127.0.0.1; listen 443; # ssl on; # ssl_certificate /etc/letsencrypt/live/samray.ren/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/samray.ren/privkey.pem; location / { # Forward SSL so that Tomcat knows what to do proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://tomcat_server; proxy_set_header X-Forwarded-Proto https; proxy_redirect off; proxy_connect_timeout 240; proxy_send_timeout 240; proxy_read_timeout 240; } location /test{ return 402; } location /weixin { # try_files $uri @proxy_to_app; return 402; } location @proxy_to_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://gunicorn_server; } } } #mail { #\t# See sample authentication script at: #\t# http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript # #\t# auth_http localhost/auth.php; #\t# pop3_capabilities \u0026#34;TOP\u0026#34; \u0026#34;USER\u0026#34;; #\t# imap_capabilities \u0026#34;IMAP4rev1\u0026#34; \u0026#34;UIDPLUS\u0026#34;; # #\tserver { #\tlisten localhost:110; #\tprotocol pop3; #\tproxy on; #\t} # #\tserver { #\tlisten localhost:143; #\tprotocol imap; #\tproxy on; #\t} #} 里面实在有太多的注释了,虽说是很好的参考,但是看多了会感觉很碍眼,所以你希望可以有一份没有注释的配置文件,你就可以使用 grep 和参数 -v:\negrep -v \u0026#34;#|^$\u0026#34; /etc/nginx/nginx.conf \u0026gt;/tmp/nging.conf 结果如下:\nworker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 1024; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; ssl_prefer_server_ciphers on; log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; $status $bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#34;$http_user_agent\u0026#34; \u0026#34;$gzip_ratio\u0026#34;\u0026#39;; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; gzip_disable \u0026#34;msie6\u0026#34;; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; ignore_invalid_headers on; client_header_timeout 240; client_body_timeout 240; send_timeout 240; client_max_body_size 100m; proxy_buffer_size 128k; proxy_buffers 8 128k; upstream tomcat_server{ server 127.0.0.1:8080 fail_timeout=0; } upstream gunicorn_server{ server 127.0.0.1:5000 fail_timeout=0; } server{ server_name 127.0.0.1; listen 443; location / { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://tomcat_server; proxy_set_header X-Forwarded-Proto https; proxy_redirect off; proxy_connect_timeout 240; proxy_send_timeout 240; proxy_read_timeout 240; } location /test{ return 402; } location /weixin { return 402; } location @proxy_to_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://gunicorn_server; } } } egrep 是 grep 的扩展,你也可以通过 -E 使用扩展功能。就这样,你就可以得到一份很“干净”的配置文件了。\n5 小结 在以前 grep 是 hacker 工具箱里面审查源代码必不可少的工具之一,但是随着技术的发展,似乎对比其他同类型的工具, grep 的性能已经难尽人意,特别是对比 ag 这个搜索神器;\n虽说很多人都已经转移到了 ag 阵营,但是因为 grep 被广泛预装到各类的Linux/Unix 机器,所以 grep 还是使用得很广泛滴。\n更多 grep 的用法就需要查询手册了:\nman grep Enjoy Shell :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/grep/","summary":"文本三剑客之 Grep grep - print lines matching a pattern 今天我想聊聊 grep 这个命令;据说,有Unix/Linux 的地方就会有 grep, 这个可能是安装得最广泛的命令之一;那么 grep 是用来","title":"Linux/Unix Shell 二三事之过滤器grep"},{"content":"今天在完成《算法》上的练习的时候,要对文件进行读写,而书上的例子是直接通过 Linux/Unix的重定向来实现的,我要把它修改成直接读取文件。\n此外,个人一直觉得Java IO 很容易混淆,因为有太多的选择(但是这也是Java 的强大之处),现在Java8 又新增了文件的API,所以我就对文件IO作了个小结\n1 Read 我今天的需求是要逐行读写文本文件,我就以此为例子了;测试文件是 /tmp/test.txt\ntest this is a test this is another test 1.1 BufferedReader 虽然已经有了Java8的 Stream, 但是经典的东西总是历久弥新的;例如 BufferedReader就是JDK1.1就发布了的文件读API (对可能出现的IOException,使用更优雅try-with-resource并免去编写大量手动关闭资源的模板代码的麻烦)\npublic void testBufferedReader(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; try(BufferedReader bufferedReader=new BufferedReader(new FileReader(filePath))){ String line; while((line=bufferedReader.readLine())!=null){ System.out.println(line); } }catch (IOException ex){ ex.printStackTrace(); //do something } 1.2 Scanner 对发布于JDK1.5的Scanner,大部份Java 程序员都是相当熟悉的,因为总是用它来读取标准输入的数据。 现在只要把从标准输入变为从文件读取数据就可以了\npublic void testScanner(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; try(Scanner scanner=new Scanner(new File(filePath))){ while(scanner.hasNextLine()){ System.out.println(scanner.nextLine()); } }catch (IOException ex){ ex.printStackTrace(); //do something } } 1.3 BufferedReader+Stream Files 类作为Java NIO 的一部分在Java 7被引入,该类提供了一系列操作文件的方法,而在Java8 又引入了另外有用的特性让Java 开发者可以更方便地操作文件。\n例如 lines() 方法,可以让 BufferedReader 可以把文件内容以 Stream 的形式返回;读取文件, 并把文件内容存储到 ArrayList.\npublic void testBufferedReaderAndStream(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; list=new ArrayList\u0026lt;\u0026gt;(); try(BufferedReader bufferedReader= Files.newBufferedReader(Paths.get(filePath))){ list=bufferedReader.lines().collect(Collectors.toList()); }catch (IOException ex){ ex.printStackTrace(); //do something } } 得益于强大的 Stream 你可以在读取文件是进行更多的操作;例如只存储含有 this 字符的行并且删除结尾的空白符\npublic void testBufferedReaderAndStream(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; list=new ArrayList\u0026lt;\u0026gt;(); try(BufferedReader bufferedReader= Files.newBufferedReader(Paths.get(filePath))){ bufferedReader.lines().filter(line-\u0026gt;line.contains(\u0026#34;this\u0026#34;)).map(String::trim) .forEach(System.out::println); }catch (IOException ex){ ex.printStackTrace(); //do something } } 1.4 lines+Stream 也可以直接使用 lines 方法来逐行读取文本文件,只是对比 newBufferedReader + Stream, 前者颗粒度更细;\npublic void testlinesAndStream(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; list=new ArrayList\u0026lt;\u0026gt;(); try(Stream\u0026lt;String\u0026gt; stringStream=Files.lines(Paths.get(filePath))){ stringStream.filter(line-\u0026gt;line.contains(\u0026#34;test\u0026#34;)).forEach(System.out::println); }catch (IOException ex ){ ex.printStackTrace(); //do something } } 如果你是读取不是很大的文件的时候,你可以一次就把文件都进内存; Files 已经为你提供这样的方法\npublic static void testReadAllLines(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; lists= null; try { lists = Files.readAllLines(Paths.get(filePath)); } catch (IOException e) { e.printStackTrace(); } for (String list : lists) { System.out.println(list); } } 需要注意的是 try-with-resource 是不支持 readAllLines .此外大文件请慎重使用 readAllLines,因为你可能出现 OutOfMemoryException\n不得不说,新加入的API的确更加优雅\n2 Write 我就把测试文件重新写到一个新的文件,实现复制的功能,因为我的文件很小,所以我直接把测试独的文件加载到内存\n2.1 BufferedWriter 与 BufferedReader 对应,对文件进行写\npublic void testBufferedWriter() { String readFilePath = \u0026#34;/tmp/test.txt\u0026#34;; String writeFilePath = \u0026#34;/tmp/test1.txt\u0026#34;; try { List\u0026lt;String\u0026gt; lines = Files.readAllLines(Paths.get(readFilePath)); try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(writeFilePath))) { for (String line : lines) { bufferedWriter.write(line+\u0026#34;\\n\u0026#34;); } } catch (IOException ex) { ex.printStackTrace(); //do something } } catch (IOException ex) { ex.printStackTrace(); //do something } } 你也可以将 BufferedReader 和 Files 结合\npublic static void testBufferedWriterAndFiles() { String readFilePath = \u0026#34;/tmp/test.txt\u0026#34;; String writeFilePath = \u0026#34;/tmp/test1.txt\u0026#34;; try { List\u0026lt;String\u0026gt; lines = Files.readAllLines(Paths.get(readFilePath)); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(Paths.get(writeFilePath))) { for (String line : lines) { bufferedWriter.write(line + \u0026#34;\\n\u0026#34;); } } catch (IOException ex) { ex.printStackTrace(); //do something } } catch (IOException ex) { ex.printStackTrace(); //do something } } 2.2 Files.write 使用 Files.write() 也可以写出相当优雅的代码\npublic void testFilesWrite() { String readFilePath = \u0026#34;/tmp/test.txt\u0026#34;; String writeFilePath = \u0026#34;/tmp/test1.txt\u0026#34;; try { List\u0026lt;String\u0026gt; lines = Files.readAllLines(Paths.get(readFilePath)); Files.write(Paths.get(writeFilePath), lines); } catch (IOException ex) { ex.printStackTrace(); //do something } } 这就是各种对文本文件进行读写的方法;不知道为什么,我觉得似乎写文件的方法似乎比读文件的方法少,例如读文件有 Scanner , 而写文件似乎没有 Printer :(\n不应该是匹配的么,或许我是不知道?\nEnjoy Java :)\n3 参考 http://winterbe.com/posts/2015/03/25/java8-examples-string-number-math-files/ http://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#lines-java.nio.file.Path- https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html ","permalink":"https://ramsayleung.github.io/zh/post/2017/java8_file_io/","summary":"今天在完成《算法》上的练习的时候,要对文件进行读写,而书上的例子是直接通过 Linux/Unix的重定向来实现的,我要把它修改成直接读取文件。","title":"Java读写文件小结"},{"content":"最近,我发现很多Emacs 用户对Ivy 很感兴趣;而且大部份用户都是已经了解过Helm 或者Ido的 当有人在Reddit 上面问 选择Helm 还是Ido这类问题的时候,我觉得我会给出我自己的选择: Ivy,即使我是一个前Helm 的狂热用户 最大或者最小\nHelm 和Ivy 都是补全框架.这意味着它们都是Emacs生态系统中用来在用户输入后缩窄可供选择选项的范围的工具。 很自然而然想起的通用例子就是搜索文件。Helm 和Ivy 都可以帮助用户快速搜索文件\n它们两者都是框架,这意味着它们都可以用在那些需要补全或者缩窄范围的复杂命令。\n例如Helm 有一个命令(helm-google-suggest)可以模拟Goole 的搜索框,并在你输入时给出相应的google 提示\nIvy 和Helm 都有相同的目标,但是它们实现的方法却是迥然不同\n现在我想站在用户的角度来比较一下这两个工具。我这里指的用户观点是我在不需要了解Helm 和Ivy 的内部工作原理的前提下对这两个工具进行比较。\n其实,因为我对 elisp还谈不上精通,所以也没办法就两者实现细节来进行比较。但是这两个工具我都使用过,所以我可以从用户的角度,跟你分享我使用它们的不同感受。最后,我从Helm 切换到了Ivy\n我想先谈Helm.当我使用Spacemacs 的时候,我学会了怎么使用Helm,以Helm 的方式思考, 如何自定义Helm,怎么把Helm 配置得称心如意。\n我想我应该算得上是一个中级的Helm 用户吧。我有读过这篇文章 还有这篇文章 以及Wiki 此外,在长达一年的时间里,我每天都是使用Helm的\nHelm 是一个非常成熟的工具.根据git 的提交历史,Helm 的开发工作是在2009年左右开始的。 在写这篇文章的时候,Helm 官方的git 仓库有超过26000行elisp 代码\ngit clone https://github.com/emacs-helm/helm.git cd helm cat *.el | wc -l # =\u0026gt; 26431 这还是没有把在MELPA 上查询到跟Helm 有关的包有142个的情况考虑在内的呢。\n你可以用Helm来完成任何事情它主要的强大之处在于你可以把Helm 和很多Emacs 的行为整合在一起。你可以以Helm 为中心构造接口,就像Spacemacs 做的那样。Helm 支持非常一致的接口,你可以通过Helm 来做任何事\n你可以搜索文件,搜索缓冲区,搜索颜色,搜索项目,搜索你最近编辑过的文件,搜索系统进程, 搜索音乐,搜索网络资源,搜索补全,搜索代码片段,搜索正则表达式,搜索命令,文档 相关描述,手册\u0026hellip;.\n你可以用Helm-projectile(一个Helm 对projectile 非常好的包装)来管理你的项目。你可以用gitignore.io来生成gitignore文件,你可以用Helm-bibtex来管理你的参考书目,你可以浏览你的火狐书签\n你可以用Helm 来完成任何事。\n基于 tuhdo 对我在Reddit 上面问题的回复,我想指出的一个特性就是Helm 是不使用 minibuffer,但是Ivy 是使用的。\n所以它可以被配置成总是在当前打开的窗口展示。对于那些大屏幕显示器的用户而言,这个特性真的非常有用,因为你的目光不用在 minibuffer 来回切换:\nFigure 1: 补全结果总是显示在同一个窗口\n最终的比较结果是Helm 是非常便利的工具,相信会有数量非常多的Spacemacs 用户告诉你同样的看法。\n而Helm 主要的缺点就是它的代码量太大了。我想虽然Helm 的代码量很大,但是它的开发者利用 elisp 成功把它打造成了一个相当快的工具了\n而且有些时候,Helm 似乎把简单的问题复杂化了;它配置起来也感觉相当臃肿;有时它也会有一些很奇怪的表现,然后导致卡顿,或者让Emacs 过载,即使你做的只是很简单的查询。\n或许那些Helm 的高手用户看到这里,会觉得如果我也是个 elisp 高手,就不会出现上述问题了。虽然我已经使用Helm 超过一年了,我还是没有找到方法让可以Helm更加稳定。我觉得Helm 在用自己做例子来讲述了什么是化简为繁吧\n你可以用Helm 来做任何事;但事实上你并不需要。你可以这样做并不意味着你应该这样做。\n在使用Helm 一年以后,我可以告诉你我只是使用了Helm 三分之一或者更小的功能。有些功能我觉得真的很棒,昨天在读了这篇文章 之后,我又发现了一些新的东西。大部分时间,我都是使用简单的命令来切换缓冲区,或者列举文件\nHelm 只是一个用来补全的包,就好像Ido或者Ivy.它可能很容易使用,一旦有人经历过配置它的困难,就会发现它很难做到让你随心所欲。\n有些人觉得只要可以让他们使用好的工具,即使他们完全不了解这些工具也无所谓。\n但是我就做不到\n\u0026ndash;abo-abo,Ivy 的开发者,回答\u0026ldquo;为什么不选择Helm\u0026rdquo; 这个问题\nIvy 为实现最小化,简单化,可定制化,可发现化而努力.这四个形容词告诉我们很多Helm 和Ivy 这两个工具间不同的设计理念。阅读Ivy介绍 以便更好了解Ivy的理念。\n在写这篇文章的时候,Ivy 只有大概3400行代码,为Ivy 所打造的生态系统:即Swipter 和 Counsel 也只有7500 行代码\ngit clone https://github.com/abo-abo/swiper.git cd swiper ## Only ivy ? cat ivy.el | wc -l # =\u0026gt; 3442 ## count lines of code into the whole swiper ecosystem cat *.el | wc -l # =\u0026gt; 7526 Ivy 真的是很容易上手,下面就是我的全部配置:\n(use-package ivy :ensure t :diminish (ivy-mode . \u0026#34;\u0026#34;) :bind (:map ivy-mode-map (\u0026#34;C-\u0026#39;\u0026#34; . ivy-avy)) :config (ivy-mode 1) ;; add ‘recentf-mode’ and bookmarks to ‘ivy-switch-buffer’. (setq ivy-use-virtual-buffers t) ;; number of result lines to display (setq ivy-height 10) ;; does not count candidates (setq ivy-count-format \u0026#34;\u0026#34;) ;; no regexp by default (setq ivy-initial-inputs-alist nil) ;; configure regexp engine. (setq ivy-re-builders-alist ;; allow input not in order \u0026#39;((t . ivy--regex-ignore-order)))) Ivy 是很低调的;它不想让你把一切都整合到Ivy去。它仅仅是提供你必需的补全。你不能像Helm 那样用Ivy 来做任何事;那为什么我还要切换到Ivy 去呢?\n虽然Ivy 已经最小化,但是我依然可以用Ivy 来代替我绝大部分日常使用的Helm命令。\n因为Ivy是如此简洁, abo-abo 在它上开发了一个叫 Counsel 的包; Counsel 可以为你提供非常非常多像你在Helm使用的命令\n你可以切换缓冲区,搜索文件,在项目级别进行搜索和替换,与Projectile 整合,搜索你最近 编辑过的文件,搜索Emacs 命令,搜索文档,搜索按键绑定,浏览 kill-ring\n让我向你介绍我是怎样用Ivy 代替Helm 的。下面是我对那些我需要使用Ivy 来代替Helm的最常用命令的总结。\n这些基本是我一直以来最常用的方法。我每分钟会使用三次的 ivy-switch-buffer ,我一天会使用五次的 helm-swoop, swiper 跟 helm-swoop 不分伯仲;\n对于那些大文件, Counsel 有 counsel-grep-or-swiper.\n我已经用一些非常非常大的标记语言的文件(一百万行左右)来测试过了,一点问题也没有。\nHelm Ivy What ? helm-mini ivy-switch-buffer search for currently opened buffers helm-recentf counsel-recentf search for recently edited files helm-find-files counsel-find-files search files starting from ./ helm-ag counsel-ag search regexp occurence in current project helm-grep-do-git-grep counsel-git-grep search regexp in current project helm-swoop swiper search string interactively in current buffer helm-show-kill-ring counsel-yank-pop search copy-paste history helm-projectile counsel-projectile search project and file in it helm-ls-git-ls counsel-git search file in current git project helm-themes counsel-load-theme switch themes helm-descbinds counsel-descbinds describe keybindings and associated functions helm-M-x counsel-M-x enhanced M-x command 我觉得你可以看到Ivy 基本的命令对比Helm 的命令也是毫不逊色的。它们可以代替你日常使用的每一条Helm命令。我不是说你可以像Helm 那样用Ivy 来做任何事,但是它已经足够好用了,正如我说的那样,你也不需要任何事都使用Helm 来完成。\n说到补全理念这个话题上,Helm 和Ivy 之间的差异并没有那么大。作为一个用户,我可以告诉你的是:Ivy 会让你感觉到更少的臃肿,更加的直观,更加地容易理解。每一次的补全都是可以预见的。\n最后,这真的跟个人的品味有关。对于我自己来说,\u0026ldquo;Ivy 还是Helm\u0026rdquo; 这样的争论跟\u0026quot;Emacs 还是Spacemacs\u0026quot; \u0026ldquo;Emacs 还是Ide\u0026rdquo; \u0026ldquo;C 还是Java\u0026rdquo; \u0026ldquo;简洁还是全能\u0026rdquo; \u0026ldquo;Thelonious 还是 Duke\u0026rdquo;(译者注,两者都是爵士乐作曲家),\u0026ldquo;Van Der Rohe 还是 Gaudi.\u0026quot;(译者注:前者是德国美国 的建筑风格,后者是西班牙加泰罗尼亚的建筑风格)这样的争论是非常相似的。\n你选择Helm呢,你会得到一个巨型的包,一系列你不会用到的特性,一堆你可能只是偶尔用一下的功能,一些你会一个小时使用50次的特性。如果你选择Ivy,你会得到一个只拥有那些让你顺心的必要特性的精简的包,你可以很容易地通过 Counsel 或者简单的函数对它进行扩展\n(ivy-read \u0026#34;Pick:\u0026#34; (mapcar #\u0026#39;number-to-string (number-sequence 1 10))) 如果你想要通过Helm 来扩展:\n(helm :sources (helm-build-sync-source \u0026#34;one-to-ten\u0026#34; :candidates (mapcar #\u0026#39;number-to-string (number-sequence 1 10)) :fuzzy-match t) :buffer \u0026#34;*helm one-to-ten*\u0026#34;) 或者简单的列表:\n(helm-comp-read \u0026#34;Pick:\u0026#34; (mapcar #\u0026#39;number-to-string (number-sequence 1 10))) Helm 为用户作了非常多的决定,Ivy 让用户按需求进行定制;Helm 通过耗费非常多的内存来变得快速,Ivy 通过保持简洁来实现快速;Helm 很成熟,Ivy 很青涩;Helm 为Emacs 提供一致性,Ivy 为Emacs 提供简洁性和可预见性;Helm 需要你进行一定的配置,Ivy 开箱即用\n我自己是稍偏向Ivy 的,因为我正在使用它; 它更符合我的口味。但是作为一个用户,Helm和Ivy并没有那么大的差别。它们都是非常优秀的包,只是以不用的方式去实现相同的目标\n原文地址 https://sam217pa.github.io/2016/09/13/from-helm-to-ivy/\n在下翻译水平有限,如有错误,还请指出\n","permalink":"https://ramsayleung.github.io/zh/post/2017/from-helm-to-ivy/","summary":"最近,我发现很多Emacs 用户对Ivy 很感兴趣;而且大部份用户都是已经了解过Helm 或者Ido的 当有人在Reddit 上面问 选择Helm 还是I","title":"(翻译)从Helm到Ivy"},{"content":"我平时也有浏览各类博客的习惯,毕竟三人行则必有我师嘛。今天在浏览关于Java的一个博客的时候,对博主的观点有一些不同的开发,但是困于没法在博客下评论,内容如下: 所以打算聊聊Java 中Collection 这个话题。(BTW,窃以为博主对Java8 新引进的Lambda, 应该了解不足)\n1 Java函数式编程 Java8 引进了函数式编程的新特性,让Java的开发人员也可以享受函数式编程的美妙,已经有很多的文章介绍函数式了,珠玉在前,我就不赘言了。\n来说说Java 的Lambda吧:Java8 对核心类库进行了改进,只要包括集合类的API和新引入的流(Stream), 流可以让开发者站在更高的 抽象层次对集合进行操作\n2 流的常用操作 2.1 collect(toList()) collect(toList()) 方法可以由Stream 值生成一个List,而Stream 的of方法可以使用初始值生成新的Stream.\nList\u0026lt;String\u0026gt; collected= Stream.of(\u0026#34;this\u0026#34;,\u0026#34;is\u0026#34;,\u0026#34;a\u0026#34;,\u0026#34;list\u0026#34;).collect(Collectors.toList()); 2.2 map 如果有一个函数可以将一种类型的值转换成另外一种类型,map 操作就可以使用该函数,将一个流中的值转换成一个新的流 2.2.1 例子 将字符变成大写格式如果用没有Lambda 时的模式编程\nList\u0026lt;String\u0026gt; oldStyle=new ArrayList\u0026lt;\u0026gt;(); for(String string : Arrays.asList(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;)){ String uppercaseString=string.toUpperCase(); oldStyle.add(uppercaseString); } 但是如果你有了Lambda\nList\u0026lt;String\u0026gt; lambdaStyle=Stream.of(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;).map(string -\u0026gt; string.toUpperCase()) .collect(Collectors.toList()); 真的有种说不出的优雅\n2.3 filter 遍历数据并检查其中的元素\n2.3.1 例子 你有一个User类,然后你想找出年龄大于30岁的用户\nprivate static List\u0026lt;User\u0026gt; users = Arrays.asList( new User(1, \u0026#34;Steve\u0026#34;, \u0026#34;Vai\u0026#34;, 40), new User(4, \u0026#34;Joe\u0026#34;, \u0026#34;Smith\u0026#34;, 32), new User(3, \u0026#34;Steve\u0026#34;, \u0026#34;Johnson\u0026#34;, 57), new User(9, \u0026#34;Mike\u0026#34;, \u0026#34;Stevens\u0026#34;, 18), new User(10, \u0026#34;George\u0026#34;, \u0026#34;Armstrong\u0026#34;, 24), new User(2, \u0026#34;Jim\u0026#34;, \u0026#34;Smith\u0026#34;, 40), new User(8, \u0026#34;Chuck\u0026#34;, \u0026#34;Schneider\u0026#34;, 34), new User(5, \u0026#34;Jorje\u0026#34;, \u0026#34;Gonzales\u0026#34;, 22), new User(6, \u0026#34;Jane\u0026#34;, \u0026#34;Michaels\u0026#34;, 47), new User(7, \u0026#34;Kim\u0026#34;, \u0026#34;Berlie\u0026#34;, 60) ); 非函数式编程(旧式):\nList\u0026lt;User\u0026gt; olderUsers = new ArrayList\u0026lt;User\u0026gt;(); for (User u : users) { if (u.age \u0026gt; 30) { olderUsers.add(u); } } 函数式编程:\nList\u0026lt;User\u0026gt; olderUsers = users.stream().filter(u -\u0026gt; u.age \u0026gt; 30).collect(Collectors.toList()); 2.4 flatMap flatMap 方法可用Stream 替换值,然后将多个Stream 连接成一个Stream 2.4.1 例子 假设有一个包含多个列表的流,希望得到所有数字的序列\nList\u0026lt;Integer\u0026gt; together=Stream.of(Arrays.asList(1,2),Arrays.asList(3,4)) .flatMap(numbers-\u0026gt;numbers.stream()).collect(Collectors.toList()); 还有其他常用的操作,我就不一一列举了,官方Quick Start有更详细的介绍。\n但是就我谈到的几种操作,应该可以对那位博主朋友的博文做出回应了,最有效优雅过滤一个Collection 的方法,我觉得是Stream 的filter\nList\u0026lt;String\u0026gt; filterExample=Stream.of(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;).filter(string-\u0026gt;string.equals(\u0026#34;a\u0026#34;)).collect(Collectors.toList()); 2.5 小结 Java Lambda的特性如果不经常使用,很容易又忘了,本文就当是对Java Lambda 的一次review吧\n不过,函数式的引用的确让Java 焕发出新的活力,记得之前一位前辈吐嘈Java语法太啰嗦,现在前辈应该会用得舒心一点吧\n备注:上面的图都是来自《Java8 函数式编程》 一书\n参考:Java8 函数式编程\n","permalink":"https://ramsayleung.github.io/zh/post/2017/java_collection_lambda/","summary":"我平时也有浏览各类博客的习惯,毕竟三人行则必有我师嘛。今天在浏览关于Java的一个博客的时候,对博主的观点有一些不同的开发,但是困于没法在博","title":"Lambda与Java Collection有感"},{"content":"如果你足够幸运(或者不幸运,取决于你怎么看待了)可以使用 git 作为你工作流的一部分。\n你可能已经 邂逅 过 magit 这个Emacs 的git接口了。 magit 是Emacs 上的非常优秀的git 接口,它假定你是了解你正在对 magit 或者 git 做的种种操作的 注意 :该文章是针对 magit 1.x 的,对 magit 2.x 并不适用 Magit 有非常完整的文档,包含了Magit 的各种操作,但是和大多数的文档一样,Magit 的文档并没有介绍如何将Magit 和你的工作流结合;\nMagit 假定你是熟悉Magit 并且了解如何合理地使用Magit(但大多数情况并不是这样).\nMagit 现在还是处于活跃的开发中。在2013年12月,增加了很多很多新的很有用特性的一次 release, 让Magit 变得比以前更强大了,所以本教程是基于比较新的Magit 版本的,并且 假定你也已经安装了新的版本\n如果想安装处于 master 分支的Magit,我建议你使用 Melpa 来安装;此外你也可以选择直接拉取magit github 仓库的最新版本,然后按照README 上面的指导来构建 magit\n这是我Magit 教程的第一部分。这部分会介绍状态窗口(status window); 已暂存和未暂存的项目 (staging and unstagin item);已经提交的更改 (committing changes) 和查看历史提交 (history view)\n1 Getting Started 首先:Magit 并没有隐藏git 的复杂性,所以,如果你想高效地使用Magit ,你最好清楚了解git 究竟做了哪些工作。\n事实上,我更原意把Magit 当作一个取代了git 枯燥的纯命令行操作的工具,它也表现得出乎意料地好。\n你可以通过 M-x magit-status 来使用Magit,该命令会打开一个窗口(如果你的缓冲区 所在的目录不是一个git 项目,Magit 会提示你进入一个git项目的目录),然后展示Magit 当前的状态。\n你是通过 magit-status 这个接口来使用Magit 的。此外,如果你是使用 Emacs VC的,你需要知道的是,Magit是没有集成到Emacs VC(Version Control)的 抽象层。\n虽然你没法在VC 使用Magit,不过你还是可以在大多数版本控制工具使用Magit 的,Magit 为这些版本控制工具都提供了统一的接口;你如果想调用Magit,你只需要 M-x magit-status\n2 The Magit Status 你首先会注意到关于Magit的事情应该就是当你打开Magit 的状态窗口时,Magit 的状态窗口是可以与你的Emacs 窗口配置配合工作的,你也可以像在其他Emacs 窗口那样,通过按下 q 来关闭窗口。\n几乎你在Magit 执行的所有操作都是通过在底部打开一个 command console 窗口,然后按下对应的单字符指令执行;你也可以重新定义你自己的指令。\n这种交互的方式真的非常好用,可能正是这么强大的特性让Magit变得如此优秀吧。\n我个人真的非常喜欢这种交互的方式,我甚至把这部分特性的代码复制到了我自己的Emacs项目上,因为这真的真的非常好用。\nMagit 之前的稳定版本在帮助用户更好使用Magit这方面做得略有不足,所以在最新的版本有了相应的改进,你可以通过按下 ~?\u0026lt;/kbd\u0026gt; 来显示一系列带注解说明的操作。我觉得在最开始的时候,Magit 真的很难用,因为我总是在「茫茫」的菜单选项中迷失,好不容易才能找到我想要的操作。\n即使是现在,也并不是所有的操作都有注解说明了;有一些命令 (对于我的工作流来说很重要的命令)依然是没有说明的,特别是用来重新定位的 E.\n3 Staging and Unstaging Items 把你的文件放到git下面是你经常需要完成工作之一,Magit 有一系列的按键绑定和工具可以帮助你更好地完成工作。\nMagit 操作不仅可以暂存文件,还可以暂存在 diff 中选定 的代码块。\nMagit\u0026quot;杀手级\u0026quot;特性之一就是它使用不同的等级来显示相关的信息。Magit 可以让你通过 tab 展开或者折叠已经暂存或者未暂存文件。\n如果你想更加细颗粒度地暂存或者未暂存文件,你可以使用 M-1 到 M-4 来操作所有的文件;此外,也可以使用 1 到 4 操作选定的文件\n等级1会把所有的东西隐藏到一个分类里面(即已暂存的文件);\n等级 2在一个分类里面只是显示文件名 (这是默认的等级);\n等级3会显示git代码块的头部;等级4 会显示所有做出了修改的代码块。\n我使用最多的是等级2和等级4, 如果你使用按键 TAB,Magit会完成你想要的等级操作的。你拥有一系列可以让你的\n生活变得更加美好的按键绑定,例如: n 和 p 可以在你前一个单元和后一个单元(通常以代码块为单元) 之间移动;M-n 和 M-p 可以在相邻单元之间移动,例如在等级4中的每个文件间移动。\n你也可以使用 + 或者 - 放大或者缩小每段代码,使用 0 可以恢复默认设置。此外你也可以按下 H 给代码块 添加额外的代码高亮。\n最后,你按下回车 RET 可以打开你修改的文件,代码块或者文件都适用该操作。\n你可以通过按下 s 或者 u 来暂存或者撤销暂存文件(或者代码块),此外,奉送一个很有用的小提示:如果你选定某部分的代码,然后按下暂存(撤销暂存)按键,Magit 会自动暂存(撤销)你选定的那部分代码。\n当你发现 diff 选定的代码块不符合你的要求的时候,你会发现这种细颗粒度的操作真的非常有用\n有时候你对某些修改并不在意,你也不关心这部分修改是否已经提交;你可以像上面的暂存(撤销暂存)操作一样,通过按下按键 K 来忽略选定的代码块和文件,并且从你的电脑删除未提交到暂存区(untracked)的文件;\n这个命令可以比暂存(撤销暂存)命令完成更多的操作,例如,删除已保存的文件或目录(stash)\n4 Committing Changes 如果你想打开提交菜单,只需按下 c,然后你就会看到琳罗满目的选项,不过大部份选项 你都是用不上的了。你真正有用的操作,不仅可以让你提交已暂存的修改,还可以完成更多的任务:\n你可以扩展(extend e) HEAD 所指向的提交 你可以修改(amend a) 有关的提交信息 如果你不喜欢现在的提交信息,你可以重写(reword r)提交信息 你同时也可以修整(fixup f)和压缩(squash s)当前这次的提交。如果你之前用 . 标记了一次提交,那么今次使用的就是被标记的那次提交。 扩展一次提交其实就是在当前提交上附加修改,所以,如果你忘记了提交本属于此次提交的东西你可以使用 扩展 选项。\n如果你想修改当前的提交信息,那就使用 修改 选项吧\n重写可以重写你的提交信息但是无需提交你已暂存的修改;如果你不小心按错了选项,想重写你的提交信息,重写 选项就是你最好的选择\n如果你想在最新一次提交下创建一个 fixup 或者 squash 提交的话,使用修整或压缩命令 可以重整或者 --(自动压缩)autosquash 最新一次提交。\n如果你不会去重写你的git历史或者你未使用过重整,你可能觉得这两个命令不是很有用\n5 Logging 我觉得Magit非常强大的特性之一就是它有不计其数的选项可以用来对你的git 历史进行 过滤,排序,查找。\nMagit 不仅可以展示你的git信息,还可以让你执行交互操作。如果你想打开日志的菜单,你只需按下 l.\n你应该知道的第一个有用的按键就是 l l, 这个 按键会为你展示缩略的日志信息:你会看到单行的提交信息, 作者的名字, 修改提交距今的时间, 树状结构的git 日志, 各种的标签信息,例如 HEAD 指针的位置或者分支标记的位置\n如果你不小心玩坏了git 的提交信息,命令 git reflog 会是你的救星;此外,对于magit 的引用日志(reflog)机制(l h),它也有很友好稳定的UI界面支持。\n引用日志和普通的日志都有非常丰富的按键绑定。在日志里,你对单个的提交可以进行非常多的操作:\n.: 为此次提交作标记以便进行后续的操作例如提交修整 (c f)或者提交压缩 (c s) x: 重置你的 HEAD 指针到选定的提交 v: 撤销提交 d: 将你的工作区与选定的提交进行比较 a: 将选定的提交作用在你的工作区 A: 选择位于你工作区顶部的提交 E: 以交互的方式重置你的 HEAD 指针到选定的工作区。如果你想重写历史,该命令会非常有用 C-w: 复制你此次提交的hash值 SPC: 展示完整的提交历史 需要注意的是:即使你关闭了日志的窗口,标记的命令还是会继续作用的;标记是非常有用的工具,但是你很容易忘记你是否曾经作过标记。如果你在magit 使用 M-n 或者 N-p 向上或者向下浏览日志, maigt 会自动为你在另外一个窗口显示提交信息\n6 Conclusion 对于有经验的Git 用户来说,Magit 是一个非常好的工具;此外,如果你是Git 的新手, Magit可以帮助你了解Git 是怎么工作的,但是它永远不会教你使用Git.\n在我看来,阻碍 你使用Magit 最大的障碍就是Magit缺乏对选项的描述说明;即使Magit 包含了成千上万 Git的选项,参数和操作,但是它并没有教你如何找到并使用这些命令。\n我发现Git 的命令行真的无可替代(不是因为我喜欢git 的命令行我才这么说,事实是它真的很棒)因为我 想要完成的操作真的隐藏得很深,没有那么容易在Magit找到。\n不过最新版本的改进真的很好,你可以通过按下 ? 查看一系列带有注解说明的命令(但不是全部命令,不过这也已经是一个很大的改进了).\n如果你曾被Magit 的学习曲线所吓倒,抑或者你已经尝试Magit, 却无奈放弃;我建议你再试一次。我打算写更多关于Magit 的博文\n原文地址 https://www.masteringemacs.org/article/introduction-magit-emacs-mode-git 在下翻译水平有限,如有错误,还请指出\n","permalink":"https://ramsayleung.github.io/zh/post/2017/magit/","summary":"如果你足够幸运(或者不幸运,取决于你怎么看待了)可以使用 git 作为你工作流的一部分。 你可能已经 邂逅 过 magit 这个Emacs 的git接口了。 magit 是Emac","title":"(翻译)An Introduction to Magit"},{"content":" fasd - quick access to files and directory\n之前一位 Windows 用户看到我在 Shell 下面的操作,他很奇怪,觉得明明已经有图形化界面,为什么还要用这种命令行呢,直接用鼠标点击不就很好了么。\n我觉得很难直接跟他解释,因为他没有用过Linux/Unix,完全不熟悉命令行,不知道其强大之处,其高效率是图形化界面完全无法比拟的(当然,命令行的学习成本和学习曲线肯定比图型化界面高), So I live in terminal.\n而今天我要介绍的神器 fasd 就是可以让命令行操作变得更加高效\n1 Fasd 在 Shell 下面有非常多的命令操作是与文件和目录相关的,如果你要进入到另外一个目 录你可以使用相对或者绝对路径来访问该目录,但是如果这是一个与当前目录不相关的目 录你就只能通过绝对路径来访问。\n以我自己的目录为例,当前目录是 home/samray.emacs.d/elisp/ ,我希望访问 Document 目录下一个的子目录 Python, 我可以通过下面的命令来访问:\ncd ~/Document/Programming/Python cd /home/samray/Document/Programming/Python 这就是我需要的命令,虽然可以通过 tab 进行目录名的补全,但是我还是觉得要输入的东西太多了(正如 Larry Wall 所说,懒惰是程序员的美德). 然后,我发现了 Fasd 这个神器。它可以让我只输入 Python 就进入到我想访问的 Python 目录,\n神奇吧!:)\nFasd以访问的频繁程度和最近是否有访问对文件和目录分配优先级,然后通过判断已访问的文件以及其优先级来切换目录或者打开文件,所以如果你之前已经访问过某个目录.\n那么 你很容易就可以切换到那个目录\n1.1 常用选项 -a(any): 匹配文件和目录\n-i(interactive): 以交互的方式选择文件或者目录\n-s(show/search): 按照优先级展示文件或者目录\n-e \u0026lt;cmd\u0026gt;:对匹配的文件调用命令\u0026lt;cmd\u0026gt;\n-d:只匹配目录\n-f:只匹配文件 Fasd 文档还建议你为 fasd的命令选项设置别名\nalias a=\u0026#39;fasd -a\u0026#39; # any alias s=\u0026#39;fasd -si\u0026#39; # show / search / select alias d=\u0026#39;fasd -d\u0026#39; # directory alias f=\u0026#39;fasd -f\u0026#39; # file alias sd=\u0026#39;fasd -sid\u0026#39; # interactive directory selection alias sf=\u0026#39;fasd -sif\u0026#39; # interactive file selection alias z=\u0026#39;fasd_cd -d\u0026#39; # cd, same functionality as j in autojump alias zz=\u0026#39;fasd_cd -d -i\u0026#39; # cd with interactive selection 这样你就可以通过 z some-dir 直接进入到某个目录或者 zz some-dir 选择进入有多个匹配的特定目录。\nFasd 还会判断应该显示所有的匹配选项或者是直接选择最佳匹配. 例如你也可以将fasd配合 subshell 使用,例如打开 foo\nvim `f foo` 又或者打开 /etc/rc.conf\nvim `f rc conf` 1.2 例子 你可以将fasd 配合正则表达式使用,例如列举以 py 结尾的最近访问的文件:\nf py$ 又或者使用Emacs 打开最近频繁访问的文件 bar\nf -e emacs bar 2 Fasd +Eshell fasd 真的可以大幅度提高效率,但是我有点不太满意的是,我是个 Emacser, 我的操作基本是在 Emacs 里完成的,而我在 Emacs里面使用的 shell 是 Eshell,Eshell 似乎不能与 fasd 无缝结合,似乎可以折腾一下。\nz 和 zz 命令是无法在Eshell 里面运行,因为 z 是 fasd_cd 的别名,而fasd_cd 是一个shell script 函数,Eshell无法运行该函数,代码如下:\nfasd_cd () { if [ $# -le 1 ] then fasd \u0026#34;$@\u0026#34; else local _fasd_ret=\u0026#34;$(fasd -e \u0026#39;printf %s\u0026#39; \u0026#34;$@\u0026#34;)\u0026#34; [ -z \u0026#34;$_fasd_ret\u0026#34; ] \u0026amp;\u0026amp; return [ -d \u0026#34;$_fasd_ret\u0026#34; ] \u0026amp;\u0026amp; cd \u0026#34;$_fasd_ret\u0026#34; || printf %s\\n \u0026#34;$_fasd_ret\u0026#34; fi } Eshell无法运行该函数,因为Eshell文档的匮乏,我也不知道如何编写跟上面代码等价的 \u0026ldquo;Eshell script\u0026rdquo;,所以就用 elisp 写一段同样功能的函数好了。\n(defun samray/eshell-fasd-z (\u0026amp;rest args) \u0026#34;Use fasd to change directory more effectively by passing ARGS.\u0026#34; (setq args (eshell-flatten-list args)) (let* ((fasd (concat \u0026#34;fasd \u0026#34; (car args))) (fasd-result (shell-command-to-string fasd)) (path (replace-regexp-in-string \u0026#34;\\n$\u0026#34; \u0026#34;\u0026#34; fasd-result)) ) (eshell/cd path) (eshell/echo path) )) 函数功能很快就写好了,实现了 z 的功能,但是原来的代码一直不能正常运行,折腾了一个多小时都没解决,输出什么都正常,最后 debug 发现是因为显示的路径后面多了一个换行符即 /home/samray 变成了 /home/samray\\n,而输出换行符又不会显示,真 的坑。\n最后为命令赋予别名就可以像在 zsh 下那样工作了:\nalias z \u0026#39;samray/shell-fasd-z $1\u0026#39; 更多的用法就要查阅官方文档了\nman fasd Enjoy Emacs and Shell :)\n参考: https://github.com/clvv/fasd\n","permalink":"https://ramsayleung.github.io/zh/post/2017/fasd-meet-eshell/","summary":"fasd - quick access to files and directory 之前一位 Windows 用户看到我在 Shell 下面的操作,他很奇怪,觉得明明已经有图形化界面,为什么还要用这种命令行呢,直接用鼠标点击不就很好了么","title":"Shell神器fasd与Eshell的不期而遇"},{"content":"最近笔者在阅读《算法》,重温经典数据结构和算法,毕竟一直以来的说法是程序就是数据结构+算法归并算法所需的时间和N*logN成正比,所以可以用归并算法处理数百万甚至更大规模的数据。\n但是归并算法也是存在不足之处的,需要额外的空间来完成排序,而且空间和N的 大小也是成正比的\n1 优化 《算法》中有提到可以通过一些细致的修改实现大幅度缩短归并排序的运行时间\n1.1 对小规模子数组使用插入排序 因为递归会使小规模问题中的方法被频繁调用,所以改进对它们的处理方法就能改进整个算法。\n对于小数组可以使用插入排序或者选择排序来避免递归调用。完整代码\n1.1.1 未改进归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; } int mid=lo+(hi-lo)/2; sort(a,aux,lo,mid);/*将左半边排序*/ sort(a,aux,mid+1,hi);/*将右半边排序*/ merge(a,aux,lo,mid,hi); } 1.1.2 改进后归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; }else if(hi-lo\u0026lt;15){ insertionSort(a,lo,hi); return; } else { int mid = lo + (hi - lo) / 2; sort(a,aux, lo, mid); sort(a,aux, mid + 1, hi); merge(a,aux, lo, mid, hi); } } Figure 1: 轨迹图如上:(图来源于《算法》)\n《算法》 中提到插入排序处理小规模的子数组(比如长度小于15) 一般可以将归并排序的运行时间缩短10%-15%. 实践出真知,还是要自己来测试一下更佳。\n1.1.3 测试1 对100*1000 个字符进行排序,结果如下:\n1.1.4 测试2 对1000*1000 个字符进行排序,结果如下:\n1.1.5 测试3 对10000*1000 个字符进行排序,结果如下\n1.1.6 测试4 对100000*1000 个字符进行排序,结果如下\n1.1.7 测试5 对500000*1000 个字符进行排序,结果如下\n1.1.8 小结 由于篇幅问题,我无法将所有的测试结果都展示出来,但是从上面的结果,可以看出对于小数组,使用插入排序的确对性能有一定幅度提高(最开始的测试可能因为数据量太小所以结果误差较大,但是这并不妨碍得出一个比较接近的结果).\n但是随着数据量的增大改进归并算法性能似乎开始下降 (未经过精确数据验证)\n1.2 测试数组是否已经有序 可以添加一个判断条件,如果 a[mid]小于等于 a[mid+1],便可认为数组已经有序并跳过 merge 方法。这个改动不影响排序的递归调用,但是任意有序的子数组算法的运行时间都变成线性的了\n1.2.1 未改进归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; } int mid=lo+(hi-lo)/2; sort(a,aux,lo,mid);/*将左半边排序*/ sort(a,aux,mid+1,hi);/*将右半边排序*/ merge(a,aux,lo,mid,hi); } 1.2.2 改进后归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; }else if(hi-lo\u0026lt;15){ insertionSort(a,lo,hi); return; } else { int mid = lo + (hi - lo) / 2; sort(a,aux, lo, mid); sort(a,aux, mid + 1, hi); if(less(a[mid+1],a[mid])){ merge(a,aux,lo,mid, hi); } } } 1.2.3 测试1 对100*1000 个字符进行排序,结果如下: 1.2.4 测试2 对1000*1000 个字符进行排序,结果如下: 1.2.5 测试3 对10000*1000 个字符进行排序,结果如下: 1.2.6 测试4 对100000*1000 个字符进行排序,结果如下: 1.2.7 测试5 对500000*1000 个字符进行排序,结果如下: 1.2.8 小结 从上面的结果可以看出,只是添加了一个判断数组是否已经有序的条件,算法性能就优化了 大概20%左右(未经过精确数据验证), 不得不说真的令人惊讶。\n注意: 运行结果跟操作系统,电脑配置,以及运行次数都相关,所以我使用的也只是很粗略的数据\n1.3 参考 http://algs4.cs.princeton.edu/home/h\n","permalink":"https://ramsayleung.github.io/zh/post/2017/merge-sort-improvment/","summary":"最近笔者在阅读《算法》,重温经典数据结构和算法,毕竟一直以来的说法是程序就是数据结构+算法归并算法所需的时间和N*logN成正比,所以可以用","title":"归并排序算法改进"},{"content":" diff - compare files line by line\n如果你有使用过git,那么你一定不会对diff 陌生,因为对你源文件和修改后的文件进行比较的就是 diff 这个大名鼎鼎的家伙了。\n多年以来, diff 都一直是非常重要的工具,上古大神 都是使用 diff 和 patch 对程序进行差分和打补丁滴(现在有git了,但是diff同样发挥着重要作用)\n1 语法 diff 的语法如下\ndiff [OPTION].... file1 file2 OPTION 指不同的选项参数,file1,file2 是文本文件的名字,如果比较的两个文件相同 diff 将不输出任何东西。如果两个文件有差异,diff 会显示一系列的指示,让你可以把第一个文件修改为与第二个文件一致\n2 例子 2.1 用法一 现在有两个文件,分别保存着不同的地址。 address1 包含:\nguangdong shanghai beijing chengdu address2 包含:\nguangdong shanghai beijin chengdu 你可以注意到两个文件的区别就是第三行的 beijing.然后运行 diff\ndiff address1 address2 输出结果:\n3c3 \u0026lt; beijing --- \u0026gt; beijin 似乎有点难以理解,输出结果描述了什么呢?其实diff 是在指导如何修改不同的文件使之一致 \u0026lt; 后接的是文件1中与文件2不同的部分, \u0026gt; 后接的是文件2中与文件1不同的部分\ndiff 的输出使用3个不同的单字符指导:a(append,追加),c(change,修改),d(delete,删除). 在上面的例子,只是看到一个 c,意味着,如果想把 address1 修改成 address2 只需将 address1 的第三行修改成 address2 的第三行\n2.2 用法2 现在把 address2 的最后一行删除,看看运行 diff 结果如何: address1 包含:\nguangdong shanghai beijing chengdu address2 包含:\nguangdong shanghai beijing diff address1 address2 输出结果:\n4d3 \u0026lt; chengdu 在该例子中,为了将 address1 变成 address2 只需删除 address1 的第四行\n2.3 用法3 现在把 address1 的最后一行删除,看看运行 diff 结果如何: address1 包含:\nguangdong shanghai beijing address2 包含:\nguangdong shanghai beijing chengdu diff address1 address2 输出结果:\n3a4 \u0026gt; chengdu 想将第一个文件转换成第二个文件,只需在第一个文件追加第二个文件的第四行(即在第一个文件的第 3 行之后追加第二个文件的第 4 行)\n3 diff 选项 因为diff 是一个相当强大也是一个相当复杂的命令,所以我没办法将所有的用法一一道 尽所以笔者将比较常用的选项列举出来\n-b:忽略制表符(不忽略所有的空白符,指忽略空白符数量的差异),例如下面的两行是相同的 a a a a -B(blank lines):忽略所有的空白行 -c(context):以上下文的形式显示差异内容,对比默认输出更加容易理解(但是也更加繁杂) -q(quiet): diff 静默设置,即如果文件file1和file2有差异,diff 也只会显示 File file1 and file2 differ -w(whitespace):忽略所有的空白符 -u(unified output): 上下文形式显示的改进,不会输出重复行 -y:将文件分成两列或多列并排进行输出(非常直观,但是输出很繁杂) 还是老话,更多的用法就需要:\nman diff ","permalink":"https://ramsayleung.github.io/zh/post/2017/diff/","summary":"diff - compare files line by line 如果你有使用过git,那么你一定不会对diff 陌生,因为对你源文件和修改后的文件进行比较的就是 diff 这个大名鼎鼎的家伙了。 多年以来","title":"Linux/Unix Shell 二三事之过滤器diff"},{"content":"1 枯树 周末回了一趟家,没带自己的笔记本,在家闲来无事,无意中看到墙角的电脑,已经尘封已久反正无事,何不玩玩这台老古董呢?于是把电脑拿去修理店把坏了的硬件修好。\n离开店的时候,老板说:“你的系统有问题,我看到你自己也有Ghost,就不帮搞这系统了, 你自己都能解决的,推荐你还是用XP吧,这电脑配置低,还是XP好用”。我忍不住回头对老板一笑 :)\n2 春至 像我这种Linuxer,这么可能再装回XP呢,最初装Win7,也是考虑到老爹的技术 hold 不住 Linux, 现在手机那么发达,他就不需要电脑了,所以,此时不装Linux,更待何时呢?\n3 Arch Linux 我没有选择 Xubuntu 这种适合老机器的 Ubuntu 衍生发行版本,因为我不喜欢Ubuntu, 所以我最后选择的是 Arch linux,官网说最低配置只需500MB内存,800MB的 硬盘存储空间,正适合家里的老家伙\n3.1 安装过程 3.1.1 下载镜像 Download Link ,在网易的镜像下载ISO, 然后用dd刻录到U盘,Windows 可以选择 USBwriter\n3.1.2 分区 使用fdisk, 我的硬盘是/dev/sda,如果还有一块硬盘,那应该就是/dev/sdb\nfdisk /dev/sda n:新建一个分区,p 指主分区,e 是指扩展分区(逻辑分区是建立在扩展分区上的) 一块硬盘主分区加上扩展分区最多只能是4个 d: 删除 m: 查询其他命令,不知道怎么操作就输入m 吧 分区结束以后,输入 w 完成分区 (我分了三个分区 /dev/sda1 -\u0026gt; swap /dev/sda2 -\u0026gt; / /dev/sda3 -\u0026gt; /home)\n3.1.3 格式化分区 格式化 sda2 sda3为ext4格式:\nmkfs.ext4 /dev/sda2 mkfs.ext4 /dev/sda3 格式化sda1 为swap(虚拟内存),一般是内存的两倍,当然如果你的内存很大的话就不用划这个分区了\nmkswap /dev/sda1 激活swap\nswapon /dev/sda1 3.1.4 挂载 将sda2挂载到/mnt,其实就是让sda2分区做系统的根分区,/mnt/home同理\nmount /dev/sda2 /mnt mount /dev/sda3 /mnt/home 3.1.5 更新pacman源 网易的源不错,编辑 /etc/pacman.d/mirrorlist 添加 Server = http://mirrors.163.com/archlinux/$repo/os/$arch\nvim /etc/pacman.d/mirrorlist 然后添加;添加完之后,更新一下\npacman -Syy 3.1.6 安装基本系统 安装基本系统到 /mnt,即sda2分区\npacstrap /mnt base base-devel 需要安装的都安装吧,然后走开煮一杯咖啡,慢慢品尝\n3.1.7 生成fstab fstab 的作用:\nThe fstab(5) file can be used to define how disk partitions, various other block devices, or remote filesystems should be mounted into the filesystem\n生成fstab,并且查看是否正确生成fstab\ngenfstab -U -p /mnt \u0026gt;\u0026gt; /mnt/etc/fstab cat /mnt/etc/fstab 3.1.8 配置系统 切换到新的系统,然后你会发现命令行提示符发生了改变\narch-chroot /mnt 设置地区\nln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 设置语言\n编辑 /etc/locale.gen,因为该文件所有的信息都是被注释滴,所以在最上面添加en_US.UTF-8 UTF-8 即可\nvim /etc/locale.gen 然后添加;添加完成后,执行 locale-gen\nlocale-gen 接着配置 locale.conf\necho LANG=en_US.UTF-8 \u0026gt; /etc/locale.conf export LANG=en_US.UTF-8 设置主机名\necho samray-arch \u0026gt; /etc/hostname 设置密码\npasswd 配置网络\npacman -S net-tools systemctl enable dhcpcd.service 安装GRUB\npacman -S grub-bios 把grub 安装到硬盘sda,如果双系统的话,还要视情况做更改\ngrub-install --recheck /dev/sda grub-mkconfig -o /boot/grub/grub.cfg 3.1.9 收尾工作 exit umount /mnt/home umount /mnt reboot 这样Arch linux 就装好了,不过你重启会发现,你的系统是没有图形化界面的\n3.2 安装桌面环境 3.2.1 安装x服务 pacman -S xorg-server xorg-server-utils xorg-xinit 3.2.2 安装显卡驱动 查找自己的显卡类型\nispci |grep VGA 然后搜索匹配自己显卡的驱动\npacman -Ss xf86-video |less Intel 集成显卡:\npacman -S xf86-video-intel 虚拟机显卡:\npacman -S xf86-video-vesa 笔记本触摸板驱动 (老家伙是台式,不需要了):\npacman -S xf86-input-synaptics 安装输入法\npacman -S scim-pinyin 先安装 slim(图像登录管理器)\npacman -S slim 安装xfce4\npacman -S xfce4 启动xfce4\nstartxfce4 基本就大功告成了,因为我的台式电脑是bios, 所以不用折腾uefi, 还有无线网络。\nAction is louder than words,还是多动手才行,我都装了三次才成功,内核空指针和段错误都遇到了 :)\n3.3 参考 https://wiki.archlinux.org/index.php/installation_guide\n","permalink":"https://ramsayleung.github.io/zh/post/2017/install_archlinux/","summary":"1 枯树 周末回了一趟家,没带自己的笔记本,在家闲来无事,无意中看到墙角的电脑,已经尘封已久反正无事,何不玩玩这台老古董呢?于是把电脑拿去修理店","title":"枯树逢春之ArchLinux领风骚"},{"content":"flask 是一个轻量级的python 框架(官网称为微型框架),很容易上手,之前因为笔者跟朋友开发小程序的时候使用过 flask,过后就遗忘了。\n为了重拾flask, 笔者决定写点小东西,之前开发小程序,不如现在再玩玩公众号开发\n1 验证服务器 开发公众号之前,要先验证服务器的有效性,官网有详细的说明:公众开发平台文档\n参数 描述 signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 timestamp 时间戳 nonce 随机数 echostr 随机字符串 校验流程:加密/校验流程如下:\n将token、timestamp、nonce三个参数进行字典序排序 将三个参数字符串拼接成一个字符串进行sha1加密 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 流程并不复杂,官网给出了代码示例,只不过是PHP的,换成python 也是很容易滴:\n@app.route(\u0026#39;/\u0026#39;,methods=[\u0026#39;GET\u0026#39;,\u0026#39;POST\u0026#39;]) def wechat(): if request.method==\u0026#39;GET\u0026#39;: token=\u0026#39;your token\u0026#39; data=request.args signature=data.get(\u0026#39;signature\u0026#39;,\u0026#39;\u0026#39;) timestamp=data.get(\u0026#39;timestamp\u0026#39;,\u0026#39;\u0026#39;) nonce =data.get(\u0026#39;nonce\u0026#39;,\u0026#39;\u0026#39;) echostr=data.get(\u0026#39;echostr\u0026#39;,\u0026#39;\u0026#39;) s=[timestamp,nonce,token] s.sort() s=\u0026#39;\u0026#39;.join(s) if(hashlib.sha1(s).hexdigest()==signature): return make_response(echostr) else: rec=request.stream.read() xml_rec=ET.fromstring(rec) tou = xml_rec.find(\u0026#39;ToUserName\u0026#39;).text fromu = xml_rec.find(\u0026#39;FromUserName\u0026#39;).text content = xml_rec.find(\u0026#39;Content\u0026#39;).text xml_rep = \u0026#34; \u0026lt;xml\u0026gt; \u0026lt;ToUserName\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/ToUserName\u0026gt; \u0026lt;FromUserName\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/FromUserName\u0026gt; \u0026lt;CreateTime\u0026gt;%s\u0026lt;/CreateTime\u0026gt; \u0026lt;MsgType\u0026gt;\u0026lt;![CDATA[text]]\u0026gt;\u0026lt;/MsgType\u0026gt; \u0026lt;Content\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/Content\u0026gt; \u0026lt;FuncFlag\u0026gt;0\u0026lt;/FuncFlag\u0026gt; \u0026lt;/xml\u0026gt;\u0026#34; 这样服务器就校验成功了,就可以编写相关的业务逻辑了\n2 歌词查询 笔者自己平时是打开音乐播放器,戴上耳机,就开始播放音乐;所以经常出现笔者听到某首歌曲觉得旋律非常熟悉,但是就无法想起歌名的情况,这种感觉实在不好,所以笔者觉得可以编写一个通过歌词查询歌曲,并返回所有歌词的功能。\n思路大概是编写爬虫,通过歌词进行查询,然后对返回的html 页面进行检索和信息提取。剩下的事就是爬虫和解析页面了,笔者是使用虾米 进行歌词查询的,使用 request 发送 http 请求,使用 lxml进行解析,其他就不一一细表了\n3 单词查询 有时候,笔者在微信需要查询单词,但是又不想退出微信,所以就打算用公众号来查单词其实很简单,就是服务获取用户发给微信公众号的数据,再去请求有道之类词典的api,再把结果返回给服务器,服务器转发给用户\n4 电影查询 有时无聊想去看电影,但是不知道看什么电影,因为选择太多,质量参差不齐的片太多了所以笔者会先去豆瓣看一下新上影的电影,看一下评分,然后再决定看什么电影。所以,笔者可以把这个功能搬到公众号来。如何实现呢?还是爬虫\n5 总结 感觉这次开发公众号,笔者就是用 flask 编写 restful api, 然后做的其他事情就是编写爬虫。\n项目github地址\n","permalink":"https://ramsayleung.github.io/zh/post/2017/weixin_flask/","summary":"flask 是一个轻量级的python 框架(官网称为微型框架),很容易上手,之前因为笔者跟朋友开发小程序的时候使用过 flask,过后就遗忘了。 为了重拾","title":"flask牛刀小试之微信公众号开发"},{"content":" cat - concatenate files and print on the standard output\n1 过滤器 何谓过滤器呢,例如cat,grep,wl 之类的命令就是过滤器了。这样的命令 读取数据,对数据执行一些操作,然后写入结果。更准确地说,过滤器就是任何能够从标准 输入读取 文本 数据,并向标准输出写入 文本 数据的命令。又因为Unix 的 KISS 设计理念,所以每个程序都被设计成能够出色完成一项特定任务的工具。又因为重定向和 管道的存在,使得可以将这些工具组合起来,发挥无穷威力\n2 cat 在shell 里面运行cat,你会被要求输入文本数据,当你输入一行数据以后,然后按下回 车你输入的数据就会显示在屏幕,当你按下 ^D(\u0026lt;ctrl\u0026gt;+d),发送eof 信号给shell,退出 cat。cat 做的事就是把你输入的字符,复制到标准输出 (一般情况是指你的屏幕).看到 这里有人或许会质疑,这东西有什么用呢?似乎什么都作不了。不,它的用处很大呢, 且容笔者细细禀来\n2.1 场景1 假如你要新建一个文本文件,里面只是很少的文本,你会怎么做呢?一般情况下,都是用 vim/emacs 新建一个文本文件,然后输入几行文字,然后保存退出。这是一般的做法, 看到这里,很自然有人会发问,难道有更优雅的解决方法?有,不用打开文本编辑器写入文本 的hacking方法:\ncat \u0026gt; data 输入数据,然后 ^D(\u0026lt;ctrl\u0026gt;+d) 保存。你就新建了一个文本了。当然,如果你已经有一个 data 文件 ,就会被代替,当然,你也可以也在原来文本末尾添加的方法:\ncat \u0026gt;\u0026gt; data 2.2 场景2 如果你有一个短文件,你想查看一下,同样,你可以使用cat\ncat \u0026lt; data 当然,你也可以省略 \u0026lt; 这个重定向符号:\ncat data 抑或是,你想显示某个大文件的最后一部分,你也可以如上操作。或许你会觉得,这个功能 很多命令也有,最典型的就是 tail. 但是如果 cat 可以很完美地很其他过滤器结合 充当整套管道线工具流的起始端,这个以后慢慢再阐述\n2.3 场景3 如果你想复制文本文件,你首先会想起什么命令? cp,很自然嘛,我也不例外,但是cat 也可以实现同样的功能,很意外吧:\ncat \u0026lt; file \u0026gt; newfile 即把 file 复制到标准输出,然后再把 file 当作标准输入复制到 newfile.hacking!\n2.4 场景4 如果你想把多个文本文件的组合到一个文件,你会怎么做?用编辑器打开所有的文件 然后 select,cut,paste,save.我也会很自然地想到这个方法,但是是否存在着更 优雅的解决方案呢?当然:\ncat file1 file2 file3 \u0026gt;newfile 3 总结 上面已经介绍了挺多cat 的使用场景了,你觉得cat 表现滴怎么样呢?相信你的感觉是 还行,但是并没有,我吹嘘的那么令人惊艳。因为这只是cat 最基本的功能,它最大的 用法还没有完全展现出来,笔者先举一例,以后再慢慢详叙:\ncat file |grep \u0026#34;something\u0026#34; |sort -n |tee newfile 语法 用法 cat \u0026gt; file 读取输入,创建新的文件或替换 cat \u0026gt;\u0026gt;file 读取输入,追加新的文件 cat file/cat \u0026lt;file 显示一个已有文件 cat \u0026lt;oldfile\u0026gt; newfile 复制一个文件 cat file1 file2 file3\u0026gt;file4 组合多个文件 ","permalink":"https://ramsayleung.github.io/zh/post/2017/cat/","summary":"cat - concatenate files and print on the standard output 1 过滤器 何谓过滤器呢,例如cat,grep,wl 之类的命令就是过滤器了。这样的命令 读取数据,对数据执行一些操作,然后写入","title":"Linux/Unix Shell 二三事之过滤器cat"},{"content":" head - output the first part of files tail - output the last part of files\n当拥有的数据太多的时候,使用cat 来展示数据的话,数据量过大,屏幕就只能显示最后一部分的数据了。\n所以如果你想选取部分的数据的话,cat 就不是一个好选择了。有两个命令可以满足你的要求,分别是 head 和 tail.顾名思义,head 选取数据的开头部分tail 是选取数据的结尾部分\n1 用法 当把 head tail 当作过滤器来使用的时候,用法很简单\n$ head data $ tail data 默认情况下 head 会选取数据开头的10行数据 tail 会选取数据最后的10行数据. 如果你想选取更多的数据的时候,你可以指定行数,例如\n$ head [-n line] data $ tail [-n line] data 其中 line 是希望选取的数据行的数量\n2 惊艳点 你可能觉得 head tail 两个命令很简单,似乎用处不大。\n是的,就笔者一直所介绍的那样,单个unix命令只是完成一个特定的工作,但是当它们组合起来的时候,就很威力无穷了\n2.1 场景1 假如你要生成一串密钥来加密你的某个文件,这是很常见的需求,你会怎么办,用python 或者 java 写一个随机数函数来实现么?无需,你用简单的过滤器加Linux/Unix\n内置的设备(dev):\n$ cat /dev/urandom | tr -cd \u0026#34;[[:alnum:]]\u0026#34; |head -c 32;echo 在Unix/Linux 的机器下,运行上面的命令就可以生成一个包含数字和字母的32个字符长的密钥了。\n/dev/urandom 是一个可以通过收集硬件驱动的环境噪音来产生伪随机数特殊的文件,tr 是转换和删除字符的命令;更多详细的东西,以后笔者会慢慢介绍滴\n2.2 场景2 在日常的开发或者运行环境中,日志是必不可少滴,但是日志是不断产生新的数据的,所以有时候就会出现用编辑器打开日志的时候,就会出现,编辑器不断提醒你文件已经发生了变化,是否重新加载,但是如果只是用cat,tail 来查看日志,日志又是保持在 打开的那个状态,新产生的日志数据是没办法浏览到,果真如此?\n其实不然, tail 可以在查看日志的时候,保持日志一直在更新。关键就在 -f 选项\n$ tail -f [-n line] file -f 选项告诉tail 当到达文件的末尾不要停止。相反,tail 要一直等下去,并且随着文件的增长,显示更多的输出 (-f -\u0026gt; follow)\n你也可以模拟日志不断生成的过程:\n$ tail -f -n 20 something.log 然后打开一个新的Shell, 运行:\ncat \u0026gt;\u0026gt; something.log 使用 \u0026gt;\u0026gt; 追加数据,就可以模拟日志生成的过程了\n3 总结 要掌握更多的用法还是要查看文档滴:\nman head Enjoy Shell :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/head_tail/","summary":"head - output the first part of files tail - output the last part of files 当拥有的数据太多的时候,使用cat 来展示数据的话,数据量过大,屏幕就只能显示最后一部分的数据了。 所以如果你想","title":"Linux/Unix Shell 二三事之过滤器head+tail"},{"content":"Percol 是Emacs 的一个非常优秀package:js2-mode作者mooz 的又一力作得益于Unix Shell的管道和重定向设计理念,percol 所有的输入输出变得可交互 percol 给我一种很熟悉的感觉,就是 Eamcs 中helm 增量补全 (incremental completion)的感觉,真的可以10倍提高工作效率。\n1 例子 假如你要用git 切换分支,但是分支很多,你不能记住你要切换的分支的名字。那么有percol 你可以:\n$ git checkout $(git branch|percol) 那样,你就可以,选择要切换的分支了\n平时在Linux/Unix 下,如果要kill 掉某个进程的话,我一般是用 htop 或者是ps 找出要kill 掉的进程的pid, 然后在 kill pid. 但是现在有了percol, 可以一步搞定所有的步骤。\n官网给出的例子函数:\nfunction ppgrep() { if [[ $1 == \u0026#34;\u0026#34; ]]; then PERCOL=percol else PERCOL=\u0026#34;percol --query $1\u0026#34; fi ps aux | eval $PERCOL | awk \u0026#39;{ print $2 }\u0026#39; } function ppkill() { if [[ $1 =~ \u0026#34;^-\u0026#34; ]]; then QUERY=\u0026#34;\u0026#34; # options only else QUERY=$1 # with a query [[ $# \u0026gt; 0 ]] \u0026amp;\u0026amp; shift fi ppgrep $QUERY | xargs kill $* } 又或者是更好地进行查找历史命令:\nfunction exists { which $1 \u0026amp;\u0026gt; /dev/null } if exists percol; then function percol_select_history() { local tac exists gtac \u0026amp;\u0026amp; tac=\u0026#34;gtac\u0026#34; || { exists tac \u0026amp;\u0026amp; tac=\u0026#34;tac\u0026#34; || { tac=\u0026#34;tail -r\u0026#34; } } BUFFER=$(fc -l -n 1 | eval $tac | percol --query \u0026#34;$LBUFFER\u0026#34;) CURSOR=$#BUFFER # move cursor zle -R -c # refresh } zle -N percol_select_history bindkey \u0026#39;^R\u0026#39; percol_select_history fi 1.1 运行截图 有时候,我需要复制当前目录下,某个文件的路径,但是无论是文件管理器,还是shell都要用鼠标来复制指定文件的路径,效率不高且很不方便。在 陈斌 代码的启发下,我自己写了一个函数来复制当前文件夹某个特定目录的路径,很方便地解决了问题:\nOS_NAME=`uname` function pclip() { if [ $OS_NAME = \u0026#34;CYGWIN\u0026#34; ]; then putclip \u0026#34;$@\u0026#34;; elif [ $OS_NAME = \u0026#34;Darwin\u0026#34; ]; then pbcopy \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xsel ]; then xsel -ib \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xclip ]; then xclip -selection c \u0026#34;$@\u0026#34;; else echo \u0026#34;Neither xsel or xclip is installed!\u0026#34; fi fi fi } function pwdf() { local current_dir=`pwd` local copied_file=`find $current_dir -type f -print |percol` echo -n $copied_file |pclip; } 更多的用法就要查看官方文档 percol\nEnjoy Shell :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/percol/","summary":"Percol 是Emacs 的一个非常优秀package:js2-mode作者mooz 的又一力作得益于Unix Shell的管道和重定向设计理念,perco","title":"Linux/Unix Shell 二三事之神器percol"},{"content":"之前看到个笑话:\nA: 在等待emacs 加载的时间里,你会干什么? B: 打开Vim,修改代码,保存,退出 有时候,经常看到社区里面有人吐嘈Emacs 什么都好,就是启动时间太长了,其实是存在一些技巧来缩短加载时间的\n1 技巧1 在你的 .emacs 或者相应的初始化文件里面添加如下代码\n# Increase the garbage collection threshold to 128 MB to ease startup (setq gc-cons-threshold (* 128 1024 1024 )) # your configuration code # ...... # Garbage collector-decrease threshold to 5 MB (add-hook \u0026#39;after-init-hook (lambda () (setq gc-cons-threshold (* 5 1024 1024)))) # init.el ends here gc-cons-threshold 指定了emacs 进行垃圾回收的阀值,默认值是 800000byte,实在是太小了,所以Emacs 会在启动期间进行非常多次的垃圾回收,启动时间自然长了。\n在加载完以后,再把 gc-cons-threshold 的值调低,当然,如果你的内存很大,也可以不改回来\n2 技巧2 (let((file-name-hander-alist nil))init.file) 包裹(wrap)你的初始化文件,即:\n(setq gc-cons-threshold (* 500 1024 1024)) (let ((file-name-handler-alist nil)) ... ** your config goes here ** ... ) (add-hook \u0026#39;after-init-hook (lambda () (setq gc-cons-threshold (* 5 1024 1024)))) (provide \u0026#39;init) ;;; init.el ends here 因为 file-name-handler-alist 的默认值是一些正则表达式,也就是说Emacs 在启动过程中加载el和elc 文件都会将文件名和正则表达式进行匹配\n3 技巧3 Emacs lisp 有一项auto-load 的技术,类似延迟加载,合理运用延迟,让笔者的Emacs启动加载时间减少一半,因为笔者用 use-package 这个macro,而 use-package 又集成了延迟加载的功能,所以笔者就直接拿自己的代码举例了\n3.1 :after ;;; Export to twitter bootstrap (use-package ox-twbs :after org :ensure ox-twbs ) :after 关键字的作用基本跟 with-eval-after-load 的作用是相同的,所以笔者所 有类似的org-mode 插件包都会在org-mode 加载以后才会加载\n3.2 :commands (use-package avy :commands (avy-goto-char avy-goto-line) :ensure t) 这里就贴上use-package文档 的说明了\nWhen you use the :commands keyword, it creates autoloads for those commands and defers loading of the module until they are used\n也就是 :commands 关键字就创建了后面所接的命令的 autoloads 机制了\n3.3 :bind :mode (use-package hi-lock :bind ((\u0026#34;M-o l\u0026#34; . highlight-lines-matching-regexp) (\u0026#34;M-o r\u0026#34; . highlight-regexp) (\u0026#34;M-o w\u0026#34; . highlight-phrase))) (use-package vue-mode :ensure t :mode (\u0026#34;\\\\.vue\\\\\u0026#39;\u0026#34; . vue-mode) :config (progn (setq mmm-submode-decoration-level 0) )) 附上文档说明\nIn almost all cases you don\u0026rsquo;t need to manually specify :defer t. This is implied whenever :bind or :mode or :interpreter is used\n也就是说,当你使用了 :bind 或者 :mode 关键字的时候,不用明确指定 :defer 也可以实现延迟加载机制。\n当然你也可以,直接使用 :defer 关键字来指定延迟加载. 不过前提是,你要明确它加载的时机\nTypically, you only need to specify :defer if you know for a fact that some other package will do something to cause your package to load at the appropriate time, and thus you would like to defer loading even though use-package isn\u0026rsquo;t creating any autoloads for you.\n贴上笔者自己的代码,可以更加清晰\n(use-package anaconda-mode :defer t :ensure t :init(progn (add-hook \u0026#39;python-mode-hook \u0026#39;anaconda-mode) (add-hook \u0026#39;python-mode-hook \u0026#39;anaconda-eldoc-mode) )) 这样 anaconda-mode 就会在 python-mode 加载以后被加载\nEnjoy Emacs :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/emacs_startup/","summary":"之前看到个笑话: A: 在等待emacs 加载的时间里,你会干什么? B: 打开Vim,修改代码,保存,退出 有时候,经常看到社区里面有人吐嘈Emacs 什么","title":"提高Emacs启动速度"},{"content":"1 Emacs Ipython 输出错误 在Emacs 运行 run-python 的时候,报错了,如下\n[?12l[?25h2+2 [J[?7h[?12l[?25h[?2004l[?7hOut[1]: 4 因为我的版本时Ipython5,查阅文档http://ipython.readthedocs.io/en/stable/whatsnew/version5.html#id1 之后,发现Ipython5 有了新的terminal 接口,和Emacs 继承的shell 不兼容,所以 会出现上述的错误,只要给Ipython 加上运行参数就能解决了,所以只要在 .emacs 或者对应的初始化文件加上下面语句\n(setq python-shell-interpreter \u0026#34;ipython\u0026#34; python-shell-interpreter-args \u0026#34;--simple-prompt -i\u0026#34;) 1.1 Update 2017-3-15 在添加了 \u0026ndash;simple-promp -i 参数以后,虽说乱码的问题解决了,但是新的问题又出现了 在Ipython 里面是没法无法输入多行内容的,即使是一个简单的循环,详情查看这条issue https://github.com/ipython/ipython/issues/9816. 现在Ipython 开发社区还没有解决这个 问题,所以现在的权宜之计就是使用 Ipython4,等到社区解决了这个问题在升级为 Ipython5\npip install --force-reinstall ipython==4.2.1 2 Emacs Ipython 的使用优化 2.1 python-pop 因为我之前使用Emacs的时候,是使用Spacemacs的配置的,但是后来觉得还是自己的 配置用的更舒服,所以又切换回自己的配置,但是我还是很想念Spacemacs的一些绑定 例如shell在底下弹出,或者是关闭,然后找到了Shell-pop 这package,就可以用回 Spacemacs的shell使用习惯。然后我觉得,Ipython shell也可以这样配置,只不过 我没有发现类似的package,又因为Emacs Lisp的强大,所以我自己写了一段小函数实现 shell-pop 的功能\n(defun samray/python-pop () \u0026#34;Run python and switch to the python buffer. similar to shell-pop\u0026#34; (interactive) (if (get-buffer \u0026#34;*Python*\u0026#34;) (if (string= (buffer-name) \u0026#34;*Python*\u0026#34;) (if (not (one-window-p)) (progn (bury-buffer) (delete-window)) ) (progn (switch-to-buffer-other-window \u0026#34;*Python*\u0026#34;) (end-of-buffer) (evil-insert-state))) (progn (run-python) (switch-to-buffer-other-window \u0026#34;*Python*\u0026#34;) (end-of-buffer) (evil-insert-state)))) 如果没有使用Evil,可以把 **(evil-insert-state)**去掉\n2.2 Ipython History 我在普通的Shell使用Ipython的时候,很自然地使用上下方向键翻到上一条/下一条 执行的命令,因为shell的使用习惯就是这样滴,但是在Emacs里面使用Ipython,上下 方向键是去到上一行/下一行,就好像 vim 的 j k,如果要翻到上一条命令,快捷键 是 M-p,实在很不习惯,所以在查了一下Emacs manual 后,我改了一下按键绑定就实现了 我想要的效果\n(define-key comint-mode-map (kbd \u0026#34;\u0026lt;up\u0026gt;\u0026#34;) \u0026#39;comint-previous-input) (define-key comint-mode-map (kbd \u0026#34;\u0026lt;down\u0026gt;\u0026#34;) \u0026#39;comint-next-input) Enjoy Emacs :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/emacs_ipython/","summary":"1 Emacs Ipython 输出错误 在Emacs 运行 run-python 的时候,报错了,如下 [?12l[?25h2+2 [J[?7h[?12l[?25h[?2004l[?7hOut[1]: 4 因为我的版本时Ipython5,查阅文档http://ipython.readth","title":"在Emacs中使用Ipython"}]
\ No newline at end of file
+[{"content":"1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。 先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI. 每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了. 今天是结课之日,我通过了结课的考试,总分100分,考了91分,喜提课程证书一枚. 在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费). 经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是) 但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈 2 Swift 初体验 Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。 按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。 因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling, Result, Generic, Enumerations, Protocol 这些概念都和Rust的大同小异。 又是由LLVM之父来操刀,所以语言本身也设计得很优雅. 让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension 功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能. 例如,以下的代码就可以扩展内置的 Double 类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换: extension Double { var km: Double { return self * 1_000.0 } var m: Double { return self } var cm: Double { return self / 100.0 } var mm: Double { return self / 1_000.0 } var ft: Double { return self / 3.28084 } } let oneInch = 25.4.mm print(\u0026#34;One inch is \\(oneInch) meters\u0026#34;) // Prints \u0026#34;One inch is 0.0254 meters\u0026#34; let threeFeet = 3.ft print(\u0026#34;Three feet is \\(threeFeet) meters\u0026#34;) // Prints \u0026#34;Three feet is 0.914399970739201 meters\u0026#34; 总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用 3 SwiftUI SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack, HStack, ZStack 就可以实现X轴,Y轴,和Z轴方向的布局 例如下面这个就是通过 ZStack 几行代码实现的叠加效果: let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple] var body: some View { ZStack { ForEach(0..\u0026lt;colors.count) { Rectangle() .fill(colors[$0]) .frame(width: 100, height: 100) .offset(x: CGFloat($0) * 10.0, y: CGFloat($0) * 10.0) } } } 除了声明式语法之外,SwiftUI让人赏心悦目的就是动画。好的动画在App里面绝对能起到画龙点睛的作用,而SwiftUI的内置动画已经非常强大了,下面就是使用内置动画实现的动画效果: struct ContentView: View { @State private var dragAmount = CGSize.zero @State private var enable = false let letters = \u0026#34;Hello, World\u0026#34; var body: some View{ HStack(spacing: 0) { ForEach(0..\u0026lt;letters.count, id: \\.self) { index in Text(String(letters[letters.index(letters.startIndex, offsetBy: index)])) .padding(5) .font(.title) .background(enable ? .green : .blue) .offset(dragAmount) .animation(.linear.delay(Double(index) / 20), value: dragAmount) } }.gesture( DragGesture() .onChanged { dragAmount = $0.translation } .onEnded { _ in dragAmount = CGSize.zero enable.toggle() } ) } } struct HeartBeatView: View { @State private var animationAmount = 1.0 var body: some View { Button(\u0026#34;SOS\u0026#34;){ } .padding(50) .background(.red) .foregroundColor(.white) .clipShape(Circle()) .overlay( Circle() .stroke(.red) .scaleEffect(animationAmount) .opacity(2 - animationAmount) .animation(.easeOut(duration: 1) .repeatForever(autoreverses: false), value: animationAmount) ) .onAppear { animationAmount = 2 } } } 而Xcode 15新增的预览功能也很好用,可以让开发者不需要启动iPhone模拟器就能预览页面效果,节省了非常多的等待时间。 4 问题 听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。 SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData 甚至只支持IOS17, 更离谱的是,SwiftUI的 BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。 不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI: 对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户, 而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。 因此SwiftUI就陷入了一个尴尬的境地,东西做得好,但是不会有人用; 没有人自然就不用有人分享,宣传这门技术,自然就导致相关的学习资料非常匮乏, 进一步加深了初学者的学习难度; 开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。 又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦. 另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage 框架,顾名思义是用来作图片处理. 但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如: func applyProcess(){ guard let outputImage = currentFilter.outputImage else {return} guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return} let uiImage = UIImage(cgImage: cgImage) processedImage = Image(uiImage: uiImage) } func loadImage() { Task{ guard let imageData = try await selectedItem?.loadTransferable(type: Data.self) else {return} guard let inputImage = UIImage(data: imageData) else {return} let beginImage = CIImage(image: inputImage) currentFilter.setValue(beginImage, forKey: kCIInputImageKey) applyProcess() } } 把 CoreImage 框架的 CIImage 转成 CoreGraphics 框架的 CGImage, 然后再把 CGImage 转换成 UIKit 框架 UIImage, 然后再转换回SwiftUI 内置的 Image 类型, 可谓是相当麻烦了. 但是对比SwiftUI只支持高版本的问题,Objective-C和Swift的互操作问题也只能算是恶心,但是起码有解决方法,对于前者,开发者是完全没法自行解决. 5 总结 过了一把野生IOS开发的瘾,但是除非是苹果愿意让SwiftUI支持低版本的IOS, 不然我是不会继续使用SwiftUI来开发IOS了,受众太有限了,也没有太大的前景。 想要支持低版本的IOS,就只能走UIKit和Objective-C这条历史老路,我对此着实是望而生畏,有空还是学习点其他有趣的东西。 https://docs.swift.org/swift-book/documentation/the-swift-programming-language/guidedtour/ \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.hackingwithswift.com/100/swiftui \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/ramsayleung?tab=repositories\u0026amp;q=\u0026amp;type=\u0026amp;language=swift\u0026amp;sort= \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://en.wikipedia.org/wiki/Chris_Lattner \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/","summary":"1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副","title":"100 Days of SwiftUI"},{"content":"2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放。 在春日的夕阳下,漫步在樱花树下,微风吹过,樱花落下, 着实有「落英缤纷」的感觉 看着眼前的樱花,想着她冬日的美貌,脑海浮起诗句: 昨日雪如花,今日花如雪 ","permalink":"https://ramsayleung.github.io/zh/post/2024/%E4%B8%89%E6%9C%88%E7%9A%84%E6%A8%B1%E8%8A%B1/","summary":"2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放","title":"三月的樱花"},{"content":"1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英语,而不是像在学校那样「学」好英语。 在《口语篇》中,李笑来提到,比口语更重要的是思考能力,英文说不出口的原因,可能是脑子里面没有什么思考沉淀的东西可以说的,并藉此推荐了三本关于文风(Style)的必读书籍,其中一本就叫:A Plain English Handbook(简明英文写作指南)2。 这本书竟然是美国的证券交易委员会(Securities and Exchange Commission)1998年编著的, 旨在指导投资机构和金融机构创建更清晰,更易理解的披露文件。 可能是投资机构写的东西,普通投资者根本看不懂,逼得证券交易委员会都要下场指导投资机构写作了。 这本小册子只有83页,内容却很详实,读完之后,觉得其中的许多技巧不只适用于英文写作, 因此就结合读后感和个人心得,分享下简明写作的心得。 2 为什么写作 以我自己的工作为例,日常开发新项目,需要撰写文档来向同事和经理介绍项目背景, 动机和具体的实现细节,以寻求支持并推动项目的进展;技术交流时,需要撰写文档分享你的成果和经验;在晋升时,需要撰写文档,给自己找数据点来说服经理,为什么要给我晋升。 需要让别人「看见」我的时候,写文章就是一种非常好的手段,默默无闻的老黄牛,是很难被人看到的,酒香也怕巷子深。 你可能会认为自己工作用不到文档,但是你总归是要向同事或者上司阐述自己的观点, 无论是述职,晋升,演讲,甚至口头汇报,用文档作腹稿, 理清脉络,做到胸有成竹。 关于写作的动机和好处,我之前写过一篇文章专门来聊:《闲聊写作的好处》,这里就不赘述了。 3 如何写 如果把文章比作一个人的话,那么文风就是人的血肉和皮囊,文章的结构就是人的骨架,只有当骨架先立了起来,才能在其上涂血肉,张皮囊。 A Plain English Handbook 主要介绍的是改善文风的技巧,那么如何立起文章的骨架呢?我推荐的是黄金圈法则和金字塔原理。 3.1 结构 3.1.1 黄金圈法则 所谓的黄金圈法则,概括来说,就是思考问题的三个层面,分别是: Why: 最内层, 为什么,做一件事的原因或者目的,也就是我们为什么做这样的事情,战略层面。 How: 中间层, 怎么做,我们如何实现我们想要做的事情, 战术层面的事 What: 最外层,事情的表象,我们具体做的每一件具体的事,执行层面的事 比如我要推动一个新项目,需要写项目文档,或者口头向老板阐述项目,如果不用黄金圈法则,可能会是这样表述的: What: 我要做一个XX的服务,会有哪些新功能 How: 我是通过什么业界领先的XX技术实现的,用到了什么组件. 如果以黄金圈法则来重写项目文档,那么文章的结构应该是如何的: Why: 我们为什么要做这个项目?这个项目能给我们带来什么好处,不做会有什么损失? How: 大方向上应该怎么实施,大概会用到什么组件,架构如何? What: 具体的底层实现是什么,每个组件是怎么实现的? 大方向没有定好,再走下去也只是南辕北辙,于事无补。 其实这篇文章也使用了黄金圈法则,开篇就介绍为什么要写作,后续再介绍如何写作。 3.1.2 金字塔原理 关于金字塔原理,我觉得冯唐的《老聃的金字塔原理》一文3已经解释得非常清晰明了了: 用一句话说,金字塔原则就是,任何事情都可以归纳出一个中心论点,而此中心论点可由三至七个论据支持,这些一级论据本身也可以是个论点,被二级的三至七个论据支持,如此延伸,状如金字塔。 如果用金字塔原理来分析「为什么小王是个好对象」的论点,那么论据就可以拆分成: 家境殷实 年入百万 有车有房无贷 父母养老无忧 前景光明 名校毕业 年纪轻轻身居中层 领导赏识 相处融洽 提供情绪价值 共同话题多 情绪稳定 颜值高 皮肤白里透红 五官端正 身材修长 穿衣显瘦,脱衣有肉 基因好,后代获得先发优势 写作时,每个一级论据就是一个大的篇章,每个二级论据就是篇章下的章节,三级论据就是章节里面的小节, 依此类推,并给予最底层的论据适当的文字描述。 如果还不够的话,还可以继续向下拆分论据。 有了黄金圈法则和金字塔原理,就很容易把一篇文章的结构给搭起来。 3.2 文风 3.2.1 明确你的观众 明确你的观众,是确保你写的文档能让人理解的最重要步骤。 不同的读者,有不同的背景,对你要传递的信息是有不同的理解难度。 以我自己为例,因为我在公众号写的文章大多都是与编程技术无关的, 那么吸引到的读者自然也不会是编程从业者,所以我在公众号里面写技术文章,基本不会有什么读者阅读。 兼之微信这个阅读平台本身的局限,大多数情况下只能是在手机上阅读, 读者无法投入大量时间「沉浸式」地主动阅读,可能是快速下拉翻页读完了。 而图表和代码在手机屏幕上展示效果不佳,就进一步影响阅读体验了。 如果我把写满技术名词和代码的文档给一个完全没有技术背景的读者来阅读, 即使我的文档写得妙笔生花,对他也没有任何信息可言。 因此我基本不会在公众号写技术文章,技术文章都放在更适合在电脑阅读的博客上。 这里还有个小技巧,就是在明确你的观众的时候,可以设定到一个具体的人, 例如是你的女友/男友,你的同事,或者是你的经理。 具体的人比抽象的概念更深入人心。 3.2.2 言简意赅 能用简洁明了的段落表达全的信息,就不要长篇大论。 读者是有心智负担的,文章的内容越长,读者的负担越重,就越有可能在还没读完的情况下将文章关闭。 另外一方面,如果要传递的信息量是固定的,你的文章内容越长,你文章的信息密度越低,通俗点来说,就是干货越少。 所以我对「万字长文,讲透xxx」,「爆肝x天,四万字长文带你解读xxx」之类的文章不感冒, 文字多也不能说明干货多,爆不爆肝和干货含量也没有逻辑关联。 现在写作不按字数算稿酬,不需要搞「文字灌水」。 《唐宋八家丛话》中有一说: 欧阳修在翰林院时,常常与同院他人出游。 一次,见有匹飞驰的马踩死了一只狗。 欧阳修说:“请你们尝试描述一下这事。” 一人说:“有犬卧于通衢,逸马蹄而杀之。” 另一人说:“有马逸于街衢,卧犬遭之而毙。” 欧阳修笑说:“像你们这样修史,一万卷也写不完。”那二人说:“那你说呢?” 欧阳修道:“逸马杀犬于道。” 那二人相互笑了起来。 3.2.3 少说黑话 少说黑话和行话,例如「组合拳」,「赋能」,「抓手」之类的,字我都认识,合起来就不明白是什么意思。 如果你明确了你的观众,你可能就会意识到你的观众大概率无法理解这些话语。 多用具体的,意思明确的词,会更让读者更容易理解。能用简单的话解释清楚一个复杂的概念,就说明你对这个概念的认识越到位。 黑话用多了,也是一种「文字腐败」。 3.2.4 控制段落长度 上文提到,读者是有心智负担,负担越大,他们就越有可能在阅读你文章时「半途而弃」。 而一大片文字密密麻麻糊在一个段落,就会进一步加重他们的心智负担。 我一般推荐80到150个字一个段落,这样看上去不至于太吓到读者。 3.2.4.1 使用空行 段落与段落之间,用 空行 分隔. 空行是个非常简单,但是却异常有效的技巧,既可以拆分段落,控制单个段落的长度,也可以表达不同段落逻辑上存在并列或者递进的关系,便于读者理解。 同样的文字,使用空行分隔段落的前后对比如下: 3.2.4.2 使用列表 另外一个控制段落长度的有用技巧,是使用列表(bullet list),可以表达列表中的每个点都是并列关系。 例如,分析跑步的好处: 跑步不仅可以增强免疫系统,帮助抵御病毒和细菌,降低患病风险; 还有助于心肺功能,降低心脏病和中风的风险; 更可以减脂、增强肌肉,改善体形; 甚至促进肠道蠕动,有助于消化。 如果换成列表,那么跑步的好处就是: 提高免疫力:增强免疫系统,帮助抵御病毒和细菌,降低患病风险 改善心血管健康:有助于心肺功能,降低心脏病和中风的风险 塑造好身材:可以减脂、增强肌肉,改善体形 促进消化:促进肠道蠕动,有助于消化 如果还有优点补充,只需要继续增加列表就好了。 3.2.5 一图/表胜千言 人的大脑对图片远比文字和声音敏感。 图片比文字来说,更容易被大脑接受,大脑储存图片信息也不需要进行过多的转译,而文字进入大脑之后,还需要用“想象力”处理成画面进行记忆,这也就是为什么带生活实例的文字会比概念化的文字更容易让人记住,因为前者更容易让你想象具体的画面。 通俗地讲,就是一图胜千言。 所以要减少读者的心智负担,那么就应该多使用图表,因为它能更直观地传递更多信息。 以国家统计局发布的2月份居民消费价格分类同比涨跌幅为例,文字描述如下: 其中,教育文化娱乐、其他用品及服务、衣着价格分别上涨3.9%、3.0%和1.6%,医疗保健、生活用品及服务、居住价格分别上涨1.5%、0.5%和0.2%;交通通信价格下降0.4%。 图表如下: 关于如何画图,我之前也写过一篇文章来分享心得:《我的画图流:画图工具与技巧分享》 或者换成表格: 类别 涨跌幅 教育文化娱乐 + 3.9% 其他用品及服务 + 3.0% 衣着 + 1.6% 医疗保健 + 1.5% 生活用品及服务 + 0.5% 居住 + 0.2% 食品烟酒 - 0.1% 交通通信 - 0.4% 4 如何写好 4.1 多读多思 杜甫说,读书破万卷,下笔如有神。诗圣的意思是如果你通读过万卷书,就好像ChatGPT一样,思如泉涌,下笔如有神 陆游又说,纸上得来终觉浅,绝知此事要躬行。陆放翁的意思是,你在书上的读到的东西,也只是信息,如果没有实践过,终究不能成为技能的。 总结两位老人家的话,也就是说,多读书是写好文章的必要条件,多读书不一定能写好文章,但是不多读书呢,就一定写不出好文章。 毕竟写出来的东西,不会凭空而来,还是你脑子思考好的结果。 4.2 多写多改 与金庸并称香港四大才子的倪匡,一生写了300多部小说、400多部电影剧本。 他曾经自嘲没有谋生本能,所以看见人家写自己也写,自称是全世界写汉字最多和最快的人,自入文坛已写作三十年, 一个星期写足七天,每天写数万字。他在创作最高峰曾同时写作十二本科幻小说。 当被问及有何建议赠给写作的人,倪匡说只有一个「写」字,只有多写才能得到更多的灵感。 苏轼的《东坡志林》写到,有人问欧阳修怎么写文章,他回答说: 无他术,唯勤读书而多为之,自工。 世人患作文字少,又懒读书,每一篇出,即求过人,如此少有至者。 疵病不必待人指摘,多做自能见之。” 世人文字写得少,又懒读书,写一篇文章就希望可以超越别人,像这样是难有写得好的人。 书读多了,落笔为文,文章写多了,自然就写好了。 写作的「捷径」就是老老实实多读书、多思考、勤写作。 多练多写多修改。 只有产量提上来了,工艺才会成熟,良品率才会提高。 5 总结 写作并非是雄关漫道,遥不可及。 大家都写过作文,写作也只是一种交流方式,无非是把脑海中的想法以文字的形式付诸于纸上,比聊天更加正式而已。 千里之行始于足下,万巻之文始于笔下,开始写就好了。 5.1 历史文章推荐 旅加经历 这些年走过的路:从广州到温哥华 加拿大之初体验 加拿大考驾照的经历 登陆加拿大一年后的体会 历史与思考 为什么梦想买不起,故乡回不去(和谐版本, 原版本4) 润向何方:不完全肉身翻墙指北 皇帝与官僚:「上面」与「下面」 闲聊写作的好处 金榜题名之后 工具与分享 我的写作流 我的画图流:画图工具与技巧分享 我的搜索流:高效搜索经验分享 最好的学习方式:费曼学习法(Feynman Technique) 系统思考:既见树木,又见森林 两个鲜为人知的Gmail地址技巧 职场与思考 那些年,我从微信支付学到的东西 https://github.com/xiaolai/everyone-can-use-english/blob/main/book/README.md \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.sec.gov/pdf/handbook.pdf \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://zhuanlan.zhihu.com/p/196733201 \u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttp://ramsayleung.github.io/zh/post/2023/%E7%BD%AE%E8%BA%AB%E4%BA%8B%E5%86%85/ \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://ramsayleung.github.io/zh/post/2024/%E7%AE%80%E6%98%8E%E5%86%99%E4%BD%9C%E6%8C%87%E5%8D%97/","summary":"1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英","title":"简明写作指南"},{"content":"1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取士以来,通过读书来改变自己乃至整个家族的命运,就已成为中国社会妇孺皆知的常识,”读书改变命运“。 古有《劝学诗》云:“男儿若遂平生志,六经勤向窗前读”;今有高悬在高中教室的励志标语:“辛苦三年,幸福一生”。 但是,进入好的大学,是否就意味着毕业时令人羡慕的工作呢?为什么社会出身劣势的学生即使进入了最好的大学,却仍然在毕业出路与生涯前景劣势明显? 对于寒窗苦读,从中国甘肃考入复旦大学的作者来说,她也有同样的疑问。 因此在研究生期间,就将学生家庭背景与教育结果之间的关联作为研究课题,并将研究生论文润色,扩展成书。 在读别人的书时,我总会想起自己的事,好比用别人的料理方式,来烹饪自己的食材,看能煮出什么样的【佳肴】。 虽然我未曾【金榜题名】,但也未妨我来分享下自己的所思所想。 2 迷宫的游戏规则 以前高中老师在我们向其抱怨各种学业压力和严苟的规章制度之后,总是会语重心长地解释一番,然后在对话的结尾补上一句,”上到大学你们就轻松啦“。 不同于高中只有成绩的单一的显式考核标准,只有一个方向和出口,大学更像是一个被精心布局的迷宫:并不存在一条”主干道“或者标准的走法,每一天的过法都有许多可能,每个人在路口处需要不断地做选择,每一条小路(社团,绩点,实习)都各有乾坤。 而大学生作为玩家,需要在小路之中穿行探索,一边选择自己路线,一边在路途收集有价值的筹码(成绩,经历,奖项等)。 当他们到达迷宫出口时,他们需要将口袋的筹码拿出来,用它们来兑换成下一个旅程的入场券。 只是,对于不同社会出身的探索者来说,这个迷宫的神秘程度是不同的。有人对里面的布局相当了解,有人半知半解,而有人只能通过道听途说略知一二。 尤其关键的是:并不是所有人都很清楚当中的游戏规则,譬如迷宫的尽头究竟有哪几个出口,而每个出口处有用的筹码又有什么不同。 而不同等级的大学,能提供的有价值的筹码数量和价值又是截然不同的。 为了区分在两种截然不同的驱动力下探索迷宫的玩家,作者将两者定义为: 目标掌控模式:了解大学及劳动力市场中的规则,因此能有意识地树立生涯目标,并且通过管理自身的行动来趋近目标。 直觉依赖模式:在陌生的大学场域中难以自我定位,从而无意识地陷入无目标状态,主要倚靠直觉和旧有习惯来组织大学生活。 家境优越的学子,在父母的指导下,可以更早和清晰地认识到大学的游戏规则,进而策略性地计划大学生活;而寒门学子,因为眼界和自身经历的局限性, 并不了解大学的规则,又会发现高中沉淀的学习方式在大学并不完全适用,难免陷入迷茫。 优势阶层的父母基于对迷宫的洞悉,为孩子织就一张“安全网”,帮助孩子认清形势,定位自身,树立目标,顺利通关;而弱势学生则没有这张安全网,只得独自在这个陌生领域无助摸索前进,父母能提供的建议越来越少,往哪走全靠自己或对或错的直觉和过去的习惯。 但因为作者只调研了名校的优等生,如果把排名后50%的学子也纳入调研范围,可能会发现还有一种【躺平放纵模式】,以高中老师口中【轻松】的方式度过了大学生涯。 3 非名校生的大学 近年来,红遍网络的“985废物”和“小镇做题家”的自嘲俯拾即是: “从一个连电影院都没有的小县城,考到了全河北最好的高中,六年之后要来到国际大都市上海了,要来到倾尽我家所有小积蓄,才能勉强付个首付的上海了。我这才发现,光考了好大学也是没有用的。” \u0026ndash; 《我上了985,211,才发现自己一无所有》 如果考上985,211的“小镇做题家”都如此自嘲一无所有,那些没有考上211的小镇考生,又该如何自处呢? 当聊起大学和学历话题的时候,我时常和名校毕业的高中好友开玩笑,我不是“小镇做题家”,虽然我和你们一样来自小镇,但是我连题都做不好。 3.1 直觉依赖者的探索之路 如果按照书中作者的分类标准,我属于来自农村的,第一代大学生(父母都不是大学生)的直觉依赖模式学子。 是那种最需要靠教育改变命运的学生,并不真的知道该在大学里如何做才能改变命运。 对于升学,出国,就业这三个出路,我一开始就把【出国】这个选项给排除了,小镇来的我,家里并没有足够的财力支撑我出国,何况我的学校也不容易申请到好学校(现在看来,这个理由略为牵强)。 升学读研也不是我的目标,我对走学术路线没有兴趣,读完研也终究要工作,计算机相关行业,研究生学历与本科学历相比,并没有跃迁式差距。 所以我一开始就选定了【就业】的路线,我也希望可以早点赚钱为父母分担。 因为没有人指导,又不清楚迷宫的规则,我并不清楚要如何做才能取得【就业】的优势,或者做好就业的准备。 大一开始时,还是按照高中的【勤学苦读】模式,晚上没有课的时候去课室学习,解高数的练习题,再预习专业课。 这样的学习模式持续了两个月,觉得相当别扭和迷茫,看到周围宿舍的大神们已经开始编写代码,甚至其中还有中学就开始自学编程的大神,我解高数题又有什么用呢。 恰好赶上学校的社团招新,我就加入一个校级社团的【网络部】,希望可以借机学习到计算机相关的知识,学习怎么修电脑或者装操作系统。 社团生活的确让我学习到基础的电脑维修知识,更重要的是,让我认识到同系的学长们,对于我想学习修电脑的兴趣,他们纠偏道,单纯的硬件组装其实没有太大的潜力,还是做软件开发更有前途。 在学长们的指导下,我开始到慕课网和W3School学习Html,CSS,Javascript这三剑客,开始学习前端开发,而后我又了解【前端开发】,【后端开发】,【移动端开发】的职业路径。 浅尝【前端开发】之后,发现自己对此并没有太多兴趣,又开始尝试【后端开发】的路线。 因为慕课网有把【后端开发】的课程列出来,我就知道【后端开发】需要学习什么样的技术,如Java/C++,数据库,计算机网络,我就可以有的放矢地去豆瓣上搜索对应的高分书籍来自学。 我的迷宫有了【地图】,我自然是要加速前行,我在大二的时候,就用豆瓣上列出来的经典教科书,把相关的必修专业课给自学完了,效果虽不如老师亲授,但终究有所获。 就如我在《那些年走过的路,从广州到温哥华》介绍过,而后就走上做项目和实习之路。 对于【直觉依赖模式】的学子来说,我觉得了解迷宫游戏规则最好的方式是和同系的学长学姐交流,因为他们曾处在和你同样的境况,又就读于相同的院校和专业,并且他们先于我们两到三年去探索这个迷宫,所以他们能提供最切实可行,并且可复制的路径。 3.2 答案并不唯一 正如作者所言,这个迷宫会有不同的出口,在大学过的每一天会将学子领入不同的小路。 二十岁的我可能希望可以从自己愿景的出口走出来,三十岁的我却有了不一样的看法,能找到自己的迷宫出口固然可贵,但是探索迷宫的过程也相当珍贵,大可不必那么功利,迷茫也是人生常态。 【就业】,【升学】,【考研】只是社会的评判标准,做个有趣的人也未尝不可。 4 大学之后 在大学的时候,受学长们的影响,觉得能去BAT(Baidu,Alibaba,Tencent)的大厂,就算是个很好的出路了。 所以当时一心把去大厂当作毕业的目标,当拿到个A厂Offer之后,就没有面试其他公司,自然也没有offer来compete。 但是我从来没有想过,拿到Offer,入职之后,又该如何? 再追求升职加薪,面向晋升编程? 以前是把高考结束,考上大学当作是马拉松的终点,上大学之后又把拿Offer当作迷宫的出口? 如果人生是个没有出口的迷宫呢。 4.1 迷宫并没有出口 作者说大学是个有多个出口的迷宫,如果把迷宫的概念延伸出来,每个人的人生从大学之后就都是迷宫了(如果不上大学,那么出路就更早分化),高中之后,就不再是以成绩作为唯一的标准。 当标准都不一样,就更没有办法衡量什么是【最好】的路,只有最适合自己的路。 享受探索迷宫的过程也可成为乐趣。 4.2 路要怎么走 过年前和发小喝茶聊天,聊到了他为人夫,为人父的事情,他提到当初他在产房等待女儿出生的时候,护士和他聊天的事: 护士问,他是否是95后,他说是。 护士说,95后现在都不想生孩子了,觉得压力很大。 我:那你有同样的感觉么? 发小:有阿,也觉得压力很大。 我笑道:那你还生? 发小笑回:世界上只有一种英雄主义,那就是认清生活的真想之后,继续热爱生活。 在两个人的笑声中,听到发小说出罗曼罗兰的名句,觉得发小已经活出这句话的精髓了。 写在最后,鸡汤一下: 路要怎么走,每个人都会有自己的选择,但了解迷宫的规则的【学子】,路可以走得更加自在。 5 参考 《金榜题名之后:大学生出路分化之迷》 《我上了985,211,才发现自己一无所有》 ","permalink":"https://ramsayleung.github.io/zh/post/2024/%E9%87%91%E6%A6%9C%E9%A2%98%E5%90%8D%E4%B9%8B%E5%90%8E/","summary":"1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取","title":"金榜题名之后"},{"content":"1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾里看花,再分享自己在加拿大的亲身经历 本文算是《那些年走过的路,从广州到温哥华》和《温哥华的初体验》的后续。 2 Work Life Balance 自我工作以来,基本就是在体验995的工作节奏,我曾经无数次【幻想】过,如果我能每天5点下班,我的生活会是怎么样的? 我会有接近6-7个小时的空闲时间,我会把这些时间用来干什么呢? 当我不曾拥有时,我总是在不断地想象。 但当我真的可以5点,甚至4点多下班的时候,我并没有我自己想象的那般激动,欣喜若狂,而是当作理所当然,很平淡地接受。 毕竟我所在的BC省,法定工作时间都只是7.5个小时,我朝九晚五,甚至有点高于本地平均水平,尤其是我在北美著名的【血汗工厂】打工,需要Oncall,甚至比本地公司还要卷,所以我开始觉得朝九晚五工作时间有点长。 人阿,就是贪婪,总是会得寸进尺,得陇望蜀。 我5点下班之后,我可以【奢侈】地花一个小时去做晚餐,然后吃完晚餐和舍友一起看个把小时的电视,一边撸着猫一边吐槽今天的工作内容;或者在天气放晴的时候,和舍友在附近的公园饭后溜圈,再考虑下明天要学习哪个视频,做点什么新菜。 饭后到睡前的时间,花一到两小时,学习一下新的技术,Swift或者Ruby on Rails,或者读读新书,又或者和家人亲友视频聊个天,互诉衷肠。 原来那些失去的,用于加班的时间,重新获得后,也只是把它还给生活本身。 3 英语 未落地加拿大时,最最忧心的问题就是自己的英语不过关,无法正常地与人沟通交流,也无法正常工作。 毕竟我此前没有考过雅思,也没有在纯英文的环境中生活过,不知道自己英语水平如何。 落地之后,强迫自己开口对话,虽然难免会出现词不达意和【执笔忘字】的情况,但终究是敢开口说话了, 难免会遇到不认识的词不标准的发音,但是快速纠错之后,情况就慢慢在好转,脸皮厚一些就好了。 后来还花了两周时间准备了雅思考试,顺便测试下自己的英语能力,然后考了个7分,好像还行。 刚开始产品经理们开会,他们都是美国人,是真能扯,语速也真的快,好像高中时候的英语听力一样,只看到两个人不停地在张嘴说话,大脑一片空白。 到后面熟悉公司的黑话之后,情况也在慢慢变好,也听懂他们在说什么了,的确也是在扯。 从以前非常紧张与同事1:1开会,当现在已经能主动和同事拉会1:1,我可以感觉到自己的听力和口语能力也在不断地提高。 说到底,外语也不是什么特别的秘技,也只是种用进废退技能而已。 4 惊喜 人言洛阳花似锦,偏我来时不逢春。 想来入行前,都是听说互联网公司的各种红利,但是当然真正来到这个行业时,才发现自己啥红利都没有吃上,来了都是当人矿的,起了个大早,赶了个晚集,还碰上各种【奇遇】。 想我22年中面试的时候,那时还在北美【大放水】,通货膨胀的期间,各种大包满天飞,我却因为 international hire的原因,只赶上个low ball 包,因为我此前已经遇过很多次,已经可以泰然处之。 但是到23年初,受美国加息降通涨的影响,Meta和Twitter开了个坏头,北美的互联网都开始裁员,我司也不例外,不仅是裁员,连发出去的Offer开始撤回或者延期,然后我的Offer 也被影响了,原来面的组岗位被取消了。 我当时的心情不算是五雷轰顶,也算得上是晴天霹雳。 还好找到新的组接收,然后岗位被搬到另外一个新组,无论如何,先干着吧,不至于还没入职就失业,起码干的事情是新的,一切都是从0开始。 5 什么TM的叫惊喜 在新组,我是组里的第一个SDE(软件工程师),之前的两位组员都是DE(数据工程师),manager也是刚升任成经理的,甚至我入职时,他的 title都还没有变成 manager。 马上我发现,组里是新人,新组,老代码,人是新的,但是代码却是历史代码,我们需要去维护这些历史代码,但是没有人能解释其中的逻辑为什么要这么写? 紧接着,我发现,代码主体都是SQL,项目的逻辑隐藏在数以万行计的SQL代码中,因为SQL的抽象程度高,就更难以理解业务逻辑了。 4月底,在我入职不到3周,我就被安排成为一个新项目的 owner,然后被告知要在半年后的 Re:Invent发布,当时我甚至不知道什么是 Re:Invent. 后来才被告知,Re:Invent之于我司,犹如【WWDC】之于Apple,【微信公开课】之于微信,都是用来发布新产品的全球大会。 我当时心想,老板还真的看得起我嘛。 本着【能力越大,责任越大】的自嘲心理,我就这样接手了这个项目,成为了Owner。 和我的直属manager,总监以及产品沟通之后发现,他们似乎只要求要做这样一个产品,但是这个产品是什么, 应该怎么做,都是完全没有概念,也没有文档。 在我的认知中,一个项目从提出到上线的完整生命周期应该包括以下的部分: 某位领导或者产品经理提出新产品的想法 完善 use case, 细化想法 产品的各个利益方(stakeholders),或者叫涉众达成共识,领导层面获批 产品出需求文档,明确要做什么,具体的业务规则是什么 技术评估需求可行性 技术出设计文档 技术根据设计方案给出排期 技术开发需求,自测,内部上线 产品及涉众验收产品 内测及公测上线 然而,我现在拿到手的只是一个模糊的需求概念和上线的日期,没有详细的需求文档,口头描述了大概要做什么。 我只能不停地追着产品经理和manager问他们我要做什么,能否先给我个需求文档,对于需求文档,产品经理也不会直说没有, 只是会说解释一通后,让你意会到没有,我只能当练习英语听力。 最后我被告知,先把senior data engineer写的一大段SQL转成服务代码,把End-to-End的结果跑出来再说。 我就不懂,既然SQL都能跑了,还要我写个服务来跑SQL呢? 咨询了一番,我还是没得出个所以然,最后只能是按照这段SQL来写设计文档,并根据设计方案开发服务。 心里第一次浮起疑问:【贵司的做事方式就是这样的么?它是怎么做到这种规模的?】 7月初,美国转来了一位L6的 senior SDE还有一位L5的SDE也加入到项目里面,以缓解资源不足的问题。 加入后不久,这两位工程师也问起了需求文档的事,得到的回复也是言不及没有需求文档,意含没有需求文档。 没有需求文档实在是没法干活,最后是我们三个技术开发溱一起,每个人把自己对需求的理解一人一句写下来,也算是人生新经历了。 7月底,服务End-to-End 跑通,将结果呈给总监与产品经理,然后总监和产品经理反馈这不是他们要的,要求修改需求。 8月,根据修改后的需求重新设计服务,分成三个模块,三个工程师每人负责一个模块。 总监和产品经理再修改需求,并要求开发进行建模,但是新需求的模型不具备可行性,产品经理无法给出具体的业务规则,最后开发无法建模,导致新需求被搁置。 9月,主力产品经理突然宣布离职,此时离Re:Invent 不到两个月。 10月初,开发按照变更后的需求完成服务开发,然后发现服务使用的源数据全部是脏数据,服务结果不可用,团队已有使用该数据源的服务也是错的,开发紧急调研,再切换到新数据源。 11月初,所有服务组件万事具备,只待Re:Invent东风,然后被产品经理告知,我们的项目不能发布,因为没有在领导层面获得批准。 所以让开发紧赶慢赶,干了半年的大项目,连审批都还没有通过。 开发项目期间不停地浮起疑问,【按照这种做事方式,这家公司是怎么做到这种规模的?】 但做人不能半途而废,过河抽桥,所以即使心中百般疑问和不解,我依旧是尽心尽力把这个项目做完。 在做完这个项目之后,我就谋求转组了,这样的做事方式着实不是我的风格,我主观认为也非长久之计。 1月,GM(老板的老板的老板)离职。 2月,总监也离开了这个部门。 6 裁员阴云 自从2022年起,中美的互联网行业都笼罩在裁员的阴云之下,只是两者背后的原因各种不同。 朋友们在我登陆加拿大之后也和跟我吐槽国内环境变差,红利期已过,我只是个臭写代码的,也分析不出其背后的原因。 但是我知道的,大洋彼岸的北美大厂也在持续裁员,首当其冲的就是Google等大厂, 在人们2024年不要再有裁员的期待中,1月Google就以裁员来开年,真是【合家富贵】。 疫情时期的【大放水】,导致大厂们都用大包疯狂扩张,为了抑制通胀而采取的加息措施让企业们紧缩信贷, 压减成本,而人力成本在互联网大厂中可谓是占大头,然后在Meta和Twitter的带头下,开始挥刀裁员。 开始时,各大厂裁起员来还有些扭捏,裁完人公司高层还会写信安慰员工,说就裁这一波,高层还会出来道歉背锅。 然而裁到现在,已经变得明目张胆,和肆无忌惮,像Google这种, 都宣布2024年会持续裁员,还有其他大厂,就没有正式宣布裁员和什么时候结束裁员,就这么裁着。 毕竟在缺乏增长点情况下,裁员能缩减开支,让财报好看。 至于打工人们的看法,从来就不会有人在乎的。 所以「工作」也回归到它本身的意义上,这也只是份工作而已,It\u0026rsquo;s just a job,不要赋予工作过多的意义。 「得益于」裁员,我现在对工作的看法已经很佛系了,以前那种拼命卷,想拿到好绩效证明自己的想法已经不复存在了,也难怪朋友会说我现在心态变好了。 7 万税之国 虽然在来加拿大之前听说过加拿大的税非常高和多,但是只有从我的钱包把钱拿走,才能切实体会到什么叫【万税之国】。 除了薪资收入30+%的个人所得税外,还有日常消费12%的消费税,15%的酒税, 以及超过50%的资本所得税(比如银行存款利息,基金,股票收益等等),各种五花八门,名目繁多的税种。 虽然知道【死亡和缴税无可避免】,但是死亡是一次性的,缴税却是持续性的。 更何况,交税后的许多社会福利却是和收入挂钩的,你的收入越低,能享受到的福利就越多,而富人又有非常多的避税手段。 像 daycare, 牛奶金,低保这些,都是和每个人的收入挂钩,低就有,高就没有。 所以说下来,而低收入者可以少交税,却问政府要钱要福利;富人又可以避税,只有老实打工的中产是被收割的,福利又少。 难怪人们总说,加拿大适合躺平吃福利,不适合来挣钱,带资来加拿大的可以靠吃政府福利过得非常滋润。 8 医疗 加拿大的医疗体系是吃全民大锅饭,免费医疗。 免费的饭一般都不会很好吃,也不会很容易吃到。 加拿大的医疗体系我还没有机会亲身体验过,但是舍友有过几次的问诊纪录,原来抽个血化验排队等个两-三个小时着实是件很稀松平常的事。 9 此处并非天堂 世界上不存在天堂,所以如果抱着前往天堂的愿景来加拿大,难免会失望,加拿大也有自身的问题。 疫情期间为了保消费实行的【大放水】政策导致持续的高通涨, 高企的物价,为了抑制通涨而实行的加息政策而导致高企的利率,7-8%的房贷车贷利率。 飙升的房价,虽然待过深圳的我觉得温哥华房价还赶不上深圳, 但是对比温哥华本地的中位数收入,温哥华的房价已经远远高于居民的中位数收入,一般人都负担不起了。 以房租举例,我现在与舍友合租,房租以人民币计价,大概是我之前在深圳的四倍。 增加的移民人口与减少的工作机会,各种【苛捐杂税】让带资过来的移民和本地的金主都不需要创办企业, 资本没法流动起来,自然不能创造就业岗位,随着移民人数的增加,以及激进的难民接收政策,就进一步加剧【僧多粥少】的问题。 而加息导致的企业的信贷紧缩,也抑制企业扩张,甚至导致企业缩减规模,进行裁员,又推进了失业的严重程度。 而政府对失业人数增多的应对措施竟然是【头痛治脚】地增加失业保险的缴纳基数,而非想办法重启经济活力,进一步扩大就业市场。 持续增多的各种税收与各种层出不穷的问题,也难怪认识的加拿大人都对现在的政府相当不满。 10 好山好水好寂寞 温哥华的自然风光的确很美,依山望海。 经历过加拿大的冬天之后,我能理解为什么当地人在夏天都一股脑地出去玩,因为夏天不玩,冬天真的没得玩。 温哥华的冬天,只有雪和雨,阴冷潮湿,早上八点半日出,下午3点半日落,日照时间也只有7-8个小时。 冬天除了滑雪和滑冰外,基本没有太多其他种类的户外活动。 而温哥华的夏天要到接近7月份才来临,那时候大家可以露营,划船,登山。 很多店铺晚上6-7点就会关门,邮递员周末也不会送信,更不会有广州那种深夜大排档的盛况。 可能是因为温哥华相对国内人少,各种活动和玩法也没有国内花,也难怪有人评价其为【好山好水好寂寞】 11 好脏好乱好热闹 回到国内时的第一感受是,好多人,真的好久没有看到过这么多人。 得益于国内相对廉价的人力以及世界工厂的地位,以致于国内相对加拿大拥有价格更便宜,品质更好的产品与服务。 即使是深夜,到处也是人头攒动,可以很轻易地朋友玩通宵,到处都是人气和烟火气。 所以总会有朋友问我,【后面你会回国么?】 我只能说,未来的事无法计划,我也没有一个确切的答案。 当初想要出来只是某些契机因缘际会的结果,未来的事谁也不知道,只能拭目以待。 ","permalink":"https://ramsayleung.github.io/zh/post/2024/%E7%99%BB%E9%99%86%E5%8A%A0%E6%8B%BF%E5%A4%A7%E4%B8%80%E5%B9%B4%E7%9A%84%E4%BD%93%E4%BC%9A/","summary":"1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾","title":"登陆加拿大一年后的体会"},{"content":"1 前言 前几天在讨论刷题工作,分享职场经历(也就是水群)的群里,有位「老好人」senior 工程师向大家倾诉他的职场烦恼。 他作为项目的后端负责人,把后端的活都又快又好地干完了,然后前端毫无进度,大家看起来还没有什么责任心和紧迫感。 如果项目不做成,原有的客户都会决定放弃公司的产品,转投其他公司。 所以这位老哥就会忍不住在干好自己的活的同时,去前端帮帮忙. 然后过了一段时间之后,不干活的前端们开始疯狂向后端推卸责任,明里暗里,阴阳怪气指责后端问题,导致项目被block住. 群友们纷纷给自己的建议,有建议和经理1:1 把事情捅出来的;有建议下次提前和经理同步进度,暴露风险的。 此时,老道的群主也出来给出自己的建议: 找到自己工作的亮点,要把亮点展示给老板们,然后离这群不干活的人远点,有啥要求就去捧杀下他们,说他们能力强,进度喜人,自己就不过去拖后腿了 通过提管理手段来暴露他们的问题,而不是直接去和经理说谁谁不好,口说无凭的; 代码质量差就多组织代码review, 没进度就每周或每两周review 进度, 没产出就建议review 代码量和设计文档. 如果没人听自己的建议,那就放慢点节奏,自己找点事消磨下时间,总不能指望没人重视自己的项目,还为它夙兴夜寐 用群主的原话来总结: 银行被抢了,是冲上去和歹徒搏斗,牺牲了还要为是不是工伤扯皮?还是顺便把自己和领导亏空的那部分也算在损失里,提拔一下? 群主的话可谓是振聋发聩。 当然,企业老板听了可能会不高兴,觉得给了员工工资,即使公司搞996,搞降本增效,裁员减薪,员工也应该献出自己的心脏。 因为我总是读些乱七八糟的东西,所以我思绪比较容易飞扬,总是会联想到一些不搭边的东西。 群主的话让我联想到了《西游记》,如果以职场视角来打开《西游记》,就会发现完全不一样的内容,也解了我的心头之惑。 2 胡说西游记 年少时读《西游记》,读到的只是神魔鬼怪,孙悟空如何大战各路觊觎唐僧肉的妖魔鬼怪,一路通关刷副本护送师父西天取经的故事。 现在想来,如果《西游记》只是日本少年热血漫画的内容类似,又是如何被位列中国的四大名著之一的呢? 有人说西游记的仙魔志怪只是皮,骨是人性,所以才能经久不衰,流传数百年。 那么人性又体现在哪呢? 2.1 困惑 先来看下我曾经困惑的地方。 每次唐僧师徒落难,孙悟空营救未果,都向去天上的神仙,或者去地府的阎王救助,众人总是口称「大圣爷」, 恭敬相待,殷勤相从,基本是有求必应。 年少时觉得合情合理,因为孙悟空是「齐天大圣」,所谓众神都小心应付着。 出来打工之后,才意识到这些个情节非常不合理: 按理说天上的众神都位列仙班,都是「体制内」有头有脸的人物, 哪天来了个石头蹦出来的破落户,连个「编制」都没有,敢自封「齐天大圣」, 还把我们的机关单位大闹了一场,让我们在领导面前丢光了脸面,连大领导「玉帝」都被牵连了。 现在他有事要来求我们帮忙, 谁有空谁帮,反正我手上的事情是非常紧急的,会影响到万千生民的,不比你救个师父重要? 你自己不是很能么,自己去救嘛,找我们干嘛? 有人可能会觉得我以「小人」之心揣度「神仙」之腹,人家都是「神仙」了,还会计较这些嘛? 肯定是能修炼到喜、怒、哀、惧、爱、恶、欲七情皆去的境界。 只是《西游记》中有明确描述,「神仙」并非是没有七情六欲的。 在《西游记》的结尾,唐僧师徒历尽艰辛,终于到了灵山见佛祖,得偿所愿要把真经取回去,却遇到佛祖座下弟子阿傩,迦叶索贿。 他们索贿不成,甚至只给了唐僧师徒无字经让他们带回去。 「佛」犹如此,「神」何以堪? 2.2 涂生死簿 在孙悟空在菩提祖师学到学到出神入化的神通和七十二变的本领后, 为了改变自己以及猴子猴孙们都逃避不了生老病死的命运,去地府把生死簿中,把猴属之类,但有名者,一概勾之: 悟空道:“我也不记寿数几何,且只消了名字便罢。取笔过来。” 那判官慌忙捧笔,饱掭浓墨。 悟空拿过簿子,把猴属之类,但有名者,一概勾之。捽下簿子道:“了帐,了帐,今番不伏你管了。”一路棒,打出幽冥界。 那十王不敢相近,都去翠云宫,同拜地藏王菩萨,商量启表,奏闻上天,不在话下。 相当于孙悟空把地府这个机构最重要的账本都给改没了,阎王这个主管相当于严重渎职啊,负有重大管理责任! 那么为什么每次孙悟空来找阎王,阎王都毕恭毕敬的呢? 以前觉得是阎王怕了孙悟空,所以因惧生敬。 现在联想到群主的话,意识到阎王不但不会因失职而沮丧,并记恨孙悟空,反而会乐得鼻涕泡都出来。 孙悟空就是「抢劫银行」的劫匪阿,自己有多少亏空和前科, 有什么亲戚好友免除死期的,有什么用钱换阳寿的事,全部都可以算到孙悟空头上。 有审计机构过来审查,我都可以说是被孙悟空涂掉的。 有这么个冤大头,阎王自然开心得不行,可能阎王他父亲都没有对他这么好,在工作上这么关照他。 2.3 纵天马 在孙悟空在东海夺取了「定海神针」作「如意金箍棒」,大闹森罗殿,私改生死簿后,玉帝大为震惊,然后听从建议, 招安了孙悟空,封为「弼马温」. 「弼马温」就是御马监正堂管事,手下还有监丞、监副、典簿、力士等大小官员不少人,管辖天马千匹, 所以是个管后勤的职位。 原来孙悟空以为这是个「一品」大官,最后却在旁人口中知晓,这个是个未入流的小官,不禁大怒,把公案推倒,在天河放走了所有的天马。 想来天马虽然名为天马,也不是以云雾为食的,想必也要有后勤草料供应,还有各种马具,马廊等配套设施。 现在孙悟空把天马都放了,那么属下的亏空,上下游的贪墨,又可以把锅栽在孙悟空头上了。 虽然我们中饱私囊,1000匹天马的财政拨款,贪了900匹,实际只有100匹,但是你孙悟空把马都放了,我们就咬死说马就是有1000匹的。 好比粮仓主管离任,对上官不满,放火把粮仓烧了,不管里面是陈米,沙土还是精米,下属一概都说是精米,那都是主管的责任了。 2.4 关系 唐僧,转世前为金蝉子,如来佛祖的二弟子,因犯错被贬,转世成为唐僧, 要通过取真经,重新成佛,回来佛祖身边。 也就是说,取经队伍里面,带头那个是如来董事长的亲信秘书,和董事长关系密切。 而取经也变成了一件在大领导「玉帝」CEO和如来董事长那挂上号的,核心KPI项目了. 那么当孙悟空来求助的时候,也就变成了一件可以在CEO和董事长那里展示自己手段的事了。 既能露脸,也能赚credit. 如果能帮上忙,那么就可以在领导面前露把脸,展示下自己的本领; 如果帮不上忙,也没什么大碍,你看孙悟空他业务能力多强,他都求助,肯定不是啥容易啃的骨头,我处理不成也很正常。 反正有事没事去老板面前刷刷脸总是好的,你没看到,领导叫踢球,群里一群人响应么? 你觉得他们是想踢球呢,还是想和领导踢球呢? 3 总结 经典之所以能经得起时间的洗礼,历久弥新,大抵是因为总有人能拿书里的事,说自己的话吧。 3.1 历史文章精选 旅加经历 这些年走过的路:从广州到温哥华 加拿大之初体验 加拿大考驾照的经历 历史与思考 为什么梦想买不起,故乡回不去(和谐版本, 原版本1) 润向何方:不完全肉身翻墙指北 皇帝与官僚:「上面」与「下面」 《君主论》:所谓「帝王心术」 工具与分享 我的写作流 我的画图流:画图工具与技巧分享 我的搜索流:高效搜索经验分享 最好的学习方式:费曼学习法(Feynman Technique) 系统思考:既见树木,又见森林 两个鲜为人知的Gmail地址技巧 职场与思考 那些年,我从微信支付学到的东西 : http://ramsayleung.github.io/zh/post/2023/%E7%BD%AE%E8%BA%AB%E4%BA%8B%E5%86%85/ \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%BB%8E%E8%A5%BF%E6%B8%B8%E8%AE%B0%E7%9C%8B%E8%81%8C%E5%9C%BA%E6%84%9F%E6%82%9F/","summary":"1 前言 前几天在讨论刷题工作,分享职场经历(也就是水群)的群里,有位「老好人」senior 工程师向大家倾诉他的职场烦恼。 他作为项目的后端负责人","title":"胡说:从西游记看职场感悟"},{"content":"1 前言 1.1 「老司机」 在高考之后,未上大学的那段时间,我就在父母的建议之下去考了驾照。 虽然持有驾照已经7-8年的时间,期间甚至驾照过期,都换过一次证件了。 所以,以驾龄论,可谓是「老司机」了,但以里程论,还是「新手」,不足一万公里。 毕竟在国内上班,可以搭乘公共交通工具,地铁,小电驴或者共享单车都可以组合使用,车不算是必需品。 尤其是在还没有买房的情况下,找地方停车都是个问题。 1.2 北美的车 但来到北美之后,发现车的确是必需品。 去美国玩的时候,发现美国真的是大农村,地广人稀,生活设施遍布在不同的地方,买个菜都要去几公里之外的超市,没有车真的是寸步难行。 相比之下,我所处的温哥华还算是方便,还有地铁和对应的接驳巴士,能满足基本的生活需要,但也仅限于生活需要。 例如想去个附近的山登高游玩,地图显示距离17公里,开车30分钟,公交2小时。 兼之冬天将近,如果下雪的话,再用两条腿,可以活动的范围就相当有限。 诸事安顿下来之后,车的优先级就提上来了;而考虑车的前提条件,就是先有驾照 1.3 驾考难度 因为中国与加拿大没有驾照互认协议,所以中国的驾照不能直接换成加拿大驾照,所以就能人工去考。 前面提到,北美的车是生活必需品,兼之从不同的好友的经历来看,考驾照似乎并不难,何况我又不是不会开车。 按照朋友们的描述,在美国,能把车开出去,再安全开回来,就能过了。 后面发现,的确如此,但是仅适用于美国,加拿大(或者说温哥华)除外。 温哥华20世纪初建城的时候,汽车还不是普遍,设计的路主要是以马车通行为主,所以老城区的路基本都很狭窄。 等到20世纪40-50年代,汽车大量普及的时候,最初的城市规划设计师就给出了面向汽车道路的城市规划, 但没有被政府接受,重新规划的蓝图就没有实现,所以温哥华城区的路都相当「紧凑」。 像BC 99这条去温哥华市中心,每天上下班必塞的高速公路,双向只有中间的一条黄线分隔,车头偏一下就被对面车道的来车给撞了: 虽然路规划得不怎么样,但是政府还是希望交通尽量高效运行,减少阻塞, 而其中的一个手段就是提高司机准入的门槛,让司机在理想条件,尽量以最高限速速度通行。 以至于刚从美国转过来的同事说,怎么感觉温哥华的人开车,都那么匆忙呢(rush). 用舍友的话来说就是, 「驾照考试难度,一般与马路杀手数量成正比;马路杀手多的地方,一般都考试难度都比较高。温哥华路况不好,容易培养马路杀手」 导致的结果就是,温哥华的驾照考试难度相对较高。 但考试难度这个东西很难量化的,每个人驾驶经验也不一样,但以我知道的例子,一位朋友,考了4次才通过,花费了4个月;还有同事的舍友,也考了4次。 2 回炉再造 虽然我在国内也开过车,但是鉴于温哥华的考试难度,兼之想一次通过考试,所以就找了位朋友推荐的教练来重新学车。 要考试通过,主要是做好这三部分的内容: 道路意识 驾驶技术 考点的熟悉程度 我本以为是我要关注的主要是第3点,没想到第3点优先级是最低的。 前面几次课程,我都是在修正国内的驾驶习惯,比如驾车时,开在最右车道时,习惯偏向左边,而不是开在车道中间。这个叫 lane position(车道位置)问题 教练说,这个很正常,因为国内经常会有行人或者自行车从非机动车道,开到了机动车道上,为了不碰到他们,就习惯向左边开一些。 还有右转的时候,习惯向外再带一些再转弯,因为又担心转弯的时候会有行人或者障碍物,所以预留出空间,这个叫 steering wheel position(转弯位置)问题。 诸如此类的问题,都是会扣分,甚至会挂掉的. 道路意识方面,教练首先纠正我的就是速度问题,限速50km/h的意思,不是说开50 km/h 以下都可以,而是说在理想情况下,要开到50 km/h, 太慢就会阻塞交通了。 比如限速50,开到40以下可以就扣分了,开到35,可能就挂了,当有车在你后面排队,就要注意了; 超速也不行,超过55可能就会挂掉。(当然,只是考试,平时超速,只要不超太多,一般不会有警察抓你的) 还有道路优先权的意识,在没有红绿灯的情况下,多车交互,什么时候,谁应该先走;对公交,行人的礼让意识。 对于消防车和救护车的紧急车辆出现在路上,双向两车道的车都要停下来, 以便紧急车辆快速通过,所以就会有路上就听到警笛声,路上所有的车都就近停下来的场景。 加拿大的基础设施真的不行,路灯都是20-30年前的款式,让我非常不习惯的是,在大多数情况,直行和左转灯是合并在一起的。 意味着当你要左转,灯变绿的时候,不一定就能转,对面直行车道的车有更高的优先权,你需要把车探出去,等待对面的直行车通行完,并且在自己这边的绿灯变黄前走掉,需要对交通变化快速反应。 因为这种种限制,就对司机的驾驶技术有较高要求,具体体现就是要快速过弯。 而温哥华的交通要求司机快速通行,路上交通状况变化很快,如果转弯太慢,灯可能就变, 行人或者其他方向的车就马上要来了,就比较容易出现危险的状况,就要求在可以通行的情况下,快速过弯。 北美这边的过弯标准是,右转绿灯,减速,shoulder check, 平稳快速转弯,加速,过程要流畅一气呵成,不能有颠簸,滞涩的感觉,让乘客紧张。 所谓的 shoulder check 就是转弯的时候,不能只看镜子,要把头转90度,转过肩膀,观察转向的盲区,避免撞上行人或者单车,这也是国内没有的要求。 所以快速转弯就要求手,脚,头协调并用。 就这样,每周一次,练了10次,花费了近三个月来练习。 3 考试 考试内容分为笔试和路试,笔试就是针对交通规则的做题,问题不大,通过之后就会拿到实习驾照,在有19岁以上的驾照持有者陪同下就能开车了。 路试就是载着考官,按照考官的指示,开25-30分钟的车。 笔试与路试都与驾校无关,也不需要有驾校这样的机构介入,考试用车可以是任意符合规定的车,比如轿车,SUV, 越野车,或者是皮卡,只要功能正常即可。 在考点区域,按照随机考官的指示开车,考察内容大概包括红绿灯右转,左转,随机临时停车, 随机地点2分钟内完成侧方停车,考官观察考生的表现,并进行打分。 视考点不同,还可能需要开一段高速,考察如何加速并入,高速超车与换道,及离开高速等等。(考官估计也会紧张,要和连驾照都没有「马路杀手」上高速) 因为练的时间比较长,兼之在考试之前去美国有过一次公路之旅,开了有600-700公里,所以一次就通过了。 考官评语: A1 Missed right turn shoulder check/lane change Good speed control shoulder check 部分还是被扣分了,做得不标准,习惯着实很难一下子养成。 这样就通过驾照考试,「又」成为一名「马路杀手」了。 整个考试的费用: 笔试: 31加元(约160人民币) 路试: 50加元(约270人民币) 驾照制作及邮寄费用: 31加元(约160人民币). 4 对比 因为在中国和加拿大都考过驾照,所以很自然地会对比两个国家的驾照考试。 加拿大考试的最大感觉就是真的要求你考完之后,是可以马上上路开车的,毕竟考试都需要在马路上真枪实弹开车的,甚至上高速。 但是你是怎么学的,他是不管的,所以也难怪北美很多人都是父母教开车,然后开自己家的车去考试。 相比之下,中国的驾照考试是相对比较「教条」和「形式化」的,以大部分人花费最多时间的科目二而言,主要是考察倒车入库,侧方停车,直角转弯,半坡起步等技能,而其中花费最多时间的是倒车入库,要求一次就能倒进去。 科目三就是加减档位,加减速,路边停车等等,我当初考试的时候,科目三还有长途,我需要和教练还有同车的考生,驱车到另外一个城市。 但是考试要求与实际开车的诉求并不契合,没有见过谁开车主要是处理倒车入库的,何况是要求一次就能停进去。 为什么要考这么机械的内容,就不能以实际开车的内容来考么? 比如让考生开个20-30分钟,然后让考官评判开得是否符合标准。 回忆起发生在我自己身上的一件事,我意识到,在当前环境下, 这个是很难落实的。 4.1 科目三经历 当时考科目三的前,我就只练习了一次,驾校教练把我们拉到一条未启用的高速公路上,给我们讲怎么起步,加减档,超车,路边停车, 向我们介绍,考试的时候,会有个考官坐在副驾驶上,然后对我们进行打分。 让我们每人开一段路,第二天就约考试了。 考试前,另外一位驾校教练,也是驾校老板单独叫我出了办公室,对我说了一番话,但原话已经记不大清了: 教练:你准备了么? 我:准备什么?考试内容么? 教练:红包阿 我:什么红包? 教练:给考官的红包 当时高中毕业的我,还不懂这人情世故,陷入了短暂的沉默。教练并没有给我太多思考的时间,继续了对话(这次的原话我倒是还记得很清楚) 教练:这红包不是给我的 教练:每个人都给考官准备了红包 教练:同车考试的人里,是有人要挂的 读懂潜台词的我意识到,相当于我被陷于了囚徒困境了,如果别人给了我没给,挂的就是我。 那我也只能给红包了,但教练的话却没有就此停下来。 教练:你知道给多少么? 我:\u0026hellip; 教练:给个500吧。 我高中毕业的时候,移动支付还不是非常普及,所以还是现金支付为主。 我当时报驾校的费用是3300元,我记得很清楚,是因为我父亲带我去镇上的驾校,从他的裤袋里面,抽出了一捆现金,放到桌上给驾校老板。 没想到,给个红包就要驾校费用的1/6. 驾校老板不为自己谋利益,还这么热心地帮考官张罗,真的难得的好人。 4.2 实用与教条 换个角度想,可能开过车的人都能意识到,耗费最多时间的科目二真的对驾驶技术的提高不大,那为什么不能使用更实用的考察方式。 因为科目二这样教条,机械的考察方式,可以通过仪器进行量化考察,仪器在大部分情况下,作假难度会高很多。 但如果使用实用的考察方式,只能使用人来主观考察,如何避免以权谋私呢?避免出现在我身上的事,发生在其他人身上。 考生能否检举这样的行为呢? 如何避免考生得到保护,不受到来自驾校和考官的打压呢? 我只知道,在加拿大,驾照考试是不需要驾校的,你有选择的自由和权利,但是考试本身不与驾校挂钩。 考试费用也是固定的。 如果有考官向我提这样的要求,我可以去驾考机构投诉,他有大概率会丢失工作。 而我既有权力选择更换考官,也可以自由更换考点。 5 后话 虽然花费3个月重新考个驾照相当费时间,但是期间的过程还是很有收获的,我也思考过规则背后的设计思路。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E6%8B%BF%E5%A4%A7%E8%80%83%E9%A9%BE%E7%85%A7%E7%9A%84%E7%BB%8F%E5%8E%86/","summary":"1 前言 1.1 「老司机」 在高考之后,未上大学的那段时间,我就在父母的建议之下去考了驾照。 虽然持有驾照已经7-8年的时间,期间甚至驾照过期,都换过一","title":"加拿大考驾照的经历"},{"content":"1 背景 我习惯使用浏览器匿名模式来打开 Youtube 视频,避免 Youtube 的推荐算法给我总是推荐同一类的视频。 但有个问题: 匿名模式下,Youtube的播放器是默认自动播放的。 虽然我可以登录 Google 账号并关闭自动播放,但是每次我使用匿名模式来浏览,关闭窗口之后,所有的操作记录都会被清除了,自动播放设置也不会被保存。 所以我使用 TamperMonkey 给 Youtube写了一个关闭自动播放的脚本,打开 Youtube 播放器时,把自动播放按钮给关闭掉,避免我使用浏览器的匿名窗口打开 Youtube 之后,Youtube自动播放导致一直有声音。 脚本逻辑很简单: // ==UserScript== // @name Disable YouTube Autoplay // @namespace http://tampermonkey.net/ // @version 0.1 // @description Automatically turn off YouTube autoplay // @author ramsayliang // @match https://*.youtube.com/* // @grant none // @icon https://www.youtube.com/favicon.ico // ==/UserScript== (function() { \u0026#39;use strict\u0026#39;; // Function to disable autoplay function disableAutoplay() { console.log(\u0026#34;disableAutoplay\u0026#34;); const autoplayToggle = document.querySelector(\u0026#39;div[class=\u0026#34;ytp-autonav-toggle-button\u0026#34;]\u0026#39;); if (autoplayToggle \u0026amp;\u0026amp; autoplayToggle.getAttribute(\u0026#39;aria-checked\u0026#39;) === \u0026#39;true\u0026#39;) { autoplayToggle.click(); console.log(\u0026#34;Disable YouTube Autoplay\u0026#34;); } } console.log(\u0026#34;Before loading\u0026#34;); // Run the function when the page loads window.addEventListener(\u0026#39;load\u0026#39;, () =\u0026gt; { // Wait for a short delay to ensure the page fully loads // setTimeout(disableAutoplay, 8000); console.log(\u0026#34;Loading page..\u0026#34;); disableAutoplay(); }, false); })(); 2 问题 但是我发现这个脚本时灵不灵,甚至有一天晚上,睡着之后被自动播放的声音吵醒了。 我就在找能稳定复现这个问题的场景,花费半个小时,终于能稳定复现问题了。 打开 Chrome 的匿名模式 打开 Youtube 首页, 地址是: youtube.com, 脚本运行. 随意点击一个视频,进行播放:https://www.youtube.com/watch?v=-pKGaxoVhok ,脚本就不会运行了。 如果我在视频播放页刷新,脚本又会重新运行。 但分析了1个小时,都没有找到原因,我甚至怀疑是 Tampermonkey 有Bug(虽然主观感觉这个可能性较小) 3 分析 结合 Tampermonkey 的表现,我觉得可能是 Tampermonkey 的执行机制有问题,可能是判断 youtube.com 和 youtube.com/watch?v=xxxx 是同一个页面,就不会运行两次。 在Stackoverflow上搜索了一下,发现果然如此: https://stackoverflow.com/questions/65017670/tampermonkey-match-not-working-when-visit-target-link-through-redirection 原来这个是feature, 不是bug. 对于 Single Page Application, Tampermonkey 无法判断页面的 DOM 是否发生变化,是否访问到新的页面了,所以不会重复执行。 分析下来,之前脚本能直接生效的原因是, 我在Chrome正常模式下打开 Youtube 首页, 右键对想要看的视频,点击\u0026quot;Open link in incognito window\u0026quot;, 因为页面是首次打开,所以就能正常运行。 但当通过Chrome 匿名模式打开 Youtube 首页,然后再点击视频播放,无法运行脚本。 因为在打开首页的时候,脚本已经运行过了,当点击跳转到指定的视频时,且 Youtube 是个 Single Page Application, 对脚本来说,页面就没有发生过变化,所以不会再运行。 如果手动刷新,页面重新加载,脚本就又会被加载。 4 解决方案 最后通过 Stackoverflow 的建议,增加一个对 DOM 事件变化的监听来解决: // https://stackoverflow.com/questions/2844565/is-there-a-javascript-jquery-dom-change-listener/39508954#39508954 // Detect the url change for programmatic navigation let lastUrl = location.href; new MutationObserver(() =\u0026gt; { const url = location.href; if (url !== lastUrl) { lastUrl = url; onUrlChange(); } }).observe(document, {subtree: true, childList: true}); // callback when url change function onUrlChange() { console.log(\u0026#39;URL changed!\u0026#39;, location.href); if(isYouTubeVideoURL(location.href)){ disableAutoplay(); } } 这样就可以正常运行了。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/tampermonkey_userscript_not_invokved_in_spa/","summary":"1 背景 我习惯使用浏览器匿名模式来打开 Youtube 视频,避免 Youtube 的推荐算法给我总是推荐同一类的视频。 但有个问题: 匿名模式下,Youtube的播放器是默认自","title":"TamperMonkey userscript在 Single Page Application 跳转链接后不运行问题分析"},{"content":"1 引子 年少的时候,很喜欢看历史书,倒不是因为「以史为鉴,可以明得失」发人深省的思考,单纯是因为手头没有太多打发时间的活动(那时还没有太多的电脑游戏和视频),而少年人最多的就是时间。 历史书里面有光怪陆离,跌宕起伏的故事,那时的我自然很容易被吸引进去。 上中学的时候,特别喜欢看三国,喜欢武将决斗,谋士运筹的故事。 再后来,随着网络小说的兴起,兼之三国的历史已经烂熟于心,没有太多的新意,就开始看各种的架空,穿越三国的网络小说。 比如:《重生三国之xx》,《xx三国》等等,后来又因为《明朝那些事儿》的爆红,开始看明史。 工作之后,出于历史的兴趣,就开始摆脱通史类的故事书,阅读一些偏学术类的历史著作。 只是在截止到这个时候,阅读的都是中国历朝历代的历史,毕竟外国人的历史有什么好读的,地名和人名这么难记。 大概在三年多前,阅读一本英语学习书籍(《Word Power Made Easy》,这也是本神书,后面我一定要好好聊聊)的时候, 该书中提到,古希腊和古罗马是现代欧美文明的精神来源,相关的传统,法律或者是宗教, 都可以从这两个古代文明找到参照物,约70%的英语单词就是衍生自古希腊语或者罗马人用的拉丁语。 何况古罗马历经共和国及帝国时代,历经1300年不倒。 对比之下,中国的朝代大多是三百年,为什么古罗马就能走出这样的历史周期律,延续千年呢? 巅峰时期,甚至将地中海变成罗马人「自己的内海」,成为一个横跨欧亚非的帝国。 想必自有其独到之处。 吃了那么久的中国菜,去尝试下西餐也不错嘛,说不定还能吃出新意呢。 2 罗马建国 公元前八世纪,传说罗马第一任国王罗穆路斯和双胞胎弟弟瑞摩斯出生后即被装到一个桶里,和唐僧一样,被投到河里遗弃。 木桶顺流而下,婴儿在桶里大声哭闹,引来了附近正在徘徊的一匹母狼。 这匹母狼没有把这两个婴儿当作午餐,反而将乳头塞进了两个婴儿的嘴里,把他们从死亡线上拉了回来。 婴儿由母狼抚养长大的故事显然过于玄幻,所以母狼在喂抱两个幼儿之后就离开了,是一个羊倌发现他们并把他们带回家了。 所以在罗马,都有各种各样母狼喂养罗穆路斯和瑞摩斯的塑像: 甚至意甲球队罗马(就是现在穆里尼奥执教的那支球队)的队徽也是母狼喂养罗穆路斯和瑞摩斯图: 在罗穆路斯18岁的时候,他和3000名追随他的拉丁人,以他自己的名字(Romulus),在台伯河下游平原的七座小山丘上建立了罗马国(Roma,或者叫罗马城): 为什么3000人就能建国?,这个没有什么硬性标准的嘛,只要实力够,自然可以自封为王。 所以也难怪曹操会说,设使天下无有孤,不知当有几人称王,几人称帝(串台了) 3 国政 罗穆路斯建国之后,作为开国之王,却没有独揽大权,他把国政分成三个机构,分别是国王,元老院和市民大会。 国王作为宗教祭祀,军事和政治的最高领导人,由市民大会投票选举产生。 罗穆路斯认为,如果没有市民大会选举,自封的王不具有执政基础。 (只能说东西方的执政想法真的不一样,尧舜禹禅让,都被儒家夸了两千年。而罗马的权力竟然不是世袭的) 罗穆路斯召集100位部族长老,设立元老院,他们的职责是为国王提忠告和建议,所以不需要通过市民大会的选举。 元老院的拉丁语是:Senātus,也是众多英语单词的词根,比如年长者(senior), 或者美国的参议院(senate), 都是元老院的衍生词。 市民大会由全体罗马市民组成,它的任务是选出以国王为首的各级政府官员。 (政府官员究竟不是国王或者上级官员指派的,无知的我又吃了一惊。) 市民大会没有制定政策的权力,但是对国王听取元老院的建议后制定出来的政策有赞成或反对的表决权。 此外,在对外关系上,是战是和,也必须征得他们的同意才可实施。 当时的罗马国规模还不大,所以还能由全体罗马市民组成市民大会,不知道市民人数多了之后,市民大会又会如何发展呢? 4 总结 原来十八世纪法国启蒙思想家孟德斯鸠的三权分立理念,在公元前八世纪的罗马城就能找到雏形。 这顿西餐,看来是越来越有趣了。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%9B%BD%E7%8E%8B%E6%98%AF%E9%80%89%E4%B8%BE%E5%87%BA%E6%9D%A5%E7%9A%84/","summary":"1 引子 年少的时候,很喜欢看历史书,倒不是因为「以史为鉴,可以明得失」发人深省的思考,单纯是因为手头没有太多打发时间的活动(那时还没有太多的电","title":"闲话罗马:国王是选举出来的"},{"content":"1 前言 前段时间流行一种关于程序员效率的说法,叫「10x程序员」,即一个好的程序员的工作效率是普通程序员的10倍。 但是,在编程界,有这么一群人,他们的工作效率,可以说是百倍,甚至千倍于普通程序员; 更令人叹服的是,他们创造了普通程序员即使百倍努力也无法写出的作品。 使用「程序员」这个职业来称呼他们,未免流于平凡,无法展现出他们竖立起的丰碑;而使用「计算机科学家」,又未免过于学术,不接地气; 那么,就回到最初,用「黑客(hacker)」这个称谓来称呼他们吧。 2 关于黑客 可能大家对「黑客 (hacker)」的印象多来自于电影,比如《黑客帝国》,就是那种在电脑面前,使用各种看不懂的工具入侵别人电脑的人。 但是这种看法大多是对于「黑客」的误解,称之为「骇客(cracker)」可能更加合适,即恶意入侵他人电脑的人。 hacker 一词又是从 hacking 衍生而来的,将 hacking 翻译成成中文网络语中的「整,搞,开干」可能会更贴切, 而最初的「黑客」指的就是一群富有创造力和兴趣的爱好者,只是比较具有代表性的是在计算机领域。 国外有个很有名的科技相关的聚合网站,叫做「Hacker News」, 其中的「Hacker」, 也是沿用黑客最初的含义。 既然提到黑客,那么有一个无法绕过去的人物,那就是今天的主角,黑客文化的领军人物:Richard Stallman 3 UNIX 3.1 分时系统 相信今天的我们,对操作系统这个概念不会陌生,在电脑上有 Windows 10, Windows 11, Windows 7 或者苹果的 MacOS操作系统,在手机上有 Android 和 IOS操作系统。 所谓的操作系统,即是一套管理硬件,发挥硬件性能的软件,避免应用程序直接和硬件打交道,省去普通程序员大量的开发成本和心智。 与今天直接在手机操作系统上,一边聊微信,一边放音乐不同,远古时候(二十世纪六十年代)的操作系统只支持批处理模式: 即用户同时提交多个任务,任务1运行完才能运行任务2,相当于你只能把音乐听完,然后关掉音乐软件,然后才能打开微信,发送聊天消息。 (请忽略远古时代还没有微信这个问题) 你可能会想,这也太挫了吧。 没错,当时的计算机科学家也这么认为的。 因此1964年,通用电气和麻省理工大学就打算合作开发一个多任务操作系统,支持多个用户,运行多个任务,名为 MULTICS 后来,AT\u0026amp;T公司的贝尔实验室也加入到这个操作系统的研发中,但是项目目标过于庞大,特性太多,性能又很低, AT\u0026amp;T见项目前景不妙,就把资源都撤了,退出了这个项目。 3.2 玩游戏玩出来的UNIX 贝尔实验室的一位工程师,名叫Ken Thompson, 刚加入 MULTICS 项目不久,公司就准备退出了,但是通用公司为了项目而准备的机器 GE-645 就还保留在贝尔实验室,Ken 就打算用这些机器写个太空旅行的游戏。 然而,Ken 写出来的游戏跑得很慢,每次运行还要75美刀,更难受的是,GE-645 这批机器,不久后就被搬回去通用公司了。 所以Ken 只好在实验室角落找了几台没人用的PDP-7, 在同事 Dennis Ritchie 的帮助下,再重写了一次游戏。 这次的游戏开发经历,加上之前的 MULTICS 项目经验,让Ken 开始研究如何使用 PDP-7 开发一个分时多任务操作系统。 然后他花费了一年的时间,和 Dennis 一起,在PDP-7上开发了一个分时多任务系统,名为UnICS,这就是第一版的 UNIX。 因为PDP-7的性能不佳,最多支持两个用户, Ken 和 Dennis 又把第一版的 UNIX迁移到 PDP-11上,为了方便迁移,还顺便发明了一门编程语言,名为 C语言,并将UnICS 改名为 UNIX. (这两位也是神) 影响后世无数操作系统的 UNIX 操作系统就此诞生,并迅速风靡各大研究机构,政府机关,企业与大学,成为70-80年代,操作系统事实上的标准 3.3 商业版本与闭源 原来的软件只是买硬件时的赠品,到七十年代未,人们开始发现,原来软件也可以卖钱,很快,制作与销售商业软件成为一门热门生意。 最开始的UNIX 版本是开放源代码供使用者的,也就是使用者不但可以安装 UNIX 系统,还可以阅读,并修改UNIX 系统的源代码。 但是贝尔实验室的母公司 AT\u0026amp;T毕竟是商业公司,把自己的源代码授权出去,后面还怎么赚钱呢? 所以在20世纪80年代相继发布的UNIX 商业版本,只发行二进制,不再包含源代码。 对于黑客来说,就是你能看到这个操作系统是怎么跑的,但是你再也无法知道他是怎么实现的了。 4 RMS Richard Matthew Stallman, 1953年出生于纽约的一个犹太家庭, 1974年毕业于哈佛大学,1975年在 MIT 攻读博士,后来退学在 MIT AI 实验室写代码。 他的名字首字母为 RMS, 早期在黑客社区混的时候,以 RMS为用户名,所以大家都叫他 RMS(后面就以RMS来称呼他了). 当时的「黑客文化」崇尚开放,分享与交流,认为分享才能促进社会进步,在这样的文化熏陶下,RMS 自然对闭源软件痛恨不已。 1980年,还在 MIT AI 实验室工作的时候,因为激光打印机和大部分工作人员都不在同一层楼,总是跑上跑下去查看打印结果和进度就很麻烦。 RMS 就给实验室的激光打印机写了一个程序: 可以在打印任务完成时,发消息通知用户;或者当打印任务卡住的时候,也发消息通知用户; 然而,因为最新版本的打印机源码不再开放,RMS写的程序就无法再适配,让他相当恼火。 以小见大,整个软件行业都在发生变化,甚至连UNIX 这样的基石软件都开始不再开放源代码授权,RMS感觉,他要站出来做些什么了。 5 GNU 5.1 荜路蓝缕 在1983年, RMS 宣布了GNU 操作系统计划,计划开发出一个兼容 Unix的源码开放的操作系统,让 Unix用户可以无缝切换到 GNU 操作系统上. GNU 就是 \u0026ldquo;GNU is Not Unix\u0026quot;的缩写(那开头的GNU又是什么意思呢? 按照程序员的行话来说,这个叫递归) 经过十多年的发展,Unix 已经成为操作系统事实上的标准,重新开发一个新的操作系统几近天方夜谭。 想象一下,有人跟你说要开发一个 Android 操作系统,用来替换掉 Google 的Android 系统,这工作量和难度可想而知,这就是现实中的想要移山的愚公,大战风车的堂吉诃德。 但是 RMS 并未被眼前的困难所吓退,而是一步一步,从0开始构建他心中的类Unix操作系统. 1984年, RMS 开发并发布GNU Emacs 这个著名的文本编辑器, 方便程序员进行代码开发; 1986年, RMS 开发并发布GNU Debugger(gdb) 调试器, 方便程序员来调试程序; Emacs + gdb 就是他那个时代的IDE 1987年, RMS 开发并发布GNU Compiler Collection(gcc) 编译器套件; 所谓的编译器,即将人写的代码,转换成机器可以运行的二进制代码。 开发出一个这样的软件就足以在计算机史上留名,RMS 在这3年间,还一口气开发出了3个,这样的技术水平和生产效率,只能让人叹服,影响力堪比盗火的普罗米修斯。 何况这些软件至今仍在迭代,被无数程序员所依赖,所使用。 比如微信的所有后台代码,都是使用GCC 编译出来的,也就是你现在也在间接使用着 RMS 当初编写的软件。 近40年过去了,市面上被广泛使用的C/C++编译器就只有三个: 微软家的 MSVC, 苹果支持开发的 Clang, 还有GNU 项目的 GCC. 除此之外,GNU 项目还开发了许多的基础设施,如GNU make, GNU grep, bash,以及志在替换掉 PS的 GIMP 等等. 除了基础设施外,GNU项目还希望类似通过美国宪法保证言论自由一样,通过法律和版权,确保软件开放源代码。 因此, 在1989年, RMS 发布了 GNU General Public License(GPL)授权, 主要内容是: 用户可以自由使用,复制,修改GPL软件, 派生的软件也必须使用GPL, 不能转换成闭源软件. 从法律层面保证了GPL软件不会被有心人直接拿去闭源赚钱。 此外, RMS 还将自由软件发展成社会运动,将软件开发这个程序员小圈子的活动,成为扩展到整个社会的思想运动。 在人类没有进化到「无私」的精神境界前(可能永远都达不到),通过GPL法律条款来保证「自由」的权利, 不得不说是一种创举,从而让世界上每个人都有机会享受到软件发展所带来的好处。 5.2 开花结果 时间来到90年代, 经过近10年的耕耘, 在基础组件和配套设施相继完善之后,GNU 项目终于来到最关键的节点,开发出可以替换Unix 系统的内核(kernel). 如果电脑硬件来比喻操作系统的话,就是内存,硬盘,主板,显示器,电源全部都就绪,就差最后的CPU, 画龙最后的点睛. 但是GNU 的内核 Hurd 却迟迟未能发布, 而天下可谓苦闭源 Unix 久矣。 在1991年, 一个叫Linus的芬兰学生在社区上发布了他自己的业余项目:一个类Unix 的操作系统内核。 他把GNU 项目的相关组件(bash和gcc)移植到这个系统,也能正常运行起来了, 这个系统就是Linux(完整的名称应该是 GNU/Linux) 自此, GNU 项目的最后一块拼图完整了, 十年磨一剑, GNU的基础组件加 Linus 的Linux内核, 一个志在替换 Unix 的操作系统终于完成了, 这就是 GNU/Linux. 苦Unix久矣的社区的开发者云集而来,为 GNU/Linux 添砖加瓦, 让GNU/Linux 成为今天的参天大树(连微软家的服务器也在运行 Linux) \u0026mdash; 只见新人笑,哪闻旧人哭. 有点离谱的是, GNU Hurd 已经开发超过30年了,还没有发布1.0(稳定可用版本). 更离谱的是,最近还有更新: 2023年6月份,还发布了2023年 版本更新: 6 轶事 6.1 教主 为什么称RMS 为教主呢? 因为RMS 创建了 Emacs 这个神的编辑器,自其诞生以来,与编辑器之神 Vi/Vim 的圣战就从未停息。 使用Emacs 的程序员与使用Vi/Vim 的程序员,一直在争论,究竟哪个才是更好的编辑器? 既然 RMS 是Emacs 的创始人,自然被使用 Emacs的人尊称为「教主」。 而这场争论已经持续近四十年,依旧没有分出胜负。 像 Google 这样浓眉大眼的家伙,还在不时地给这场战争拱火: 6.2 教主与教主 乔布斯被「果迷」尊称为「教主」,大家可能不知道的是,这两位「教主」曾经有过一场交锋。 1993年, 当时乔布斯还在 NeXT公司, 买下了 Objective-C 语言来开发应用程序(后来的IOS用的也是 Objective-C), 使用的编译器也是 GCC. NeXT 修改了 GCC的源码,以便增加对 Objective-C 的支持,而GCC 使用的又是GPL 授权,而根据GPL 的授权,任何对GPL软件的修改,也必须要开放源代码。 所以乔布斯就问RMS, 他能否把 GCC 拆分成两部分,一部分是原来GCC, 继续开放源代码;另外一部分是增加 Objective-C 的GCC 编译器前端,闭源收费商用。 RMS 回复,当然是不可以。我估计老爷子心想,防的就是你这种人。 乔布斯只好将 Objective-C 编译器的前端也以GPL 授权开放出源代码。 \u0026mdash; 若干年后,苹果计划开发自己的编译器,因为设计以及授权的原因,在谋求与 GCC的合作未果后,转而支持 LLVM 的clang, 那也是后话了. 6.3 中国芯 根据 RMS 自述, 他之前用的一直是中国科学院设计的龙芯处理器的龙梦电脑, 虽然这台电脑的性能,显示尺寸(只有9英寸)都无法让RMS 满意,但是这台电脑的是完全自由的,包括硬件, bios, 软件: What hardware do you use? I am using a Lemote Yeelong, a netbook with a Loongson chip and a 9-inch display. This is my only computer, and I use it all the time. I chose it because I can run it with 100% free software even at the BIOS level. 在性能和自由之间,他一如既往地选择了「自由」 根据RMS 官网的描述, 他不用intel 或者 amd 的芯片,是因为他们都有后门: Reasons not to use Intel Don\u0026rsquo;t use Intel processors newer than Core2, because they have the \u0026ldquo;management engine\u0026rdquo; back door. Recent AMD processors have a similar problem, but we do not yet have an article about it. 不过,据闻他的龙梦电脑被偷了之后,他也就换到 ThinkPad 上了: As of 2022 I use a Thinkpad x200 computer, which has a free initialization program (Libreboot) and a free operating system (Trisquel GNU/Linux). 6.4 抠脚 菜的抠脚就听说过,强得抠脚又是什么呢? 因为他真的抠脚(字面意思),还吃回去了。 7 总结 他是天才黑客,是自由软件的精神领袖,是知行合一的孤勇者,更是个凡人堆里的理想主义者. 当然,还是我大 Emacs 神教的教主. 8 参考 https://en.wikipedia.org/wiki/GNU_Compiler_Collection https://en.wikipedia.org/wiki/Richard_Stallman https://en.wikipedia.org/wiki/GNU_Emacs https://en.wikipedia.org/wiki/GNU_Debugger https://stallman.org/ https://usesthis.com/interviews/richard.stallman/ ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%BC%BA%E5%BE%97%E6%8A%A0%E8%84%9A%E7%9A%84%E6%95%99%E4%B8%BBrms/","summary":"1 前言 前段时间流行一种关于程序员效率的说法,叫「10x程序员」,即一个好的程序员的工作效率是普通程序员的10倍。 但是,在编程界,有这么一群人","title":"黑客列传:强得抠脚的教主RMS"},{"content":"1 关于 Demo 昨天下班路上,和朋友闲聊的时候,想起了当年大学时候看过的《李开复自传》的一个故事。 当年李开复在卡内基梅隆大学的研究方向是语音识别,即如何将人说的声音,转变成计算机可以识别的文字内容。 他的语音识别的研究成果还被《商业周刊》评选为「1988年最重要科学创新奖」。 但是令我印象深刻的并非是语音识别的成果,而是他导师教他的,如何向世人展示他的成果的市场营销手段: 1988 年 4 月,我受邀到纽约参加一年一度的世界语音学术会议,发表学术论 文。赴会的一个月前,我的导师瑞迪教授又给我上了一课,但是不是学术方面,而 是市场方面的。 他对我说:“学术演讲的 30 分钟,你只要讲 25 分钟就行了,最后 5 分钟你拿 一个话筒传给观众,让他们自己试试,这个系统是不是真的。” 我说:“但是,会场噪音很大,一定会打折扣,达不到 96%成功率,而且那么多日本 学者,他们的口音我的系统可没听过。” 老师说:“实际上你的识别率是 90%还是 96%,没有什么差别。我们这么做的 目的,不是要监测你的识别率,而是要造成一个效果,让每个学者终生都会记得, 第一次接触不指定语者系统就是在纽约,在李开复的演讲上。” 在学术结果和演示效果的交互相映之下,李开复的研究成果撼动了整个学术领域,认为他的研究成果,建立起了人机沟通的桥梁。 纵然演示者的PPT美轮美奂,演讲舌灿莲花,带来的冲击,远不如用户亲身体验来得强烈。 2 MVP与及早反馈 无论是所谓的敏捷和精益迭代开发,都强调快速试错,快速反馈,开发最小可用的产品(minimial viable product, MVP)。 所谓的快速试错,及早反馈,就是把产品原型做出来,然后让用户进行体验,收集用户反馈,再根据用户的评价,进行后续的优化和调整。 这样的理念无缝是非常有价值的,可以避免花了好几年,大量人力物力,做了一个过时或者不受市场青睐的产品。 而其中的「用户」,并不一定指的是最终使用你产品的「用户」,你的产品经理,组长,总监都是你的用户。 他们才是能决定你的产品方向的人,所以在做完产品原型之后,应该尽快让他们尝试产品原型,可以及早得到反馈和修改建议。 在展示 Demo 的时候,也应该由他们亲身去尝试产品,观察他们作为新用户的使用习惯; 以此得到的反馈和惊喜,也会比工程师亲身演示来得更真实和贴切。 此外,正如《动物庄园》里面说「所有动物生而平等,但有些动物比其他动物更平等」。 每个用户的反馈和建议都应该被平等对待的,只是他们的意见比普通用户的更平等。 而从管理者的角度来说,管理者对员工抱有高信任度的终究在少数,即使每位员工起早贪黑地干活,写周报,日报;开晨会,周会汇报进度; 管理者难免会有疑问,项目什么时候才能做完,他们是否有在认真干活? 可能会有管理者跳出来说,「哪有这样的想法?」。 但事实就是许多的管理措施,都体现出这种不信任。 而提供MVP供管理者体验就是不断地告知管理者项目的进度: 项目正在从蓝图,变为现实。 3 后话 但,尽早反馈,快速试错,从来都不应该成为加班的借口或理由。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%BA%A7%E5%93%81%E7%9A%84%E5%BF%AB%E9%80%9F%E8%AF%95%E9%94%99%E4%B8%8E%E5%8F%8A%E6%97%A9%E5%8F%8D%E9%A6%88/","summary":"1 关于 Demo 昨天下班路上,和朋友闲聊的时候,想起了当年大学时候看过的《李开复自传》的一个故事。 当年李开复在卡内基梅隆大学的研究方向是语音识别,即","title":"产品的快速试错与及早反馈"},{"content":"1 前言 之前写《软件工程师的软技能指北》系列的时候,就有个挺想聊的话题的,就是写作。 其实不只是对软件工程师而言,我觉得对于所有人而言,都应该尝试下写作。 所以今天就来闲聊下写作的好处。 2 提升表达能力 社会上,或者是网络上,都会有对软件工程师(俗称码农)的刻板印象:加班多,情商低,表达能力不行,不修边幅。 国内国外,基本如此。 我去办信用卡的时候,负责帮我办卡的银行工作人员就两次问我,你们是否就只需要一天对着电脑,敲键盘就可以了? 虽说这只是刻板印象,但是的确切中了部分要点(起码对于我个人而言)。 以表达能力为例,我理解的表达能力好,就是能简洁明了,逻辑清晰地把一件复杂的事情描述清楚。 逻辑太跳跃,或者思路不流畅,就很容易让人听得云里雾里。 而表达能力本身又非常重要,无论与家人沟通,同事合作或者晋升答辩,良好的表达能力都能事半功倍。 而写作就要求你把自己脑海中以网状交织的知识,以结构清晰的方式,呈现给读者,做到「娓娓道来」。 在这个过程中,你的文字表达能力能得到提升,口头表达能力也会得到提升。 因为两者是相通的,都要求头脑对需要表达的内容具有层次性和条理性,只是最终的输出手段有差别。 3 加深理解 在写作时,我总有种奇怪的感觉「这个东西我懂,但是我写不出来」。 其内在原因是,对于该领域的内容,「我懂,但不是完全理解,无法做到信手拈来。」 因为要给写一篇让人能读懂的文章,势必要从基本的概念开始讲起,然后层层递进, 如果你对该领域的知识体系理解不到位,就会出现卡壳,写不出来的情况。 写作过程就促使你回头重新学习,弥补薄弱之处,进而加深对整个体系的理解。 所以写作本身就是在践行最好的学习方法:《费曼学习法》。 只是从给小朋友讲解,变成了写作,向所有读者分享。 4 促进内容传播 对比常见的沟通(如微信聊天,面对面交流等)和信息交流方式(音频,视频),文章拥有更好的传播优势。 如果你是面对面与人交流,或者微信聊天,你的交流方式是点对点的,只限于对面的人,你无法将信息广播给其他人; 而文章传播是点对面的,文章可以被复制,粘贴以及转发,自然拥有更广的受众。 又因为面对面交流,或者微信聊天是点对点的,所以你回答A的问题,可能也会被B问到,但是你却无法「复用」你的答案; 而文章是可被复用的,如果A和B看完文章,疑问自消。 而对比音频,视频等多媒体内容,文章的传播成本更低,可以直接被转发; 此外文章的阅读成本也比音频,视频更低,你可以检读,跳读,搜索文章内容,而视频只能从头看完,才能知道其究竟介绍了什么内容。 5 建立影响力 无论是有意还是无意,当你写的文章被阅读,被传播之后,你就在建立影响力。 如果需要建立影响力,可以通过演讲,制作B站或Youtube视频,或者写作来实现。所以会有这样的话: 如果你是个外向的人,你就去演讲和拍视频。 如果你是个内向的人,你就去写作。 但从传播学的角度来说,演讲和视频的传播优势都不如文章。 而建立个人影响力,都可能会对你的事业和心理健康起到促进作用。 从事业的角度来分析,建立影响力可以建立个人品牌,积累个人的影响力,助力职业发展和提升。 更多的人知道你,你才会有更多的机会,毕竟有人的地方才会有机会。 从自我实现的角度来分析,你的影响力越大,你的读者越多,你传播的知识可以影响和帮助到读者就越多,你就越能满足心理学家马斯洛所说的「自我实现」需求。 6 碰撞交流的火花 写文章本质就是在分享观点,当文章被传播,有了读者之后,自然会有人对你的观点持赞同态度,有人持保留意见。 读者就有可能向你阐述他们自己的想法: 当你有一个苹果,我也有一个苹果,我们交换了苹果,也只有一个苹果; 但当你有一个想法,我也有一个想法之后,我们交换想法,我们就有了两个想法。 要做到闻过则喜非常难,但是不同的观点就相当于一面镜子,可以让我们审视自己原来的观点是否合理。 他人的观点也给我们提供了换位思考的机会,从他人的观点切入,了解别人是如何思考的,避免「同温层效应」,只听到自己想听到的观点。 7 记录思考与成长 所谓「雁过留声」,又所谓「雁去无痕」。 横向对比,每个人都是独立的个体;纵向对比,每个人在不同的时期又会有不同的思考和感悟。 中学时候,语文老师总是会鼓励大家写日记,或者周记,说可以提高自己的作文水平。 所以我当时「轻信」老师的建议,尝试写了近1年的日记和周记,希望可以借此提高下自己的作文成绩。 但是即使我写了一年的笔记,也没有见语文老师多给我的作文一些分,感觉用处着实不大,然后就放弃了。 前段时间,在家里的柜子发现我这些用稚嫩笔迹写下的日记,翻看着有些泛黄的,写着的各种生活小事或者心情的纸张,忍不住笑了起来。 又或者翻开自己大二大三写的博客,记录着自己当初学习的一些笔记,稚嫩的思考,都会有种翻看旧照片的感觉。 写作,大概就相当于是用笔触作胶卷,给当下自己的思考和感悟拍下一幅幅「游客照」,以待日后再聆听昔日「雁行」时所留下过的声音。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E9%97%B2%E8%81%8A%E5%86%99%E4%BD%9C/","summary":"1 前言 之前写《软件工程师的软技能指北》系列的时候,就有个挺想聊的话题的,就是写作。 其实不只是对软件工程师而言,我觉得对于所有人而言,都应该尝","title":"闲聊写作的好处"},{"content":"1 前言 人并非全知全能,工作和生活难免会有各种的疑问,有问题自然可以询问有经验的同事或朋友。 但为了避免一有问题就去问人,给别人造成困扰,更推荐的就是: 自己先搜索,然后再去问人(Do a search before you ask a question) 当然,如果你不想打扰他人,直接问ChatGPT也未尝不可,只是答案的准确性不一定有保证。 如何高效地搜索,缩小搜索的范围,如何快速地检索到答案呢? 那么我来分享一下自己的个人经验: 2 Google Search 虽然我认为「搜索并不仅限于使用搜索引擎」,但是「搜索引擎」却是搜索并不可少的一部分。 虽然搜索引擎有很多,但是我基本只用 Google;如果没法使用 Google, 那么推荐使用Bing, 反正百度不在我的推荐之列. Google 搜索的界面很简单,只有一个搜索框,用户只需要把想要搜索的内容输入进去并回车即可。 比如搜索:「cpp modules」,返回了 7,320,000条结果。 搜索结果太多,我想对搜索内容进行筛选,google 就提供了相当多的搜索指令(search operator) 2.1 时间 cpp modules是c++20 才新增的特性,如果我想按时间搜索下相关的内容,可以使用 :before, :after 指令,后面跟着一个日期: cpp modules :before 2020 可以看到搜索结果变成了185,000条,并且返回的搜索结果都是在 2020 年以前的纪录,这个在查看历史新闻时特别有用,比如看历史合订本。 2.2 站点 如果你只想搜索某个站点,但是这个站点没有提供搜索功能(比如学校或者公司官网),或者搜索质量不够好,那么就可以加上 site: 的关键词, 要求 Google 只返回某个网站的检索结果: 比如我想看下 jetbrains家的IDE 对 c++ 20 Modules的支持程度: cpp modules site:jetbrains.com 又或者,我搜索网站的时候,想把某个网站排除掉, 比如使用中文搜索编程相关关键词的时候,经常会被CSDN 的垃圾内容污染,那么就可以使用 -, 来排除掉某些内容. cpp modules -microsoft 原来排名第二的 Miscrosft 就被过滤掉了. 2.3 社交媒体 如果你想在社交媒体上搜索某个关键词,那么可以使用 @ 后跟社交媒体的名字来进行搜索,例如 \u0026ldquo;cpp modules @twitter\u0026rdquo; 或者 \u0026ldquo;cpp modules @reddit\u0026rdquo;, 可以把 @ 理解成是 :site 指令的简化版本. 只是社交媒体(social media)的定义比较含糊, Google没有给出具体的说明,但是比较有名的社交媒体都是支持的. cpp modules @reddit cpp modules @zhihu 2.4 文件类型 可以通过 filetype 来指定想要搜索的文件类型,比如想搜索 pdf 相关的内容: cpp modules filetype:pdf 这个在知道书名,想要搜索电子书的时候特别有用. 2.5 关键字匹配 Google 支持若干个关键字匹配的指令: 双引号: \u0026ldquo;cpp modules\u0026rdquo;, 精确匹配,只匹配包含\u0026quot;cpp modules\u0026quot;的内容 \u0026#34;cpp modules\u0026#34; 搜索结果变成 3530 条纪录了. 星号: \u0026ldquo;* modules\u0026rdquo;, 通配符,所有包含 \u0026ldquo;modules\u0026quot;的内容都会被检索出来。个人觉得用处不大,只会让搜索结果膨胀. OR: \u0026ldquo;cpp or module\u0026rdquo;, 匹配包含 \u0026ldquo;cpp\u0026rdquo; 或者\u0026quot;module\u0026rdquo; 的内容, or 可以使用竖线代替 | 个人觉得用处不大,也只会让搜索结果膨胀 AND: \u0026ldquo;cpp and module\u0026rdquo;, 匹配包含 \u0026ldquo;cpp\u0026rdquo; 与\u0026quot;module\u0026quot; 的内容, and 可以使用与符号代替 \u0026amp; 3 Custome Search 前面提到「搜索并不仅限于使用搜索引擎」,是因为有很多内容,搜索引擎检索不到。 比如在公司内网的信息,Google 再强大,也不可能会检索得到的,因为不公开。 这个时候就可以借助浏览器的 Custom Search能力(Chrome 叫 Site Search, Firefox叫 Keyword Search)。 举个例子,我的老东家用的是代码搜索工具是 OpenGrok, 可以搜索整个事业群的代码,支持多种语言,可以搜索代码的定义,引用,历史记录等。 (下文以同样使用 OpenGrok 部署的开源项目 LibreOffice 的代码为例子) 因为在日常开发的时候,遇到陌生的函数名或者枚举定义,就需要看下他们的定义与实现,看下有没有问题: 比如想看下 contains 这个函数的实现: 或者想看下 Intersection 这个函数的引用,看下其他人是怎么用这个函数的,我也顺便抄下。 一般的步骤是: 打开或切换到浏览器(Chrome/Firefox) 打开内网网站链接, 在例子中就是 https://opengrok.libreoffice.org 点击 Definition 或者 Symbol 输入或者粘贴想要查询的内容,比如 contains 一套流程下来,大概需要30-40秒,不能说很慢吧,但是起码算不上快。 但是如果使用 Custom Search, 大概可以缩短至 7-8秒, 并且适用于绝对大部分的网站. 首先把查询函数引用的url 复制下来, 观察: https://opengrok.libreoffice.org/search?full=\u0026amp;defs=\u0026amp;refs=Intersection\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=refs\u0026amp;searchall=true\u0026amp;si=refs refs 后面跟着的就是需要查询的内容, 即 Intersection, 将 Intersection 替换成 %s : https://opengrok.libreoffice.org/search?full=\u0026amp;defs=\u0026amp;refs=%s\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=refs\u0026amp;searchall=true\u0026amp;si=refs 3.1 Chrome/Chromium Site Search 打开Chrome/Chromium -\u0026gt; 点击设置(Setting) -\u0026gt; 点击搜索引擎(Search Engine) -\u0026gt; Manage search engines and site search -\u0026gt; Site search [Add] Search Engine: OpenGrok Code Search Find Reference(取个有意义的名字) Keyword: csr URL: https://opengrok.libreoffice.org/search?full=\u0026amp;defs=\u0026amp;refs=%s\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=refs\u0026amp;searchall=true\u0026amp;si=refs 然后,在Chrome 的浏览器地址,输入 csr, 空格, 再搜索 Intersection, 回车。就可以直接在Chrome 地址栏里面搜索指定网页的代码. 而搜索代码定义,URL 如下: https://opengrok.libreoffice.org/search?full=\u0026amp;defs=Intersection\u0026amp;refs=\u0026amp;path=\u0026amp;hist=\u0026amp;type=cxx\u0026amp;xrd=\u0026amp;nn=19\u0026amp;si=defs\u0026amp;searchall=true\u0026amp;si=defs 只需要将 defs 后面的内容修改成 %s, 再建一个新的site search, 名为 Opengrok Code Search Find Definition, keyword 为 csd, 就可以快速搜索代码定义. 如果想要搜索其他网站,比如公司内网: https://search.xxoa.com/query=Foobar, 只需要把查询内容修改为 %s, 再新建个Site Search 即可。 在老东家,搜索错误码,或者是搜索内网上的文章,我都是这么干的;所以到新东家之后,我也是这么搞的。 3.2 Firefox Firefox 也提供类似的功能,叫 Keyword Search, 添加起来甚至更方便: 打开想要搜索的网站 在搜索框点击鼠标右键,然后会看到一个「Add a Keyword for this Search\u0026hellip;」 修改名字与 keyword 然后,在 Firefox 的浏览器地址,输入 csd, 空格, 再搜索 Intersection, 回车。就可以直接在 Firefox 地址栏里面搜索指定网页的代码. 如果没有右键时没有找到 「Add a Keyword for this Search\u0026hellip;」的选项,也可以使用添加书签的方式,手动添加一个 keyword search: 4 Alfred Web Search 如果使用的是 Mac OS, 那么通过Alfred 插件的 Web Search功能,甚至可以不用手动切换到浏览器,直接就可以进行搜索,可以把搜索流的耗时进一步缩短到1-3秒。 Alfred -\u0026gt; Preference -\u0026gt; Web Search -\u0026gt; Add custome Search 除了要将 %s 换成 {query} 之外, 其他添加的步骤与 Site Search 一致: 录制 Gif 只花了1.5 秒. 5 总结 Perl语言之父Larry Wall 有句广为人知的名言:「程序员要有三大美德:急躁,懒惰,自大」。 急躁意味着不愿意花时间等待缓慢的程序,会想办法优化程序; 自大意味着不愿让人指谪,对自身要求强,要写出高质量的代码; 懒惰意味着不想花精心做重复无用的事情,会想办法自动化,让电脑帮忙处理。 \u0026ldquo;We will encourage you to develop the three great virtues of a programmer: laziness, impatience, and hubris.\u0026rdquo; \u0026ndash; LarryWall 而我对搜索流的优化,就是在培养「急躁」与「懒惰」的美德。 6 延伸阅读 我的各种「流」: 我的写作流:写作工具与平台分享 我的画图流:画图工具与技巧分享 7 参考 Mozilla Support: How to search IMDB, Wikipedia and more from the address bar Google Document: Refine web searches Google Document: Do an Advanced Search on Google ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%88%91%E7%9A%84%E6%90%9C%E7%B4%A2%E6%B5%81/","summary":"1 前言 人并非全知全能,工作和生活难免会有各种的疑问,有问题自然可以询问有经验的同事或朋友。 但为了避免一有问题就去问人,给别人造成困扰,更推荐","title":"我的搜索流:高效搜索经验分享"},{"content":"1 前言 分享两个鲜为人知,但是却相当有用的 Gmail 地址技巧。「鲜为人知」并非是标题党,而是引用Gmail 博客原话: I recently discovered some little-known ways to use your Gmail address that can give you greater control over your inbox and save you some time and headache. 2 技巧 假设你的Gmail 地址是 xiaoming@gmail.com: 2.1 加号 你可以将在用户名后面增加一个加号 +, 并在加号后面增加任意数量的字符,比如 xiaoming+happy@gmail.com, xiaoming+upset@gmail.com, Gmail 都会把这些地址当作成 xiaoming@gmail.com, 发送到你的地址邮箱中。 2.2 点号 你也可以在地址的任意地方插入任意数量的点号: ., 比如 x.i.a..o.ming@gmail.com, xiao...mi..ng@gmail.com, Gmail 都会把点号忽略掉,解析成 xiaoming@gmail.com 3 用途 技巧比较简单,寥寥数语就说完了,好像也没有什么大不了,有什么用处么? 这个就要发挥想象力了。 3.1 用途一:重复注册用户 这个主要是针对能使用邮箱注册的网站,可能大多数是国外网站。 如果网站的邮箱地址校验正则写得不好,允许加号和点号,不知道Gmail的这两个规则,那么 xiaoming+user1@gmail.com, xiaoming+user2@gmail.com, xi..aoming@gmail.com 就会被认为是三个不同的邮箱地址,就可以重复注册。 在薅羊毛等需要重复注册用户的场景就比较有用了。 3.2 用途二:溯源 个人邮箱难免会收到一些奇怪的邮件,例如:猎头的招聘邮件,钓鱼邮件等等。 收到这些邮件的第一反应肯定是把邮件删掉,之后就会思考,究竟是哪里泄漏了个人邮箱。 而通过 Gmail 加号的技巧,我就可以做到垃圾邮件溯源. 首先,在注册每个网站的时候,都给他们加上一个tag, 例如注册Twitter, 那就用 xiaoming+twitter@gmail.com, 如果注册Github, 那就用 xiaoming+github@gmail.com, 依此类推。 只要有垃圾邮件,我就能通过加号的后缀,知道是哪个浓眉大眼的网站把我的信息给泄漏出去了。 比如下面这个垃圾邮件,我就知道它是通过爬虫爬取我Github 公开邮件群发的. 我就可以选择不公开 Github 邮箱,来避免后续收到类似的邮件。 4 参考 Google Gmail Blog: 2 hidden ways to get more from your Gmail address ","permalink":"https://ramsayleung.github.io/zh/post/2023/gmail%E5%9C%B0%E5%9D%80%E7%9A%84%E9%9A%90%E8%97%8F%E6%8A%80%E5%B7%A7/","summary":"1 前言 分享两个鲜为人知,但是却相当有用的 Gmail 地址技巧。「鲜为人知」并非是标题党,而是引用Gmail 博客原话: I recently discovered some little-known ways to use your Gmail address that can give you greater control","title":"两个鲜为人知的Gmail地址技巧"},{"content":"1 前言 学习一门语言和学习手艺,过程差不多,没有太多的捷径可走,除了练习,还是练习。 无论是以前,还是现在,去公司上班,都需要接近一个小时的时间通勤。 为了不浪费通勤的一小时,我大多会在路上收听英文播客来练习英语听力。 2 工具 以前是坐班车上班,经常是听着听着英语听力就睡着了,毕竟播客的对话有深有浅,听不懂就容易睡着,英语练习就变成班车补觉。 虽然各种英语学习心得都强调多听的重要性,但是架不住着实听不懂,Podcasts App又没有办法展示字幕,你只知道你听不懂这个单词,但是却不知道这个单词究竟是什么? 不会的内容就不会有机会改善。 最近接触到一个很优秀的 Podcasts APP, 名为 Snipd, 可以通过AI自动把播客内容翻译成字幕。 说来有趣,这个Podcasts 软件的产品初衷并不是为了英语学习,而是类似视频截图,将播客的精彩瞬间和金句分享出来。 但是声音是很难以视觉化的方式来进行分享,转发的,所以他们就直接将当前播放进度前后80秒的内容以字幕形式呈现。 如果想要记录生词,可以直接点击创建「Create snip」,将句子保存下来,相当于保存了生词的上下文。 对于字幕生成,我现在发现,Snipd是采用离线缓存+在线生成的方式的: 如果是热门播客,可能就有用户已经提交了生成字幕请求,其他用户直接点开播客就可以直接展示; 对于冷门播客,需要我点击生成字幕,等待个10分钟,他们后台生成完成后会再通知我。 使用这个App还有一个附带的好处:可以收听非常多的海外播客。 因为中国什么都会有特供版本,播客也不例外。 如果使用的是国区的 Apple Id, 那么使用Iphone 自带的Podcasts App, 有非常多优秀的海外播客都无法搜索到(毕竟「收听敌对电台」) 而这个Snipd App可以搜索到非常多的海外播客,而大部分的英文播客都是海外播客。 3 播客 推荐几个我经常收听的英文播客: 3.1 Healthy hacker 网站链接:https://www.healthyhacker.com/ 一个从苹果天才吧电脑维修员工,成长为Github 工程师的小哥Chris Hunt主持的播客,我个人的最爱,主要是分享一些 Chris 自己觉得有趣的东西。 Chris 声音热情洋溢,可惜播客在2019年之后就没有更新了。 从天才吧员工成长为Github 工程师的那一期: 《11: Growing as a programmer》 3.2 THE CHANGELOG 网站链接:https://changelog.com/podcast 主要是分享软件工程,极客和行业创新,也有不少大咖上过播客,比如: Ruby On Rail之父 DHH, Sqlite 作者 D. Richard Hipp, Ruby之父,以及K\u0026amp;R 中的K( Brian Kernighan) . 3.3 Daily Easy English Expression 网站地址: https://dailyeasyenglish.libsyn.com/ 一个美国老师每期分享的地道英语词句的表达,每期只有几分钟。因为主持人是专业的英语外教,所以语速较慢,难度较低,非常好懂。 我在好几年前就在Youtube关注这个老师的口语教程,叫做 Daily English Dictation, 深入浅出,娓娓道来。 B 站上也有搬运Youtube的教程:每日英语听写 Daily English Dictation 1-400 翻开2020年的笔记,当时一天学习一课 Daily English Dictation,我学习到142课然后就放弃了。 3.4 THE HANSELMINUTES PODCAST 网站链接:https://www.hanselminutes.com/ 微软的 Scott Hanselman 主持的播客,类似技术杂谈,在英文技术类播客中也非常有名,他的角色类似个布道师。 3.5 Lex Fridman Podcast 网站:https://www.hanselminutes.com/ Lex Fridman 是俄裔计算机科学家,在MIT任职,他说话的方式很真诚,口音很好听. 他的访谈对象通常都非常大牌,比如是 Facebook 创始人 Mark Zuckerberg, 特斯拉的Elon Musk, 还有计算机的殿堂大神Donald Knuth等等. 只是他的访谈一般都很长,2-3个小时,我一般需要用一周的通勤时间来听完一期节目。 3.6 BBC 6 Minute English 网站: https://www.bbc.co.uk/learningenglish/english/features/6-minute-english BBC 主持的英语学习播客,顾名思义,每期6分钟,都是纯正的英音,女主持的英音尤其悦耳。 每期都截取一小道报道或者对话,然后学习一些新词,以练带学。 4 总结 突然意识到,收听播客和小时候通过收音机收听各种电台节目,如「评书讲古」似乎是异曲同工。 虽然媒介在改变,但是对好内容的需求却是一直不变的。 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%8B%B1%E8%AF%AD%E5%90%AC%E5%8A%9B%E5%AD%A6%E4%B9%A0%E5%B7%A5%E5%85%B7%E5%88%86%E4%BA%AB/","summary":"1 前言 学习一门语言和学习手艺,过程差不多,没有太多的捷径可走,除了练习,还是练习。 无论是以前,还是现在,去公司上班,都需要接近一个小时的时间","title":"英语听力学习工具分享"},{"content":"1 问题 最近整理了桌上乱糟糟的线,把原来使用aux 线连接的蓝牙音响换成通过蓝牙连接。 然后就发现一个问题,只要音响没有发出声音超过30分钟,蓝牙音响就会断开连接,并且自动关机,即使蓝牙音响连接着电源。 一番搜索之后,就在知乎上发现了这个问题:求问如何避免蓝牙音箱自动关机? 但里面提到的解决方案,大多只适用于特定平台,例如Windows 或者Macos, 没有提到 Linux 上的解决方案。 每过半个小时手动打开蓝牙音响再连接的方式,实在是太蠢了。 2 灵感 但是知乎问题里面的部分回答给了我灵感,让我们想起国内某些APP 为了保活,避免被系统kill 掉,在后台播放无声音频的操作。 我可以以低音量循环播放一段音频,以实现保活的作用: mpg123 -f 1000 ~/music/listen_to_the_sea.mp3 --loop -1 mpg123 是mp3 播放命令行, -f 1000 参数的含义是:100%的音量是32768, 1000 约等于是1000/32768 = 3% 的音量, -loop -1 就是指无限循环播放。 man mpg123 ... -f factor, --scale factor Change scale factor (default: 32768). --loop times for looping track(s) a certain number of times, \u0026lt; 0 means infinite loop (not with --random!). 3 优化 这样就实现了一个可用的版本,只是还要依赖一个 mp3 文件,肯定还有优化的空间。 一番调研之后发现, play/sox 命令可以播放指定频率和时长的声音,可以播放20 hz以下的声音,这个频率下的声音人耳是听不到的: play -q -n synth 10 sin 20 -q: 不显示播放进度条 -n synth 10 播放10秒的音频 sin 20 频率为20 hz(如果听到了,可以设置成更低) 执行命令之后,可以使用 pavucontrol 命令查看声音输出,应该是类似这样的效果: 4 定时执行 一直开着个terminal 窗口运行命令有点麻烦,这种重复性的工作,就可以交给 crontab, 让它每分钟执行一次,每次播放10秒。 * * * * * play -q -n synth 10 sin 20 但实际运行,发现声音不能如预期那样播放。一番搜索之后,发现 StackExchange 上有个答案提到需要 export 个环境变量,所以最好创建个脚本 play_beep.sh: #!/bin/bash export XDG_RUNTIME_DIR=/run/user/1000 play -q -n synth 10 sin 10 echo $(date) # 打印日期,主要是为了方便排查 然后再安装一个 crontab 任务: * * * * * /usr/bin/sh /home/ramsay/code/shell/play_beep.sh \u0026gt;\u0026gt; /tmp/beep.log 2\u0026gt;\u0026amp;1 经过验证,一天都没有断开过蓝牙,自动关机了。 5 参考 求问如何避免蓝牙音箱自动关机? Can I use cron to chime at top of hour like a grandfather clock? ","permalink":"https://ramsayleung.github.io/zh/post/2023/linux%E4%B8%8B%E5%A6%82%E4%BD%95%E9%81%BF%E5%85%8D%E8%93%9D%E7%89%99%E9%9F%B3%E7%AE%B1%E8%87%AA%E5%8A%A8%E5%85%B3%E6%9C%BA/","summary":"1 问题 最近整理了桌上乱糟糟的线,把原来使用aux 线连接的蓝牙音响换成通过蓝牙连接。 然后就发现一个问题,只要音响没有发出声音超过30分钟,蓝牙","title":"Linux下如何避免蓝牙音箱自动关机"},{"content":"1 前情提要 软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 2 前言 让我静静,我只想写代码 化用《我的团长我的团》里面,孟烦了父亲的一句话: 为何诺大的公司,放不下一张能安静写代码的书桌? 在我此前的固有认知里,所谓的软件工程师就应该安安静静地写代码,但为何我总是求而不得呢? 但事实是,在软件开发的大部分时间里,我们都是在与「人」交流,而非与「计算机」交流。 即使我们编写代码,首先也是让「人」去理解,其次才是让机器来执行,否则直接写二进制代码即可。 而在程序优化中,有一条金科玉律:「针对热点代码进行优化」,因为那是性价比最高的优化策略。 既然软件开发中,大部分时间都是与人交流,那么如果能提高与人交流的效率,那么我们的开发效率也会相应地大幅提高。 3 原则 3.1 尊重 与人相处时,最重要的概念之一(可能没有之一),就是尊重他人,每个人心底都是渴望被尊重的。 所谓的尊重体现在各种的细节里面,例如: 尊重他人的观点和言论,留意倾听,眼神放在对方身上,不随意打断别人。 尊重他人的时间,不迟到。 尊重他人的成果和工作,引用时注意作者与链接等。 尊重别人的空间,不在工位附近大声开会,尽量找个会议室。 被尊重是每个人最基本的需要,也是很容易忽略的地方。 我自己也会在心急时,直接把别人的话打断掉,所以自己在这方面还有很大的改善空间。 3.2 不随意批评 因为国内普遍存在的各种上下级等级关系和官本位思想。 遇到阻碍或者问题时,很容易通过「批评」来推动和开展工作,甚至很容易出现所谓的「PUA」话术: 其实,我对你是有一些失望的。当初给你定级px,是高于你面试时的水平的。 我是希望进来后,你能够拼一把,快速成长起来的。 px这个层级,不是把事情做好就可以的 你的产出,和同层级比,是有些单薄的,马上要到年底了,加把劲儿。 什么,这个事情排期要2周,1周就可以了,没有多少工作的。 我自己也亲耳听过类似的话,心情着实是难受。 事实上,如果真的把「尊重」这个基本原则考虑在内,鼓励与赞扬是比批评更有用的工具。 我现在的 manager 是个白人,他就很喜欢夸人,我私下喊他做「夸夸群群主」。 我和组员刚来的时候,可能他担心我们不适宜,或者是不干活,我们做了一些工作之后,总是在换着法子在夸我们: Thanks you for help to our team, your work makes a great difference. You are doing a great job, I am impressed by the way you tackled the problems. You will be successful in Amazon, I am pretty confident about that. 虽然知道老板目的还是想让我们干活,但是被人夸的感觉肯定比被人用鞭子抽打的感觉要好。 见贤思其焉,所以我也学老板多夸人。 有一次和舍友去一家韩餐餐馆吃饭,炸鸡很好吃,其他菜也不错。 上完菜后,韩国小姐姐过来问我们还有需要,我就说,「all foods are delicious, especially the fried chinken」。 小姐姐开心得拍起了小手。 身为中国人,可能从小被教育要内敛和矜持,但我们大可不必太高冷,不要吝啬自己的溢美之词。 3.3 换位思考 高效沟通的另外一个要点就是换位思考,从别人的角度,而不是自己的角度来思考问题。 在沟通对话中,什么对于他们来说是重要的?他们想要的是什么? 最好的方案是一个共赢的方案,可以把多方的诉求都包括在内。 一个非常有效的技巧就是,在开始你自己的观点,先重复一次别人的观点,这样就给对方一个明确的信号,我是真的考虑过你的观点的。 举个例子,前段时间发了一个 Amazon Canada 招聘的文章,有朋友闻讯而来,给我发简历,让我内推到系统中。 只是他没有预料到的是,发送完简历后,马上就收到一封笔试邮件,要求在一周内完成笔试。 朋友觉得时间太紧,没有准备好,于是邮件告知我准备放弃。 我思索片刻之后,决定与recruiter 沟通下,询问能否推迟笔试截止时间。 因为对于朋友而言,他的诉求肯定是有充足的时间来准备; 而对于recruiter 而言,她们办这个event ,也是希望有尽量多的候选人参加,有尽量多的候选人通过。 所以推迟笔试时间,以便朋友参与笔试,是一个符合多方诉求的方案,最后recruiter 的回复也是可以推迟时间: 3.4 充分的上下文 高效沟通和决策的前提是,提供足够的,充分的信息。 也就是说,在你提问题或者沟通的时候,把问题的上下文信息给提供清楚。 以前经常会遇到的一种情形是,在企业微信被人拉到一个群里,然后被@, 「xx哥,帮忙看下这个问题」。 我也很想帮忙,但是我连问题是什么都不清楚,我是没有办法解决的。 一个群几十上百条信息,我是没有精力去逐条翻聊天纪录的。 然后,很快就会有人打电话过来,让我解决这个xx问题。 如果想要我快速解决问题的话,麻烦首先要给出定义,问题是什么?然后再给出问题的上下文,这样我才能方便排查问题。 但这还不是最佳的咨询姿势,我推崇的咨询方式是所谓的 STAR 方法或者叫「Search before Asking」。 4 STAR 方法:高效提问 所谓的STAR method, 是四个单词的首字母缩写,分别是: Situation(场景), Task(任务), Action(行动),Result(结果)。即: situation: 描述问题的背景,这个问题是什么,以及你为什么需要做这个事情 task: 你具体的任务是什么,你需要做什么 action: 你做了什么事情?你的行动是什么. result: 结果如何,你得出的结论是什么? 前面提到过,尊重是与人交流的基本原则, 尊重自然包括尊重别人的时间,不做伸手党。 在咨询别人问题的时候,不仅要把问题说清楚,还需要把自己的调查和排查结果告诉别人,即所谓的「search before asking」,这样给人的印象是我尝试自己来解决,但解决无果才来请教你。 既表现出对别人能力的尊重,也显示出自己是经过调查才发问的,避免询问一些低级,Google 就能找到答案的问题。 没有人喜欢伸手党,你直接拿个问题,不经自己思考去询问别人,这就不是交流沟通,是「空手套方案」了。 别人没有这样的义务来给你提供解决方案。 所以我向别人求助,无论是企业微信,邮件,还是当面求教,流程一般是: 我现在尝试解决xx问题,我要去解决这个问题的原因是yyy 我尝试了解法1, 解法2,都无法解决,这是我的日志 尝试这几种解法都不能解决问题,不能你能否根据你的经验,给我提供点思路呢?或者是我漏了什么关键步骤么? 或者是. 我现在尝试解决xx问题,我要去解决这个问题的原因是yyy 我尝试了方案a xxxx, 得出的结果是xx, 然后我再尝试了方案b xxx, 得出的结果是xxx. 我个人感觉这两种方案各有优劣,分别是xxx, 我倾向方案a, 原因是xxx. 想请教下,你的看法是觉得哪个方案更优,或者你有什么建议么? 提供足够的信息和选项给别人做选择题,让不是提供个空白问卷让别人做主观题。 毕竟大多数人都喜欢做选择题,省时省力。 \u0026mdash; \u0026lt;2023-05-29 一\u0026gt; 关于如何提问,《How to ask questions the smart way》一文已经把要点给掰碎讲清楚了,推荐阅读。 5 云雨伞: 有效提建议 来源于日本知名咨询师大石哲之的著作《靠谱:顶尖咨询师教你的工作基本功》,简单有效,原理概括起来就一句话: 天上出现乌云,眼看要下雨,带上伞比较好。 其中的「云」代表通过观察得到的客观事实;「快要下雨」,是从客观事实得出的分析;「带上伞」这个是根据分析给出的建议。 这就所谓的「云雨伞」模型的来源,运用「云雨伞」模型提建议,有理有据有方案,能让对方更愿意接受。 青史留名,给老板提建议(画饼)的名篇《隆中对》,也运用了「云雨伞」模型: 亮答曰:“自董卓以来,豪杰并起,跨州连郡者不可胜数。 \u0026hellip; 荆州北据汉、沔,利尽南海,东连吴会,西通巴、蜀,(描述「云」,表达事实) 此用武之国,而其主不能守,此殆天所以资将军,(推测「雨」,即分析利弊) 将军岂有意乎?(带上「伞」,即提出建议) 益州险塞,沃野千里,天府之土,高祖因之以成帝业。(描述「云」,表达事实) 刘璋暗弱,张鲁在北,民殷国富而不知存恤,智能之士思得明君。 将军既帝室之胄,信义著于四海,总揽英雄,思贤如渴,若跨有荆、益,保其岩阻,西和诸戎,南抚夷越,外结好孙权,内修政理; (推测「雨」,即分析利弊) 天下有变,则命一上将将荆州之军以向宛、洛,将军身率益州之众出于秦川,百姓孰敢不箪食壶浆以迎将军者乎? 诚如是,则霸业可成,汉室可兴矣。(带上「伞」,即提出建议) 这一番结合「云雨伞」的建议(画饼),让老板直呼,「孤之有孔明,犹鱼之有水也」 6 总结 所谓的「Skill = knowledge + practice」,知道一项知识,如果不运用,没有办法修炼成技能的。 毕竟「纸上得来终觉浅,绝知此事要躬行」。 要多实践才能提高沟通能力。 推荐几本读过的,关于沟通,心理学与咨询的好书,推荐度由高至低: 《非暴力沟通》, 豆瓣评分:8.7(当然,评分只是一个参考项) 《社会性动物》,豆瓣评分:9.0 《靠谱》:豆瓣评分:7.6 《QBQ!問題背後的問題》,豆瓣评分:7.4 软技能系列的下一篇是: 软件工程师的软技能指北(四):简历篇 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E8%BD%AF%E6%8A%80%E8%83%BD%E6%8C%87%E5%8C%97_%E9%AB%98%E6%95%88%E4%BA%A4%E6%B5%81%E7%AF%873/","summary":"1 前情提要 软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 2 前言 让我静静,我只想写代码 化用《我的团长我的团》里面,孟","title":"软件工程师的软技能指北(三):高效交流篇"},{"content":"1 主业 Ramsay 是位软件工程师,以写程序为业.\n推崇开源与动手精神,使用Emacs 与Linux 多年,喜欢动手折腾。 写过C++, Java, Python, Javascript/Typescript这四种语言的生产代码,喜欢Rust 与 Ruby,常用EmacsLisp 为自己的工作流写小工具。 2 写作 从16年开始在博客上写博文,期间经历过多次迁移,但大多时候,博客的读者都只有我自己。 博客还有一个对应的英文博客,名为「In pursuit of Simplicity」,主要是记录一些英文写作与感悟的平台。 Simple is Beautiful 非纯技术文章大多会同步发布到公众号「宫孙说」上: 3 开源 在 Github 上有若干个开源项目,但目前主要在维护RSpotify 这个使用Rust 语言编写的 Spotify SDK。 在Stackoverflow 上也会帮忙解答其他开发者的疑问,反馈社区。 4 交流 Ramsay 喜读书,电影,历史;亦爱生活,料理;闲暇时常涂鸦写作,抒发感想。 绿蚁新醅酒,红泥小火炉。 晚来天欲雪,能饮一杯无? 望与君,共举杯。 5 友链 AsyncX: 🌌 Per Aspera Ad Astra ","permalink":"https://ramsayleung.github.io/zh/about_me_zh/","summary":"1 主业 Ramsay 是位软件工程师,以写程序为业. 推崇开源与动手精神,使用Emacs 与Linux 多年,喜欢动手折腾。 写过C++, Java, Python, Javascript","title":"关于我"},{"content":"1 前情提要 软件工程师的软技能指北(一):总览篇 2 前言 打工是不可能打工的,这辈子都不可能打工的。 3 心态转变 很多软件工程师容易把自己定义成「写代码的」,或者是「码农」,就是以写软件为生的人。 也只愿意接受写代码相关的任务,什么文档,设计,需求分析,是一概不想理的,我就是一把唆。 也有工程师觉得,反正我把事情做好也只有这么点工资,摆烂收入也不一定会下降,那不如就躺平,反正我的收入是固定。 也不能说毫无道理,只是把自己定位成「需求翻译机」,着实和「流水线的工人」区别不大。 随着自动化技术的进步,「流水线工人」很容易就被机器人所取代,它们只要能源充足,就可以24小时不停地产出 但是踏实干活的工程师,也难免容易有与以上类似的疑惑。 那不如换个思路: 把你的工作当成是你自己的生意(business),那你眼中的一切都会变得截然不同。 3.1 客户 既然是生意,自然要找对目标客户。 如果把工作当成生意,那么你的客户就是你的雇主,虽然你的客户大多数情况下只有一个。 但是很多的公司,都是靠给某一个大客户供货而做大做强的。 3.1.1 客户(customer)与用户(user) 谈起business, 我就想聊一下个人对于客户与用户的浅薄见解。 归纳起来,就是两点: 商业公司总是客户第一 用户不等于客户 简而言之,用户是使用某项服务或者产品的人,而客户是为某项服务或者产品付费的人。 举个例子,经常有人说,微信不注重用户体验,微信不倾听用户的声音,微信有着地球上第二傲慢的产品经理团队(第一可能是苹果)。 就我在微信的开发经历而言,的确如此。 没有见过哪些产品经理提的需求是来自于改善用户体验的, 腾讯内网上都是挂着各种反馈微信用户体验的帖子,最后都是以「这个问题,楼主可以私聊我们讨论」结束的。 因为,对于微信而言,微信用户只是使用微信这个软件的人,而不是为微信付费的人,不是微信收入的来源。 对于微信支付而言,客户是各种接入微信支付的商户,因为每笔交易,他们要交约等于交易金额 0.0021%或者更多的手续费,属于躺着赚钱的模式; 对于微信朋友圈,公众号而言,客户是各种广告主; 在微信用户面前,微信就是个爹,教育你们怎么使用微信;但是在微信客户面前,比如美团,快手这些微信支付的大客户,微信就是孙子。 要做什么需求,产品经理根本没有办法推;要什么时候上线,就什么时候上线,即使不合理,也只能回来压榨工程师的时间。 毕竟客户说了,你们不做我们就切到支付宝去。 所以微信用户本质上只是微信收入来源的耗材和燃料,反正用户离不开微信这口灶,产品经理为什么还要听燃料的心声呢。 当然,背后的商业逻辑是这样,用户体验又是另外一回事了。 3.2 产品 既然是生意,那么自然要有可以营利的产品或服务,对于大部分工程师而言,他们能提供的产品,就是生产软件的服务。 那和「写代码的」也没有什么差别嘛? 稍安勿躁,这只是第一步嘛。 如果我们提供的生产软件的服务是生意的话,那么要想营利,产生更大的利润,就需要我们考虑一个问题: 如何大家都是生产软件的生意,你的产品又如何从同质化严重的同行中脱颖而出。 3.3 竞争优势 搞低价倾销(加班巻死他们)? 这也是个可行但不能持久的法子: 毕竟你搞低价倾销,即使把生意都抢到,你产能有限,客户的单不一定都能接过来; 另外低价倾销,只会把市场搞坏,降低了利润空间,只会让客户单方面受益 强中自有强中手,一山还有一山高,万一遇到比你还能搞低价倾销的同行,那不是哑巴吃黄莲,有苦说不出嘛。 所以最优解应该是你提供更优质的服务,将优质服务作为自己的竞争优势。 既然要提供优势的服务,就需要 学会与客户沟通交流,先明确客户的需求, 然后分析需求,明确这服务是否客户想要的, 再动工建设,保证最终成品贴近客户的诉求。 或者是成为某个领域的专家,提供差异化的服务。 所谓人无我有,人有我优。 看到这里,有朋友可能会质疑:即使我做了这么多,做得这么好,但是工资(产品的售价)还是不涨阿,那还有什么意思? 如果把这个当作自己的生意,提供优质服务之后,自然是需要和客户重新谈合同的嘛(加薪)。 如果谈不拢,那就换家客户就好了,反正我只要产品够好,自然不缺客户,我还可以拿现有的供货合同和未来的客户谈。 生意是自己,服务做优质之后,最终受益的还是自己(当然,需要些时间和策略) 3.4 大厂光环 所谓的大厂光环,和偶像光环类似,就觉得个人会因为进去某个公司,把平台优势当作自己的成就,从而骄傲了起来。 坦白讲,以前我也有大厂光环,在自己去了某家大厂之后。 走路的时候,头抬得更高了,背挺得更直了,以便于胸前的工牌更加醒目。 如果把自己的职业生涯比作一门生意后,我想我应该不会再为与某个客户合作而沾沾自喜,毕竟客户的商业成就,与我关系不大。 客户可以有很多个,没有必要为别人的成就而自得不已。 最近新读到一首诗,唐代孟郊的《劝学》: 击石乃有火,不击元无烟。 人学始知道,不学非自然。 万事须己运,他得非我贤。 青春须早为,岂能长少年。 万事须己运,他得非我贤。 4 十项全能 既然是要做生意,那么只会写代码,注定是不可行。 毕竟没有见过哪家成功的商业公司,只在车间生产产品即可,不需要一系列配套的商业运作流程: 营销与广告,打造个人品牌,写博客或者做Up主 持续学习,没有什么生意是一成不变,就能从爷爷辈做到孙子辈的 如何提升个人效率,以更少的投入获取更多的产出 如何理财,管理你生意的营收与支出 如何健身,管理你自己的身材 旋转720度,落地无水花 诸如此类,这些技能要求也就变成理所当然。 5 总结 把工作当作生意的思路转变,只是第一步。 套用《霸王别姬》的一句台词: 今儿个是破题儿 文章还在后头呢 客户或公司是一个抽象的概念,实际也是由形形色色的人组成。 与客户合作,实际是与各种人打交道,如何高效沟通和交流就是一个非常有用的技能。 但对于曾经社恐的我来说,跨出第一步却是非常艰难。 所以软技能系统的下一篇是: 软件工程师的软技能指北(三):高效交流篇 对于后续的篇章,呼应上文,我有了大概腹稿,分别是: 软件工程师的软技能指北(四):简历篇 软件工程师的软技能指北(五):面试篇 软件工程师的软技能指北(六):谈薪篇 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E8%BD%AF%E6%8A%80%E8%83%BD%E6%8C%87%E5%8C%97_%E4%BA%8B%E4%B8%9A%E7%AF%872/","summary":"1 前情提要 软件工程师的软技能指北(一):总览篇 2 前言 打工是不可能打工的,这辈子都不可能打工的。 3 心态转变 很多软件工程师容易把自己定义成「写代","title":"软件工程师的软技能指北(二):事业篇"},{"content":"1 目录 为了方便阅读,把本系列的文章的目录整理如下:\n软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 软件工程师的软技能指北(三):高效交流篇 2 背景 上大学时,曾经看过一本书《软技能,代码之外的生存指南》,主要介绍程序员要想取得成功的职业生涯,所必需的软技能。\n所谓的软技能,是区别与使用C++或Java 编写业务逻辑和单元测试代码,使用Docker 部署等「硬技能」,而是注重职场,心态,身体与理财等各方面提升的「软技能」。\n大学时候的我还沉迷于编写各种很cool 的代码,学各种编程语言,觉得此书不过是面向程序员的「鸡汤」一本。\n2.1 再读 工作第三年时,换了一份工作,对职业充满了迷茫与困惑,然后再读了一次这本书,得出的结论是书挺不错的, 但是不符合中国程序员的国情。\n比如理财方面,建议程序员在低房价时买房,我也想买,但是在深圳只能是望楼兴叹;\n健身方面,下班后多运动,自己做饭,控制饮食以增肌减脂,着实没有时间自己做饭。\n但里面的思路和哲学很有参考意义。\n2.2 三读 最近突然想起了这本书,搜索之后发现,英文原版在2020能出了第二版,而中文版本也在2022年翻译出版了;\n因为书再版了,所以又重读了一次这本书,英文书名会更正常一些:Soft Skills: The Software Developer\u0026rsquo;s Life Manual\n得出的结论还是与之前一致:「书中的思路和哲学很有参考意义,做法却不一定适用」。\n但区别在于,我这次打算把我自己的做法与行动也总结下来,「用他的旧瓶子,装我的新酒」。\n就成功的标准而言,我的职业生涯还远谈不上成功,甚至可以说还有很多失败之处,这也算是我自己反思的心得。\n毕竟也不只有别人优秀的经历,我这种潦倒的经历也是可以参考的。\n见贤思齐焉,见不贤而内自省也。\n这让我想起了Netflix 的高分纪录片《我心永随桑德兰》,别人拍纪录片是记录成功,这部豆瓣9.2分的片子却是在记录失败:\n英超保级劲旅桑德兰,在2017年终于花光自己在英超的保级运气,从顶级联赛英超跌入二级联赛英冠。\n然后俱乐部高层想拍一部纪录片,记录自己奋发图强,卧薪尝胆杀回英超的经历,以此吸引投资者,最后却是二连跳, 跌入到三级联赛英甲的神剧情。\n把我不开心的事说出来,拿出来给大家一起开心下嘛。\n3 软技能 就软技能而言,绝不止「软件工程师」这个职业需要这个技能,而是绝大部分职业都需要的。\n因为绝大部分的工作都是与人打交道,而软技能就是如果更高效与人沟通的关键技能。\n如果有「软件工程师」认为自己只要写代码就可以了(我曾经就是这么认为的);或者觉得,写代码才是最有趣的部分(我现在也是这么认为的),我不想理这么多事情;\n那么你的可替代性就非常高,当代码生成工具足够成熟之后,你就可以被裁掉了。\n之前办信用卡时候,我说的职业是Software Engineer,银行客户经理问我, 你们是不是只要埋头对着电脑敲键盘即可,不需要和人说话的?\n(我心想,你是美剧看多了吧)\n而事实恰恰与直觉相反,软件工程师大部分时间都在与人打交道:\n拿到需求时,需要分析需求的可行性,与产品经理扯皮,理清模糊之处 撰写设计文档,和组员及老板介绍方案,比较方案优劣,选择最优解 与上下游团队扯皮,求下游团队帮忙干活,给上游团队表演太极 和老板画饼,只要再给我些时间,定然能做得成绩斐然。 哪项不是与人打交道呢?项项都是我缺乏的技能阿,我就只会接活,干活,然后再接再干。\n老黄牛听到我这境况,估计都得叫我一声兄弟;流水线见我这际遇,也会直呼一声「内行」\n所以「软技能」真的是不可或缺。\n或许是我给银行客户经理的答案不合他意,我信用卡申请被拒了;\n或许我「软技能」再强大一些就能通过了.\n4 主旨 原书把主旨分成七部分,分别是:\n事业(Career): 像经营企业一样,打磨自己的职业生涯 自我营销(Marketing Yourself): 通过高质量文章和视频,打造自己的个人品牌 学习(Learning): 终身学习,自我学习 生产力(Productivity):提高生产力的方式 理财(Finanacial):如何复利,创造被动收入 健身(Fitness):健康,有型的体魄 心态(Mindset):培养积极的心态,To be a better man(woman). 我就不会按照作者的主旨来阐述我自己的想法,毕竟「他瓶装我酒」,想怎么「装」就是我自己的选择了。\n感触多些的主旨,就拆分开多几篇文章,没有太多感慨的部分,可能就选择性省略了。\n5 总结 所以要「软硬都抓,内外兼修」,才能成长为一个优秀的软件工程师。\n只会写代码的软件工程师,真的是注定吃亏,酒香也怕巷子深。 这是多次吃亏之后得出的经验总结。\n其他的职业与岗位也大抵如此。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%80%BB%E8%A7%88%E7%AF%87/","summary":"1 目录 为了方便阅读,把本系列的文章的目录整理如下: 软件工程师的软技能指北(一):总览篇 软件工程师的软技能指北(二):事业篇 软件工程师的软技能","title":"软件工程师的软技能指北(一):总览篇"},{"content":"1 Amazon Canada国内专场招聘 刚刚有群里消息灵通的小伙伴分享,Amazon Canada(主要Base 在Vancouver )会在国内的深圳,上海及周边(杭州,苏州)和香港有专场的招聘会。\n最近有比较多的同学和朋友咨询加拿大工作的事情,所以就把招聘Event 分享出来。\n本次招聘的主要对象是L5的工程师,L4和L6也有少数的HC.\n国内专场招聘的链接:\nhttps://amazon.jobs/en/jobs/2361958/hong-kong-event-sde-amazon-stores\n二维码:\n可以微信或者邮箱找我帮忙内推, 我可以帮忙递简历和咨询进度,我邮箱是 cmFtc2F5bGV1bmcrYW16bl9oaXJlX2V2ZW50QGdtYWlsLmNvbQo= (base64 decode),希望能做个摆渡人吧。\n1.1 面试流程 与招聘的 recruiter 沟通过,应该还不会有 in person 的面试,还是线上的virtual 面试,给面试候选人发一个会议链接,然后进行线上面试。\n所以招聘写着HongKong Event, 也并不需要去香港面试。\n这次主要是面向SDE(Software Development Engineer), 职位大多是L5.\n面试时间大概是6月底,所以大概有2个月的时间来准备。\n以我个人经验,L5面试流程大概是:\nOnline Assessment(OA), 做一到两道算法题,难度大概是Leetcode Medium - Hard, 多刷题库,有机会试上原题。 Phone Screen(Phone Interview), 视OA 结果而定,根据OA 的答题结果,系统会给出建议是否需要Phone Screen, 如果答得比较好,可能就不需要。我当时也没有这一轮 4轮onsite, 一天搞完,包括: 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + 一题算法题,Leetcode Medium 水平 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + 一题算法题,Leetcode Medium 水平 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + System Design(SD) 一至两道基于Leadership Principle的 Behavioral Qusetion(BQ) + Object Oriented Design(OOD) 拿 Offer 2 Q\u0026amp;A 在微信上和朋友分享之后,有比较多的朋友咨询问题,我就尽量总结下Q\u0026amp;A\n2.1 面试是全英文的么? 如无意外,是的。毕竟去了温哥华工作,也不可能只说中文。\n2.2 我英语口语不行,可以试试么? 其实,只要能沟通就会了,不是雅思考试,不需要4个7,能让面试官听懂就可以了。\n即使去尝试下,最多也只是付出时间成本,也损失不了什么。\n小马过河,你也不知道你是松鼠还是黄牛。\n2.3 面试通过后公司帮办工签么? 是的,公司会指派对应的律所来帮忙申请工签,只需要按律所要求提供材料即可。\n时间大概需要4-6个月,视情况而定。\n配偶和子女也可以一并办理签证。\n2.4 面试通过后多久入职? 一般大概需要半年,主要是办工签,等IRCC 审批。\n2.5 Amazon不是刚裁员,怎么又招人了? well, I don\u0026rsquo;t know,我也想知道。\n2.6 Amazon的面试难么? 这个嘛,只能说见仁见智。\n只是相对来说,Amazon 是北美大厂FANNG 里面的地板难度,主要是多刷题,就有机会遇到原題。\n多刷面经,亚麻的面试套路还是相对固定的。\n2.7 Behavioral Qusetion 与Leadership Principle 究竟是什么? BQ(Behavioral Qusetion),即行为面试问题,Amazon 面试官会通过BQ考察你的性格特征和职场软技能,判断你与企业的文化、价值观等原则是否匹配,最终决定是否录用你。\n而判断标准就是Amazon 的17条「价值观」,即 Leadership Principle, 例如:\nCustomer Obsession Ownership Invent and Simplify \u0026hellip; 具体可见官网说明:https://www.amazon.jobs/content/en/our-workplace/leadership-principles\n常见的问题如:\nTell me a time when you had a deal with a very difficult customer Tell me about a time when you took on something significant outside your area of responsibility Tell me about something you deliver above standards Tell me about a time when you couldn\u0026rsquo;t meet your deadline Tell me about xxx 相当于命题作文,需要结合你自己的简历内容,提前准备小故事\n2.8 招聘的业务部门是哪些? 据recruiter 所说,这次是个专场的招聘,相当于是放到一个大池子里面,通过之后再做Team Match, 所以她们也不知道具体的业务团队是哪些?\n2.9 我要刷多少题Leetcode 呢? well, 这个也很难回答,毕竟每个人能力不一样,但一般来说,300题 + Amazon 的题库应该可以应付了。\n2.10 我是写xxx 语言的,可以去面试么? 就我所知,无论你是写 Javascript/Java/C++/C/Python/Ruby/Assembly 还是Lisp,\n只要你能通过面试即可,使用什么语言都没关系。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/amazon_canada_hiring_event/","summary":"1 Amazon Canada国内专场招聘 刚刚有群里消息灵通的小伙伴分享,Amazon Canada(主要Base 在Vancouver )会在国内的深圳,上海","title":"做个摆渡人:Amazon Canada的国内专场招聘"},{"content":"1 起 落地加拿大已经大半个月了,也开始工作了,心情也从期待,紧张,忐忑,彷徨,兴奋到现在逐渐平静下来。\n来到一个陌生的国家,使用不一样的货币,说不一样的语言,过不一样的生活和工作习惯。一切如初生婴儿一般,需要重新学习和适应。\n现在就来分享下我与加拿大之初体验。\n2 衣 出发前,在广东的天气都快接近30度,在家的我已经把拖鞋,短裤都已经穿起来了。\n虽然我所在的城市已经是加拿大的最南端,但我落地加拿大之后的第一感觉就是:冷。\n看了下手机上的天气预报,温度只有5度,还刮起了风。\n落地之后,我的穿着从短裤变成了棉衣,只是我穿着行李箱拿出来的棉衣都觉得冷,怎么看到街上的行人只穿了个薄外套或者夹克,都丝毫没有寒意。\n天气太冷,以至于我都无法出门跑步了。\n除去冷之外,温哥华的另外一个特点就是多雨,所以温哥华(Vancouver)也被称为是雨哥华(Raincouver),之前还遇到下冰雹:\n后来,我发现,无论大太阳还是下雨,温哥华本地人都不打伞。因此,我得出一个结论,他们的衣服是防水的。\n那么问题就来了,他们的裤子防水么?\n2.1 地理位置 家人开始总会很疑惑,为什么我说我去的城市是温哥华(Vancouver), 他们看到我的定位叫列治文/列士满(Richmond), 我不会去错地方,进了传销窝吧。\n所谓的温哥华一般指的是大温哥华地区(Greater Vancouver), 或温哥华都会区(Metro Vancouver), 是指温哥华市和周围的卫星城组成的都会区,约等于粤港澳大湾区,但是不同城市之间的距离就小很多了,大体与广州和佛山类似。\n对这些大温城市的「刻板」印象:\nVancouver(温哥华):真-温哥华,大温地区唯一的「城里(downtown)」, CBD,潮牌夜店与流浪汉所在地。 North Vancouver(北温):风景好,本地人居多。 West Vancouver(西温): 巨富聚居,豪宅云集之地,俞敏洪家就在温西。 Burnaby(巴拿比):地理位置处于大温中心,有BC省最大的购物中心,新城区。 Surrey(索里):印度族裔聚居之地,治安较差。 Richmond(列治文):华人聚居之地,目测50%以上都是华裔。白人在这里,算是「外国人」,被华裔移民戏称为「老家」。 3 食 因为我有一个挑剔的广东胃,来之前,我还担心来到加拿大之后不适应,只能委屈我的胃消化薯条,炸鸡,汉堡了。\n没有想到 Richmond 被誉为北美最多中国美食的城市(另外一个可能是多伦多)。\n3.1 酒楼 这里遍地写满繁体中文的饭店和酒楼,再上服务员的粤语招待,让我有种身处香港的错觉。\n而饭菜品尝下来,这里的店的水准和味道,可谓吊打深圳(深圳能有什么好吃,就随处可见的椰子鸡),估摸能比得上广州。当然,只是味道,价格要贵很多。\nRichmond 饭菜虽美味,但北美有个我至今也难以适应的风俗:给小费。\n加拿大的小费还是税后的,一般要给个15%. 只能默默地为国内的服务业从业者哀叹下,国内人工真廉价。\n3.2 做饭 酒楼饭店虽多,但总不能天天下馆子,所以就需要拿出我多年的「煮饭公」经验了。\n而住处1km内,就有华人超市「大统华」和香港风格的菜市场,我平时在国内用到的酱料和厨具,在 Richmond 的菜市场都能买到,甚至包括盐焗鸡粉和磨刀石,着实有点离谱。\n因为有这样的便利条件,我就能吃到有家味道的饭菜:\n回公司办公时,因为公司在温哥华市中心,附近真的只有汉堡和炸鸡,就一定要自己带饭了。\n不过,公司办公楼外的风景着实很不错:\n4 住 4.1 选择 新到一个城市,最重要就是要找到住处,有个可落脚的窝。\n之前做过攻略,预期是住在 Richmond 或者Burnaby,为了实地考察究竟哪里更宜居,我通过酒店和Airbnb 民宿,把Vancouver 市中心, Richmond, Burnaby都住了两天,得出的结论是:\nVancouver downtown: 又贵,又小,流浪汉又多,附近又没有便利的生活设施 Burnaby: 环境虽好,多公寓,但是附近没有便利的生活设施,如果没有车,购物,买菜都不方便。 Richmond: 好吃的多,生活设施方便,但距离市中心稍远,通勤时间略长。 权衡利弊之后,最后毫无悬念地选择住在了 Richmond.\n4.2 租售比 租房时,从网站上浏览温哥华租金历史的变化趋势,看到这两年的租金翻了一倍。\n一个两室的房子,如果靠近天车站和超市,基本都要3000加元,通货膨胀压力太大,真的吃不消。\n从深圳来的人,难免会对房价感兴趣,就看了一下住处附近的房价。\n温哥华西靠大海,北朝雪山,身处加国最南边,气温宜人,风景好又宜居,所以温哥华的房价一直高居加国前茅。兼之这两年房价又猛涨,已经到了让普通人咋舌的地步了。\n只不过加币兑换人民币,汇率是1:5,如果温哥华的房价以人民币换算过来,还是比不上深圳。\n不知道这是否值得深圳人民骄傲呢。\n4.3 租房条件 和国内租房,房子基础家居齐全,能够「拎包入住」的情况不同,北美租房的「标配」是只有房屋一间,家居,床垫等都需要自行采购。\n有家居配置的反而是「高配」,幸运的是,我和舍友找到这样的房子恰好是「高配」,房东也是位很nice 的女士。\n此外,与国内租房只需要签租房合同,按时交租不同,在加拿大租房还需要额外提供相当多的信息:\ncredit history, 房东用来审核租房是否有不良的信用纪录,毕竟这里是个信用社会。而我初来乍到,信用卡都还没有,自然就没有credit history, 所以就需要交半个月的押金。 工作offer 和收入证明,证明是有稳定收入的。 护照原件。 验资,提供存款纪录,证明可支付三个月的房租 额外提供更多的信息,也换来了相应的法律保障。\n法律规定了房东一年的租金涨价幅度不能超过通货膨胀的水平,如果租房选择继续次年续租,租金最高涨幅2%。就不会出现腾讯员工尊享「12折」租房优惠的情况。\n如果房东想要给房子大幅涨租金,唯一的方法应该是等租房合同到期,选择不续租,然后加租金再次出租。\n5 行 就公共交通设施的便利程度,与广州或深圳相比,温哥华真的算是「大农村」,或者整个北美都是大农村。\n与广州密布全城,甚至跨城的地铁相比,温哥华只有短短的几条地上铁线路(这里的地上铁,称为天车(skytrain)),搭乘公共交通出行相当不便,所以买车是必然的选择。\n不过想来也合理,毕竟加拿大地广人稀,如果要建设公共交通设施,覆盖面小了,受惠人群有限;覆盖面大,成本也大限上升,但人流量的上限也就在这里,投入产出比太低。\n像广州地铁的「死亡3号线」这种情况,是不可能在加国出现的,毕竟这里的自行车都能扛到天车里面去,可见人流量之稀疏。\n但因为我还没有加国的驾照,要通勤方便的话,只能选择住在天车站附近,通过11号车和天车换乘。\n5.1 高速 因为需要采购生活必备品,没有车着实不便,我们就租了个车,由有北美生活经验的舍友来开车,开到Burnaby 去采购。\n开着开着,导航就说顺着高速走,我很奇怪,问舍友,我们上高速了么?我怎么没有看到收费站。\n舍友科普到,这里的高速是没有收费站的,并且超速是不能使用摄像头抓拍的,是不算的。\n如果要抓超速,只能由警察现场拿测速仪「人赃俱获」,并开罚单。所以这里的高速限速90km/h, 但是大家都开得很快。\n话音刚落,就看到路边有位警察拿测速仪如雕塑般在太阳底下一动不动,现场教学,吓了我一跳,这位警察真的是辛苦了。\n我脑海里浮现的,就是《逃学威龙2》里面,星爷去当交通警察,拿着相机拍照的画面。难怪星爷要蹲点测速了:\n顺便感叹一下,北美的地是真的多,宜家在 Burnaby 的店,开得像个城堡一样大:\n5.2 过马路 我预想到会遇到很多状况,但是我没有预想到,过马路我都要学。\n加国马路上的绿灯亮的时候,相同方向斑马线的灯却不一定会绿起来。\n以致于我等了5分钟的灯,从路上的红灯等到绿灯,再转回红灯,都没有看到斑马线的灯绿起来,我还在想,这路口的灯是坏了嘛?\n直到后来有位行人加入等待的行列,按了信号灯上的一个装置,信号灯过了一会才绿了起来。\n个人猜想,是因为加国地广人稀,并不一定有那么多的人要过马路,为了提高车的通行速度,如果没有行人显式标识要过马路,就不需要提醒司机,有人要过马路,再提醒司机。\n6 钱 6.1 支付方式 在国内,已经习惯了不带钱包,只带手机出门。\n但在加国,除去部分华人超市与市场,基本看不到支付宝和微信支付这两家国内常见三方支付的身影,只能使用现金或信用卡,部分商家还支持Apple Pay\n刚开始时,就试过延续国内的习惯,出门吃饭没有带钱包,只能到店里后,又走回去拿钱包的情况。\n作为之前在微信支付的打工人,很自然地会去思考,为什么像支付宝/微信支付这样的三方支付没有在加国流行起来。\n观察下来,发现加国的POS机刷卡服务非常发达,无论是普通的快餐店,还是城里的商场,无一例外,都支持刷储蓄卡或者信用卡。\n消费者只需要拿卡贴近一下机器,滴一下,就能完成付款,体验比微信支付扫码还方便;如果是消费金额较大,就需要输入密码进行支付。\n除去pos刷卡体系完善外,像加拿大这样的北美国家,是建立在信用卡体系上的信用社会。前面提到的credit history 会和你的信用卡记录挂钩,也就是说,信用卡消费在加国不仅是一种支付方式,也是一种建立信用的途径。\n而信用卡,在国内,还只是一种超前消费的支付方式。\n对于有消费力,不需要透支的消费者而言,使用信用卡和储蓄卡并没有差别,信用卡积分也没有用处。\n而国内因为此前金融业发展落后于北美,pos 刷卡远没有北美普及,而通过微信支付和支付宝,相当于直接跳过了pos 机普及的过程,直接信息化。\n所以说微信支付和支付宝的发展是契合了中国的现状的弯道超车,但国外发达的pos 刷卡体系,也没有引进微信支付这样的三方支付方式的诉求,也难怪微信支付和支付宝在北美出海不顺利。\n6.2 货币 一个比较有趣的事情就是,加拿大现在还在使用硬币,硬币金额由大到小分别是:2元,1元,50分,25分,10分,5分。\n而除了2元外,其他的硬币都没有写数字,像我这样开始不熟悉硬币的话,要盯着硬币看很久,才能找到5 cents, 或者 25 cents的标识。\n硬币一般头像面是女王的头像,背面是动物。\n为什么还会有10分,5分的硬币呢,因为还有$1.89这样的标价,给现金的时候,就会给回你10分。\n只是让人相当不习惯的是,5分的硬币竟然是比10分大的。\n6.3 物价 温哥华的物价是真的高,据说2022年,加国的通货膨胀达到了8%, 而物价上涨的幅度可能还不止于此,让我这个之前挣人民币的感觉压力山大。\n和朋友提到物价这个事之后,他就说,你后面挣的是加元,那感觉就会好的啦。\n即使挣的是加元,感觉压力也大嘛。\n真的和深圳一样,「哪里挣钱哪里花,一分别想带回家」\n6.4 税 据说加拿大的福利很好,但是羊毛出自羊身上,福利都是从公民身上收税收过来的,毕竟政府不会产生价值。\n所以无论吃饭还是购物,小票上都可以看到加的税,购物需要交12%的税,分别是5%的联邦商品与消费税(federal Goods and Services Tax (GST)), 以及7% 的BC省销售税(Provincial Sales Tax (PST));吃饭要交5% 的税,可谓是雁过拔毛了。\n问题就来了,为什么在国内消费,消费者感觉不到加的税呢,是否不需要加税呢?\n正如我之前说的,「羊毛出自羊身上」,哪个国家都不例外。\n宜家在国内的增值税大概是17%, 只是国内大部分商家的票据,应有关部门要求,都不会把税率打印出来。具体的税率,要开增值发票的时候才会展示出来。\n只要不让羊知道羊毛出自羊身上就好。\n7 网 坦言之,加拿大的手机网络是真的又贵又垃圾,25G流量的套餐,需要50加元,而信号又一言难尽。\n在住处房间里面,即使是使用号称信号最强的Rogers 家的卡,也只有一格信号,在车库,就只能打紧急电话,信号完全没有了。\n在刚搬进去,还没有装wifi 的时候,每天都是戒网治疗,而戒断反应又相当剧烈。微信文字有时能发出来,有时发不出来;晚上发不出去,早上能发出去。\n要和家人或妹子语音聊天,只能到阳台外面去,温哥华的室外还只有不到10度,真的是一边聊天,一边在发抖;即使阳光信号也不稳定,有时延迟严重,说话靠喊才能听到。\n我在想,即使我去贵州的深山里面,信号还是满格的;即使20年前也不需要如此通信吧。\n移动,联通,电信,我应该向你们道个歉。\n与同行一对比,你们都打出了苹果vs诺基亚的表现了。\n8 天 总说国外的月亮更圆,晚上我盯着天空的月亮看,并不觉得更圆,但是脑海总是涌现苏轼的名句:\n但愿人长久,千里共婵娟\n可能要十万里才能共婵娟了。\n国外的月不见得更圆,但天着实更蓝:\n9 地 加拿大人与动物相处得很友好,路边随处可见各种动物与飞鸟。\n天上与窗外飞过的海鸥(看来以后吃薯条要当心了):\n车库溜进来的松鼠,看起来像大号的老鼠\n街上的兔子\n10 乱 因为加拿大可以合法吸大麻,所以在温哥华市区中心,有很多人吸大麻。\n初时,我还不知道那股刺鼻的味道是什么,直到舍友科普,我才知道,原来那是大麻。\n在大麻味道飘进鼻腔时,也能理解为什么加拿大被称为「加麻大」 .\n正如朋友所言,在国内吸二手烟,在这里吸二手麻。\n另外,Vancouver 市中心也真的多流浪汉,让农村来的我震惊不已,不过舍友说这比对美国,真的算小巫见大巫。\n有流浪汉的地方,治安自然不会太好;而downtown 商铺加装的铁闸门,也似乎在印证我的想法。\n之前,温哥华市长也来了一波「清理低端人口」的措施,不知道从哪里学来的。但是,这终究是治标不治本的手段,又能把人赶到哪里去呢。\n如果有工作,有住处,可能大部分人都不想当嬉皮士。\n只能说,没事多待在村里,离市中心远点来了。\n11 花 来到之后才知道,温哥华有非常多的樱花,可谓随处可见。\n4月的时候,樱花烂漫盛放,还有日本的樱花赏在温哥华公园举行。\n人站在樱花树下,着实有种误入「樱花源」,「落英缤纷」的感觉:\nblooming\n12 语 在落地之前一直担心语言不够用,落地之后,意识到我的语言是「够用」又不「够用」。\n「够用」是指日常生活,购物,工作交流基本都能应付过来,无障碍。\n而「不够用」是指难免会出现「词不达意」,或者「欲语还休」的情况,说明表达能力还不够用;\n此外,软件工程师的工程并不只是限于写代码,那是「硬技能」,还需要有对应的「软技能」,而沟通就是非常重要的「软技能」。\n如果技术生涯还想继续向上走,免不了要与更多人,更多团队交流,要扯皮,要对线,单纯地say yes 和 no 都不够用。\n语言可以说决定了技术生涯的天花板,所以现在每天还会去抽时间学习英语。\n现在可以说是「沉浸式学习」,有更多地学以致用的机会,相信坚持下来,效果会更佳。\n13 终 大半个月的时候,可以说看到了,体验到非常多不一样的事物与人文,但一切终究还是停留在走马观花,蜻蜓点水的程度。\n有朋友问我,这是我想要的么?\n我只能说,还不确定,初到时,人容易被好奇和新鲜被迷惑,这最终只能由时间来赋予,由时间来洗尽铅华。\n但有些理所当然的权利,在失而复得之后,却不会引起人的注意力,因为那是我们本就拥有的。\n去地铁站坐车,再也不需要大包小包过安检。\n打开网页,访问Google, 再也无需使用代理。\n过了许久之后,我才突然意识到,无论线上或线下,出行都不需要被「安检」了。\n关于这篇文章的一个小彩蛋:\n以「衣食住行」开篇,但写着写着,干脆把子标题都用一个字代替好了,因为这样看起来很有趣,就成这个样子了。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E6%8B%BF%E5%A4%A7%E4%B9%8B%E5%88%9D%E4%BD%93%E9%AA%8C/","summary":"1 起 落地加拿大已经大半个月了,也开始工作了,心情也从期待,紧张,忐忑,彷徨,兴奋到现在逐渐平静下来。 来到一个陌生的国家,使用不一样的货币,说","title":"加拿大之初体验"},{"content":"1 前言 从微信支付离职,我能带走什么?文档,代码,设计方案还是微信支付的漏洞?\n如果我带走这些资产,那我现在就在深圳的看守所里面吃着公家饭了。\n既然这些资产不能带走,那么我能带走什么?\n如果沉下心思考,就会发现,这些资产价值并不大,对于工程师而言,也没有领导想象中的那么重要,除非我们试图将代码放在黑市售卖。\n对于业务开发而言,也可能是同样的道理。业务开发每天对着业务需求做CRUD,可能会羡慕开发底层组件的工程师,可以学习并提升技术水平,而自己技术水平还是在原地打转,能学习到的东西随着时间的推移,越来越少。\n王安石的《游褒禅山记》有这样的感叹:\n夫夷以近,则遊者众;险以远,则至者少;而世之奇伟瑰怪非常之观,常在于险远,而人之所罕至焉;故非有志者,不能至也。\n所谓的「险以远」,并不特指深奥难懂的底层组件技术,也指思考的深度;\n如果多去思考技术和业务,挖掘背后的本质,我们也可以看到许多「世之奇伟瑰怪非常之观」\n1.1 鱼与渔 文档,代码,设计都是针对特定问题的解决方案,如果离职到新公司之后,我们遇到的问题肯定不会完全一样,或者手头可用的工具不一样,那么这些资产的价值就会打折扣。\n更何况这些资产都是「一次性的」,用完即止;是属于「授人以鱼不如授人以渔」中的「鱼」;是「生产线」上的「成品」,而我对能生产「成品」的「生产线」更感兴趣。\n二战结束以后,美国把1600多名德国科学家、工程师、技术人员带到美国,包括沃纳.冯.布劳恩和他的V-2火箭研究团队;\n而苏联凭借地理位置靠近德国占领了一些重要的工厂,比如著名的德国光学巨头卡尔蔡司公司,苏联几乎搬空了该公司的设备,把1万多台设备中的9000多台都搬到了苏联。\n有人对现成的「鱼」感兴趣,也有人对未来的「渔」感兴趣,我属于后者。\n2 思路 既然选择「渔」,那么,要怎么挑选适合的「渔」来丰富自己的「渔库」呢?\n两千多年前的老师孔子就已经给出自己的答案:\n见贤思齐焉,见不贤而内自省也\n见到那些优秀的实践和思路,就学下来;对于有弊端的实践,就要分析弊端形成的原因,再想办法避免和改进,别人掉进去的坑,我们就不要进去凑热闹了。\n3 贤 3.1 模式化 1994年,4个博士合著了一本书,书中对常见的设计问题进行了分类,归纳与总结,并且针对每一类问题,给出可重用的解决方案。他们将这些可以复用的解决方案,称之为设计模式(design pattern)。\n这本书也成为软件工程和面向对象设计经久不衰的经典。\n这本书即是《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software),这四位博士也被称为Gang of Four (GoF)\nA design pattern is the re-usable form of a solution to a design problem.\n那么什么是模式呢?\n按照另外一本经典名著《面向模式的软件架构卷一》的定义:\n当专家求解一个问题时,他们一般不会发明一种和已有解决方案完全不同的方案来处理这个问题。他们往往想起已解决过的相似的问题,并重用其解法的精华来解决新问题。\n在微信支付研发理念中,程序设计和开发,很多问题都是类似,或者是重复出现的。\n针对此类重复问题,直接复制代码来解决,是下下策。\n对代码进行抽象,复用代码来解决重复问题,也是下策。因为使用公共库会导致代码之间无法隔离,并且把逻辑隐藏在公共库,会导致无法分析代码的调用关系。\n微信支付研发理念推崇的上策是对问题进行抽象,归纳出这类问题的通用解法,即模式;更进一步的是,为模式定义对应的代码模板,直接生成代码。\n即使不生成代码,也可以将模式实现成对应的组件或库,方便直接调用。\n具体例子如:\n微信支付就总结常见的分布式事务场景,设计和开发了分布式事务编排中间件。通过在画板编排事务资源,即可生成对应的代码模板,开发者只需要在指定的地方编写个性化代码即可。\n针对常见的领域服务,抽象了基于状态机和事件驱动的模型,设计了领域服务的代码生成组件。可通过绘制状态机UML图,直接生成接口代码,由开发者填充实现。\n以上算是技术组件的模式化,对业务开发而言,还有对业务的模式化。\n比如对扣款模式进行抽象,扣款时开启事务,进行风控校验,创建(或不创建)业务单,查询支付方式,轮询支付方式进行扣款,异常关单等。\n当时组里的大神龙哥,就是对已有的扣款模式进行了抽象,基于面向对象,设计成同步扣款框架,定义了以上的接口,由业务进行继承和扩展。再使用同步扣款框架对已有的3个类似但不完全一致的代扣扣款业务进行了重构,把扣款模式都统一了。\n3.2 复盘 没有人能保证自己写的代码绝对不会出错,当错误与问题不期而至的时候,我们能做的就是将「错误」的效益最大化,即从「错误」中吸引教训,做到「不二过」。\n复盘,就是在「错误」中吸引教训,做到「不二过」的手段。Amazon 也有类似的概念与机制,称为 Correctness Of Error(COE)\n我们一直说「失败是成功之母」,但根据生物学常识,只有「成功才是成功之母」,或者说「小步的成功才是大步成功之母」,别人踩过的坑,我们就不要进去了。\n复盘的一般步骤:\n回顾目标 故障影响 时间精确到分钟(甚至秒级别)的过程回顾。比如是新需求写出一级故障的bug, 就从拿到需求,设计方案,开发,部署上线,流量灰度,问题告警,处理手段,到故障排除,每个时间点操作都写下来。 分析问题原因,挖掘导致故障的表面原因与根本原因 总结针对问题的改进措施。 落实改进措施 通过这样的复盘过程,确保同样的问题不会再次出现。\n这样的工作方式和理念,无论是对个人还是组织,才同样适用。\n3.3 持续学习 微信支付一直在推广全栈工程师,认为只从自己做的事情来思考问题,容易导致盲维和短板,看待问题的眼光容易受限。\n此外,根据《人月神话》的理念,工程师之间的沟通成本,会随着人数的增加,呈指数水平上涨。而成为全栈工程师,可以一个人处理完需求,沟通成本就下降到0,极大地提交工作效率。\n微信支付的全栈工程师定义是前端工程师 + 服务端工程师 + 数据开发工程师。\n当然,某一端的开发工程师,不会某天突然自己变成全栈工程师,这些都是需要持续学习的,人总是需要不断提升自己的。\n不能人为能给自己设限,把自己定义成「前端工程师」,「后端工程师」,或者「数据工程师」,应该是「工程师」。\n3.4 需求分析 每个工程师都需要做需求,与正确地做需求相比,做正确的需求显然更重要。\n如何确保做正确的需求呢?\n微信支付选择的方法论是:需求分析与业务建模,脱胎自UML专家潘家宇的著作《软件方法》。大概的流程是:\n寻找老大(需要满足谁的诉求) 寻找业务用例(业务执行者做什么事情,比如QQ音乐用户购买QQ音乐会员,就是一个业务用例) 根据业务用例,寻找系统用例。(例如商户发起扣款是一个系统用例;扣款成功回调通知商户也是一个系统用例) 将需求的业务规则,总结归纳成系统用例的规则。 当然,业务用例和系统用例这套东西,可能只有微信支付用。但找准客户,帮客户解决真正的痛点,创造真正的价值,这个是有普适性的。\n做需求时,可以多问这两个问题:\n谁是我们的客户。 我们在帮他们解决什么问题。 3.5 “云雨伞” “云雨伞”这个概念来自内部的一份PPT,讲述的是如何更好地向别人提出建议,内容大概是:\n屋外乌云密布,儿子要出门,妈妈对儿子说,马上要下雨,淋雨容易生病,把伞带上吧。\n“云雨伞”的步骤就是:\n指出现状:乌云密布,马上要下雨 导致的问题与影响:淋雨容易生病 提出措施和建议:把伞带上。 通过这样的表述方式,会比「把伞带上」这样直接命令的话,更容易让人接受。\n当然,如果阅读过《非暴力沟通》,会发现“云雨伞”的表述,其实是《非暴力沟通》总结的有效沟通方式的简化版本:\n清楚地表达观察结果 表达感受 说出是什么需求和原因导致了这样的感受 具体的请求 当然,总是强调「云雨伞」的做法,把问题归咎到提问者身上,我是不赞同的。\n领导经常说,提问题的时候,要把自己的解决方案也提出来,没有人喜欢听吐槽。\n话虽如此,但是我想起之间还在蚂蚁时,一位P10工程师的文章,《没有答案,也可以提问题》。\n提问题是为了帮助组织发现问题,如果不能吐槽的话,很多问题也不会被发现,自然也得不到解决,毕竟也没有人喜欢帮别人的问题提解决方案。\n\u0026lt;2023-05-20 六\u0026gt;\n针对如何高效交流,我写了一篇自己的心得文章:软件工程师的软技能指北(三):高效交流篇\n3.6 一致性 领导总说,软件工程的本质就是管理和控制复杂度,而一致性就是减少复杂度的有力工具。所谓的一致性,可以理解成统一的流程,统一的组件等等\n在这种理念的驱动下,微信支付内部使用统一的编程语言,统一的工具库,统一的存储组件(使用别的存储需要特殊审批和说明),统一的数据访问组件,使用统一的研发流程。\n保证每个研发工程师,即使调到微信支付的其他团队,也是使用同样的工具,即插即用,和车床生产的螺丝一样。\n开始我对这样的理念是持支持态度的,但到AWS以后,我的想法发生了动摇。\n因为我发现AWS的工具真的是琳琅满目,应有尽有,而Amazon也并未对使用什么样的组件作要求。\n反正AWS对各种组件的支持都很好,所以业务团队可以自行选择适合自身业务的任意组件,能完成需求就好。\n所以我现在不确定,通过追求一致性来降低复杂度这样的做法是否合理。\n3.7 设计优于实现 从2020年初起,微信支付内部的需求都需要先写设计文档,Leader 评审通过才能开发。\n设计时有个非常关键的点,就是列出所有能想到的可行方法,而后比较各个方案的优劣,再作出取舍,选择最终方案。\n软件工程没有银弹,系统/软件设计就是不断地在做取舍,当然,人生也是。\n设计才是最重要的,而编码和实现都是简单的,因为这只是水到渠成的事(我也不是说可以不用重视代码质量,毕竟这是吃饭的手艺)\n我个人觉得,对于业务开发(或者对于软件工程师)而言,不要过多花时间关注在编码上,而应该是花时间思考需求和问题,找到好的设计上。\n良好设计带来的红利,是要多于良好编码带来的红利的。\n如果把编码比作战术,设计就是战略,不要让战术的勤奋,掩盖了战略上的懒惰。\n编码算是建筑的外墙和玻璃,而设计就是承重墙和地基,毕竟换皮容易换根难。\n微信支付对于业务代码的态度是,能生成就尽量生成,就不要人写了,要多花些时间在设计上。\n4 总结 拿走「代码,文档」终究是术,学走「思想和理念」才是道。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%BB%8E%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E7%A6%BB%E7%BA%BF_%E6%88%91%E5%B8%A6%E8%B5%B0%E4%BA%86%E4%BB%80%E4%B9%88/","summary":"1 前言 从微信支付离职,我能带走什么?文档,代码,设计方案还是微信支付的漏洞? 如果我带走这些资产,那我现在就在深圳的看守所里面吃着公家饭了。 既","title":"那些年,我从微信支付学到的东西"},{"content":"1 前言 因为之前我发文总结了一些打工心得,提到我最终选择了去加拿大,有比较多朋友对此比较感兴趣(或者有疑问):为什么选择的是加拿大?而不是xx国。\n我写了这篇文章来总结下个人的分析和见解,以下纯属个人见解,每个人应该结合自身实际情况具体分析。若有疑问,建议进一步咨询中介。\n如无兴趣,博君一笑\n2 基本认知 2.1 为什么要出国 要想清楚为什么要出国?\n世界上没有天堂,跟你说存在天堂的,只会是骗你的。\n想清楚为什么而出国,才能建立好认知,这个事关你能否在国外能坚持下来。\n一切都要像婴儿一样,重新开始学习。\n切忌为了出国而出国,我就是想出去。\n冲动的情绪就像一阵风,来得快,去得也快,在挫折面前很容易就演变成沮丧;只有深思熟虑后的决定,才能经得起考验。\n2.2 小马过河 和世界上大多事情一样,出国这事也是「小马过河」。\n光听别人说,自己不去实践和调查,总是无法过这条「河」的。你听到的案例可能是来自「松鼠」或「老牛」,但你要清楚自己的定位,你可能是「马」。\n因为出国的途径和排列组合着实非常多,你能通过的,我不一定能通过,反之亦然。\n而你需要做的,就是在诸多的排列组合中,找出对你而言,成本最能接受,可行性最高的组合。\n2.3 身份 无论是想要去外国工作,旅游还是生活,「身份」是一切的前提。\n「身份」可以理解成别国给你发的准入许可证,如果没有「身份」,你就无法在这个国家定居或工作,除非你愿意当「黑户」。\n而签证就可以理解成是短期(十年以内)的准入证明,比如留学签证,旅游签证,工作签证。如果两个国家关系友好,那么两个国家公民在对方国家短暂(少于半年)的逗留,有可能就不需要签证,比如日本公民2023年可以访问190个国家或地区不需要签证,台湾有近145个免密国家或地区。\n绿卡, permanent resident(PR), 即永久居民。以前美国permanent resident的卡是绿色的所以叫绿卡,后面变成一种对PR的统称了。如果你拿了某国绿卡之后,你就可以永久逗留在某个国家了,但此时你还是原来国家的国籍。\n入籍,更换国籍,从法律上正式成为x国人,拥有x国护照。\n3 常见途径 3.1 读书 去某个国家读书,工作,然后申请绿卡定居下来,是大部分人出国定居的途径,也相对而言为稳妥的途径。\n难度:中等 成本:较高 风险:较低 成本包括时间成本和金钱成本。\n时间成本,如果你是读master, 你需要花费2-3年来完成学业;读书期间,需要学费和生活费,这个就是金钱成本。如果你本来已经在工作,后面去读书,那么在此期间损失的收入也算是金钱成本的一部分。\n一般而言,如果留学生毕业后能找到支持工作签证的工作,那么就可以拿到工作签证,继续留在这个国家,然后排期等绿卡。\n当然,那只是一般而言。如果你要去美国这样的热门国家读书,找到工作之后也不是直接给你发工作签证H1B的,因为僧多粥少,需要抽签。\n近些年来,H1B 中签率逐年下降,现在大概在20%, 理工科学生毕业后最多能参加3次H1B抽签。\n3.2 工作 如果你已经在工作,不想花成本读书,那么直接申请某个国家的工作,也是一个可行的路径:\n难度:较高 成本:低 风险:低 但是你就需要研究你能否胜任某个国家的工作,并且雇主能否帮你解决工作签证问题。\n像清洁,外卖这些体力劳动,大部分成年人都能胜任,但是他们的雇主大多无能力(意愿)帮你解决工作签证问题。\n另外,也需要考虑你的心仪国家的签证体系,是否对国外务工者足够友好。\n以美国举例,除非是杰出人才,不然想直接从外国去美国打工,基本没戏。杰出人才的标准大概是博士学历,发了一堆的顶会论文,有客观指标和数据来证明你足够「杰出」。\n相对而言,加拿大,日本,新加坡,欧洲国家基本都可以申请工作签证,但各有各的门槛。\n比如加拿大,从国外直接招人,需要先申请LMIA(Labour Market Impact Assessment),相当繁琐和复杂,就是说明为什么这个人要从国外招,不优先考虑我们加拿大国内的劳动力,避免过多的外来劳工冲击本地劳动力市场,当时律所帮忙,整LMIA都花了2-3个月。\n新加坡还有个 EP工签,满足一定的薪水条件即可;日本也同理,程序员能面上日本的公司,基本能申请到签证。\n这个途径主要就和心仪国家以及是自身能力相关,基本没有什么成本,风险也低。\n3.2.1 内部转岗 这个算是求职的分支途径,对于跨国大公司,可能在世界各地都有分部。那自然就有人会想,我能否先面试到中国的分部公司,然后再内部转岗到心仪的国家所在的部门呢。\n难度:中等 成本:低 风险:低 心理压力:max 真的是个小机灵鬼。\n但是这个主要是和公司策略以及是目标国家签证体制相关。\n再以大家关注的美国为例,这种内部转岗到美国需要的签证是L1 签证,分为L1A 和L1B.\nL1A是发给高管的,有效期七年;L1B是发给普通打工人的,有效期五年。在座的可能都还是打工人,所以我们就来看下L1B。\n那L1B和H1B的差别是什么呢?H1B 可以跳槽,L1B不能跳槽。\n也就是在你拿L1B 签证期间,需要一直为这家公司打工,如果中途被裁,那就只能回国了。\n因为L1B这样被人拿捏,所以L1B 一般都是拿low ball,就不要想着拿高薪大包了。\n当然L1B 也可以排队绿卡和抽H1B,只是看看留学生H1B 的中签率,就能想象到没有L1B 抽H1B 中签率了。\n所以L1 签证需要在较长时间里,承受非常大的心理压力。\n3.3 结婚 通过和公民或者绿卡持有者结婚获得移民资格,路径非常简单,成功率与个体强相关。\n难度:因人而异 成本:低 风险:很低 不过多展开\n3.4 投资 某些国家,可以通过投资一定的钱,获得工作签证或绿卡。因为我没有这样的实力,所以完全没有了解过。\n难度:因人而异 成本:高 风险:低 3.5 曲线/非正当途径 了解到的,不建议途径:\n政治庇护 「走线」,非法入境,然后黑下来; 曲线途径,在心仪国家产子,「父凭子贵」。\n无论去哪,都是为了更好地生活,不要为了润而润。\n4 国家分析 4.1 常见选择 妈妈常和我说,「人往高处走」。对于我而言,既然是出国是为了更好地生活,那选择自然是发达国家。\n美国,加拿大,日本,澳大利亚,英国,德国,荷兰,法国,新西兰,新加坡等等。\n4.2 见解与分析 4.2.1 美国 难怪很多人都想去美国,毕竟我们的教科书上也说,美国是世界上唯一的超级大国。\n美国工作机会多,工作薪资高,税收较低(相对于列表中的其他国家),挣到钱才能更好地生活。\n对于计算机相关行业从业者来说,美国就是最好的工作地。\n也因为持有这种想法的人非常多,导致去美国的难度较高,而常见的出国途径也只有这几种,详见前文分析。\n4.2.2 欧陆国家:德国,荷兰,英国,法国 德国,荷兰和英国都是欧洲大陆的国家,因此可以把他们都放在同一类型里面。\n欧陆国家的特点就是生活非常非常躺,会有各种的福利和假期,如果不想卷,在欧陆国家生活会是一个很不错的选择。\n就计算机行业而言,欧陆国家算不温不火,美国的企业也在欧洲设有分部。\n但是天底没有免费的午餐,这些福利都是来自于纳税人的税收,福利越多,税收自然越重(反过来却不一定成立,某些国家税收非常重,但是基本无福利)\n并且,这种普遍吃大锅饭的氛围,也不卷,也就导致欧陆的薪资不高(相对美国而言)。\n年薪10万欧元已经是比较高的薪资,但可能要交1/3 - 1/2的税。\n还有一个问题,就是如果想申请这些欧陆国家的绿卡,除了英国外,基本都需要学习第二门外语,德语,荷兰语,法语等等。\n并且,华人在欧陆的数量也不多。\n个人主观感觉,英国工作岗位没有那么多,德国和荷兰比较缺IT的劳动力,我在Linkedin 更新简历后,有比较多的德国和荷兰的recruiter 和猎头找过来。\n做高频交易的:\n4.2.3 日本 虽然因为历史和文化的原因,很多朋友情感上对日本持否定态度,但无可否认的是,日本是地理位置距离中国最近的几个老牌发达国家。\n日本可能是对程序员而言,最容易来的发达国家之一(可能没有之一),对学历要求低(大专以上),对年龄也没有要求。\n签证比五眼等国家的好拿,而且对人的要求也很低,并不需要你的日语有多么溜,只要能正常交流,把工作做出来,来日本还是很容易的。\n对于二次元爱好者来说,日本来谓是圣地。\n虽说身处东亚的日本也卷,但那是相对西方发达国家而言的。\n前段时间看到个新闻,说因为日本的低生育率,政府都要严格禁止企业加班了,对于习惯了996的中国程序员而言,日本可以说是很佛系了。\n日本很多公司实行的是终生雇佣制,也就是意味着,公司很难开除你。\n同是黄种人,在外貌上与日本人几无差异,生活习惯也类似,当然除非口语能练习得与日本人一样好,不然开口就有差别了。\n距离中国的距离也近,从东京飞到中国的最南边香港,也只需要4个小时。\n但平心而论,日本的IT业并不发达,甚至可以说比较落后。\n在日本最top 的薪资应该是日本Google,5年以上的工程师大概能开出2000 千万日元的薪资;次top的就是日本亚麻,Indeed,PayPay 5年以上的工程师大概能开出1000-1500千万日元的薪资,所以日本的薪资在国内是没有竞争力的。\n日经中文网有这样一条新闻,细看下来非常能反应现状:\n但因为长年累积下来的卷文化,东亚三国的生育率都逐年下降,未来社会可能缺乏活力。\n在日本,想要拿PR,需要在日本居住10年,期间不能有任何犯罪记录,不能有失信行为,要遵守公序良俗做一个守法的移民。\n但通过高度人才签证,理论上最快一年就能拿到永久。\n高度人才签证是2017年推出的新政策,一定程度上说明了日本人才的紧缺,该签证采取的是打分制。\n在日本工作生活三年或者一年即可申请,其中70分-79分者原则上是3年,80分以上则只需要理论上的一年就可以拿到永驻资格。\n我参照打分表,给自己估了一下分,可以去到80分以上。\n4.2.4 新加坡 新加坡有非常多国内公司的分部或者总部,比如Shopee, 字节跳动,Tiktok;也有非常多跨国公司的亚太总部放在新加坡,也有非常多聚居的华人,不会有陌生和疏离之感。\n所以对于很多人来说,新加坡是出国的首选,无论是内部转岗或者是直接申请新加坡的公司;部分新加坡公司甚至可以使用中文来面试。\n此外,根据美国-新加坡自由贸易协定,新加坡的公民(绿卡持有者不行)可以申请H1B1工作签证去美国工作。\n但对我来说,新加坡这个选项,很快被我排除掉了,原因如下:\n新加坡国土面积太小,俗称坡县。国土面积小,可容纳公民少,缺乏战略纵深,容易受地缘政治影响。近些年因为大量移民进入,物价与房租飞涨,说明不堪重负了。 新加坡也很卷,因为大量中国公司和移民的涌入,导致新加坡也卷了起来。如果选择继续卷,何必出国再卷呢。 新加坡的PR不好拿,理论上新加坡的EP工签两年内就可以申请绿卡,但是据说绿卡很玄学。 对我而言,新加坡是个面积缩小,难度强化版本的海外深圳。\n4.2.5 澳大利亚,新西兰 澳大利亚和新西兰合并在一起了,都在南半球。\nIT行业比较一般,本土公司是Atlassian,Canva,国际公司在澳大利亚都有分部,如Google, Amazon 这些,高级工程师大概能给到15-20W澳元,工资对比国内没有明显优势。\n环境优美,工作也不卷,对新移民友好。\n因为身处在南半球岛国上,即使因为地缘政治,出现战争也难涉及这两个岛国。\n4.2.6 加拿大 加拿大有非常多的华人,华人社区非常多,在温哥华的Richmond 地区,街上商铺的招牌有许多使用的都是中英双语,听着街上的粤语,甚至有种在香港的感觉。\nIT业还可以,大部分的知名美国公司在加拿大有分部,例如Google, Meta, Amazon, Microsoft 等等。\n拿到工签落地之后,考出符合要求的语言成绩,就可以申请绿卡。\n根据北美自由贸易协定,美国给予加拿大和墨西哥公民的非移民工作签证(TN签证)。\n所以入籍加拿大之后,可以申请TN签证南下美国打工。\n加拿大可能是对新移民最友好和宽松的国家之一。\n加拿大的Express Entry 项目,支持在加拿大境外申请加拿大的绿卡,会根据你的经历,学历,语言成绩进行打分,入池排队,分数高的就可以直接获得加拿大的绿卡。\n但加拿大也有许多不足之处,冷,税收高,工资低。\n另外,加拿大三面环大洋,南面是盟国美国,所以除非是外星人入侵,不然战争是没有可能波及加拿大的。\n4.2.7 香港,台湾 香港IT行业就业机会较少,互联网很少,大多是交易或者投行公司。\n香港也不是个适居的地方,物价高,房价尤其高,和新加坡一样。\n随着中国经济的发展以及《国安法》的实施,香港和深圳的差距进一步缩小。\n通过优才计划,要7年才能拿到香港居民身份证。\n对于非广东人来说,香港的官方语言粤语一样算外语。\n台湾很好,经济发达,免费医疗和教育,同根同源,都不需要适应期。\n但中国大陆公民没有身份可以去台湾。\n签证是邦交两国之间的准入身份,台湾与中国大陆肯定不会是邦交国关系。\n4.2.8 总结 个人向:\n5 申请国外工作流程 如何将一头大象放去冰箱:\n打开冰箱门 将大象放进去 关闭冰箱门 程序员如何申请国外的工作:\n在Leetcode (非大陆版本)上面刷题,基本所有的公司都需要解算法题。这个就是游戏规则,你喜欢或者不喜欢,规则都不会改变 学习并准备 System Design 的知识 使用 Linkedin (非大陆版本),将个人信息更新成英文,撰写英文简历,选择心仪国家和公司进行投递;或者等猎头和recruiter 找上门。 在一亩三分地(https://www.1point3acres.com/) 上查看面经 面试 拿 Offer 具体每一步要如何展开,每个人都会不一样,无法一概而论。\n6 总结 种一棵树最好的时间是十年前,其次是现在。\n无论是去哪个国家,学会外语是第一要务,这个决定了你能否通过别国公司的面试,以及能否正常地在外国生活。\n无论你的外语水平什么样,无论是什么语言,英语也罢,日语也罢,现在开始学习都不会迟,因为它决定你的职业上限。\n凡事预则立,不预则废。无论要做什么,都需要提前准备。出国也罢,在国内也罢,都需要事先做好准备。\n自学能力,无论什么时候,都需要学习,固步自封不会有任何的改变。在一个新的环境里,你的知识储备随时都可能不够用。\n信息检索与分析能力,很多解决方案和知识就在哪里,如果不会检索和分析,你就一直待在井里,观着天。\n不会有人随时,免费,耐心地给你解答问题的,Google 和 ChatGPT 除外。在询问别人前,自己先找下答案。\n勇气,人类的赞歌就是勇气的赞歌,没有勇气,想法就永远不会变成现实。\n7 参考 润学:如何寻找适合自己的方案 润学:日本攻略 日本IT人才短缺,收入低於平均工資 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%B6%A6%E5%90%91%E4%BD%95%E6%96%B9_%E4%B8%8D%E5%AE%8C%E5%85%A8%E8%82%89%E8%BA%AB%E7%BF%BB%E5%A2%99%E6%8C%87%E5%8C%97/","summary":"1 前言 因为之前我发文总结了一些打工心得,提到我最终选择了去加拿大,有比较多朋友对此比较感兴趣(或者有疑问):为什么选择的是加拿大?而不是xx","title":"润向何方:不完全肉身翻墙指北"},{"content":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一段旅途;\n失大于得,那就说明这段时间是混过去了,唯有过后空叹,无可奈何花落去。\n但无论是失大于得,还是得大于失,已经过去的就注定成为历史,无法挽回,人终究只能是向前看。\n谨以本文,纪念我至今的打工生涯。\n2 广州 2.1 自学 虽然当初没有去到我想去的大学,但我也不想虚度大学四年时光。\n我对自己的大学生涯有很高的要求,我上的学校可能并不如其他人,但是我大学要学会的东西并不能比其他人差。\n大一计算机导论的老师对我们说,国内很多教材编写得可能并没有那么好,最好还是看国外的经典教材。\n因此,我把教材大多换成了国外的教材,并通过配套的网课进行自学\n在大二的时候,把计算机相关的课程,例如计算机网络,数据库,数据结构,算法,C语言与Java语言等都自学完了。\n2.2 大二第一份 Offer 并开始和同学组队写程序,我负责用Java写后端,参加各种比赛。\n当时和同学模仿「超级课程表」这个APP,写了一个我们学校版本的仿制品,称为「眸知(MooApp)」.\n因为我们学校最有标识的神兽是:动物科学院放养的一群黄牛,而黄牛叫声类似Moo,因此就起了这样的一个名字(粤语发音就类似:无知,就什么也不知道)\n做完「眸知」这个App之后,就开始一鱼多吃,把它投到所有能参加的比赛,并在其中一个叫「穗港IT应用系统开发大赛」获得了三等奖。\n颁奖典礼上邀请了加州州立大学的教授刘颖作程序开发经验分享,在分享过程中,刘颖先生提了一些问题作为互动,大多问题我都举手作答。\n会后,刘颖教授与我交流,询问我是否有意愿去他在深圳的创业公司(6滴科技有限公司)实习,怀揣着忐忑的心情,我表达了同意。\n就这样,我在大二暑假,在无需面试的情况,我拿到了人生的第一份实习Offer。(Offer来得太容易,当时同学还担心我去深圳进传销窝了)\n实习期间,我主要是参与电商系统DevOps 功能的开发,主要是通过Docker 来做CI/CD。\n但现在回想,当时并没有什么产出,大部分时间都在学习各种文档和概念(此前对Docker, CI, CD等根本没有认知),在老刘的指导下编写 Shell脚本。\n可能是我学习态度还算勤勉,工作尚且认真,在实习期结束,我也拿到了公司的 Return Offer。\n暑假结束,大三开始了。我用实习工资,给自己交了大三的学费。\n2.3 大三实习 Offer 大三要上的主要是专业选修课,大三下学期,我选的课是「面向对象分析与设计」,并且我此前已经自学过这门课了。\n就尝试和授课老师曾玲(我们同学口中的「老奶奶」,是个专业水平非常高,并且人非常好的老师)商量,我已经自学完这门课了,可否申请不来上课,按照提交作业和考试,我继续去深圳实习。\n令人惊讶的是,老奶奶竟然答应了我这个要求,于是我又回去了深圳实习,过上了边打工边上课的生活。\n(再回首,现在会觉得当初自己没有好好学习,光顾着打工,却不知未来打工之路漫长无期;但是没法用现在的标准去要求过去的自己,大二时家里生了变故,我希望自己能解决学费和生活费,我需要收入。)\n这次参与的是电商平台的后端开发,我们当时的代码都是开源项目,所以还能在Github 上找到源码\n虽然我拿到了Return Offer, 但我感觉当时在做的业务并没有太多前景,无论是电商平台还是跨境电商,都已经是一片红海;\n开发流程和开发工具也相对简陋,并且我想去BAT大厂见识下,于是我开始准备大三暑假的实习。\n当时很天真,因为我只会Java, BAT 里面只有Alibaba 是用 Java 的,其余两家用的是 C++,所以我就只投了阿里。\n而在选择事业群的时候,我也不知道阿里云,菜鸟是干什么的,所以就投了我用过的淘宝和支付宝。\n简历被支付宝的面试官捞起了,凭着这些年的实习打工和各种开发工具,开发框架折腾经验,通过了三轮面试,顺利拿到了支付宝的实习Offer,而要去的部门是芝麻信用。\n当时自我感觉还不错,毕竟我知道这个部门。\n3 杭州 3.1 芝麻信用 当时支付宝的总部还在黄龙时代,距离新总部Z空间建成还有1年多的时间。\n在新人培训结束的那个午后,我戴着实习生的工牌,迎着风走出公司大门。\n回头看去,那个印着支付宝的Logo的大楼赫然立在身后,我面带微笑,感觉前景充满了希望。\n但我当时还没有考虑Return Offer的事,还以为一切都是水到渠成的。\n在实习期的两个月里面,一边学习蚂蚁的各种中间件和Sofa框架,一边跟着导师尝试做需求。但却没有做出成果,以此证明自己有留用能力的紧迫感。\n每天都是开心地过着,甚至导师还会用他的内网权限,带着年轻的我,一起看内网阿里味的相亲帖子。\n直到留用面试的到来,其他的实习生同学都在紧张地准备,还拉上同事帮忙模拟面。\n与密锣紧鼓准备的其他同学相比,我却还在自我感觉良好。因为没有找其他同事模拟面试,我甚至都不知道终面面试会问什么。\n以现在的眼光来讲,终面面试,我发挥得是一塌糊涂,完全是答非所问,也没有结合业务和自己实习做的事情。所以,我理所当然地没有拿到Return Offer.\n而更为糟糕的是,因为我是迷之自信,以为自己可以拿到Return Offer, 就没有去准备秋招面试。\n所以到实习结束,我是0 Offer在手,并且秋招已经结束,即使拿着在蚂蚁金服的实习经验,投递的简历也石沉大海。\n我就这样回了广州,时间又来到了大四。\n4 广州 我在懊悔和自责中继续投递着简历,但是依旧音讯全无。\n我甚至去参加了腾讯的霸面,但是在酒店枯坐了一下午,也没有等来任何的面试机会。\n我不禁焦急了起来,然后开始临时抱佛脚地学习起了C++.\n这个时候,师兄帮我找了个机会,把我的简历发到了他在UC的部门群里面,由此我获得了一次面试实习的机会,表现达标的话,可以留用。\n我相当珍惜这次的机会,在面试前做了很多准备。\n这次只有一轮面试,面试官是中心的总监,在紧张和不安中,我基本回答上了面试官的问题,面试官让我回去等消息。\n不久后,我收到了HR的电话,通知我面试通过了,但是现在秋招已经结束了,已经没有实习生的HC了。\n但他们给我提供了一个选项,以合作伙伴(即外包)的身份入职工作半年,再视表现决定是否录用。\n虽然知道我可能被白嫖,但是我别无选择,只能努力向前,争取留用。\n4.1 UC 我就在UC 开始了自己的外包(实习生)之旅,邮箱也从之前的 gongsun@alipay.com 变成了 wb-lzr345319@alibaba-inc.com, 但平心而论,团队Leader 和导师真的是在用心指导和培养我。\n因为有了在芝麻信用的翻车之鉴,我也格外珍惜这次的机会,所以花了很多心思和精力学习业务和参与开发。\n与之前在芝麻只写运营系统代码不同,我这次写的代码是真的运行在生产系统上。\n当时在UC 做的是业务存储系统,提供一个通用的存储模型供其他业务使用。\n使用方定义模型,直接把数据存储在我们的系统里面,我们提供通用的数据访问接口,有点类似内部的PAAS 平台。\n有趣的是,当时的团队是使用 HBase 来做实时系统的存储的,并使用Mysql 作备份,通过消息队列在两套存储系统之间作数据同步,然后再从 Mysql 拉取数据,写入到 ElasticSearch, 通过ES 提供各种个性化的查询接口。\n使用 Hbase 主要是看上了它的水平扩展能力,非常易于扩展。\n但 Hbase 此前主要是配合Hadoop 作离线计算,UC内部没有其他团队有类似的实践,所以有很多问题需要解决。\n比如FGC降低系统吞吐量,负载均衡切换到其他的节点,在大流量情况下造成雪崩,把所有节点打挂,导致系统不可用的问题,就需要针对GC 参数作调优,减少GC Stop The World 的影响。\n我从这个系统中学习到非常多系统设计和热点调优的知识,上手实操又加深了我对这些开源中间件的认知, 初窥了系统设计的门道。\n虽说此前我连 Hadoop 和 ElasticSearch 是什么都不知道。\n半年之期很快就到了,因为之前的翻车状况太过惨烈,这次的留用面试,我提前两周就开始准备PPT,并和导师总结实习结果和收获。\n凭借半年的实习期的表现,以及最后的面试发挥,我通过留用面试,我把这个Offer 拿到手了。\n但是,我没有选择这个Offer。\n4.2 再话蚂蚁金服 在UC 工作4个多月后,我在v2ex 上面看到一个帖子,说蚂蚁这个团队秋招还有名额,欢迎应届生投递。\n因为在UC 留用的事情不确定,本着多个Offer 多份保障的心思,我尝试着投递了简历,很快就收到面试电话邀约。\n还是熟悉的三轮面试,但是可以深切感受到,校招的三轮面试,难度要远大于实习的面试。\n终面时,面试官(我的未来二级主管)甚至问到数据库怎么水平扩容,要怎么分库分表,事务如何保障等等。\n如果没有在UC 实习的这段经历,我可能真的没法办法回答得上,这些都不是看看面经就能回答上的问题。我感觉也超出了校招对应届生的要求,就这样被「拷问」了接近一个半小时,远超出正常的面试时间。\n经过这三轮面试之后,我终于收到了第一份校招 Offer,兜兜转转,又拿到了蚂蚁金服的Offer 了。可谓「山穷水尽疑无路,柳暗花明又一村」:\n4.3 抉择 所以,我拿到了两份校招Offer,两份来自「阿里」的 Offer 。\n最后,在权衡发展前景,技术成长和个人成长等因素,我选择了蚂蚁金服的Offer, 就这样,我以不一样的途径,又回到了蚂蚁这个最开始的地方。\n这次去的部门是网商银行。\n如果从现在的眼光来看,实习时没有在芝麻信用留用,不见得是件坏事。\n因为芝麻信用用户虽然多,但是没有找到业务发展的突破点和营收方向。在集团层面,已经连续几次被打业绩差了。(阿里人熟知的3.25)\n5 杭州 5.1 近卫军 2018年,一群来自天南海北的应届生来到了蚂蚁金服,公司开展了为期1个月的脱产培训,名为「青年近卫军」培训,我也认识了一群好朋友。\n在这一个月的培训里面,上午来自不同部门的专家对我们进行组件和技术的培训,下午我们组队开发项目 mini-alipay 项目,然后为了赶进展,开始体验到传说中的996的工作节奏。\n最后我们成功完成了自己的一个mini-alipay 的Android App, 并且凭借这个项目,收获到一篮子的奖项。\n我很自然地会以为,我「重生」的蚂蚁之旅,也会是这样顺利。\n5.2 客户域 我当时任职的团队是网商银行的客户域,负责处理网商银行所有的用户与商户信息,算是基础团队。\n客户域非常值得称道的是,使用的是蚂蚁集团内部总结的金融数据模型「飞马模型」进行重构的,对数据模型进行了清晰的划分,可以称之为标杆。\n又因为客户域属于整个网商银行的底层服务,被非常多的服务所依赖,所以系统设计和空灾就要做得非常扎实,我也因此受益匪浅。\n在客户域待了八个月后,有一天导师来和我们说,客户域的业务要移交给北京的团队;虽然知道阿里的文化有「拥抱变化」,只是未曾想,变化来得如此之快。\n5.3 聚合收单 客户域的业务移交后,原团队的同事因为没有业务可干,分别被分流到其他团队。我来到了聚合收单团队;\n所谓的聚合收单,即所谓的四方支付,在微信支付和支付宝支付外,再增加一层代理商的角色,为直连商户或服务商接入微信支付和支付宝。\n那商户不能自己接入微信支付和支付宝么?当然可以,聚合支付只是可以帮你同时接入这两家。\n这也是这个业务的问题所在,只能作为通道存在,不具有任何的门槛和粘性,商户可以随时切走。\n其兴也勃焉 其亡也忽焉。\n聚合收单巅峰时,曾代理微信支付10%的交易量;但在微信支付发现这种代理行为,并进行打击之后,业务量急据萎缩,聚合收单团队又面临解散。\n在聚合收单待了10个月之后,我又无事可干了。\n5.4 金融网络 这一次,我和老板详谈,希望可以到个稳定的团队,可以踏实地工作。而老板手下能满足我要求的就是另外一个团队:金融网络。\n对于网商银行,或者支付宝,微信支付等三方支付而言,必须要和其他的银行打交道,通过指令进行扣款/扣款。\n因此就需要与每个银行进行对接,这个就是金融网络团队的工作。\n或许会有人问,不能接入一个统一的代理中继,这样就不需要几百个银行,每个都对接一次了。这个中继是存在的,就是网联。\n但是多一个中继,就需要多一份成本,人家又不可能给你白干,需要收手续费的。所以为了降低成本,也需要分别对接不同的银行。\n网商银行的金融网络是从支付宝fork 过来的,不同的是,支付宝有100多号人的团队维护,网商银行的金融网络团队,加上我也不过8个人。\n金融网络维护的系统,庞大,灵活且复杂。很多功能,复杂到都没有人能说清它是怎么工作的,也没有文档或者资料,一切都靠口口相传。\n因为金融网络复杂又重要,被整个网商银行所依赖,就导致金融网络很容易出故障。\n在这样的环境里面,我又坚持了半年,感觉着实看不到什么前景和机会。\n频繁的业务变更,两年时间,经历了3个团队,兼之晋升和绩效的问题,导致我心生去意。\n5.5 加班与学习 杭州是996之都,而阿里可以说是996的发源地。因此,在蚂蚁金服,想正常上下班基本是种奢望。\n我很敬仰的一位博主随想君对996工作制的认知是,996工作制只不过「劫贫济富」的缩影。\n996工作制对工程师职业生涯的影响非常不利,主要是:\n压缩了员工的业余时间,因此减少了员工的自学时间,你更加没有时间去自学,去提升自己的能力;如果能力得不到提升,你在人力市场中的「议价能力/谈判筹码」也就得不到提升;然后只能继续接受这种变态的工作时间,这是个恶性循环。身陷其中,并越来越无法自拔 消耗了员工的自控力,也就减少了自学的「动力」:如果你的工作不是你的兴趣所在,长时间加班之后,回到家里,你很难再有动力去学习其它新技能。 对健康的负面影响 对家庭的负面影响 如何走出996的怪圈呢?关键在于时间与坚持。\n每天挤出的时间不需要很多,哪怕半小时到一小时,足矣。这里的关键在于「坚持」。\n如果你能坚持每天挤出“半小时到一小时”用来自学,大约1到2年时间,就会有效果——你的能力就会有提升\n提升自己的能力,是摆脱这个怪圈的第一步。\n蚂蚁的工作强度虽然大,但只是995,又因为我住在公司旁边,所以省去了通勤的时间,不玩游戏,又省下不少时间。\n每天晚上回去,花一个小时看书和学习;周末和近卫军的小伙伴韬然一起去学习半天到一天,然后另外一天去踢球。\n就这样,我每年大概看完了20本书,专业书看得比较慢,花了1年多的时间,学习了C++,算是入了门。\n英语是不能放下的,听,说,读,写;除了说的机会不大,读和写都尽量保持着,使用英文进行搜索,阅读英文文章;使用英语回复Github 和Stackoverflow 的问题。\n在2020年的时候,又开始自学日语。学好语言,机会总会多些的。\n5.6 面试 在决定离开之后,又开始了面试之路。\n5.6.1 腾讯 因为好朋友在腾讯是做计费和结算系统的,然后就把我简历推给到他们部门,就这样开始了腾讯的面试之旅。\n一面很顺利,在马路边一边散步一边电话面试,问题都不难。\n二面总监面也还不错,问到的系统设计问题以及取舍,组件选型等问题我都能回答上来。\n本来面试就差不多结束了,从电话那头,总监听起来也还挺满意的,最后问了我一个问题,我是怎么看待加班的。我就把我的观点和对996的看法如实告知了总监,感觉电话那头的面试官陷入了沉默,面试就这样结束了。\n然后,我二面就挂了,我不知道是否因为我太坦诚。或者我应该说不排斥加班,就能结束这个话题了。\n5.6.2 微软 因为不想加班,所以就尝试外企,就找微软的朋友内推了简历,便有了人生第一次的外企面试经历。\n外企基本不考察项目经历和计算机原理(即所谓的八股文),基本只看解算法题,而我当时在leetcode 上也就解决了不到200道题。\n当时令我惊讶的是无法约上他们面试官的时候,我希望是可以中午面试,HR反馈员工中午休息,不面试。我打算是5点半之后面试,HR反馈大家下班了,不会进行面试。外企都这么早下班的么?这是我们这种996打工人无法想象的事情。\n我都打算是请假面试了,最后是微软的面试官进行妥协,回家之后来面试我。面试时候,我甚至可以听到面试官孩子在旁边玩耍的笑声。\n面试官问了3道算法题,我只做出来了一题半,那半题是使用暴力解法解出来的,时间复杂度基本没法看。\n剩下的时间就和面试官相互沉默与尴尬,即使面试官给我提示,我也没有思路做出来。\n解题的确是需要训练的,这一面自然是面试失败了。\n当时可以说是相当沮丧。\n5.6.3 微信支付 这样又过去了一个多月。\n好朋友给我推荐了微信支付的岗位,说是有个师兄在学校的群里发的。\n我就尝试投了一下简历,这连串的面试失败让我对自己没有什么信心,何况这还是微信支付。\n微信支付一面的面试官面试内容比较有广度,从工程实践,面向对象设计,设计模式问到了分布式系统算法。\n最后的20分钟又上一道算法题,我解出来之后,又追问我怎么证明我是对的。我只能当场写几个test case 来断言一下,只能说我的解法能覆盖到这些case。\n然后一面就通过了。\n新奇的是,在通过一面之后,一面面试官询问我是否愿意做一道笔试题。其实这个也不算征询我的意见,如果想继续面试的话,笔试题只能做。\n只是这道笔试题,需要两周的时间才能完成,也就是我拿到了一个完整的需求,要求2周内完成:依照微信客户端,实现微信支付委托代扣服务列表和服务详情查询。\n面试的时候说语言不限,现在又要求我使用C++ 和grpc 完成,说考察我的学习能力,还好我都学过。\n但我就没见过这种面试要求,可能微信支付比较牛吧,我只能这么安慰自己。\n就唯有白天和晚上上班,下班后加班到凌晨来做这个笔试题。\n花了两周时间,撰写了设计文档,使用C++17写完了这个需求,并附上完整的测试case,得到的反馈是还不错。\n就这样,推进到第三面(如果笔试题算二面的话)三面面试官问题都非常有深度,但都是从浅入深,针对我给出的答案进行发问,没有实际的工程经验和思考,只靠面经是无法水过去的。\n后来就是面委面,不过因为我的级别不到高级工程师(9级及以上),所以只是微信支付内部的面委。\n因为我的C++ 不够扎实,担心面试官问我C++, 面试前又恶补了一波;\n万万没想到,面试官都是在问我Java,还有相当宽泛的问题,HTTPS是怎么实现的?\n我都不知道这是否是压力面试,我回答什么,对面都不给反馈,就这么听着,让我觉得面试体验非常差,但最终都过了。\n然后就到了HR面,不是说后面还有面试么?为什么要先来HR面?\nHR面通过后,来到GM面,即所谓的总经理面,面试前,被要求用一周时间,针对笔试题,做一个述职PPT,并给了我述职大纲。\n这都是些什么面试要求,还要画PPT?\n只能按照要求,晚上回去埋头写PPT。GM面使用30分钟给GM讲完PPT,回答了几个面试官的问题,然后就结束了;后面就被通知通过了。\n我还以为GM面是走个过场,后来才知道有非常多的面试者GM 面被GM问得体无完肤,因为 GM 想要既会做的,又会说的。\n只说不做的假把式和只做不说的傻把式都不要。\n就这样又到了HR面,怎么要面试两次HR,比阿里的HR面还要多。\n这样就通过了所有轮次的面试,收到了微信支付的Offer。\n面试要求和面试花样比别家多,待遇却不比别家高。\n但最后还是选择了微信支付的Offer, 毕竟这是微信,想去看下。\n就这样,在2020年,我回到了广东,去了深圳。\n6 深圳 之前听人说,深圳是一座只适合的打工的城市。\n来了之后发现,的确如此。\n6.1 微信支付 6.1.1 业务 在微信支付的人才会意识到,微信和微信支付更像是两个截然不同的公司,微信支付与腾讯的财付通关系反而要比微信本身更密切。\n我所在的团队在微信支付做的是委托代扣业务,在微信支付内部,与收银台,付款码并称基础支付,虽然现在已经很少用这个称呼了。\n委托代扣业务常见的业务场景就是免密支付和自动续费:\n如乘坐滴滴或者骑行共享自行车,在行程结束后,商家自动扣款,这就是免密支付;每个月腾讯视频,QQ音乐自动扣月费,那就是自动续费。\n所谓的委托代扣,即是用户委托商户发起扣款,建立委托关系后,商户可以在用户无需输入密码验证身份的情况下,发起扣款。\n所以委托代扣的业务流程分成两步:\n用户和商户建立委托关系,称为「签约」。这是一次性动作,只需要授权一次。 商户请求微信支付,对用户发起「扣款」。 我之前还在腾讯内网写了一篇文章来介绍委托代扣的业务场景,可惜我自己已经看不到了。\n委托代扣每天有海量的交易请求,即使在整个微信支付也是排得上号的(不然怎么会叫基础支付),而微信支付对系统可用性的要求是99.999%, 也就是意味着全年的不可用时长不能超过5分钟。\n在一个海量交易系统,需要实现5个9的可用性,难度可以说是非常高,因此需要做的事情非常多。\n与之前在蚂蚁团队动荡的经历不同,直到我离开微信支付,我都一直在委托代扣团队工作。所以我能从中学习到非常多关于如何构建高可用分布式系统的经验和知识\n6.1.2 加班与学习 无论在哪个大厂,加班也是绕不开的话题。\n微信支付也不例外,微信是有名的卷厂。\n据我观察,广州总部的工作节奏大概是11115,因为他们下班得晚,所以上班得晚,而我所在的微信支付稍好,大概是995, 1095.\n在我的认知中,我是很排斥996这种工作制,而正如前文所说的那样,个人要摆脱996这种工作制,只有合理利用时间,坚持学习。\n而健康的体魄又是实现任何想法的前提,所以身体和头脑,都需要锻炼。\n因为我租住的房子,地铁和公交都不便利,因此乘坐公司的班车上下班就是我的最佳选择。\n虽说腾讯标榜弹性工作制,但是却有很多潜规则。\n例如班车在9点前,把员工送回到公司上班,就是其中一条。\n另外一条就是,不同小区,对应上班的班车只有一趟,下班班车有多趟。因为公司「期望」员工在固定时间前回公司上班,可以加班到不同的时间点下班。\n因为班车要9点到公司,就要求班车必须较早出发,即8:14分出发,因此我每天必须7:50起床赶班车。\n为了早起赶班车,我又必须在晚上23:30前睡觉,不然起不来。\n因此,我每天的时间安排基本被固定下来了,再结合我自己的学习和运动计划,就变成了一个时间表:\n7:50:起床 8:14:乘坐班车 8:14 - 9:00:在车上阅读电子书或听英文Podcast(推荐几个Podcast:个人最爱 Healthy hacker, The Changelog, Let\u0026rsquo;s Master English) 9:00 - 9:15/9:20:早餐 9:30 - 12:00:工作 12:00 - 14:00 午休时间:健身1小时,半小时洗澡+吃午饭 14:00 - 18:00 工作 18:00 - 18:40 晚饭 18:40 - 20:10/40 工作 20:10 - 20:40 下班班车 21:00 - 23:00 学习半小时日语或英语,阅读1小时书或维护开源项目或和妹子聊天或看视频,洗漱 23:30 - 7: 50 睡觉 这样的时间表,从2020到2023,持续了近三年。\n6.1.3 魔幻2022 2022年是魔幻的一年。\n在疫情层面,深圳在农历新年之后,就开始了长达一个月的封城,并拉开了持续一整年的核酸大戏的序幕。\n在公司层面,腾讯从2022年开始,就宣布了降本增效的大政方针,用通俗的话讲,就是裁员降薪。从年初每天刷屏的毕业论文(被裁员同事写的感想),到年中宣布绩效与晋升改革,缩减高绩效名额,增加低绩效名额,晋升与涨薪脱钩,晋升机会从一年两次缩减为一年一次等等。\n在个人层面,2022年是厚积薄发的一年。\n我站在智哥的基础上,花了近4个月,把负债沉重的祖传签约链路给重构了,并梳理清楚了签约链路的业务规则,沉淀成文档。\n花了1年多的时间,从0搭建了代扣的数据仓库。\n花了1个多月时间,从0重新搭建了一套类似委托代扣签约的免密收银台签约链路。\n在腾讯KM平台输出了十多篇文章,有超过5篇入选/获得双月度的腾讯知识奖,1篇获得年度腾讯知识奖,影响力超过了99%的同事。\n一边是个人能力和认知的进,一边是公司待遇和前景的退,还有疫情的前途未卜,难免令人心生迷茫,不知前路在何方。\n6.1.4 骆驼身上的稻草 如果一直给骆驼加稻草,可能会看到骆驼最终倒下,却不知道一把稻草里面,哪根是最后一根让骆驼倒下的稻草。\n同组刚结婚购房的小伙伴,因为降本增效的政策,被毕业了。\n2022年的两次的绩效考核,我的业绩都是 outstanding, 总评都只是 good, 而绩效又直接与收入回报挂钩。\n2022年6月,本来我已经满了晋升高级工程师的停留时限要求,但是公司的一纸改革,直接把这次年中的晋升机会抹掉。\n某天清晨,当我如往常一样准备穿衣上班,却突然发现小区因为疫情被毫无征兆地封控三天。网上的蔬菜食物早被抢购一空,冰箱冷藏层找到的,数周前购买的冰鲜鸡腿,才让我得以饱食。\n封控的第四天凌晨4点,舍友因为肾结石发作,敲响了我的房门,我唯有先向居委会申请通行证,才被允许出门看急诊。直到2个小时之后,我们才走出了小区门。\n如果舍友的病在封控期结束的早一天发作,我都不知道要如何才能出得了这道每天进出的门。\n好朋友4年T10的晋升速度,与我可能6年还停留在T8的差距; 深圳高企的房价以及我增长缓慢的收入。\n微信支付,在各种压力之下,变得越发地像一个工厂,而每个开发者,都只是流水线上的工人。\n或许,我可以尝试去其他国家,去看下那个不一样的世界。\n6.1.5 面试 自从公司明里暗里宣布裁员开始,我就开始重新在 Leetcode 上面刷题,坚持每天一题,我不喜欢被动应对。\n有了尝试去其他国家的想法之后,我就在Linkedin 上面更新了英文简历和自己的简介,然后就有不同的国家的recruiter找上我。\n排掉哪些我不想去的国家(比如坡县),排掉某些我不感兴趣的公司(某跳动,某Tiktok),排掉哪些我不感兴趣的职位,我约了两家来自不同国家的公司面试。\n一个是来自的日本的 paypay, 是日本最大的三方支付公司,模仿的是支付宝。比较吸引我的点是:\n他们的公司75%都是外国人。 允许在日本任何地方远程办公,如果愿意在东京办公,有额外的补贴。我面试时视频见过的3个面试官+ recruiter,就没有一个是在公司环境办公的 较高的薪资,日本的IT公司薪资普遍不高,但paypay 给的薪资,能比得上0.75个日本Google 较新的技术栈,他们用的Java版本是JDK17,存储竟然用的是 TIDB. 因为之前一直在学日语,所以刚开始时,还尝试用日语和这家公司recruiter 打招呼,类似《大家的日语》第一课:\n我: はじめまして、わたしわ梁です\nHR: はじめまして、よろしくお願いします\n我: よろしくお願いします\n当然,后面我就切换回英文了,毕竟我的日语口语还不支持我完成面试。paypay 是一轮笔试加四轮面试\n另外一家就是AWS,base 在Canada,毕竟我没有身份可以去美帝。\nAWS也是一轮笔试加四轮面试,笔试还是很有难度的,一道大概leetcode medium + 一道leetcode hard+ 原题,那道 hard+ 的题,如果不是刷过原题,我是解不出来的。\n因为面试的是SDE2,所以这四轮面试是:\n解算法题 + 2个 LP 问题,算法题判断多叉树是否存在指定路径,如果存在,返回该路径。 解算法题 + 2个 LP 问题,算法题是Top K freqent element 问题 System Design + 2个 LP 问题,设计一个日志系统。 Object-Oriented Designa + 2个 LP 问题, 根据需求,用面向对象设计类,算是算法题与面向对象的结合版本。 所谓的LP 问题,指的是 Leadership Principles, 就是 Amazon 的企业文化里面有16条Leadership Principles, 他们会针对这些准则,让你给合个人经历,讲你自己的故事。\n例如,告诉我一个你没有在deadline 前完全项目的经历?主要是看你如何介绍背景,阐述问题,你的行动,最后的结果。即所谓的STAR: Situation, Task, Action, Result. 通过你的经历和应对,判断你是否是个合格的候选人。\n我花了一个月的时间,写了20多个故事的英文底稿,基本覆盖了这16条principles, 把这些故事双面打印出来,用了大概11页纸。\n最后面试都通过了,我选择了这个温哥华的 Offer。\n6.1.6 离开 在每两周至少至少交付一个需求的前提下,我写的生产代码,没有出过一次故障,我没有写过一次复盘,我写过的最大的bug 就是读写文件时,没有对指针判空,导致文件不存在时,服务coredump。\n曹操在评注《孙子兵法》时,有一句批注,「善战者无赫赫之功」。善于指挥的人,没有跌宕起伏的故事,没有赫赫有名的战功。\n我喜欢四平八稳,而不是狼烟四起,再四处救火,不是「扶大厦于将倾」,方显「英雄本色」。\n我更倾向于设计(尽量)不会倾的大厦,所以就没有什么存在感,故而平平无奇,会被认为,换谁来都可以。\n就这样,到了樱花盛开的季节,也到了离开的季节。\n3月,我离开了微信支付。\n坐上了前往温哥华的班机。\n7 温哥华 对于习惯了只有夏季的广东人来说,温哥华的春天比广东的冬天还冷。\n但新的开始,总是伴随着与众不同。\n那温哥华的冬天是怎么的呢?只能等到冬天来了才知道。\n我的未来会是怎么样的呢?也只有未来来了才知道。\n8 后话 好友总问我,你每天这样的忙碌,还给自己的时间表排得这么满,不觉得累的么?你是怎么坚持的?\n我希望可以追上期望中的自己,每次想到,我这样的坚持可以让我摆脱这样生活,我的动力就涌出来了。\n所谓知人者智,自知者明,我只是个没有天赋,也没有资源的普通人,想要追上期望中的自己,坚持就是我最大的天赋。\n中学时有篇文章是帝师宋濂讲自己早年求学经历,勉励学子马生专心治学的《送东阳马生序》\n余幼时即嗜学。家贫,无从致书以观,每假借于藏书之家,手自笔录,计日以还。\n天大寒,砚冰坚,手指不可屈伸,弗之怠。录毕,走送之,不敢稍逾约。\n以是人多以书假余,余因得遍观群书。\n既加冠,益慕圣贤之道 。\n又患无硕师名人与游,尝趋百里外,从乡之先达执经叩问。\n先达德隆望尊,门人弟子填其室,未尝稍降辞色。\n余立侍左右,援疑质理,俯身倾耳以请;或遇其叱咄,色愈恭,礼愈至,不敢出一言以复;俟其欣悦,则又请焉。\n故余虽愚,卒获有所闻。\n\u0026hellip;\n我自己的经历和成就,自知无法与宋濂先生相比。但十数年后,再读宋先生的《送东阳马生序》,却有了不一样的感悟。\n故余虽愚,卒获有所闻。\n我虽然普通,但是坚持还是有收获了。\n9 延伸阅读 《为什么梦想买不起,故乡回不去》 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E8%BF%99%E4%BA%9B%E5%B9%B4%E8%B5%B0%E8%BF%87%E7%9A%84%E8%B7%AF_%E4%BB%8E%E5%B9%BF%E5%B7%9E%E5%88%B0%E6%B8%A9%E5%93%A5%E5%8D%8E/","summary":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一","title":"这些年走过的路:从广州到温哥华"},{"content":"1 糖葫芦 2 答案 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E7%8F%AD%E8%B4%B9%E4%B8%8E%E5%B9%B4%E7%BB%88%E5%A5%96/","summary":"1 糖葫芦 2 答案","title":"加班费与年终奖"},{"content":"1 前言 因为ChatGPT 的爆红,最近基于在 ChatGPT 的工具如雨后春笋般冒出来,在 Twitter 上,基本每周都可以看到开发者发布基于 ChatGPT 的新应用(这些人不用上班的么?)。\n而使用过好多的 ChatGPT 应用后,最惊艳的是 @yetone 开发的是 openai-translator 这款应用,支持「翻译」,「润色」,「总结」三种功能。\n1.1 开发历程 因为在Twitter 上关注了 @yetone, 所以能从推文看到@yetone 的开发历程:\n最开始 yetone 是为 Bob 开发了基于 ChatGPT-api 的 openai-translator 插件,广受Bob用户的好评。后面yetone 码力全开,又乘胜追击,为 Bob 开发了基于ChatGPT-api 的润色和语法纠错 openai-polisher 插件,完美替代了 Grammarly.\n因为这两个插件的出色表现,很多非Bob 用户和 Mac 用户也希望可以尝鲜,因此 yetone 就徇众要求, 「糊」(yetone的原话)了一个浏览器插件,这就是 openai-translator ,而后就一发不可收拾了。\n因其出色的表现,在Github 和 Hacker News 上爆火了。\n然后 yetone 又将openai-translator 浏览器插件进行打包,做成跨平台的桌面端应用。\n2 效果 测试文章来自Github Blog:Raising the bar for software security: GitHub 2FA begins\n2.1 翻译(translate) 翻译的用户体验,对比我之前一直在使用的沙拉划词(ext-saladict):\n沙拉划词:\nopenai-translator:\n翻译的效果(最核心的功能)\n测试文章片段:\nWe want enrolling your GitHub account in 2FA to be as easy as possible, using methods that are reliable and secure so you always have access to your account (and no one else does!). To prepare for this program we’ve been busy enhancing that experience. Here are a few of the highlights:\n有道翻译与Google翻译:\n我们希望在2FA中注册您的GitHub帐户尽可能简单,使用可靠和安全的方法,以便您始终可以访问您的帐户(没有其他人可以!)。为了准备这个节目,我们一直在忙着提高这种体验。以下是其中的一些亮点\n我们希望使用可靠且安全的方法在2FA中注册您的GitHub帐户尽可能容易,因此您始终可以访问您的帐户(而且没有其他人可以!)。为了准备该计划,我们一直在忙于增强这种体验。以下是一些亮点:\nDeepL翻译:\n我们希望为您的GitHub账户注册2FA时尽可能简单,使用可靠和安全的方法,这样您就可以始终访问您的账户(而没有其他人可以访问!)。为了准备这个项目,我们一直在忙着增强这种体验。以下是其中的几个亮点。\nopenai-translator 翻译:\n我们希望让您的GitHub账户启用双重身份验证变得尽可能简单,使用可靠和安全的方法,以便您始终可以访问自己的账户(而别人则不能!)。为了准备这个计划,我们一直在不断改进用户体验。以下是其中的亮点:\n2.2 润色(polish) 语法纠错及词句润色的效果:\n测试文章片段:\nWe want enrolling your GitHub account in 2FA to be as easy as possible, using methods that are reliable and secure so you always have access to your account (and no one else does!). To prepare for this program we’ve been busy enhancing that experience. Here are a few of the highlights:\n对比我之前一直使用的 Language Tool:\n估值 100 亿刀的 Grammarly:\nDeepL 家新出的基于AI的写作助手DeepL Write:\nopenai-translator:\n因为openai-translator没有给出润色前后的比对,我们可以通过 diff 工具查看下:\n2.3 总结(summarize) 这个应该是openai-translator 特有的功能,可针对长文给出总结与概要:\n原文片段:\nOver the course of the next year, we’ll be reaching out to groups of developers and administrators, starting with smaller groups on March 13, to notify them of their 2FA enrollment requirement. This gradual rollout will let us make sure developers are able to successfully onboard, and make adjustments as needed before we scale to larger groups as the year progresses.\nIf your account is selected for enrollment, you will be notified via email and see a banner on GitHub.com, asking you to enroll. You’ll have 45 days to configure 2FA on your account—before that date nothing will change about using GitHub except for the reminders. We’ll let you know when your enablement deadline is getting close, and once it has passed you will be required to enable 2FA the first time you access GitHub.com. You’ll have the ability to snooze this notification for up to a week, but after that your ability to access your account will be limited. Don’t worry: this snooze period only starts once you’ve signed in after the deadline, so if you’re on vacation or out of office, you’ll still get that one week period to set up 2FA when you’re back at your desk.\nSo, what if you’re not in an early enrollment group but you want to get started? Click here and follow a few easy steps to enroll in 2FA.\nopenai-translator 总结后的片段:\nGitHub will gradually roll out 2FA enrollment requirements to developers and administrators over the next year, starting with smaller groups on March 13.\nThose selected for enrollment will be notified via email and a banner on GitHub.com, with 45 days to configure 2FA before being required to enable it upon accessing the site. A snooze period of up to one week is available after the deadline has passed.\nAnyone can enroll in 2FA by following a few easy steps provided by GitHub.\n3 使用方式 所谓 action is louder than words, 不看广告看疗效。\n评价一个产品好不好用,只有用过才知道。体验openai-translator 最简单快捷的方式就是使用浏览器插件,免于安装桌面应用。\n而你所需要的只是一个chatgpt 账号+ 一个浏览器插件:\n1.注册安装 openai, 此处省去指引,有非常多的教程。\n2.获取 openai api key,并复制此key\n3.打开 Chrome web store, 搜索 OpenAi Translator, 并点击安装\n4.点击搜件,粘贴刚刚复制的api-key:\n5.划词,并点击 openai-translator 图标进行体验。\n4 总结 ChatGPT 向我们展示了 GPT 模型的伟大之处。但模型虽强,阳春白雪,终究是离普通用户太远。\n是无数个像yetone 这样的开发者,用产品展示给用户看,GPT 模型是如何的伟大。\n向 yetone 致敬。\n5 参考 bob-plugin-openai-polisher bob-plugin-openai-translator openai-translator @yetone ","permalink":"https://ramsayleung.github.io/zh/post/2023/openai-translator/","summary":"1 前言 因为ChatGPT 的爆红,最近基于在 ChatGPT 的工具如雨后春笋般冒出来,在 Twitter 上,基本每周都可以看到开发者发布基于 ChatGPT 的新应用(这些人不用上班的么","title":"OpenAI-translator: 基于ChatGPT的划词翻译及润色应用"},{"content":"1 技巧 对于使用org-mode 格式的文本,例如Emacs官方 tree-sitter 的使用教程\n在线阅读不是很易读,相当于人脑解析 org-mode. 我的个人习惯是使用 eww 浏览器来阅读:\n复制网页链接 使用 eww 打开链接 major-mode 切换到 org-mode, 就可以愉快地使用 Emacs 来阅读 org-mode 文本. ","permalink":"https://ramsayleung.github.io/zh/post/2023/emacs%E6%8A%80%E5%B7%A7%E5%88%86%E4%BA%AB_%E4%BD%BF%E7%94%A8eww%E6%89%93%E5%BC%80%E5%9C%A8%E7%BA%BForg-mode%E6%96%87%E6%A1%A3/","summary":"1 技巧 对于使用org-mode 格式的文本,例如Emacs官方 tree-sitter 的使用教程 在线阅读不是很易读,相当于人脑解析 org-mode. 我的个人习惯是使用 eww 浏览器来阅读","title":"Emacs技巧分享: 使用eww打开在线org-mode文档"},{"content":"1 技巧 分享一下平时使用 dired-mode 批量修改文件名的技巧:\nC-x C-f 指定的文件目录,进入 dired-mode C-x C-q dired-toggle-read-only: Edit Dired buffer with Wdired. 批量修改,手段有 使用 query-replace 批量修改文件名 使用evil的多行编辑模式 使用 rectangle-command: C-x r t string-rectangle C-c C-c 提交修改或 C-c C-k 放弃修改 Figure 1: 使用 rectangle-command 进行批量修改\nFigure 2: 使用 evil的多行编辑模式进行批量修改\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%8A%80%E5%B7%A7%E5%88%86%E4%BA%AB_dired%E6%89%B9%E9%87%8F%E4%BF%AE%E6%94%B9%E6%96%87%E4%BB%B6%E5%90%8D/","summary":"1 技巧 分享一下平时使用 dired-mode 批量修改文件名的技巧: C-x C-f 指定的文件目录,进入 dired-mode C-x C-q dired-toggle-read-only: Edit Dired buffer with Wdired. 批量修改,手段有 使用 query-replace 批量修改文件名 使用evil的多","title":"Emacs 技巧分享:dired-mode 批量修改文件名"},{"content":"1 非必要不加班 2 八小时工作制 3 后话 如有雷同,可能在同一家公司打工。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%8A%A0%E7%8F%AD%E7%94%B3%E8%AF%B7/","summary":"1 非必要不加班 2 八小时工作制 3 后话 如有雷同,可能在同一家公司打工。","title":"加班申请"},{"content":"1 弹性打卡 2 鄙人张麻子 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E5%BC%B9%E6%80%A7%E6%89%93%E5%8D%A1/","summary":"1 弹性打卡 2 鄙人张麻子","title":"弹性打卡"},{"content":"1 灵感来源 周末的时候,和好朋友出去玩,聊到了自媒体,就提起我这个新开的公众号。朋友就提了问题,能否写些能让人看得懂的内容。\n因为我之前都写博客为主,一篇长博客几千字,主要涉及计算机和历史相关的内容,涉及到具体问题和算法,就会比较深入。\n毕竟深度和广度无法兼顾,比如我写的《深入浅出Count-Min Sketch算法》可能大部分读者既不关注,也不知道是个什么东西。\n微信公众号这个阅读载体就注定无法阅读知识密度比较高的内容,毕竟拿起手机可能只是想阅读一些下饭的内容, 你写个长篇大论,人家还不一定有意愿看完。\n经典的传媒学著作《娱乐至死》就有这样的观点: 大众化的媒介必定也是娱乐化的\n还有一个原因是,我之前写的博文大多都无法直接发表在公众号上,比如《为什么梦想买不起,故乡回不去》, 我删改了8次才过审。\n还有另外一篇历史著作《天朝的崩溃》的读后思考,发表之后直接被删除。 内容不过从兵力,武器,政治制度,科技层面分析为什么鸦片战争中失败的是清军,而非来袭的英军。\n所谓知人者智,自知者明。搞清楚自己的定位很重要。所以我决定换种方式来阐述自己的想法,通过xkcd 风格的漫画来展现自己的想法。\nxkcd 是国外有名的网络漫画网站,主要与科技相关,风格就是火柴人简笔画,但充满哲理,回味无穷。\n所以我决定东施效颦,也用xkcd 风格的简笔画来表达。\n2 上菜 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%B3%84%E5%AF%86/","summary":"1 灵感来源 周末的时候,和好朋友出去玩,聊到了自媒体,就提起我这个新开的公众号。朋友就提了问题,能否写些能让人看得懂的内容。 因为我之前都写博客","title":"泄密"},{"content":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%; 月活用户4.89亿。Spotify成为第一家订阅用户数突破2亿的音乐流服务。\n而我突然意识到,我那个使用Rust, 为Spotify开发,挂在Sptofiy官网的library:RSpotify,已经维护有五年了:\n一件用爱发电的开源项目要坚持维护五年,也是有很多话可以说的。\n2 起源 还记得大三暑假时,也就是2017年,当时找好了实习,并且拿到了Return Offer. 在拿到Offer之后,实习还没有离职,就想学习一门新的编程语言。\n因为之前用的是都是Java,Python之类的带GC的编程语言,就想学习点硬核,偏底层的编程语言。 本来是想学C++,结果在知乎看了一圈之后,大家都说C++要没落了,推荐学习Rust。(然后我现在靠写C++混饭吃)\n搜索了Rust的信息,发现它性能媲美C/C++, 又不需要手动管理内存,还连续几年荣膺Stackoverflow的 Most Loved Programming Language, 就它了。就开始了一边实习一边摸鱼学习了Rust的旅程。\n因为大学前三年把学分都已经修满了,所以大四一整个学年都不需要上课了,就有时间折腾。\n在学了2-3个月之后,就想拿Rust来写些项目。 但因为我只会写Web应用,又没有想到能写什么,到时就用Rust写了个博客,并将博客从原来的Github Pages迁移到自建的博客上。\n很臭屁地在 V2ex 和 Reddit 分享用Rust重写博客的经历,V2ex 一群人问我为什么不用PHP/xxx语言写,Reddit社区就友好很多。 (然后过了5年之后,服务器欠费,又把自建博客迁移回Github Pages。当然,那是后话了。)\n在花了2-3个月写完博客之后,觉得自己入门Rust,就想写个开源项目,感受下与其他开发者协作的场景。\n当时看到个网易云音乐命令行版本的播放器 musicbox, 当时我在用的是Spotify,就希望可以为Spotify写个类似的播放器。\n虽说Spotify API是对外开放,但直接使用HttpClient来请求HTTP API有点太祼,所以就希望使用先封装个library,方便后续的Rust应用直接调用,就不需要自己操心Http请求了。\n这就是RSpotify这个库的来源。\n这次,我就只在 Reddit和博客 上分享使用Rust来写library 的经历了。\n3 演进 3.1 野蛮生长阶段 刚开始写RSpotify的时候,对于如何设计一个易用,友好的library 完全没有头绪,毕竟设计好用的类库需要相当的经验沉淀。\n对于没有设计思路的我而言,当时能想来的解决方案是去Spotify官方列出来的library看下,哪个语言的library看得懂,star又多,就把这个library 翻译到Rust上。\n就把目光瞄准到Python版本的 spotipy 上。\n在2018-01-08 提交了第一个commit, 经过一个多月的日夜施工,终于在2018-02-18 完成了所有的API接口开发,发布了 0.1版本\n虽然这是我这个学生写的第一个Rust库,但是开源项目需要的标准配置,我还是都加上了:\n自动化流水线,Travis(当时Github Action还没有出现) 齐全的文档说明 完整的单元测试用例 使用示例 README说明与License 为了吸引其他开发者来协作开发,所有的资料都是英文的。\n不过从Rust 包托管网站 crates.io 的数据可以看到,0.1版本只有300+的下载量,几乎没有什么人在用。\n3.2 async 阶段 时间来到2019年,对于Rust社区来说,最激动人心的应该是Rust 1.39版本,将正式包含 async/await 特性,自那天起,Rust正式支持异步编程。\n自此之后,Rust社区在做的事情,就是把已有Rust代码疯狂升级到async await,RSpotify虽迟,但也赶上了这波潮流。\n当时RSpotify 请求Spotify的API使用的HTTP库是 reqwest,在 reqwest 支持异步模式之后,开发者 Alexander就提了一个超大的PR,把所有已有的api全部修改成async, 我就乐见其成,就把这个PR合并了。\n有社区的同学抱怨说异步模式的代码不好使用,他对性能没有什么要求,能否保留同步模式的接口调用。\n后来为了兼顾同步模式和异步模式这两种调用方式,Alexander 又提了一个超大超大的PR,把现有的异步模式代码复制一份,然后把async 关键字去掉。\n从此以后,RSpotify就需要同时维护两份几乎相同的代码,每次新增,修改,删除都需要确保同时变更两份代码。 着实痛苦不堪,但我也没有思考出更优解。\n这时候,后来和我共同维护RSpotify 的开发者 Mario 出现了。\n3.3 maybe_async 阶段 当时RSpotify最大的问题在于有两份几乎一样,但是使用同步调用和异步调用模式的代码。\n而异步调用的代码,返回参数都是一个 Future\u0026lt;T\u0026gt; ,将真正的响应结果封装在一个 Future 结构里面。\n所以当时Mario 提出的第一个解决思路,是将对异步代码进行封装,使用同步调用的runtime调用异步函数,然后再把响应结果返回回去:\n// 异步代码 async fn original() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { reqwest::get(\u0026#34;https://www.rust-lang.org\u0026#34;) .await? .text() .await } lazy_static! { // Mutex to have mutable access and Arc so that it\u0026#39;s thread-safe. static ref RT: Arc\u0026lt;Mutex\u0026lt;runtime::Runtime\u0026gt;\u0026gt; = Arc::new(Mutex::new(runtime::Builder::new() .basic_scheduler() .enable_all() .build() .unwrap())); } // 同步版本代码 fn with_block_on() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { RT.lock().unwrap().block_on(async move { original().await }) } 再通过Rust macro来为每个async 函数生成一个block_on 版本的函数。\n但实际上,发现编写macro太复杂,并且这种方案不够灵活,实现起来也相当复杂。\n然后Mario 又调研出一种新方案,通过 maybe_async 这个库在同步和异步模式之间切换。 默认是异步模式,但可以通过 features = [\u0026quot;is_sync\u0026quot;] 编译选项来切换到同步模式,maybe_async 就会把所有的async/await 关键字给去掉。\n这个方案简单,可读性高,易于扩展,也不需要维护复杂的 macro 代码。\n这也是我们最终采取的方案,重构之后,把 blocking 目录的近万行复制粘贴而来的代码删除掉,就非常爽。\n3.4 二次重构 阶段 前面提到,RSpotify最开始是直接翻译spotipy的代码。 因为Python是弱类型,而Rust是强类型,直接翻译,难免会有不少代码,写法没有纯正的Rust味道。\n社区的Kestrer同学就在一个issue里面,给RSpotify提了近90条优化建议,指出了RSpotify中设计的各种问题,包括强类型运用不当,使用过多的原始类型,函数出入参设计不够优雅,授权流程设计不够易用等等。\n这么多的优化建议,可以看出Kestrer真的花了很多时间来阅读和改善RSpotify的代码,盛情难却(可见原来的代码是多烂, 有非常多激发社区同学参与改进的空间).\n别人指出问题,就要好好优化。\n所以我和Mario就分别对每个接口返回的数据模型,数据模型与Json间转换的序列化方式,授权流程作了改进。 并对RSpotify这个library作了拆分,按照功能,拆分成 model, http, macros 三个单独的library。\n期间提了大概20多个PR,花了超过一年的时间,才处理完Kestrer 提的所有建议。\n3.5 pre-release 阶级 在开源社区里面,有一个约定俗成的规范: 当一个library 发布1.0 之后,就代表这个库已经处于稳定状态,不会再出现大量breaking change 的情况了。(py2, py3不在此约束内)\n而经过4年的开发,RSpotify 已经步入一个相对稳定的开发状态,没有太多的breaking change 或重构了,开始为发布正式的1.0release 版本作准备。\n当功能与架构相对稳定后,近一年时间,我和Mario就开始优化RSpotify的易用性,比如\n添加更多,针对不同场景的 examples; 尽可能地去掉 unsafe 代码; 为返回列表的API提供同步及异步版本的Iterator支持; 保持向前兼容的情况下,尽量使已有接口更加Rust化; 添加更多的自动化检查,如检查代码中文档的链接是否404; 性能优化,减少不必要的内存分配 目前版本已经去到了 0.11.6, 功能也相对稳定, 预计不久后就会正式发布1.0版本。\n4 感悟 4.1 开源协作 截至到2023-02-09,RSpotify一共有1673次commit, 但我和Mario都只贡献了1/3的commit,剩下的commit都是社区的其他开发者提交的。\n从RSpotify的演进历程也可以看出,我只是从0开发了最初版本的RSpotify,后面都是随着Rust的演进,有不同的开发者帮忙优化与迭代,我做的事情就从单纯的creator, developer 变成maintainer, reviewer,负责review其他开发者的PR。\n可以说,如果没有其他开发者的贡献与协作,RSpotify不会演进成现在的样子。\n如何吸引更多的开发者加入,让他们乐于为项目作贡献,我个人的见解是:\n所有文档,注释,commit message, issue, CHANGELOG等材料,都只使用英文。 标准的开源协作流程; issue, PR, CHANGELOG 都提供标准模板 要添加新特性,修改已有功能的时候,新建issue讨论动机与可行性 每个PR都需要一个Peer Reviewer review后才能合并 每次发新版本,都需要在 CHANGELOG 注明大的特性变更,以及breaking change 文档,示例,开发指引,测试case完备,降低新开发者参与的成本。 be nice,态度友好,针对issue,PR都尽量回复,理性,友善讨论。 开源协作的一个感受就是,在Github讨论问题的时候,可能突然有位大佬也加入群聊。\n比如和Mario讨论, 增加更多更严格cargo clippy 的rule,以便让编译器帮我们发现更多潜在问题时,cargo clippy 的maintainer 也加入讨论,就什么rule 更合适,给出自己的建议。\n4.2 收获 我用C++已经混了三年的饭吃了,但还只能看到C++的门槛,没法说入了C++的门。\n同理,虽然距离我学习Rust已经过去6年了,我依然感觉我还不会Rust,都是编译器教我写代码。\n在Review别人代码的过程中,我也学习到非常多「地道」和高级的Rust用法,项目维护的经验.\n想到的点:\n使用Rust的macro来减少copy-paste的代码(但复杂的 macro,基本不具备可读性。) 使用serde 自定义序列化函数; 以workspace 模式管理多个crates; 编写 async/await 的异步代码; 使用标准库的Trait, 风格契合标准库; 结合thiserror 和anyhow 处理异常; 通过自动化和模式化,减少项目维护的成本(能用机器做的,就不要用人做)。 规范的开发流程,包括commit message, issue, PR, CHANGELOG, release note 等等 期间把收获与心得写了两篇文章:\nThe lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable 4.3 关于开源 维护这个项目5年之后,对于「开源」有了些不一样的理解。\n在1970 年代,Richard Stallman发起自由软件运动,旨在推广用户有使用,复制,研究,修改和分发软件的社会运动。 自由软件运动人士认为自由软件的精神应该贯彻到所有软件。\n在90年代,又兴起了开源软件运动,则计算机软件的源代码是可以公开,随意获取的。 (自由软件与开源软件不是同一个概念,自由软件定义更为严格)\n在那个崇尚黑客精神的年代,开源是「目的」,是为了贯彻自由的精神。\n以前听到某某公司内部有好用的工具,组件,框架时,总会问一句,为什么他们不像Google一样把它们开源出来。\n现在的想法可能是,为什么要开源出来,价值和收益是什么?\n开源一个项目的目的可能是:\n我做了个很有用,很有趣的东西,就想分享出来。但是我个人人力有限,大家一起来帮忙做大做好。(Linux, Ruby On Rails等) 我们做了个好东西,我们要抢占市场。我们就开源,搞人海战术,让竞品淹没在人民群众的汪洋大海中,让我们的东西成为事实的标准。(Android,Chromium, Kubernetes, Vscode) 就想开源让你们见识下大佬是怎么样子的。 个人理解,开源是「手段」,而非「目的」\n对于商业公司而言,如果没有收益,为什么要把花钱雇的人写的内部组件开源出来呢?总不成是为了在B站上博取小朋友的称赞吧。\n而公司内部的组件,往往是与业务共生,高度适配的,藕断丝连,没有那么容易开源的。\n商业公司,只谈收益与预期,如果名声能卖钱,估计也会拿来换取利润。\n更重要的是,开源并不是简单把代码公开出来。\n软件和生物一样,是有生命的,需要长期维护的,而不是一个commit把所有代码一把push 到Github就完事了,或者然后过了几年又push一把,更新几百个文件。\n开源是一个技术与管理结合的决定,需要把开发模式都切换到开源社区,决策过程与动机要对社区可见。\n不仅让人能从代码中读懂功能是「什么」,也要从动机讨论中知道「为什么」要这么改。\n4.4 些许成果 在Github上,收获了495个star,被1108个仓库及18个package 所依赖,而其中Alexander的 spotify-tui 就是我期望做的终端版本的Spotify。\n开源的好处就是,在开发好基础设施之后,自然就会有其他有相同想法的同学,把应用开发出来。\ncrates.io 的统计,总计被下载23w次,当然包括很多CI的重复下载。\n对于有Rust,Spotify,Library等诸多定语的RSpotify来说,目标受众本来就不多,能有现在这样的用户量已远超我最初了预期了。\n5 总结 虽然我未曾从这个项目上获得到一分物质上的回报,但在创建这个项目的时候,我可能不会想到,我能维护它长达五年。\n天上的云,飘来又飘走;开源的项目,挖坑又弃坑。\n视线望不到下一个五年,唯有且行且看。\n6 参考 Spotify is first music streaming service to surpass 200M paid subscribers RSpotify The lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable spotify-tui ","permalink":"https://ramsayleung.github.io/zh/post/2023/rspotify_%E4%B8%80%E4%B8%AA%E7%94%A8%E7%88%B1%E5%8F%91%E7%94%B5%E4%BA%94%E5%B9%B4%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","summary":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%","title":"RSpotify: 一个用爱发电五年的开源项目"},{"content":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%; 月活用户4.89亿。Spotify成为第一家订阅用户数突破2亿的音乐流服务。\n而我突然意识到,我那个使用Rust, 为Spotify开发,挂在Sptofiy官网的library:RSpotify,已经维护有五年了:\n一件用爱发电的开源项目要坚持维护五年,也是有很多话可以说的。\n2 起源 还记得大三暑假时,也就是2017年,当时找好了实习,并且拿到了Return Offer. 在拿到Offer之后,实习还没有离职,就想学习一门新的编程语言。\n因为之前用的是都是Java,Python之类的带GC的编程语言,就想学习点硬核,偏底层的编程语言。 本来是想学C++,结果在知乎看了一圈之后,大家都说C++要没落了,推荐学习Rust。(然后我现在靠写C++混饭吃)\n搜索了Rust的信息,发现它性能媲美C/C++, 又不需要手动管理内存,还连续几年荣膺Stackoverflow的 Most Loved Programming Language, 就它了。就开始了一边实习一边摸鱼学习了Rust的旅程。\n因为大学前三年把学分都已经修满了,所以大四一整个学年都不需要上课了,就有时间折腾。\n在学了2-3个月之后,就想拿Rust来写些项目。 但因为我只会写Web应用,又没有想到能写什么,到时就用Rust写了个博客,并将博客从原来的Github Pages迁移到自建的博客上。\n很臭屁地在 V2ex 和 Reddit 分享用Rust重写博客的经历,V2ex 一群人问我为什么不用PHP/xxx语言写,Reddit社区就友好很多。 (然后过了5年之后,服务器欠费,又把自建博客迁移回Github Pages。当然,那是后话了。)\n在花了2-3个月写完博客之后,觉得自己入门Rust,就想写个开源项目,感受下与其他开发者协作的场景。\n当时看到个网易云音乐命令行版本的播放器 musicbox, 当时我在用的是Spotify,就希望可以为Spotify写个类似的播放器。\n虽说Spotify API是对外开放,但直接使用HttpClient来请求HTTP API有点太祼,所以就希望使用先封装个library,方便后续的Rust应用直接调用,就不需要自己操心Http请求了。\n这就是RSpotify这个库的来源。\n这次,我就只在 Reddit和博客 上分享使用Rust来写library 的经历了。\n3 演进 3.1 野蛮生长阶段 刚开始写RSpotify的时候,对于如何设计一个易用,友好的library 完全没有头绪,毕竟设计好用的类库需要相当的经验沉淀。\n对于没有设计思路的我而言,当时能想来的解决方案是去Spotify官方列出来的library看下,哪个语言的library看得懂,star又多,就把这个library 翻译到Rust上。\n就把目光瞄准到Python版本的 spotipy 上。\n在2018-01-08 提交了第一个commit, 经过一个多月的日夜施工,终于在2018-02-18 完成了所有的API接口开发,发布了 0.1版本\n虽然这是我这个学生写的第一个Rust库,但是开源项目需要的标准配置,我还是都加上了:\n自动化流水线,Travis(当时Github Action还没有出现) 齐全的文档说明 完整的单元测试用例 使用示例 README说明与License 为了吸引其他开发者来协作开发,所有的资料都是英文的。\n不过从Rust 包托管网站 crates.io 的数据可以看到,0.1版本只有300+的下载量,几乎没有什么人在用。\n3.2 async 阶段 时间来到2019年,对于Rust社区来说,最激动人心的应该是Rust 1.39版本,将正式包含 async/await 特性,自那天起,Rust正式支持异步编程。\n自此之后,Rust社区在做的事情,就是把已有Rust代码疯狂升级到async await,RSpotify虽迟,但也赶上了这波潮流。\n当时RSpotify 请求Spotify的API使用的HTTP库是 reqwest,在 reqwest 支持异步模式之后,开发者 Alexander就提了一个超大的PR,把所有已有的api全部修改成async, 我就乐见其成,就把这个PR合并了。\n有社区的同学抱怨说异步模式的代码不好使用,他对性能没有什么要求,能否保留同步模式的接口调用。\n后来为了兼顾同步模式和异步模式这两种调用方式,Alexander 又提了一个超大超大的PR,把现有的异步模式代码复制一份,然后把async 关键字去掉。\n从此以后,RSpotify就需要同时维护两份几乎相同的代码,每次新增,修改,删除都需要确保同时变更两份代码。 着实痛苦不堪,但我也没有思考出更优解。\n这时候,后来和我共同维护RSpotify 的开发者 Mario 出现了。\n3.3 maybe_async 阶段 当时RSpotify最大的问题在于有两份几乎一样,但是使用同步调用和异步调用模式的代码。\n而异步调用的代码,返回参数都是一个 Future\u0026lt;T\u0026gt; ,将真正的响应结果封装在一个 Future 结构里面。\n所以当时Mario 提出的第一个解决思路,是将对异步代码进行封装,使用同步调用的runtime调用异步函数,然后再把响应结果返回回去:\n// 异步代码 async fn original() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { reqwest::get(\u0026#34;https://www.rust-lang.org\u0026#34;) .await? .text() .await } lazy_static! { // Mutex to have mutable access and Arc so that it\u0026#39;s thread-safe. static ref RT: Arc\u0026lt;Mutex\u0026lt;runtime::Runtime\u0026gt;\u0026gt; = Arc::new(Mutex::new(runtime::Builder::new() .basic_scheduler() .enable_all() .build() .unwrap())); } // 同步版本代码 fn with_block_on() -\u0026gt; Result\u0026lt;String, reqwest::Error\u0026gt; { RT.lock().unwrap().block_on(async move { original().await }) } 再通过Rust macro来为每个async 函数生成一个block_on 版本的函数。\n但实际上,发现编写macro太复杂,并且这种方案不够灵活,实现起来也相当复杂。\n然后Mario 又调研出一种新方案,通过 maybe_async 这个库在同步和异步模式之间切换。 默认是异步模式,但可以通过 features = [\u0026quot;is_sync\u0026quot;] 编译选项来切换到同步模式,maybe_async 就会把所有的async/await 关键字给去掉。\n这个方案简单,可读性高,易于扩展,也不需要维护复杂的 macro 代码。\n这也是我们最终采取的方案,重构之后,把 blocking 目录的近万行复制粘贴而来的代码删除掉,就非常爽。\n3.4 二次重构 阶段 前面提到,RSpotify最开始是直接翻译spotipy的代码。 因为Python是弱类型,而Rust是强类型,直接翻译,难免会有不少代码,写法没有纯正的Rust味道。\n社区的Kestrer同学就在一个issue里面,给RSpotify提了近90条优化建议,指出了RSpotify中设计的各种问题,包括强类型运用不当,使用过多的原始类型,函数出入参设计不够优雅,授权流程设计不够易用等等。\n这么多的优化建议,可以看出Kestrer真的花了很多时间来阅读和改善RSpotify的代码,盛情难却(可见原来的代码是多烂, 有非常多激发社区同学参与改进的空间).\n别人指出问题,就要好好优化。\n所以我和Mario就分别对每个接口返回的数据模型,数据模型与Json间转换的序列化方式,授权流程作了改进。 并对RSpotify这个library作了拆分,按照功能,拆分成 model, http, macros 三个单独的library。\n期间提了大概20多个PR,花了超过一年的时间,才处理完Kestrer 提的所有建议。\n3.5 pre-release 阶级 在开源社区里面,有一个约定俗成的规范: 当一个library 发布1.0 之后,就代表这个库已经处于稳定状态,不会再出现大量breaking change 的情况了。(py2, py3不在此约束内)\n而经过4年的开发,RSpotify 已经步入一个相对稳定的开发状态,没有太多的breaking change 或重构了,开始为发布正式的1.0release 版本作准备。\n当功能与架构相对稳定后,近一年时间,我和Mario就开始优化RSpotify的易用性,比如\n添加更多,针对不同场景的 examples; 尽可能地去掉 unsafe 代码; 为返回列表的API提供同步及异步版本的Iterator支持; 保持向前兼容的情况下,尽量使已有接口更加Rust化; 添加更多的自动化检查,如检查代码中文档的链接是否404; 性能优化,减少不必要的内存分配 目前版本已经去到了 0.11.6, 功能也相对稳定, 预计不久后就会正式发布1.0版本。\n4 感悟 4.1 开源协作 截至到2023-02-09,RSpotify一共有1673次commit, 但我和Mario都只贡献了1/3的commit,剩下的commit都是社区的其他开发者提交的。\n从RSpotify的演进历程也可以看出,我只是从0开发了最初版本的RSpotify,后面都是随着Rust的演进,有不同的开发者帮忙优化与迭代,我做的事情就从单纯的creator, developer 变成maintainer, reviewer,负责review其他开发者的PR。\n可以说,如果没有其他开发者的贡献与协作,RSpotify不会演进成现在的样子。\n如何吸引更多的开发者加入,让他们乐于为项目作贡献,我个人的见解是:\n所有文档,注释,commit message, issue, CHANGELOG等材料,都只使用英文。 标准的开源协作流程; issue, PR, CHANGELOG 都提供标准模板 要添加新特性,修改已有功能的时候,新建issue讨论动机与可行性 每个PR都需要一个Peer Reviewer review后才能合并 每次发新版本,都需要在 CHANGELOG 注明大的特性变更,以及breaking change 文档,示例,开发指引,测试case完备,降低新开发者参与的成本。 be nice,态度友好,针对issue,PR都尽量回复,理性,友善讨论。 开源协作的一个感受就是,在Github讨论问题的时候,可能突然有位大佬也加入群聊。\n比如和Mario讨论, 增加更多更严格cargo clippy 的rule,以便让编译器帮我们发现更多潜在问题时,cargo clippy 的maintainer 也加入讨论,就什么rule 更合适,给出自己的建议。\n4.2 收获 我用C++已经混了三年的饭吃了,但还只能看到C++的门槛,没法说入了C++的门。\n同理,虽然距离我学习Rust已经过去6年了,我依然感觉我还不会Rust,都是编译器教我写代码。\n在Review别人代码的过程中,我也学习到非常多「地道」和高级的Rust用法,项目维护的经验.\n想到的点:\n使用Rust的macro来减少copy-paste的代码(但复杂的 macro,基本不具备可读性。) 使用serde 自定义序列化函数; 以workspace 模式管理多个crates; 编写 async/await 的异步代码; 使用标准库的Trait, 风格契合标准库; 结合thiserror 和anyhow 处理异常; 通过自动化和模式化,减少项目维护的成本(能用机器做的,就不要用人做)。 规范的开发流程,包括commit message, issue, PR, CHANGELOG, release note 等等 期间把收获与心得写了两篇文章:\nThe lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable 4.3 关于开源 维护这个项目5年之后,对于「开源」有了些不一样的理解。\n在1970 年代,Richard Stallman发起自由软件运动,旨在推广用户有使用,复制,研究,修改和分发软件的社会运动。 自由软件运动人士认为自由软件的精神应该贯彻到所有软件。\n在90年代,又兴起了开源软件运动,则计算机软件的源代码是可以公开,随意获取的。 (自由软件与开源软件不是同一个概念,自由软件定义更为严格)\n在那个崇尚黑客精神的年代,开源是「目的」,是为了贯彻自由的精神。\n以前听到某某公司内部有好用的工具,组件,框架时,总会问一句,为什么他们不像Google一样把它们开源出来。\n现在的想法可能是,为什么要开源出来,价值和收益是什么?\n开源一个项目的目的可能是:\n我做了个很有用,很有趣的东西,就想分享出来。但是我个人人力有限,大家一起来帮忙做大做好。(Linux, Ruby On Rails等) 我们做了个好东西,我们要抢占市场。我们就开源,搞人海战术,让竞品淹没在人民群众的汪洋大海中,让我们的东西成为事实的标准。(Android,Chromium, Kubernetes, Vscode) 就想开源让你们见识下大佬是怎么样子的。 个人理解,开源是「手段」,而非「目的」\n对于商业公司而言,如果没有收益,为什么要把花钱雇的人写的内部组件开源出来呢?总不成是为了在B站上博取小朋友的称赞吧。\n而公司内部的组件,往往是与业务共生,高度适配的,藕断丝连,没有那么容易开源的。\n商业公司,只谈收益与预期,如果名声能卖钱,估计也会拿来换取利润。\n更重要的是,开源并不是简单把代码公开出来。\n软件和生物一样,是有生命的,需要长期维护的,而不是一个commit把所有代码一把push 到Github就完事了,或者然后过了几年又push一把,更新几百个文件。\n开源是一个技术与管理结合的决定,需要把开发模式都切换到开源社区,决策过程与动机要对社区可见。\n不仅让人能从代码中读懂功能是「什么」,也要从动机讨论中知道「为什么」要这么改。\n4.4 些许成果 在Github上,收获了495个star,被1108个仓库及18个package 所依赖,而其中Alexander的 spotify-tui 就是我期望做的终端版本的Spotify。\n开源的好处就是,在开发好基础设施之后,自然就会有其他有相同想法的同学,把应用开发出来。\ncrates.io 的统计,总计被下载23w次,当然包括很多CI的重复下载。\n对于有Rust,Spotify,Library等诸多定语的RSpotify来说,目标受众本来就不多,能有现在这样的用户量已远超我最初了预期了。\n5 总结 虽然我未曾从这个项目上获得到一分物质上的回报,但在创建这个项目的时候,我可能不会想到,我能维护它长达五年。\n天上的云,飘来又飘走;开源的项目,挖坑又弃坑。\n视线望不到下一个五年,唯有且行且看。\n6 参考 Spotify is first music streaming service to surpass 200M paid subscribers RSpotify The lesson learned from refactoring rspotify Let\u0026rsquo;s make everything iterable spotify-tui ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E4%B8%80%E4%B8%AA%E7%94%A8%E7%88%B1%E5%8F%91%E7%94%B5%E4%BA%94%E5%B9%B4%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","summary":"1 前言 一周前看到个新闻,Spotify在其第四季度财报中披露,截至2022年12月31日,它的付费订阅用户数达到了2.05亿,同比增长14%","title":"一个用爱发电五年的开源项目"},{"content":"1 前言 在公众号开通10天之际,我已经在公众号写了10来篇文章了,也基本熟悉了公众号的写作,发文以及与用户的互动流程。\n对比个人博客,公众号多了很多额外的限制和规则。 所谓人在屋檐下,不得不低头,选择在公众号这个平台创作,自然只能接受各种合理和不合理的规则。\n在了解清楚规则是「什么」之后,我开始思考「为什么」会有这些规则?许多问题的本质,就藏在诸多的「为什么」后面。\n所以就来分享下我个人对公众号产品规则的思考与见解,仅为一家之言。\n2 审核 我的文章《为什么梦想买不想,故乡回不去》大概有7到8次发布失败,原因都是「审核不通过」。 虽说「自由」是创作的基础,但是既然选择公众号,只能接受就被审核的命运。\n但这里让我抓狂的是,公众号不会像个编辑一样告诉我哪里审核不通过,什么地方需要修改, 只有一句含糊的「此内容因违规无法查看,请前往草稿箱修改后重试」:\n我只能重读文章,把那些我认为「不利于团结」的话逐条删除,然后重新发布,审核不通过,如此循环。\n思考下来,公众号平台不告诉作者哪里审核不通过的原因可能是:\n让作者自我审查,所谓「刑不可知 则威不可测」: 如果告知作者哪里有问题,也就是让作者知道高压线在哪里,作者就会肆无忌惮地在高压线下起舞。 哪天「上面」觉得作者舞姿过于妖娆,那么公众号平台就会一起受罚。\n所以就只告诉作者有高压线,但是这条高压线却是移动且隐形的,只有被电到才知道线在何方,让作者心中时刻紧绷着一根弦。\n指明修改点还可能会让「不利于团结」的内容扩散。 假如一个电影解说公众号,写了一篇解读诺兰蝙蝠侠的文章审核不通过,作者原来以为只是血腥场面审核不通过,没想到公众号却标明审核不通过的是「小丑」。\n作者只会好奇,为什么「小丑」一词会被敏感,就会了解原因。 根据传播学原理,只会导致原来需要被和谐的和「小丑」相关的内容以另类的形式传播起来。\n3 图片素材 把在外部写好的文章复制到公众号编辑器的时候,所有的图片都要上传到公众号的图片素材库上,无法直接使用图片的链接。\n很多程序员(比如我)喜欢把博客搭建在Github Pages上,然后把图片都上传到Github 仓库,然后把博客内容复制到公众号时,就会出现图片获取失败的问题。\n原因无非是伟大的防火墙把Github给墙了,导致微信的服务器无法拉取到Github 的图片。\n问题就来了,为什么公众号要大费周章把图片拉取并保存起来,直接使用图片链接来访问不好么?\n个人理解,主要是出于「用户体验」与「安全」两方面考虑:\n如果让用户通过图片链接直接访问外部的图片服务器,图片链接可能失效,也就是图片无法再访问,就出现「图裂」了的情况,极大地影响用户体验。\n也有可能是,图片服务器能访问,但是访问速度非常慢,用户加载图片速度非常慢,用户只会抱怨微信的公众号怎么这么卡,图片都加载不出来。 用户可不管是外部服务器,还是公众号服务器,反正都是微信卡,骂人只会挑最显眼的来骂。\n另外一种可能是图片链接原来是一张正常的图片,待文章阅读量到10W+之后,就换成一张敏感图片,那么就会防不胜防。\n从系统安全的角度来说,到外部服务器拉取文件,保存并展现给用户,是一个危险的操作,这样的操作应该越少越好,微信这样的APP注定会是各种攻击者的目标。\n所以一次性,把图片拉取到公众号内部的服务器,就是最稳妥的方法\n4 限制外链 在公众号文章里面,是无法插入外部网站链接的,只能引用公众号文章,或者公众号文章链接。\n让我这种习惯在文章末尾写上一大堆引用和参考链接的人无所适从,毕竟不注明引用,就有剽窃之嫌。\n这样的做法,背后的暖心原因大概是:\n避免为外部网站和App引流,避免把用户从公众号的生态,引流到抖音号,小红书号上,避免从平台沦为引流工具,「肉要都烂在锅里」,内容只能在微信生态内流传 安全因素:避免公众号作者把各种诈骗,钓鱼链接放到公众号里面,让用户在公众号上受害,避免成为黑产的温床,保证微信的口碑。 毕竟用户受骗骂人的时候,才不会骂公众号作者,只会骂最显眼的那个,就是公众号平台,乃至微信。 话虽如此,但无法引用外部链接的做法,让我这种崇尚开放Web精神的人,相当难受。\n5 群发次数 对文章,公众号有所谓的「群发」和「发布」之分。\n「群发」是指把文章推送给所有的关注者,并且会出现在作者公众号的主页。而「发布」只是将文章发布出来,不会推送,也不会出现在主页,但是可以被链接到。\n「群发」是限制次数的,一天只能「群发」一次,但是「发布」却是无限制的。\n「群发」的次数限制成一次,我理解是保证用户体验避免过多的公众号消息推送,给用户造成困扰,让公众号成为商户引流推广的工具。毕竟根据墨菲定律,可能会被滥用的规则,就一定要被滥用。\n另外一个是对作者的潜在约束,让作者好好珍惜「群发」的机会,尽量写出好文章。按照《影响力》里提到的「稀缺」原理,机会越少见, 价值似乎越高。\n同样的原理,还运用到「账号详情」信息的修改,限制修改次数,就能让用户审慎修改,审慎着,审慎着,可能就不会修改了。\n对于系统而言,修改1次与修改100次成本几乎无差别,只是通过产品规则,来作限定,人为制造「稀缺」。\n6 限制修改 公众号文章一经「发布」或「群发」,就无法大幅修改内容,只能更正最多20个错别字,这个规则让许多作者大为诟病,认为限制了其修改文章的权利。\n那么为什么会有这条规则呢?\n我个人揣摩,认为是公众号产品认为文章因为是类似报纸书刊等出版物,落笔无悔,一旦写出去的文章,就无法修改(错别字除外)。\n因为公众号是和微信这个聊天工具紧密结合在一起的,如果分享,转发的过程中,文章的内容发生多次变更,就会出现「罗生门」的情况,就是没有人知道文章内容最初的观点究竟是什么。\n那些事后诸葛亮就会跑出来,说自己是事前诸葛亮。 比如那些鼓吹俄罗斯2小时攻陷基辅的军事爱好者们,虽然一年过去了,但是在他们看来,还没有到2小时。\n另外一个就是对「作者」的约束,当你知道你写出去的东西无法修改,你应付审慎对待,文理顺畅,前后呼应的情况才能发布,迫使作者尽量产出高质量的文章。\n7 总结 电视剧《雍正王朝》中,为描绘年羹尧身为大将军,生活奢华,介绍其有一道菜,名为小炒肉。\n虽为小炒肉,但这道菜做法十分复杂,就是在取肉之前,要找数名壮汉对肥猪展开暴力围殴。\n棍棒之下,猪吃痛嚎叫,血管也因此爆裂,猪血会聚集至脊梁附近,由此,厨师才会下刀,将猪杀死后去其尾骨附近的精肉。 据称,此法制作出来的猪肉,因为有鲜血的浸透,吃起来会极为鲜嫩可口,味道不逊色于各类山珍海味。\n所谓的「产品规则」,背后都是「人性」,都是对作者与读者心理的揣摩和把玩。 公众号对作者施加诸多的限制与规则,无非是像壮汉殴猪,希望最终可以取出精肉,以飨读者。\n难怪文章不好看,因为作者是头猪。难怪文章好看,因为作者是头挨过打的猪。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%80%9D%E8%80%83_%E5%85%AC%E4%BC%97%E5%8F%B7%E8%83%8C%E5%90%8E%E7%9A%84%E4%BA%A7%E5%93%81%E9%80%BB%E8%BE%91/","summary":"1 前言 在公众号开通10天之际,我已经在公众号写了10来篇文章了,也基本熟悉了公众号的写作,发文以及与用户的互动流程。 对比个人博客,公众号多了","title":"思考:公众号背后的产品逻辑"},{"content":"1 前言 古人云:「一图胜千言」。 一幅合适的图片可以清晰地向读者表达我们的意图,又因为我们人脑的作用机制,阅读一张图片所耗费的脑力要远少于一段文字,故而我们对图片更加深刻。\n古人又云,「工欲善其事,必先利其器」,那么我就来分享一下我使用得顺手的画图工具与画图技巧。\n2 excalidraw excalidraw 是我最常用的画图工具,是一款开源的手绘画风的画板工具,图形风格是简洁而精美,一经使用,便爱不释手。\n非常适合构建原型或阐述想法\n我见证它在Github上的star数从10k涨至现在的40k,表明众多用户对它的喜爱。\nexcalidraw提供了基本的图形,如矩形,图形,菱形,文本,箭头等,稍经组合,就可以绘制很精美,简洁的图画。\n2.1 涂鸦之作 Hadoop 词频计算:\n数据治理:\n数据未分层:\n数据分层:\n因为excalidraw 相当的灵活,甚至系统循环图我都是使用它来绘制的:\n系统循环图:\n2.2 素材库 如果基本的图形无法满足诉求的话,excalidraw 还提供了在线library,供设计师把他们的图形,图标分享给其他用户。例如系统架构图,AWS组件图,UML图,手绘人物图等等,应有尽有,不一而足。\n素材库:\n(商户系统的头像就是引用自 library)\n2.3 在线协作 excalidraw 还支持端对端加密的在线协作,只需要将一个链接发送给协议方,就能实现画图在线协作:\nhttps://excalidraw.com/#room=91bd46ae3aa84dff9d20,pfLqgEoY1c2ioq8LmGwsFA 在远程会议,需要多方画图协作沟通的时候非常有用。\n2.4 技巧分享 excalidraw 画曲线的技巧\n按住Control/Command, 然后双击线条,进入曲线编辑模式 然后拖动线条,使用Control/Command + D 在末尾增加一个端点,或者使用删除键删除一个端点(留意excalidraw 工具栏下方的操作提示) 绘制曲线:\n我在拙作《我的写作流》中提到过,我倾向「本地化」+ 「文本化」 + 「版本管理」 + 「云同步」的知识管理文案,对于图片管理,我也是类似的倾向。\n因为图片是二进制流,无法做版本管理,所以我一般会把excalidraw 文件保存到本地,保存成xxx.excalidraw 的文件,实际是Json 文本;然后再导出成png, svg 等各种形式的图片文件。\n如果需要修改图片或者复制,剪切,只需要导入xxx.excalidraw,修改保存成新的excalidraw 文件,即可以实现「版本管理」\n原来excalidraw 有个限制,就是一次只能编辑一个excalidraw 文件,经@qisdreamyan 提醒,Vscode的excalidraw 插件支持直接在Vscode 里面编辑excalidraw 文件,那么就可以同时编辑多个文件啦。\n目前excalidraw 美中不足的一点就是,不支持手绘风格的非拉丁文字体,如中文,日文字体等,很早之前就有issue在谈论了,目前还没有什么进展。\n3 graphviz 我主要是用graphviz 来绘制复杂的关系图,timeline图。 它系出名门,出自大名鼎鼎的的AT\u0026amp;T实验室,类似微软出的「Visio」,但两者有个本质的差别。\n就是「Visio」是手动的,需要绘图者指定点线之间的布局,而graphviz 是自动布局的,只要将告知graphviz点与线的关系,graphviz 就能实现「自动布局」。\n如果是绘制简单的布局的图表,「自动布局」与「手动布局」差别不大。\n但如果是绘制复杂的图画,「手动布局」不仅繁琐,还不美观,而「自动布局」都能帮我们轻松搞定,为我们节省非常多的精力。\n不看广告,看疗效,来看下我使用graphviz 画出的图:\n土地财政时间线:\n西方哲学史演进历程:\nGraphviz 官方示例库:\nUnix 家谱:\n数据结构:\n更多更复杂的示例,可见官方的gallery\n3.1 快速入门 graphviz 使用所谓的「dot 语言(language)」这种标记语言来描述图形,然后再由命令行生成图片。\n程序员们可以把这个理解成,从源码编译到可执行文件。\n3.1.1 有向图 (digraph)与无向图 (graph) dot语言支持两种图形,分别是有向图 (digraph)与无向图(graph).\n定义一个无向图\ngraph mygraph { 1 -- 2 -- 3; 2 -- 4; } // graph 标识来定义一个无向图 定义一个有向图:\ndigraph mydigraph { 1 -\u0026gt; 2 -\u0026gt; 3; 2 -\u0026gt; 4; } // digraph 标识来定义一个无向图 命名规范与C家族的编程语言类似:图形关系定义在花括号{} 中;每条语句以 ; 结尾; // 表示单行注释, /**/表示多行注释\n3.1.2 节点(node) mydigraph 是图形名,1, 2 是节点名(node), 两个节点构成一条边(edge)。在图的定义中,相同的名称就代表同一个节点。\n当dot 编译器遇到一个新的名称,就认为是新的节点\n3.1.3 属性(property) 属性可以设置在节点和边上,通过「方括号 []」来定义属性,属性之间用英文逗号分隔。\n属性的定义采用如下的格式:\n属性名 = 属性值 常见的属性有:\nlabel: 标题 color: 颜色 style: 样式 shape: 形状 strict graph { // 设置节点属性 1 [shape=box]; 3 [shape=triangle]; // 设置边属性 1 -- 2 [color=blue]; 1 -- 3 [style=dotted]; } 属性还可以作用于图(graph)上,常用的属性包括:\nlabel:标题 bgcolor:颜色 fontname:字体名称(【不】影响节点和连线) fontsize:字体大小(【不】影响节点和连线) fontcolor:字体颜色(【不】影响节点和连线) center:是否居中绘制 digraph graph_attr { graph[bgcolor=\u0026#34;yellow\u0026#34; label=\u0026#34;标题\u0026#34; fontsize=24 fontcolor=\u0026#34;green\u0026#34;]; 1 -\u0026gt; 2; 1 -\u0026gt; 3; } 更多的属性可见官网:Attributes\n3.1.4 子图(subgraph) subgraph 的作用主要有 3 个:\n表示图的结构,对节点和边进行分组 提供一个单独的上下文设置属性(类似操作系统里面不同的线程,有不同的线程变量) 针对特定引擎使用特殊的布局。比如下面的例子,如果 subgraph 的名字以 cluster 开头,所有属于这个子图的节点会用一个矩形和其他节点分开。 digraph graphname{ a -\u0026gt; {b c}; c -\u0026gt; e; b -\u0026gt; d; subgraph cluster_bc { bgcolor=red; b; c; } subgraph cluster_de { label=\u0026#34;Block\u0026#34; d; e; } } 3.1.5 图布局(layout) 默认情况下图是从上到下布局的(rankdir \u0026quot;TB\u0026quot;),通过设置 rankdir\u0026ldquo;LR\u0026rdquo; 可以让图从左到右布局。\n默认布局(From top to bottom)\ndigraph { rankdir=\u0026#34;TB\u0026#34; a -\u0026gt; b -\u0026gt; c; } From Left to right:\ndigraph { rankdir=\u0026#34;LR\u0026#34; a -\u0026gt; b -\u0026gt; c; } 该属性只针对图(graph)生效.\n3.1.6 示例 再回头看下,「土地财政时间线」这图的源代码:\ndigraph 土地财政时间线 { size=\u0026#34;7,8\u0026#34;; node [fontsize=24, shape = plaintext]; 1976 -\u0026gt; 1985; 1985 -\u0026gt; 1994; 1994 -\u0026gt; 1998; 1998 -\u0026gt; 1999; 1999 -\u0026gt; 2000; 2000 -\u0026gt; 2001; 2001 -\u0026gt; 2002; 2002 -\u0026gt; 2008; 2008 -\u0026gt; 2009; 2009 -\u0026gt; 2014; 2014 -\u0026gt; 2015; node [fontsize=20, shape = box]; { rank=same; 1976 \u0026#34;改革开放\u0026#34;; } { rank=same; 1985 \u0026#34;财政包干\u0026#34;; } { rank=same; 1994 \u0026#34;分税制改革\u0026#34;; } { rank=same; 1998 \u0026#34;住房商品化改革\u0026#34; \u0026#34;《中华人民共和国土地管理法》实施\u0026#34;; } { rank=same; 1999 \u0026#34;土地财政兴起\u0026#34;; } { rank=same; 2000 \u0026#34;工业化\u0026#34; \u0026#34;城市化\u0026#34; \u0026#34;土地金融\u0026#34;; } { rank=same; 2001 \u0026#34;房价\u0026#34;; } { rank=same; 2002 \u0026#34;所得税改革\u0026#34;; } { rank=same; 2008 \u0026#34;金融危机\u0026#34; \u0026#34;四万亿刺激\u0026#34;; } { rank=same; 2009 \u0026#34;房地产与基建投资激增\u0026#34;; } { rank=same; 2014 \u0026#34;产能积压\u0026#34; \u0026#34;库存过剩\u0026#34;; } { rank=same; 2015 \u0026#34;棚改货币化\u0026#34; \u0026#34;涨价去库存\u0026#34; ; } \u0026#34;改革开放\u0026#34; -\u0026gt; \u0026#34;财政包干\u0026#34;; \u0026#34;财政包干\u0026#34; -\u0026gt; \u0026#34;分税制改革\u0026#34;[label=\u0026#34;地方政府收入下降\u0026#34;]; \u0026#34;分税制改革\u0026#34; -\u0026gt; \u0026#34;所得税改革\u0026#34;[label=\u0026#34;地方政府占比下降\u0026#34;]; \u0026#34;所得税改革\u0026#34; -\u0026gt; \u0026#34;土地金融\u0026#34;[label=\u0026#34;促进\u0026#34;] \u0026#34;分税制改革\u0026#34; -\u0026gt; \u0026#34;土地财政兴起\u0026#34;[label=\u0026#34;推动\u0026#34;] \u0026#34;土地财政兴起\u0026#34; -\u0026gt; \u0026#34;工业化\u0026#34;; \u0026#34;土地财政兴起\u0026#34; -\u0026gt; \u0026#34;城市化\u0026#34;; \u0026#34;土地金融\u0026#34; -\u0026gt; \u0026#34;城市化\u0026#34;[label=\u0026#34;促进\u0026#34; color =\u0026#34;red\u0026#34;]; \u0026#34;土地金融\u0026#34; -\u0026gt; \u0026#34;房价\u0026#34;[label =\u0026#34;推高\u0026#34;] \u0026#34;城市化\u0026#34; -\u0026gt; \u0026#34;房价\u0026#34;[label =\u0026#34;推高\u0026#34;] \u0026#34;土地财政兴起\u0026#34; -\u0026gt; \u0026#34;土地金融\u0026#34;[label=\u0026#34;地方政府收入增加\u0026#34;]; \u0026#34;土地金融\u0026#34; -\u0026gt; \u0026#34;工业化\u0026#34;[label=\u0026#34;促进\u0026#34; color =\u0026#34;red\u0026#34;]; \u0026#34;住房商品化改革\u0026#34; -\u0026gt; \u0026#34;土地财政兴起\u0026#34;[label=\u0026#34;停止福利分房\u0026#34;]; \u0026#34;《中华人民共和国土地管理法》实施\u0026#34; -\u0026gt; \u0026#34;土地财政兴起\u0026#34;[label=\u0026#34;限制农业用地非农用途\u0026#34;]; \u0026#34;金融危机\u0026#34; -\u0026gt; \u0026#34;四万亿刺激\u0026#34;; \u0026#34;四万亿刺激\u0026#34; -\u0026gt; \u0026#34;房地产与基建投资激增\u0026#34;[label=\u0026#34;宽松货币政策\u0026#34;] \u0026#34;房地产与基建投资激增\u0026#34; -\u0026gt; \u0026#34;产能积压\u0026#34;; \u0026#34;房地产与基建投资激增\u0026#34; -\u0026gt; \u0026#34;库存过剩\u0026#34;; \u0026#34;房地产与基建投资激增\u0026#34; -\u0026gt; \u0026#34;土地金融\u0026#34;[label=\u0026#34;强化\u0026#34;]; \u0026#34;产能积压\u0026#34; -\u0026gt; \u0026#34;涨价去库存\u0026#34;; \u0026#34;库存过剩\u0026#34; -\u0026gt; \u0026#34;涨价去库存\u0026#34;; \u0026#34;棚改货币化\u0026#34; -\u0026gt; \u0026#34;房价起飞\u0026#34;; \u0026#34;涨价去库存\u0026#34; -\u0026gt; \u0026#34;房价起飞\u0026#34;; \u0026#34;房价\u0026#34; -\u0026gt; \u0026#34;房价起飞\u0026#34;[label =\u0026#34;逐年上涨\u0026#34;]; } 3.1.7 编辑器支持 如果是Emacs 用户,可以使用graphviz-dot-mode 来编辑并预览生成的图片,效果如下:\n虽然我是重度Emacs 用户,但是在Emacs上实时预览图片效果并不好。\nEmacs对查看图片功能支持不够强大,无法通过鼠标放大缩小,并实时预览图片。\n如果需要实时预览graphviz 生成的图片,我个人更加推荐使用Vscode + graphviz 插件 :\n4 plantuml 身为程序员,免不了撰写各种设计方案,绘制各种序列图,类图,活动图,状态机图等等各种UML图。\n而plantuml 就是这样一个绘图组件,支持绘制各种程序开发需要用到的图。\nplantuml 依赖的底层组件就有前文提到的graphviz,所以plantuml的语法也类似graphviz, 通过自定义的标记语言,来描述不同图形之间的关系,「自动布局」并绘制。\n学过UML规范的同学应该都知道这些图应该怎么画,我就拿几个常见的图来举个例子。\n4.1 时序图 plantuml 提供不同的组件供时序图使用。不同的组件有不同的形状,默认情况下,组件的声明顺序就是他们的展示顺序。\n使用-\u0026gt; 来表示在两个组件/参与者(participant) 之间传递消息,\u0026lt;-- 表示回包信息。\n@startuml participant Participant as Foo actor Actor as Foo1 boundary Boundary as Foo2 control Control as Foo3 entity Entity as Foo4 database Database as Foo5 collections Collections as Foo6 queue Queue as Foo7 Foo -\u0026gt; Foo1 : To actor Foo -\u0026gt; Foo2 : To boundary Foo -\u0026gt; Foo3 : To control Foo -\u0026gt; Foo4 : To entity Foo -\u0026gt; Foo5 : To database Foo -\u0026gt; Foo6 : To collections Foo -\u0026gt; Foo7: To queue Foo \u0026lt;-- Foo7: Response from queue @enduml 时序图的更多用法可见官网文档:Sequence-Diagram\n4.2 活动图 @startuml start repeat :Test something; if (Something went wrong?) then (no) #palegreen:OK; break endif -\u0026gt;NOK; :Alert \u0026#34;Error with long text\u0026#34;; repeat while (Something went wrong with long text?) is (yes) not (no) -\u0026gt;//merged step//; :Alert \u0026#34;Success\u0026#34;; stop @enduml 4.3 编辑器 如果需要实时预览,个人推荐Vscode + plantuml插件来绘制plantuml 图,所见即所得,实时预览,并提供代码补全:\n5 matplotlib 这就是个绘图库了,主要是用来绘制各种图表,比如折线图,饼图,直方图等,通常是配合数据分析使用,还支持xkcd 风格。\n之前在上MIT 6.00网课的时候,John Guttag教授出了一个概率统计题,一个醉汉每次向四个方向中任意一个方向走一步,500步后,醉汉是离原点越来越近呢,还是越来越远?\n下面是Python代码实现,模拟醉汉行为:\n# 运行下面代码前,确保依赖已安装 # pip3 install matplotlib --user import math, random import matplotlib.pyplot as plt plt.xkcd() class Location(object): def __init__(self, x, y): self.x = float(x) self.y = float(y) def move(self, xc, yc): return Location(self.x + float(xc), self.y + float(yc)) def getCoords(self): return self.x, self.y def getDist(self, other): ox, oy = other.getCoords() xDist = self.x - ox yDist = self.y - oy return math.sqrt(xDist**2 + yDist **2) class CompassPt(object): possibles = (\u0026#39;N\u0026#39;, \u0026#39;S\u0026#39;, \u0026#39;E\u0026#39;, \u0026#39;W\u0026#39;) def __init__(self, pt): if pt in self.possibles: self.pt = pt else: raise ValueError(\u0026#34;in CompassPt.__init__\u0026#34;) def move(self, dist): if self.pt == \u0026#34;N\u0026#34;: return (0, dist) elif self.pt == \u0026#34;S\u0026#34;: return (0, -dist) elif self.pt == \u0026#34;E\u0026#34;: return (dist, 0) elif self.pt == \u0026#34;W\u0026#34;: return (-dist, 0) else: raise ValueError(\u0026#34;in CompassPt.move\u0026#34;) class Field(object): def __init__(self, drunk, loc): self.drunk = drunk self.loc = loc def move(self, cp, dist): oldLoc = self.loc xc, yc = cp.move(dist) self.loc = oldLoc.move(xc,yc) def getLoc(self): return self.loc def getDrunk(self): return self.drunk class Drunk(object): def __init__(self, name): self.name = name def move(self, field, time = 1): if field.getDrunk() != self: raise ValueError(\u0026#34;Drunk.move called with drunk not in field\u0026#34;) for i in range(time): pt = CompassPt(random.choice(CompassPt.possibles)) field.move(pt, 1) def performTrial(time,f ): start = f.getLoc() distances = [0.0] for t in range(1, time+1): f.getDrunk().move(f) newLoc = f.getLoc() distance = newLoc.getDist(start) distances.append(distance) return distances def firstTest(): drunk= Drunk(\u0026#34;Homser Simpson\u0026#34;) for i in range(5): f = Field(drunk, Location(0, 0)) distances = performTrial(500, f) plt.plot(distances) plt.title(\u0026#34;Homer\u0026#39;s random Walk\u0026#34;) plt.xlabel(\u0026#34;Time\u0026#34;) plt.ylabel(\u0026#34;Distance from origin\u0026#34;) fname = \u0026#34;images/mit6.00/simulation_random_walk_trail1.png\u0026#34; plt.savefig(fname) return fname return firstTest() 模拟5次,生成出来的xkcd风格的图表:\n5.1 再话org-mode 在《我的写作流》里面,我有提到过,我使用Emacs + org-mode 来编写文章,对比markdown 或者其他的标记语言,org-mode 有一个巨大的优势,就是org-mode 借助内置的org-babel 组件,可以直接运行代码。\n在markdown 里面,下面的代码块的用处仅仅是语法高亮:\n```python print(\u0026#34;helloworld\u0026#34;) ``` 但在 org-mode, 下面的代码块是可运行的,我只要在Emacs中按下C-c C-c,就会运行代码,并输出helloword。\n#+begin_src python print(\u0026#34;helloworld\u0026#34;) #+end_src 看起来作用不大,但是和 graphviz, plantuml, matplotlib 结合,就会产生无穷的威力:只要我把绘图源码写好,然后再按下 C-c C-c,就能自动生成图片,并自动插入到当前这篇文章中(当然,如果代码写错了,是编译生成不出图片的)。\n根本不需要手动编译,生成图片,然后再把图片以markdown格式手动插入: ![图片](链接) 。\n上面的概率统计模拟图也是这样生成出来的,写好Python 代码,然后按下 C-c C-c\n一切都浑然天成。\n6 那些年,我使用过的绘图工具 都是曾经使用过,现在也基本弃用的工具:\nWord:最开始时也不知道什么画图工具,就使用Word 来画图。 PPT:写技术方案基本不用了,画PPT做分享和述职,就还只能继续使用。 drawio: 功能丰富,但图形有种说不出的丑,并且绘制起来不顺手 processon: 在线绘图服务,免费版本有绘画张数限制(不记得是10张还是15张);对于Saas服务而言,数据不属于用户。公司倒闭或限制用户,就有丢失数据风险。 图表与文章一样,都是资产。\n对于这样的重要资产,我还是倾向于「本地化」+ 「文本化」+ 「版本管理」+ 「云同步」的方案,保证图表既易于修改,又无丢失风险。\n7 结语 金庸笔下的「独孤求败」的用剑之道:「四十岁后,不滞于物,草木竹石均可为剑。自此精修,渐进于无剑胜有剑之境」\nexcalidraw, graphviz 也好, plantuml, org-mode 也罢,只是「器」,都只是用来表达想法与智慧的工具。\n所谓「飞花摘叶皆可伤人,草木竹石均可为剑」,真正的大牛,即使不使用画图工具,寥寥数语就会把一个复杂的概念解释得清楚明了。\n厚积而薄发,选择合适的「剑」很重要,但「内功」的修炼同样重要。\n8 参考 Graphviz Documentation Graphviz 入门指南 【自动】绘图工具 Graphviz ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%88%91%E7%9A%84%E7%94%BB%E5%9B%BE%E6%B5%81/","summary":"1 前言 古人云:「一图胜千言」。 一幅合适的图片可以清晰地向读者表达我们的意图,又因为我们人脑的作用机制,阅读一张图片所耗费的脑力要远少于一段文","title":"我的画图流:画图工具与技巧分享"},{"content":"1 前言 年后在家略为空闲,就写了篇思考与感悟:\n「上面的意思是好的,就是下面的人执行出了问题」\n「下面的人也没办法,毕竟这是上面的规定」\n那事情没做好,究竟是谁的原因?\n「上面」与「下面」的关系与内在逻辑究竟是怎么样的呢?\n让我们翻开中国漫长的历史,一探皇帝与官僚之间的关系.\n2 牧民之道 皇帝是国家的最高统治者,号为「天子」,代表上天来统治臣民,具有至高无上的权力。\n虽然想当皇帝的人有很多个,但皇帝这个岗位实际只能有一个人在职.\n「普天之下,莫非王土,率土之滨,莫非王臣」,这么多的「王臣」与「王土」,皇帝一个人自然是管理不过来,就需要三公九卿等文武百官。\n封建官僚拿着皇帝给予的权力和薪水,逐级管理着小农。\n最底层是万千小农,他们对帝国纳税,用自己的血汗钱养活帝王与封建官僚。\n3 利益之差 封建官僚是一种“压力单向传导机制”,压力只能逐级向下传导,封建官僚不但不会分散压力,而且会为了自身利益扩大这种压力。这样,压力传导到最后的小农便会呈几何级数扩张。\n疫情期间,各地都出现「层层加码」的现象,这也是一种压力单向传导机制作用的现象。\n对封建官僚来说,假如中央要求州上缴赋税10万石,每个郡平均上缴1万石;每个州向郡下达任务时,可能会要求上缴10万石,每个县平均上缴5千石;郡向县下达任务,可能就会要求每个县上缴一万石;\n这样既能满足上级的要求,又可以截留剩余的税赋,还师出有名,可以把锅甩给上级。\n说到底对国家财富具有所有权的,是帝王,天下是一家一姓的地盘,民不聊生对他们也没有好处。\n封建官僚却完全不同,只要达到目的,管你大浪滔天,反正又不是我的天下。\n3.1 王朝周期循环 中国封建王朝轮回更替两千多年, 尽管每朝每代制度不尽相同,但是总逃脱不了所谓的王朝周期循环:\n阶段1:新朝初立,连年征战国家元气损伤,人口凋零,开国君主多是刀枪入库,马放南山,休养生息,鼓励民众生产,轻徭薄赋 阶段2: 王朝强盛,人口剧增,土地兼并,社会阶级逐步分化 阶段3: 王朝末期,土地集中在少数人手上,农民失去土地,失去工作,成为流民,揭杆而起。一切变为废墟,破而后立,直至新朝建立, 开启新一轮循环。 如果从封建官僚的角度来分析王朝周期循环的原因:\n皇权只有借助封建官僚才能统治整个帝国,但是封建官僚自身就是一个强势分利集团,他们会借助手中的权力疯狂掠夺帝国财富。\n皇权根本就无法彻底遏制这种掠夺,毕竟联系皇权和封建官僚的纽带恰恰就是掠夺财富的权力。\n封建官僚对财富的掠夺将成为帝国难以治愈的沉疴。\n强势分利集团完全不遵守财富规则,毕竟他们既是运动员,又是裁判,又怎么会遵守规则呢?\n最终,掠夺超出了帝国居民承受的极限,人们失去了土地、失去了工作、没有能力组建家庭,最终成为流民。\n3.2 官僚利益 总有人奇怪,为什么明朝皇帝崇祯不像唐玄宗,宋高宗那样,在贼兵临城时,跑路迁都南京。\n如果再翻开史料,重现当时的场景,就会发现,既有崇祯主观的因素,也有官僚在背后推波助澜。\n崇祯十七年,当时李自成已兵临北京,形势危急,右庶子李明睿劝崇祯放弃北京,尽快南迁,皇帝告诉他:“汝意与朕合,但外边诸臣不从,奈何?”\n李明睿说:“天命微密,当内断圣心,勿致噬脐之忧。”并请崇祯勿犹豫,尽快决断。\n崇祯帝一直有意迁都,崇祯对众臣说:“李明睿有疏劝朕南迁。国君死于社稷,朕将何往?又劝朕教太子先往南京,诸卿以为如何?”\n首辅陈演反对南迁,并示意兵科给事中光时亨,严厉谴责李明睿,扬言:“不杀李明睿,不足以安定民心。”其事遂不了了之\n为什么官员们把调子提得这么高呢,莫非迁都真的事不可为?\n平心而论,崇祯着实算不上一位好老板,性格存在缺陷,求治心切,生性多疑,刚愎自用,并且有不少恶劣的前科:\n「崇祯十五年, 松山、锦州失守,洪承畴降清,崇祯想和满清议和而和兵部尚书陈新甲暗中商议计划, 没想到事情泄漏,被朝臣知晓,明朝士大夫鉴于南宋的教训,皆以为与满人和谈为耻。\n崇祯就把议和责任都推到兵部尚书陈新甲, 并将其下狱斩首。」\n面对这样性格的老板,即使知道迁都是个明智之选,官僚们明面上都不会赞同这样的建议。\n如果能打退贼兵,老板如果面子挂不住,又要追究迁都的责任,那我们附和迁都建议的都可能完蛋。如果打不退贼兵,双膝朝下,跪谁不是跪,只是换个老板而言。\n所以对于官僚来说,最佳的选择就是把调子拨得高高的,君王死社稷,我们都要和陛下共存亡,死战不退。这样官员们就能立于不败之地。\n崇祯十七年,李自成入北京,崇祯皇帝自缢于煤山,首辅陈演想逃离北京,但因家产太多而未果。他主动向农民军献白银四万两。稍后,其家仆告发,说他家中地下藏银数万。农民军掘之,果见地下全是白银。\n另一民变领袖张献忠后来称帝,以陈演的女儿为皇后,陈演的儿子为翰林学士。\n想起韩国电影《辩护人》中关于爱国者的论述:\n你不是真正的爱国者\n你是让善良无罪的国家生病的蛆虫,军事政权肮脏的帮手而已\n说出真相,那才是真正的爱国\n官僚利益与皇帝利益并不一致,管你洪水滔天,还是民不聊生,反正民不是我家的,国不是我家的,钱才是我家的。\n3.3 服从性测试 对于皇帝而言,官员的能力,品格,尚在其次,皇帝最关心的是忠诚。毕竟品格越高尚,能力越出色,但是怀有二心,对皇帝的危害就越大。\n所以皇帝就派自己家奴去监视官员。但俗话说,「人心隔肚皮」,皇帝也没有办法知道官员的心思。皇帝是想到的办法,就和我们玩狼人杀一样,就是「观其言,察其行」,看你们是否表里不一。\n所以对于官员们「层层加码」的行为,就能明白其背后的逻辑:对于「上面」的命令,「下面」的执行不到位,就容易被理解是对「上面」不忠诚。\n所谓「忠诚不绝对,就是绝对不忠诚」,何况即使你100%执行到位,隔壁的同行150%执行到位。相比之下,你就变成执行不到位,就容易「内巻」起来,变成相互竞争加码。\n对于官僚而言,正确的命令要坚决执行,不正确的命令也要坚决执行。前者容易理解,后者又有什么解究呢?\n对于不正确的命令,如果官员执行到位,他不会有任何的错,因为错在「上面」。但因为「上面」不可能出错,所以就变成了大家都没有错。如果执行不到位,那么「上面」就可以认为,是执行有问题,而不是决策有问题,其罪在你。\n因此,权衡之下,官员的最佳选择就是加倍执行命令,无论对错,甚至还可以夹带点私货。\n何况,不正确的命令也是一种服从性测试。如果君王知道你在民怨沸腾的情况,还把不正确的命令也如实执行了,会认为你对君王忠心不二,简在帝心。\n不正确的命令也要执行,那小农的死活怎么办?\n权力运行自有其规律:**权力只对来源负责**.\n老爷心善,见不得穷人, 把他们赶走吧。\n4 决策权与信息权之争 皇帝拥有至高无上的权力,是帝国的主人,对帝国的政策拥有最终的决策权。\n拥有决策权,并不意味着皇帝都能在问题上作出正确的抉择,官僚们只能俯首听令。\n面对皇帝的政令,官僚们自有应对之策,皇帝拥有决策权,但做出决策的前提是有充足的信息,对问题有充分的了解。但皇帝囿于深宫之中,又如何能知天下事呢,只能听从官僚们的汇报,所以说,影响皇帝决策的信息权,掌握在官僚手上。\n官僚们可以通过隐藏,截留,篡改信息,以误导皇帝作出有利于自己的决策。\n《天朝的崩溃》就有提到,原来在鸦片战争期间,直到英军直逼天津大沽口, 道光皇帝才知道战事之靡烂,防备之严峻,而北京几乎无险可守,此前道光皇帝还一直以为清军战事占优。\n而皇帝对官僚们隐瞒信息的招数,自然是心知肚明,所以自明朝起,皇帝就开设东厂与锦衣卫,通过特务机构来打破封建官僚的信息垄断。\n受电视剧影响,我们总以为东厂首领权倾朝野,对百官予取予求。但本质上,东厂首领钦差掌印太监,只是皇帝的家权,他们的权力都来源于皇权,所以他们忠诚的对象只能是皇帝。又因为他们生理缺陷,无法生育,也无法将权力延续,自然不会对皇权产生威胁。\n即使是明熹宗权倾朝野的九千岁魏忠贤,在崇祯登基后,一纸诏书就将其赐死。\n通俗理解,在明朝以前,是官僚集团与皇帝在玩二人转;皇帝招架不过来,就拉了些信得过的家奴,和官僚一起斗地主。\n在现代社会,各种媒体资讯发达,官僚已经无法垄断信息权。\n想起英剧《是,大臣》里面的一个情节,外交部大臣竟然是通过电视来了解最新的国际时事的,既滑稽又现实。毕竟记者都跑得比较快。\n4.1 信息反馈与决策 信息权无法再被垄断,但还可以人为制造信息的茧房:\n王小波先生在《沉默的大多数》中有一篇文章,名为《花剌子模信使问题》:\n据野史记载,中亚古国花剌子模有一古怪的风俗,凡是给君王带来好消息的信使,就会得到提升,给君王带来坏消息的人则会被送去喂老虎。\n于是将帅出征在外,凡麾下将士有功,就派他们给君王送好消息,以使他们得到提升;有罪,则派去送坏消息,顺便给国王的老虎送去食物。\n虽说信使带来的消息的好坏,决定人并不是信使本身,并不妨碍君王把他们当作老虎的点心。\n正常人都会趋利避害,长此下去,君王只会听到各种花团锦簇的好消息,不会听到任何反映现实的坏消息,就这样活在了自己制造的信息茧房里。缺乏现实感,又怎能作出正确的决定的呢。\n唐太宗李世民曾说过:「以铜为鉴,可以正衣冠;以人为鉴,可以明得失;以史为鉴,可以知兴替」。\n所谓的「以人为鉴」就是指臣下能如实说清事实,指明对错。君主要有容人之量,臣下要有犯颜直谏的勇气。\n即使魏征经常把李世民怼得脑壳疼,甚至把李世民的宠物鸟都憋死了(太宗怀鹞),也没见魏征去当老虎点心。\n「以人为鉴」既要有识人之明,又要有容人之量,更要有自省之心。只愿开美颜,就只会看到美照,苏大强都觉得自己是吴彦祖。\n5 出路在何方 抗日战争胜利前夕, 1945年7月1日至5日,黄炎培等人访问延安,试图调解国共关系,化解政治危机。\n7月4日下午,毛泽东接待黄炎培等人,黄炎培说:“中国历史上的王朝都存在一个从兴起到消亡的周期率,一部历史,「政怠宦成」的也有,「人亡政息」的也有,「求荣取辱」也有, 总之没有能跳出这「周期率」。\n毛泽东欣然笑对道:“我们已经找到新路,我们能够跳出这「周期率」。这条新路,就是民主,(我们要)用民主来打破历代从艰苦创业到腐败灭亡的「周期率」,跳出这种兴亡「周期率」。”\n6 参考 《中国是部金融史》 维基百科:崇祯帝 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E7%9A%87%E5%B8%9D%E4%B8%8E%E5%AE%98%E5%83%9A/","summary":"1 前言 年后在家略为空闲,就写了篇思考与感悟: 「上面的意思是好的,就是下面的人执行出了问题」 「下面的人也没办法,毕竟这是上面的规定」 那事情没做","title":"皇帝与官僚:「上面」与「下面」"},{"content":"1 前言 自大学起,已经写过好几年的东西,写作工具和流程也反复折腾过好多回. 目前的写作流程已经很流程顺手,所以想分享一下。\n2 写作工具 我不倾向任何专用格式的写作软件(如Word), 或者笔记云服务(EverNote,xx笔记等)。\n因为前者只能使用专门的软件打开,和该软件绑定,会导致文本内容被专用的软件绑架。 可能过个10年,可能该软件不再流行,你甚至无法打开自己的文章。\n对于笔记服务,你的文章数据甚至不掌握在你手里,该服务停用,你的数据就会全部丢失。\n所以我倾向于「本地化储存」+「文本格式」 + 「易于同步」的写作方案\n最后我选择的写作工具是 Emacs + org-mode,不熟悉org-mode 的读者可能不知道 org-mode 是什么,可能理解成类似是markdown 的标记语言,但是和Emacs 结合后,易用性与扩展性比markdown 提高了一个数量级。\n这篇文章就是使用 Emacs + org-mode 写出来的:\n3 写作平台 3.1 博客 在大学时期,我使用的是 org-page + org-mode 来自建博客在 Github Pages, 写了大概一年的博客文章。\n后来因为 org-page 总是存在各种奇怪的小bug,博客样式也只有那么几种,不能满足折腾欲望很强烈的我 (这种行为和年少时喜欢在QQ空间换皮肤差不多)。\n大概是2018年,我当时学习了Rust,打算找个机会来练手,然后就使用Rust搭建了个人的博客,开源在Github上,还有100+的star,就这样,在自己的博客上又写了几年文章。\n直到2022年,因为工作太忙碌,没有太多时间来维护博客的代码以及运维博客的服务。 自建博客的初衷只是为了有个地方可以承载我写的内容,只是因为年轻,有精力折腾,就自己写代码,搭建服务。\n但总体而言,写作的流程并不顺手: 因为我一直都是使用org-mode 来写作,而我自己的博客只支持markdown 格式,这就意味着我必须每次写作完成后,需要将org-mode 转换成markdown , 然后再在博客后台发布。\n如果涉及到图片就更麻烦,需要将图片逐张上传到图床,然后再插入到org-mode中, 而博客使用的markdown 编辑器过了5年后,开发者已经不维护了,所以免不了又会有各种小问题。\n我又尝试了Emacs + org-mode + hugo 的博客方案,发现使用起来非常舒心,发布文章几乎无成本。正好碰上博客服务器到期,就干脆切换回Github Pages.\nhugo 是使用markdown 来构建网站的框架,而我使用的org-mode,所以还需要一个工具将org-mode 转换为hugo markdown,这个就是Emacs 插件ox-hugo。当然, hugo 原生也支持org-mode, 只是功能支持不完整, 不及ox-hugo.\nox-hugo 只需要将在org-mode 内容的最开始插入标记:\n#+HUGO_BASE_DIR: ~/code/org/ramsayleung.github.io #+HUGO_SECTION: post/2023 #+HUGO_CUSTOM_FRONT_MATTER: :toc true #+HUGO_AUTO_SET_LASTMOD: t #+HUGO_DRAFT: false #+DATE: [2023-01-25 Wed 14:25] #+TITLE: 我的写作流 #+HUGO_TAGS: writing #+HUGO_CATEGORIES: writing Emacs一个快捷键 C-c C-e H h 就可以将org-mode 转换成hugo 格式的markdown:\n结合Github Action,就可以自动把导出的markdown 博文部署到Github Pages,称得上是一键部署。\n不过,说出来有点心酸,我在博客写的文章,读者大多是我自己。\n3.2 公众号 在过新年的时候,闲来无事,新开了一个公众号「宫孙说」,用来「一鱼多吃」,将自己文章也转到公众号。\n因为Richard Stallman的影响,我一直是倾向于开放,自由的软件与生态,所以对于公众号这样仅限于「微信」封闭生态的平台不感冒。\n另外,关于「公众号」的看法,我是与陈皓一致的,详见他的文章《为什么我不在微信公众号上写文章》\n但不可否认的是,「公众号」是国内最完善的创作平台,有完整的分享,阅读生态。\n毕竟我不只写技术文章,还会写一些历史,人文类的文章,我希望可以分享给朋友,但Github Pages会间歇性被墙,朋友们大多是在微信阅读文章,虽然博客支持移动端,但是体验终究不及「公众号」。\n屈服于现实压力,我最终开了这个公众号。\n3.3 知乎 知乎也是国内创作氛围相对自由和蓬勃的平台,本着「一鱼多吃」的心态,我也会把文章转载到知乎上。\n3.4 KM 这是公司内部的知识交流创作平台,算是私域流量。有点难以置信的是,在这个读者数量远不如外部的平台,我写的文章有最多的阅读与收藏. 或许是因为我写的内容贴近业务,是同事比较感兴趣的。\n3.5 一鱼多吃的问题 将同一篇文章分发到多个平台,就难免会有不同平台格式不通的问题。\n上面这些平台,都是不支持 org-mode 这种相对小众的文本格式,所以 markdown 算是比较理想的中间格式,可以先把org-mode 转换成hugo markdown,再将hugo markdown 转换成markdown:\n使用 ox-hugo,可以将org-mode 转换成hugo markdown , 至于将hugo markdown 转换成markdown, 我写了个脚本 hugomd2md.sh来处理:\n除了将hugo markdown 转换成markdown,使用Github Pages 当作图床,还在最后插入一个公众号广告。\n4 写在最后 因为使用Emacs + org-mode 写文章非常顺手,所以我写下了很多的文稿,只是思绪杂乱,不方便都发布出来.\n现在正逐步将存稿往公众号平台上迁移,只是公众号有诸多限制,其中一条是一天只能群发一篇文章,只能「小刀锯大树」,慢慢来了。\n","permalink":"https://ramsayleung.github.io/zh/post/2023/%E6%88%91%E7%9A%84%E5%86%99%E4%BD%9C%E6%B5%81/","summary":"1 前言 自大学起,已经写过好几年的东西,写作工具和流程也反复折腾过好多回. 目前的写作流程已经很流程顺手,所以想分享一下。 2 写作工具 我不倾向任何","title":"我的写作流"},{"content":"1 前言 有心仪工作的城市房价太高,而房价合适的城市没有心仪的工作。\n梦想买不起,故乡回不去。\n眼看着大城市一座座高楼拔地而起,却难觅容身之所。\n为什么房子这么贵?为什么归属感这么低?为什么非要孤身在外地闯荡,不能和父母家人在一起?\n这些问题都与地方政府推动经济发展的模式有关,《置身事内》这本书都给出了自己的解答。\n结合书中的内容和我自己的阅历,我也有了些自己的浅薄见解,我希望把作者的书,读成我自己的书。\n先把形成这个结果的历史原因与决策摆出来:\n2 改革开放 1978年12月18日中共十一届三中全会后,开始实施的一系列以经济为主的改革措施,可总结为“对内改革,对外开放”。\n这是一切故事的起点。\n3 分税制改革 办事要花钱,如果没钱,话说得再好听也难以落实。钱从哪来,从税里来。所以要真正理解政府行为,必然要了解财税\n3.1 财政包干:1985-1993 财政承包始于1980年,中央与省级财政之间对收入和支出进行包干,地方可以留下一部分增收。\n1980—1984年是财政包干体制的实验阶段,1985年以后全面推行,建立了“分灶吃饭”的财政体制.\n既然是承包,当然要根据地方实际来确定承包形式和分账比例,所以财政包干形式五花八门,各地不同。比较流行的一种是“收入递增包干”。\n以1988年的北京为例,是以1987年的财政收入为基数,设定一个固定的年收入增长率4%,超过4%的增收部分都归北京,没超过的部分则和中央五五分成。假如北京1987年收入100亿元,1988年收入110亿元,增长10%,那超过了4%增长的6亿元都归北京,其余104亿元和中央五五分成。\n广东的包干形式更简单,1988年上解中央14亿元,以后每年在此基础上递增9%,剩余的都归自己。也就是说,如果后续广东的财政收入增长高于9%, 那么能留下的钱就会越来越多(事实也是如此)\n财政承包制下,交完了中央的,剩下的都是地方自己的,因此地方有动力扩大税收来源,大力发展经济。\n3.1.1 问题:地方越富,中央越穷 税制是这样设计,随着时间的推移,却出现了中央财政越来越穷的问题。\n正常人都希望是自己手上的钱越来越好,要交的钱越少越好,地方政府也不例外。\n一方面,地方政府控制预算收入增长,避免增长过快,毕竟账面上增加得越多,要交的就越多;另外一方面,虽然地方预算内的税收收入要和中央分成,但预算外收入则可以独享,地方政府就可以通过给企业免税,再用其他手段,把应收的税款收回来,就能免于与中央分成。\n就出现了,经济发展越来越好,地方政府越来越富裕,中央却越来越穷的问题。\n翻开中国漫长的历史,就会发现,「弱干强枝」乃取祸之道,不利于政治稳定,所以税制是到了非改不可的地步了。\n3.2 分税制改革:1994 1994年的分税制改革把税收分为三类:中央税(如关税)、地方税(如营业税)、共享税(如增值税).\n分税制改革中最重要的税种是增值税,占全国税收收入的1/4。改革之前,增值税(即产品税)是最大的地方税,改革后变成共享税,中央拿走75%,留给地方25%. 企业只要开工生产,不管盈利与否都得交增值税,规模越大缴税越多。\n分税制改革,地方阻力很大。比如在财政包干制下过得很舒服的广东省,就明确表示不同意分税制。与广东的谈判能否成功,关系到改革能否顺利推行。为了这项改革的展开,朱镕基总理亲自带队,用两个多月的时间先后走了十几个省,面对面地算账,深入细致地做思想工作,最后广东还是服从了大局。\n分税制是20世纪90年代推行的根本性改革之一,也是最为成功的改革之一。中央占全国预算收入的比重从改革前的22%一跃变成55%,并长期稳定在这一水平。但分税制改革的影响深远,还远不止于此。\n有钱才能办事,而税收又关系到政府能收到多少钱。\n而分税制改革又调整了税收分配模式,直接影响到地方政府的财税收入,为地方政府后续搞「土地财政」埋下了伏笔。\n4 土地财政 4.1 缘起 1994的分税制改革并没有改变地方政府以经济建设为中心的任务,却减少了其手头可支配的财政资源。\n对于地方政府而言,钱变少了,活还是要照干,地方政府环视一圈,盘了下自己手上有什么资源可以用,最后把目光投到拥有的,最有价值的资源:**土地**。\n4.2 土地财政 所谓“土地财政”,不仅包括巨额的土地使用权转让收入,还包括与土地使用和开发有关的各种税收收入。其中大部分税收的税基是土地的价值而非面积,所以税收随着土地升值而猛增。\n这些税收分为两类,一类是直接和土地相关的税收,其收入百分之百归属地方政府; 另一类税收则和房地产开发和建筑企业有关,主要是增值税和企业所得税,要与中央分成。\n4.3 土地金融 再穷的国家也有大片土地,土地本身并不值钱,值钱的是土地之上的经济活动。\n若土地只能用来种小麦,价值便有限,可若能吸引来工商企业和人才,价值想象的空间就会被打开,笨重的土地就会展现出无与伦比的优势:它不会移动也不会消失,天然适合做抵押,做各种资本交易的压舱标的,身价自然飙升。\n地方政府就可以把与土地相关的未来收入资本化,去获取贷款和各类资金,将“土地财政”的规模成倍放大为“土地金融”\n4.3.1 城投公司 法律规定,地方政府不能从银行贷款,2015年之前也不允许发行债券,所以政府要想借钱投资,需要成立专门的公司。\n这些公司名称大多有「建设投资」和「投资开发」的字样,因此统称「城投公司」\n5 房价与债务 5.1 房价的影响因素 房价短期内受很多因素影响,但中长期主要由供求决定。无论是发达国家还是发展中国家,房屋供需都与人口结构密切相关,因为年轻人是买房主力。\n年轻人大都流入经济发达城市,但这些城市的土地供应又受政策限制,因此房屋供需矛盾突出,房价居高不下。\n\u0026ldquo;好消息\u0026quot;是因为房价高企等种种原因,中国的出生人口正逐年下降,按照「官方数据」, 2021年全年出生人口1062万人,自然增长率为0.34%. 而2022年的人口数据还没有公布,各方估计有望下降到1000万以下,甚至实现自然增长率负增长的目标。\n这就是意味着,对于等等党来说,再多等个十几年,大概率就没有年轻人和你争买房子了,就可以在梦想的地方,以梦想的价格买到想要的房子。\n5.2 政府债务与房价 随着城市化和商品房改革,土地价值飙升,政府不仅靠土地使用权转让收入支撑起了“土地财政”,还将未来的土地收益资本化,从银行和其他渠道借入了天量资金,利用“土地金融”的巨力,推动了快速的工业化和城市化。\n但同时也积累了大量债务。这套模式的关键是土地价格。\n只要不断地投资和建设能带来持续的经济增长,城市就会扩张,地价就会上涨,就可以偿还连本带利越滚越多的债务。\n可经济增速一旦放缓,地价下跌,土地出让收入减少,累积的债务就会成为沉重的负担,可能压垮融资平台甚至地方政府。\n而据中国冰川思想库研究员提供的数据,截至2022年,中国城投债(就是地方政府控制的公司借的钱)规模可能达到65万亿,中国人均负债5万元。\n我画了一张系统循环图来分析其中的因果关系:\n(系统循环图:A -\u0026gt;(+) B代表: A的增加会导致B的增加,A的减少会导致B的减少;A-\u0026gt;(-)B代表:A的增加会导致B的减少,反之亦然)\n可见,主客观上,政府和官员都不会想土地价格降下来,因为一降下来,地方政府可能被债务压垮,官员个人待遇也会大受影响。\n另外,因为政府垄断了土地,而土地的供给又影响土地出让的价格,所以政府必然会制造供给侧的短缺,以拉高土地出让的价格。\n而土地出让价格又是房价的大头,所以政府是不会想房价降下来的。\n5.3 银行信贷 2008年至2009年,为应对全球金融危机,我国迅速出台“4万亿”计划,同时不断降准降息,放宽银行信贷(也就是所谓的「大放水」),这些资金找到了基建和房地产两大载体,相关投资迅猛增加。\n虽说银行增加货币供给,增加信贷比重,但是各种实体企业总是喊「借钱难,借不到钱」,这是因为银行尤其偏爱以土地和房产为抵押物的贷款。\n先看住房按揭: 银行借给张三100万元买房,实质不是房子值100万元,而是张三值100万元,因为他未来有几十年的收入。\n但未来很长,张三有可能还不了钱,所以银行要张三先抵押房子,才肯借钱。房子是个很好的抵押物,不会消失且容易转手,只要这房子还有人愿意买,银行风险就不大。而房子具有普适性,李四要买的房子可能就是张三要卖的房子。\n若没有抵押物,张三的风险就是银行的风险,但有了抵押物,风险就由张三和银行共担。张三还要付30万元首付,相当于抵押了100万元的房子却只借到了70万元,银行的安全垫很厚。\n再来看企业贷款: 银行贷给企业家李四500万元买设备,实质也不是因为设备值钱,而是用设备生产出的产品值钱,这500万元来源于李四公司未来数年的经营收入。\n但作为抵押物,设备的专用性太强,价值远不如住房或土地,万一出事,想找到人接盘并不容易。就算有人愿意接,价格恐怕也要大打折扣,所以银行风险不小。但若李四的企业有政府担保,甚至干脆就是国企,银行风险就小多了。\n这就是加大流动性,资金也流不到实业企业中去的原因。\n这也是个马太效应,除了有政府担保外,借了钱又还不上的企业成了大爷,银行担心这家企业还不上钱,破产,坏账,也有意愿再借钱给他,借新债还旧债。\n5.4 居民债务 根据央行的统计,2008年之后的10年,我国房价急速上涨,按揭总量越来越大,居民债务负担上涨了3倍多。\n居民债务中有53%是住房贷款,24%是各类消费贷(如车贷)。这一数据可能还低估了与买房相关的债务,毕竟一些消费贷也被用来交首付买房了。\n总体看来,我国居民的债务负担不低,且仍在快速上升。最主要的原因是房价上涨。\n而居民债务居高不下,就很难抵御经济衰退,尤其是房产价格下跌所引发的经济衰退。\n低收入人群的财富几乎全部是房产,其中大部分是欠银行的按揭,负债率很高,很容易受到房价下跌的打击。\n5.5 房价下跌与债务风险 债务关系让经济各部门之间的联系变得更加紧密,任何部门出问题都可能传导到其他部门,一石激起千层浪,形成系统风险。\n银行既贷款给个人,也贷款给企业。若有人不还房贷,银行就会出现坏账,需要压缩贷款;\n得不到贷款的企业就难以维持,需要减产裁员;于是更多人失去工作,还不上房贷;银行坏账进一步增加,不得不继续压缩贷款……\n如此,恶性循环便产生了:\n如果房价下跌太多,房子的价值比要还房贷还低,而还贷圧力又超出可承受范围,债务人就有可能放弃首付,断供,造成银行坏账.\n如果各部门负债都高,那应对冲击的资源和办法就不多,风吹草动就可能引发危机, 原因有二:\n负债率高的经济中,资产价格的下跌往往迅猛。若债务太重,收入不够还本,甚至不够还息,就只能变卖资产,抛售的人多了,资产价格就会跳水 资产价格下跌会引起信贷收缩,导致资金链断裂。借债往往需要抵押物(如房产和煤矿),若抵押物价值跳水,债权人(通常是银行)坏账就会飙升,不得不大幅缩减甚至干脆中止新增信贷,导致债务人借不到钱,资金链断裂,业务难以为继。 一个部门的负债对应着另一个部门的资产。债务累积或“加杠杆”的过程,就是人与人之间商业往来增加的过程,会推动经济繁荣。而债务紧缩或“去杠杆”也就是商业活动减少的过程,会带来经济衰退。\n举例来说,若房价下跌,老百姓感觉变穷了,就会勒紧裤腰带、压缩消费。东西卖不出去,企业收入减少,就难以还债,债务负担过高的企业就会破产,银行会出现坏账,压缩贷款,哪怕好企业的日子也更紧了。\n说出来有点难以置信,就是房价下跌,最终会影响到卖皮肤氪金的游戏公司和收交易手续费的金融科技公司,甚至出现裁员的情况。\n基于以上分析,我认为未来的房价会是「跌不下,涨不上,买不起,卖不动」\n5.5.1 疫情与债务风险 如果把疫情这个超大号黑天鹅与政府的应对造成的影响再纳入分析范畴,就会变成这个样子:\n5.6 人口与债务风险 如果把人口数量这台灰犀牛纳入到分析范畴,就会变成这个样子\n(系统循环图的 -||-\u0026gt; 标识表示滞后效应,产生作用需要时间)\n6 解决办法 6.1 政府债务 任何国家的债务问题,解决方案都可以分成两个部分:一是偿还已有债务;二是遏制新增债务,改革滋生债务的政治、经济环境。\n6.1.1 偿还已有债务 如果借来的钱能用好,能变成优质资产、产生更高收入,那债务负担就不是问题。\n但如果投资失败或干脆借钱消费挥霍,那就没有新增收入,还债就得靠压缩支出:居民少吃少玩,企业裁员控费,政府削减开支。\n但甲的支出就是乙的收入,甲不花钱乙就不挣钱,乙也得压缩支出。\n大家一起勒紧裤腰带,整个经济就会收缩,大家的收入一起减少。若收入下降得比债务还快,债务负担就会不降反升。\n还债让债务人不好过,赖账让债权人不好过。所以偿债过程很痛苦,还有可能陷入经济衰退。\n相比之下,增发货币也能缓解债务负担,「似乎」还不那么痛苦,因为没有「明显」的利益受损方,实施起来阻力也小.\n但增发货币大概率会导致通货膨胀,国民手中的钱变得不值钱了,相当于全体国民为政府的债务买单\n按照我们政府对国民负责任的态度,相信最终还是会选择这条路。\n6.1.2 遏制新增债务 理解了各类债务的成因之后,也就不难理解遏制新增债务的一些基本原则:限制房价上涨,限制“土地财政”和“土地金融”,限制政府担保和国有企业过度借贷,等等。\n但困难在于,就算搞清楚了原因,也不一定就能处理好后果,因为“因”毕竟是过去的“因”,但“果”却是现在的“果”,时过境迁,很多东西都变了。\n好比一个人胡吃海塞成了大胖子,要想重获健康,少吃虽然是必须的,但简单粗暴的节食可能会出大问题,必须小心处理肥胖引起的很多并发症。\n为此,近几年政府出台了限制供给侧的政策,包括「房住不炒」,「三道红线」等等。\n只是,在疫情的第三年,在清零导致的财政压力下,这些限制又有解绑的趋势。\n6.2 居民债务 要化解居民债务风险,除了遏制房价上涨势头以外,根本的解决之道还在于提高收入,尤其是中低收入人群的收入,鼓励他们到能提供更多机会和更高收入的地方去工作。\n让地区间的经济发展和收入差距成为低收入人群谋求发展的机会,而不是变成人口流动的障碍。\n很多事情都是提出愿景容易,执行落地难:\n按照有关部门统计,2020年,民营企业从业人员占城镇就业的83%, 也就是说,要提高居民收入,就要做多做大做强民营企业,毕竟系统调优,也是针对热点,调优效果最好。\n要发展好民营企业,就需要建立好的营商环境;\n完善法律法规,不要再你法我笑;\n提高宽松自由的环境,放宽媒体限制;\n政府退出各种高利润的垄断行业,不再与民争利,不要既当裁判,又当运动员。\n关于官营商业,盐铁专营的弊端,二千年前的《盐铁论》中,贤良文学已经阐述得非常清晰了。\n尊重与保护劳动者权益,把劳动者当作「人」而非「矿」 要让人口流动起来,就要改革革户籍制度,不要把农民绑死在土地上 效率低,营收差的国企要有退出机制,政府不能无限地为其输血,兜底 要完善法律法规,就要做到依法治国。年少时,听到依法治国,「有法可依,有法必依」八个字的准则,觉得理所当然。年纪渐长,才读懂背后的含义,原来有很多「有法不依,无法可依」的情况,也难怪会出现「你法我笑」的情况。 要政府不再与民争利,退出垄断行业,就要改革已有的经济结构。 要创新,就需要提供自由宽松的环境,因为创新就和基因突变一样,是无序的,甚至开始是异端的。量变产生质变,只有有足够多的创新,才能由市场进行选择,优胜劣汰,留下市场认可的创新。产品创新和生物进化一样,都是肥沃土壤,长出百花齐放的结果,不是哪个造物主指明的方向。 开放媒体,加强对行政执行和腐败的监管。 政府瘦身,建立小政府,减少对市场的干预,也减少对财政的压力。 解法办法就在这里,但知易行难。 改革从来都不是请客吃饭,都是要下大决心,有壮士断腕的狠劲。\n这些都是结构性的问题,只能做结构性改革。\n但「深化改革」的口号,从我上小学,一直喊到我上大学,至于我们是离目标越来越近,还是越来越远,南辕北辙,摸着石头倒车。\n相信大家心里还是有杆称的。\n对于历史债务,Leader们常挂在嘴边的话是:「不重构是等死,重构是找死」,但是我们选择的路,大多是找死。\n7 总结 7.1 以古为鉴 年少时读史,觉得汉武帝刘彻战功赫赫,封狼居胥,扬我大汉雄武之风,不愧为武帝之名。\n年青时再读史,会读到许多对汉武帝的批评,说其穷兵黩武,不惜民力,无数英魂埋骨他乡,户籍几近减半。对批评甚是不解,认为凡事都会有代价。\n年长些,出外为三餐奔波谋生,再读史,就会对汉武帝产生不一样的看法:十年匈奴之战,败光了他祖父和父亲「文景之治」所积累的财富家底,政府财政全面恶化,就出台了影响中国两千年的「盐铁官营」政策,与民争利,不如此不足以应付政府的开销;后来甚至出台了告发富人瞒税,告发人可获告发金额一半的,严重败坏社会道德「告缗令」,令汉朝的中产阶级和富人阶级被一扫而空,甚至出现100倍于前代的超级通货膨胀。\n就我这种黔首看来,武帝可谓是独夫,我不想成为代价。\n但即使「雄才大略」如汉武帝,晚年面对糜烂的政局,还是下了「轮台罪己诏」,向天下臣民承认自己的错误。\n7.2 写在最后 在经济规律面前,无论多强势的强人,都只能屈服。\n而无形的经济规律,可以理解成国人所谓的「势」,理解经济规律之后,我们可以顺势而为,而非逆势而动,被时代的大势无情碾过。\n毕竟古诗有云:「时来天地皆同力 运去英雄不自由」\n8 参考 《置身事内》 中国人口十年图谱 轮台诏 65万亿的城投债 ","permalink":"https://ramsayleung.github.io/zh/post/2023/%E7%BD%AE%E8%BA%AB%E4%BA%8B%E5%86%85/","summary":"1 前言 有心仪工作的城市房价太高,而房价合适的城市没有心仪的工作。 梦想买不起,故乡回不去。 眼看着大城市一座座高楼拔地而起,却难觅容身之所。 为什","title":"为什么梦想买不起,故乡回不去"},{"content":"1 Definition In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertx v, u comes before v in the ordering.\nIt sounds pretty academic, but I am sure you are using topological sort unconsciously every single day.\n2 Application Many real world situations can be modeled as a graph with directed edges where some events must occur before others. Then a topological sort gives an order in which to perform these events, for instance:\n2.1 College class prerequisites You must take course b first if you want to take course a. For example, in your alma mater, the student must complete PHYS:1511(College Physics) or PHYS:1611(Introductory Physics I) before taking College Physics II.\nThe courses can be represented by vertices, and there is an edge from College Physics to College Physics II since PHYS:1511 must be finished before College Physics II can be enrolled.\n2.2 Job scheduling scheduling a sequence of jobs or tasks based on their dependencies. The jobs are represented by vertices, and there is an edge from x to y if job x must be completed before job y can be started.\nIn the context of a CI/CD pipeline, the relationships between jobs can be represented by directed graph(specifically speaking, by directed acyclic graph). For example, in a CI pipeline, build job should be finished before start test job and lint job.\n2.3 Program build dependencies You want to figure out in which order you should compile all the program\u0026rsquo;s dependencies so that you will never try and compile a dependency for which you haven\u0026rsquo;t first built all of its dependencies.\nA typical example is GNU Make: you specific your targets in a makefile, Make will parse makefile, and figure out which target should be built firstly. Supposing you have a makefile like this:\n# Makefile for analysis report output/figure_1.png: data/input_file_1.csv scripts/generate_histogram.py python scripts/generate_histogram.py -i data/input_file_1.csv -o output/figure_1.png output/figure_2.png: data/input_file_2.csv scripts/generate_histogram.py python scripts/generate_histogram.py -i data/input_file_2.csv -o output/figure_2.png output/report.pdf: report/report.tex output/figure_1.png output/figure_2.png cd report/ \u0026amp;\u0026amp; pdflatex report.tex \u0026amp;\u0026amp; mv report.pdf ../output/report.pdf Make will generate a DAG internally to figure out which target should be executed firstly with typological sort:\n3 Directed Acyclic Graph Back to the definition, we say that a topological ordering of a directed graph is a linear ordering of its vertices, but not all directed graphs have a topological ordering.\nA topological ordering is possible if and only if the graph has no directed cycles, that is, if it\u0026rsquo;s a directed acyclic graph(DAG).\nLet us see some examples:\nThe definition requires that only the directed acyclic graph has a topological ordering, but why? What happens if we are trying to find a topological ordering of a directed graph? Let\u0026rsquo;s take the figure 3 for an example.\nThe directed graph problem has no solution, this is the reason why directed cycle is forbidden\n4 Kahn\u0026rsquo;s Algorithm There are several algorithms for topological sorting, Kahn\u0026rsquo;s algorithm is one of them, based on breadth first search.\nThe intuition behind Kahn\u0026rsquo;s algorithm is pretty straightforward:\nTo repeatedly remove nodes without any dependencies from the graph and add them to the topological ordering\nAs nodes without dependencies are removed from the graph, the original nodes depend on the removed node should be free now.\nWe keep removing nodes without dependencies from the graph until all nodes are processed, or a cycle is detected.\nThe dependencies of one node are represented as in-degree of this node.\nLet\u0026rsquo;s take a quick example of how to find out a topological ordering of a given graph with Kahn\u0026rsquo;s algorithm.\nNow we should understand how Kahn\u0026rsquo;s algorithm works. Let\u0026rsquo;s have a look at a C++ implementation of Kahn\u0026rsquo;s algorithm:\n#include \u0026lt;deque\u0026gt; #include \u0026lt;vector\u0026gt; // Kahn\u0026#39;s algorithm // `adj` is a directed acyclic graph represented as an adjacency list. std::vector\u0026lt;int\u0026gt; findTopologicalOrder(const std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; \u0026amp;adj) { int n = adj.size(); std::vector\u0026lt;int\u0026gt; in_degree(n, 0); for (int i = 0; i \u0026lt; n; i++) { for (const auto \u0026amp;to_vertex : adj[i]) { in_degree[to_vertex]++; } } // queue contains nodes with no incoming edges std::deque\u0026lt;int\u0026gt; queue; for (int i = 0; i \u0026lt; n; i++) { if (in_degree[i] == 0) { queue.push_back(i); } } std::vector\u0026lt;int\u0026gt; order(n, 0); int index = 0; while (queue.size() \u0026gt; 0) { int cur = queue.front(); queue.pop_front(); order[index++] = cur; for (const auto \u0026amp;next : adj[cur]) { if (--in_degree[next] == 0) { queue.push_back(next); } } } // there is no cycle if (n == index) { return order; } else { // return an empty list if there is a cycle return std::vector\u0026lt;int\u0026gt;{}; } } 5 Bonus When a pregnant woman takes calcium pills, she must make sure also that her diet is rich in vitamin D, since this vitamin makes the absorption of calcium possible.\nAfter reading the demonstration of topological ordering, you (and I) too should take a certain vitamin, metaphorically speaking, to help you absorb. The vitamin D I pick for you (and myself) is two leetcode problems, which involve with the most typical use case of topological ordering \u0026ndash; college class prerequisites:\nCourse Schedule Course Schedule II 6 Reference Topological Sort | Kahn\u0026rsquo;s Algorithm | Graph Theory Directed Acyclic Graph Hands-on Tutorial on Make Topological sorting ","permalink":"https://ramsayleung.github.io/zh/post/2022/topological_sorting/","summary":"1 Definition In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertx v, u comes before v in the ordering.\nIt sounds pretty academic, but I am sure you are using topological sort unconsciously every single day.\n2 Application Many real world situations can be modeled as a graph with directed edges where some events must occur before others.","title":"Topological Sort"},{"content":"1 前言 「最好的学习方式」\n在如今打广告也需要遵守广告法的时代,用这样的标题来描述某个学习方法难免会让人觉得言过其实,不客气的朋友可能会直接说「营销味」十足。\n不看广告看疗效,「言过其实」还是「名符其实」,只有试过才知道。\n如果「尝试」也过于麻烦,那不如来看下提出该学习方法的理查德·费曼(Richard Phillips Feynman)其人。\n1.1 费曼其人 理查德·菲利普斯·费曼(Richard Phillips FeynmanA), 美国理论物理学家,以对量子力学的研究闻名于世,除此之外,他还是量子计算领域的先驱,并提出了纳米技术的概念。\n因对量子电动力学的贡献,于1965年共同获得诺贝尔物理学奖。\n费曼发展了得到广泛应用的亚原子粒子行为的图像化数学表述——费曼图(Feynman diagram)。费曼图长这个样子:\n费曼在世时是世界上最有名的科学家之一。\n1999年,在英国学术期刊《物理世界》举办的130位世界顶尖物理学家参与的票选活动中,费曼跻身十大有史以来最伟大物理学家之列\n二战期间他曾参与曼哈顿计划,协助原子弹的开发,而后在1980年代因参与调查挑战者号航天飞机灾难而为公众熟知。\n1.1.1 趣事 费曼有一辆有名的货车(The Feynman Van),他在这辆车上画满了以他名字命名的费曼图(Feynman diagram):\n虽然费曼他偶尔会开这辆车,但是大部分时间都是他的妻子(Gweneth)开。\n有一次,她妻子开这辆车出去,等待红灯时,一个识货的司机走过来问,为什么她开着一辆画满费曼图的货车,她回答到,因为我是费曼的妻子。\nAlthough Richard occasionally used the van to commute from his home in Altadena to Caltech, the van was usually driven by his wife, Gweneth.\nOne time a perplexed motorist waiting at a red light asked the unidentified woman why she was driving a van with Feynman diagrams on the side.\nHer answer: “Because my name is Gweneth Feynman.”\n另外一个小彩蛋,这辆车曾经出现在《生活大爆炸上》,看谢尔顿的翻脸速度和崇拜表情,大概就能感受到费曼有多牛。\nYour browser does not support the video tag.\n(视频来自知乎问题:费曼是一个什么样的人,答主@流川枫)\n2 理念 费曼学习法,核心理念就是:\n学习一种新事物最好的方法是,用你的话讲给别人听。\n通过向别人清楚的解说某一事物,来确认自己是否真的弄懂了这件事。\n所以说,学习最好的方式,是把你学到的东西教给别人。\n学习步骤如下:\n2.1 学习并汇总 选择一个你想要学习的领域或者课题,然后学习,再将你所学到的所有内容给记录下来。\n做笔记,摘抄,或者写下自己的理解都可以。\n2.2 用自己的话向别人解释 找一个小白或者小朋友,用你「自己的话」来复述你学到的内容。\n你可以援引自己笔记的内容,但是不要说那些专业术语,只用你自己的话来解释。\n因为用自己的话,才能说话你把学习的内容真正消化了,直接使用专业术语,只能说明你背下来了,不一定能说明你理解了。\nSimple is beautiful.\n目标是 通顺,通透 地向这个「无知」的小朋友解释你所学到的内容。\n2.3 反馈,改进 在步骤二,你可能会出现四种情况。\n2.3.1 欲语还休 情况一,算是最糟糕的情况,你可能不知道从何讲起,说你还对该领域还缺乏「系统」的理解,就需要返回步骤一重新学习。\n2.3.2 跌跌撞撞 情况二,算是最常见的情况,你能用自己话讲出来,但是讲得「磕磕碰碰」,一直会卡壳,小白也听得一知半解。\n说明你对该领域有一个的了解,但还没完全「理顺」整个体系,这里就可以针对「卡壳」的点,有针对性地回到目步骤一进行学习。\n2.3.3 深入不浅出 情况三,你能用自己的话用该领域的内容通顺地讲出来,但是小白没有完全理解,就说明你讲得不够「通透」,不够「深入浅出」。\n这个时候就可以询问小白,是哪个部分没有讲清楚,这个部分就是你盲点,拼图缺乏这块,导致没法构建完体系。\n再回去步骤一针对学习。\n2.3.4 娓娓道来 在这种情况下,你能顺滑,流畅地使用自己的话向小白解释你学到的内容,小白也能完全领会 到你所讲述的知识,那就说明你已经完全掌握了这个领域的知识。\n不要吝啬赞美的言词,大声地表扬下自己。\n2.4 总结 2.4.1 双赢 当你「通顺」,「通透」地向一个小白介绍完的你所学的知识时,你一定会有新的收获和感慨,把你新学到的知识再总结下来。\n这就是输出的收获和乐趣,所以这也是一个「双赢」的学习法,既能育人,也能育已。\n听众既能受益,输出知识的作者本身也能提高个人能力。\n如果能吸引到读者高质量的问题,那么作者在解答的过程中,又可以进一步受益和得到提升,这还是一个良性循环。\n2.4.2 换位思考 为了践行这种「以教代学」的学习方法,在向小白讲授内容的时候,为了更好地帮助他们理解,你就可能需要切换到「小白视角」,以他们的角度来思考问题。\n久而久之,你的「换位思考」和「共情」能力自然就会得到提升。\n3 输出 最好的学习方法是输出,而我这篇文章就是在用自己的话向大家介绍「费曼学习法」,而这个行为本身也是在实践「费曼学习法」。\n如果大家看完本文,能理解「费曼学习法」是什么,那也就说明我已经掌握了「费曼学习法」(否则我就讲不清楚)!\n干杯!\nPS:\n这应该算编译器自举(bootstrap)了 :)\n4 参考 The Feynman Technique: The Best Way to Learn Anything The Feynman Van 费曼是一个什么样的人 如何【系统性学习】——从“媒介形态”聊到“DIKW 模型” ","permalink":"https://ramsayleung.github.io/zh/post/2022/feynman_technique/","summary":"1 前言 「最好的学习方式」 在如今打广告也需要遵守广告法的时代,用这样的标题来描述某个学习方法难免会让人觉得言过其实,不客气的朋友可能会直接说「","title":"最好的学习方式:费曼学习法(Feynman Technique)"},{"content":"1 前言 秦人不暇自哀而后人哀之,后人哀之而不鉴之,亦使后人而复哀后人也\n每个朝代灭亡之后,就会由下一个朝代的史官编写,如元朝灭亡之后,就由明朝来撰写《元史》。\n虽然国民党政权在大陆败退之后,没有史官来专门编写《民国史》,但还是有许多资料来探究国民党失败的原因。 而《党员,党权与党争》就从组织角度来考察国民党失败的原因。\n想来,以后也会有类似的,研究共产党的学术资料。\n2 以俄为师 中国国民党承续兴中会,同盟会,国民党,中华革命党而来,期间不仅名称数度更易,其组织形式亦几度因革。 1924年国民党改组,借鉴俄国布尔什维克的组织模式,建立了一套全新的政党组织体系。\n有感于国民党党务不振,孙中山于1924年从组织技术层面学习苏俄的建党,治党方法。 实现三民主义为体,俄共组织为用,从党的组织结构,党章党纪,基层组织建制,基于党的军队建设,全面向苏俄学习,企图打造出一个组织严密,富有战斗力的新党。\n只是,国民党师法俄共的组织形式,将党建在国上,实行以党治国,一党专政。 但是,孙中山三民主义理念中的政治蓝图又是基于西方民主体制而设计的。\n国民党借鉴了两个不能同时并立的政治架构,拼装了一台不伦不类的政治机器:\n成立国民政府后一方面依照西方分权学说,成立五院(所谓的五权分立,即在西方的行政权,立法权,司法权外,再增加考试权和监察权), 另一方面又依照苏俄党治学说,设立集权的中执会,中政会。\n这种兼收并蓄,可谓错漏百出。\n3 清党分共 在1924年的国民党一大上,孙中山提出了「联俄」,「联共」,「扶助工农」的三大政策, 期望在苏俄和中共共产党的帮助下,完成反帝反封的国民革命。\n而国民党与共产党的合作关系,是被称为「党内合作」的合作关系,并不是国民党与共产党两个政党开展合作, 而是共产党员以个人身份加入国民党,成为跨党党员,开展工作。\n国共两党的党员,在实际的工作中,是呈现出「国民党在上层,共产党在下层」这样的状态。 国民党差不多专做上层的工作,而下层的民众运动,国民党员参加的少,共产党员参加的多,因此形成了一种畸形的发展。\n到1926年,国民党二大召开前后,甚至出现共产党「包办」大部分国民党党务的现象. 据中共领导人称,在国民党二大召开前后,大约有90%的国民党地方组织处于共产党员和国民党左派的领导之下。\n1924-1927年的国共关系,既是一种相互合作的关系,又是一种相互竞存的关系。\n到第一次国共合作的后期,国共合作关系已经从原来的国民党「容共」,变成共产党「容国」。 即使共产党员远少于国民党员,但因为其组织严密,工作高效,已经成为国民党党内之党。\n两党组织动作的巨大反差,国民党内部一方面为内部组织松懈而忧虑,更对共产党组织严密而恐惧, 这种忧虑和恐惧衍化为「分共」,「反共」的主张与行动,另一方面促使部分富有革命热情的国民党青年, 在强烈对比之下,加下到组织更严密的共产党去,这又加深了国民党内部的恐惧与忧虑。\n在此种忧虑与压力之下,1927年4月12日蒋介石下令武力清党分共,屠杀共产党员,就变得理所当然。 如果蒋不分共,国民党的情况也不会好转,只会被和平演变成共产党。\n4 党务不振 「四一二」清共,对共产党造成了巨大的伤害,让共产党意识到「文斗」虽强,但是还是干不过刀把子,悟出了枪杆里出政权的道理。\n很多人没有意识到的是,清共对国民党同样造成巨大的伤害. 因为共产党使用秘密党员制,因此,要分共时,并不知道哪个是共产党员,哪个是国民党员。\n蒋介石的亲信陈果夫曾经使用过一种荒谬儿戏至极的方法来辨别两党党员:让屋子里面的人相互殴打,然后他认为打着打着,共产党员就会帮助共产党员,国民党员就会帮助国民党员,就能借此分出两党党员。\n因为分不清共产党员,就免不了杀错人,把「像」共产党的党员杀完之后,就开始把有革命热情的左派国民党员和群众都当成共产党杀了,清共之旋风越刮越大,甚至国民党民都无人敢做真正的革命之事. 地主劣绅趁机诬陷减租减息等不利于现有特权阶级的国民党党员为共产党员,以除之而后快,以致人人自危。 甚至还有索婚不成,诬其为共党以杀之的可悲之事。\n此种分党手段造成的严重后果就是,国民党在国民心中的民心一落千丈,并再也回不去了。 而在基层乡镇,因此大量干实事的国民党员被杀,导致基层权力空虚,地主劣绅趁虚而入,摇身一变成为国民党员,把持基层党权。\n在蒋介石独裁及派系核相争的帝王心术之下,中上层的党员,忠于派系多过忠于党,对权力的向往大于对革命理想的践行,党统在各派系激烈的权力斗争中濒于破裂,党务亦在派系的内耗中日趋衰微。\n蒋既无决心重组党务,又不愿意放弃手中独裁的权力,失去民心,党心,国民党的党务建设有如泥牛入海,再也无法拉回来了. 自北伐后,国民党民心日失,党务难振。\n自分党至国民党败退台湾,党务再没再振之机。\n5 以军治党 自孙中山逝世,蒋介石依靠黄埔系的党军之力上位,靠军权登上国民党权力的高位。 而因为党权不振,无法通过党来治理国家,蒋就希望通过军队来治理国家,由原来的以党治军,变成以军治党。\n蒋的军治理念,首先表现为放大军人在国家社会中的作用,蒋认为,在任何时候,任何国家,军人应该是社会的主导群体。 其次,强调军队组织在国家和社会的各个领域的普适性,认为无论古今中外,要组织成一个健全的国家和社会,都是要全国军队化。\n蒋介石对军权和军治的过分迷恋,分散甚至取代了他对党治和党务组织建设的关注与考虑。 而蒋重军轻党的原因是多方面的,在与汪精卫及胡汉民的「继承人」之争中,前者都是通过党权与蒋所持的军权相争,蒋难免会有把党权作为障碍。 其次,1931-1932年之交,蒋二次下野时,反思认为国民党的各级党组织并不忠于他,而他的一生中,最为依赖的权势资源就是军队,拿着个锤子,难免看什么都像钉子。\n在蒋介石重军轻党的主导下,军权日趋膨胀,党权日趋低落,从中央到地方,军权凌驾于党政之上,党治徒有其表,军治实际取代了党治。\n而军治,既是党权不振的因,又是党权不振的果,可谓因果纠缠,恶性循环。\n6 总结 国民党依照俄共实行一党专政,而在实际运行中,其组织散漫性,又像西方议会政党,只重政见不重组织。 国民党是一个弱势独裁政党,国民党并非不想独裁,而是独裁之心有余,独裁之力不足。\n国民党和中国共产党都是以俄为师的中国学生,花开两朵,各表一枝。 全盘俄化的中国共产党赢了只学到半套功夫的国民党,而国民党后又败在年轻的民进党手下。\n蒋公一再以「亡党亡国」警示其党员,然而,事实却是,党破山河在,党亡国不亡。而后之视今,亦尤今之视昔。\n只是未想,时运弄人,两个学生尚在,引以为师的苏共竟先消散在历史的尘埃中。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%85%9A%E5%91%98_%E5%85%9A%E6%9D%83%E4%B8%8E%E5%85%9A%E4%BA%89/","summary":"1 前言 秦人不暇自哀而后人哀之,后人哀之而不鉴之,亦使后人而复哀后人也 每个朝代灭亡之后,就会由下一个朝代的史官编写,如元朝灭亡之后,就由明朝来","title":"党员,党权与党争"},{"content":"1 前言 历史教科书说,与英国的第一次鸦片战争,敲开了清朝的大门,清朝也因此和外国签订了一系列不平等条约. 而因为清朝的落伍,导致清朝不敌英国.\n教科书却未曾告诉我们,清朝落伍在什么地方,当时清朝的正确之途又在何方。\n而这些内容,《天朝的崩溃》都会告诉我,还原历史,以当时人的眼光,再去审视这场影响深远的战争。\n以史为鉴,以彼「天朝」审视此「天朝」。\n2 军事力量 尽管现代人已对战争下了数以百计的定义,但是,战争最基本的实质只是两支军事力量之间的对抗。\n鸦片战争中英武器的水平,概括来说:\n英军已处于步发展的火器时代,而清军仍处于冷热兵器混用的时代.\n2.1 武器装备 2.1.1 火器 火药和管型火器都是中国发明的,但中国一直处于前科学时期,没有形成科学理论和实验体系,使中国火器发展受到了根本性制约。\n至鸦片战争期间,清军使用的火器,主要不是中国发明的,而是仿照明代引进的「佛郞机」,「鸟铳」等西方火器样式制作。\n由此可以说,清军使用的是自制的老式的「洋枪洋炮」,就样式而言,与英军相比,整整落后二百余年。\n2.1.2 火炮 清军使用的火炮,如同其鸟枪,其原型可追溯至明代。 清军的火炮从样式上,主要是仿照西方17世纪至18世纪的加农炮. 与英军相比,清军火炮在样式及机制原理上大体相同,但两者的差别在于制造工艺引起的质量问题:\n铁质差\n工业革命使英国的冶炼技术改观,铁质大为提高,为铸造高质量的火炮提供了良好的原料。\n清朝的冶炼技术落后,炉温低,铁水无法提纯,含杂质多,铸造出来的火炮十分粗糙,气孔气泡多,容易炸膛:\n关天培称,在1835年,他在佛山制造59门新炮,试放时就炸裂10门,损坏3门。\n清军针对此问题主要采用两策: 一是加厚火炮的管壁,使清军的火炮极为笨重,数千斤巨炮,威力还不如西方小炮; 二是使用铜作为铸炮材料,但铜资源缺乏\n铸炮工艺落后\n英国已经使用铁模工艺,并使用车床对炮膛内部加工,使之更为光洁。\n清朝此时仍沿用落后的泥模工艺,铸件毛糙,又未对炮膛做深入加工,致使炮弹射出后,弹道紊乱,降低射击精度。\n英方因此科学的进步,对火药燃烧,弹道,初速度等方面做研究,使火炮设计较合理。\n炮架和瞄准器具不全或不完善\n炮架是调整火炮射击和高低夹角的器具,清军对此不甚重视,火炮没有炮架,只是固定严重限制了射击角度。\n又没有瞄准器具,只能靠经验操作,全凭感觉。\n炮弹种类少,质量差;\n英军的炮弹有实心弹,霰弹,爆破弹等,而清军只有效能最差的实心弹,且弹体粗糙,弹径小。\n2.1.3 火药 与枪炮相关联的,是火药。\n鸦片战争时期,中英火药处于同一发展阶段,皆为黑色有烟火药。\n然而,因为质量问题,使中英在火药上的差距大于前面所提到的火炮。\n这里面的关键,是科学和工业。\n1825年,歇夫列里在经过多次实验后,提出了黑色火药的最佳化学反应方程式,英国据此方程式,配制了枪用发射火药和炮用发射火药。\n除了科学带来的理论进步外,工业革命又带来了机械化的生产,通过先进的工业设备,提炼高纯度的硝和硫; 使用蒸汽装置和水压机进行火药加工,使颗粒均匀,保证优良品质。\n而火药虽起源于中国,发展却主要凭借经验,鲜有理论的层层揭示,也靠手工制作,靠舂碾,颗粒粗糙。\n2.1.4 舰船 对照中英武器装备,差距最大者,莫过于舰船。\n英国海军为当时世界之最,拥有各类舰船400余艘,其主要作战军舰仍为木制风帆动力,似与清军同类,当相较之下,有下列特点:\n用坚实木料制作,能搞风涛而涉远洋 船体下部为双层,抗沉性能好,外包铜片,防蛀防火 船上有两桅或三桅,有数十具风帆,能利用各种风向航行 军舰较大,排水量从百余吨到千余吨 安炮较多,有10至120门不等 此外,诞生于工业革命末期的蒸汽动力铁壳轮船,也开始装备海军。\n清军的海军,时称“水师”,并不以哪一国的舰队为作战对象,其对手仅仅是海盗。\n2.2 兵力与编制 武器装备有着物化的形态,其优劣易于察觉,因此不同的人们都得出相同的结论:清朝处于兵器上的劣势。\n许多人又不约而同地指出:清朝在鸦片战争中处于兵力上的优势\n单从数字来看,这是事实。 清朝有八旗兵20万,绿营兵60万,总兵力达80万,而英国远征军,海陆军合并计算,大约7000人。\n但就作者考证,清军不是一支纯粹的国防军,而是同时兼有警察,内卫部队,国防军三种职能。\n以绿营军为例,绿营军大多是以数名,十数名,最多数百名(200名)分散在当时的市镇要冲等地。 清朝是靠武力镇压而建立起来的高度中央集权的政权,军队是其支柱,而当时清朝没有警察, 维持社会治安,保持政治秩序就成了清军最重要最大量的日常任务。\n所以理解成,总兵力80万,里面有很大一部分是警察和城管,打小贩可能在行,打英军就不大行了。\n因为清军没有常备的国防机动力量,因此抽调是鸦片战争中清军集结的唯一方法。 而鸦片战争中每个省能抽调的兵力,不足万人(四川最多是7500人),所以只能从各地,东拼西凑出兵力,兵不知将,将不知兵。\n更要命的是清军的调兵速度,鸦片战争期间,清军调兵的大概速度是,邻省约30至40天,隔一二省约50天,隔三省约70天,隔四省约90天以上。 如此缓慢的调兵速度,使清军丧失了本土作战的有利条件。\n当时英海军从南非的开普敦驶至香港约60天,从印度开来约30至40天,即使英国本土开来也不过4个月。 蒸汽机的出现,轮船的使用,又大大加速了英军的速度,从孟买到澳门,只需25天。\n也就是英军从印度调兵,可能比清政府调兵还快. 何况,英军坐船,清军靠人力,劳师远征,到达战场也没法形成战斗力。\n2.3 士兵与军官 清朝的兵役制度是一种变形的募兵制。早期的八旗是兵民合一的制度,清入关后,人丁生繁,兵额固定。 绿营兵募自固定的兵户,与民户相比,兵户出丁后可免征钱粮赋税。\n清兵收入不高,大抵可养活自己,但无法养活家人,因此兵丁大多去有第二职业. 把当兵当作固定的旱涝保收的「铁杆庄稼」,值班充差操演时上班打卡,其他时间则操持旧业。\n清军军官的来源,主要是两途:一是行伍出身;二是武科举出身。\n正如认为八股文可以治天下一样荒谬,清代武科举场内考试项目是武经七书(《孙子》,《吴子》这些). 与近代战争的要求南辕北辙,因为很多考生不识字,导致错误百出,因此武科举以外场为主,集中一项,即拉硬弓。\n清代军官的升迁,除军功外,均需考弓马技能,若不能合格,不得晋职。\n用今日的眼光观之,这种方法挑选聘出来不过是一句优秀的士兵,而不是领兵的军官。\n由此,在当时人的心目中,军官只是一介鲁莽武夫,「不学无术」成为军官的基本标志。 军人的身份为社会所鄙视。所谓无官不贪,军官也不能免俗。主要手段有:\n吃空额:这个就是人人知详的手段了。 克兵饷:传统手法 创意手段:浙江军官出售兵缺(毕竟是国企岗位);广东绿营开赌收费;福建水师就比较有创意,将战船租赁给商人贩货运米 因此,在鸦片战争中,清军在作战中往往一触即溃,大量逃亡,坚持抵抗者殊少。 在这种情况下,谈论人的因素可以改变客观上的不利条件,又似毫无基础可言:毕竟,清军已经腐败。\n3 政治与文化 3.1 定于一尊 自明朝废宰相之位后,中国王朝政府已经没有首相,皇权得到前所未有的加强,由皇帝独断朝纲。 清朝有着宰相之名的军机大臣,不过是按照老板意思,草拟旨意的秘书。\n这种政权体制下,一切的决断都由君主作出,也就一切都取决于君主的好恶与见识。\n如果是个英明无比的君上,大概能发挥该政体的长处. 但君主能成为君主,不是因为他有着治理国家的才能,只是因为他是上一代的君主的几个继承人之一。\n因此,从统计学来说,出现一个平庸之主的概率就会非常大。\n而英国当时已实行君主立宪,由首相代替国王治理国家,首相有对应选拨淘汰机制,不至于久为庸人。\n人臣诸事听命于君主,没有任何的灵活性(除非是抗旨不遵),也不想因此做事而承担相应的任何责任。 这一幕,似乎在当下,又再度重演。\n全书看下来,把权力全部集中于一个人身上,有着巨大的风险与弊端。 各级政府有处理事务的责任,但却没有自由采取措施处理事务的权利。\n事事都要先问询君主,而君主对事情发展的了解,仅限于臣下的启奏, 臣下出于种种考虑,又不会(或不能)把完全事情的全貌告知君主,导致决策最需要的信息疏漏,让君主决策时误判。\n又因为决策错误的责任不能归咎于君主,因此决策失误,人臣也很难指出,只能任由事情发展; 而臣下为了避免背上违背上意,抗旨不遵的责任,避免做多错多,往往又会听任问题发展,直到出现更大问题。\n这是严重缺乏灵活度和弹性的制度。 在黑天鹅来临时,君臣按此种方式来处理突发事件,非常容易让事情的严重性和后果扩大,直到难以收拾的地步。\n英军的来临是黑天鹅,疫情的到来也是黑天鹅。\n关于皇帝与官僚的关系分析,可以参考《皇帝与官僚:「上面」与「下面」》:\n3.2 天朝上国与蛮夷 鸦片战争以前,中华文明一直是相对独立地发展的,并以其优越性,向外输出,在东亚地区形成了以中国为中心的汉文化圈。\n长此以往,中国人习惯以居高临下的姿态,环视四方。清王朝正是在这种历史沉淀中,发展完备了「天朝」对外体制。\n在古代,依据儒家的经典,中国皇帝为「天子」,代表「天」来统治地上的一切。 皇帝直接统治的区域,相对于周边的「蛮荒」之地,为「天朝上国」。 而周边的地区的各国君主,出于种种动机,纷纷臣属于中国,向清王朝纳贡,受清王朝册封。 至于藩属国以外的国家,包括西方各国,清王朝一般皆视之为「化外蛮夷之邦」。\n3.3 奸臣与忠臣 在清政府禁止鸦片这种败坏国民的货物的贸易,销毁无数鸦片之后;英国贸易负责人向英国求援,请求派遣军队,通过武力打开中国市场。\n当时的中国元首,最高领导人道光皇帝在知道有胆敢犯上的蛮夷军队时,自然是一心剿灭。 不成想蛮夷军队,船坚炮利,连克广东城市,而又闻其是因为林则徐禁烟,受了「冤屈」, 找大皇帝作主伸冤,因英夷「情词恭顺」,于是心思由剿转向「抚」。\n而主抚停战期间,辗转听闻了臣下转述的,已经隐瞒许多实情的和约要求,又大为火光,旨意顿变,由主「抚」转向「剿」。 至此,大皇帝心意已决,「剿」,且天朝无任何败之理由。 直至,英军兵临南京城下,切断北方的粮食运输,道光妥协求和。\n令人惊讶的是,主持过战局的12个官员,几乎没有一个是如实向老板反馈实情的. 以致于到鸦片战争末期,道光老板还觉得英军只是船坚炮利,但腰硬腿直,不擅长陆战。\n每个主持的官员,战前都是和老板说,武备准备充足,定叫英夷有来无回。 但每场战争之后,老板收到的是城破师溃的消息,当然,下属不会直说,而是以各种故事来包装,美化自己,比如皆因汉奸协助云云。\n在鸦片战争中,主持战局的,除去林则徐,历史刀笔下的诸人,大多是「奸臣」,「卖国贼」。 为何会导致这种结果呢,而他们是否真的卖国呢?作者就给出了自己的解释:\n从功利主义的角度来看,这种说法首先有利于道光帝。 在皇权至上的社会,天子被说成至圣至明,不容许也不「应该」犯任何错误。 尽管皇帝握有近于无限的权力,因而对一切事件均应该负有程度不一的责任。\n但当时人们对政治的批判,,最多只能到大臣一级。 由此产生了中国传统史学,哲学中的「奸臣」模式:「奸臣」欺蒙君主,滥用职权,结党营私,致使国运败落; 一旦除去「奸臣」,圣明重开,拨月见月。不是党的政策不好,只是下面的人执行变了形,党是英明的。\n这一模式使皇帝直接免除了承担坏事的责任,至多不过是用人不周不察,而奸臣去承担责任,充当替罪羊。\n此外,按照「夷夏」的观念,这些蛮夷胆敢进犯天朝,唯一正确的方法就是来一个「大兵进剿」,杀他个片甲不留。 既然「剿夷」是唯一正确之途,时人也就合乎逻辑地推论: 战争失败的原因在于「剿夷」不力,之所以「剿夷」不力,在于有「奸臣」的破坏,能对战局产生影响, 肯定不止一个奸臣,那些战败的官员,一定没有尽忠报国。 与奸臣截然对立的,是忠臣的精忠报国。\n于是,时人把希望寄托于或阵亡(如关天培),或主战到底的官员(林则徐)。 他们的结论是:只要重用林则徐,中国就可能胜利,如果沿海彊臣均同林则徐,如果军机阁均同林则徐,中国一定会胜利。 看完全书的人会意识到,林则徐只不过是和其他人一般的清朝官员,只是开明些罢了。\n忠奸论所能得出的结论是,中国想要取得战争的胜利,只需罢免奸臣及其同党,重用林则其同志即可,不必触动中国的现状。 也就是说,只要换几个人就行,不需要做改革。\n忠奸论的最终结论是,为使忠臣得志,奸臣不生,就必须加强中国的纲纪伦常,强化中国的传统。 也就是,鸦片战争所暴露出来的,不是「天朝」的弊端,不是中华的落伍; 反而证明了中国的圣贤经典,天朝制度的正确性,坏就坏在一部分「奸臣」并没有照此办理。\n于是,中国此时的任务,不是改革旧体制,而是加强旧体制\n4 失败的价值 一个失败的民族在战后认真思过,幡然变计,是对殉国者最大的尊崇,最好的纪念。清军将士流淌的鲜血,价值就在于此。\n可是,清朝呢?它似乎仍未从「天朝」的迷梦中醒来,勇敢地进入全新的世界,而是依然如故,就像一切都没有发生。\n《天朝的崩溃》成书为20世纪末,人们说,19世纪是英国人的世纪。20世纪是美国人的世纪。21世纪呢?\n也有些黑头发黄皮肤的人宣称,21世纪是中国人的世纪。 可是,真正的要害在于中国人应以什么样的姿态进入21世纪?中国人怎么才能赢得这一称号。\n人们只有明白看清了过去,才能清晰地预见未来。 一个民族对自己历史的自我批判,正是它避免重蹈历史覆辙的坚实保证。\n而认清弊端,是修正弊端的必经之路。\n「后人哀之而不鉴之,亦使后人而复哀后人也」。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%A4%A9%E6%9C%9D%E7%9A%84%E5%B4%A9%E6%BA%83/","summary":"1 前言 历史教科书说,与英国的第一次鸦片战争,敲开了清朝的大门,清朝也因此和外国签订了一系列不平等条约. 而因为清朝的落伍,导致清朝不敌英国. 教","title":"天朝的崩溃"},{"content":"1 命运的必然 公元前219年,汉尼拨翻过阿尔卑斯山,向意大利发起进攻,指挥官科尔涅利乌斯迎战汉尼拨,被重伤。幸而从手下的新兵抢回到营地,而这名新兵,恰好是首次参加战斗的,科尔涅利乌斯的儿子,年仅17岁西庇阿。\n3年后,当罗马人集结举国之力,与汉尼拨展开坎尼会战,最终惨败, 仅有不足万人逃回了罗马。而身在岳父埃米利乌斯军团的西庇阿, 再次见证罗马军团败于汉尼拨的精妙战术之下,也又一次在汉尼拨的手下逃脱。\n公元前218年,汉尼拨从西班牙出发,翻过阿尔卑斯山,进攻意大利。为了让汉尼拨无法从西班牙获取支援与补给,并釜底抽薪,彻底把把汉尼拨关在意大利,西庇阿的父亲与叔叔,执政官科尔涅利乌斯与弟弟格奈乌斯各率领一个军团进攻西班牙。\n历时8年,科尔涅利乌斯与格奈乌斯终于夺取了迦太基统治下的西班牙三分之一的区域,但两支军团却需要去面对迦太基3支军队\n。公元前211年初夏,科尔涅利乌斯军团遭受到三支迦太基军队的围攻,兵力处于绝对劣势的罗马军队,其退路被切断,最后被一举消灭。迦太基的一支军队在打败了科尔涅利乌斯后,又袭击了不远处正在行军的格奈乌斯军团。在遭到三倍于自己的敌军进攻后,这支罗马军团与友军一样也被消灭。科尔涅利乌斯兄弟花了整整8年时间,取得的成果转眼化为云烟。\n罗马绝对不能放弃西班牙战线,为此需要一句统帅担任该战线的总指挥官。但此时,罗马已经向汉尼拨发起了正式攻势。在这种情况下,罗马没有可以动用的将领派往其他战线。\n一个年纪不大的年经人推开了元老院的大门, 在年已64岁的元老院第一人费边与59岁的“罗马之剑”马尔凯鲁斯眼里,这个24岁的年经人看上去,像个未成年人。\n他叫普布利乌斯-科尔涅利乌斯-西庇阿,他主动请缨,要求担任西班牙战线的总指挥官,代替战死在西班牙的父亲完成任务。在共和政体下的罗马,指挥一个由两个军团共2.5万名到3万名士兵所组成的作战单位的统帅,迄今为止,这类职位都是由执政官或法务官担任的。这两个官职的资格年龄都在40岁以上。\n一年后,西庇阿说服了元老院的元老,以「前法务官」之职指挥军团,即使现在他从来没有担任过法务官,时年25岁。\n2 闪耀西班牙 公元前210年,这位25岁的年经统帅,来到罗马军队在西班牙的军队,迎接他的是历经8年的艰苦战斗幸存下来的士兵,他所做的第一件事就是要消除士兵们的失败阴影。他把他们召集起来说,昨天的事情已经过去,一切明天开始。他还说,虽然自己年龄不大,但是,海神波塞冬在保佑自己,他甚至让大家相信,自己真正的父亲不是战死在西班牙的科尔涅利乌斯,而是海神波塞冬。(这些军事奇才们,为了笼住部下士兵的心,通常会绿掉自己的母亲)笃信诸神的罗马士兵,听了这番话以后,开始觉得自己一方一定能获胜。\n接着这位年经的统帅开始收集包含友邦马赛在内的所有地方的情报,包含地研,气候,原住民族的分页情况,迦太基军队所在的位置,兵力等等。\n而后,西庇阿的第一个目标就是汉尼拨在西班牙的根据地,家族经营10年的城堡,「新迦太基」卡塔赫纳。\n仅一天时间,西庇阿就攻克了敌人的根据地\u0026ndash;卡塔赫纳,而迦太基的三支军队根本来不及赶回来增援。\n通过这场战斗的胜利,这位年轻人使罗马完全恢复了两年前因其父亲和叔叔失败而失去的在西班牙的势力。\n次年,西庇阿迎战三支军队,趁着三支军队还没有合兵于一处,首先击败了汉尼拨二弟哈斯鲁鲍尔率领的军队, 消灭8000人,俘虏1.2万人。\n公元前207年,汉尼拨的幼弟马可尼与迦太基将领吉斯戈合兵一处,共7.4万士兵,与西庇阿4.8万士兵决战。最后,以汉尼拨的围歼战法,打败了迦太基的军队,仅有6000人逃离战场。\n公元前206年的冬季,西庇阿在已经到手的西班牙境内留下两个军团守卫,自己带着长期在这里坚守战斗的老兵,走海路回罗马,他已经四年未回那里了。\n3 进攻迦太基 回到罗马后,西庇阿向元老院汇报在西班牙的战况,即使他还不是议员,离30岁的年龄资格还差几个月。结束汇报后,没有要求举行凯旋仪式。他的战线无愧于凯旋将军的称号,但在共和政体的罗马,指挥一个战略单位,即两个军团的总指挥官资格必须是年龄40岁以上的执政官,前执政官,法务官或前法务官。\n西庇阿是破例作为总司令官被派往西班牙,而当时他的年龄只有25岁,即使完成西班牙霸权的现在,他也只有29岁。在这个年龄上要求元老院为他举行凯旋行为,似乎过于奢望\n对罗马将军来说,凯旋仪式是至高荣誉,西庇阿放弃了这一荣誉,取而代之,他要求元老院推荐自己为第二年即公元前205年度的执政官候选人。即使是明年,西庇阿也只有30岁,与执政官的年龄差距仍是10岁。\n结果,深受民众喜好的西庇阿,在市民大会上,以压倒多数的票数当选执政官。\n执政官是共和政体罗马的最高官职,同时也是军队的最高司令官,由市民大会选举产生。但是,这两位执政官的任地原则上通过抽签决定。但事实上,指挥一个战略单位即两个军团的所有总司令官,包括两位执政官负责的战场都是由元老院决定的。 但西庇阿请求元老院让自己负责北非战线。\n而后,西庇阿在元老院展开演讲,阐述自己的「围迦救罗」战略:\n早晚我会和汉尼拨交锋,但是,这场交锋,不是他在发动进攻的时候。我要主动出击,引他出击,让他不得不与我展开会战。战场不应该在卡拉布里亚已经毁了一半的城堡,而是在迦太基!\n因为这与现有的持久战战略相违背,也正因为持久战战略,才把汉尼拨逼到无处腾挪之地,现状的确没有必要改变,不然有让汉尼拨猛虎出笼的危险。但西庇阿的战法又的确有可取之处。最后,双方达到一个折中的意见,既不伤害元老院年长者的体面,又不违背年轻议员们的变革意愿:西庇阿的任地在西西里岛,并授予一个权力,如有必要,第二年可以进军非洲。\n只是作为执政官,他必须放弃指挥在首都完成编组的两个军团的指挥权,也就是放弃执政官应得到的指挥正规军的权利。取之代之,他有权在西西里招募志愿兵。元老院明确表示:将来西庇阿远征非洲,不是元老院认可的军事行动,一旦远征失败,责任不在元老院,而在他个人。\n公元前204年春天,西庇阿以前执政官身份,率领志愿军踏上了非洲的土地。以2.6万士兵,迎战迦太基与盟国努来米底亚联军,共9.3万人。以汉尼拨的围歼战术,再次获胜,直逼迦太基首都。\n恐慌中的迦太基政府,发出了召回汉尼拨的命令。\n4 击败汉尼拨 在《汉尼拨的遥望》一文中,曾经提到, 罗马人与汉尼拨进行过五次决战,前四次都以罗马人大败而收场,而现在终于来到了16年后的第五场会战,扎马战役。由归国的汉尼拨,迎战学习其战术的「学生」西庇阿。\n如果要列举古代5位名将,汉尼拨和西庇阿一定是其中的两位。如果要列举迄今为止历史上的10位优秀将领,他们二人无疑也会位列其中。虽然历史造就了无数优秀的武将,但是,发生在具备同等才能的人之间的会战,却少之又少。这少而又少的事情,就在扎马战场上演。\n在开战前一日,西庇阿和汉尼拨各带一队骑兵离开了各自的营地, 各带翻译,前往指定的地点. 指定的地点位于两军之间的一个小山丘上,骑兵留在山下,只有二将带着翻译继续前往。\n具有同等才华的名将交战极为罕见,在交战的前一天,这样的两个人坐在一起会谈,在历史上也是空前绝后的。\n公元前202年,秋日的阳光柔柔地照在扎马和纳拉格拉之间开阔的平原上。两军分别在这里摆下阵型。迦太基军队的总指挥是汉尼拨,战斗力为步兵4.6万人,骑兵4000人,合计5万人。罗马军队由西庇阿担任总指挥,步兵共3.4万人,骑兵6000人,共计4万人。\n扎马平原重现了14年前坎尼平原上发生的事情,只是,对象变了。\n古代屈指可数的名将,45岁的汉尼拨只有眼睁睁地看着自己的亲兵纷纷被杀,超过2万个战士在扎马遭到全歼,还有多达2万人被捕。余下的人向着10行程之外的首都迦太基逃去。汉尼拨只带着数名骑兵,逃离战场。扎马战役中,罗马方面的战死者是1500人,西庇阿完胜。西庇阿也成为唯一一个击败汉尼拨的罗马将领。\n普布利乌斯-科尔涅利乌斯-西庇阿从此被人们尊称为“阿非利加努斯”,意思是“征服非洲的人”。\n5 风流总被雨打风吹去 15年后,被尊称为阿非利加努斯,长年占据元老院“第一人”的西庇阿,被检举。质疑部分军费,在西庇阿远征马其顿王国中下落不明,要求其接受传讯与审查。甚至提起17年前,西庇阿在西西里曾经越权,前往任地外的地方攻打迦太基军队之事。\n像这种没有事实根据的检举,与其说是检举,不如说是弹劾,政敌把西庇阿赶下来的一种手段。审判的第一天,在两位检举人的轮番揭发中结束,第二天由被告为自己作辩护。\n「这让想起了《三体》里面的执剑人,罗维。放下剑后,被控谋杀罪」\n这一天,西庇阿迟到了,他带着一大群朋友和支持者来到人群簇拥的会场:\n两位护民官及罗马各位市民,今天,是我在非洲扎马与汉尼拔和迦太基军队作战,有幸取得胜利的第15个纪念日。在这样一个值得纪念的日子里,我建议让我们忘掉一切争执,大家团结一心,向诸神奉上我们的感恩之心。\n现在,我就要出发前往卡匹托尔山,向供奉在那里的、以最高神朱庇特和朱诺女神及密涅瓦女神为代表的诸神表示感谢,感谢他们给了我和参加那次战役的所有罗马市民为祖国罗马的自由和安全竭尽全力的机会。\n各位,如果你们愿意,我诚心邀请各位与我同行。希望各位和我一样对诸神心怀感激之情。因为从我17岁开始到已显老迈的现在,是罗马的各位市民给了我打破常规的机遇,让我有机会发挥了自己的才能。\n说完,没等人们开口回答,西庇阿就离开了会场,他的身后,不只有他的朋友们和支持者们。因为所有罗马人终于都想了起来,元老院议会站起了身,旁听的市民们离开了会场,连书记员都放下铁笔,跟随在西庇阿之后。会场只剩下两个检举人和西庇阿的政敌。\n历史学家李维写道:这一天,身着托加的西庇阿已经不见昔日的风采。他脑袋秃顶,身体羸弱。但是,市民们对他的敬爱之情,胜过第二次布匿战争结束,从非洲凯旋时,人们向三十几岁的年轻胜将发出喝彩声,并送上鲜花。李维还说:“这一天,成了西庇阿灿烂辉煌的最后一天。”\n第二天,西庇阿离开了罗马,在海边的别墅过起了隐居生活。4年后,西庇阿-阿非努加利斯在别墅出世,享年52岁,但他留下遗言,拒绝葬在祖祖辈辈的墓地里,原因是墓地在罗马境内:\n不知感恩的祖国,你们有何资格拥有我的遗骨\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%A4%A9%E9%80%89%E4%B9%8B%E5%AD%90%E8%A5%BF%E5%BA%87%E9%98%BF/","summary":"1 命运的必然 公元前219年,汉尼拨翻过阿尔卑斯山,向意大利发起进攻,指挥官科尔涅利乌斯迎战汉尼拨,被重伤。幸而从手下的新兵抢回到营地,而这名","title":"罗马人的故事(二):天选之子西庇阿"},{"content":"1 夫国以一人兴,以一人亡 经过三次,历经百年的布匿战争,腓尼基民族建立起来的北非强国迦太基灭亡,地中海被罗马人称为“我们的海”。\n可以说,如果没有布匿战争,即使罗马能称霸地上海,也不会只历经短短的130年。\n在三次布匿战争中,有如同漩涡中心一般的两个人,让罗马与迦太基的命运,漩向了两个不同的方向,其中一个,即为汉尼拨。\n2 蛰伏 第一次布匿战争,历时23年,因西西里岛的城邦大国锡拉库萨企图入侵邻国墨西拿而起,演变为新兴农业大国与传统海洋强国迦太基的全面战争,最终以罗马胜出,西西里岛的城邦或成为同盟国,或成为行省;迦太基全面退出西西里岛而结束。\n而后期指挥迦太基军队的总督,正是汉尼拨的父亲,哈米尔卡。哈米尔卡在第一次布匿战争的最后6年,曾经英勇奋战,但却不得不代表迦太基政府向罗马求和。\n当时还不到40岁的哈米尔卡为此备感屈辱,时刻不忘记一雪前耻; 而这样的意志,由年幼的长子,汉尼拨所继承。\n哈米尔卡在第一次布匿战争后,离开了纷争的祖国,带领家人与追随者,跨上了征服西班牙,建立新根据地之路。\n苦心经营10余年,西班牙成为迦太基新的财源,弥补失掉的西西里岛。在18岁那年,父亲在一场攻城战中去世,可即使如此,汉尼拨从未曾忘记父亲的遗志,征服罗马。\n26岁那年,在所有方面都已经成熟起来之后,汉尼拨担任了西班牙的总督。28岁,汉尼拨开始他复仇罗马的行动,从攻打罗马的同盟城市萨贡托开始。\n罗马与同盟城市关系类似互生互存,同盟城市享有与罗马市民一样的权利,除了无法像罗马市民那样享有选举权与被选举权,参与罗马的国政,而他们的义务即是在战时,为罗马提供兵力。身为盟主,罗马有保护同盟城市的义务。\n起初,罗马尝试通过外交手段来解决问题,但汉尼拨对此只是一味的拖延与搪塞。8个月后,萨贡托被攻破,全城居民被贩卖为奴,至此,罗马对迦太基宣战。\n3 出世 汉尼拨的终极目标是打败罗马,但以西西里岛为战场的第一次布匿战争已经证明,在意大利以外的地方作战,不可能战胜罗马。要战胜罗马,战场只能在罗马的土地上,在意大利境内进行。\n但意大利地形像一只靴子一样伸向地中海,东西两侧是海,南面是西西里岛,在当时,三侧防线可谓固若金汤,根本无法进入意大利本土。\n可走的路,只有从北方进入意大利。但是,那是一条未有前人尝试过的路。公元前218年,汉尼拨率领他的军队,向北横渡埃布罗河,翻过比利牛斯山脉,进入现在的法国,当时的高卢,再渡过罗纳河,横穿法国,最后,翻越阿尔卑斯山,进入意大利。\n其战略之宏大,2000年后的今天看来,依旧震撼。从西班牙出发时,汉尼拨有士兵共5.9万人,而5个月后,当他们历经风雪雨水,泥沼森林,翻越过阿尔卑斯山时,只剩下步兵2万人,骑兵6千人,一路上留下了多达3.3万人的尸骸,但他做到了前人未曾尝试的伟业。\n4 闪耀 第二次布匿战争,被罗马人称为「汉尼拨战争」,罗马人共与汉尼拨正面发生了五次会战,还有三次罗马人与其他迦太基将领的会战。\n前四次会战发生在相近的3年间。以武力闻名意大利的罗马,四次会战战果如下:\n4.1 第一次会战,提契诺战役 时间:公元前219年\n罗马指挥官:科尔涅利乌斯,兵力:两个军团,约2.1万 汉尼拨,兵力:2.3万从西班牙带来的士兵与中途加入的高卢士兵1万人,共3.3万人 战果:汉尼拨大胜,罗马指挥官被重伤。 4.2 第二次会战,特雷比亚战役 时间:公元前219年\n罗马指挥官:塞姆普罗尼乌斯,兵力4万人,其中骑兵4000人 汉尼拨,兵力3.8万人,骑兵1万人 战果:汉尼拨可忽略不计,歼灭罗马2万人,俘虏1万人,幸存者不超过1.5万人;汉尼拨大胜。 4.3 第三次会战,特拉西梅诺战役 公元前217年\n罗马指挥官,弗拉米尼,2.5万罗马士兵 汉尼拨,因为有不满罗马政策的高卢人加入,兵力涨到5万人 战果,以有备算无备,汉尼拨大胜。超过2万名罗马士兵战死,仅有2000人逃回罗马,而汉尼拨侧损失侧2000人,大多还是高卢士兵。 4.4 第四次会战,坎尼会战 时间,公元前216年\n罗马军,指挥官特雷恩蒂乌斯,兵力87200人,其中7200人是骑兵 汉尼拨,兵力50000人,其中10000人是骑兵 战果,汉尼拨大胜,战死5500人, 其中三分之二是高卢兵,罗马方战死超7万人。 4.5 战略之战 罗马统治意大利的方式,并不是用武力征服整个意大利,然后将原有民族变化奴隶,然后让罗马人移居到这些城市,以此来统治大片的土地。战败的城市,变成罗马的同盟城市,享有与罗马市民几乎一样的权利,承担相应的兵役义务。而汉尼拨认为,即使占领罗马首都,也没法彻底消灭罗马,消灭罗马的唯一方法,就是让罗马联盟分崩离析。只有这样打掉罗马的外围后才能一举占领对方的大本营\n因此,在获得4埸会战后,汉尼拨仍在坚持离间罗马与同盟城市的关系,而非直接挥军攻打罗马的首都。\n而接连在会战中输给汉尼拨,让罗马人不得不承认,在会战中,没有办法能战胜汉尼拨,因此执政官费边提出了「为了不输给汉尼拨,只要不交手的就可以」的持久战战略,主张围困汉尼拨,缩小汉尼拨的活动空间。\n就这样,汉尼拨与罗马进入了相持阶段,这一相持,就在罗马的国土上,相持了整整16年。\n5 暗淡 在汉尼拨与罗马相持及反围困的年岁里,一位年经的执政官提出了登陆汉尼拨的宗主国迦太基,开展「围迦救罗」的战略公元前204年春天,这位年轻的执政官,率领2.6万的士兵,踏上了非洲的土地。而后,迦太基在本国境内的第一次会战中,吃了败仗,尚不习惯这种事情的迦太基陷入了深深的恐慌中,恐慌中的迦太基政府接受了把汉尼拨召回国,与罗马人决一死战的提议。\n那一年,汉尼拨接到了回国的命令。现存文献中找不到任何描述他接到命令时内心波动的记录。这一年,他已经44岁,距离进入意大利,第16个年头快要过去了。\n为了父亲的遗志,为了自己的理想,这个男人在敌国领土,坚持了16年。而在这16年间,除了一次有4000名士兵在罗马军队的进攻下投降罗马以外,没有一个人,真的没有一个人,离开汉尼拨。\n汉尼拨并不是一个随和的人,更别说和士兵们打成一片,既然如此,在任何时间都不失孤傲的汉尼拨被逼进弹丸之地后,士兵们依然追随于他,空间是为什么呢?\n也许像马基雅维利说的那样,一方面可能是慑于他的威严,另一方面,对这位才能卓越却陷于困境的男人,也许有一种谅解的情感。\n一位领袖之所以优秀不是因为他具备卓越的才能,而是他能让追随者觉得自己在这个集体中必不可少。人与人之间能长期维持的关系,一定是相互依存的关系。不是相互依存的关系,很难指望会长久。\n汉尼拨接到回国命令时,就在克罗托内。克罗托内是一个港口城市,向南延伸的一个海角有一座神殿,供奉的是这一带希腊族信奉的女神赫拉。\n现在,神殿只剩下一根圆柱,在古代却是一个因漂亮外形而闻名的神殿。接到回国命令后,44岁的迦太基统帅命人在这座神殿祭坛的一面墙上,嵌入一块刻有文字的铜板。铜板记录了汉尼拨离开西班牙以后的所有战果。\n(罗马人对汉尼拨留下的这些东西一定恨之入骨,但是,直到50年后,历史学家李维看到时,这块铜板依然完好无损,不得不说,罗马人很有意思)\n汉尼拨乘船返回迦太基,船队离开克罗托内港,向迦太基驶去。\n很长一段时间,可以从船上清楚地看到矗立在海角前端的白色大神殿渐渐远去,直到消失在遥远的地平线。没有史料记载快满45岁的汉尼拔是怀着怎样的心情遥望这座神殿的。也许他根本就没有看\n而漩涡中心的另一人,名为西庇阿,即是那位年轻的执政官。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E6%B1%89%E5%B0%BC%E6%8B%A8%E7%9A%84%E9%81%A5%E6%9C%9B/","summary":"1 夫国以一人兴,以一人亡 经过三次,历经百年的布匿战争,腓尼基民族建立起来的北非强国迦太基灭亡,地中海被罗马人称为“我们的海”。 可以说,如果没","title":"罗马人的故事(二):汉尼拨的遥望"},{"content":"来自《罗马人的故事》第一册,介绍罗马的时候,总离不开希腊文明。\n希腊文明起源于公元前2000年前后的克里特岛。因为特里克岛比希腊本土更靠近当时的先进文明之国埃及。新的文明往往出自自身的周边\n1 克里特文明 克里特文明的鼎盛时期据说是在公元前1700年到前1500年前后(要用据说一词,大概是因为太久远,没有相应的史料)。\n以公元前1350年前后为界,爱琴海的主人克里特文明急速衰退,不清楚是大地震的缘故,还是因为来自希腊本土的进犯。\n总之,到了公元前1350年前后,首都克诺索斯遭到破坏,优雅而华丽的克里特文明敲响了晚钟。\n2 迈锡尼文明 曾经的周边变成了中心,在它周围又形成了新的周边。位于希腊本土南部伯罗奔尼撒半岛的迈锡尼一带成了希腊文明新的中坚力量-历史上称为迈锡尼文明。当时好像还是军人统治的国体,这些军人因荷马史诗《伊利亚特》和《奥德赛》而为后世的我们所熟知(的确令人惊讶,这样的世界名著,竟然成书于3000多年前)\n然而,以公元前1200年为界,迈锡尼文明也消失了,作为迈锡尼中坚力量的人或被杀,或被逼为奴,从而被彻底挤出历史的舞台,被北方南下的多利亚民族所消灭。在此以后,整个希腊沉寂了整整400年,公元前1200年至前800年长达400年的沉寂时期,在希腊史上称作“希腊的中世”,意思是一切归于沉寂,夹在以活跃为特点的两个时代的时期。\n3 城邦国家时代 公元前800年前后,希腊人走出他们的“中世”,进入统称为城邦国家的时代。由多利亚人建起来的斯巴达和因多利亚人入侵而出逃的阿卡亚人建立的国家雅典成为城邦国家的代表。随着城邦国家的诞生,希腊人开始了向海外的殖民运动。\n希腊人的殖民运动分两个时期进行,第一次是殖民运动是在公元前9世纪末到前8世纪初,殖民对象主要集中在小亚细亚西岸。爱琴海的意思是多岛海。\n第二次殖民运动是在第一次殖民运动之后,即过去了约半个世纪的公元前8世纪中叶前后。这一时期的殖民范围从爱琴海扩展到整个地中海。\n希腊本土的希腊人去得最多的地方是意大利南部,在海上可以与他们抗衡的,在当时只有由腓尼基人殖民而建起来的迦太基\n对于希腊人来说,公元前8世纪是向海外发展的时期,也是充实国内的时期。正是在这个时期,形成了最能有效发挥希腊人活力的城邦国家,而希腊人发明的国体\u0026ndash;城邦\u0026ndash;的代表就是雅典和斯巴达\n3.1 雅典 3.1.1 贵族政体 传说中雅典的创立者是推翻了克里特暴君米诺斯王的忒修斯,雅典初斯的政体是王政,并于公元前8世纪改为贵族政体。\n在该政体下,9们贵族出身的执政官在一年的任期内,分管内政,军事和宗教,由其他贵族组成的长老会辅佐,自由市民组成的市民大会没有发言权,形同虚设.\n3.1.2 梭伦改革 进入公元前7世纪,这种贵族政体渐渐地暴露出与雅典现状的不相适应,相对于经济基础建立在土地所有制上的贵族阶级,依靠工商业强大起来的新兴阶段开始抬头,但是却空有经济实力却无法参与国政。开始了反抗贵族之路。\n这些自由市民取得的第一个胜利是公元前620年的法律条文化,贵族阶级也因此失去了司法权,无法像在法律不成文的时代那样随心所欲。\n但是这并不能消除“自由市民”的一不满,于是,梭伦登场了,公元前594年,开始了历史著名的“梭伦改革”。\n梭伦自己既不属于新兴的工商业阶级,也不是出身重债缠身的自耕农阶级,而是拥有大片土地,在雅典举足轻重的名门望族之后。\n梭伦首先制定了拯救被重债缠身的自耕农的政策并使之法制化。为此,农民的债务被大幅度地削减。\n同时,他还废除了因无法偿还政务而被迫为奴的旧制度,彻底废除了在古代被认为理所当然地以人身偿还债务的制度,这是古代社会第一个尊重人权的例子\n梭伦自身似乎是个温和的自由主义者,他拒绝了激进派市民的提案,即没收私有土地为国有,然后将土地重新进行平等分配的提案,对此梭伦写道:\n我们给了市民们适当的名誉。我们不剥夺他们已有的权利,但也不再新加任何权利。\n梭伦改革的最大着眼点是政治改革:他首先开展了人口改革,并以调查结果为依据,制定了个人权利与其所拥有的不动产成正比的政策。如此一来,参与国政的权利不再受出身左右。\n梭伦根据财产的多少,将雅典市民分成四个等级,根据收入,从高到低依次为第一等级,第二等级,第三等级及无产市民构成的第四等级。\n各等级业务:\n第一等级,第二等级:义务服骑兵兵役,自备军备,军装与马匹 第三等级:义务服重装步兵兵役,自备军备,军装 第四等级:义务提供轻装步兵或舰队成员 各等级义务:\n政府要职由第一,第二等级的市民担任 行政官僚由第三等级担任 第四等级只有选举权 3.1.3 克利斯梯尼改革 开始投资动产的雅典市民,迟早会对以不动产为基础的政体心怀不满,只是没人敢于正面挑战梭伦的权威。在梭伦死后,庇西特拉图作为独裁者登上了历史的舞台。政体的变迁可以从教科书上学到,但是判断一种政体的好坏,有时和教科书不一样,在庇西特拉图独裁的20年间,不仅给雅典带来和平与秩序,还带来了经济上空前繁荣。\n公元前510年,庇西特拉斯的儿子的独裁统治被克利斯梯尼推翻,面对矛盾重重的雅典,克里斯梯尼开始了他的改革。借用亚里士多德的话,他“将体制改革得更加民主”\n行政改革:将雅典的领地分成三大区,每个大区划分20个小区,各小区根据人口再分若干“居民区”。而后, 这种“居民区”成为雅典的行政基础。此项改革被认为是历史上第一个因单纯行政上的目的而将国土进行分割的例子 政治体制改革:克利斯梯尼的改革出现的政体叫民主政体 强化市民大会权力:20岁以上的所有雅典市民有权参加市民大会,实行一人一票(直接民主)。成为国家最高权力机构,决定与外国缔结和约等外交及政府首脑选举等内政事务 保留梭伦改革的四个等级,但划分标准从原来的农业收入,变成无行业区别的收入 五百人会议:由30岁以上的雅典市民组成,负责处理日常政务。(即官僚或政府公职人员) 国家战略官:由任期一年的10人组成政府官员,重新命名为“内阁” 陶片放逐法:市民可以将自己希望放逐的人名字写在陶片上,在市民大会上投票,每年只要过半数,就有权把市民认为权威和权力将会威胁雅典的市民逐出国外10年。放逐不会损害该市民的名誉,即使遭到放逐,当事人也不会觉得羞愧,他不会失去市民权,财产不会被没收,只是被逐出雅典。显然是为了防止某人独裁(但是也会被用来对付政敌) 由此,诞生了世界史上第一个由普通市民直接参与国政的政体。在雅典,无论多么无知,只要是市民,他的权利都受到绝对的尊重。但是不具备市民权这一形式的国籍的人则完全没有参政权。关于成为市民的条件,可以参考罗马人的故事第一册提到的市民条件\n苏格拉底说过,祖国的法律即使不好也要遵守,为此他拒绝了逃亡国外的劝告,而被处以死刑。同时哲学家的亚里士多德则不愿殉法,早早溜之大吉。对于雅典市民来说,雅典是他的祖国,对于出生地不在雅典的亚里士多德,他没有义务为雅典的法律牺牲自己。\n但是,和市民缴纳同样的税金,非但没有被选举权,甚至连选举权也得不到承认的国家,在当今世界也不少见(比说要求人民感恩政府的某国)\n3.1.4 伯利克里时代 伯里克利从进入雅典政界的那一年起,在长达30年的时间里,几乎年年当选“国宝战略官”,并且大部分时间被选为议长。同时代的历史学家修昔底德告诉我们,伯里克利曾经说过这样一段话:\n我们雅典人无须羡慕任何其他国家的政体。我们的政体不是模仿他国得来的。我们的制度要成为别人的模范。我们的政体之所以称为民主政体,是因为政权在多数公民手中,而非少数人手中。\n在这一政体下,每个人在法律上都是平等的;担负公职的人能够得到的荣誉,不是因其出身,而在于他的努力和贡献。任何人,只要他能够对国家有所贡献,绝对不会因贫穷而默默无闻。\n我们的日常生活和政治生活一样,享有充分的自由。雅典市民享有的自由程度之高,甚至连怀疑、妒忌都是自由的。……尽管如此,我们可以享受各种娱乐,丰富我们的精神世界,忘却日间劳作的辛苦。每年在规定的日子里,举行各种比赛和祭祀,不忘让我们的居所变得更加舒适。……\n在教育制度上,我们的竞争对手(隐指斯巴达人)从孩提时代起,即加以最严格的训练,使其成为勇敢的人,而在我们的国家里,对孩子的教育没有他们那样严酷。但是,当危机来临时,我们表现出来的勇气不在他们之下。\n我们不学习他们通过非人的残酷训练来应对考验,我们用每个人所具备的能力,即决断力,来应对考验。我们的勇气不是产生于法律的要求,而是源于每个雅典市民在日常生活中各自的行为准则。……\n我们爱美,但我们有度;我们尊重智慧,但绝不迷恋于此;我们追求财富,但我们只会尽可能地利用它,而不以此炫耀。\n在雅典,贫穷不可耻,可耻的是不为脱离贫穷而努力。\n我们尊重个人利益,却是为了更加关心公共利益。这是因为在追求个人利益为目的的事业中表现出的能力,同样可以服务于公共事业。\n在雅典,一个不关心政治的人,我们不会认为他爱好和平,我们认为他不具有市民的资格。\n时隔2500年后,作为一句x国人,读起这段话,不免心生感慨: 2500年前雅典人的权利与追求,我们到现在都还没有实现,甚至越走越远\n与伯里克利同时代,留下《伯罗奔尼撒战争史》的历史学家修昔底德对伯里克利的雅典所作的评价是:\n表面上看实行的是民主政体,实际上是一个人统治的国家\n3.2 斯巴达 公元前1200年前后,多利亚民族挥军南下,征服了土著居民后,建立了城邦国家\u0026ndash;斯巴达。\n征服者的子孙,构成现有统治阶级的斯巴达人,他们是自由市民及其家人,共约1万人。服兵役是这些血统纯正的斯巴达人的唯一工作,参与国政的权利也只有这些人享有\n有市民权的斯巴达人并非一成年便有权出席市民大会,行使自己的一票权。他们必须等到30岁才能享有这些权利。市民大会由30岁以上的斯巴达人组成,险些之外,还有长老会议。\n长老会议:共有议会28人,年龄都在60岁以上,由市民大会选出,任期为终身制 国王:人数为2位,由两家名门望族世袭,同时执政。即二头政治 公元前7世纪后半中,来库古进行的改革进一步稳固了这一体制,使得斯巴达的风格更加激进,与梭伦的改革决定了雅典的风格一样,来库古决定了斯巴达的风格\n3.2.1 来库古改革 来库古的改革使斯巴达更加彻底成为一个军事大国,斯巴达人的日常生活更是以军务为至高目的。\n孩子一出生,就要经过长老们的检查。经过检查,判断一个孩子是否能健康、平安地长大,被认为不够健壮的孩子当即会被抛弃或沦为奴隶。\n被认为有希望成为强壮战士的婴儿由父母抚养到6岁,一到7岁,便要离开父母开始集体生活。他们与同龄人一同生活,按照以培养合格战士为目的的严密计划接受教育。\n到了20岁,斯巴达人就开始服兵役,一直到60岁退役。30岁之前有义务过集体生活,即使结了婚,晚上也必须回到兵营。无论是少年的宿舍还是战士的兵营,都没有相应的建筑物,他们都要生活在帐篷里。这样做的目的是为了使斯巴达人可以忍受恶劣的环境。\n在过了30岁才被认为是独立市民的斯巴达人,可以和妻儿一起在有墙有屋顶的室内生活,也只有独立市民才能享有这种权利。\n由于斯巴达人一切都服从于军事目的,所以其军事力量之强大令人惊畏。尽管军队数量很少,但是其威名甚至远震波斯。在希腊,一提起精锐部队,指的就是斯巴达的步兵军团。\n但是,斯巴达除了战士什么都不产。哲学、科学、文学、历史、建筑和雕刻,没有留下任何一样东西。非要说留下了什么东西,那就是一个词——“斯巴达式的”。\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E5%B8%8C%E8%85%8A%E6%96%87%E6%98%8E/","summary":"来自《罗马人的故事》第一册,介绍罗马的时候,总离不开希腊文明。 希腊文明起源于公元前2000年前后的克里特岛。因为特里克岛比希腊本土更靠近当时","title":"罗马人的故事(一):希腊文明"},{"content":"1 前言 以史为鉴,可以知兴替。而罗马,作为西方文明的来源之一,有着漫长辉煌无比的历史,其影响直至今天仍可见。因此,我开了个新坑,阅读盐野阿姨的《罗马人的故事》。当然,这只能算是历史的科普书,其学术价值约等于当年明月的《明朝那些事》,就很适合我这样的非专业人士阅读。\n根据传说, 罗马于公元前753年建国, 根据史实, 罗马于公元前270年完成了意大利半岛的统一\n2 王政时代 2.1 建国之王罗穆路斯 18岁的罗穆路斯, 带领3000名拉丁人, 定都罗马于台伯河东岸. 将国政分成三个机构, 分别是国玉, 元老院和市民大会, 并由这三方共同治理罗马. 就这一分权的举措, 就与东方帝国走上了不同的道路.\n国王: 作为宗教祭祀, 军事和政治的最高领导人, 国王由市民大会投票选举产生 元老院: 由贵族长老组成, 其职责是向国王提出忠告与建议 市民大会: 由全体罗马市民组成, 它的任务是选出以国王为首的各级政府官员, 市民大会没有制定政策的权力, 但是对国王制定的政策有赞成或反对的表决权. 此外, 对外关系上, 是战是和, 也必须说征得他们的同意才可实施 2.2 第二代国王努马 在适当的时候, 把适当的人放在适当的位置上施展才华的事例, 在各民族走向兴盛的历程中比比皆是. 而领导者政德不修, 让整个国家与民族走向衰頹, 也正在上演.\n罗穆路斯死后就任的国王是努马, 历史学习李维在\u0026lt;罗马史\u0026gt;中关于努马的功绩是这样描述的:\n就任王位后的努马, 试图对依靠武力和战争打下建国基础的罗马进行立法和习俗的改革.\n这里所谓的立法改革, 不是要制定全新的法律, 而是要建立秩序, 要让当时逞强好胜的罗马人懂得做人的礼法. 在了解自身力量的局限性的同时, 让他们懂得要对超越自身极限心存畏惧.\n努马认为除了为防御而战之外, 这一时期的罗马不需要战争. 他集中力量发展农业和畜牧业. 目的是在战争取得服务后, 即使不对失败者进行掠夺也能做到自给自足.(在2700年前, 有这样长远的眼光, 着实令人敬佩)\n努马还对罗马市民进行了职业分工, 让每个人归属于有独立保护神的团体.\n努马为了使人们的日常生活变得有序, 还进行了历法改革, 根据月亮的盈亏, 把一年分成12个月, 规定总天数为355天(而650年后, 恺撒才重新修正历法, 把一年总天数定为365天, 也就是今天依旧沿用的历法)\n因为罗马是多神教, 努马还对这些神进行了整顿, 设立了等级制度, 让大家懂得尊重诸神的重要性(有信仰的确让人心存敬畏). 像犹太教, 基督教即是一神教, 即只允许有信仰一个神.\n一神教和多神教的区别不只在于纯粹的神的数量, 还在于是否认同他人信奉的神. 认可他人的神, 意味着认可他人的存在.\n希腊历史学家狄厄尼索斯在其著作\u0026lt;古罗马史\u0026gt;中说过这样的一句话:\n使罗马强大起来的要因在于他们对宗教的见解之中\n对罗马人来说, 宗教不是指导原理, 它只是精神寄托. 因为有宗教信仰, 人性不再受到禁锢. 想到在某个政党的统治下, 连信仰自由这样的自由都失去了.\n和罗马人一样从不向神祈求纠正人类伦理道德的希腊人转而在哲学中探索真理. 因此:\n向宗教寻求纠正人类行为准则的是犹太人\n向哲学寻求纠正人类行为准则的是希腊人\n向法律寻求纠正人类行为准则的是罗马人\n2.3 第三代国王托里斯-奥斯蒂吕斯 继努马之后, 登上王位的是托里斯-奥斯蒂吕斯. 他是拉丁系罗马人, 和罗穆路斯一样, 是个崇尚对外进攻的男人.\n托里斯攻占了拉丁民族的发祥地, 阿鲁巴, 并将居民强行迁居罗马, 但给予他们罗马市民的身份, 吸纳他们成功罗马国民, 进一步壮大罗马.\n托里斯率领罗马军队一次又一次出征, 取得比罗穆路斯还辉煌的军事战绩, 他的统治历时32年, 不过根据历史学家李维的说法, 他是死于雷劈.\n2.4 第四代国王安库斯-马尔西乌斯 安库斯是努马的外孙, 成为国王后统治长达25年, 其中免不了挥军与罗马周围的部族战斗. 除了战斗, 他还完成了几件大事:\n在台伯河架起了第一座桥梁, 上的是把位于西岸的贾尼科洛山和集中在东岸的七个山丘联系起来 他征服了位于台伯河河口的奥斯提亚, 为此罗马终于得以和地中海直接连通. 并在奥斯提亚周边的海滩发展制盐业, 为罗马人提供了不是流通货币的货币( 2.5 第五代国王塔克文-普里斯库斯 塔克文, 一个来自伊特鲁里亚的移民, 在国王死后, 他毛遂自荐要竞选罗马国王, 大概是开展选举活动的第一个罗马人.\n成为罗马第五代国王的塔克文显示出了他超强的领导能力. 在37年的统治期间, 不仅使罗马的势力范围得到进一步的扩张, 而且罗马内部也发生了巨大的改变, 同时市民的生活水平也得到极大的提升, 罗马一跃成为名副其实的罗马城邦.\n他即位后做的第一件事情是增加元老院的人数, 自从罗穆路斯设立元老院以来, 人数一直维持在100人, 而塔克文将它增加到200人, 以此塞进自己的忠实支持者, 以稳固自己的权力\n塔克文与前任国王们一样, 继续征战四方, 但战斗结束后, 他没有让战败者移民罗马, 而是给他们市民权, 继而同化他们.\n他还开发罗马人居住的七个山丘之间的湿地, 挖掘下水渠, 构建大规模的下水道网络. 塔克文领导下的排水开垦事业不仅增加了可用土地资源, 而且也为罗马形成一体, 促进各民族之间的交流, 直到了不可磨灭的作用.\n而后, 塔克文还在最高的卡匹托尔山丘建起神殿, 专门供奉罗马诸神中的最高神朱庇特神, 其他诸神也都各基所.\n2.6 第六代国王塞尔维乌斯-图里乌斯 在当时先王的排水开垦事业和朱庇特神殿建造工程完成之后, 作为先王女婿的国王塞尔维乌斯-图里乌斯的之急就是保卫全罗马的城墙建设.\n这座城墙在经过了2500年后的今天依然叫\u0026quot;塞尔维乌斯城墙\u0026quot;, 在现代罗马, 随处可见其断壁残垣.\n在塞尔维乌斯成就的功绩中, 最重要的莫过于军队体制的改革. 他所进行的这一改革不仅涉及税制改革, 而且涉及选举制度的改革.\n作为一个国民, 他所承担的义务一是缴纳税金, 二是保家卫国. 在古代, 很多国家都以服兵役的形式来抵直接税, 罗马如此, 希腊如此. 只有做到这一点的, 他才是独立的市民. 作为独立的市民, 自然会有相应的权利. 市民的权利就是投票权. 所以军队体制等于税制, 也等于选举制, 这一等式成立, 并且天衣无缝.\n而2500年后, 仍然有某些国家, 只有缴纳税金的义务, 却没有选举的权利.\n另外, 罗马选举制实行的不是一人一票制, 而是按军团的最小单位, 每百人队一票. 百人队中的100个人首先要在内部进行讨论, 形成的统一意见就体现在这一票上. 其实, 它相当于小的选举区制.\n看起来是否很熟悉, 个人感觉, 美国的选举人制度就是来借鉴自罗马的百人一票, 总统选举, 在某个州赢得50%的票, 就可以赢得全部选举人票.\n2.7 最后一位国王: \u0026ldquo;傲慢者塔克文\u0026rdquo; 当塞尔维乌斯执政44年之后, 野心勃勃的塔克文的孙子反叛国王, 与妻子, 即国王的女儿一起谋杀了国王. 并在国内实行独裁统治, 从来不向元老院征求任何意见或建议, 也从业不问市民大会同意与否, 因此市民在背后称其为\u0026quot;傲慢者塔克文\u0026quot;\n在国内实行独裁统治地专制君主\u0026quot;傲慢者塔克文\u0026quot;在军事方面却表现出卓越的才能. 在与周边部族的战斗中, 罗马几乎都是常胜军.\n在一个人强大的时候丑闻不会招惹你, 而一旦显出疲态, 丑闻将毫不留情地击垮你. 即使丑闻与你无关, 但是作为有效武器, 它的作用不可小觑.\n国王有一个儿子叫塞克斯图斯, 看上了亲戚科拉提努斯的妻子琉克蕾西娅, 欲火中烧的年轻人乘琉克蕾西娅的丈夫不在家的夜里, 来到女人的家中, 用短剑相威胁, 占有了女人的身体.\n当天夜里, 琉克蕾西娅就给在罗马的父亲和正在出征的丈夫分别送去一封信, 令其速归. 坐在床上沉浸在悲愤之中的琉克蕾西娅向赶来的父亲与丈夫及朋友说完事情的经过, 就拿出短刀刺向自己的胸膛, 她呼吸艰难地要在场所有男人发誓为她报仇后, 就永远地闭上了眼睛.\n琉克蕾西娅的遗体被送到罗马, 放置在古罗马广场的演讲台上, 面对这一惨状, 人们纷纷指责国王和他一家的蛮横与傲慢. 因此有丈夫的朋友布鲁特斯向市民作出演讲, 历数国王的罪行, 提议将国王和他的家人逐出罗马. 市民纷纷云从.\n\u0026ldquo;傲慢者塔克文\u0026quot;的统治持续了25年, 随着第七代国王塔克文的统治结束, 罗马的王政时代也宣告结束. 时间是公元前509年. 从罗穆路斯于公元前753建国到这一年, 罗马已经走过了244年.\n3 共和时代 随后的罗马进入了共和政体, 迎来了执政官统治的时代. 和从前一样, 执政官也由市民大会选举产生, 任期由终身改为短短的一年, 还有, 原来由一位国王统治改由两位执政官共同治理.\n在战时, 可以任命独裁官, 任期为6个月, 所有人需听从独裁官命令(包括执政官)\n3.1 路奇乌斯-尤尼乌斯-布鲁特斯 巧妙利用丑闻推翻王政的最大功臣是路奇乌斯-尤尼乌斯-布鲁特斯. 他是随后延续500年的共和制罗马的创始人.\n路奇乌斯-尤尼乌斯-布鲁特斯是历史上难得一见的, 兼具先见之明和行动力的人. 因为他的母亲是被逐出罗马的国王塔克文的姐妹, 所以他和国王是舅甥关系. \u0026ldquo;布鲁特斯\u0026quot;这个姓不是他的原姓, 而是他的外号, 意思为\u0026quot;傻瓜\u0026rdquo;. 据说他在专横跋扈的塔克文时代, 一直隐忍着被蔑称为\u0026quot;傻瓜\u0026rdquo;, 结果, 这个外号就成了他的姓氏.\n他认为罗马已经长大, 完全可以废除效率很高却只受国王个人意志左右的王政制度. 他在与被放逐国王企图夺回罗马的一战中, 与国王之子, 表兄阿隆斯激战对决, 双双殞命.\n3.2 \u0026ldquo;亲民者\u0026quot;瓦莱里乌斯 共和政体的创立者布鲁特斯的壮烈牺牲让罗马人悲痛不已. 然而他们的眼泪未干, 就开始猜疑了幸存的执政官瓦莱里乌斯. 认为瓦莱里乌斯凯旋时所乘战车为四匹白马, 过于高调, 有炫耀王者风范的意思, 再者瓦莱里乌斯家里富有, 位置居市中心, 建筑气派, 像国王的居所.\n瓦莱里乌斯为避流言, 连夜拆除自家的房子, 并于便宜地段建了简陋的房屋, 并向众人自由进出, 以示一心为公.\n瓦莱里乌斯而后制定法律, 改善国政:\n制定有关国库的法律, 在王政时代, 国库由国王掌管, 现在则交由财务官管理. 作为政治军事最高权力者的执政不干预国家财政一法赢得了市民们的喝彩. 制定了诉讼的法律: 凡是享有罗马市民权的人, 对法务官作出的判决有权向市民提起诉讼. 有点难以想象, 这是2500年前的法律, 着实体现出对人的权利的尊重. 它的制定, 为后世罗马留下了极其重要的法的概念 过度在乎舆论而制定的一条法律: \u0026ldquo;凡是觊觎王位之人, 无论是谁, 其生命和财产将为诸神所有\u0026rdquo;. 也就是说, 即使杀了人, 只要有证据证明被杀的人对王位有所企图, 就可以赦免杀人者. 证明企图这个本身就相当模糊, 就相当于在法律上开了个免除罪罚的口子. 在公元前509年至前503年的6年间, \u0026ldquo;亲民者\u0026quot;瓦莱里乌斯共当选了四届执政官, 因此期间实施的政策可以认为基本出自这位\u0026quot;亲民者\u0026rdquo;:\n盐收归国有: 过去奥斯提亚盐田出产的盐是由个人经营, \u0026ldquo;亲民者\u0026quot;经营权收了回来, 改由政府经营. 他试图通过这一改变, 来恢复因伊特鲁里亚人外流而日渐下滑的罗马经济. 当时罗马还没有流通货币, 盐在交易外国商品中充当了货币的角色. 相当于货币国有化.\n如果改革仅此而已, 那么只能使用高价盐交易商品的商人对通商的兴趣就会大大减弱, 对恢复经济于事无补. 于是, \u0026ldquo;亲民间\u0026quot;降低了向他们征收的间接税, 因此还吸引了一些本不从商的人也开始纷纷从商(通过税收调整经济政策, 2500年前执政者都知道使用的手段, 某些国家, 就只会在经济下行的时候, 还加税)\n\u0026ldquo;亲民者\u0026quot;非常欢迎外国人移民罗马, 在罗马邻近部族中, 有人说同属拉丁民族之间, 拥有相同语言和相同诸神的拉丁人之间相互争斗毫无意义. (可见, 罗马人是相当开放的, 并没有以血统论身份, 而是真的做到, 来了就是罗马人)\n公元前503年, 罗马改为共和制已经6年了, 这一年, \u0026ldquo;亲民者\u0026quot;撒手人寰, 离世人而去, 此时的瓦莱里乌斯已经散尽万贯家财, 边丧葬费都拿不出来, 是每个罗马人自发捐款, 为\u0026quot;亲民者\u0026quot;举行了葬礼. 和布鲁特斯死时一样, 罗马女人像为父亲离世那样, 服丧一年\n罗马共和政体由布鲁特斯播下种子, 又在\u0026quot;亲民者\u0026quot;的施政中深深扎下了根. 在这两人之后的罗马, 再也没有出现过试图复辟王政的人.\n3.3 贵族与平民的对立 在进行共和政体的罗马, 在其后的80年, 一直到公元前367年, 始终处于摇摆不定的不稳定状态, 贵族和平民之争一直没有得到有效的遏制. 造成这种情况的原因可以列举如下几个:\n归咎于农牧民族的罗马人自古以来的保守性格. 罗马人本能地厌恶改革, 即使璚非改革不可的时候, 进展也很缓慢. 一旦改革成功, 不会轻易改变. 罗马贵族抗争的态度非常强硬, 并且, 罗马的贵族阶级拥有强大的力量, 足以和平民阶级一决高下. 尽管罗马平民强烈要求少数人统治的政体下的机会均等, 但是他们并没有要求改变少数人统治的政体, 即寡头政治. 尽管他们要求授予自己的代表以统治的权力, 但是他们并没有要求让平民阶级的所有人都参与政权. 王政时期的罗马:\ngraph 王政{ rankdir=LR; 国王 -- 市民大会; 元老院 -- 市民大会; 元老院 -- 国王; } 共和政体的罗马:\ngraph{ 执政官 -- 市民大会; 元老院 -- 市民大会; } 国王是终身制, 由市民大会选举产生, 经元老出家人确认同意, 一位国王只要在王位上坐上30-40年, 势必与元老院的关系变得很松散, 权力的独立性也会很高.\n因为元老院的职责只剩下向国王提建议和劝告. 与此相反, 所有罗马市民都可以参加市民大会, 因为有权对国王行使的政治策略和军事行动投票赞成或反对. 因此, 国王政体的权力构造呈三足鼎立, 是非常稳定的.\n进入共和政体的罗马, 权力构造发生了变化, 由两个执政官同时执政取代了以前的国王, 尽管可以多次当选, 但每次任期都是一年, 而年年选择两位执政官的, 就是各派势力首脑组成的团体\u0026ndash;元老院. 于是, 执政官和元老院之间的距离自然是逐年缩短, 渐渐地, 三足中的两足出现重叠, 直至合二为一.\n共和政体诞生之初的十几年里, 罗马不得不举国一致共同对外, 但是与此同时, 罗马的平民阶级也认识到自己的力量, 他们意识到, 没有他们的参战, 无何止的战斗既不能取胜也无法坚持下去.\n几乎年年不断的战事使他们不得不长时间离开他们工作的农田, 牧场, 施工现场或商店, 直接导致平民阶级的经济状况越来越差. 另一方面, 贵族阶级有大片的农田, 牧场作后盾, 即使不劳动, 经济也不至于很快衰退.\n对抗越演越烈, 平民甚至还有在参加战斗期间, 家里财产因负债被出售或没收的风险, 因此民愤激昂, 外敌来犯时, 再没有人响应执政官的号召, 一致对敌, 平民们固守在埃斯奎里山和阿文庭山,拒不出来, 这是罗马历史最早的罢工运动. 而后罗马市民又进行了第二次罢工运动\n最终的结果是, 与贵族谈判后, 决定设立一个专门以保护平民阶级利益和权利为上的的职位, 这个职位叫护民官, 就任职位的必须是平民阶级出身. (这就是最早的, 用脚投票的结果, 权利不是别人施舍来的, 是自己争取来的). 护民官有权对执政官作出的决定行使否认权, 但限制时, 战时不得行使.\n罗马军常年去外敌作战, 虽说不是无敌之师, 但基本都是罗马军占优势, 而问题恰出于此. 通常罗马在取得战斗胜利后, 不会把对方部族置于彻底的统治之下, 他们通常会接收战败方的部分\u0026quot;所有地\u0026rdquo;, 把其中一半作为同盟国赢得的份额, 另一半留作自己的\u0026quot;公有地\u0026quot;出租给罗马市民.\n围绕公有地的出租份额比例, 再次引起贵族与平民的对立, 平民阶级认为仅有地的出租公配偏向贵族阶级, 而贵族阶级则以尊重私有财产的法律为挡箭牌, 抵制平等分配. 并且, 肥沃的土地分配给贵族, 自己只能得到贫瘠的土地, 平民阶级反应强烈.\n另外, 平民阶级提出要求法律的成文化: 法律只要还停留在口头约定上, 在执行时, 就容易偏向所有法律制定权势贵族阶级. 因此, 要求法律成文合情合理. 成文的法律谁都能看到, 执行起来就可以做到客观公允(所谓法不可知, 则威不可测, 这样的道理2000年前的罗马人都认识到了, 而在某些国家, 法律就制定得非常模糊, 方便政府解释, 造成群体普遍违法, 应政府需要, 选择性执法. 法律变成政府抓人的大网, 而某些发言人还能公言说出, 不要拿法律当挡箭牌这样的话.)\n而后, 贵族应平民要求, 编写成文法\u0026lt;十二表法\u0026gt;, 又名十二铜表法, 因为它是一项一项刻在铜板上的12条法律. 此前, 平民与贵族是不允许通婚的, 在\u0026lt;十二铜表法\u0026gt;出台4年后, 一项允许贵族与平民通婚的法律出台了, 这一法律的出台, 对平民阶级的人才培养直到了积极的促进作用. 因为在教育制度不健全的那个时代, 出身和门第就是接受教育的标志.\n尽管如此, 公元前449年至前367年80多年里, 罗马一直处于探索过程之中, 尝试尝试废除二人的执政官, 代之以六人的军事指挥官(头太多也不行吧), 可能是考虑到把两个人行使的权力分散到六个人的手上, 结果却, 每当需要统一指挥时, 不得不一次次地任命独裁官.\n公元前396年, 经过10年的漫长战争, 罗马终于成功攻取了伊特鲁里亚非常强大的城市维爱, 为此罗马, 举国同庆, 而战斗一结束, 平民与贵族又展开了斗争, 平民建议在刚刚攻取的维爱设立第二个首都, 距离罗马20公里, 地位等同于罗马, 看来他们是和贵族玩累了, 就打算自己另开地图.\n即使独裁官强烈反对, 但是还是有一半的人离开, 罗马去了维爱. 此时, 他们所不知道的是, 善战民族凯尔特人从北方, 向罗马攻来了.\n3.4 凯尔特人的入侵 公元前390年7月18日, 罗马军队在台伯河上游迎战来敌, 结果大败而回, 凯尔特人开进了毫无防御的罗马, 罗马城门大开, 沦陷, 并被蹂躪了7个月. 罗马与凯尔特人和谈, 兼之凯尔特人不习惯城里生活, 便拿着300公斤的金块, 解除了对罗马7个月的占领, 离开了.\n而后, 罗马的拉丁同盟见罗马被凯尔特人打败, 并分崩离析, 甚至转脸成为罗马的敌人, 试图乘机消灭罗马.\n罗马就此走上了重建与应对围攻之路, 建国360年, 共和政体实行100年后的罗马, 不得不从头来过.\n希腊历史学家波利比乌斯认为公元前390年凯尔特人的入侵, 是罗马开始走向强大的第一步. 不小心跌入谷底后, 唯一办法就是爬上来. 尽管罗马人在公元前390年一度跌入谷底, 但是, 罗马人终究是罗马人, 尽管速度缓慢, 他们还是一步一步地爬了上来.\n重建罗马, 按照英国学者的研究, 公元前390年后的罗马人必须解决的问题, 按罗马人排列的顺序如下:\n注重防卫的同时, 重建被毁的罗马 与叛离的旧同盟各部族作战, 以此确保边境安全 消除贵族与平民的对立, 实现社会安定和舆论统一, 而这必然意味着政治改革. 3.5 政治改革 到了公元前4世纪前半叶的罗马, 已经具备实施根本性改革的一切内外条件了.\n公元前367年, 罗马史上划时代的法律\u0026lt;李锡尼法\u0026gt;得以实施, 在这部法中, 首先废除了六人军事指挥官政体, 恢复二人执政官制度, 明确今后罗马将实行寡头政制, 即少数人的领导体制.\n其次, 规定共和政府的所有要职向平民出身的人开放(不得不说这是一个非常高明的决定, 以前平民要求的是两个执政官, 平民占一个名额, 现在全面开放正如他们所希望, 而平民出身的李锡尼制定了\u0026lt;李锡尼法\u0026gt;, 贵族为这一想法的法制化投了赞成票, 他们选择不以阶级分配要职, 而是全面开放.\n如果按贵族和平民分配官职, 首先有悖机会均等, 即才能不足, 可能仅仅因为其出身平民即可成为执政官, 另外虽然以废除差别为目的分配官职, 却反而会出现强化差别的结果,两派一直处于敌对状态. 现在采取机会平等, 而非结果平等, 即可能出现两个平民出身的执政官, 也可能出现两个贵族出身的执政官)\n在\u0026lt;李锡尼法\u0026gt;实施若干年后, 又出台了一部法, 此法规定, 凡是担任过重要公职的人, 不论贵族还是平民, 都有权取得元老院议席, 即使是以保护平民阶级为己任的护民官, 在离任后也可以成为元老院议员.\n(我认为由那些具备丰富经验和出类拔萃的能力, 但不需要经过选举的人们组成的机构是共和政体下不可或缺的机构, 正因为他们远离选举, 所以他们可以从长远视角去制定一贯的政策, 即避免为迎合民意, 作出损害公众利益的事, 防止民粹, 美国的参议院就是参考了元老院)\n顺便跑个题, 编程随想君在介绍美国选举制度的就是, 就有提及参议院:\n下面大致列举参众两院的差别:\n参议员任期(6年)是众议员任期(2年)的三倍(甚至超过总统的4年任期) 参议员是由州议会选出(这点在20世纪初出现变化,下面会聊到),而众议员是由选区的选民直接选出 参议员的任职资格比众议员更高 有些职能是参议院可以干而众议院干不了的,比如: 4.1 总统提出的重要人事任命(比如最高法院大法官),须由参议院审批才生效 4.2 总统批准的国与国之间的条约,须由参议院审批才生效 众议员选举是对应到人口数量的(每N个选民划定一个选区, 每个选区选出一个众议员), 因此众议院更像选民的[传声筒], 能迅速反馈选区的民意, 但这种机制容易被民意裹挟.\n为了体现制衡,国会的【任何法案】要参众两院都批准才能成为法律。而且两院投票通过的法案文本必须是【完全一样】的。\n由于参议院的性质,使得它比较稳重。关于这点,作为“美国国父”之一的华盛顿打了个比方(大意是):\u0026ldquo;把热咖啡从众议院这个杯子倒入参议院这个杯子,使之冷却一下\u0026rdquo;。\n他的意思是:有了参议院的制衡,可以防止众议院一时头脑发热而让某个不恰当的法案获得通过。\n跑题之再跑题, 这种把主题相同内容的书籍相互关联的阅读方式, 被称为主题阅读(来自如何阅读一本书的这本书)\n从此, 罗马不再是贵族政体, 而是变成名副其实的寡头政体国家, 所谓贵族政体是由贵族出身的少数人统治绝大多数人的政体, 而寡头政体在少数人统治多数人的这一点上与贵族政体相同, 但是, 对少数人的血统没有要求. (寡头政体也是分情况的, 罗马这种由市民大会选举出来的执政官执政的方式是少数人统治的寡头政体, 像俄罗斯这种普京一人转的, 也是寡头政体, 但是他是上了不会下来. 而80年代某国的老人政治, 一群老家伙在最高领导人之上继续执政的方式, 也是寡头政体, 他们也不是选举出来的)\n3.6 罗马政体 以前在教科书上, 学到的\u0026quot;真理\u0026quot;是: 经济基础决定上层建筑(即政治体系), 现在的认识是, 政治体系是一切的基础, 涉及国民的生活形式, 经济也自然是受政治的影响.\ndigraph G{ subgraph K{ 法务官[label =\u0026#34;法务官 1人-\u0026gt;2人-\u0026gt;16人\u0026#34;]; 财务检查官[label = \u0026#34;财务检查官 2人-\u0026gt;40人\u0026#34;]; 财务官[label=\u0026#34;财务官 2人\u0026#34;]; 按察官[label=\u0026#34;按察官 4人\u0026#34;]; 元老院议员[label=\u0026#34;元老院议员 300人\u0026#34;]; 执政官[label=\u0026#34;执政官 2人\u0026#34;]; 独裁官[label=\u0026#34;独裁官 1人\u0026#34;]; 骑兵长官[label=\u0026#34;骑兵长官 实质上的副官 1人\u0026#34;]; 市民大会[label=\u0026#34;市民大会 全体市民\u0026#34;] 市民大会-\u0026gt;法务官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;财务检查官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;财务官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;按察官[label = \u0026#34;选出\u0026#34;]; 市民大会-\u0026gt;元老院议员[label = \u0026#34;选出\u0026#34;]; 元老院议员-\u0026gt;执政官[label=\u0026#34;承认劝告\u0026#34;]; 执政官-\u0026gt;独裁官[label=\u0026#34;指名\u0026#34;]; 独裁官-\u0026gt;骑兵长官[label=\u0026#34;任命\u0026#34;]; } subgraph H{ 平民大会[label=\u0026#34;平民大会(仅限平民)\u0026#34;]; 护民官[label=\u0026#34;护民官2人-\u0026gt;10人\u0026#34;]; 平民大会 -\u0026gt; 护民官[label=\u0026#34;选出\u0026#34;]; } } 3.6.1 执政官 执政官取代王政时代的国王, 是共和政体下的罗马的最高官职, 由市民大会选举产生, 经元老院批准后就职. 其过程与国王一样, 但国王是终身制, 而执政官的任期只有短短的一年, 执政官允许再选, 年龄下限为40岁.\n两位执政官相互有权对对方的想法或做法使否决权, 一项政策, 只要两位执政官都不同意, 就不能付诸实施.\n执政官是内政的最高领导人, 同时在战场担任指挥重任, 军务是执政官最重要的工作.\n一旦执政官各持己见, 互不妥协, 他们就会任命独裁者, 一统指挥权.\n3.6.2 独裁官(Dictator) 它是当国家处于非常时期时任命的一个官职, 意思是临时独裁执政官, 与其他官职通过选举产生不同, 独裁官由两位执政官中的一人指定即可. 独裁官除了无权决定政体之外, 在任何问题上享有绝对的决定权, 对于独裁官的作出决定, 任何人无权反对, 独裁官的任期很短, 为6个月, 人数当然只有1人.\n独裁官有权任命\u0026quot;骑兵长官\u0026rdquo;, 相当于副官, 两位执政官在任命独裁官的同时必须授受独裁官的命令.\n3.6.3 法务官(Praetor) 法务官任期为一年, 最初由一个人担任, 后来成倍增加, 最后达到16人. 既然翻译成了法务官, 负责的自然是司法事务, 最初, 当执政官上前线时, 法务官负责管理后方, 后来慢慢变身为司法责任人.\n身处该官职的人很多时候也要上战场, 要求法务官年龄在40岁以上, 基于一旦需要就可以代替执政官指挥军队的考虑.\n此外, 执政官不在时, 法务官要担任议长, 召集在首都罗马举行的市民大会\n3.6.4 财务检察官(Quaestor) 财务检察官的人数最初是2个人, 到了共和政体末期增加40个人, 任期一年, 年龄要求30岁以上.\n账务检察官的任务, 有一项是负责前线的财务, 这是很重要的工作. 例如军费是否过于浪费等.\n3.6.5 财务官(Censor) 这一官职最初是为人口调查而设, 因此在共和政体的初期, 不是每年, 而是每五年进行一次人口调查时才会选举, 任期一年半以上. 财务官人数是两人, 年龄要求不明.\n在罗马进行人口调查, 不是调查总人口, 而是调查户主们的财政状况, 对于未如实申报财政状况的人, 无论是贵族还是其他什么人, 财务官都有权告发. 之所以说他们权力很大, 原因就在于此.\n险些之外, 从国有土地的使用国库的收支, 都由他们进行严格t监督, 同时他们还有权决定公路及上下道建设的开支, 所以, 说他们是国家财政的责任人未尝不可. 掌握金钱收支的人就是掌握权力的人.\n3.6.6 按察官(Aedilis) 在所有官职中, 只有这一官职从设立之初就明确了当选者所属的出身阶级\u0026ndash;贵族和平民平分秋色, 各选两个人, 大概是担任这一官职的人需要直接且经常接触市民的缘故. 任期一年, 年龄要求在30岁以上. 年轻人也有机会担任的官职.\n按察官的任务:\n是策划组织庆祝和祭祀活动, 其中包括举办运动会的工作 是与公安警察相关的工作 保障粮食的供给, 由于农村地区自给自足, 所以按察官负责的是首都罗马的粮食供给 负责道路维修, 交通整顿和上下水道管理 对各种违法行为作出罚款金额的决定以作为惩戒 对市场进行严格监管以保障市场的公平运行. 如此看来, 该官职似乎是一个权威不高, 涉及领域繁多, 事务忙碌且报酬微薄的官职. 但实际并非如此, 由于这一官职所负责的领域大多和民众的生活息息相关, 因此就争取民众支持而言, 是一个非常理想的职位.\n3.6.7 护民官(Tribunus Plebis) 护民官是代表平民阶级的一个官职, 因此, 非平民阶级出身的人无缘这个官职, 由平民大会上选举产生, 平民大会只有平民阶级出身的人才有权参加, 护民官的任期为一年, 对年龄没有限制.\n护民官最主要的任务当然是保护平民的权利, 因此, 他们有权对政府所作出的决定行使否认权, 但在战时不得使用这种权力.\n因利害冲突, 护民官有可能遭到某些行事过激的贵族的暗算, 所以, 为了防止此类事件发生, 护民官享有人身不可侵犯的特别权利, 这一权利边执政官都不享有.\n护民官的人数最下2个人, 后来不断增加, 最后达到了10人之多\n3.6.8 元老院(Senatus) 在罗马, 作为国家最高决策机构, 市民大会一直存在. 除了护民官, 以执政官为首的政府要职全部由该\u0026quot;国会\u0026quot;选举产生\n只有元老院议员不需要经过选举, 但是这绝不意味着只要过了30岁就可以自动得到议席, 正因为如此, 它还是世袭的. 只有经过相当严格的甄别, 确认其见识, 责任心, 能力和经验都符合的人才允许进入元老院. 当然, 出身名门望族的人, 相对多一些优势.\n其实, 元老院充分发挥了心脏的作用, 现在, 它的功绩已经成为西洋史的常识. 作为上院的名称, 美国, 法国, 意大利和加拿大, 都愿意留下\u0026quot;senatus\u0026quot;的称谓. 就像历史学家波得比乌斯指出的那样, 罗马在健全的元老院开始发挥作用以后, 彻底摆脱了迷惘, 勇敢地走上了繁荣之路, 无疑人人都会对此心生羡慕.\n3.7 政治建筑的杰作 善于从失败中学习, 并在此基础上挣脱已有观念的束缚, 提高自己, 然后再重新站起来, 这就是罗马人的性格.\n这不是说失败是好事, 失败没有什么好或不好, 失败只是失败. 重要的是如何从失败中站起来, 也就是怎样对待失败.\n进入公元前4世纪后半叶, 改变了对外关系形态后的罗马人, 没有改变公元前8世纪罗穆路斯以来的罗马人的一个特点, 即同化失败者. \u0026lt;列传\u0026gt;作者普鲁塔克认为罗马强大的首要原因就是他们这种性格\n3.8 市民权 3.8.1 权利 不论动产还是不动产, 保证一切私有财产. 允许私有财产自由买卖 享有选举权和被选举权, 有参与国政的权利 有依法接受审判的权利. 同时, 在罗马, 被判死刑后, 有权向市民大会提出诉讼, 也就是上诉权. 事实上, 有罗马市民权的人极少被执行死刑 有证据证明是有独立, 自由身份的成熟男子 3.8.2 业务 首先,有义务服务军务, 17岁至45岁为现役, 46岁至60岁为预备役. 这项义务替代了市民另一项义务, 即纳税的业务. 以间接税为主的古代税制中, 直接税用兵役相抵的情况很普遍, 因此, 军税又叫\u0026quot;血税\u0026rdquo;\n法律并未规定市民不得缴纳税款以逃避军务, 只是, 对于罗马人来说, 这样太可耻. 在罗马, 以经济行为纳税的只有不享有市民权而不承担军备义务的非市民以及经济富裕但没有孩子的女人.\n3.8.3 条件 成为市民的条件:\n在雅典, 要取得雅典市民权必须父母双方都是雅典人, 即使在鼎盛期也是如此, 斯巴达也一样. 但在罗马则不同, 只要生活在罗马就可以取得市民权, 而且这一情形延续了相当长的时间.\n对于市民权, 希腊人和罗马人的区别也体现在奴隶的处境上.\n在希腊, 奴隶终身为奴是普遍现象, 但是罗马的奴隶有路可选, 奴隶主为了回报奴隶长年的无偿奉献会还奴隶以自由, 或者奴隶能够用自己积攒起来的钱赎回自由(奴隶竟然还可以有自己的财产).\n获得自由的奴隶叫解放奴隶, 他们的子孙可以取得罗马譏权, 至于有了市民权后, 能否在社会上出人头地, 就看个人的才能和运气了.\n希腊哲学家亚里士多德把奴隶和家畜作了比较, 并写下这样的一句话:\n在有用方面, 两者几乎没有区别. 奴隶和家畜用他们的肉体为我们人类所用, 这一方面是一样的.\n比亚里士多德早200年的罗马第六代国王塞尔维乌斯-图里乌斯说过这样的话, 尽管有传言说他本人是奴隶出身:\n奴隶和自由民的不同不是先天造成的, 而是生来遭遇的命运不同而已.\n","permalink":"https://ramsayleung.github.io/zh/post/2022/%E7%BD%97%E9%A9%AC%E4%B8%8D%E6%98%AF%E4%B8%80%E5%A4%A9%E5%BB%BA%E6%88%90%E7%9A%84/","summary":"1 前言 以史为鉴,可以知兴替。而罗马,作为西方文明的来源之一,有着漫长辉煌无比的历史,其影响直至今天仍可见。因此,我开了个新坑,阅读盐野阿姨的","title":"罗马人的故事(一):罗马不是一天建成的"},{"content":"1 前言 如果鲁迅先生都叫不醒的人,可以是真的叫不醒了。从前我对鲁迅先生的印象,就停留在语文课本上的《社戏》, 玩梗的《闰土》和《孔乙已》,和其他出现在教科书上的作者无异。\n甚至因为官方的推荐,产生了反感之心。毕竟年青时认为,文人大都有风骨,受朝廷推崇之人,怕是无甚风骨,心中印象自然不佳。而后,年岁渐长,读到了不同的声音,诸如编程随想君的《面对共产党——民国人文大师的众生相》一文中提到了饱受争议的鲁迅。毛先生对鲁迅的点评:\n周海婴在《鲁迅与我七十年》一书中提及了一段往事。 1957年反右的时候,罗稷南当着老毛的面提了一个问题:要是今天鲁迅还活着,他可能会怎样?毛腊肉沉思片刻,回答说:\u0026ldquo;要么是关在牢里还要写,要么他识大体不做声。\u0026rdquo;\n而鲁迅自我的点评:\n在《鲁迅纪念集》第1辑第68页,记录了鲁迅向李霁野复述了一段他跟冯雪峰的对话,时间是1936年4月。\n鲁迅:你们来时,我要逃亡,因为首先要杀的,恐怕是我。 冯雪峰则连忙摇头摆手应之:那弗会,那弗会!\n想来,鲁迅也并非郭沫若之流, 是写得出《毛主席是我爷爷》这样的颂歌的人。\n2 振聋发聩 如果鲁迅不弃医从文,可能民国会多一个分析人体病理的名医,会少一个鞭辟国民精神的执戈披甲之士。\n2.1 铁屋子 “假如一间铁屋子,是绝无窗户而万难破毁的,里面有许多熟睡的人们,不久都要闷死了,然而是从昏睡入死灭,并不感到就死的悲哀。现在你大嚷起来,惊起了较为清醒的几个人,使这不幸的少数者来受无可挽救的临终的苦楚,你倒以为对得起他们么?”\n“然而几个人既然起来,你不能说决没有毁坏这铁屋的希望。”\n是的,我虽然自有我们的确信,然而说到希望,却是不能抹杀的,因为希望是在于将来,决不能以我之必无的证明,来折服了他之所谓可有。\n人总是要怀有希望的,纵使四周昏暗无光,纵使你挺身而出会受到众人的背弃。清醒总是痛苦的,更痛苦的是面对困境无能为力,而鲁迅决心做出改变,由他而始,怀抱希望。\n2.2 吃人 凡事总须研究,才会明白。古来时常吃人,我也还记得,可是不甚清楚。我翻开历史一查,这历史没有年代,歪歪斜斜的每叶上都写着“仁道义德”几个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是“吃人”!\n古人书上全是仁义,心里全是吃人。现在的人是嘴上全是主义,心里全是生意。看来今人是不如古人的,真是一代不如一代。\n2.3 你也配姓赵 那是赵太爷的儿子进了秀才的时候,锣声镗镗的报到村里来,阿Q正喝了两碗黄酒,便手舞足蹈的说,这于他也很光采,因为他和赵太爷原来是本家,细细的排起来他还比秀才长三辈呢。其时几个旁听人倒也肃然的有些起敬了。\n那知道第二天,地保便叫阿Q到赵太爷家里去;\n太爷一见,满脸溅朱,喝道:“阿Q!你这浑小子!你说我是你的本家么?”\n阿Q不开口。赵太爷愈看愈生气了,抢进几步说:“你敢胡说!我怎么会有你这样的本家?你姓赵么?”\n阿Q不开口,想往后退了。\n赵太爷跳过去,给了他一个嘴巴。\n“你怎么会姓赵!——你那里配姓赵!”\n你也配姓赵,韭菜还操着镰刀的心,你哪里配姓赵呢。\n2.4 闰土 这来的便是闰土。虽然我一见便知道是闰土,但又不是我这记忆上的闰土了。他身材增加了一倍;先前的紫色的圆脸,已经变作灰黄,而且加上了很深的皱纹;眼睛也像他父亲一样,周围都肿得通红,这我知道,在海边种地的人,终日吹着海风,大抵是这样的。他头上是一顶破毡帽,身上只一件极薄的棉衣,浑身瑟索着;手里提着一个纸包和一支长烟管,那手也不是我所记得的红活圆实的手,却又粗又笨而且开裂,像是松树皮了。\n我这时很兴奋,但不知道怎么说才好,只是说:“阿!闰土哥,——你来了?……”\n我接着便有许多话,想要连珠一般涌出:角鸡,跳鱼儿,贝壳,猹,……但又总觉得被什么挡着似的,单在脑里面回旋,吐不出口外去。\n他站住了,脸上现出欢喜和凄凉的神情;动着嘴唇,却没有作声。他的态度终于恭敬起来了,分明的叫道:“老爷!……”\n我似乎打了一个寒噤;我就知道,我们之间已经隔了一层可悲的厚障壁了。我也说不出话。\n年青时读课文的时候,还未能体会到与童年好友重逢时的喜悦碰撞两人身份差异, 导致友谊不再的悲凉。想必那里鲁迅在十二月冰井里打的寒噤。这种感觉大概和,你和初恋情人偶遇,相谈甚欢,然后有个男孩在旁边叫,妈妈,我们回家吧,差不大多。\n3 总结 不知道鲁迅活到今天,会写出怎么样的作品。时代的土壤之于作家,就有如阳光之于鲜花,想来文学也该有时势造英雄之说。如果鲁迅还活到今天,肯定会写出比《呐喊》精妙百倍的文章,令后人拜服。\n我建议鲁迅先生将其文集取名为《捂嘴》\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%91%90%E5%96%8A/","summary":"1 前言 如果鲁迅先生都叫不醒的人,可以是真的叫不醒了。从前我对鲁迅先生的印象,就停留在语文课本上的《社戏》, 玩梗的《闰土》和《孔乙已》,和其他","title":"呐喊"},{"content":"1 Design principle 在谈论四个设计的基本准则前, 作者强调了关于命名的重要性.\n作者举了一个例子, 在圣诞节, 他收到一本书介绍树木的分类, 他注意到一种叫Joshua tree的树, 造型奇特. 他想, 如果我看过, 我肯定会记得, 毕竟形状特别. 当他走出家门时, 发现社区80%的院子都有这种树, 但他此前从未注意到. 一旦你可以叫出它的名字, 你就发现它随处可见\nOnce you can name something, you\u0026rsquo;re conscious of it. You have power over it. You\u0026rsquo;re in control. You own it.\n深有体会. 树尤如此, 设计准则亦如是.\nGood design is as easy as:\nLearn the basic principles: They\u0026rsquo;re simpler than you might think Recognize when you\u0026rsquo;re not using them: Put it into words - name the problem Apply the principles: Be amazed 1.1 Proximity The principle of Proximity states: Group related item together. 有关联的元素, 位置上让它们更接近, 以表示它们有关联的一个群组而不是若干个无关联散落的元素\n当然, 有关联才放在一起, 没关联就不要硬挤过来. 留下视觉距离让读者可以判别出他们的关系. 这也和生活经验吻合, 可以从两人的物理距离判别出他们的关系\n如下例子:\nThe idea of proximity doesn\u0026rsquo;t mean that everything is closer together; it means elements that are ntellectually connected, those that have some sort of communication relationship, should also be visually connected.\nIt\u0026rsquo;s all about space. The principle of Proximity helps you focus on space and what it can do for communication\n1.2 Alignment The Principle of Alighment states: Nothing should be placed on the page arbitrarily. Every item should have a visual connection with something else on the page. The principle of alignment forces you to be conscious \u0026ndash; no longer can you just throw things on the page and see where they stick\n通过对齐, 可以让元素之间产生联系, 使杂乱的设计变得有条理, 通过布局来展现关联. 如下图分析\n又或者是:\n通常来说, 左对齐或者右对齐会比居中对齐有更强烈的视觉效果, 因为居中对齐两边不对齐, 就会让我有种未对齐的感觉\n对于页面, 可分析其页面元素的对齐, 然后修正成统一的对齐方式. 而下图的书页, 左右都对齐了, 还增加了缩进和行分隔, 看起来就清晰多了.\nI am giving you a number of rules here, and it\u0026rsquo;s true that rules are made to be broken. But remember the Rule about Breaking Rules: You must know what the rule is before you can break it\n1.3 Repetition The Principle of Pepetition States: Repeat some aspect of the design throught the entire piece. The repetitive element may be a bold font, a thick rule(line), a certain bullet, design element, color, format, spatial relationships, etc. It can be anything that a reader will visually recognize\n重复是一致性的一种实现, 但重复并不止于一致性, 它还是一种统一设计中各个元素的有力手段. 还是熟悉的名片:\n1.4 Contrast The Principle of Contrast states: Contrast Various elements of the piece to draw a reader\u0026rsquo;s eye itno the page. If two items are not exactly the same, then make them different. Really different\n对比有很多手法, 诸如大与小, 复古与新潮, 强与弱, 明与暗, 粗糙与细滑, 水平与垂直等等.\n需要注意的是, 如果两个元素有区分, 但本质无差别, 那就不是 contrast, 而是 conflict.\nThere is one more general guiding principle of Design(and of Life): Don\u0026rsquo;t be wimp\n突然意识到, 本书的PDF 版本的排版和字段, 图片也是相当舒服的\n2 Design with Type 接下来大部分内容都关于Type, 关于印刷, 关于字体种类, 不是很感兴趣, 所以就草草涉猎过.\n","permalink":"https://ramsayleung.github.io/zh/post/2021/the_nondesigners_design_book/","summary":"1 Design principle 在谈论四个设计的基本准则前, 作者强调了关于命名的重要性. 作者举了一个例子, 在圣诞节, 他收到一本书介绍树木的分类, 他注意到一种叫Josh","title":"The Non-designer's design book"},{"content":"1 前言 阿德勒与弗洛伊德, 荣格并称为\u0026quot;心理学三大巨头\u0026quot;, \u0026lt;被讨厌的勇气\u0026gt;这本书主要是以对话的形式来讲述阿德勒的心理学. 类似苏格拉底的对话, 认为\u0026quot;任何人都可以随时获得幸福\u0026quot;, 并给出了\u0026quot;自我接纳\u0026quot;, \u0026ldquo;他者信赖\u0026quot;和\u0026quot;他者贡献\u0026quot;三个手段\n你认为\u0026quot;人是可以改变\u0026quot;的么?\n2 目的论 弗洛伊德所创建的心理学主张原因论, 即现在的不幸是由过去的原因造成的, 现在的我(结果)是由过去的事情(原因)所决定, 最常见的原因是童年不幸. 虽说这个被现代心理学家证实是过于片面, 但是对于编故事还是很有用, 比如\u0026lt;穆赫兰道\u0026gt;, \u0026lt;致命ID\u0026gt;等电影灵感都来自弗洛伊德的原因论.\n而阿德勒心理学主张目的论, 考虑的不是过去的\u0026quot;原因\u0026rdquo;, 而是现在的\u0026quot;目的\u0026quot;. 阿德勒说, 决定我们自己的不是\u0026quot;经验本身\u0026quot;而是\u0026quot;赋予经验的意义\u0026quot;.\n按照书中的例子, 青年的一个朋友一直宅在家不愿出门, 一出门就会全身不舒服. 按照原因论, 即他过去受过某种创伤, 所以他无法走出家门; 而按照目的论, 即是他不想走出家门, 然后再为自己找不能出门的原因.\n按照阿德勒的观点, 任何经历本身并不是成功或者失败的原因. 我们并非因为自身经历中的刺激\u0026ndash;所谓的心理创作-而痛苦, 事实上我们会从经历中发现符合我们目的的因素. 决定我们自身的不是过去的经历, 而是我们自己赋予经历的意义\n经历本身不会决定什么. 我们给过去的经历赋予了什么样的意义, 这直接决定了我们的生活. 人生不是由别人赋予的, 而是由自己选择的, 是自己选择自己如何生活.\n3 选择生活 重要的不是被给予什么, 而是如何去利用被给予的东西. 因此按照书中的观点, 你的不幸, 皆是自己\u0026quot;选择\u0026quot;的:\n但是, 现在的你之所以不幸正是因为你亲手选择了\u0026quot;不幸\u0026quot;, 而不是因为生来就不幸.\n我个人觉得这样的观点是过分一元论, 过分强调主观意识对客观世界的影响.\n因为生活都是你自己选择的, 即使人们有各种不满, 但还是认为保持现状更加轻松, 更能安心. 是人们常常下定决心\u0026quot;不改变\u0026quot;\n4 一切烦恼的来源 \u0026ldquo;一切烦恼都是人际关系的烦恼\u0026rdquo;(这个是否也算过分绝对呢?)这个是阿德勒心理学的一个基本概念. 如果这个世界没有人际关系, 如果这个宇宙没有他人只有自己, 那么一切烦恼也都将消失\n自卑感来自主观的臆造, 例如的身高的自卑是比对出来的, 没有人关注你的身高.(BBS的相亲帖子都是有身高限定的). 我们无法改变客观事实, 但可以任意改变主观解释.\n自卑情结是把自己的自卑感当作某种借口使用的状态, 例如, 我找不到女朋友是因为不够高; 将原本没有任何因果关系的事情解释成似乎有重大因果关系.\n我正好看过亲密关系, 实验证明, 身高对吸引力的确有非常大的影响\n健全的自卑感不是来自与别人的比较, 而是来自与\u0026quot;理想的自己\u0026quot;的比较, 人生不是与他人的比赛.\n在独自成行的活动中, 人生的确不是与他人的比赛, 但部分活动的确是与他人的比赛. 比如求偶\n情绪波动时易发怒, 但发怒只是一种表达方式, 怒气终归是为了达成目的的一种手段和工具; 那么既然发怒是交流的一种形态, 不使用发怒这种方式也可以交流的.\n5 自由是不再寻求认可 阿德勒心理学否定寻求他人的认可. 其实, 我们\u0026quot;并不是为了满足别人的期待而活着\u0026quot;的. 倘若自己都不为自己活出自己的人生, 那还有谁会为自己而活呢?\n5.1 课题分离 课题是你我在做的事情. 面对课题, 首先要思考\u0026quot;这是谁的课题\u0026quot;, 把自己的课题与别人的课题分离开.\n例如书中的例子, 学习是孩子的课题, 无论父母多么关心孩子, 都不应干涉孩子的课题. 基本上, 一切人际关系矛盾都起因于对别人的课题妄加干涉或者自己的课题被别人妄加干涉\n5.2 放开烦恼 人为什么在意别人的视线呢? 阿德勒心理学给出的答案非常简单, 那就是因为还不会进行课题分离. 把原本是别人的课题当作自己的课题.\n伸伸手即可触及, 但又不踏入对方领域, 保持这种适度距离非常重要.\n对认可的追求, 扼杀了自由, 自由就是被别人讨厌. 不畏惧被人讨厌是勇往直前, 不随波逐流而是激流勇进, 这才是对人而言的自由.\n拼命寻求认可反而是以自我为中心\n在只关心\u0026quot;我\u0026quot;这个意义上来讲, 是以自我为中心. 你正因为不想被他人认为自己不好, 所以才在意他人的视线. 这不是对他人的关心, 而是对自己的执著.\n6 追求幸福 把对自己的执著(self interest)转换成对他人的关心(social interest), 建立起共同体感觉. 这需要以下三点做好:\n自我接纳 他者信赖 他者贡献 6.1 自我接纳 自我接纳是指假如做不到就诚实地接受这个\u0026quot;做不到的自己\u0026quot;, 然后尽量朝着能够做到的方向去努力, 不对自己撒谎.\n对得了60分的自己说\u0026quot;这次只是运气不好, 真正的自己能得100分\u0026quot;, 这是自欺欺人; 与此同时, 在诚实地接受60分的自己的基础上努力思考\u0026quot;如何才能接近100分\u0026quot;, 这就是自己接纳\n上帝, 请赐予我平静, 去接受我无法改变的; 给予能手, 去改变我能改变的; 赐我智慧, 分辨这两者的区别 \u0026ndash;尼布尔的祈祷文\n6.2 他者信赖 在相信他人的时候不附加任何条件. 即使没有足以构成信用的客观依据也依然相信, 不考虑抵押之类的事情, 无条件的相信. 这就是信赖.\n如果你认为无条件地相信他人可能会被背叛, 阿德勒心理学认为决定背不背叛的不是你, 那么他人的课题, 你只需要考虑\u0026quot;我该怎么做\u0026quot;.\n(按照这个观点, 如果没有建立起信赖的条件, 是你没有足够信赖对方, 正因为有这种忧虑, 以致于与任何人都无法建立起深厚的关系. 我觉得是过于理想化, 建立稳定关系的过程本身就像刺猬相互取暖, 从慢慢接近开始的).\n6.3 他者贡献 对他人寄予信赖就是把他人当作伙伴, 正因为是伙伴, 所以才能信赖, 如果还是伙伴, 也就做不到信赖. 对作为伙伴的他人给予影响, 作出贡献, 这就是他者贡献\n人只有在能够感觉到“我对别人有用”的时候才能体会到自己的价值。但是,这种贡献也可以通过看不见的形式实现。只要有“对别人有用”的主观感觉,即“贡献感”就可以。\n并且,书中还得出了这样的结论:幸福就是“贡献感”\n7 此时此刻 人生是连续的刹那, 根本不存在过去和未来, 我们的人生只存在于刹那之中. 人生像是在每一个瞬间不停旋转起舞的连续的刹那, 并且, 蓦然四顾时常常会惊觉: 已经来到这里了么?\n请你想象一下自己站在剧场舞台上的样子. 此时, 如果整个会场都开着灯, 那就可以看到山顶位的观众席. 但是, 如果强烈的聚光灯打向自己, 那就边最前排都看不见.\n我们的人生也完全一样. 正因为把模糊而微弱的光人生整体, 所以才能够看到过去和未来; 不, 是感觉能够看到. 如果把强烈的聚光灯对准\u0026quot;此时此刻\u0026quot;, 那就会既看不到过去也看不到未来.\n我自己无论怎样回顾之前的人生也无法解释自己为什么会走到\u0026quot;此时此刻\u0026quot;\n只要不迷失他者贡献这个引导之星就可以, 只要朝着这个方向前进就可以获得幸福.\n让我们度过各自的夜晚, 然后迎来新的早晨吧.\n8 总结 读完《亲密关系》再读《被讨厌的勇气》, 会觉得书中的结论得出没有经过对应的实验论证, 实验结果可信度存疑.\n但是本书的确提出了很多有趣的观点, 接受的人就会觉得胜读十年书, 不接受的人就会觉得又是鸡汤一碗.\n我对书中的部分观点表示认可, 比如自由, 此时此刻的观点; 对于一切烦恼来自人际关系的论点觉得过于绝对, 目的论的观点让人印象深刻, 但过分强调主观意识, 忽视客观世界.\n重要的不是被给予了什么, 而是如何去利用被给予的东西\n这观点我是非常赞同的, 但是也要关注, 你到底被给予了什么. 过分强调主观意识, 会让人盲目乐观. 既要抬头看天, 也要低头踏地.\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E8%A2%AB%E8%AE%A8%E5%8E%8C%E7%9A%84%E5%8B%87%E6%B0%94/","summary":"1 前言 阿德勒与弗洛伊德, 荣格并称为\u0026quot;心理学三大巨头\u0026quot;, \u0026lt;被讨厌的勇气\u0026gt;这本书主要是以对话的形式来讲述阿德勒的","title":"被讨厌的勇气"},{"content":"1 Preface I have been maintained a legacy distributed timer for months for my employer, then some important pay business are leveraging on it, with 1 billion tasks handled every day and 20k tasks added per second at most.\nEven though it\u0026rsquo;s old and full of black magic code, but it also also have insighted and well-designed code. Based on this old, running timer, I summarize and extract as this article, and it wont include any running code(perhaps pseudocode, and a lot of figures, as an adage says: A picture is worth a thousand words).\nif you are curious about the reason(I personally suggest to watch the TV series Silicon Valley, Richard has gave us a good example and answer)\n2 Design 2.1 Algorithm There are several algorithms in the world to implement timer, such as Red-Black Tree, Min-Heap and timer wheel. The most efficient and used algorithm is timer wheel algorithm, and it\u0026rsquo;s the algorithm we focus on.\nAs for timing wheel based timer, it can be modelled as two internal operations: per-tick bookkeeping and expiry processing.\nPer-tick bookkeeping: happens on every \u0026rsquo;tick\u0026rsquo; of the timer clock. If the unit of granularity for setting timers is T units of time (e.g. 1 second), then per-tick bookkeeping will happen every T units of time. It checks whether any outstanding timers have expired, and if so it removes them and invokes expiry processing. Expiry processing: is responsible for invoked the user-supplied callback (or other user requested action, depending on your model). 2.1.1 Simple Timing Wheels The simple timing wheel keeps a large timing wheel, the below timing wheel has 8 slots, and each slot is holding the task which is going to be expired. Supposing every slot presentes one second(one tick as a second), then the current slot is slot 1, if we want to add a task needed to be triggered 2s later, then this task will be inserted into slot 3.\nper-tick bookkeeping: O(1) What happen if we want to add a task needed to be launched 20s later, the answer is we have no way to do so since there are only 8 slots. So if we have a large period of timer task, we have to maintain a large timing wheel with tons of slots, which requires exponential amount of memory.\n2.1.2 Hashed Timing Wheel Hashed Timing Wheel is an improved simple timing wheel. As we mentioned before, it will consume large resources if timer period is comparatively large. Instead of using one slot per time unit, we could use a form of hashing instead. Construct a circular buffer with a fixed number of slots(such as 8 slots). If current slot is 0, we want store 3s later task, we could insert into slot 3, then if we want bookkeep 9s-later task, we could insert into slot 1(9 % 8 = 1)\nper-tick bookkeeping: O(1) - O(N) It\u0026rsquo;s a tradeoff strategy, We trade space with time.\n2.1.3 Hierarchical Timing Wheels Since simple timing wheels and hashed timing wheel come with drawback of time efficiency or space efficiency. Back to 1987, after studying a number of different approaches for the efficient management of timers, Varghese and Lauck posted a paper to introduce Hierarchical Timing Wheels\nJust make a long story short, I won\u0026rsquo;t dive deep into hierarchical timing wheels, you could easily understand it by a real life reference: the old water meter\nthe firse level wheel(seconds wheel) rotates one loop, triggering the second level(minutes wheel) ticks one slot, same for the third level(hour wheel). Therefore, we present a day(60*60*24 seconds) with 60+60+24 slots. If we want to present a month, we only need to a four level wheel(month wheel) with 30 slots.\nper-tick bookkeeping: O(1) 2.2 Per-tick bookkeeping After introducing timing wheel algorithm, let\u0026rsquo;s go back to the topic about designing a reliable distributed timer, it\u0026rsquo;s essential to decide how to store timer task. Taking implementation complexity and time, space trade off, we choose the Hashed Timing Wheel algorithm.\nThere are several internal components developed by my employer, one of them is named TableKV, a high-availability(99.999% ~ 99.9999%) NoSql service. TableKV supports 10m buckets(the terminology is table) at most, every table comes with full ACID properties of transactions support. You could simply replace TableKV with Redis as it provides the similar bucket functionality.\n2.2.1 Insert task into slot We are going to implement Hashed Timing Wheel algorithm with TableKV, supposing there are 10m buckets, and current time is 2021:08:05 11:17:33 +08=(the UNIX timestamp is =1628176653), there is a timer task which is going to be triggered 10s later with start_time = 1628176653 + 10 (or 100000010s later, start_time = 1628176653 + 10 + 100000000), these tasks both will be stored into bucket start_time % 100000000 = 28176663\n2.2.2 Pull task out from slot As clock tick-tacking to 2021:08:05 11:17:43 +08(1628176663), we need to pull tasks out from slot by calculating the bucket number: current_timestamp(1628176663) % 100000000 = 28176663. After locating the bucket number, we find all tasks in bucket 28176663 with start_time \u0026lt; current_timestamp=, then we get all expected expiry tasks.\n2.3 Global clock and lock As we mentioned before, when the clock tick-tacks to current_time, we fetch all expiry tasks. When our service is running on a distributed system, it\u0026rsquo;s universal that we will have multiple hosts(physical machines or dockers), with multiple current_times on its machine. There is no guarantee that all clocks of multiple hosts synchronized by the same Network Time Server, then all clocks might be subtly different. Which current_time is correct?\nIn order to get the correct time, it\u0026rsquo;s necessary to maintain a monotonic global clock(Of course, it\u0026rsquo;s not the only way to go, there are several ways to handle time and order). Since everything we care about clock is Unix timestamp, we could maintain a global system clock represented by Unix timestamp. All machines request the global clock every second to get the current time, fetching the expiry tasks later.\nWell, are we done? Not yet, a new issue breaks into our design: if all machines can fetch the expiry tasks, these tasks will be processed more than one time, which will cause essential problems. We also need a mutex lock to guarantee only one machine can fetch the expiry task. You can implement both global clock and mutex lock by a magnificent strategy: an Optimistic lock\nAll machines fetch global timestamp(timestamp A) with version All machines increase timestamp(timestamp B) and update version(optimistic locking), only one machine will success because of optimistic locking. Then the machine acquired mutex is authorized to fetch expiry tasks with timestamp A, the other machines failed to acquire mutex is suspended to wait for 1 seconds. Loop back to step 1 with timestamp B. We could encapsulate the role who keep acquiring lock and fetch expiry data as an individual component named scheduler.\n2.4 Expiry processing Expiry processing is responsible for invoked the user-supplied callback or other user requested action. In distributed computing, it\u0026rsquo;s common to execute a procedure by RPC(Remote Procedure Call). In our case, A RPC request is executed when timer task is expiry, from timer service to callback service. Thus, the caller(user) needs to explicitly tell the timer, which service should I execute with what kind of parameters data while the timer task is triggered.\nWe could pack and serialize this meta information and parameters data into binary data, and send it to the timer. When pulling data out from slot, the timer could reconstruct Request/Response/Client type and set it with user-defined data, the next step is a piece of cake, just executing it without saying.\nPerhaps there are many expiry tasks needed to triggered, in order to handle as many tasks as possible, you could create a thread pool, process pool, coroutine pool to execute RPC concurrently.\n2.5 Decoupling Supposing the callback service needs tons of operation, it takes a hundred of millisecond. Even though you have created a thread/process/coroutine pool to handle the timer task, it will inevitably hang, resulting in the decrease of throughout.\nAs for this heavyweight processing case, Message Queue is a great answer. Message queues can significantly simplify coding of decoupled services, while improving performance, reliability and scalability. It\u0026rsquo;s common to combine message queues with Pub/Sub messaging design pattern, timer could publish task data as message, and timer subscribes the same topic of message, using message queue as a buffer. Then in subscriber, the RPC client executes to request for callback service.\nAfter introducing message queue, we could outline the state machine of timer task:\nThanks to message queue, we are able to buffer, to retry or to batch work, and to smooth spiky workloads\n2.6 High availability guarantee 2.6.1 Missed expiry tasks A missed expiry of tasks may occur because of the scheduler process being shutdown or being crashed, or because of other unknown problems. One important job is how to locate these missed tasks and re-execute them. Since we are using global `current_timestamp` to fetch expiry data, we could have another scheduler to use `delay_10min_timestamp` to fetch missed expiry data.\nIn order to look for a needle in a haystack, we need to set a range(delay_10min - current time), and then to batch find cross buckets. After finding these missed tasks, the timer publishes them as a message to message queue. For other open source distributed timer projects like Quartz, which provides an instruction to handle missed(misfire) tasks: Misfire instructions\nIf your NoSql component doesn\u0026rsquo;t support find-cross-buckets feature, you could also find every bucket in the range one by one.\n2.6.2 Callback service error Since the distributed systems are shared-nothing systems, they communicate via message passing through a network(asynchronously or synchronously), but the network is unreliable. When invoking the user-supplied callback, the RPC request might fail if the network is cut off for a while or the callback service is temporarily down.\nRetries are a technique that helps us deal with transient errors, i.e. errors that are temporary and are likely to disappear soon. Retries help us achieve resiliency by allowing the system to send a request repeatedly until it gets an explicit response(success or fail). By leveraging message queue, you obtain the ability for retrying for free. In the meanwhile, the timer could handle the user-requested retries: It\u0026rsquo;s not the proper time to execute callback service, retry it later.\n3 Conclusion After a long way, we are finally here. The final full architecture would look like this:\nThe whole process:\nAdding a timer task, with specified meta info and task info Inserting task into bucket by hashed timing wheel algorithm(With task_state set to pending) Fetch_current scheduler tries to acquire lock and get global current time The Acquired lock scheduler fetches expiry tasks Return the expected data. \u0026amp; 7. Publishing task data as message to MQ with thread pool; And then set task_state to delivered Message subscriber pulls message from MQ Sending RPC request to callback service(set task_state to success or fail) Retry(If necessary) Wish you have fun and profit\n4 Reference Paper: Hashed and Hierarchical Timing Wheels: Efficient Data Structures for Implementing a Timer Facility Hashed and Hierarchical Timing Wheels ","permalink":"https://ramsayleung.github.io/zh/post/2021/how_to_design_a_reliable_distributed_timer/","summary":"1 Preface I have been maintained a legacy distributed timer for months for my employer, then some important pay business are leveraging on it, with 1 billion tasks handled every day and 20k tasks added per second at most.\nEven though it\u0026rsquo;s old and full of black magic code, but it also also have insighted and well-designed code. Based on this old, running timer, I summarize and extract as this article, and it wont include any running code(perhaps pseudocode, and a lot of figures, as an adage says: A picture is worth a thousand words).","title":"How To Design A Reliable Distributed Timer"},{"content":"1 Distributed Systems for fun and profit source: http://book.mixu.net/distsys/\n2 1. Basic 2.1 Basic concept 本章介绍了分布式系统的基本概念, 例如 scalablity, perfomance, latency, availability\n关于 latent, 这里给出了一个很cool的描述:\nFor example, imagine that you are infected with an airbone virus that turns people into zombies. The laten period is the time between when you became infected, and when you turn into a zombie. That\u0026rsquo;s latency: the time during which something that has already happed is concealed from view.\n关于 availability, 计算公式是:\nAvailability = uptime / (uptime + downtime)\n2.2 Failt tolerance 设计一个可靠的分布式系统, 相当程度上是设计一个fault tolerance 系统, failure 一词 在此章出现了很多次. But without knowing every single aspect of the system, the best we can do is design for fault tolerance.\nFault tolerance: ability of a system to behave in a well-defined manner once faults occur. Fault tolerance boils down to this: define what faults you expect and then design a system or an algorithm that is tolerant of them. You can\u0026rsquo;t tolerate faults you haven\u0026rsquo;t considered.\n限制分布式系统的主要是两个物理因素:\n节点数(你想要更多的存储空间, 更强的计算能力, 自然需要更多的节点)\n节点间的距离(信息传输, 光速是上限)\n从设计系统的角度来考虑这两个限制:\n节点数越多, 出错(failure)的概率就越高(降低可用性, 增加了管理成本) 节点数越多, 节点之间的通信就越多(限制节点数与性能之间的线性增长) 距离越大, 节点通信的延迟就大(性能下降) 2.3 Abstraction and model 因为真实世界有很多与问题域无关的干扰因素, 为了排队这些干扰, 我们引入了Abstraction和Model的概念.\nAbstraction: it make things more manageable by removing real-world aspects that are not relevant to solving a problem.\nModel: it describes the key properties of a distributed system in a precise manner.\n基于不同维度, 可以总结出不同的 Model:\nSystem model(asynchronous/synchronous) Failure model(crash-fail, partitions, Byzantine) Consistency model(strong, eventual) 2.4 Partition and replicate 数据在不同节点之间如何存储是个非常关键的问题, 目前有两种经典的数据存储技术, 分片(partitioning)与冗余(replication):\npartitioning: data set can be split over multiple nodes to allow for more parallel processing.\nreplication: data set can be copied or cached on different nodes to reduce the distance between the client and the server and for greater fault tolerence.\npartitioning: 相当每个节点存储一部分数据, 所有节点的数据汇总起来就是该系统存储的总数据. 但是某个节点挂了, 该节点的数据就丢了\nreplication: 不同节点都存储同一份数据, 这样就可以减少读取不同数据的开销, 以及避免某个节点挂了, 导致部分数据不可用的情况. 但是需要更多的存储空间且不同节点之间数据的同步又是个大问题, 可以说是按下葫芦浮起瓢\nTo replication! The cause of, and the solution to all of life\u0026rsquo;s problems - Homer J Simpson(我很喜欢这句话)\n3 2. Up and down the level of abstraction 3.1 Abstraction 何谓抽象, 有过编程经验的我们自然不会陌生. 抽象, 即去除真实世界与问题域无关的干扰, 专注于问题本身, 使解决方案可被广泛采用.\nAbstractions make the world manaeable: simpler problem statements - free of reality - are much more analytically tractable and provided that we did not ignore anything essential, the solutions are widely acclicable.\n3.2 A system model 分布式系统的关键属性是分布式, 或者说, 程序运行在分布式环境:\n并发运行在独立节点\n通过网络通信, 可能出现某种不确定性或消息丢包\n没有共享内容或共享锁\n上面的特定会带来诸多的影响:\n每个节点都并发运行程序\n本地为先: 每个节点都可以快速访问他们的本地状态, 而所有关于全局状态的信息都有可能是过时的\n节点可能挂掉, 并从故障中恢复回来\n消息可能延迟或丢失(不同于节点故障, 通常很难区分节点故障或网络故障)\n节点间的时钟可能不同步(本地时间与全局时间不一定对应, 且很难观察到异常)\n通过定义一个模型(model)来标识实现一个分布式系统需要交互的环境与机制:\na set of assumptions about the environment and facilities on which a distributed system is implemented\nA robust system model is one that makes the weakest assumptions: any algorithm written for such a system is very tolerant of different environments, since it makes very few and very weak assumptions.\n模型需要越少的假设条件, 可以适应的环境就越多. 等价交换, fair enough.\n3.2.1 Nodes in our system model 节点是作计算与存储的主机(物理或虚拟主机), 它们有如下属性:\n可以运行程序\n可以存储数据到volatile memory(例如内存)或stable state(日志或磁盘)\n拥有时钟(可以准的或者是不准的)\n有很多的故障模型(failure models) 描述了节点挂掉(fail)的方式, 实际中, 大部分的系统都假设是个crash-recovery failure model, 即节点可能挂掉, 但是能从某个状态中恢复回来.\nA crash-recovery failure model: that is, nodes can only fail by crashing, and can(possibly) recover after crashing at some later point.\n3.2.2 Communication links in our system model communication links 不知道应该怎么翻译, 通讯链路? 不译也罢\ncommunication links 用于沟通不同的节点, 允许信息在双向流动. 部分算法假设网络是可靠的: 消息永不丢失并且永不延迟. 虽说这样假设有些许道理, 但是通常我们都是假设网络是不可靠, 因此消息可能丢失或者延迟.\n节点故障 vs 网络分区故障: 3.2.3 Timing/ordering assumptions 分布式系统在物理上分布在不同的位置, 这就会无可避免地会带来一个问题, 如果节点之间的距离不同, 那么节点之间的消息将会以不同的时间点, 甚至不同的顺序到达.\n有两个主要的时序模型:\nSynchronous system model: Process execute in lock-step; there is a known upper bound on message transimission delay; each process has an accurate clock.(这里的 known upper bound 指的是就是传输层的最大等待时间, 超过即由传输层进行重试. 当然, 这样的假设不大现实, 所以这样的模型实际应用比较少) Asynchronous system model: No timing assumptions - e.g. processes execute at indenpendent rates; there is no bound on message transmission delay; useful clocks doesn\u0026rsquo;t exist. 3.2.4 The consensus problem 共识问题(consensus problem) 是商业分布式系统关注的核心问题之一, 所谓的共识问题, 即若干个计算机或节点就某个值达到共识, 更正式的说法是:\nAgreement: Every corrent process must agree on the same value(不搞多数人的暴政, 只搞全体的暴政) Integrity: Every corrent process decides at most one value, and if it decides some value, then it must have been proposed by some process Termination: All processess eventually reach a decision.(不达到共识, 今天你就出不了这个门) Validity: If all corrent processes propose the same value V, then all correct processes decide V.(咱也不能赖皮) 3.3 The FLP impossibility result FLP 不可能原理(FLP impossibility result, 以作者的首字母命名, Fischer, Lynch and Patterson): 在网络可靠, 但节点失效(即便只有一个)的最小化异步模型系统中, 不可能存在一个可以解决一致性问题的确定性共性算法.\nThere does not exist a (deterministic) algorithm for the consensus problem in an asynchronous system subject to failures, even if message can never be lost, at most one process may fail, and it can only fail by carshing(stoppping executing).\nFLP impossibility result 告诉我们: 不要浪费时间, 去试图为异步分布式系统设计面向任意场景的共识算法(别忙了, 白费力气的).\nFLP impossibility result 定义了一个最坏情况, 在允许节点失效的情况下, 异步系统无法确保共识在有限时间内完成, 包括Paxos, Raft等算法也都存在无法达成共识的极端情况, 只是在工程实践中这种情况出现的概率很小.\n3.4 The CAP theorem 非常有名的CAP 定理, 即无法同时达到C,A,P三个属性, 这三个属性是:\nConsistency: all nodes see the same data at the same time.(准确来说, 应该是Strong Consistency)\nAvailability: node failures do not prevent survivors from continuing to operate.\nPartition tolerance: the system continues to operate despite message loss due to network and/or node failure.\n最多只能有两个属性被满足, 如下图:\n同时满足三个属性情况是无法实现的, 即中间交集处. 而满足两个属性的系统模型有如下三个:\nCA(consistency + availability): 弱化分区, 保证一致性和可用性, 也变成单机程序, 个人认为Oracle就是其中典范\nCP(consistency + partition tolerance): 弱化可用性, 可能出现无法提供可用结果的情形, 允许少数节点不可用. 典型算法就是Paxos\nAP(availability + partition tolerance): 弱化一致性, 节点之间可能失去联系, 导致全局数据不一致. 典型例子就是诸多的NoSql\nCA 和CP 模型都提供强一致的模型, 唯一的差别是, CA系统不允许任何节点故障, 因为CA系统无法区别节点故障和网络故障, 为了避免状态不一致, 只能停写; 而对于 2f+1 个节点的CP系统, 允许 f 个节点故障, 是因为其能通过 single-copy consistency 机制, 能保证状态能达到最终一致, 避免出现状态不一致, 从而支持部分节点不可用\n因此, 选择了网络分区, 就需要在高可用和强一致性之间作取舍, 而系统设计即是在基于不同的场景, 作出不同的取舍.\n同样, 强一致性和高性能也存在矛盾, 要保证强一致性, 自然需要节点之间通信达到共识, 这自然会拉高延迟, 这也要系统设计者作出取舍.\n3.5 Consistency model 何谓一致性模型呢:\nConsistency model: a contract between programmer and system, wherein the system guarantees that if the programmer follows some specific rules, the results of operations on the data will be predictable.\n一致性模型可以区分成两类: 强和弱一致性模型\n强一致性模型: Linearizable consistency Sequential consistency 弱(非强)一致性模型 Client-centric consistency models Causal consistenc: strongest model available Eventual consistency models 3.5.1 Strong consistency model Lineariable consistency 和 Sequential consistency 两个模型很相似, 关键的区别是: Lineraiable consistency 要求操作生效的顺序与操作的实际顺序相等; 而Sequential consistency允许对操作进行重新排序, 只要在每个节点上观察到的顺序保持一致.\n只要实现了强一致性模型, 模型保证将单机程序扩展到分布式集群时, 程序不会出现任何问题(程序本身的bug就另当别论), 而使用其他非一致性模型的扩展程序时, 就可能会出现问题.\n3.5.2 Client-centric consistency model 以客户为中心的一致性(Client-centric consistency model)模型是指以某种方式涉及客户或会话概念的一致性模型.\n例如, 以客户为中心的一致性模型可以保证客户永远不会看到一个数据项的旧版本. 这通常是通过在客户端库中建立额外的缓存来实现的.\n感觉这种模型见得不是很多.\n3.5.3 Eventual consistency 最终一致性(Eventual consistency)模型: 如果你停止更新值, 那么间隔某段时间后, 所有的节点都会看到同样的值. 那么某段时间是多少呢? 如果不能给该值给定个严格下限, 这个模型就和\u0026quot;人最终总会死\u0026quot;模型一样, 用处并不大.\n4 3. Time and Order 时序(Time and order)在分布式系统中非常重要, 那么为什么它这么重要呢? 前方提到分布式系统的目标就是可以像在单台机器上解决问题那样, 在多台机器解决同样的问题.\n单台机器运行模型是: 单个程序, 单进程, 单内存空间, 运行在单个CPU上, 而操作系统把多个CPU可能运行多个程序,共享内存的情况给抽象掉, 每个操作就好像人们通过一道门一样, 有预先定义好的前者与后者.\n现在中, 分布式系统运行在多个节点上, 可能同时拥有多个CPU和待运行操作. 如果还想像单台机器那样, 定义一个全序关系(total order), 要不就需要一个精确的时钟, 每个操作一个时间戳, 通过时间戳推算出执行顺序; 要不就需要额外的通信, 指定对应的序号.\n但维护精确的时钟困难且不可靠, 额外的通信成本高.\n4.1 Total and partial order 全序关系(total order)和偏序关系(partial order), 两个数学概念, 作者的解释有点简略, 当初上数学课又没有好好听课, 所以相关的概念不是很理解, 知乎上面有个挺详尽的解释:\n假设A是一个集合 {1,2,3} ;R是集合A上的关系,例如{\u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;1,2\u0026gt;,\u0026lt;1,3\u0026gt;,\u0026lt;2,3\u0026gt;}\n自反性:任取一个A中的元素x,如果都有\u0026lt;x,x\u0026gt;在R中,那么R是自反的.\n对称性:任取一个A中的元素x,y,如果\u0026lt;x,y\u0026gt; 在关系R上,那么\u0026lt;y,x\u0026gt; 也在关系R上,那么R是对称的.\n反对称性:任取一个A中的元素x,y(x!=y),如果\u0026lt;x,y\u0026gt; 在关系R上,那么\u0026lt;y,x\u0026gt; 不在关系R上,那么R是反对称的.\n传递性:任取一个A中的元素x,y,z,如果\u0026lt;x,y\u0026gt;,\u0026lt;y,z\u0026gt; 在关系R上,那么 \u0026lt;x,z\u0026gt; 也在关系R上,那么R是对称的.\n偏序: 设R是非空集合A上的关系,如果R是自反的,反对称的,和传递的,则称R是A上的偏序关系.\n全序:如果R是A上的偏序关系,那么对于任意的A集合上的 x,y,都有 x \u0026lt;= y,或者 y \u0026lt;= x,二者必居其一,那么则称R是A上的全序关系.\n所以可以看到,全序也是一种偏序. 偏序究竟在说啥,关键在于反对称性上,就是说,\u0026lt;x,y\u0026gt; 在关系R上,那么 \u0026lt;y,x\u0026gt; 不在关系R上,那我问你,\u0026lt;y,x\u0026gt; 关系是啥,就是大家都不知道.\n所以说偏序就在于你的集合A={1,2,3,4},有一些元素的关系根据R你是得不出的. 那么既然你不知道这个\u0026lt;y,x\u0026gt;,那么全序关系上,就多加一个条件,都有 x \u0026lt;= y,或者 y \u0026lt;= x,二者必居其一,这样你总知道了吧.\n偏序举例:假设有 A={1,2,3,4},假设R是集合A上的关系:{\u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;4,4\u0026gt;,\u0026lt;1,2\u0026gt;,\u0026lt;1,4\u0026gt;,\u0026lt;2,4\u0026gt;,\u0026lt;3,4\u0026gt;},\n那么:\n自反性:可以看到 \u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;4,4\u0026gt; 都在R中,满足. 反对称性:由于 \u0026lt;1,1\u0026gt;,\u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt;,\u0026lt;4,4\u0026gt; 不属于 x !=y ,所以不考虑这4种,对于 \u0026lt;1,2\u0026gt;,有 \u0026lt;2,1\u0026gt; 不在R中;对于\u0026lt;2,4\u0026gt; 有\u0026lt;4,2\u0026gt;不在R中;对于\u0026lt;3,4\u0026gt; 有\u0026lt;4,3\u0026gt; 不在 R中,满足. 传递性:\u0026lt;1,1\u0026gt;\u0026lt;1,2\u0026gt;在R中,并且\u0026lt;1,2\u0026gt;在R中;\u0026lt;1,1\u0026gt;\u0026lt;1,4\u0026gt;在R中,并且\u0026lt;1,4\u0026gt;在R中;\u0026lt;2,2\u0026gt;\u0026lt;2,4\u0026gt;在R中,并且\u0026lt;2,4\u0026gt;在R中;\u0026lt;3,3\u0026gt;\u0026lt;3,4\u0026gt;在R中,并且\u0026lt;3,4\u0026gt;在R中;等等其他,满足. 所以说R是偏序关系.\n全序举例:\n假设有 A={a,b,c},假设R是集合A上的关系:{\u0026lt;a,a\u0026gt;,\u0026lt;b,b\u0026gt;,\u0026lt;c,c\u0026gt;,\u0026lt;a,b\u0026gt;,\u0026lt;a,c\u0026gt;,\u0026lt;b,c\u0026gt;}和上述一样,可以证明具有自反性,反对称性,传递性,所以是偏序的.\n又因为有 \u0026lt;a,b\u0026gt;,\u0026lt;a,c\u0026gt;,\u0026lt;b,c\u0026gt;, 也就是说两两关系都有了,所以满足对于任意的A集合上的 x,y,都有 x \u0026lt;= y,或者 y \u0026lt;= x,二者必居其一,所以说是全序关系.\n目前还不是很清楚有什么用处.\n4.2 What\u0026rsquo;s time Time is a source of order. 时间可以解析成以下三种形式:\nOrder: 当提到时间是序列的来源时, 我们是在说: 我们可以给无序事件指派时间戳, 以此排序 我们可以使用时间戳来指定操作或者消息分发的顺序 我们可以通过时间来判断某个时间是否在另外一个事件前发生. Interpretation: 时间戳所代表的时间可以解析成秒, 分, 时, 日等人类可读的标识. Duration: 代表真实世界流逝的时间. 计算机世界的算法通常不关系时间的绝对值或人类可读的标识, 但duration 可被用于作某些重要的判断, 例如某个节点是延迟高还是挂了. 4.3 Does time process at the same rate everywhere 在任何地方, 时间流逝的速度是一样的么? 对于这个问题, 有三个不同的答案(即使是在物理世界, 答案也是, 不一样. 相对论说的):\n\u0026ldquo;Global clock\u0026rdquo;: yes \u0026ldquo;Local clock\u0026rdquo;: no, but \u0026ldquo;No clock\u0026rdquo;: no 4.3.1 Time with a \u0026ldquo;Global-clock\u0026rdquo; assumption 全局时钟(Global clock)模型假设有一个完美的全局时钟, 并且所有节点都与这个时钟通信, 且每个节点都精确同步, 且没有时间漂移:\n但是实际不存在这样的时钟, 任何的时钟都会有一定的精度损失, 并且时钟很难避免出现问题: 有人误修改了时钟; 或者有台过时的机器加入集群, 或者时钟因硬件故障而出现漂移.\n不过, 工程实践中的确有程序使用这样的模型:\nFacebook的Cassandra: 假设时钟是同步的, 因为它使用时间戳来处理写冲突, 以最新的时间为准 Google的Spanner: 使用TrueTime API, 保证时间同步的条件下, 又消除了时间漂移的最坏情况. 4.3.2 Time with a \u0026ldquo;Local-clock\u0026rdquo; assumption 每台机器有自己的时钟, 但是没有全局时钟, 这意味着你不能使用时间戳来比较两台不同机器操作的顺序.\n此模型更接近于真实世界, 它指定一个偏序关系: 节点内是有序的, 但是无法仅通过时间戳来进行跨节点排序.\n4.3.3 Time with a \u0026ldquo;No-clock\u0026rdquo; assumption 顾名思义, 无时钟模型不使用时间来排序, 而是使用另外的方式, 例如逻辑时间.\n这个模型也是偏序的: 事件在某个节点, 可以只通过计数器来进行排序, 但是跨系统排序需要额外的信息交换\n当时钟不再使用, 跨节点排序的最大精度上限就由通信延迟决定了. 逻辑时间, 最有名的就是Lamport clock, Lamport就是Paxos之父.\n4.4 Vector clocks(time of causal order) 物理时钟通过计数数和通信来跨分布式系统决定事件顺序, 而 Lamport clock 和 Vector clock是物理时钟的代替品. 使用Lamport clock的时候, 每个进程通过以下规则来维护一个计数器:\n每当一个进程起作用(does work)时, 递增计时器 每当一个进程发送一条消息时, 附带上此计时器 当收到一条消息的时候, 更新计时器的值为 max(local_counter, received_counter) + 1 Lamport clock是偏序关系, 当 timestamp(a) \u0026lt; timestamp(b), 意味着:\na 可能比 b 先发生 a 可能无法与 b 进行比较 已知的时钟一致性条件: 如果一个事件A比事件B先发生, 那么事件A的逻辑时钟也会先于事件B. 如果 a 和 b 来自相同的因果史, 即两个时间戳值都是由同一个进程产生; 或者 b 是 a 发送的消息的响应, 那么 a 先于 b 发生.\n由此可见, 如果两个系统没有交集, 那么它们的时钟值将无法比较. 不过, 这里还有个关键的属性, 某个节点而言, 如果它以 ts(a) 来发送消息, 并以 ts(b) 收取此条消息的响应, 那么 ts(b) \u0026gt; ts(a)\nVector clock是Lamport clock 的扩展, 对于有 N 个节点的分布式系统, 会维护一个size = N的数组 [t1, t2, ....], 对应记录每个节点的计数. 计数值的更新规则:\n每当一个进程起作用(does work)时, 递增数组对应该进程的计数值 每当一个进程发送一条消息, 附带上整个计数器数组 每当收到一条消息: 更新数组中的每个元素 max(local, received) 递增数组中当前节点对应的计数值 4.5 Failure detector(time for cutoff) 假设程序运行在某个节点A上, 它与节点B通信异常了, 怎么判断是网络延迟还是节点B挂了呢? 如果过了相当一段时间后, 我们可以判断节点B挂了, 那么相当一段时间又是多长呢?\nChandra 提出了failure detector这个概念来解决这个问题, failure detector包括两个属性, 完整性(completeness)与精确性(acuracy).\nStrong completeness: Every crashed process is eventually suspected by every correct process Weak completeness: Every crashed process is eventually suspected by some correct process Strong accuracy: No correct process is suspected ever Weak accuracy: Some correct process is never suspected. 完整性比准确性更容易实现, 避免错付没有问题的进程是很难的, 除非可以假设消息延迟的上限, 但是这样的假设只能在同步模型中实现, 所以failure detector在同步模型中可以达到相当的高精度. 而对于没有延迟上限的系统(异步模型), failure detector能做到的极限就是最终准确.\n下面的图阐述了系统模型与问题可解决性之间的关系:\n理想情况下, 我们希望failure detector可以根据网络条件动态调整检测阈值, 以此避免硬编码阈值, 例如TCP的动态超时计算算法那样. Cassandra 使用的是 accrual failure detector, 输出结果是一个故障的可疑度(0到1之间值), 而不是简单的 挂/没挂, 以此为应用根据可疑度自行决断, 提供更高的灵活度.\n4.6 Time, order and performance 使用全序关系也是可能的, 但是为了协调全局顺序, 会付出高昂的性能代价.\n如果你对时间_顺序_同步性要求没有那么高, 你可以获得相当的性能提升. 那么, 什么时候需要顺序来保证正确性呢? 后面提到 CALM定理 会为你提供答案.\n说到底, 又是取舍的话题, 下面的情景只存在电影中:\n5 4. Replication: Preventing Divergence 复制(replication)是分布式系统需要考虑的诸多问题中的其中一项, 但是却非常关键, 并与选主(leader election), 失败检测(failure detection), 共识(consensus)和原子广播(atomic broadcast)等问题息息相关.\n现在让我们先来看复制长什么样, 假设有某个数据库, 存储初始状态的数据, 客户端请求修改数据状态:\n复制的模式可以划分成以下几个步骤:\n(Request) 客户端发送请求到服务端 (Sync) 同步流程开始处理 (Response) 响应返回到客户端 (Async) 异步流程开始处理 由此可见, 复制模式可以划分为同步复制(Synchronous replication)与异步复制(Asynchronous replicatin)\n5.1 Synchronous replication 同步复制模式(也被称为active/eager/push/pessimistic replication):\n我们可以看到三个不同的阶段:\n首先,客户端发送请求. 接下来,处理此前提到的同步复制流程, 即客户端被阻塞至服务端响应. S1与S2, S3通信, 并等待, 直到收到所有服务器的响应 向客户端发送响应, 告知其结果(成功或失败) 可以观察到, 这是一种 N 对 N 的模式: 在返回响应前, 该请求必须被每个节点所确认, 任何一个节点都不允许丢数据, 否则系统就无法成功向所有节点写入数据, 就无法继续提供写入服务, 可能只允许提供只读服务. 此外, 该系统的响应速度取决于最慢那台服务器的响应速度.\n虽说同步模式性能有所欠缺, 但是能提供强持久性保证(strong durability guarantees): 只要向客户端返回成功, 就能保证所有节点写入成功, 要丢失这次的更新数据, 就需要所有节点都丢失.\n5.2 Asynchronous replication 异步模式(也被称为 passive/pull/lazy replication):\n在收到客户端的请求后, Master 节点(S1)立即返回了一个响应, 然后在若干时间后, 执行异步复制: Master 节点以某种模式与其他节点通信, 通知他们更新数据副本. 细节就取决具体的算法.\n这是一个 1 对 N 的模型, 立即返回一个响应, 然后在稍后的某个时间进行复制. 从性能的角度解析, 异步模式可以快速响应, 客户端无需阻塞等待.\n但只能提供弱持久性保证(weak durability guarantees), 只有一个节点持有更新数据, 如果这个节点挂了, 又还没有复制到其他节点, 数据就永久丢失了, 所以给客户端响应成功, 也只能表示, 有概率复制成功.\n5.3 An overview of major replication approaches 在讨论了两种基本的复制模式:同步复制和异步复制之后,我们来看看主要的复制算法.\n有很多对复制算法进行分类的指标, 作者认为第二个指标是(继同步与异步之后):\n避免数据不一致的复制算法 承受数据不一致风险的复制算法 第一类算法的特征是它们\u0026quot;看起来像个单机系统\u0026quot;, 尤其是出现故障的时候, 算法确保系统中只有一个复本是可用的(active), 此外, 该系统保证副本数据总是一致的(consensus problem).\n不同复制算法需要交换的消息数:\n1 * n 条消息(asynchronoous primary/backup) 2 * n 条消息(synchronous primary/backup) 4 * n 条消息(2-phase commit, Multi-Paxos) 8 * n 条消息(3-phase commit, Paxos with repeated leader election) 不同的复制算法有不同的特点, 以下是改编自Google 的Ryan Barret 的一张图:\n上图中的一致性、延迟、吞吐量、数据丢失和故障转移的特点,实际上可以追溯到两种不同的复制模式:\n同步复制(在响应前等待)和异步复制(立即响应).\n当你等待时,你会得到更差的性能,但有更强的保证.\n5.4 Primary/backup replication 主从复制, 可能是最常用的复制算法, 也是最基本的复制算法. 所有更新操作都是在主机进行, 然后复制数据到备机. 有两种变体:\nsynchronous primary/backup replication asynchronous primary/backup replication 前者需要两条消息(update + ack), 而后者只需要一条消息(update). 主_从复制非常常见, MySQL 复制使用的就是主_从复制, MySQL 支持三种模式复制:\n同步: 客户端请求, 先写入主机, 然后同步到所有备机, 成功后响应客户端, 在此之间, 阻塞客户端(性能最差) 异步: 客户端请求, 先写入主机, 然后响应客户端, 再同步备机(同步备机前主机挂, 则丢失数据) 半同步: 客户端请求, 先写入主机, 再同步到备机, 响应客户端, 然后再同步到其他备机(可靠性与性能的折衷) 但是即使是半同步模式, 也会存在问题:\n主机收到一个更新请求,并将其同机给备机 备机同步成功并ACK主机 主机在向客户端响应请挂了 客户端认为是提交失败, 但实际备机写入成功; 如果备机升主, 数据就会有问题.\n所以可见, 主/从复制模式提供的一致性保证, 只能说明是\u0026quot;尽力而为\u0026quot;. 为了避免异常导致一致性保证被违反, 我们需要增加多一轮的消息传递, 就来到了段提供协议(2PC)\n5.5 Two phase commit (2PC) 下图说明两阶段提交(2PC)的消息流:\n[ Coordinator ] -\u0026gt; OK to commit? [ Peers ] \u0026lt;- Yes / No [ Coordinator ] -\u0026gt; Commit / Rollback [ Peers ] \u0026lt;- ACK 2PC流程如下:\n在一阶段(投票), 协调者将更新请求发送给所有的参与者, 每个参与者处理请求并决定是commit 还是 rollback. 在二阶段(决定), 协调者决定结果并通知每个参与者. 2PC容易出现阻塞,因为单个节点的故障(参与者或协调者)会阻塞进度,直到该节点恢复; 并且因为它是 N 对 N, 所以在最慢的节点确认前不能写入. 因此2PC的性能不理想也是情理之中.\n回想之前的CAP理论, 2PC就属于是CA, 它弱化了分区容忍性,所以2PC解决的故障模型不包括网络分区, 当出现网络分区的时候, 2PC就懵了, 不知所措, 只能等待网络分区合并.\n2PC在性能和容错性之间取得了很好的平衡,这就是为什么它在关系型数据库中一直很受欢迎.\n然而,较新的系统通常使用分区容忍的共识算法(partition tolerant consensus algorithms),因为这样的算法可以提供从临时网络分区中的自动恢复,以及更优雅地处理节点间延迟的增加.\n5.6 Partition tolerant consensus algorithms 提起分区容忍的共识算法, 最有名的就是Paxos算法, 但是Lamport大神把他的论文写小说, 让大家苦不堪言, 正因为Paxos出了名的难理解, 所以出现了Raft. 但Goole Chubby的作者Mike Burrow认为:\nThere is only one consensus protocol, and that\u0026rsquo;s Paxos – all other approaches are just broken versions of Paxos. (世界上只有一种共识协议,就是Paxos,其他所有共识算法都是Paxos 的残缺版本)\n首先让我们看下分区容忍算法的特点:\n5.6.1 Network partition 网络分区是指一个或几个节点的网络链接失效. 节点本身继续保持活跃,它们甚至可以接收来自网络分区一侧的客户端的请求.\n网络分区很棘手的,因为在网络分区期间,不可能区分一个节点是挂了呢还是出现网络分区导致请求不可达. 如果发生了网络分区,但没有节点发生故障,那么系统就被分成两个分区. 如下图:\n2个节点时, 节点故障vs 网络分区:\n3个节点时, 节点故障vs 网络分区:\n分裂成两个网络分区时, 共识算法必须要处理这种对称情况, 只允许一个网络分区保持活跃.\n5.6.2 Majority decisions 出现网络分区时, 是怎么保证只有一个分区是活跃的呢? 答案就来自于日常生活: 投票. 只要只要 N 个节点中的 (N/2+1) 投票同意更新, 系统就更新成功, 可以继续运行, 少数派就可以自己独处, 直到分区合并.\n因此, 使用共识算法的系统的节点数都是奇数(3,5或7), 避免出现无法投票成功的情况. 例如,如果节点数为3,那么系统可以容忍一个节点故障;有5个节点,系统容忍两个节点故障.\n除去网络分区, 基于多数人投票的共识算法还可以容忍抖动或故障导致的反对票, 并以此提供系统健壮性.\n5.6.3 Roles 构建一个系统有两种途径:所有节点可以有相同的责任,或者节点可以有单独的、不同的角色.\n而共识算法通常选择为每个节点设置不同的角色, Paxos中的提议者与投票者, 对应Raft中的领导与从众, 也就是领袖与民众. 拥有领袖是一种优势, 可以让系统更有效率.\n角色在系统正常运行过程是固定的, 但这并不意味着领导不会出现意外, 或者可以无限期当领导.\n5.6.4 Epochs 在Paxos和Raft算法, 正常运行的每个一个时间被称为一个纪元(有魔戒的味道了. 在Raft中被称为期限), 在每个纪元期间,只有一个节点是指定的领导者.\n选举成功后,同一个领导者会协调到纪元结束(任期到了就要换人, 不能賴着不走.). 如上图所示(来自Raft的论文),一些选举可能会失败,导致纪元立即结束.\n5.6.5 Leader changes 所有节点开始时都是民众;一个节点在开始时被选为领袖. 在正常运行期间,领袖与民众通过心跳包保持通信,民众可以检测领导是否挂了或被分割到另一个网络分区.\n当一个节点检测到领导变得没有反应时(或者在最初的情况下,不存在领导者),它就会切换到一个中间状态(在Raft中称为 \u0026ldquo;候选\u0026rdquo;),在那里它将术语/周期值增加1,启动竞选程序并竞争成为新领导(王侯将相, 宁有种乎).\n为了当选为领导,一个节点必须获得多数票. 分配选票的一种方法是以先来先得的方式分配选票;这样一来,最终会选出一个领导者.\n5.6.6 Normal operation 在正常操作中,所有的提议都要经过领导节点.\n当客户端提交一个提案(如更新操作), 领导会联络所有节点. 如果不存在竞争性的提议, 领导者就会提出该值. 如果大多数民众接受该值, 那么该值就被认为被接受了. 一旦一个提案被接受, 其值就不能改变, 关于此, Lamport的表述是:\nP2: If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v.\n为了实现这个属性, 领导提案前必须先询问民众, 编号最高的提案与值是哪个, 如果领导发现现在已经有一个提案在执行, 那么就必须先把这个提案执行完, 而不是提出自己的提案.\n即:\nP2c. For any v and n, if a proposal with value v and number n is issued [by a leader], then there is a set S consisting of a majority of acceptors [followers] such that either (a) no acceptor in S has accepted any proposal numbered less than n, or (b) v is the value of the highest-numbered proposal among all proposals numbered less than n accepted by the followers in S.\n这是Paxos算法的核心, 为了确保领导在咨询民众编号最高的提案与值时, 没有竞争提案出现,领导要求民众不能接受比当前编号低的提案. 所以把Lamport的话组合起来, 使用Paxos算法要达到一个共识, 需要两轮的沟通:\n[ Proposer ] -\u0026gt; Prepare(n) [ Followers ] \u0026lt;- Promise(n; previous proposal number and previous value if accepted a proposal in the past) [ Proposer ] -\u0026gt; AcceptRequest(n, own value or the value [ Followers ] associated with the highest proposal number reported by the followers) \u0026lt;- Accepted(n, value) 第一轮, 带上最近的提案与值, 就是为了避免在第一轮问询与第二轮更新之间, 接受一个新更新请求, 就工程实践的角度来看, 此处的previous value with the highest proposal number 相当于是一个乐观锁, 避免被更少的值所更新; 假如中间插入的更新请求成功, highest proposal number 就不是第一轮问询持有的值, 更新就会失败, 避免被覆写.\n5.7 Paxos, Raft, ZAB 有名的分区容忍性共识(Partition tolerant consensus algorithms)算法:\nPaxos: 以希腊的Paxos岛命名, 最重要的分区容忍性共识算法之一(甚至可以把之一去掉), 以难实现难理解著称(但Lamport说Paxos 很简单, 受不了一群人整天问他Paxos算法, 就写了篇 Paxos Make Simple的论文, 人与神的差距), 被用在谷歌的许多系统中, 例如Chubby, BigTable, Spanner等等 ZAB: the Zookeeper Atomic Broadcast protocol, 被用在Apache Zookeeper中, Zookeeper 可以算是Chubby的开源版本 Raft: 对Paxos算法的简化和改进, 被认为更易于学习与理解. 5.8 Replication methods with strong consistency 以下是支持强一致性复制算法的一些特征:\nPrimary/Backup 单一, 静态 Master 节点(主机) Slave 节点(备机)不参与执行操作 复制延迟无上限 不支持分区 手动/临时故障切换,不容错,\u0026ldquo;热备份\u0026rdquo; 2PC 一致表决: commit or rollback(to be or not to be) 静态 Master 节点 不支持分区, 对尾部延迟(tail latency)敏感 Paxos 多数人投票 动态 Master 节点 可应对N/2 - 1个节点的故障 对尾部延迟(tail latency)不敏感 6 5. Replication: Accepting Diveragence 笔记待续\n","permalink":"https://ramsayleung.github.io/zh/post/2021/distributed_system_for_fun_and_profit/","summary":"1 Distributed Systems for fun and profit source: http://book.mixu.net/distsys/ 2 1. Basic 2.1 Basic concept 本章介绍了分布式系统的基本概念, 例如 scalablity, perfomance, latency, availability 关于 latent, 这里给出了一个很cool的描述: For example, imagine that you are infected with an airbone virus that turns people","title":"(笔记)Distributed Systems for fun and profit"},{"content":"1 前言 领导平日开会时,总是对我们说,你们思考问题角度太单一与片面,没有想清楚问题背后的逻辑与作用因子。\n所以领导总是会不厌其烦地给我们推荐《系统思考》这本书:\n初读觉得平平无奇,后来开始使用《系统思考》的系统循环图来分析问题。 既能帮助自己理解清问题的脉络,又能清晰地向他人阐述自己的思路,可谓利器一件。\n2 概念 所谓的系统思考, 即在真实世界中,问题不是简单且孤立的,而是相互联系,交织影响的。\n所以处理真实世界中复杂问题的最佳方式就是用整体的观点观察周围的事物。\n只有拓宽视野,才能避免“竖井”式思绪和组织“近视”问题,做到既见树木,又见森林。\n所谓的系统,是由一群相互连接的实体构成的整体。\n系统具有两个关键特性, 即自组织和涌现。\n自组织:在没有外力的干涉下,动态系统仍能展示出某种的稳定结构, 即自行车的运动,鸟群的盘旋等 涌现:存在一股将给定系统与周围环境联系起来的能量流. 自组织系统与周围环境的能量交换,构成\u0026quot;开放系统\u0026quot;(有点玄幻) 3 系统循环图 系统循环图是进行系统思考的主要工具\n3.1 连接 所有的系统循环图都具有如下基本形式:\n“原因”处于连接箭头的起点,而“结果”处在箭头的尾部。\nS型(+)连接: 原因的增长而导致结果也增长的连接 O型(-)连接: 原因的增长导致了结果的下降的连接 系统循环图中的每个连接必须是S型连接或者O型连接两者中的一种\u0026ndash;不会有其他可能性\n3.2 回路 代表因果链的回路最终连接自己身上,整个回路没有起点,没有终点,每项事物都最终和其他事物产生联系,这样的回路就被称为反馈回路\n3.2.1 增加回路 随着环的每次旋转而不断得到加强,这个情况被称为正反馈,与此对应的系统循环图被称为正反馈回路或增强回路\n相同的算法, 不同的输入导致不同的输出; 因此对于增加回路, 如果输入为正, 则为良性循环; 否则则为恶性循环。\n3.2.2 调节回路 寻求达到某个特定的目标, 减少目标与现实差距的回路,被称为调节回路\n3.2.3 分辨增加回路与调节回路 对于任何连续闭合回路, 如果有偶数个O型连接,该回路为增加回路;否则为调节回路。\n3.3 绘制系统循环图黄金法则 了解问题的边界 从有趣的地方开始 询问“它将驱动什么”以及“它的驱动力是什么” 不要陷入混乱 不要使用动词,请使用名词 不要使用类似于“在xxxx方面增长/降低”这样的词 不要害怕从未出现过的项目 随着进展及时确定连接类型 坚持就是胜利,持续前进吧 好图表必须反映实况 不要爱上你的图表 没有“已经完成”的图表 4 实操 使用系统循环图来分析「置身事内」一书中的政府土地财政与债务风险的问题:\n5 后续 有用的东西就到这里了,剩下的都是例子和实践。\n讲的东西可能很有用,但是用简单的话就能讲清楚,最后写成了一本书,有点灌水了。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E7%B3%BB%E7%BB%9F%E6%80%9D%E8%80%83/","summary":"1 前言 领导平日开会时,总是对我们说,你们思考问题角度太单一与片面,没有想清楚问题背后的逻辑与作用因子。 所以领导总是会不厌其烦地给我们推荐《","title":"系统思考"},{"content":"1 前言 因为此前读了彼德.海斯勒的《寻路中国》,对中国的社会内在结构与驱动力产生了兴趣.\n当然, 我是无法像彼得那样子, 驾车环游中国来作田野调查. 既然无法亲身躬行, 那么只有从纸上得来了. 以此契机, 阅读了费孝通先生的名作《乡土中国》\n2 社会结构 阅读完《乡土中国》之后,有种拨开迷雾, 豁然开朗的感觉, 解答了困扰我许久的问题。因为最近在阅读和学习《系统思考》,于是便使用系统循环图总结了费先生在书中的解析。\n2.1 农业为本 从基层上看去, 中国社会是乡土性的, 而我们的民族自古而来, 就在拖泥带水下田讨生活的, 长江与黄河流域全是农业区. 这样说来, 我们确是和泥土分不开了, 从土里长出过光荣的历史, 自然也会受到土的束缚, 现在很有些飞不上天的样子.\n农业和游牧或工业不同, 它是直接取资于土地的, 游牧的人可以逐水草而居, 飘忽不定; 做工业的人可以择地而居, 迁移无碍. 而种地的人却搬不动地, 长在土里的庄稼行动不得。\n当然, 乡村人口是不可能一直固定的, 因为人口一直在增加, 一块地只要经过几代的繁殖, 人口就到了饱和点; 过剩的人口自得宣泄出外, 负起锄头去另辟新地。可是老根不常动。这是宣泄出外的人,像是从老树上被风吹出去的种子,找到土地的生存,又形成一个小小的家庭殖民地,找不到土地的也就在各式各样的命运下被淘汰,或是发迹。\n《国富论》中提到一个社会越是发展,社会分工就越细,但农业除外。耕种活动中的分工程度很低,至多是男女间有一些分工,例如女人插秧,男人锄地。这种合作与其说是为了增加效率,不如说是因为在某一时间男人忙不过来,家里人来帮忙。\n既然耕种不需要精细的分工,那为什么却会聚集起各种大大小小的村子呢?\n中国农民聚村而居的原因大致有以下几点:\n每家所耕的面积小,所谓小农经营,所以聚在一起住,住宅和农场不会距离过分远 需要水利的地方,有合作的需要,在一起住,合作起来比较方便 为了安全,人多了容易保卫 土地平等继承的原则下,兄弟分别继承祖上的遗业,使人口在一地方一代一代地积起来,成为相当大的村落。 2.2 熟人社会 生活上被土地被囿住的乡民,他们平素所接触的是生而与俱的人物,正像我们的父母兄弟一般,并不是由于我们选择得来的关系,而是无须选择,甚至先我而在的一个生活环境。\n熟悉是从时间里,多方面,经常的接触中所发生的亲密的感觉,这感觉是无数次小摩擦里练习出来的结果。\n而文字发生之初是“结绳记事”,需要结绳来记事是为了在空间和时间中人和人的接触发生了阻碍,我们不能当面讲话,才需要找一些东西来代话。 而熟悉的环境中,我们通过言语,动作,表情就可以表达自己的感想和想法,自然无需退而求其次去使用文字,也难怪文字此前在乡村用处有限,以致于多数人都是不识文字的“文盲”。\n现代社会是个陌生人组成的社会,各人不知道各人的底细,所以得讲明白; 还要怕口说无凭,要立字为据,这样才发生了法律。 在乡土社会中法律是无从发生的。“这不是见外了么?”乡土社会里从熟悉得到信任。\n2.3 差序格局 在《乡土中国》中,费孝通先生提出了一个差序格局的概念,用以描述中国社会和西方社会结构的差异。\n西洋的社会有些像我们在田里捆柴,几根稻草束成一把,几把束成一扎,几扎束成一捆,几捆束成一挑。每一根柴在整个挑里都属于一定的捆,扎,把。我们可以称之为团体格局。而中国的社会结构和西洋的格局是不相同的,我们的格局不是捆一捆扎清楚的柴,而是好像把一块石头丢在水面上所发生的一圈圈推出去的波纹。每个人都是他社会影响所推出去的圈子的中心。被圈子的波纹所推及的就发生联系。此为所谓的差序格局。\n我们的社会中最重要的亲属关系就是这种丢石头形成同心圆波纹的性质。亲属关系是根据生育和婚姻事实所发生的社会关系。从生育和婚姻所结成的网络,可以一直推出去包括无穷的的人,过去的,现在的和未来的人物。这个网络像个蜘蛛的网,有一个中心,就是自己。每个人都有这么一个以亲属关系布出去的网,但是没有一个网所罩住的人是相同的。\n穷在闹市无人问,富在深山有人知。中国传统结构中的差序格局具有这样伸缩能力。在乡下,家庭可以很小,而一到有钱的地主和官僚阶层,可以大到像个小国。中国人也特别对世态炎凉有感触,正因为这富于伸缩的社会圈子会因中心势力的变化而大小。\n2.4 私德与法律 社会结构格局的差别引起了不同的道德观念。道德观念是在社会里生活的人自觉应当遵守社会行为规范的信念。\n在西方的“团体格局”中,道德的基本观众建筑在团体和个人的关系上。团体是一束人和人的关系,是一个控制和个人行为的力量,是一种组成分子生活所依赖的对象,是先于任何个人而又不能脱离个人的共同意志。 在“团体格局”的社会中才发生笼罩万有的神的观念。团体对个人的关系的象征在社对信徒的关系中,是有个赏罚的裁判者,是个公正的维持者,是个万能的保护者。\n如果要了解西洋的“团体格局”社会中的道德体系,决不能离开他们的宗教观念。宗教的虔诚和信赖不但是他们道德观念的来源,而且还是支持行为规范的力量,是团体的象征。\n在象征着团体的神的观念下,有着两个重要的派生观念:每个个人在神前的平等;一是神对每个个人的公道。上帝在冥冥之中,象征着团体无形的实在;但是在执行团体的意志时,还得有人来代理。“代理者”Minister是团体格局的社会中一个基本的概念。执行上帝意志的牧师是Minister, 执行团体权力的官吏也是Minister, 都是“代理者”,而不是神或团体的本身。\n这上帝和牧师,国家和政府的分别是不容混淆。\n神对每个个人是公道,是一视同仁的,是爱的;如何代理者违反了这些“不证自明的真理”,代理者就失去了代理的资格。团体格局的道德体系中于是发生了权利的观念。人对人得互相尊重权利,团体对个人也必须保障这些个人的权利,防止团体代理人滥用权力,于是产生了宪法。\n而在以自己为中心的社会关系网络中,最主要的自然是\u0026quot;克己复礼\u0026quot;, \u0026ldquo;壹是皆以修身为本\u0026rdquo;\u0026ndash;这是差序格局中道德体系的出发点。一个差序格局的社会,是由无数私人关系搭成的网络。这网络的每一个结都附着一种道德要素,因之,传统的道德里不另找出一个笼统的道德观念,所有的价值标准也不能超脱于差序人伦而存在。\n2.5 家族与感情 在西洋,家庭是团体性的社群,这一点我在上面已经说明有严格的团体界限。因为这缘故,这个社群能经营的事务很少,主要的是生育儿女。但在中国乡土社会中,家并没有严格的团体界限,可以沿依需要,沿亲属差序向外扩大。中国的家扩大的路线是单系的,就是只包括父系的这一方面。\nFigure 1: 家族关系\n中国的家是一个事业组织,家的大小是依着事业的大小而决定的。一切事业都不能脱离效率的考虑。求效率就得讲纪律;纪律排斥私情的宽容。在中国的家庭里有家法,在夫妇间得相敬,女子有着三从四德的标准,亲子间讲究负责与服从。\n因为社群不限于夫妻,功能不限于生育,难怪两性间的矜持和保留,不肯像西洋人一般的在表面上流露。 所谓感情相当于普通所谓激动,动了情,甚至说动了火。感情的激动改变了原有的关系,即如果要维持固定的社会关系,就得避免感情的激动。稳定社会关系的力量,不是感情,而是了解。\n如此的社会,如此的家庭关系,每个人都只是完成必备工作的「工具」,人性,作为人的诉求着实不在考虑之内。 所谓了解,是接受着同一的意义体系。同样的刺激会引起同样的反应。乡土社会是靠亲密和长期的共同生活来配合各个人的相互行为,社会的联系是长成的,是熟习的,到某种程度使人感觉到是自动的。\n恋爱是一项探险,是对未知的摸索。这和友谊不同,友谊是可以停止在某种程度上的了解,恋爱却是不停止的,是追求。这种企图并不以实用为目的,是生活经验的创造,也可以说是生命意义的创造,但不是经济的生产,不是个事业。\n社会秩序范围着个性,为了秩序的维持,一切足以引起破坏秩序的要素都被遏制着。男女之间的鸿沟从此筑下。乡土社会是个男女有别的社会,也是个安稳的社会。\n2.6 伦理与教化 礼是社会公认合式的行为规范。合于礼的就是说这些行为是做得对的,对是合式的意思。\n如果单从行为规范一点说,本和法律无异,法律也是一种行为规范。礼和法不相同的地方是维持规范的力量。法律是靠国家的权力来推行的。“国家”是指政治的权力,在现代国家没有形成前,部落也是政治权力。而礼却不需要这有形的权力机构来维持。维持礼这种规范的是传统。\n传统是社会所累积的经验。行为规范的目的是在配合人们的行为以完成社会的任务,社会的任务是在满足社会中各分子的生活需要。人们要满足需要必须相互合作,并且采取有效技术,向环境获取资源。\n这套方法并不是由每个人自行设计,或临时聚集了若干人加以规划的。人们有学习的能力,上一代所试验出来有效的结果,可以教给下一代。这样一代一代地累积出一套帮助人们生活的方法。\n从每个人说,在他出生之前,已经有人替他准备下怎样去应付人生道上所可能发生的问题了。他只要“学而时习之”就可以享受满足需要的愉快了。\n乡土社会是安土重迁的,生于斯、长于斯、死于斯的社会。不但是人口流动很小,而且人们所取给资源的土地也很少变动。在这种不分秦汉,代代如是的环境里,个人不但可以信任自己的经验,而且同样可以信任若祖若父的经验\n在都市社会中一个人不明白法律,要去请教别人,并不是件可耻之事。事实上,普通人在都市里居住,求生活,很难知道有关生活、职业的种种法律。法律成了专门知识。不知道法律的人却又不能在法律之外生活。\n但是在乡土社会的礼治秩序中做人,如果不知道“礼”,就成了撒野,没有规矩,简直是个道德问题,不是个好人\n所谓礼治就是对传统规则的服膺。生活各方面,人和人的关系,都有着一定的规则。行为者对于这些规则从小就熟习,不问理由而认为是当然的。长期的教育已把外在的规则化成了内在的习惯。维持礼俗的力量不在身外的权力,而是在身内的良心。\n每个人知礼是责任,社会假定每个人是知礼的,至少社会有责任要使每个人知礼。所以“子不教”成了“父之过”。这也是乡土社会中通行“连坐”的根据。儿子做了坏事情,父亲得受刑罚,甚至教师也不能辞其咎,教得认真,子弟不会有坏的行为。打官司也成了一种可羞之事,表示教化不够。\n3 总结 虽说《乡土中国》写作时间已经过去了80年,中国也在城填化的方向大踏步前进,但是费先生的书仍然让我更加清晰地了解中国,了解中国人的人情世故,婚姻,传统背后深层的动机。\n说到底,“土气”才是最中国的地方。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E4%B9%A1%E5%9C%9F%E4%B8%AD%E5%9B%BD/","summary":"1 前言 因为此前读了彼德.海斯勒的《寻路中国》,对中国的社会内在结构与驱动力产生了兴趣. 当然, 我是无法像彼得那样子, 驾车环游中国来作田野调查.","title":"乡土中国"},{"content":"1 前言 1.1 关于君主论 国内的电视剧总会提到所谓的「帝王心术」,「帝王心术」究竟是什么? 马基雅维利就从他的视角,向世人剖析,什么是帝王心术,一个优秀的君主应该如何言行处事。\n《君主论》是文艺复兴时期,意大利城邦国家佛罗伦萨共和国的执政官马基雅维利的著作,他的主张有违常人道德观念: 「如有必要,君主是应该使用不道德的手段去实现目标(例如荣誉和生存)」。\n被誉为「一本毁誉参半的奇书,一直被奉为欧洲历代君主的案头之书」\n一八一五年滑铁卢战役后,普鲁士士兵在拿破仑的座驾中发现了一份写满批注的、以《君主论》为主体的《马基雅维利著作集》。\n难怪这本书会被后世诸多政治家所批评,因为马基雅维利把那些「只能做,不能说」的事情,都清楚地写了出来,让世人更好地了解君主行为背后的逻辑。连「马基雅维利主义」都成为了「权术」的代名词。\n从某个角度而言,马基雅维利就像是普罗米修斯,把政治家们的火种盗走,送给了人类。\n1.2 动机 攀附下古人, 既然马基雅维利不是君主可以写出《君主论》, 那么我这个既不是君主的人, 也未曾豪言「彼可取而代之」,立志成为君主的普通人, 也来感受下《君主论》背后的权术.\n我既不想吃猪肉,也不想看猪跑,只是想领会一下,他们是怎么赶猪的。\n正如那些描绘风景的人一样, 为了考察山岳的性质和高地的高度, 就置身到平原, 而为了考察平原便必须高踞顶峰. 同样的道理,\n要真正认识人民本质的人, 需要站在君主的位置上, 而真正认识君主本质的人则需要站在人民的位置上.\n2 道德与政治 马基雅维利认为君主治国不一定恪守道德. 传统所谓的正义, 自由, 宽厚, 信仰,虔诚等美德没有自身的价值, 因为\u0026quot;人们实际上怎样生活与人们应当怎样生活之间有很大距离\u0026quot;\n作为君主, 如果只是善良就会灭亡; 一个君主必须狐狸般的狡猾, 狮子般的凶狠\n对于守信义之类的美德, 君主的正确态度是: 在守信有好处时应当守信, 否则不要守信. 君主有时候必须不讲信义, \u0026ldquo;但是必须把这种品格掩饰好,必须习惯于冒充善者、口是心非的伪君子\u0026rdquo;\n做君主的并没必要条条具备上述的品质(各种传统美德), 但是非常有必要显得好像有这些品质.\n也难怪马基雅维利会被人恨之入骨, 他就像皇帝的新衣里面的小孩, 把人们都共知的但不敢说出的话说了出来, 以个人的私德来要求政治人物, 难免过于可笑;\n毕竟政治从来都只论利弊, 不论道德.\n2.1 苏德互不侵犯条约 谁成想过,在爆发20世纪最为惨烈,最为致命,双方战损近3000千万人的战争「苏德战争」的前两年, 苏联才和德国签订了《苏德互不侵犯条约》:\n希特勒计划在1939年9月1日攻击波兰,因此指示外长里宾特洛甫在8月23日前往苏联,指示其接受苏联的所有条件, 以避免入侵波兰时,又要面对法国和苏联同时介入。\n最后,双方在8月23日签订苏德互不侵犯条约,希特勒和斯大林更协议瓜分波兰。\n在协议签定背后的博弈与斗争:\n俄国十月革命后,由于意识形态及苏联的领土扩张主义等原因,西方国家与苏联的矛盾激化了。 而纳粹德国也反对共产主义,纳粹德国的崛起对苏联夺取波兰领土形成阻碍。\n希特勒一方面谋划入侵波兰,将东普鲁士和德国本土连接,一方面又加紧准备向西方侵略扩张。\n斯大林认为英法不愿和德国开战,故此放弃与英法共同抗纳粹德国,改为与纳粹德国保持表面上的友好关系, 既可以从中谋取利益,又可争取时间及空间应对德国的军事行动,更可趁机侵占芬兰和波罗的海三国。\n苏联先在1939年8月和纳粹德国订立苏德商业协议,为德国提供生产武器的物资, 而德国则向苏联提供机器及车辆等技术产品,增进苏德关系。\n纳粹德国密谋吞并波兰时,苏德展开秘密谈判,苏联决定参与入侵行动, 苏联认为得到波兰东部可达成领土向西扩张,又可构建面对德国的缓冲地带。 另一方面,希特勒为了达成闪电战军事效果,避免过早与苏联发生冲突,故也愿意与苏联签订非战条约\n这两位「元首」虽不是君主,但却有与君主一样的道德观,就是「没有道德」。\n即使明知道将来会成为死敌,但为了当下的利益,也可以携手结成同盟。 既然为了利益,死敌能成为同盟;那为了利益,刀剑相加,自然再合理不过。\n3 人性 马基雅维利对人性有清醒的认识:\n人类本性总是忘恩负义, 变化多端, 弄虚作假, 怯懦软弱, 生性贪婪的\n当你对他们有利用价值的时候, 他们可以为你奉献牺牲; 当你失去利用价值时, 他们会毫不犹豫地抛弃你.\n对人们最好是加以安抚, 要不然就必须消灭掉. 这是因为人们如果受到了轻微的侵害, 仍有能力进行报复; 但是对于沉重的伤害, 他们就无能为力了.\n因此, 当我们对一个人进行侵害时, 应该彻底, 不留后患, 不给他任何报复的机会.\n唐高祖武德九年,秦王李世民在玄武门发动兵变,亲手射死他的哥哥、太子李建成,弟弟李元吉也死于这场兵变。 为斩草除根,他把李建成和李元吉的子女全数杀死。\n假如任何人相信一个大人物因为给予新的恩惠就忘却旧日的损害, 他只能是自欺欺人.\n越王卧薪尝胆,以及邓小平三起三落。\n3.1 渭水之盟 唐高祖武德九年(626年),玄武门之变,李世民杀死当时太子李建成和弟弟李元吉,不久李渊禅位,李世民继受皇位。\n八月,东突厥伺机入侵,攻至距首都长安仅40里的泾阳(今陕西咸阳泾阳县)。\n此时唐朝政局不稳,唐太宗李世民被迫设疑兵之计,亲率高士廉、房玄龄等6骑在渭水隔河与颉利可汗对话,又赠予金帛财物,并与之结盟.\n与异族于首都结城下之盟,不可谓不是奇耻大辱,尤其是对李世民这样的英武之主而言,故而又称渭水之辱。\n而后贞观元年,唐太宗励精图治,并且挑拨颉利、突利二可汗和突厥与铁勒诸部的关系,由是内外离怨,诸部多叛。\n贞观四年,唐军灭东突厥,颉利可汗被押往长安。唐太宗在他面前列举其罪,然后免其一死,安置他在唐长安城。\n颉利在长安“郁郁不得志,与其家人或相对悲歌而泣”。贞观八年,颉利亡于长安。\n4 为君之道 以史为鉴, 总结出来的为君之道, 以此巩固君权:\n避免蔑视与憎恨: 尊重臣民的财产及其妇女, 一言九鼎, 努力使自己的行为表现得伟大, 英勇, 严肃, 庄重, 坚韧不拔 支配权力: 君主必须把承担责任的事情让他人办理, 而把施恩的事情交由自己掌管(施恩我做, 背锅你来; 类似韩非子所说的二柄) 巩固基本盘: 平民, 贵族, 军队, 人人有不同的诉求, 如果都能满足自然最好; 否则先优先满足基本盘, 即统治基础. 展现才能: 成就伟大的事业, 作出卓越的范例, 展示自身的能力, 作出非凡之事. 任用贤才: 任用有才之人, 促进国家和城市繁荣, 物尽其用, 人尽其才. 远离谄媚之人: 提拔敢言之人, 尽量避免被谄媚的谎言所蒙蔽, 听别人的建议, 自己做明智的决定, 没有判断力的君主注定走向灭亡. 君主的核心职责是让合适的人,做合适的事,选贤举能,毕竟贤明如诸葛孔明者,也无法事事躬亲。\n如若事事都由君主处理,精力精悍如朱元璋或者雍正,一年无休都无法把国事处理过来。\n何况集权于一身者,必然集怨于一身,毕竟你的权力只能从别人手上抢过来,这个集权过程本身就会树敌无数。 何况集权于一身,事情搞砸了,也没有人可以来背锅,所以不要把那么多「小组长」的职务挂身上。\n为君主分忧可不止为君主做事,还要包括为君主背锅。\n4.1 崇祯与满清议和 崇祯十五年,崇祯帝密召兵部尚书陈新甲主持与满清议和,由马绍愉北上洽谈。\n一日,马绍愉从边关发回议和条件的密函,陈新甲置于案上,其家童误以为是《塘报》,交给各省驻京办事处传抄,事起泄露,群臣哗然。\n明朝士大夫鉴于南宋的教训,皆以为与满人和谈为耻。\n新甲不引罪,反自诩其功,言称此皆陛下旨意。崇祯更加愤怒。(不帮忙背锅,还甩锅给皇帝了,也难怪崇祯会那么生气)\n崇祯十五年七月二十九日将陈新甲下狱,后以失陷城寨为罪名而斩首。(恼羞成怒了)\n陈新甲既死,明朝丧失最后一次议和的机会。\n5 总结 君主论的核心是, 如何建设强大的国家, 并为达目的, 不择手段; 可为崇高理想, 行卑劣之事.\n如果马基雅维利生在中国,估计会与韩非子引为知己,毕竟马基雅维利的主张与商鞅与韩非子的法家思想,有相当多的相似之处。\n虽说如此, 但我个人觉得, 人生而为人, 还是应该有值得坚守之事, 权谋手段纵使能奏一时之效, 恐怕也无法万世通行.\n最后还是应该回到仁爱道德这条老路上, 毕竟仁者无敌嘛.(儒家的想法)\n但多读点书, 看下权谋之术终究无大错. 好比即使我不去忽悠人, 也可以学下忽悠的本领, 防止被人忽悠.\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%90%9B%E4%B8%BB%E8%AE%BA/","summary":"1 前言 1.1 关于君主论 国内的电视剧总会提到所谓的「帝王心术」,「帝王心术」究竟是什么? 马基雅维利就从他的视角,向世人剖析,什么是帝王心术,一个优","title":"《君主论》:所谓「帝王心术」"},{"content":"Iterate through pagination in the Rest API\n1 Preface About 4 months ago, icewind1991 created an exciting PR that adding Stream/Iterator based versions of methods with paginated results, which makes enpoints in Rspotify more much ergonomic to use, and Mario completed this PR.\nIn order to know what this PR brought to us, we have to go back to the orignal story, the paginated results in Spotify\u0026rsquo;s Rest API.\n2 Orignal Story Taking the artist_albums as example, it gets Spotify catalog information about an artist\u0026rsquo;s albums.\nThe HTTP response body for this endpoint contains an array of simplified album object wrapped in a paging object and use limit field to control the number of album objects to return and offset field to set the index of the first album to return.\nSo designed endpoint in Rspotify looks like this:\n/// Paging object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#object-pagingobject) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Page\u0026lt;T\u0026gt; { pub href: String, pub items: Vec\u0026lt;T\u0026gt;, pub limit: u32, pub next: Option\u0026lt;String\u0026gt;, pub offset: u32, pub previous: Option\u0026lt;String\u0026gt;, pub total: u32, } /// Get Spotify catalog information about an artist\u0026#39;s albums. /// /// Parameters: /// - artist_id - the artist ID, URI or URL /// - album_type - \u0026#39;album\u0026#39;, \u0026#39;single\u0026#39;, \u0026#39;appears_on\u0026#39;, \u0026#39;compilation\u0026#39; /// - market - limit the response to one particular country. /// - limit - the number of albums to return /// - offset - the index of the first album to return /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) pub fn artist_albums\u0026lt;\u0026#39;a\u0026gt;( \u0026amp;\u0026#39;a self, artist_id: \u0026amp;\u0026#39;a ArtistId, album_type: Option\u0026lt;\u0026amp;\u0026#39;a AlbumType\u0026gt;, market: Option\u0026lt;\u0026amp;\u0026#39;a Market\u0026gt;, ) -\u0026gt; ClientResult\u0026lt;Page\u0026lt;SimplifiedAlbum\u0026gt;\u0026gt;; Supposing that you fetched the first page of an artist\u0026rsquo;s ablums, then you would to get the data of the next page, you have to parse a URL:\n{ \u0026#34;next\u0026#34;: \u0026#34;https://api.spotify.com/v1/browse/categories?offset=2\u0026amp;limit=20\u0026#34; } You have to parse the URL and extract limit and offset parameters, and recall the artist_albums endpoint with setting limit to 20 and offset to 2.\nWe have to manually fetch the data again and again until all datas have been consumed. It is not elegant, but works.\n3 Iterator Story Since we have the basic knowledge about the background, let\u0026rsquo;s jump to the iterator version of pagination endpoints.\nFirst of all, the iterator pattern allows us to perform some tasks on a sequence of items in turn. An iterator is responsible for the logic of itreating over each item and determining when the sequence has finished.\nIf you want to know about about Iterator, Jon Gjengset has covered a brilliant tutorial to demonstrate Iterators in Rust.\nAll iterators implement a trait named Iterator that is defined in the standard library. The definition of the trait looks like this:\npub trait Iterator { type Item; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt;; // methods with default implementations elided } By implementing the Iterator trait on our own types, we could have iterators that do anything we want. Then working mechanism we want to iterate over paginated result will look like this:\nNow let\u0026rsquo;s dive deep into the code, we need to implement Iterator for our own types, the pseudocode looks like:\nimpl\u0026lt;T\u0026gt; Iterator for PageIterator\u0026lt;Request\u0026gt; { type Item = ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt; { match call endpoints with offset and limit { Ok(page) if page.items.is_empty() =\u0026gt; { we are done here None } Ok(page) =\u0026gt; { offset += page.items.len() as u32; Some(Ok(page)) } Err(e) =\u0026gt; Some(Err(e)), } } } In order to iterate paginated result from different endpoints, we need a generic type to represent different endpoints. The Fn trait comes to our mind, the function pointer that points to code, not data.\nThen the next version of pseudocode looks like:\nimpl\u0026lt;T, Request\u0026gt; Iterator for PageIterator\u0026lt;Request\u0026gt; where Request: Fn(u32, u32) -\u0026gt; ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;, { type Item = ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt; { match (function_pointer)(offset and limit) { Ok(page) if page.items.is_empty() =\u0026gt; { we are done here None } Ok(page) =\u0026gt; { offset += page.items.len() as u32; Some(Ok(page)) } Err(e) =\u0026gt; Some(Err(e)), } } } Now, our iterator story has iterated to the end, the next item is that current full version code is here, check it if you are interested in :)\n4 Stream Story Are we done? Not yet. Let\u0026rsquo;s move our eyes to stream story.\nThe stream story is mostly similar with iterator story, except that iterator is synchronous, stream is asynchronous.\nThe Stream trait can yield multiple values before completing, similiar to the Iterator trait.\ntrait Stream { /// The type of the value yielded by the stream. type Item; /// Attempt to resolve the next item in the stream. /// Returns `Poll::Pending` if not ready, `Poll::Ready(Some(x))` if a value /// is ready, and `Poll::Ready(None)` if the stream has completed. fn poll_next(self: Pin\u0026lt;\u0026amp;mut Self\u0026gt;, cx: \u0026amp;mut Context\u0026lt;\u0026#39;_\u0026gt;) -\u0026gt; Poll\u0026lt;Option\u0026lt;Self::Item\u0026gt;\u0026gt;; } Since we have already known the iterator, let make the stream story short. We leverage the async-stream for using macro as Syntactic sugar to avoid clumsy type declaration and notation.\nWe use stream! macro to generate an anonymous type implementing the Stream trait, and the Item associated type is the type of the values yielded from the stream, which is ClientResult\u0026lt;T\u0026gt; in this case.\nThe stream full version is shorter and clearer:\n/// This is used to handle paginated requests automatically. pub fn paginate\u0026lt;T, Fut, Request\u0026gt;( req: Request, page_size: u32, ) -\u0026gt; impl Stream\u0026lt;Item = ClientResult\u0026lt;T\u0026gt;\u0026gt; where T: Unpin, Fut: Future\u0026lt;Output = ClientResult\u0026lt;Page\u0026lt;T\u0026gt;\u0026gt;\u0026gt;, Request: Fn(u32, u32) -\u0026gt; Fut, { use async_stream::stream; let mut offset = 0; stream! { loop { let page = req(page_size, offset).await?; offset += page.items.len() as u32; for item in page.items { yield Ok(item); } if page.next.is_none() { break; } } } } 5 Appendix Whew! It took more than I expected. Since iterators is the Rust features inspired by functional programming language ideas, which contributes to Rust\u0026rsquo;s capability to clearly express high-level ideas at low-level performance.\nIt\u0026rsquo;s good to leverage iterators wherever possible, now we can be thrilled to say that all endpoints don\u0026rsquo;t need to manuallly loop over anymore, they are all iterable and rusty.\nThanks Mario and icewind1991 again for their works :)\n","permalink":"https://ramsayleung.github.io/zh/post/2021/iterate_through_pagination_api/","summary":"Iterate through pagination in the Rest API\n1 Preface About 4 months ago, icewind1991 created an exciting PR that adding Stream/Iterator based versions of methods with paginated results, which makes enpoints in Rspotify more much ergonomic to use, and Mario completed this PR.\nIn order to know what this PR brought to us, we have to go back to the orignal story, the paginated results in Spotify\u0026rsquo;s Rest API.\n2 Orignal Story Taking the artist_albums as example, it gets Spotify catalog information about an artist\u0026rsquo;s albums.","title":"Let's make everything iterable"},{"content":"1 前言 这是我读的第二本Peter Hessler(何伟)的书, 第一本是在大学期间, 经朋友推荐读起的, 再次拾起他的书, 便是这本.\n一如前作, 他的文笔还是那般地平静隽永, 充满各种不期而至的幽默, 引人入胜, 手不释卷.\n2 长城, 乡村, 工厂 本书的主题是用汽车来感受中国的变迁, 记录下他眼中的中国工业革命之路.\n第一部分, 追寻长城的殘亘断砖, 游历中国大半个北方, 见识在现代化的进程下, 各种历史与村庄的消亡.\n第二部分, 在北京附近的农村定居, 并融入当地村民的生活, 见证农村的日益变化.\n第三部分, 是现代化进程的主力, 工厂的变迁之旅, 着眼于浙江的工厂, 以此来感受中国那如同19世纪工业革命的洪流.\n把目光放在每一个个体上, 记录着这个国家的一个个小人物, 如何成为时代中的历史人物.\n\u0026lt;2022-02-26 Sat\u0026gt;\n看到徐州丰县为了避免记者进入八孩妈所在的村子,把整个村子用铁皮围了起来,由此想起了长城,想起了Beyond《长城》里面的歌词:\n围着老去的国度,围着事实的真相。\n3 熟悉与陌生 在涪陵的生活经历, 让他抹去了中国与外国的差别, 用中国通这样的词语来形容他, 甚至会显得苍白, 他对中国社会的观察和理解, 甚至超越了许多身处其中的中国人.\n读此书的时候, 有种非常奇妙的感觉, 时常在熟悉与陌生之间, 在现实与魔幻之间不停地交替, 他写出了那种说不清, 道不明的微妙之处. 当局者迷, 旁观者清, 在一团迷雾之中理不清的头绪, 原来在旁人看来却异常清晰.\n尤其令我感触良多的是, 写出了中国人心照不宣的潜规则, 写出了对生活的反思, 对世界的思考. 尤其是关于房价与地产的观察和思考, 可以说具有相当的洞察力.\n3.1 房地产 各地都在基建, 大兴土木, 招商引资, 但是资金从何而来呢?\n沿海地区的各大城市的一半的财政收入来源于地产, 每个城市的市长相当于企业的CEO, 想的是在5年任期内多搞钱, 玩法就是从农村买来土地, 然后修建对应的开发区, 拉高地价, 再转手以城市的土地价格卖出, 一进一出, 获利丰厚.\n对于农民来说, 只有土地的使用权, 没有所有权, 村集体也是这般, 所以当政府以城市化进程征收土地时, 农民并没有反对的余地, 而征地补偿款是以农村土地的价格补偿的.\n所以说当卖地是政府的财政收大头, 还指望政府帮忙控制房价, 客观事实上就如同缘木求鱼.\n更何况有金融的系统性风险, 房产一旦降下来, 当房子的价值甚至少于要还的房贷, 只会导致贷款坏账, 进而引发银行业的蝴蝶效应.\n3.2 政治与官僚 作为一个在中国长期居住的外国人, 少不了和各种政府官员打交道, 因此对人民公仆们有相当深的认识:\n税务局尤其重要-如果没把这些干部们弄高兴,他们会让你的企业完全垮掉。 「知道在中国是怎么回事吧:偷税漏税,」高老板说道。他这么说的意思,是指如果工厂要跟着大家做那种低报营业收入的勾当,先得跟那些干部们把关系搞好\n\u0026ldquo;我们还没有做到这一步,但稍后,我们肯定要请税务局的官员们出去吃吃饭,\u0026ldquo;他说。我问他,这些宴席上要不要送礼,他摇了摇头。\u0026ldquo;饭桌上是不送礼的,\u0026ldquo;他跟我讲,\u0026ldquo;那些事情都是单独的。要送礼,得到他们家里去。\u0026rdquo;\n位干部都不太注重着装,不过,他们还是高昂着头,其中一个拿出丽水市国税局的身份卡晃了一下。他姓刘,穿着蓝色牛仔裤和橘黄色T恤衫。 他蓄着平头,这样的发型在中国一般意味着麻烦来了。在中国,那是恃强凌弱者的经典发型,我只要看见这样的平头,心就会不由自主地往下沉\n在工业城镇,人们对于当地政府的态度大多漠不关心。很多人抱怨当地政府官员贪污腐化,可说起这些事情的时候用的尽是些非常抽象的话语,因为他们跟领导干部很少有正面接触\n等到人们真正向政府求助时,那通常是走投无路的标志\n实际上,石帆的所有人都在抱怨搬迁这件事,其中有几个人怒气很大,他们已经准备正式上访。他们的目标,是要找到政府里面某个级别更高的官员。\n跟许许多多中国人一样,他们对当局有一种根深蒂固的信任,觉得贪腐只是地方一级官员的毛病。他们去了省城杭州,在各个专门设置的办公室里排队等候,以期引起某个官员的关注\n中国人即对公共事务, 政治缺少关注, 但日常又始终被政治空气所萦绕。你不来关心政治, 政治也会来关心你.\n中国人应该主动关心政治,政治不只是头上的一片飘渺的云,政治与你的工作,税收,教育,养老,医疗,防疫和各种民生问题息息相关。\n如果每个公民是计算机里面的独立进程,政治就是运行每个进程的底层操作系统,与每个进程「生死悠关」。\n4 写在最后 读何伟的书, 你不会有外国人写中国人的感觉, 不会有那种被故意夸大或者美化的故事, 你读到的, 只是一个温柔的旁观者, 用他的笔触, 记录他所看到的事, 每一个同时代的中国人都可能经历过的事而言.\n\u0026lt;2021-07-03 Sat\u0026gt; 在Twitter上看到了消息,据说因为被学生举报,兼之中美关系恶化,美国作家何伟未获四川大学续聘,学期结束后他不得不与家人离开中国返美,希望今后能再回到中国。\n唉,一声叹息。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%AF%BB%E8%B7%AF%E4%B8%AD%E5%9B%BD/","summary":"1 前言 这是我读的第二本Peter Hessler(何伟)的书, 第一本是在大学期间, 经朋友推荐读起的, 再次拾起他的书, 便是这本. 一如前作, 他的文","title":"《寻路中国》:一个中国通眼中的中国"},{"content":"1 前言 在今天的大众媒体和图书市场上, 到处充斥着关于潜能提升, 心理操控, 占卜星座, 催眠读心等伪心理学的主题.\n而这本书就是想拔除迷雾, 去伪存真, 告诉读者什么才是真正的心理学.\n2 可证伪性 科学家们提到「可解的问题」时, 通常指的是「可校验的理论」.\n「可校验的理论」的定义在科学上是非常明确的: 这个理论是有可能被证伪的, 如果一个理论不可证伪, 并且和自然界的真实事件没有关联, 那么它就是无用的.\n不可证伪的例子:\n18世纪的医生本杰明.拉什(benjamin Rush), 在面对袭击而来的黄热病的时候, 使用的是放血疗法(用手术刀或水蛭吸血的方式离开身体, 顺便插个历史故事, 美国国父华盛顿就是死于这种疗法), 他为许多病人实施了这种疗法, 当他自己被感染时, 也是如法炮制.\n拉什坚信自己的理论是正确的, 拉什认为如果病人好转, 就被作为 放血疗法有效的证据, 如果病人死掉, 就被拉什解释为病人已经病入膏肓, 无药可救. 如此一来, 拉什的放血疗法就是不可证伪的, 立于不败之地. 可证伪性,让我想起了卡尔·波普尔对对马克思理论的批判,认为其总是预设了立场与动机甚至预设结论来判断资本主义必将被共产主义取代,是不可证伪的伪科学的教条(波普尔给出专业的批判)。资本主义必将被共产主义取代,那为什么到现在还没有发生?」,辩护者如是说:「那是资本家采取诸如提高工人福利等手段,延缓了资本主义被推翻的过程」。\n按照这样的说法,马克思主义的预言没有成真,并不是不成真,而是时间还没有到,是因为资本家有这样那样的措施,总之存在各种原因,就存在马克思主义是错的这一种原因。\n马克思主义和占星术也有相同的奥秘嘛。好的理论能够做出具体的预测, 具有高度的可证伪性. 相比于一个不精确的预测, 一个具体的预测如果得到证实, 会为产生这个预测的理论提供更大的支持. 事前预测, 而不是事后諸葛亮解释.\n3 个案与见证 个案研究和见证叙述在心理学(以及其他科学)研究的早期阶段是有用的, 但在研究的后期, 当对理论进行检验的之时, 个案研究就毫无用处. 因为个案是不可重复的, 可能是实验过程导致的偏差. 「反对者很喜欢用个例来反驳,如指责某国的国民普遍被压迫,某国新闻发言人的反驳逻辑竟然是,为什么我没有感受到压迫?」另外安慰剂效应和鲜活性问题也说明个案的不可靠\n安慰剂效应: 无论治疗是否有效, 人们都会报告某种疗法曾经对他们有所帮助. 也就是你什么都没有做, 只要别人对你说, 你接受了治疗, 你都会有一定概率变好. 正如《绿野仙踪》中, 仙女并没有真的给铁皮人一个心脏, 没有给稻草人一个大脑, 也没有给狮子勇气, 但是他们都感觉更好了.\n鲜活性问题: 当面临问题解决或决策情境的时候, 人们会从记忆中提取与当前情境有关的信息. 因此, 人们倾向于利用更容易获得的, 能够用来解决问题或做出决策的信息. 例如, 有10w人在美团上推荐了一个店, 但是你一个朋友说这家真难吃, 你很可能会决定不再去这家店, 即使有10w推荐过.\n4 相关与因果 两个变量之间仅仅存在相关, 并不能保证一个变量变化就会导致另一个的变化, 也就是相关并不意味关系.\n正如文中的例子, 据观察, 家庭中家用电器越多的人, 使用避孕工具就会越多. 那能否得出这样的结论? 烤箱导致人们使用避孕工具.\n根据常识, 答案自然是不. 我们会认识到, 这两个变更可能有相关, 但不是因果关系, 这两个变量可能通过其他变量联系起来.\n我们知道, 教育水平和避孕工具使用和社会经济地位都有关系, 经济水平高的家庭会拥有更多的家用电器.\n两个变量除了不一定会有因果关系外, 还可能存在不同的方向, 即我们假设A的变化引起B的变化, A-\u0026gt;B, 但实际可能是相反的作用方向, B-\u0026gt;A\n5 操纵与控制 科学家最有力的武器就是实验, 而实验的核心就是操纵, 控制, 比较, 即控制变量来比较. 在实验中, 研究者要对被假设为原因的变量进行操纵, 通过实验控制和随机分配来保持对其他所有变量不变, 然后观察这个假设变量是否会产生影响.\n80年前, 有一匹名叫聪明汉斯的马, 似乎知道如何算术, 无论给汉斯出加法, 减法, 乘法, 汉斯都能用它的蹄子敲出答案, 人们都惊呆了, 认为这匹马会思考, 具有数学能力.\n一位名为芬斯特的心理学家解开了谜团, 他发现这匹马对视觉线索极其敏感, 它能察觉人类头部的细微动作, 于是他设计了一个方法来测试马的能力:\n就是让不知道答案的提问者向这匹马提问, 或者让提问者在马的视线范围以外呈现问题, 而在这些情况下, 汉斯就失去了它的\u0026quot;数学能力\u0026quot;\n原来, 汉斯是一个非常细心的人类行为的观察者, 当它正在敲出答案的时候, 它会观察训练员或者出题者的头部.\n当汉斯接近答案的时, 训练员会下意识地稍微歪一下他的头, 然后汉斯就会停下来.\n可见, 从\u0026quot;马能敲出正确答案\u0026quot;就得出\u0026quot;马具有数学能力\u0026quot;的结论是不符合逻辑的.\n对照和实验, 就能识破大部分伪科学的骗局.\n6 聚合性证据 关于科学的典型误解: 公众误认为, 某一科学研究领域中的所有问题都能通过某个关键实验得到解决, 或者某个重要灵感成就了理论的进步, 并彻底颠覆了先前众多研究者累积的全部知识(可能是好莱坞的电影看多了)\n但实际的科学是蹒跚而曲折的, 渐进地进行整合. 正如没有哪个实验可以一捶定音的, 但是每一个实验至少都能帮助我们排除一些可能的解释, 并让我们在接近真理的道路上向前迈进.\n所谓的聚合性证据, 就是来自多个方面, 多个角度的证据, 联系起来, 交叉印证某个理论的正确性, 新的理论不仅能解释新的科学数据, 也必须能解释已有的数据\n延伸:\n频率-效力效应: 一个陌生但看似有理的论断, 不管是真是假, 只要经过不断地重复, 就会增加人们对它的相信程度\n例子:\n有名的\u0026quot;曾参杀人案\u0026quot; 法西斯宣传部部长戈林的名言, 一个谎言, 被重复一万次之后, 也会成为真理. 7 概率推理 大多数学科, 包括心理学所得出的都是概率式的结论, 大多数情况下会发生, 但并非任何情况下都会发生. 例如, 吸烟危害健康, 但不是说吸烟你就一定死, 但是吸烟的人的死亡率是会比不吸烟的人高.\n\u0026ldquo;某某人\u0026quot;统计学: 用特例来否定统计学概率. 你说吸烟死亡率更高, 你看街口的张三, 从12岁吸烟, 每天三包烟, 现在87岁了, 还不是好好的么.\n当我们面对和过去持有的观察相矛盾, 同时又是强有力的证据时, 无所不在的\u0026quot;某某人\u0026quot;总是会立刻跳出来否定这些统计规律\n赌徒谬误: 即倾向于将过去事件和未来事件之间联系起来, 而实际上两者是独立的. 连开了十局\u0026quot;大\u0026quot;之后, 下一局开\u0026quot;大\u0026quot;的概率被认为会高于50%; 连续生了两个女儿之后, 第三个孩子会被认为更有可能是儿子.\n再谈鲜活性问题: 当人们遇到具体的, 具有鲜活性的证据时, 就把概率信息抛到一边了. 他们没有考虑到, 较大的样本能够提供对于总体数值更为精确的估计\n虽然科学的结论并非是100%准确, 但根据心理学研究及理论所做出的预测仍然是有用的.\n8 偶然性 人们很难认识到, 行为事件结果的变化中有一部分是由偶然因素造成的, 也就是说, 行为的变化有一部分是随机因素作的结果.\n科学的预测应该是概率性的, 是对总体趋势的概率性预测. 在解释人类行为的原因方面, 统计预测(基于群体统计趋势的预测)远远优于临床预测(基于个人经验预测)\n当人们相信两类事件在通常情况下应该一起发生时, 就会认为自己频繁地看到了同时发生的现象, 甚至当这两类事件的同时出现是随机的, 并不比任何其他两个事件同时发生的频率更高时也是如此.\n人们总是倾向于看到自己想看到的事情. 而试图去解释偶然事件的倾向可能源于我们深切地渴望相信自己是可以控制这些事件的\n控制错觉: 人们错误地相信他们的参与行为能够决定随机事件. 常见的就是打麻将的时候, 拿到麻将之后, 不会直接看, 而是搓着麻将, 小心翼翼地开牌, 但是这样并不会改变什么.\n9 总结 虽然, 通篇介绍的都是心理学, 译标题也是对伪心理学说不, 但实际的阐述适用于所有科学的方法论. 科学, 是一种思考和观察事物以便深入理解其运行机制的方法.\n科学进步的方式是:\n提出理论解释世界中的特定现象, 根据这些理论做出预测, 实证地检验这些假设, 基于检验的结果对理论进行修正(通常次序为:理论\u0026mdash;预测\u0026mdash;检验\u0026mdash;修正)\n译名为\u0026quot;对伪心理学说不\u0026rdquo;, 实际是\u0026quot;对伪科学说不\u0026quot;\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%AF%B9%E4%BC%AA%E5%BF%83%E7%90%86%E5%AD%A6%E8%AF%B4%E4%B8%8D/","summary":"1 前言 在今天的大众媒体和图书市场上, 到处充斥着关于潜能提升, 心理操控, 占卜星座, 催眠读心等伪心理学的主题. 而这本书就是想拔除迷雾, 去伪存真,","title":"对伪心理学(科学)说不"},{"content":"1 前言 非暴力沟通, 通过体会言语背后的情感, 进而体察他人的内心, 与他人建立情感上的联系. 通俗地说, 就是个心理学家教你, 应该如何说话.\n2 非暴力沟通模型 非暴力沟通模型:\n非暴力沟通指导我们转变谈话和聆听的方式。我们不再条件反射式地反应,而是去明了自己的观察、感受和愿望,有意识地使用语言。\n非暴力沟通的精髓在于对观察、感受、需要、请求 四个要素的觉察,而不在于使用什么字眼进行交流。\n首先,留意发生的事情。我们此刻观察到什么?不管是否喜欢,只是说出人们所做的事情。要点是,清楚地表达观察的结果,而不判断或评估。\n接着,表达感受,例如受伤、害怕、愤怒等。\n然后,说出哪些需要导致那样的感受。\n一旦诚实地表达自己后,提出第四个要素-具体的请求。\n这一要素明确告知他人,我们期待采取何种行动,来满足我们。\n举例:一位母亲对处在青春期的儿子:“小明,看到咖啡桌下的两只袜子(观察),我不太高兴(表达感受),因为我看重整洁(需要),你是否愿意将袜子拿到房间或放进洗衣机?(明确地请求)\n抽象模型:\n留意发生的事情, 客观地表达观察结果 表达我的感受 说明导致感觉的原因 提出请求 3 用心倾听 倾听的第一步, 是留意他人的感受而不是说教. 而体会他人的感觉和需要, 但遭遇他人的痛苦时, 我们常常急于提建议, 安慰或表达我们的态度和感受.\n以下的行为会妨碍我们体会他人的处境:\n建议: \u0026ldquo;我想你应该\u0026hellip;\u0026hellip;\u0026rdquo;\u0026quot; 比较: \u0026ldquo;这算不了什么. 你听听我的经历\u0026hellip;\u0026hellip;\u0026rdquo; 说教: \u0026ldquo;如果你这样做\u0026hellip;\u0026hellip;你将会得到很大的好处.\u0026rdquo; 安慰: \u0026ldquo;这不是你的错;你已经尽最大努力了.\u0026rdquo; 回忆: \u0026ldquo;这让我想起\u0026hellip;\u0026hellip;\u0026rdquo; 否定: \u0026ldquo;高兴一点. 不要这么难过.\u0026rdquo; 同情: \u0026ldquo;哦,你这可怜的人\u0026hellip;\u0026hellip;\u0026rdquo; 询问: \u0026ldquo;这种情况是什么时候开始的?\u0026rdquo; 辩解: \u0026ldquo;我原想早点打电话给你,但昨晚\u0026hellip;\u0026hellip;\u0026rdquo; 纠正: \u0026ldquo;事情的经过不是那样的.\u0026rdquo; 在倾听他人的观察, 感受, 需要和请求之后, 我们可以主动表达我们的理解.\n如果我们已经领会了他们的意思, 我们的反馈将帮助他们意识到这一点. 而非暴力沟通建议我们使用疑问句来给予他人反馈.\n而在给他人反馈时, 我们语气十分重要. 一个人在听别人谈自己的感受和需要时, 将会留意其中是否包含着批评和嘲讽.\n如果我们的语气很肯定, 仿佛在宣布他们的内心世界, 那么, 通常不会有好的反应.\n4 区分观察和评论 不区分观察和评论, 人们将倾向于听到批评.\n社会心理学中有一条阿伦森第一定律:人们在解释令人讨厌的行为时,倾向于给作恶者贴上标签,由此而将这个人从\u0026quot;我们这些好人\u0026quot;中排除。因此,我们需要的是描述观察而不是对他人的行为进行评论,从以下的例子中可以看出观察与评论的差别:\n使用的语言没有体现出评论人对其评论负有责任: 评论:你太大方了。观察:当我看到你把吃饭的钱都给了别人,我认为你太大方了。\n把对他人思想、情感或愿望对推测当作唯一的可能: 评论:她无法完成工作。观察:我不认为她能完成工作。\n把预测当作事实: 评论:如果你饮食不均衡,你的健康就会出问题。观察:如果你饮食不均衡,我就会担心你的健康会出问题。\n缺乏依据: 评论:米奇花钱大手大脚。 观察:米奇上周买书花了一千元。\n评价他人时,把评论当作事实: 评论:欧文是个差劲的前锋观察:在过去五场比赛中,欧文没有进一个球\n使用形容词和副词时,把评论当作事实: 评论:索菲长得很丑观察:索菲对我没有什么吸引力\n不带评论的观察是人类智力的最高形式.\n同理, 需要区分事实与观点.\n5 体会和表达感受 示弱有助于解决冲突.\n区分感受和自我评论.\n听到不中听的话的四种选择:\n责备自己 指责他人 体会自己的感受和需要 体会他人的感受和需要 表达自己的诉求和需要, 如果不表达, 他人就对你的需要一无所知.\n6 请求帮助 提出具体的请求, 你的请求越明确, 就越可能得到快速和正面的回应. 提出的要求越含糊, 就越难实现. 明确谈话的目的. 我们在说话的时, 并不知道自己想要什么. 表面上是在与人谈话, 实际上是自说自话, 进而导致我们的谈话对象不知道如何回应. 请求反馈. 如果无法确定对方是否明白, 我们可能就需要得到反馈, \u0026ldquo;我的意思清楚了吗?\u0026rdquo; 了解他人的反应. 对方此时的感受 对应此时的想法 对方是否接受我们的请求 参加集体讨论时, 说清楚我们希望得到怎样的反馈, 是至关重要的. 区分请求和命令 7 沟通常识与技巧 在一个生气的人面前, 永远不要用「不过」,「可是」,「但是」之类的词语 - 如果我们致力于满足他人及自己健康成长的需要, 那么, 即使艰难的工作也不乏乐趣. 反之, 如果我们的行为是出于义务, 职责, 恐惧, 内疚或羞愧, 那么, 即使有意思的事情也会变得枯燥无味.\n用\u0026quot;我选择做\u0026hellip;是因为我想要\u0026hellip;\u0026ldquo;来代替 \u0026ldquo;不得不\u0026rdquo;\n生气时, 用\u0026quot;我生气是因为我需要..\u0026ldquo;来取代\u0026quot;我生气是因为他们\u0026hellip;\u0026rdquo;\n表达愤怒的四个步骤是:\n停下来,除了呼吸,什么都别做 想一想是什么想法使我们生气了 体会自己的需要 表达感受和尚未满足的需要 表达感激的方式:\n对方做了什么事情使我们的生活得到了改善 我们有哪些需要得到了满足 我们的心情是怎样的. 容易引起纷争的沟通方式\n对他人作出价值观与道德评判, 而他人的评价实际反映了我们的需要和价值观 比较也是评判的一种形式.(你和完美男人的身材比较, 你和莫扎特12岁时的成就比较) 回避责任(有些事不得不做; 你让我伤透了心; 主动承担责任的表述: 我选择xx, 因为我想xx) 强人所难 人们越是习惯于评定是非, 他们也就越倾向于追随权威, 来获得正确和错误的标准. 一旦专注于自身的感受, 我们就不再是好奴隶和好属下.\n其实, 在我们责备和批评的背后, 间接隐含着我们的期望没有得到满足的心情.\n8 总结 我看完如来神掌的秘笈,不代表我就学会了如来神掌。看过的是信息,学到的是知识,用上的才是技能。\n人是动物,不是机器,情绪总是有的,希望我可以籍此在急躁时控制住我的情绪,提高我的表达与共情能力,更好地与人沟通。\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E9%9D%9E%E6%9A%B4%E5%8A%9B%E6%B2%9F%E9%80%9A/","summary":"1 前言 非暴力沟通, 通过体会言语背后的情感, 进而体察他人的内心, 与他人建立情感上的联系. 通俗地说, 就是个心理学家教你, 应该如何说话. 2 非暴力沟","title":"非暴力沟通"},{"content":"1 前言 为什么我们对容易被人上当受骗,为什么有些人总能说服别人?\n如何对「对他人施加影响(忽悠)」, 达到让人顺从的效果?\n这本书高屋建瓴地总结了的种种伎俩,从心理学的角度来分析,总结了6种心理武器, 揭示游说高手们如何使用这些手段,让我们普通人就范。\n如果想学习如何在交际或商业活动中说服别人,这书可谓必读之作。如果不想忽悠人, 也可以学下怎么反忽悠, 不成为《卖拐》的范厨师。\n信息安全也有类似的概念,认为人是系统的脆弱之处。 因此可以针对「人」来实施「入侵」,即所谓的社会工程学(当然,也可以通俗地理解成忽悠)\n2 心理学原理 2.1 对比原理 基于先前所发生事件的性质,相同的东西会显得极为不同, 要是第二样东西跟第一样东西有着相当的不同,那么,我们往往会认为两者的区别比实际上更大。 这样一来,如果我们先搬一种轻的东西,再拿一件重的东西,我们会觉得第二件东西比实际上更沉;而要是我们一开始直接就搬这件重东西,反倒不会觉得有这么沉\n先买了昂贵的东西, 再拿出便宜的东西, 就觉得会更便宜, 更有购买的欲望.\n聚会时,要是我们先跟一个非常帅气的人聊天, 接着插进来一个相貌平平的家伙, 我们会觉得第二个人简直没劲透了-而他其实没有那么索然寡味啦.\n2.2 光环效应 一个人的正面特征就能主导他人看待此人的眼光.(而颜值魅力基本就是这样的特征)\n我们会自动给长得好看的人添加一些正面特点, 比如有才华, 善良, 诚实和聪明等. 而且我们在作出这些判断的时候并没有意识到颜值在其中发挥的作用.\n2.3 自动反应 很多时候,我们在对某人或某事做判断的时候,并没有用上所有可用的相关信息。\n相反,我们只用到了所有信息里最具代表性的一条(颜值, 身材, 衣着等等最外显的特征)\n3 六种武器 3.1 互惠 3.1.1 原理 有债必还:\n要是人家给了我们什么好处,我们应当尽量回报. 正是因为有了互惠体系,人类才成为人类, 由于我们的祖先学会了在「有债必还的信誉网」里分享食物和技巧,我们才变成了人(没有学会分享的人,大概率在进化中被淘汰了)\n强加恩惠\n其他人,不管有多奇怪、讨厌、不受欢迎,只要先给我们点小恩小惠,就能提高我们照着其要求做的概率.\n一个人靠着硬塞给我们一些好处,就能触发我们的亏欠感\n毕竟不能拿了好处不干事。\n不对等交换\n原理要求,某一种行为需要以与其类似的行为加以回报。\n人家施恩于你,你必以恩情报之,不理不睬是不行的,以怨报德更加不可以。\n但这里面也有着很大的灵活性,别人最初给予的小小恩惠,能够让当事人产生亏欠感,最终回报以大得多的恩惠\n为什么最初的小小善意往往刺激人们回报以大得多的恩惠?原因在于\n亏欠感让人觉得很不舒服 违背互惠原理,接受而不试图回报他人善举的人,是不受社会群体欢迎的 互惠式让步\n我们已经看到,这一规则造成的后果之一是,面对接受的善意,我们感到有义务要偿还;而这一规则带来的另一后果则是,倘若有人对我们让了步,我们便觉得有义务也退让一步\n互惠原理通过两条途径来实现相互让步。 头一条很明显:它迫使接受了对方让步的人以同样的方式回应; 第二条尽管不那么明显,但更为关键:由于接受了让步的人有回报的义务,人们就乐意率先让步,从而启动有益的交换过程\n鲁讯先生说 \u0026ldquo;中国人的性情总是喜欢调和、折中的,譬如你说,这屋子太暗,说在这里开一个天窗,大家一定是不允许的。但如果你主张拆掉屋顶,他们就会来调和,愿意开天窗了\u0026rdquo;\n可能不只是因为中国人温和, 而是互惠原理在起作用.\n3.1.2 套路 \u0026ldquo;先予后取\u0026rdquo;: 先强给路人送礼物, 即使他们不喜欢礼物, 然后再募捐。 \u0026ldquo;赠送免费样品\u0026rdquo;: 向潜在客户送上少量的相关产品,看看他们是否喜欢; 实际是作为一份礼物, 暗中却把礼物天然具备的亏欠感给释放了出来。 \u0026ldquo;拒绝-后撤\u0026rdquo;(中国人所说的,以退为进): 先提一个稍过分的要求, 被拒绝后, 再提出原本的要求.(看过《亮剑》电视剧的朋友可能会发现,李云龙对上级提要求时,经常使这招) 3.1.3 如何拒绝 以直报直: 倘若别人的提议我们确实赞同,那就不妨接受它;倘若这一提议别有所图,那我们就置之不理. 真诚的礼物是礼物, 需要回报; 有目的的礼物只是销售手法, 不是礼物, 可以礼貌道谢后, 把对方送出门. 3.2 承诺和一致 3.2.1 原理 人人都有一种言行一致(同时也显得言行一致)的愿望\n一旦我们作出了一个选择,或采取了某种立场,我们立刻就会碰到来自内心和外部的压力,迫使我们按照承诺说的那样去做。 在这样的压力之下,我们会想方设法地以行动证明自己先前的决定是正确的\n言出必行\n依照人们的普遍感觉,言行不一是一种不可取的人格特征。 信仰、言语和行为前后不一的人,会被看成是脑筋混乱、表里不一,甚至精神有毛病的。\n另一方面,言行高度一致大多跟个性坚强、智力出众挂钩,它是逻辑性、稳定性和诚实感的核心\n因此: 一开始就拒绝,比最后反悔要容易\n欺骗自己\n显然, 一旦作出艰难的选择, 人就很乐意相信自己选对.\n事实上,我们所有人都会一次次地欺骗自己,以便在作出选择之后,坚信自己做得没错\n公开承诺\n公开承诺往往具有持久的效力, 每当一个人当众选择了一种立场,他便会产生维持它的动机,因为这样才能显得前后一致\n额外的努力\n为一个承诺付出的努力越多,它对承诺者的影响也就越大. 费尽周折才得到某样东西的人,比轻轻轻松就得到的人,对这件东西往往更为珍视\n他人的感观\n周围的人认为我们什么样,对我们的自我认知起着十分重要的决定作用.\n一旦主动作出了承诺,自我形象就要承受来自内外两方面的一致性压力。 一方面,是人们内心里有压力要把自我形象调整得与行为一致;另一方面,外部还存在一种更为鬼祟的压力,人们会按照他人对自己的感知来调整形象\n内心的抉择\n只有当我们认为外界不存在强大的压力时,我们才会为自己的行为发自内心地负起责任.\n此认识在教育孩子上有重要意义: 对于我们希望孩子真心相信的事情,绝不能靠贿赂或威胁让他们去做,贿赂和威胁的压力只会让孩子暂时顺从我们的愿望\n3.2.2 套路 \u0026ldquo;挖坑\u0026rdquo;: 先叫你作出承诺(也即选择立场,公开表明观点), 然后再提出与你承诺相关的要求 \u0026ldquo;登门槛\u0026rdquo;: 以小请求开始, 积小成大, 最终要人答应更大请求. (国人常见的话术,「来了都来了」,「大过年的」,「他还是个孩子」) \u0026ldquo;抛低球\u0026rdquo;: 先给人一个甜头,诱使人作出有利的购买决定. 而后, 等决定作好了, 交易却还没最终拍板, 卖方巧妙地取消了最初的甜头, 交易敲定之后,买方又不好反悔。 \u0026ldquo;恭維\u0026rdquo;: 先夸别人大方, 乐善好施; 然后过段时间再去募捐. 挖坑示例:\n电话募捐时, 先问问你的近况和身体. 当你客套回复, 还不错的时候, 表明事事顺利,募捐员逼你资助那些过得不咋样的人.\n人要是刚刚才说了自己感觉挺好或者过得不错,哪怕这么说不过是出于社交时的客套,马上就作出一副小气样会显得很尴尬\n3.2.3 如何拒绝 紧随内心: 死脑筋地保持一致愚不可及, 尽管保持一致一般而言是好的,甚至十分关键,我们也必须避免愚蠢的死脑筋. 明牌: 只需要一语道破他们在利用承诺和一致原理, 承认自身有不足, 就可以光明正大地拒绝 3.3 社会认同 3.3.1 原理 在判断何为正确时, 我们会根据别人的意见行事, 而我们对社会认同的反应方式完全是无意识的, 条件反射式. 要想把人说服, 我们提供任何证据的效果都比不上别人的行动.\n多元无知效应\n现场有大量其他旁观者在场时, 旁观者对紧急情况伸出援手的可能性最低. 原因:\n周围有其他可以帮忙的人, 单个人要承担的责任就减少 很多时候, 紧急情况乍看起来并不会显得十分紧急. 每个人都得出判断:既然没人在乎,那就应该没什么问题 多元无知效应似乎在陌生人里显得最为突出:\n因为我们喜欢在公众面前表现得优雅又成熟,又因为我们不熟悉陌生人的反应,所以,置身一群素不相识的人里面,我们有可能无法流露出关切的表情,也无法正确地解读他人关切的表情\n有样学样\n我们会根据他人的行为来判断自己怎么做才合适,尤其是在我们觉得这些人跟自己相似的时候。\n3.3.2 套路 \u0026ldquo;罐头笑声\u0026rdquo;:电视台播放情景喜剧时, 在\u0026quot;观众应该笑\u0026quot;的地方插入笑声录音。 \u0026ldquo;托儿\u0026rdquo;: 在捐款_表演, 安排几个托儿, 到特定时间时, 这些托儿就上台捐款_热烈鼓掌或者热泪盈眶 \u0026ldquo;模仿教育\u0026rdquo;: 想让小朋友学习某个技能/习惯某样焦虑的物品, 给他看同龄人是怎么做的.(内卷教育) 3.3.3 如何拒绝 学习识别: 面对明显是伪造的社认同会证据,我们只要多保持一点警惕感,就能很好地保护自己了 3.4 喜好 3.4.1 原理 我们大多数人总是更容易答应自己认识和喜欢的人所提出的要求.\n3.4.2 喜欢别人的理由 外表魅力: 人人都喜欢看得好看的人, 颜值高有令人低估的巨大优势; 结合光环效应, 颜值高的人犯罪在概率上会比颜值低的人少判几年 相似性: 我们喜欢与自己相似的人, 不管相似之处是在观点, 个性, 背景还是生活方式上. 恭維: 千穿万穿, 马屁不穿 接触与合作: 熟悉会影响人的喜好 条件反射与关联: 糟糕的消息会让报信人也染上不祥, 人总是自然而然地讨厌带来坏消息的人, 哪怕报信人跟坏消息一点关系也没有. 3.4.3 套路 \u0026ldquo;代言\u0026rdquo;: 汽车广告里总站着一堆漂亮的女模特?广告商希望她们把自己积极的特性-漂亮, 性感投射到汽车身上; 运动员代言, 明星代言, 奥运会赞助, 为了让观众把自己的产品跟当前的文化热潮关联起来, 把产品和运动员关联起来.(条件反射与关联) \u0026ldquo;午宴术\u0026rdquo;: 就餐期间接触到的人或事物更为喜爱, 把接触到的事物和美好关联起来.(条件反射与关联) \u0026ldquo;粉丝效应\u0026rdquo;: 将自身与偶像或球队关联起来, 展示积极的联系,隐藏消极的联系,努力让旁观者觉得我们更高大,更值得喜欢.(条件与反射关联) 粉丝效应补充:\n倘若我们觉得自己看起来不怎样,那么我们就很有可能使用这一效应。\n每当我们的公众形象受损,我们就会产生强烈的欲望,宣扬自己跟其他成功者的关系,借此恢复自身形象。 同时,我们还会小心避免暴露自己与失败者之间的关系\n但在我们以个人成就为傲的时候,我们不会沾别人的光。 只有当我们在公在私的威望都很低的时候,我们才会想借助他人成功来恢复自我形象\n对于自我意识太差的人, 他们内心深处的个人价值感过低,没办法靠推动或实现自身成就来追求荣誉,只能靠着吹嘘自己与他人成就的关系来找回尊严,\n他们的成就并不来自本身\n光环效应 + 一致性原理 + 粉丝效应:\n基于光环效应, 偶像的某个[显著的]局部特征(如颜值)的看法被盲目扩大化, 变成对此人整体的看法. 又因为一致性原理的存在(当你正面评价某对象(包括人或事物)时,你在潜意识里会排斥该对象的负面评价;反之亦然;)\n当有人提出偶像的不足时, 即所谓的黑点的时候, 粉丝很自然地愤怒起来, 因为影响了偶像在他们心目中的形象, 自然地与提出问题的人对线起来, 进而涉及到对应的偶像, 无可避免地battle起来;\n最后因为粉丝效应的存在, 很自然把偶像的成功当作自己的成功, 竭力维护偶像, 做出什么事情也都不奇怪了.\n3.4.4 如何拒绝 反思我们是不是觉得自己超乎寻常地迅速、热烈地喜欢上了对方 区分请求人和请求本身 3.5 权威 3.5.1 原理 我们从小被教育服从权威, 而服从权威, 总是能给我们带来一些实际的好处. 部分是因为权威(老师, 家长)更有智慧, 部分是因为权威(老板, 法官)手里攥着对我们的奖惩.\n《圣经旧约》用充满恭敬的行文讲述了上帝权威的故事:只因上帝有了吩咐(哪怕没有半点解释),亚伯拉罕就愿意把利剑插入自己小儿子的心脏。 通过这个故事,我们知道判断一个行为正确与否,跟它有没有意义、有没有危害、公不公正、符不符合通常的道德标准没有关系,只要它来自更高权威的命令,那就是对的。\n3.5.2 象征权威的符号 在没有真正权威的情况下, 象征权威的符号也能十分有效地触发我们的顺从态度. 象征权威的符号:\n头衔: 头衔是最难也最容易得到的权威象征 衣着: 人靠衣装, 佛靠金装 身份标志(珠宝, 豪车): 珠宝, 豪车承载着地位和身份的光环 3.5.3 套路 \u0026ldquo;假扮权威\u0026rdquo;: 广告商利用我们对医生的尊重, 找演员假扮医生, 宣传他们的产品(利用了权威原理带来的影响力,却根本不曾拿出一个真正的权威,光是看起来像权威就足够了) \u0026ldquo;违反自身利益来赢取信任\u0026rdquo;: 销售员为客户争取低价, 与老板吵得不可开交(让客户以为销售员站在己方, 实际争取来的是销售员的心理价位); 还有就是审讯时的红脸和黑脸. 3.5.4 如何拒绝 提高对权威力量的警惕性:\n权威的资格(如, 这个是否是真正的医生) 权威的资格是否跟眼前的主题相关(如, 是真的医生和买这个商品有没有联系) 事出反常则必有妖:\n思考: 陌生人是否真的愿意为了我们, 牺牲个人利益.\n3.6 稀缺 3.6.1 原理: 机会越少见, 价值似乎越高; 对失去某种东西的恐惧,似乎要比对获得同一物品的渴望,更能激发人们的行动力.\n逆反心理:\n机会越来越少的话,我们的自由也会随之丧失。而我们又痛恨失去本来拥有的自由.\n保住既得利益的愿望, 是心理逆反理论的核心.\n每当有东西获取起来比以前难, 我们拥有它的自由受了限制, 我们就越发地想要得到它.\n(罗密欧·蒙特鸠与朱丽叶·凯普莱特是莎士比亚笔下的悲剧人物,两人相爱,两个家族却是世仇。 为了反抗父母拆散他们的企图,他们双双自杀殉情,用这种最极端的悲剧方式来声张自由意志。如果父母不反对, 听凭这对青年男女自由恋爱,他们的浓情蜜意说不定只是初恋时短暂的冲动罢了)\n2022年,哈萨克斯坦一则呼吁年轻人积极参加总统选举投票和其他各类政治议题的广告《他们说:别来投票》,广告内容就是一桌子政客劝年轻人不要来投票,这个国家的未来由他们决定就好。通过逆反心理来劝年轻人来参加投票。\n从充裕到稀缺\n较之一贯短缺, 从充裕变成短缺的物品, 人们的反应更应积极(自由这种东西, 给一点又拿走, 比完全不给更危险)\n而因社会需求而变成稀缺, 会让人变成更加渴望(参与竞争稀缺资源的感觉,有着强大的刺激性; 渴望拥有一件众人争抢的东西,几乎是出于本能的身体反应)\n3.6.2 套路 \u0026ldquo;数量有限\u0026rdquo;:告诉顾客,某种商品供不应求,不见得随时都有 \u0026ldquo;最后期限\u0026rdquo;: 告诉顾客, 这是获得产品的最后机会, 过期不候 \u0026ldquo;通过封杀来传播\u0026rdquo;: 基于逆反心理, 越被限制的信息, 人们越有兴趣知道.(比如,电影海报说,仅限18岁以上观众,电影票会更易用售光) \u0026ldquo;独家信息\u0026rdquo;: 要是我们觉得没法从别处获取某条信息, 我们就会认为它更具说服力 \u0026ldquo;制造竞争\u0026rdquo;: 只有一样商品, 但是卖家找来了多个买家 3.6.3 如何拒绝 一旦在顺从环境下体验到高涨的情绪,我们就可以提醒自己:说不定有人在玩弄稀缺手法,必须谨慎行事。 喜悦并非来自对稀缺商品的体验,而来自对它的占有. 反思, 我们是真的需要某样商品, 还是单纯想要占有它. 4 总结 总结了这么多招式,不知道诸位不看「菜谱」看「兵法」的「范厨师」学会没有,又是否能运用自如呢?\n所谓「忽悠人之心不可有,防忽悠人之心不可无」,多读几本心理学的书,可以让家里少几件贵价且无用的商品,何乐而不为呢。\n觉得意犹未尽的,推荐去看下社会心理学名作《社会性动物》,进阶之作。\n或者可以看下「范厨师」被忽悠的经典小品:《卖拐》\n","permalink":"https://ramsayleung.github.io/zh/post/2021/%E5%BD%B1%E5%93%8D%E5%8A%9B/","summary":"1 前言 为什么我们对容易被人上当受骗,为什么有些人总能说服别人? 如何对「对他人施加影响(忽悠)」, 达到让人顺从的效果? 这本书高屋建瓴地总结了的","title":"影响力"},{"content":"1 前言 如果生活没有泪水, 欢乐还有意义么?\n2 新世界 在这个未来的美丽新世界\n不再有家庭, 不再有父亲 母亲, 这两个词甚至成为极端下流的名词,\n不再有胎生, 不再有家境的差异, 所有人都在瓶子中诞生, 被预设了各种条件, 各种限制;\n不再有异议, 在睡眠中就被灌输了各种关于集体, 社交, 性, 幸福的观念. \u0026ldquo;人人彼此相属\u0026rdquo;, 类似这样的话语, 在你还是孩童时期, 每晚都会在你耳边重复;\n不再会有宗教, 科学, 文学, 音乐, 艺术, 鲜花和大自然, 一切都被限定接触;\n不再会有婚姻, 不再需要对某人忠贞, 连只交一个男朋友都会成为异类, 滥交会成为一种常态;\n不再会有衰老和病痛, 人人都如年青人一般, 只会在某天突然死亡;\n不再有痛苦和烦恼, 连毒品都被全社会推崇, 一片解千愁.\n不再有阶级和人与人的差别, 你的种姓等级, 身高, 样貌, 智力都在培养瓶中被预设好了.\n这样的新世界, 你期待么?\n就如苏格拉底的疑问一样: 快乐的猪和痛苦的人, 你想成为哪个?\n3 反乌托邦 作为反乌托邦的三部曲之一, 《美丽新世界》总是会被拿来和另外一部伟大作品《1984》作比较, 但这是两部完全不同的作品.\n我在看《1984》时候, 感觉是深深的恐惧, 是斯大林式和希特勒式统治方式, 是中国人熟悉的统治方式.\n付诸于谎言和恐惧来支配人民, 但是在恐惧背后, 还是有残存的希望的, 大家还是能意识到老大哥的存在的, 也怀抱着「我们终将在没有黑暗的地方相见」的信念.\n但是看《美丽新世界》, 只有无尽的绝望; 非暴力控制, 基因改造, 无处不在的服从性训练;\n人已非人, 不在有思考的权利和能力, 已经无力对此一切作出反抗.\n4 你所追求之事 美丽新世界不是把人类想要的一切都为人类奉上了么? 只是作为等价交换, 人类献祭了自己作为的灵魂. 如果这样的东西不是你想要的? 你想要的究竟是什么呢?\n我想要回自由, 我想要回泪水, 我想要回痛苦, 我想要回上帝, 即使我不相信上帝, 我想要回诗歌, 我想要回真正的危险, 最重要的是我想要回选择的权利, 我想重新成为人.\n「要是每一次暴风雨之后, 都有这样和煦的阳光, 那么尽管让狂风肆意地吹, 把死亡都吹醒了吧」, 没有暴风雨, 只有阳光的日子, 有何意义呢?\n5 写到最后 「你的1984终将过去,我的美丽新世界总会到来」 \u0026ndash; 赫胥黎\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E7%BE%8E%E4%B8%BD%E6%96%B0%E4%B8%96%E7%95%8C/","summary":"1 前言 如果生活没有泪水, 欢乐还有意义么? 2 新世界 在这个未来的美丽新世界 不再有家庭, 不再有父亲 母亲, 这两个词甚至成为极端下流的名词, 不再有胎生","title":"美丽新世界"},{"content":"1 前言 在肺炎肆虐的2020年, 阅读 这本书想来会别有一番滋味, 因为会有一种身临其境的奇妙之感, 这也是我在2020年结束前想要阅读这本书的初衷.\n\u0026lt;2022-02-26 六\u0026gt;\n没想到,疫情肆虐了两年多,仍未散去如鼠疫那般散去,还越演越烈。\n2 故事的展开 先来介绍下故事中的主要人物:\n里厄大夫: 抗争疫病的主心骨, 为人坚定果敢, 沉稳可靠. 塔鲁: 疫城奥兰城的短暂居客, 体魄健壮, 为人宽厚, 组建志愿者队伍救助患者, 里厄大夫的得力助手 朗贝尔:巴黎一家大报馆的年轻记者, 因出差奥兰而滞留疫城. 疫情开始时把自己视同局外人, 千方百计想要脱身回巴黎与恋人团聚, 后受到里厄大夫和塔鲁等人的精神感召, 加入了志愿队 帕纳卢:神父, 擅长讲道, 声音洪亮, 充满激情; 后加入志愿队 科塔尔:边缘人物,因有案底随时可能被捕而过度紧张, 曾上吊自杀被救; 鼠疫爆发封城后他如鱼得水,因走私而阔绰,还想帮助朗贝尔私自出城 北非小城奥兰, 在毫无症兆的情况, 大批老鼠呕血而亡, 而后鼠疫尾随而来.\n开始, 政府对此是否真正出现鼠疫表示疑虑, 然后在里厄大夫等少数医生的力证下, 政府才真正承认出现了鼠疫, 而后封关断航, 封锁疫城, 为避免鼠疫通过书信传播, 书信也被迫中断, 与世隔绝疫城内外的人们联系的唯一途径, 仅剩余电报的廖廖数语.\n看来,政府都大差不差.\n疫情开始后, 居民以为鼠疫不过个过路的不速之客, 示威一番之后便是离去, 自然是舞照跳, 歌照唱.\n而后鼠疫这个瘟神开始露出獠牙, 肆意开始吞噬着一条条鮮活的生命. 在死神滴血的鐮刀面前, 人们开始惊恐, 在宗教面前, 在各种主面前寻求救赎; 也有自认为局外人的人群试图寻求各种门路逃离此座疫城, 离开本与\u0026quot;他们无关\u0026quot;的瘟神.\n然而死神的意志又岂会因凡人的乞求而转换呢, 患病人数日渐增多, 接纳病人的场所从医院扩展到改建的学校, 里厄大夫及他的同事, 即使竭尽全力, 也没有丝毫减缓死神收割病人的速度, 患病的人还在不断增加; 而面对患病的亲人, 人们从惊恐, 震惊, 不舍到默然接受;\n在前方一片黑暗的情况, 塔鲁组建志愿队, 自愿协助医生救助病人, 坚持做应该做的事情, 承担失去生命的风险, 甚至不知道此举是否会对局势产生一丝改变, 后来朗贝尔和帕纲卢神父也在里厄大夫和塔鲁的感召下, 加入了志愿队救助患者.\n就这样, 从初春到深秋, 鼠疫一直笼罩在奥兰海滨小城上空, 这个鼠疫的瘟神, 他像撒旦那样漂亮, 像疫病本身那样闪光, 就停在人们的屋顶上方,右手执红色猎矛,抬起有他的头那么高,左手指着哪家的房舍.\n其兴也勃焉, 其亡也忽焉. 鼠疫的离去, 就像他的到来一样毫无预兆, 就这样, 在肆虐近一年后, 鼠疫就节节衰退,而后一蹶不振, 在鼠疫就此离去前, 在病魔似乎受严寒、灯火和人群的驱赶, 逃出本城黑暗幽深的洞穴之时, 却向为疫病耗尽力量的塔鲁的身躯发起最后攻击, 并就此夺走了他的生命.\n这是最后一次失败,而这次失败终结了战争,将和平本身变成一种永难治愈的伤痛\n在塔鲁染病离去后, 紧随而来的是里厄医生在外治疗的妻子病逝的消息.\n3 苦难是一面照妖镜 经典的作品, 都是能反馈人性和人生的作品, 毕竟时代在变, 人也在变, 但有些东西不会随时代洪流而有多少变化, 人性如此, 对待人生的态度亦如此.\n关于人性, 虽然书名是, 故事写的也是鼠疫, 但鼠疫不过是一个舞台, 一面镜子, 照出疫病肆虐下的形形色色的人性, 照出那些隐藏在笑谈下的真面目.\n关于人生, 加缪借书中主角的经历, 给出了三种不同的态度:\n第一种, 颓废, 堕落, 无所事事, 像科塔尔那样选择自杀来逃避一切, 最后在疫情结束后陷入疯狂;\n第二种, 笃信宗教, 相信人类是有罪的, 应当受到上帝的惩罚, 却因为目睹法官的幼子临终前痛苦不堪的样子, 而对人类本身有罪, 需要上帝救赎的信条产生怀疑, 而后怀疑信仰, 最后疑似感染鼠疫而亡;\n第三种, 即是与命运奋战到底, 一次次被打倒, 又一次次重新站起来, 直至像塔鲁那样子与鼠疫斗争到最后一口气, 获得寻觅已久的安宁.\n你自己, 在这个世界上没有任何意义, 鼠疫中的所有角色死亡后都对这个世界没有任何影响, 但是你可以选择活得精彩,\n这真是个残酷又真实的哲理, 又像极了人生.\n4 神父的讲道 神父有雄辩之名, 里厄也评价神父: \u0026ldquo;他讲道好, 做得更好\u0026rdquo;, 而神父第一次讲道, 关于人类本身是有罪的讲道, 当时看得我心潮澎湃, 只看文字都觉得自己罪孽深重,\n也难怪在现场看live的听众会在神父面前全部下跪忏悔, 来称赏下加缪的文笔:\n帕纳卢神父中等身材,但是很敦实。他两只大手抓住木栏,俯依在讲坛前沿,只能看到他那厚实的黑色形体,顶着满面红光的脸颊,戴着一副钢丝边眼镜。他的嗓音洪亮,充满激情,能传出去很远,一上来就抛出一句激烈的话,铿锵有力地抨击全体听众:\u0026ldquo;弟兄们,你们在受苦受难。弟兄们,你们这是咎由自取。\u0026ldquo;全场一阵骚动,一直波及广场上的人。\n他接下来说的话,从逻辑上看,似乎同他这句悲愤的开场白并无紧密关系。可是他的演说越往下听,我们的同胞才越明白,神父演说的方法巧妙,仿佛猛然一击,和盘托出他这场讲道的主题。\n果然,帕纳卢抛出了这句话,紧接着就引述《出埃及记》中有关埃及发生鼠疫的段落,并且说道:\u0026ldquo;这种灾难在历史上头一次出现,就是要打击上帝的敌人。\n法老违抗天意,于是鼠疫就迫使他屈膝。有史以来,上帝降以灾难,让那些狂妄者和盲目者都匍匐在他的脚下。\u0026rdquo;\n外面的雨更狂了,在急雨噼啪敲窗的声音而突显的绝对肃静中,神父讲出最后这句话,声音极其响亮,有几名听众略微犹豫一下,便不由自主地滑下座椅,跪到跪凳上。\n其他一些人以为应当效仿,结果陆陆续续,不大工夫全场听众都跪下了,寂静中只听见几张椅子的吱嘎声响。这时,帕纳卢神父又挺起身子,深吸一口气,调门越来越高,继续说道:\u0026ldquo;如果说今天,鼠疫降临到你们头上,就是因为反思的时刻到了。\n义人自不必恐惧,而恶人却理应颤抖。世界好似无比巨大的麦场,灾难如同连枷,无情地击打人类这片麦子,直到麦粒脱离麦秸。麦秸要多于麦粒,被召去的人也要多于上帝的选民,而这场灾难并不是上帝的初衷。\n这个世界同邪恶妥协时间太久了,这个世界依赖上天的宽容时间也太久了。只要痛悔一下,就可以为所欲为。要表示痛悔,人人都觉得游刃有余。时候一到,肯定就会有悔恨的感觉。不过,在那之前,最简便的做法就是放任自流,余下的事就交由仁慈的上帝去处理了。要知道,这种状况不能持续下去了。上帝那张慈悲的面孔,太久太久俯视这座城市的居民,等得厌倦了,他那永恒的希望化为失望,已经移开了目光。\n我们失去了上帝的光明,就这样长期陷入鼠疫的黑暗啦!\u0026rdquo;\n大堂里有人像急躁的马那样,打了一声鼻息。\n神父停顿了一下,放低声调接着说道:\u0026quot;《圣徒传》[插图]上能看到这样一段话:在亨伯特国王[插图]统治伦巴第[插图]的时期,意大利遭受鼠疫的大浩劫,幸免于难者少得可怜,仅仅够埋葬死者了。\n鼠疫肆虐最凶的地方,当属罗马和帕维亚。一个善良的天使显形了,他命令恶神手持狩猎的长矛,去敲击各家各户,每家挨几下敲击,就要抬出多少死人。\u0026rdquo;\n帕纳卢说到此处,伸出两只短粗的手臂,指着教堂前广场的方向,仿佛让人透过摇曳的雨幕看什么东西,他用力朗声说道:\u0026ldquo;弟兄们,如今在我们街道上奔跑的,是同样的死亡的追猎。你们瞧啊,这个鼠疫的瘟神,他像撒旦那样漂亮,像疫病本身那样闪光,就停在你们的屋顶上方,右手执红色猎矛,抬起有他的头那么高,左手指着你们哪家的房舍。\n此时此刻,他的手指也许正指向您家的房门,长矛击打着房门的木板;此时此刻,鼠疫瘟神走进您的家,坐到您的房间里,等待您回去。瘟神守在那里,耐心等待,十分专注,就像人世的秩序那样胸有成竹。\n他那只手要朝你们伸去,世间任何力量,即使人类的科学,你们要记清,即使人类的科学也无济于事,无法使你们免遭打击。你们将在血淋淋的痛苦的打麦场上,被打得血肉横飞,最终连同麦秸一起被抛弃。\u0026rdquo;\n神父讲到此处,越发展现这场灾难的悲惨景象。他又提起那根在城池上空盘旋的长矛,随意打击,落下又起来时血淋淋的,总之将鲜血和痛苦散布开来,\u0026ldquo;以便播种,准备收获真理\u0026rdquo;。\n这一和谐复合长句讲完之后,帕纳卢神父停了一下,他的头发披散在前额上,浑身颤抖,而双手又将这颤动传给讲台。\n接着,他的声音低沉下来,但以责备的口吻说道:\u0026ldquo;是的,反思的时刻到了。你们原以为,只要星期天来拜拜天主就够了,其余的日子就可以任性妄为了。你们还曾想,随便跪拜跪拜,就足以救赎你们罪恶的放肆行为。\n然而,上帝可不是这样不冷不热的。这种若即若离的关系,不足以赢得上帝的无限慈爱。他希望看到你们的时间更长些,这才是他爱你们的方式,老实说,这也是唯一爱的方式。这就是为什么,上帝等你们不来,实在厌倦了,就让灾难来光顾你们,正如有史以来,灾难光顾了所有罪恶深重的城市那样。\n现在你们懂得了什么是罪孽,正如古代该隐[插图]及其儿子们、大洪水之前的人们、所多玛和蛾摩拉[插图]两城的居民、法老和约伯,以及所有受到天谴的人,无不懂得了什么是罪孽。自从封城的那一天起,你们就跟灾难一起被关在城墙之内,你们也就跟所有上述那些人一样,换了一副新眼光看待人和事物了。\n现在,你们终于懂得了,必须归到根本上来。\u0026rdquo;\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E9%BC%A0%E7%96%AB/","summary":"1 前言 在肺炎肆虐的2020年, 阅读 这本书想来会别有一番滋味, 因为会有一种身临其境的奇妙之感, 这也是我在2020年结束前想要阅读这本书的初衷.","title":"鼠疫"},{"content":"The lesson learned from refactoring rspotify\n1 Preface Recently, I and Mario are working on refactoring rspotify, trying to improve performance, documentation, error-handling, data model and reduce compile time, to make it easier to use. (For those who has never heard about rspotify, it is a Spotify HTTP SDK implemented in Rust).\nI am partly focusing on polishing the data model, based on the issue created by Koxiaet.\nSince rspotify is API client for Spotify, it has to handle the request and response from Spotify HTTP API.\nGenerally speaking, the data model is something about how to structure the response data, and used Serde to parse JSON response from HTTP API to Rust struct, and I have learnt a lot Serde tricks from refactoring.\n2 Serde Lesson 2.1 Deserialize JSON map to Vec based on its value. An actions object which contains a disallows object, allows to update the user interface based on which playback actions are available within the current context.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;disallows\u0026#34;: { \u0026#34;resuming\u0026#34;: true } ... } The original model representing actions was:\n#[derive(Clone, Debug, Serialize, PartialEq, Eq)] pub struct Actions { pub disallows: HashMap\u0026lt;DisallowKey, bool\u0026gt; } #[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, Hash, ToString)] #[serde(rename_all = \u0026#34;snake_case\u0026#34;)] #[strum(serialize_all = \u0026#34;snake_case\u0026#34;)] pub enum DisallowKey { InterruptingPlayback, Pausing, Resuming, ... } And Koxiaet gave great advice about how to polish Actions:\nActions::disallows can be replaced with a Vec\u0026lt;DisallowKey\u0026gt; or HashSet\u0026lt;DisallowKey\u0026gt; by removing all entires whose value is false, which will result in a simpler API.\nTo be honest, I was not that familiar with Serde before, after digging in its official documentation for a while, it seems there is now a built-in way to convert JSON map to Vec\u0026lt;T\u0026gt; base on map\u0026rsquo;s value.\nAfter reading the Custom serialization from documentation, there was a simple solution came to my mind, so I wrote my first customized deserialize function.\nI created a dumb Actions struct inside the deserialize function, and converted HashMap to Vec by filtering its value.\n#[derive(Clone, Debug, Serialize, PartialEq, Eq)] pub struct Actions { pub disallows: Vec\u0026lt;DisallowKey\u0026gt;, } impl\u0026lt;\u0026#39;de\u0026gt; Deserialize\u0026lt;\u0026#39;de\u0026gt; for Actions { fn deserialize\u0026lt;D\u0026gt;(deserializer: D) -\u0026gt; Result\u0026lt;Self, D::Error\u0026gt; where D: Deserializer\u0026lt;\u0026#39;de\u0026gt;, { #[derive(Deserialize)] struct OriginalActions { pub disallows: HashMap\u0026lt;DisallowKey, bool\u0026gt;, } let orignal_actions = OriginalActions::deserialize(deserializer)?; Ok(Actions { disallows: orignal_actions .disallows .into_iter() .filter(|(_, value)| *value) .map(|(key, _)| key) .collect(), }) } } The types should be familiar if you\u0026rsquo;ve used Serde before.\nIf you\u0026rsquo;re not used to Rust then the function signature will likely look a little strange. What it\u0026rsquo;s trying to tell is that d will be something that implements Serde\u0026rsquo;s Deserializer trait, and that any references to memory will live for the 'de lifetime.\n2.2 Deserialize Unix milliseconds timestamp to Datetime A currently playing object which contains information about currently playing item, and the timestamp field is an integer, representing the Unix millisecond timestamp when data was fetched.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;timestamp\u0026#34;: 1490252122574, \u0026#34;progress_ms\u0026#34;: 44272, \u0026#34;is_playing\u0026#34;: true, \u0026#34;currently_playing_type\u0026#34;: \u0026#34;track\u0026#34;, \u0026#34;actions\u0026#34;: { \u0026#34;disallows\u0026#34;: { \u0026#34;resuming\u0026#34;: true } } ... } The original model was:\n/// Currently playing object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/player/get-the-users-currently-playing-track/) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CurrentlyPlayingContext { pub timestamp: u64, pub progress_ms: Option\u0026lt;u32\u0026gt;, pub is_playing: bool, pub item: Option\u0026lt;PlayingItem\u0026gt;, pub currently_playing_type: CurrentlyPlayingType, pub actions: Actions, } As before, Koxiaet made a great point about timestamp and =progress_ms=(I will talk about it later):\nCurrentlyPlayingContext::timestamp should be a chrono::DateTime\u0026lt;Utc\u0026gt;, which could be easier to use.\nThe polished struct looks like:\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CurrentlyPlayingContext { pub context: Option\u0026lt;Context\u0026gt;, #[serde( deserialize_with = \u0026#34;from_millisecond_timestamp\u0026#34;, serialize_with = \u0026#34;to_millisecond_timestamp\u0026#34; )] pub timestamp: DateTime\u0026lt;Utc\u0026gt;, pub progress_ms: Option\u0026lt;u32\u0026gt;, pub is_playing: bool, pub item: Option\u0026lt;PlayingItem\u0026gt;, pub currently_playing_type: CurrentlyPlayingType, pub actions: Actions, } Using the deserialize_with attribute tells Serde to use custom deserialization code for the timestamp field. The from_millisecond_timestamp code is:\n/// Deserialize Unix millisecond timestamp to `DateTime\u0026lt;Utc\u0026gt;` pub(in crate) fn from_millisecond_timestamp\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;DateTime\u0026lt;Utc\u0026gt;, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { d.deserialize_u64(DateTimeVisitor) } The code calls d.deserialize_u64 passing in a struct. The passed in struct implements Serde\u0026rsquo;s Visitor, and look like:\n// Vistor to help deserialize unix millisecond timestamp to `chrono::DateTime` struct DateTimeVisitor; impl\u0026lt;\u0026#39;de\u0026gt; de::Visitor\u0026lt;\u0026#39;de\u0026gt; for DateTimeVisitor { type Value = DateTime\u0026lt;Utc\u0026gt;; fn expecting(\u0026amp;self, formatter: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!( formatter, \u0026#34;an unix millisecond timestamp represents DataTime\u0026lt;UTC\u0026gt;\u0026#34; ) } fn visit_u64\u0026lt;E\u0026gt;(self, v: u64) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { ... } } The struct DateTimeVisitor doesn\u0026rsquo;t have any fields, it just a type implemented the custom visitor which delegates to parse the u64.\nSince there is no way to construct DataTime directly from Unix millisecond timestamp, I have to figure out how to handle the construction. And it turns out that there is a way to construct DateTime from seconds and nanoseconds:\nuse chrono::{DateTime, TimeZone, NaiveDateTime, Utc}; let dt = DateTime::\u0026lt;Utc\u0026gt;::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc); Thus, what I need to do is just convert millisecond to second and nanosecond:\nfn visit_u64\u0026lt;E\u0026gt;(self, v: u64) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { let second = (v - v % 1000) / 1000; let nanosecond = ((v % 1000) * 1000000) as u32; // The maximum value of i64 is large enough to hold millisecond, so it would be safe to convert it i64 let dt = DateTime::\u0026lt;Utc\u0026gt;::from_utc( NaiveDateTime::from_timestamp(second as i64, nanosecond), Utc, ); Ok(dt) } The to_millisecond_timestamp function is similar to from_millisecond_timestamp, but it\u0026rsquo;s eaiser to implement, check this PR for more detail.\n2.3 Deserialize milliseconds to Duration The simplified episode object contains the simplified episode information, and the duration_ms field is an integer, which represents the episode length in milliseconds.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;audio_preview_url\u0026#34; : \u0026#34;https://p.scdn.co/mp3-preview/83bc7f2d40e850582a4ca118b33c256358de06ff\u0026#34;, \u0026#34;description\u0026#34; : \u0026#34;Följ med Tobias Svanelid till Sveriges äldsta tegelkyrka\u0026#34; \u0026#34;duration_ms\u0026#34; : 2685023, \u0026#34;explicit\u0026#34; : false, ... } The original model was\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct SimplifiedEpisode { pub audio_preview_url: Option\u0026lt;String\u0026gt;, pub description: String, pub duration_ms: u32, ... } As before without saying, Koxiaet pointed out that\nSimplifiedEpisode::duration_ms should be replaced with a duration of type Duration, since a built-in Duration type works better than primitive type.\nSince I have worked with Serde\u0026rsquo;s custome deserialization, it\u0026rsquo;s not a hard job for me any more. I easily figure out how to deserialize u64 to Duration:\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct SimplifiedEpisode { pub audio_preview_url: Option\u0026lt;String\u0026gt;, pub description: String, #[serde( deserialize_with = \u0026#34;from_duration_ms\u0026#34;, serialize_with = \u0026#34;to_duration_ms\u0026#34;, rename = \u0026#34;duration_ms\u0026#34; )] pub duration: Duration, ... } /// Vistor to help deserialize duration represented as millisecond to `std::time::Duration` struct DurationVisitor; impl\u0026lt;\u0026#39;de\u0026gt; de::Visitor\u0026lt;\u0026#39;de\u0026gt; for DurationVisitor { type Value = Duration; fn expecting(\u0026amp;self, formatter: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!(formatter, \u0026#34;a milliseconds represents std::time::Duration\u0026#34;) } fn visit_u64\u0026lt;E\u0026gt;(self, v: u64) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { Ok(Duration::from_millis(v)) } } /// Deserialize `std::time::Duration` from millisecond(represented as u64) pub(in crate) fn from_duration_ms\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;Duration, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { d.deserialize_u64(DurationVisitor) } Now, the life is easier than before.\n2.4 Deserialize milliseconds to Option Let\u0026rsquo;s go back to CurrentlyPlayingContext model, since we have replaced millisecond (represents as u32) with Duration, it makes sense to replace all millisecond fields to Duration.\nBut hold on, it seems progress_ms field is a bit different.\nThe progress_ms field is either not present or a millisecond, the u32 handles the milliseconds, as its value might not be present in the response, it\u0026rsquo;s an Option\u0026lt;u32\u0026gt;, so it won\u0026rsquo;t work with from_duration_ms.\nThus, it\u0026rsquo;s necessary to figure out how to handle the Option type, and the answer is in the documentation, the deserialize_option function:\nHint that the Deserialize type is expecting an optional value.\nThis allows deserializers that encode an optional value as a nullable value to convert the null value into None and a regular value into Some(value).\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CurrentlyPlayingContext { pub context: Option\u0026lt;Context\u0026gt;, #[serde( deserialize_with = \u0026#34;from_millisecond_timestamp\u0026#34;, serialize_with = \u0026#34;to_millisecond_timestamp\u0026#34; )] pub timestamp: DateTime\u0026lt;Utc\u0026gt;, #[serde(default)] #[serde( deserialize_with = \u0026#34;from_option_duration_ms\u0026#34;, serialize_with = \u0026#34;to_option_duration_ms\u0026#34;, rename = \u0026#34;progress_ms\u0026#34; )] pub progress: Option\u0026lt;Duration\u0026gt;, } /// Deserialize `Option\u0026lt;std::time::Duration\u0026gt;` from millisecond(represented as u64) pub(in crate) fn from_option_duration_ms\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;Option\u0026lt;Duration\u0026gt;, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { d.deserialize_option(OptionDurationVisitor) } As before, the OptionDurationVisitor is an empty struct implemented Visitor trait, but key point is in order to work with deserialize_option, the OptionDurationVisitor has to implement the visit_none and visit_some method:\nimpl\u0026lt;\u0026#39;de\u0026gt; de::Visitor\u0026lt;\u0026#39;de\u0026gt; for OptionDurationVisitor { type Value = Option\u0026lt;Duration\u0026gt;; fn expecting(\u0026amp;self, formatter: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!( formatter, \u0026#34;a optional milliseconds represents std::time::Duration\u0026#34; ) } fn visit_none\u0026lt;E\u0026gt;(self) -\u0026gt; Result\u0026lt;Self::Value, E\u0026gt; where E: de::Error, { Ok(None) } fn visit_some\u0026lt;D\u0026gt;(self, deserializer: D) -\u0026gt; Result\u0026lt;Self::Value, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { Ok(Some(deserializer.deserialize_u64(DurationVisitor)?)) } } The visit_none method return Ok(None) so the progress value in the struct will be None, and the visit_some delegates the parsing logic to DurationVisitor via the deserialize_u64 call, so deserializing Some(u64) works like the u64.\n2.5 Deserialize enum from number An AudioAnalysisSection model contains a mode field, which indicates the modality(major or minor) of a track, the type of scle from which its melodic content is derived. This field will contain a 0 for minor, a 1 for major, or a -1 for no result.\nThe response JSON data from HTTP API:\n{ ... \u0026#34;mode\u0026#34;: 0, \u0026#34;mode_confidence\u0026#34;: 0.414, ... } The original struct representing AudioAnalysisSection was like this, since mode field was stored into a f32=(=f8 was a better choice for this case):\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct AudioAnalysisSection { ... pub mode: f32, pub mode_confidence: f32, ... } Koxiaet made a great point about mode field:\nAudioAnalysisSection::mode and AudioFeatures::mode are f32=s but should be =Option\u0026lt;Mode\u0026gt;=s where =enum Mode { Major, Minor } as it is more useful.\nIn this case, we don\u0026rsquo;t need the Opiton type and in order to deserialize enum from number, we firstly need to define a C-like enum:\npub enum Modality { #[serde(rename = \u0026#34;0\u0026#34;)] Minor = 0, #[serde(rename = \u0026#34;1\u0026#34;)] Major = 1, #[serde(rename = \u0026#34;1\u0026#34;)] NoResult = -1, } pub struct AudioAnalysisSection { ... pub mode: Modality, pub mode_confidence: f32, ... } And then, what\u0026rsquo;s the next step? It seems serde doesn\u0026rsquo;t allow C-like enums to be formatted as integers rather that strings in JSON natively:\nworking version: { ... \u0026#34;mode\u0026#34;: \u0026#34;0\u0026#34;, \u0026#34;mode_confidence\u0026#34;: 0.414, ... } failed version: { ... \u0026#34;mode\u0026#34;: 0, \u0026#34;mode_confidence\u0026#34;: 0.414, ... } Then the failed version is exactly what we want. I know that the serde\u0026rsquo;s official documentation has a solution for this case, the serde_repr crate provides alternative derive macros that derive the same Serialize and Deserialize traits but delegate to the underlying representation of a C-like enum.\nSince we are trying to reduce the compiled time of rspotify, so we are cautious about introducing new dependencies. So a custom-made serialize function would be a better choice, it just needs to match the number, and convert to a related enum value.\n/// Deserialize/Serialize `Modality` to integer(0, 1, -1). pub(in crate) mod modality { use super::enums::Modality; use serde::{de, Deserialize, Serializer}; pub fn deserialize\u0026lt;\u0026#39;de, D\u0026gt;(d: D) -\u0026gt; Result\u0026lt;Modality, D::Error\u0026gt; where D: de::Deserializer\u0026lt;\u0026#39;de\u0026gt;, { let v = i8::deserialize(d)?; match v { 0 =\u0026gt; Ok(Modality::Minor), 1 =\u0026gt; Ok(Modality::Major), -1 =\u0026gt; Ok(Modality::NoResult), _ =\u0026gt; Err(de::Error::invalid_value( de::Unexpected::Signed(v.into()), \u0026amp;\u0026#34;valid value: 0, 1, -1\u0026#34;, )), } } pub fn serialize\u0026lt;S\u0026gt;(x: \u0026amp;Modality, s: S) -\u0026gt; Result\u0026lt;S::Ok, S::Error\u0026gt; where S: Serializer, { match x { Modality::Minor =\u0026gt; s.serialize_i8(0), Modality::Major =\u0026gt; s.serialize_i8(1), Modality::NoResult =\u0026gt; s.serialize_i8(-1), } } } 3 Move into module Update:\n2021-01-15\nfrom(to)_millisecond_timestamp have been moved into its module millisecond_timestamp and rename them to deserialize \u0026amp; serialize from(to)_duration_ms have been moved into its module duration_ms and rename them to deserialize \u0026amp; serialize from(to)_option_duration_ms have been moved into its module option_duration_ms and rename them to deserialize \u0026amp; serialize 4 Summary To be honest, it\u0026rsquo;s the first time I have needed some customized works, which took me some time to understand how does Serde works. Finally, all investments paid off, it works great now.\nSerde is such an awesome deserialize/serialize framework which I have learnt a lot of from and still have a lot of to learn from.\n5 Reference Deserializing optional datetimes with serde PR: Keep polishing the models PR: Refactor model PR: Deserialize enum from number ","permalink":"https://ramsayleung.github.io/zh/post/2020/serde_lesson/","summary":"The lesson learned from refactoring rspotify\n1 Preface Recently, I and Mario are working on refactoring rspotify, trying to improve performance, documentation, error-handling, data model and reduce compile time, to make it easier to use. (For those who has never heard about rspotify, it is a Spotify HTTP SDK implemented in Rust).\nI am partly focusing on polishing the data model, based on the issue created by Koxiaet.\nSince rspotify is API client for Spotify, it has to handle the request and response from Spotify HTTP API.","title":"Serde Tricks"},{"content":"局中人的思考\n1 前言 最近深圳的特殊工时制度搞得甚嚣尘上, 兼之有位与互联网行业完全无交集的朋友咨询我, 为什么你们程序员要996呢?\n有感于此, 写下自己这个局中人的理解和看法. 下面的内容大部分来源于2020年年后, 解答某国外新闻系同学关于国内996工作制度的疑问.\n平时我很少写时政人文类的文章, 一方面是这类话题容易引发口水仗, 另外一方面是因为我的学识不足以阐述清楚问题的根源, 只是本次的话题, 我也算是利益相关者, 所以就发表下个人感想.\n2 (昔日)现状 言归正传, 我本科毕业之后来了蚂蚁金服(aka, 支付宝/Alipay)工作, 至今已经将近两年. 日常的工作时间是 9.am - 10/11/12.pm, 什么时候下班取决于工作量的多少, 以及是否需要发布迭代.\n虽然没有明确要周未需要工作, 但是在周未, Leader 时常也会用钉钉/电话和你沟通, 并没有这是周未, 不应该再工作的觉悟.\n至于 9.am 这个时间点, 是新任CEO强制要求的, 要求 9.am 到公司, 9:15.am 晨会(站会), 美其名为敏捷开发. 在我入职的时候, 弹性上下班的工作制度也是会执行的, 即如果你晚上工作到比较晚(11.pm), 你早上可以晚点来(10.am 后), 现在是你 12.am 之后下班, 明天还要 9.am 到公司晨会.\n想起个比较心酸的有趣事:\n曾经我们组员在会议室闭关(也就一群人被关在会议室里面封闭开发), 公司邮件说事业群下个月要开年会, 因为我们的年会不是年年开, 时有时无, 并不知道为什么, 然后有个同事说了下, 不是要在年会宣布996吧?\n(杭州一家叫有赞的企业刚刚在年会宣布996) 众人大笑; 然后另外一位同事略显无奈地接了句, \u0026ldquo;如果能9点下班, 那就好咯\u0026rdquo;, 众人笑声更甚.\n3 见解 为什么我们工作要这么累, 连9点下班也是一种奢望; 为什么我们要996, 不能 work life balance呢? 关于这样的问题, 我也曾思考过, 下面是我不成熟的见解:\n3.1 革新与底层技术 从革新与底层技术方面来说, 我们没有经历过工业革命, 没有以技术去推动社会生产力进步的传统, 这三十年的发展很大一部分是全球化与人口红利的结果, 即秦晖教授所言的低人权优势. 同理, 中国互联网只有业务模式的创新, 并没有基础技术的革新与壁垒, 所谓的新四大发明便是如此;\n因为没有技术壁垒, 你做的东西, 别人也容易仿制, 所以只能和别人比速度, 通过快速迭代来抢占市场, 难免就出现拼命加班的情况.\n3.2 企业 而从企业的角度来说, 以这个号称996始祖之一的公司举例, 他们的目的就是要不择手段地实现利益最大化, 员工利益的保障只能靠资本家良心发现了, 而资本家只是资本的人格化, 资本是不论对错, 只谈利弊的.\n3.3 公司文化 此外, 某司从公司创始人到公司文化, 都喜欢洗脑, 洗脑纲领即所谓的价值观, 最近还出了新的价值观, 号为新六脉神剑.\n公司还抽了两天下午, 全员脱产学习, 场景让我梦回大学思修毛概课堂. 其中有一条为, 客户第一, 员工第二, 股东第三. 有同事质疑, 既然公司把员工第二写到价值观, 为何员工还要996, 我们做的是体力劳动还是脑力劳动. 公司HR答复: 这是你们自己的选择? 同事追问, 为何对公司有益的事, 公司要强力推行, 为何要公司让步, 造益员工的时候, 又要让员工自行选择, 是否双标呢? 公司HR: 此事容后再议.\nHR 和老板们在公司内部群解答新六脉神剑, 总能在让员工纷纷点赞, 双击666, 不知道是反串黑还是幸存者偏见了.\n3.4 政府及立法角度 从政府及立法角度来说, 到了这一步, 员工的权利只能由政府来保障, 需要对企业作限制, 然而我们的政府对这种创造大量GDP的企业, 只会当作爸爸, 又怎会处罚呢? 你见过在南山区法院打赢腾讯的么? 在西湖区打赢支付宝的么?\n而我们又没有投票权, 政府要加税就加税, 要监控你就监控你, 要发红头文件就发, 要保大企业就保大企业, 我们又能怎样? 政府不鼓励大企业实行996就不错了, 还处罚他们?(好吧, 一语成谶)\n目前的执政党是靠工人运行起家的, 靠组建工会的方式来对抗无底线的加班也不实际, 能否成立工会都是个问题.\n3.5 员工自身 还有员工自身的原因, 或出于对物质的更高追求, 或出于生存的压力, 身边自然不会少自愿加班的人, 他们大多有家有口, 步入中年, 而他们作为三/四口之家的唯一劳动力, 想要在杭州安家, 想有自己的房子, 需要负出自己的时间, 精力与健康.\n也因为这样的人, 使996得而蔚然成风, 但为何买一套自己的房子需要付出如此大的代价, 引申出来又是一个复杂的问题. 关于房地产的分析, 可以参考下这两篇文章:\n帮你分析中国的房地产市场 2010年的房地产调控,我们收获了什么?写在房价暴涨前 4 更新 2020-10-14:\n大半年的时间过去了, 针对这个问题, 我个人有了些新的感想. 除去上述因素, 我们这些从业者还很容易陷入到一个剧场效应的怪圈中:\n4.1 剧场效应 想象此刻你正在影院看电影, 如果所有人都坐着看其实很轻松而且看的很清楚, 但如果有人选择站起来, 那他会获得更好的视野, 代价就是劳累.\n不过接踵而至的问题是其他人的体验就会变差, 为了应对这个局面那你也「不得不」站起来.\n最后的结果就是除了第一排以外的所有人都站着看完了电影, 十分劳累不说视野未必比都坐着好.\n第一排的就是坐在食物链顶部的资本家, 开始的时候, 给高于其他人报酬, 以利诱之, 让你加入到996的行列, 当你身在其中的时候, 以你为榜样裹挟其他人入场.\n当所有人都变成996的时候, 自然不可能再给你高于其他人报酬。 最后的结果就是所有人都在原来的薪酬水准上, 付出更多的时间和精力, 甚至得到更低的时薪.\n这就涉及到另外一个问题, 是否给钱就可以996加班, 只要钱给够就可以为所欲为? 有感于某电商企业月工双休的传闻.\n其实不是的, 持「有钱就可以996加班」这样的观点的人, 很容易变成影院第一个站起来的人, 成为破窗效应里面第一块被打破的窗户, 某种程度上说, 你正在卖力地帮着你的顾主压榨未来的自己:\n她那时还太年轻,不知道命运所赠送的礼物,早已在暗中标好了价格 \u0026ndash; 《断头王后》\n996加班是违法的, 即使修改劳动法也改变不了这个事实, 钱给够就可以996加班, 后面会演变成钱给够就可以007加班, 最后会变成摩登时代里面的工人, 我们都变成人肉干电池.\n这也是法律禁止器官买卖的原因, 即使双方同意, 不然很容易演变成强者对弱者的剥削.\n5 解法 那996加班就没有解法么? 我们就避免不了人肉干电池的命运么?\n其实解法还是有的, 在有人站起来的时候, 如果有工作人员把他呵斥, 让他坐下去, 否则就赶他出去, 相信他会老实下来, 但是工作人员又不看电影, 为何要为你们操心, 反正你们已经买票了;\n不过, 如果有多个电影院可以选, 情况就会有所不同了.\n另外一个解法就是, 在有人站起来的时候, 前面和后面的观众都集体把他呵斥下去, 不然饮料, 食物都往他头上招呼.\n只是人要主动站出来, 为自己争取权益, 并不容易.\n但须知, 世界上没有从天而降的英雄, 只有挺身而出的凡人.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/996%E6%88%90%E5%9B%A0/","summary":"局中人的思考 1 前言 最近深圳的特殊工时制度搞得甚嚣尘上, 兼之有位与互联网行业完全无交集的朋友咨询我, 为什么你们程序员要996呢? 有感于此, 写下","title":"为什么我们要996"},{"content":"1 前言 尾调用消除(tail call elimination, TCE)是函数式编程的重要概念, 有时也被称为尾调用优化(tail call optimization, TCO), 作用是将尾递归函数转化成循环, 避免创建许多栈帧, 减少开销.\n遗憾的是, Java不支持TCE, 所以本文主要是介绍, 如何使用java8特性, 基于堆来实现尾递归优化.\n一个有趣的事,这篇文章是我在阿里ATA上发的最后一篇文章。发在内网的第二天,也就是我的last day,有位P8的同事在钉钉上夸我文章写得好,只回复了一句,还未来得及多交流几句,我的离职流程就走完,钉钉被强制下线了,甚至没看到这位同事的回复。\n2 尾调用与尾递归 想要了解尾递归优化, 首先要了解下什么是尾调用.\n尾调用的概念非常简单, 一言以蔽之, 指函数的最后一步是调用另一个函数. 以斐波那契数列为例:\npublic int fac(int n) { if (n \u0026lt; 2) { return 1; } return n * fac(n - 1); } 虽说上面的函数看起来像是尾调用函数, 但实际上它只是普通的递归函数, 因为它最后一步不是调用函数, 它只是作了加法计算, 上面的逻辑等同于:\npublic int fac(int n){ if(n \u0026lt; 2){ return 1; } int accumulator = fac(n - 1); return n * accumulator; } 既然调用 fac(n-1)函数的目的是为了获取累加值, 那么我们自然将累加值抽出来, 然后把上面的斐波那契数列函数改成尾调用函数呢:\npublic int fac(int n) { return facTailCall(1, n); } public int facTailCall(int accumulator, int n) { if (n \u0026lt; 2) { return accumulator; } return facTailCall(n * accumulator, n - 1); } 函数调用自身, 称为递归函数. 如果尾调用函数自身, 就称为尾递归函数. 那尾递归函数有什么用呢? 仅仅是将斐波那契数列的累加值抽了出来么?\n要回答这个问题, 让我们先把目光投回到递归版本的斐波那契数列, 当调用 fac(6)时发生了什么事情:\n6 * fac(5) 6 * (5 * fac(4)) 6 * (5 * (4 * fac(3))) // N次展开之后 6 * (5 * (4 * (3 * (2 * (1 * 1))))) // \u0026lt;= 最终的展开 // 到这里为止, 程序做的仅仅还只是展开而已, 并没有运算真正运运算, 接下来才是运算 6 * (5 * (4 * (3 * (2 * 1)))) 6 * (5 * (4 * (3 * 2))) 6 * (5 * (4 * 6)) // N次调用之后 720 // \u0026lt;= 最终的结果 fac(10000) // =\u0026gt; java.lang.StackOverflowError 从上面的例子可以看出, 普通递归的问题在于展开的时候会需要非常大的空间, 这些空间指的就是函数调用的栈帧, 每一次递归的调用都需要创建新的栈帧, 递归调用有对应的深度限制, 这个限制就是栈的大小.\n默认栈空间从32kb到1024kb不等, 具体取决于Java版本和所用的系统, 对于64位的java8程序而言, 递归的最大次数约为8000.\n我们也没法通过增加栈的大小来增加递归的次数, 栈的大小相当于是一个全局配置, 所有的线程都会使用相同的栈, 增加栈的大小只是浪费资源而言.\n那有没有方法可以避免上述的 StackOverflowError 呢? 那当然是有的, 答案就是上文提到的尾递归.\n让我们来观察下尾递归版本的斐波那契数列, 看看调用 facTailCall(1, 6) 会发生什么事情?\nfacTailCall(1, 6) // 1 是 fac(0) 的值 facTailCall(6, 5) facTailCall(30, 4) facTailCall(120, 3) facTailCall(360, 2) facTailCall(720, 1) 720 // \u0026lt;= 最终的结果 facTailCall(1, 15000) // java.lang.StackOverflowError 与上方的普通递归函数相比, 尾递归函数在展开的过程中计算并且缓存了结果, 使得并不会像普通递归函数那样展开出非常庞大的中间结果, 但是尾递归函数还是递归函数, 如果不作尾递归优化(TCO), 依然会出现 StackOverflowError.\n所谓的尾递归优化, 可以简单理解成将尾递归函数优化成循环; 在函数式编程中, 是鼓励大家使用递归, 而不是循环来解决问题.\n这是因为循环会引入变量, 而变量是函数式编程中被视为洪水猛兽一样的存在.\n但如果递归调用的深度比较大, 栈帧会开辟很多, 一来是浪费空间, 二来性能也必然会下降(有很多读写内存操作);\n相反, 如果使用循环, 则只在一个函数栈空间里, 不会开辟更多的空间, 所以使用循环, 性能要好于递归.\n所以在函数式编程语言中, 如Scheme, Haskell, Scala, 尾递归优化是标配, 所以不会出现 StackOverflowError\n(define (fact x) (define (fact-tail x accum) (if (= x 0) accum (fact-tail (- x 1) (* x accum)))) (fact-tail x 1)) (fact 1000000), ;;; 返回一个很大很大的数, 使用的空间与(fact 3)相当 遗憾的是, Java并不支持尾递归优化.\n3 基于堆的尾递归 尾递归优化的一大用处是维持常数级空间, 保证不会爆栈.\n既然爆栈的原因是栈空间不足, 又无法扩大栈的空间, 那么只能把函数存在其他地方, 比如堆(heap). 使用堆来抽象递归, 那么需要做的事情如下:\n表示一个函数的调用 把函数调用存储在栈式结构中, 直到条件终止 以后进先出(LIFO)的顺序调用函数 为此我们可以定义一个名为TailCall的抽象类, 它有两个子类: 其一表示挂起一个函数以再次调用该函数对下一步求值, 如下, 先暂停f()的调用, 先调用出g()的结果, 再对f()进行求值, 此子类名为Suspend:\ndef f(): return g() + 1 而一个函数的调用可以通过java8引入的Supplier\u0026lt;T\u0026gt;类来表示, 以此来存储函数, T为TailCall, 表示下一个递归调用.\n这样一来, 就可以通过每个尾调用引用下一个调用的方式来构造一个隐式链表, 完成栈式数据结构存储的要求.\n另一个子类表示返回一个调用, 它应该返回结果, 不会持有到一个TailCall的引用, 因为已经没有下一个TailCall了, 所以其名为Return.\n其外, 还需要几个额外的抽象方法: 返回一个调用, 返回结果, 以及判断是否判断TailCall是Suspend还是Result, 接口及子类实现如下:\n/** * @author Ramsay/Ramsayleung@gmail.com * Create on 7/5/20 */ public abstract class TailCall\u0026lt;T\u0026gt; { public abstract TailCall\u0026lt;T\u0026gt; resume(); public abstract T eval(); public abstract boolean isSuspend(); public static class Return\u0026lt;T\u0026gt; extends TailCall\u0026lt;T\u0026gt; { private final T t; private Return(T t) { this.t = t; } @Override public TailCall\u0026lt;T\u0026gt; resume() { throw new IllegalStateException(\u0026#34;Return has no more TailCall\u0026#34;); } @Override public T eval() { return t; } @Override public boolean isSuspend() { return false; } } public static class Suppend\u0026lt;T\u0026gt; extends TailCall\u0026lt;T\u0026gt; { private final Supplier\u0026lt;TailCall\u0026lt;T\u0026gt;\u0026gt; resume; private Suppend(Supplier\u0026lt;TailCall\u0026lt;T\u0026gt;\u0026gt; resume) { this.resume = resume; } @Override public TailCall\u0026lt;T\u0026gt; resume() { return resume.get(); } @Override public T eval() { TailCall\u0026lt;T\u0026gt; tailCall = this; while (tailCall.isSuspend()) { tailCall = tailCall.resume(); } return tailCall.eval(); } @Override public boolean isSuspend() { return true; } } public static \u0026lt;T\u0026gt; Return\u0026lt;T\u0026gt; tReturn(T t){ return new Return\u0026lt;\u0026gt;(t); } public static \u0026lt;T\u0026gt; Suppend\u0026lt;T\u0026gt; suppend(Supplier\u0026lt;TailCall\u0026lt;T\u0026gt;\u0026gt; supplier){ return new Suppend\u0026lt;\u0026gt;(supplier); } } Return并没有实现resume方法, 只是简单地抛出了异常, 因为前文提到过, Return表示最后一个调用, 没有下一个调用了, 自然无法实现resume方法;\n同理, 只要不是最后一个调用, 就没法实现eval()方法, 因为最后的一个调用才能返回结果.\n那为啥Suspend还实现了eval方法呢? 主要是不让用户感知函数调用并返回结果的逻辑, 将其内敛到Suspend内. 现在让我们来看看效果:\n/** * @author Ramsay/Ramsayleung@gmail.com * Create on 7/5/20 */ public class TailCallTest { /** * 尾递归版本斐波那契数列 */ public int fac(int accumulator, int n) { return facTailCall(accumulator, n).eval(); } public TailCall\u0026lt;Integer\u0026gt; facTailCall(int accumulator, int n) { if (n \u0026lt; 2) { return TailCall.tReturn(accumulator); } return TailCall.suppend(() -\u0026gt; facTailCall(accumulator * n, n - 1)); } /** * 递归版本的两数相加 */ public int addRecur(int x, int y) { return y == 0 ? x : addRecur(++x, --y); } /** * 尾递归优化版本的两数相加 */ public int addTCO(int x, int y) { return addTailCall(x, y).eval(); } public TailCall\u0026lt;Integer\u0026gt; addTailCall(int x, int y) { int _x_plus_one = x + 1; int _y_minus_one = y - 1; return y == 0 ? TailCall.tReturn(x) : TailCall.suppend(() -\u0026gt; addTailCall(_x_plus_one, _y_minus_one)); } @Test public void addTest() { addRecur(10, 10); // =\u0026gt; 20 addRecur(10, 10000); // StackoverFlowError addTCO(3, 100000); // =\u0026gt; 100003 } @Test public void test() { fac(1, 6); // =\u0026gt; 720 fac(1, 600000); // 数字过大溢出, 返回0, 且没有出现 StackOverflowError } } 4 总结 至此, 我们通过java8的lambda, Supplier接口实现了基于堆的尾递归优化, 虽说没有优化成常数空间, 但终归解决了递归过深时, 栈空间不足导致 StackOverflowError的问题.\n而按照Stackoverflow问题的说法, java不支持尾调用的原因如下:\nIn jdk classes there are a number of security sensitive methods that rely on counting stack frames between jdk library code and calling code to figure out who\u0026rsquo;s calling them.\n后续java版本也暂无支持尾递归优化的计划, 无奈摊手.jpg\n5 参考 https://en.wikipedia.org/wiki/Tail_call Functional Programming in Java NightHacking with Venkat Subramaniam Designing tail recursion using java 8 ","permalink":"https://ramsayleung.github.io/zh/post/2020/java%E5%AE%9E%E7%8E%B0%E5%B0%BE%E9%80%92%E5%BD%92%E4%BC%98%E5%8C%96/","summary":"1 前言 尾调用消除(tail call elimination, TCE)是函数式编程的重要概念, 有时也被称为尾调用优化(tail call optimization, TCO), 作用是将尾递归函数转化成循环, 避免创建许","title":"java8基于堆实现尾递归优化"},{"content":"1 导语 呆在那里, 还是走开, 结果一样. \u0026ndash; 加缪《局外人》(又译作, 异乡人)\n2 妈妈走了 \u0026ldquo;今天, 妈妈死了, 也许是昨天\u0026rdquo;. 某天, 养老院来电通知男主默尔索妈妈去世了, 他前去奔丧, 却在守灵时抽烟, 喝咖啡, 跟人闲聊, 昏昏欲睡, 记不起母亲的岁数, 拒绝看母亲入入殓前的最后一面, 甚至未曾因母亲的离开而有一丝悲伤, 为能睡足12小时而高兴, 母亲入土第二天, 与女友游泳, 看喜剧, 发生关系.\n开头便用妈妈去世这件事刻画出默尔索的性格, 对周围一切事物的疏离与冷漠, 对世俗规则与戒条的忽视.\n3 身处局外, 看破世俗 3.1 关于爱 默尔索的女友玛莉想知道他是否爱自己, 他说\u0026quot;如果一定要说的话, 我大概是不爱的\u0026quot;;\n关于是否想和自己结婚, 他说\u0026quot;怎么都行\u0026quot;, \u0026ldquo;如果她想, 我们可以结婚\u0026rdquo;, 在他看来, 人们常常挂在嘴边的爱并不能说明什么, 这种关于爱的问答就好像一个语言游戏, 其实没有什么意义, 也不能证明什么, 只不过问答双方依旧乐此不疲, 但默尔索已经看透这些事情, 只是他拒绝参加这个语言游戏.\n3.2 关于异乡人 局外人的另一译名为《异乡人》, 后面了解到, 对比巴黎, 默尔索所处的殖民地为异乡, 人们以巴黎为荣, 而巴黎又代表了世俗的看法, 养老院的门房想让默尔索知道, 他是个巴黎人, 很怀念巴黎的生活;\n默尔索的女友玛莉很乐意去巴黎; 马颂的太太有巴黎口音; 在默尔索杀人案(和一起弑父案)开庭期间, 有巴黎派来的记者; 老板想在巴黎开个新的办事处; 当玛莉问他对巴黎的看法时, 他说: \u0026ldquo;那里满脏的, 到处都是鸽子和阴暗的庭院, 而且人的肤色很苍白\u0026rdquo;.\n阿尔及利亚之于巴黎是为异乡, 默尔索之于世俗规则是为异乡人, 或者, 这便是异乡人这个译名的来由.\n说到底, 默尔索不想因为这些世俗观点而改变自己的生活方式, 不想做出一丝改变, 他以一种迟钝的态度应对着这个世界.\n4 罪行与罪人 预审法官就对如何审理默尔索杀人一案指出了方向: 本案关注的还是罪行, 而是犯罪的人.\n他对默尔索说, \u0026ldquo;我真正感兴趣的, 是您本人\u0026rdquo;. 默尔索杀了人, 然而法庭却一直在讨论\u0026quot;他在母亲的葬礼上有没有哭\u0026quot;, 因为他在母亲的丧礼不哭, 所以他有可能成为下一起案子的杀人犯, 由此推断出, 他的坏是他的本质, 甚至论证成了确凿的杀人动机.\n在我们的社会里, 一个人在母亲的葬礼上没有哭, 他就会有被判死刑的危险.\n5 丧与醒悟 默尔索作为局外人的领悟, 让其萦绕着一种无所谓的丧感.\n\u0026ldquo;对于我真正感兴趣的事我也许没有绝对的把握, 他对于我感兴趣的事情我是有绝对的把握的\u0026rdquo;, 神父找他聊的事正好是他不感兴趣的事, 所以他坦然走向断头台. \u0026ldquo;什么样的生活都差不多, 人们永远无法改变生活\u0026rdquo;, 他觉得这一切都不重要, 所以他拒绝了老板让他去巴黎工作的建议. \u0026ldquo;人生在世, 永远也不该演戏作假\u0026rdquo;, 正是这样的人生准则最后导致了他庭审被判斩首. 在生命的最后时刻, 我也不知道默尔索是觉悟到失去后才懂得珍惜的道理, 被关进监狱才想起自由的宝贵, 要被斩首才领会到生命的价值;\n还是说在生命将要走到尽头的时候, 才理解了母亲在养老院找了个新\u0026quot;男友\u0026quot;的原因.\n\u0026ldquo;从我遥远的未来, 一股暗潮穿越尚未到来的光阴冲击着我, 流过至今我所度过的荒谬人生, 洗清了过去那些不真实的岁月里人们为我呈现的假象\u0026rdquo;\n6 写在最后 人生在世, 终究是没法活成默尔索的样子, 或者说活成默尔索的样子, 对于身边的人来说, 是一种折磨.\n人是一种群居动物, 过分的冷漠与疏离, 只会让群体离你越来越远, 自以为是的冷漠, 终究还是会走向悲剧.\n有感于最近发生的诸事, 有感于我不正确的为人处事的方式.\n最好的方式是, 看清游戏的本质, 并且以此赢得游戏, 说出那些无所谓的话, 于人于已无半点用处.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E5%B1%80%E5%A4%96%E4%BA%BA/","summary":"1 导语 呆在那里, 还是走开, 结果一样. \u0026ndash; 加缪《局外人》(又译作, 异乡人) 2 妈妈走了 \u0026ldquo;今天, 妈妈死了, 也许是昨天\u0026rdquo;. 某天","title":"局外人"},{"content":"1 背景介绍 笔者目前在蚂蚁金服-网商银行做后端开发, 因为在组内毕业时间最短(2年), 所以经常会被Leader当成免费的HR去找校招简历, 所以见过不少的简历(\u0026gt;100份), 把收到简历之后, 有时会给简历打分, 然后再给到老板.\n因为见过不少的简历, 发现有些学历,经历优秀的同学, 因为没有好好写简历而被埋没, 也见过通过简历, 放大自身优点的同学. 所以在这里, 以前人的姿态, 斗胆谈一个应届生如何写好一份简历的技巧, 也希望给各位同学带来一点帮忙.\n2 自我介绍 自我介绍, 无需赘言, 就是把你的个人信息简明介绍完, 包括教育经历, 专业, 邮箱, 电话, Github地址(如果有优秀项目的话, 如果只是注册了个账号, 还是不要放上去).\n需要注意的是, 关于是否放照片这一点, 个人倾向于不放, 作为技术开发, 放不放都无所谓, 放了容易分散注意力. 如果要放照片, 照片请做到简洁, 得体.\n3 实习经历/项目经历 对于开发岗, 实习经历和项目经历是重要的栏目, 也是面试官期待看到的栏目, 因为应届生没有工作经历, 所以就只好写实习经历和项目经历, 对于实习经历/项目经历, 按时间升序或者降序排列, 不要太乱, 个人推荐的格式:\nxx 公司/xx 项目, 时间: 2020.03-2020.xx 1. 项目背景一句话、 2. 自己在项目里负责的工作 3. 用到的技能/思考的过程或者难点攻克的过程 4. 项目的结果或者我的成绩 总而言之, 参考STAR法则. 需要避免的一些问题:\n技术无关的事情少写, 更不要写一些大家都知道的事情. 在项目中负责”代码的编写, 用例的测试, 以及相关文档的校对/编辑”, 总结来说, 你写代码了, 但是做了啥呢? 没体现. 避免流水账, 希望可以简洁明了, 突出重点, 使用STAR法则, 参见如何使用STAR法则写自己的简历啊 避免写和你面试岗位不相关的内容, 我去当家教了, 我把它写到简历里, 但是你面试的是技术岗位, 不是老师. 4 个人技能 将个人技能按照熟悉程度降序排列, 通过项目和技能介绍, 给面试官留下一种”喜欢学习新事物, 喜欢挑战, 喜欢折腾, 有geek精神”. 列下需要注意的点:\n避免主观内容, 比如吃苦耐劳, 善于学习这些; 招聘面试很重要的一点是筛选出符合有相关专业/潜力的同学, 这些都是通过客观条件体现的, 比如你的项目, 竞赛, 论文等, 尝试通过能力和项目来证明, 而不是自己主观评价. 程序开发是技术活, 对于应届生而言, 讲究的是 Talk is cheap, Show me your work. 尝试提供事实支撑; 如”熟悉Spring框架”的表述, 肯定不如”了解Spring框架, 读过部分代码, 包括容器依赖注入, 控制反转, 总结相关的设计模式”等. 不要写一些和技术无关的技能, 如”会PS, 有驾照”这类. 四六级, 雅思/托福, 日语N1/N2这些语言技能可以加上 5 顶级期刊论文/Acm竞赛 这些都是重要加分项, 如果有的话, 就把期刊论文和Acm竞赛的获奖经历, 列出来, 提高面试官的期望值, 按奖项/论文的含金量降序排列, 如果没有的话, 就跳过.\n6 其他亮点 大部分的同学可能都没有Github 1w+的star, 没有为Linux Kernel/Netty/Redis/Mysql这些项目贡献过代码 ,没发过顶级期刊的论文, 就觉得自惭形愧, 一无是处.\n我觉得并非如此, 我觉得折腾过Vim/Emacs, 熟悉使用Zsh+Tmux+Git, 熟悉Linux(关于熟悉的标准, 参见下文), 也是亮点;\n并非要做到最好, 才叫有亮点; 也并非产出对应的结果才讲亮点, 对于学生而言, 探索/折腾的过程同样重要; 此外, 没有哪个专家不是从菜鸟开始起步的; 接下来我会列举下我认为亮点的地方:\n参与开源项目, 有一定的star/follower, 比如我到现在都在维护Rust的一个开源库, 也写过700+star的爬虫. 有自己的blog, 很多新的技术就可以在blog实践, 也有地方可以沉淀自己的思考, 包括遇到的问题及其排查思路与过程, 记录有趣的事情等等, 但如果都是搬运的文章就算了. 研究过开源技术, 如我自己折腾过常用的Linux发行版本, 个人开发日常使用Linux, 使用Emacs超过5年, 自己编写Shell脚本管理电脑, 在17年开始学习Rust等等. 阅读相关项目源码, 有相应的总结/思考. 如Jdk/JUC源码, Spring源码, Tomcat源码, Netty源码, 记录在自己blog上. 了解/使用多种语言, Java/C++/C/Python/Go/Rust/Sql/Shell, 这个就不一一列举了. 总而言之, 自己的思考_动手折腾_新鲜事物的探索, 都可以像亮点.\n7 个人评价/兴趣爱好 公司招聘是选择有能力, 并且合适的同学, 并不是相亲, 所以老板并不关心你的兴趣爱好和个人评价; 在面试中, 你应该是由面试官评价, 自我评价并没有什么用处, 写上去还占空间.\n8 细节 需要明确的一点, 在面试官面试你之前, 你的简历就是你最大的推销手段, 你的简历代表着你在和其他上百名的竞争者做着竞争.\n因此你的简历每多打磨一分, 你的在众多简历中脱颖而出的机会就多了一分, 所以简历需要精心打磨, 那么很多细节就应该注意, 说下我看到的细节点:\n文档格式: 简历的文件类型最好用pdf, 很多技术开发用的是Mac, 如果用的是word, 可能遇到各种问题, 排版也可能会乱掉, 对于pdf而言就不存在这样的问题, 速度也足够行. 简历模板: 可以的话, 请不要用 word 套模板, 要套模板就用latex, 不用调格式, 例如: https://github.com/billryan/resume 对于伸手党同学, 注册这个网站, 把你的简历内容替换掉模板即可: https://www.overleaf.com/project/5e6c67ac54a3190001a2fed7 如果这样还不会的话, 那就\u0026hellip; 简历篇幅: 应届生的简历最好一页写完, 如果一页没写完, 第二页只多了一点内容, 就会显得很难受. 简历命名: 发送简历给面试官, 或者简历收集同学的时候, 请不要用”个人简历_我的简历”这类的名字, 谁知道”个人_我”指的是谁, 推荐命名: 学校_学历_姓名_求职意愿.pdf 如: xx大学_硕士_宫xx_后端开发.pdf 技术熟悉程度: 精通, 熟悉, 了解; 这些用词请注意, 按我的理解, \u0026ldquo;了解\u0026quot;要起码用这个技术自己做过一点东西, 平时关心相关的新闻和前沿进展; \u0026ldquo;熟悉\u0026quot;则是平时经常用到这个技术, 或者曾经在很长一段时间内以它为主做过开发;\u0026ldquo;精通\u0026quot;则起码要能把它从头到尾理解得非常透彻才能算是. 如果你是了解, 然后简历说是精通, 面试官对你的期望会拔高, 然后发现你是了解, 那心理就会有落差. 举例 ,我精通git, 然而只会git add/git commit/git push, 连git bisect都没听过, 那就\u0026hellip; 参与程度; 参与, 负责; 请注意用词, 参与系统开发表现对某个功能模块清楚, 负责表示所有设计考虑, 技术实现都清楚. 和你面试工作相关的东西不要写; 如我是学生会干部, 这个没啥用, 我们要的不是干部, 而是有相关专业技能的人才. 9 总结 总而言之, 写好简历可以做到扬长避短, 最大限度突出亮点的作用, 如果你觉得实在绞尽脑汁都没有什么可以写的话, 或者你应该重新去做些个人项目, 积累经验再来投递.\n说了这么多, 因为拿份示例出来了, 因为我已经工作2年, 已经找不到找实习当时的简历了, 所以拿了基友的简历过来, 基友拿到了AWS的offer, 他的实习/学术项目已经足够丰富, 其他内容就做了取舍(已获得基友授权, 基于本人要求, 去掉个人信息):\nFigure 1: 8ZxIzt.jpg\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E5%BA%94%E5%B1%8A%E7%94%9F%E5%A6%82%E4%BD%95%E5%86%99%E5%A5%BD%E6%8A%80%E6%9C%AF%E7%AE%80%E5%8E%86/","summary":"1 背景介绍 笔者目前在蚂蚁金服-网商银行做后端开发, 因为在组内毕业时间最短(2年), 所以经常会被Leader当成免费的HR去找校招简历, 所以见","title":"应届生如何写好技术简历"},{"content":"1 Preface Today, I am exited to introduce you the v0.9 release I have been continued to work on it for the past few weeks that adds async/await support now!\n2 The road to async/await What is rspotify: \u0026gt; For those who has never heared about rspotify before, rspotify is a Spotify web Api wrapper implemented in Rust.\nWith async/await\u0026rsquo;s forthcoming stabilization and reqwest adds async/await support now, I think it\u0026rsquo;s time to let rspotify leverage power from async/await. To be honest, I was not familiar with async/await before, because of my Java background from where I just get used to multiple thread and sync stuff(Yes, I know Java has future either).\nAfter reading some good learning resources, such as Async book, Zero-cost Async IO, I started to step into the world of async/await. async/await is a way to write functions that can \u0026ldquo;pause\u0026rdquo;, return control to the runtime, ant then pick up from where they left off.\nI think perhaps the most important part of async/await is runtime, which defines how to schedule the functions.\nNow, by leveraging the async/await power of reqwest, rspotify could send HTTP request and handle response asynchronously.\nFuthermore, not only do I refactor the old blocking endpoint functions to async/await version, but also keep the old blocking endpoint functions with a new additional feature blocking, then other developers could choose API to their taste.\n3 Overview album example:\nuse rspotify::client::Spotify; use rspotify::oauth2::SpotifyClientCredentials; #[tokio::main] async fn main() { // Set client_id and client_secret in .env file or // export CLIENT_ID=\u0026#34;your client_id\u0026#34; // export CLIENT_SECRET=\u0026#34;secret\u0026#34; let client_credential = SpotifyClientCredentials::default().build(); // Or set client_id and client_secret explictly // let client_credential = SpotifyClientCredentials::default() // .client_id(\u0026#34;this-is-my-client-id\u0026#34;) // .client_secret(\u0026#34;this-is-my-client-secret\u0026#34;) // .build(); let spotify = Spotify::default() .client_credentials_manager(client_credential) .build(); let birdy_uri = \u0026#34;spotify:album:0sNOF9WDwhWunNAHPD3Baj\u0026#34;; let albums = spotify.album(birdy_uri).await; println!(\u0026#34;{:?}\u0026#34;, albums); } Just change the default API to async, and moving the previous synchronous API to blocking module.\nNotes that I think the v0.9 release of rspotify is going to be a huge break change because of the support for async/await, which definitely breaks backward compatibility.\nSo I decide to make an other break change into the next release, just refactoring the project structure to shorten the import path:\nbefore:\nuse rspotify::spotify::client::Spotify; use rspotify::spotify::oauth2::SpotifyClientCredentials; after:\nuse rspotify::client::Spotify; use rspotify::oauth2::SpotifyClientCredentials; the spotify module is unnecessary and inelegant, so I just remove it.\n4 Conclusion rspotify v0.9 is now available! There is documentation, examples and an issue tracker!\nPlease provide any feedback, as I would love to improve this library any way I can! Thanks @Alexander so much for actively participate in the refactor work for support async/await.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/async_await_for_rspotify/","summary":"1 Preface Today, I am exited to introduce you the v0.9 release I have been continued to work on it for the past few weeks that adds async/await support now!\n2 The road to async/await What is rspotify: \u0026gt; For those who has never heared about rspotify before, rspotify is a Spotify web Api wrapper implemented in Rust.\nWith async/await\u0026rsquo;s forthcoming stabilization and reqwest adds async/await support now, I think it\u0026rsquo;s time to let rspotify leverage power from async/await.","title":"rspotify has come to async/await"},{"content":"1 前言 \u0026lt;枪炮, 病菌与钢铁\u0026gt;以一个新几内亚政治家耶利的问题展开,\u0026ldquo;为什么你们白人制造了那么多的货物并将它们运到新几内亚来, 而我们黑人却几乎没有属于我们自己的货物呢?\u0026rdquo;, 由此引出此书后续的的核心主题:\n为什么这个世界的财富与权力的分配会是现在这个样子的, 来自欧亚大陆的民族, 尤其是仍然生活在欧洲与东亚的民族, 以及移居到北美的民族, 控制着世界的财富与权力.\n其他民族, 包括大多数非洲人, 已经摆脱了欧洲人的殖民统治, 但在财富与权力方面仍然远远落在后面. 简而言之, 为什么在不同的大陆上人类以如此不同的速度发展呢?\n2 粮食生产与大陆轴线 Jared 提出了四组差异, 并用大量篇幅介绍这四组差异如何让欧亚大陆成为政治, 经济与军事的强者, 而不是现代人类的发祥地非洲, 不是美洲或者澳大利亚.\n2.1 可驯化动植物品种的差异 第一组差异是各大陆在可以用作驯化的起始特种的野生动植物品种方面的差异. 这是因为, 粮食生产之所以具有决定性的意义, 在于它能积累剩余粮食以养活不从事粮食生产 的专门人材, 同时也在于它能形成众多的人口, 从而甚至在发展出任何技术与政治优势之前, 仅仅凭借人多就可以拥有军事上的优势.\n由于这两个原因, 从小小的不成熟的酋长管辖地阶段向经济复杂的, 社会分层次的, 政治上集中的社会发展的各个阶段, 都是以粮食生产为基础的.\n就可驯化的生物物种而言, 欧亚大陆最为得天独厚, 非洲次之, 美洲又次之, 而澳大利亚最下. 令人惊讶的是, 原来美洲驯化的动物只有羊驼, 还只是小部分地方有, 而马是由欧洲殖民者带到美洲的, 部分印第安部落成为驯马的高手又是后来的事了, 看来我原来一直都被美国西部片给误导了\n2.2 影响传播和迁移速度的差异 第二组因素就是那些影响传播和迁移速度的因素, 而这种速度在大陆与大陆之间差异很大.\n在欧亚大陆速度最快, 这是由于它是的东西向主轴线和它的相对而言不太大的生态与地理障碍, 对于作物和牲畜的传播来说, 这个道理是最简单不过, 因为这种传播大大依赖于气候, 因而也就是大大依赖于纬度.\n同样的道理也适用于技术的发明, 如果不用对特定的环境加以改变就能使这些发明得到最充分的利用的话. 传播的速度在非洲就比较缓慢, 而在美洲就尤其缓慢, 这是由于这两个大陆的南北向主轴线生地理与生态障碍.\n2.3 影响大陆内部传播的差异 与影响大陆内部传播的这些因素相关的, 是第三组影响大陆之间传播的因素, 大陆与大陆之间传播的难易程度是不同的, 因为某些大陆比另一些大陆更为孤立.\n在过去的6000年, 传播最容易的是从欧亚大陆到非洲撒哈拉沙漠以南地区, 非洲大部分牲畜就是通过这种传播得到的.\n但东西半球的传播, 则没有对美洲的复杂社会作出任何贡献, 这些社会会在低纬度与欧亚大陆隔着宽阔的海洋, 而在高纬度又在地形和适合狩猎采集生活的气候之间又与欧亚大陆相去甚远.\n对于原始的澳大利亚来说, 由于印度尼西亚群岛的一道道水上障碍把它同欧亚大陆隔开, 欧亚大陆对它的唯一的得到证明的贡献就是澳洲野狗.\n2.4 面积与人口总数方面的差异 第四组也是最后一组因素是各大陆之间在面积和人口总数方面的差异.\n更大的面积或更多的人口意味着更多的潜在发明者, 更多的互相竞争的社会, 更多的可以采用的发明创造\u0026ndash;以及更大的采用和保有发明创造的压力, 因为任何社会如果不这样做就往往会被竞争对手所淘汰.\n3 中国的落后原因分析 文中除了阐述了为什么这个世界的财富与权力分配是现在这个样子之外, 还尝试解答一个问题: 既然新月沃地和中国最早产生粮食中心, 那么为什么现在先进的是欧洲, 而不是新月沃地与中国呢?\n对于新月沃地的解答是环境破坏导致新月沃地在后来成为了沙漠与半沙漠, 不再具有粮食生产中心的后续优势, 继而被欧洲取代, 我不了解阿拉伯历史, 因此不置可否.\n3.1 统一与分裂 而对于中国的分析则是提出了一个分裂有益的观点: 对于欧洲而言, 割裂的地理环境使其无法成为大一统的国家, 而中国则是因为历史, 地理的原因, 从秦始皇之后就是一个统一的国家, 即使有过分裂, 但也没有形成持续分裂的国家. 而这种大一统是有害的, 适合的分裂反而有益.\nJared 举出的例子是, 当初郑和下西洋时, 他的宝船代表着当时世界最先进的造船技术, 而却因为政治斗争失败, 明王朝停止派遣舰队远航并拆掉船坞, 禁止后续远洋航行.\n而欧洲的哥伦布曾请求葡萄牙国王派船让他向西航行探险, 他的请求被国王拒绝, 于是他就求助于梅迪纳-塞多尼亚公爵, 也遭到拒绝, 接着他又去请求梅迪纳-塞利伯爵, 依然遭到拒绝, 最后他求助于西班牙国王与王后, 他们拒绝了他的第一次请求, 但后来在他再次提出请求时总算同意.\n如果欧洲在这头3个统治者中任何一个的统治下统一起来, 它对欧洲殖民也许一开始就失败了. Jared 认为, 正是由于欧洲是分裂的, 哥伦布才成功地于第五次在几百个王公贵族中说服一个来赞助他的航海事业.\n虽说 Jared 提出了一个新奇的观点, 但我对这个的观点并不认同, 虽然我并不能像你回答耶利的问题那样, 总结出种种的因由, 但我觉得你的观点并不正确.\n像你而言, 中国人在1405年就已经率领28000船员到达非洲东海岸, 但是中国人并没有对他们建立殖民统治, 只是对这些非洲国家或者部落进行访问; 而达伽马和哥伦布到非洲与美洲的一个地方就要殖民一个地方, 中国人与欧洲人的差异在何处? 导致这种差异的原因是什么?\n3.2 地理决定论 Jared 他现在是手上拿着一个锤子(地理决定论), 看着什么都觉得是钉子.\n我觉得原因在于政治制度与文化传统的原因导致中国在明朝之后落后于欧洲的, 中国的统一王朝封建制度在初期是先进的制度, 而到后期逐步成为一种阻碍, 权力过于集中于一人之手(而平庸的君主总是比贤明的君主稀少).\n另外一个重要的因素是儒家思想, 作为王朝思想统治的手段, 目的只是制定一系列的标准与守则, 让人忠于王朝统治者, 类似中世纪的宗教, 有差异的是中国并没有出现文艺复兴, 而在南宋进一步加强.\n但是这些观点只是我一个工科男没有证明根据的臆测, 或许有一天, Jared 或者其他人可以给出一个让我信服的观点.\n4 总结 许久没有看过历史相关的著作, 虽说我自诩是个历史的爱好者.\nJared 这本书的确解答了我许多关于世界历史与现状的疑问, 甚至配得上人类简史这个名号, 但是此书也引起了我关于自己国家的思考与疑问, 为什么中国会变得落后于人, 原因何在?\n最后, 谨以书中的一句话结束此文, 也送给欧洲人民, 与其他大洲人民共勉:\n环境改变了, 过去是第一并不能保证将来也是第一.\n所谓萧瑟秋风今又是, 换了人间.\n","permalink":"https://ramsayleung.github.io/zh/post/2020/%E6%9E%AA%E7%82%AE_%E7%97%85%E8%8F%8C%E4%B8%8E%E9%92%A2%E9%93%81/","summary":"1 前言 \u0026lt;枪炮, 病菌与钢铁\u0026gt;以一个新几内亚政治家耶利的问题展开,\u0026ldquo;为什么你们白人制造了那么多的货物并将它们运到新几内亚","title":"枪炮, 病菌与钢铁"},{"content":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一段旅途;\n失大于得则证明这段时间虚度罢了, 却无法重来. 本文便是对过去一年得与失的总结.\n无可奈何花落去,似曾相识燕归来.\n2 工作 我所在的项目组做的是对B端的聚合收单业务, 有蚂蚁的big title 背书, 服务一堆的服务商, 但业务主导一切, 一切以业务为中心, 技术并没有话语权.\n而后业务突遭变故, 业务接近停滞. 都要以为要重新准备简历了, 要拥抱变化了. 然后后面业务重新复活, 继续挣扎, 在生与死之间反复横跳, 为了复活做各种奇形怪状的需求, 也未见有起色.\n在整个由死重生的过程中对于收单业务有了重新的认识, 对了公司也有了新的认识, 对于自己的地位与作用也有新的认识.\n说到底, 我自己只是个工具人, 对于完全业务化的系统, 技术的作用着实毫不起眼, 充斥着无力感. 因此工作上免不了彷徨与迷茫.\n另外一方面, 因为这样的业务状况, 我也如自己预想中那般, 绩效拿了3.5, 无晋升提名. 在花呗的室友, 同一天入职, 类似的绩效, 晋升了. 可见, 选择着实比努力更重要点.\n3 读书 打算用读书冲淡工作变故而来的彷徨感, 兼之对于自身的不满与现实的疑惑, 寄望于通过多读书充实自己和从书中得到解答, 因此今年读了不少的书, 基本每本书都写了笔记与感悟:\n读完的书:\n追风筝的人 双城记 浮生六记 围城 沉默的大多数 苏菲的世界 月亮与六便士 netty实战 java并发编程实战 Effective C++ 在读的书\nUnix网络编程(读了1/3) 枪炮, 病菌与钢铁 除此之外, 还看了各种文章, 关于电影, 财经, 政治以及历史.\n总结下来基本是每个月读完一本书, 虽说与去年20本的目标还有差距, 但这年来读的书, 着实解答了我不少疑问.\n例如我现在为什么会996(实际上9105或者9115)? 原因可以说是多方面的:\n从革新与底层技术方面来说, 我们没有经历过工业革命, 没有以技术去推动社会生产力进步的传统, 这三十年的发展很大一部分是全球化与人口红利的结果.\n同理, 中国互联网只有业务模式的创新, 并没有基础技术的革新与壁垒, 所谓的新四大发明便是如此; 因为没有技术壁垒, 你做的东西, 别人也容易仿制, 所以只能和别人比速度, 难免就出现拼命加班的情况.\n而从企业的角度来说, 以这个号称996发源地的公司举例, 他们的目的就是要不择手段地实现利益最大化, 员工利益的保障只能靠资本家良心发现了, 而资本家只是资本的人格化, 资本是不论对错, 只谈利弊的.\n从政府及立法角度来说, 到了这一步, 员工的权利只能由政府来保障, 需要对企业作限制, 然而我们的政府对这种创造大量GDP的企业, 只会当作爸爸, 又怎会处罚呢? 你见过在南山区法院打赢腾讯的么? 在西湖区打赢支付宝的么?\n而我们又没有投票权, 政府要加税就加税, 要保大企业就保大企业, 我们又能怎样? 政府不鼓励大企业实行996就不错了, 还处罚他们?\n当然, 还有自身的原因, 身边自然不会少自愿加班的人, 而他们作为三口之家的唯一劳动力, 想要在杭州安家, 想有自己的房子, 需要负出自己的时间, 精力与健康, 也因为这样的人, 使996得而蔚然成风, 但为何买一套自己的房子需要付出如此大的代价, 引申出来又是一个复杂的问题, 可以参考下这两篇文章\n每周转载 天涯kkndme 神贴聊房价 诸如此类的感悟, 是我在今年读书后, 对心中疑惑的思考. 读书的作用就如小恶魔 Tyrion Lannister所说的那般, 好脑筋需要书本, 就如同宝剑需要磨刀石.\n4 其他 对于杭州有了新的认识, 借用下别人对杭州的评价:\n马路平整, 四季分明, 冬暖夏凉, 房价便宜, 美食多样, 工资够用, 一天工作 8 小时, 地铁发达, 大公司多, 小公司都很专业\n只需将上面的内容反转一下, 就可以知道杭州的实况. 去掉古代文人墨客诗文的滤镜, 杭州也就是这样罢了.\n逐渐明白, 技术并不是万能, 甚至用处并不是那么大.\n明白自己的无知和渺小, 很多事情并不能用技术来解决, 如工作遇到的变故等, 过于沉迷技术会形成一个误区, 以为什么都能用技术解决(当然, 也不能以此为借心放弃自己).\n明白了每项技术都用其存在的意义及背景, 如什么场景都用javascript自然不行, 但在浏览器场景不用javascript, 自然也是不行的.\n并技术没有对错, 争论哪个技术最好, 哪个编程语言更佳, 脱离场景毫无意义, 文人相轻又能解决什么问题呢?\n开始学习其他技术无关的知识与技能; 重新练习口琴; 开始有意识地控制体重;\n开始按照无器械健身的相关指南锻炼; 每周基本都有去运动, 游泳或者踢球;\n开始补经典的番; 想学日语, 想去日本看看; 想去加拿大看看, 心生去意;\n5 展望 把去年的展望搬过来, 机智如我:\n了解分布式, 高可用的知识,争取通过实战掌握; 读完《netty in action》; 通过许家纯大大的教程,自己实现一个Rpc 框架;读sofa-bolt, sofa-rpc 和 Netty 的源码 成为一个掌握金融知识的计算机从业人员 读完20本书 结束单身狗的生活 借一句诗勉励自己:\n沉舟侧畔千帆过,病树前头万木春.\n","permalink":"https://ramsayleung.github.io/zh/post/2019/2019%E6%80%BB%E7%BB%93/","summary":"1 前言 人总是健忘的, 所以在行走一段人生旅途之后, 总要不自觉地停下来, 整理下前段时间的得与失, 得大于失证明这段时间没有浪费, 欣喜之余, 准备下一","title":"2019年总结: 人生如逆旅, 我亦是行人"},{"content":"1 前言 JDK 提供了各种功能强大的工具类, 宛如装备齐全的军火库, 而容器就是其中一项内置的利器, 提供了包括诸多常用的数据结构, 下图对 JDK 已有容器进行了概括:\nFigure 1: JDK 容器\n不过, 虽然 JDK 的容器类已经五花八门, 琳琅满目, 但是某些很有用的容器类 JDK 依然欠缺, 而 Guava 恰如其分地填补了这些空缺, 开发了 JDK 所欠缺的容器类, \u0026ldquo;造福大众\u0026rdquo;.\n此外, 虽然引入了新的容器类, 但 Guava 实现了 JDK 的 Collection 接口, 保证 Guava 的容器类能够与 JDK 的容器类”和谐共处”, 避免不必要的”纷争”.\n2 Multiset 假设你是个书店的老板, 你想统计下书店里不同书籍的存货量, 你可能写下这样的实现:\nMap\u0026lt;String, Integer\u0026gt; counts = new HashMap\u0026lt;String, Integer\u0026gt;(); for (String book : bookNames) { Integer count = counts.get(book); if (count == null) { counts.put(book, 1); } else { counts.put(book, count + 1); } } 嗯, 我现在想改需求, 我想知道书店里共有多少本书? 怎么办呢? 把 counts 的 value 都加起来?\n对于这样的要求, Guava 提供了一个更好的解决方案: 一个新类型容器 Multiset , 它支持新增多个相同类型的元素并统计. 维基百科给出的关于 Multiset 的解释:\n这是个成员可以出现多次的集合(Set), 也被称为背包(bag)\n大名鼎鼎的《算法/Algorithm》也给出过 bag 的解释和实现.\n需要注意的是, multisets 的成员是无序的, {a,a,b} 和 {a,b,a} 这两个集合在 multisets 看来是相等.\n我们可以从两个角度来分析 multisets :\nmultisets 就好像一个ArrayList\u0026lt;E\u0026gt;, 只不过是无序的. 当把它当作ArrayList\u0026lt;E\u0026gt;时:\n调用add(E)函数, 增加给定元素的出现次数 调用iterator()函数, 获取一个 multisets 的迭代器, 用来迭代每个元素 调用size()函数, 获取所有元素出现次数之和 multisets 就好象一个Map\u0026lt;E, Integer\u0026gt;, 包含元素和对应的数量, 只不过数量只能为正数. 当把它当作Map\u0026lt;E, Integer\u0026gt;的时候:\n调用count(Object)函数获取某个特定元素的出现次数. 调用entrySet()函数返回一个Set\u0026lt;Multiset.Entry\u0026lt;E\u0026gt;\u0026gt;, 大概类似一个 Map 返回 entrySet . 调用 elementSet 函数返回一个Set\u0026lt;E\u0026gt;对象, 返回所有的元素(去掉重复的元素) 2.1 Multiset 的例子 粗略介绍完 Multiset 之后, 现在就让我们用它重新实现原来的需求:\n@Test public void testMultiset() { final String POTTER = \u0026#34;Potter\u0026#34;; Multiset\u0026lt;String\u0026gt; bookstore = HashMultiset.create(); bookstore.add(POTTER); bookstore.add(POTTER); bookstore.add(\u0026#34;四体\u0026#34;); bookstore.add(\u0026#34;五体\u0026#34;); Assert.assertTrue(bookstore.contains(POTTER)); Assert.assertEquals(2, bookstore.count(POTTER)); Assert.assertEquals(4, bookstore.size()); bookstore.remove(POTTER); Assert.assertTrue(bookstore.contains(POTTER)); Assert.assertEquals(1, bookstore.count(POTTER)); } multisets 完美满足了我们的需求.\n2.2 Multiset 并不是一个 Map 需要注意的是, Multiset 虽然与Map\u0026lt;E, Integer\u0026gt;类似, 但 Multiset 并不是一个Map\u0026lt;E, Integer\u0026gt;, 请不要混淆它们两个.\n最大的差别是, Multiset 实现了 Collection 接口, 完全遵守 Collection 接口需要满足的协议, 而 Map 和 Collection 是完全不同的接口, 这点需要牢记于心. 还有其他的差别, 诸如:\nMultiset\u0026lt;E\u0026gt;出现的次数只能是正数, 没有任何元素的出现次数会是负数的, 出现次数为 0 的元素会被认为不存在, 这样的元素是不会出现在elementSet()和entrySet()的返回结果中的. 而Map\u0026lt;E, Integer\u0026gt;肯定不会有这样的限制. multiset.size()返回所有元素出现次数之和, 如果想要知道有多少个不重复的元素, 可以使用elementSet().size(), 例如{a,a,b}, elementSet.size()返回结果是 2, multiset.size()返回结果是 3. multiset.iterator()用于迭代每个出现的元素, 所以迭代次数和multiset.size()的值一样的. Multiset\u0026lt;E\u0026gt;支持增加元素, 删减元素, 或者通过 setCount 函数直接设置元素的出现次数, setCount(a, 0)的意思等于将删除所有的 a 元素. multiset.count(elem): 如果元素 elem 不存在, 那么返回值总是 0. 而 Map 对于不存在的元素, 返回的是 null . 2.3 Multiset 实现 鉴于 Multiset 只是个接口, Guava 提供许多的接口实现, 大致可以与 Java 中的容器对应上:\nMap MultiSet 支持 null 元素 HashMap HashMultiset Yes TreeMap TreeMultiset Yes LinkedHashMap LinkedHashMultiset Yes ConcurrentHashMap ConcurrentHashMultiset No ImmutableMap ImmutableMultiset No 3 Multimap 又来假设, 你是个班主任, 刚刚考完试, 你想记录下班里所有同学的成绩, 你可能写下这样的实现:\nInteger studentScore = 60; String studentName = \u0026#34;Alan\u0026#34;; // 每个学生的成绩单 Map\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt; studentScoresMap = new HashMap\u0026lt;\u0026gt;(); // 如果 Alan 还没记录各科成绩的列表, 就新建一个列表 List\u0026lt;Integer\u0026gt; studentScores = studentScoresMap.get(studentName); if (studentScores == null) { studentScores = new ArrayList\u0026lt;\u0026gt;(); studentScoresMap.put(studentName, studentScores); } // 然后将某个科目的成绩加进去 studentScores.add(studentScore); 一个学生考试要考多个科目, 自然就会有多个学科成绩, 也就出现了一个 key 需要对应多个 value 的情况. 使用Map\u0026lt;K, List\u0026lt;V\u0026gt;\u0026gt;或者Map\u0026lt;K, Set\u0026lt;V\u0026gt;\u0026gt;这样的方式构建 key-values 自然可以, 只不过显得不甚优雅.\n为此, Guava 提供了新的容器类型来应对一个 key 对应多个 values 的场景: Multimap . 同样的, 我们也可以从两个角度来理解 Multimap :\n一个 key 对应一个 value , 同样的 key 可以存在多个: a -\u0026gt; 1 a -\u0026gt; 2 a -\u0026gt; 4 b -\u0026gt; 3 c -\u0026gt; 5 或者一个 key 对应一个列表的 value : a -\u0026gt; [1, 2, 4] b -\u0026gt; [3] c -\u0026gt; [5] 通常来说, 最好以第一种方式来理解 Multimap 接口, 不过你也可以以第二种方式来获取数据: asMap()函数, 返回一个 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; 对象.\n需要注意的是, 不存在 1 个 key 对应 0 个 value 的情况, 不会有空的值列表这样的说法, 要不一个 key 对应至少一个 value , 要不就是这个 key 不存在于这个 Multimap .\n一般来说, 我们不会直接使用 Multimap 接口, 使用的是它的子接口; Multimap 接口提供了两个子接口: ListMultimap 和 SetMultimap , 大致类似于 Map\u0026lt;K, List\u0026lt;V\u0026gt;\u0026gt;和 Map\u0026lt;K, Set\u0026lt;V\u0026gt;\u0026gt;.\n3.1 Multimap 的例子 现在让我们用 Multimap 重新实现一次学生不同科目的成绩单:\nString alan = \u0026#34;Alan\u0026#34;; String turing = \u0026#34;Turing\u0026#34;; // 创建一个 ListMultimap ListMultimap\u0026lt;String, Integer\u0026gt; studentScoresMap = MultimapBuilder.hashKeys().arrayListValues().build(); studentScoresMap.put(alan, 95); studentScoresMap.put(alan, 88); studentScoresMap.put(turing, 100); Assert.assertEquals(3, studentScoresMap.size()); List\u0026lt;Integer\u0026gt; alanScores = studentScoresMap.get(alan); Assert.assertEquals(2, alanScores.size()); alanScores.clear(); Assert.assertEquals(0, studentScoresMap.get(alan).size()); 3.1.1 构造 细心的同学可能会发现, 上面创建 ListMultimap 的方式不是直接调用实现类的.create()函数, 而是使用 MultimapBuilder .\n并不是 Multimap 的实现没有提供.create()方法, 是通过 MultimapBuilder 创建 Multimap 实现会更加便利一点, 使用hashKeys()函数创建的就是一个 HashMap , 使用treeKeys()函数创建的就是一个 TreeMap .\n3.1.2 修改 Multimap.get(key)返回的就是特定 key 关联的集合, 对于一个 ListMultimap , 返回的就是一个 List ; 对于一个 SetMultimap , 返回的就是一个 Set .\n实际返回的是集合的引用, 所以对这个返回集合的操作, 将直接反馈在 Multimap 实例上. 如上面的例子所示, 把学生 alan 返回列表的数据清空, 在 ListMultimap 的数据也相应地被清空了.\n3.2 视图 所谓的视图(Views), 我理解就是看待事物的方式和角度, 称为视图(或者视角\u0026rsquo;perspective\u0026rsquo;).\nMultimap 提供了若干个有用的视图:\nasMap 把 Multimap\u0026lt;K,V\u0026gt; 看作一个 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; , 返回一个的 map 对象支持 remove 操作, 但不支持 put 和 putAll 操作.\n值得关注的是: 当对应的 key 不存在的时候, multiMap 返回的是一个新构造的, 为空的集合, 如果你想在对应的 key 不存在的时候返回空指针(就好像 HashMap 那样), 你可以通过 asMap().get(key) 实现这样的效果\nstudentScoresMap.asMap().remove(alan); // 抛出 UnsupportedOperationException 异常 studentScoresMap.asMap().put(\u0026#34;key\u0026#34;, Lists.newArrayList()); // 抛出 UnsupportedOperationException 异常, 除非 anotherScores 是个空的 Map studentScoresMap.asMap().putAll(anotherScores); // 返回空的集合 Collection\u0026lt;Integer\u0026gt; Elons = studentScoresMap.get(\u0026#34;Elon\u0026#34;); // 返回空指针 studentScoresMap.asMap().get(\u0026#34;Elon\u0026#34;); entries 把 Multimap 内所有的记录(entry)看作 Collection\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt;, 如前文的 studentScoresMap.entries() 返回的就是: [{\u0026quot;Alan\u0026quot;: 95}, {\u0026quot;Alan\u0026quot;: 88}, {\u0026quot;Turing\u0026quot;: 100}]. keySets 把 Multimap 内所有的不重复的 key 看作一个 Set . 如前文的 studentScoresMap.keySets() 返回的就是: Set([\u0026quot;Alan\u0026quot;,\u0026quot;Turing\u0026quot;]). keys 把 Multimap 内所有的 key 看作一个前文提到的 Multiset , 可以从这个 Multiset 删除元素, 但不能新增元素, 如前文的 studentScoresMap.keys() 返回的就是: Multiset([\u0026quot;Alan\u0026quot;,\u0026quot;Alan\u0026quot;, \u0026quot;Turing\u0026quot;]). values() 把 Multimap 内所有的 value 看作一个集合, 相当于把所有 key 对应的 value 集合串联起来, 如前文的 studentScoresMap.values() 返回的就是: [95, 88, 100] 3.3 Multimap 也不是一个 Map 严格来说, 即使 Multimap 名字中带有 map, 甚至 map 可能用来实现 Multimap , 但一个 Multimap\u0026lt;K,V\u0026gt; 终究不是一个 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt;. 它们之间的差异包括:\nMultimap.get(key) 返回的对象总是不为空指针的, 即使查询的 key 不存在, 返回的是个空的集合. 而 Map.get(key) 查询的 key 不存在, 返回的就是空指针. 前文提到过, 如果想要让 Multimap 在查询 key 不存在的时候返回空指针, 可以使用 Multimap.asMap().get(key). Multimap.containsKey(key) 在 values 集合为空的时候就会返回 false, 例如 studentScoresMap.putAll(\u0026quot;elon\u0026quot;, Lists.newArrayList()); Assert.assertFalse(studentScoresMap.containsKey(\u0026quot;elon\u0026quot;)), 但对于 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; 而言, 返回的就会是 true, 因为 value 不为 null. Multimap.size() 返回的是所有记录的总数的, 即把所有的 value 的数量累加起来, 而 Map\u0026lt;K, Colleciton\u0026lt;V\u0026gt;\u0026gt; 返回的就是 key 对应的数量. 3.4 实现 Multimap 提供了若干个不同类型的实现, 你可以使用对应的实现来取代原来 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt; 的地方:\n实现 key 表现得类似\u0026hellip; value 表现得类似\u0026hellip; ArrayListMultimap HashMap ArrayList HashMultimap HashMap HashSet LinkedListMultimap LinkedHashMap LinkedList LinkedHashMultimap LinkedHashMap LinkedHashSet TreeMultimap TreeMap TreeSet ImmutableListMultimap ImmutableMap ImmutableList ImmutableSetMultimap ImmutableMap ImmutableSet 上述的实现, 除了不可变的实现之外, 其他都支持 null key 与 null value. 并非所有的实现底层用的都是 Map\u0026lt;K, Collection\u0026lt;V\u0026gt;\u0026gt;, 有好几个实现出于性能的考虑, 实现了自定义的 hash 表.\nMultimap 还支持自定义 value 的集合形式, 如 List 形式或者 Set 形式, 详情可见 Multimaps.newMultimap(Map, Supplier\u0026lt;Collection\u0026gt;)\n4 BiMap 继续假设, 你是个班主任, 你有个学生名字与学号的名单, 你有时会通过名字查询对应学号, 有时又会根据学号反查询学生名字, 通常来说, 你会这么实现这个名单:\nMap\u0026lt;String, String\u0026gt; nameToId = Maps.newHashMap(); Map\u0026lt;String, String\u0026gt; idToName = Maps.newHashMap(); nameToId.put(\u0026#34;Linus\u0026#34;, \u0026#34;0001\u0026#34;); idToName.put(\u0026#34;0001\u0026#34;, \u0026#34;Linus\u0026#34;); // 如果 0001 这个学号, 或者 Linus 这个名字已经存在了, 会发生什么事情呢? // 会出现很微妙的 bug, 为了避免出现这种情况, 你需要手动维护这种限制 不得不说, 通过两个 Map 和实现 value 反查 key 的传统做法并不优雅, 即增加了心理负担, 又容易出 bug.\n幸运的是, Guava 有一个名为 BiMap 类库, 提供了通过 value 也反查 key 的特性. 一个BiMap\u0026lt;K,V\u0026gt;是一个Map\u0026lt;K,V\u0026gt;, 提供了如下功能:\n允许通过 inverse() 函数调转 key-value, 从 Map\u0026lt;K,V\u0026gt; 变成 Map\u0026lt;V,K\u0026gt; 保证所有的 value 都是唯一的, values() 函数返回一个包含所有 value 的 Set 如果 value 已经存在, 那么 BiMap.put(key,value) 会抛出一个 IllegalArgumentException 异常, 如果想强制删除掉原来的 value , 并插入一对新的 key-value, 可以使用 Bimap.forcePut(key,value) 4.1 BiMap 例子 让我们用 BiMap 来重新实现学生名字和学号的名单:\nBiMap\u0026lt;String, String\u0026gt; userId = HashBiMap.create(); userId.put(\u0026#34;Linus\u0026#34;, \u0026#34;0001\u0026#34;); String user = userId.get(\u0026#34;Linus\u0026#34;); // 反向查询, 通过学号查询名字. String idForUser = userId.inverse().get(\u0026#34;0001\u0026#34;); // 抛出异常: java.lang.IllegalArgumentException: value already present: 0001 userId.put(\u0026#34;RMS\u0026#34;, \u0026#34;0001\u0026#34;); 4.2 BiMap 实现 key-value map 实现 value-key map 实现 对应的 BiMap HashMap HashMap HashBiMap ImmutableMap ImmutableMap ImmutableBiMap EnumMap EnumMap EnumBiMap EnumMap HashMap EnumHashBiMap 5 Table 假设还是个班主任, 现在你需要制作一个包含学号, 姓名与成绩的名单, 然后可以通过姓名或者学号进搜索, 你会怎么实现呢? 什么? 用 excel? 你好幽默啊!\n// key: 学号, value: {姓名: 成绩} Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; studentScores = Maps.newHashMap(); Map\u0026lt;String, Integer\u0026gt; linus = Maps.newHashMap(); linus.put(\u0026#34;Linus\u0026#34;, 99); studentScores.put(\u0026#34;0001\u0026#34;, linus); // 通过学号获取成绩 Assert.assertEquals(1, studentScores.get(\u0026#34;0001\u0026#34;).size()); for (Map.Entry\u0026lt;String, Integer\u0026gt; element : studentScores.get(\u0026#34;0001\u0026#34;).entrySet()) { String name = element.getKey(); Integer scores = element.getValue(); } // 通过姓名获取成绩 for (Map.Entry\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; element : studentScores.entrySet()) { String id = element.getKey(); Map\u0026lt;String, Integer\u0026gt; nameScores = element.getValue(); if (nameScores.containsKey(\u0026#34;Linus\u0026#34;)) { Integer score = nameScores.get(\u0026#34;Linus\u0026#34;); } } 不得不说, 用 Map\u0026lt;R, Map\u0026lt;C, V\u0026gt;\u0026gt; 的形式来实现多 key 搜索非常难受, 算法效率变为 O(n), 线性时间复杂度, 不但不优雅, 还容易出错, 如果我是班主任, 我就辞职了, 给我个 excel 不行么?\nexcel 是没有的了, 但是 Guava 提供了一个类 excel 的多 key 存储/搜索的容器: Table, 它支持以行和列维度搜索.\n5.1 Table 例子 让我们用 Table 重新实现一次可根据姓名与学号进行搜索的成绩单:\n// 从左到右各列分别是: 学号, 姓名, 成绩 Table\u0026lt;String, String, Integer\u0026gt; idNameScoreTranscript = HashBasedTable.create(); idNameScoreTranscript.put(\u0026#34;0001\u0026#34;, \u0026#34;Linus\u0026#34;, 99); idNameScoreTranscript.put(\u0026#34;0002\u0026#34;, \u0026#34;Aaron\u0026#34;, 100); idNameScoreTranscript.put(\u0026#34;0001\u0026#34;, \u0026#34;RMS\u0026#34;, 98); idNameScoreTranscript.put(\u0026#34;0004\u0026#34;, \u0026#34;RMS\u0026#34;, 97); /// 返回结果: /// Linus: 99 /// RMS: 98 idNameScoreTranscript.row(\u0026#34;0001\u0026#34;); /// 返回结果: /// 0001: 98 /// 0004: 97 idNameScoreTranscript.column(\u0026#34;RMS\u0026#34;); 正常来说, 不会有两个学号一样的学生, 只是为了展示 Table 用法而这样造数据. row, column 函数可能让人比较迷惑, 这两个函数是怎么搜索的?\n其实很简单, row 是以第一个 key 来搜索, 而 column 以第二个 key 来搜索, 如图:\nFigure 2: row: 以第一个 key 来搜索\nFigure 3: column: 以第二个 key 来搜索\n5.2 Table 视图 一往常, Table 也提供了若干个视图:\nrowMap(), 把 Table\u0026lt;R, C, V\u0026gt; 看作一个 Map\u0026lt;R, Map\u0026lt;C, V\u0026gt;\u0026gt;, 同样的, rowKeySet()返回一个Set\u0026lt;R\u0026gt;. row(r) 返回一个非空的 Map\u0026lt;C, V\u0026gt; 的引用, 对返回的 Map 的修改也会反馈给持有该引用 Table. 类似地, column(c) 返回一个非空的 Map\u0026lt;R, V\u0026gt; 的引用, 对返回的 Map 的修改也会反馈给持有该引用 Table. cellSet() 把Table\u0026lt;R, C, V\u0026gt;看作一个 Table.Cell\u0026lt;R, C, V\u0026gt;, Cell 与 Map.Entry 十分类似, 只不过它有两个 key, 形式是 (r,c)=v, 而 Map.Entry 是 key = value. 5.3 Table 实现 Table 依旧提供了若干个实现, 列表如下:\nTable\u0026lt;R, C, V\u0026gt; 类似的 Map\u0026lt;R, Map\u0026lt;C, V\u0026gt;\u0026gt; HashBasedTable HashMap\u0026lt;R, HashMap\u0026lt;C, V\u0026gt;\u0026gt; TreeBasedTable TreeMap\u0026lt;R, TreeMap\u0026lt;C, V\u0026gt;\u0026gt; ImmutableTable ImmutableMap\u0026lt;R, ImmutableMap\u0026lt;C, V\u0026gt;\u0026gt; ArrayTable ImmutableMap\u0026lt;R, ImmutableMap\u0026lt;C, V\u0026gt;\u0026gt;, 特别的一个 6 ClassToInstanceMap 目前, 我们介绍过的 Map, 无论是原生 Jdk 的 Map, 抑或是 Guava 的 Map, key 都是同一个类型的.\n这是因为 Map 的签名是 Map\u0026lt;K,V\u0026gt;, 实例的时候, 只能实例成某具体一个类型的参数. 所谓凡事都有例外, 有没有支持 key 是不同类型的 map 呢? 自然是有的, Guava 的 ClassToInstanceMap 就可以支持多个类型的 key.\n为什么它可以实现多个类型的 key 呢? 因为 ClassToInstanceMap 的签名声明为 Map\u0026lt;Class\u0026lt;? extends B\u0026gt;, B\u0026gt;, 通过传入不同类型的 Class 对象, 实现类型不同的 =key=(如果你要说, 即使传入不同类型的 Class 对象, 它只有一个 Class, 没有实现多个不同类型的 key 阿! 你也可以这样理解, well, 咬文嚼字就没有什么意义了)\n6.1 ClassToInstanceMap 例子 ClassToInstanceMap\u0026lt;Number\u0026gt; numberDefault = MutableClassToInstanceMap.create(); numberDefault.put(Integer.class, 10); numberDefault.put(Long.class, 20L); // 编译失败 //numberDefault.put(String.class, \u0026#34;string\u0026#34;); Assert.assertEquals(Long.valueOf(20L), numberDefault.getInstance(Long.class)); 如果查看源码, 可以发现, ClassToInstanceMap\u0026lt;B\u0026gt; 只有一个类型参数 B:\npublic interface ClassToInstanceMap\u0026lt;B\u0026gt; extends Map\u0026lt;Class\u0026lt;? extends B\u0026gt;, B\u0026gt; 很明显的, 类型 B 限制了 key 与 value 的类型。\n对于 value 的限制, 就和常规的 map 一样; 而对于 key 而言, 泛型实例化时的参数类型只能是 B, 或者是 B 的子类, 例如: ClassToInstanceMap\u0026lt;Number\u0026gt;, 那么这个 map 的 key 类型必须是 Number 或 Number 的子类, 而传入的 Integer 和 Long 都是 Number 子类, 因此能编译通过。\n如果传入的是 String, 不符合声明, 编译就报错了.\n需要注意的是, 和 Map\u0026lt;Class, Object\u0026gt; 一样, 一个 ClassToInstanceMap 可以包含着是原始类型的 value, 而原始类型与它对应的包装类型并不是同一种类型, 不要混淆了哦\n6.2 ClassToInstanceMap 实现 ClassToInstanceMap 提供了两个实现:\nClassToInstanceMap 类似的 Map MutableClassToInstanceMap Map\u0026lt;Class, Object\u0026gt; ImmutableClassToInstanceMap ImmutableMap\u0026lt;Class,Object\u0026gt; 7 RangeSet 目前为止, 我们介绍过的新类型容器都是常见的 Map/Set/Table, 现在我们就来介绍一个表示区间的容器: RangeSet. 一个 RangeSet, 表示一个包含无连接的, 不为空的区间的集合, 例如包含一个整数区间的 RangeSet: {[1,5], [7,9)}.\n在 RangeSet 中, 区间是由类 Range 来表示的, 当把一个区间加入到一个可变的 RangeSet 时, 任何有交集的区间都会被合并, 为空的区间就会被忽略, 例如将区间 [3,5] 加入到已有的 RangeSet {[2,4]}, 就会被合并成 {[2,5]}, 这个也符合我们日常的生活经验.\n7.1 RangeSet 例子 让我们现在来看一下 RangeSet 的两个例子, 一个是整数的 RangeSet\nRangeSet\u0026lt;Integer\u0026gt; rangeSet = TreeRangeSet.create(); // {[2,4]} rangeSet.add(Range.closed(2, 4)); // {[2,5]} rangeSet.add(Range.closed(3, 5)); // {[1,10]} rangeSet.add(Range.closed(1, 10)); // 无连接上的区间: {[1,10], [11,15)} rangeSet.add(Range.closedOpen(11, 15)); // 连接上的区间; {[1,10], [11,20)} rangeSet.add(Range.closedOpen(15, 20)); // 空区间, 被忽略; {[1,10],[11,20)} rangeSet.add(Range.openClosed(0, 0)); // 分割区间 [1,10]; {[1,5],[10,10],[11,20)} rangeSet.remove(Range.open(5, 10)); 在上面的例子中, [2,4] 和 [3,5] 这两个区间有交集, 所以它们被自动合并到一起了, 而对于区间 [1,10] 和 [11,15), 10 相邻的整数就是 11, 但两个区间也没有合并起来, 因为它们没有相交, 如果想要他们合并起来, 可以手动调用 Range.canonical(DiscreteDomain), 即:\n// {[1,10]} rangeSet.add(Range.closed(1, 10).canonical(DiscreteDomain.integers())); // 连接上的区间: {[1,15)} rangeSet.add(Range.closedOpen(11, 15)); 另外一个例子是日期的 RangeSet:\nRangeSet\u0026lt;LocalDate\u0026gt; rangeSet = TreeRangeSet.create(); // {[2019-10-10, 2019-12-25]} rangeSet.add(Range.closed(LocalDate.parse(\u0026#34;2019-10-10\u0026#34;), LocalDate.parse(\u0026#34;2019-12-25\u0026#34;))); // {[2019-10-10, 2019-12-30)} rangeSet.add(Range.closedOpen(LocalDate.parse(\u0026#34;2019-12-24\u0026#34;), LocalDate.parse(\u0026#34;2019-12-30\u0026#34;))); Assert.assertTrue(rangeSet.contains(LocalDate.parse(\u0026#34;2019-10-20\u0026#34;))); // {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)} rangeSet.remove(Range.closed(LocalDate.parse(\u0026#34;2019-10-20\u0026#34;), LocalDate.parse(\u0026#34;2019-10-30\u0026#34;))); Assert.assertFalse(rangeSet.contains(LocalDate.parse(\u0026#34;2019-10-20\u0026#34;))); // [2019-11-10,2019-11-25] 在 `{[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}`的区间包围内 Assert.assertTrue(rangeSet.encloses(Range.closed(LocalDate.parse(\u0026#34;2019-11-11\u0026#34;), LocalDate.parse(\u0026#34;2019-11-20\u0026#34;)))); Assert.assertEquals(Range.closedOpen(LocalDate.parse(\u0026#34;2019-10-10\u0026#34;), LocalDate.parse(\u0026#34;2019-10-20\u0026#34;)), rangeSet.rangeContaining(LocalDate.parse(\u0026#34;2019-10-19\u0026#34;))); // {[2019-10-10, 2019-12-30)} Range\u0026lt;LocalDate\u0026gt; span = rangeSet.span(); Assert.assertEquals(LocalDate.parse(\u0026#34;2019-10-10\u0026#34;), span.lowerEndpoint()); Assert.assertEquals(LocalDate.parse(\u0026#34;2019-12-30\u0026#34;), span.upperEndpoint()); RangeSet 提供了若干个查询函数, 用法在上面的代码已经展示了, 查询函数列表:\ncontains(C): RangeSet 最基础的查询操作, 判断任意的元素是否在 RangeSet 内. rangeContaining(C): 与 contains(C) 类似, 判断任意的元素是否在 RangeSet 内, 如果在的话返回一个对应的区间, 否则返回空指针. 如上代码, 有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 而元素 2019-10-19 在区间 [2019-10-10, 2019-10-20) 内, 因此 rangeContaining(C) 函数返回的就是 [2019-10-10, 2019-10-20). encloses(Range\u0026lt;C\u0026gt;): 判断任意的区间是否在 RangeSet 的包围中. span: 返回一个最小区间, 包含 RangeSet 中的所有区间, 如有: RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, span 函数返回的区间就是 {[2019-10-10, 2019-12-30)}. 7.2 RangeSet 视图 依照惯例, RangeSet 也提供了若干个视图:\ncomplement(): 返回某个 RangeSet 的补集, 返回结果也是个 RangeSet, 如有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 那它的补集就是: RangeSet: {(-∞,2019-10-10), [2019-10-20,2019-10-30], [2019-12-30,+∞)}, 分别是三个区间: 负无穷到 2019-10-10, 2019-10-20 到 2019-10-30, 以及 2019-12-30 到正无穷. subRangeSet(Range\u0026lt;C\u0026gt;): 返回某个 RangeSet 相交的子区间, 如有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 取子区间 [2019-11-10,2019-11-20], 那么返回结果就是 {[2019-11-10, 2019-11-20]}; 如果取子区间 [2019-10-15, 2019-11-20], 那么返回结果就是 {[2019-10-10, 2019-10-20), (2019-10-30, 2019-11-20]} asRanges(): 把 RangeSet 当作一个 Set\u0026lt;Range\u0026lt;C\u0026gt;\u0026gt;, 如有 RangeSet: {[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}, 返回结果就是: Set({[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}) 7.3 RangeSet 实现 RangeSet 提供了两个实现:\nRangeSet 类似的 Set\u0026lt;Range\u0026gt; TreeRangeSet TreeSet\u0026lt;Range\u0026gt; ImmutableRangeSet ImmutableSet\u0026lt;Range\u0026gt; 8 RangeMap 既然能以区间集作为容器, 那么能否把区间当作 Map 的 key 呢? 答案是当然可以, Guava 就提供了一个这样的容器: RangeMap.\n需要注意的是, 不像 RangeSet 那样, 相邻或者相交的区间不能连接起来的, 即使毗邻的区间映射的是同一个 value.\n9 RangeMap 例子 RangeMap\u0026lt;Integer, String\u0026gt; rangeMap = TreeRangeMap.create(); // {[1,10] =\u0026gt; \u0026#34;foo\u0026#34;} rangeMap.put(Range.closed(1, 10), \u0026#34;foo\u0026#34;); // {[1, 3] =\u0026gt; \u0026#34;foo\u0026#34;, (3, 6) =\u0026gt; \u0026#34;bar\u0026#34;, [6, 10] =\u0026gt; \u0026#34;foo\u0026#34;} rangeMap.put(Range.open(3, 6), \u0026#34;bar\u0026#34;); // {[1, 3] =\u0026gt; \u0026#34;foo\u0026#34;, (3, 6) =\u0026gt; \u0026#34;bar\u0026#34;, [6, 10] =\u0026gt; \u0026#34;foo\u0026#34;, (10,20) =\u0026gt; \u0026#34;foo\u0026#34;} rangeMap.put(Range.open(10, 20), \u0026#34;foo\u0026#34;); // {[1, 3] =\u0026gt; \u0026#34;foo\u0026#34;, (3, 5) =\u0026gt; \u0026#34;bar\u0026#34;, (11, 20) =\u0026gt; \u0026#34;foo\u0026#34; rangeMap.remove(Range.closed(5, 11)); Assert.assertSame(\u0026#34;foo\u0026#34;, rangeMap.get(3)); Range\u0026lt;Integer\u0026gt; span = rangeMap.span(); Assert.assertEquals(span.lowerEndpoint(), Integer.valueOf(1)); Assert.assertEquals(span.upperEndpoint(), Integer.valueOf(20)); // {[12, 15]} =\u0026gt; \u0026#34;foo\u0026#34; RangeMap\u0026lt;Integer, String\u0026gt; subRangeMap = rangeMap.subRangeMap(Range.closed(12, 15)); Assert.assertEquals(subRangeMap.span().lowerEndpoint(), Integer.valueOf(12)); Assert.assertEquals(subRangeMap.span().upperEndpoint(), Integer.valueOf(15)); Assert.assertEquals(\u0026#34;foo\u0026#34;, subRangeMap.get(12)); RangeMap 提供的查询函数不多, 满打满算也只有 get(K) 和 span 这两个函数.\n9.1 RangeMap 视图 RangeMap 提供的视图也不多, 只有两个:\nasMapOfRanges(), 把 RangeMap 看作一个 Map\u0026lt;Range\u0026lt;K\u0026gt;, V\u0026gt;, 可以用来遍历 RangeMap subRangeMap(Range\u0026lt;K\u0026gt;), 返回某个 RangeMap 相关区间的子区间以及对应的 value, 如有 RangeMap: {[1, 3] =\u0026gt; \u0026quot;foo\u0026quot;, (3, 5) =\u0026gt; \u0026quot;bar\u0026quot;, (11, 20) =\u0026gt; \u0026quot;foo\u0026quot;, 取子区间 [12,15], 返回结果就是 {[12, 15]} =\u0026gt; \u0026quot;foo\u0026quot;; 如果取子区间[4,12], 返回结果就是: {[4,5) =\u0026gt; bar, (11, 12] =\u0026gt; foo} 9.2 RangeMap 实现 RangeMap 提供了两个实现:\nRangeMap 类似的 Map\u0026lt;Range, V\u0026gt; TreeRangeMap TreeMap\u0026lt;Range, V\u0026gt; ImmutableRangeMap ImmutableMap\u0026lt;Range\u0026lt;K, V\u0026gt;\u0026gt; 10 总结 介绍完新类型的容器之后, 希望大家对这些新类型容器熟悉起来, 应对需求来也能得心应手 :)\n","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E6%96%B0%E7%B1%BB%E5%9E%8B%E5%AE%B9%E5%99%A8/","summary":"1 前言 JDK 提供了各种功能强大的工具类, 宛如装备齐全的军火库, 而容器就是其中一项内置的利器, 提供了包括诸多常用的数据结构, 下图对 JDK 已有容器进行了","title":"guava 探究系列之五:新类型容器"},{"content":" 一本披着悬疑小说外衣的西方哲学史.\n1 前言 关于哲学, 一般人第一反应应该是那三个有名的哲学问题, 灵魂三问(配上相应的表情包的食用更佳):\n我是谁? 我来自哪里? 我在做什么?\n我像其他人一样, 对于哲学的概念就只有上面三个问题, 以及高中政治里面提到的各种\u0026quot;正确的\u0026quot;, \u0026ldquo;错误的\u0026quot;哲学概念.\n记得在大一的时候, 有诸多空闲时间, 当时就想找本科普读物, 了解哲学的概念.\n毕竟,计算机科学的尽头是物理, 物理的尽头是数学, 数学的尽头是哲学, 哲学的尽头是神学.(第一句是我自己加的), 正本清源一番也不错, 只是没有预料到的是, 这本书要4年之后才会看完.\n2 讲故事的高超技巧 个人觉得《苏菲的世界》 最让人称道的是说故事的方式, 全书前半部分的内容如果拍成电影, 那么应该是部惊悚电影.\n经常会出现艾伯特和苏菲聊着聊着哲学的时候, 出现一些不可思议的事情, 例如艾伯特的狗汉密士会开口说话, 诸如此类的事情.\n其次还有贯穿全书前半部分的疑团, 席德是谁? 她和苏菲有何关系? 她的少校父亲又是谁? 为何他会被艾伯特隐晦地称为上帝?\n相信其他初读此书的读者会像我一样, 落入到层层迷雾之中.\n而到全书过半, 到\u0026quot;柏客来\u0026quot;一章, 所有的谜团才被一一揭晓: 原来前面那么长篇幅描写的哲学家艾伯特和中学生苏菲都是虚构的, 只是席德的生日礼物中的角色.\n读到这一章的时候, 着实有种拍案叫绝的感觉, 为作者的构思所深深折服. 这让我想起了楚门的世界, 相信苏菲会和楚门有相同的感触; 原来自己的世界, 只是别人精心设计的剧本, 原来一切都是虚无, 如梦幻泡影.\n同时, 一个想法不可抵制地浮上水面: 如果苏菲只是席德讲义夹的一个人物, 席德也不过是手上的一本书的虚拟的中学生, 而我又会是谁操纵的人偶呢?\n这个想法有点可怕, 现在想来, 如果同样内容的故事, 如果以线性的述事方式进行平铺直述, 相信感染力会大打折扣, 而让书中人物和读者相互交错, 相互影响的述事方式, 想来也是作者的\u0026quot;诡计\u0026quot;之一, 让读者像哲学家一样思考自己存在的意义.\n3 哲学时间线 《苏菲的世界》以苏菲和艾伯特的经历为骨架, 串连起整个西方哲学史, 而粘连在骨架上的血肉就是一个个的哲学家和哲学理论, 把冰冷的哲学概念变成鲜活的体会, 寓教于乐中. 鉴于里面的内容繁多, 我也不对每个哲学家的理论感兴趣, 所以就把近三千年的重要哲学流派做成时间线, 耗费了近一下午的工夫才画完:\nFigure 1: 西方哲学史\n其中红色箭头指影响和传承, 黑色箭头指的是时间线顺序, 例如柏拉图生活的时期在苏格拉底之后, 其哲学理论又受到苏格拉底的学说的影响. 所以有一红一黑两个箭头从苏格拉底指向柏拉图.\n4 我看哲学 目前读过两本哲学书, 一本是《苏菲的世界》, 另外一本是高中政治教材《哲学生活》.\n看完《苏菲的世界》之后, 才发现, 哲学不会有明确的正确/错误之分, 不会有人轻易地给一种哲学下定论, 哲学是一种看待问题的方式, 指导我们如何寻求问题的答案.\n一个不持怀疑态度, 轻易对各种问题下定论的人, 既武断, 也殊为不智. 纵观整个西方哲学史, 我打算来谈谈我感兴趣的哲学家和哲学理论.\n4.1 圣奥古斯丁和圣多玛斯 引起我注意的是两位中世纪的哲学家(也可以称为神学家), 但是我感兴趣的不是他们的哲学, 而是他们阐述自己理论的手段和方式.\n众所周知, 中世纪也被称为黑暗时代, 在近千年的时间, 教会控制了所有的教育和思想, 神学大行其道, 上帝几近成为唯一的真理, 古希腊哲学毫无疑问地被基督教会钳制, 这个时候推行柏拉图和亚里斯多德哲学思想的难度可想而知.\n而圣奥古斯丁和圣多玛斯能成为中世纪最著名的哲学家, 自然有其独到之处, 而他们都做着一件类似的事情, 将古希腊的哲学思想进行包装, 进行基督教化, 为其披上宗教的外衣, 用基督教的\u0026quot;新瓶\u0026quot;装上古希腊的旧酒. 这种巧妙的传道方式不得不让人赞赏, 实乃大智慧.\n4.2 黑格尔 黑格尔用自己的智慧对历史和真理做了概述性总结, 就真理而言, 真理是主观的,他不承认在人类的理性之外有任何\u0026quot;真理\u0026quot;存在。他说,所有的知识都是人类的知识。思想(或理性)的历史就像河流。人的思考方式乃是受到宛如河水般向前推进的传统思潮与当时的物质条件的影响。\n因此你永远无法宣称任何一种思想永远是对的。只不过就你当时所置身之处而言,\n这种思想可能是正确的.\n老实说, 《苏菲的世界》全书提到不同时期, 不同类型的哲学, 但是很多哲学的观点我到现在都没有明白其意思, 也有很多看懂后, 并不赞同的观点, 而黑格尔的观点正好是我能理解, 又为之赞同的观点, 这是一种与时俱进, 闪烁着人类智慧光芒的见解.\n思想也不再会简单地被评判成正确/错误, 一切都需要与时代结合. 举例来说, 牛顿的三大定律在他的时代, 甚至之后的几百年都是正确的, 只是忽略了特定的情况, 而爱因斯坦的相对论把这个短板给弥补上了, 我觉得以黑格尔的历史观解释, 加上时代的限制, 则牛顿和爱因斯坦的观点也都是正确的.\n4.3 马克思 建国30年的时间都是以马克思主义哲学来建设的, 效果怎么样, 历史已有定论. 此外《苏菲的世界》的中文版(萧宝森译)中(就是我看的这版), 部分内容被中国政府(文化产业部)要求删除,如马克思部分结尾处的32个段落. 所以, 不言自明咯.\n5 名句 你觉得自己好像刚从一个梦幻中醒来. 我是谁? 你问道.\n我有时会有这种奇怪的想法, 很多时候是在读书或者写字, 突然对这些汉字感到陌生, 这些是什么? 为什么我在看这些图案(文字)?\n遗憾的是, 当我们成长时, 不仅习惯了有地心引力这回事, 同时也很快地习惯了世上的一切. 我们在成长的过程中,似乎失去了对世界的好奇心.\n事实的确如此. 因为我们都长大了.\n由于种种理由.大多数人都忙于日常生活的琐事,因此他们对于这世界的好奇心都受到压抑.\n像我现在这样天天加班,压抑的又何止好奇心呢?\n每一个生物都是理型世界中永恒范式的不完美复制品 \u0026ndash; 柏拉图\n而\u0026quot;真理\u0026quot;就是这个过程, 因为在这个历史的过程之外, 没有外在的标准可以判定什么是最真, 最合理的.\n你不能从古代, 中世纪, 文艺复兴时期或启蒙运动时期挑出一些思想, 然后说它们是对的, 或是错的. 同样的, 你也不能说柏拉图是错的, 亚理士多德是对的, 或者说休姆是错的, 而康德或谢林是对的, 这样的思考方式是反历史的.\n6 总结 最后, 借用我看到的一个比喻, 《苏菲的世界》就像一本哲学的请帖, 如果对请帖上说到的菜感兴趣的话, 就可以去找地方\u0026quot;试吃\u0026rdquo;, 如果你对《苏菲的世界》上提到的某道菜感兴趣, 想去试吃的话, 说明《苏菲的世界》, 这份请帖写得非常合格呢.\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E8%8B%8F%E8%8F%B2%E7%9A%84%E4%B8%96%E7%95%8C/","summary":"一本披着悬疑小说外衣的西方哲学史. 1 前言 关于哲学, 一般人第一反应应该是那三个有名的哲学问题, 灵魂三问(配上相应的表情包的食用更佳): 我是谁?","title":"小说体西方哲学史: 苏菲的世界"},{"content":"1 前言 以前没有读过王小波作品时候, 经常能看到王小波身上的各种标签, 中国第一代程序员, 现代伟大作家诸如此类.\n但是没有读过王小波的作品, 对于他的印象终究是道听途说, 所以抽空把王小波《沉默的大多数》这部作品看完.\n以前以为这作品批判的是大多数无主见, 只会冷眼旁观的麻木的芸芸众生. 然而真正读完这部作品之后, 才会发现, 根本不是这样的.\n2 韩寒《青春》 记得我在大学时候读过韩寒的一本杂文集, 名为《青春》, 当时记得是在二手书摊里低价买回来的, 当时只是想看一下, 这位颇具争议的\u0026quot;名作家\u0026quot;的作品究竟如何.\n后来看下去才发现, 原来\u0026quot;杂文集\u0026quot;是博客文章的集合, 韩寒的很多文章看起来都有意针砭时弊, 但是却总给人一种流于表面的感觉, 无处着力, 并没有对种种社会现象的背后深层原因进行分析.\n像他那些批评政府的博文, 阅读起来感觉很爽. 但是看完之后并没有对各种社会问题的背后原因有多少了解, 感觉就是避重就轻.\n3 历史与真相 《沉默的大多数》读下去之后, 也发现这部作品也是王小波各种专栏作品的集合, 但是不同于《青春》, 《沉默的大多数》确实给了我一种杂文的感觉, 毕竟我对杂文的印象还停留在鲁迅的文章中.\n与鲁迅先生的作品类似, 《沉默的大多数》也对诸多的社会现象进行了批评和讽刺, 只是王小波的行文风格不如鲁迅先生如刀枪那般硬朗, 王小波更多的是幽默, 更为内敛的讽刺. 例如王小波提到的优越感的时候, 说到:\n有些作品. 有些人能欣赏, 有些人则看不懂, 这就是说, 有些人的幸福能力较为优越. 这个优越最招人嫉妒.\n消除这种优越感的方法之一就是给聪明人头上一闷棍, 把他打笨. 但打轻了不管用, 打重了会把脑子打出来, 这又不是我们的本意. 另一种方法则是: 一旦聪明人和傻人发生了争执, 我们总说傻人有理, 久而久之, 聪明人也会变傻. 这种法子现在正用着呢.\n对, 这种方法现在也正用着呢. 诸如此类睿智又充满机锋的句子在文中比比皆是.\n作为历史的亲历者, 又作为历史的见证者, 像王小波这样坦白敢言的作者的确不多, 在说到他们上山下乡的时候, 明确说到这是一个大坏事, 好事就是好事, 坏事就是坏事, 不需要文过饰非, 这的确是难得的品格. 毕竟当全部人都在说1+1=3的时候, 你说出1+1=2是多么的难能可贵.\n毕竟大部分的君王都是中亚古国花剌子模的君王那般, 给君王带来好消息的信使, 就会得到提升, 给君王带来坏消息的人则会被送去喂老虎, 人都会趋利避害, 就没人敢说真话了, 现在都没人敢说真话了, 说真话的那个人都被海葬了.\n突然明白书名《沉默的大多数》的意思, 原来你不是在批判沉默的大多数, 而是你在写坚持做那少数人的历程, 正如你所说, 这是一个熵减的过程.\n4 国学 天不生仲尼, 万古长如夜. 高中的时候, 时常拿这个当作作文的点睛之笔, 即使和作文的主题不搭, 也可以拿来用, 无须顾忌.\n对于国学, 古人也是这般使用. 关于王小波对于孔子和孟子的评价, 我是持赞同态度的, 对于孔子强调的礼, 王小波认为和\u0026quot;文化革命\u0026quot;里搞的那些仪式差不多.\n什么早请求晚汇报, 他都经历过. 我也觉得, 这些礼对于人幸福的提高, 学识的增长没有什么帮助, 为的是区别阶级和权利, 无权者向有权者行礼, 这才是核心.\n这就好像我司用工牌带子的颜色来区别阶级一样, 本质都是权力和地位. 至于孟子, 王小波觉得孟子甚偏执, 表面上体面, 其实心底有股邪火, 比方说,他提到墨子,杨朱, \u0026ldquo;无君无父, 是禽兽也\u0026rdquo;.\n不同意见, 就觉得别人是禽兽, 这样的学术胸怀不甚宽广. 我最喜欢的是王小波关于这么多读书人把四书五经研究了两千年的比喻:\n二战时期, 有一位美国将军深入敌后, 不幸被敌人堵在了地窖里, 敌人在头上翻箱倒柜, 他的一位随行人员却咳嗽起来 . 将军给了随从一块口香糖让他嚼, 以此压制咳嗽, 但是该随从嚼了一会儿, 又伸手来要, 理由是: 这一块太没味道. 将军说: 没味道不奇怪, 我给你之前已经嚼了两个钟头了| 我举这个例子是要说明, 四书五经再好, 也不能几千年地念; 正如口香糖再好吃, 也不能抱着人地嚼\u0026hellip;\n过去钻石四书五经, 现在钻研《红楼梦》. 我承认, 我们晚生一辈在这方面差得很远, 但也未尝不是一件好事. 四书五经也好, 《红楼梦》也罢, 本来只是几本书, 却硬要把整个大千世界都塞在其中. 我相信世界不会因此得益, 而是因此受害.\n要把大千世界硬塞进的书何止是《红楼梦》, 还有某些主义书籍, 某些思想书籍. 以前的人以以前的四书五经治国, 现在的人以现在的四书五经治国, 历史并没有发生改变, 只是轮回了.\n5 尊严 《沉默的大多数》一书最后的篇幅都是用来讲述尊严的, 在我们的国家里, 言必称集体, 话必及党国, 平常很少提到个人, 中学政治书也曾经提到, 当个人利益与集体利益发生冲突时,诚信守则要求我们站在集体利益一边.\n而在王小波看来, 在国外人们对时事做出价值评判的时, 总是从两个独立的方面来进行: 一个方面是国家或者社尊严, 这像是时事的经线; 另一个方面是个人尊严, 这像时事的纬线.\n回到国内, 一条纬线就像是没有, 连尊严这个字眼也感到陌生了.\n这不禁让我想起美国梦和中国梦, 人民网甚至发文对比过美国梦和中国梦, 循例作了一番批判. 列出七大点, 例如中国梦是国家的富强, 美国梦是个人的富裕; 中国梦的目的是民族振兴,美国梦的目的是个人成功等等.\n给我的感觉是中国梦以集体为荣, 个人为耻一样, 然后国家不是一个虚无飘渺的实体, 是由诸多的基本单位组成的, 而这些基本单位就是由一个家庭和个体组成的, 如果个人不实现富裕, 国家的富强从何而来, 莫非是空中花园, 凭空而来?\n但国情如此, 由不得我们多言. 然而王小波早已懂得如何将个人尊严加塞于国家和政府尊严之中:\n作为一个知识分子, 我发现自己曾有一种特别的虚伪之处, 虽然一句话说不清, 但可以举些例子来说明. 假如我看到火车上特别挤, 就感慨一声道: 这种事居然发生中华人民共和国的土地上! 假如我看到厕所特脏, 又长叹一声: 唉! 北京市这是怎么搞的嘛! 我的确觉得国家和政府的尊严受到损失, 并为此焦虑着. 当然, 我自己也想要点个人尊严, 但以个人名义提出就过于直露. 不够体面\u0026ndash;言必称天下, 不以个人面目出现, 是知识分子的尊严所在.\n因为尊严是属于个人的, 不可压缩的空间, 这块空间要靠自己来捍卫\u0026ndash;捍卫指的意思是敢争, 敢打官司, 敢动手(勇斗歹徒). 我觉得人还是有点尊严的好, 假如个人连个待的地方都没有, 就无法为人做事, 更不要说做别人的典范.\n原来我们没有尊严的原因都是自找的, 并没有所谓的天赋人权, 有的只有自己争取而来的权利和尊严.\n6 总结 读完《沉默的大多数》之后, 对于王小波有了初步的认识, 他是个特立独行的人, 是个敢说话的人, 也是个有着时代印记的普通人,\n文章有种言有尽而意无穷的滋味. 更多的感受恐怕要看完他的代表作《黄金时代》之后才好继续评价, 就先这样了.\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E6%B2%89%E9%BB%98%E7%9A%84%E5%A4%A7%E5%A4%9A%E6%95%B0/","summary":"1 前言 以前没有读过王小波作品时候, 经常能看到王小波身上的各种标签, 中国第一代程序员, 现代伟大作家诸如此类. 但是没有读过王小波的作品, 对于他的","title":"沉默的大多数"},{"content":"1 前言 先此声明, 个人倾向于将Collection翻译成容器, 将Set翻译成集合.\n已经许久没有更新Guava研读系列的文章, 今天要介绍的是Guava的不可变容器.\n2 关于不可变对象 不可变的对象有许多的优点, 如下:\n线程安全, 可以在多线程之间使用也不用担心有竞争条件的风险 可以放心地用于不被信任的第三方类库 不用考虑支持可变性, 无需额外的空间和时间消耗. 可用作常量使用 使用对象的不可变拷贝是一项良好的编程防御策略, 为此, Guava提供了许多简单易用的, 实现了标准库Collection接口的不可变容器, 当然也包括实现了他们自家Collection接口的不可变容器.\n虽然通过JDK的静态方法Collection.unmodifiableXXX可以使用内置不可变容器, 但是在Guava团队的同学看来, 它们有若干的不足(又到了喜闻乐见的黑JDK的环节):\n笨重; 使用起来很笨重, 不够赏心悦目和优雅. 不安全; 上述静态方法返回的容器只有在没有对象持有原来容器的情况下才是真正不可变的. 例如, 当想要通过可变Map=ids=来生成一个不可变Map的时候,=Collections.unmodifiableMap(ids)=, 如果有多个对象持有ids时, 静态方法返回的对象就不是真正的不可变. 具体的分析可以参考StackOverFlow关于unmodifiableMap和ImmutableMap的讨论 低效; 静态方法生成的不可变容器和可变容器有着同样的性能开销, 包括并发修改, 动态扩容等(对于真正的不可变容器而言, 这些都是不会出现的操作) 综上所述, 如果你不想修改某个容器, 或者你想把某个容器当作不可变常量, 把这个容器变成一个不可变容器是一个很好的手段(使用Guava的不可变容器).\n此外, 在之前的文章中, 我阐述过Guava对于空指针的态度是尽量不要使用空指针, Guava的类库对于空指针都是快速失败的, Guava的不可变容器也是不例外的, 是拒绝接受空指针的.\n3 代码实例 前面详细介绍了不可变容器, 现在是时候来看一下Guava不可变容器的代码例子:\npublic static final ImmutableSet\u0026lt;String\u0026gt; COLOR_NAMES = ImmutableSet.of( \u0026#34;red\u0026#34;, \u0026#34;orange\u0026#34;, \u0026#34;purple\u0026#34;); class Foo { final ImmutableSet\u0026lt;Bar\u0026gt; bars; Foo(Set\u0026lt;Bar\u0026gt; bars) { this.bars = ImmutableSet.copyOf(bars); // defensive copy! } } 前文提到的, Collections.unmodifiableXXX(mutableXXX), Collections方法不能提供真正的不可变容器, 除非没有对象持有可变对象mutableXXX的引用\n那么Guava的不可变容器又是否是真正的不可变呢? 以ImmutableSet为例, 发现所有可以修改ImmutableSet对象的操作函数, 包括add, remove, addAll, removeAll等函数都被重载, 然后标注成@Deprecated, 重载函数的内容就是抛出UnsupportedOperationException异常, 所以不可能修改ImmutableSet对象的内容:\n/** * Guaranteed to throw an exception and leave the collection unmodified. * * @throws UnsupportedOperationException always * @deprecated Unsupported operation. */ @Deprecated @Override public final boolean add(E e) { throw new UnsupportedOperationException(); } 至于持有mutableXXX对象引用, 修改mutableXXX对象内容导致不可变内容发生改变的情况也不会发生:\n@Test public void testImmutable() { Set\u0026lt;String\u0026gt; colors = Sets.newHashSet(); colors.add(\u0026#34;blue\u0026#34;); Set\u0026lt;String\u0026gt; modifiableSet = Collections.unmodifiableSet(colors); Set\u0026lt;String\u0026gt; unmodifiableSet = Collections.unmodifiableSet(new HashSet\u0026lt;\u0026gt;(colors)); final ImmutableSet\u0026lt;String\u0026gt; COLOR_NAMES = ImmutableSet.copyOf(colors); colors.add(\u0026#34;yellow\u0026#34;); // 不会修改不可变集合的值 Assert.assertFalse(COLOR_NAMES.contains(\u0026#34;yellow\u0026#34;)); // 修改引用导致集合值发生修改 Assert.assertTrue(modifiableSet.contains(\u0026#34;yellow\u0026#34;)); // 因为没有对象持有new HashSet\u0026lt;\u0026gt;(colors)的引用, 所以unmodifiableSet是不可变集合, 不能修改 Assert.assertFalse(unmodifiableSet.contains(\u0026#34;yellow\u0026#34;)); Assert.assertTrue(colors.contains(\u0026#34;yellow\u0026#34;)); } 查看ImmutableSet.copyOf(Set\u0026lt;T\u0026gt;)函数的源码, 发现不可变集合的实现逻辑和在构造函数新建对象实现对象引用拷贝的逻辑一致, 即和Collections.unmodifiableSet(new HashSet\u0026lt;\u0026gt;(colors))的逻辑一样的:\npublic static \u0026lt;E\u0026gt; ImmutableSet\u0026lt;E\u0026gt; copyOf(Collection\u0026lt;? extends E\u0026gt; elements) { /* * TODO(lowasser): consider checking for ImmutableAsList here * TODO(lowasser): consider checking for Multiset here */ if (elements instanceof ImmutableSet \u0026amp;\u0026amp; !(elements instanceof ImmutableSortedSet)) { @SuppressWarnings(\u0026#34;unchecked\u0026#34;) // all supported methods are covariant // 新建对象, 拷贝对象引用 ImmutableSet\u0026lt;E\u0026gt; set = (ImmutableSet\u0026lt;E\u0026gt;) elements; if (!set.isPartialView()) { return set; } } else if (elements instanceof EnumSet) { return copyOfEnumSet((EnumSet) elements); } Object[] array = elements.toArray(); return construct(array.length, array); } 4 具体细节 下面我们来讨论一下各种不可变容器的具体使用细节.\n4.1 构造不可变容器 关于如何构造一个不可变容器, Guava提供的手段是多种多样的:\n使用copyOf静态方法, 例如ImmutableSet.copyOf(set), 这种构造方法与JDK不可变容器的构造方式类似Collections.unmodifiableXXX(mutableXXX) 使用of静态方法, 例如ImmutableSet.of(\u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;, \u0026quot;c\u0026quot;)或者ImmutableMap.of(\u0026quot;a\u0026quot;, 1, \u0026quot;b\u0026quot;, 2), 前文已经介绍过, 在此就不赘言 使用Builder构造不可变容器, 例如: public static final ImmutableSet\u0026lt;Color\u0026gt; GOOGLE_COLORS = ImmutableSet.\u0026lt;Color\u0026gt;builder() .addAll(WEBSAFE_COLORS) .add(new Color(0, 191, 255)) .build(); 不过某些不可变容器的builder方法废弃了, 如ImmutableSortedSet的builder方法就被替换成了naturalOrder.\n此外, 对于有序容器(sorted collections)而言, 容器内的元素的顺序是按照构造时元素的插入顺序排列的, 例如如下代码\nfinal ImmutableSet\u0026lt;String\u0026gt; alphaTable = ImmutableSet.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;, \u0026#34;b\u0026#34;); alphaTable.forEach(System.out::println); // 结果为 a b c d 4.2 asList函数 所有的不可变容器都提供了一个asList方法来返回一个不可变列表ImmutableList, 所以即使你把数据存在一个不可变有序集合ImmutableSortedSet, 你也可以通过下标索引获取最小的元素或者第n小的元素, 如:\nfinal ImmutableSet\u0026lt;Integer\u0026gt; numberSet = ImmutableSortedSet.\u0026lt;Integer\u0026gt;naturalOrder() .add(2, 3, 1) .add(4, 5, 6).build(); numberSet.asList().get(0) # 结果为1 4.3 智能的copyOf函数 前文提到, 不可变容器都提供了一个copyOf方法用于从另外一个容器构造出一个不可变容器. 值得指出的是不可变容器的copyOf方法在不需要拷贝数据的时候就会尽量避免拷贝数据, 但这是什么意思呢? 假如有如下的代码:\nImmutableSet\u0026lt;String\u0026gt; foobar = ImmutableSet.of(\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;, \u0026#34;baz\u0026#34;); thingamajig(foobar); void thingamajig(Collection\u0026lt;String\u0026gt; collection) { ImmutableList\u0026lt;String\u0026gt; defensiveCopy = ImmutableList.copyOf(collection); ... } 在上面的代码调用ImmutableList.copyOf(foobar)函数的时候, 函数的内部实现不会逐个拷贝, 而会直接通过foobar.asList()函数返回一个不可变值列表, 这样实现的算法时间复杂度就是O(1), 而不是O(n), 实现性能消耗的最小化, 这也就是小标题智能指的意思.\n但是需要注意的是, 并不是所有的不可变容器之间的转换都能实现O(1)时间复杂度, 例如ImmutableSet.copyOf(ImmutableList)就只能逐个元素拷贝, 时间复杂度退化到O(n).\n5 JDK容器与Guava不可变容器 对于JDK提供的标准容器, Guava提供了相应的不可变容器实现, 对于Guava自家的容器, Guava也提供了对应的不可变容器, 具体实现对比如下:\nInterface JDK or Guava? Immutable Version Collection JDK ImmutableCollection List JDK ImmutableList Set JDK ImmutableSet SortedSet=/=NavigableSet JDK ImmutableSortedSet Map JDK ImmutableMap SortedMap JDK ImmutableSortedMap Multiset Guava ImmutableMultiset SortedMultiset Guava ImmutableSortedMultiset Multimap Guava ImmutableMultimap ListMultimap Guava ImmutableListMultimap SetMultimap Guava ImmutableSetMultimap BiMap Guava ImmutableBiMap ClassToInstanceMap Guava ImmutableClassToInstanceMap Table Guava ImmutableTable 6 总结 因为不可变容器不会在运行时改变他们的内部状态, 所以他们是线程安全和无副作用的.\n因为这些属性, 不可变容器在多线程环境就会变得特别有用, 可以安全地传递数据. 总而言之, 生活和工作或许可以多拥抱变化, 对于代码, 最好还是多保持不变地好.\n7 参考 Immutable Collections ","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%AE%B9%E5%99%A8/","summary":"1 前言 先此声明, 个人倾向于将Collection翻译成容器, 将Set翻译成集合. 已经许久没有更新Guava研读系列的文章, 今天要介绍的是Gu","title":"guava探究系列之四:不可变容器"},{"content":"1 前言 Java 是一门集大成的面向对象语言, 在Java的世界里, 一切皆对象, 而Object类就是所有对象的默认父类. Guava 提供了若干个工具方法来扩展Object类的通用能力.\n2 equals 在Java的编程世界, 比较两个对象是个很常见的操作, Object类也提供了一个equals方法来判断对象是否相等. 但是Object使用的equals方法有诸多不便, 最痛苦的是无处不在的NullPointerException, 例如:\npublic testEqueal(Object input){ this.equals(input); } 但当 this指针指向一个空对象的时候, 就会出现null.testEqueal(input)的情形, 就会抛出NPE. 为了让equals方法更易用, Guava提供了一个Objects.equal(Object a, Object b)方法来判断两个对象是否相等. 用法如下:\nObjects.equal(\u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;); // returns true Objects.equal(null, \u0026#34;a\u0026#34;); // returns false Objects.equal(\u0026#34;a\u0026#34;, null); // returns false Objects.equal(null, null); // returns true 可能是Java语言的开发者也意识到Object.equals方法的不便, 所以在JDK7的时候, 官方也提供了Objects.equals(Object a, Object b)的方法, Guava的竞品自然也没了用武之处。\n不过, 说实话, 无论是JDK的Objects.equals, Object.equal还是Guava的Object.equal(), 在日常的开发中也用的不多, 用的最多的是Apache Common库的各种Utils工具, 比较String类型用的是StringUtils.equals(), 比较容器(Collection)用的CollectionUtils.isEqualCollection(), 毕竟这些工具要更高级和完善(有趣的是, JDK的方法名是equals, Guava的方法名是equal, 下文提到的JDK的hash方法名叫hash, Guava叫hashCode).\n2.1 hashCode 在《Effective Java》的条款9中说到:\nAlways override hashCode when you override equals\n就是说在你重写equals方法的时候, 记得重写hashCode方法, 因为按照Java的约定, 如果两个对象通过调用equals方法判断是相等的话, 它们调用hashCode()方法的返回结果也是一样的.\n《Effective Java》 给出的重写建议是把一个对象的所有字段进行计算取得一个hash值, 示例代码如下:\nprivate volatile int hashCode; public class User { private String name; private int age; private String passport; @Override public int hashCode() { int result = hashCode; if(result == 0){ result = 17; // Aribtrary number. result = 31 * result + name.hashCode(); result = 31 * result + age; // 31 is an odd prime result = 31 * result + passport.hashCode(); hashCode = result; } return hashCode; } } 这样的计算方式虽然有效, 但是未免过于烦琐, 还要手动计算每个字段. 为此, Guava 提供了一个 Objects.hashCode(field1, field2, ..., fieldn的方法, 用于对所有的字段计算hash值, 用法如下:\npublic class User { public int hashCode() { return Objects.hashCode(name, age, passport); } } 看起来简洁多了。然后在Java7的时候, JDK也推出了一个Objects.hash(field1, field2,...,fieldn)的方法, 而Guava的竞品很快就被废弃了. 我都在想JDK是不是在吸收Guava的精华, 毕竟实现都一样!( ̄▽ ̄)\n3 toString 《Effective Java》的条款10说到:\nAlways override toString\n也就是说, 《Effective Java》建议所有的类都重写toString()方法. 其实toString()方法不是给程序看的, 而是给开发者自己看的.\n据说, 好看的toString()方法的输出结果可以让程序员更愉悦, 可见颜值处处都有用. 比较常见的重写toString()的方式是把所有的字段拼接输出, 只不过手动拼接有点累.\n省心的是, Intellij Idea 为开发者提供了生成toString()的快捷方式, 如下图:\nFigure 1: Idea生成toString\n如果觉得Idea生成的toString()有太多的拼接字符串, 还可以试试Guava提供的toString()工具方法: MoreObjects.toStringHelper, 具体用法如下:\n@Test public void test() { System.out.println(MoreObjects.toStringHelper(this) .add(\u0026#34;name\u0026#34;, \u0026#34;Linus\u0026#34;) .toString()); System.out.println(MoreObjects.toStringHelper(\u0026#34;TestToStringHelper\u0026#34;) .add(\u0026#34;method\u0026#34;, \u0026#34;toStringHelper\u0026#34;) .toString()); } // 结果如下: // ToStringTest{name=Linus} // TestToStringHelper{method=toStringHelper} 使用方法也是很明了, 就不过多赘述.\n4 compare/compareTo 既然前面提到了《Effective Java》, 那么基于前后呼应的原则, 最后也免不了要再引用一下《Effective Java》:\n条款12: Consider implementing Comparable\n不像前文介绍过的方法, compareTo方法并不是Object类的方法, 而是Comparable接口的方法. 这个方法和前文提到的equals方法类似, 只不过用法不一样. compareTo通常用于排序, 如下面的代码就是对实现了Comparable接口的Person对象的列表进行排序:\nclass Person implements Comparable\u0026lt;Person\u0026gt; { private String lastName; private String firstName; private int zipCode; public int compareTo(Person other) { int cmp = lastName.compareTo(other.lastName); if (cmp != 0) { return cmp; } cmp = firstName.compareTo(other.firstName); if (cmp != 0) { return cmp; } return Integer.compare(zipCode, other.zipCode); } } public class CompareTest { @Test public void testSort(){ Person[] persons = new Person[2]; persons[0] = new Person(\u0026#34;Ma\u0026#34;, \u0026#34;Jack\u0026#34;, 12345); persons[1] = new Person(\u0026#34;Ma\u0026#34;, \u0026#34;Pony\u0026#34;, 65432); Arrays.sort(persons); Arrays.stream(persons).forEach(person -\u0026gt; System.out.println(person.getFirstName())); } } 上面的代码的逻辑就是先比较Person.lastName, 如果相等再比较Person.firstName, 如果前面的条件还是相等, 就再比较Person.zipCode.\n代码的含义相当清晰, 只是有不少的模板代码, 如果能减少这些模板代码, 那就更好了. 幸运的是, Guava 提供了一个 ComparisonChain来处理这些模板逻辑, 应用ComparisonChain之后的代码如下:\npublic class ComparisonChainPerson implements Comparable\u0026lt;ComparisonChainPerson\u0026gt; { private String lastName; private String firstName; private int zipCode; @Override public int compareTo(ComparisonChainPerson that) { return ComparisonChain.start() .compare(this.lastName, that.lastName) .compare(this.firstName, that.firstName) .compare(this.zipCode, that.zipCode) .result(); } } 确实简洁了许多~~\n5 总结 到本文为止, Guava提供的基本工具类就已经介绍完了,暂时告一段落了, 接下来就要介绍Guava最常用的工具之一: 各种容器(Collections)\n","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E7%B1%BB%E6%94%B9%E5%96%84%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95/","summary":"1 前言 Java 是一门集大成的面向对象语言, 在Java的世界里, 一切皆对象, 而Object类就是所有对象的默认父类. Guava 提供了若干个工具方法来扩展Ob","title":"Guava探究系列之三:改善通用方法"},{"content":"1 前言 记得高中讀各种经典名著的时候, 曾经蜻蜓点水般翻阅过《围城》一书. 白衣苍狗, 时过景迁, 再讀《围城》, 有了不一样的感受. 正如《围城》开篇所言:\n围在城里的人想逃出来, 城外的从想冲进去, 对婚姻也罢, 职业也罢, 人生的愿望大都如此.\n这实在是至理名言. 婚姻我尚且未曾有过经验, 但是对于职业却确有体会, 记得当初大二的我, 目标只是毕业后去网易写程序, 因为网易的食堂有名, 好吃.\n只是没想到当初找实习的时候, 不报什么希望的大阿里顺利通过面试, 而所倾心的网易连面试的机会都没有, 就这样, 我来了中国互联网的龙头公司-阿里巴巴.\n来了阿里之后, 对围城的认识就更加真实有体感. 面对996的福报, 很多人想要逃出来, 而外面却还有诸多\u0026quot;入福无门\u0026quot;的同行, 着实是人生何处不围城!\n2 一个残酷但真实的故事 你方鸿渐拿着前岳父的资助出国读书, 过着富二代的生活, 却一事无成买假文凭回国. 刚回国时, 被不知真相的众人仰慕与包容, 拒绝了爱耍小聪明苏小组的示爱, 欺骗了原是真爱的唐小姐.\n生活开始磨平你的棱角, 为了生活远走内陆任教, 又因无谓的高傲放弃留下来的机会, 甚至到后来每次的谋生都需要仰仗看不上当初的朋友.\n与心机的孙柔嘉结婚, 有了一段世俗的婚姻, 且又因为无谓的倔强和尊严搞得家不成家, 一地鸡毛, 甚至连当初爱慕你的女生都对你和你的妻子不屑一顾, 你的人生一直滑落, 直至谷底.\n你觉得你的人生是座围城, 可被困在围城里的又何止你一个人, 世俗之人都囿于这围城中, 我也在进围城的路上.\n3 一本关于中国人的书 记得大学时, 在宿舍附近的图书室的角落, 曾读过一本\u0026quot;经典名作\u0026quot;\u0026ndash;《丑陋的中国人》. 书中诸多演讲内容, 除了书名, 我已基本忘却, 只记得当初看得不以为然, 觉得这位作家强把全世界人的陋习安在中国人头上, 然后一顿狠批. 这本书应该叫《丑陋的人类》.\n重讀《围城》, 为文中对中国人的描述之准确而拍案叫绝, 而后又不禁深思, 中国人竟然是这样的?\n此外, 《围城》有许多有趣的情景, 只不过我没有疏理清楚, 与其刻意地寻找, 不如偶然地发现, 姑且想到哪, 就写到哪了.\n4 语言艺术 4.1 讽刺的语言艺术 年龄看上去有二十五六, 不过新派女人的年龄好比旧式女人合婚帖上的年庚, 需要考订学家所谓外证来断定真确性, 本身是看不出来的.\n原来 70 年前,女人的保养技术已经如此了得了(围城中很多话语, 女权主义者看完想锤钱老)\n做媒和做母亲是女人的两个基本欲望. 有鸡鸭的地方, 粪多; 有年轻女人的地方, 笑多.(女人和鸡鸭类比~~) 鸿渐暗笑女人真是天生的政治家, 她们俩背后彼此诽谤, 面子上这样多情, 两个政敌在香槟酒会上碰杯的一套工夫, 怕也不过如此. 她眼睛并不顶大, 可是灵活温柔, 反衬得许多女人的大眼睛只像政治家的大话,大而无当. 还有读了好几次才明白的句子, 着实让人看得无言以对, 原来输钱是要开心的: \u0026gt; 太太不忠实, 偷人, 丈夫做了乌龟, 买彩票准中头奖,赌钱准羸. 所以,他说,男人赌钱输了, 该引以自慰\n4.2 搭讪的语言艺术 开篇时的方鸿渐, 也是长袖善舞, 让诸多女人倾心, 从归国船上的鲍小组, 到方文纨, 再到唐晓芙. 想必与鸿渐的风趣诙谐不无关系, 搭讪的技术也是一流:\n你表姐说你朋友很多, 我不配高攀, 可是很想在你的朋友里凑个数 我想去吃, 对自己没有好借口, 借你们两位的名义,自己享受一下, 你就体贴下情, 答应了罢~ 5 中国人的人情世故 细读之后, 发现《围城》写的就是中国人的种种人情世故. 如果没有足够的人生阅历, 相信很难将中国人的人情世界写的如此生动, 跃然于纸上, 难怪钱老会被评价为极通世故.\n读完《围城》, 甚至可以根据其中的内容写一部中国人行为处世教程, 教你如何做个\u0026quot;合格的中国人\u0026quot;\n5.1 陆子瀟教你如何低调装 x 他(陆子瀟)亲戚曾经写给他一封信, 这左角印\u0026quot;行政院\u0026quot;的大信封上大书着\u0026quot;陆子瀟先生\u0026quot;, 就仿佛行政院都要让他正位居中似的.\n他写给外交部那位朋友的信, 信封虽然不大, 而上面开的地址\u0026quot;外交部欧美司\u0026quot;六字,笔酣墨饱, 字字端楷, 文盲在黑夜里也该一目了然. 这一封来函, 一封去信, 轮流地在他桌上装点着.\n一个装腔作势的人(装 x 犯)的形象跃然于纸上\n5.2 陆子瀟教你如何欲擒故纵 陆子瀟想向方鸿渐打探自己爱慕的孙小姐的信息, 不直接向鸿渐了解孙小姐的信息,反而整天阴阳怪气地说鸿渐和孙小姐关系很好, 时刻关注着孙小姐和鸿渐,又不愿直说:\n子瀟又尖刻地瞧鸿渐一眼道: \u0026ldquo;我以为你们(方与孙)经常见面\u0026rdquo;\u0026hellip;. 待鸿渐与孙小姐交谈离开后, 鸿渐刚回房, 陆子瀟就进来, 说:\u0026ldquo;咦, 我以为你跟孙小姐同吃晚饭去了. 怎么没有去?\u0026rdquo;\n当鸿渐看破了陆子瀟的爱慕之情之后,愿意给陆子瀟介绍孙小姐之后, 陆子瀟又不愿承认, 又怀疑鸿渐的动机:\n子瀟猜疑地细看鸿渐道: \u0026ldquo;你不是跟她很好么?夺人所爱, 我可不来. 人弃我取, 我更不来\u0026rdquo;\n诸如此类的描述, 《围城》还有很多, 就不必一一列举了.\n6 中国人的政治 所谓有人的地方就有江湖, 有江湖的地方就有纷争. 且来看看钱老笔下国人的江湖:\n6.1 所谓的民主 鸿渐学校仿照牛津,剑桥要搞个导师制, 老师与学生要在食堂一同进餐. 而人家牛津, 剑桥饭前饭后都会用拉丁文祝福, 本着学就要学彻底, 鸿渐的学校也要效仿祝福. 李梅亭绞尽脑汁想了个\u0026quot;一粥一饭, 要思来之不易\u0026quot;, 大家哗然大笑.\n儿女成群的经济系主任自言自语:\u0026ldquo;干脆大家像我儿子一样,念\u0026rsquo;吃饭前, 不要跑; 吃饭后, 不要跳'\u0026rdquo;\n高松年直对他眨白眼, 一壁严肃地说:\u0026ldquo;我觉得在坐下吃饭以前, 由训导长领导学生静默一分钟, 想想国家抗战时期的民生问题的艰难,我们吃饱了肚子应当怎样报效国家社会, 这也是很意思的举动. 经济系主任忙说:\u0026ldquo;我愿意把主席的话作为我的提议. \u0026ldquo;.\n李梅亭附议, 高松年付表决, 全体通过.\n这不禁让我想起了公司的 outing, 就是公司组织的\u0026quot;自费旅游\u0026rdquo;. 最开始大家民主投票, 提出自己想去的地方,然后集体投票, 大家选择了几个地方投票之后,老板给了建议, 然后大家全体决议, 就去老板建议的地方, 一如书中所述.\n6.2 请客吃饭 没想到, 请客吃饭, 还有如此多的讲究:\n请吃饭好比播种子; 来的客人里有几个是吃了不还请的, 例如最高上司和低级小职员;\n有几个一定还席的, 例如地址和收入相等的同僚,\n这样, 种一頓饭可以收获几顿饭.\n6.3 比较和虚荣 汪先生得意地长叹道: \u0026ldquo;这算得什么呢!我有点东西, 这一次全丢了.\n两位没看见我南京的房子\u0026ndash;房子总算没给日本人烧掉, 里面的收藏陈设都不知下落. 幸亏我是个达观的人,否则真要伤心死呢.\u0026rdquo;\n这类的话, 他们近来不但听熟, 并且自己也说惯了. 这次在兵灾当然使许多有钱, 有房子的人流落做穷光蛋, 同时也让不知多少的穷光蛋有机会追溯自己为过去的富翁.\n7 名句摘抄 自己太不成了, 撒了谎还要讲良心, 真是大傻瓜.\n事实上, 一个人的缺点正像猴子的尾巴, 猴子蹲在地面的时候, 尾巴是看不见的, 直到他向树上爬, 就把后部供大众瞻仰, 可是这红臀长尾巴本来就有, 并非地位爬高了的新标识.\n拥挤里的孤寂, 热闹里的凄凉, 使他像许多住在这孤岛上的人, 心灵也仿佛一个无凑畔的孤岛.\n我常说, 结婚不能太冒昧, 譬如这个人家里有没有住宅,就应该打听打听. (原来 70 年前, 结婚就需要有房子了)\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%9B%B4%E5%9F%8E/","summary":"1 前言 记得高中讀各种经典名著的时候, 曾经蜻蜓点水般翻阅过《围城》一书. 白衣苍狗, 时过景迁, 再讀《围城》, 有了不一样的感受. 正如《围城》开篇所","title":"人生何处不围城"},{"content":"1 前言 根据防御式编程的要求, 在日常的开发中, 总少不了对函数的各种入参做校验, 以便保证函数能按照预期的流程执行下去.\n比如各种费率的值就没可能是负数, 如果费率出现负数, 所以数据有问题, 我们需要做的事情就是把这些有问题的数据挑出来. 自己手写这些校验函数未免过于繁琐, 所幸的是我们需要的函数已经有现成的:\nGuava 提供了一系列的静态方法用于校验函数和类的构造器是否符合预期, 并称其为前置条件(preconditions). 如果前置条件校验失败, 就会抛出一个指定的异常.\n2 前置函数特征 目前的前置校验方法有如下特征: 须需要, 下面例子中的checkArgument函数可以替换成任何一个前置条件校验函数\n这些前置方法一般接受一个布尔表达式作为入参,并判断表达是否为true, 格式如: Preconditions.checkArgument(a\u0026gt;1) // 如果表达式为false, 抛出IllegalArgumentException 除了用于判断的布尔表达式之外, 前置方法可以接受一个额外的Object作为入参, 在抛出异常的时候, 把Object.toString()作为异常信息, 如: public enum ErrorDetail { SC_NOT_FOUND(\u0026#34;404\u0026#34;, \u0026#34;Resource could not be fount\u0026#34;); // 省略部分内容 @Override public String toString() { return \u0026#34;ErrorDetail{\u0026#34; + \u0026#34;code=\u0026#39;\u0026#34; + code + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, description=\u0026#39;\u0026#34; + description + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } @Test public void testCheckArgument() { Preconditions.checkArgument(1 \u0026gt; 2, ErrorDetail.SC_NOT_FOUND); } // 结果如下: // java.lang.IllegalArgumentException: ErrorDetail{code=\u0026#39;404\u0026#39;, description=\u0026#39;Resource could not be fount\u0026#39;} Guava的前置表达式还支持类似printf函数那样的格式化输出错误信息, 只不过出于兼容性和性能的考虑, 只支持使用%s指示符格式化字符串, 不支持其他类型. 如: int i=-1; checkArgument(i \u0026gt;= 0, \u0026#34;Argument was %s but expected nonnegative\u0026#34;, i); // 结果如下: // java.lang.IllegalArgumentException: Argument was -1 but expected nonnegative 3 前置条件函数介绍 须注意的是, 下面介绍的checkArgument, checkArgument, checkState函数都有三个对应的重载函数,分别对应前文所述的三种特征, 下文不会三种函数都介绍, 只介绍标准格式的前置条件函数. 以checkArgument函数为例, 三个重载函数分别是(忽略函数体):\npublic static void checkArgument(boolean expression); public static void checkArgument(boolean expression, @Nullable Object errorMessage); public static void checkArgument(boolean expression,@Nullable String errorMessageTemplate,@Nullable Object... errorMessageArgs) 3.1 checkArgument 函数的签名如下:\npublic static void checkArgument(boolean expression); 入参是一个布尔表达式, 函数校验这个表达式是否为true, 如果为false, 抛出IllegalArgumentException. 例子如下:\n@Test public void testCheckArgument() { Preconditions.checkArgument(1 \u0026gt; 2); } 3.2 checkNotNull 这是个泛型函数, 函数签名如下:\npublic static \u0026lt;T\u0026gt; T checkNotNull(T reference); 入参是个任意类型的对象, 函数校验这个对象是否为null, 如果为空, 抛出NullPointerException, 否则直接返回该对象, 所以checkNotNull的用法就比较有趣, 可以在调用setter方法前作前置校验. 例子如下:\nPreconditionTest caller = new PreconditionTest(); caller.setErrorDetail(Preconditions.checkNotNull(ErrorDetail.SC_INTERNAL_SERVER_ERROR)); 3.3 checkState 函数签名如下:\npublic static void checkState(boolean expression); 看着这个函数, 我个人感觉很奇怪: 这个函数和checkNotNull函数功能非常相似, 实现也基本一样, 都是判断表达式是否为true, 只是抛出的异常不一样而已, 是否有必要开发这个函数. 两个函数的实现如下:\npublic static void checkArgument(boolean expression) { if (!expression) { throw new IllegalArgumentException(); } } public static void checkState(boolean expression) { if (!expression) { throw new IllegalStateException(); } } 此外, 因为这两个函数相当类似, 就不展示相应例子了.\n3.4 checkElementIndex 函数签名如下:\npublic static int checkElementIndex(int index, int size); 这个函数用于判断指定数组, 列表, 字符串的下标是否越界, index是下标, size是数组, 列表或字符串的长度, 下标的有效范围是[0,数组长度) 即 0\u0026lt;=index\u0026lt;size. 如果数组下标越界(即index=\u0026lt;0 或者 =index=\u0026gt;==size), 那么抛出IndexOutOfBoundsException异常, 否则返回数组的下标, 也就是index. 例子如下:\nPreconditions.checkElementIndex(\u0026#34;test\u0026#34;.length(), \u0026#34;test\u0026#34;.length()); // 运行结果: // 抛出异常: java.lang.IndexOutOfBoundsException: index (4) must be less than size (4) Assert.assertEquals(3, Preconditions.checkElementIndex(\u0026#34;test\u0026#34;.length() - 1, \u0026#34;test\u0026#34;.length())); // 运行结果: // 通过 4 checkPositionIndex 函数的签名如下:\npublic static int checkPositionIndex(int index, int size); 这个函数和checkElementIndex非常类似, 连Guava wiki的说明也基本一致(只有一个单词不同).\n除了一点, checkElementIndex函数的下标有效范围是[0, 数组长度), 而checkPositionIndex函数的下标有有效范围是[0, 数组长度], 即0\u0026lt;=index\u0026lt;=size. 例子如下:\nPreconditions.checkPositionIndex(\u0026#34;test\u0026#34;.length() + 1, \u0026#34;test\u0026#34;.length()); // 运行结果: // 抛出异常: java.lang.IndexOutOfBoundsException: index (5) must be less than size (4) Assert.assertEquals(4, Preconditions.checkPositionIndex(\u0026#34;test\u0026#34;.length(), \u0026#34;test\u0026#34;.length())); // 运行结果: // 通过 4.1 checkPositionIndexes 函数的签名如下:\npublic static void checkPositionIndexes(int start, int end, int size); 这个函数是用于判断[start,end]这个范围是否是个有效范围, 即[start, end] 是否在[0, size] 范围内(如果[start, end] 和[0, size]相同, 也认为在范围内), 如果不在, 则抛出IndexOutOfBoundsException异常. 例子如下:\nPreconditions.checkPositionIndexes(1, 3, 2); // 运行结果: // 抛出异常: java.lang.IndexOutOfBoundsException: end index (3) must not be greater than size (2) Preconditions.checkPositionIndexes(0, 2, 2); // 运行结果: // 校验通过 5 前置条件在实际项目的应用 前置条件在检验条件不成交的时候抛的异常类型虽说是合情合理(比如, checkArgument函数抛出IllegalArgumentException),\n但是对于业务系统来说, 你抛出个IllegalArgumentException或者NullPointerException, 接口调用方对于这个异常摸不着头脑, 虽说只是正常的数据问题, 还是很容易觉得接口提供方服务出了问题, 甚至还会被质疑技术不过硬.\n咱们又不是底层组件, 抛个NPE, 着实是不成体统. 基于各种有的没的的原因, 我们的业务系统在使用前置条件的时候进行了封装, 将前置条件抛出的异常进行了转换, 换成正常的业务异常, 提供完整的异常信息, 代码如下:\n// 封装代码: public final class AssertUtils { /** * 检查条件表达式是否为真 * * @param expression 条件表达式 * @param errDetailEnum 错误码 * @param msgTemplate 错误消息模板 * @param vars 占位符对应变量 * @throws BkmpException 条件表达式结果为假 */ public static void checkArgument(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate, Object... vars) { try { Preconditions.checkArgument(expression); } catch (IllegalArgumentException e) { throw new BkmpException(errDetailEnum, msgTemplate, vars); } } /** * 检查条件表达式是否为假 * * @param expression 条件表达式 * @param errDetailEnum 错误码 * @param msgTemplate 错误消息模板 * @param vars 占位符对应变量 * @throws BkmpException 条件表达式结果为假 */ public static void checkArgumentNotTrue(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate, Object... vars) { try { Preconditions.checkArgument(!expression); } catch (IllegalArgumentException e) { throw new BkmpException(errDetailEnum, msgTemplate, vars); } } } // 省略其他部分的封装 // 调用例子: AssertUtils.checkArgument(merchantEntity.exist(), ErrDetailEnum.DATA_NOT_EXIT, \u0026#34;商户不存在\u0026#34;); 6 Guava Precondition vs Apache Common Validate 自古文无第一, 武无第二, 文人之间的口水战总是少不了的.\n没想到这不是国人的专利, 原来国外也有文人相轻的风气: Guava wiki 在介绍完preconditions之后, 还踩了一波竞品Apache Common Validate, 认为Guava的preconditions 比Apache Common 更加清晰明了, 也更加美观,\n我个人对Apache Common Validate 了解不深, 也不好随意置喙. 除了踩竞品之外, Guava wiki 还提了两点最佳实践(best practice):\n使用前置条件校验的时候, 推荐每个校验条件单独一行, 这样即更了然, 出问题也更方便调试. 使用前置条件校验的时候, 尽量提供有用的错误信息, 这样可以更快地定位问题. 7 总结 代码大全一书有一章是关于防御式编程的, 用于提高程序的健壮性, 主要思想是子程序应该不因传入错误数据而被破坏,要保护程序免遭非法输入数据的破坏.\n而Guava的preconditions 就是实现防御式编程的有力工具呢. oh yeah!\n8 参考 PreconditionsExplained ","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E4%BC%98%E9%9B%85%E6%A0%A1%E9%AA%8C%E6%95%B0%E6%8D%AE/","summary":"1 前言 根据防御式编程的要求, 在日常的开发中, 总少不了对函数的各种入参做校验, 以便保证函数能按照预期的流程执行下去. 比如各种费率的值就没可能是","title":"Guava探究系列之二: 优雅校验数据"},{"content":"1 前言 To be, or not to be, that is the question:\n先来看看奆佬们关于空指针的看法:\nNull sucks - Doug Lea(JCP,Java并发编程实战作者, Java巨佬)\nI call it my billion-dollar mistake. - Sir C. A. R. Hoare, 空指针的发明者\n按照Guava wiki的说法, 大部分的Google代码都是不支持使用空指针(下文用null表示空指针)的,\n如接近95%的集合类都不支持使用null作为集合元素. 像Google这样的大公司明确不建议使用null自然是有其原由的, 不会无的放矢. 那具体原因是什么呢?待下文为你细细道来;\n2 空指针的问题 2.1 空指针语意隐晦不明 null的语意并不了然明确, 即当一个函数返回null, 我们并不知道null的意思是指返回结果理应为空? 还是指函数没有达到预期结果, 返回null表示失败?\n举个常见的例子, 当调用Map.get(key)获取key对应的value的时候, 返回结果为null; null是指找不到这个key对应的value? 还是说这个key对应的value本身就是null, 原来是通过Map.put(key,value)赋值的呢? null甚至可以是代表其他东西!\n老实说, 当我们获得一个null, 我们并不清楚它究竟指的是啥, 除非有对应的javadoc 进行了说明.\n2.2 空指针”暗藏杀机” null除了语意不明外, 还非常容易在不经意间挖坑坑人. 例如有下面的代码:\nprivate String testNull(String input) { if (random.nextInt() % 2 == 0) { return input; } else { return null; } } @Test public void useNull() { String foo = testNull(\u0026#34;foo#bar\u0026#34;).split(\u0026#34;#\u0026#34;)[0]; String bar = testNull(\u0026#34;foo#bar\u0026#34;).split(\u0026#34;#\u0026#34;)[1]; } 可能你会说,这样的明显有坑的代码, 程序员理所当然会注意, 并对null指针进行校验的.\n但事实并非如此, 因为null是一个特殊类型, 它可以表示一切的类型, 所以上面的代码是肯定可以编译通过的. 没有了编译器的约束, 只要使用testNull函数的时候没有查看源码, 或者源码非常复杂, 一下子理不清思路, 防御式编程落实不到, 就会忽略了null, 运行时就有可能抛出NullPointException, 导致程序crash. 这种情况真的防不胜防.\n3 Guava对于空指针的态度 因为上文提到或者隐藏但没提到的种种问题, Guava的诸多类库在设计时就不支持null.\n如果检测到null的存在, Guava的类库就会快速失败(fail fast),一般的处理策略是抛出异常. 虽说null存在种种的坑, 但null依旧是Java的一项关键特性, 因此Guava的类库也不能将null彻底拒之门外.\n此外, Guava秉承既然不能消灭null, 那就把null建设得更好用的理念, 除了提供了一些工具可以让开发者避免使用null, 还提供了可以让开发者更易于使用null的工具.\n4 Optional 在很多情况下, 程序员使用null是为了表示有些值可能存在或者不存在. 我们又可以用熟悉的Map.get(key)函数来举例, 如果规定null不能作为value值使用(但事实并非如此), 那么当这个函数返回null时就代表没有找到这个key对应的value.\n为了应对这种使用null的情况, Guava团队参考其他语言(例如Scala)应对null的实践, 开发了Optional\u0026lt;T\u0026gt;类. Optional类表示那些可能为空的值, 一个Optional类要不包含一个非空的T类型的对象引用(这种情况下, 我们称引用对象是存在的-\u0026ldquo;present\u0026rdquo;), 要不什么东西都不包含(这种情况被, 我们说引用对象是不存在的-\u0026ldquo;absent\u0026rdquo;), 除此之外, Optional不存在其他情况, 更没有可能是null.\n4.1 Java8的Optional 鉴于我对Optional类的兴趣, 我用下面这条命令找了一个Guava库Optional开发的最初提交历史:\nfind guava/ -name \u0026#34;Optional.java\u0026#34; -print | xargs -I \u0026#39;{}\u0026#39; git log --pretty=tformat:%cd-%aN-%s --date=iso |tail -n2 # 结果如下 # 2009-09-15 19:50:59 +0000-kevinb@google.com-Initial code dump: version 9.09.15 # 2009-06-18 18:11:55 +0000-(no author)-Initial directory structure. 从Guava的commit历史中, 我们可以知道Optional最开始是在2009年开始开发的, 而10年前还是Java6的时代, Java7都尚未发布.\n在那个”远古年代”, 是Guava的Optional一直引领着Java的抗击null重任, 为众多的蒙受”空指针之苦”的Java的程序员带来希望之光.\n而当时光的脚步终于来到2014年3月18号, 在这一天, Java程序员迎来了Java8, 这是自Java5发布以来最激动人心的发布. 这天之后, 尘埃落定, Optional, Stream, Lambda等诸多令人期待已久的特性终于成为Java的标准库的一部分, 而这也意味, Guava的Optional已经完成了自己的使命, 成为历史.\nGuava的Optional类与JDK的Optional功能类似, 既然JDK的Optional已成为正统, 那么下面我就不再介绍Guava的Optional=(Guava的wiki本来是有较大篇幅介绍自家的=Optional, 个人感觉已经意义不大), 转而介绍JDK的Optional=(下文通称为=Optional).\n4.2 Optional构造方式 在使用Optional之前, 首先需要了解如果构造Optional对象, 方式有如下几种:\n4.2.1 声明一个空的Optional对象 可以通过静态工厂方法Optional.empty, 创建一个空的Optional对象:\nOptional\u0026lt;T\u0026gt; optional = Optional.empty(); 4.2.2 根据一个非空值创建Optional 还可以使用静态工厂方法Optional.of, 依据一个非空值创建一个Optional对象:\nOptional\u0026lt;T\u0026gt; optional = Optional.of(objectT); 需要注意的是, 按照Optional的源码声明, 如果传入的objectT为null, 那么Optional就会立刻抛出NullPointException=(这就是快速失败-fail fast), 而还是等到访问=optional属性时才返回一个错误.\n/** * Returns an {@code Optional} with the specified present non-null value. * * @param \u0026lt;T\u0026gt; the class of the value * @param value the value to be present, which must be non-null * @return an {@code Optional} with the value present * @throws NullPointerException if value is null */ public static \u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; of(T value) { return new Optional\u0026lt;\u0026gt;(value); } 4.2.3 可接受null的Optional 最后, 使用静态工厂方法Optional.ofNullable, 我们可以创建一个允许null的Optional的对象:\nOptional\u0026lt;T\u0026gt; optional = Optional.ofNullable(objectT); 如果objectT为null, 那么得到的Optional对象就是个空对象.\n4.3 Optional的消费方式 4.3.1 Optional与Stream的邂逅 既然Optional在Oracle的文档中被定性为一个容器(container),\n那么对于一个容器, 我们关注的点无非是这个容器如何存*(对于Optional来说是构造)和如何取*这两件事而已(也就是消费). 在谈Optional的消费接口之前, 先来回顾一下Java8引进的Stream操作(关于Java8 Stream操作的说明已经汗牛充栋了, 既然珠玉在前, 我就不赘言了), 常用的Stream操作函数有如下几个:\nfilter map flatmap peek reduce 更多的函数可以参考Oracle文档 因为前文已经说过Optional是容器类, 那么按理来说, 正常容器类支持的Stream操作, Optional也支持.\n只不过在Java8的时候, Optional只支持filter,=map=和flatmap这三个Stream操作.\n可能是因为Java委员会的奆佬们也觉得Optional身为一个容器类只支持三个Stream操作有点丢人, 所以在Java9, Optional增加了一个Optional.stream()这样一个可以返回Stream对象的函数, 让Optional拥有了容器类操作Stream的所有能力, 重振了身为一个容器的荣光. Optional与Stream结合使用的示例如下:\npublic String getCarInsuranceName(Optional\u0026lt;Person\u0026gt; person) { return person.flatMap(Person::getCar) .filter(car-\u0026gt;car.getName().equals(\u0026#34;Spaceship\u0026#34;)) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse(\u0026#34;Unknown\u0026#34;); 4.3.2 默认行为及解引用Optional对象 除了使用Stream来消费Optional对象, 还可以使用解引用读取Optional实例中的变量值以及定义默认行为, 具体函数说明如下:\nget()是这些方法中最简单但又最不安全的方法. 如果变量存在, 它直接返回封闭的变量值. 否则就抛出一个NoSuchElementException异常. 所以, 除非是非常确定Optional变量一定包含值, 否则使用这个函数就相当容易踩坑. 此外, 使用这个函数和直接进行null检查差别并不大. orElse(T other) 该函数允许在Optional对象不存在的时候提供一个默认值(也是我个人最常用的使用方式之一) orElseGet(Supplier\u0026lt;? extends T\u0026gt; other)是orElse函数的延迟调用版, Supplier方法只有在Optional对象不含值的时候才执行. 如果创建默认值是件耗时操作, 那么可以使用这种方式来提升性能, 又或者某个函数仅在Optional为空的时候才调用, 也可以使用这种方式 orElseThrow(Supplier\u0026lt;? extends X\u0026gt; exceptionSupplier) 和get方法非常类似, 这两个函数都会在Optional对象为空时, 抛出异常, 但差别在于orElseThrow可以指定抛出的异常类型 ifPresent(Consumer\u0026lt;? super T\u0026gt;)和orElseGet函数类似, 可以在变量存在的时候执行传入的函数, 否则就不进行任何操作. 4.3.3 Optional 实战示例 在啰啰嗦嗦介绍了一系列Optional的概念之后, 是时候来看一下Option的实例了. 现存的Java API几乎都是通过返回一个null的方式表示所需的值的缺失, 或者由于某些原因计算无法得到所需的值.\n在上文, 我们已经给null盖棺定论了, null是有坑的, 甚至是有害的, 所以要尽量少用null. 而现存的海量Java API都已经使用null作为返回结果, 我们没可能把这些API都重构成返回一个Optional对象的, 但眼看着Optional这样一个设计更完善无法在已有的Java API中使用未免令人心有不甘.\n现实中, 可能我们无法修改这些API的签名, 但是我们却可以很轻易地用Optional对象对这些API的返回值进行封装. 现在还是用熟悉的Map举例, 假设有一个Map\u0026lt;String, Object\u0026gt;的对象, 在查询key对应的value时, 如果value不存在, 那么调用Map.get(key)就会返回一个null:\nObject value = map.get(key); 现在, 每次使用value都需要进行空指针判断, 着实是太繁琐. 为了解决这个问题, 可以使用Optional.ofNullable函数进行优化:\nMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;); String value = Optional.ofNullable(map.get(\u0026#34;foo\u0026#34;)).map(Object::toString).orElse(\u0026#34;helloworld\u0026#34;); 这样, 每次使用value都不会再有NullPointException的忧虑.\n5 结语 本文最开始只是想阐述Guava类库使用空指针和避免使用空指针的设计理念, 只是因为Guava大部分类库都是不支持null, 因此使用Guava自家的Optional类来代替null的大部分应用场景, 而Guava自家的Optional无可避免地被JDK的Optional取代,\n所以本文大部份的内容也变成对JDK的Optional的探讨. 相信下篇文章会有所改观, 总不可能Guava所有的工具类, 都有JDK对应的竞品, 如果真是这样的话, JDK应该改名为GDK :)\n6 参考 Using and avoiding Java8 in Action Oracle java doc about Optional ","permalink":"https://ramsayleung.github.io/zh/post/2019/guava%E6%8E%A2%E7%A9%B6%E7%B3%BB%E5%88%97%E4%B9%8B%E4%BD%BF%E7%94%A8%E5%92%8C%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8%E7%A9%BA%E6%8C%87%E9%92%88/","summary":"1 前言 To be, or not to be, that is the question: 先来看看奆佬们关于空指针的看法: Null sucks - Doug Lea(JCP,Java并发编程实战作者, Java巨佬) I call it my billion-dollar mistake. - Sir C.","title":"Guava探究系列之一: 使用和避免使用空指针"},{"content":"1 前言 转眼间, 我已经工作一年了\n去年的6月28日, 我到了杭州, 入职了的一家全国闻名的金融科技公司, 开始了自己的职业生涯.\n2 关于工作 2.1 工作后 工作之后, 日常会回想起学校的生活. 工作对比学生生活, 最大的差别就是, 我再也不能随心所欲, 尤其在大学时候, 想上课就上课, 想出外旅行就出外旅行, 什么事情都不想做的时候, 还能躺在床上睡觉.\n而工作就是工作, 在公司,你需要做的就是不停地工作, 而9105已经是常态. 说好的弹性工作, 也可以通过要求早上9.15开晨会的形式来花式强制打卡, 员工除了被动接受, 也不会有其他的选择. 看来, 资本家果然是资本的人格化.\n2.2 拥抱变化 我的公司和我的国家一样, 也会有各种的价值观, 而”拥抱变化”就是其中一项. 其意思在网上已经流传甚广, 公司内部也各种解读, 总结来说就是有任何看起来不好的事情, 都可以用”拥抱变化”来概述.\n入职不到半年, 我便经历了一次”拥抱变化”: 原来的组被解散, 我被分流到新的组. 而入职一年后, 我又将要经历另外一次”拥抱变化”. 不禁感概, \u0026ldquo;拥抱变化\u0026quot;果然是我司的价值观.\n3 关于读书 入职以后, 有感自我提升的迫切性, 前后买了近两打书, 最近搬家之后, 房子没有书柜, 书全放床上, 占了三分之一的地方, 颇有种”著作等身”的感觉. 这两打书,既有计算机相关的, 也有非计算机相关的书籍,\n目前我大概读了三分之一, 也记录了相关的读书笔记心得. 目前在读的书名著是: 《苏菲的世界》,《围城》, 《枪炮、病菌与钢铁》, 几本书之间切换, 计算机的主要是 《Unix网络编程》.\n之前搬家时, 才发现我的书点了两个箱子, 把和我一起搬东西的新舍友累个半死; 虽说如此, 书仍旧会继续, 因为书中记录的知识和书的价格相比, 书的价格实在是便宜.\n4 关于生活 搬了一次家, 和朋友合租了间在7楼的房子, 现在住的地方离公司, 直线距离200M, 上班步行10分钟, 大部分时间是用在上下楼和等红灯. 因为现在住得近, 所以中午能和舍友一起自己做饭并在家午休.\n除了舍友是个素食主义者和公司把午休时间从12:00-14:00改为12:00-13:30之外, 生活上的其他事情还是挺不错的.\n入职半年之后, 可能是因为工作压力的缘故, 体重竟然比在校期间下降了5-6Kg, 而后可能是因为逐渐适应工作的强度, 体重渐渐恢复正常. 目前无肌肉, 无赘肉.\n感情生活: 为0, 按下不表\n5 其他小事 弃用Emacs, 转向Vscode, 因为工作事情繁多缘故, 没有时间与精力研究Emacs, 遂转投Vscode, 目前一切尚可, 暂无回归Emacs之意. 学习五笔, 已经能熟练使用, 本文即用五笔书写. 最开始原因是拼音重码率高(如ji,si,shi,zhi, 有太多同音字), 不容易记录文言文\u0026lt;\u0026gt;笔记, 且我使用的系统是Linux, Linux上的拼音并不好用, 于是用五一的三天假期学习五笔, 上手有难度, 但是熟悉后会比拼音好用, 熟悉费时约一个月. 勤勉一年, 绩效尚可, 无晋升提名. 6 Guava开坑序言 不甘心于碌碌无为, 每日只是搬砖, 工作一年后也没有什么值得引以为豪的地方, 所以需要决定开个大坑: 研读Guava代码, 并翻译其文档.\nGuava是Java程序员工具箱中的一把瑞士军刀, 与Apache common类库齐名, 这样有名的类库, 对于一个合格的Java程序员, 自然是不能不读. 但是我无意于单纯的翻译文章, 这样的文章已经太多了,\n我期待自己能结合实际的场景和源码解读Guava, 所谓”知其然知其所以然”. 这也是对自己的一个期许, 唯愿自己不要轻易放弃, 须知: 千淘万漉虽辛苦, 吹尽狂沙始到金.\n7 憧憬与展望 借用高适的一首诗以寄期望:\n千里黄云白日曛 北风吹雁雪纷纷 莫愁前路无知己 天下谁人不识君\n希望自己还能保持乐观~~\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%B7%A5%E4%BD%9C%E4%B8%80%E5%91%A8%E5%B9%B4%E8%AE%B0/","summary":"1 前言 转眼间, 我已经工作一年了 去年的6月28日, 我到了杭州, 入职了的一家全国闻名的金融科技公司, 开始了自己的职业生涯. 2 关于工作 2.1 工作后 工作","title":"工作一年记"},{"content":"浮生六记共有六卷,如今仅存四卷, 而\u0026quot;浮生\u0026quot;一名来自李太白的\u0026lt;春夜宴从弟桃花园序>中的名句\u0026quot;而浮生若梦,为欢几何\u0026quot;.\n主要是围绕作者沈复与夫人陈芸的闲情逸事, 如闺房之乐,诗酒之乐,游玩之乐等等.\n1 卷一 闺房记乐 卷一主要描写的是沈复与陈芸幼时相识,青年婚后为欢之事. 因为作者伉俪和读者我都是青年, 虽说相隔二百多年,也不免会有诸多相似之处,读来也会有许多有趣之事.\n卷一读起来最大的感受,以现在流行语说就是, 沈复和陈芸这俩小青年在疯狂\u0026quot;撒狗粮, 秀恩爱\u0026quot;,字里行眼,情深不禁跃然于纸上.\n卷一都是种种日常趣事,印象深刻, 读来令人忍俊不禁,我便挑几处与大家分享:\n1.1 深夜煮粥 沈复十三岁时,陈芸堂姐嫁人,沈复随母亲同去观礼,当日送嫁回城已经深夜了,沈复肚子饿了想找吃的,家中老妪给沈复做枣脯,沈复嫌太甜,然后淑姐(陈芸)把沈复悄悄带到自己的房间,拿出自己的热粥和小菜给沈复吃.\n这个时候陈芸堂兄芸衡在门外叫唤陈芸,让她快点出来. 陈芸答道,我太累了,要休息了.\n怎知玉衡推门而入, 发现我在吃粥. 就笑着对陈芸说,之前我问你还有没有粥,你说吃完了, 原来你是留给自己的丈夫呢.\n陈芸觉得非常害羞,大家都在哗笑. 我也闹小孩子脾气,和老妪先回家了.(原来两个小青年青梅竹马,早已暗生情愫)\n1.2 女着男装行庙会 离沈复家不远处,有一个名为洞庭君祠的庙,每逢神诞,热闹非凡,按照沈复自己的描述, 花光灯影,宝鼎香浮,若龙宫夜宴.\n这么热闹的场景,沈复自然回家对妻子讲述,陈芸叹息道,可惜我不是男子阿,去不了呢.\n沈复出了个主意, 你穿我的衣服,戴我的帽子,女扮男装就可以啦(这年青人真会玩).\n在庙中游玩的时候,都没有人认出陈芸是女子,旁人问沈复这是何人的时候,沈复说这是我表弟呢(真的皮).\n最后走到庙庆负责人家眷坐的地方的时候,陈芸上去攀谈,不自觉地把手放到了一位年青妇人的肩上, 然后旁边的侍女怒而起,斥骂道:\u0026ldquo;哪里来的狂徒呢,竟然在这里非礼人\u0026rdquo;.\n眼看局势就要控制不住,陈芸把帽子一脱,头发一甩(这个动作是我脑补的), 示意道, 我也是女子呢. 大家都惊呆了,而后转怒为欢(感觉陈芸真的会玩, 也率性)\n1.3 见湖生情, 与妓同饮 沈复父亲朋友病逝,沈复奉父命前去吊唁,途径太湖.\n而陈芸想要去见识一下太湖风光,就找了个借口和沈复一起乘船前去,见到太湖时直呼:\u0026ldquo;此太湖耶?今得见天地之宽, 不虚此生矣!想闺中人有终身不能见此者\u0026rdquo;(感觉有点心酸, 两百年前的女子竟可能终身不得见此景).\n归途中,沈复,陈芸与船家女素云共饮, 乘兴而来,大醉而归.\n后来沈复朋友的夫人对陈芸说,听说前几天你老公和两个妓女在船上饮酒,你知道不. 陈芸回答到,其中一个就是我呢,然后将事情始末告诉这位夫人.\n1.4 不知夭寿之机,此已伏矣 诸如此类趣事还有许多, 如陈芸为沈复纳妾之类的,而沈复和陈芸耳鬢相磨,亲如形影之情言语尚不能尽述,只觉得羡杀旁人(看两百多年前的年轻夫妻秀恩爱,感觉非常有趣)\n只是卷一回忆欢乐之趣事时,时常会有\u0026quot;不知夭寿之机,此已伏矣\u0026quot;,\u0026ldquo;真所谓乐极灾生, 亦是白头不终之兆\u0026quot;之类的话语,让人惴惴不安,知道佳人终究会陨落。\n正应了柳三变那句\u0026quot;应是良辰美景虚设,便纵有千种风情,更与何人说\u0026rdquo;, 不禁令人扼腕叹息.\n2 卷二 闲情记趣 这卷主要是作者平时闲情时的活动, 比如种花, 对对子之类.\n因为个人对这些山水盘栽, 吟诗作文这些文人墨客活动兴趣不大, 所以蜻蜓点水地阅读了这一卷. 这卷有一段名句, 出现在了中学课本上:\n余忆童稚时, 能张口对日, 明察秋毫. 见藐小微物. 必细察其纹理, 故时有物外之趣. 夏蚊成雷, 私拟作群鹤舞空. 心之所向, 则或千或百, 果然鹤也; 昂首观之, 项为之强. 又留蚊于素帐中, 徐喷以烟, 使其冲烟飞鸣, 青云白鹤观, 果如鹤唳云端, 怡然称快. 于土墙凹凸处, 花台小草丛杂处, 常蹲其身, 使与台齐, 定神细视: 以丛草为林, 以虫蚁为兽, 以土砾凸者为丘, 凹者为壑, 神游其中, 怡得自得. 记得当初上学的时候还背这篇文章, 情景还历历在目, 文章还记得大部分, 但是一切都已事是人非.\n3 卷三 坎坷记愁 这卷的内容从标题都可以看出来, 基本就是惨.\n按照作者的描述, 有如下的惨事:\n沈复父亲因为误会妻子识字, 却不愿意为沈复母亲写信, 导致父亲很生气(怒). 实际是母亲不让芸写信, 芸怕影响和母亲关系, 就没有向父亲辨解, 也阻止了沈复去辨解.(感觉沈复为自已的懦弱甩锅) 父亲想要找小老婆, 芸就为父亲寻小老婆, 被母亲知道后, 母亲也对芸不满了. 沈复弟弟启堂让嫂子作担保借钱, 然后被父亲发现了, 询问弟弟详情, 弟弟甩锅说什么都不知道. 然后父亲大怒(怒甚), 就要沈复赶妻子出门, 还骂了沈复一顿.(感觉这父亲也太激动了, 莫非是因为芸为他找小老婆的事情被大老婆知道了? 小儿子说不知道就是没有借錢?) 芸的弟弟去世了, 妈妈也去世了 芸的病情因为种种事情开始恶化. 沈复也友人借钱作担保, 然后友人就跑了, 沈复作为担保人, 理所当然地被追债. 在家的时候被上门追债, 妻子的友人华氏恰好也派人上门找芸. 沈复老爸又生气了, 以为这是憨园(妓女)的人, 把沈复又骂了一顿, 说芸不守妇道, 和妓女结交, balabala.(人家芸和憨园交往是在为你儿子找老婆啊) 经过这么多的破事之后, 芸决定去朋友华氏处养病. 跑路怕被追债的人发现, 只能未亮就走. 这时还有一对子女未曾安置好, 然后草率地把女青君嫁人作童养媳, 子逢森安置在沈复父亲家中. 到了朋友家中, 沈复想要做点事, 又没錢, 就打算去找堂姐夫追债, 一番波折之后, 只追回20两, 途中还差点没钱吃饭, 住店. 在芸身体好得差不多之后, 离开了朋友家, 去了沈复工作的地方, 华氏还送芸一个仆人, 名为阿双, 过了不久, 沈复就被裁员了. 沈复只好再去讨债, 最后讨得25两. 芸在沈复讨债回来之后, 病情急转直下, 最后说完一番遗言后, 溘然长往, 香消玉殒. 芸死后, 沈复扶棺归家, 能被弟弟忽悠去了扬州. 后来, 收到儿子的信: 父亲病重, 沈复还担心父亲还在生气, 不知道应不应该回去. 然后, 很快就收到父亲的死讯. 被弟弟排挤出家门, 没拿到一分遗产, 与儿子逢森吿别, 只身去四川. 半年后收到女儿来信, 儿子逢森去世. 卷一曾经透露,陈芸与沈复终难厮守, 但是这卷看来, 相守23年, 也堪算白头偕半老, 终究比纳兰容若与妻子卢氏相爱3年之后, 妻子因病香消玉殒来得要好.\n纳兰的悼念妻子卢氏的诸多诗词, 也不禁令人为之叹息. 沈复与陈芸相守半生, 着实不算乐极生悲. 纳兰的悼念词也真的是让人看得悲从中来:\n我是人间惆怅客, 知君何事泪纵横, 断肠声里忆平生。\n谁念西风独自凉?萧萧黄叶闭疏窗,沉思往事立残阳。 被酒莫惊春睡重,赌书消得泼茶香,当时只道是寻常。 (或者说这种事不能比惨)\n\u0026ldquo;曾经沧海难为水, 除却巫山不是云\u0026rdquo;\u0026ndash; 致陈芸.\n沈复对陈芸的确是位贴心丈夫, 但是却不是一位好丈夫, 因为他不能保护好自已的妻子, 更不能照顾好自己的子女, 逢森的早逝沈复绝对是有责任, 可真算是位\u0026quot;渣父\u0026quot;.\n4 卷四 浪游记快 顾名思义, 这卷的内容就是沈复浪浪浪, 各种游玩, 观赏.\n各种活动诸如: - 沈复15岁时, 去寻觅名妓苏小小之墓, 初时只是斗丘黄土, 在乾隆询问过之后, 苏小小之墓日渐隆重, 吊古骚人可轻易寻至.\n年幼的沈复感慨到: 余思古来烈魄忠魂湮没不传者,因不可胜数, 即传而不久者, 亦不为少; 小小一名妓耳, 自南齐至今, 尽人而知之, 此殆灵气所钟, 为湖山点缀耶? (我觉得是因为文人骚客都是男的, 喜欢名妓多于豪杰是合理的, 是生物进化的自然选择. 有李白的诗为证: 美酒樽中置千斛,载妓随波任去留)\n幼时从师在春和景明之际扫墓同游, 挖竹笋, 作羹汤, 游水洞, 不亦乐乎.\n少时与思斋先生共赴寒山登高,与知己鸿干四处闲玩, 更被人认为是风水先生, 是来找墓地的.\n凡所种种, 不一而足. 卷四中沈复游山玩水, 有时写的是山水, 有时写的还是山水, 只不过里头指的却是人世间。\n如登石镜山的时候, 有感于山中僻庵的小沙弥吃了肉馒头后拉肚子, 跟同仁说到:\u0026ldquo;作和尚者, 必居此等僻地, 终身不见不闻, 或可修养静. 若吾乡之虎丘山, 终日目所见者妖童艳妓,耳所听者弦索笙歌, 鼻所闻者佳肴美酒, 安得身如枯木, 心如死灰哉?\u0026rdquo; (我觉得嘛, 这种只是初级高僧的修炼方式, 抵抗诱惑的方式只是去个没有诱惑的地方. 真正经得起考验的高僧应该是经得住繁华, 耐得住寂寞, 所谓酒肉穿过, 佛祖心中留嘛)\n卷四中还有大段篇幅描写的是去我大广东嫖妓的事(文人墨客都这么骚的么?), 嫖妓的时候嫌弃本地妓女异服, 就喜欢扬帮妓女, 还把妓女带回住所,导致被人敲诈,要跑路, 后面嫖妓还嫖出了自豪感,觉得每个妓女都喜欢我. 原文如此说道:\n余则惟喜儿一人. 偶独往, 或小酌于平台,或清淡于寮内, 不令唱歌, 不强多饮, 温存体恤, 一艇怡然. 邻妓皆羡之. 有空闲无客者, 知余在寮, 必来相访. 合帮之妓无一不识. 每上其艇, 呼余声不绝. 余亦左顾右盼, 应接不暇,此虽挥霍万金所不能致者.(看来沈复嫖出了境界)\n后来沈复他爹不允许沈复再来广东, 沈复知道喜儿为他的离开伤心不已, 几寻短见, 沈复感慨到 \u0026ldquo;半年一觉扬帮梦, 赢得花船薄倖名\u0026rdquo;(我还是觉得他在炫耀)\n5 总结 仅存的四卷已经读完了, 最大的感受是沈复夫人陈芸的风采, 着实令人神往. 难怪林语堂先生会说芸是\u0026quot;中国文学中一个最可爱的女人\u0026quot;, 也诚非过誉.\n所以私以为卷一的闺房记乐才是全书的精华, 至于沈复的浪游记快和闲情记趣, 因为我个人对这些内容不甚感兴趣, 所以只是浮光掠影地过了一遍.\n最后, 既然是以李白的\u0026quot;而浮生如梦, 为欢几何\u0026quot;一诗开始的, 那就以这首诗结束吧:\n夫天地者万物之逆旅也;光阴者百代之过客也。 而浮生若梦,为欢几何? 古人秉烛夜游,良有以也。 况阳春召我以烟景,大块假我以文章。 会桃花之芳园,序天伦之乐事。 群季俊秀,皆为惠连;吾人咏歌,独惭康乐。 幽赏未已,高谈转清。 开琼筵以坐花,飞羽觞而醉月。 不有佳咏,何伸雅怀? 如诗不成,罚依金谷酒数。\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E6%B5%AE%E7%94%9F%E5%85%AD%E8%AE%B0/","summary":"浮生六记共有六卷,如今仅存四卷, 而\u0026quot;浮生\u0026quot;一名来自李太白的\u0026lt;春夜宴从弟桃花园序>中的名句\u0026quot;而浮生若梦,为","title":"浮生六记"},{"content":"1 前言 最近花时间,看完了狄更斯先生的名著<双城记>,除了那脍炙人口的开篇名句之外,我还看到了一些其它的内容, 待我细细述来\n2 说说作品本身的东西 老实说,在读复活篇和金线篇的时候,我是时常看到昏昏欲睡的,狄更斯先生用了 2/3的篇幅来描述马奈特医生一家和他们周遭的朋友与琐事,看着这些家常里短的描述,我是充满期待和疑惑的, 期待即将到来的转折,疑惑马奈特医生入狱的原因,期待法国大革命的上演,期待种种铺垫最后的爆发.\n只是我等待的时间未免过长,狄更斯先生的铺垫也着实太久,几次都看不下去,几欲放弃,直到看到法国大革命的爆发,书一切的暗线和铺垫才开始聚集起来, 在最后的一篇暴风雨的踪迹,将故事推向高潮. 我个人感觉最充满悬念和出人意料的地方有几处:\n卡顿先生在达尔再次入狱的时候,终于显露身影,和巴塞德这个密探博弈了起来,按照卡顿先生的说法,一副博命的牌局.\n已经稳居上风的卡顿先生说到巴塞德同伴是克莱时,自信的神色仿佛再次回到巴塞德这个密探, 这个密探甚至拿出了克莱的死亡证明说明卡顿先生说错了(当时很奇怪的是,为啥会把别人的死亡证明随身放身上),\n这时候进来的杰里先生跳起来说棺材中根本没有人,克莱是诈死,棺材中根本没有人,他就知道没有人.\n杰里先生的说明就让人想起之前有相当篇幅描述杰里先生表面是个跑腿的随从,背地是个掘墓偷尸的生意人的伏笔, 在这时候引爆之前的伏笔,不禁让人拍案叫绝\n卡顿先生在赢了巴塞德密探之后的要求,在达尔行刑前见他一面,联想起达尔和卡顿先生样貌想像的伏笔. 不看后面的篇幅,也能猜出卡顿先生的调包之计,他去为达尔去死,践行他之前对露西小姐的承诺\n最后行刑时,紧握卡顿先生手的那个姑娘,她的表妹是否是德日发夫人呢?\n行刺伯爵,把伯爵的头从伯爵府中取走的雅克,究竟是谁?\n最后终于明白,原著标题 A Tale of Two Cities 中的 Two Cities 指的是什么? 巴黎和伦敦\n其它在阅读是感叹,现在却想不起来的情节\n在看书的时候,很容易将自己代入到故事中去,把自己想像成书中的种种人物或者联想至其它人物.\n坦白说,我觉得卡顿先生死得其所,因为他前半生空有一身才华,但是却甘心堕落,成为狮子身边的胡狼,不想改变现状,但是对于卡顿先生为什么堕落却未有解释.\n卡顿先生不想成为死去没有人挂念和尊敬的人,不想孤独地死去,在失去(他也未曾争取过)露西小姐之后.\n卡顿先生就用另外一种方式让露西小姐永记自己,以牺牲自己的形式,最后,他也达成了自己的愿望, 用一种(肉体)死代替了另外一种(精神)死: 我(卡顿先生)知道,他俩(露西和达内)在对方心中深受尊重,视为神圣,可我在他们心目中,更受尊重,更为神圣.\n我并没有觉得德日发夫人的复仇有什么问题,父亲被逼害,姐姐被人侮辱并致死,姐夫被杀,哥哥被杀,向惨剧的始作俑者复仇,自然也是情理之中.\n只是最后我觉得需要谴责的只是想在向埃尔瑞蒙德家族复仇之后,祸及马奈特医生,露西小姐和小露西未免扩大仇恨,最后德日发夫人也为此付出了代价\n马奈特医生让我联想到了一位获得诺贝尔和平奖,却无缘领取荣誉 ,癌症病逝后还被海葬的作家.\n只是马奈特医生能在关押 10 多年后出狱,重新发挥影响力,而这位作家却没机会继续为国家和人民燃尽余辉.\n如果这位作家能活着出狱,并能见证国家的革命,他也会像马奈特先生那样,因为10多年的巴士底狱囹圉经历而备受尊敬.\n露西过于脸谱化,只有担忧, 关怀和爱,似乎没有其它情绪一样,只存在文学作品中的人物\n3 说说作品之外的东西 3.1 家有倔子,不败其家; 国有诤臣,不亡其国 按照作者在书中的序言所述,最开始作者是想写一个为了心爱的姑娘而甘为情敌牺牲的故事,只是到后来,脱离作者最初的设想,成为一部讽谏的作品.\n当时作者所处的年代,英国内外交困,整个欧洲风起云涌,作者忧心当初 60多年前法国大革命在英国重现,所以写下这部世界名著,表达自己担忧,劝谏并警告当时的当权者和广大的民众.\n历史总是相似的,古今中外从不缺位卑未敢忘忧国的仁人志士,狄更斯的作品让我想起了中国 100 年前,也有位先生以笔代药,针对当时世人的种种弊病下药.\n我有时候会有\u0026quot;奇思妙想\u0026quot;, 如果那位先生活在当下,会有怎样的反映,又会有怎样的待遇?如查尔斯.达内那般被革命群众投入牢狱? 还是如现在这般受人拥戴? 这真的是个奇怪的想法\n3.2 民主啊,多少罪名都是假你的名义干出来的 容我修改了书中提到的罗兰夫人的名言,自由和民主都是很容易被滥用的借口,时常蒙上不白之冤,为莫须有的罪名负责.\n书中关注革命群众的描述,非常容易让人产生联想,只不过是\u0026quot;公民/雅克\u0026quot;变成\u0026quot;同志\u0026quot;, 书中革命群众攻占巴士底监狱之后,导致的持续数年的乱象总给我一种熟悉的感觉,我似乎在什么地方见过这种场景,当然也是在书中的描述见到的.\n<亮剑>一书,原书有 2/3 的篇幅都是描述建国后的事情,其中那场从 1966 年5 月发起的,持续十年的运动,就占据一般的篇幅,<亮剑>中的红卫兵与<双城记>中的革命群众非常相似,差别仅在刀斧变成了枪炮,审判法庭变成了批斗会,失控的革命运动总是这般的相似,总是如此充满破坏力,以至于在拨乱反正之后,留下的伤口也是许久才能结痂.\n而那些失去生命的,去侍奉吉萝亭的\u0026quot;幸运儿\u0026quot;总是没有机会回来的,例如九月屠杀丧命的犯人,替代达内的卡顿,不一而足.而这些阿,都是自由(民主)你所背上的罪名啊.\n4 名句 书中那些令人过目难忘的语句:\n那是最美好的时代,那是最糟糕的时代; 那是个睿智的年月,那是个蒙味的年月;那是信心百倍的时期,那是疑虑重重的时期; 那是阳光普照的季节, 那是黑暗笼罩的季节; 那是充满希望的春天,那是让人绝望的冬天; 我们面前无所不有, 我们面前一无所有; 我们大家都在直升天堂, 我们大家都在直下地狱 耶稣说,复活在我,生命也在我. 信我的人,虽然死了,也必复活; 凡活着信我的人, 也必不死 自由啊,多少罪名都是假你的名义干出来的(我在本书知道这句名句,就姑且算该书的名句) 你替他去死么? 确如译者宋兆霖所说,狄更斯是为语言大师\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%8F%8C%E5%9F%8E%E8%AE%B0%E8%AF%BB%E5%90%8E%E6%9C%89%E6%84%9F/","summary":"1 前言 最近花时间,看完了狄更斯先生的名著<双城记>,除了那脍炙人口的开篇名句之外,我还看到了一些其它的内容, 待我细细述来 2 说说作品本身的东西","title":"双城记读后有感"},{"content":"浅谈Java公平锁与内存模型\n1 前言 春天来了,春招还会远么? 又到了春招的季节,随之而来的是各种的面试题。今天就看到组内大佬面试实习生的一道Java题目:\n编写一个程序,开启 3 个线程A,B,C,这三个线程的输出分别为 A、B、C,每个线程将自己的 输出在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如:ABCABCABC\u0026hellip;.\n2 经过 出于好奇的心态,我花了点时间来尝试解决这个问题, 主要的难点是让线程顺序地如何顺序地输出,线程之间如何交换。\n很快就按着思路写出了一个版本,用Lock 来控制线程的顺序,A,B,C线程依次启动,因为A线程先启动,所以A线程会最先拿到锁,B,C阻塞;但是A输出完字符串,释放锁,B 线程获得锁,C,A线程阻塞; 依此循环:\npublic void Test(){ private static Integer index = 0; Lock lock = new ReentrantLock(); @Test public void testLock(){ Thread threadA = work(i -\u0026gt; i % 3 == 0, () -\u0026gt; System.out.println(\u0026#34;A\u0026#34;)); Thread threadB = work(i -\u0026gt; i % 3 == 1, () -\u0026gt; System.out.println(\u0026#34;B\u0026#34;)); Thread threadC = work(i -\u0026gt; i % 3 == 2, () -\u0026gt; System.out.println(\u0026#34;C\u0026#34;)); threadA.start(); threadB.start(); threadC.start(); } private Thread work(Predicate\u0026lt;Integer\u0026gt; condition, Runnable function) { return new Thread(() -\u0026gt; { while (index \u0026lt; 30) { lock.lock(); if (condition.test(index)) { function.run(); index++; } lock.unlock(); } }); } } 输入结果如我预期那般,ABCABC交替输出,也成功输出了10次,奇怪的是A,B却多输出了一次? 为什么会多输出一次,不是应该恰好是输出30次么, 为什么会多输出一次A,B 真的百思不得其解. 所以我把index 也打印出来查看, 结果相当奇怪:\n... function.run(); System.out.println(index); .... 为什么A 会是30, B会是31, 不是有(index.intvalue\u0026lt;30) 的条件判断么, 为什么还会出现这样的数据?灵异事件? 3 解惑 灵异事件自然是不存在的,仔细分析了一番代码之后,发现了问题:\nwhile (index.intValue() \u0026lt; 30) { // 1 lock.lock(); // 2 if (condition.test(index.intValue())) { function.run(); index++; } lock.unlock(); } 将1,2行的操作做了这三件事,如下:\n线程读取index的值 比较index的值是否大于30 3. 如果小于30, 尝试获取锁 换言之,当index=29时,线程C持有锁,但是锁只能阻止线程A,线程B修改index的值,并不能阻止线程A,线程B在获取锁之前读取index的值,所以线程A读取index=29,并把值保持到线程的内部,如下图:\n当线程C执行完,还没释放锁的时候,线程A的index值为29;当线程C释放锁,线程A获取锁,进入同步块的时候,因为 Java内存模型有内存可见性的要求, 兼之Lock的实现类实现了内存可见,所以线程A的index值会变成30,\n这就解析了为什么线程A index=30的时候能跳过(index.intValue\u0026lt;30)的判断条件,因为执行这个判断条件的时候线程A index=29, 进入同步块之后变成了30:\n把问题剖析清楚之后,解决方案就呼之欲出了:\nwhile (index.intValue() \u0026lt; 30) { // 1 lock.lock(); // 2 if(index\u0026gt;=30){ continue; } if (condition.test(index.intValue())) { function.run(); index++; } lock.unlock(); } 这种解决方法不禁让我想起单例模式里面的双重校验:\npublic static Singleton getSingleton() { if (instance == null) { //Single Checked synchronized (Singleton.class) { if (instance == null) { //Double Checked instance = new Singleton(); } } } return instance ; } 只是当时并不清楚Double Checked的作用,究竟解决了什么问题?\n只是知道不加这条语句就会造成初始化多个示例,的确是需要知其然知其所以然.\n4 公平锁问题 前文说到,\n这个程序是用Lock 来控制线程的顺序,A,B,C线程依次启动,因为A线程先启动,所以A线程会最先拿到锁,B,C阻塞;\n但是A输出完字符串,释放锁,B 线程获得锁,C,A线程阻塞; 依此循环。\n粗看似乎没什么问题, 但是这里是存在着一个问题: 当线程A释放锁的时候,获取锁的是否一定是线程B, 而不是线程C, 线程C是否能够”插队”抢占锁?\n这个就涉及到了公平锁和非公平锁的定义了:\n公平锁: 线程C不能抢占,只能排队等待线程B 获取并释放锁\n非公平锁:线程C能抢占,抢到锁之后线程B只能继续等(有点惨!)\n而ReentrantLock默认恰好是非公平锁, 查看源码可知:\n/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } 因此为了规避非公平锁抢占的问题, 上述的代码在同步块增加了判断条件:\nif (condition.test(index.intValue())) { .... } 只有符合条件的线程才能进行操作,否则就是线程自旋.(但是加锁+自旋实现起来,效率不会太高效!)\n5 小结 写一条面试题的答案都写得是问题多多的,不禁令人沮丧,说明自己对Java的并发模型理解还有很大的提高。 不过在排查问题的过程中,通过实践有体感地理解了Java的内存模型,发现Java内存模型并不是那么地曲高和寡,在日常的开发中也是很常见的.\n费了一番工夫排查之后,终究是有新的收获的\n","permalink":"https://ramsayleung.github.io/zh/post/2019/%E4%B8%80%E6%9D%A1%E7%BB%8F%E5%85%B8%E9%9D%A2%E8%AF%95%E9%A2%98%E5%BC%95%E5%8F%91%E7%9A%84%E6%80%9D%E8%80%83/","summary":"浅谈Java公平锁与内存模型 1 前言 春天来了,春招还会远么? 又到了春招的季节,随之而来的是各种的面试题。今天就看到组内大佬面试实习生的一道Ja","title":"一条经典面试题的错误答案引发的思考"},{"content":"刷POJO类的变更行覆盖率\n1 反射大法好 1.1 背景 众所周知,蚂蚁对代码质量要求很高,质量红线其中一项指标就是变更行覆盖率。如果你的变更行覆盖率没有达到80%,测试同学是不会允许你上测试环境的(如果对此有所不满,测试同学就会过来捶你)。 为了提高代码质量,这项要求倒是无可厚非,变更的代码逻辑需要充分的测试;但是如果我新增了一堆的POJO类,只是为了逻辑模型,变更行也会变得非常可观。为了覆盖这些POJO类的变更,你免不了会测试一堆的Getter/Setter 方法:\nFigure 1: getter/setter\n(红色是指没有覆盖到的变更行)\n1.2 反射 如果为了变更行覆盖了,我要写上一堆的Getter/Setter 方法测试用例,测试用例也只是单纯调用一下方法,未免过于痛苦,能否偷个懒,解决覆盖率问题,也不需手写这些没啥用的测试用例.\n但是一时间没有想到解决方法,我就一边写这些没啥用的用例,一边思考,突然发现了规律:\npublic SomeType getXxxx(){} public void setXxxx(SomeType Xxx){} public SomeType getYyy(){} public void setYyyy(SomeType Yyyy){} 所有这些方法都是的前缀都是 set/get (真.废话),如果我能获取一个Pojo类所有的方法,然后循环执行所有以get/set开头的方法,不就不用手动写方法了么?\npublic class MerchantBusiModelTest { protected static final Logger LOGGER = LoggerFactory.getLogger(ModelUtils.class); /** * get类型方法的前缀 */ private static final String GET_METHOD_PREFIX = \u0026#34;get\u0026#34;; /** * set类型方法的前缀 */ private static final String SET_METHOD_PREFIX = \u0026#34;set\u0026#34;; MerchantBusiModel merchantBusiModel = new MerchantBusiModel(); @Test public void testModel() { Method[] methods = merchantBusiModel.getClass().getDeclaredMethods(); for (Method method : methods) { if (Modifier.isPublic(method.getModifiers()) \u0026amp;\u0026amp; method.getName().startsWith(GET_METHOD_PREFIX)) { Object[] parameters = new Object[method.getParameterCount()]; try { method.invoke(merchantBusiModel, parameters); LoggerUtil.warn(LOGGER, \u0026#34;调用方法, method: {}.{}\u0026#34;, merchantBusiModel.getClass().getSimpleName(), method.getName()); } catch (IllegalAccessException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, merchantBusiModel.getClass().getName(), method.getName()); } catch (InvocationTargetException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, merchantBusiModel.getClass().getName(), method.getName()); } } } } } 这样很快就把MerchantBusiModel所有的get方法执行了(set 方法也同理啦),调用结果如下:\n19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632162,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getMcc] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getOutMerchantId] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getMerchantName] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getMerhantType] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632256,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getDealType] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632257,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getAlias] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632257,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getLegalPerson] 19/03/14 10:46:32 WARN util.ModelUtils: (,N,20190314104632257,-,,,-,-,-,)[调用方法, method: MerchantBusiModel.getPrincipalCertType] 省略一大片类似的输出,省点篇幅 1.3 org.reflections.Reflections 通过反射,就很完美地解决了POJO类的变更行覆盖率问题了,反正POJO类都是Getter/Setter 方法,我的反射方法能把它们全覆盖上啦 (๑\u0026gt;◡\u0026lt;๑) .\n很快,我就遇到了另外的一个问题: 像MerchantBusiModel这样的Model类有十几二十个,难道每个Model我都需要写一个XxxModelTest的测试类么?也实在是太痛苦了,也太不优雅了(其实是我懒),能不能自动把所有的Model类扫出来,然后循环执行每个Model的Getter/Setter方法呢?\n因为这些Model都是继承一个统一的基类BaseBusiModel, 能否把这个基类的所有子类搞出来,这样就可以开心地用反射解决问题了.\n调研一番之后发现,Jdk 的反射方式不支持遍历父类所有子类的方法,这做法行不通阿!!!\n在我都几乎要放弃,要手写所有ModelTest的时候,我在StackOverFlow上面找到了 reflections 这第三方包,发现这个包非常强大(niubility), 可以获取基类的子类或者接口的实现类:\nReflections reflections = new Reflections(\u0026#34;my.project\u0026#34;); Set\u0026lt;Class\u0026lt;? extends SomeType\u0026gt;\u0026gt; subTypes = reflections.getSubTypesOf(SomeType.class); 简直了。在这”牛包”的帮助下,成功实现了扫描某个package下面所有基类的实现类的方法, 我的用例有救了:\npublic class ModelTest { protected static final Logger LOGGER = LoggerFactory.getLogger(ConvertorTest.class); private static final String PACKAGE_NAME = \u0026#34;xxx.xxx.core.service.v1.busimodel\u0026#34;; // model所有的包 @Test public void testModel() { Reflections reflections = new Reflections(PACKAGE_NAME); Set\u0026lt;Class\u0026lt;? extends BaseBusiModel\u0026gt;\u0026gt; classes = reflections.getSubTypesOf(BaseBusiModel.class); for (Class\u0026lt;? extends BaseBusiModel\u0026gt; clazz : classes) { if (Modifier.isAbstract(clazz.getModifiers())) { continue; } BaseBusiModel modelInstance = null; try { modelInstance = clazz.newInstance(); } catch (IllegalAccessException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法IllegalAccessException异常, clazz: {}\u0026#34;, e, clazz.getName()); } catch (InstantiationException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法InstantiationExceptionn异常, clazz: {}\u0026#34;, e, clazz.getName()); } ModelUtils.invokeGetAndSetMethod(modelInstance); } } } ModelUtils.invokeGetAndSetMethod(modelInstance); 这个静态方法就是上一节反射方法的完整可用版:\npublic class ModelUtils { protected static final Logger LOGGER = LoggerFactory.getLogger(ModelUtils.class); /** * get类型方法的前缀 */ private static final String GET_METHOD_PREFIX = \u0026#34;get\u0026#34;; /** * get类型方法的前缀 */ private static final String SET_METHOD_PREFIX = \u0026#34;set\u0026#34;; /** * 调用clazz 对象的所有get, set方法 * * @param clazz */ public static void invokeGetAndSetMethod(Object clazz) { invokeMethodWithPrefix(GET_METHOD_PREFIX, clazz); invokeMethodWithPrefix(SET_METHOD_PREFIX, clazz); } /** * 通过方法前缀调用方法 * * @param prefix * @param instance */ public static void invokeMethodWithPrefix(String prefix, Object instance) { Method[] methods = instance.getClass().getDeclaredMethods(); for (Method method : methods) { if (Modifier.isPublic(method.getModifiers()) \u0026amp;\u0026amp; method.getName().startsWith(prefix)) { Object[] parameters = new Object[method.getParameterCount()]; try { method.invoke(instance, parameters); } catch (IllegalAccessException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, instance.getClass().getName(), method.getName()); } catch (InvocationTargetException e) { LoggerUtil.warn(LOGGER, \u0026#34;调用方法异常, method: {}.{}\u0026#34;, e, instance.getClass().getName(), method.getName()); } } } } } 1.4 总结 Reflections 包是真的强,有空要去看一下源码 懒惰是程序员的第一生产力, 这话真不是我编的,是Perl 语言之父 Larry Wall 说的 加了其他两个类似功能的反射测试类,我的变更行覆盖率暴增30% (可以看出我这次的变更主要是新增模型和工具类,这样反射才能调用规律性代码) Java大法好,Java世界那么大,还需要我好好探索. ","permalink":"https://ramsayleung.github.io/zh/post/2019/%E5%A6%82%E4%BD%95%E5%88%B7pojo%E7%B1%BB%E7%9A%84%E5%8F%98%E6%9B%B4%E8%A1%8C%E8%A6%86%E7%9B%96%E7%8E%87/","summary":"刷POJO类的变更行覆盖率 1 反射大法好 1.1 背景 众所周知,蚂蚁对代码质量要求很高,质量红线其中一项指标就是变更行覆盖率。如果你的变更行覆盖率没有","title":"How to fool the Jacoco ◜◡‾"},{"content":"1 前言 最近读完了《追风筝的人》这部书,这部书给我一种熟悉的感觉,一种时代史诗的感觉。后来终于想起,这种时代史诗的感觉《霸王别姬》也有过\n2 兄弟情 《追风筝的人》前面有很大一部分都是在描写哈桑和阿米尔的活动,两个孩童每日的游戏,只有他们两个人,一个哈扎拉男孩和一个普什图男孩,亲密无间。通过阿米尔对孩童时和哈桑游戏的会议,即向读者展示了他们的感情,也披露 昔日的阿富汗是那般的安静祥和。他们之间最激动兴奋的活动就是斗风筝:用自己的风筝割断别人风筝线,最后幸存者获胜,追到割下的风筝会成为荣耀的战利品。而阿米尔是斗风筝的好手,哈桑是追风筝的好手,真的是注定的伙伴。\n3 突变 历史上存在无数的种族冲突,有的成为历史的一部分,有的成为生活的一部分,比如美国当初的黑人和白人,比如阿富汗的哈扎拉人和普什图人;甚至还有我无法理解的教派争端,如逊尼派和什叶派的冲突。正如哈桑和阿米尔的亲密,在自诩的 \u0026ldquo;卫道者\u0026quot;眼中既是原罪。正是这样的原罪,导致哈桑在阿米尔赢得喀布尔风筝大赛之后,奋力追着阿米尔的战利品的时候,\u0026ldquo;卫道者\u0026quot;阿塞夫侵犯了哈桑,目睹一切的阿米尔选择了避让,背叛了那个说\u0026quot;为你,千千万万遍\u0026quot;的男孩。这事成为阿米尔 以后的梦魇,也成为阿米尔和哈桑破裂的导火索。对不起一个人,心存愧疚,难道最好的方式是永远不见被自己所负之人?眼不见为净?\n孩童关系发生突变的时候,阿富汗国家形势也发生了突变:苏联入侵阿富汗,开始了对阿富汗16年之久的征伐。我自己也曾查阅资料,还是无法明白为什么苏联要入侵阿富汗,对我而言只是心存疑惑的事,对阿富汗人民来说,就是苦难的来源。 喀布尔曾经的风筝,歌谣,烤羊羔,如今变成了地雷,坦克,火箭炮,阿富汗的人民的遭遇,不禁让我想起当初的日本铁蹄下中国,真的是宁为太平犬,毋为乱世人\n篇中多次提到的羔羊意象,如影片《沉默的羔羊》那样,成为主角心中的梦魇。那羔羊被杀前的眼神,那迫在眉睫的厄运,是为了某个崇高的目的。而后的阿富汗人民都成为了羔羊,或许这是真主的安排的磨练,或许这样的想法能稍稍欢慰那些饱经磨难的心,或许\u0026hellip;\n4 赎罪之路 二人关系突变之后,哈桑和父亲离开了喀布尔,阿米尔和父亲也因为国家的突变离开了阿富汗,辗转之后去了美国。阿米尔若干年后在美国求学,娶妻,立业,而后父亲离世,时间慢慢向前走,平淡而甜蜜。直到阿拉辛的一通电话,将阿米尔拉回了那个 记忆中苦难的祖国,阿拉辛的话:\n来吧,这里有再次成为好人的路\n就这样阿米尔走上了赎罪之路,为自己对哈桑曾经的懦弱赎罪,也为父亲的欺骗赎罪-哈桑是父亲的私生子,父亲背叛了阿里。而后,\u0026ldquo;为你,千千万万遍\u0026quot;的哈桑,因为自己的忠诚付出了生命的代价, 后面就是阿米尔拯救哈桑儿子索拉博,拯救曾经的自己的事情了\n5 时代史诗 以前的阿富汗,只是新闻上的一处地名,经常伴随着爆炸和恐怖袭击。而《追风筝的人》让我意识到,这也是一片有血有肉,曾经有过欢声笑语的土地,而战争带走了一切,从苏联,到塔利班,再到美国,战火无情的肆虐着那片充满苦难的土地。\n《追风筝的人》给我的那种熟悉的感觉,《霸王别姬》也曾有过,写的是程蝶衣和段小楼两个戏子的故事,说的却是整个20世纪风云变幻的中国,从清朝到民国,日本人也来了,而后战争结束,但却不代表河清海晏,毕竟塔利班也带来过和平,也被当作过英雄。\n好的作品总是有共通之处的,总是能触动人心,正如最好的战争电影总是反战的,人文关怀总是可以引起共鸣,读过《追风筝的人》虽未令我潸然泪下,但也感人至深。篇末的阿米尔为索拉博追到风筝,也追到那个期许的自己,那条再次成为好人的路,他找到了.\n6 那些直达人心的句子 为你,千千万万遍(这不只是一份诺言) 记住,阿米尔少爷,没有鬼怪,只是个好日子(没想到,阿米尔少爷成为湖里的鬼怪) 没有良心,没有美德的人不会痛苦 当罪行导致善行,那就是真正的救赎 我们生活的喀布尔是个奇怪的地方,在那儿,有些事情比真相更重要 ","permalink":"https://ramsayleung.github.io/zh/post/2019/%E8%BF%BD%E9%A3%8E%E7%AD%9D%E7%9A%84%E4%BA%BA/","summary":"1 前言 最近读完了《追风筝的人》这部书,这部书给我一种熟悉的感觉,一种时代史诗的感觉。后来终于想起,这种时代史诗的感觉《霸王别姬》也有过 2 兄弟","title":"追风筝的人"},{"content":"一晃,2018年已经过去了\n6月25日,拖着行李,从广州来了杭州\n告别了学校,从学生变成了一个社会人\n既然选择了远方, 便只顾风雨兼程 \u0026ndash; 汪国真\n1 工作 从工作上来说,我”换”了两份工作,阿里大文娱和蚂蚁金服; 阿里大文娱-UC 2017.11-2018.5 实习,然后毕业之后入职蚂蚁金服-微贷-网商银行,主要是负责客户相关的业务;工作很累,但是总归是有收获的.\n入职蚂蚁之后,感觉就是忙,很忙。从新人培训的近卫军到回归日常业务,每天都有各种各样的事情需要处理,加班已经成为了工作中”不可磨灭的一部分”了\n刚入职的时候,给自己定了目标:业务上熟悉自己客户相关的业务,熟悉领域模型,继而从客户延伸了解整个网商银行的业务,学习金融知识;技术上学习组里的高可用架构,如何实现分布式系统的高可用,学习高并发-高可用-分布式-Java/蚂蚁中间件 生态;争取一年P6\n但是大半年下来,基本都是没有达到自己的预期目标,目测升P6的目标基本也是凉了。反思没有达到预期的原因;自身原因有之,外部原因亦有之.\n10,11月这两个月,组里的同事被拉去做各种项目,之剩下包括我在内的两个开发,面对一堆需求,资源最紧张的时候,我们每个人,每个迭代需要开发3个需求,然而一个迭代开发加自测只有一周多的时间,实在是忙。\n忙导致的副作用就是累,而后下班回家只想睡觉,每天学习一个小时的目标早已抛之脑外。每天被需求推着走,没有对需求后面的意义进行思考,只是简单的需求翻译器,并不会有多少成长,兼之对需求不了解,导致需求发现变更的时候手忙脚乱.\n12月之后需求缓下来之后,就开始有时间对之前做的事做个总结,可以对之前完成需求时积下来的问题进行反刍,结合现有的模型进行理解,过程虽费时,总归有收获;现有的业务开发开始渐入佳境,然后就开始”拥抱变化”,客户的业务全部交接别的团队,客户的团队被分流到其他团队,负责别的业务。\n以前总是听说阿里的”拥抱变化”,没想到来得如此之快,这么快就有了体感。\n2 生活 2017.12.31-2018.12.31, 单身, 按下不表\n2018.6, 从UC离职之后,趁着还有些许学生时光,就和两个好友去了趟顺德,品尝一下顺德的美食,所谓食在广州, 厨出凤城,广州生活了四年,是时候去尝尝凤城(顺德)的滋味。\n4天的微游,终究是不虚此行,在蝦炳海鲜吃到了最好吃的烧鹅,每天去公寓对面的茶楼喝早茶,去了清晖园游园,也去了民信老店尝了各式甜品(感觉民信真的不咋地,贵且不说,味道还不咋地,不值特意来)\n2018.8, 团队outing去了趟庐山(基本自费),庐山果真是个避暑圣地,把穿着短袖短裤,并只带了短袖短裤的这个广东人冻成dog。\n不过无论如何,庐山还是不虚此行的\n2018.10, 害怕挂了,开始重新踢球当作运动.\n开始只有十分钟体能,全程只能散步,真的是丢人。过了两个多月,体能提升到了三十分钟,优秀\n2018.12, 毕业半年, 轻了十斤左右。看来工作真的是烧脑,占体重8%的器官,消费了超过20%的能量\n3 读书 工作之后,买了两打书,分类大概是计算机相关/非计算书籍=3/1,然而过去近四个月,也只是读了不到三分之一的书,快餐文化盛行的今天,看来很难沉下心看书(不要甩锅呢)\n计算机相关: 重温了《Effective Java》, 《java并发编程实战》, 《深入理解Java虚拟机》和《Java8 in action》《CSAPP》(没读完),新入了《C++primer》和 《UNIX环境高级编程》(没读完)\n非计算书籍大概读了《活着》,《追风筝的人》\n饭仍是要继续吃,书也是要继续读的.\n4 美食 来了杭州之后,只做过一次饭,做给舍友吃, 幸好舍友还是吃得挺开心的 :)\n而后再也没有做过。 看了三部美食纪录片聊以自慰 《人生一串》,《人间风味》,《寻味顺德》。\n再次感谢《人生一串》,为广东人洗白,看来广东人吃得真的很正常,一点也不重口。\n嗯,在杭州想念广东的味道了,想念不吃辣的味道,想家了.\n5 展望 一年P6(感觉没戏了,那就两年P6) 了解分布式, 高可用的知识,争取通过实战掌握; 读完《netty in action》, 通过许家纯大大的教程,自己实现一个Rpc 框架;读Sofa-rpc 和 Netty 的源码 成为一个掌握金融知识的计算机从业人员 读完20本书 结束单身狗的生活(估计也没戏了) ","permalink":"https://ramsayleung.github.io/zh/post/2019/2018%E6%80%BB%E7%BB%93/","summary":"一晃,2018年已经过去了 6月25日,拖着行李,从广州来了杭州 告别了学校,从学生变成了一个社会人 既然选择了远方, 便只顾风雨兼程 \u0026ndash; 汪国真 1 工作","title":"迟来的2018年总结"},{"content":"Maven 在工作中的经验以及《Maven 实战》读后感\n1 前言 蚂蚁金服的伯岩大大曾经说 Java 生态都太重量级,连Maven 都是怪兽级的构建工具,需要整整一本书来讲解. 平心而论,Maven 的确\u0008如此, 但是无论是怪兽级,还是迪迦级的工具,只要能把事情做好了就是好工具, 而 Maven 恰恰就是这样的工具\n2 配置文件 2.1 pom.xml 就好像 Unix 平台的 Make 对应的 MakeFile,Cmake对应的 CmakeFile.txt, Maven 项目的核心是 pom.xml, POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖等等,可以 pom.xml 是 Maven 一切实践的基础\n3 依赖管理 3.1 坐标 Maven 仓库中有成千上万个构件(jar,war 等文件),Maven 如何精确地找到用户所需的构件呢,用的就是坐标。说起坐标,可能第一反映是平面几何中的 x,y坐标,通过 x,y坐标来唯一确认平面中的一个点,而Maven 的坐标就是用来唯一标识一个构件。\nMaven 通过坐标为构件引入了秩序,任何一个构件都需要明确定义自己的坐标,而坐标是由以下元素组成:groupId, artifactId, version, packaging, classifier, scope, exclusions等。一个典型的Maven 坐标:\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-beans\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 坐标元素详解:\ngroupId(必填): 定义当前Maven 项目隶属的实际项目, 一般是域名的方向定义 artifactId(必填): 定义实际项目中的一个Maven 项目,推荐的做法是使用实际项目名称作为 artifactId 的前缀, 比如上例的 artifactId 是 spring-beans,使用了实际项目名 spring 作为前缀 version(必填): 定义了Maven 项目当前所处的版本,如上例版本是 1.2.6 packaging(选填): 定义了Maven 项目的打包方式。打包方式和所生成的构建的文件扩展名对应,如果上例增加了\u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt;元素,最终的文件名为spring-beans-1.2.6.jar(Maven 打包方式默认是 jar),如果是 web 构件,打包方式就是 war,生成的构件将会以.war 结尾 classifier: 用来帮助定义构建输出的一些附属构件. 附属构建和主构件对应,如上例的主构件是spring-beans-1.2.6.jar, 这个项目还会通过使用一些插件生成`=spring-beans-1.2.6-doc.jar=, spring-beans-1.2.6-source.jar, 其中包含文档和源码 exclusions: 用来排除依赖 scope: 定义了依赖范围,例如 junit 常见的scope 就是\u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt;, 表示这个依赖只对测试生效 3.2 依赖范围 上文提到,JUnit 依赖的测试范围是test,测试范围用元素scope 表示。首先需要知道,Maven 在编译项目主代码的时候需要使用一套classpath,上例在编译项目主代码的时候就会用到spring-beans,该文件以依赖的方式呗引入到classpath 中。\n其次,Maven 在执行测试时候会使用另外一套 classpath。如上文提到的 JUnit 就是以依赖的方式引入到测试使用的 classpath,需注意的是这里的依赖范围是test. 最后,项目在运行的时候,又会使用另外一套的 classpath,上例的spring-beans就是在该classpath里,而JUnit 则不需要。\n简而言之,依赖范围就是用来控制依赖与这是那种 classpath (编译classpath, 测试 classpath, 运行 classpath 的关系,Maven 有以下几种依赖范围:\ncompile: 编译依赖范围,如果没有显式指定scope, 那么compile就是默认依赖范围,使用此依赖范围的Maven 依赖,对于编译,测试,运行三种 classpath 都是有效的 test: 测试依赖范围,指定了该范围的依赖,只对测试 classpath 有效,在编译或者运行项目的时候,无法使用该依赖;典型例子就是 JUnit provided: 已提供依赖范围。使用此依赖范围的 Maven 依赖,对于编译和测试classpath 有效,但在运行时无效 runtime:运行时依赖范围。使用此依赖范围的 Maven 依赖,对于测试和运行的classpath 有效,但在编译主代码时无效 import: 导入依赖范围,该依赖范围不会对三种 classpath 产生实际的影响 system: 系统依赖方位。与 provided 依赖范围完全一致, 即只对编译和测试的classpath有效,对运行时的 classpath 无效. 但是,使用system 范围的依赖必须通过systemPath 元素显式地指定依赖文件的路径 如: \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.sql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jdbc-stdext\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;system\u0026lt;/scope\u0026gt; \u0026lt;systemPath\u0026gt;${java.home}/lib/rt.jar\u0026lt;/systemPath\u0026gt; \u0026lt;/dependency\u0026gt; 由于此类依赖不是通过Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用 上述除import 以外的各种依赖范围与三种classpath 的关系如下:\n依赖范围 scope 对于编译classpath有效 对于测试classpath 有效 对于运行时classpath有效 例子 compile Y Y Y spring-core test \u0026ndash; Y \u0026ndash; JUnit provided Y Y \u0026ndash; servlet-apt runtime \u0026ndash; Y Y JDBC 驱动实现 system Y Y \u0026ndash; 本地的,java类库以外的文件 4 仓库 上文提及了依赖管理,通过声明的方式指定所需的构件,那么是从哪里获取所需的构件的呢?答案是 Maven 仓库,Maven 仓库可以分为两类: 本地仓库和远程仓库。\n当 Maven 需要根据坐标寻找构件的时候,它首先会查找本地仓库,如果本地仓库存在该构件,则直接使用,如果本地不存在该构件,或者需要查看是否有更新的构件版本,Maven 聚会去远程仓库查找,发现需要的构件之后,下载到本地仓库在使用.\n如果本地和远程仓库都没有所需要的构件,那么 Maven 就会报错。如果需要细化远程仓库的类型,还可以分成中央仓库,私服和其他公共库。\n中央仓库:Maven 核心自带的的远程仓库,它包含了绝大部分开源的构件。在默认的配置下,当本地仓库没有 Maven 需要的构件的时候,它就会尝试从中央仓库下载。 私服:为了节省带宽和时间,可以在内网假设一个特殊的仓库服务器,用来代理所有的外部的远程仓库 Figure 1: repo\n4.1 SNAPSHOT 在Maven 的世界中,任何一个项目或者构件都必须有自己的版本,版本可能是 1.0.0, 1.0-alpha-4,2.1-SNAPSHOT 或者 2.1-20181028-11, 其中 1.0.0, 1.0-alpha-4 是稳定的发布版本,而 2.1-SNAPSHOT 或者 2.1-20181028-11 是稳定的快照版本。\nMaven 为什么要区分快照版本和发布版本呢?难道1.0.0 不能解决么?为什么需要2.0-SNAPSHOT。\n我对此 SNAPSHOT 这个特性印象非常深刻,在蚂蚁金服的新人培训中,其中就有一项是大家协作完成一个 Mini Alipay,一个 Mini Alipay 分成三个应用bkonebusiness, bkoneuser, bkoneacccount,以SOA 的架构进行拆分,应用之间相互依赖。\n在开发过程中,bkoneuser 经常需要将最新的构件共享 bkonebusiness, 以供他们进行测试和开发。\n因为bkoneuser本身也在快速迭代中,为了让bkonebusiness 用到最新的代码,我们不断地变更版本,1.0.1, 1.0.2, 1.0.3,\u0026hellip; bkoneuser 不断发版本,bkonebusiness 不断升版本,甚至有一次bkoneuser 在没有更新版本号的情况下发布了最新代码,而 bkonebusiness 已经有原来版本的 jar 包,所以就没有去远程仓库拉取最新的代码,就出问题了\u0026hellip;.\n其实 Maven 快照版本就是为了解决这种问题,防止滥用版本号和及时拉取最新代码。\nbkoneuser 只需将版本指定为1.0.1-SNAPSHOT, 然后发布到远程服务器,在发布的工程中,Maven 会自动为构件打上时间戳,比如 1.0.1-20181028.120112-13 表示 2018年10月28号的12点01分12秒的13次快照,有了时间戳,Maven 就能随时找到仓库中该构件1.0.1-SNAPSHOT版本的最新文件。\n这是,bkonebusiness对于 bkoneuser的依赖,只要构建bkonebusiness,Maven就会自动从仓库中检查 bkoneuser的罪行构建,发现有更新便进行下载。\n基于快照版本,bkonebusiness 可以完全不用考虑 bkoneuser 的构建,因为它总是拉取最新版本的 bkoneuser,这个是 Maven 的快照机制进行保证。\n如果到了 release,就要及时将 1.0.1-SNAPSHOT, 否则 bkonebusiness 在构建发布版本的时候可能拉取到最新的有问题的版本.\n4.2 仓库搜索服务 在公司开发的时候有私服,但是在开发自己项目的时候,我一般到 SnoaType Nexus 找对应的构件\n5 插件与生命周期 5.1 何为生命周期 在有关 Maven 的日常使用中,命令行的输入往往就对应了生命周期,如 mvn package 就表示执行默认的生命周期阶段 package.\nMaven 的生命周期是抽象的,其实际行为都由插件来完成,如package 阶段的任务就会有maven-jar-plugin 完成。\nMaven的生命周期就是为了对所有的构建过程进行抽象和统一,包括项目的清理,初始化,编译,测试,打包,集成测试,验证,部署等几乎所有的构建步骤。\n需要注意的是 Maven 的生命周期是抽象的,这意味着生命周期本身不作任何实际的工作,实际的任务(如编译源代码)都交由插件来完成. 每个步骤都可以绑定一个或者多个插件行为,而且Maven 为大多数构建步骤编写并绑定了默认的插件\n例如:针对编码的插件有 maven-compiler-plugin,针对测试的插件有maven-surefire-plugin 等,用户几乎不会察觉插件的存在\n5.2 三套生命周期 Maven 有用三套相互独立的生命周期,它们分别是clean, default , site. clean 生命周期的目的是清理项目,default 生命周期的目的是构件项目,而 site 生命周期的目的是建立项目站点\n5.2.1 clean 生命周期 clean 生命周期主要是清理项目,它包含三个阶段:\npre-clean: 执行一些清理前需要完成的工作 clean 清理上一次构造生成的文件 post-clean 执行一些清理后需要完成的工作 5.2.2 default 生命周期 default 生命周期奠定了真正构件时所需要执行的所有步骤,它是所有生命周期最核心的部分,其包含的阶段如下:\nvalidate initialize generate-sources process-sources 处理项目主资源文件。一般来说,是对src/main/resources 目录内的内容进行变量替换的工作后,复制到项目输出的主classpath 目录中 generate-resources process-resources compile 编译项目的主源码,一般来说,是编译 src/main/java 目录下的java 文件至项目输出的主 classpath 目录中 process-classes generate-test-sources process-test-sources 处理项目测试资源文件。一般来说,是对src/test/resources 目录的内容进行变量替换等工作后,复制到项目输出的测试classpath 目录中 generate-test-resources process-test-resources test-compile 编码项目的测试代码。一般来说,是编译 src/test/java 目录下的java 文件至项目输出的测试classpath 目录中 process-test-classes test 使用单元测试框架运行测试,测试代码不会被打包或部署 prepare-packae package 接受编译好的代码,打包或可发布的格式,如 jar pre-integration-test integration-test post-integration-test vertify install 将包安装到Maven 本地仓库,供本地其他Maven 项目使用 deploy 将最终的包复制到远程仓库,共其他开发人员和Maven 项目使用 5.2.3 site 生命周期 site 生命周期的目的是建立和发布项目站点,生命周期包含如下阶段\npre-site 执行一些在生成项目站点前需要完成的工作 site 生成项目站点文档 post-site 执行一些在生成项目站点之后需要完成的工作 site-deploy 将生成的项目站点发布到服务器上 5.2.4 命令行和生命周期 从命令行执行Maven 任务的最主要方式就是调用 Maven的生命周期阶段。需要注意的是,各个生命周期是相互独立的,而一个生命周期的阶段是有前后依赖关系的。\n下面以一些常见的Maven 命令为例,解释其执行的生命周期阶段:\nmvn clean: 该命令调用clean 生命周期的clean 阶段。实际执行的阶段为clean 生命周期的pre-clean 和clean 阶段 mvn test: 该命令调用default 生命周期的test 阶段。实际执行的阶段是 default 生命周期的 validate, initialize, 直到 test 的所有阶段。这也解释了为什么在测试的时候,项目的代码能够自动得以编译 mvn clean install: 该命令调用 clean 生命周期的clean 阶段和default 生命周期的 install 阶段。实际执行的阶段为 clean 生命周期的 pre-clean, clean 阶段,以及default 生命周期的从validate 到 install 的所有阶段。该命令结合了两个生命周期,在执行真正的项目构建之前清理项目是一个很好的实践 6 继承 如bkoneuser 的项目结构所示:\nFigure 2: bkoneuser 的项目结构\n按照 DDD(Domain Driven Design) 的驱动,bkoneuser 下有多个对应的子模块,每个模块也是一个 Maven 项目,每个模块里面可能有相同的依赖,如 SpringFramework 的 spring-core, spring-beans, spring-context 等。\n如果每个子模块都维护一份大致相同的依赖,那么就有10几份相同的依赖,这还会随着子模块的增多而变得庞大。\n如果我们工程师的嗅觉, 会发现有很多的重复依赖,面对重复应该怎么办?通过抽象来减少重复代码和配置,而 Maven 提供的抽象机制就是继承(还有聚合,只是个人觉得不如继承常用).\n在 OOP 中,工程师可以建立一种类的父子结构,然后在父类中声明一些字段供子类继承,这样就可以做到“一处声明,多处使用”, 类似地,我们需要创建 POM 的父子结构,然后在父POM 中声明一些供子 POM 继承,以实现“一处声明,多处使用”\n6.1 配置示例 parent 的配置如下:\n\u0026lt;groupId\u0026gt;com.minialipay\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;bkgponeuser-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;bkgponeaccount.common.service.facade.version\u0026gt;1.1.0.20180919\u0026lt;/bkgponeaccount.common.service.facade.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;module\u0026gt;app/core/service\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/core/model\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/biz/shared\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/biz/service-impl\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/util\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/service/facade\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/service/integration\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/common/dal\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;app/test\u0026lt;/module\u0026gt; \u0026lt;/modules\u0026gt; 需要主要的关键点是parent 的 packaging 值必须是 pom, 而不是默认的 jar, 否则则无法进行构件.\n而 modules 元素则是实现继承最核心的配置,通过在打包方式为 pom 的Maven 项目中声明任意数量的 module 来实现模块的继承, 每个 module的值都是一个当前POM 的相对目录,比如 app/core/service 就是说子模块的POM在 parent 目录的下的 app/core/service目录\n6.2 子模块配置示例 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;com.minialipay\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;bkgponeuser-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;relativePath\u0026gt;../../../pom.xml\u0026lt;/relativePath\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;bkgponeuser-core-service\u0026lt;/artifactId\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; 上述pom 中使用 parent 元素来声明父模块,parent 下元素groupid, artifactId 和 version 指定了父模块的坐标,这三个元素是必须。\n元素 relativePath 表示父模块POM的相对路径, ../../../pom.xml 指父POM的位置在三级父目录上\n6.3 可继承的POM 元素 可继承元素列表及简短说明:\ngroupId: 项目Id, 坐标的核心元素 version:项目版本, 坐标的核心元素 description: 项目的描述信息 organization: 项目的组织信息 inceptionYear: 项目的创始年份 url: 项目的url 地址 developers: 项目的开发者信息 contributors: 项目的贡献者信息 distributionManagement:项目的部署配置 issueManagement: 项目的缺陷跟踪系统信息 ciManagement: 项目的持续继承系统信息 scm: 项目的版本控制系统信息 mailingLists: 项目的邮件列表信息 properties: 自定义的Maven 属性 dependencies: 项目的依赖配置 dependencieyManagemant: 项目的依赖管理配置 repositories: 项目的仓库配置 build: 包括项目的源码目录配置,输出目录配置,插件配置,插件管理配置等 reporting: 包括项目的报告输出目录配置,报告插件配置等 6.4 dependencyManagement 依赖管理 可继承列表包含了 dependencies 元素,说明是会被继承的,这是我们就会很容易想到将这一特性应用到 bkoneuser-parent 中。子模块同时依赖 spring-beans,=spring-context=,=fastjson= 等, 因此可以将这些依赖配置放到父模块 bkoneuser-parent 中,子模块就能移除这些依赖,简化配置.\n这种做法可行,但是存在问题,我们可以确定现有的子模块都是需要 spring-beans, spring-context 这几个模块的,但是我们无法确定将来添加的子模块就一定需要这四个依赖.\n假设将来项目中要加入一个app/biz/product, 但是这个模块不需要 spring-beans, spring-context, 只需要 fastjson, 那么继承 bkoneuser 就会引入不需要的依赖,这样是非常不利于项目维护的!\nMaven 提供的 dependencyManagement 元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在 dependencyManagement 元素下的依赖声明不会引入实际的依赖,不过它能够约束 dependencies 下的依赖使用。\n例如在 bkoneuser-parent 用 dependencyManagement声明依赖:\n\u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.33\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.7\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 在 app/core/service 子模块进行引用:\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 子模块的fastjson 依赖只配置了 groupId 和 artifactId, 省去了 version , 而 junit 依赖 不仅省去了version, 连scope 都省去了。\n《Maven 实战》作者强烈推荐使用这种方式,其主要原因在与在父POM 中使用 dependencyManagement 声明依赖能够统一规范依赖的版本,当依赖版本在父POM中声明之后,子模块在使用依赖的时候就无须声明版本,也就不会发生多个子模块使用依赖版本不一致的情况\n7 依赖冲突 在Java 项目中,随着项目代码量的增长,各种问题就会接踵而至,jar 包冲突就是其中一个最常见的问题. jar 冲突常见的异常: NoSuchMethodError, NoClassDefFoundError\n7.1 成因 当Maven根据pom文件作依赖分析, 发现通过直接依赖或者间接依赖, 有多个相同groupId, artifactId, 不同 version 的依赖时, 它会根据两点原则来筛选出唯一的一个依赖, 并最终把相应的jar包放到 classpath下:\n依赖路径长度: 比如应用的pom里直接依赖了A, 而A又依赖了B, 那么B对于应用来说, 就是间接依赖, 它的依赖路径长度就是2. 长度越短, 优先级越高. 当出现不同版本的依赖时, maven优先选择依赖路径短的依赖. 依赖声明顺序: 当依赖路径长度相同时, POM 里谁的声明在上面, Maven 就选择谁. public class A { private B b =new B(); public void func_a(){ b.func_b(); } } // 来自b-1.0.jar public class B { private C c=new C(); public void func_b(){ c.func_c(); } } // 来自c-1.0.jar public class C{ public void func_xxx(){ } public void func_c(){ } } // 来自c-1.1.jar public class C{ public void func_xxx(){ } public void func_c1(){ } } // d.1.0.jar public class D{ // 来自c-1.1.jar C c = new C() public void func_d(){ c.func_xxx(); } } public class MyMain{ public static void main(String[] args){ new A().func_a() } } 应用程序里有个A类, 里面含有一个属性B, 这个B类来自 b-1.0.jar 包. A类有个 func_a() 方法, 里面会调用b类的 func_b 方法.B类含有一个属性C, 这个C类来自c-1.0.jar. B类还提供一个方法 func_b(), 里面调用C类的 func_c() 方法.\n这时, 应用程序的主POM里间接依赖了 c-1.1.jar 包, 但是这个jar里的C类中已经把 func_c() 删除了.\n这样由于B类使用的 c-1.0.jar 对于应用程序来说, 是间接依赖, 依赖路径长度是2 (A -\u0026gt; B -\u0026gt; C), 比应用程序主pom中间接依赖的 c-1.1.jar 路径(D-\u0026gt;C)长, 最后就会被maven排掉了 (也就是应用程序的 classpath 下, 最终会保留 c-1.1.jar).\n最后执行main函数时, 就会报 NoSuchMethodError, 也就是找不到C类中 func_c() 方法.\n7.2 解决方案 强制Maven 使用c-1.0.jar, 也就是将c-1.1.jar排除掉:\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.d\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;d\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;com.c\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;c\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 在 d.1.0.jar 的依赖排除 c.1.1.jar 的时候,不需要指定版本, 因为这个时候d.1.0.jar 的依赖的版本一定是 c.1.1.jar. 需要注意的是,如果 d 使用了c.1.1.jar 的 func_c1(),排掉 c.1.1.jar 是会报错的,因为满足了B类的 func_c() 就无法满足 D 类的 func_c1(), 这个就是著名的“菱形依赖问题”(diamond dependency problem)。\n不得不说,入职的时候,遇上了各种jar 包冲突的问题,排包都排出心得. 在此推荐个排包神器, Intellij Idea 的插件:maven helper, 比手动-verbose:class + mvn dependency:tree排包方便多了\n8 总结 的确,写到这里,必须再次承认 Maven 是怪兽级的 构建工具,但是同样无可否认的是,它出色的构建和依赖管理功能。写go 语言的时候,我多希望有个 Maven 可以用呢 ╥﹏╥\u0026hellip;\n","permalink":"https://ramsayleung.github.io/zh/post/2018/maven%E5%B0%8F%E8%AE%B0/","summary":"Maven 在工作中的经验以及《Maven 实战》读后感 1 前言 蚂蚁金服的伯岩大大曾经说 Java 生态都太重量级,连Maven 都是怪兽级的构建工具,需要整整一本书","title":"Maven 小记"},{"content":"纪念我即将终结的大学时光\n1 前言 转眼到了5月,广州的夏天来的特别早。学校的花已谢幕,而我在上个月已经拍完毕业照,在上星期已经完成了毕业答辨,如无意外,我大学的旅程已要走到尽头。\n而我大一入学的场景还历历在目,恍如昨日。\n2 迷茫 往事如烟,当初因高考失利(是个人都说自己失利, 这不禁让我想起了\u0026lt;\u0026gt;, 每个罪犯都自嘲说自己无罪),入学时充满不甘。\n但是心知一切已成事实,我自己无论怎样不甘和愤懑,都是于事无补的。当时的我一心想要弥补当初的失利,只是不知道如何去规划,如何去做。开始时是效仿高中时的学习方法,好好上课,努力做题,大一上学期大半个学期就是这么度过的。\n只是这样无甚成效,也没有一个准确的评判标准来衡量我的努力。后来经常向助班师兄请教,希望师兄可以帮我走出困扰,与师兄聊了很多次,师兄建议我学好专业课,多实践, 学习计算机不能像高中那样学。\n3 奋进 助班师兄的建议以及身边同学的激励(业神在大一的时候已经可以自己编写绕过游戏的程序保护的游戏外挂,可以自己和大三的师兄组建工作室赚外快)让我有了方向.\n所以从大一下学期到大三上学期,我大部份时间都是在图书馆和工作室度过,在图书馆自学计算机系的专业课,从操作系统到计算机网络,再到数据库,而后在工作室中跟着其他师兄做各种项目。\n那些年在学校的图书馆还是借阅了很多书的,虽说有些只是借了之后就还回去了,但是大部份的书还是有看的,如图:\nFigure 1: 借书\n大二寒暑假放假后,我都会留校去工作室写项目,记得大二那年的寒假,也就是2016年的春节前后,在广州下了雪(只不过山东的同学说那只能算是冰渣子),那时我骑车从宿舍去 工作室,每次都把手冻得通红。现在想来,真的感慨万分\n4 机遇 4.1 创业公司 经过大一大二的恶补之后,我那时基本可以写些简单的项目了,正好那个时候同班的聪哥想要组队去参加比赛,就带上了我。\n聪哥负责移动端,我负责后端,我们一起写了个类似超级课程表简化版的APP参加比赛,幸运地拿了校内计算机比赛的冠军,后来我们那同样的作品去参加穗港澳的一个计算机比赛,也侥幸地拿了个三等奖。\n在颁奖典礼上,我偶遇了人生中 第一个大「Boss」\u0026ndash; 老刘,老刘的title 很多,只记得其中的几个:曾在上世纪90年代任职于微软,惠普华南区总裁,现在是美国一所大学的终身副教授,国内电子科技大学的教授。当时在颁奖典礼既技术分享沙龙上,老刘问了几个问题,在场的同学应该是过于羞涩,所以只有我举手回答。\n分享之后老刘就和我交流,当时老刘在一个深圳的创业公司担任VP, 交流过后就邀请我去他的公司实习,就这样我就在大二暑假的时候拿了第一份实习Offer.\n在老刘的教导下,实习的最大收获是视野和信心,看到很多学校老师不能教给我的东西,比如开发规范,上线流程等。在老刘的鼓励下,我觉得自己并不比其他人差。\n4.2 阿里 4.2.1 蚂蚁金服 在大三下学期找实习的时候,在面蚂蚁金服的时候,幸运地通过内推面笔试,在一面二面技术面,面试官考察的问题我也恰好有了解过,就这样我就侥幸拿到名额不多的阿里实习Offer.\n在蚂蚁金服只是实习了一个暑假,具体的需求只是完成了两三个,更多地是在学习蚂蚁金服的技术体系,了解这么大用户量的公司的开发流程,如何在保证代码质量的前提下 进行开发。\n4.2.2 阿里大文娱 在蚂蚁金服实习回来之后,已经大四的我不想在宿舍虚度光阴,因为我大四已经没有任何课了,所以我在广州另外找了个实习--阿里大文娱-UC.\n是Kevin 把我招到UC 的,Kevin很看好我,把我安排到了新成立的核心组,是负责UC 国内业务的平台组,组里的目标是可以发展成可以支撑起100亿量级数据的平台,我入职的时候正是平台刚起步的时候,所以我算是见证了平台的负责。\n而在UC 的半年,是我技术成长最快的半年,Java 的GC, 平台的双活,控流,资源融断等高可用策略,分布式的存储和搜索(Hbase+ElasticSeach)到数据库的优化和基于 shardingKey 的分库分表。\n虽然我不是平台的核心开发者,但是作为参与者,我也是获益匪浅。只是4月底,实习近半年后,我拿到了UC 的Offer,只不过我最终离开了UC, 选择蚂蚁金服,我依然感谢UC 所有帮过我的同事。\n5 展望 在我参加工作前的最后一个半月,我回到了宿舍,看我自己喜欢的动漫,看我自己喜欢的书,吹奏我喜欢的乐曲,登录上我5年没玩的游戏,折腾起如智力游戏般的CPP;\n在我还是学生时,做我自己喜欢的事。峥嵘四载,今再望,诸事宛如梦中。前路茫茫,不失辛酸与希望,突然想起高中英语口试前的那句名言:生活就像海洋,只有意志坚强的人才能到达彼岸。\n","permalink":"https://ramsayleung.github.io/zh/post/2018/farewell_to_my_university_time/","summary":"纪念我即将终结的大学时光 1 前言 转眼到了5月,广州的夏天来的特别早。学校的花已谢幕,而我在上个月已经拍完毕业照,在上星期已经完成了毕业答辨,如","title":"恰同学少年"},{"content":"Socket 泄漏引起的Tomcat 宕机问题分析\n在2018年4月9号下午,收到反馈:测试集群部分接口访问有问题,请求时而正常,时而超时。\n最近的测试环境真的是问题多多,可是测试环境就是我搭建的,冏。查看日志发现87 这台 服务器的Tomcat 无法访问:\n2018-04-09 17:41:31,568 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 2018-04-09 17:41:33,168 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 1 Linux 文件句柄限制 报错看起来像是进程打开文件句柄的个数达到了linux的限制。而这种限制是分为系统层面的和用户层面的\n1.1 系统层面 系统层面的在:/proc/sys/fs/file-max里设置\ncat /proc/sys/fs/file-max 2442976 1.2 用户层面 用户层面的限制在:/etc/security/limits.conf里设定。通过ulimit -a 查看系统允许单个进程打开的最大文件数:\nulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 192059 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 65536 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 10240 cpu time (seconds, -t) unlimited max user processes (-u) 65535 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited 单个进程可以打开的最大文件数是 65536\n2 lsof 显示大量open file 按照Tomcat 给出的报错信息,登录87 这台服务器检查打开的文件数,发现打开的文件超过70000:\nlsof |wc -l 75924 然后找出打开文件数最多的进程,按文件数降序排列,左边是 open file 的数量,右边是进程ID:\nlsof -n|awk \u0026#39;{print $2}\u0026#39;| sort | uniq -c | sort -nr | head 65966 25204 5374 20179 184 27275 65 5361 61 29421 16 22177 14 19751 12 22181 12 22179 12 22178 发现 25204 这个进程打开了大量的文件,已经超过了单个进程的最大文件数限制。而这个进程就是部署的java 应用对应的进程。打开的文件句柄数量已经超过Linux 限制, Tomcat 无法创建新的socket 连接。\n3 can\u0026rsquo;t identify protocol 用 lsof 查看 java 应用打开的文件的时候,发现有非常多奇怪的输出:\njava 25204 nemo *516u sock 0,6 0t0 215137625 can\u0026#39;t identify protocol java 25204 nemo *517u sock 0,6 0t0 215137626 can\u0026#39;t identify protocol java 25204 nemo *518u sock 0,6 0t0 215137627 can\u0026#39;t identify protocol java 25204 nemo *519u sock 0,6 0t0 215137628 can\u0026#39;t identify protocol java 25204 nemo *520u sock 0,6 0t0 215137629 can\u0026#39;t identify protocol java 25204 nemo *521u sock 0,6 0t0 215137630 can\u0026#39;t identify protocol java 25204 nemo *522u sock 0,6 0t0 215137631 can\u0026#39;t identify protocol java 25204 nemo *523u sock 0,6 0t0 215137634 can\u0026#39;t identify protocol java 25204 nemo *524u sock 0,6 0t0 215137635 can\u0026#39;t identify protocol java 25204 nemo *525u sock 0,6 0t0 215137636 can\u0026#39;t identify protocol java 25204 nemo *526u sock 0,6 0t0 215137637 can\u0026#39;t identify protocol java 25204 nemo *527u sock 0,6 0t0 215137638 can\u0026#39;t identify protocol java 25204 nemo *528u sock 0,6 0t0 215137639 can\u0026#39;t identify protocol java 25204 nemo *529u sock 0,6 0t0 215137640 can\u0026#39;t identify protocol java 25204 nemo *530u sock 0,6 0t0 215137641 can\u0026#39;t identify protocol java 25204 nemo *531u sock 0,6 0t0 215137642 can\u0026#39;t identify protocol java 25204 nemo *532u sock 0,6 0t0 215137644 can\u0026#39;t identify protocol java 25204 nemo *533u sock 0,6 0t0 215137646 can\u0026#39;t identify protocol 统计之后发现, can't identify protocol 这样的文件数量非常多:\nlsof -p 25204|grep \u0026#34;can\u0026#39;t identify protocol\u0026#34;|wc -l 64214 也就是大部份打开的文件都是属于 cant' identify protocol 的文件。\n4 问题定位 Google 搜索之后发现,这个 cant' identify protocol 的东东出现的原因是因为 这些 sockets 处于 CLOSED 的状态,但是却没有真正close 掉,正处于 half-close 状态。因此,如果使用 netstat 来查看socket 状态,是不会显示这些 half-close的 socket 的:\nnetstat -nat |wc -l 881 使用 netstat 的改进版本 ss 就能发现大量处于 Closed 状态的 socket:\nss -s Total: 76052 (kernel 76254) TCP: 75924 (estab 123, closed 75524, orphaned 0, synrecv 0, timewait 173/0), ports 104 Transport Total IP IPv6 * 76254 - - RAW 0 0 0 UDP 9 6 3 TCP 116 80 36 INET 125 86 39 FRAG 0 0 0 接着查看内核的 socket 情况:\ncat /proc/net/sockstat sockets: used 75724 TCP: inuse 886 orphan 0 tw 0 alloc 72134 mem 222 UDP: inuse 5 mem 0 UDPLITE: inuse 0 RAW: inuse 0 FRAG: inuse 0 memory 0 很多的 socket 处于 alloc, 只有少量的 socket 处于 inuse. 可以确认是 java 应用出现了 socket fd 的泄漏。 但是为什么会有那么多的socket 泄漏呢?\n5 大胆假设 现在可以确定的是 java应用出现了问题,导致了socket 泄漏,让 Tomcat 无法建立新连接,最终宕机。既然导致问题出现的是 java 应用,那么就应该去检查应用日志。\n2018-04-09 17:41:31,491 - [ERROR] - from com.alibaba.druid.pool.DruidDataSource in Druid-ConnectionPool-CreateScheduler--4-thread-214 create connection error, url: jdbc:mysql://test-server-host:3306/db_name?readOnlyPropagatesToServer=false\u0026amp;rewriteBatchedStatements=true\u0026amp;failOverReadOnly=false\u0026amp;socketTimeout=6000\u0026amp;connectTimeout=20000\u0026amp;zeroDateTimeBehavior=convertToNull\u0026amp;allowMultiQueries=true\u0026amp;characterEncoding=utf-8\u0026amp;autoReconnect=true com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up. at sun.reflect.GeneratedConstructorAccessor169.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.Util.getInstance(Util.java:408) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:918) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2163) at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2088) at com.mysql.jdbc.ConnectionImpl.\u0026lt;init\u0026gt;(ConnectionImpl.java:806) at com.mysql.jdbc.JDBC4Connection.\u0026lt;init\u0026gt;(JDBC4Connection.java:47) at sun.reflect.GeneratedConstructorAccessor152.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:410) at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:328) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:148) at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:211) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:142) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1423) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1477) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.runInternal(DruidDataSource.java:1884) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.run(DruidDataSource.java:1849) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. at sun.reflect.GeneratedConstructorAccessor157.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:341) at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2251) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2104) ... 21 common frames omitted Caused by: java.net.SocketException: Too many open files at java.net.Socket.createImpl(Socket.java:460) at java.net.Socket.getImpl(Socket.java:520) at java.net.Socket.setTcpNoDelay(Socket.java:980) at com.mysql.jdbc.StandardSocketFactory.configureSocket(StandardSocketFactory.java:132) at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:203) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:300) ... 23 common frames omitted 2018-04-09 17:41:31,568 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 2018-04-09 17:41:33,168 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 2018-04-09 17:41:34,470 - [ERROR] - from com.alibaba.druid.pool.DruidDataSource in Druid-ConnectionPool-CreateScheduler--4-thread-216 create connection error, url: jdbc:mysql://test-server-url:3306/db_name?readOnlyPropagatesToServer=false\u0026amp;rewriteBatchedStatements=true\u0026amp;failOverReadOnly=false\u0026amp;socketTimeout=6000\u0026amp;connectTimeout=20000\u0026amp;zeroDateTimeBehavior=convertToNull\u0026amp;allowMultiQueries=true\u0026amp;characterEncoding=utf-8\u0026amp;autoReconnect=true com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up. at sun.reflect.GeneratedConstructorAccessor169.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.Util.getInstance(Util.java:408) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:918) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2163) at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2088) at com.mysql.jdbc.ConnectionImpl.\u0026lt;init\u0026gt;(ConnectionImpl.java:806) at com.mysql.jdbc.JDBC4Connection.\u0026lt;init\u0026gt;(JDBC4Connection.java:47) at sun.reflect.GeneratedConstructorAccessor152.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:410) at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:328) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:148) at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:211) at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:142) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1423) at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1477) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.runInternal(DruidDataSource.java:1884) at com.alibaba.druid.pool.DruidDataSource$CreateConnectionTask.run(DruidDataSource.java:1849) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. at sun.reflect.GeneratedConstructorAccessor157.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:341) at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2251) at com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2104) ... 23 common frames omitted Caused by: java.net.SocketException: Too many open files at java.net.Socket.createImpl(Socket.java:460) at java.net.Socket.getImpl(Socket.java:520) at java.net.Socket.setTcpNoDelay(Socket.java:980) at com.mysql.jdbc.StandardSocketFactory.configureSocket(StandardSocketFactory.java:132) at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:203) at com.mysql.jdbc.MysqlIO.\u0026lt;init\u0026gt;(MysqlIO.java:300) ... 25 common frames omitted 2018-04-09 17:41:34,769 - [ERROR] - from org.apache.tomcat.util.net.NioEndpoint in http-nio-47001-Acceptor-0 Socket accept failed java.io.IOException: Too many open files at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:825) at java.lang.Thread.run(Thread.java:745) 检查日志发现,在 Tomcat 彻底挂机之前,曾经有比较大量的数据源连接池出错,无法访问 Mysql, 但是非常奇怪的是,在87 这台机器上面,是可以使用 mysql 命令行连接到测试数据库的,说明 Mysql 的连接是没有问题。\n但是数据源连接就会出错!! 真的是很奇怪,为什么连接池会报错,有没有可能是这些异常导致 socket 泄漏呢?后来,在本地运行应用,有时候会发现IDE 的控制台报错:\n2018-04-11 09:43:48,363 - [ERROR] - from com.alibaba.druid.pool.DruidDataSource in poolTaskScheduler-11 discard connection com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet successfully received from the server was 100,610 milliseconds ago. The last packet sent successfully to the server was 0 milliseconds ago. at sun.reflect.GeneratedConstructorAccessor108.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3556) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3456) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3897) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2524) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2677) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2545) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2503) at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1369) at com.alibaba.druid.filter.FilterChainImpl.statement_executeQuery(FilterChainImpl.java:2363) at com.alibaba.druid.filter.FilterAdapter.statement_executeQuery(FilterAdapter.java:2481) at com.alibaba.druid.filter.FilterEventAdapter.statement_executeQuery(FilterEventAdapter.java:302) at com.alibaba.druid.filter.FilterChainImpl.statement_executeQuery(FilterChainImpl.java:2360) at com.alibaba.druid.proxy.jdbc.StatementProxyImpl.executeQuery(StatementProxyImpl.java:211) at com.alibaba.druid.pool.DruidPooledStatement.executeQuery(DruidPooledStatement.java:138) at com.taobao.tddl.atom.jdbc.TStatementWrapper.executeQuery(TStatementWrapper.java:315) at com.taobao.tddl.group.jdbc.TGroupStatement.executeQueryOnConnection(TGroupStatement.java:549) at com.taobao.tddl.group.jdbc.TGroupStatement$4.tryOnDataSource(TGroupStatement.java:633) at com.taobao.tddl.group.jdbc.TGroupStatement$4.tryOnDataSource(TGroupStatement.java:615) at com.taobao.tddl.group.dbselector.AbstractDBSelector.tryOnDataSourceHolder(AbstractDBSelector.java:155) at com.taobao.tddl.group.dbselector.OneDBSelector.tryExecuteInternal(OneDBSelector.java:52) at com.taobao.tddl.group.dbselector.AbstractDBSelector.tryExecute(AbstractDBSelector.java:405) at com.taobao.tddl.group.dbselector.AbstractDBSelector.tryExecute(AbstractDBSelector.java:412) at com.taobao.tddl.group.jdbc.TGroupStatement.executeQuery(TGroupStatement.java:488) at com.taobao.tddl.group.jdbc.TGroupStatement.executeInternal(TGroupStatement.java:131) at com.taobao.tddl.group.jdbc.TGroupStatement.execute(TGroupStatement.java:101) at com.taobao.tddl.repo.mysql.spi.My_JdbcHandler.executeQuery(My_JdbcHandler.java:521) at com.taobao.tddl.repo.mysql.spi.My_Cursor.init(My_Cursor.java:106) at com.taobao.tddl.repo.mysql.handler.QueryMyHandler.handle(QueryMyHandler.java:89) at com.taobao.tddl.executor.AbstractGroupExecutor.executeInner(AbstractGroupExecutor.java:47) at com.taobao.tddl.executor.AbstractGroupExecutor.execByExecPlanNode(AbstractGroupExecutor.java:36) at com.taobao.tddl.executor.TopologyExecutor.execByExecPlanNode(TopologyExecutor.java:66) at com.taobao.tddl.executor.MatrixExecutor.execByExecPlanNodeByOne(MatrixExecutor.java:670) at com.taobao.tddl.executor.MatrixExecutor.execByExecPlanNode(MatrixExecutor.java:659) at com.taobao.tddl.executor.MatrixExecutor.execute(MatrixExecutor.java:137) at com.taobao.tddl.matrix.jdbc.TConnection.executeSQL(TConnection.java:241) at com.taobao.tddl.matrix.jdbc.TPreparedStatement.executeSQL(TPreparedStatement.java:64) at com.taobao.tddl.matrix.jdbc.TStatement.executeInternal(TStatement.java:133) at com.taobao.tddl.matrix.jdbc.TPreparedStatement.execute(TPreparedStatement.java:49) at sun.reflect.GeneratedMethodAccessor148.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.ibatis.logging.jdbc.PreparedStatementLogger.invoke(PreparedStatementLogger.java:59) at com.sun.proxy.$Proxy102.execute(Unknown Source) at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:63) at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79) at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63) at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325) at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83) at sun.reflect.GeneratedMethodAccessor146.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49) at fastfish.interceptor.DbLogInterceptor.intercept(DbLogInterceptor.java:49) at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61) at com.sun.proxy.$Proxy100.query(Unknown Source) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141) at sun.reflect.GeneratedMethodAccessor145.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:434) at com.sun.proxy.$Proxy87.selectList(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:231) at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:128) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:68) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53) at com.sun.proxy.$Proxy124.selectAll(Unknown Source) at fastfish.services.BusinessService.getAll(BusinessService.java:73) at fastfish.services.BusinessService.loadDB(BusinessService.java:38) at sun.reflect.GeneratedMethodAccessor190.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.runAndReset$$$capture(FutureTask.java:308) at java.util.concurrent.FutureTask.runAndReset(FutureTask.java) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost. at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3008) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3466) ... 83 common frames omitted 是数据池连接出错。但是我本地的应用确实是可以访问测试数据库的, 比较有趣的异常就是\nCaused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost. 数据还没有读完, Connection 就丢了。为什么会 lost connection 呢,可能数据库出问题,也可能是网络出了问题。\n我还能从数据库读到数据,说明数据库没问题的,兼之这个异常只是偶尔出现,所以可能就是网络出问题了。\n如此说来,是否可能是因为测试环境网络不稳定,连接池无法和 Mysql 保持连接,在丢掉 Connection 之后,连接池重新发起连接,但是因为网络不稳定又丢掉了Connection, 不断循环这个过程,导致建立的 socket 连接越 来越多,但是建立的 socket 很快就被Close 掉了,内核又没有把这些 Close 掉的 socket 资源回收掉,因此打开的 socket 文件越来越多,最后导致 Tomcat 因为打开的文件过多无法建立新的 socket 连接。\n6 小心求证 如果连接池真的不断尝试连接Mysql 的话,必定会建立很多的连接,而Mysql 是会将这些记录保存下来的,检查Mysql 的变量:\n查看Mysql 的文档关于 Connection 和 Thread_connected 的说明:\nConnections\nThe number of connection attempts (successful or not) to the MySQL server.\nThreads_connected\nThe number of currently open connections.\n也就是说,当时共有20000 多的连接请求,但是真正被 Mysql accpet 并且服务的只有 28 个连接。看来的确是因为连接池的连接导致 socket 泄漏\n6.1 更新 和运维同学沟通之后,发现丢连接的原因不是网络不稳定,而是测试集群都是虚拟机,内存 用光,导致无法建立新的连接,内核释放一部分资源之后又可以建立连接了。内存用完,我能怎么办,我也很无奈。\n7 解决方法 虽说基本确定了 socket 泄漏的源头,但是对于内核为什么无法回收已经关闭 socket 的原因依然不明确。\n最令人百思不得其解的是,部署了应用的测试服务器有两台,另外一 台服务器也有同样的连接池问题,但是却没有出现 socket 泄漏问题, 出现泄漏的只有 87 这台机器。真的令人费解. 所以最后解决方法就是撤下 87 服务器的应用,换一台服务器来部署。\n新的服务器部署应用之后虽说也有同样的数据库连接池异常,但是却没有出现 socket 泄漏,初步定位是 87这台机器的内核环境存在问题。\n8 参考 tcp-socket文件句柄泄漏/ lsof-cant-identify-protocol/ ","permalink":"https://ramsayleung.github.io/zh/post/2018/lsof_cant_identify_protocol/","summary":"Socket 泄漏引起的Tomcat 宕机问题分析 在2018年4月9号下午,收到反馈:测试集群部分接口访问有问题,请求时而正常,时而超时。 最近的测试环境真","title":"lsof can't identify protocol"},{"content":"1 背景 在2018年4月4号早上,业务方反应Hbase 读超时,无法读取当前数据。然后发现测试环境的 Hbase region server 全部宕机,已经无可用Region Server. 因为公司的机器的Ip 和Host 不便在博文展示,所以我会用:\n192.168.2.1: node-master 192.168.2.2: node1 192.168.2.3: node2 来代替\n2 Region Server 宕机原因分析 经查看日志,发现三台部署了Hbase 的服务器,分别是node-master 192.168.2.1, node1 192.168.2.2,=node2 192.168.2.3=. node1 机器在2018-03-13 14:47:55 收到了Shutdown Message, 停了Region Server. node-master这台机器在2018-03-20 10:13:07收到了Shutdown Message, 停掉了Region Server.\n也就是说在3月下旬到昨天,Hbase 一直只有一台Region Server 在运行。而在昨天,2018-04-03 23:19:35, 剩下的最后一台机器也收到了Shutdown Message, 因此把剩下的最后一台Region Server 停掉,测试 环境的Hbase 全部下线。那么,为什么这三台服务器会收到Shutdown Message 呢?\n2.1 node1 先从 node1这台机器开始分析,关于 Region Server 退出的日志显示如下:\n2018-03-13 14:47:49,665 INFO [main-SendThread(node-master:2181)] zookeeper.ClientCnxn: Unable to reconnect to ZooKeeper service, session 0x161d6c1ae910001 has expired, closing socket connection 2018-03-13 14:47:49,706 FATAL [main-EventThread] regionserver.HRegionServer: ABORTING region server node1,60020,1519732610839: regionserver:60020-0x161d6c1ae910001, quorum=node-master:2181,node1:2181,node2:2181, baseZNode=/hbase regionserver:60020-0x161d6c1ae910001 received expired from ZooKeeper, aborting org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode = Session expired at org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher.connectionEvent(ZooKeeperWatcher.java:700) at org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher.process(ZooKeeperWatcher.java:611) at org.apache.zookeeper.ClientCnxn$EventThread.processEvent(ClientCnxn.java:522) at org.apache.zookeeper.ClientCnxn$EventThread.run(ClientCnxn.java:498) 2018-03-13 14:47:49,718 FATAL [main-EventThread] regionserver.HRegionServer: RegionServer abort: loaded coprocessors are: [org.apache.hadoop.hbase.coprocessor.MultiRowMutationEndpoint] 2018-03-13 14:47:50,705 WARN [DataStreamer for file /hbase-nemo/WALs/node1,60020,1519732610839/node1%2C60020%2C1519732610839.default.1520922158622 block BP-1296874721-192.168.2.1-1519712987003:blk_1073743994_3170] hdfs.DFSClient: DataStreamer Exception org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.LeaseExpiredException): No lease on /hbase-nemo/oldWALs/node1%2C60020%2C1519732610839.default.1520922158622 (inode 18837): File is not open for writing. [Lease. Holder: DFSClient_NONMAPREDUCE_551822027_1, pendingcreates: 1] at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.checkLease(FSNamesystem.java:3612) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.getAdditionalDatanode(FSNamesystem.java:3516) at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.getAdditionalDatanode(NameNodeRpcServer.java:711) at org.apache.hadoop.hdfs.server.namenode.AuthorizationProviderProxyClientProtocol.getAdditionalDatanode(AuthorizationProviderProxyClientProtocol.java:229) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.getAdditionalDatanode(ClientNamenodeProtocolServerSideTranslatorPB.java:508) at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java) at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:617) at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1073) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2086) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2082) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.Subject.doAs(Subject.java:422) at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1693) at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2080) at org.apache.hadoop.ipc.Client.call(Client.java:1471) at org.apache.hadoop.ipc.Client.call(Client.java:1408) at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:230) at com.sun.proxy.$Proxy16.getAdditionalDatanode(Unknown Source) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.getAdditionalDatanode(ClientNamenodeProtocolTranslatorPB.java:429) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:256) at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:104) at com.sun.proxy.$Proxy17.getAdditionalDatanode(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.hbase.fs.HFileSystem$1.invoke(HFileSystem.java:279) at com.sun.proxy.$Proxy18.getAdditionalDatanode(Unknown Source) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1228) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-13 14:47:53,803 FATAL [regionserver/node1/192.168.2.2:60020] regionserver.HRegionServer: ABORTING region server node1,60020,1519732610839: org.apache.hadoop.hbase.YouAreDeadException: Server REPORT rejected; currently processing node1,60020,1519732610839 as dead server 从 14:47:49 开始, Hbase 没法和 Zookeeper 通信,连接时间超时。翻查 Zookeeper 的日志,发现Zookeeper 的日志有如下内容:\n2018-03-13 14:47:46,926 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910002 for client /192.168.2.2:51611, probably expired 2018-03-13 14:47:49,612 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910001 for client /192.168.2.2:51612, probably expired 说明Hbase 和 ZooKeeper 的通信的确去了问题。连接出问题以后,集群就会认为 这个 Hbase 的节点出了故障,宕机,然后就把这个节点当作 DeadNode, 这个节点的 RegionServer 就下线了。\n2.2 node-master 现在再来看看node-master这台机器的日志\n2018-03-20 10:12:19,986 INFO [main-SendThread(node-master:2181)] zookeeper.ClientCnxn: Unable to read additional data from server sessionid 0x361d65049260001, likely server has closed socket, closing socket connection and attempting reconnect 2018-03-20 10:12:20,841 INFO [main-SendThread(node1:2181)] zookeeper.ClientCnxn: Opening socket connection to server node1/192.168.2.2:2181. Will not attempt to authenticate using SASL (unknown error) 2018-03-20 10:12:43,747 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node1:2181)] zookeeper.ClientCnxn: Client session timed out, have not heard from server in 60019ms for sessionid 0x161d65049590000, closing socket connection and attempting reconnect 2018-03-20 10:12:44,574 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node-master:2181)] zookeeper.ClientCnxn: Opening socket connection to server node-master/192.168.2.1:2181. Will not attempt to authenticate using SASL (unknown error) 2018-03-20 10:12:44,575 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node-master:2181)] zookeeper.ClientCnxn: Socket connection established, initiating session, client: /192.168.2.1:58042, server: node-master/192.168.2.1:2181 2018-03-20 10:12:44,577 INFO [regionserver/node-master/192.168.2.1:60020-SendThread(node-master:2181)] zookeeper.ClientCnxn: Session establishment complete on server node-master/192.168.2.1:2181, sessionid = 0x161d65049590000, negotiated timeout = 90000 2018-03-20 10:12:49,625 INFO [main-SendThread(node1:2181)] zookeeper.ClientCnxn: Socket connection established, initiating session, client: /192.168.2.1:46815, server: node1/192.168.2.2:2181 2018-03-20 10:12:53,258 WARN [ResponseProcessor for block BP-1296874721-192.168.2.1-1519712987003: blk_1073747108_6286] hdfs.DFSClient: Slow ReadProcessor read fields took 70070ms (threshold=30000ms); ack: seqno: -2 reply: 0 reply: 1 downstreamAckTimeNanos: 0, targets: [DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]] 2018-03-20 10:12:53,259 WARN [ResponseProcessor for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286] hdfs.DFSClient: DFSOutputStream ResponseProcessor exception for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286 java.io.IOException: Bad response ERROR for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286 from datanode DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK] at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer$ResponseProcessor.run(DFSOutputStream.java:1002) 2018-03-20 10:12:53,259 WARN [DataStreamer for file /hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323 block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286] hdfs.DFSClient: Error Recovery for block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286 in pipeline DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]: bad datanode DatanodeInfoWithStorage[192.168.2.2:50010,DS-4eb97418-f0a1-45a7-b335-83f77e4d6a7b,DISK] 2018-03-20 10:12:53,264 WARN [DataStreamer for file /hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323 block BP-1296874721-192.168.2.1-1519712987003:blk_1073747108_6286] hdfs.DFSClient: DataStreamer Exception java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:12:53,265 WARN [sync.4] hdfs.DFSClient: Error while syncing java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:12:53,266 ERROR [sync.4] wal.FSHLog: Error syncing, request close of WAL java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:12:53,266 INFO [sync.4] wal.FSHLog: Slow sync cost: 474 ms, current pipeline: [DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]] 2018-03-20 10:13:05,816 INFO [regionserver/node-master/192.168.2.1:60020.logRoller] wal.FSHLog: Slow sync cost: 12546 ms, current pipeline: [DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]] 2018-03-20 10:13:05,817 ERROR [sync.0] wal.FSHLog: Error syncing, request close of WAL java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:13:05,817 ERROR [regionserver/node-master/192.168.2.1:60020.logRoller] wal.FSHLog: Failed close of WAL writer hdfs://node-master:19000/hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323, unflushedEntries=1 org.apache.hadoop.hbase.regionserver.wal.FailedSyncBeforeLogCloseException: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hbase.regionserver.wal.FSHLog$SafePointZigZagLatch.waitSafePoint(FSHLog.java:1615) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.replaceWriter(FSHLog.java:833) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:699) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:13:05,818 FATAL [regionserver/node-master/192.168.2.1:60020.logRoller] regionserver.HRegionServer: ABORTING region server node-master,60020,1519720160721: Failed log close in log roller org.apache.hadoop.hbase.regionserver.wal.FailedLogCloseException: hdfs://node-master:19000/hbase-nemo/WALs/node-master,60020,1519720160721/node-master%2C60020%2C1519720160721.default.1521509628323, unflushedEntries=1 at org.apache.hadoop.hbase.regionserver.wal.FSHLog.replaceWriter(FSHLog.java:882) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:699) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: org.apache.hadoop.hbase.regionserver.wal.FailedSyncBeforeLogCloseException: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hbase.regionserver.wal.FSHLog$SafePointZigZagLatch.waitSafePoint(FSHLog.java:1615) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.replaceWriter(FSHLog.java:833) ... 3 more Caused by: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.findNewDatanode(DFSOutputStream.java:1162) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.addDatanode2ExistingPipeline(DFSOutputStream.java:1236) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.setupPipelineForAppendOrRecovery(DFSOutputStream.java:1404) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.processDatanodeError(DFSOutputStream.java:1119) at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:622) 2018-03-20 10:13:05,818 FATAL [regionserver/node-master/192.168.2.1:60020.logRoller] regionserver.HRegionServer: RegionServer abort: loaded coprocessors are: [] 2018-03-20 10:13:05,997 INFO [regionserver/node-master/192.168.2.1:60020.logRoller] regionserver.HRegionServer: Dump of metrics as JSON on abort: 从上面的日志可以看到 node-master与node1机器通信,获取node1 的响应失败,认为node1 是 bad DataNode,接着集群想要把出现问题的DataNode 下掉,却发现没有多余DataNode 来替换, 紧接着在Syncing 时出错,关闭 WAL 失败, 最后就停掉了Region Server. 比较关键的时机如下:\n2018-03-20 10:12:53,265 WARN [sync.4] hdfs.DFSClient: Error while syncing 2018-03-20 10:12:53,266 ERROR [sync.4] wal.FSHLog: Error syncing, request close of WAL 2018-03-20 10:13:05,817 ERROR [sync.0] wal.FSHLog: Error syncing, request close of WAL 2018-03-20 10:13:06,397 ERROR [regionserver/node-master/192.168.2.1:60020] regionserver.HRegionServer: Shutdown / close of WAL failed: java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try. (Nodes: current=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]], original=[DatanodeInfoWithStorage[192.168.2.1:50010,DS-84998b22-8294-44ed-90fd-9c1a78d0f558,DISK], DatanodeInfoWithStorage[192.168.2.3:50010,DS-d94668c9-66f4-40f6-b38f-83f14b26c2b4,DISK]]). The current failed datanode replacement policy is DEFAULT, and a client may configure this via \u0026#39;dfs.client.block.write.replace-datanode-on-failure.policy\u0026#39; in its configuration. 期间HDFS 同步出错,尝试关闭WAL, 失败。失败的原因是无法用健康的节点替换出了问题的节点, 应该是健康的节点数太少了。最后在多次尝试关闭WAL都因为IOException 失败之后, RegionServer 下线。只是为什么尝试关闭WAL 失败需要关闭Region Server 依然存疑。\n2.3 node2 node2 是Hbase 集群最后一台机器,当node2 倒下了,Hbase 就真的完全宕机了。\n2018-04-03 23:19:33,472 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.LogRoller: Aborting java.io.IOException: cannot get log writer at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:365) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.createWriterInstance(FSHLog.java:724) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:689) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.SafeModeException): Cannot create file/hbase-nemo/WALs/node2,60020,1519732668326/node2%2C60020%2C1519732668326.default.1522768773233. Name node is in safe mode. Resources are low on NN. Please add or free up more resources then turn off safe mode manually. NOTE: If you turn off safe mode before adding resources, the NN will immediately return to safe mode. Use \u0026#34;hdfs dfsadmin -safemode leave\u0026#34; to turn safe mode off. at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.checkNameNodeSafeMode(FSNamesystem.java:1418) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFileInt(FSNamesystem.java:2674) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFile(FSNamesystem.java:2561) at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.create(NameNodeRpcServer.java:593) at org.apache.hadoop.hdfs.server.namenode.AuthorizationProviderProxyClientProtocol.create(AuthorizationProviderProxyClientProtocol.java:111) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.create(ClientNamenodeProtocolServerSideTranslatorPB.java:393) at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java) at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:617) at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1073) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2086) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2082) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.Subject.doAs(Subject.java:422) at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1693) at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2080) at org.apache.hadoop.ipc.Client.call(Client.java:1471) at org.apache.hadoop.ipc.Client.call(Client.java:1408) at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:230) at com.sun.proxy.$Proxy16.create(Unknown Source) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.create(ClientNamenodeProtocolTranslatorPB.java:296) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:256) at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:104) at com.sun.proxy.$Proxy17.create(Unknown Source) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.hbase.fs.HFileSystem$1.invoke(HFileSystem.java:279) at com.sun.proxy.$Proxy18.create(Unknown Source) at org.apache.hadoop.hdfs.DFSOutputStream.newStreamForCreate(DFSOutputStream.java:1897) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1738) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1698) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:450) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystemLinkResolver.resolve(FileSystemLinkResolver.java:81) at org.apache.hadoop.hdfs.DistributedFileSystem.createNonRecursive(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1124) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1100) at org.apache.hadoop.hbase.regionserver.wal.ProtobufLogWriter.init(ProtobufLogWriter.java:90) at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:361) ... 4 more 2018-04-03 23:19:33,501 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.HRegionServer: ABORTING region server node2,60020,1519732668326: IOE in log roller java.io.IOException: cannot get log writer at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:365) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.createWriterInstance(FSHLog.java:724) at org.apache.hadoop.hbase.regionserver.wal.FSHLog.rollWriter(FSHLog.java:689) at org.apache.hadoop.hbase.regionserver.LogRoller.run(LogRoller.java:148) at java.lang.Thread.run(Thread.java:748) Caused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.SafeModeException): Cannot create file/hbase-nemo/WALs/node2,60020,1519732668326/node2%2C60020%2C1519732668326.default.1522768773233. Name node is in safe mode. Resources are low on NN. Please add or free up more resources then turn off safe mode manually. NOTE: If you turn off safe mode before adding resources, the NN will immediately return to safe mode. Use \u0026#34;hdfs dfsadmin -safemode leave\u0026#34; to turn safe mode off. at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.checkNameNodeSafeMode(FSNamesystem.java:1418) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFileInt(FSNamesystem.java:2674) at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFile(FSNamesystem.java:2561) at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.create(NameNodeRpcServer.java:593) at org.apache.hadoop.hdfs.server.namenode.AuthorizationProviderProxyClientProtocol.create(AuthorizationProviderProxyClientProtocol.java:111) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.create(ClientNamenodeProtocolServerSideTranslatorPB.java:393) at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java) at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:617) at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1073) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2086) at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:2082) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.Subject.doAs(Subject.java:422) at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1693) at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2080) at org.apache.hadoop.ipc.Client.call(Client.java:1471) at org.apache.hadoop.ipc.Client.call(Client.java:1408) at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:230) at com.sun.proxy.$Proxy16.create(Unknown Source) at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.create(ClientNamenodeProtocolTranslatorPB.java:296) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:256) at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:104) at com.sun.proxy.$Proxy17.create(Unknown Source) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.hbase.fs.HFileSystem$1.invoke(HFileSystem.java:279) at com.sun.proxy.$Proxy18.create(Unknown Source) at org.apache.hadoop.hdfs.DFSOutputStream.newStreamForCreate(DFSOutputStream.java:1897) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1738) at org.apache.hadoop.hdfs.DFSClient.create(DFSClient.java:1698) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:450) at org.apache.hadoop.hdfs.DistributedFileSystem$7.doCall(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystemLinkResolver.resolve(FileSystemLinkResolver.java:81) at org.apache.hadoop.hdfs.DistributedFileSystem.createNonRecursive(DistributedFileSystem.java:446) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1124) at org.apache.hadoop.fs.FileSystem.createNonRecursive(FileSystem.java:1100) at org.apache.hadoop.hbase.regionserver.wal.ProtobufLogWriter.init(ProtobufLogWriter.java:90) at org.apache.hadoop.hbase.wal.DefaultWALProvider.createWriter(DefaultWALProvider.java:361) ... 4 more 可以看到上面的日志出现IO 出现异常,无法获取 log writer:\n2018-04-03 23:19:33,472 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.LogRoller: Aborting java.io.IOException: cannot get log writer 2018-04-03 23:19:33,501 FATAL [regionserver/node2/192.168.2.3:60020.logRoller] regionserver.HRegionServer: ABORTING region server node2,60020,1519732668326: IOE in log roller java.io.IOException: cannot get log writer 而无法获取 log writer, 对日志进行写入的原因是:\nCaused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.namenode.SafeModeException): Cannot create file/hbase-nemo/WALs/node2,60020,1519732668326/node2%2C60020%2C1519732668326.default.1522768773233. Name node is in safe mode. Resources are low on NN. Please add or free up more resources then turn off safe mode manually. NOTE: If you turn off safe mode before adding resources, the NN will immediately return to safe mode. Use \u0026#34;hdfs dfsadmin -safemode leave\u0026#34; to turn safe mode off. NameNode 进入了safe-mode, 关于safe-mode 的描述: \u0026gt;During start up the NameNode loads the file system state from the fsimage and the edits log file. It then waits for DataNodes to report their blocks so that it does not prematurely start replicating the blocks though enough replicas already exist in the cluster. During this time NameNode stays in Safemode. Safemode for the NameNode is essentially a read-only mode for the HDFS cluster, where it does not allow any modifications to file system or blocks. Normally the NameNode leaves Safemode automatically after the DataNodes have reported that most file system blocks are available. If required, HDFS could be placed in Safemode explicitly using bin/hadoop dfsadmin -safemode command. NameNode front page shows whether Safemode is on or off. A more detailed description and configuration is maintained as JavaDoc for setSafeMode().\nNameNode 进入safe-mode 的原因是因为 node-master这台Master 机器的磁盘被应用日志打满了,导 致 NameNode 进入了只读的 safe-mode. 因为NameNode 进入readonly 的safe-mode 就无 法写入日志, 所以 Hbase 在出现异常之后,就开始把Hbase 的信息 dump 出来,并关闭 Region Server, 导致整个Hbase 集群宕机。\n对于node2 Region Server 下线的原因,猜测是 NameNode 服务器的磁盘用完,导致NameNode 进入read-only 的safe-mode, 又因为Hbase 存储的核心之一是WAL(write-ahead-log, 预写日志),较长时间无法写入日志,最终导致 Region Server 下线。\n3 分析小结 经过这样的一翻排查,可以得出结论,最开始 node1 因为Hbase 和 ZooKeeper 的通信出现问题, 被认为是问题节点,下线了Region Server;\n一个星期之后,node-master这台机器在同步的时候 出现问题,想要关闭WAL, 但是却因为没有充足的健康节点来替换出现问题的node1, 导致关闭 WAL 失败,也下线了Region Server. node2 这台机器因为作为 NameNode 的node-master服务器的磁盘用 完,导致NameNode 进入read-only 的safe-mode, 又因为Hbase 存储的核心之一是 WAL(write-ahead-log, 预写日志),较长时间无法写入日志,最终导致 Region Server 下线。\n4 其他 还有一个关键点是为什么Hbase 和Zookeeper 的连接超时,Zookeeper 的日志只是简单地说明:\n2018-03-13 14:47:46,926 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910002 for client /192.168.2.2:51611, probably expired 2018-03-13 14:47:49,612 [myid:1] - INFO [QuorumPeer[myid=1]/0.0.0.0:2181:ZooKeeperServer@588] - Invalid session 0x161d6c1ae910001 for client /192.168.2.2:51612, probably expired 为什么 session 会无效,日志并没有给出说明,个人猜测可能是因为在部署了 Hbase/Zookeeper 的服务器上还部署了应用。\n应用或者是Hbase 导致的长GC 导致ZooKeeper 停顿,并且导致session 超时无效。\n5 结语 和同事交流之后,觉得以上的分析只是基于日志的猜测,可能Hbase 宕机的原因正如我所说, 或者另有原因,所以现在最关键的措施是加上对Hbase 的各种监控。\n在Hbase 宕机的时候, 参考日志和详细的监控,比如连接数,CPU 使用率,内存,集群负载情况,每个节点情况。不然再遇到一次宕机,还是只能看日志,猜原因。\n话分两头,现在的分析主要是基于Hbase 和ZooKeeper 的日志进行分析,简而言之就是捞日 志,查看信息; 捞日志,查看信息;通过工具找出日志中隐藏的关键时机,然后对时机前后发生的事情进行分析,这也是一个有趣的过程。\n只是从1G 多的日志里面找出想要的内容,也不是一个容易的过程。\n","permalink":"https://ramsayleung.github.io/zh/post/2018/hbase_crash/","summary":"1 背景 在2018年4月4号早上,业务方反应Hbase 读超时,无法读取当前数据。然后发现测试环境的 Hbase region server 全部宕机,已经无可用Region Server. 因为","title":"记一次Hbase 宕机原因分析"},{"content":"从Mysql, Hbase 迁移数据\n1 Mysql 数据迁移 搭建完之后就是数据迁移了,mysql 的数据迁移比较简单。在旧的机器用 mysqldump 把所 有的数据导出来,然后传到新的环境然后导出:\n旧环境导出: mysqldump -u root -p --all-databases \u0026gt; all_dbs.sql 新环境导入: mysql -u root -p \u0026lt; all_dbs.sql Mysql 集群在数据迁移的时候是在提供服务的,所以自然会有新数据写入,但因为是测试环 境,所以在迁移过程中可以忽略新数据写入的影响。不然这又会是一个大问题~\n2 Hbase 数据迁移 2.1 Hbase 集群复制(cluster replication) 2.1.1 配置旧集群和新集群 在新的集群,创建和旧集群一样的表结构(table schema)和列族(column family),这样新的集群就知道在接收到旧集群数据时候怎么去保存。下面是具体的步骤:\n通过以下命令启动 Hbase Shell: hbase shell 通过下面的命令获取已有的表的元数据: hbase\u0026gt; describe \u0026#34;content\u0026#34;; 输入结果如下:\nTable content is ENABLED content, {TABLE_ATTRIBUTES =\u0026gt; {coprocessor$1 =\u0026gt; \u0026#39;|org.apache.phoenix.coprocessor.ScanRegionObserver|805306366|\u0026#39;, co processor$2 =\u0026gt; \u0026#39;|org.apache.phoenix.coprocessor.UngroupedAggregateRegionObserver|805306366|\u0026#39;, coprocessor$3 =\u0026gt; \u0026#39;|or g.apache.phoenix.coprocessor.GroupedAggregateRegionObserver|805306366|\u0026#39;, coprocessor$4 =\u0026gt; \u0026#39;|org.apache.phoenix.copr ocessor.ServerCachingEndpointImpl|805306366|\u0026#39;} COLUMN FAMILIES DESCRIPTION {NAME =\u0026gt; \u0026#39;baseinfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536\u0026#39; , IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} {NAME =\u0026gt; \u0026#39;extrainfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536 \u0026#39;, IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} 2 row(s) in 0.3060 seconds 复制以下内容到编辑器,并按要求进行修改: {NAME =\u0026gt; \u0026#39;baseinfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536\u0026#39; , IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} {NAME =\u0026gt; \u0026#39;extrainfo\u0026#39;, DATA_BLOCK_ENCODING =\u0026gt; \u0026#39;NONE\u0026#39;, BLOOMFILTER =\u0026gt; \u0026#39;ROW\u0026#39;, REPLICATION_SCOPE =\u0026gt; \u0026#39;0\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;NONE\u0026#39;, VERSIONS =\u0026gt; \u0026#39;1\u0026#39;, MIN_VERSIONS =\u0026gt; \u0026#39;0\u0026#39;, TTL =\u0026gt; \u0026#39;FOREVER\u0026#39;, KEEP_DELETED_CELLS =\u0026gt; \u0026#39;FALSE\u0026#39;, BLOCKSIZE =\u0026gt; \u0026#39;65536 \u0026#39;, IN_MEMORY =\u0026gt; \u0026#39;false\u0026#39;, BLOCKCACHE =\u0026gt; \u0026#39;true\u0026#39;} 将TTL =\u0026gt; 'FOREVER' with TTL 修改成 org.apache.hadoop.hbase.HConstants::FOREVER\n在列族的描述之间加上逗号,用来在新建的时候分隔列族\n去掉文本中的换汉符(\\n, \\r), 这样文本就会变成单行文本\n通过下面的语句在新的集群创建新的相同的表:\n2.1.2 CopyTable 按照Apache 官方文档的介绍,Hbase 支持两种形式的数据备份,分别是停服和不停服的。\n我选择的是不停服的形式,停服的代价太大。而不停服的数据备份有三种方案,我选择是的 CopyTable方案。\nAdd Peer\n因为 CopyTable 需要源机房和目标机房是网络连通,并且是目标集群在源集群的 peer list里面。所以要先在源集群添加 peer. 按照 Hbase cluster replication 关于添加 peer 的说明:\nadd_peer \u0026lt;ID\u0026gt; \u0026lt;CLUSTER_KEY\u0026gt; Adds a replication relationship between two clusters. + ID — a unique string, which must not contain a hyphen. + CLUSTER_KEY: composed using the following template, with appropriate place-holders: `hbase.zookeeper.quorum:hbase.zookeeper.property.clientPort:zookeeper.znode.parent` + STATE(optional): ENABLED or DISABLED, default value is ENABLED 而 hbase.zookeeper.quorum 可以在目标集群的 $HBASE_HOME/conf/hbase-site.xml 目录找到设置的值; hbase.zookeeper.property.clientPort 可以在 $HBASE_HOME/conf/hbase-site.xml指定或者是在 $ZOOKEEPER_HOME/conf/zoo.cfg通 过 clientPort 指定; zookeeper.znode.parent 默认值是 /hbase\n所以,在 Hbase Shell 运行下面的命令来添加 peer\nadd_peer \u0026#39;1\u0026#39;, \u0026#34;node-master,node1,node2:2181:/hbase\u0026#34; 2.1.3 复制表和数据 CopyTable 的命令说明如下:\n./bin/hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help /bin/hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help Usage: CopyTable [general options] [--starttime=X] [--endtime=Y] [--new.name=NEW] [--peer.adr=ADR] \u0026lt;tablename\u0026gt; 可以指定需要复制的数据的时间间隔,也可以不指定。那么默认是全部数据,以 cset_content 表为例,复制一个小时的数据:\nbin/hbase org.apache.hadoop.hbase.mapreduce.CopyTable --starttime=1265875194289 --endtime=1265878794289 --peer.adr=node-master,node1,node2:2181:/hbase --families=baseinfo,extrainfo cset_content 然后Hadoop 就会启动一个 MapReduce 的Job来运行这个 CopyTable任务。而我要复制所 有的数据,就需要把列族和表都列出来\n3 结语 折腾一波之后,终于把环境弄好。如果目标机房和源机房不同的话,也可以尝试使用 Hbase 的 Exporter 和 Importer\n","permalink":"https://ramsayleung.github.io/zh/post/2018/store_cluster_migrate2/","summary":"从Mysql, Hbase 迁移数据 1 Mysql 数据迁移 搭建完之后就是数据迁移了,mysql 的数据迁移比较简单。在旧的机器用 mysqldump 把所 有的数据导出来,然后传到新的环","title":"记存储集群的一次迁移过程(下)"},{"content":"搭建和配置 Hadoop, Zookeeper, Hbase\n1 前言 最近负责公司测试环境的迁移,主要包括 Hbase+Mysql 存储集群的迁移,消息队列,缓存组件的迁移, 而我打算说说存储集群的迁移。因为公司的机器的Ip 和Host 不便在博文展示,所以我会用:\n192.168.2.1: node-master 192.168.2.2: node1 192.168.2.3: node2 来代替公司的机器和域名。\n2 搭建新环境 2.1 Hadoop 搭建流程 2.1.1 Hadoop 集群的架构 在配置 Hadoop 的主从节点(master/slave)之前,先来了解一下 Hadoop 集群的组件作用;\nmaster 节点负责担任管理分布式文件系统以及进行相应的资源调度的角色: NameNode: 管理分布式文件系统并且感知数据块在集群的存储位置 ResourceManager: 管理 YARN 任务,并且负责在 slave 节点调度和处理 slave 节点负责担任存储真实的数据并且提供运算 YARN 任务的能力的角色: DataNode: 负责物理存储真实的数据 NodeManager: 管理在该节点 YARN 任务的具体执行。 master 和 slave 的角色不一定像上面划分得泾渭分明,比如 master 节点也可以是 dataNode,这个就看具体配置了。\n2.1.2 配置JDK Hadoop 集群需要JAVA 环境,而Linux 的发行版本一般都是默认带有JDK 的,只是OpenJDK 而不是 Oracle JDK, 如果需要修改JDK 的版本,可以自行修改,网上已经有很多安装JDK 的教程,我就不一一讲解了。\n2.1.3 修改host 因为需要不同的机器之间通信,所以需要先配置好Ip 和域名的映射。修改每台机器的 /etc/hosts 文件,加上以下内容:\n192.168.2.1: nodw-master 192.168.2.2: node1 192.168.2.3: node2 2.1.4 新建 hadoop 用户 虽说我可以用我自己的登录名来配置和运行 hadoop, 但是出于安全的考虑,还是在每个节点创建一个专门用来运行 hadoop 集群的用户比较好。\nuseradd hadoop passwd hadoop 2.1.5 SSH 免密码登录 因为在 Hadoop 集群中, node-master 节点会通过SSH连接和其他节点进行通信,所以需要为 Hadoop 集群配置免密码校验的通信。首先以 hadoop 用户身份登录到 node-master 节点,然后生成 SSH的公私钥:\nssh-keygen -b 4096 然后把公钥复制到其他的节点,如果你想要把 node-master也当作dataNote的话,就需要把公钥也复制到 master节点:\nssh-copy-id -i $HOME/.ssh/id_rsa.pub hadoop@node-master ssh-copy-id -i $HOME/.ssh/id_rsa.pub hadoop@node1 ssh-copy-id -i $HOME/.ssh/id_rsa.pub hadoop@node2 谨记:复制的是”公钥”,不是”私钥”.\n2.1.6 安装hadoop 1. 下载hadoop 安装包,以 hadoop登录node-master,采用 wget命令下载: wget http://archive.cloudera.com/cdh5/cdh/5/hadoop-2.6.0-cdh5.7.1.tar.gz\n创建一个hadoop目录,将各个组件都安装在这个目录。 mdkir ~/hadoop tar -zxvf hadoop-2.6.0-cdh5.7.1.tar.gz -C ~/hadoop 2.1.7 修改配置文件 所有修改的 hadoop配置文件都位于 ~/hadoop/etc/hadoop/ 目录\ncd ~/hadoop/etc/hadoop 设置 NameNode 位置\nvim core-site.xml: 修改成以下内容:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.defaultFS\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://node-master:19000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.trash.interval\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;10080\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.trash.checkpoint.interval\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;10080\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 设置 HDFS 路径\nvim hdfs-site.xml,修改内容为:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.replication\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;1\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/home/hadoop/hadoop/data/temp\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master:50070\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.secondary.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node1:50090\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.webhdfs.enabled\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.data.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/home/hadoop/hadoop/data/hdfs\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 将 YARN 设置成任务调度器(Job Scheduler)\ncp mapred-site.xml.template mapred-site.xml 然后修改配置,将 yarn 设置成 MapReduce 操作的默认框架: vim mapred-site.xml:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.framework.name\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;yarn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master:10020\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.webapp.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master:19888\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 配置 YARN\nvim yarn-site.xml:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.acl.enable\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;0\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.hostname\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.aux-services\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;mapreduce_shuffle\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 配置 master 节点\n因为公司的机器内存较大,兼之机器数不多,所以我就把两台机器都都当作 master: vim masters修改文件为:\nnode-master node1 配置 slave 节点\n把全部节点都当作数据几点(dataNode): vim slaves:\nnode-master node1 node2 修改 Hadoop 环境变量配置\n这个配置是否修改就视情况而定了。 vim hadoop-env.sh:\n#因为ssh的端口不是默认的22,需要重新指定 export HADOOP_SSH_OPTS=\u0026#34;-p 9922\u0026#34; #如果报错java_home找不到,可在这里重新指定 #export JAVA_HOME=/usr/java/jdk1.8.0_161/ 在其它的节点安装并且解压,不要修改配置文件 将配置文件同步到slave主机\nscp -r -P 9922 /home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc/hadoop/ node1:/home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc scp -r -P 9922 /home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc/hadoop/ node2:/home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1/etc 修改环境变量(所有节点都要配置) 编辑 `.bash_profile`: `vim ~/.bash_profile` 加入: ```shell export JAVA_HOME=/usr/java/jdk1.8.0_161/ export JRE_HOME=/usr/java/jdk1.8.0_161/jre export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH export HADOOP_HOME=/home/hadoop/hadoop/hadoop-2.6.0-cdh5.7.1 export HBASE_HOME=/home/hadoop/hadoop/hbase-1.2.0-cdh5.7.1 export HADOOP_MAPRED_HOME=${HADOOP_HOME} export HADOOP_COMMON_HOME=${HADOOP_HOME} export HADOOP_HDFS_HOME=${HADOOP_HOME} export YARN_HOME=${HADOOP_HOME} export HADOOP_YARN_HOME=${HADOOP_HOME} export HADOOP_CONF_DIR=${HADOOP_HOME}/etc/hadoop export HDFS_CONF_DIR=${HADOOP_HOME}/etc/hadoop export YARN_CONF_DIR=${HADOOP_HOME}/etc/hadoop PATH=$PATH:$HOME/bin:$JAVA_HOME/bin:$HADOOP_HOME/sbin:$HBASE_HOME/bin:$HADOOP_HOME/bin export PATH ``` 然后加载 `~/.bash_profile`: `source ~/.bash_profile` 格式化 HDFS 就像其它的文件系统那样,在使用之前需要格式化,HDFS 这个分布式文件系统也不例外。在 `node-master`,运行: ```nil hdfs namenode -format ``` 那么,到目前为止, Hadoop 就已经安装和配置好了。 2.1.8 运行和监控 HDFS 启动HDFS\n在 node-master 的 /home/hadoop/hadoop/sbin/ 目录运行下面的命令以启动 HDFS:\n./start-dfs.sh 然后就会启动 NameNode 和 SecondaryNameNode, 然后继续启动 DataNode.\n验证HDFS\n可以在各个节点通过 jps 检查HDFS 的运行状态。比如在 node-master 运行 jps:\n12243 NameNode 2677 ResourceManager 19593 Jps 15036 DataNode node1:\n30464 DataNode 13094 Jps 28589 SecondaryNameNode 停止HDFS\n在 node-master 的 /home/hadoop/hadoop/sbin/ 目录运行下面的命令以停止HDFS:\n./stop-dfs.sh 监控HDFS\n如果你在启动 HDFS 之后,想要获取关于 HDFS 的详细信息,你可以使用 hdfs dfsadmin -report 命令, 例如在 node-master 运行 hdfs dfsadmin -resport, 输出如下:\nConfigured Capacity: 1201169780736 (1.09 TB) Present Capacity: 1129442681745 (1.03 TB) DFS Remaining: 1129442358161 (1.03 TB) DFS Used: 323584 (316 KB) DFS Used%: 0.00% Under replicated blocks: 0 Blocks with corrupt replicas: 0 Missing blocks: 0 Missing blocks (with replication factor 1): 0 ------------------------------------------------- Live datanodes (3): Name: 192.168.2.3:50010 (node2) Hostname: node2 Decommission Status : Normal Configured Capacity: 400389926912 (372.89 GB) DFS Used: 102400 (100 KB) Non DFS Used: 24759164513 (23.06 GB) DFS Remaining: 375630659999 (349.83 GB) DFS Used%: 0.00% DFS Remaining%: 93.82% Configured Cache Capacity: 0 (0 B) Cache Used: 0 (0 B) Cache Remaining: 0 (0 B) Cache Used%: 100.00% Cache Remaining%: 0.00% Xceivers: 11 Last contact: Mon Mar 05 14:05:38 CST 2018 Name: 192.168.2.2:50010 (node1) Hostname: node1 Decommission Status : Normal Configured Capacity: 400389926912 (372.89 GB) DFS Used: 77824 (76 KB) Non DFS Used: 23483977479 (21.87 GB) DFS Remaining: 376905871609 (351.02 GB) DFS Used%: 0.00% DFS Remaining%: 94.13% Configured Cache Capacity: 0 (0 B) Cache Used: 0 (0 B) Cache Remaining: 0 (0 B) Cache Used%: 100.00% Cache Remaining%: 0.00% Xceivers: 7 Last contact: Mon Mar 05 14:05:38 CST 2018 Name: 192.168.2.1:50010 (node-master) Hostname: node-master Decommission Status : Normal Configured Capacity: 400389926912 (372.89 GB) DFS Used: 143360 (140 KB) Non DFS Used: 23483956999 (21.87 GB) DFS Remaining: 376905826553 (351.02 GB) DFS Used%: 0.00% DFS Remaining%: 94.13% Configured Cache Capacity: 0 (0 B) Cache Used: 0 (0 B) Cache Remaining: 0 (0 B) Cache Used%: 100.00% Cache Remaining%: 0.00% Xceivers: 7 Last contact: Mon Mar 05 14:05:39 CST 2018 或者可以使用更加友好的 Web 管理界面,在浏览器输入: http://node-master-ip:50070, 然后你就可以看到如下的监控界面: 使用 HDFS\n既然 HDFS 可以跑起来了,现在就需要添加一点数据以测试 HDFS 了。 在HDFS 的根目录下新建一个 test 目录:\nhdfs dfs -mkdir /test 然后在本地创建一个 helloworld 文件,内容如下:\nBye world! 接着把 helloworld 文件放置到HDFS的 /test 目录下:\nhdfs dfs -put helloworld /test 最后查看文件是否存在:\nhdfs dfs -ls /test 2.2 小结 至此,如果一切顺利的话, 那么Hadoop 集群就运行起来了。因为我是需要用Hbase 作存储集群,暂不需用Yarn 作计算,所以我就没有介绍启动 Yarn 的 流程了。\n2.3 ZooKeeper 搭建流程 因为需要用 ZooKeeper 来管理集群,所以也需要安装 ZooKeeper. 而 ZooKeeper 的安装和 配置也是用 hadoop 用户进行操作的。\n2.3.1 安装zookeeper (每个节点同样的操作) 1. 下载zookeeper安装包,登录主机,采用wget命令下载: wget http://archive.cloudera.com/cdh5/cdh/5/zookeeper-3.4.5-cdh5.7.1.tar.gz\n解压安装到hadoop目录,将各个组件都安装在这个目录。 tar -zxvf zookeeper-3.4.5-cdh5.7.1.tar.gz -C ~/hadoop 2.3.2 配置 ZooKeeper 修改zoo.cfg (所有机器一样的配置): vim /home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1/conf/zoo.cfg, 配置文件内容如下:\n# The number of milliseconds of each tick tickTime=2000 maxSessionTimeout=300000 # The number of ticks that the initial # synchronization phase can take initLimit=10 # The number of ticks that can pass between # sending a request and getting an acknowledgement syncLimit=5 # the directory where the snapshot is stored. # do not use /tmp for storage, /tmp here is just # example sakes. dataDir=/home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1/data # the port at which the clients will connect clientPort=2181 # # Be sure to read the maintenance section of the # administrator guide before turning on autopurge. # # http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance # # The number of snapshots to retain in dataDir #autopurge.snapRetainCount=3 # Purge task interval in hours # Set to \u0026#34;0\u0026#34; to disable auto purge feature #autopurge.purgeInterval=1 server.1=node-master:2888:3888 server.2=node1:2888:3888 server.3=node2:2888:3888 创建myid文件 (不同主机不同数字)\ncd {data_dir} # 按照上面的配置,就应该是/home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1/data vim myid myid的数字要与zoo.cfg配置的一一对应。即要对应:\nserver.1=node-master:2888:3888 server.2=node1:2888:3888 server.3=node2:2888:3888 也就是 node-master 的 myid是1, node1的 myid是 2,依次类推。需要注意的 是,数字前后都不能有空格!\n启动 ZooKeeper\n在 node-master 的 /home/hadoop/hadoop/zookeeper-3.4.5-cdh5.7.1 目录,运行以 下命令:\nsh bin/zkServer.sh start 其它相应的命令如下:\n启动ZK服务: sh bin/zkServer.sh start 查看ZK服务状态: sh bin/zkServer.sh status 停止ZK服务: sh bin/zkServer.sh stop 重启ZK服务: sh bin/zkServer.sh restart 验证 ZooKeeper\n可以通过调用 jps 或者 bin/zkCli.sh 来验证 Zookeeper 的运行情况:\n./zkCli.sh -server 192.168.2.1:2181 2.4 HBase 搭建流程 2.4.1 安装Hbase 下载hbase 安装包,登录主机,采用wget命令下载: wget http://archive.cloudera.com/cdh5/cdh/5/hbase-1.2.0-cdh5.7.1.tar.gz 2、解压安装到hadoop目录(3台主机同样操作) tar -zxvf hbase-1.2.0-cdh5.7.1.tar.gz -C ~/hadoop 2.4.2 修改hbase的配置文件(所有主机一样的配置) 修改 hbase-1.2.0-cdh5.7.1/conf 目录下的文件: 1. 修改 hbase-site.xml 内容如下:\n\u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.rootdir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://node-master:19000/hbase-${user.name}\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.master\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.cluster.distributed\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/home/hadoop/hadoop/data/hbase-${user.name}\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.zookeeper.quorum\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;node-master, node1, node2\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 修改 hbase-env.sh: export JAVA_HOME=/usr/java/jdk1.8.0_161/ (无法识别系统的环境变量,在这里直接指定) export HBASE_MANAGES_ZK=false (关闭自带的zookeeper) export HBASE_SSH_OPTS=\u0026#34;-p 9922\u0026#34; (端口号不是默认的22,要改为9922) 修改 =regionservers=(指定regionservers的主机地址): node-master node1 node2 2.4.3 启动 Hbase 在 node-master 的 /home/hadoop/hadoop/hbase-1.2.0-cdh5.7.1/bin 目录下运行:\nstart-hbase.sh 2.4.4 监控 Hbase 可以在浏览器查看 HBase 集群的信息:\nHMaster web 管理信息:http://192.168.2.1:60010/master-status HregionServer 管理信息:http://192.168.2.1:60030/ http://192.168.2.2:60030/ http://192.168.2.3:60030/\n3 结语 整个 Hbase 集群应该搭建完了,关于Mysql 搭建的文章就太多了,我也不赘言了。至此,所有存储的组件就已经安装完毕并已启用,但是都是没有数据的,接下来我们需要做的是如何将旧的测试环境的数据迁移到新的测试环境。考虑到这篇内容已经很长了,剩下的内容我就另外写一篇博文了。\n4 参考 http://blog.csdn.net/u010824591/article/details/51174099 https://yq.aliyun.com/articles/26415 https://linode.com/docs/databases/hadoop/how-to-install-and-set-up-hadoop-cluster/ ","permalink":"https://ramsayleung.github.io/zh/post/2018/store_cluster_migrate1/","summary":"搭建和配置 Hadoop, Zookeeper, Hbase 1 前言 最近负责公司测试环境的迁移,主要包括 Hbase+Mysql 存储集群的迁移,消息队列,缓存组件的迁移, 而我打算说说存储集群的迁移。因为公司的","title":"记存储集群的一次迁移过程(上)"},{"content":"开发第一个Rust crate 的感受和踩到的坑\n最近写了人生第一个 Rust crate\u0026ndash; rspotify. 虽说并不是什么惊天地,泣鬼神的大作,但是也是我花费了近两个月实现的。\n现在就来聊聊这个开发过程的感悟和踩到的坑\n1 感悟 1.1 函数的缺省值 因为我是参考着 Python 版本的 Spotify API SDK 来写 rspotify的,Spotify 某些API 需要请求的时候附加上默认值,例如在获取一个歌手最热的10首歌的时候需要指定country.\n因为Python 的函数是有缺省参数的,所以用 python 来实现就很方便\ndef artist_top_tracks(self, artist_id, country=\u0026#39;US\u0026#39;): \u0026#34;\u0026#34;\u0026#34; Get Spotify catalog information about an artist\u0026#39;s top 10 tracks by country. Parameters: - artist_id - the artist ID, URI or URL - country - limit the response to one particular country. \u0026#34;\u0026#34;\u0026#34; trid = self._get_id(\u0026#39;artist\u0026#39;, artist_id) return self._get(\u0026#39;artists/\u0026#39; + trid + \u0026#39;/top-tracks\u0026#39;, country=country) 但是用 Rust 来实现的时候,问题就来了,因为Rust 是没有缺省参数的。而Rust 处理缺省参数的策略一般是Builder Pattern:\nstruct Part1 { points: u32, tf: f64, dt: f64 } impl Part1 { fn new() -\u0026gt; Part1 { Part1 { points: 30_u32, tf: 3_f64, dt: 0.1_f64 } } fn tf(mut self, tf: f64) -\u0026gt; Self { self.tf = tf; self } fn points(mut self, points: u32) -\u0026gt; Self { self.points = points; self } fn dt(mut self, dt: f64) -\u0026gt; Self { self.dt = dt; self } fn run(self) { // code here println!(\u0026#34;{:?}\u0026#34;, self); } } //调用函数 Part1::new().points(10_u32).run(); Part1::new().tf(7_f64).dt(15_f64).run(); 具体情况具体分析,就 rspotify 而言, Builder Pattern 并不适用,因为 rspotify 有很多函数都需要缺省参数,而不同函数的缺省值可能又不一样。\n例如,有些函数的 offset参数是 0, 而另外一些函数的 offset 参数是1. 为此,我还在 Reddit 发贴询问意见,PM_ME_WALLPAPER 建议我用Into\u0026lt;Option\u0026lt;T\u0026gt;\u0026gt;:\nfn foo\u0026lt;T: Into\u0026lt;Option\u0026lt;usize\u0026gt;\u0026gt;\u0026gt;(limit: T) { let limit = limit.into().unwrap_or(10); … } 在他的建议下,我把 artist_top_tracks() 修改成:\npub fn artist_top_tracks( \u0026amp;self, artist_id: \u0026amp;mut str, country: impl Into\u0026lt;Option\u0026lt;String\u0026gt;\u0026gt;, ) -\u0026gt; Option\u0026lt;FullTracks\u0026gt; { let mut params: HashMap\u0026lt;\u0026amp;str, String\u0026gt; = HashMap::new(); params.insert(\u0026#34;country\u0026#34;, country.into().unwrap_or(\u0026#34;US\u0026#34;.to_owned())); let trid = self.get_id(Type::Artist, artist_id); let mut url = String::from(\u0026#34;artists/\u0026#34;); url.push_str(\u0026amp;trid); url.push_str(\u0026#34;/top-tracks\u0026#34;); match self.get(\u0026amp;mut url, \u0026amp;mut params) { Some(result) =\u0026gt; { // let mut albums: Albums = ; match serde_json::from_str::\u0026lt;FullTracks\u0026gt;(\u0026amp;result) { Ok(_tracks) =\u0026gt; Some(_tracks), Err(why) =\u0026gt; { eprintln!(\u0026#34;convert albums from String to Albums failed {:?}\u0026#34;, why); None } } } None =\u0026gt; None, } } 虽说不如Python 那样优雅,但是看起来还是不错滴\n1.2 错误处理 对于一个 library 而言,错误处理是设计的重要一环。\n因为我之前只有开发应用的经验, 而开发应用的错误处理和开发类库的错误处理显然需要考虑的东西不一样,所以我还谨慎思考过这个问题。后来,我决定不处理调用Spotify API 或者其他操作导致的错误,将错误进行一次包装(wrap), 然后再返回给library 的调用者。\n最开始的时候,我是自己定义错误类型的,后来觉得过于累赘,就用上error_chain. 用上 error_chain 之后, errors.rs这个文件也非常简单:\n///The kind of spotify error. use serde_json; error_chain! { errors {} foreign_links { Json(serde_json::Error) #[doc = \u0026#34;An error happened while serializing JSON\u0026#34;]; } } 而刚刚我提到了只对错误作简单的包装,得益于 error_chain的设计,这个特性也很容易实现:\npub fn convert_result\u0026lt;\u0026#39;a, T: Deserialize\u0026lt;\u0026#39;a\u0026gt;\u0026gt;(\u0026amp;self, input: \u0026amp;\u0026#39;a str) -\u0026gt; Result\u0026lt;T\u0026gt; { let result = serde_json::from_str::\u0026lt;T\u0026gt;(input) .chain_err(|| format!(\u0026#34;convert result failed, content {:?}\u0026#34;,input))?; Ok(result) } 这个函数是将 Spotify 的响应体映射成对应的 object(例如 playlist, album 等). 如果转换过程出错了,那么就返回convert result failed, content {:?}错误信息之后,返回 serde_json 转换时出现的错误信息。\n1.3 Reddit+clippy 剩下的是在纠结定义一个函数传参的时候是传值,参数是 mutable 还是 immutable, 以及其他类似的考虑。\n或许 Effective Rust 和 More Effective Rust 出现之后,我读完就知道什么样的设计才是 best practice. 因为有诸多设计的不确定,所以在完成rspotify 90% 的代码量之后,我在 Reddit 上发贴,邀请社区的同学来 review code 以帮我完善代码。\n他们的确给了我很多建议,我也根据他们的建议修改 rspotify. 在经过人肉 code review 之后,是时候祭出 clippy 这个大杀器, clippy 就代码的编写给出了非常多的建议,比如将函数 Vec\u0026lt;String\u0026gt; 的参数类型修改成 \u0026amp;[String], 因为函数并没有使用(consume) 这个参数,所以传引用比传值更合适,类似 的建议不胜枚举。\n最后在 clippy 的建议下, 我几乎将所有的 clippy warning 都消除掉。 邀请别人经常帮你 review code 有点不实际,但是 clippy 确是不会因为帮你审查代码而感到厌烦的,真的是非常强大的工具\n2 坑 2.1 Debugger 虽说 Rust 也有Debugger\u0026ndash; gdb-rust. gdb 我以前写c 的时候用过,gdb 熟悉程度虽然谈 不上精通,但是也能熟练使用。但是用gdb-rust 调试并不是非常便利,比如在使用 Rocket 这个Web框架的时候,就很难使用gdb来调试Web程序。\n虽说 intellij-rust 这个 Intellij Idea 的插件也支 持Debugger, 但是只有配合Clion才能使用。因为 只有 Clion 才能调用 gdb, 无奈。所以在开发 rspotify 的时候,我用得都是 println!()调试大法。\n2.2 编译器Bug 战战兢兢地开发着,终于到发布到 crates.io 的大喜日子了,怎知在发布之后一直没办法看到生成的文档,本地不是一切正常么?\n后来在社区 同学的提醒下, 我才发现我踩到了 Rust 编译器的一个bug, 最后我就顺手提交了一个 issue, 虽说这个问题已经在 nightly 里面修复了。\n3 结语 前后两个月的时间,终于发布了 rspotify. 项目不大,但是也是我花费时间,精力去开发的,也得到其他同学的肯定,喔耶 :)\n","permalink":"https://ramsayleung.github.io/zh/post/2018/rspotify/","summary":"开发第一个Rust crate 的感受和踩到的坑 最近写了人生第一个 Rust crate\u0026ndash; rspotify. 虽说并不是什么惊天地,泣鬼神的大作,但是也是我花费了近两个月实现的。 现在就来聊聊","title":"rspotify– 我的第一个Rust crate"},{"content":"自定义错误和error_chain 库\n1 前言 上一篇文章聊到 Rust 的错误处理机制,以及和 Java 的简单比较,现在就来聊一下如何在 Rust 自定义错误,以及引入 error_chain这个库来优雅地进行错误处理。\n还有,少不了用 Java 来做对比咯:)\n1.1 Java 自定义异常 前文简单提到 Java 的错误和异常但是继承自一个 Throwable的父类,既然异常是继承自异常父类的,我们自定义异常的时候, 也可以模仿JDK, 继承一个异常类:\npublic class MyException extends Exception { public MyException(String message) { super(message); } } 这样就定义了属于自己的异常. 只需要继承 Exception,然后调用父类的构造方法。\n不过 对于那些复杂的项目,这样的例子未免过于简单。现在就来看一个我项目的中的一个异常类:\npublic final class MyError extends RuntimeException { /** * */ private static final long serialVersionUID = 1L; private static boolean isFillStack = true; private Integer httpStatusCode; private Integer code; private String message; private MyError(int httpStatusCode, int code, String message) { super(message, null, isFillStack, isFillStack); this.httpStatusCode = httpStatusCode; this.code = code; this.message = message; } public MyError(MyErrorCode myErrorCode, Object... messageArgs) { this(myErrorCode.getHttpStatusCode(), myErrorCode.getErrorCode(), MessageFormat.format(myErrorCode.getMessagePattern(), messageArgs)); } public static MyError throwError(MyErrorCode myErrorCode, Object... messageArgs) { throw new MyError(myErrorCode, messageArgs); } public static MyError internalServerError(String logId) { throw new MyError(MyErrorCode.INTERNAL_SERVER_ERROR, logId); } public static MyError DataError(String logId) { throw new MyError(MyErrorCode.DATA_ERROR, logId); } public static MyError BadParameterError(String logId) { throw new MyError(MyErrorCode.BAD_PARAMETER_ERROR, logId); } } 这是我去掉了多余方法和变量的简化版,但是也足以一叶知秋了。\nMyError这个异常类是 继承于 RuntimeException的,并调用了 RuntimeException的构造方法。\n因为我的项目是 WEB 服务的业务层,要处理大量的逻辑,难免会出现异常.\n比如说可能调用方调用接口 的时候,入参不符合规范,我就抛出一个经过包装的 BadParameterError 异常,对于接 口调用方,这样会比一个单纯的 400 错误要友好,其他的异常也是同理。\n1.2 Rust 自定义错误 对于习惯了 OOP 编程的同学来说,Java 的异常是很容易理解,但是回到 Rust 身上,Rust是没有父类一说的,显然,Rust 是没可能套用 Java 的自定义异常的方式的。\nRust 用的是 trait, trait就有点类似 Java 的 =interface=(只是类似,不是等同!).\n按照 Rust 的规范,Rust 允许开发者定义自己的错误,设计良好的错误应该包含以下的特性:\n使用相同的类型(type)来表示不同的错误 错误中包含对用户友好的提示(我也在上面提到的) 能便捷地与其他类型比较,例如: Good: Err(EmptyVec) Bad: Err(\u0026quot;Please use a vector with at least one element\u0026quot;.to_owned()) 包含与错误相关的信息,例如: Good: Err(BadChar(c, position)) Bad: Err(\u0026quot;+ cannot be used here\u0026quot;.to_owned()) 可以很方便地与其他错误结合 use std::error; use std::fmt; use std::num::ParseIntError; type Result\u0026lt;T\u0026gt; = std::result::Result\u0026lt;T, DoubleError\u0026gt;; #[derive(Debug, Clone)] // 自定义错误类型。 struct DoubleError; // 不同的错误需要展示的信息也不一样,这个就要视情况而定,因为 DoubleError 没有定义额外的字段来保存错误信息 // 所以就现在就简单打印错误信息 impl fmt::Display for DoubleError { fn fmt(\u0026amp;self, f: \u0026amp;mut fmt::Formatter) -\u0026gt; fmt::Result { write!(f, \u0026#34;invalid first item to double\u0026#34;) } } // 实现 error::Error 这个 trait, 对DoubleError 进行包装 impl error::Error for DoubleError { fn description(\u0026amp;self) -\u0026gt; \u0026amp;str { \u0026#34;invalid first item to double\u0026#34; } fn cause(\u0026amp;self) -\u0026gt; Option\u0026lt;\u0026amp;error::Error\u0026gt; { // Generic error, underlying cause isn\u0026#39;t tracked. None } } fn double_first(vec: Vec\u0026lt;\u0026amp;str\u0026gt;) -\u0026gt; Result\u0026lt;i32\u0026gt; { vec.first() // Change the error to our new type. .ok_or(DoubleError) .and_then(|s| s.parse::\u0026lt;i32\u0026gt;() // Update to the new error type here also. .map_err(|_| DoubleError) .map(|i| 2 * i)) } fn print(result: Result\u0026lt;i32\u0026gt;) { match result { Ok(n) =\u0026gt; println!(\u0026#34;The first doubled is {}\u0026#34;, n), Err(e) =\u0026gt; println!(\u0026#34;Error: {}\u0026#34;, e), } } fn main() { let numbers = vec![\u0026#34;42\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; let empty = vec![]; let strings = vec![\u0026#34;tofu\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); } 这段代码的运行结果如下:\nThe first doubled is 84 Error: invalid first item to double Error: invalid first item to double 1.3 error_chain 虽说 Rust 自定义错误很灵活和方便,但是如果每次定义异常都需要实现 Display 和 Error, 未免过于繁琐,现在来介绍 error_chain 这个类库。\nerror_chain 是由 Rust 项目组的 leader\u0026ndash;Brian Anderson 编写的异常处理库,可以让你更舒心简单不粗 暴地定义错误。\n1.3.1 error_chain示例 以上面的 DoubleError为例,并改写 error_chain 的官方例子 以实现相同的效果,代码如下:\n// `error_chain!` 的递归深度 #![recursion_limit = \u0026#34;1024\u0026#34;] //引出 error_chain 和相应的宏 #[macro_use] extern crate error_chain; //将跟错误有关的内容放入 errors module, 其他需要用到这个错误module 的模块就通过 // use errors::* 来引入所有内容 mod errors { // Create the Error, ErrorKind, ResultExt, and Result types error_chain! { errors{Double{ description(\u0026#34;invalid first item to double\u0026#34;) display(\u0026#34;invalid first item to double\u0026#34;) }} } } use errors::*; pub type Result\u0026lt;T\u0026gt; = ::std::result::Result\u0026lt;T, ErrorKind\u0026gt;; fn main() { let numbers = vec![\u0026#34;42\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; let empty = vec![]; let strings = vec![\u0026#34;tofu\u0026#34;, \u0026#34;93\u0026#34;, \u0026#34;18\u0026#34;]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); } fn double_first(vec: Vec\u0026lt;\u0026amp;str\u0026gt;) -\u0026gt; Result\u0026lt;i32\u0026gt; { vec.first() // Change the error to our new type. .ok_or(ErrorKind::Double) .and_then(|s| s.parse::\u0026lt;i32\u0026gt;() // Update to the new error type here also. .map_err(|_| ErrorKind::Double) .map(|i| 2 * i)) } fn print(result: Result\u0026lt;i32\u0026gt;) { match result { Ok(n) =\u0026gt; println!(\u0026#34;The first doubled is {}\u0026#34;, n), Err(e) =\u0026gt; println!(\u0026#34;Error: {}\u0026#34;, e), } } 运行这代码可以得到和上面小节同样的输出。\n1.3.2 error_chain 详解 刚刚就先目睹了一下 error_chain 的芳容了,现在是时候来解剖一下 error_chain, 这次就以 error_chain的 example来解释\n// Simple and robust error handling with error-chain! // Use this as a template for new projects. // `error_chain!` can recurse deeply #![recursion_limit = \u0026#34;1024\u0026#34;] // Import the macro. Don\u0026#39;t forget to add `error-chain` in your // `Cargo.toml`! #[macro_use] extern crate error_chain; // We\u0026#39;ll put our errors in an `errors` module, and other modules in // this crate will `use errors::*;` to get access to everything // `error_chain!` creates. mod errors { // Create the Error, ErrorKind, ResultExt, and Result types error_chain! { } } use errors::*; fn main() { if let Err(ref e) = run() { println!(\u0026#34;error: {}\u0026#34;, e); for e in e.iter().skip(1) { println!(\u0026#34;caused by: {}\u0026#34;, e); } // The backtrace is not always generated. Try to run this example // with `RUST_BACKTRACE=1`. if let Some(backtrace) = e.backtrace() { println!(\u0026#34;backtrace: {:?}\u0026#34;, backtrace); } ::std::process::exit(1); } } // Most functions will return the `Result` type, imported from the // `errors` module. It is a typedef of the standard `Result` type // for which the error type is always our own `Error`. fn run() -\u0026gt; Result\u0026lt;()\u0026gt; { use std::fs::File; // This operation will fail File::open(\u0026#34;contacts\u0026#34;) .chain_err(|| \u0026#34;unable to open contacts file\u0026#34;)?; Ok(()) } 重要的信息例子已经作了注释,现在就来看看用法。首先来看看 main 函数\nfn main() { if let Err(ref e) = run() { println!(\u0026#34;error: {}\u0026#34;, e); for e in e.iter().skip(1) { println!(\u0026#34;caused by: {}\u0026#34;, e); } // The backtrace is not always generated. Try to run this example // with `RUST_BACKTRACE=1`. if let Some(backtrace) = e.backtrace() { println!(\u0026#34;backtrace: {:?}\u0026#34;, backtrace); } ::std::process::exit(1); } } 可以看出,这个函数的大部份逻辑是进行错误处理,例如返回自定义的 Result和 Error, 然后处理这些错误。上面的处理流程显示了 error_chain 从某个错误继承而来的三样信息:最近出现的错误(即e),导致错误的调用链,原来错误的堆栈信息 (e.backtrace())\n2 小结 刚刚的例子只是 error_chain 小试了一波牛刀,如果想要了解更多关于 Rust 异常处理 的细节,就需要看看 Rust 的文档咯\n3 参考 rust by example starting with error chain 24-days-rust-error_chain/ handling errors in rust error chain error handle ","permalink":"https://ramsayleung.github.io/zh/post/2018/error_handle_in_rust_2/","summary":"自定义错误和error_chain 库 1 前言 上一篇文章聊到 Rust 的错误处理机制,以及和 Java 的简单比较,现在就来聊一下如何在 Rust 自定义错误,以及引入 er","title":"Rust的错误处理(二)"},{"content":"拉上Java 来谈谈 Rust的错误处理\n1 前言 每个语言都会有异常处理机制(没有异常处理机制的语言估计也没有人会用了),Rust 自然也不例外,所以今天我就来谈Rust 的异常处理,因为 Rust 的异常处理跟常见的语言 (Java/Python 等)的处理机制差异略大,所以打算拉个上个语言,对比着解释. 没错,这 个光荣的任务就落到了 Java 身上\n2 Java 的异常处理 在谈 Rust 的异常处理之前,为了把它们之前的差异讲清楚,先来聊一下 Java 的异常处理。\nFigure 1: Java exception hierarchy\n如上面的简易图所示, Java 的异常都是继承于 Throwable 这个分类的,而异常又是分 成不同的类型: Error, Exception; Exception 又分成 Checked Exception 和 RuntimeException.\nError 一般都是了出现严重的问题,按照JDK 注释的说法,都是不应该 try-catch的:\nAn {() Error} is a subclass of {() Throwable} that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.\n比如虚拟机挂了,或者JRE 出了问题就可能是 Error,前几天我就遇到一个JRE 的 Bug, 整个项目都挂 了:\nFigure 2: JRE fatal error\n我还顺便给 Oracle 报了个Bug :)\n至于RuntimeException 就是类似数组越界,空指针这些异常,即无法在程序编译时发现,只有在运行的时候才会出 现的问题,所以叫做运行时异常(RuntimeException).\n3 Checked Exception Java的Checked Exception, 也就是Java 要求你必须在函数的类型里面声明或处理它可能抛出的异常。比如,你的函数如果是这样:\nvoid foo(string filename) throws IOException { File file = new File(filename); BufferedReader br = new BufferedReader(new FileReader(file)); String st; while ((st = br.readLine()) != null) System.out.println(st); } } Java 要求你必须在函数头部写上 throws IOException 或者是必须用 try-catch处理这个异常,因为readline() 的方法签名是:\nString readLine(boolean ignoreLF) throws IOException { } 所以编译器要求必须要处理这个异常,否则它就不能编译。\n同理,在使用 foo()这个函数 的时候,可能会抛出 IOException 这个异常,由于编译器看到了这个声明,它会严格检 查你对 foo 函数的用法。\n在我看来,CheckedException是Java 优良的设计之一,正因 为Checked Exception的存在,会更容易编写出正确处理错误的程序,更健壮的程序\n4 Rust 的异常处理 Rust 是一个注重安全(Safety)的语言,而错误处理也是 Rust关注的要点之一。\nRust 主要是将错误划分成两种类型,分别是可恢复的错误(recoverable error) 和不可恢复错误 (unrecoverable error).\n出现可恢复的错误的原因多种多样,例如打开文件的时候,文件找不到或者没有读权限等,开发者就应该对这种可能出现的错误进行处理;\n而不可恢复的错误就可能是Bug 引起的,比如数组越界等。而其他常见的语言一般是没有没有区分 recoverable error和 unrecoverable error的. 比如 Python, 用的就是 Exception.\n而Rust 是没有 Exception, Rust 用 Result\u0026lt;T, E\u0026gt; 表示可恢复错误, 用 panic!() 来表示出现错误,并且中断程序的执行并退出(不可恢复错误)。\nResult 是Rust 标准库的枚举:\npub enum Result\u0026lt;T, E\u0026gt; { Ok(T), Err(E), } T和E都是泛型,T表示程序执行正常的时候的返回值,那E自然是程序出错时的返回 值。以标准库的打开文件的函数为例, std::io::File 的 open() 函数的签名如下:\npub fn open\u0026lt;P: AsRef\u0026lt;Path\u0026gt;\u0026gt;(path: P) -\u0026gt; io::Result\u0026lt;File\u0026gt; { OpenOptions::new().read(true).open(path.as_ref()) } 忽略这个方法的参数,只看返回值类型:io::Result\u0026lt;File\u0026gt;, 又因为有 type Result\u0026lt;T\u0026gt; Result\u0026lt;T, Error\u0026gt;;=\n这个 typedef 语句,所以返回值的完整版本时io::Result\u0026lt;File,io::Error\u0026gt;, 即调用 open 这个函数的时候,可能出现错误,出现错误时候返回一个 io::Error, 如果调用open没有问题的话,就会返回一个 File 的结构体,所以这个就类似 Java 的CheckedException,\n只要声明了函数可能出现问题,在调用函数的时候就必须处理可能出现的错误,不然编译器就不会让你通过(Rust 的编译器就像位父亲那样对开发者耳提面命), 例如:\nmatch File::open(\u0026amp;self.cache_path) { Ok(file) =\u0026gt; println!(\u0026#34;{:?}\u0026#34;,file), Err(why) =\u0026gt; { panic!(\u0026#34;couldn\u0026#39;t open {:?}\u0026#34;, why.description()) } }; 5 Java 的异常传递 在程序中,总会有一些错误需要处理,但是却不应该在错误出现的函数进行处理的情况(或者是,你很懒惰,只想应付一下编译器,不想处理出现的异常 :)\n比如你正在编写一个类 库,里面有很多的IO 操作,有IO 操作的地方就有可能出现IOException. 如果出现异常, 你不应该自己在类库把异常给 try-catch了,如果这样,使用你类库的开发者就没办法知 道程序出现了异常,异常的堆栈也丢了。\n比较合理的做法是,把IOException捕捉了,然后对 IOException 做一层包装,然后再抛给类库的调用者,例如:\npublic void doSomething() throws WrappingException{ try{ doSomethingThatCanThrowException(); } catch (SomeException e){ e.addContextInformation(\u0026#34;there is something happen in doSomething() function, `Some Exception` is raised, balabala\u0026#34;); //throw e; //throw e, or wrap it see next line. throw new WrappingException(e, more information about Some Exception, balabala); } finally { //clean up close open resources etc. } } 当然,你也可以在添加了额外的信息之后,直接把原来的异常抛出来\n6 Rust 的异常传递 刚刚谈了 Java 的异常传递,现在轮到 Rust 的异常传递了,既然Rust 没有 Exception一说,那 Rust 传递的自然也是 Result\u0026lt;T,E\u0026gt; 这个枚举类型(这里针对的是 可恢复错误,不可恢复错误出现错误的时候,会返回错误并弹出程序,自然不存在异常传递).\n先来看看 Rust 的异常传递的例子:\nuse std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -\u0026gt; Result\u0026lt;String, io::Error\u0026gt; { let f = File::open(\u0026#34;hello.txt\u0026#34;); let mut f = match f { Ok(file) =\u0026gt; file, Err(e) =\u0026gt; return Err(e), }; let mut s = String::new(); match f.read_to_string(\u0026amp;mut s) { Ok(_) =\u0026gt; Ok(s), Err(e) =\u0026gt; Err(e), } } 例子来自 Rust Book\n先来看看函数的返回值 Result\u0026lt;String,io::Error\u0026gt;, 也就是说, read_username_from_file 正确执行的时候返回是 String, 错误的时候,返回的是 io::Error. 这里的异常传递是在出现 io::Error的时候,将错误原样返回,不然就是返 回函数执行成功的结果。\n就异常传递的方式而言,Rust 和 Java 是大同小异:声明可能抛出的异常和成功时返回的结果,然后在遇到错误的时候,直接(或者包装一下)返回错误。\n6.1 ? 关键字 虽说 Rust 的异常处理很清晰,但是每次都要 match 然后返回未免太繁琐了,所以 Rust 提供了一个语法糖来显示繁琐的异常传递:用 \u0026ldquo;?\u0026rdquo; 关键字进行异常传递:\nuse std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -\u0026gt; Result\u0026lt;String, io::Error\u0026gt; { let mut f = File::open(\u0026#34;hello.txt\u0026#34;)?; let mut s = String::new(); f.read_to_string(\u0026amp;mut s)?; Ok(s) } 同样的功能,但是模板代码却减少了很多 :)\n6.2 unwrap 和 expect 虽说 Rust 的可恢复错误设计得很优雅,但是每次遇到可能出现错误得地方都要显示地进行 处理,不免让人觉得繁琐.\nRust 也考虑到这种情况了,提供了 unwrap() 和 expect()让你舒心简单粗暴地处理错误:在函数调用成功的时候返回正确的结果,在 出现错误地时候直接 panic!(),并退出程序\n6.2.1 unwrap fn main() { let f = File::open(\u0026#34;hello.txt\u0026#34;).unwrap(); } 打开 hello.txt这个文件,能打开就返回文件 f,不能打开就 panic!() 然后退出程序。\nthread \u0026#39;main\u0026#39; panicked at \u0026#39;called `Result::unwrap()` on an `Err` value: Error { repr: Os { code: 2, message: \u0026#34;No such file or directory\u0026#34; } }\u0026#39;, /stable-dist-rustc/build/src/libcore/result.rs:868 6.2.2 expect expect()和 unwrap()类似,只不过 expect()可以加上额外的信息:\nuse std::fs::File; fn main() { let f = File::open(\u0026#34;hello.txt\u0026#34;).expect(\u0026#34;Failed to open hello.txt\u0026#34;); } 出现错误的时候,除了显示应有的错误信息之外,还会显示你自定义的错误信息:\nthread \u0026#39;main\u0026#39; panicked at \u0026#39;Failed to open hello.txt: Error { repr: Os { code: 2, message: \u0026#34;No such file or directory\u0026#34; } }\u0026#39;, /stable-dist-rustc/build/src/libcore/result.rs:868 以上代码来自 Rust book\n7 结语 以上只是浅谈了 Rust 的错误处理,以及和 Java 的异常处理机制的简单比较,接下来我会 谈谈如何自定义Error以及使用 erro_chain 这个库来优雅地进行错误处理 :)\n如果想了解更多关于 Rust 异常处理的内容,可以查阅 Rust book Error handle\n8 参考 propagating exceptions Rust book IO Result ","permalink":"https://ramsayleung.github.io/zh/post/2018/error_handle_in_rust_1/","summary":"拉上Java 来谈谈 Rust的错误处理 1 前言 每个语言都会有异常处理机制(没有异常处理机制的语言估计也没有人会用了),Rust 自然也不例外,所以","title":"Rust的错误处理(一)"},{"content":"1 前言 目标: 在Eshell中像在bash/zsh中使用fzf那般搜索历史命令\n2 fzf 我的主力Shell 是Eshell, 但是平时我也会用Zsh, 而fzf 是一个非常好用的命令行工具,用了fzf搜索历史命令:\nFigure 1: fzf\n3 Eshell 我日常的操作基本都是在 Eshell 上面进行的,不过 Eshell 是没办法直接像 Bash 那样调用 fzf来查找命令历史的,所以我希望把这个功能迁移到到Eshell 上面来。\n我在 Emacs 使用的补全框架是 Ivy/Counsel,它有一个 counsel-esh-history的命令可以使用 Ivy 来搜索命令,但是没办法使用用户已经输入的内容来过滤命令,所以我就在自己折腾了一个\ncounsel-esh-history 命令。效果如下:\nFigure 2: 感觉很不错嘛 :)\n4 源代码 得益于 Ivy强大的内置函数, 功能实现起来相当便利,完整代码如下:\n(defun samray/esh-history () \u0026#34;Interactive search eshell history.\u0026#34; (interactive) (require \u0026#39;em-hist) (save-excursion (let* ((start-pos (eshell-bol)) (end-pos (point-at-eol)) (input (buffer-substring-no-properties start-pos end-pos))) (let* ((command (ivy-read \u0026#34;Command: \u0026#34; (delete-dups (when (\u0026gt; (ring-size eshell-history-ring) 0) (ring-elements eshell-history-ring))) :preselect input :action #\u0026#39;ivy-completion-in-region-action)) (cursor-move (length command))) (kill-region (+ start-pos cursor-move) (+ end-pos cursor-move)) ))) ;; move cursor to eol (end-of-line) ) 代码不是很复杂, 主要功能是获取用户输入的命令, 然后把所有的历史命令读取出来,最后使用ivy-read内置的ivy-completion-in-region-action功能, 用用户的输入的命令与历史命令进行匹配, 由用户选择最终的命令.\nivy-read是Emacs内置completing-read的函数的强化, 关于ivy-read具体用法可以参考文档ivy-read.\n5 总结 最后, 我也顺便把代码分享到 Emacs社区, 而 manateelazycat也把这段代码的功能加入到aweshell, Oh yeah !\n","permalink":"https://ramsayleung.github.io/zh/post/2017/search_eshell_history_like_fzf/","summary":"1 前言 目标: 在Eshell中像在bash/zsh中使用fzf那般搜索历史命令 2 fzf 我的主力Shell 是Eshell, 但是平时我也会用Zsh, 而","title":"Eshell实现fzf的历史命令搜索功能"},{"content":"python 与嵌入式关系数据库 sqlite3的邂逅\nSQLite 是一个非常优秀的嵌入式数据库,非常轻量,可以与 Mysql, PostgreSQL 这样的 大型数据库互补使用. 而 Python 标准库中的 sqlite3 模块实现了兼容 SQLite 的 Python DB-API 2.0接口, 因此我们可以很方 便地使用 sqlite3 模块来操作 SQLite\n1 入门 1.1 创建数据库 SQLite 数据库是存储在文件系统的单个文件上的,所以如果数据库文件不存在,那么在第一次访问这个数据库,就会创建相应的数据库文件。\nimport os import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; db_exist = os.path.exists(db_filename) conn = sqlite3.connect(db_filename) if db_exist: print(\u0026#39;Database exists\u0026#39;) else: print(\u0026#34;Database does not exist\u0026#34;) conn.close() 上面的例子会在连接数据库之前检查一下数据库文件是否存在,然后使用 connect() 函数连接数据库。\n你在执行该代码之前查看一下当前目录的话,如果不存在 sqlite3_demo.db 的话,那么跑完这段代码,你应该会看到 sqlite3_demo.db 文件的.\n这段代码本身是没有做多少事,我只是用它来阐述一下 SQLite 的原理\n1.2 创建表 那么,现在,让我们用 SQLite 来做点数据库的本份工作。先创建一张表,接下来的操作都会围绕着这张表进行。\nuser.sql:\ncreate table role( name text primary key, description text ); create table user ( id integer primary key autoincrement not null, name text, phone_number integer, birthday date, role text not null references role(name) ); 然后使用 Connection 对象的 executescript() 函数来创建表以及插入对应的数据\nimport os import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; schema_filename = \u0026#39;user.sql\u0026#39; db_exists = os.path.exists(db_filename) with sqlite3.connect(db_filename) as conn: if db_exists: print(\u0026#39;Creating schema\u0026#39;) with open(schema_filename, \u0026#39;rt\u0026#39;) as f: schema = f.read() conn.executescript(schema) print(\u0026#39;Inserting initial data\u0026#39;) conn.executescript(\u0026#34;\u0026#34;\u0026#34; insert into role (name,description) values (\u0026#39;student\u0026#39;,\u0026#39;This is a student\u0026#39;); insert into role (name,description) values (\u0026#39;teacher\u0026#39;,\u0026#39;This is a teacher\u0026#39;); insert into user (id,name,phone_number,birthday,role) values (1,\u0026#39;Samray\u0026#39;,12345678,\u0026#39;2017-11-10\u0026#39;,\u0026#39;student\u0026#39;); insert into user (id,name,phone_number,birthday,role) values (2,\u0026#39;Paul\u0026#39;,3231546,\u0026#39;2017-11-11\u0026#39;,\u0026#39;student\u0026#39;); insert into user (id,name,phone_number,birthday,role) values (3,\u0026#39;Trump\u0026#39;,13254768,\u0026#39;2017-11-12\u0026#39;,\u0026#39;teacher\u0026#39;); \u0026#34;\u0026#34;\u0026#34;) 1.3 检索数据 如果想要使用检索存储在 user 表中的数据,那么就需要从数据库连接对象 Connection 中创建一个 Cursor对象。\n而Cursor 对象负责与数据库进行交互并获取 数据\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: cursor = conn.cursor() cursor.execute(\u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=\u0026#39;student\u0026#39; \u0026#34;\u0026#34;\u0026#34;) for row in cursor.fetchall(): id, name, phone_number, birthday = row print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( id, name, phone_number, birthday)) SQLite3 数据库的查询分成两步。首先,使用 Cursor 对象的 execute() 对象执行查询语句,告诉数据库引擎我们需要什么样的数据,然后,使用 fetchall() 函数把数据集从数据库的返回结果中取出来。\n返回结果是包含着一系列 tuple 的列表,而tuple 中对应着的数据就是 select 语句指定返回的字段值。\nfetchall()函数是把所有符合 条件的结果一次性返回,如果需要的话,我们可以使用fetchone()函数返回单条记录, 或者使用fetchmany()返回固定数量的记录\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: cursor = conn.cursor() cursor.execute(\u0026#34;\u0026#34;\u0026#34; select name, description from role where name=\u0026#39;teacher\u0026#39; \u0026#34;\u0026#34;\u0026#34;) name, description = cursor.fetchone() print(\u0026#39;Role details for {} ({}) \\n\u0026#39;.format(name, description)) cursor.execute(\u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=\u0026#39;student\u0026#39; \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;/nNext 10 tasks:\u0026#39;) for row in cursor.fetchmany(10): id, name, phone_number, birthday = row print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( id, name, phone_number, birthday)) 使用 fetchmany() 函数需要注意的是,当你指定的数量超过了符合条件的全部记录的数量的时候,fetchmany()只会返回全部记录的数量。\n例如上面的代码里面,我想要 fetchmany() 返回10条记录,但是我的数据库只有2条符合条件的数据,而 fetchmany() 之后返回两条记录\n1.4 Row 对象 在先前的内容内,我已经提到,数据库返回的数据行都是以 tuple的形式返回的,所以 程序调用者必须知道查询语句字段的顺序,然后在tuple取出记录的时候把字段名和变量名一一对应上,例如 name, description = cursor.fetchone().\n查询语句中字段不多的时候或许还能记住,但是如果字段值多了起来,就很容易出现问题.\n如果可以像value=dict['key'] 那样使用键值对的形式获取数据,那样就方便很多.\n而sqlite3也有为你提供这样便利的操作,诀窍就在使用 Row 对象。sqlite3 可以把查询结果映 射到 Row 对象,然后我们就可以通过Row[字段名'] 这种方式来获取指定字段对应的值。\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(\u0026#34;\u0026#34;\u0026#34; select name, description from role where name=\u0026#39;teacher\u0026#39; \u0026#34;\u0026#34;\u0026#34;) name, description = cursor.fetchone() print(\u0026#39;Role details for {} ({}) \\n\u0026#39;.format(name, description)) cursor.execute(\u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=\u0026#39;student\u0026#39; \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;/nNext 10 tasks:\u0026#39;) for row in cursor.fetchmany(10): print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( row[\u0026#39;id\u0026#39;], row[\u0026#39;name\u0026#39;], row[\u0026#39;phone_number\u0026#39;], row[\u0026#39;birthday\u0026#39;])) 通过指定 Connection 对象的 row_factory 属性就可以控制查询结果集返回的对象。\n在上面的代码,我们使用了 Row 对象而不是 tuple 来获取数据,而程序的执行结果都是相同,但是程序的健壮性就得到了提高。\n1.5 在查询中使用变量 我们上面的代码里面的查询语句都是硬编码的,不利于扩展。如果你希望可以使用更灵活的查询语句,你可能会去用字符串拼接查询语句。\n但是这样的做法是不被提倡的,因为很容易出现安全问题,比如说 SQL 注入. 比较提倡的方式是在执行 execute() 函数的时候进行 变量替换,使用变量替换可以避免SQL注入攻击,因为那些不被信任的代码没办法被解析。\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; with sqlite3.connect(db_filename) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() sql = \u0026#34;\u0026#34;\u0026#34; select id,name,phone_number,birthday from user where role=:role_name \u0026#34;\u0026#34;\u0026#34; cursor.execute(sql, {\u0026#39;role_name\u0026#39;: \u0026#39;student\u0026#39;}) print(\u0026#39;/nNext 10 tasks:\u0026#39;) for row in cursor.fetchmany(10): print(\u0026#39;{:2d} {} {:\u0026lt;10} [{:\u0026lt;8}]\u0026#39;.format( row[\u0026#39;id\u0026#39;], row[\u0026#39;name\u0026#39;], row[\u0026#39;phone_number\u0026#39;], row[\u0026#39;birthday\u0026#39;])) 如上面的代码所示,使用 :role_name 占位符来表示 role_name变量, 然后在执行 SQL 语句的时候把 role_name的值传到 SQL 语句里面去。\n1.6 批量插入 我们之前提到的插入都是使用 execute() 函数逐条插入的,但是 sqlite3 也是支持批 量插入的, 使用 executemany()函数就可以实现一次插入批量的数据,而函数的底层也 是对插入多条数据的循环进行了优化的,这些就无需调用者操心了。\nuser.csv\nbirthday,name,id,phone_number 2018-11-30,Torres,22,98564311 2010-08-10,Messi,12,81582236 2018-11-21,Saul,9,23564548 import csv import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; data_filename = \u0026#39;users.csv\u0026#39; SQL = \u0026#34;\u0026#34;\u0026#34; insert into user (id,name,phone_number,birthday,role) values (:id,:name,:phone_number,:birthday,\u0026#39;student\u0026#39;) \u0026#34;\u0026#34;\u0026#34; with open(data_filename, \u0026#39;rt\u0026#39;) as csv_file: csv_reader = csv.DictReader(csv_file) with sqlite3.connect(db_filename) as conn: cursor = conn.cursor() cursor.executemany(SQL, csv_reader) 我们从 csv 文件中批量导入数据,而Python 的标准库也内置了 CSV 的解析器,使用 DictReader 就是将 csv 文件解析成 {'id':22,'birthday':'2018-11-30','name':'Torres','phone_number':98564311}的形式 然后配合上面提到的命名变量,把所有数据插入到数据库。\n2 进阶 自定义数据库列类型 SQLite 的数据列原生支持整型(integer), 浮点数(floating point), 文本类型 (text), 并且由 sqlite3 转换成 Python内置的数据类型。\n例如:数据库的整型可以转 换成Python 的 int 或者是 long, 具体取决于值的大小;文本类型默认会转换成 str 类型,除非我们修改了 Connection 对象的 text_factory 属性。\n虽然 SQLite 内部支持的数据类型不多,但是得益于 sqlite3 的内置机制的支持,我们可以 定义程序自己的数据列。\nimport sqlite3 import sys db_filename = \u0026#39;sqlite3_demo.db\u0026#39; sql = \u0026#39;select id,name,birthday from user\u0026#39; def show_birthday(conn): conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(sql) row = cursor.fetchone() for col in [\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;birthday\u0026#39;]: print(\u0026#39;{:\u0026lt;8} {:\u0026lt;10} {}\u0026#39;.format(col, row[col], type(row[col]))) return print(\u0026#39;Without type detection:\u0026#39;) with sqlite3.connect(db_filename) as conn: show_birthday(conn) print(\u0026#39;\\nWith type detection:\u0026#39;) with sqlite3.connect(db_filename, detect_types=sqlite3.PARSE_DECLTYPES,) as conn: show_birthday(conn) 如上面的代码所示,如果你想在Python 数据类型和 SQLite 数据列转换的时候使用 SQLite 原本不支持的类型,你可以在调用 connect() 函数的时候,传一个 detect_types 参数进去,而 PARSE_DECLTYPES 的意思是指转换成字段声明时候的类型, 比如 birthday 声明成 datetime类型,但是没有指定成 PAESE_DECLTYPES 的时候, 转换成 str, 指定后,转换成 datetime.\n现在我们就来说说怎么定义自己的数据列类型:\n我们需要注册两个函数,一个函数把 Python 对象转换成 byte string 存储到数据 库里面去,这个函数被称为 adapter(适配器); 既然有从Python 对象转换到数据库存储 对象的函数,那么自然就有从数据库存储转换成 Python 对象的函数,这个函数被称为 converter(转换器).\n然后就需要使用 register_adapter() 函数将一个函数注册成 adapter 函数,至于register_converter()函数,也是同理可得了。\nimport pickle import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def adapter_func(obj): \u0026#34;\u0026#34;\u0026#34;Covert from python to sqlite3 representation \u0026#34;\u0026#34;\u0026#34; print(\u0026#39;adapter_func({})\\n\u0026#39;.format(obj)) return pickle.dumps(obj) def converter_func(data): \u0026#34;\u0026#34;\u0026#34;Convert from sqlite3 to python representation \u0026#34;\u0026#34;\u0026#34; print(\u0026#39;converter_func({})\u0026#39;.format(data)) return pickle.loads(data) # custom type class MyObj: def __init__(self, arg): self.arg = arg def __str__(self): return \u0026#39;MyObj({!r})\u0026#39;.format(self.arg) # Register functions sqlite3.register_adapter(MyObj, adapter_func) sqlite3.register_converter(\u0026#34;MyObj\u0026#34;, converter_func) # Create some objects to save to_save = [ (MyObj(\u0026#39;this is a value to save\u0026#39;),), (MyObj(42),) ] with sqlite3.connect(db_filename, detect_types=sqlite3.PARSE_DECLTYPES) as conn: conn.execute(\u0026#34;\u0026#34;\u0026#34; create table if not exists obj ( id integer primary key autoincrement not null, data MyObj ) \u0026#34;\u0026#34;\u0026#34;) cursor = conn.cursor() cursor.executemany(\u0026#34;insert into obj (data) values (?)\u0026#34;, to_save) # Query the database for the objects just saved cursor.execute(\u0026#34;select id, data from obj\u0026#34;) for obj_id, obj in cursor.fetchall(): print(\u0026#39;Retrieved\u0026#39;, obj_id, obj) print(\u0026#39; with type\u0026#39;, type(obj)) print() 上面的例子使用了Python 标准库的 pickle 模块,将一个 Python 对象转换成可以保存 到数据库的字符串,然后使用 pickle 把字符串转换成Python 对象。\n这就基本实现了自定义的数据类型。不过我们自己实现的这种自定义数据类型是有局限的,我们只能把整个 Python 对象当作字符串来查询,而没办法针对 Python 对象的属性进行查询,如果你感兴趣的话,你可以看看 Python ORM 框架是怎么实现这些功能的。\n2.1 事务 谈及关系型数据库,必不可少的一定是事务。对于事务的见解,网上的资料都已经浩如烟海 了,那么,就要我们直接来说一下 SQLite 事务的使用\n2.1.1 commit 对数据库的修改操作,无论是新增(insert) 还是更新 (update), 都需要调用 commit() 来保存。\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def show_role(conn): cursor = conn.cursor() cursor.execute(\u0026#39;select name, description from role\u0026#39;) for name, description in cursor.fetchall(): print(\u0026#39; \u0026#39;, name) with sqlite3.connect(db_filename) as conn1: print(\u0026#39;Before changes:\u0026#39;) show_role(conn1) # Insert in one cursor cursor1 = conn1.cursor() cursor1.execute(\u0026#34;\u0026#34;\u0026#34; insert into role (name, description) values (\u0026#39;president\u0026#39;,\u0026#39;well, this is a president\u0026#39;) \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;\\nAfter changes in conn1:\u0026#39;) show_role(conn1) # 在没有提交事务之前,使用其它的数据库连接进行查询 print(\u0026#39;\\nBefore commit:\u0026#39;) with sqlite3.connect(db_filename) as conn2: show_role(conn2) # 提交事务,然后使用另外的数据库连接进行查询 conn1.commit() print(\u0026#39;\\nAfter commit:\u0026#39;) with sqlite3.connect(db_filename) as conn3: show_role(conn3) commit() 函数的调用结果可以被使用若干个数据库连接的程序查询到,在第一个数据库连接插入了一行新的数据,另外两个数据库连接尝试读取到新插入的数据。\n当 show_role() 函数在 conn1 提交事务之前被调用,返回结果就取决于调用 show_role() 是哪个数据连接了。\n因为是通过 conn1来修改数据库,所以它可以看到修改后的数据,但是 conn2看不到。在提交事务之后(commit()) ,通过其他的数据库连接 (conn3)也可以看到修改结果了\n2.1.2 rollback 未提交的修改可以通过调用rollback() 函数全部丢弃。通常 commit() 和 rollback() 函数都是在 try-except 语句块的不同地方被调用的,例如错误异常触发, 事务回滚(rollback)\nimport sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def show_role(conn): cursor = conn.cursor() cursor.execute(\u0026#39;select name, description from role\u0026#39;) for name, description in cursor.fetchall(): print(\u0026#39; \u0026#39;, name) with sqlite3.connect(db_filename) as conn1: print(\u0026#39;Before changes:\u0026#39;) show_role(conn1) try: # Delete cursor1 = conn1.cursor() cursor1.execute(\u0026#34;\u0026#34;\u0026#34; delete from role where name=\u0026#39;president\u0026#39; \u0026#34;\u0026#34;\u0026#34;) print(\u0026#39;\\nAfter delete\u0026#39;) show_role(conn1) # 模拟接下来的操作出现了错误 raise RuntimeError(\u0026#39;This is an error\u0026#39;) except Exception as error: # 丢弃之前的修改 print(\u0026#39;Error:\u0026#39;, error) conn1.rollback() else: # 保存修改,提交事务 conn1.commit() print(\u0026#39;\\nAfter rollback:\u0026#39;) show_role(conn1) 在调用 rollback() 函数回滚事务之后,对数据库的修改都丢弃了。\n2.2 内存型数据库 正如我们先前提到的,SQLite 是文件型数据库,它通过文件系统来管理数据库。但是 SQLite 也可以把整个数据库放到内存中去。\nimport sqlite3 schema_filename = \u0026#39;user.sql\u0026#39; with sqlite3.connect(\u0026#39;:memory:\u0026#39;) as conn: conn.row_factory = sqlite3.Row print(\u0026#39;Creating schema\u0026#39;) with open(schema_filename, \u0026#39;rt\u0026#39;) as f: schema = f.read() conn.executescript(schema) print(\u0026#39;Inserting initial data\u0026#39;) conn.execute(\u0026#34;\u0026#34;\u0026#34; insert into role (name,description) values (\u0026#39;Admin\u0026#39;, \u0026#39;wow, administrator\u0026#39; ) \u0026#34;\u0026#34;\u0026#34;) data = [ (\u0026#39;Xi\u0026#39;, 119, \u0026#39;1910-10-03\u0026#39;,\u0026#39;president\u0026#39;), (\u0026#39;Jiang\u0026#39;, 110, \u0026#39;2020-10-10\u0026#39;,\u0026#39;president\u0026#39;), (\u0026#39;Mao\u0026#39;, 10086, \u0026#39;2010-10-17\u0026#39;,\u0026#39;president\u0026#39;), ] conn.executemany(\u0026#34;\u0026#34;\u0026#34; insert into user (name, phone_number, birthday,role) values (?, ?, ?,?) \u0026#34;\u0026#34;\u0026#34;, data) print(\u0026#39;Dumping:\u0026#39;) for text in conn.iterdump(): print(text) 想要把 SQLite 当作内存型数据库,只需在调用 connect() 函数的时候,使用 :memory: 参数而不是数据库文件的文件名。\n需要注意的是,每一个 connect() 函数都会打开新建一个数据库实例,所以在一个数据库连接上的修改是不会影响其它的连接的。\n而 iterdump() 函数会返回一个迭代器,输出一系列对数据库修改的 SQL.\n最后需要注意的是,使用内存型的数据库是有风险的,要切记这一点。\n2.3 在SQL 使用 Python 函数 SQLite 支持在查询的时候使用注册了的 Python函数的,这个特性就使我们在可以获取到 查询结果之前先对数据进行加工,或者调用Python 函数实现那些 纯SQL 力所不能及的功能\nimport codecs import sqlite3 db_filename = \u0026#39;sqlite3_demo.db\u0026#39; def encrypt(s): print(\u0026#39;Encrypting {!r}\u0026#39;.format(s)) return codecs.encode(s, \u0026#39;rot-13\u0026#39;) def decrypt(s): print(\u0026#39;Decrypting {!r}\u0026#39;.format(s)) return codecs.encode(s, \u0026#39;rot-13\u0026#39;) with sqlite3.connect(db_filename) as conn: conn.create_function(\u0026#39;encrypt\u0026#39;, 1, encrypt) conn.create_function(\u0026#39;decrypt\u0026#39;, 1, decrypt) cursor = conn.cursor() # Raw values print(\u0026#39;Original values:\u0026#39;) query = \u0026#34;select id, name from user\u0026#34; cursor.execute(query) for row in cursor.fetchall(): print(row) print(\u0026#39;\\nEncrypting...\u0026#39;) query = \u0026#34;update user set name = encrypt(name)\u0026#34; cursor.execute(query) print(\u0026#39;\\nRaw encrypted values:\u0026#39;) query = \u0026#34;select id, name from user\u0026#34; cursor.execute(query) for row in cursor.fetchall(): print(row) print(\u0026#39;\\nDecrypting in query...\u0026#39;) query = \u0026#34;select id, decrypt(name) from user\u0026#34; cursor.execute(query) for row in cursor.fetchall(): print(row) print(\u0026#39;\\nDecrypting...\u0026#39;) query = \u0026#34;update user set name = decrypt(name)\u0026#34; cursor.execute(query) 通过 create_function() 注册了两个可供 SQL 使用的函数,而 create_function() 的参数分别是定义函数的名字,函数传递的参数的个数,以及源函数\n3 总结 虽说 SQLite 只是一个嵌入式的轻量数据库,但是麻雀虽小,五脏俱全嘛。\n内置的 sqlite3 库为Python 和 SQLite 的沟通构建了一个便捷的桥梁,但是这个桥梁只是个木桥,如果你希望使用斜拉索跨海大桥的话,你就需要去了解 sqlalchemy, 那是一个功能完善的 ORM 框架 :)\n4 参考 sqlalchemy sqlite sqlite3 python3 module of week ","permalink":"https://ramsayleung.github.io/zh/post/2017/python_with_sqlite3/","summary":"python 与嵌入式关系数据库 sqlite3的邂逅 SQLite 是一个非常优秀的嵌入式数据库,非常轻量,可以与 Mysql, PostgreSQL 这样的 大型数据库互补使用. 而 Python 标准库中的 sqlite3 模块实","title":"用python 来操控 sqlite3"},{"content":"1 博客迁移 我将博客从 Github Page 迁移到现在的博客上,原来基于 Gtihub Page,使用 Emacs, org-mode 和 org-page 的博客其实也相当好用,只是某一些我想要的功能却缺失,所以我就自己花时间动手写了现在这个博客,并且将原来的博文迁移\n\u0026lt;2022-02-25 五\u0026gt;\n没想到五年后,我又从自建的Blog,又迁移回Github Page。当时org-page bug不少,支持不是很及时,虽然org-mode很好用,但最后走到自建的博客的路子上。\n使用Rust练手写的博客很不错,但是写作workflow 还不够滑顺,使用orgmode写作,导出为markdown;对于图片,只能先上传到图床,然后再插入到文章里面。\n所以现在切换回Github Page,使用org-mode和ox-hugo, 不需要修改就可以直接把文章发布成博客,又可以愉快地写文章了。\n2 为什么要写博客 虽说我写博客的时间不长,但是当9月份为止,我已写了不少博文的。那么,为什么要写博文呢?很明显,这是一个费时费精力的工作,那么为什么我还要写呢?我自己思考过这个问题,我觉得,原因有下:\n2.1 技术沉淀 在平时的工作生活中,我免不了会碰到种种问题,了解这些问题,思考解决方案并最终付诸行动,这本身就是一个学习和进步的过程。而把其中的想法感悟写成博文会更有益于我的技术沉淀\n2.2 提高组织文字的能力 对于很多工科生来说,不擅文字,不擅言语表达估计是他们撕不掉的标签之一,但是,学会恰当地表达自己的想法是一项非常重要的技能,写博客可以提高自己的语言组织能力。此外,一位前辈曾经告诫我,如果你连文字都没办法组织好,我怎么相信你可以把你的代码组织好呢?\n2.3 便于了解自我 当在编写博客的时候,你会有诸多的想法和思考,然后你会把你的思考一点一滴付诸于笔尖,你的博文越来越清晰了,你的自己的认识也会越来越清晰,你最终会了解到自己是一个什么样的人,喜欢做的事是什么,想要的又是什么?\n2.4 分享与交流 你有一个苹果,我有一个苹果,我们交换了苹果,我们还只是拥有一个苹果;但是,你有一种想法,我有一种想法,我们交换了想法,我们就有两个想法。写博客就是一种双向的交流方式,笔者介绍自己的观点,读者发表自己的评论,思想由此而激荡,甚至孕育出新的想法\n3 结语 每个人总会有不同的想法,不同的际遇,如果你想和他人分享而不知从何言起,与何人言?何不付诸博客呢?\n","permalink":"https://ramsayleung.github.io/zh/post/2017/blog/","summary":"1 博客迁移 我将博客从 Github Page 迁移到现在的博客上,原来基于 Gtihub Page,使用 Emacs, org-mode 和 org-page 的博客其实也相当好用,只是某一些我想要的功能却缺失,所以我就自己","title":"为什么要写博客"},{"content":"我最近编写了两只京东商品和评论的分布式爬虫来进行数据分析,现在就来分享一下。\n1 爬取策略 众所周知,爬虫比较难爬取的就是动态生成的网页,因为需要解析 JS, 其中比较典型的例子就是淘宝,天猫,京东,QQ 空间等。\n所以在我爬取京东网站的时候,首先需要确定的就是爬取策略。因为我想要爬取的是商品的信息以及相应的评论,并没有爬取特定的商品的需求。所以在分析京东的网页的 url 的时候, 决定使用类似全站爬取的策略。 分析如图:\n可以看出,京东不同的商品类别是对应不同的子域名的,例如 book 对应的是图书, mvd 对应的是音像, shouji 对应的是手机等。\n因为我使用的是获取 \u0026lt;a href\u0026gt; 标签里面的 url 值,然后迭代爬取的策略。所以要把爬取的 url 限定在域名为jd.com 范围内,不然就有可能会出现无限广度。\n此外,有相当多的页面是不会包含商品信息的;例如: help.jd.com, doc.jd.com 等,因此使用 jd.com 这个域名范围实在太大了,所以把所需的子域名都添加到一个 list :\njd_subdomain = [\u0026#34;jiadian\u0026#34;, \u0026#34;shouji\u0026#34;, \u0026#34;wt\u0026#34;, \u0026#34;shuma\u0026#34;, \u0026#34;diannao\u0026#34;, \u0026#34;bg\u0026#34;, \u0026#34;channel\u0026#34;, \u0026#34;jipiao\u0026#34;, \u0026#34;hotel\u0026#34;, \u0026#34;trip\u0026#34;, \u0026#34;ish\u0026#34;, \u0026#34;book\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;health\u0026#34;, \u0026#34;baby\u0026#34;, \u0026#34;toy\u0026#34;, \u0026#34;nong\u0026#34;, \u0026#34;jiu\u0026#34;, \u0026#34;fresh\u0026#34;, \u0026#34;china\u0026#34;, \u0026#34;che\u0026#34;, \u0026#34;list\u0026#34;] 2 提取数据 在确定了爬取策略之后,爬虫就可以不断地进行工作了。那么爬虫怎么知道什么时候才是商品信息的页面呢?再来分析一下京东的商品页面:\n从上面的信息可以看出,每个商品的页面都是以 item.jd.com/xxxxxxx.html 的形式存 在的;而 xxxxxxx 就是该商品的 sku-id. 所以只需对 url 进行解析,子域名为 item 即商品页面,就可以进行爬取。\n页面提取使用 Xpath 即可,也无需赘言。不过,需要注 意的是对商品而言,非常重要的价格就不是可以通过爬取 HTML 页面得到的。\n因为价格是经常变动的,所以是异步向后台请求的。对于这些异步请求的数据,打开控制台,然后刷新,就可以看到一堆的 JS 文件,然后寻找相应的请求带有 \u0026ldquo;money 或者price\u0026rdquo; 之类关 键字的 JS 文件,应该就能找到。\n如果还没办法找出来的话,Firefox 上有一个 user-agent-switcher 的扩展,然后通过这个扩展把自己的浏览器伪装成 IE6, 相信所有 花俏的 JS 都会没了, 只剩下那些不可或缺的 JS, 这样结果应该一目了然了,这么看来 IE6 还是有用滴。最终找到的URL 如下 https://p.3.cn/prices/mgets?callback=jQuery6646724\u0026amp;type=1\u0026amp;area=19_1601_3633_0.137875165\u0026amp;pdtk=9D4RIAHY317A3bZnQNapD7ip5Dg%252F6NXiIXt90Ahk0if2Yyh39PZQCuDBlhN%252FxOch3MpwWpHICu4P%250AVcgcOm11GQ%253D%253D\u0026amp;pduid=14966417675252009727775\u0026amp;pdpin=%25E5%2585%2591%25E9%2587%2591%25E8%25BE%25B0%25E6%2589%258B\u0026amp;pdbp=0\u0026amp;skuIds=J_3356012\u0026amp;ext=10000000\u0026amp;source=item-pc\n不得不说,URL 实在是太长了。\n根据经验,大部分的参数应该都是没什么用的,应该可以去掉的,所以在浏览器就一个个参数去掉,然后试试请求是否成功,如果成功,说明此参数无关重要,最后简化成: http://p.3.cn/prices/mgets?pduid={}\u0026amp;skuIds=J_{} sku_id 即商品页面的 URL中包含的数字,而 pduid 则是一随机整数而已,用 random.randint(1, 100000000) 函数解决。\n3 商品评论 商品的评论也是以 sku-id 为参数通过异步的方式进行请求的,构造请求的方法跟价格类 似,也不需过多赘述。\n只是想要吐嘈一下的是,京东的评论是只能一页页向后翻的,不能跳转。还有一点就是,即使某样商品有 10+w 条评论,最多也只是返回 100页的数据。 略坑\n4 反爬虫策略 商品的爬取策略以及提取策略都确定了,一只爬虫就基本成型了。但是一般比较大型的网站都有反爬虫措施的。所以道高一尺,魔高一丈,爬虫也要有对应的反反爬虫策略\n4.1 禁用 cookie 通过禁用 cookie, 服务器就无法根据 cookie 判断出爬虫是否访问过网站\n4.2 轮转 user-agent 一般的爬虫都会使用浏览器的 user-agent 来模拟浏览器以欺骗服务器 (当然,如果你是一只什么 user-agent都不用耿直的小爬虫,我也无话可说).\n为了提高突破反爬虫策略的成功率,可以定义多个 user-agent, 然后每次请求都随机选择 user-agent。\n4.3 伪装成搜索引擎 要说最著名的爬虫是谁?肯定是搜索引擎,它本质上也是爬虫,而且是非常强大的爬虫。\n而且这些爬虫可以光明正大地去爬取各式网站,相信各式网站也很乐意被它爬。\n那么, 现在可以通过修改 user-agent 伪装成搜索引擎,然后再结合上面的轮转 user-agent,\n伪装成各式搜索引擎:\n\u0026#39;Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)\u0026#39;, \u0026#39;DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)\u0026#39;, \u0026#39;Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)\u0026#39;, \u0026#39;ia_archiver (+http://www.alexa.com/site/help/webmasters; crawler@alexa.com)\u0026#39;, 4.4 代理 IP 虽说可以伪装成搜索引擎,但是因为 http 请求是建立在三次握手之上的,爬虫的 IP 还是会被记录下来的,如果同一个 IP 访问得太频繁,那基本就可以确定是一只爬虫了,然后就把它的 IP 封掉,温和一点的就会叫你输入验证码,不然就返回 403.\n对待这种情况,就需要使用代理 IP 了。\n只是代理 IP 都有不同程度的延迟,并且免费的 IP 大多不能用,所以这是不得而为之了\n5 扩展成分布式爬虫 一台机器的爬虫可能爬取一个网站可能需要 100 天,而且带宽也到达瓶颈了,那么是否可以提高爬取效率呢?\n那就用 100台机器,1天应该就能爬取完 (当然,现实并非如此美好).\n这个就涉及到分布式的爬虫的问题。而不同的分布式爬虫有不同的实现方法,而我选择了 scrapy 和 redis 整合的 scrapy-redis 来实现分布式,URL 的去重以及调度都有了相应的实现了,也无需额外的操心\n6 爬虫监控 既然爬虫从单机变成了分布式,新的问题随之而来:如何监控分布式爬虫呢?在单机的时候,最简单的监控 \u0026ndash; 直接将爬虫的日志信息输出到终端即可。\n但是对于分布式爬虫,这样的做法显然不现实。我最终选择使用 graphite 这个监控工具。\n6.1 scrapy-graphite 参考 Github上 distributed_crawler 的代码,将单机版本的 scrapy-graphite 扩展成基于分布式的 graphite 监控程序,并且实现对 python3 的支持。\n6.2 docker 但是 graphite 只是支持 python2, 并且安装过程很麻烦,我在折腾大半天后都无法安装成功,实在有点沮丧。最后想起了伟大的 docker, 并且直接找到已经打包好的image. 数行命令即解决所有的安装问题,不得不说:docker, 你值得拥有。运行截图:\n7 爬虫拆分 本来爬取商品信息的爬虫和爬取评论的爬虫都是同一只爬虫,但是后来发现,再不使用代理 IP 的情况下,爬取到 150000 条商品信息的时候,需要输入验证码。\n但是爬取商品评论的爬虫并不存在被反爬策略限制的情况。所以我将爬虫拆分成两只爬虫,即使无法爬取商品信息的时候,还可以爬取商品的评论信息。\n8 小结 在爬取一天之后,爬虫成果:\n8.1 评论 8.2 评论总结 8.3 商品信息 商品信息加上评论数约 150+w.\n9 参考及致谢 https://github.com/noplay/scrapy-graphite https://github.com/gnemoug/distribute_crawler https://github.com/hopsoft/docker-graphite-statsd 10 项目源码 https://github.com/samrayleung/jd_spider\n","permalink":"https://ramsayleung.github.io/zh/post/2017/jd_spider/","summary":"我最近编写了两只京东商品和评论的分布式爬虫来进行数据分析,现在就来分享一下。 1 爬取策略 众所周知,爬虫比较难爬取的就是动态生成的网页,因为需要","title":"从京东\"窃取\"150+万条数据"},{"content":"1 发现帅气的提示符 近日,笔者在浏览 Reddit 的时候,发现了一位 Emacs 用户把他的 Eshell 提示符修改得很帅,如图:\n本着拿来主义的想法,我就直接把这位小哥的代码添加到了我的配置文件里面:\n(setq eshell-prompt-function (lambda () (concat (propertize \u0026#34;┌─[\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (user-login-name) \u0026#39;face `(:foreground \u0026#34;red\u0026#34;)) (propertize \u0026#34;@\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (system-name) \u0026#39;face `(:foreground \u0026#34;blue\u0026#34;)) (propertize \u0026#34;]──[\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (format-time-string \u0026#34;%H:%M\u0026#34; (current-time)) \u0026#39;face `(:foreground \u0026#34;yellow\u0026#34;)) (propertize \u0026#34;]──[\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (concat (eshell/pwd)) \u0026#39;face `(:foreground \u0026#34;white\u0026#34;)) (propertize \u0026#34;]\\n\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize \u0026#34;└─\u0026gt;\u0026#34; \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) (propertize (if (= (user-uid) 0) \u0026#34; # \u0026#34; \u0026#34; $ \u0026#34;) \u0026#39;face `(:foreground \u0026#34;green\u0026#34;)) ))) 效果自然是很 sexy.\n2 与原有提示符冲突 但是我原来使用的 eshell-prompt-extra 的效果就被覆盖了。而 eshell_prompt_extra 可以提供的额外信息非常多,包括:git, python virtualenv, 以及远程登录时的主机信息,如图:\n如果用上这个 sexy 的提示符,eshell-extra-prompt 的额外的信息就不能显示,感觉好亏:(\n鱼和熊掌我都想要,似乎太贪心了?怎么办,自己去修改 eshell_prompt_extra 的源码 :).\n3 折腾源码 eshell_prompt_extra 这个包注释加上全部代码也只是 400 行,代码也写得很清晰. 其中大部份是辅助函数,而 Eshell 的提示符效果是通过两个 eshell-theme 函数来实现的。use-package 的配置:\n(use-package eshell-prompt-extras :ensure t :load-path \u0026#34;~/Code/github/eshell-prompt-extras\u0026#34; :config (progn (with-eval-after-load \u0026#34;esh-opt\u0026#34; (use-package virtualenvwrapper :ensure t) (venv-initialize-eshell) (autoload \u0026#39;epe-theme-lambda \u0026#34;eshell-prompt-extras\u0026#34;) (setq eshell-highlight-prompt nil eshell-prompt-function \u0026#39;epe-theme-lambda)) )) 而 epe-theme-lambda 的代码如下:\n(defun epe-theme-lambda () \u0026#34;A eshell-prompt lambda theme.\u0026#34; (setq eshell-prompt-regexp \u0026#34;^[^#\\nλ]*[#λ] \u0026#34;) (concat (when (epe-remote-p) (epe-colorize-with-face (concat (epe-remote-user) \u0026#34;@\u0026#34; (epe-remote-host) \u0026#34; \u0026#34;) \u0026#39;epe-remote-face)) (when epe-show-python-info (when (fboundp \u0026#39;epe-venv-p) (when (and (epe-venv-p) venv-current-name) (epe-colorize-with-face (concat \u0026#34;(\u0026#34; venv-current-name \u0026#34;) \u0026#34;) \u0026#39;epe-venv-face)))) (let ((f (cond ((eq epe-path-style \u0026#39;fish) \u0026#39;epe-fish-path) ((eq epe-path-style \u0026#39;single) \u0026#39;epe-abbrev-dir-name) ((eq epe-path-style \u0026#39;full) \u0026#39;abbreviate-file-name)))) (epe-colorize-with-face (funcall f (eshell/pwd)) \u0026#39;epe-dir-face)) (when (epe-git-p) (concat (epe-colorize-with-face \u0026#34;:\u0026#34; \u0026#39;epe-dir-face) (epe-colorize-with-face (concat (epe-git-branch) (epe-git-dirty) (epe-git-untracked) (let ((unpushed (epe-git-unpushed-number))) (unless (= unpushed 0) (concat \u0026#34;:\u0026#34; (number-to-string unpushed))))) \u0026#39;epe-git-face))) (epe-colorize-with-face \u0026#34; λ\u0026#34; \u0026#39;epe-symbol-face) (epe-colorize-with-face (if (= (user-uid) 0) \u0026#34;#\u0026#34; \u0026#34;\u0026#34;) \u0026#39;epe-sudo-symbol-face) \u0026#34; \u0026#34;)) 代码主要逻辑是调用之前定义的辅助函数,判断是否需要显示 git, python, 远程主机等信息,然后对相应的提示符进行拼接。\n而其中出现得比较频繁的 epe-colorize-with-face 就是作者定义的一个宏(macro), 用来显示字符串以及对应的 face(其实就是不同的颜色啦). 看懂了代码就好办了,现在就可以自己添加一个 Eshell 主题。\n3.1 定义所需的 face 因为我需要显示的 face(颜色), eshell-extra-prompt 并没有定义,所以就只好自己动手啦:\n(defface epe-pipeline-delimiter-face \u0026#39;((t :foreground \u0026#34;green\u0026#34;)) \u0026#34;Face for pipeline theme delimiter.\u0026#34; :group \u0026#39;epe) (defface epe-pipeline-user-face \u0026#39;((t :foreground \u0026#34;red\u0026#34;)) \u0026#34;Face for user in pipeline theme.\u0026#34; :group \u0026#39;epe) (defface epe-pipeline-host-face \u0026#39;((t :foreground \u0026#34;blue\u0026#34;)) \u0026#34;Face for host in pipeline theme.\u0026#34; :group \u0026#39;epe) (defface epe-pipeline-time-face \u0026#39;((t :foreground \u0026#34;yellow\u0026#34;)) \u0026#34;Face for time in pipeline theme.\u0026#34; :group \u0026#39;epe) 然后就是按着原有的 Eshell 提示符来组装一个新的 Eshell 主题了,然后把这个主题定义成 pipeline (其实是我自己也没想出比较新颖的名字啦):\n(defun epe-theme-pipeline () \u0026#34;A eshell-prompt theme with full path, smiliar to oh-my-zsh theme.\u0026#34; (setq eshell-prompt-regexp \u0026#34;^[^#\\nλ]* λ[#]* \u0026#34;) (concat (if (epe-remote-p) (progn (concat (epe-colorize-with-face \u0026#34;┌─[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (epe-remote-user) \u0026#39;epe-pipeline-user-face) (epe-colorize-with-face \u0026#34;@\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (epe-remote-host) \u0026#39;epe-pipeline-host-face)) ) (progn (concat (epe-colorize-with-face \u0026#34;┌─[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (user-login-name) \u0026#39;epe-pipeline-user-face) (epe-colorize-with-face \u0026#34;@\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (system-name) \u0026#39;epe-pipeline-host-face))) ) (concat (epe-colorize-with-face \u0026#34;]──[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (format-time-string \u0026#34;%H:%M\u0026#34; (current-time)) \u0026#39;epe-pipeline-time-face) (epe-colorize-with-face \u0026#34;]──[\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face (concat (eshell/pwd)) \u0026#39;epe-dir-face) (epe-colorize-with-face \u0026#34;]\\n\u0026#34; \u0026#39;epe-pipeline-delimiter-face) (epe-colorize-with-face \u0026#34;└─\u0026gt;\u0026#34; \u0026#39;epe-pipeline-delimiter-face) ) (when epe-show-python-info (when (fboundp \u0026#39;epe-venv-p) (when (and (epe-venv-p) venv-current-name) (epe-colorize-with-face (concat \u0026#34;(\u0026#34; venv-current-name \u0026#34;) \u0026#34;) \u0026#39;epe-venv-face)))) (when (epe-git-p) (concat (epe-colorize-with-face \u0026#34;:\u0026#34; \u0026#39;epe-dir-face) (epe-colorize-with-face (concat (epe-git-branch) (epe-git-dirty) (epe-git-untracked) (let ((unpushed (epe-git-unpushed-number))) (unless (= unpushed 0) (concat \u0026#34;:\u0026#34; (number-to-string unpushed))))) \u0026#39;epe-git-face))) (epe-colorize-with-face \u0026#34; λ\u0026#34; \u0026#39;epe-symbol-face) (epe-colorize-with-face (if (= (user-uid) 0) \u0026#34;#\u0026#34; \u0026#34;\u0026#34;) \u0026#39;epe-sudo-symbol-face) \u0026#34; \u0026#34;)) 4 总结 这样一个新的 Eshell 主题就完工了,然后我给 eshell-extra-prompt 发了一个Pull Request, 最终效果如下:\nEnjoy Emacs, Enjor Tweaking :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/tweak_eshell_prompt/","summary":"1 发现帅气的提示符 近日,笔者在浏览 Reddit 的时候,发现了一位 Emacs 用户把他的 Eshell 提示符修改得很帅,如图: 本着拿来主义的想法,我就直接把这位小哥的代码添加","title":"Eshell提示符优化"},{"content":"1 前言 几天前 Goolge 在 I/O 大会上宣布了 Android 将官方支持 Kotlin, 这意味着 Android开发者可以更好地使用 Kotlin 开发 Android.\n我虽不是 Android 开发者,但是也为 Android 开发者多了一个选择而感到高兴,略显意外的是,接下来到处可以看到 \u0026ldquo;Java已死,Kotlin 当立\u0026rdquo; 之类的言论。\n一群人围在一起诉说被 Java \u0026ldquo;折磨\u0026rdquo; 的血泪史,然后为Kotlin 的到来欢欣鼓舞。我学过挺多的语言,也并不是一个 \u0026ldquo;Java 卫道士\u0026rdquo;.\n但是看到很多人都说 \u0026ldquo;Java 的语法啰嗦,每次都要编写一大段 \u0026ldquo;Setter/Getter\u0026rdquo; 这类的模板代码,还有各种的 Bean; 真的好累\u0026rdquo; 我就觉得其实很多人都是人云亦云,他们也并没有对 Java 有多少关注。\n其实 Java8 发布以后,使用 Java8 的函数式进行编码已经可以减少很多代码了;其次,一个新颖的类库也可以帮 Java 的代码进行瘦身 \u0026ndash; Lombok\n2 Lombok 2.1 简介 很多开发者都对模板代码嗤之以鼻,但是 Java 中就有很多非常类似且改动很少的样板 代码。\n这问题一方面是由于类库的设计决定,另外一方面也是 Java 的自身语言的特性。 而 Lombok 这个项目就是希望通过注解来减少模板代码。\n就注解而言,大多是各类框架用于生成代码 (典型的就是 Spring 和 Hibernate 了),而很少直接使用注解生成的代 码。\n因为如果想要直接在程序中使用注解生成的代码,就意味着在代码进行编译之前,注解就要进行相应的处理。这似乎是没可能发生的事情: 在编译代码之前使用编译后生成的代码。\n但是 Lombok 在 IDE 的配合下就真的做到在开发的时候就插入相应的代码。\n2.2 @Getter and @Setter 百闻不如一见,还是直接看例子吧。\n对于使用过 Java 的开发者而言,我相信他们最熟悉的肯定是 Java 无处不在的封装以及对应的 Getter/Setter. 现在就来看一下如何 为最常见的模板代码瘦身。\n未使用 Lombok 的代码:\nprivate boolean employed = true; private String name; public boolean isEmployed() { return employed; } public void setEmployed(final boolean employed) { this.employed = employed; } protected void setName(final String name) { this.name = name; } 并没有上面特点,还是 \u0026ldquo;旧把式\u0026rdquo;. 那么现在来看一下使用了 Lombok 的同等作用的代 码:\n@Getter @Setter private boolean employed = true; @Setter(AccessLevel.PROTECTED) private String name; 除了必要的变量定义以及 @Getter, @Setter 注解以外,没有了其他的东西的。\n但是使用 Lombok 的代码比原生的 Java 代码少了很多行,定义的类的属性越多,减少的代码数就越可观。\n而 @Getter 和 @Setter 注解的作用就是为一个类的属性生成 getter 和 setter 方法,而这些生成的方法跟我们自己编写的代码是一样的。\n2.3 @NonNull 相信每一个使用过 Java 的开发者都不会对空指针这个异常陌生吧,因为 NullPointException 导致了各种 Bug, 以至于它的发明者 Tony Hoare 都自嘲到他创 造了价值十亿的错误 (\u0026ldquo;Null Reference: The Billion Dollar Mistake\u0026rdquo;).\n因此在Java 的代码中,出于安全性的考虑,对于可能出现空指针的地方,都需要进行空指针 检查,自然无可避免地产生了很多的模板代码。\n而 Lombok 引入的 @NonNull 注解可以让需要进行空指针检查的代码 fast-fail; 这样就无需显示添加空指针检查了。\n当为类 的属性添加了 @NonNull 注解以后,在对应的 setter 函数,Lombok 也会生成对应的 空指针检查。例子:使用 Lombok 对 Family 类添加注解:\n@Getter @Setter @NonNull private List\u0026lt;Person\u0026gt; members; 对应的相同作用的原生代码:\n@NonNull private List\u0026lt;Person\u0026gt; members; public Family(@NonNull final List\u0026lt;Person\u0026gt; members) { if (members == null) throw new java.lang.NullPointerException(\u0026#34;members\u0026#34;); this.members = members; } @NonNull public List\u0026lt;Person\u0026gt; getMembers() { return members; } public void setMembers(@NonNull final List\u0026lt;Person\u0026gt; members) { if (members == null) throw new java.lang.NullPointerException(\u0026#34;members\u0026#34;); this.members = members; } 不得不感慨,使用 Lombok, 敲击键盘的次数都成指数级下降.\n2.4 @EqualsAndHashCode 因为 Java 的 Object 类存在用于比较的 equals() 以及对应的 hashCode() 方法,而很多类都经常需要重写这两个方法来实现比较操作。\n比较的操作大多是逐一比较子类的属性,而计算 hash 值的函数也基本是逐一取各个属性的 hash 值,然后与固定值相乘在相加. 这样的操作并不需要复杂算法,完成的都是重复性的 \u0026ldquo;体力活\u0026rdquo;.\n幸运的是, Lombok 也提供了相应的注解来减少这些模板代码。类级别的 @EqualsAndHashCode 注解可以为指定的属性生成 equals() 方法和 hashCode() 方法。\n默认情况下,所有非静态或者没被标注成 transient 的属性都会被 equals() 和 hashCode() 方 法包含在内。当然,你也可以使用 exclude 声明不需要被包含的属性。\n例子:使用了 @EqualAndHashCode 注解的代码\n@EqualsAndHashCode(callSuper=true,exclude={\u0026#34;address\u0026#34;,\u0026#34;city\u0026#34;,\u0026#34;state\u0026#34;,\u0026#34;zip\u0026#34;}) public class Person extends SentientBeing { enum Gender { Male, Female } @NonNull private String name; @NonNull private Gender gender; private String ssn; private String address; private String city; private String state; private String zip; } 对应的相同作用的原生代码:\npublic class Person extends SentientBeing { enum Gender { /*public static final*/ Male /* = new Gender() */, /*public static final*/ Female /* = new Gender() */; } @NonNull private String name; @NonNull private Gender gender; private String ssn; private String address; private String city; private String state; private String zip; @java.lang.Override public boolean equals(final java.lang.Object o) { if (o == this) return true; if (o == null) return false; if (o.getClass() != this.getClass()) return false; if (!super.equals(o)) return false; final Person other = (Person)o; if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false; if (this.gender == null ? other.gender != null : !this.gender.equals(other.gender)) return false; if (this.ssn == null ? other.ssn != null : !this.ssn.equals(other.ssn)) return false; return true; } @java.lang.Override public int hashCode() { final int PRIME = 31; int result = 1; result = result * PRIME + super.hashCode(); result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); result = result * PRIME + (this.gender == null ? 0 : this.gender.hashCode()); result = result * PRIME + (this.ssn == null ? 0 : this.ssn.hashCode()); return result; } } 模板代码和使用了 Lombok 的代码简洁程度而言,差距越来越大了\n2.5 @Data 下面我就来介绍一下在我项目中使用最频繁的注解 @Data.\n使用 @Data 相当于同时在类级别使用 @EqualAndHashCode 注解以及我未曾提及的 @ToString 注解 (这个应该可以从注解名字猜出注解的作用), 以及为每一个类的属性添加上 @Setter 和 @Getter 注解。\n在一个类使用 @Data, Lombok 还会为该类生成构造函数。\n例子:使用 了 @Data 注解的函数:\n@Data(staticConstructor=\u0026#34;of\u0026#34;) public class Company { private final Person founder; private String name; private List\u0026lt;Person\u0026gt; employees; } 同等作用的原生 Java 代码:\npublic class Company { private final Person founder; private String name; private List\u0026lt;Person\u0026gt; employees; private Company(final Person founder) { this.founder = founder; } public static Company of(final Person founder) { return new Company(founder); } public Person getFounder() { return founder; } public String getName() { return name; } public void setName(final String name) { this.name = name; } public List\u0026lt;Person\u0026gt; getEmployees() { return employees; } public void setEmployees(final List\u0026lt;Person\u0026gt; employees) { this.employees = employees; } @java.lang.Override public boolean equals(final java.lang.Object o) { if (o == this) { return true; } if (o == null) { return false; } if (o.getClass() != this.getClass()) { return false; } final Company other = (Company) o; if (this.founder == null ? other.founder != null : !this.founder.equals(other.founder)) { return false; } if (this.name == null ? other.name != null : !this.name.equals(other.name)) { return false; } if (this.employees == null ? other.employees != null : !this.employees.equals(other.employees)) { return false; } return true; } @java.lang.Override public int hashCode() { final int PRIME = 31; int result = 1; result = result * PRIME + (this.founder == null ? 0 : this.founder.hashCode()); result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode()); return result; } @java.lang.Override public java.lang.String toString() { return \u0026#34;Company(founder=\u0026#34; + founder + \u0026#34;, name=\u0026#34; + name + \u0026#34;,\u0026#34; + \u0026#34; employees=\u0026#34; + employees + \u0026#34;)\u0026#34;; } } 差别更加显而易见了\n3 小结 我在日常的开发中,使用的开发语言主要是 Java, 我也学习过其他的语言,所以 Java 和其他语言相比的优缺点也了然于心。\nJava 绝佳的工程性,优秀的 OOP 范式,以及大量的类库,框架 (例如 Spring \u0026ldquo;全家桶\u0026rdquo;), 以及 JIT 带来的接近 C++ 的性能,但是 Java 语法实在啰嗦,需要编写很多的模板代码,以至于经常出现将小项目写成中项目,中项目写成大项目的烦恼,更被戏称为 \u0026ldquo;搬砖\u0026rdquo;.\n现在看来,Lombok 为 Java 减少的模板代码实在算是造福 Java 开发者,让开发者在获得 Java 优势的时候,还可以尽量少地打字,可谓是来的及时。\n4 参考 https://projectlombok.org/index.html ","permalink":"https://ramsayleung.github.io/zh/post/2017/lombok/","summary":"1 前言 几天前 Goolge 在 I/O 大会上宣布了 Android 将官方支持 Kotlin, 这意味着 Android开发者可以更好地使用 Kotlin 开发 Android. 我虽不是 Android 开发者,但是也为 Android 开发者多了一个选择","title":"为Java瘦身 – Lombok"},{"content":"最近我需要为运行的分布式系统某部分模块构造系统唯一的ID, 而 ID 需要是数字的形式,并应该尽量的短。不得不说,这是一个有趣的问题\n1 若干实现策略 查阅完相关的资料,发现为分布式系统生成唯一 ID 方法挺多的,例如:\nUUID 使用一个 ticket server, 即中央的服务器,各个节点都从中央服务器取 ID Twitter 的 Snowflake 算法 Boundary 的 flake 算法 其中 UUID 生成的 ID 是字符串+数字,不适用; ticket server 的做法略麻烦,笔者 并不想为了个 ID 还要去访问中央服务器;剩下就是 Snowflake 和 flake 算法, flake 算法生成的是 128 位的 ID, 略长;所以最后笔者选择了 Snowflake 算法。\n2 Snowflake 算法实现 本来 Twitter 的算法是有相应实现的,不过后来删除了;笔者就只好自己卷起袖子自己 实现了:(\n虽说 Twitter 没有了相应的实现,但是 Snowflake 算法原理很简单,实现起来并不难.\n2.1 Snowflake 算法 Snowflake 算法生成 64 位的 ID, ID 的格式是 41 位的时间戳 + 10 位的截断的 mac 地址 + 12 位递增序列:\n\u0026#34;\u0026#34;\u0026#34;id format =\u0026gt; timestamp | machineId|sequence 41 | 10 |12 \u0026#34;\u0026#34;\u0026#34; 2.2 Snowflake 实现 2.2.1 生成时间戳 Java 内置了生成精确到毫秒的时间戳的方法,非常便利:\nlong timestamp = System.currentTimeMillis(); 2.2.2 生成递增序列 12 bits 的最大值是 2**12=4096, 所以生成递增序列也非常简单:\nprivate final long sequenceMax = 4096; //2**12 private volatile long sequence = 0L; public void generateId(){ //do something sequence = (sequence + 1) % sequenceMax; //do something } 2.2.3 获取 Mac 地址 我们通过获取当前机器的 IP 地址以获取对应的物理地址:\nprotected long getMachineId() throws GetHardwareIdFailedException { try { InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); long id; if (network == null) { id = 1; } else { byte[] mac = network.getHardwareAddress(); id = ((0x000000FF \u0026amp; (long) mac[mac.length - 1]) | (0x0000FF00 \u0026amp; (((long) mac[mac.length - 2]) \u0026lt;\u0026lt; 8))) \u0026gt;\u0026gt; 6; } return id; } catch (SocketException e) { throw new GetHardwareIdFailedException(e); } catch (UnknownHostException e) { throw new GetHardwareIdFailedException(e); } } 又因为 Mac 地址是6 个字节 (48 bits),而需要的只是 10 bit, 所以需要取最低位的 2个字节 (16 bits),然后右移 6 bits 以获取 10 个 bits 的 Mac地址\n2.3 Snowflake 完整代码 下面是 Snowflake 的 Java 实现:\npublic class IdGenerator { // id format =\u0026gt; // timestamp |datacenter | sequence // 41 |10 | 12 private final long sequenceBits = 12; private final long machineIdBits = 10L; private final long MaxMachineId = -1L ^ (-1L \u0026lt;\u0026lt; machineIdBits); private final long machineIdShift = sequenceBits; private final long timestampLeftShift = sequenceBits + machineIdBits; private static final Object lock=new Object(); private final long twepoch = 1288834974657L; private final long machineId; private final long sequenceMax = 4096; //2**12 private volatile long lastTimestamp = -1L; private volatile long sequence = 0L; private static volatile IdGenerator instance; public static IdGenerator getInstance() throws Exception { IdGenerator generator=instance; if (instance == null) { synchronized(lock){ generator=instance; if(generator==null){ generator=new IdGenerator(); instance=generator; } } } return generator; } private IdGenerator() throws Exception { machineId = getMachineId(); if (machineId \u0026gt; MaxMachineId || machineId \u0026lt; 0) { throw new Exception(\u0026#34;machineId \u0026gt; MaxMachineId\u0026#34;); } } public synchronized Long generateLongId() throws Exception { long timestamp = System.currentTimeMillis(); if (timestamp \u0026lt; lastTimestamp) { throw new Exception( \u0026#34;Clock moved backwards. Refusing to generate id for \u0026#34; + ( lastTimestamp - timestamp) + \u0026#34; milliseconds.\u0026#34;); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) % sequenceMax; if (sequence == 0) { timestamp = tillNextMillis(lastTimestamp); } } else { sequence = 0; } lastTimestamp = timestamp; Long id = ((timestamp - twepoch) \u0026lt;\u0026lt; timestampLeftShift) | (machineId \u0026lt;\u0026lt; machineIdShift) | sequence; return id; } protected long tillNextMillis(long lastTimestamp) { long timestamp = System.currentTimeMillis(); while (timestamp \u0026lt;= lastTimestamp) { timestamp = System.currentTimeMillis(); } return timestamp; } protected long getMachineId() throws GetHardwareIdFailedException { try { InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); long id; if (network == null) { id = 1; } else { byte[] mac = network.getHardwareAddress(); id = ((0x000000FF \u0026amp; (long) mac[mac.length - 1]) | (0x0000FF00 \u0026amp; (((long) mac[mac.length - 2]) \u0026lt;\u0026lt; 8))) \u0026gt;\u0026gt; 6; } return id; } catch (SocketException e) { throw new GetHardwareIdFailedException(e); } catch (UnknownHostException e) { throw new GetHardwareIdFailedException(e); } } } 正如笔者所言,算法并不难,就是分别获取时间戳, mac 地址,和递增序列号,然后移位得到 ID. 但是在具体的实现中还是有一些需要注意的细节的。\n2.3.1 线程同步 因为算法中使用到递增的序列号来生成 ID,而在实际的开发或者生产环境中很可能不止一个线程在使用 IdGenerator 这个类,如果这样就很容易出现不同线程的竞争问题,所以我使用了单例模式来生成 ID, 一方面更符合生成器的设计,另一方面因为对生成 ID的方法进行了同步,就保证了不会出现竞争问题。\n2.3.2 同一毫秒生成多个 ID 因为序列号长度是 12个 bit, 那么序列号最大值就是2**12=4096了,此外时间戳是精确到毫秒的,这就是意味着,当一毫秒内,产生超过 4096 个 ID 的时候就会出现重复的ID.\n这样的情况并不是不可能发生,所以要对此进行处理;所以在 generateId() 函数中:\nif (lastTimestamp == timestamp) { sequence = (sequence + 1) % sequenceMax; if (sequence == 0) { timestamp = tillNextMillis(lastTimestamp); } } 有以上的一段代码。当现在的时间戳与之前的时间戳一致,那么就意味着还是同一毫秒,如果序列号为 0, 就说明已经产生了 4096 个 ID了,继续产生 ID,就会出现重复 ID, 所以要等待一毫秒,这个就是 tillNextMills() 函数的作用了。\n3 小结 算法虽然简单,但是在找到 Snowflake 算法之前,笔者尝试了挺多的算法,但是都是因为不符合要求而被一一否决, 而 Snowflake 算法虽然简单,但是胜在实用。最后附上我写的 snowflake 算法的 Python 实现: Snowfloke\n","permalink":"https://ramsayleung.github.io/zh/post/2017/distributed_system_unique_id/","summary":"最近我需要为运行的分布式系统某部分模块构造系统唯一的ID, 而 ID 需要是数字的形式,并应该尽量的短。不得不说,这是一个有趣的问题 1 若干实现策略 查","title":"关于分布式系统唯一ID的探究"},{"content":"笔者近来闲来无事,又因为有需要构造全局唯一 ID 的需求,所以就去看了 UUID 这个提供稳定的系统唯一标识符的类的源码\n1 UUID variant 事实上是存在很多中 UID 的不同实现的的,但是 UUID 里面默认是使用 \u0026ldquo;加盐\u0026rdquo;(Leach-Salz)实现,但是也可以使用其他的实现。\n2 Layout of variant2(Leach-Salz) UUID 加盐的 UUID 的结构布局如下:最高位 (most significant) 的64 位长整型值由下面的的无符号位组成:\n0xFFFFFFFF00000000 time_low //时间的低位值 0x00000000FFFF0000 time_mid //时间的中位值 0x000000000000F000 version // 说明 UUID 的类型,1,2,3,4 分别代表 基于时间,基于 DEC,基于命名,和随机产生的 UUID 0x0000000000000FFF time_hi //时间的高位值 最低位 (least significant) 的 64 位长整型由以下的无符号位组成:\n0xC000000000000000 variant //说明UUID 的结构布局,并且只有在类型 2 (加盐类型), 结构布局才有效 0x3FFF000000000000 clock_seq 0x0000FFFFFFFFFFFF node 3 UUID constructor UUID 类有两个构造函数,分别是 public 和 private 修饰的构造函数\n3.1 private UUID private 类型的构造函数以一个 byte 数组为构造参数:\n/* * Private constructor which uses a byte array to construct the new UUID. */ private UUID(byte[] data) { long msb = 0; long lsb = 0; assert data.length == 16 : \u0026#34;data must be 16 bytes in length\u0026#34;; for (int i=0; i\u0026lt;8; i++) msb = (msb \u0026lt;\u0026lt; 8) | (data[i] \u0026amp; 0xff); for (int i=8; i\u0026lt;16; i++) lsb = (lsb \u0026lt;\u0026lt; 8) | (data[i] \u0026amp; 0xff); this.mostSigBits = msb; this.leastSigBits = lsb; } private 构造器完成的工作主要是通过左移位,与运算和或运算对 mostSigBit 和 leastSigBit 赋值。 private的构造函数只能在类本身被调用, 该构造器的用法会在接下来阐述。\n3.2 public UUID public 类型的构造器接受两个 long 类型的参数,即上面提到的最高位和最低位:\n/** * Constructs a new {@code UUID} using the specified data. {@code * mostSigBits} is used for the most significant 64 bits of the {@code * UUID} and {@code leastSigBits} becomes the least significant 64 bits of * the {@code UUID}. * * @param mostSigBits * The most significant bits of the {@code UUID} * * @param leastSigBits * The least significant bits of the {@code UUID} */ public UUID(long mostSigBits, long leastSigBits) { this.mostSigBits = mostSigBits; this.leastSigBits = leastSigBits; } 使用最高位和最低位的值来构造 UUID, 而最高位和最低位的赋值是在 private 的构造器里面完成的。\n4 UUID type 4.1 type 4 \u0026ndash; randomly generated UUID 现在就看看使用频率最高的 UUID 类型 \u0026ndash; 随机的 UUID 以及随机生成 UUID 的函数: randomUUID()\n/** * Static factory to retrieve a type 4 (pseudo randomly generated) UUID. * * The {@code UUID} is generated using a cryptographically strong pseudo * random number generator. * * @return A randomly generated {@code UUID} */ public static UUID randomUUID() { SecureRandom ng = Holder.numberGenerator; byte[] randomBytes = new byte[16]; ng.nextBytes(randomBytes); randomBytes[6] \u0026amp;= 0x0f; /* clear version */ randomBytes[6] |= 0x40; /* set to version 4 */ randomBytes[8] \u0026amp;= 0x3f; /* clear variant */ randomBytes[8] |= 0x80; /* set to IETF variant */ return new UUID(randomBytes); } 关于调用到的 Holder 变量的定义:\n/* * The random number generator used by this class to create random * based UUIDs. In a holder class to defer initialization until needed. */ private static class Holder { static final SecureRandom numberGenerator = new SecureRandom(); } 上面用到 java.security.SecureRandom 类来生成字节数组, SecureRandom 是被认为是达到了加密强度 (cryptographically strong) 并且因为不同的 JVM 而有不同的实现的。所以可以保证产生足够 \u0026ldquo;随机\u0026quot;的随机数以保证 UUID 的唯一性。\n然后在即将用来构造的 UUID 的字节数组重置和添加关于 UUID 的相关信息,例如版本,类型信息等,然后把处理好的字节数组传到 private 的构造器以构造 UUID。这里的randomUUID 静态方法就是通过静态工厂的方式构造 UUID.\n4.2 type 3 \u0026ndash; name-based UUID 在上面关于 UUID 结构布局的时候提到,UUID 有四种类型的实现,而类型3 就是基于命名的实现:\n/** * Static factory to retrieve a type 3 (name based) {@code UUID} based on * the specified byte array. * * @param name * A byte array to be used to construct a {@code UUID} * * @return A {@code UUID} generated from the specified array */ public static UUID nameUUIDFromBytes(byte[] name) { MessageDigest md; try { md = MessageDigest.getInstance(\u0026#34;MD5\u0026#34;); } catch (NoSuchAlgorithmException nsae) { throw new InternalError(\u0026#34;MD5 not supported\u0026#34;, nsae); } byte[] md5Bytes = md.digest(name); md5Bytes[6] \u0026amp;= 0x0f; /* clear version */ md5Bytes[6] |= 0x30; /* set to version 3 */ md5Bytes[8] \u0026amp;= 0x3f; /* clear variant */ md5Bytes[8] |= 0x80; /* set to IETF variant */ return new UUID(md5Bytes); } MessageDigest 是 JDK 提供用来计算散列值的类,使用的散列算法包括Sha-1,Sha-256 或者是 MD5 等等。\nnameUUIDFromBytes 使用 MD5 算法计算传进来的参数 name 的散列值,然后在散列值重置,添加 UUID 信息,然后再使用生成的散列值 (字节数组)传递给 private 构造器以构造 UUID.\n这里的 nameUUIDFromBytes 静态方法也是通过静态工厂的方式构造 UUID.\n4.3 type 2 \u0026ndash; DEC security 在 JDK 的 UUID 类中并未提供 基于 DEC 类型的 UUID 的实现。\n4.4 type 1 \u0026ndash; time-based UUID 与基于命名和随机生成的 UUID 都有一个静态工厂方法不一样, 基于时间的 UUID 并不存在静态工厂方法,time-based UUID 是基于一系列相关的方法的:\n4.4.1 timestamp public long timestamp() { if (version() != 1) { throw new UnsupportedOperationException(\u0026#34;Not a time-based UUID\u0026#34;); } return (mostSigBits \u0026amp; 0x0FFFL) \u0026lt;\u0026lt; 48 | ((mostSigBits \u0026gt;\u0026gt; 16) \u0026amp; 0x0FFFFL) \u0026lt;\u0026lt; 32 | mostSigBits \u0026gt;\u0026gt;\u0026gt; 32; } 60 个bit长的时间戳是由上面提到的 time_low time_mid time_hi 构造而成的。\n而时间的计算是从 UTC 时间的 1582 年 10月 15 的凌晨开始算起,结果的值域在 100-nanosecond 之间。\n但是这个时间戳的值只是对基于时间的 UUID 有效的,对于其他类型的 UUID, timestamp() 方法会抛出UnsuportedOperationException异常。\n4.4.2 clockSequence() public int clockSequence() { if (version() != 1) { throw new UnsupportedOperationException(\u0026#34;Not a time-based UUID\u0026#34;); } return (int)((leastSigBits \u0026amp; 0x3FFF000000000000L) \u0026gt;\u0026gt;\u0026gt; 48); } 14 个 bit 长的时钟序列值是从 该UUID 的时钟序列域构造出来的(clock sequence filed).\n而时钟序列域通常是用来保证基于时间的 UUID 的唯一性。跟 timestamp() 函数一样, clockSequence() 函数也只对基于时间的 UUID 有效。 对于其他类型的 UUID, 它会抛出UnsuportedOperationException异常。\n4.4.3 node() 48 个 bit 长的节点值是从该 UUID 的节点域 (node filed) 构造出来的。节点域通过保存运行 JVM 机器的局域网地址 (IEEE 802) 来保证该机器生成 UUID 的空间唯一性。\n和上述方法一样, node() 方法只对基于时间的 UUID 有效,对于其他类型的 UUID 该方法会抛出UnsuportedOperationException异常。\n对应 field 的图示\n0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time_low | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time_mid | time_hi_and_version | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |clk_seq_hi_res | clk_seq_low | node (0-1) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | node (2-5) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 5 FromString()/ToString() 5.1 toString() 以字符串的形式表示 UUID, 格式说明:\nhexDigit = \u0026#34;0\u0026#34; | \u0026#34;1\u0026#34; | \u0026#34;2\u0026#34; | \u0026#34;3\u0026#34; | \u0026#34;4\u0026#34; | \u0026#34;5\u0026#34; | \u0026#34;6\u0026#34; | \u0026#34;7\u0026#34; | \u0026#34;8\u0026#34; | \u0026#34;9\u0026#34; | \u0026#34;a\u0026#34; | \u0026#34;b\u0026#34; | \u0026#34;c\u0026#34; | \u0026#34;d\u0026#34; | \u0026#34;e\u0026#34; | \u0026#34;f\u0026#34; | \u0026#34;A\u0026#34; | \u0026#34;B\u0026#34; | \u0026#34;C\u0026#34; | \u0026#34;D\u0026#34; | \u0026#34;E\u0026#34; | \u0026#34;F\u0026#34; hexOctet = \u0026lt;hexDigit\u0026gt;\u0026lt;hexDigit\u0026gt; time_low = 4*\u0026lt;hexOctet\u0026gt; time_mid = 2*\u0026lt;hexOctet\u0026gt; time_high_and_version = 2*\u0026lt;hexOctet\u0026gt; variant_and_sequence = 2*\u0026lt;hexOctet\u0026gt; node = 6*\u0026lt;hexOctet\u0026gt; UUID = \u0026lt;time_low\u0026gt; \u0026#34;-\u0026#34; \u0026lt;time_mid\u0026gt; \u0026#34;-\u0026#34; \u0026lt;time_high_and_version\u0026gt; \u0026#34;-\u0026#34; \u0026#34;variant_and_sequence\u0026#34; \u0026#34;-\u0026#34; \u0026lt;node\u0026gt; 而关于这些不同 field 的大小,之前的内容已经有图示,需要的可以去回顾。\n/** Returns val represented by the specified number of hex digits. */ private static String digits(long val, int digits) { long hi = 1L \u0026lt;\u0026lt; (digits * 4); return Long.toHexString(hi | (val \u0026amp; (hi - 1))).substring(1); } public String toString() { return (digits(mostSigBits \u0026gt;\u0026gt; 32, 8) + \u0026#34;-\u0026#34; + digits(mostSigBits \u0026gt;\u0026gt; 16, 4) + \u0026#34;-\u0026#34; + digits(mostSigBits, 4) + \u0026#34;-\u0026#34; + digits(leastSigBits \u0026gt;\u0026gt; 48, 4) + \u0026#34;-\u0026#34; + digits(leastSigBits, 12)); } 5.2 fromString() 与 toString() 函数功能相反, fromString() 函数的作用就是将字符串形式的对象解码成 UUID 对象:\npublic static UUID fromString(String name) { String[] components = name.split(\u0026#34;-\u0026#34;); if (components.length != 5) throw new IllegalArgumentException(\u0026#34;Invalid UUID string: \u0026#34;+name); for (int i=0; i\u0026lt;5; i++) components[i] = \u0026#34;0x\u0026#34;+components[i]; long mostSigBits = Long.decode(components[0]).longValue(); mostSigBits \u0026lt;\u0026lt;= 16; mostSigBits |= Long.decode(components[1]).longValue(); mostSigBits \u0026lt;\u0026lt;= 16; mostSigBits |= Long.decode(components[2]).longValue(); long leastSigBits = Long.decode(components[3]).longValue(); leastSigBits \u0026lt;\u0026lt;= 48; leastSigBits |= Long.decode(components[4]).longValue(); return new UUID(mostSigBits, leastSigBits); } 6 使用场景 UUID 一般用来生成全局唯一标识符,那么 UUID 是否能保证唯一呢?以UUID.randomUUID() 生成的 UUID 为例,从上面的源码,除了 version 和 variant是固定值之外,另外的 14 byte 都是足够随机的.\n如果你生成的是 128 bit 长的 UUID 的话,理论上是 2的14x8=114次方才会有一次重复。这是个什么概念的呢? 即你每秒能 生成 10 亿个 UUID, 在100年以后,你就有 50%的可能性产生一个重复的 UUID了,是不是很开心呢?\n即使你使用 UUID.randomUUID.getLeastSignificant() 生成长整型的ID, 你理论上需要生成 2的56次方个 ID 后才会产生一个重复的 ID, 所以你可以放心地使用 UUID 了 :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/uuid/","summary":"笔者近来闲来无事,又因为有需要构造全局唯一 ID 的需求,所以就去看了 UUID 这个提供稳定的系统唯一标识符的类的源码 1 UUID variant 事实上是存在很多中 UID 的不同实现","title":"Java UUID 源码剖析"},{"content":"分享一下平时工作生活中编写的一些脚本片段(一直更新). 适用于 OS X 和 Linux\n1 准备工作 因为我比较多的脚本都是基于 percol 这个神器,所以需要先安装 percol, 如果 不了解 percol 的话,可以翻看一下我之前的文章 Linux/Unix Shell 二三事之神器percol .\n我一般将写好的函数 source 命令添加到 Shell. 例如脚本函数都在一个叫tool_function.sh 的文件里面,而我使用 Zsh, 则只需要在 .zshrc 添加一句语句:\nsource /path/to/tool_function.sh 如果使用 Bash, 添加到 .bashrc 即可。\n2 有趣的脚本 2.1 SSH 免密码登录 SSH 基本就是登录远程服务器的标配了,只是每次登录服务器都要输入密码,未免太麻烦了(好吧,我拥有懒惰这个美德),所以我决定配置 SSH 的免密码登录。代码如下:\nfunction config_ssh_login_key(){ if [ $# -lt 3 ];then echo \u0026#34;Usage: $(basename $0) -u user -h hostname -p port\u0026#34; kill -INT $$ fi #if public/private key doesn\u0026#39;t exist ,generate public/private key if [ -f ~/.ssh/id_rsa ];then echo \u0026#34;public/private key exists\u0026#34; else ssh-keygen -t rsa fi while getopts :u:h:p: option do case \u0026#34;$option\u0026#34; in u) user=$OPTARG;; h) hostname=$OPTARG;; p) port=$OPTARG;; *) echo \u0026#34;Unknown option:$option\u0026#34;;; esac done if [ -z \u0026#34;$port\u0026#34; ];then port=22 fi #check whether it is the first time to run this script and whether authorized_keys exists # ssh_host_and_user=\u0026#34;$1@$2\u0026#34; authorized_keys=\u0026#34;$HOME/.ssh/authorized_keys\u0026#34; read -r -s -p \u0026#34;$user@$hostname\u0026#39;s password:\u0026#34; password if sshpass -pv $password ssh -p \u0026#34;$port\u0026#34; \u0026#34;$user@$hostname\u0026#34; test -e \u0026#34;$authorized_keys\u0026#34;;then echo \u0026#34;authorized key exists\u0026#34; kill -INT $$ else sshpass -p $password ssh $user@$hostname -p $port \u0026#34;mkdir -p ~/.ssh;chmod 0700 .ssh\u0026#34; sshpass -p $password scp -P $port ~/.ssh/id_rsa.pub $user@$hostname:~/.ssh/authorized_keys # ssh-copy-id \u0026#34;$user@$hostname -p $port\u0026#34; fi } 基本做法就是生成一对公私密钥,然后把公钥发送到服务器。而脚本其他的部分就是判断密钥是否存在,修改密钥权限等工作。用法也很简单,假如你把以上脚本保存到了一个叫 config_ssh_login_key.sh 的文件:\nbash config_ssh_login_key.sh -h your-server-ip -u user -p 2222 当然,如果你按照我的前面提到的做法,用 source 命令引入脚本,你可以直接在命令行输入:\nconfig_ssh_login_key -u root -h your-server-ip 如果端口未指定,默认端口为 22\n2.2 生成若干位密钥 生成若干位的密钥是常见的需求,得益于 Linux/Unix 命令行强大的过滤器,所以只需把命令整理成脚本即可:\n# generate key function gkey(){ if [ -n \u0026#34;$1\u0026#34; ];then local length=\u0026#34;$1\u0026#34; else local length=32 fi OS_NAME=$(uname) if [ $OS_NAME = \u0026#34;Darwin\u0026#34; ]; then LC_CTYPE=C cat /dev/urandom |tr -cd \u0026#34;[:alnum:]\u0026#34;|head -c \u0026#34;$length\u0026#34;;echo else cat /dev/urandom |tr -cd \u0026#34;[:alnum:]\u0026#34;|head -c \u0026#34;$length\u0026#34;;echo fi } 用法:\ngkey 45 即生成一个45位字符的随机密钥,如果没有指定长度的话,默认是 32 位。因为 OS X和 Linux 的 tr 使用有差异,所以要处理一下\n2.3 复制命令行输出 有时可能需要复制某个命令的输出,一般的做法都是运行某个命令,用鼠标选中,然后复制。例如在生成密钥之后,需要复制到项目的配置文件。但是每次都要用鼠标,效率实在不高。这个功能其实可以脚本实现:\nOS_NAME=$(uname) function pclip() { if [ $OS_NAME = \u0026#34;CYGWIN\u0026#34; ]; then putclip \u0026#34;$@\u0026#34;; elif [ $OS_NAME = \u0026#34;Darwin\u0026#34; ]; then pbcopy \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xsel ]; then xsel -ib \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xclip ]; then xclip -selection c \u0026#34;$@\u0026#34;; else echo \u0026#34;Neither xsel or xclip is installed!\u0026#34; fi fi fi } 备注:这个脚本不是我原创,取自 陈斌 博客。\n在 Linux 运行这脚本需要先安装 xsel 或者是 xclip 命令。结合生成密钥的命令使用:\ngkey -28|pclip 这样,生成的密钥就被复制到系统上了。\n2.4 复制当前目录 有时候,我需要复制当前目录下某个文件的路径,但是无论是文件管理器,还是在Shell 中都要用鼠标选中然后复制指定文件的路径,效率不高且很不方便。所以我通过结合 percol 和上面提高的 pclip 函数改进了做法:\nfunction pwdf() { local current_dir=`pwd` local copied_file=`find $current_dir -type f -print |percol` echo -n $copied_file |pclip; } 只需在 Shell 中输入 pwdf, 然后选择需要复制的路径即可。 运行截图:\n\u0026lt;2017-05-22 Mon\u0026gt; Update\n2.5 判断 Unix 系统的版本 因为我经常需要在不同的 Unix 机器之间切换,例如工作用的 Mac OS X, 另外一台笔记本上的 Fedora, 还有一台工作站上的 Arch Linux, 以及各种发行版本的 VPS 等,在不同的发行版本或者系统之间切换,我希望我常用的工具也可以很轻易地移植到不同的发行版本上。\n但是不同的发行版本使用不同的包安装管理器,例如 OS X 上的 brew, Fedora 的 dnf, Centos 的 yum, Ubuntu 上的 apt-get 等等。如果可以通过使用脚本来实现根据不同的发行版本使用不同的包安装管理器安装软件,这样就省心很多。\n# GetOSVersion function GetOSVersion { # Figure out which vendor we are if [[ -x \u0026#34;`which sw_vers 2\u0026gt;/dev/null`\u0026#34; ]]; then # OS/X os_VENDOR=`sw_vers -productName` elif [[ -x $(which lsb_release 2\u0026gt;/dev/null) ]]; then os_VENDOR=$(lsb_release -i -s) if [[ \u0026#34;Debian,Ubuntu,LinuxMint\u0026#34; =~ $os_VENDOR ]]; then os_PACKAGE=\u0026#34;deb\u0026#34; elif [[ \u0026#34;SUSE LINUX\u0026#34; =~ $os_VENDOR ]]; then lsb_release -d -s | grep -q openSUSE if [[ $? -eq 0 ]]; then os_VENDOR=\u0026#34;openSUSE\u0026#34; fi elif [[ $os_VENDOR == \u0026#34;openSUSE project\u0026#34; ]]; then os_VENDOR=\u0026#34;openSUSE\u0026#34; elif [[ $os_VENDOR =~ Red.*Hat ]]; then os_VENDOR=\u0026#34;Red Hat\u0026#34; fi os_CODENAME=$(lsb_release -c -s) elif [[ -r /etc/redhat-release ]]; then # Red Hat Enterprise Linux Server release 5.5 (Tikanga) # Red Hat Enterprise Linux Server release 7.0 Beta (Maipo) # CentOS release 5.5 (Final) # CentOS Linux release 6.0 (Final) # Fedora release 16 (Verne) # XenServer release 6.2.0-70446c (xenenterprise) # Oracle Linux release 7 os_CODENAME=\u0026#34;\u0026#34; for r in \u0026#34;Red Hat\u0026#34; CentOS Fedora XenServer; do os_VENDOR=$r done if [ \u0026#34;$os_VENDOR\u0026#34; = \u0026#34;Red Hat\u0026#34; ] \u0026amp;\u0026amp; [[ -r /etc/oracle-release ]]; then os_VENDOR=OracleLinux fi elif [[ -r /etc/SuSE-release ]]; then for r in openSUSE \u0026#34;SUSE Linux\u0026#34;; do if [[ \u0026#34;$r\u0026#34; = \u0026#34;SUSE Linux\u0026#34; ]]; then os_VENDOR=\u0026#34;SUSE LINUX\u0026#34; else os_VENDOR=$r fi os_VENDOR=\u0026#34;\u0026#34; done # If lsb_release is not installed, we should be able to detect Debian OS elif [[ -f /etc/debian_version ]] \u0026amp;\u0026amp; [[ $(cat /proc/version) =~ \u0026#34;Debian\u0026#34; ]]; then os_VENDOR=\u0026#34;Debian\u0026#34; fi export os_VENDOR } 2.6 根据不同的发行版本安装软件 刚刚上面的脚本是为了准确判断出所有的 *nix 系统的,但是方便起见,也可以直接使用uname 命令\nif [ \u0026#34;$(uname)\u0026#34; == \u0026#34;Darwin\u0026#34; ]; then # Do something under Mac OS X platform echo \u0026#34;This is mac os\u0026#34; # check if brew exists type brew\u0026gt;/dev/null 2\u0026gt;\u0026amp;1 || { echo \u0026gt;\u0026amp;2 \u0026#34; require brew but it\u0026#39;s not installed. Aborting.\u0026#34;; exit 1; } echo \u0026#34;install htop\u0026#34; brew install htop echo \u0026#34;install ag\u0026#34; brew install ag echo \u0026#34;install httpie\u0026#34; brew install httpie echo \u0026#34;install fasd\u0026#34; brew install fasd echo \u0026#34;install tree\u0026#34; brew install tree echo \u0026#34;install shellcheck\u0026#34; brew install shellcheck echo \u0026#34;install guile\u0026#34; brew install guile echo \u0026#34;install proxychains-ng\u0026#34; brew install proxychains-ng echo \u0026#34;install pandoc\u0026#34; brew install pandoc echo \u0026#34;install markdown\u0026#34; brew install markdown echo \u0026#34;install cloc\u0026#34; brew install cloc elif [ \u0026#34;$(expr substr $(uname -s) 1 5)\u0026#34; == \u0026#34;Linux\u0026#34; ]; then # Do something under GNU/Linux platform GetOSVersion if [ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Ubuntu\u0026#34; ] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Debian\u0026#34; ]] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;LinuxMint\u0026#34; ]]; then # install htop sudo apt-get install htop -y # install httpie sudo apt-get install httpie -y # install ag sudo apt-get install silversearcher-ag -y # install zeal sudo apt-get install zeal -y # install ncdu sudo apt-get install ncdu -y # install i3 sudo apt-get install i3 -y # install emacs (i could die without it) sudo apt-get install emacs -y # install vim sudo apt-get install vim -y # install tree sudo apt-get install tree -y # install shellcheck sudo apt-get install shellcheck -y # install guile (scheme compiler) sudo apt-get install guile -y # install source code pro font [ -d /usr/share/fonts/opentype ] || sudo mkdir /usr/share/fonts/opentype sudo git clone https://github.com/adobe-fonts/source-code-pro.git /usr/share/fonts/opentype/scp sudo fc-cache -f -v # install proxychains-ng sudo apt-get install proxychains-ng -y # install pandoc sudo apt-get install pandoc -y sudo apt-get install markdown -y sudo apt-get install cloc -y elif [ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Fedora\u0026#34; ] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;CentOS\u0026#34; ]] || [[ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Korora\u0026#34; ]]; then # install ag sudo yum install -y the_silver_searcher # install zeal sudo yum install -y zeal # install httpie sudo yum install -y httpie # install htop sudo yum install -y htop # install ncdu sudo yum install -y ncdu # install vim sudo yum install -y vim # install emacs sudo yum install -y emacs # install i3 sudo yum install -y i3 # install tree sudo yum install -y tree # install shellcheck sudo yum install ShellCheck -y # install guile sudo yum install guile -y # install source code pro font sudo yum install adobe-source-code-pro-fonts -y # install proxychains-ng sudo yum install proxychains-ng -y sudo yum install pandoc -y sudo yum install markdown -y # count line and space in code sudo yum install cloc -y elif [ \u0026#34;$os_VENDOR\u0026#34; == \u0026#34;Arch\u0026#34; ] ; then # install ag sudo pacman -S -y the_silver_searcher # install zeal sudo pacman -S -y zeal # install httpie sudo pacman -S -y httpie # install htop sudo pacman -S -y htop # install ncdu sudo pacman -S -y ncdu # install vim sudo pacman -S -y vim # install emacs sudo pacman -S -y emacs # install i3 sudo pacman -S -y i3 # install tree sudo pacman -S -y tree # install shellcheck sudo pacman -S ShellCheck -y # install guile sudo pacman -S guile -y # install source-code-pro font sudo pacman -S adobe-source-code-pro-fonts -y # install proxychains-ng sudo pacman -S proxychains-ng -y sudo pacman -S pandoc -y sudo pacman -S markdown -y sudo pacman -S ripgrep -y sudo pacman -S cloc -y fi elif [ \u0026#34;$(expr substr $(uname -s) 1 10)\u0026#34; == \u0026#34;MINGW32_NT\u0026#34; ]; then # Do something under 32 bits Windows NT platform echo \u0026#34;This is 32-bit windows\u0026#34; elif [ \u0026#34;$(expr substr $(uname -s) 1 10)\u0026#34; == \u0026#34;MINGW64_NT\u0026#34; ]; then # Do something under 64 bits Windows NT platform echo \u0026#34;this is 64-bit windows\u0026#34; fi 2.7 加密目录 每个人都会有需要只属于自己的东西,保护这些东西最好的办法就是对其进行加密:\n2.7.1 加密 使用 tar 和 openssl 对目录进行加密,先使用 tar 归档当前文件,然后使用 aes256 算法进行加密:\ntar -czf - * | openssl enc -e -aes256 -out encrypted.tar.gz 2.7.2 解密 把加密后的归档文件解密到当前命令:\nopenssl enc -d -aes256 -in encrypted.tar.gz| tar xz -C $(pwd) ","permalink":"https://ramsayleung.github.io/zh/post/2017/share_shell_script/","summary":"分享一下平时工作生活中编写的一些脚本片段(一直更新). 适用于 OS X 和 Linux 1 准备工作 因为我比较多的脚本都是基于 percol 这个神器,所以需要先安装 percol, 如果 不了","title":"脚本分享"},{"content":"因为需要编写 RESTful api 测试的缘故,重拾了 Spock 这个适用于 Groovy/Java 的测试 框架,顺便把以前写的一篇旧文整理了一下,权当重温。\n1 关于 Spock Spock 是一个适用于 Java(Groovy) 的一个优雅并且全面的测试框架, 说 Spock 全面,是 因为 Spock 集成了现有的 Java 测试库;至于为什么赞美 Spock 优雅,阅读完全文你就会 有体会的了\n因为基于 Groovy, 使得 Spock 可以更容易地写出表达能力更强的测试用例。又因为它内置 了 Junit Runner, 所以 Spock 兼容大部分的 IDE,测试工具,和持续集成服务器。接下来 就介绍一下 Spock 的特性\n2 Spock 特性 内置支持 mocking stubbing,可以很容易地模拟复杂的类的行为 Spock 实现了 BDD 范式(behavior-driven development) 与现有的 Build 工具集成,可以用来测试后端代码,Web 页面等等 兼容性强,内置 Junit Runner, 可以像运行 Junit 那样运行 Spock,甚至可以在同一个项 目里面同时使用两种测试框架 取长补短,吸收了现有框架的优点,并加以改进 Spock 代码风格简短,易读,表达性强,扩展性强,还有更清晰显示 bug 3 为什么是 Spock Spock 似乎有很多不错的特性,但是为什么有 Junit 这个那么强大的测试框架, 还要去 使用 Spock 呢? 甚至可以用 Spock 来代替 Junit 呢? 下面就用一些简单的例子来诠释 一下Spock 的强大. 以一个简单的加法为例:\nJunit 的测试用例\nSpock 的测试用例\n是否觉得耳目一新呢? 因为 Spock 支持以类人类语言的形式来定义方法名, 所以对比 Junit 的测试用例, 你会发现 Spock 的测试用例, 只需函数名, 就可以清晰了解这个测 试的用途\n接下来, 再写一个乘法的类, 然后人为地加入一个 Bug, 再看看 Junit 和 Spock 的表现\n如果测试 fail, 会出现什么情况呢?\n显而易见,Junit 只是显示了结果不等,却没办法究竟判断是加法还是乘法出现了 bug, 但是 Spock 就很清晰地给出了答案。不难看出 Spock 的语法更加简洁, 优雅; 此外, 得 益于 Spock 独特的命名方式,只需查看函数名字便可以了解测试用例的目的,无需额外 的注释。而这只是 Spock 和 Junit 的一部分差异,其他的差异,接下来会继续说明。\n4 Spock 语法 4.1 Specification class MyFirstSpecification extend Specification{ //fields //fixture methods //feature methods //helper methods } Specification 是指一个继承于 spock.lang.Specification 的一个 Groovy 类. 而 Specification 的名字一般是跟系统或者业务逻辑有关的组合词,例如之前的AdderSpec\n4.2 Fields 实例化一个类\ndef obj = new ClassUnderSpecification() def coll = new Collaborator() 4.3 Feature Methods Feature Methods 指具体的测试用例方法\ndef \u0026#34;pushing an element on the stack\u0026#34;() { // blocks go here } 4.4 Fixture Methods def setup() {} // run before every feature method def cleanup() {} // run after every feature method def setupSpec() {} // run before the first feature method def cleanupSpec() {} // run after the last feature method 关于 Fixture Methods 的作用,笔者引用一下官方文档的一段话\nFixture methods are responsible for setting up and cleaning up the environment in which feature methods are run. Usually it’s a good idea to use a fresh fixture for every feature method, which is what the setup() and cleanup() methods are for. All fixture methods are optional.\n简而言之, Fixture methodr 是进行初始化或者收尾工作的。为了更好地理解 Spock 的特性,可以用 Spock 和 Junit 进行比较,(图截自官网)\n以上就是 Spock 的基本用法, 也只能说是中规中矩,难言惊艳。那么,接下来介绍的 就是 Spock killer 级别的特性了\n4.5 Blocks 关于 Blocks 的用法, 这里引用官网的一段话\nSpock has built-in support for implementing each of the conceptual phases of a feature method. To this end, feature methods are structured into so-called blocks. Blocks start with a label, and extend to the beginning of the next block, or the end of the method. There are six kinds of blocks: setup, when, then, expect, cleanup, and where blocks\n简而言之, 这些内置的功能强大的 blocks, 就是帮助开发者编写单元测试的语法糖\n下面就了解一下不同 Block 的功能\n4.5.1 The given: block given: 应该包含所有的初始化条件或者初始化类,例如你可以把要测试的类的实例化放在 given. 总而言之, given 就是放置所有单元测试开始前的准备工作的地方\n4.5.2 The setup: block setup: 笔者个人理解功能跟 given 很相似,所以初始化的时候可以二选一(笔者 个人推荐用 given,因为这样更符合 BDD 范式)\n4.5.3 The when: blcok when: 是 Spock 测试中最重要的一部分,这里放置的就是你要测试的代码,和你如 何测试的用例,这里的测试代码应该尽可能地短。有经验的 Spock 用户可以直接看 when: block 就了解测试流程了\n4.5.4 The then: block then: block 包含隐式的断言, 补充一下,Spock 是没有 assert 这个断言函数的, Spock 使用的是 assertion, 笔者个人理解成这是一种隐式的断言。概括来说, then 就是放置你预期测试结果的地方。\n现在已经把 given-when-then 粗略地解释了一下, 现在就通过代码阐述具体的用法. 首先确定一下需求; 假设现在要测试一个通过网站来销售电脑的电商平台, 如下图 (图 截自 java_test_with_spock 一书)\n然通过模拟用户添加商品到购物车, 以展示 Spock 的用法\npublic class Product{ private String name; private int price; private int weight; } public class Basket{ public void addProduct(Product product){ addProduct(product,1) } public void addProduct(Product product,int times){ //some code about business } public int getCurrentWeight(){ // } public int getProductTypesCount(){ // } } 然后编写 Spock 的测试用例\ndef \u0026#34;A basket with one product has equal weight\u0026#34;(){ given: \u0026#34;an empty basket and a Tv\u0026#34; Product tv=new Product(name:\u0026#34;bravia\u0026#34;,price:1200,weight:18) Basket basket=new Basket() when:\u0026#34;user wants to buy the TV\u0026#34; basket.addProduct(tv) then:\u0026#34;basket weight is equal to the TV\u0026#34; basket.currentWeight==tv.weight } 现在对 Spock 有一个初步的认识了。也可以使用 given-when-then 这 \u0026ldquo;三板斧\u0026rdquo; 来写 一些逻辑不是非常复杂的测试用例了。\n4.5.5 The and: block and: 它的用法有点像语法糖,它自己本身是没有什么功能,它只是拿来扩展其他的 功能的. 用上面的例子来解释一下用法:\ndef \u0026#34;A basket with one product has equal weight\u0026#34;(){ given: \u0026#34;an empty basket \u0026#34; Basket basket=new Basket() and: \u0026#34;several products\u0026#34; Product tv=new Product(name:\u0026#34;bravia\u0026#34;,price:1200,weight:18) Product camera=new Product(name:\u0026#34;panasonic\u0026#34;,price:350,weight:2) Product hifi=new Product(name:\u0026#34;jvc\u0026#34;,price:600,weight:5) when:\u0026#34;user wants to buy the TV abd the camera and the hifi\u0026#34; basket.addProduct(tv) basket.addProduct(camera) basket.addProduct(hifi) then:\u0026#34;basket weight is equal to all product weight\u0026#34; basket.currentWeight==(tv.weight+camera.weight+hifi.weight) } 从上面的代码可以看出,given 和 and 都用来进行类初始化,只是根据 Basket 和 Product 类型进行了细分。如下图\n使用 and block 可以代码结构更简洁优雅. 此外, 如果 and 是紧跟在 when 后 面, 那么 and 就据有和 when block 一样的功能,依此类推\n4.5.6 The expect: block expect 是一个很强大的特性,它用很多种用法,最常用的用法就是把 given-when-then 都结合起来\ndef \u0026#34;An empty basket has no weight\u0026#34;(){ expect:\u0026#34;zero weight when nothing is added\u0026#34; new Basket().currentWeight==0 } 或者是以下这种形式\ndef \u0026#34;An empty basket has no weight(alternative)\u0026#34;(){ given:\u0026#34;an empty basket\u0026#34; Basket basket=new Basket() expect:\u0026#34;that the weight is 0\u0026#34; basket.currentWeight==0 } 又或者用 expect 提前进行条件判断\ndef \u0026#34;A basket with two products weights as their sum (precondition)\u0026#34;() { given: \u0026#34;an empty basket, a TV and a camera\u0026#34; Product tv = new Product(name:\u0026#34;bravia\u0026#34;,price:1200,weight:18) Product camera = new Product(name:\u0026#34;panasonic\u0026#34;,price:350,weight:2) Basket basket = new Basket() expect:\u0026#34;that nothing should be inside\u0026#34; basket.currentWeight == 0 basket.productTypesCount == 0 /* expect: block performs intermediate assertions*/ when: \u0026#34;user wants to buy the TV and the camera\u0026#34; basket.addProduct tv basket.addProduct camera then: \u0026#34;basket weight is equal to both camera and tv\u0026#34; basket.currentWeight == (tv.weight + camera.weight) /* then: block examines the final result*/ } 上面那个例子是在添加产品之前检查初始化条件,这种情况下,能更容易看出是哪里测试 fail\n4.5.7 The clean: block clean 就相当于在所有的测试结束以后执行的操作,例如,如果在测试中新建了 IO 流, 就可以在 clean 里面关闭 IO 流,那样就可以保证代码的正确性了\n4.6 Spock killer future 确定需求:(例子来自 Java_test_with_spock 一书),假设有一个核反应堆,这个反应 堆的系统组成:\n多个烟雾感应器(输入)\n3 个辐射感应器(输入)\n现在的压力值(输入\n报警器(输出)\n疏散命令(输出)\n通知操作员关闭反应堆(输出) 系统如图\n系统相关设定:\n如果压力值超过 150,报警器报警\n如果 2 个或者更多的烟雾感应器被触发,那么报警器报警,通知操作员关闭反应堆\n如果辐射值超过 100,警报器报警,通知操作员关闭反应堆,并马上疏散人群\n输入输出对应关系\n现在,假如用 Junit 来写测试用例\n@RunWith(Parameterized.class) public class NuclearReactorTest { private final int triggeredFireSensors; private final List\u0026lt;Float\u0026gt; radiationDataReadings; private final int pressure; private final boolean expectedAlarmStatus; private final boolean expectedShutdownCommand; private final int expectedMinutesToEvacuate; public NuclearReactorTest(int pressure, int triggeredFireSensors, List\u0026lt;Float\u0026gt; radiationDataReadings, boolean expectedAlarmStatus, boolean expectedShutdownCommand, int expectedMinutesToEvacuate) { this.triggeredFireSensors = triggeredFireSensors; this.radiationDataReadings = radiationDataReadings; this.pressure = pressure; this.expectedAlarmStatus = expectedAlarmStatus; this.expectedShutdownCommand = expectedShutdownCommand; this.expectedMinutesToEvacuate = expectedMinutesToEvacuate; } @Test public void nuclearReactorScenario() { NuclearReactorMonitor nuclearReactorMonitor = new NuclearReactorMonitor(); nuclearReactorMonitor.feedFireSensorData(triggeredFireSensors); nuclearReactorMonitor.feedRadiationSensorData(radiationDataReadings); nuclearReactorMonitor.feedPressureInBar(pressure); NuclearReactorStatus status = nuclearReactorMonitor.getCurrentStatus(); assertEquals(\u0026#34;Expected no alarm\u0026#34;, expectedAlarmStatus, status.isAlarmActive()); assertEquals(\u0026#34;No notifications\u0026#34;, expectedShutdownCommand, status.isShutDownNeeded()); assertEquals(\u0026#34;No notifications\u0026#34;, expectedMinutesToEvacuate, status.getEvacuationMinutes()); } @Parameters public static Collection\u0026lt;Object[]\u0026gt; data() { return Arrays .asList(new Object[][] { { 150, 0, new ArrayList\u0026lt;Float\u0026gt;(), false, false, -1 }, { 150, 1, new ArrayList\u0026lt;Float\u0026gt;(), true, false, -1 }, { 150, 3, new ArrayList\u0026lt;Float\u0026gt;(), true, true, -1 }, { 150, 0, Arrays.asList(110.4f, 0.3f, 0.0f), true, true, 1 }, { 150, 0, Arrays.asList(45.3f, 10.3f, 47.7f), false, false, -1 }, { 155, 0, Arrays.asList(0.0f, 0.0f, 0.0f), true, false, -1 }, { 170, 0, Arrays.asList(0.0f, 0.0f, 0.0f), true, true, 3 }, { 180, 0, Arrays.asList(110.4f, 0.3f, 0.0f), true, true, 1 }, { 500, 0, Arrays.asList(110.4f, 300f, 0.0f), true, true, 1 }, { 30, 0, Arrays.asList(110.4f, 1000f, 0.0f), true, true, 1 }, { 155, 4, Arrays.asList(0.0f, 0.0f, 0.0f), true, true, -1 }, { 170, 1, Arrays.asList(45.3f, 10.3f, 47.7f), true, true, 3 }, }); } 各种输入输出数据以及 getter setter 耦合在一起,代码变得难读起来. 此外,除了可 读性, 还有更严重的问题,假如需求要增加一个输入或者增加一个输出呢, 就只能改 变数据结构, 这样的代码真的难以维护。不知道 Spock 的表现又如何呢?\nclass NuclearReactorSpec extends spock.lang.Specification{ def \u0026#34;Complete test of all nuclear scenarios\u0026#34;() { given: \u0026#34;a nuclear reactor and sensor data\u0026#34; NuclearReactorMonitor nuclearReactorMonitor =new NuclearReactorMonitor() when: \u0026#34;we examine the sensor data\u0026#34; nuclearReactorMonitor.feedFireSensorData(fireSensors) nuclearReactorMonitor.feedRadiationSensorData(radiation) nuclearReactorMonitor.feedPressureInBar(pressure) NuclearReactorStatus status = nuclearReactorMonitor.getCurrentStatus() then: \u0026#34;we act according to safety requirements\u0026#34; status.alarmActive == alarm status.shutDownNeeded == shutDown status.evacuationMinutes == evacuation where: \u0026#34;possible nuclear incidents are:\u0026#34; pressure | fireSensors | radiation || alarm | shutDown | evacuation 150 | 0 | [] || false | false | -1 150 | 1 | [] || true | false | -1 150 | 3 | [] || true | true | -1 150 | 0 | [110.4f ,0.3f, 0.0f] || true | true | 1 150 | 0 | [45.3f ,10.3f, 47.7f] || false | false | -1 155 | 0 | [0.0f ,0.0f, 0.0f] || true | false | -1 170 | 0 | [0.0f ,0.0f, 0.0f] || true | true | 3 180 | 0 | [110.4f ,0.3f, 0.0f] || true | true | 1 500 | 0 | [110.4f ,300f, 0.0f] || true | true | 1 30 | 0 | [110.4f ,1000f, 0.0f] || true | true | 1 155 | 4 | [0.0f ,0.0f, 0.0f] || true | true | -1 170 | 1 | [45.3f ,10.3f, 47.7f] || true | true | 3 } } 除了上面提及的 given-when-then 范式外,还多了一个之前没见过的 where block。现 在就来认识一下 Spock 的 killer 特性. 可以看到 Spock 的输入输出参数都保存在类 似表格的数据结构,其实这是 Spock 的 Parameterized tests,而在 || 符号左边的 是输入,右边的输出,每一列开始都是该参数的属性名,这样就可以很便捷地在 then 判断输出结果是否符合预期结果. 而数据添加或者减少输入参数或者输出结果的操作, 只需在 where block 里面对应地添加或者减少具体的参数,整个操作一目了然. 参数 的新增或者移除也很容易地实现\n5 结语 笔者在项目中正是使用 Spock 编写测试, 或许对比 Junit, Spock 在流行度方面还难而 望其项背, 但是综合多方考虑,Spock 真的值得一试,兼之 Groovy 语言的语法加成,就 有一种在使用脚本编写 Java 的感觉 (好吧,笔者知道 Groovy 就是基于 jvm 的脚本), 无需再为 Java 啰嗦的语法而烦恼。此外 Spock还有很多很强大的功能,例如内置的 Mocking Stubbing (Junit 需要第三方库支持), 还有支持企业级应用,Spring, Spring boot, 和 Restful service 测试等。更多的用法,就要查阅官方文档了\n6 参考 Java Testing with Spock Spock Framework Reference Documentation ","permalink":"https://ramsayleung.github.io/zh/post/2017/spock/","summary":"因为需要编写 RESTful api 测试的缘故,重拾了 Spock 这个适用于 Groovy/Java 的测试 框架,顺便把以前写的一篇旧文整理了一下,权当重温。 1 关于 Spock Spock 是一个适用于 Java(Groovy) 的一个优雅并","title":"Spock 一个优雅的Groovy/Java测试框架"},{"content":"1 重要性 笔者最近都在负责项目中关于日志的部分,因为跟日志打交道比较多,所以有一些关于日 志感受和技巧想要分享一下。\n笔者认为对于各种程序和应用,日志都是非常重要的,因为程序在部属到服务器之后,开发者是没办法像在本地开发那样可以充分了解程序发生的状况,而使用日志可以让开发者了解运行中的程序的状态,即使出现了错误,或者是系统挂了,也可以从日志中分析原因。\n所以换句话说,日志的重要程度甚至可以称得上是不可或缺。接下来,笔者将会以 Python 中的 logging 模块为例阐述日志。\n2 关于日志 2.1 使用 print 函数输出? 日志是为了输出程序的运行状态,那么可否使用 print 函数进行 logging 的工作呢?\n我并不建议把 print() 函数当作日志使用 (当然,如果你一定要这么用,我也拦不住);不建议使用 print 进行logging 原因有:\n无法在不修改源代码的情况下,控制日志的输出 日志信息可能跟程序输出的有用数据混杂,导致输出的数据不可读或者非常难读 print 无法将日志信息输出到除标准输出以外的目标 (例如文件,socket,SMTP 服务器等) 无法根据错误信息的等级进行动态输出,因为 print 函数的作用只是输出信息 可能对于非常简单的小程序,开发者可以使用 print 进行日志输出,但是对于比较大型的程序,系统内置的 logging 类库或许是更好的选择\n2.2 日志需要记录的是什么 Python 的日志类库 logging 可以让开发者根据不同场景使用不同的日志等级以输出 不同的日志信息。\n而日志需要记录的最基本的信息又是什么呢?要想回答这个问题,先和我一起回顾一下日志的功能:记录程序的状态,为程序的开发和调试提供便利!\n所谓方便调试,需要记录的必然包括可以帮助更快定位到错误的有用信息:\nLogger 的名字 (比较常用的做法都是 __name__,即当前文件的信息) 具体日期 (这个可以帮助确定出错的具体场景) 方法名 源代码行数 异常的 traceback 信息 这只是最基本的信息,具体还要根据场景添加其它有用信息;比如对于分布式的程序,肯定还要记录其它节点的名字,IP 等有用信息。\n3 Logging 的正确姿势 3.1 使用 Python 的 logging 模块 我认为,使用 Python 的标准日志库是比较好的实践,因为标准库已经提供了开箱即用的特性,无需重复造轮子。Python 的 logging 模块也很容易上手,举个小例子:\nimport logging logging.basicConfig(level=logging.DEBUG) # define a logger logger = logging.getLogger(__name__) #Info level msg logger.info(\u0026#39;Info level message\u0026#39;) #Debug level msg logger.debug(\u0026#39;Debug level message\u0026#39;) #Warning level msg logger.info(\u0026#39;Warning level message\u0026#39;) 日志输出如下:\nINFO:__main__: Info level message DEBUG:__main__: Debug level message WARN:__main__: Warning level message 3.2 记录异常信息 日志一个非常重要的作用就是调试,所以记录出现异常的地方是有必要,并且需要记录栈的调用信息。例如:\ntry: open(\u0026#39;file_not_exist.txt\u0026#39;, \u0026#39;wt\u0026#39;) except Exception, e: logger.error(\u0026#39;Failed to write a file\u0026#39;,exc_info=True) 通过将 exc_info 设置成 True, 栈的调用信息就会记录到日志里面。而也可以使用 logger.exception(message,*args) 方法,它等同于 logger.error(msg,exc_info=True,*args) 方法。\n3.3 使用日志文件轮转控制器 (rotating file handler) 如果使用日志文件控制器 (FileHandler), 不断地运行程序,就会产生越来越多的日志 信息或者是日志文件。\n为了控制日志文件的数量,可以使用 RotatingFileHandler 自 动新建新的日志文件,并且保留旧的日志文件,当产生一定数量的日志文件之后,就会 自动删除掉最旧的日志文件。例如:\nhandler = logging.handlers.RotatingFileHandler( LOG_FILENAME, maxBytes=20, backupCount=5, ) my_logger.addHandler(handler) 就是日志文件大小超过20个字节 (当然,真实情况不会那么小的阀值),就创建一个新的日志文件,把原来的日志文件,例如叫 example.log 重命名为 example.log.1,然后新建的日志文件就会被命名为_example.log_, 一直到产生了6个日志文件,即 example.log.5, 继续记录日志,最开始的第一个日志就会被删除。\n3.4 使用日志服务器 对于那些分布式的应用,或者部署多台服务器上有不同日志的程序而言,逐个服务器或者节点查看日志实在太可怕了. 这个时候,就可以设置一个日志服务器,把重要的日志信息发送到日志服务器,你就在日志服务器上监控各个节点的日志状态了。\nlogging-cookbook 的例子:\n客户端或者节点:\nimport logging import logging.handlers rootLogger = logging.getLogger(\u0026#39;\u0026#39;) rootLogger.setLevel(logging.DEBUG) socketHandler = logging.handlers.SocketHandler(\u0026#39;localhost\u0026#39;, logging.handlers.DEFAULT_TCP_LOGGING_PORT) # don\u0026#39;t bother with a formatter, since a socket handler sends the event as # an unformatted pickle rootLogger.addHandler(socketHandler) # Now, we can log to the root logger, or any other logger. First the root... logging.info(\u0026#39;Jackdaws love my big sphinx of quartz.\u0026#39;) # Now, define a couple of other loggers which might represent areas in your # application: logger1 = logging.getLogger(\u0026#39;myapp.area1\u0026#39;) logger2 = logging.getLogger(\u0026#39;myapp.area2\u0026#39;) logger1.debug(\u0026#39;Quick zephyrs blow, vexing daft Jim.\u0026#39;) logger1.info(\u0026#39;How quickly daft jumping zebras vex.\u0026#39;) logger2.warning(\u0026#39;Jail zesty vixen who grabbed pay from quack.\u0026#39;) logger2.error(\u0026#39;The five boxing wizards jump quickly.\u0026#39;) 日志服务器:\nimport logging import logging.handlers import pickle import socketserver import struct class LogRecordStreamHandler(socketserver.StreamRequestHandler): \u0026#34;\u0026#34;\u0026#34;Handler for a streaming logging request. This basically logs the record using whatever logging policy is configured locally. \u0026#34;\u0026#34;\u0026#34; def handle(self): \u0026#34;\u0026#34;\u0026#34; Handle multiple requests - each expected to be a 4-byte length, followed by the LogRecord in pickle format. Logs the record according to whatever policy is configured locally. \u0026#34;\u0026#34;\u0026#34; while True: chunk = self.connection.recv(4) if len(chunk) \u0026lt; 4: break slen = struct.unpack(\u0026#39;\u0026gt;L\u0026#39;, chunk)[0] chunk = self.connection.recv(slen) while len(chunk) \u0026lt; slen: chunk = chunk + self.connection.recv(slen - len(chunk)) obj = self.unPickle(chunk) record = logging.makeLogRecord(obj) self.handleLogRecord(record) def unPickle(self, data): return pickle.loads(data) def handleLogRecord(self, record): # if a name is specified, we use the named logger rather than the one # implied by the record. if self.server.logname is not None: name = self.server.logname else: name = record.name logger = logging.getLogger(name) # N.B. EVERY record gets logged. This is because Logger.handle # is normally called AFTER logger-level filtering. If you want # to do filtering, do it at the client end to save wasting # cycles and network bandwidth! logger.handle(record) class LogRecordSocketReceiver(socketserver.ThreadingTCPServer): \u0026#34;\u0026#34;\u0026#34; Simple TCP socket-based logging receiver suitable for testing. \u0026#34;\u0026#34;\u0026#34; allow_reuse_address = True def __init__(self, host=\u0026#39;localhost\u0026#39;, port=logging.handlers.DEFAULT_TCP_LOGGING_PORT, handler=LogRecordStreamHandler): socketserver.ThreadingTCPServer.__init__(self, (host, port), handler) self.abort = 0 self.timeout = 1 self.logname = None def serve_until_stopped(self): import select abort = 0 while not abort: rd, wr, ex = select.select([self.socket.fileno()], [], [], self.timeout) if rd: self.handle_request() abort = self.abort def main(): logging.basicConfig( format=\u0026#39;%(relativeCreated)5d %(name)-15s %(levelname)-8s %(message)s\u0026#39;) tcpserver = LogRecordSocketReceiver() print(\u0026#39;About to start TCP server...\u0026#39;) tcpserver.serve_until_stopped() if __name__ == \u0026#39;__main__\u0026#39;: main() 通过给 logger 添加一个SocketHandler 就可以把日志事件发送到服务器端\n3.5 使用配置文件 虽然开发者可以使用 Python 代码来配置日志系统,但是这样是很不灵活的,每次修改日志等级还需要去改动代码。\n而使用配置文件无疑是一个更好的选择,例如 json 或者是 yaml 文件,这样就可以在 json/yaml 文件中加载日志配置了。以 Django 项目的配置文件为例,我改成了 json 格式:\n{ \u0026#34;version\u0026#34;: 1, \u0026#34;disable_existing_loggers\u0026#34;: True, \u0026#34;formatters\u0026#34;: { \u0026#34;verbose\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s\u0026#34; }, \u0026#34;simple\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;%(levelname)s %(message)s\u0026#34; }, }, \u0026#34;filters\u0026#34;: { \u0026#34;special\u0026#34;: { \u0026#34;()\u0026#34;: \u0026#34;project.logging.SpecialFilter\u0026#34;, \u0026#34;foo\u0026#34;: \u0026#34;bar\u0026#34;, } }, \u0026#34;handlers\u0026#34;: { \u0026#34;null\u0026#34;: { \u0026#34;level\u0026#34;: \u0026#34;DEBUG\u0026#34;, \u0026#34;class\u0026#34;: \u0026#34;django.utils.log.NullHandler\u0026#34;, }, \u0026#34;console\u0026#34;: { \u0026#34;level\u0026#34;: \u0026#34;DEBUG\u0026#34;, \u0026#34;class\u0026#34;: \u0026#34;logging.StreamHandler\u0026#34;, \u0026#34;formatter\u0026#34;: \u0026#34;simple\u0026#34; }, \u0026#34;mail_admins\u0026#34;: { \u0026#34;level\u0026#34;: \u0026#34;ERROR\u0026#34;, \u0026#34;class\u0026#34;: \u0026#34;django.utils.log.AdminEmailHandler\u0026#34;, \u0026#34;filters\u0026#34;: \u0026#34;special\u0026#34; } }, \u0026#34;loggers\u0026#34;: { \u0026#34;django\u0026#34;: { \u0026#34;handlers\u0026#34;: \u0026#34;null\u0026#34;, \u0026#34;propagate\u0026#34;: true, \u0026#34;level\u0026#34;: \u0026#34;INFO\u0026#34;, }, \u0026#34;django.request\u0026#34;: { \u0026#34;handlers\u0026#34;: [\u0026#34;mail_admins\u0026#34;], \u0026#34;level\u0026#34;: \u0026#34;ERROR\u0026#34;, \u0026#34;propagate\u0026#34;: false, }, \u0026#34;myproject.custom\u0026#34;: { \u0026#34;handlers\u0026#34;: [\u0026#34;console\u0026#34;, \u0026#34;mail_admins\u0026#34;], \u0026#34;level\u0026#34;: \u0026#34;INFO\u0026#34;, \u0026#34;filters\u0026#34;: [\u0026#34;special\u0026#34;] } } } 以及加载 json 文件到日志配置中:\nimport json import logging.config def setup_logging(): \u0026#34;\u0026#34;\u0026#34; Setup logging configuration \u0026#34;\u0026#34;\u0026#34; with open(\u0026#39;logging_configuration.json\u0026#39;, \u0026#39;rt\u0026#39;) as f: config = json.load(f) logging.config.dictConfig(config) 使用 json 还有一个好处是标准库已经内置了 json 模块,无需像 yaml 那样需要安装额外的模块,不过我更推崇 yaml, 因为清晰之余,还可以少打很多字 :)\n3.6 对于不同的代码,使用不同的日志等级 因为一个项目不同代码要求不一样,也无需把每一个实现细节都记录在日志,只需要根 据不同的实现,使用不同的日志等级,例如使用 Debug 记录系统启动,处理业务逻辑 请求的信息,使用 Error, 记录系统的出错信息,可以结合堆栈分析原因,等等。\n此外,Logger 实例可以被配置成基于名字的树状结构。 每一个部件都定义了一个基础的名字,对应的模块被设置成子节点。而 root logger 没有名字。如图:\n就配置 logging 而言,我认为树状结构是非常有用的,因为无需为每一个 logger 都设置handler. 如果一个 logger 没有 handler 的话,它就会让父节点来处理。所以 对于对于大部份的应用而言,只需配置 root logger, 而所有的信息都会发送到同一个 地方\n而树状结构可以对应用的不同部分使用不同的日志等级,不同的 handler, 不同的formatter, 以更好地控制日志信息\n3.7 使用结构化日志 虽然大部份的日志信息对于人类都是可读的,但是对于程序而言,就很难进行解析了。\n这个时候,为了方便程序进行解析,我建议使用结构化格式的日志,这样就不再需要各种复杂的正则表达式来解析日志了。得益于内置的 json 模块,使用 json 就可以很简单地生成的利于程序解析结构化日志,以 logging cookbook 中的例子说明:\nimport json import logging class StructuredMessage(object): def __init__(self, message, **kwargs): self.message = message self.kwargs = kwargs def __str__(self): return \u0026#39;%s \u0026gt;\u0026gt;\u0026gt; %s\u0026#39; % (self.message, json.dumps(self.kwargs)) _ = StructuredMessage # optional, to improve readability logging.basicConfig(level=logging.INFO, format=\u0026#39;%(message)s\u0026#39;) logging.info(_(\u0026#39;message 1\u0026#39;, foo=\u0026#39;bar\u0026#39;, bar=\u0026#39;baz\u0026#39;, num=123, fnum=123.456)) 日志输出结果如下:\nmessage 1 \u0026gt;\u0026gt;\u0026gt; {\u0026#34;fnum\u0026#34;: 123.456, \u0026#34;num\u0026#34;: 123, \u0026#34;bar\u0026#34;: \u0026#34;baz\u0026#34;, \u0026#34;foo\u0026#34;: \u0026#34;bar\u0026#34;} 3.8 参考 https://logmatic.io/blog/python-logging-with-json-steroids/ https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/ https://docs.python.org/3/howto/logging-cookbook.html https://pymotw.com/3/logging/index.html 3.9 小结 虽然这次的日志阐述是以 Python 的日志模块举例,但是绝大部分的语言都内置或者是有第三方的日志支持,所以我分享的技巧还是可以应用到其他的语言的。\n这些都是我在日常项目中的一点体会,与诸君共赏罢。Enjoy :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/logging/","summary":"1 重要性 笔者最近都在负责项目中关于日志的部分,因为跟日志打交道比较多,所以有一些关于日 志感受和技巧想要分享一下。 笔者认为对于各种程序和应用,","title":"你所不可或缺的 – logging"},{"content":"笔者最近思考如何编写高效的爬虫; 而在编写高效爬虫的时候,有一个必需解决的问题就是: url 的去重,即如何判别 url 是否已经被爬取,如果被爬取,那就不要重复爬取。\n一般如果需要爬取的网站不是非常庞大的话,使用Python 内置的 set 就可以实现去重了,但是使用 set 内存利用率不高,此外对于那些不像Python 那样用 hash 实现的 set 而言,时间复杂度是 log(N),实在难说高效。\n1 Bloom Filter 那么如何实现高效的去重呢? 笔者查阅资料之后得知:使用布隆过滤器 (Bloom Filter).\n布隆过滤器可以用于快速检索一个元素是否在一个集合中。布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数(Hash函数)。\n而一般的判断一个元素是否在一个集合里面的做法是:用需要判断的元素和集合中的元素进行比较,一般的数据结构,例如链表,树,都是这么实现的。\n缺点是:随着集合元素的增多,需要比较的元素也增多,检索速度就越来越慢。\n而使用布隆过滤器判重可以实现常数级的时间复杂度(检索时间不随元素增长而增加).那么布隆过滤器又是怎样实现的呢\n1.1 布隆过滤器实现原理 一个Bloom Filter是基于一个m位的位向量(Bit Vector),这些位向量的初始值为0, 并且有一系列的 hash 函数,hash 函数值域为1-m.在下面例子中,是15位的位向量,初始值为0以空白表示,为1以颜色填充\n现在有两个简单的 hash 函数:fnv,murmur.现在我输入一个字符串 \u0026ldquo;whatever\u0026rdquo; ,然后分别使用两个 hash 函数对 \u0026ldquo;whatever\u0026rdquo; 进行散列计算并且映射到上面的位向量。\n可知,使用 fnv 函数计算出的 hash 值是11,使用 murmur 函数计算出的 hash 值是4. 然后映射到位向量上:\n如果下一次,笔者要判断 whatever 是否在字符串中,只需使用 fnv 和 murmur 两个 hash 函数对 whatever 进行散列值计算,然后与位向量做 \u0026ldquo;与运算\u0026rdquo;,如果结果为0, 那么说明 whatever 是不在集合中的,因为同样的元素使用同一个 hash 函数产生的值每次都是相同的,不相同就说明不是同一个元素。\n但是如果 \u0026ldquo;与运算\u0026rdquo; 的结果为1,是否可以说明 whatever 就在集合中呢?其实上是不能100% 确定的,因为 hash 函数存在散列冲突现象 (即两个散列值相同,但两个输入值是不同的), 所以布隆过滤器只能说\u0026quot;我可以说这个元素我在集合中是看见过滴,只是我有一定的不确定性\u0026quot;.\n当你在分配的内存足够大之后,不确定性会变得很小很小。\n你可以看到布隆过滤器可以有效利用内存实现常数级的判重任务,但是鱼和熊掌不可得兼,付出的代价就是一定的误判 (机率很小),所以本质上,布隆过滤器是 \u0026ldquo;概率数据结构 (probabilistic data structure)\u0026rdquo;.\n这个就是布隆过滤器的基本原理。当然,位向量不会只是15位,hash函数也不会仅是两个简单的函数. 这只是简化枝节,为了清晰解述原理而已。\n2 Python BloomFilter 算法都是为了实际问题服务的,又回到爬虫这个话题上。在了解布隆过滤器原理之后,可以很容易地实现自己的布隆过滤器,但是想要实现一个高效健壮的布隆过滤器就需要比较多的功夫了,因为需要考虑的问题略多。\n幸好,得益Python 强大的社区,已经有Python BloomFilter 的库。一个文档中的简单例子:\nfrom pybloomfilter import BloomFilter bf = BloomFilter(10000000, 0.01, \u0026#39;filter.bloom\u0026#39;) with open(\u0026#34;/usr/share/dict/words\u0026#34;) as f: for word in f: bf.add(word.rstrip()) print \u0026#39;apple\u0026#39; in bf 结果为 True\n3 总结 原理就说得差不多了,要想对布隆过滤器有更深的认识,还需要更多的实战。多写,多思考。 Enjoy Python,Enjoy Crawler :)\n4 参考 https://llimllib.github.io/bloomfilter-tutorial/ https://en.wikipedia.org/wiki/Bloom_filter ","permalink":"https://ramsayleung.github.io/zh/post/2017/bloom_filter/","summary":"笔者最近思考如何编写高效的爬虫; 而在编写高效爬虫的时候,有一个必需解决的问题就是: url 的去重,即如何判别 url 是否已经被爬取,如果被爬取,那就不要","title":"爬虫高效去重之布隆过滤器"},{"content":"不久前,Apple 的文件系统 (Apple File System) 新推出,然后各方便一起挤身向前对APFS \u0026ldquo;评头品足\u0026rdquo;,我是不了解 APFS ,所以也没有什么发言权嘛,不过话分两头;\n对Linux的文件系统,我还是有了解过的,所以可以聊聊Linux 的文件系统;与Windows 和Apple 两家商业公司不同,Linux 是开源的,因此,只要你有足够的时间和能力,你就可以自己写一个文件系统,这个也是Linux 文件系统众多的主要原因。\n那么有众多的Linux 文件系统,它们的差异,优缺点又是什么呢?\n1 File System 在开始 \u0026ldquo;大话\u0026rdquo; 各种文件系统的时候,我先想谈谈什么是文件系统。\n一般用户平时都会有上百G 的数据,那么多的数据,应该怎么保存呢?\n不知道怎么回答,可以先类比一下,想象一下你有很多书,你会怎么放置你的书籍呢?直接丢在客厅中间?或者是按书内容分类 放到书架上?如果分类的话,是按怎么划分呢?你买回来的书架一次可以放置多少书籍呢?怎么才能更快地找到你要找的书呢?\n其实文件系统处理,检索文件和你放置,查找书籍是类似的!此外,在Windows 下常见到的格式化,其实就是在不同的文件系统之间切换,那么为什么数据会丢失呢?\n你可以类比成你家里装修,想换新的书架,但是如果不把原来书架上的书籍搬出准备装修的屋子,装修时肯定会损坏书籍嘛!\n2 Journaling 在比较文件系统之前,先聊聊文件系统中的日志 (Journaling), 而有些文件系统是有日志功能的,而有些是没有日志功能的;\n为了了解它们之间的差异,就需要先了解一下什么是日志。日志的出现本质而言都是为了更好地保存数据。\n假设你正在向磁盘里写入文件,突然间断电了,但是你的文件是没有完成写入磁盘的,而你的电脑是不知道是否已经完成写入,所以下次重新来电启动电脑的时候,是没有“人”告知电脑是否要重新写数据的?这样数据就丢失了。\n能不能在文件还没有完成写回磁盘又中途出现错误的时候,告诉系统你的 目标文件还有数据没有写回磁盘,你要记得完成这项工作阿?\n当然可以,这就是文件系统中日志的功能,在日志的协助下,你的电脑会在日志中记着,“我要把某某文件写入到磁盘”,如果顺利完成写入磁盘工作,日志的这项记录就会被删除,如果中途出现异常,例 如断电了,重新启动的时候,你的系统就会发现,我要写入某某文件的工作还没完成,它就会继续未了的事业,这样就可以保障数据不会丢失。\n(而你日常工作生活中,数据的丢失是因为你没有保存数据,即没有把数据写入到磁盘). 如图:\n因为写入日志需要额外的工作,所以需要额外的资源,但是这样的消耗是相当值得地。\n3 Comparison 下面对比一下 各种文件系统的特性\nFigure 1: 图来自archwiki\n如果你想了解你的Linux 内核所支持的文件系统,你可以\ncat /proc/filesystems 现在就来比较一下各种常见的Linux文件系统\n3.1 Ext Ext 是指\u0026quot;Extended file system(扩展文件系统)\u0026quot;,应该算是Linux 文件系统里面的老大爷了,它是从经典的 Minix 的文件系统衍生过来的,为Linux 专门设计的,但是它缺乏很多重要的特性,比如上面提到的日志功能,所以大部份的Linux 发行版本都是不支持 Ext 了\n3.2 Ext2 Ext2 也是不支持日志的,但是它是第一个支持扩展文件属性和2T 容量的Linux 文件系统,但是正由于Ext2 不支持日志功能,它可以更少地写磁盘,所以它适合像USB 这种闪存,但是Ext2 无法被Windows 识别的,所以它的闪存功能更多地被FAT32和exFAT所代替,换言之,Ext2 用的也不多\n3.3 Ext3 Ext3 就是Ext2 带有日志功能的扩展,并且Ext3 也向后兼容Ext2,所以你在Ext2 和Ext3 之间切换也是不需要重新格式化滴,但是最常用的还不是Ext3,而是Ext4 :)\n3.4 Ext4 Ext4 也是向后兼容 Ext3 和Ext2 的,所以你是可以在Ext4,Ext3,Ext2 之间切换而无需格式化文件系统。\nExt4 包含很多新的特性,例如支持存储更大的文件,支持延迟分配以 改进对闪存的支持,还能有效地减少文件的碎片化,提高利用效率。\n显而易见,Ext4 是最先进的 Ext 系列的文件系统,也是大部分Linux 发行版本的默认文件系统\n3.5 ZFS ZFS 最初是给Sun 的Solaris 设计的,Sun 被收购后,现在是属于Oracle 的,ZFS 支持 大量非常先进的特性,比如说 快照 (snapshot),动态存储 (dynamic disk striping), 驱动池等 (drive pool);\n此外ZFS 文件的每个文件都是有校验和的,所以通过检查校验和,就能确定文件的完整性。但是,虽说ZFS 非常强大,却因为ZFS license 的缘故, ZFS 无法添加到 Linux 内核的。如果你非常想要尝试ZFS 的话,你可以自行添加对Linux 发行版本上面添加ZFS 的支持\n3.6 BrtFS Brtfs 是由Oracle 设计的一个支持写时复制 (copy on write) 的现代文件系统;\n而Btrfs 的意思是 B 树文件系统 (B-Tree File System),它支持大量非常先进的特性,例如 动态inode 分配,数据校验和,有效的增量备份,驱动池,最大支持 2^64 byte 容量即16 Eib 大的文件。\nBtrFS 是被设计成取代Ext 系列的文件系统的,只不过因为现在的BtrFS 还没有足够成熟,所以还没有大公司在生产环境使用 BtrFS,但是BtrFS 的未来可期\n3.7 JFS JFS 是IBM 为IBM 自家的AIX 操作系统设计的日志文件系统 (Journaled File System), 后来迁移到了Linux 系统上 (HP-UX 也有一个叫做JFS 的文件系统).\n在AIX 系统上是存在过两代的JFS文件系统的,分别是 JFS1和JFS2,而Linux 上就只有JFS2了(Linux 上的JFS都是指JFS2)。\nJFS 无论在处理大文件还是小文件都有非常不错的表现,并且CPU 占用也是比较低的;JFS 也是支持非常多的特性的,例如 B+ 树,动态Inode 分配,并发IO等。\nJFS 也是一个设计优秀的文件系统并且支持大部分的Linux 发行版本,但是因为它最初是为AIX 设计,所以在处于生产环境上的Linux服务器的测试就不如Ext :(.\n3.8 ReiserFS ReiserFs 是第一个被引进Linux 标准内核的日志文件系统(在内核版本为2.4.1的时候引进),也是Linux 文件系统的一次飞跃,那时的ReiserFS 包含了很多Ext 没有的新特性。\n虽说 ReiserFS 在Linux 有一个非常华丽的开头,但是后来ReiserFS 的开发就陷入了停滞,因为ReiserFS 的核心开发者Hans Reiser(ReiserFS 名字的来由)因为谋杀妻子而被收监 :(。\n而后来的ReiserFS 也没有出现在主要的Linux 内核版本里面,虽说ReiserFS是非常好的文件系统,但是它前景如何,我们也只能拭目以待了\n3.9 XFS XFS 最初是Silicon Graphics 为SGI IRX 操作系统设计的64位高性能文件系统,在2001 年迁移到了Linux.\n得益于XFS 基于 allocation groups 的设计,XFS 拥有非常优秀的并行IO 能力,并支持延迟分配 (delayed allocation) 以改进文件碎片化,在某种程度上,XFS 和Ext 有一定的相似;\n此外,虽说XFS 是高性能的文件系统,但是那只是针对大文件而言的,对于小文件XFS 就有点力所不能及 (当然,这是相对而言).\n所以如果是需要 经常处理大文件的服务器,XFS 会是一个很好的选择\n4 小结 如果将Linux 的文件系统进行分类的话,还可以分成 FUSE (Filesystem in Userspace, 让用户在没有权限的情况下,创建自己的文件系统), Stackable file System (先进的多层次统一文件系统), Read-only file systems (只读文件系统), Clustered file systems (集群文件系统)等等。\nLinux 的文件系统还有很多,每一种都有自己的特点;但是如果你想问:\u0026ldquo;Linux 最好的文件系统是什么?\u0026quot;;这个问题就跟 \u0026ldquo;最好的Linux 发行版本是什么?\u0026rdquo;, \u0026ldquo;最好的文本编辑器是什么?\u0026rdquo; 一样,是没有标准答案,一千个人都有一千个哈姆雷特了,你的哈姆雷特是什么样子,只有你自己清楚。\n不同的文件系统对应不同的场景,只有针对特定场景的最优解决方案!如果还不知道怎么选择,那就选择Ext4 吧,无法做出选 择时,默认的就是最好 :)\n5 参考 https://en.wikipedia.org/wiki/File_system https://btrfs.wiki.kernel.org/index.php/Main_Page https://en.wikipedia.org/wiki/JFS_(file_system) https://en.wikipedia.org/wiki/ReiserFS https://www.howtogeek.com/howto/33552/htg-explains-which-linux-file-system-should-you-choose/ https://en.wikipedia.org/wiki/ZFS https://en.wikipedia.org/wiki/XFS https://wiki.archlinux.org/index.php/file_systems ","permalink":"https://ramsayleung.github.io/zh/post/2017/linux_file_system/","summary":"不久前,Apple 的文件系统 (Apple File System) 新推出,然后各方便一起挤身向前对APFS \u0026ldquo;评头品足\u0026rdquo;,我是不了解 APFS ,所以也没有什么","title":"大话Linux文件系统"},{"content":"笔者最近一直在思考,关于工具,关于折腾,关于其中的付出与收获\n1 乐趣 1.1 Linux 回顾笔者大学,从大一开始就是一个不停折腾的过程,在其他的同学还在用Windows玩游戏的时候,笔者已经把系统换成Linux了.\n记得最开始装的第一个发行版本是 Kali Linux一个黑客和安全专家使用的发行版本,上面有不计其数的渗透工具;毕竟每一个学 计算机的孩子心中都是有个 hacker dream的嘛,笔者也不例外:)。\n只是笔者最开始并没有能力去使用Kali Linux; 甚至连基本的命令都完全不了解;笔者相当沮丧,因为 hacker并不是想象中的那么容易的. 笔者后来就把自己的系统重装,装了个 Ubuntu, 买 了一本《鸟哥的Linux私房菜》,一边学,一边用,就这样进了Linux的坑了。\n《鸟哥的私房菜》大概看了两年,翻过好几次了,后来也看了《服务器篇》,前后共看了近十本Linux的书籍吧,整个大学大概在自己电脑上前后装了10种的发行版本吧\n1.2 Vim/Emacs 《鸟哥Linux私房菜》一书中,鸟哥强推Vim, 其他的Linux论坛也对 Vim 推崇备至,笔者 很自然就随大流去学习Vim了,开始的时候,真的非常不习惯,编辑个文本还要分那么多 的模式,真的是反人类,连个单词都不能输入.\n后来,好不容易输完数据之后,又不知道怎么保存 (Ctrl-S? 想多了), 然后直接关闭,重新打开又有什么提示说是否恢复数据。 觉得为何有这样异类难用的编辑器, 真不知道为什么那么多人推崇。\n但当笔者坚持这种煎熬半个月以后,就发现其他的编辑器都非常低效,没错,就是非常低效,又要鼠标, 又要键盘,不断地切换,效率实在太低了。\n就这样,笔者糊里糊涂就进入了Vim的阵营,直到遇到 Vim实用技巧 这本神书,它跟你讲述了如何实现 Vim \u0026ldquo;Edit Text at the speed of thought\u0026rdquo; 的理念,的确是神书. 自然,笔者对Vim就更 \u0026ldquo;坚贞不渝\u0026rdquo; 了;\n直到有一天,在浏览Linux/Unix历史的时候掀开了 Editor War(Vim与Emacs之战)一章, 那些 Emacser 竟敢宣称 Emacs 比 Vim 好用,笔者对此并不服气,不相信有比Vim强的编辑器,这可是编辑器之神阿,而笔者是一个很实在的人,没用过 Emacs 是不会随便发 言的,所以就跑去折腾Emacs ,打算折腾回来再跟 Emacser 论道,结果嘛,笔者就 \u0026ldquo;叛 逃\u0026rdquo; 到了 Emacs 了 :)。\n作为一个曾经的 Vim 粉丝,笔者就抛开 \u0026ldquo;宗教因素\u0026rdquo; 比较一下 Vim 跟 Emacs:\nVim的 modal edit 是最好的,真的难有敌手,所以这也是为什么在各种的 IDE/editor 都有 Vim 插件的原因; 但 Emacs 的扩展性也是无可匹敌 (毕竟是伪 装成编辑器的操作系统,只缺一个好用的编辑器),又因为 Emacs lisp这种真正的编程 语言(对比之下 viml真的很弱)的存在, Emacs 就有了无限可能,这也是 Emacs 上面有非常多高质量的插件的原因之一,其中最典型的例子就是 Org-mode ,无愧神器之名,笔者现在的博文也是在 Emacs 里面利用 Org-mode 编写,然后发布的。 而至于选择神之编辑器还是编辑器之神,那就是信仰的抉择了。笔者选择了在 Emacs 里面使用 Vim 的编 辑模式 Evil :)\n1.3 Misc 除了折腾编辑器之外,笔者还折腾了各种的命令行,Shell 脚本,还有 Firefox, Chrome浏览器。当初那些 Windows 用户一直说 Linux 的桌面丑,笔者就去了折腾各种 的桌面环境 (window manager)这种折腾可不是 Windows 上面的切换壁纸哦,后来把桌 面折腾得非常炫,以至同学看到笔者的电脑就说我装了黑苹果,然而事实并非如此。\n如果你也好奇那些炫酷的 Linux/Unix 桌面,可以查看 https://reddit.com/r/unixporn 上面有各种 Linuxer/Unixer 分享的炫酷桌面\n2 投入产出比 2.1 值得否? 笔者的大学基本都是在学习并折腾各种的工具或者技术,并且乐在其中.\n但是有一天当笔 者又在跟朋友推荐 Vim/Emacs, 或许是笔者喋喋不休实在太多次了,朋友回了笔者一句 \u0026quot; notepad++, sublime text 不一样可以写代码,你为什么还要花那么多时间去折腾这些东西呢,你写脚本都可以直接用IDE,为什么还要自己折腾呢,把时间花到其他地方不更好么?\u0026quot;.\n笔者难以反驳,笔者之前一直是玩得很开心,从未曾考虑过这个问题,所以那个时候开始询问自己,这是否值得,自己是否要把时间用到其他地方?\n在之后的一段 时间,笔者都难掩沮丧,因为觉得自己浪费了很多的时间来完成一些无用功!\n2.2 长期投资 但是最终笔者还是解答了自己的疑问! 笔者之前付出是绝对值得的,先不说笔者在其中获得的乐趣,乐趣是无价的嘛 :)\n笔者在折腾的过程中也学到很多新的东西: 为了用好我配置的 Emacs, 笔者使用 Emacs 写了很多不同的脚本,这种感觉就好像,侠士为了展示手中利刃之威力,苦练武艺; 而在折腾 Emacs lisp 的过程中,也学习很多函数式编程的思想,甚至掌握了一门新的语言 \u0026ndash; elisp, 虽说它的语法很奇怪。\n其实笔者的付出是长期投资,学会了 Vim 的 moral edit, 也可以在其他 IDE使用嘛,这并不矛盾的,无鼠标操作是非常高效的,也是所谓的 modern editor 无法比拟的。\n最重要的是,在折腾过程中所培养的解决困难的动手能力,也是可以受益终生的,笔者知道如何去google,如何去查找文档,如何去提问; 而且在不停的折腾过程中,你对某样技术的理解是单纯的理论学习无法比拟的;\n在大学的操作系统课,笔者基本是没听老师讲解课程的,因为老师讲的,笔者基本都知道,甚至实践过。\n3 工具集 在经历大学的折腾后,笔者现在很多的工具集都基本确定下来了;这些也是对笔者而言, 最高效的工具集\n3.1 编辑器 Emacs 神之编辑器,主力编辑器 个人配置 Vim 编辑器之神,一般在服务器改改配置的时候用 3.2 浏览器 Chrome 不常用,特定情况下使用 Firefox 日常浏览器,笔者也折腾过非常久,所以即使 Chrome 很强,笔者只为 Firefox 倾心 3.3 FireFox扩展 因为 FireFox 对插件的限制相对宽松,所以社区开发出了非常多非常强的插件,笔者就 列举一下自己使用的扩展集吧\nbitwarden -免费的密码管理器,比LastPass强 Bluhell Firewall-轻量级的广告拦截器,和隐私保护 Clear Cache -更方便清除缓存 FalshGot -下载扩展器,配合axel或者aria2使用更佳 FoxyProxy -类似Chrome SwitchOmega,但是略有不如,配合Shadowsocks翻墙,必备 Ghostery -隐私保护 Greasemonkey -用户自定义插件管理器,神器 HttpRequester -类似Chrome Postman,发送Http请求 HTTPtoHTTPS -尽可能使用Https,提高安全性 KeySnail -把Firefox快捷键设置为Emacs快捷键,无鼠标操作,你也可以为该插件编 写插件.神器,这个是我无法切换回Chrome的原因 Octotree -以树状目录来浏览Github代码,非常方便 uBlock Origin -广告blocker,低资源要求,感觉比Adblock plus好用 User Agent Switcher -切换User Agent,写爬虫时非常有用 Xpath checker -直接获取Dom节点的Xpath,配合Lxml解析网页非常高效 Firebug -神器,但是已经停止开发了。 3.4 桌面 i3wm, 在折腾过炫酷的 KDE, Gnome, xfce, 而笔者最后选择的是 i3这个平铺桌面,可 以实现无鼠标操作,非常轻量。\n3.5 命令行 3.5.1 Shell zsh -配合oh-my-zsh,可以非常高效,但是使用频率不高 Eshell -与Emacs集成,是笔者的主力Shell,不过某些Eshell不支持的操作,只好在 zsh完成 3.5.2 过滤器 ag grep的加强版,速度快 ripgrep 最快的命令搜索工具 percol 过滤文本,神器 fasd 目录跳转,文件查找,高效 3.5.3 misc httpie http客户端,发送http请求 htop top的改进版,信息更详细 glances 一个好用的系统监控工具 ncdu Linux最好用的磁盘分析工具 git Linus又一神作 其它就是常用的内置命令了\n3.6 影音 VLC Linux最好用的播放器 网易云音乐 国产良心音乐软件 musicbox 网易云音乐的社区命令行版本 3.7 其它 Fcitx -中文输入 VirtualBox -开源虚拟机 Shadowsocks 翻墙必备 Zeal 类似Mac 上的Dash,查看各种文档 Intellij Idea Java IDE(写Java 我是不会使用Emacs 的:) ) Datagrip SQL IDE 使用最频繁的就是 I3+Firefox+Emacs,实现无鼠标操作,因为使用鼠标太慢了,效率太 低。笔者也不是一个疯子,所以只会用Emacs 做力所能及的事情,煮咖啡就算了。\n4 结语 如果让笔者的大学重来一遍,估计笔者还是会这样折腾,因为自己动手的感觉还是很美好,充满成就感,这也是玩游戏所不能给予我的感觉,毕竟 hacker 不是想出来的嘛,是做出来的。\n更新 2017-4-21\n附上一篇关于折腾的文章 (需翻墙) The importance of ZheTeng\nEnjoy tweaking;Enjoy Linux :) ","permalink":"https://ramsayleung.github.io/zh/post/2017/about_tool_about_tweak/","summary":"笔者最近一直在思考,关于工具,关于折腾,关于其中的付出与收获 1 乐趣 1.1 Linux 回顾笔者大学,从大一开始就是一个不停折腾的过程,在其他的同学还在用Wi","title":"关于工具,关于折腾"},{"content":"近两日,闲来无事,就写了些端口扫描器,重温TCP/IP协议栈的部分原理。\n1 端口扫描器 所谓的端口扫描器,其实是用来检测目标服务器有哪些端口开放所使用的工具,一般是管理员用来进行安全加固,检测是否有无意开放的端口;或者是恶意攻击的人员在进行攻击前的准备工作。\n所以综述上下,端口扫描器是用来确定目标机器 (本地机器或者远程机器)的特定服务的可用性\n2 端口扫描原理 上面提到过,端口扫描器是用来确定目标机器的服务的可用性的;那么具体是怎么确定的呢?如果还没有答案的话,可以换个角度来思考这个问题。\n假如你想确定邻居家的妹子是否在家,你会怎么办?这不简单么,问一下不就清楚了么?对阿,对于服务器的端口也可以适用这样的方法嘛。端口扫描的原理都是“问一下”,只是问的方法不一样而已,就好像你是决定直接过去敲邻居门,还是打电话过去一样,殊途同归,方法是没有对错的之分,差异只是方法的优劣。\n2.1 TCP连接扫描 这是最简单的一种方法,一般被称为连接扫描,即利用 socket 对目标机器进行连接尝试,如果能够成功建立三次握手连接,那就说明你用 socket 连接的端口是开放的;然后你就可以断开连接,扫描下一个目标端口了 (如果不断开连接,这就是一种 DDOS攻击了).\n只不过TCP连接扫描不是很常用,不仅是因为容易被发现,而且你的IP地址也可能会被目标地址记录下来的(对于攻击者来说,隐藏身份是很重要的)\n2.1.1 代码解析: def scan(self, args): host, port = args try: # Create a TCP socket and try to connect # AF_INET for ipv4,AF_INET6 for ipv6 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) sock.close() return host, port, True except (socket.timeout, socket.error): return host, port, False 因为原理很简单,所以核心代码也是很简洁的,只是建立 socket 然后进行连接,如果连接不上,就很大几率说明端口是关闭的 (并不是绝对的,例如socket超时的异常可能就是因为网络异常,不一定是目标机器的缘故)\n2.2 SYN扫描 再回顾一下TCP的三次握手:\n2.2.1 TCP三次握手 TCP建立连接时,首先客户端和服务器处于close状态。 然后客户端发送SYN同步位,此时客户端处于SYN-SEND状态,服务器处于lISTEN状态,当服务器收到SYN以后,向客户端发送同步位SYN和确认码ACK,然后服务器变为SYN-RCVD,客户端收到服务器发来的SYN和ACK 后,客户端的状态变成ESTABLISHED(已建立连接), 客户端再向服务器发送ACK确认码,服务器接收到以后也变成ESTABLISHED。然后服务器客户端开始数据传输 如图:\nFigure 1: 图来源于Google\n2.2.2 SYN扫描原理 SYN+ACK\n那么现在再回到SYN扫描上来.如果在发送第一次握手的 SYN flag 时,目标机器回复了SYN+ACK,这不就说明笔者发送的TCP包中的目标端口是开放的么!如果不开放,服务器就不会期待第三次握手了,也不会给笔者发送 SYN+ACK 了;如图:\n图来自 http://resources.infosecinstitute.com/port-scanning-using-scapy/\nRST\n如果第二次握手的时候,目标机器回复的不是 SYN+ACK, 而是 RST, 就说明TCP包中的目标端口在目标机器上是关闭的;如图\n图来自 http://resources.infosecinstitute.com/port-scanning-using-scapy/\nFiltered\n上面提及了目标端口的开放和关闭两种状态,那么,还有没有其他状态呢?什么,还有其他状态?\n如果就SYN扫描而言,就还有 filtered被过滤之一说,如果还有加上其他扫描技术, 就还有其他状态了。\n回到SYN扫描,当返回的不是服务器想建立第二次握手的包,而是ICMP的包就有可能被过滤,例如响应信息是ICMP错误信息类型3代码3(无法到达目标:端口不可达)这里出现的端口不可达,可能就是被防火墙过滤了,如果是类型3代码13(无法到达目标:通信被管理员禁止),那也是被过滤了。\n更多信息就要查询ICMP的官方文档 了\n2.2.3 代码解释 def scan(self, args): dst_ip, dst_port = args src_port = RandShort() answered, unanswered = sr(IP(dst=dst_ip) / TCP(sport=src_port, dport=dst_port, flags=\u0026#34;S\u0026#34;), timeout=self.timeout, verbose=False) for packet in unanswered: return packet.dst, packet.dport, \u0026#34;Filtered\u0026#34; for (send, recv) in answered: if(recv.haslayer(TCP)): flags = recv.getlayer(TCP).sprintf(\u0026#34;%\u0026#34;) if(flags == \u0026#34;SA\u0026#34;): # set RST to server in case of ddos attack send_rst = sr(IP(dst=dst_ip) / TCP(sport=src_port, dport=dst_port, flags=\u0026#34;R\u0026#34;), timeout=self.timeout, verbose=True) return dst_ip, dst_port, \u0026#34;Open\u0026#34; elif (flags == \u0026#34;RA\u0026#34; or flags == \u0026#34;R\u0026#34;): return dst_ip, dst_port, \u0026#34;Closed\u0026#34; elif(recv.haslayer(ICMP)): icmp_type = recv.getlayer(ICMP).type icmp_code = recv.getlayer(ICMP).code if(icmp_type == ICMP_TYPE_DESTINATION_UNREACHABLE and icmp_code in ICMP_CODE): return dst_ip, dst_port, \u0026#34;Filtered\u0026#34; else: return dst_ip, dst_port, \u0026#34;CHECK\u0026#34; 核心代码很简单,就是发送建立连接的握手请求,然后根据不同的返回结果判断不同的状态。\n如果端口确定是开放,那就发送 R flag给目标机器结束握手 (如果不结束握手的话,那就是DDOS,这也是DDOS最常用的手段); 因为这次不是使用操作系统原生的 socket, 而是自行构造发送 IP数据包,所以需要使用一个很强大的构造 操作各种数据包的工具 \u0026ndash; scapy\n(顺便说一下,如果在Windows下安装 scapy,需要非常多的步骤,如果是Unix/Linux,只需几行命令:) )\n3 后话 简单的扫描器就已经完成了,加上多线程的功能提高性能。\n很想吐嘈一下,真的对Python 的多线程恨铁不成钢,只好换成多进程;也给 Python2 Python3 API的改变折腾得够呛,不禁让笔者怀念起Java:(\n其实正如笔者开头所言的,你确定隔壁家妹子是否在家的方法有很多,你扫描端口的方法也有很多:例如 XMAS scan(TCP圣诞树扫描), FIN scan,Null scan, ACK scan, Window scan, UDP scan等。\n当然你如果不想针对各种扫描都写一个扫描器,你可以使用 nmap 这个地球最强大的扫描器 (没有之一). 在Python也已经有与nmap整合的强大的包 python-nmap\n扫描器完整代码地址 https://github.com/samrayleung/PortScanner\n参考\nhttp://resources.infosecinstitute.com/port-scanning-using-scapy/ ","permalink":"https://ramsayleung.github.io/zh/post/2017/port_scanner/","summary":"近两日,闲来无事,就写了些端口扫描器,重温TCP/IP协议栈的部分原理。 1 端口扫描器 所谓的端口扫描器,其实是用来检测目标服务器有哪些端口开放","title":"Python多线程端口扫描器"},{"content":"文本三剑客之 Grep\ngrep - print lines matching a pattern\n今天我想聊聊 grep 这个命令;据说,有Unix/Linux 的地方就会有 grep, 这个可能是安装得最广泛的命令之一;那么 grep 是用来干什么的呢?\ngrep 其实是用来在文件中搜索特定内容或者模式的工具(配合正则表达式“食用”,味道更佳 :))现在就来一起看看grep 的用法\n1 基本用法 1.1 基础用法 现在假设有一个简单的文本文件(双城记开头)tinytale.txt,内容如下\nit was the best of times it was the worst of times it was the age of wisdom it was age of foolishness it was the epoch of belief it was the epoch of incredulity it was the season of light it was the season of darkness IT WAS THE SPRING OF HOPE IT WAS THE WINTER OF DESPAIRE\n现在开始介绍 grep 的基本用法: grep 的基本用法很简单的,假设我想要搜索单词 darkness\ngrep darkness /tmp/tinytale.txt 输出如下:\nit was the season of light it was the season of darkness 1.2 结合正则表达式 默认情况下, grep 是开启正则表达式的模式的,所以你可以直接在文件搜索中使用 正则表达式。现在在文件中搜索以字母 e 开头后接三个字符,然后以 h 结尾的单词:\ngrep \u0026#34;e...h\u0026#34; /tmp/tinytale.txt 输出如下:\nit was the epoch of belief it was the epoch of incredulity 可以看到,正则表达式匹配了 epoch 这个单词。正则表达式的威力无与伦比的,把 grep和正则表达式结合起来可以更好地发挥 grep 这个工具的潜力;而本文主要是介绍 grep, 更多有关正则表达式的用法不细讲了\n1.3 统计出现的次数 有时,如果你需要统计某种模式或者某个单词出现的个数,你会发现 grep 非常有用;\n要实现该功能,只需给 grep 添加 -c 参数;例如统计单词 the 出现的个数:\ngrep -c the /tmp/tinytale.txt 结果输出如下:\n4 文本中包含4个 the\n1.4 忽略大小写 前面提到, grep 默认是使用正则表达式来搜索文件的,所以 grep 是区分大小写的;\n如果你想修改 grep 的默认行为来忽略大小写,你可以添加 -i 参数\ngrep -i the /tmp/tinytale.txt 输出结果如下:\nit was the best of times it was the worst of times it was the age of wisdom it was age of foolishness it was the epoch of belief it was the epoch of incredulity it was the season of light it was the season of darkness IT WAS THE SPRING OF HOPE IT WAS THE WINTER OF DESPAIRE 可以发现 THE 也是可以被 grep 搜索到的;但是如果没有添加 -i ,你只会看到4行输出。\n当然你可以在正则表达式里面添加忽略大小写的模式,只是直接添加 -i 会简单很多。\n2 搜索多个文件 上面搜索的都只是单个文件,而 grep 可以让你同时搜索多个文件;现在就来看看怎么搜索多个文件吧。\n下面两种写法结果都是一样的,但是我个人推崇第一种,因为可以输入更少一些内容 :)\ngrep belief /tmp/{tinytale.txt,tale.txt} grep belief /tmp/tinytale.txt /tmp/tale.txt 输出结果如下:\ntinytale.txt:it was the epoch of belief it was the epoch of incredulity tale.txt:it was the epoch of belief it was the epoch of incredulity tale.txt:pains of by rearing her in the belief that her father was dead tale.txt:this was no passive belief but an active weapon which they flashed tale.txt:belief in solomon deducting a mere trifle for this slight mistake tale.txt:you will bear testimony to what i have said and to your belief in it tale.txt:herself into the show of a belief that they would soon be reunited 可以看到, grep 把匹配到单词的那一行内容和对应的文件都显示出来了,你就可以很方便地看到搜索结果,并知道匹配单词的来源。\n如果你也像我这样,不想输入那么多的内容,你可以使用正则表达式匹配所有的文本文件,如下:\ngrep belief /tmp/*.txt 输出结果也会跟上面一致 (假设你 tmp 目录下只有两个文本文件); 我告诉grep 搜索**/tmp** 下所有的 .txt 文件。\n2.1 递归搜索 你也可以使用 grep 递归搜索目录;你只需在指定目录后,添加 -R , grep 就会 递归搜索指定目录的所有子目录。我已经把当前目录切换到 /tmp:\ngrep -R \u0026#34;belief\u0026#34; . 输出结果如下:\n./tale.txt:it was the epoch of belief it was the epoch of incredulity ./tale.txt:pains of by rearing her in the belief that her father was dead ./tale.txt:this was no passive belief but an active weapon which they flashed ./tale.txt:belief in solomon deducting a mere trifle for this slight mistake ./tale.txt:you will bear testimony to what i have said and to your belief in it ./tale.txt:herself into the show of a belief that they would soon be reunited ./tinytale.txt:it was the epoch of belief it was the epoch of incredulity 结果展示了一系列在当前目录和子目录匹配 belief 的文件。此外你也可以排除掉某些你 不需要搜索的文件,例如有一个 foo.xml 的文件,里面也可能会有 belief 这个单词, 但是你就是不想搜索这个文件,或者全部的 .xml 文件,你可以这么玩:\ngrep -R --exclude=\u0026#34;*.xml\u0026#34; \u0026#34;belief\u0026#34; . 3 在标准输入搜索 grep 也是过滤器,所以 grep 自然而然具有处理标准输入输出的能力了;处理其他命令的输出结果也是 grep 非常常用的场景之一。假设你现在的 vim 突然卡顿,挂了:),你想要 kill 掉 vim 的进程,你可以:\nps -e|grep vim 结果输出如下:\nsamray 21939 1 0 19:42 ? 00:00:00 gvim 其中第一条记录就是你想要搜索的进程了,你运行 kill 21939 就可以杀掉 vim 的进程了;因为我系统的是图型化界面的 vim, 所以是 gvim.\n正如我之前的文章提到的那样,单纯的过滤器的用处似乎不大,但是如果结合起来就会威力无穷至于,如何结合,就需要慢慢探索了。\n4 反向搜索 现在执行的搜索都是匹配搜索,即将匹配的内容显示出来,而 grep 还有反向搜索的功能 (invert Searches)就是将不包含有指定模式的内容显示出来。\n该功能在用来修改有很多注释的配置文件时特别有用;例如常用的服务器软件 nginx 的配置文件是默认是含有很多注释的,如下\n#user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 1024; # multi_accept on; } http { ## # Basic Settings ## sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # server_tokens off; # server_names_hash_bucket_size 64; # server_name_in_redirect off; include /etc/nginx/mime.types; default_type application/octet-stream; ## # SSL Settings ## ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; ## # Logging Settings ## log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; $status $bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#34;$http_user_agent\u0026#34; \u0026#34;$gzip_ratio\u0026#34;\u0026#39;; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; ## # Gzip Settings ## gzip on; gzip_disable \u0026#34;msie6\u0026#34;; # gzip_vary on; # gzip_proxied any; # gzip_comp_level 6; # gzip_buffers 16 8k; # gzip_http_version 1.1; # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; ignore_invalid_headers on; client_header_timeout 240; client_body_timeout 240; send_timeout 240; client_max_body_size 100m; proxy_buffer_size 128k; proxy_buffers 8 128k; upstream tomcat_server{ server 127.0.0.1:8080 fail_timeout=0; } upstream gunicorn_server{ server 127.0.0.1:5000 fail_timeout=0; } server{ server_name 127.0.0.1; listen 443; # ssl on; # ssl_certificate /etc/letsencrypt/live/samray.ren/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/samray.ren/privkey.pem; location / { # Forward SSL so that Tomcat knows what to do proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://tomcat_server; proxy_set_header X-Forwarded-Proto https; proxy_redirect off; proxy_connect_timeout 240; proxy_send_timeout 240; proxy_read_timeout 240; } location /test{ return 402; } location /weixin { # try_files $uri @proxy_to_app; return 402; } location @proxy_to_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://gunicorn_server; } } } #mail { #\t# See sample authentication script at: #\t# http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript # #\t# auth_http localhost/auth.php; #\t# pop3_capabilities \u0026#34;TOP\u0026#34; \u0026#34;USER\u0026#34;; #\t# imap_capabilities \u0026#34;IMAP4rev1\u0026#34; \u0026#34;UIDPLUS\u0026#34;; # #\tserver { #\tlisten localhost:110; #\tprotocol pop3; #\tproxy on; #\t} # #\tserver { #\tlisten localhost:143; #\tprotocol imap; #\tproxy on; #\t} #} 里面实在有太多的注释了,虽说是很好的参考,但是看多了会感觉很碍眼,所以你希望可以有一份没有注释的配置文件,你就可以使用 grep 和参数 -v:\negrep -v \u0026#34;#|^$\u0026#34; /etc/nginx/nginx.conf \u0026gt;/tmp/nging.conf 结果如下:\nworker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 1024; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; ssl_prefer_server_ciphers on; log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; $status $bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#34;$http_user_agent\u0026#34; \u0026#34;$gzip_ratio\u0026#34;\u0026#39;; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; gzip_disable \u0026#34;msie6\u0026#34;; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; ignore_invalid_headers on; client_header_timeout 240; client_body_timeout 240; send_timeout 240; client_max_body_size 100m; proxy_buffer_size 128k; proxy_buffers 8 128k; upstream tomcat_server{ server 127.0.0.1:8080 fail_timeout=0; } upstream gunicorn_server{ server 127.0.0.1:5000 fail_timeout=0; } server{ server_name 127.0.0.1; listen 443; location / { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://tomcat_server; proxy_set_header X-Forwarded-Proto https; proxy_redirect off; proxy_connect_timeout 240; proxy_send_timeout 240; proxy_read_timeout 240; } location /test{ return 402; } location /weixin { return 402; } location @proxy_to_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://gunicorn_server; } } } egrep 是 grep 的扩展,你也可以通过 -E 使用扩展功能。就这样,你就可以得到一份很“干净”的配置文件了。\n5 小结 在以前 grep 是 hacker 工具箱里面审查源代码必不可少的工具之一,但是随着技术的发展,似乎对比其他同类型的工具, grep 的性能已经难尽人意,特别是对比 ag 这个搜索神器;\n虽说很多人都已经转移到了 ag 阵营,但是因为 grep 被广泛预装到各类的Linux/Unix 机器,所以 grep 还是使用得很广泛滴。\n更多 grep 的用法就需要查询手册了:\nman grep Enjoy Shell :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/grep/","summary":"文本三剑客之 Grep grep - print lines matching a pattern 今天我想聊聊 grep 这个命令;据说,有Unix/Linux 的地方就会有 grep, 这个可能是安装得最广泛的命令之一;那么 grep 是用来","title":"Linux/Unix Shell 二三事之过滤器grep"},{"content":"今天在完成《算法》上的练习的时候,要对文件进行读写,而书上的例子是直接通过 Linux/Unix的重定向来实现的,我要把它修改成直接读取文件。\n此外,个人一直觉得Java IO 很容易混淆,因为有太多的选择(但是这也是Java 的强大之处),现在Java8 又新增了文件的API,所以我就对文件IO作了个小结\n1 Read 我今天的需求是要逐行读写文本文件,我就以此为例子了;测试文件是 /tmp/test.txt\ntest this is a test this is another test 1.1 BufferedReader 虽然已经有了Java8的 Stream, 但是经典的东西总是历久弥新的;例如 BufferedReader就是JDK1.1就发布了的文件读API (对可能出现的IOException,使用更优雅try-with-resource并免去编写大量手动关闭资源的模板代码的麻烦)\npublic void testBufferedReader(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; try(BufferedReader bufferedReader=new BufferedReader(new FileReader(filePath))){ String line; while((line=bufferedReader.readLine())!=null){ System.out.println(line); } }catch (IOException ex){ ex.printStackTrace(); //do something } 1.2 Scanner 对发布于JDK1.5的Scanner,大部份Java 程序员都是相当熟悉的,因为总是用它来读取标准输入的数据。 现在只要把从标准输入变为从文件读取数据就可以了\npublic void testScanner(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; try(Scanner scanner=new Scanner(new File(filePath))){ while(scanner.hasNextLine()){ System.out.println(scanner.nextLine()); } }catch (IOException ex){ ex.printStackTrace(); //do something } } 1.3 BufferedReader+Stream Files 类作为Java NIO 的一部分在Java 7被引入,该类提供了一系列操作文件的方法,而在Java8 又引入了另外有用的特性让Java 开发者可以更方便地操作文件。\n例如 lines() 方法,可以让 BufferedReader 可以把文件内容以 Stream 的形式返回;读取文件, 并把文件内容存储到 ArrayList.\npublic void testBufferedReaderAndStream(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; list=new ArrayList\u0026lt;\u0026gt;(); try(BufferedReader bufferedReader= Files.newBufferedReader(Paths.get(filePath))){ list=bufferedReader.lines().collect(Collectors.toList()); }catch (IOException ex){ ex.printStackTrace(); //do something } } 得益于强大的 Stream 你可以在读取文件是进行更多的操作;例如只存储含有 this 字符的行并且删除结尾的空白符\npublic void testBufferedReaderAndStream(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; list=new ArrayList\u0026lt;\u0026gt;(); try(BufferedReader bufferedReader= Files.newBufferedReader(Paths.get(filePath))){ bufferedReader.lines().filter(line-\u0026gt;line.contains(\u0026#34;this\u0026#34;)).map(String::trim) .forEach(System.out::println); }catch (IOException ex){ ex.printStackTrace(); //do something } } 1.4 lines+Stream 也可以直接使用 lines 方法来逐行读取文本文件,只是对比 newBufferedReader + Stream, 前者颗粒度更细;\npublic void testlinesAndStream(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; list=new ArrayList\u0026lt;\u0026gt;(); try(Stream\u0026lt;String\u0026gt; stringStream=Files.lines(Paths.get(filePath))){ stringStream.filter(line-\u0026gt;line.contains(\u0026#34;test\u0026#34;)).forEach(System.out::println); }catch (IOException ex ){ ex.printStackTrace(); //do something } } 如果你是读取不是很大的文件的时候,你可以一次就把文件都进内存; Files 已经为你提供这样的方法\npublic static void testReadAllLines(){ String filePath=\u0026#34;/tmp/test.txt\u0026#34;; List\u0026lt;String\u0026gt; lists= null; try { lists = Files.readAllLines(Paths.get(filePath)); } catch (IOException e) { e.printStackTrace(); } for (String list : lists) { System.out.println(list); } } 需要注意的是 try-with-resource 是不支持 readAllLines .此外大文件请慎重使用 readAllLines,因为你可能出现 OutOfMemoryException\n不得不说,新加入的API的确更加优雅\n2 Write 我就把测试文件重新写到一个新的文件,实现复制的功能,因为我的文件很小,所以我直接把测试独的文件加载到内存\n2.1 BufferedWriter 与 BufferedReader 对应,对文件进行写\npublic void testBufferedWriter() { String readFilePath = \u0026#34;/tmp/test.txt\u0026#34;; String writeFilePath = \u0026#34;/tmp/test1.txt\u0026#34;; try { List\u0026lt;String\u0026gt; lines = Files.readAllLines(Paths.get(readFilePath)); try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(writeFilePath))) { for (String line : lines) { bufferedWriter.write(line+\u0026#34;\\n\u0026#34;); } } catch (IOException ex) { ex.printStackTrace(); //do something } } catch (IOException ex) { ex.printStackTrace(); //do something } } 你也可以将 BufferedReader 和 Files 结合\npublic static void testBufferedWriterAndFiles() { String readFilePath = \u0026#34;/tmp/test.txt\u0026#34;; String writeFilePath = \u0026#34;/tmp/test1.txt\u0026#34;; try { List\u0026lt;String\u0026gt; lines = Files.readAllLines(Paths.get(readFilePath)); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(Paths.get(writeFilePath))) { for (String line : lines) { bufferedWriter.write(line + \u0026#34;\\n\u0026#34;); } } catch (IOException ex) { ex.printStackTrace(); //do something } } catch (IOException ex) { ex.printStackTrace(); //do something } } 2.2 Files.write 使用 Files.write() 也可以写出相当优雅的代码\npublic void testFilesWrite() { String readFilePath = \u0026#34;/tmp/test.txt\u0026#34;; String writeFilePath = \u0026#34;/tmp/test1.txt\u0026#34;; try { List\u0026lt;String\u0026gt; lines = Files.readAllLines(Paths.get(readFilePath)); Files.write(Paths.get(writeFilePath), lines); } catch (IOException ex) { ex.printStackTrace(); //do something } } 这就是各种对文本文件进行读写的方法;不知道为什么,我觉得似乎写文件的方法似乎比读文件的方法少,例如读文件有 Scanner , 而写文件似乎没有 Printer :(\n不应该是匹配的么,或许我是不知道?\nEnjoy Java :)\n3 参考 http://winterbe.com/posts/2015/03/25/java8-examples-string-number-math-files/ http://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#lines-java.nio.file.Path- https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html ","permalink":"https://ramsayleung.github.io/zh/post/2017/java8_file_io/","summary":"今天在完成《算法》上的练习的时候,要对文件进行读写,而书上的例子是直接通过 Linux/Unix的重定向来实现的,我要把它修改成直接读取文件。","title":"Java读写文件小结"},{"content":"最近,我发现很多Emacs 用户对Ivy 很感兴趣;而且大部份用户都是已经了解过Helm 或者Ido的 当有人在Reddit 上面问 选择Helm 还是Ido这类问题的时候,我觉得我会给出我自己的选择: Ivy,即使我是一个前Helm 的狂热用户 最大或者最小\nHelm 和Ivy 都是补全框架.这意味着它们都是Emacs生态系统中用来在用户输入后缩窄可供选择选项的范围的工具。 很自然而然想起的通用例子就是搜索文件。Helm 和Ivy 都可以帮助用户快速搜索文件\n它们两者都是框架,这意味着它们都可以用在那些需要补全或者缩窄范围的复杂命令。\n例如Helm 有一个命令(helm-google-suggest)可以模拟Goole 的搜索框,并在你输入时给出相应的google 提示\nIvy 和Helm 都有相同的目标,但是它们实现的方法却是迥然不同\n现在我想站在用户的角度来比较一下这两个工具。我这里指的用户观点是我在不需要了解Helm 和Ivy 的内部工作原理的前提下对这两个工具进行比较。\n其实,因为我对 elisp还谈不上精通,所以也没办法就两者实现细节来进行比较。但是这两个工具我都使用过,所以我可以从用户的角度,跟你分享我使用它们的不同感受。最后,我从Helm 切换到了Ivy\n我想先谈Helm.当我使用Spacemacs 的时候,我学会了怎么使用Helm,以Helm 的方式思考, 如何自定义Helm,怎么把Helm 配置得称心如意。\n我想我应该算得上是一个中级的Helm 用户吧。我有读过这篇文章 还有这篇文章 以及Wiki 此外,在长达一年的时间里,我每天都是使用Helm的\nHelm 是一个非常成熟的工具.根据git 的提交历史,Helm 的开发工作是在2009年左右开始的。 在写这篇文章的时候,Helm 官方的git 仓库有超过26000行elisp 代码\ngit clone https://github.com/emacs-helm/helm.git cd helm cat *.el | wc -l # =\u0026gt; 26431 这还是没有把在MELPA 上查询到跟Helm 有关的包有142个的情况考虑在内的呢。\n你可以用Helm来完成任何事情它主要的强大之处在于你可以把Helm 和很多Emacs 的行为整合在一起。你可以以Helm 为中心构造接口,就像Spacemacs 做的那样。Helm 支持非常一致的接口,你可以通过Helm 来做任何事\n你可以搜索文件,搜索缓冲区,搜索颜色,搜索项目,搜索你最近编辑过的文件,搜索系统进程, 搜索音乐,搜索网络资源,搜索补全,搜索代码片段,搜索正则表达式,搜索命令,文档 相关描述,手册\u0026hellip;.\n你可以用Helm-projectile(一个Helm 对projectile 非常好的包装)来管理你的项目。你可以用gitignore.io来生成gitignore文件,你可以用Helm-bibtex来管理你的参考书目,你可以浏览你的火狐书签\n你可以用Helm 来完成任何事。\n基于 tuhdo 对我在Reddit 上面问题的回复,我想指出的一个特性就是Helm 是不使用 minibuffer,但是Ivy 是使用的。\n所以它可以被配置成总是在当前打开的窗口展示。对于那些大屏幕显示器的用户而言,这个特性真的非常有用,因为你的目光不用在 minibuffer 来回切换:\nFigure 1: 补全结果总是显示在同一个窗口\n最终的比较结果是Helm 是非常便利的工具,相信会有数量非常多的Spacemacs 用户告诉你同样的看法。\n而Helm 主要的缺点就是它的代码量太大了。我想虽然Helm 的代码量很大,但是它的开发者利用 elisp 成功把它打造成了一个相当快的工具了\n而且有些时候,Helm 似乎把简单的问题复杂化了;它配置起来也感觉相当臃肿;有时它也会有一些很奇怪的表现,然后导致卡顿,或者让Emacs 过载,即使你做的只是很简单的查询。\n或许那些Helm 的高手用户看到这里,会觉得如果我也是个 elisp 高手,就不会出现上述问题了。虽然我已经使用Helm 超过一年了,我还是没有找到方法让可以Helm更加稳定。我觉得Helm 在用自己做例子来讲述了什么是化简为繁吧\n你可以用Helm 来做任何事;但事实上你并不需要。你可以这样做并不意味着你应该这样做。\n在使用Helm 一年以后,我可以告诉你我只是使用了Helm 三分之一或者更小的功能。有些功能我觉得真的很棒,昨天在读了这篇文章 之后,我又发现了一些新的东西。大部分时间,我都是使用简单的命令来切换缓冲区,或者列举文件\nHelm 只是一个用来补全的包,就好像Ido或者Ivy.它可能很容易使用,一旦有人经历过配置它的困难,就会发现它很难做到让你随心所欲。\n有些人觉得只要可以让他们使用好的工具,即使他们完全不了解这些工具也无所谓。\n但是我就做不到\n\u0026ndash;abo-abo,Ivy 的开发者,回答\u0026ldquo;为什么不选择Helm\u0026rdquo; 这个问题\nIvy 为实现最小化,简单化,可定制化,可发现化而努力.这四个形容词告诉我们很多Helm 和Ivy 这两个工具间不同的设计理念。阅读Ivy介绍 以便更好了解Ivy的理念。\n在写这篇文章的时候,Ivy 只有大概3400行代码,为Ivy 所打造的生态系统:即Swipter 和 Counsel 也只有7500 行代码\ngit clone https://github.com/abo-abo/swiper.git cd swiper ## Only ivy ? cat ivy.el | wc -l # =\u0026gt; 3442 ## count lines of code into the whole swiper ecosystem cat *.el | wc -l # =\u0026gt; 7526 Ivy 真的是很容易上手,下面就是我的全部配置:\n(use-package ivy :ensure t :diminish (ivy-mode . \u0026#34;\u0026#34;) :bind (:map ivy-mode-map (\u0026#34;C-\u0026#39;\u0026#34; . ivy-avy)) :config (ivy-mode 1) ;; add ‘recentf-mode’ and bookmarks to ‘ivy-switch-buffer’. (setq ivy-use-virtual-buffers t) ;; number of result lines to display (setq ivy-height 10) ;; does not count candidates (setq ivy-count-format \u0026#34;\u0026#34;) ;; no regexp by default (setq ivy-initial-inputs-alist nil) ;; configure regexp engine. (setq ivy-re-builders-alist ;; allow input not in order \u0026#39;((t . ivy--regex-ignore-order)))) Ivy 是很低调的;它不想让你把一切都整合到Ivy去。它仅仅是提供你必需的补全。你不能像Helm 那样用Ivy 来做任何事;那为什么我还要切换到Ivy 去呢?\n虽然Ivy 已经最小化,但是我依然可以用Ivy 来代替我绝大部分日常使用的Helm命令。\n因为Ivy是如此简洁, abo-abo 在它上开发了一个叫 Counsel 的包; Counsel 可以为你提供非常非常多像你在Helm使用的命令\n你可以切换缓冲区,搜索文件,在项目级别进行搜索和替换,与Projectile 整合,搜索你最近 编辑过的文件,搜索Emacs 命令,搜索文档,搜索按键绑定,浏览 kill-ring\n让我向你介绍我是怎样用Ivy 代替Helm 的。下面是我对那些我需要使用Ivy 来代替Helm的最常用命令的总结。\n这些基本是我一直以来最常用的方法。我每分钟会使用三次的 ivy-switch-buffer ,我一天会使用五次的 helm-swoop, swiper 跟 helm-swoop 不分伯仲;\n对于那些大文件, Counsel 有 counsel-grep-or-swiper.\n我已经用一些非常非常大的标记语言的文件(一百万行左右)来测试过了,一点问题也没有。\nHelm Ivy What ? helm-mini ivy-switch-buffer search for currently opened buffers helm-recentf counsel-recentf search for recently edited files helm-find-files counsel-find-files search files starting from ./ helm-ag counsel-ag search regexp occurence in current project helm-grep-do-git-grep counsel-git-grep search regexp in current project helm-swoop swiper search string interactively in current buffer helm-show-kill-ring counsel-yank-pop search copy-paste history helm-projectile counsel-projectile search project and file in it helm-ls-git-ls counsel-git search file in current git project helm-themes counsel-load-theme switch themes helm-descbinds counsel-descbinds describe keybindings and associated functions helm-M-x counsel-M-x enhanced M-x command 我觉得你可以看到Ivy 基本的命令对比Helm 的命令也是毫不逊色的。它们可以代替你日常使用的每一条Helm命令。我不是说你可以像Helm 那样用Ivy 来做任何事,但是它已经足够好用了,正如我说的那样,你也不需要任何事都使用Helm 来完成。\n说到补全理念这个话题上,Helm 和Ivy 之间的差异并没有那么大。作为一个用户,我可以告诉你的是:Ivy 会让你感觉到更少的臃肿,更加的直观,更加地容易理解。每一次的补全都是可以预见的。\n最后,这真的跟个人的品味有关。对于我自己来说,\u0026ldquo;Ivy 还是Helm\u0026rdquo; 这样的争论跟\u0026quot;Emacs 还是Spacemacs\u0026quot; \u0026ldquo;Emacs 还是Ide\u0026rdquo; \u0026ldquo;C 还是Java\u0026rdquo; \u0026ldquo;简洁还是全能\u0026rdquo; \u0026ldquo;Thelonious 还是 Duke\u0026rdquo;(译者注,两者都是爵士乐作曲家),\u0026ldquo;Van Der Rohe 还是 Gaudi.\u0026quot;(译者注:前者是德国美国 的建筑风格,后者是西班牙加泰罗尼亚的建筑风格)这样的争论是非常相似的。\n你选择Helm呢,你会得到一个巨型的包,一系列你不会用到的特性,一堆你可能只是偶尔用一下的功能,一些你会一个小时使用50次的特性。如果你选择Ivy,你会得到一个只拥有那些让你顺心的必要特性的精简的包,你可以很容易地通过 Counsel 或者简单的函数对它进行扩展\n(ivy-read \u0026#34;Pick:\u0026#34; (mapcar #\u0026#39;number-to-string (number-sequence 1 10))) 如果你想要通过Helm 来扩展:\n(helm :sources (helm-build-sync-source \u0026#34;one-to-ten\u0026#34; :candidates (mapcar #\u0026#39;number-to-string (number-sequence 1 10)) :fuzzy-match t) :buffer \u0026#34;*helm one-to-ten*\u0026#34;) 或者简单的列表:\n(helm-comp-read \u0026#34;Pick:\u0026#34; (mapcar #\u0026#39;number-to-string (number-sequence 1 10))) Helm 为用户作了非常多的决定,Ivy 让用户按需求进行定制;Helm 通过耗费非常多的内存来变得快速,Ivy 通过保持简洁来实现快速;Helm 很成熟,Ivy 很青涩;Helm 为Emacs 提供一致性,Ivy 为Emacs 提供简洁性和可预见性;Helm 需要你进行一定的配置,Ivy 开箱即用\n我自己是稍偏向Ivy 的,因为我正在使用它; 它更符合我的口味。但是作为一个用户,Helm和Ivy并没有那么大的差别。它们都是非常优秀的包,只是以不用的方式去实现相同的目标\n原文地址 https://sam217pa.github.io/2016/09/13/from-helm-to-ivy/\n在下翻译水平有限,如有错误,还请指出\n","permalink":"https://ramsayleung.github.io/zh/post/2017/from-helm-to-ivy/","summary":"最近,我发现很多Emacs 用户对Ivy 很感兴趣;而且大部份用户都是已经了解过Helm 或者Ido的 当有人在Reddit 上面问 选择Helm 还是I","title":"(翻译)从Helm到Ivy"},{"content":"我平时也有浏览各类博客的习惯,毕竟三人行则必有我师嘛。今天在浏览关于Java的一个博客的时候,对博主的观点有一些不同的开发,但是困于没法在博客下评论,内容如下: 所以打算聊聊Java 中Collection 这个话题。(BTW,窃以为博主对Java8 新引进的Lambda, 应该了解不足)\n1 Java函数式编程 Java8 引进了函数式编程的新特性,让Java的开发人员也可以享受函数式编程的美妙,已经有很多的文章介绍函数式了,珠玉在前,我就不赘言了。\n来说说Java 的Lambda吧:Java8 对核心类库进行了改进,只要包括集合类的API和新引入的流(Stream), 流可以让开发者站在更高的 抽象层次对集合进行操作\n2 流的常用操作 2.1 collect(toList()) collect(toList()) 方法可以由Stream 值生成一个List,而Stream 的of方法可以使用初始值生成新的Stream.\nList\u0026lt;String\u0026gt; collected= Stream.of(\u0026#34;this\u0026#34;,\u0026#34;is\u0026#34;,\u0026#34;a\u0026#34;,\u0026#34;list\u0026#34;).collect(Collectors.toList()); 2.2 map 如果有一个函数可以将一种类型的值转换成另外一种类型,map 操作就可以使用该函数,将一个流中的值转换成一个新的流 2.2.1 例子 将字符变成大写格式如果用没有Lambda 时的模式编程\nList\u0026lt;String\u0026gt; oldStyle=new ArrayList\u0026lt;\u0026gt;(); for(String string : Arrays.asList(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;)){ String uppercaseString=string.toUpperCase(); oldStyle.add(uppercaseString); } 但是如果你有了Lambda\nList\u0026lt;String\u0026gt; lambdaStyle=Stream.of(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;).map(string -\u0026gt; string.toUpperCase()) .collect(Collectors.toList()); 真的有种说不出的优雅\n2.3 filter 遍历数据并检查其中的元素\n2.3.1 例子 你有一个User类,然后你想找出年龄大于30岁的用户\nprivate static List\u0026lt;User\u0026gt; users = Arrays.asList( new User(1, \u0026#34;Steve\u0026#34;, \u0026#34;Vai\u0026#34;, 40), new User(4, \u0026#34;Joe\u0026#34;, \u0026#34;Smith\u0026#34;, 32), new User(3, \u0026#34;Steve\u0026#34;, \u0026#34;Johnson\u0026#34;, 57), new User(9, \u0026#34;Mike\u0026#34;, \u0026#34;Stevens\u0026#34;, 18), new User(10, \u0026#34;George\u0026#34;, \u0026#34;Armstrong\u0026#34;, 24), new User(2, \u0026#34;Jim\u0026#34;, \u0026#34;Smith\u0026#34;, 40), new User(8, \u0026#34;Chuck\u0026#34;, \u0026#34;Schneider\u0026#34;, 34), new User(5, \u0026#34;Jorje\u0026#34;, \u0026#34;Gonzales\u0026#34;, 22), new User(6, \u0026#34;Jane\u0026#34;, \u0026#34;Michaels\u0026#34;, 47), new User(7, \u0026#34;Kim\u0026#34;, \u0026#34;Berlie\u0026#34;, 60) ); 非函数式编程(旧式):\nList\u0026lt;User\u0026gt; olderUsers = new ArrayList\u0026lt;User\u0026gt;(); for (User u : users) { if (u.age \u0026gt; 30) { olderUsers.add(u); } } 函数式编程:\nList\u0026lt;User\u0026gt; olderUsers = users.stream().filter(u -\u0026gt; u.age \u0026gt; 30).collect(Collectors.toList()); 2.4 flatMap flatMap 方法可用Stream 替换值,然后将多个Stream 连接成一个Stream 2.4.1 例子 假设有一个包含多个列表的流,希望得到所有数字的序列\nList\u0026lt;Integer\u0026gt; together=Stream.of(Arrays.asList(1,2),Arrays.asList(3,4)) .flatMap(numbers-\u0026gt;numbers.stream()).collect(Collectors.toList()); 还有其他常用的操作,我就不一一列举了,官方Quick Start有更详细的介绍。\n但是就我谈到的几种操作,应该可以对那位博主朋友的博文做出回应了,最有效优雅过滤一个Collection 的方法,我觉得是Stream 的filter\nList\u0026lt;String\u0026gt; filterExample=Stream.of(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;).filter(string-\u0026gt;string.equals(\u0026#34;a\u0026#34;)).collect(Collectors.toList()); 2.5 小结 Java Lambda的特性如果不经常使用,很容易又忘了,本文就当是对Java Lambda 的一次review吧\n不过,函数式的引用的确让Java 焕发出新的活力,记得之前一位前辈吐嘈Java语法太啰嗦,现在前辈应该会用得舒心一点吧\n备注:上面的图都是来自《Java8 函数式编程》 一书\n参考:Java8 函数式编程\n","permalink":"https://ramsayleung.github.io/zh/post/2017/java_collection_lambda/","summary":"我平时也有浏览各类博客的习惯,毕竟三人行则必有我师嘛。今天在浏览关于Java的一个博客的时候,对博主的观点有一些不同的开发,但是困于没法在博","title":"Lambda与Java Collection有感"},{"content":"如果你足够幸运(或者不幸运,取决于你怎么看待了)可以使用 git 作为你工作流的一部分。\n你可能已经 邂逅 过 magit 这个Emacs 的git接口了。 magit 是Emacs 上的非常优秀的git 接口,它假定你是了解你正在对 magit 或者 git 做的种种操作的 注意 :该文章是针对 magit 1.x 的,对 magit 2.x 并不适用 Magit 有非常完整的文档,包含了Magit 的各种操作,但是和大多数的文档一样,Magit 的文档并没有介绍如何将Magit 和你的工作流结合;\nMagit 假定你是熟悉Magit 并且了解如何合理地使用Magit(但大多数情况并不是这样).\nMagit 现在还是处于活跃的开发中。在2013年12月,增加了很多很多新的很有用特性的一次 release, 让Magit 变得比以前更强大了,所以本教程是基于比较新的Magit 版本的,并且 假定你也已经安装了新的版本\n如果想安装处于 master 分支的Magit,我建议你使用 Melpa 来安装;此外你也可以选择直接拉取magit github 仓库的最新版本,然后按照README 上面的指导来构建 magit\n这是我Magit 教程的第一部分。这部分会介绍状态窗口(status window); 已暂存和未暂存的项目 (staging and unstagin item);已经提交的更改 (committing changes) 和查看历史提交 (history view)\n1 Getting Started 首先:Magit 并没有隐藏git 的复杂性,所以,如果你想高效地使用Magit ,你最好清楚了解git 究竟做了哪些工作。\n事实上,我更原意把Magit 当作一个取代了git 枯燥的纯命令行操作的工具,它也表现得出乎意料地好。\n你可以通过 M-x magit-status 来使用Magit,该命令会打开一个窗口(如果你的缓冲区 所在的目录不是一个git 项目,Magit 会提示你进入一个git项目的目录),然后展示Magit 当前的状态。\n你是通过 magit-status 这个接口来使用Magit 的。此外,如果你是使用 Emacs VC的,你需要知道的是,Magit是没有集成到Emacs VC(Version Control)的 抽象层。\n虽然你没法在VC 使用Magit,不过你还是可以在大多数版本控制工具使用Magit 的,Magit 为这些版本控制工具都提供了统一的接口;你如果想调用Magit,你只需要 M-x magit-status\n2 The Magit Status 你首先会注意到关于Magit的事情应该就是当你打开Magit 的状态窗口时,Magit 的状态窗口是可以与你的Emacs 窗口配置配合工作的,你也可以像在其他Emacs 窗口那样,通过按下 q 来关闭窗口。\n几乎你在Magit 执行的所有操作都是通过在底部打开一个 command console 窗口,然后按下对应的单字符指令执行;你也可以重新定义你自己的指令。\n这种交互的方式真的非常好用,可能正是这么强大的特性让Magit变得如此优秀吧。\n我个人真的非常喜欢这种交互的方式,我甚至把这部分特性的代码复制到了我自己的Emacs项目上,因为这真的真的非常好用。\nMagit 之前的稳定版本在帮助用户更好使用Magit这方面做得略有不足,所以在最新的版本有了相应的改进,你可以通过按下 ~?\u0026lt;/kbd\u0026gt; 来显示一系列带注解说明的操作。我觉得在最开始的时候,Magit 真的很难用,因为我总是在「茫茫」的菜单选项中迷失,好不容易才能找到我想要的操作。\n即使是现在,也并不是所有的操作都有注解说明了;有一些命令 (对于我的工作流来说很重要的命令)依然是没有说明的,特别是用来重新定位的 E.\n3 Staging and Unstaging Items 把你的文件放到git下面是你经常需要完成工作之一,Magit 有一系列的按键绑定和工具可以帮助你更好地完成工作。\nMagit 操作不仅可以暂存文件,还可以暂存在 diff 中选定 的代码块。\nMagit\u0026quot;杀手级\u0026quot;特性之一就是它使用不同的等级来显示相关的信息。Magit 可以让你通过 tab 展开或者折叠已经暂存或者未暂存文件。\n如果你想更加细颗粒度地暂存或者未暂存文件,你可以使用 M-1 到 M-4 来操作所有的文件;此外,也可以使用 1 到 4 操作选定的文件\n等级1会把所有的东西隐藏到一个分类里面(即已暂存的文件);\n等级 2在一个分类里面只是显示文件名 (这是默认的等级);\n等级3会显示git代码块的头部;等级4 会显示所有做出了修改的代码块。\n我使用最多的是等级2和等级4, 如果你使用按键 TAB,Magit会完成你想要的等级操作的。你拥有一系列可以让你的\n生活变得更加美好的按键绑定,例如: n 和 p 可以在你前一个单元和后一个单元(通常以代码块为单元) 之间移动;M-n 和 M-p 可以在相邻单元之间移动,例如在等级4中的每个文件间移动。\n你也可以使用 + 或者 - 放大或者缩小每段代码,使用 0 可以恢复默认设置。此外你也可以按下 H 给代码块 添加额外的代码高亮。\n最后,你按下回车 RET 可以打开你修改的文件,代码块或者文件都适用该操作。\n你可以通过按下 s 或者 u 来暂存或者撤销暂存文件(或者代码块),此外,奉送一个很有用的小提示:如果你选定某部分的代码,然后按下暂存(撤销暂存)按键,Magit 会自动暂存(撤销)你选定的那部分代码。\n当你发现 diff 选定的代码块不符合你的要求的时候,你会发现这种细颗粒度的操作真的非常有用\n有时候你对某些修改并不在意,你也不关心这部分修改是否已经提交;你可以像上面的暂存(撤销暂存)操作一样,通过按下按键 K 来忽略选定的代码块和文件,并且从你的电脑删除未提交到暂存区(untracked)的文件;\n这个命令可以比暂存(撤销暂存)命令完成更多的操作,例如,删除已保存的文件或目录(stash)\n4 Committing Changes 如果你想打开提交菜单,只需按下 c,然后你就会看到琳罗满目的选项,不过大部份选项 你都是用不上的了。你真正有用的操作,不仅可以让你提交已暂存的修改,还可以完成更多的任务:\n你可以扩展(extend e) HEAD 所指向的提交 你可以修改(amend a) 有关的提交信息 如果你不喜欢现在的提交信息,你可以重写(reword r)提交信息 你同时也可以修整(fixup f)和压缩(squash s)当前这次的提交。如果你之前用 . 标记了一次提交,那么今次使用的就是被标记的那次提交。 扩展一次提交其实就是在当前提交上附加修改,所以,如果你忘记了提交本属于此次提交的东西你可以使用 扩展 选项。\n如果你想修改当前的提交信息,那就使用 修改 选项吧\n重写可以重写你的提交信息但是无需提交你已暂存的修改;如果你不小心按错了选项,想重写你的提交信息,重写 选项就是你最好的选择\n如果你想在最新一次提交下创建一个 fixup 或者 squash 提交的话,使用修整或压缩命令 可以重整或者 --(自动压缩)autosquash 最新一次提交。\n如果你不会去重写你的git历史或者你未使用过重整,你可能觉得这两个命令不是很有用\n5 Logging 我觉得Magit非常强大的特性之一就是它有不计其数的选项可以用来对你的git 历史进行 过滤,排序,查找。\nMagit 不仅可以展示你的git信息,还可以让你执行交互操作。如果你想打开日志的菜单,你只需按下 l.\n你应该知道的第一个有用的按键就是 l l, 这个 按键会为你展示缩略的日志信息:你会看到单行的提交信息, 作者的名字, 修改提交距今的时间, 树状结构的git 日志, 各种的标签信息,例如 HEAD 指针的位置或者分支标记的位置\n如果你不小心玩坏了git 的提交信息,命令 git reflog 会是你的救星;此外,对于magit 的引用日志(reflog)机制(l h),它也有很友好稳定的UI界面支持。\n引用日志和普通的日志都有非常丰富的按键绑定。在日志里,你对单个的提交可以进行非常多的操作:\n.: 为此次提交作标记以便进行后续的操作例如提交修整 (c f)或者提交压缩 (c s) x: 重置你的 HEAD 指针到选定的提交 v: 撤销提交 d: 将你的工作区与选定的提交进行比较 a: 将选定的提交作用在你的工作区 A: 选择位于你工作区顶部的提交 E: 以交互的方式重置你的 HEAD 指针到选定的工作区。如果你想重写历史,该命令会非常有用 C-w: 复制你此次提交的hash值 SPC: 展示完整的提交历史 需要注意的是:即使你关闭了日志的窗口,标记的命令还是会继续作用的;标记是非常有用的工具,但是你很容易忘记你是否曾经作过标记。如果你在magit 使用 M-n 或者 N-p 向上或者向下浏览日志, maigt 会自动为你在另外一个窗口显示提交信息\n6 Conclusion 对于有经验的Git 用户来说,Magit 是一个非常好的工具;此外,如果你是Git 的新手, Magit可以帮助你了解Git 是怎么工作的,但是它永远不会教你使用Git.\n在我看来,阻碍 你使用Magit 最大的障碍就是Magit缺乏对选项的描述说明;即使Magit 包含了成千上万 Git的选项,参数和操作,但是它并没有教你如何找到并使用这些命令。\n我发现Git 的命令行真的无可替代(不是因为我喜欢git 的命令行我才这么说,事实是它真的很棒)因为我 想要完成的操作真的隐藏得很深,没有那么容易在Magit找到。\n不过最新版本的改进真的很好,你可以通过按下 ? 查看一系列带有注解说明的命令(但不是全部命令,不过这也已经是一个很大的改进了).\n如果你曾被Magit 的学习曲线所吓倒,抑或者你已经尝试Magit, 却无奈放弃;我建议你再试一次。我打算写更多关于Magit 的博文\n原文地址 https://www.masteringemacs.org/article/introduction-magit-emacs-mode-git 在下翻译水平有限,如有错误,还请指出\n","permalink":"https://ramsayleung.github.io/zh/post/2017/magit/","summary":"如果你足够幸运(或者不幸运,取决于你怎么看待了)可以使用 git 作为你工作流的一部分。 你可能已经 邂逅 过 magit 这个Emacs 的git接口了。 magit 是Emac","title":"(翻译)An Introduction to Magit"},{"content":" fasd - quick access to files and directory\n之前一位 Windows 用户看到我在 Shell 下面的操作,他很奇怪,觉得明明已经有图形化界面,为什么还要用这种命令行呢,直接用鼠标点击不就很好了么。\n我觉得很难直接跟他解释,因为他没有用过Linux/Unix,完全不熟悉命令行,不知道其强大之处,其高效率是图形化界面完全无法比拟的(当然,命令行的学习成本和学习曲线肯定比图型化界面高), So I live in terminal.\n而今天我要介绍的神器 fasd 就是可以让命令行操作变得更加高效\n1 Fasd 在 Shell 下面有非常多的命令操作是与文件和目录相关的,如果你要进入到另外一个目 录你可以使用相对或者绝对路径来访问该目录,但是如果这是一个与当前目录不相关的目 录你就只能通过绝对路径来访问。\n以我自己的目录为例,当前目录是 home/samray.emacs.d/elisp/ ,我希望访问 Document 目录下一个的子目录 Python, 我可以通过下面的命令来访问:\ncd ~/Document/Programming/Python cd /home/samray/Document/Programming/Python 这就是我需要的命令,虽然可以通过 tab 进行目录名的补全,但是我还是觉得要输入的东西太多了(正如 Larry Wall 所说,懒惰是程序员的美德). 然后,我发现了 Fasd 这个神器。它可以让我只输入 Python 就进入到我想访问的 Python 目录,\n神奇吧!:)\nFasd以访问的频繁程度和最近是否有访问对文件和目录分配优先级,然后通过判断已访问的文件以及其优先级来切换目录或者打开文件,所以如果你之前已经访问过某个目录.\n那么 你很容易就可以切换到那个目录\n1.1 常用选项 -a(any): 匹配文件和目录\n-i(interactive): 以交互的方式选择文件或者目录\n-s(show/search): 按照优先级展示文件或者目录\n-e \u0026lt;cmd\u0026gt;:对匹配的文件调用命令\u0026lt;cmd\u0026gt;\n-d:只匹配目录\n-f:只匹配文件 Fasd 文档还建议你为 fasd的命令选项设置别名\nalias a=\u0026#39;fasd -a\u0026#39; # any alias s=\u0026#39;fasd -si\u0026#39; # show / search / select alias d=\u0026#39;fasd -d\u0026#39; # directory alias f=\u0026#39;fasd -f\u0026#39; # file alias sd=\u0026#39;fasd -sid\u0026#39; # interactive directory selection alias sf=\u0026#39;fasd -sif\u0026#39; # interactive file selection alias z=\u0026#39;fasd_cd -d\u0026#39; # cd, same functionality as j in autojump alias zz=\u0026#39;fasd_cd -d -i\u0026#39; # cd with interactive selection 这样你就可以通过 z some-dir 直接进入到某个目录或者 zz some-dir 选择进入有多个匹配的特定目录。\nFasd 还会判断应该显示所有的匹配选项或者是直接选择最佳匹配. 例如你也可以将fasd配合 subshell 使用,例如打开 foo\nvim `f foo` 又或者打开 /etc/rc.conf\nvim `f rc conf` 1.2 例子 你可以将fasd 配合正则表达式使用,例如列举以 py 结尾的最近访问的文件:\nf py$ 又或者使用Emacs 打开最近频繁访问的文件 bar\nf -e emacs bar 2 Fasd +Eshell fasd 真的可以大幅度提高效率,但是我有点不太满意的是,我是个 Emacser, 我的操作基本是在 Emacs 里完成的,而我在 Emacs里面使用的 shell 是 Eshell,Eshell 似乎不能与 fasd 无缝结合,似乎可以折腾一下。\nz 和 zz 命令是无法在Eshell 里面运行,因为 z 是 fasd_cd 的别名,而fasd_cd 是一个shell script 函数,Eshell无法运行该函数,代码如下:\nfasd_cd () { if [ $# -le 1 ] then fasd \u0026#34;$@\u0026#34; else local _fasd_ret=\u0026#34;$(fasd -e \u0026#39;printf %s\u0026#39; \u0026#34;$@\u0026#34;)\u0026#34; [ -z \u0026#34;$_fasd_ret\u0026#34; ] \u0026amp;\u0026amp; return [ -d \u0026#34;$_fasd_ret\u0026#34; ] \u0026amp;\u0026amp; cd \u0026#34;$_fasd_ret\u0026#34; || printf %s\\n \u0026#34;$_fasd_ret\u0026#34; fi } Eshell无法运行该函数,因为Eshell文档的匮乏,我也不知道如何编写跟上面代码等价的 \u0026ldquo;Eshell script\u0026rdquo;,所以就用 elisp 写一段同样功能的函数好了。\n(defun samray/eshell-fasd-z (\u0026amp;rest args) \u0026#34;Use fasd to change directory more effectively by passing ARGS.\u0026#34; (setq args (eshell-flatten-list args)) (let* ((fasd (concat \u0026#34;fasd \u0026#34; (car args))) (fasd-result (shell-command-to-string fasd)) (path (replace-regexp-in-string \u0026#34;\\n$\u0026#34; \u0026#34;\u0026#34; fasd-result)) ) (eshell/cd path) (eshell/echo path) )) 函数功能很快就写好了,实现了 z 的功能,但是原来的代码一直不能正常运行,折腾了一个多小时都没解决,输出什么都正常,最后 debug 发现是因为显示的路径后面多了一个换行符即 /home/samray 变成了 /home/samray\\n,而输出换行符又不会显示,真 的坑。\n最后为命令赋予别名就可以像在 zsh 下那样工作了:\nalias z \u0026#39;samray/shell-fasd-z $1\u0026#39; 更多的用法就要查阅官方文档了\nman fasd Enjoy Emacs and Shell :)\n参考: https://github.com/clvv/fasd\n","permalink":"https://ramsayleung.github.io/zh/post/2017/fasd-meet-eshell/","summary":"fasd - quick access to files and directory 之前一位 Windows 用户看到我在 Shell 下面的操作,他很奇怪,觉得明明已经有图形化界面,为什么还要用这种命令行呢,直接用鼠标点击不就很好了么","title":"Shell神器fasd与Eshell的不期而遇"},{"content":"最近笔者在阅读《算法》,重温经典数据结构和算法,毕竟一直以来的说法是程序就是数据结构+算法归并算法所需的时间和N*logN成正比,所以可以用归并算法处理数百万甚至更大规模的数据。\n但是归并算法也是存在不足之处的,需要额外的空间来完成排序,而且空间和N的 大小也是成正比的\n1 优化 《算法》中有提到可以通过一些细致的修改实现大幅度缩短归并排序的运行时间\n1.1 对小规模子数组使用插入排序 因为递归会使小规模问题中的方法被频繁调用,所以改进对它们的处理方法就能改进整个算法。\n对于小数组可以使用插入排序或者选择排序来避免递归调用。完整代码\n1.1.1 未改进归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; } int mid=lo+(hi-lo)/2; sort(a,aux,lo,mid);/*将左半边排序*/ sort(a,aux,mid+1,hi);/*将右半边排序*/ merge(a,aux,lo,mid,hi); } 1.1.2 改进后归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; }else if(hi-lo\u0026lt;15){ insertionSort(a,lo,hi); return; } else { int mid = lo + (hi - lo) / 2; sort(a,aux, lo, mid); sort(a,aux, mid + 1, hi); merge(a,aux, lo, mid, hi); } } Figure 1: 轨迹图如上:(图来源于《算法》)\n《算法》 中提到插入排序处理小规模的子数组(比如长度小于15) 一般可以将归并排序的运行时间缩短10%-15%. 实践出真知,还是要自己来测试一下更佳。\n1.1.3 测试1 对100*1000 个字符进行排序,结果如下:\n1.1.4 测试2 对1000*1000 个字符进行排序,结果如下:\n1.1.5 测试3 对10000*1000 个字符进行排序,结果如下\n1.1.6 测试4 对100000*1000 个字符进行排序,结果如下\n1.1.7 测试5 对500000*1000 个字符进行排序,结果如下\n1.1.8 小结 由于篇幅问题,我无法将所有的测试结果都展示出来,但是从上面的结果,可以看出对于小数组,使用插入排序的确对性能有一定幅度提高(最开始的测试可能因为数据量太小所以结果误差较大,但是这并不妨碍得出一个比较接近的结果).\n但是随着数据量的增大改进归并算法性能似乎开始下降 (未经过精确数据验证)\n1.2 测试数组是否已经有序 可以添加一个判断条件,如果 a[mid]小于等于 a[mid+1],便可认为数组已经有序并跳过 merge 方法。这个改动不影响排序的递归调用,但是任意有序的子数组算法的运行时间都变成线性的了\n1.2.1 未改进归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; } int mid=lo+(hi-lo)/2; sort(a,aux,lo,mid);/*将左半边排序*/ sort(a,aux,mid+1,hi);/*将右半边排序*/ merge(a,aux,lo,mid,hi); } 1.2.2 改进后归并排序 public static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){ if(hi\u0026lt;=lo){ return; }else if(hi-lo\u0026lt;15){ insertionSort(a,lo,hi); return; } else { int mid = lo + (hi - lo) / 2; sort(a,aux, lo, mid); sort(a,aux, mid + 1, hi); if(less(a[mid+1],a[mid])){ merge(a,aux,lo,mid, hi); } } } 1.2.3 测试1 对100*1000 个字符进行排序,结果如下: 1.2.4 测试2 对1000*1000 个字符进行排序,结果如下: 1.2.5 测试3 对10000*1000 个字符进行排序,结果如下: 1.2.6 测试4 对100000*1000 个字符进行排序,结果如下: 1.2.7 测试5 对500000*1000 个字符进行排序,结果如下: 1.2.8 小结 从上面的结果可以看出,只是添加了一个判断数组是否已经有序的条件,算法性能就优化了 大概20%左右(未经过精确数据验证), 不得不说真的令人惊讶。\n注意: 运行结果跟操作系统,电脑配置,以及运行次数都相关,所以我使用的也只是很粗略的数据\n1.3 参考 http://algs4.cs.princeton.edu/home/h\n","permalink":"https://ramsayleung.github.io/zh/post/2017/merge-sort-improvment/","summary":"最近笔者在阅读《算法》,重温经典数据结构和算法,毕竟一直以来的说法是程序就是数据结构+算法归并算法所需的时间和N*logN成正比,所以可以用","title":"归并排序算法改进"},{"content":" diff - compare files line by line\n如果你有使用过git,那么你一定不会对diff 陌生,因为对你源文件和修改后的文件进行比较的就是 diff 这个大名鼎鼎的家伙了。\n多年以来, diff 都一直是非常重要的工具,上古大神 都是使用 diff 和 patch 对程序进行差分和打补丁滴(现在有git了,但是diff同样发挥着重要作用)\n1 语法 diff 的语法如下\ndiff [OPTION].... file1 file2 OPTION 指不同的选项参数,file1,file2 是文本文件的名字,如果比较的两个文件相同 diff 将不输出任何东西。如果两个文件有差异,diff 会显示一系列的指示,让你可以把第一个文件修改为与第二个文件一致\n2 例子 2.1 用法一 现在有两个文件,分别保存着不同的地址。 address1 包含:\nguangdong shanghai beijing chengdu address2 包含:\nguangdong shanghai beijin chengdu 你可以注意到两个文件的区别就是第三行的 beijing.然后运行 diff\ndiff address1 address2 输出结果:\n3c3 \u0026lt; beijing --- \u0026gt; beijin 似乎有点难以理解,输出结果描述了什么呢?其实diff 是在指导如何修改不同的文件使之一致 \u0026lt; 后接的是文件1中与文件2不同的部分, \u0026gt; 后接的是文件2中与文件1不同的部分\ndiff 的输出使用3个不同的单字符指导:a(append,追加),c(change,修改),d(delete,删除). 在上面的例子,只是看到一个 c,意味着,如果想把 address1 修改成 address2 只需将 address1 的第三行修改成 address2 的第三行\n2.2 用法2 现在把 address2 的最后一行删除,看看运行 diff 结果如何: address1 包含:\nguangdong shanghai beijing chengdu address2 包含:\nguangdong shanghai beijing diff address1 address2 输出结果:\n4d3 \u0026lt; chengdu 在该例子中,为了将 address1 变成 address2 只需删除 address1 的第四行\n2.3 用法3 现在把 address1 的最后一行删除,看看运行 diff 结果如何: address1 包含:\nguangdong shanghai beijing address2 包含:\nguangdong shanghai beijing chengdu diff address1 address2 输出结果:\n3a4 \u0026gt; chengdu 想将第一个文件转换成第二个文件,只需在第一个文件追加第二个文件的第四行(即在第一个文件的第 3 行之后追加第二个文件的第 4 行)\n3 diff 选项 因为diff 是一个相当强大也是一个相当复杂的命令,所以我没办法将所有的用法一一道 尽所以笔者将比较常用的选项列举出来\n-b:忽略制表符(不忽略所有的空白符,指忽略空白符数量的差异),例如下面的两行是相同的 a a a a -B(blank lines):忽略所有的空白行 -c(context):以上下文的形式显示差异内容,对比默认输出更加容易理解(但是也更加繁杂) -q(quiet): diff 静默设置,即如果文件file1和file2有差异,diff 也只会显示 File file1 and file2 differ -w(whitespace):忽略所有的空白符 -u(unified output): 上下文形式显示的改进,不会输出重复行 -y:将文件分成两列或多列并排进行输出(非常直观,但是输出很繁杂) 还是老话,更多的用法就需要:\nman diff ","permalink":"https://ramsayleung.github.io/zh/post/2017/diff/","summary":"diff - compare files line by line 如果你有使用过git,那么你一定不会对diff 陌生,因为对你源文件和修改后的文件进行比较的就是 diff 这个大名鼎鼎的家伙了。 多年以来","title":"Linux/Unix Shell 二三事之过滤器diff"},{"content":"1 枯树 周末回了一趟家,没带自己的笔记本,在家闲来无事,无意中看到墙角的电脑,已经尘封已久反正无事,何不玩玩这台老古董呢?于是把电脑拿去修理店把坏了的硬件修好。\n离开店的时候,老板说:“你的系统有问题,我看到你自己也有Ghost,就不帮搞这系统了, 你自己都能解决的,推荐你还是用XP吧,这电脑配置低,还是XP好用”。我忍不住回头对老板一笑 :)\n2 春至 像我这种Linuxer,这么可能再装回XP呢,最初装Win7,也是考虑到老爹的技术 hold 不住 Linux, 现在手机那么发达,他就不需要电脑了,所以,此时不装Linux,更待何时呢?\n3 Arch Linux 我没有选择 Xubuntu 这种适合老机器的 Ubuntu 衍生发行版本,因为我不喜欢Ubuntu, 所以我最后选择的是 Arch linux,官网说最低配置只需500MB内存,800MB的 硬盘存储空间,正适合家里的老家伙\n3.1 安装过程 3.1.1 下载镜像 Download Link ,在网易的镜像下载ISO, 然后用dd刻录到U盘,Windows 可以选择 USBwriter\n3.1.2 分区 使用fdisk, 我的硬盘是/dev/sda,如果还有一块硬盘,那应该就是/dev/sdb\nfdisk /dev/sda n:新建一个分区,p 指主分区,e 是指扩展分区(逻辑分区是建立在扩展分区上的) 一块硬盘主分区加上扩展分区最多只能是4个 d: 删除 m: 查询其他命令,不知道怎么操作就输入m 吧 分区结束以后,输入 w 完成分区 (我分了三个分区 /dev/sda1 -\u0026gt; swap /dev/sda2 -\u0026gt; / /dev/sda3 -\u0026gt; /home)\n3.1.3 格式化分区 格式化 sda2 sda3为ext4格式:\nmkfs.ext4 /dev/sda2 mkfs.ext4 /dev/sda3 格式化sda1 为swap(虚拟内存),一般是内存的两倍,当然如果你的内存很大的话就不用划这个分区了\nmkswap /dev/sda1 激活swap\nswapon /dev/sda1 3.1.4 挂载 将sda2挂载到/mnt,其实就是让sda2分区做系统的根分区,/mnt/home同理\nmount /dev/sda2 /mnt mount /dev/sda3 /mnt/home 3.1.5 更新pacman源 网易的源不错,编辑 /etc/pacman.d/mirrorlist 添加 Server = http://mirrors.163.com/archlinux/$repo/os/$arch\nvim /etc/pacman.d/mirrorlist 然后添加;添加完之后,更新一下\npacman -Syy 3.1.6 安装基本系统 安装基本系统到 /mnt,即sda2分区\npacstrap /mnt base base-devel 需要安装的都安装吧,然后走开煮一杯咖啡,慢慢品尝\n3.1.7 生成fstab fstab 的作用:\nThe fstab(5) file can be used to define how disk partitions, various other block devices, or remote filesystems should be mounted into the filesystem\n生成fstab,并且查看是否正确生成fstab\ngenfstab -U -p /mnt \u0026gt;\u0026gt; /mnt/etc/fstab cat /mnt/etc/fstab 3.1.8 配置系统 切换到新的系统,然后你会发现命令行提示符发生了改变\narch-chroot /mnt 设置地区\nln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 设置语言\n编辑 /etc/locale.gen,因为该文件所有的信息都是被注释滴,所以在最上面添加en_US.UTF-8 UTF-8 即可\nvim /etc/locale.gen 然后添加;添加完成后,执行 locale-gen\nlocale-gen 接着配置 locale.conf\necho LANG=en_US.UTF-8 \u0026gt; /etc/locale.conf export LANG=en_US.UTF-8 设置主机名\necho samray-arch \u0026gt; /etc/hostname 设置密码\npasswd 配置网络\npacman -S net-tools systemctl enable dhcpcd.service 安装GRUB\npacman -S grub-bios 把grub 安装到硬盘sda,如果双系统的话,还要视情况做更改\ngrub-install --recheck /dev/sda grub-mkconfig -o /boot/grub/grub.cfg 3.1.9 收尾工作 exit umount /mnt/home umount /mnt reboot 这样Arch linux 就装好了,不过你重启会发现,你的系统是没有图形化界面的\n3.2 安装桌面环境 3.2.1 安装x服务 pacman -S xorg-server xorg-server-utils xorg-xinit 3.2.2 安装显卡驱动 查找自己的显卡类型\nispci |grep VGA 然后搜索匹配自己显卡的驱动\npacman -Ss xf86-video |less Intel 集成显卡:\npacman -S xf86-video-intel 虚拟机显卡:\npacman -S xf86-video-vesa 笔记本触摸板驱动 (老家伙是台式,不需要了):\npacman -S xf86-input-synaptics 安装输入法\npacman -S scim-pinyin 先安装 slim(图像登录管理器)\npacman -S slim 安装xfce4\npacman -S xfce4 启动xfce4\nstartxfce4 基本就大功告成了,因为我的台式电脑是bios, 所以不用折腾uefi, 还有无线网络。\nAction is louder than words,还是多动手才行,我都装了三次才成功,内核空指针和段错误都遇到了 :)\n3.3 参考 https://wiki.archlinux.org/index.php/installation_guide\n","permalink":"https://ramsayleung.github.io/zh/post/2017/install_archlinux/","summary":"1 枯树 周末回了一趟家,没带自己的笔记本,在家闲来无事,无意中看到墙角的电脑,已经尘封已久反正无事,何不玩玩这台老古董呢?于是把电脑拿去修理店","title":"枯树逢春之ArchLinux领风骚"},{"content":"flask 是一个轻量级的python 框架(官网称为微型框架),很容易上手,之前因为笔者跟朋友开发小程序的时候使用过 flask,过后就遗忘了。\n为了重拾flask, 笔者决定写点小东西,之前开发小程序,不如现在再玩玩公众号开发\n1 验证服务器 开发公众号之前,要先验证服务器的有效性,官网有详细的说明:公众开发平台文档\n参数 描述 signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 timestamp 时间戳 nonce 随机数 echostr 随机字符串 校验流程:加密/校验流程如下:\n将token、timestamp、nonce三个参数进行字典序排序 将三个参数字符串拼接成一个字符串进行sha1加密 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 流程并不复杂,官网给出了代码示例,只不过是PHP的,换成python 也是很容易滴:\n@app.route(\u0026#39;/\u0026#39;,methods=[\u0026#39;GET\u0026#39;,\u0026#39;POST\u0026#39;]) def wechat(): if request.method==\u0026#39;GET\u0026#39;: token=\u0026#39;your token\u0026#39; data=request.args signature=data.get(\u0026#39;signature\u0026#39;,\u0026#39;\u0026#39;) timestamp=data.get(\u0026#39;timestamp\u0026#39;,\u0026#39;\u0026#39;) nonce =data.get(\u0026#39;nonce\u0026#39;,\u0026#39;\u0026#39;) echostr=data.get(\u0026#39;echostr\u0026#39;,\u0026#39;\u0026#39;) s=[timestamp,nonce,token] s.sort() s=\u0026#39;\u0026#39;.join(s) if(hashlib.sha1(s).hexdigest()==signature): return make_response(echostr) else: rec=request.stream.read() xml_rec=ET.fromstring(rec) tou = xml_rec.find(\u0026#39;ToUserName\u0026#39;).text fromu = xml_rec.find(\u0026#39;FromUserName\u0026#39;).text content = xml_rec.find(\u0026#39;Content\u0026#39;).text xml_rep = \u0026#34; \u0026lt;xml\u0026gt; \u0026lt;ToUserName\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/ToUserName\u0026gt; \u0026lt;FromUserName\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/FromUserName\u0026gt; \u0026lt;CreateTime\u0026gt;%s\u0026lt;/CreateTime\u0026gt; \u0026lt;MsgType\u0026gt;\u0026lt;![CDATA[text]]\u0026gt;\u0026lt;/MsgType\u0026gt; \u0026lt;Content\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/Content\u0026gt; \u0026lt;FuncFlag\u0026gt;0\u0026lt;/FuncFlag\u0026gt; \u0026lt;/xml\u0026gt;\u0026#34; 这样服务器就校验成功了,就可以编写相关的业务逻辑了\n2 歌词查询 笔者自己平时是打开音乐播放器,戴上耳机,就开始播放音乐;所以经常出现笔者听到某首歌曲觉得旋律非常熟悉,但是就无法想起歌名的情况,这种感觉实在不好,所以笔者觉得可以编写一个通过歌词查询歌曲,并返回所有歌词的功能。\n思路大概是编写爬虫,通过歌词进行查询,然后对返回的html 页面进行检索和信息提取。剩下的事就是爬虫和解析页面了,笔者是使用虾米 进行歌词查询的,使用 request 发送 http 请求,使用 lxml进行解析,其他就不一一细表了\n3 单词查询 有时候,笔者在微信需要查询单词,但是又不想退出微信,所以就打算用公众号来查单词其实很简单,就是服务获取用户发给微信公众号的数据,再去请求有道之类词典的api,再把结果返回给服务器,服务器转发给用户\n4 电影查询 有时无聊想去看电影,但是不知道看什么电影,因为选择太多,质量参差不齐的片太多了所以笔者会先去豆瓣看一下新上影的电影,看一下评分,然后再决定看什么电影。所以,笔者可以把这个功能搬到公众号来。如何实现呢?还是爬虫\n5 总结 感觉这次开发公众号,笔者就是用 flask 编写 restful api, 然后做的其他事情就是编写爬虫。\n项目github地址\n","permalink":"https://ramsayleung.github.io/zh/post/2017/weixin_flask/","summary":"flask 是一个轻量级的python 框架(官网称为微型框架),很容易上手,之前因为笔者跟朋友开发小程序的时候使用过 flask,过后就遗忘了。 为了重拾","title":"flask牛刀小试之微信公众号开发"},{"content":" cat - concatenate files and print on the standard output\n1 过滤器 何谓过滤器呢,例如cat,grep,wl 之类的命令就是过滤器了。这样的命令 读取数据,对数据执行一些操作,然后写入结果。更准确地说,过滤器就是任何能够从标准 输入读取 文本 数据,并向标准输出写入 文本 数据的命令。又因为Unix 的 KISS 设计理念,所以每个程序都被设计成能够出色完成一项特定任务的工具。又因为重定向和 管道的存在,使得可以将这些工具组合起来,发挥无穷威力\n2 cat 在shell 里面运行cat,你会被要求输入文本数据,当你输入一行数据以后,然后按下回 车你输入的数据就会显示在屏幕,当你按下 ^D(\u0026lt;ctrl\u0026gt;+d),发送eof 信号给shell,退出 cat。cat 做的事就是把你输入的字符,复制到标准输出 (一般情况是指你的屏幕).看到 这里有人或许会质疑,这东西有什么用呢?似乎什么都作不了。不,它的用处很大呢, 且容笔者细细禀来\n2.1 场景1 假如你要新建一个文本文件,里面只是很少的文本,你会怎么做呢?一般情况下,都是用 vim/emacs 新建一个文本文件,然后输入几行文字,然后保存退出。这是一般的做法, 看到这里,很自然有人会发问,难道有更优雅的解决方法?有,不用打开文本编辑器写入文本 的hacking方法:\ncat \u0026gt; data 输入数据,然后 ^D(\u0026lt;ctrl\u0026gt;+d) 保存。你就新建了一个文本了。当然,如果你已经有一个 data 文件 ,就会被代替,当然,你也可以也在原来文本末尾添加的方法:\ncat \u0026gt;\u0026gt; data 2.2 场景2 如果你有一个短文件,你想查看一下,同样,你可以使用cat\ncat \u0026lt; data 当然,你也可以省略 \u0026lt; 这个重定向符号:\ncat data 抑或是,你想显示某个大文件的最后一部分,你也可以如上操作。或许你会觉得,这个功能 很多命令也有,最典型的就是 tail. 但是如果 cat 可以很完美地很其他过滤器结合 充当整套管道线工具流的起始端,这个以后慢慢再阐述\n2.3 场景3 如果你想复制文本文件,你首先会想起什么命令? cp,很自然嘛,我也不例外,但是cat 也可以实现同样的功能,很意外吧:\ncat \u0026lt; file \u0026gt; newfile 即把 file 复制到标准输出,然后再把 file 当作标准输入复制到 newfile.hacking!\n2.4 场景4 如果你想把多个文本文件的组合到一个文件,你会怎么做?用编辑器打开所有的文件 然后 select,cut,paste,save.我也会很自然地想到这个方法,但是是否存在着更 优雅的解决方案呢?当然:\ncat file1 file2 file3 \u0026gt;newfile 3 总结 上面已经介绍了挺多cat 的使用场景了,你觉得cat 表现滴怎么样呢?相信你的感觉是 还行,但是并没有,我吹嘘的那么令人惊艳。因为这只是cat 最基本的功能,它最大的 用法还没有完全展现出来,笔者先举一例,以后再慢慢详叙:\ncat file |grep \u0026#34;something\u0026#34; |sort -n |tee newfile 语法 用法 cat \u0026gt; file 读取输入,创建新的文件或替换 cat \u0026gt;\u0026gt;file 读取输入,追加新的文件 cat file/cat \u0026lt;file 显示一个已有文件 cat \u0026lt;oldfile\u0026gt; newfile 复制一个文件 cat file1 file2 file3\u0026gt;file4 组合多个文件 ","permalink":"https://ramsayleung.github.io/zh/post/2017/cat/","summary":"cat - concatenate files and print on the standard output 1 过滤器 何谓过滤器呢,例如cat,grep,wl 之类的命令就是过滤器了。这样的命令 读取数据,对数据执行一些操作,然后写入","title":"Linux/Unix Shell 二三事之过滤器cat"},{"content":" head - output the first part of files tail - output the last part of files\n当拥有的数据太多的时候,使用cat 来展示数据的话,数据量过大,屏幕就只能显示最后一部分的数据了。\n所以如果你想选取部分的数据的话,cat 就不是一个好选择了。有两个命令可以满足你的要求,分别是 head 和 tail.顾名思义,head 选取数据的开头部分tail 是选取数据的结尾部分\n1 用法 当把 head tail 当作过滤器来使用的时候,用法很简单\n$ head data $ tail data 默认情况下 head 会选取数据开头的10行数据 tail 会选取数据最后的10行数据. 如果你想选取更多的数据的时候,你可以指定行数,例如\n$ head [-n line] data $ tail [-n line] data 其中 line 是希望选取的数据行的数量\n2 惊艳点 你可能觉得 head tail 两个命令很简单,似乎用处不大。\n是的,就笔者一直所介绍的那样,单个unix命令只是完成一个特定的工作,但是当它们组合起来的时候,就很威力无穷了\n2.1 场景1 假如你要生成一串密钥来加密你的某个文件,这是很常见的需求,你会怎么办,用python 或者 java 写一个随机数函数来实现么?无需,你用简单的过滤器加Linux/Unix\n内置的设备(dev):\n$ cat /dev/urandom | tr -cd \u0026#34;[[:alnum:]]\u0026#34; |head -c 32;echo 在Unix/Linux 的机器下,运行上面的命令就可以生成一个包含数字和字母的32个字符长的密钥了。\n/dev/urandom 是一个可以通过收集硬件驱动的环境噪音来产生伪随机数特殊的文件,tr 是转换和删除字符的命令;更多详细的东西,以后笔者会慢慢介绍滴\n2.2 场景2 在日常的开发或者运行环境中,日志是必不可少滴,但是日志是不断产生新的数据的,所以有时候就会出现用编辑器打开日志的时候,就会出现,编辑器不断提醒你文件已经发生了变化,是否重新加载,但是如果只是用cat,tail 来查看日志,日志又是保持在 打开的那个状态,新产生的日志数据是没办法浏览到,果真如此?\n其实不然, tail 可以在查看日志的时候,保持日志一直在更新。关键就在 -f 选项\n$ tail -f [-n line] file -f 选项告诉tail 当到达文件的末尾不要停止。相反,tail 要一直等下去,并且随着文件的增长,显示更多的输出 (-f -\u0026gt; follow)\n你也可以模拟日志不断生成的过程:\n$ tail -f -n 20 something.log 然后打开一个新的Shell, 运行:\ncat \u0026gt;\u0026gt; something.log 使用 \u0026gt;\u0026gt; 追加数据,就可以模拟日志生成的过程了\n3 总结 要掌握更多的用法还是要查看文档滴:\nman head Enjoy Shell :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/head_tail/","summary":"head - output the first part of files tail - output the last part of files 当拥有的数据太多的时候,使用cat 来展示数据的话,数据量过大,屏幕就只能显示最后一部分的数据了。 所以如果你想","title":"Linux/Unix Shell 二三事之过滤器head+tail"},{"content":"Percol 是Emacs 的一个非常优秀package:js2-mode作者mooz 的又一力作得益于Unix Shell的管道和重定向设计理念,percol 所有的输入输出变得可交互 percol 给我一种很熟悉的感觉,就是 Eamcs 中helm 增量补全 (incremental completion)的感觉,真的可以10倍提高工作效率。\n1 例子 假如你要用git 切换分支,但是分支很多,你不能记住你要切换的分支的名字。那么有percol 你可以:\n$ git checkout $(git branch|percol) 那样,你就可以,选择要切换的分支了\n平时在Linux/Unix 下,如果要kill 掉某个进程的话,我一般是用 htop 或者是ps 找出要kill 掉的进程的pid, 然后在 kill pid. 但是现在有了percol, 可以一步搞定所有的步骤。\n官网给出的例子函数:\nfunction ppgrep() { if [[ $1 == \u0026#34;\u0026#34; ]]; then PERCOL=percol else PERCOL=\u0026#34;percol --query $1\u0026#34; fi ps aux | eval $PERCOL | awk \u0026#39;{ print $2 }\u0026#39; } function ppkill() { if [[ $1 =~ \u0026#34;^-\u0026#34; ]]; then QUERY=\u0026#34;\u0026#34; # options only else QUERY=$1 # with a query [[ $# \u0026gt; 0 ]] \u0026amp;\u0026amp; shift fi ppgrep $QUERY | xargs kill $* } 又或者是更好地进行查找历史命令:\nfunction exists { which $1 \u0026amp;\u0026gt; /dev/null } if exists percol; then function percol_select_history() { local tac exists gtac \u0026amp;\u0026amp; tac=\u0026#34;gtac\u0026#34; || { exists tac \u0026amp;\u0026amp; tac=\u0026#34;tac\u0026#34; || { tac=\u0026#34;tail -r\u0026#34; } } BUFFER=$(fc -l -n 1 | eval $tac | percol --query \u0026#34;$LBUFFER\u0026#34;) CURSOR=$#BUFFER # move cursor zle -R -c # refresh } zle -N percol_select_history bindkey \u0026#39;^R\u0026#39; percol_select_history fi 1.1 运行截图 有时候,我需要复制当前目录下,某个文件的路径,但是无论是文件管理器,还是shell都要用鼠标来复制指定文件的路径,效率不高且很不方便。在 陈斌 代码的启发下,我自己写了一个函数来复制当前文件夹某个特定目录的路径,很方便地解决了问题:\nOS_NAME=`uname` function pclip() { if [ $OS_NAME = \u0026#34;CYGWIN\u0026#34; ]; then putclip \u0026#34;$@\u0026#34;; elif [ $OS_NAME = \u0026#34;Darwin\u0026#34; ]; then pbcopy \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xsel ]; then xsel -ib \u0026#34;$@\u0026#34;; else if [ -x /usr/bin/xclip ]; then xclip -selection c \u0026#34;$@\u0026#34;; else echo \u0026#34;Neither xsel or xclip is installed!\u0026#34; fi fi fi } function pwdf() { local current_dir=`pwd` local copied_file=`find $current_dir -type f -print |percol` echo -n $copied_file |pclip; } 更多的用法就要查看官方文档 percol\nEnjoy Shell :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/percol/","summary":"Percol 是Emacs 的一个非常优秀package:js2-mode作者mooz 的又一力作得益于Unix Shell的管道和重定向设计理念,perco","title":"Linux/Unix Shell 二三事之神器percol"},{"content":"之前看到个笑话:\nA: 在等待emacs 加载的时间里,你会干什么? B: 打开Vim,修改代码,保存,退出 有时候,经常看到社区里面有人吐嘈Emacs 什么都好,就是启动时间太长了,其实是存在一些技巧来缩短加载时间的\n1 技巧1 在你的 .emacs 或者相应的初始化文件里面添加如下代码\n# Increase the garbage collection threshold to 128 MB to ease startup (setq gc-cons-threshold (* 128 1024 1024 )) # your configuration code # ...... # Garbage collector-decrease threshold to 5 MB (add-hook \u0026#39;after-init-hook (lambda () (setq gc-cons-threshold (* 5 1024 1024)))) # init.el ends here gc-cons-threshold 指定了emacs 进行垃圾回收的阀值,默认值是 800000byte,实在是太小了,所以Emacs 会在启动期间进行非常多次的垃圾回收,启动时间自然长了。\n在加载完以后,再把 gc-cons-threshold 的值调低,当然,如果你的内存很大,也可以不改回来\n2 技巧2 (let((file-name-hander-alist nil))init.file) 包裹(wrap)你的初始化文件,即:\n(setq gc-cons-threshold (* 500 1024 1024)) (let ((file-name-handler-alist nil)) ... ** your config goes here ** ... ) (add-hook \u0026#39;after-init-hook (lambda () (setq gc-cons-threshold (* 5 1024 1024)))) (provide \u0026#39;init) ;;; init.el ends here 因为 file-name-handler-alist 的默认值是一些正则表达式,也就是说Emacs 在启动过程中加载el和elc 文件都会将文件名和正则表达式进行匹配\n3 技巧3 Emacs lisp 有一项auto-load 的技术,类似延迟加载,合理运用延迟,让笔者的Emacs启动加载时间减少一半,因为笔者用 use-package 这个macro,而 use-package 又集成了延迟加载的功能,所以笔者就直接拿自己的代码举例了\n3.1 :after ;;; Export to twitter bootstrap (use-package ox-twbs :after org :ensure ox-twbs ) :after 关键字的作用基本跟 with-eval-after-load 的作用是相同的,所以笔者所 有类似的org-mode 插件包都会在org-mode 加载以后才会加载\n3.2 :commands (use-package avy :commands (avy-goto-char avy-goto-line) :ensure t) 这里就贴上use-package文档 的说明了\nWhen you use the :commands keyword, it creates autoloads for those commands and defers loading of the module until they are used\n也就是 :commands 关键字就创建了后面所接的命令的 autoloads 机制了\n3.3 :bind :mode (use-package hi-lock :bind ((\u0026#34;M-o l\u0026#34; . highlight-lines-matching-regexp) (\u0026#34;M-o r\u0026#34; . highlight-regexp) (\u0026#34;M-o w\u0026#34; . highlight-phrase))) (use-package vue-mode :ensure t :mode (\u0026#34;\\\\.vue\\\\\u0026#39;\u0026#34; . vue-mode) :config (progn (setq mmm-submode-decoration-level 0) )) 附上文档说明\nIn almost all cases you don\u0026rsquo;t need to manually specify :defer t. This is implied whenever :bind or :mode or :interpreter is used\n也就是说,当你使用了 :bind 或者 :mode 关键字的时候,不用明确指定 :defer 也可以实现延迟加载机制。\n当然你也可以,直接使用 :defer 关键字来指定延迟加载. 不过前提是,你要明确它加载的时机\nTypically, you only need to specify :defer if you know for a fact that some other package will do something to cause your package to load at the appropriate time, and thus you would like to defer loading even though use-package isn\u0026rsquo;t creating any autoloads for you.\n贴上笔者自己的代码,可以更加清晰\n(use-package anaconda-mode :defer t :ensure t :init(progn (add-hook \u0026#39;python-mode-hook \u0026#39;anaconda-mode) (add-hook \u0026#39;python-mode-hook \u0026#39;anaconda-eldoc-mode) )) 这样 anaconda-mode 就会在 python-mode 加载以后被加载\nEnjoy Emacs :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/emacs_startup/","summary":"之前看到个笑话: A: 在等待emacs 加载的时间里,你会干什么? B: 打开Vim,修改代码,保存,退出 有时候,经常看到社区里面有人吐嘈Emacs 什么","title":"提高Emacs启动速度"},{"content":"1 Emacs Ipython 输出错误 在Emacs 运行 run-python 的时候,报错了,如下\n[?12l[?25h2+2 [J[?7h[?12l[?25h[?2004l[?7hOut[1]: 4 因为我的版本时Ipython5,查阅文档http://ipython.readthedocs.io/en/stable/whatsnew/version5.html#id1 之后,发现Ipython5 有了新的terminal 接口,和Emacs 继承的shell 不兼容,所以 会出现上述的错误,只要给Ipython 加上运行参数就能解决了,所以只要在 .emacs 或者对应的初始化文件加上下面语句\n(setq python-shell-interpreter \u0026#34;ipython\u0026#34; python-shell-interpreter-args \u0026#34;--simple-prompt -i\u0026#34;) 1.1 Update 2017-3-15 在添加了 \u0026ndash;simple-promp -i 参数以后,虽说乱码的问题解决了,但是新的问题又出现了 在Ipython 里面是没法无法输入多行内容的,即使是一个简单的循环,详情查看这条issue https://github.com/ipython/ipython/issues/9816. 现在Ipython 开发社区还没有解决这个 问题,所以现在的权宜之计就是使用 Ipython4,等到社区解决了这个问题在升级为 Ipython5\npip install --force-reinstall ipython==4.2.1 2 Emacs Ipython 的使用优化 2.1 python-pop 因为我之前使用Emacs的时候,是使用Spacemacs的配置的,但是后来觉得还是自己的 配置用的更舒服,所以又切换回自己的配置,但是我还是很想念Spacemacs的一些绑定 例如shell在底下弹出,或者是关闭,然后找到了Shell-pop 这package,就可以用回 Spacemacs的shell使用习惯。然后我觉得,Ipython shell也可以这样配置,只不过 我没有发现类似的package,又因为Emacs Lisp的强大,所以我自己写了一段小函数实现 shell-pop 的功能\n(defun samray/python-pop () \u0026#34;Run python and switch to the python buffer. similar to shell-pop\u0026#34; (interactive) (if (get-buffer \u0026#34;*Python*\u0026#34;) (if (string= (buffer-name) \u0026#34;*Python*\u0026#34;) (if (not (one-window-p)) (progn (bury-buffer) (delete-window)) ) (progn (switch-to-buffer-other-window \u0026#34;*Python*\u0026#34;) (end-of-buffer) (evil-insert-state))) (progn (run-python) (switch-to-buffer-other-window \u0026#34;*Python*\u0026#34;) (end-of-buffer) (evil-insert-state)))) 如果没有使用Evil,可以把 **(evil-insert-state)**去掉\n2.2 Ipython History 我在普通的Shell使用Ipython的时候,很自然地使用上下方向键翻到上一条/下一条 执行的命令,因为shell的使用习惯就是这样滴,但是在Emacs里面使用Ipython,上下 方向键是去到上一行/下一行,就好像 vim 的 j k,如果要翻到上一条命令,快捷键 是 M-p,实在很不习惯,所以在查了一下Emacs manual 后,我改了一下按键绑定就实现了 我想要的效果\n(define-key comint-mode-map (kbd \u0026#34;\u0026lt;up\u0026gt;\u0026#34;) \u0026#39;comint-previous-input) (define-key comint-mode-map (kbd \u0026#34;\u0026lt;down\u0026gt;\u0026#34;) \u0026#39;comint-next-input) Enjoy Emacs :)\n","permalink":"https://ramsayleung.github.io/zh/post/2017/emacs_ipython/","summary":"1 Emacs Ipython 输出错误 在Emacs 运行 run-python 的时候,报错了,如下 [?12l[?25h2+2 [J[?7h[?12l[?25h[?2004l[?7hOut[1]: 4 因为我的版本时Ipython5,查阅文档http://ipython.readth","title":"在Emacs中使用Ipython"}]
\ No newline at end of file
diff --git a/zh/index.xml b/zh/index.xml
index 9a649c4f..fcbaa65c 100644
--- a/zh/index.xml
+++ b/zh/index.xml
@@ -19,10 +19,10 @@
Tue, 09 Apr 2024 19:04:00 -0700
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软
+ 1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副
1 缘起
-我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
-先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
+我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
+先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
-并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
-经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
-但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
+在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
+经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
+但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
-Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
-按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
+Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
+按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
-又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
+又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
@@ -64,7 +64,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户, 而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
diff --git a/zh/post/2024/100_days_of_swiftui/index.html b/zh/post/2024/100_days_of_swiftui/index.html
index 414f3d58..49ea1944 100644
--- a/zh/post/2024/100_days_of_swiftui/index.html
+++ b/zh/post/2024/100_days_of_swiftui/index.html
@@ -1,6 +1,6 @@
-100 Days of SwiftUI | 自由庄园
+100 Days of SwiftUI | 自由庄园
-100 Days of SwiftUI
目录
1 缘起
我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
今天是结课之日,我通过了结课的考试,总分100分,考了91分,喜提课程证书一枚.
并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
+100 Days of SwiftUI
目录
1 缘起
我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
今天是结课之日,我通过了结课的考试,总分100分,考了91分,喜提课程证书一枚.
在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
var km: Double { return self * 1_000.0 }
var m: Double { return self }
var cm: Double { return self / 100.0 }
@@ -13,7 +13,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
3 SwiftUI
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
let colors: [Color] =
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
3 SwiftUI
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
let colors: [Color] =
[.red, .orange, .yellow, .green, .blue, .purple]
@@ -76,9 +76,9 @@
}
}
}
-
而Xcode 15新增的预览功能也很好用,可以让开发者不需要启动iPhone模拟器就能预览页面效果,节省了非常多的等待时间。
4 问题
听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户,
-而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
因此SwiftUI就陷入了一个尴尬的境地,东西做得好,但是不会有人用;
没有人自然就不用有人分享,宣传这门技术,自然就导致相关的学习资料非常匮乏, 进一步加深了初学者的学习难度;
开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
+
而Xcode 15新增的预览功能也很好用,可以让开发者不需要启动iPhone模拟器就能预览页面效果,节省了非常多的等待时间。
4 问题
听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
+更离谱的是,SwiftUI的 BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户,
+而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
因此SwiftUI就陷入了一个尴尬的境地,东西做得好,但是不会有人用;
没有人自然就不用有人分享,宣传这门技术,自然就导致相关的学习资料非常匮乏, 进一步加深了初学者的学习难度;
开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
let uiImage = UIImage(cgImage: cgImage)
diff --git a/zh/post/index.html b/zh/post/index.html
index e1b470aa..3a1fa09c 100644
--- a/zh/post/index.html
+++ b/zh/post/index.html
@@ -1,6 +1,6 @@
Posts | 自由庄园
Posts
-
100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
四月 9, 2024 · 5 分钟 · 2289 字 · Ramsay Leung 三月的樱花
2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放...
三月 24, 2024 · 1 分钟 · 145 字 · Ramsay Leung 简明写作指南
1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英...
三月 15, 2024 · 9 分钟 · 4453 字 · Ramsay Leung 金榜题名之后
1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取...
二月 25, 2024 · 7 分钟 · 3470 字 · Ramsay Leung 登陆加拿大一年后的体会
1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾...
二月 25, 2024 · 11 分钟 · 5142 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
+100 Days of SwiftUI
1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副...
四月 9, 2024 · 5 分钟 · 2360 字 · Ramsay Leung 三月的樱花
2024的温哥华比2023来得温暖,年底的大雪没有持续多久就消融,去年三月还寒意深深,现在已春意浓浓. 温哥华的樱花也比去年提早了半个多月盛放...
三月 24, 2024 · 1 分钟 · 145 字 · Ramsay Leung 简明写作指南
1 前言 最近在阅读李笑来的《人人都能用英语》1,想要继续提升自己的英语能力。 李笑来是新东方出来的英语教学名师,此书由浅入深来介绍如何「用」好英...
三月 15, 2024 · 9 分钟 · 4453 字 · Ramsay Leung 金榜题名之后
1 前言 过年期间,趁着各种零碎的闲暇时间,将一本探究学生出身与毕业出路生涯前景的书看完,名为《金榜题名之后:大学生出路分化之迷》。 大约自科举取...
二月 25, 2024 · 7 分钟 · 3470 字 · Ramsay Leung 登陆加拿大一年后的体会
1 前言 不知不觉,落地加拿大已经快一年,套用句老话,真的是光阴似箭。 想来蜜月期已过,可以去掉刚落地时【兴奋】和【新奇】的滤镜,从道听途说,到雾...
二月 25, 2024 · 11 分钟 · 5142 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
Powered by
Hugo &
PaperMod
\ No newline at end of file
diff --git a/zh/post/index.xml b/zh/post/index.xml
index 64824c57..37e928e9 100644
--- a/zh/post/index.xml
+++ b/zh/post/index.xml
@@ -19,10 +19,10 @@
Tue, 09 Apr 2024 19:04:00 -0700
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软
+ 1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副
1 缘起
-我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
-先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
+我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
+先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
-并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
-经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
-但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
+在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
+经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
+但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
-Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
-按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
+Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
+按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
-又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
+又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
@@ -64,7 +64,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
3 SwiftUI
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
@@ -150,9 +150,9 @@
4 问题
-听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
+更离谱的是,SwiftUI的 BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户,
而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@
开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
-但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
diff --git a/zh/sitemap.xml b/zh/sitemap.xml
index b1c4541c..ce600223 100644
--- a/zh/sitemap.xml
+++ b/zh/sitemap.xml
@@ -3,10 +3,10 @@
xmlns:xhtml="http://www.w3.org/1999/xhtml">
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/categories/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/tags/ios/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/categories/ios/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/post/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/tags/swift/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/categories/swift/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/tags/swiftui/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/categories/swiftui/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/tags/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
https://ramsayleung.github.io/zh/
- 2024-04-09T22:16:43-07:00
+ 2024-04-09T22:28:36-07:00
ios | 自由庄园
ios
-
100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
四月 9, 2024 · 5 分钟 · 2289 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
+100 Days of SwiftUI
1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副...
四月 9, 2024 · 5 分钟 · 2360 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
Powered by
Hugo &
PaperMod
\ No newline at end of file
diff --git a/zh/tags/ios/index.xml b/zh/tags/ios/index.xml
index 855a26ae..e0dde2ce 100644
--- a/zh/tags/ios/index.xml
+++ b/zh/tags/ios/index.xml
@@ -19,10 +19,10 @@
Tue, 09 Apr 2024 19:04:00 -0700
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软
+ 1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副
1 缘起
-我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
-先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
+我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
+先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
-并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
-经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
-但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
+在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
+经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
+但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
-Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
-按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
+Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
+按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
-又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
+又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
@@ -64,7 +64,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
3 SwiftUI
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
@@ -150,9 +150,9 @@
4 问题
-听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
+更离谱的是,SwiftUI的 BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户,
而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@
开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
-但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
diff --git a/zh/tags/swift/index.html b/zh/tags/swift/index.html
index c122b104..ac1fad81 100644
--- a/zh/tags/swift/index.html
+++ b/zh/tags/swift/index.html
@@ -1,6 +1,6 @@
swift | 自由庄园
swift
-
100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
四月 9, 2024 · 5 分钟 · 2289 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
+100 Days of SwiftUI
1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副...
四月 9, 2024 · 5 分钟 · 2360 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
Powered by
Hugo &
PaperMod
\ No newline at end of file
diff --git a/zh/tags/swift/index.xml b/zh/tags/swift/index.xml
index a2f07652..027ef866 100644
--- a/zh/tags/swift/index.xml
+++ b/zh/tags/swift/index.xml
@@ -19,10 +19,10 @@
Tue, 09 Apr 2024 19:04:00 -0700
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软
+ 1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副
1 缘起
-我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
-先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
+我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
+先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
-并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
-经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
-但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
+在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
+经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
+但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
-Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
-按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
+Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
+按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
-又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
+又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
@@ -64,7 +64,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
3 SwiftUI
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
@@ -150,9 +150,9 @@
4 问题
-听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
+更离谱的是,SwiftUI的 BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户,
而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@
开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
-但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
diff --git a/zh/tags/swiftui/index.html b/zh/tags/swiftui/index.html
index 038a9030..d0d4489e 100644
--- a/zh/tags/swiftui/index.html
+++ b/zh/tags/swiftui/index.html
@@ -1,6 +1,6 @@
swiftui | 自由庄园
swiftui
-
100 Days of SwiftUI
1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软...
四月 9, 2024 · 5 分钟 · 2289 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
+100 Days of SwiftUI
1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副...
四月 9, 2024 · 5 分钟 · 2360 字 · Ramsay Leung See this site’s source code here, licensed under GPLv3 ·
Powered by
Hugo &
PaperMod
\ No newline at end of file
diff --git a/zh/tags/swiftui/index.xml b/zh/tags/swiftui/index.xml
index e3ed7d57..c83ac551 100644
--- a/zh/tags/swiftui/index.xml
+++ b/zh/tags/swiftui/index.xml
@@ -19,10 +19,10 @@
Tue, 09 Apr 2024 19:04:00 -0700
https://ramsayleung.github.io/zh/post/2024/100_days_of_swiftui/
- 1 缘起 我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软
+ 1 缘起 我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副
1 缘起
-我花了半年多的时间,在工作之余的闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体现下IOS开发,再看下有没有机会通过写软件来做点副业。
-先花了大概3个月时间,通过 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
+我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。
+先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI.
每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.
-并在整个课程中,写了19个IOS App, 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的注册费).
-经过这100节课和19个APP的训练,我已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
-但是在苹果做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
+在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).
+经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈(干)工程师了(不是)
+但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈
2 Swift 初体验
-Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会被苹果正式推出的一门编程语言。
-按照官方的说法,Swift从Objective-C, Rust, Haskell, Ruby, Python, C# 都有不同程度的借鉴和学习。
+Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。
+按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。
因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难,=Optional=, Error Handling
, Result
, Generic
, Enumerations
, Protocol
这些概念都和Rust的大同小异。
-又因为是由LLVM之父来操刀,所以语言本身也设计得很优雅.
+又是由LLVM之父来操刀,所以语言本身也设计得很优雅.
让我眼前一亮的可能是借鉴自 C# Extension Methods 5的 extension
功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.
例如,以下的代码就可以扩展内置的 Double
类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:
extension Double {
@@ -64,7 +64,7 @@
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"
-
总体而言, Swift是一门吸收了众多PL理论的现代编程语言
+
总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用
3 SwiftUI
SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack
, HStack
, ZStack
就可以实现X轴,Y轴,和Z轴方向的布局
例如下面这个就是通过 ZStack
几行代码实现的叠加效果:
@@ -150,9 +150,9 @@
4 问题
-听起来好像很美好, IDE新功能好用,编程语言优雅, UI框架简洁好用,但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
+听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有大问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。
SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData
甚至只支持IOS17,
-更离谱的是,SwiftUI的BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
+更离谱的是,SwiftUI的 BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。
不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:
对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户,
而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。
@@ -161,7 +161,7 @@
开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。
又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.
另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage
框架,顾名思义是用来作图片处理.
-但之前是使用Objective-C写的,直接使用SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
+但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:
func applyProcess(){
guard let outputImage = currentFilter.outputImage else {return}
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}