-
+
- 学习 IOS 开发(Swift)并做一款 APP 上架到Apple Store +
- 学会尤克里里 +
- 每月至少读一本书 +
- Marry +
diff --git "a/2015/2016\345\277\203\346\204\277\345\215\225/index.html" "b/2015/2016\345\277\203\346\204\277\345\215\225/index.html" new file mode 100644 index 0000000000..1a72122cd9 --- /dev/null +++ "b/2015/2016\345\277\203\346\204\277\345\215\225/index.html" @@ -0,0 +1,504 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +公冶长一生治学,德才兼备,虽然做过牢,可孔子不觉得他有罪,就把女儿嫁给了他;
子谓公冶长:“可妻也,虽在缧绁之中,非其罪也!”以其子妻之。
南宫适知晓分寸,进退有度,好社会能干、坏社会能自保,孔子把侄女嫁给了他;
子谓南容:“邦有道不废;邦无道免于刑戮。”以其兄之子妻之。
宓子贱仁德好学,刚正不阿;
子谓子贱:“君子哉若人!鲁无君子者,斯焉取斯?”
子贡心高气傲,有点喜欢自我标榜,孔子总是苦口婆心地对他旁敲侧击;
子贡问曰:“赐也何如?”子曰:“女,器也。”曰:“何器也?”曰:“瑚琏也。”
子贡曰:“我不欲人之加诸我也,吾亦欲无加诸人。”子曰:“赐也,非尔所及也。”
冉雍朴素踏实、勇于实干;
或曰:“雍也仁而不佞。”子曰:“焉用佞?御人以口给,屡憎于人。不知其仁,焉用佞?”
漆雕开稳重谦虚,志向高远但也沉得住气;
子使漆雕开仕,对曰:“吾斯之未能信。”子说。
子路是个急性子,自视甚高,直来直去,有什么说什么;
子曰:“道不行,乘桴浮于海,从我者其由与?”子路闻之喜,子曰:“由也好勇过我,无所取材。”
子路有闻,未之能行,唯恐有闻。
公西赤善于外交,口才一流;
“赤也何如?”子曰:“赤也,束带立于朝,可使与宾客言也。不知其仁也。”
冉求多才多艺,特别会管钱,但是因为帮季氏敛财,受到孔子的严厉批评,后来跟孔子学习之后逐渐成为仁德之人;
“求也何如?”子曰:“求也,千室之邑,百乘之家,可使为之宰也,不知其仁也。”、
宰我调皮捣蛋,能言善辩,他最出名的事就是白天睡觉被老师骂;
宰予昼寝,子曰:“朽木不可雕也,粪土之墙不可杇也,于予与何诛?”子曰:“始吾于人也,听其言而信其行;今吾于人也,听其言而观其行。于予与改是。”
申枨精通六艺,但欲望比较强,孔子觉得他还没培养出刚健的气质。
子曰:“吾未见刚者。”或对曰:“申枨。”子曰:“枨也欲,焉得刚。”
参考:
]]>在读《金钱心理学》时,我脑海中经常飘出一句励志的话:「种一棵树最好的时间是十年前,其次是现在」,刚刚查了一下这句话的来源,出自非洲经济学家丹比萨·莫约的《援助的死亡》一书,巧合的是也是一位经济学家说的。我现在已经开始了超长线的定投计划,用十年时间来定投黄金和标普500,自从开始定投后就出现了两种有冲突的念头:既想让时间过快一点,好让我完成我的定投目标,见证时间和复利带来的强大效果,又想让时间过慢一些,自己还不想那么快的老去,想再多一些时间陪伴孩子们,更不想眼睁睁看着父母一天天的老去。这本书还纠正了我一个错误观念,我之前认为财富跟赚钱多少成非常强的正相关性,这本书告诉我并不是这样,收入当然是一部分,但对大部分人来说更重要的是节俭和储蓄。
这本书中没有教我们认识各种指标,都是一些软技能,下边是我从这本书中摘录下的句子,通过这些句子也能感受到这本书再讲的是什么样的理财观念。最后我会在写一写我准备开启的一段超长线投资计划。
我为自己设计了一个为期至少10年的理财规划,规则很简单:通过蚂蚁财富,每天分别定投100元到「黄金ETF」 和「标普500ETF」,如果行情出现1%的回撤时则当日加仓50元,当盈利超过30%时则赎回30%的仓位,后边再慢慢加仓定投回去。
我们来粗略估算了一下10年下来的投资金额:
1 | # 每年大约有250个交易日,每年的投资额为: |
按照10年的投资回报率80%(虽然市场不可预测,但人总要有些盼头,况且我定投的两项在过去十年中回报都超过了200%),这样10年下来大概会有44万的投资回报,加上本金刚好100万。虽然这么长的时间只回报44万看起来不多,但这对我来说这是一种无痛的投资方式,用作者的话是:「用能让你睡踏实的方式来理财」。当然在这个过程中有可能还会根据我的生活水平来调整定投金额,比如5年后我提前把房贷还完了,也许就能出多个2、3倍的闲钱用于定投。另外,本段开头也说了,我这个投资计划短则持续10年,长则持续20年、30年,也许在复利的作用下收益远远不止于我上边计算出来的那么多。
]]>把第一版功能完成后,这几天工作不是那么多,于是我从Github上找到了这个库https://github.com/langchain4j/langchain4j ,从名字就能看出来,这个项目是参考的 Python 的LangChain,Java 库的命名很有意思,很喜欢叫 xxxx4j,4j 的意思是 for Java,比如 log4j。
我大致看了一下介绍,功能还算完备,给出的demo来看使用方式上可读性也很高,更重要的一点是支持古老的Java8。于是我在项目中进行了引入,将已有代码进行了改造,在跑直接调用 OpenAI 的例子时很顺利,当我切换为 Azure 后问题出现了,报错堆栈如下:
1 | Exception in thread "main" java.lang.NoClassDefFoundError: reactor/core/Disposable |
我按照堆栈的引导,一步一步去看代码,发现是在创建 HttpClient 对象时挂了,我进到 ConnectionProvider
源码中查看,确实找不到上边说的 Disposable
类,这个类来自 reactor-core
包。通过IDE跳转进的路径看到,目前项目中所使用的 reactor-core
版本是 2.0.8.RELEASE,我找到最新 3.6.7 版本的 reactor-core
源码看了下是有Disposable
这个类的。
一开始我认为是 langchain4j 的这个项目有问题,去 Github 的 Issue 中搜了下并没有相关的提问,于是我自己开始尝试动手解决,尝试了以下几种方式都不行:
reactor-core
langchain4j-azure-open-ai
下的 reactor-core
依赖,保证我自己引入的最新版本生效reactor-netty-core
的最新版langchain4j
的依赖在做上边的第2步时,启动调试后可以看到,IDE在进入ConnectionProvider
后确实可以正常跳转进Disposable
了,但最终还是报错。通过依赖分析也没有发现和 reactor
的任何冲突,一直搞到晚上下班也没解决。
今天早上上班后我换了个思路来排查这个项目,创建了一个新项目,只引入 langchain4j
的依赖,可以正常执行,接下来我把我们项目中其他依赖项引进来,发现还是没问题,当我把 parent 引入后问题出现了。虽然 parent 的 pom 文件在远端,但IDEA提供了一个功能,可以修改本地的文件来进行调试,我用二分法删除 parent 中的依赖,最终将问题定位在了:
1 | <dependency> |
parent 中 spring.boot.version
的值是 1.5.7.RELEASE
,我在上上家公司写Java时就有这个版本了,是个非常老的版本,但升级 SpringBoot 关联的问题会更多。我继续深入进去看,在 spring-boot-dependencies
的 pom 文件中 properties
指定了reactor.version
为 2.0.8.RELEASE,这下破案了。之前我无法通过依赖分析找到冲突,也是因为依赖是在 parent 指定的,且这个依赖版本无法在后续进行修改。
有种覆盖 parent 版本号的方式是在自己项目的父 pom 中的dependencyManagement
下进行声明,我尝试在 dependencyManagement
加上如下片段:
1 | <dependency> |
此时报了另一个错误:
1 | java.lang.VerifyError: class io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe overrides final method close.(Lio/netty/channel/ChannelPromise;)V |
回到最开始的问题,报错误的根本原因是,初始化 Azure模型时需要构造一个 HttpClient,默认情况下会使用ConnectionProvider
来构造。看了下 AzureOpenAiChatModel
的 builder 方法,支持自己传入 OpenAIClient
,而 OpenAIClient
可以自己构造 HttpClient
,通过这个文档看到 https://learn.microsoft.com/en-us/azure/developer/java/sdk/http-client-pipeline HttpClient 有多种实现,其中可以用 OkHttpClient 来实现,于是我进行了以下魔改:
1 | private static OpenAIClient setupSyncClient(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) { |
从开源代码中拷贝出 setupSyncClient
和 setupOpenAIClientBuilder
方法,并对setupOpenAIClientBuilder
中的HttpClient httpClient 的创建逻辑进行了调整
1 | // before |
初始化Azure模型时传入我自己的 client:
1 | // 默认生成的client使用NettyAsyncHttpClientProvider和SpringBoot所依赖的版本不兼容,改用OkHttpAsyncClientProvider进行重写 |
并在工程中引入 azure-core-http-okhttp
的依赖
1 | <dependency> |
再次执行还是报错了,不过这次的错误变为:
1 | java.lang.NoClassDefFoundError: reactor/util/context/ContextView |
还是 reactor 的问题,但可以看到,现在已经不再使用 reactor.core.Disposable
了,也许升级一下 reactor-core
可以解决,我再次在项目的 parent 的dependencyManagement
下引入
1 | <dependency> |
再次尝试,问题解决。
]]>昨天早上坐在旁边的同事告诉我,他的女朋友周末把我的博客通过文字转语音的方式边听边做家务,并且想要人肉催更。每次听到有人说读了我的博客,而且希望催更,我都既兴奋又诚惶诚恐。兴奋是因为有人能喜欢读我喜欢写的东西,惶恐是因为居然有人喜欢我写的东西。
实际在看似停更的这小半年来我并没有停,并且再坚持每日一更,只不过内容放在了另一个站点上,域名是 https://diary.jiapan.me/ 。从域名可以看出,这是我写日记的地方,站点标题叫「小小的避难所」,灵感来自毛姆写的《阅读是一座随身携带的避难所》这本书的书名,正如名字写的这样,我把那里作为我的避难所来记录、倾诉我的所感所想。当然那个站点上的内容也不是每天都会更新发布,而是根据我的心情,想起来了就整理一批我在Notion中写的内容发布出去。
进入避难所有一点小小的门槛,需要留下你的邮箱和阅读原因,邮箱只要是常见域名就可以,会收到一个入场验证码,输入验证码后再说明原因就可以进入了,原因我并不会审核,只要大于5个字符就可以了。
设置门槛的原因有两个,首先是我希望让这些内容可控,我需要知道都被谁访问过,其次是我不希望这些内容会被爬虫抓到,或者说可以通过搜索引擎搜到。
为什么我把那些内容单独隔离到了另一个站点内,而没有放在这里,因为那些都是我的日常碎碎念、流水账,每一篇内容都写的很零散,每天晚上我会回顾一下今天值得纪念的事情。拿出几样来记录一下,没有任何主题。
这个博客内大部分内容都是围绕着一个主题来写的,但这种写法很费精力,而且实话实说我并没有那么多干货。当然我也知道写这种结构化的文章相比写流水账,对个人来说会有更好的提升,我想先通过记录流水账的方式把写作这个习惯培养起来,然后再慢慢进阶。
所以,如果想继续读我流水账的朋友可以左转进去我的小小避难所,但我也先在这里做个免责声明(狗头保命),那些内容确实不体系化,没有营养,没有干货,读后可能会让你大失所望。引用曹公的一句话:满纸荒唐言。
顺便说一句,昨天和老板提了离职,准备开启一段新的征程大海,去向暂时保密,等未来有了水花再回来聊一聊这段经历叭。
]]>回归模型就像是一个预测机器,可以帮助我们猜测事物的未来。假设你喜欢吃冰淇淋,而冰淇淋的价格通常会随着天气变化而变化。现在,我们可以观察天气情况和冰淇淋的价格,然后用这些信息来猜测未来的价格。
比如,如果明天是个炎热的夏天,天气很热,那么冰淇淋的价格可能会比较高,因为很多人想要买冰淇淋来解暑。相反,如果明天是个寒冷的冬天,天气很冷,那么冰淇淋的价格可能会比较低,因为很少人会想要吃冰淇淋。
回归模型就是通过观察过去的天气和冰淇淋价格的关系,来预测将来的价格。它会考虑到很多因素,例如天气、季节和需求,然后给我们一个猜测的价格。虽然它不能百分之百准确地猜测价格,但它可以给我们一个大概的预测,帮助我们做决策。
分类模型就像是一个分类小助手,可以帮助我们将东西归类。想象一下,你有很多玩具,例如球、娃娃和积木。现在,你想要把它们分类整理,把球放在一起、把娃娃放在一起,积木也放在一起。
分类模型就是帮助我们做这个分类工作的机器。它会观察玩具的特点,比如形状、颜色和材质,然后根据这些特点把它们分成不同的类别。就像是在玩玩具时,你可以根据它们的外观和特点来决定它们应该放在哪个盒子里。
分类模型可以帮助我们在很多不同的情况下进行分类,比如识别动物、区分水果、辨别颜色等。它可以根据事物的特征将它们分成不同的组别,让我们更好地理解和组织世界。
不管哪个证据,宝玉肯定是农历四月底五月初的生日,既阳历(公历)五月底六月初。我按照农历四月二十六这个日期,随机查了几个农历对应的公历(能力有限,只能查到1900年以后的),都是落在双子座的时间范围内。双子座的时间范围是公历5月21日~6月21日。
]]>投资组合理论是由美国经济学家哈里·马科维茨(Harry Markowitz)于20世纪50年代提出的理论框架,也被称为现代投资组合理论(Modern Portfolio Theory,MPT)。该理论旨在帮助投资者在风险和收益之间取得最佳平衡。投资组合理论的核心思想是通过将多种资产组合在一起,以最小化给定预期收益水平下的投资组合风险,或在给定风险水平下最大化预期收益。
想象一下,你有一个盒子,里面装着各种不同的玩具,比如娃娃、小车和积木。每个玩具就像是不同的投资。现在,假设你想保护你的玩具并确保它们的价值随着时间增长。
投资组合理论就像是一种决定你的盒子里应该有多少个不同玩具的方法。你要选择适当的玩具组合,这样你才能获得最好的结果。
但是,这里有个诀窍:不同的玩具有不同的风险和回报。有些玩具可能更有价值,但风险也更大,而其他的可能更安全,但增长速度较慢。所以你需要决定你愿意承担多少风险。
投资组合理论帮助你找到合适的平衡点。它建议你选择一种玩具组合,这样你就可以把风险分散开来。这意味着如果一个玩具表现不好,其他的玩具仍然可以让你获得回报。
简而言之,投资组合理论就是帮助你选择合适的玩具组合,以平衡风险,并让你的盒子里的玩具保持增值。
假设你有1000美元,你有三种不同的投资选项:股票、债券和黄金。
现代投资组合理论认为,投资者可以通过合理地分配资金来平衡风险和回报。
首先,你需要了解每种投资的预期回报和风险。假设股票的预期回报是10%,债券是5%,黄金是3%。同时,股票的风险最高,债券次之,黄金的风险最低。
现代投资组合理论建议你根据你的风险承受能力和目标来分配资金。假设你对风险比较保守,你可以将60%的资金分配给债券,30%分配给股票,剩下的10%分配给黄金。
通过这样的分配,你在投资组合中平衡了风险和回报。债券的较高配比可以提供稳定的回报,股票的适度配置可以获得更高的回报,黄金的配置可以提供一定的保值功能。
现代投资组合理论的关键思想是通过将资金分配到不同的资产上,以实现风险的分散化。这样,即使某个资产表现不佳,其他资产仍可以为你的投资组合提供回报。
风险平价模型(Risk Parity Model)是一种投资组合管理方法,旨在通过平等分配投资组合中不同资产的风险,实现更平衡的风险暴露。与传统的投资组合管理方法相比,风险平价模型更加关注风险分散和资产间的相关性。
风险平价起源自一个目标收益率为10%、波动率为10%~12%的投资组合,是美国桥水创始人瑞·达利欧在1996年创立的一个投资原则,既全天候资产配置原则。
现在,想象一下你有一张画纸,上面有很多不同的颜色。每种颜色就像是投资中的不同资产,比如红色代表股票,蓝色代表债券,黄色代表房地产等等。
风险平价模型就是一种方法,让你在画纸上均匀涂上不同的颜色。这样,每种颜色(也就是每种资产)都有相同的风险,就像画纸上每个区域的颜色一样多。
为什么要这样做呢?因为不同的颜色(或资产)有不同的风险和回报。有些颜色可能非常亮,表示它们的风险更高,但潜在回报也更大。而有些颜色可能相对较暗,表示它们的风险较低,但潜在回报也较小。
风险平价模型帮助你确保你的画纸上每个颜色(或资产)的风险都是一样的。这样,如果一个颜色表现不好,其他颜色仍然可以给你带来回报。
简而言之,风险平价模型就是让你在画纸上均匀地涂上不同的颜色,以确保不同资产的风险是平衡的,并让你的投资更加稳定。
假设你有1000美元,你想将其投资于两种不同的资产:股票和债券。
股票通常风险较高,但潜在回报也更高,而债券被认为更安全,但回报较低。
在风险平价模型中,你不仅仅是平均分配你的资金到股票和债券上(各500美元),而是根据每种资产的风险来分配你的投资。
假设股票的风险更高,你决定将70%的投资分配给债券,30%分配给股票。这种分配是根据每种资产的风险贡献应该相等的理念来确定的。
通过这样做,你在投资组合中平衡了风险。如果股票表现不佳,对债券的较高配置可以帮助抵消损失,并为你的整体投资提供更稳定性。另一方面,如果股票表现出色,较小的配置也不会对整体投资组合的表现产生太大影响。
风险平价模型旨在通过考虑不同资产的风险来实现资产间的平衡。它帮助你进行投资多样化,并更有效地管理风险。
巴菲特是历史上最伟大的价值投资者,他的交易逻辑的核心是:寻找优质企业并长期持有这些企业的股票。如何判断一个企业是否优质?这时就可以依据上边提到的4个指标来进行判断了。
这个指标可以帮助投资者评估企业的盈利能力和管理效率。
净资产收益率是用来衡量企业利润与其净资产之间关系的指标。
它反映了企业利用所有者权益实现的盈利能力。
净资产收益率的计算公式为:净资产收益率 = 净利润 / 平均净资产。
净利润
指的是企业在一定时期内扣除所有成本和费用后所剩下的利润。
平均净资产
是指企业在一定期间内的
资产净值的平均值
。
假设有一家公司,在某一年度的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。
首先,计算平均净资产: 平均净资产 = (期初净资产 + 期末净资产)/ 2 = (2000万元 + 2500万元)/ 2 = 2250万元
接下来,计算净资产收益率: 净资产收益率 = 净利润 / 平均净资产 = 500万元 / 2250万元 ≈ 0.2222 或 22.22%
在这个例子中,公司的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。
通过计算,得到平均净资产为2250万元。
最后,通过将净利润除以平均净资产,得到净资产收益率为22.22%。
这个例子中的数据说明了净资产收益率的计算方法。净资产收益率衡量了企业在一定期间内每单位净资产所创造的净利润水平。在这种情况下,净资产收益率为22.22%,表示该公司在该年度内每单位净资产创造了22.22%的净利润。
这个指标可以帮助投资者了解企业的盈利能力和生产成本控制情况。
毛利率是用来衡量企业销售产品或提供服务后的毛利润与销售收入之间的关系的指标。
它反映了企业在销售过程中所保留的利润比例。
毛利率的计算公式为:毛利率 = 毛利润 / 销售收入。
毛利润
是指企业在销售产品或提供服务后剩余的销售收入减去与销售直接相关的成本。
销售收入
是指企业在一定时期内通过销售产品或提供服务所获得的总收入。
如果一个行业的毛利率低于20%,那么几乎可以断定这个行业存在着过度竞争。
这个指标可以帮助投资者评估企业的盈利能力和经营效率。
净利率是用来衡量企业净利润与销售收入之间关系的指标。
它反映了企业在销售过程中实现的净利润比例。
净利率的计算公式为:净利率 = 净利润 / 销售收入。
假设有一家制造公司,它生产并销售手机。在某一年度,公司的销售收入为1000万元。与销售直接相关的成本包括原材料、直接劳动和制造费用,总计为600万元。此外,公司还有其他间接费用和费用,如销售费用、管理费用和利息费用等,总计为200万元。
根据上述数据,我们可以计算毛利润和净利润:
毛利润 = 销售收入 - 与销售直接相关的成本 = 1000万元 - 600万元 = 400万元
净利润 = 毛利润 - 其他间接费用和费用 = 400万元 - 200万元 = 200万元
在这个例子中,公司的销售收入为1000万元,与销售直接相关的成本为600万元,其他间接费用和费用为200万元。
毛利润表示在销售过程中,公司通过销售所保留的利润。在这种情况下,公司的毛利润为400万元,即销售收入减去与销售直接相关的成本。
净利润则是在扣除所有成本和费用后所得到的最终利润。在这个例子中,公司的净利润为200万元,即毛利润减去其他间接费用和费用。
毛利润关注销售过程中所保留的利润,而净利润则考虑了所有与经营活动相关的费用和收入。
毛利率和净利率是两个常用的财务指标,用于衡量企业的盈利能力。它们之间的区别在于考虑的成本因素不同。
总结起来,毛利率关注的是销售收入和与销售直接相关的成本之间的关系,它衡量了企业从核心业务中获得的利润比例。而净利率则考虑了所有成本和费用,包括销售成本以外的费用,衡量了企业在所有经营活动中实现的净利润比例。净利率相对于毛利率更全面地反映了企业的盈利能力和经营效率。
市盈率是衡量股票相对于每股盈利的价格的指标。
它是投资者评估一家公司的股票是否被低估或高估的重要指标。
市盈率的计算公式为:市盈率 = 股票市场价格 / 每股税后收益。
股票市场价格指的是股票在市场上的交易价格,也就是投资者购买或出售股票所需支付或获得的价格。
每股税后收益
是指企业每股普通股的税后净利润,也可以理解为每一股票所对应的盈利。
较高的市盈率可能意味着市场对该股票有较高的期望和溢价,而较低的市盈率可能意味着市场对该股票的期望较低。
假设有一家公司,它在某一年度的每股收益为10元,而股票的市场价格为100元。
首先,计算市盈率: 市盈率 = 市场价格 / 每股收益 = 100元 / 10元 = 10倍
在这个例子中,每股收益为10元,市场价格为100元。
通过计算,得到市盈率为10倍。
ROE高和净利率高通常被视为公司财务状况较好的指标,但并不意味着这些公司一定是好的投资对象。以下是一些需要考虑的因素:
因此,高ROE和净利率只是投资决策的起点,而不是唯一的决策依据。投资者应该进行全面的研究和分析,以综合考虑多种因素,并结合自己的投资目标和风险承受能力做出决策。
虽然ROE和ROI都涉及利润和投资,但ROE主要关注企业的盈利能力和资产利用效率,而ROI主要关注特定投资项目或资产的回报率,它们评估的对象和应用范围不同。
在评估企业绩效时,ROE和ROI通常会结合使用,以提供更全面的分析。ROE可以衡量企业整体的盈利能力和管理效率,而ROI可以帮助评估具体投资项目的回报情况。
]]>本月最后一天是我入职 TT 的三周年,我依然向往我刚入职 TT 后近一年左右的时光,那个时候 TT 还有一点点外企文化,不具体展开讲了,用几个词形容就是:包容、信任、自驱、敢于试错。我那时也非常庆幸自己入职一家好公司,当时的 TT 被称为互联网最后一片净土,也确实对得起小而美的称号。
我一年半前主动要求过一次转岗,从直播转到推荐,刚来推荐组的时候,每次晨会听到大家工作那么饱和我都很焦虑,所以我也能体会大家现在的感受。
上周有一天kq因为白天开了一整天的会,但他手里的一个技术驱动项目进度还差一些,晚上下班后我问他走不走,他说得加班把技术驱动搞完,不然第二天早会没得说。我知道他是在开玩笑,不过那句「不然第二天早会没得说」这句话我确实也在心中说过好多次。
我不希望大家每天为了考虑早会上要说什么而有压力,甚至出现为了说点什么而被迫找点琐碎而无意义的事情做,也不希望大家靠堆砌很多工作量来证明自己的能力和重要性。我希望大家的工作可以更专注、聚焦、深入、认真、细致一些,不要东一榔头西一棒槌。我特别喜欢一句话:不要用战术上的勤奋,来掩盖战略的懒惰。
所以我打算尝试取消早会,取消也许是长期的,也许是暂时的,还要看取消后的效果和公司的要求。对于我来说开晨会是正确地做事,现在取消周会是做正确的事(大家可以想想这两句话的区别),结果是否正确现在不得而知。
不开晨会建立在大家自驱的基础上,也建立在我对大家充分了解和信任的基础上,我一直相信信任是促使人们进步的最大动力,因为信任能够让人们表现出自己最好的一面。
我们组内的方向比较多,每个人的工作内容不尽相同,每日同步给所有人的意义不是很大,靠每周周会来做一次相互了解和同步就够了。
我们现在早会最大的益处其实是收集大家日常工作中遇到的问题,我们取消了早会,大家的问题就不要再等到第二天早会上再提了,有了问题随时提,不要因为没了早会的要求就掩盖问题,如果后边发现出现了问题被掩盖的现象,我们还会恢复早会。
在团队划分上,为了便于管理和领域打通,jw 没有再把工程和核心拆成两条线,但大家也能看到kq在推荐工程上的经验比我多的多,而且在核心需求比较多的时候我也确实无法两头都顾及到。再加上由于取消早会后反馈周期的加长,项目的跟进上不可避免会相较之前难度更大,所以我在这里也给kq提个要求,后边我们两个做下分工,所有核心项目我这边都会去了解背景、方案、进度和风险,所有推荐项目kq也要做到这几点,包括内部、产品和对外支持的项目。
再回到大家的工作上,大家在有项目、有工作任务的时候就聚焦于手头的工作,力求完美。如果有几天真的没有那么忙时就适当放松,学习一些感兴趣的东西,工作应该有张有弛,一直紧绷和一直放松都不是正常的状态。大家学习的时候尽量学习和我们业务相关的东西,我们组包含了公司内两大块最重要的业务:推荐和 IM,所以要想学肯定是有的学的。我也非常鼓励大家去发现、解决、优化工作中遇到的业务和技术痛点,这会让大家获取更大收益,包括能力上的和绩效结果上的。如果公司内的业务无法满足自己,也可以学习其他自己感兴趣的东西,比如 Web3或者学一门新的编程语言等等。我推荐作为程序员的大家,有精力的话每年学一门新的语言。编程语言会限制我们的思维模式,如果你长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考。
除了工作还有大家的工作状态,每个月总有那么几天不想工作,实在不想工作的那一天就让自己松弛一些。我自己很容易焦虑,所以我很羡慕能拥有松驰感的人。根据我的经验,一个正常排期3-5天的项目如果在状态佳而且无打扰的情况下,大概率一天就能把代码写完,这种状态也叫心流,有本叫《心流》的书大家感兴趣也可以看看。
最后,希望大家未来有一天回忆起在 TT 的工作(或实习)经历觉得是有意义的,而不是给大家留下痛苦、无效忙碌的一段经历。
]]>半夜睡不着时,我想起了白天一件有点内疚的事:
我们有个卧室是专门给念念准备的,墙壁刷成了淡粉色,还买了她喜欢的床。装好床的那天,她高兴极了,在自己的床上蹦了好久,一直想着如何装饰自己的房间。
这次回来,她看到自己的床上放了登登的衣服,地上也有一些其他的杂物。于是,她把那些不属于她的东西全都扔到了其他房间。我当时很严肃地批评了她,告诉她如果不让别人把东西放到她的房间,她以后也就别进其他房间了。她当时一脸惶恐,赶紧把她刚才扔出去的东西一件件搬回来,以讨好我。
深夜静悄悄的时候,我想到念念在这件事上并没有错。既然我已经告诉过她那是她的房间,那么她就有权利让自己的房间保持干净和整洁。再者,还有一个月念念就6岁了,我们之前蜗居在60多平的房子里,她一直没有属于自己的空间。第一次拥有自己的房间肯定是非常想占为己有的,我可以理解她,因为我小时候也有这样的想法。想想自己小时候,如果得到了自己非常喜爱的东西,肯定也不愿意让别人糟蹋。在拥有自己房间这一点上,我觉得非常亏欠她,在北京这个寸土寸金的地方只能委屈一下她了。
我们计划国庆节前带念念去趟上海迪士尼实现她的公主梦,我对自己的唯一要求是对她多一些耐心,不要因为她的一些小孩子的无理要求而对她发脾气。我就她这么一个女儿,不宠着她宠谁呢。去迪士尼的钱用的是我准备买摩托车的钱,之前因为考试失利,摩托车驾照考了两次,第二次考完后摩托车就对我没那么大吸引力了,所以也迟迟没有订车,这笔钱拿出来带念念去玩一趟把。
距上次去远的地方玩刚好过去3年,上一次是离职上家公司入职 TT 之前,到新疆玩了一个星期,一晃三年过去了,时间真快。说到这里,我奉劝各位还没结婚、没生娃的朋友及时行乐,趁着自由能出去玩就多出去玩。也奉劝那些不想结婚、不想生娃的朋友,如果一个人过得开心,请坚持你们的想法。
]]>上午和路秘书一起送小念去上陶艺课,她和我一起来的原因是想给那个安排我们进来的老师送两盒月饼,她知道这个活我这种笨嘴笨舌的人肯定完不成,而且我一点都不擅长这些。一开始我也觉得她完不成,认为老师不会轻易收家长东西的。没想到在路秘书的再四推让下,那个老师最后还是接了我们的东西,还主动和我们说下学期可以再给我们推荐一些其他课程。我非常非常佩服路秘书这种有社交牛逼症的人。
距下课还有一个半小时,我和路秘书压了40分钟马路,走到了一个距离上课地点最近的一个瑞幸,中间经过铁路高架桥看到一列高铁经过,路秘书跟我讲了一个当年追她的男生后来进了铁路局工作的一段故事。我们到瑞幸后我点了一杯之前没喝过的咖啡,在那里歇了20分钟,之后一人骑了一个共享单车回到了上课地点。因为平时上下班路程上的需要,我开了哈罗和滴滴两个共享单车平台的月卡,所以今天我用每个平台扫了一个,骑车就没有花钱。
中午回家后路秘书亲自操刀给我剪了个头发,以后又可以在剪头发的开销上省下一笔钱了。过程中我爸作为有8年理发经验的人进行了友情指导。之后去稻香村买了些熟食,我还给自己买了三块在疫情居家办公期间发现的一个好吃的糕点——山楂锅盔,强烈爱吃山楂口味的小伙伴尝一尝。买完熟食回家收拾了一些东西就来新家了,吃饭过程中还喝了两盅酒,现在还晕乎乎的。
小念今天带回了她的第一件陶艺作品,一只啄木马笔筒,里边插了扭扭棒做的花:
]]>序列化的情况比较简单:
1 | type A struct { |
上边这段代码输出:
1 | {"Case":1,"Cas_E":3,"ok":4} |
casE
这个字段没有输出,原因是因为他是个不可导出的私有字段,即使设置了 tag 也不可序列化。CaSE
序列话后的 key 为 ok
是因为我们给它指定了 tag序列化的情况稍微有点复杂,其整体的优先级为:
我们看几个例子:
情况1,带 tag 的两个字段都无法匹配上(精准匹配+模糊匹配),不带 tag 的两个字段都可以模糊匹配上,优先赋值给前边声明的字段:
1 | type B struct { |
情况2,带 tag 的其中一个字段可以模糊匹配上:
1 | type B struct { |
情况3,带 tag 的两个字段都可以匹配上,第一个模糊匹配,第二个精准匹配:
]]>我有时会写一些不想在日常中表达的内容,这些内容不太想被熟悉的人看到。
但互联网是公开的,既然我把内容发在了网上就应该有被看到的准备,即使未来某一天看到了其实也不要紧。
使用自己的博客想写点什么就写点什么。
我的这个博客域名 jiapan.me 在国外注册、没有实名、没有备案,所以基本上没有被审查的风险,但我通常也会遵纪守法,所幸我的域名还没有被墙,站点也托管在 Cloudflare,国内大部分地区也是可以正常访问的。
使用自己的博客想怎么魔改就怎么魔改。
博客实际上是个网站,只要你懂点前端就可以对自己的站点就行修改,如果用的是一个开源的博客生成工具,那么可以直接使用其他人提前写好的主题,那么多主题总有一款符合你的审美。
再搭配上独一无二的域名,更能体现出个性了。
公众号后台可以看到粉丝数、浏览量这些数据,这无形中给了写作者很大的压力,每次写文章都会考虑这篇文章可以涨几个粉、能带来多少阅读量之类的数据。
我这个站点干脆不统计这些数据,我不在意有多少人读、多少人访问。不用每天绞尽脑汁去想如何打造10万+,如何打造爆款。
使用自己的博客想写什么主题就写什么主题。
在平台上写作还要为垂直领域而困扰,不同领域要迎合不同的读者,在个人网站上就没这个困扰了,我的地盘听我的。
在这里,我心情不好时可以吐槽,有了兴趣可以聊聊技术,郁闷时可以抒发感情,激情时可以干一碗鸡汤。
在商业平台内发布,如果这个平台需要变现的话,会在你的文章中间或者四周插入一些广告,有些广告会很 low,很影响阅读体验。
除了平台会插入广告,在一些平台上写作时写作者也可以允许平台插入广告,和平台进行广告收益分成,我目前没有将写流水账当成个营收手段的计划。
有一个词叫「私域流量」或者「私域变现」,我总觉得这种词带一些诈骗性质,我天然反感这种硬生生造出来的词。好多人说做公众号就是做私域,这也致使我反感去运营一个公众号。
使用自己的博客想什么时候写就什么时候写。
在平台上写,发布是一个问题,登录后台困难重重,要经过好几道验证,发布的时候也很麻烦,需要确认一堆内容,而且大部分平台不支持Markdown,但程序员最喜欢的编辑格式就是Markdown。
在公众号发文章,还有发布频率限制,修改起来也很麻烦,何必受这个窝囊气。
公众号这类的平台采用的是 push 模式,写完一篇文章后会 push 给你的订阅者,订阅者们会陷入在一个围墙内,只能看到他们订阅的内容,逐渐生成知识壁垒,每次收到 push 来的新内容还会产生焦虑感。
博客类的站点才用的是pull 模式,内容写完后就放在这里,各位看官想什么时候看就什么时候看,在你需要的时候来它就静静的在这里。
这也跟我的性格相符,我喜欢自己做决定,不喜欢被 push 的感觉。
微信只是国内的一个聊天工具,虽然国外用的也比较多,但绝大部分还是中国用户。包括知乎、简书这些平台,我不保证它们在50年后还能活着,如果它真的不在了,用户在上边发布的内容是不是也就不在了。
自己搭建一个博客,给域名续上几十年费,页面托管在一个国际主流服务商上,比如 Cloudflare或者 Github,基本就可以永存了。
在平台上写作需要遵守平台规范、更加谨言慎行,一个不注意触犯了平台上的限制那篇文章可能就没了,更严重一些的整个账号就没了,意味着之前发布的文章跟着受到了牵连,之前经营的成果付之一炬。
大部分平台,尤其是公众号,是很封闭的,这也意味着你写的内容在搜索引擎上检索不到,不仅搜索引擎搜不到,出了微信就很少看到了。
我发现谷歌对独立的博客还是很友好的,我有好几篇流水账通过某个关键字可以排在首页,而且有几篇我随手记问题解决方案帮助过很多人。
Web 和域名都是伟大的发明,就像我前边说的,微信未来有一天会死去,但 Web 和域名服务一定会长期留在这个世界上。
虽然我是个悲观主义者,但我还是非常向往活着,我希望我的这些碎碎念能一直留在这个世界上,证明我存在过。
就像《寻梦环游记》里所说的:死亡不是生命的终点,遗忘才是。
]]>“对生活的感激程度其实就是生活的充实程度。当我们对生活麻木,对一切习以为常的时候,其实我们的生活就已经死亡了”
「哈佛幸福课」的第8节,讲得是感激的重要性。作者建议我们把感激培养成一种习惯,当我们感激时,副交感神经系统功能增强,使我们变平静,从而加强免疫系统。
在提到如何培养感激时,作者提了一个行动方式:每天睡前写下5件值得感激的事。
培养一个能力的最佳方式就是实践,通过一次又一次感激来培养感激,我从6月21日开始实施这个行动,不过我稍稍给自己降低了一点点要求,每天记录3条值得感激的事,我同时把这个行动项录入到 Things 中对我进行每日提醒。
我是用 Notion 来记录这些感激内容的,每个月新建一个新的页面,每天一个大标题。使用 Notion 我可以随时随地记录,比如在地铁上、公司里、家里,从第一天开始记录到今天已经将近4个月了。
每天的持续记录使我发现,原来我身边有那么多事值得感激,但我之前已经习以为常,认为这些都理所当然。在写感激过程中,感激最多的肯定是在背后支持我的家人,除此之外我还会感激之前没有意识到的事物,感激的对象也不止有实实在在的人,还有身边给我提供便利的物品。
比如下边这段:
最上边两条我感激了两位同事,一位帮我一起沟通绩效结果,另一位是我现在的 Leader,和我一起梳理一些重点项目;接下来我还感激了「哈罗单车」,那一天是个周五,天气很好,晚上下班早,我骑着单车从公司回家,一路上风景也很好;第二天8月5日是个周六,我早上开车回老家,路上狂风大作电闪雷鸣遇上了大暴雨和大雾,我和我的车经过4小时路程,它安全的把我带回了家;最下边那条,是我回家后带念出去玩,突然感觉她长大了,之前在游乐场玩的时候一定要我陪着,这一次她可以自己玩耍了。
再来随便看两天的:
这两天也是周末,我感激了华为安装师傅、感激了家具安装师傅、感激了木工师傅、感激了北京的交通、感激了念念、感激了游戏厅的抓娃娃机。
在我写这篇流水账翻看这些感激记录的过程中,又能回忆起当时的喜悦,一定程度上起到了日记的作用。每条记录用一句话描述,不会有很大的写作压力,刚开始确实不容易发现那么多值得感激的事,随着自己记录的越来越多,就会越擅长发现生活中值得自己感激的地方。
有时我还会感激自己,比如下边几条:
在记录感激的时候,我不会强迫自己,如果某一天心情实在糟糕,可以允许自己只写一两条,某一天过得充实的时候也写过六七条。
通过记录这几个月的感激,我能很明显感受到自己情绪好了很多,不再那么偏激,能够从积极的方面思考问题了,注意力会放在积极正面的事情上,和其他人打交道时会思考对方有什么优点值得我学习,有时我还会把之前遇到后会非常生气的事换个思路去看。
我们应该心怀感激,而不是等到不幸发生时才意识到之前的自己错过了多么美好的时光。
世界上有很多美好的事物,但我们很快就会适应且不再察觉它们。每天两次花一分钟时间留意周遭的一切,花一分钟的时间,在上班的路上看看美丽的草地、青翠的树、美丽的雪。晚上用一分钟去回忆,回想你度过的一天,写下让你心怀感激的事物。
]]>中间尝试读书、冥想、听相声都没有缓解
刚刚把小红书、Twitter、脉脉这些会给我制造焦虑或者杀时间的 APP 卸载了
我第一次失眠是在高中时,在这之前我是每天都要午睡的体质
高中时非常喜欢班里一个女生,她也喜欢我
第一次失眠的原因是我们考试考砸了,我向她保证我们一起好好学习
然后那个晚上整晚都在迫切的希望自己早点睡着,早上早点起来开始学习
结果第一个不眠之夜就这么诞生了
到现在十五年了,不要说午睡,晚上很容易整晚无法入睡
运气好的话有时可以靠一片处方安眠药胡乱睡几个小时
高中时就开始了为了治疗失眠的求医之路
我也忘了那时候都吃些什么药了,反正是一把一把吃,也不见效
从失眠第一天开始,就像突然失去了睡眠的这项基本技能
躺在床上很虚无,忘记了该如何入睡
现在我会定期去医院的神经内科,开精神类处方安眠药
为了方便我都是挂周末取药的临时号,好几次医生都劝我挂个普通号或者专家号好好看看
但当我说我这个症状已经十几年了之后,医生也就不再说什么
据说失眠的人会出现在别人的梦里
既然我失眠了,希望梦到我的那个人可以一夜好眠
]]>这篇流水账我想聊聊我选择不去大厂的几个原因。
大学刚毕业时,因为年少轻狂,那时候互联网环境也比较好,两年内跳了3次。因为有过频繁换工作的经历,到后来我就对换工作这件事没那么强的意愿了,再换工作时会认真权衡利弊,而且给自己定下了之后每份工作要做3年以上的目标。
到今年我已经工作8年多了,已经换过不下4份工作,换工作都是一件成本极高的事,不管是对个人还是对前东家或者新东家。尤其是对个人,换工作后要重新熟悉环境,重新结交人脉、重新认识上下游、重新了解新公司的技术栈…
刚换工作后的半年内很多事情对我来说都会是全新的。因为成本极高,所以换工作一定一定要慎重,今年五月份我们组有过一轮人员地震,有三个同学因为出国或者回老家发展,在深思熟虑后选择了离职,还有两个看到突然走了好几个人心里痒,仓促的面了外边的机会,匆匆忙忙跳了槽,前段时间聊起来那些匆忙跳槽的都有些后悔。
我现在所在公司,平均工作时间是10:30-19:30,去掉中午2小时休息时间,工作时长为7小时。尽管我中午不午休,拿这个时间来运动、看书、刷题、写流水账,但这也是一大块属于我自己的时间,不管上午的事情有没有完成,午休这段时间都不会有人来找我。
去大厂后,晚上七点半下班基本属于奢望了,至少会再多出2个多小时的工作时长,相比现在的工作时长多出了30%,按照现在的市场行情,我不确定我通过跳槽可以再获得30%以上的涨幅,而且即便是获得了30%涨幅,按照工作时长来算,我也只是平薪跳槽,划不来。
我现在的团队也招了2个从字节跳槽进来的新同学,这边让他们很满意的一点是晚上9点后不可能有人突然拉他们进会。我告诉他们,不仅晚上9点后不会,晚上8点后就不会有人再找你了,除非线上炸了。
不知道是不是自己身体不行,我是真的卷不动,下午7点后没有任何想工作的动力,不知道大厂里每天干到晚上十点多的同行们是怎么坚持下来的。
这不是谦虚,我在很多方面都不具备大厂喜欢的能力,比如应试能力。我觉得大厂面试和中国的应试制度有些相似,通过背一些工作中实际用不到的八股问题进行面试,通过多伦面试后进入公司,而不是看一些更实际的能力,我也能理解这种做法,因为找工作的人太多了,这是最高效筛选人才的一种方法。
我在做面试官的时候不喜欢问八股文,我会主要关注对方在工作之余做了些什么、写过什么软件。如果一个人不爱一件事,他就不可能把它做得真正优秀,要是他很热爱编程,就不可避免地会开发自己的项目。
我那个去字节的领导跟我说他们在新员工入职第三个月的时候要做工作汇报,入职这三个月内并不是像我现在公司这样给新员工充分的时间安心学习新东西,而是上来就介入工作,在汇报时不仅要讲自己对这三个月工作的理解,还要讲工作的成果和输出。这种做汇报展示成果的能力也是我欠缺的,我也不擅长公众演讲。
大厂因为发展快,人员变动也相对较快,我遇到好几个朋友和我说他的 Leader 比他小,另一个说他的 Leader 是95后之类的。
一个好的直属 Leader 对工作体验太重要了,在工作中伴随我们最久对我们的影响最大的人就是直属 Leader。我不太相信一个工作两三年的人有特别好的管理能力。对管理的认识虽然可以靠书本学习一些驭人之术来提升,但更多的是靠人生阅历,前者是 PUA,后者是真正的管理。但要做到后者是需要时间的,就像我们不可能找10个孕妇来一个月内生出一个宝宝一样。
我那个去了字节的领导第三天就要求去参加季度规划会,之前他的话语权很重,大家都会听他的,但他在字节的第三天,就在会上比被自己小的产品经理diss,问他是不是不了解背景,质疑他的能力。这也是我前边说过的温情,一个稍微成熟点的,有点社会阅历的成年人不会对一个刚入职3天的人讲出那种话。我的自尊心很重、心眼很小,承受不了职场PUA…
有人生阅历的 Leader 更加善而坚定,更加有管理上的温情,这样的领导能站在员工的角度理解员工,照顾员工的感受,真正为员工着想。
另外大厂里还会有各种「嫡系」文化,在有裁员指标时,通常裁的不是能力不行的,而是非嫡系的。在有晋升指标时最先安排的也是嫡系里的“自己人”。
我深知人外有人天外有天,我可以在小公司里混的如鱼得水,但放到大厂的人才荟聚的地方也许就是一颗再普通不过的螺丝钉。
我不想在一个默默无闻的岗位工作,这种地方不会让我感受到成就感,很容易失去工作的动力。而大公司就是这么一个地方,大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。
我也许更擅长把一件事情从0做到80分,但从80做到100甚至120分不是我擅长的,而这是在大厂里需要具备的精益求精的能力。我更喜欢做宽而不是做专,喜欢做个八面手而不是一颗螺丝钉,由此也可以看出小公司更适合我一些。
我不想离开现在的公司的最主要原因还是工作时长方面,虽然现阶段的我需要钱,去大公司确实可以用时间换钱,但综合考虑各种因素,对于这个年龄和家庭情况的我已经不再合适。留给那些还年轻、还有梦想的年轻人们去闯一闯吧,未来属于他们。
]]>我举个例子,你媳妇和你妈吵架,你在他们中间就属于第三者,你起到的作用举足轻重,处理好能家和万事兴,处理不好能鸡飞狗跳。我不知道其他人,我是非常不擅长处理这种事的,我经历的鸡飞狗跳太多了🥲。
我不是一个合格的第三者,但我非常敬佩能把事处理的非常妥当的那些第三者。在我看来合格的第三者应该像袭人那样,大事化小、小事化无。
有一回宝玉去薛姨妈家,伺候宝玉的李奶妈拦着不让他多吃酒,回去后李奶妈还把宝玉给晴雯留的豆腐皮包子吃了,宝玉要喝茶时小丫头们说李奶妈把他泡好的枫露茶喝了,宝玉气的摔了茶碗,嚷嚷着要把李奶妈赶出去。不一会贾母房里的小丫头就来问发生了什么事,袭人站出来说是她不小心喝水时打碎了杯子,她不想大晚上的让贾母担心,没有提任何李奶妈的事。
后边还有一回李奶妈把宝玉留给袭人的酥酪吃了,袭人外出回来后,宝玉让人去把酥酪取来,丫鬟们回李奶妈吃了,宝玉正要发火,袭人说“原来是留的这个,多谢费心。前儿我吃的时候好吃,吃过了好肚子疼,足的吐了才好。他吃了倒好,搁在这里白糟蹋了。”就这样又化解了这一次危机。如果换成其他爱作妖的丫鬟,比如晴雯这种爆炭脾气的,非得把事闹大了不可(一会我说个关于晴雯的事)。
宝玉也有很多大事化小、小事化无的高光时刻,说一个例子,一次藕官在大观园里烧纸钱祭奠已经死去的、她之前的戏搭子菂官,被一个老婆子撞见,老婆子抓着藕官要去找太太。宝玉经过遇到此事,按常理,宝玉也可能会责备烧纸钱的人,但他看到藕官满面泪痕,他心想这个小戏子一定有她的心事,背后有无法言说的委屈,宝玉先把事情的真实原因放在后边,先自己站出来说是他让藕官烧的,就这样救下了藕官。
只要将心比心,你就会对一个人的伤心有所关怀,它既不是法律,也不是道德,而是在法律跟道德之外人内心最柔软的那个部分。
公司里,小领导在不同场合对自己的下属进行评价也能看出是否是一个合格的第三者。大老板们不了解一线员工的状态,需要小领导来反馈一下,如果小领导总抓着其他人的缺点去评判,不能避重就轻、善于发现别人的优点是万万不可的。你随随便便几句话,可能带给对方的就是天差地别的结果。
我们不要做老好人,也不要做煽风点火、唯恐天下不乱的人。如果能预测到一件小事在往恶性的方面发展,而你又是参与其中的一个人,不妨尝试化解一下。
不光宝玉身边的袭人,凤姐的特别助理平儿在这方面做的也非常出色,最著名的一回莫过于「俏平儿情掩虾须镯」,这一回中平儿和晴雯的处理方式形成了极大的反差。平儿在大雪天跟宝玉、湘云一起在野外烧烤,吃鹿肉时把镯子摘下放在了一旁,吃完后发现不见了,经过排查发现是宝玉屋里小丫鬟坠儿偷拿了。
平儿考虑到宝玉对丫鬟们很好,原文是这样写的:“我赶忙接了镯子,想了一想:宝玉是偏在你们身上留心用意、争胜要强的。”,「留心用意」,是说宝玉没有用管理丫头的方法管理她们,他相信人性有一种更高的自觉;「争强要胜」是说他希望自己房里的丫头,没有严格的法的约束也能有人性的自觉。如果平儿把这件事爆出来宝玉肯定会被人议论过于放纵自己的丫鬟,而那个丫鬟也会被赶出去,在那个社会如果一个丫鬟被一个大户人家赶出去基本就等于判了死刑(后边晴雯就是这么死的),所以平儿想把这件事掩盖下来,以后让大家提防着点坠儿就好了。她和麝月商议后打算不把这件事告诉正在生病的爆炭脾气的晴雯,谁成想他们的对话被宝玉听到了,宝玉还是转述给了晴雯,晴雯气的对那个小丫鬟又打又骂,假借宝玉之名把坠儿赶了出去。
思考:坠儿出了这种事,等于是宝玉对人性实验的一次失败,可是最大的为难在于,十次有九次失败,我们还要不要为那一次留下余地。
同样的事件,用不同的方式表达,起到的效果也大不一样,比如一个总打败仗的将军,我们可以说他屡战屡败,也可以说他屡败屡战,两个读起来相近的句子,含义却差了十万八千里,这又涉及到了语言的艺术。
最后讲个有趣的典故吧,大家都听过一个顺口溜「二十三,糖瓜粘」。”糖瓜”是一种用黄米和麦芽熬制成的粘性很大的糖,为什么腊月二十三要做糖瓜呢,因为传说这一天灶王爷要去天上,像玉帝报告每户人家这一年做了好事还是坏事,所以百姓们就把糖黏在炉口来贿赂灶王爷,意思是让灶王爷嘴巴甜一点,上天以后讲这一家人的好话。
民间的有趣就在于,他们会觉得没有什么东西是躲不过去的,就看你用什么方法。这个跟人的生命力有关。所谓生命力,就是灾难不再是灾难,危机不再是危机。我们在生活中,有时候遇到一点小事就觉得过不去了,其实就是生命力弱了。
]]>初中时和我一个班的有个L姓女生,因为长得好看性格又好非常受欢迎,那个时候她有非常多的追求者。她和班里一个当时身高已经超过一米九的韩国籍男生交往过(我当时上的是一个国际学校,有很多韩国交换生),和体育委员交往过,还和其他班的男生交往过,这几个仅是我知道的,不知道的可能更多。
我和她是同一个县的,每周五会一起坐大巴车回老家,我也鼓起过勇气约她来我家一起写暑假作业,那时候真的只是写作业而已。我知道自己几斤几两,也听到过她在私下里对我的评价,知道自己不可能,而且看到她身边整天有那么多人围绕,很是羡慕,甚至有些自卑,所以不敢有任何逾矩的想法。
好巧不巧,我们两个高中又去了一个学校,但这次没有在一个班里。她凭借着自己的优势又成了学校里的小红人,我们班也有好几个仰慕者。其中有一个W姓的男生看她戴了红框眼镜,在还不知道她名字的情况下就用了小红这个昵称来称呼她,当这个W姓的男生知道我和她是老乡后羡慕不已,整天问我很多问题,比如:你说小红有男朋友了吗?小红喜欢什么样的男生?我作为他的可靠线人乐此不疲的和他一起探讨。
高中时她偶尔遇到糟心事的时候会和我这个不可能的备胎倾诉,可能因为我那时候没什么经历,也不能给她出什么好建议,给她出主意的人很多,能静静听她讲的没几个,她就把我当成了一个特别好的倾诉对象。
高考时她通过艺考去了湖南的一所大学,我留在了河北,她的大学生活非常丰富,我就通过她的朋友圈又看了她四年,我也会有一搭没一搭的在微信问候一下她。那时候流行微博,我还在微博上偷偷关注了她。她和我说她想用Instagram,我就指导她一步步进行科学上网的配置,后来也顺理成章关注了她的Instagram账号,她Ins上的很多照片是没有发在朋友圈和微博的,我就觉得自己发现了她的秘密基地,有些窃喜。
一转眼又4年过去了,大学毕业前我在石家庄实习,本来是打算留在石家庄了,可看同学们都来了北京有些心痒痒。毕业第二天给公司领导提了离职,同一天收到了北京的一个面试通知,我在公司楼道里和对方聊了几句,对方问了我一些问题就给我发了offer,如果是现在这么卷的环境我肯定连一个offer也拿不到。
等我到北京开始上班后,看到L回石家庄了,准备在石家庄创业之类的,而且看起来是单身状态。我有些后悔来北京,幻想如果没有来我是不是也许会有什么机会?但既然已经来了就好好在北京发展吧,我们继续有一搭没一搭偶尔互相发个消息。
又过了半年她可能在石家庄不太顺利,也来了北京,在北京找了份工作,没多久在北京认识了新的男朋友,又没多久和男朋友吵架对方把他赶出去,她当时不敢和家里说,也没钱在外边住,就找我借了几千块钱,我毫不犹豫借给了她,这笔钱过了好久才还回来。
我刚来北京不久有段创业经历,是做一个类似探探的产品,我邀请她来我们APP注册发照片。每天通过后台数据看到她被很多人点赞我内心里替她高兴。
再后来我结婚了,作为同学、老乡的身份会继续每隔几个月问问她怎么样,不知道是不是巧合,有好几次我问她的时候都碰巧她遇上困难,和我聊聊她的遭遇。
实际上我们从高中毕业后就再也没见过面,之后的所有交流都是在微信上,她有时也会突然来找,甚至还和我说她梦到了我。
现在她的生活依然丰富多彩,全国各处旅游打卡吃美食,而且是个滑雪手、摩托车手。工作也是换了一份又一份,很早之前我问时是在做婚礼策划,过一段时间再问时准备开个精酿小酒馆,她就像一个神,让人捉摸不定,我是泯然众生中的一个守望者。
2021年她在朋友圈晒了结婚证,巧的是那个男生也姓贾。去年他们举办了婚礼,她穿婚纱真好看。
]]>“鸡瓜子”是什么?就是用手撕出来的鸡小腿部分的腱子肉。因为常常活动,所以那块肉的弹性最好。富贵之家能把一个食之无味的茄子,经过这么复杂的环节来制作,做的这么精细。
还有一次宝玉被他的爸爸暴打后,王夫人问他想吃什么,宝玉回说:“也倒不想什么吃,倒是那一回做的那小荷叶儿莲蓬儿的汤还好”。这个莲蓬汤倒不是什么山珍海味,只是做起来很麻烦,当年元妃省亲时做过一次。因为是给皇帝准备吃的,非同小可,既不能过于奢华,又要十分讲究。莲蓬是用银模子刻出来的,库房的人把模子找出来后,薛姨妈看到后说:“你们府上都想绝了,吃碗汤还有这些样子。若不说出来,我见这个也不认得这是作什么用的”。薛姨妈也是大户人家,就连她都没见过这么精细的模子,可想而知贾家在饮食上有多么讲究了。
相较于富贵过好几代的家族,暴发户是不知道怎么吃的,以为大鱼大肉就叫吃了。富贵人家吃的其实并不是山珍海味,他们讲究的是做工的细腻,到最后就变成了文化。
除了在吃上,贾家在穿戴上也是非常讲究,举几个例子:贾母的软烟罗、平儿的虾须镯、宝玉的雀金裘、湘云的凫魇裘……。
通过上边这些内容,我想引出一个更普世的观点:没有钱的人永远无法想象有钱人过的是什么样的生活,平时会使用什么样的东西,就像段子中皇帝的金锄头一样。
下边用几个我使用过的稍微好一点的物品举例,这些物品价格确实会稍贵一点,但也不是什么奢侈品,限于我目前的人生阅历也只能用这些来说明了。
一个戴森吹风机3000多,普通家庭是绝对不会买的。我们家几年前一直在用其他品牌的吹风机,也没感觉有什么问题,后来我们帮一个保险销售介绍客户,完成了很多任务,作为奖励她送了我们一台戴森吹风机,自从用上以后就再也用不惯其他吹风机了。
前一阵子搬家,我把戴森拿到了新家使用,因为我爸妈还要在之前的房子住,那边需要一个新吹风机。我看到这两年一个国产的品牌「徕芬」吹风机很火,外形也和戴森很像,就买了一个给他们用。前两天我回家用了一次徕芬,实话实说,如果我之前没有用过戴森,我一定觉得这个吹风机非常好用,但用过了更好的对比之下才知道还有很大差距。
传统的门锁都会外露一个弹簧的探头,用来在关门时将门卡住。探头上下两个角很尖,不注意时会磕碰到人,家里有小孩的情况下,如果小孩正好跟门锁差不多高,在跑来跑去时会更危险。日常关门时,因为探头要和门框上的凹槽摩擦,还会有很大的噪音。因为探头存在阻力,在关这种门时,通常是用手把门把手转到下边,再去将门关严,或者需要很用力地去关。
装修新房时,才知道现在已经有了无形的锁具,门在开启状态时探头是不会外漏的,只有将门关闭后探头才会弹出,避免了磕碰还更静音了。想把门关严时也不用捉着把手去关了,直接推门就可以。我没有研究它的原理,猜测是用了磁铁之类的。
另一个和装修有关的是新家里的淋浴设备和零冷水燃气热水器。在我没有用新的花洒之前,没觉得之前用过的花洒有什么问题,用过之后觉得之前花洒水量太小了。前两天再去用之前的就觉得身上的沫子半天才能冲干净,新的淋浴一瞬间就冲完了。
还有支持恒温的零冷水燃气热水器,如果没有接触过,我真的不知道洗澡水居然可以不需要等待,每次打开直接出热水,温度也是之前设置好的恒定温度,完全不用担心忽冷忽热的问题。
上个周末和家人去吃了一次比格自助,79一位。如果家庭条件一般,自助吃的比较少的话,就会觉得比格很不错了,当然比格在这个价位里也确实不错。但如果吃过更好的,就会知道比格的食材还差很多。
其实我也没什么资格评判比格,因为我吃的比较多的也是比格或者比格这个价位的自助,只是在公司团建的时候有幸吃过其他稍微高档一些的,比如第六季、水木锦堂之类的。但次数有限,那些更高级的,上千块的自助还没有体验过。
我现在只开过 20w 以内的车,已经觉得很好了。50w 以上的车还没有开过,更别提百万级别的豪车了。我相信我现在一定无法想象出开豪车的体验和惊喜。
如果我以后有机会能开上,再来更新使用体验😂
]]>最后定位到是苹果搞的 AWDL 引起的,AWDL 全称:Apple Wireless Direct Link 苹果无线直连,用于 AirDrop、AirPlay 和其他服务的低延迟高速率 WIFI 点对点传输功能。苹果为它提供了独立的网络接口,可以通过 ifconfig awdl0 看到其状态。
苹果的操作内核为1个 WiFi Broadcom 硬件芯片提供了多个 WiFi 接口:
通过拥有多个接口,我们的电脑就能够在 en0 上建立标准 WiFi 连接,同时在 awdl0 上广播、浏览和解析点对点连接。
这导致的问题是信号不稳定,只要 AWDL 处于活动状态,它就会持续在后台探测附近的其他设备,在使用时会短暂干扰 WiFi 运作,在目前无线网络连接和 AWDL 频道直接来回切换。猜测在公司时问题更严重是因为公司的无线AP 比较多,导致的干扰也就更强。
在网上查找解决方案的时候发现 Apple 芯片的 Mac 更容易出这个问题,比如 M1、M2。
我前边提到的工具WiFriedX实际上就是通过关闭 AWDL 来解决网络不稳定的问题,但我发现它关闭的并不是那么彻底,关闭一段时间后,又在后台被其他进程开启。
我通过手动的方式关闭 awdl0 网卡:
1 | sudo ifconfig awdl0 down |
在刚执行完后查询状态时,确实改为了 inactive,过了一会发现又变回了 active。查资料说的是如果本地启动了 AirDrop,AWDL 将立即重新启用;Bonjour discovery 还将每隔几分钟重新启用一次 AWDL。
感谢开源社区,已经有其他人发现了这个 AWDL 的坑,并且也想长期关闭它,于是写了脚本来在后台持续监听这块网卡的状态并将其关闭。
核心代码如下:
1 | #!/usr/bin/env bash |
这段逻辑会每秒钟检测一次 awdl0 网卡状态,如果是开启就进行关闭。
运行这段代码可以达到永久关闭 awdl0 网卡的效果,但是如果是我们每次手动运行它会比较麻烦,每次重启电脑后还要记得再次运行。于是大神们继续封装,将这个代码在系统后代常驻运行,重启时也会自动启动。
通过下边这个命令,可以把上边的脚本放在后台服务中一直执行,同时跟随系统启动:
1 | curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash |
关闭后会影响 AirDrop 功能,如果想用手机给电脑投个文件或者照片之类的就很不方便。
如果要恢复 AWDL 可以使用下边的命令:
1 | curl -s https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/cleanup-and-reenable-awdl.sh | bash &> /dev/null |
在 shell 的 rc 文件中配置两个 alias,就可以实现快捷键一键开启和关闭 AWDL 功能了:
1 | alias awdldown='curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash' |
",'
'+j.at+" , ")}for(var i in _)if(_.hasOwnProperty(i)){var o=_[i];n.set(i,o)}n.setACL(R()),n.save().then(function(t){"Anonymous"!=_.nick&&A.default.store.set(h.MetaCacheKey,{nick:_.nick,link:_.link,mail:_.mail});var n=e.$el.find(".vnum");try{j.rid?S(t,(0,A.default)('.vquote[data-self-id="'+j.rid+'"]'),!0):(Number(n.text())?n.text(Number(n.text())+1):e.$el.find(".vcount").show().find(".vnum").text(Number(n.text())+1),S(t,e.$el.find(".vcards")),w.skip++),z.removeAttr("disabled"),e.$loading.hide(),e.reset()}catch(t){(0,x.default)(e,t,"save")}}).catch(function(t){(0,x.default)(e,t,"commitEvt")})};z.on("click",P),(0,A.default)(document).on("keydown",function(e){e=event||e;var t=e.keyCode||e.which||e.charCode;((e.ctrlKey||e.metaKey)&&13===t&&P(),9===t)&&("veditor"==(document.activeElement.id||"")&&(e.preventDefault(),D(o[0]," ")))}).on("paste",function(e){var t="clipboardData"in e?e.clipboardData:e.originalEvent&&e.originalEvent.clipboardData||window.clipboardData;t&&L(t.items,!0)}),o.on("dragenter dragleave dragover drop",function(e){e.stopPropagation(),e.preventDefault(),"drop"===e.type&&L(e.dataTransfer.items)});var L=function(e,t){for(var n=[],r=0,i=e.length;r]+>/g,""))});else if(-1!==a.type.indexOf("image")){n.push(a.getAsFile());continue}}N(n)},N=function t(n,r){r=r||0;var i=n.length;if(i>0){var a=n[r];z.attr({disabled:!0});var u="![Uploading "+a.name+"...]("+r+")";D(o[0],u),U(a,function(s){500!=s.code?(o.val(o.val().replace(u,"!["+a.name+"]("+s.data.url+")\r\n")),(0,l.default)(o[0]),++r2?o=!!AV.applicationId&&!!AV.applicationKey:i.default.deleteInWin("AV",0)}o?t&&t():i.default.sdkLoader("//unpkg.com/leancloud-storage@3/dist/av-min.js","AV",function(n){var r="https://",i="",a=e.app_id||e.appId,u=e.app_key||e.appKey;if(!e.serverURLs)switch(a.slice(-9)){case"-9Nh9j0Va":r+="tab.";break;case"-MdYXbMMI":r+="us."}i=e.serverURLs||r+"leancloud.cn",AV.init({appId:a,appKey:u,serverURLs:i}),o=!0,t&&t()})}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(95),o=r(i),a=n(111),u=r(a),s=n(112),l=r(s),c=n(109),f=r(c),p=n(110),d=r(p),h={zh:u.default,"zh-cn":u.default,"zh-CN":u.default,"zh-TW":l.default,en:f.default,"en-US":f.default,ja:d.default,"ja-JP":d.default};t.default=function(e,t){return!h[e]&&e&&t&&(h[e]=t),new o.default({phrases:h[e||"zh"],locale:e})}},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){if(e.$el&&e.$loading.hide().$nodata.hide(),"[object Error]"==={}.toString.call(t)){var n=t.code||t.message||t.error||"";if(isNaN(n))e.$el&&e.$nodata.show(' An error occurred: "+e+" '+JSON.stringify(t)+"
");else{var r=e.i18n.t("code-"+n),i=(r=="code-"+n?void 0:r)||t.message||t.error||"";101==n||-1==n?e.$nodata.show():e.$el&&e.$nodata.show('Code '+n+": "+i+"
")}}else e.$el&&e.$nodata.show(''+JSON.stringify(t)+"
")}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(94),o=n(54),a=r(o),u=n(86),s=r(u),l=n(3),c=r(l),f=n(13),p=r(f),d=new i.marked.Renderer;d.code=function(e,t){return'
"},i.marked.setOptions({renderer:"hljs"in window?d:new i.marked.Renderer,highlight:function(e,t){return"hljs"in window?t&&hljs.getLanguage(t)&&hljs.highlight(t,e,!0).value||hljs.highlightAuto(e).value:(0,s.default)(e)},gfm:!0,tables:!0,breaks:!0,pedantic:!1,sanitize:!0,sanitizer:a.default,smartLists:!0,smartypants:!0,headerPrefi:"v-"}),t.default=function(e){return(0,i.marked)(p.default.parse(e,!0))}},function(e,t,n){"use strict";t.__esModule=!0,t.recordIPFn=t.fetchQQFn=void 0;var r=n(3),i=function(e){return e&&e.__esModule?e:{default:e}}(r),o=n(6),a=function(e,t){var n=i.default.store.get(o.QQCacheKey);n&&n.qq==e?t&&t(n):i.default.ajax({url:"//valine.api.ioliu.cn/getqqinfo",method:"POST",body:{qq:e}}).then(function(e){e.json().then(function(e){e.errmsg||(i.default.store.set(o.QQCacheKey,e),t&&t(e))})})},u=function(e){i.default.ajax({url:"//api.ip.sb/jsonip",method:"jsonp"}).then(function(t){e(t.ip)})};t.fetchQQFn=a,t.recordIPFn=u},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){if(!e)return"";try{var n=i(e).getTime(),o=(new Date).getTime(),a=o-n,u=Math.floor(a/864e5);if(0===u){var s=a%864e5,l=Math.floor(s/36e5);if(0===l){var c=s%36e5,f=Math.floor(c/6e4);if(0===f){var p=c%6e4;return Math.round(p/1e3)+" "+t.t("seconds")}return f+" "+t.t("minutes")}return l+" "+t.t("hours")}return u<0?t.t("now"):u<8?u+" "+t.t("days"):r(e)}catch(e){}};var r=function(e){var t=o(e.getDate(),2),n=o(e.getMonth()+1,2);return o(e.getFullYear(),2)+"-"+n+"-"+t},i=function e(t){return t instanceof Date?t:!isNaN(t)||/^\d+$/.test(t)?new Date(parseInt(t)):/GMT/.test(t||"")?e(new Date(t).getTime()):(t=(t||"").replace(/(^\s*)|(\s*$)/g,"").replace(/\.\d+/,"").replace(/-/,"/").replace(/-/,"/").replace(/(\d)T(\d)/,"$1 $2").replace(/Z/," UTC").replace(/([+-]\d\d):?(\d\d)/," $1$2"),new Date(t))},o=function(e,t){for(var n=e.toString();n.length'+(t&&hljs.getLanguage(t)?hljs.highlight(t,e).value:c.default.escape(e))+"
"+s(e.message+"",!0)+"
";throw e}}e.defaults=a();var k=/[&<>"']/,E=/[&<>"']/g,F=/[<>"']|&(?!#?\w+;)/,C=/[<>"']|&(?!#?\w+;)/g,S={"&":"&","<":"<",">":">",'"':""","'":"'"},_=function(e){return S[e]},O=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,B=/(^|[^\[])\^/g,j=/[^\w:]/g,$=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i,T={},I=/^[^:]+:\/*[^/]*$/,z=/^([^:]+:)[\s\S]*$/,P=/^([^:]+:\/*[^/]*)[\s\S]*$/,R={exec:function(){}},M=function(){function t(t){this.options=t||e.defaults}var n=t.prototype;return n.space=function(e){var t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}},n.code=function(e){var t=this.rules.block.code.exec(e);if(t){var n=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:v(n,"\n")}}},n.fences=function(e){var t=this.rules.block.fences.exec(e);if(t){var n=t[0],r=D(n,t[3]||"");return{type:"code",raw:n,lang:t[2]?t[2].trim():t[2],text:r}}},n.heading=function(e){var t=this.rules.block.heading.exec(e);if(t){var n=t[2].trim();if(/#$/.test(n)){var r=v(n,"#");this.options.pedantic?n=r.trim():r&&!/ $/.test(r)||(n=r.trim())}var i={type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:[]};return this.lexer.inline(i.text,i.tokens),i}},n.hr=function(e){var t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}},n.blockquote=function(e){var t=this.rules.block.blockquote.exec(e);if(t){var n=t[0].replace(/^ *> ?/gm,"");return{type:"blockquote",raw:t[0],tokens:this.lexer.blockTokens(n,[]),text:n}}},n.list=function(e){var t=this.rules.block.list.exec(e);if(t){var n,r,i,a,u,s,l,c,f,p,d,h,v=t[1].trim(),g=v.length>1,m={type:"list",raw:"",ordered:g,start:g?+v.slice(0,-1):"",loose:!1,items:[]};v=g?"\\d{1,9}\\"+v.slice(-1):"\\"+v,this.options.pedantic&&(v=g?v:"[*+-]");for(var y=new RegExp("^( {0,3}"+v+")((?: [^\\n]*)?(?:\\n|$))");e&&(h=!1,t=y.exec(e))&&!this.rules.block.hr.test(e);){if(n=t[0],e=e.substring(n.length),c=t[2].split("\n",1)[0],f=e.split("\n",1)[0],this.options.pedantic?(a=2,d=c.trimLeft()):(a=t[2].search(/[^ ]/),a=a>4?1:a,d=c.slice(a),a+=t[1].length),s=!1,!c&&/^ *$/.test(f)&&(n+=f+"\n",e=e.substring(f.length+1),h=!0),!h)for(var b=new RegExp("^ {0,"+Math.min(3,a-1)+"}(?:[*+-]|\\d{1,9}[.)])");e&&(p=e.split("\n",1)[0],c=p,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!b.test(c));){if(c.search(/[^ ]/)>=a||!c.trim())d+="\n"+c.slice(a);else{if(s)break;d+="\n"+c}s||c.trim()||(s=!0),n+=p+"\n",e=e.substring(p.length+1)}m.loose||(l?m.loose=!0:/\n *\n *$/.test(n)&&(l=!0)),this.options.gfm&&(r=/^\[[ xX]\] /.exec(d))&&(i="[ ] "!==r[0],d=d.replace(/^\[[ xX]\] +/,"")),m.items.push({type:"list_item",raw:n,task:!!r,checked:i,loose:!1,text:d}),m.raw+=n}m.items[m.items.length-1].raw=n.trimRight(),m.items[m.items.length-1].text=d.trimRight(),m.raw=m.raw.trimRight();var D=m.items.length;for(u=0;u
\n":"'+(n?e:s(e,!0))+"
\n"},n.blockquote=function(e){return""+(n?e:s(e,!0))+"
\n"+e+"
\n"},n.html=function(e){return e},n.heading=function(e,t,n,r){return this.options.headerIds?"
\n":"
\n"},n.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+""+r+">\n"},n.listitem=function(e){return"\n\n"+e+"\n"+t+"
\n"},n.tablerow=function(e){return"\n"+e+" \n"},n.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+""+n+">\n"},n.strong=function(e){return""+e+""},n.em=function(e){return""+e+""},n.codespan=function(e){return""+e+"
"},n.br=function(){return this.options.xhtml?"
":"
"},n.del=function(e){return""+e+""},n.link=function(e,t,n){if(null===(e=f(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"+n+""},n.image=function(e,t,n){if(null===(e=f(this.options.sanitize,this.options.baseUrl,e)))return n;var r='":">"},n.text=function(e){return e},t}(),q=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,n){return""+n},t.image=function(e,t,n){return""+n},t.br=function(){return""},e}(),W=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var n=e,r=0;if(this.seen.hasOwnProperty(n)){r=this.seen[e];do{r++,n=e+"-"+r}while(this.seen.hasOwnProperty(n))}return t||(this.seen[e]=r,this.seen[n]=0),n},t.slug=function(e,t){void 0===t&&(t={});var n=this.serialize(e);return this.getNextSafeSlug(n,t.dryrun)},e}(),V=function(){function t(t){this.options=t||e.defaults,this.options.renderer=this.options.renderer||new Q,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new q,this.slugger=new W}t.parse=function(e,n){return new t(n).parse(e)},t.parseInline=function(e,n){return new t(n).parseInline(e)};var n=t.prototype;return n.parse=function(e,t){void 0===t&&(t=!0);var n,r,i,o,a,u,s,c,f,p,d,h,v,g,m,y,b,D,x,w="",A=e.length;for(n=0;n0&&"paragraph"===m.tokens[0].type?(m.tokens[0].text=D+" "+m.tokens[0].text,m.tokens[0].tokens&&m.tokens[0].tokens.length>0&&"text"===m.tokens[0].tokens[0].type&&(m.tokens[0].tokens[0].text=D+" "+m.tokens[0].tokens[0].text)):m.tokens.unshift({type:"text",text:D}):g+=D),g+=this.parse(m.tokens,v),f+=this.renderer.listitem(g,b,y);w+=this.renderer.list(f,d,h);continue;case"html":w+=this.renderer.html(p.text);continue;case"paragraph":w+=this.renderer.paragraph(this.parseInline(p.tokens));continue;case"text":for(f=p.tokens?this.parseInline(p.tokens):p.text;n+1An error occurred:
"+s(e.message+"",!0)+"";throw e}},A.Parser=V,A.parser=V.parse,A.Renderer=Q,A.TextRenderer=q,A.Lexer=U,A.lexer=U.lex,A.Tokenizer=M,A.Slugger=W,A.parse=A;var H=A.options,K=A.setOptions,G=A.use,Z=A.walkTokens,J=A.parseInline,X=A,Y=V.parse,ee=U.lex;e.Lexer=U,e.Parser=V,e.Renderer=Q,e.Slugger=W,e.TextRenderer=q,e.Tokenizer=M,e.getDefaults=a,e.lexer=ee,e.marked=A,e.options=H,e.parse=X,e.parseInline=J,e.parser=Y,e.setOptions=K,e.use=G,e.walkTokens=Z,Object.defineProperty(e,"__esModule",{value:!0})})},function(e,t,n){"use strict";function r(e){var t={};return c(f(e),function(e){var n=e[0],r=e[1];c(r,function(e){t[e]=n})}),t}function i(e,t){var n=r(e.pluralTypeToLanguages);return n[t]||n[m.call(t,/-/,1)[0]]||n.en}function o(e,t,n){return e.pluralTypes[t](n)}function a(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function u(e){var t=e&&e.prefix||"%{",n=e&&e.suffix||"}";if(t===y||n===y)throw new RangeError('"'+y+'" token is reserved for pluralization');return new RegExp(a(t)+"(.*?)"+a(n),"g")}function s(e,t,n,r,i){if("string"!=typeof e)throw new TypeError("Polyglot.transformPhrase expects argument #1 to be string");if(null==t)return e;var a=e,u=r||w,s="number"==typeof t?{smart_count:t}:t;if(null!=s.smart_count&&e){var l=i||D,c=m.call(e,y),f=n||"en",p=x(l,f),v=o(l,p,s.smart_count);a=h(c[v]||c[0])}return a=g.call(a,u,function(e,t){return d(s,t)&&null!=s[t]?s[t]:e})}function l(e){var t=e||{};this.phrases={},this.extend(t.phrases||{}),this.currentLocale=t.locale||"en";var n=t.allowMissing?s:null;this.onMissingKey="function"==typeof t.onMissingKey?t.onMissingKey:n,this.warn=t.warn||v,this.tokenRegex=u(t.interpolation),this.pluralRules=t.pluralRules||D}var c=n(48),f=n(100),p=n(105),d=n(25),h=n(103),v=function(e){p(!1,e)},g=String.prototype.replace,m=String.prototype.split,y="||||",b=function(e){var t=e%100,n=t%10;return 11!==t&&1===n?0:2<=n&&n<=4&&!(t>=12&&t<=14)?1:2},D={pluralTypes:{arabic:function(e){if(e<3)return e;var t=e%100;return t>=3&&t<=10?3:t>=11?4:5},bosnian_serbian:b,chinese:function(){return 0},croatian:b,french:function(e){return e>=2?1:0},german:function(e){return 1!==e?1:0},russian:b,lithuanian:function(e){return e%10==1&&e%100!=11?0:e%10>=2&&e%10<=9&&(e%100<11||e%100>19)?1:2},czech:function(e){return 1===e?0:e>=2&&e<=4?1:2},polish:function(e){if(1===e)return 0;var t=e%10;return 2<=t&&t<=4&&(e%100<10||e%100>=20)?1:2},icelandic:function(e){return e%10!=1||e%100==11?1:0},slovenian:function(e){var t=e%100;return 1===t?0:2===t?1:3===t||4===t?2:3}},pluralTypeToLanguages:{arabic:["ar"],bosnian_serbian:["bs-Latn-BA","bs-Cyrl-BA","srl-RS","sr-RS"],chinese:["id","id-ID","ja","ko","ko-KR","lo","ms","th","th-TH","zh"],croatian:["hr","hr-HR"],german:["fa","da","de","en","es","fi","el","he","hi-IN","hu","hu-HU","it","nl","no","pt","sv","tr"],french:["fr","tl","pt-br"],russian:["ru","ru-RU"],lithuanian:["lt"],czech:["cs","cs-CZ","sk"],polish:["pl"],icelandic:["is"],slovenian:["sl-SL"]}},x=function(){var e={};return function(t,n){var r=e[n];return r&&!t.pluralTypes[r]&&(r=null,e[n]=r),r||(r=i(t,n))&&(e[n]=r),r}}(),w=/%\{(.*?)\}/g;l.prototype.locale=function(e){return e&&(this.currentLocale=e),this.currentLocale},l.prototype.extend=function(e,t){c(f(e||{}),function(e){var n=e[0],r=e[1],i=t?t+"."+n:n;"object"==typeof r?this.extend(r,i):this.phrases[i]=r},this)},l.prototype.unset=function(e,t){"string"==typeof e?delete this.phrases[e]:c(f(e||{}),function(e){var n=e[0],r=e[1],i=t?t+"."+n:n;"object"==typeof r?this.unset(r,i):delete this.phrases[i]},this)},l.prototype.clear=function(){this.phrases={}},l.prototype.replace=function(e){this.clear(),this.extend(e)},l.prototype.t=function(e,t){var n,r,i=null==t?{}:t;if("string"==typeof this.phrases[e])n=this.phrases[e];else if("string"==typeof i._)n=i._;else if(this.onMissingKey){var o=this.onMissingKey;r=o(e,i,this.currentLocale,this.tokenRegex,this.pluralRules)}else this.warn('Missing translation for key: "'+e+'"'),r=e;return"string"==typeof n&&(r=s(n,i,this.currentLocale,this.tokenRegex,this.pluralRules)),r},l.prototype.has=function(e){return d(this.phrases,e)},l.transformPhrase=function(e,t,n){return s(e,t,n)},e.exports=l},function(e,t,n){"use strict";function r(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}/* +object-assign +(c) Sindre Sorhus +@license MIT +*/ +var i=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,u,s=r(e),l=1;l
${highlightKeyword(content, slice)}...
`; + }); + + resultItem += '最近刚好听到一句话「学霸两支笔,差生文具多」,我觉得里边的差生形容我再适合不过了。我总是喜欢以「磨刀不误砍柴工」、「工具善其事必先利其器」的“借口”去找寻那些(可能)有用、能提高我效率的五花八门的工具,我日常用到的工具太多了,下边盘点几个我在今年上半年入手并且用认为好用的几个新工具,包括 Native 工具、Web 工具和 Chrome 插件。
\n我平时喜欢听一些内容,比如蒋勋讲解的红楼梦,有时候听到好的段落想把那些部分记录下来,但又不想一个字一个字地手敲,所以一直在找一款好用的语音转文字的工具,最后被一个叫《 组织进化论 》的播客节目安利了飞书 App 里的「飞书妙记」的功能,这里提一句「组织进化论」也是字节孵化的一档节目。
\n我为了试用这个功能而下载了飞书,给我惊喜的是真的非常好用(包括飞书和飞书妙记),不仅支持导入音频文件转文字还支持实时录制的音频转文字,如果是多个人对话的话,可以自动帮我们把多个说话人区分出来。这就不止可以让我从有声书中提取内容了,它还可以让我更轻松地记录会议内容:好记性不如烂笔头,开会的时候打开录音模式,会后「妙记」就帮我把逐字稿生成了,参考着逐字稿写会议纪要就不会漏掉任何重要内容了。我还尝试使用在面试的场合,比如下图是我近期面的一个候选人,可以给我在后期写面评时提供抓手。
\n\n妙记还可以把对话中的关键词帮我们提取出来:
\n\n最最重要的是,它还是免费的,我之前用过几个收费的,比如科大讯飞,不管是价格还是可操作性方面都被飞书妙记吊打,但是有一说一正确性方面相比科大讯飞,妙记还是有一定提升空间的。
\nMenubarX 是一款非常实用的 macOS 小工具,它可以让你在菜单栏固定任何网页,供随时使用,就像原生 App 一样,我们可以将常用网站放在菜单栏使用。网友们已经把这个工具玩出花了,有在里边刷 Twitter、Ins 的,有用来看行情的甚至还有用来养鱼的,真成了个摸鱼 APP。
\n我自己最常用的就是用来看日历,用了好多日历 APP 都感觉不尽人意,比如有些不支持农历显示、有些不支持节假日显示,有些每周第一天是周日而且无法修改等等。在没有 MenubarX 之前,我每次在项目排期或者其他原因需要看日历时,都是手动在浏览器打开 https://wannianrili.bmcx.com/ ,这是我所找到的最简洁、实用的日历界面,但无奈没有 APP。
\n有了 MenubarX 问题就解决了,我直接将它固定到了我的顶部菜单栏,很方便就能唤出,而且可以设置独立的快捷键。因为 MenubarX 的窗口可以模拟成手机的尺寸和 UserAgent,所以日历页面就更简洁了,PC 版右侧的黄历移到了下边,这样就可以一目了然看到我最关注的日期部分了。
\n\n下载地址:https://apps.apple.com/app/id1575588022
\n我是在作者刚刚上架的时候开始的,那时候解锁 Pro 版限免,现在貌似是 30RMB。
\nImage Smith 这是个图片压缩工具,压缩后的图片体积可以减少非常多,而且压缩后的图片质量我们肉眼看不出差别(反正我是看不出来)。
\n我有时会把手机拍的照片放到博客中,比如这篇:https://jiapan.me/2022/da-guan-yuan/,原图一张十几 MB,压缩后可以到几 MB,这样能节省一半多的空间和带宽,而且效果上没有差别。
\n\n下载地址:https://apps.apple.com/app/id1623828135
\n这个工具也是我在作者刚上架限免时候下载的,现在卖 30RMB。类似的图片压缩工具还有一些在线版本,不想花钱买的可以试试:
\n为了用户体验和性能,我在有 Native 版本的情况下就不用 Web 版的,所以上边那几个在线版本我没有深入使用和对比过,都是之前收藏的以备不时之需,我按照网站的颜值排了序,大家可以自己测评一下看看哪个更好用。
\n这个工具可以帮我在使用不同的软件时自动切换到不同的输入法,避免我们因切换输入法而打断思路,比如在用开发工具(如 IDEA、GoLand、WebStorm)时切换到英文,在使用钉钉、微信这类聊天工具时切换到百度输入法,甚至支持在浏览不同的网站时使用不同的输入法。还可以做到自动记录上一次在某个软件中使用的输入法,下次再切回这个软件时自动切换为上一次使用的输入法。
\n\n每次从一个软件换到另一个软件时,Input Source Pro 都会贴心的提醒我当前在用(后这切换后)的输入法是哪个,但这个功能有个不好的地方,会在我要截图时给我带来困扰,每次我都要等 3 秒,等那个输入法悬浮提示消失后才能截图。
\n这个工具的同样想法我之前也想到过,而且还和朋友讨论过,无奈吃了不会 macOS 开发的亏。既然有人做了,而且做的还不错,那咱也就没必要再惦记着造轮子了。
\n官方地址:https://inputsource.pro/zh-CN
\nZenUML 是一款用伪代码画时序图的在线工具,画出来的时序图简洁漂亮,左边写代码右侧的时序图实时生成,所见即所得,而且可以直接导出 PNG 或者 JPG 格式的图片,登录后还可以将作品进行保存。我最近用它画了好多图,经常在做完方案介绍后被人问到图是用什么软件画的。
\n\n而且这还个良心工具,除非你想为信仰充值升级到 Pro 版,它们的唯一区别是 Pro 版没有代码行数的限制。免费版代码限制多少行我并不是很清楚,我之前画过几个很复杂的流程图都没有触发限制,说明我完全没有升级 Pro 版的必要。
\n\n\n类似工具还有:
\n\nripgrep 是一个支持递归和正则且性能超强的文本搜索命令行工具,类似于系统自带的 grep,但是甩 grep 几十条街。
\nMac 安装:
\nbrew install ripgrep |
虽然它叫 ribgrep,为了让用户更方便使用,它在命令行中输入 rg
就可以使用了。我会把所有公司的项目放到同一个目录下,有次一个同事问我是否知道某个类型的消息是哪个服务发出的,我通过这个工具快速给了他答案:
ripgrep 是开源、用 Rust 编写的:https://github.com/BurntSushi/ripgrep
\ntldr 是 too long; didn’t read 的缩写,tldr 这个工具也是出于同样的目的,告诉我们某个命令行的最常用、最实用的用法。我们在用某个命令时,如果看它的 help 可能会看到巨多无比的参数,从网上搜又会很费时,这时候 tldr 就派上用场了。
\n比如以上边刚刚介绍的 rg
为例,我想知道它的常用功能都有那些,如果用 rg --help
参数会多到直接让我放弃,看下 tldr 的效果:
~ took 5s ➜ tldr rg |
tldr 用一句话给我们描述了 rg 的功能,并给出了官方地址。下边还列了一些常见用法,比如不加任何参数可以递归查询当前目录下所有文件。rg --type filetype regular_expression
可以指定要查询的文件类型,等等。是不是比官方手册实用很多而且省去了到网上查一圈的麻烦。
还有很多小伙伴经常忘记 tar
的用法,也可以用 tldr 来做个快速回顾:
~ ➜ tldr tar |
Mac 安装:
\nbrew info tldr |
tldr 也是个开源项目,Github 地址:https://github.com/tldr-pages/tldr
\nGitToolBox 是个 IDE 插件,这个插件可以直接让我们看到光标所在代码行的提交信息(提交人, 提交时间, CommitMessage),不用再通过侧边栏的 Annotate with Git Blame
来查看了,很方便。
Relingo 是个浏览器插件,可以在阅读英语文章时自动标记出那些我们可能生疏单词的解释,当我们认识某个单词后鼠标悬浮到对应单词然后在弹出框上打个勾,之后就不会再标记这个单词了,还可以对已经掌握的单词进行回顾,是个阅读英语文章和学习英语的不错工具。
\n\n官方地址:https://relingo.net/zh/index
\nLanguage Reactor 也是个浏览器插件,可以让我们在 Youtube 看英语视频时通过字幕学习遇到的句子或单词。打开视频后会自动在视频右侧加载出字幕列表,可以直接点击某行字幕将视频进度跳转到我们想看的那句话,在视频中鼠标悬浮在某个单词上后视频会自动暂停播放,然后弹出这个单词的解释,鼠标移开后自动开始播放。
\n\nRelingo 也带类似功能,但是术业有专攻,我觉得 Reactor 做的更好一些,所以如果两个插件都装了的话,需要手动把 Relingo 的字幕功能关闭。
\n\n官方地址:https://www.languagereactor.com/
Chrome 安装地址:https://chrome.google.com/webstore/detail/language-reactor/hoombieeljmmljlkjmnheibnpciblicm?hl=zh-CN
上边的工具大部分都是我通过 Twitter 发现的,而且有几个是刚刚一出来我就开始用的,具体可以看下我这篇文章:https://jiapan.me/2022/what-i-access-to-information/
\n"},{"title":"2023年清明节无题","url":"/2023/2023-Qingming-Festival/","content":"现在时间是2023年4月5日,清明节,凌晨2点,失眠。
\n在清明节前一天有位亲属去世了,需要回老家来奔丧。
\n上午10点半得知的消息,先从公司乘地铁回家,由于离家太远,12点多才到家,实属无奈。
\n简单收拾了些衣物开车上路,一路上都在下雨,时而大雨,时而小雨。中间在服务器休息+充电一小时,下午4点到的老家。
\n亲人是突然去世的,去世前没有经历过太多痛苦,也没有拖累家人长期的照料,也算是喜丧吧。
\n回来跟家里人闲聊过程中,得知小时候和我经常一起玩的哥哥,因为赌博现在已经倾家荡产了,把周围七大姑八大姨的钱借了个遍,甚至刷爆很多张信用卡来拆东墙补西墙,去年也找我借了几万块钱,说是生意周转使用,过一段时间就还,但迟迟没有还。现在还背了很多高利贷,身体也非常糟糕,本来已经有了轻生一走了之的念头,多亏家里人发现的及时,和他做了些心理工作,让他给那些借了钱的人挨个道歉,说明错误,并打下欠条,后边通过变卖家产去一点点偿还。
\n久赌无胜家。
\n最后用AI帮我补充的句子来结尾:
\n\n\n"},{"title":"关于成年人的30个生存技巧","url":"/2023/30-Survival-Tips-for-Adults/","content":"在这样的节日里,我们不仅要缅怀已故亲人,也要珍惜眼前人,因为生命短暂而珍贵。同时,也要引以为戒赌博的危害,要远离赌博,珍爱自己和家人的幸福生活。
\n
在分布式系统中错误是不可能避免的,我们在分布式系统中,能做的不是避免错误,而是要把错误的处理当成功能写在代码中。
\n"},{"title":"《人类简史》摘抄","url":"/2021/A-brief-history-of-humankind/","content":"我们从农业革命能学到的最重要一课,很可能就是物种演化上的成功并不代表个体的幸福。
\n就演化而言,牛可能是有史以来最成功的动物。但同时,它们也是地球上生活最悲惨的动物。
\n在农业革命之后,人类成了远比过去更以自我为中心的生物,与“自己家”紧密相连,但与周遭其他物种画出界线。
\n历史只告诉了我们极少数的人在做些什么,而其他绝大多数人的生活就是不停挑水耕田。
\n史上的场场战争和革命,多半起因都不是粮食短缺。
\n虚构故事的力量强过任何人的想象。
\n大多数的人类合作网络最后都成了压迫和剥削。
\n支持它们的社会规范既不是人类自然的天性本能,也不是人际的交流关系,而是他们都相信着共同的虚构神话故事。
\n人人生而平等,造物者赋予他们若干不可剥夺的权利,其中包括生命权、自由权和追求幸福的权利。
\n演化的基础是差异,而不是平等。每个人身上带的基因码都有些许不同,而且从出生以后就接受着不同的环境影响,发展出不同的特质,导致不同的生存概率。“生而平等”其实该是“演化各有不同”。
\n个体诞生的背后就只是盲目的演化过程,而没有任何目的。所以“造物者赋予”其实就只是“出生”。
\n“自由”就像是“平等”、“权利”和“有限公司”,不过是人类发明的概念,也只存在于人类的想象之中。
\n我们相信某种秩序,并非因为它是客观的现实,而是因为相信它可以让人提升合作效率、打造更美好的社会。
\n伏尔泰就曾说:“世界上本来就没有神,但可别告诉我的仆人,免得他半夜偷偷把我宰了。”
\n如果不是大多数中国人都相信仁义礼智信,儒家思想绝不可能持续了两千多年。如果不是大多数的美国总统和国会议员都相信人权,美国的民主也不可能持续了250年。如果不是广大的投资人和银行家都相信资本主义,现代经济体系连一天也不可能继续存在。
\n而要怎样才能让人相信这些秩序?
\n有三大原因,让人类不会发现组织自己生活的种种秩序其实是想象:
\n旅游业真正卖的可不是机票和饭店房间,而是旅游中的经验。
\n想象建构的秩序并非个人主观的想象,而是存在于主体之间(inter-subjective),存在于千千万万人共同的想象之中。
\n“客观”、“主观”和“主体间”的不同:
\n“主体间”事物的存在,靠的是许多个人主观意识之间的连接网络。
\n为了改变现有由想象建构出的秩序,就得先用想象建构出另一套秩序才行。
\n身为人类,我们不可能脱离想象所建构出的秩序。
\n智人的社会秩序是通过想象建构,维持秩序所需的关键信息无法单纯靠DNA复制就传给后代,需要通过各种努力,才能维持种种法律、习俗、程序、礼仪,否则社会秩序很快就会崩溃。
\n人类的大脑并不是个很好的储存设备,主要原因有三。
\n演化压力让人类的大脑善于储存大量关于动植物、地形和社会的信息。
\n然而在农业革命之后,社会开始变得格外复杂,另一种全新的信息类型也变得至关重要:数字。
\n虽然这些符号现在被称为“阿拉伯数字”,但其实是印度人发明的。
\n之所以现在我们会称“阿拉伯数字”,是因为阿拉伯人攻打印度时发现了这套实用的系统,再加以改良传到中东,进而传入欧洲。
\n文字是采用实体符号来储存信息的方式。
\n文字对人类历史所造成最重要的影响:它逐渐改变了人类思维和看待这个世界的方式。
\n人类创造出了由想象建构的秩序、发明了文字,以这两者补足我们基因中的不足。
\n历史的铁则告诉我们,每一种由想象建构出来的秩序,都绝不会承认自己出于想象和虚构,而会大谈自己是自然、必然的结果
\n根据著名的婆罗门教神话,诸神是以原人普罗沙(Purusa)的身体创造这个世界:他的眼睛化成太阳,他的大脑化成月亮,他的口化成了婆罗门(祭司),他的手化成了刹帝力(贵族、武士),他的大腿化成了吠舍(农民和商人等平民),而他的小腿则化成了首陀罗(仆人)。
\n国古代的《风俗通》也记载,女娲开天辟地的时候要造人,一开始用黄土仔细捏,但后来没有时间余力,便用绳子泡在泥里再拉起来,飞起的泥点也化成一个一个的人,于是“富贵者,黄土人;贫贱者,引绳人也”
\n这些阶级区别不过全都是人类想象的产品罢了。
\n就目前学者研究,还没有任何一个大型人类社会能真正免除歧视的情形。
\n人类要让社会有秩序的方法,就是会将成员分成各种想象出来的阶级
\n让某些人在法律上、政治上或社会上高人一等,从而规范了数百万人的关系。
\n有了阶级之后,陌生人不用浪费时间和精力真正了解彼此,也能知道该如何对待对方。
\n就算身处不同阶级的人发展出了完全一样的能力,因为他们面对的游戏规则不同,最后结果也可能天差地别。
\n这些阶级制度开始时多半只是因为历史上的偶发意外,但部分群体取得既得利益之后,世世代代不断加以延续改良,才形成现在的样子。
\n纵观历史,几乎所有社会都会以“污染”和“洁净”的概念来做出许多社会及政治上的区隔,而且各个统治阶级利用这些概念来维系其特权也是不遗余力。
\n非洲人在基因上的优势(免疫力)竟造成了他们在社会上的劣势:正因为他们比欧洲人更能适应热带气候,反让他们成了遭到欧洲主人蹂躏的奴隶
\n“黑人”成了一种印记,人们觉得他们天生就不可靠、懒惰,而且愚笨。
\n随着时间推移,这些偏见只会越来越深。正由于所有最好的工作都在白人手上,人们更容易相信黑人确实低人一等。
\n恶性循环:某个偶然历史事件,成了僵化的社会制度常规。
\n随着时间流逝,不公不义的歧视常常只是加剧而不是改善。富者越富,而贫者越贫。教育带来进一步的教育,而无知只会造成进一步的无知
\n不同的社会,想象出的阶级制度也就相当不同。
\n有某种阶级制度却是在所有已知的人类社会里都有着极高的重要性:性别的阶级。
\n现在人体的所有器官早在几亿年前就已经出现了原型,而现在所有器官都不只做着原型所做的事。
\n各种规定男人就该如何、女人就该怎样的法律、规范、权利和义务,反映的多半只是人类的想象,而不是生物天生的现实。
\n生物上,人类分为男性和女性。所谓男性(male),就是拥有一个X染色体和一个Y染色体,所谓女性(female)则是拥有两个X染色体。
\n要说某个人算不算“男人”(man)或“女人”(woman),讲的就是社会学而不是生物学的概念了。
\n强壮分了许多种,像是女人一般来说比男人更能抵抗饥饿、疾病和疲劳,而且也有许多女人能跑得比男人更快,挑得比男人更多。
\n人类历史显示,肌肉的力量和社会的权力还往往是呈反比。在大多数社会中,体力好的反而干的是下层的活。
\n在智人内部的权力链里,聪明才智及社交技巧也会比体力更重要。
\n常常军队的领导人从没当过一天兵,只因为他们是贵族、富人或受过教育,高级将领的荣耀也就落在他们头上。
\n战争可不是什么单纯的酒吧打架,需要非常复杂的组织、合作和安抚手段。真正胜利的关键,常常是能够同时安内攘外,并看穿他人思维(尤其是敌国的思维)。
\n父权制度其实并没有生物学上的基础,而只是基于毫无根据的虚构概念。
\n人类几乎从出生到死亡都被种种虚构的故事和概念围绕,让他们以特定的方式思考,以特定的标准行事,想要特定的东西,也遵守特定的规范。
\n虽然每种文化都有代表性的信仰、规范和价值,但会不断流动改变。只要环境或邻近的文化改变,文化就会有所改变及因应,文化内部也会自己形成一股改变的动力。
\n正如中世纪无法解决骑士精神和基督教的矛盾,现代社会也无法解决自由和平等的冲突。
\n就像两个不谐和音可以让音乐往前进,人类不同的想法、概念和价值观也能逼着我们思考、批评、重新评价。一切要求一致,反而让心灵呆滞。
\n一般认为认知失调是人类心理上的一种问题,但这其实是一项重要的特性,如果人真的无法同时拥有互相抵触的信念和价值观,很可能所有的文化都将无从建立,也无以为继。
\n几千年来,我们看到规模小而简单的各种文化逐渐融入较大、较复杂的文明中,于是世界上的大型文化数量逐渐减少,但规模及复杂程度远胜昨日。
\n合久必分只是一时,分久必合才是不变的大趋势。
\n世界上没有什么社会性动物会在意所属物种的整体权益。
\n钱让我们能够快速、方便地比较不同事物的价值(例如苹果、鞋子甚至离婚这件事),让我们能够轻松交换这些事物,也让我们容易累积财富。
\n“人人都想要”正是金钱最基本的特性。人人都想要钱,是因为其他人也都想要钱,所以有钱就几乎可以换到所有东西
\n理想的金钱类型不只能用来交换物品,还能用来累积财富。
\n正因为有了金钱概念,财富的转换、储存和运送都变得更容易也更便宜,后来才能发展出复杂的商业网络以及蓬勃的市场经济
\n不管是贝壳还是美元,它们的价值都只存在于我们共同的想象之中。
\n金钱并不是物质上的现实,而只是心理上的想象。所以,金钱的运作就是要把前者转变为后者。
\n金钱正是有史以来最普遍也最有效的互信系统。
\n真正要用的时候,白银和黄金只会做成首饰、皇冠以及各种象征地位的物品;换言之,都是在特定文化里社会地位高的人所拥有的奢侈品。它们的价值完全只是因为文化赋予而来。
\n大约在公元前640年,土耳其西部吕底亚(Lydia)王国的国王阿耶特斯(Alyattes)铸造出史上第一批硬币
\n硬币上的印记代表着某些政治权力,能够确保硬币的价值。
\n就算是在宗教上水火不容的基督徒和穆斯林,也可以在金钱制度上达成同样的信仰。原因就在于宗教信仰的重点是自己相信,但金钱信仰的重点是“别人相信”
\n所有人类创造的信念系统之中,只有金钱能够跨越几乎所有文化鸿沟,不会因为宗教、性别、种族、年龄或性取向而有所歧视。也多亏有了金钱制度,才让人就算互不相识、不清楚对方人品,也能携手合作。
\n金钱制度有两大原则:
\n金钱还有更黑暗的一面。虽然金钱能建立起陌生人之间共通的信任,但人们信任的不是人类、社群或某些神圣的价值观,而只是金钱本身以及背后那套没有人性的系统。
\n多数过去的文化,早晚都是遭到某些无情帝国军队的蹂躏,最后在历史上彻底遭到遗忘。
\n在21世纪,几乎所有人的祖先都曾经属于某个帝国。
\n帝国是一种政治秩序,有两项重要特征:
\n这里要特别强调,帝国的定义就只在于文化多元性和疆界灵活性两项,至于起源、政府形式、领土范围或人口规模则并非重点。并不是一定要有军事征服才能有帝国。
\n帝国正是造成民族多样性大幅减少的主因之一。
\n帝国的标准配备,常常就包括战争、奴役、驱逐和种族屠杀。
\n帝国四处征服、掠夺财富之后,不只是拿来养活军队、兴建堡垒,同时也赞助了哲学、艺术、司法和公益。现在人类之所以有许多文化成就,常常背后靠的就是剥削战败者。
\n智人本能上就会将人类分成“我们”和“他们”。所谓的“我们”,有共同的语言、宗教和习俗,我们对彼此负责,但“他们”就不干我们的事。“
\n西方认为所谓公义的世界应该是由各个独立的民族国家组成,但古代中国的概念却正好相反,认为政治分裂的时代不仅动荡不安,而且公义不行。
\n至于现代许多的美国人,他们也认为美国必须负起道义责任,让第三世界国家同样享有民主和人权。
\n中国的帝国大计执行得更为成功彻底。中国地区原本有许许多多不同的族群和文化,全部统称为蛮族,但经过两千年之后,已经成功统合到中国文化,都成了中国的汉族(以公元前206年到公元220年的汉朝为名)。
\n现今的文化又有大多数都是帝国的遗绪。
\n历史就是无法简单分成好人和坏人两种。自己常常就是跟着走坏人的路。
\n但在金钱和帝国之外,宗教正是第三种让人类统一的力量
\n在历史上,宗教的重要性就在于让这些脆弱的架构有了超人类的合法性。
\n宗教是“一种人类规范及价值观的系统,建立在超人类的秩序之上”。
\n宗教认为世界有一种超人类的秩序,而且并非出于人类的想象或是协议。以这种超人类的秩序为基础,宗教会发展出它认为具有约束力的规范和价值观。
\n某个宗教如果想要将幅员广阔、族群各异的人群都收归旗下,就还必须具备另外两种特质。第一,它信奉的超人类秩序必须普世皆同,不论时空而永恒为真。第二,它还必须坚定地将这种信念传播给大众。换句话说,宗教必须同时具备“普世特质”和“推广特质”。
\n农业革命开始,宗教革命便随之而来。
\n农业革命最初的宗教意义,就是让动植物从与人类平等的生物,变成了人类的所有物。
\n很多古代神话其实就是一种法律契约,人类承诺要永远崇敬某些神灵,换取人类对其他动植物的控制权。
\n真正让多神论与一神论不同的观点,在于多神论认为主宰世界的最高权力不带有任何私心或偏见,因此对于人类各种世俗的欲望、担心和忧虑毫不在意。
\n二元论宗教信奉着善与恶这两种对立力量的存在。二元论与一神论不同之处在于,他们相信“恶”也是独立存在,既不是由代表“善”的神所创造,也不归神所掌管。二元论认为,整个宇宙就是这两股力量的战场,世间种种就是两方斗争的体现。
\n诺斯替教和摩尼教认为,善神创造了精神和灵魂,而恶神创造了物质和身体。根据这种观点,人就成了善的灵魂和恶的身体之间的战场。
\n基督徒大致上是信奉一神论的上帝,相信二元论的魔鬼,崇拜多神论的圣人,还相信泛神论的鬼魂。
\n像这样同时有着不同甚至矛盾的思想,而又结合各种不同来源的仪式和做法,宗教学上有一个特别的名称:综摄(syncretism)。很有可能,综摄才是全球最大的单一宗教。
\n佛陀的教诲一言以蔽之:痛苦来自欲望;要从痛苦中解脱,就要放下欲望;而要放下欲望,就必须训练心智,体验事物的本质。
\n自由人文主义追求的,是尽可能为个人争取更多自由;而社会人文主义追求的,则是让所有人都能平等。
\n纳粹并不是反人性。他们之所以同自由人文主义、人权和共产主义站在对立面,反而正是因为他们推崇人性,相信人类有巨大的潜力。
\n我们刚刚踏入第三个千禧年,演化人文主义的未来仍未可知。
\n越来越多科学家认为,决定人类行为的不是什么自由意志,而是荷尔蒙、基因和神经突触——我们和黑猩猩、狼和蚂蚁并无不同。
\n商业、帝国和全球性的宗教,最后终于将几乎每个智人都纳入了我们今天的全球世界。
\n历史的铁则就是:事后看来无可避免的事,在当时看来总是毫不明显。
\n混沌系统分成两级:
\n究竟为什么要学历史?历史不像是物理学或经济学,目的不在于做出准确预测。我们之所以研究历史,不是为了要知道未来,而是要拓展视野,要了解现在的种种绝非“自然”,也并非无可避免。未来的可能性远超过我们的想象。
\n历史的选择绝不是为了人类的利益。
\n迷因学假设,就像是生物演化是基于“基因”这种有机信息单位的复制,文化演化则是基于“迷因”(meme)这种文化信息单位的复制。
\n\n\n模因,又译媒因、觅母、米姆、弥等。目前比较公认的定义是通过模仿在人与人之间传播的思想、行为或风格,通常是为了传达模因所代表的特定现象、主题或意义。
\n
现代科学与先前的知识体系有三大不同之处:
\n科学革命并不是“知识的革命”,而是“无知的革命”。
\n对“知识”的考验,不在于究竟是否真实,而在于是否能让人类得到力量或权力。
\n许多的科学研究和科技发展,正是由军事所发起、资助及引导
\n就目前所知,火药的发明其实是一场意外,原本的目的是道士想炼出长生不老药来。
\n纵观历史,社会上有两种贫穷:
\n许多社会现在的问题是营养过剩,胖死比饿死的概率更高。
\n人类所有看来无法解决的问题里,有一项最为令人烦恼、有趣且重要:死亡。
\n当时最聪明的人才,想的是如何给死亡赋予意义,而不是逃避死亡。
\n人之所以会死,可不是什么神的旨意,而是因为各种技术问题,像是心脏病,像是癌症,像是感染。而每个技术问题,都可以找到技术性的解决方案。
\n现在所有最优秀的人才可不是浪费时间为死亡赋予意义,而是忙着研究各种与疾病及老化相关的生理、荷尔蒙和基因系统。
\n科学革命的一大计划目标,就是要给予人类永恒的生命。
\n唯一一个让死亡仍然占据核心的现代意识形态就是民族主义。在那些绝望到极点但又同时充满诗意的时刻,民族主义就会向人承诺,就算你牺牲了生命,但你会永远活在国家整体的永恒记忆里。只不过,这项承诺实在太虚无缥缈,恐怕大多数民族主义者也不知道这究竟说的是什么意思。
\n科学活动并不是处于某个更高的道德和精神层面,而是也像其他的文化活动一样,受到经济、政治和宗教利益的影响。
\n现代科学之所以能在过去500年间取得如同奇迹般的成果,有很大程度必须归功于政府、企业、基金会和私人捐助者愿意为此投入数十亿美元的经费。
\n科学研究之所以能得到经费,多半是因为有人认为这些研究有助于达到某些政治、经济或宗教的目的。
\n真正控制科学发展进度表的,也很少是科学家。
\n科学并无力决定自己的优先级,也无法决定如何使用其发现。
\n科学研究一定得和某些宗教或意识形态联手,才有蓬勃发展的可能。意识形态能够让研究所耗的成本合理化。
\n在过去500年间,科学、帝国和资本之间的回馈循环无疑正是推动历史演进的主要引擎。
\n虽然我们常常不愿意承认,但现在全球所有人的穿着、想法和品位几乎就都是欧洲人的穿着、想法和品位。
\n中国和波斯其实并不缺乏制作蒸汽机的科技(当时要照抄或是购买都完全不成问题),他们缺少的是西方的价值观、故事、司法系统和社会政治结构,这些在西方花了数个世纪才形成及成熟,就算想要照抄,也无法在一夕之间内化。
\n欧洲帝国主义之所以要前往遥远的彼岸,除了为了新领土,也是为了新知识。
\n在15、16世纪,欧洲人的世界地图开始出现大片空白。从这点可以看出科学心态的发展,以及欧洲帝国主义的动机。
\n误以为发现美洲的人是亚美利哥·韦斯普奇,因此为了向他致敬,这片大陆就被命名为“America”(美洲)。
\n绝大多数的大帝国向外侵略只着眼于邻近地区,之所以最后幅员广大,只是因为帝国不断向邻近地区扩张而已。
\n虽然偶尔会有某个雄心勃勃的统治者或冒险家,展开长途的征讨或探险,但通常都是顺着早已成形的帝国道路或商业路线。。
\n郑和下西洋得以证明,当时欧洲并未占有科技上的优势。真正让欧洲人胜出的,是他们无与伦比而又贪得无厌、不断希望探索和征服的野心。
\n所有的非欧洲政权中,第一个派出军事远征队前往美洲的是日本。
\n现代科学和现代帝国背后的动力都是一种不满足,觉得在远方一定还有什么重要的事物,等着他们去探索、去掌握。
\n科学能够从思想上让帝国合理化。
\n正因为帝国与科学密切合作,就让它们有了如此强大的力量,能让整个世界大为改观;也是因为如此,我们很难简单断言它们究竟是善是恶。正是帝国创造了我们所认识的世界,而且,其中还包含我们用以判断世界的意识形态。
\n最早的梵语母语民族是在大约3000年前、从中亚入侵印度,他们自称为“雅利亚”(Arya)。而最早的波斯语母语者则自称为“艾利亚”(Airiia)。
\n对今日许多精英分子而言,要比较判断不同人群的优劣,几乎讲的总是历史上的文化差异,而不再是种族上的生物差异。
\n不论是科学还是帝国,它们能够迅速崛起,背后都还潜藏着一股特别重要的力量:资本主义。
\n真正让银行(以及整个经济)得以存活甚至大发利市的,其实是我们对未来的信任。“信任”就是世上绝大多数金钱的唯一后盾。
\n人类发展出“信用”这种金钱概念,代表着目前还不存在、只存在于想象中的货品。
\n现代经济的奇妙循环:
\n民间企业的获利正是社会整体财富和繁荣的基础。
\n所谓的“资本主义”(Capitalism),认为“资本”(capital)与“财富”(wealth)有所不同。
\n资本主义的影响范围逐渐超越了单纯的经济领域,现在它还成了一套伦理,告诉我们该有怎样的行为,该如何教育孩子,甚至该如何思考问题。
\n资本主义认为经济可以无穷无尽地发展下去,但这和我们日常生活观察到的宇宙现象完全背道而驰。
\n印钞票的是银行和政府,但最后埋单的是科学家。
\n欧洲人征服世界的过程中,所需资金来源从税收逐渐转为信贷,而且也逐渐改由资本家主导,一切的目标就是要让投资取得最高的报酬。
\n为了掌控哈德孙河这个重要商业通道,西印度公司在河口的一座小岛上开拓了一个殖民地,名为“新阿姆斯特丹”(New Amsterdam)。这个殖民地不断遭受美国原住民威胁,英国人也多次入侵,最后在1664年落入英国手中。英国人将这个城市改名“纽约”(New York,即“新约克”,约克为英国郡名)。当时西印度公司曾在殖民地筑起一道墙,用来抵御英国人和美国原住民,这道墙的位置现在成了世界上最著名的街道:华尔街(Wall Street,直译为“墙街”)。
\n在1717年,密西西比河下游河谷其实大约只有沼泽和鳄鱼,但密西西比公司却是撒着漫天大谎,把这个地方描述得金银遍地、无限商机。许多法国贵族、商人和城市里那些冷漠的中产阶级都信了这套谎言,于是密西西比公司股价一飞冲天。公司上市的股价是每股500里弗(livre)。1719年8月1日,股价涨到每股2750里弗。8月30日,股价已经飙升到每股4100里弗;9月4日升上每股5000里弗。等到12月2日,密西西比公司的股价每股超过10000里弗大关。当时,整个巴黎街头洋溢着一种幸福感。民众卖掉了自己所有的财产,借了大笔的金钱,只为了能够购买密西西比公司的股票。每个人都相信自己找到了快速致富的捷径。
\n密西西比泡沫可以说是史上最惨烈的一次金融崩溃。法国王室的金融体系一直没能真正走出这场重大的打击。
\n至于打下印度次大陆的,同样也不是英国官方,而是英国东印度公司的佣兵。这家公司的成就甚至比荷兰东印度公司更加辉煌
\n一直要到1858年,英国王室才将印度及英国东印度公司的军队收编国有
\n这世界上根本不可能有完全不受政治影响的市场。毕竟,经济最重要的资源就是“信任”,而信任这种东西总是得面对种种的坑蒙拐骗。光靠着市场本身,并无法避免诈欺、窃盗和暴力的行为。这些事得由政治系统下手,立法禁止欺诈,并用警察、法庭和监狱来执行法律。
\n论听来十分完美,但实际上却是漏洞百出。如果真的是完全自由的市场,没有国王或神职人员来监督,贪婪的资本家就能够通过垄断或串通来打击劳工
\n如果真的是完全自由的市场,没有国王或神职人员来监督,贪婪的资本家就能够通过垄断或串通来打击劳工。
\n这是自由市场资本主义美中不足之处。它无法保证利润会以公平的方式取得或是以公平的方式分配。
\n人类的历史从来不是洁白无邪,随着现代经济成长,全球各地还有无数的大小罪恶和灾难正在上演。
\n就像农业革命一样,所谓的现代经济成长也可能只是个巨大的骗局。虽然人类和全球经济看来都在继续成长,但更多的人却活在饥饿和困乏之中。
\n每次即将因为能源或原料短缺而使经济成长趋缓的时候,就会有资金投入科学研究,解决这项问题。这种做法屡屡奏效,有时候让人更有效利用现有资源,有时候找出了全新的能源和材料。
\n过去可能会有人认为,像这样大规模使用资源,很快就会耗尽所有能源和原料,很快只能靠着回收垃圾撑下去了。然而,实际状况却正好相反。在1700年,全球运输工具使用的原料多半是木材和铁,但今天我们却有各式各样的新材料任君挑选,像是塑料、橡胶、铝和钛,这一切我们的祖先都完全一无所知。另外,1700年的马车主要是由木匠和铁匠手工人力制作,但在现在的丰田车厂和波音公司工厂里,我们靠的是燃油引擎和核电厂来推动生产。类似的革命在几乎所有产业领域无处不在。我们将它称为“工业革命”
\n人类历史在过去一直是由两大周期来主导:植物的生长周期,以及太阳能的变化周期。
\n工业革命的核心,其实就是能源转换的革命。
\n我们能使用的能源其实无穷无尽。讲得更精确,唯一的限制只在于我们的无知。
\n在地心引力下将一颗小苹果抬升一米,所需的能量就是一焦耳;
\n工业革命最重要的一点,其实在于它就是第二次的农业革命。
\n正是因为农业释放出了数十亿的人力,由工厂和办公室吸纳,才开始像雪崩一样有各种新产品倾泻而出。
\n消费主义的美德就是消费更多的产品和服务,鼓励所有人应该善待自己、宠爱自己,就算因为过度消费而慢慢走上绝路,也是在所不惜。
\n购物已成为人类最喜爱的消遣,而且消费性产品也成了家人、朋友、配偶之间不可或缺的中介。各种宗教节日(例如圣诞节)都已经成了购物节。
\n肥胖这件事,可以说是消费主义的双重胜利。
\n资本主义和消费主义的伦理可以说是一枚硬币的正反两面,将这两种秩序合而为一。
\n人类能用的资源其实不断增加,而且这个趋势很可能还会继续。
\n与中世纪农民和鞋匠相比,现代工业对太阳或季节可说是完全不在乎,更重视的是要追求精确和一致。
\n1847年,英国各家火车业者齐聚一堂,研拟同意统一协调所有火车时刻表,一概以格林尼治天文台的时间为准,而不再遵循利物浦、曼彻斯特、格拉斯哥或任何其他城市的当地时间。在火车业者开了头之后,越来越多机构跟进这股风潮。最后在1880年,英国政府迈出了前所未有的一步,立法规定全英国的时刻表都必须以格林尼治时间为准。
\n直到现在,新闻广播开头的第一条仍然是现在时间,就算战争爆发也得放在后面再报。
\n一般人每天会看上几十次时间,原因就在于现代似乎一切都得按时完成。
\n很多时候,王国和帝国就像是收着保护费的黑道集团。国王就是黑道大哥,收了保护费就得罩着自己的人民,不受附近其他黑道集团或当地小混混骚扰。除此之外,其实也没什么功用。
\n年轻人越来越不需要听从长辈的意见,而一旦孩子的人生出了任何问题,似乎看来总是可以怪在父母头上。
\n现代所兴起的两大想象社群,就是“民族”和“消费大众”。
\n消费主义和民族主义可说是夙夜匪懈,努力说服我们自己和其他数百万人是一伙的,认为我们有共同的过去、共同的利益以及共同的未来。
\n民族竭尽全力,希望能掩盖自己属于想象的这件事。大多数民族都会声称自己的形成是自然而然、天长地久,说自己是在最初的原生时代,由这片祖国土地和人民的鲜血紧密结合而成。但这通常就是个夸大其词的说法。虽然民族确实有悠久的源头,但因为早期“国家”的角色并不那么重要,所以民族的概念也无关痛痒。
\n现有的民族多半是到了工业革命后才出现。
\n狄更斯写到法国大革命,就说“这是最好的年代,也是最坏的年代”
\n虽然可能会有某些小规模边界冲突,但现在除非发生了某个世界末日等级的事件,否则几乎不可能再次爆发传统的全面战争。
\n如果说有个最高诺贝尔和平奖,应该把奖颁给罗伯特·奥本海默以及和他一起研发出原子弹的同事。有了核武器之后,超级大国之间如果再开战,无异等于集体自杀。
\n现在有四大因素形成了一个良性循环。
\n现在正面临着全球帝国的形成。而这个帝国与之前的帝国也十分类似,会努力维持其疆域内的和平。正因为全球帝国的疆域就是全世界,所以世界和平也就能得到有效的维持。
\n就算是都市中产阶级,过着舒适的生活,生活中却再也没有什么比得上狩猎采集者猎到长毛象那种兴奋和纯粹的快乐。
\n然智人确实取得了空前的成就,或许值得沾沾自喜,但代价就是赔上几乎所有其他动物的命运。
\n金钱确实会带来快乐,但是有一定限度,超过限度之后的效果就不那么明显。
\n另一项有趣的发现是疾病会短期降低人的幸福感,但除非病情不断恶化,或是症状带有持续、让人无力的疼痛,否则疾病并不会造成长期的不快。
\n对快乐与否的影响,家庭和社群要比金钱和健康来得重要。
\n多项重复研究发现,婚姻美好与感觉快乐,以及婚姻不协调与感觉痛苦,分别都呈现高度相关。
\n快乐并不在于任何像是财富、健康甚至社群之类的客观条件,而在于客观条件和主观期望之间是否相符。
\n重要的是要知足,而不是一直想要得到更多。
\n在我们试着猜测或想象其他人有多快乐的时候(可能是现在或过去的人),我们总是想要设身处地去想想自己在那个情况下会如何感受。但这么一来,我们是把自己的期望放到了别人的物质条件上,结果当然就会失准。
\n如果说快乐要由期望来决定,那么我们社会的两大支柱(大众媒体和广告业)很有可能正在不知不觉地让全球越来越不开心。
\n有没有可能,第三世界国家之所以会对生活不满,不只是因为贫穷、疾病、腐败和政治压迫,也是因为他们看到了第一世界国家的生活标准?
\n纵观历史,穷人和受压迫者之所以还能自我安慰,就是因为死亡是唯一完全公平的事。
\n人类演化的结果,就是不会太快乐,也不会太痛苦。我们会短暂感受到快感,但不会永远持续。迟早快感会消退,让我们再次感受到痛苦。
\n演化就把快感当成奖赏,鼓励男性和女性发生性行为、将自己的基因传下去。如果性交没有高潮,大概很多男性就不会那么热衷。但同时,演化也确保高潮得迅速退去。如果性高潮永续不退,可以想象男性会非常开心,但连觅食的动力都没了,最后死于饥饿,而且也不会有兴趣再去找下一位能够繁衍后代的女性。
\n人类的生化机制就像是个恒温空调系统。
\n已婚的人比单身和离婚的人更快乐,但这不一定代表是婚姻带来了快乐,也有可能是快乐带来了婚姻。
\n那些生化机制天生开朗的人,一般来说都会是快乐和满足的。而这样的人会是比较理想的另一半,所以他们结婚的概率也比较高。
\n快乐不只是“愉快的时刻多于痛苦的时刻”这么简单。相反,快乐要看的是某人生命的整体;生命整体有意义、有价值,就能得到快乐。
\n只要有了活下去的理由,几乎什么都能够忍受。
\n从我们所知的纯粹科学角度来看,人类的生命本来就完全没有意义。人类只是在没有特定目标的演化过程中,盲目产生的结果。
\n我们对生活所赋予的任何意义,其实都只是错觉。
\n所谓的快乐,很可能只是让个人对意义的错觉和现行的集体错觉达成同步而已。只要我自己的想法能和身边的人的想法达成一致,我就能说服自己、觉得自己的生命有意义,而且也能从这个信念中得到快乐。
\n奉若圭臬:比喻把某些言论或事当成自己的准则。
\n自由主义政治的基本想法,是认为选民个人最知道好坏,我们没有必要由政府老大哥来告诉人民何者为善、何者为恶。
\n佛教认为,快乐既不是主观感受到愉悦,也不是主观觉得生命有意义,反而是在于放下追求主观感受这件事。
\n人想要离苦得乐,就必须了解自己所有的主观感受都只是一瞬间的波动,而且别再追求某种感受。
\n苦真正的来源不在于感受本身,而是对感受的不断追求。
\n在所有目前进行的研究当中,最革命性的就是要建构一个直接的大脑–计算机双向接口,让计算机能够读取人脑的电子信号,并且同时输回人脑能够了解的电子信号
\n我们这个现代晚期的世界,是有史以来第一次认为所有人类应享有基本上的平等,然而我们可能正准备要打造出一个最不平等的社会。
\n我们真正应该认真以对的,是在于下一段历史改变不仅是关于科技和组织的改变,更是人类意识与身份认同的根本改变。
\n拥有神的能力,但是不负责任、贪得无厌,而且连想要什么都不知道。天下危险,莫此为甚。
\n我的电脑在公司使用无线网络时经常性断网,为了有稳定的网络我在工位时经常接根网线,使用网线连接。之前公司运维给了个叫 WiFriedX 的工具来解决这个问题,最近发现问题又出现了,开会时断网非常耽误事,所以就又着手开始排查。
\n最后定位到是苹果搞的 AWDL 引起的,AWDL 全称:Apple Wireless Direct Link 苹果无线直连,用于 AirDrop、AirPlay 和其他服务的低延迟高速率 WIFI 点对点传输功能。苹果为它提供了独立的网络接口,可以通过 ifconfig awdl0 看到其状态。
\n\n苹果的操作内核为1个 WiFi Broadcom 硬件芯片提供了多个 WiFi 接口:
\n通过拥有多个接口,我们的电脑就能够在 en0 上建立标准 WiFi 连接,同时在 awdl0 上广播、浏览和解析点对点连接。
\n这导致的问题是信号不稳定,只要 AWDL 处于活动状态,它就会持续在后台探测附近的其他设备,在使用时会短暂干扰 WiFi 运作,在目前无线网络连接和 AWDL 频道直接来回切换。猜测在公司时问题更严重是因为公司的无线AP 比较多,导致的干扰也就更强。
\n在网上查找解决方案的时候发现 Apple 芯片的 Mac 更容易出这个问题,比如 M1、M2。
\n我前边提到的工具WiFriedX实际上就是通过关闭 AWDL 来解决网络不稳定的问题,但我发现它关闭的并不是那么彻底,关闭一段时间后,又在后台被其他进程开启。
\n我通过手动的方式关闭 awdl0 网卡:
\nsudo ifconfig awdl0 down |
在刚执行完后查询状态时,确实改为了 inactive,过了一会发现又变回了 active。查资料说的是如果本地启动了 AirDrop,AWDL 将立即重新启用;Bonjour discovery 还将每隔几分钟重新启用一次 AWDL。
\n\n感谢开源社区,已经有其他人发现了这个 AWDL 的坑,并且也想长期关闭它,于是写了脚本来在后台持续监听这块网卡的状态并将其关闭。
\n核心代码如下:
\n#!/usr/bin/env bash |
这段逻辑会每秒钟检测一次 awdl0 网卡状态,如果是开启就进行关闭。
\n运行这段代码可以达到永久关闭 awdl0 网卡的效果,但是如果是我们每次手动运行它会比较麻烦,每次重启电脑后还要记得再次运行。于是大神们继续封装,将这个代码在系统后代常驻运行,重启时也会自动启动。
\n通过下边这个命令,可以把上边的脚本放在后台服务中一直执行,同时跟随系统启动:
\ncurl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash |
关闭后会影响 AirDrop 功能,如果想用手机给电脑投个文件或者照片之类的就很不方便。
\n如果要恢复 AWDL 可以使用下边的命令:
\ncurl -s https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/cleanup-and-reenable-awdl.sh | bash &> /dev/null |
在 shell 的 rc 文件中配置两个 alias,就可以实现快捷键一键开启和关闭 AWDL 功能了:
\nalias awdldown='curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash' |
今天看到亚马逊云服务的广告,说现在开通AWS可以免费用一年。然后我爱占小便宜心又犯了,所以绑定信用卡,开通了AWS,在这过程中,不知道为什么扣了我两笔6.59的钱,想联系客服也找不到人。。
\n进入AWS主页后看到有很多服务可以用,实在眼花缭乱,先一个一个来,我猜测EC2的意思就和阿里云主机是一个意思,所以就开通了一个,选择的是新加坡节点,果然开通时提示我可以免费用一年。既然这样的话,不如拿来搭一个ss服务器吧,哈哈哈~(因为其他暂时没想到做什么用,也许以后我会在这上边部署一个爬虫之类的)
\n在此过程中,让我下载了一个pem
格式的私钥文件,用来登录。
登录命令:ssh -i "aws-for-panmax.pem" ubuntu@ec2-54-169-92-35.ap-southeast-1.compute.amazonaws.com
进来之后,我使用sudo adduser panmax
创建了新的账户。
我ping了一下twitter,延迟200+,有些略失望~
\n\n更新和安装需要用到的包:
\nsudo apt-get update
sudo apt-get install nginx
sudo apt-get install mysql-client-5.5 mysql-server-5.5
sudo apt-get install php5 php5-fpm php5-cli php5-cgi php5-mysql php5-gd
以上这些如果没有报错,就证明安装成功了。安装mysql过程中需要创建root密码。
\n接下来创建数据库:
\nmysql -u root -p
输入安装mysql时设置的root密码。
创建shadowsocks数据库:
\ncreate database shadowsocks
然后建立一个名为ss,密码为ss的MySQL用户,因为这个用户只能本地登录,所以密码简单点也无所谓:
\ngrant all privileges on shadowsocks.* to ss@localhost identified by 'ss';
到这步,我们的数据库已经完成了,,下面我们来安装shadowsocks ss-panel supervisor,一次执行下面的命令:
\nsudo apt-get install python-pip git python-m2crypto
sudo pip install cymysql
git clone -b manyuser https://github.com/mengskysama/shadowsocks.git
cd shadowsocks/shadowsocks/
然后我们来修改配置文件/root/shadowsocks/shadowsocks/Config.py
\n#Config |
然后我们还要修改这个文件/root/shadowsocks/shadowsocks/config.json
\n{ |
然后我们来导入数据库。进入MySQL:
\nmysql -u root -p
use shadowsocks;
source ~/shadowsocks/shadowsocks/shadowsocks.sql;
exit
导入数据库之后,我们在shadowsocks目录下运行一下server.py,python server.py
没有error的话,ctrl + c结束进程,我们进行下一步,安装守护进程,这样重启以后或者程序崩了还能自己重启。
\nsudo apt-get install python-pip python-m2crypto supervisor
然后我们需要新建两个文件,具体如下:
\nsudo vim /etc/supervisor/conf.d/shadowsocks.conf
内容:
\n[program:shadowsocks] |
再创建一个文件:
\nsudo vim /etc/supervisor/conf.d/cgi.conf
内容:
\n[program:cgi] |
然后命令:
\ncd shadowsocks/shadowsocks
service supervisor start
supervisorctl reload
在以下两个文件/etc/profile和 /etc/default/supervisor结尾添加如下代码(/etc/default/supervisor不存在,直接sudo vi /etc/default/supervisor 即可):
\nulimit -n 51200 |
至此ss的后端服务已经搞定了,现在我们来整前端界面:
\ncd /usr/share/nginx/
wget -b v2 https://github.com/orvice/ss-panel/archive/master.zip
安装解压软件:
\nsudo apt-get install unzip
解压文件:
\nsudo unzip master.zip
然后重命名文件夹,
\nmv ss-panel-master ss
现在来修改文件夹权限,
\ncd /usr/share/nginx/
sudo chmod 777 * -R /usr/share/nginx/html
sudo chmod 777 * -R /usr/share/nginx/ss
sudo chown -R www-data:www-data /usr/share/nginx/html
sudo chown -R www-data:www-data /usr/share/nginx/ss
然后我们需要将ss-pane中的数据库导入我们刚刚创建的数据库中,还是进入MySQL:
\nmysql -u root -p
use shadowsocks;
source /usr/share/nginx/ss/sql/invite_code.sql;
然后我们需要将ss-pane中的数据库导入我们刚刚创建的数据库中,查看/usr/share/nginx/ss/sql下的内容,把里边的文件导入:
\n例如:
\nuse shadowsocks; |
然后我们来修改配置文件
\ncd /usr/share/nginx |
修改congfig.php
里边的数据库相关配置信息
到此,ss-panel前端界面也安装完毕,然后我们需要修改一下Nginx配置文件
\ncd /etc/nginx/sites-available/ |
修改为server {
listen 443;
server_name localhost;
server_name_in_redirect off;
root /usr/share/nginx/ss;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?q=$uri&$args;
}
location ~ \\.php$ {
include /etc/nginx/fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/ss$fastcgi_script_name;
}
}
然后重启一下
\nroot@ubuntu-512mb-sfo1-01:~# shutdown -r now |
完成。
\n/admin 进入管理员界面。
\n默认帐号:first@blood.com
\n默认密码:1993
\n绑定域名:
\n新增CNAME,主机记录ss,记录值为AWS EC2的公有 DNS
。
英文原文地址: Airbnb React/JSX Style Guide
\n\n\n用更合理的方式书写 React 和 JSX
\n
一个文件内只包含一个 React 组件。
\n总是使用 JSX 语法。
\nReact.createElement
,除非你从一个不是 JSX 的文件初始化你的应用。React.createClass
vs statelessclass extends React.Component
,除非你有一个非常好的理由要使用 mixins。 eslint: react/prefer-es6-class react/prefer-stateless-function// bad |
// bad |
// bad |
.jsx
React 组件的扩展名。ReservationCard.jsx
// bad |
ReservationCard.jsx
应该有一个 ReservationCard 的引用名称。 然而,如果是在目录中的组件, 应该使用 index.jsx
作为文件名并且使用目录名称作为组件名:// bad |
displayName
属性来命名组件,应该使用类的引用名称。// bad |
// bad |
JSX
的属性都采用双引号("
),其他的 JS
都使用单引号。eslint: jsx-quotes\n\n为什么这样做?
\nJSX
属性 不能包含转义的引号, 所以当输入"don't"
这类的缩写的时候用双引号会更方便。标准的 HTML 属性通常也会使用双引号替代单引号,所以 JSX 属性也会遵守这样的约定。
// bad |
// bad |
// bad |
true
的时候,忽略它的值。 eslint: react/jsx-boolean-value// bad |
// bad |
// bad |
// bad |
function ItemList(props) { |
\n\n为什么这样做? 在 render 方法中的 bind 调用每次调用 render 的时候都会创建一个全新的函数。
\n
// bad |
// bad |
static
方法constructor
getChildContext
componentWillMount
componentDidMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
componentDidUpdate
componentWillUnmount
onClickSubmit()
或者 onChangeDescription()
render
函数中的 getter 方法 比如 getSelectReason()
或者 getFooterContent()
renderNavigation()
或者 renderProfilePicture()
render
propTypes
, defaultProps
, contextTypes
等……import React, { PropTypes } from 'react'; |
React.createClass的排序:eslint: react/sort-comp
\ndisplayName
propTypes
contextTypes
childContextTypes
mixins
statics
defaultProps
getDefaultProps
getInitialState
getChildContext
componentWillMount
componentDidMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
componentDidUpdate
componentWillUnmount
onClickSubmit()
or onChangeDescription()
getSelectReason()
or getFooterContent()
renderNavigation()
or renderProfilePicture()
render
\n\n","categories":["翻译"],"tags":["React Native","JS"]},{"title":"App架构设计经验谈:接口的设计","url":"/2016/App%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%BB%8F%E9%AA%8C%E8%B0%88-%E6%8E%A5%E5%8F%A3%E7%9A%84%E8%AE%BE%E8%AE%A1/","content":"为什么? isMounted是一种反模式,当使用 ES6 类风格声明 React 组件时该属性不可用,并且即将被官方弃用。
\n
原文地址:http://keeganlee.me/post/architecture/20160107
\nApp与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉。
\n现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程是:
\n然而,此种验证方式存在一个安全性问题:当登录接口被劫持时,黑客就获取到了用户密码和token,后续则可以对该用户做任何事情了。用户只有修改密码才能夺回控制权。
\n如何优化呢?第一种解决方案是采用HTTPS。HTTPS在HTTP的基础上添加了SSL安全协议,自动对数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,安全性可以提高很多。不过,SSL也不是绝对安全的,也存在被劫持的可能。另外,服务器对HTTPS的配置相对有点复杂,还需要到CA申请证书,而且一般还是收费的。而且,HTTPS效率也比较低。一般,只有安全要求比较高的系统才会采用HTTPS,比如银行。而大部分对安全要求没那么高的App还是采用HTTP的方式。
\n我们目前的做法是给每个接口都添加签名。给客户端分配一个密钥,每次请求接口时,将密钥和所有参数组合成源串,根据签名算法生成签名值,发送请求时将签名一起发送给服务器验证。类似的实现可参考OAuth1.0的签名算法。这样,黑客不知道密钥,不知道签名算法,就算拦截到登录接口,后续请求也无法成功操作。不过,因为签名算法比较麻烦,而且容易出错,只适合对内的接口。如果你们的接口属于开放的API,则不太适合这种签名认证的方式了,建议还是使用OAuth2.0的认证机制。
\n我们也给每个端分配一个appKey,比如Android、iOS、微信三端,每个端分别分配一个appKey和一个密钥。没有传appKey的请求将报错,传错了appKey的请求也将报错。这样,安全性方面又加多了一层防御,同时也方便对不同端做一些不同的处理策略。
\n另外,现在越来越多App取消了密码登录,而采用手机号+短信验证码的登录方式,我在当前的项目中也采用了这种登录方式。这种登录方式有几种好处:
\n接口的数据一般都采用JSON格式进行传输,不过,需要注意的是,JSON的值只有六种数据类型:
\n所以,传输的数据类型不能超过这六种数据类型。以前,我们曾经试过传输Date类型,它会转为类似于”2016年1月7日 09时17分42秒 GMT+08:00”这样的字符串,这在转换时会产生问题,不同的解析库解析方式可能不同,有的可能会转乱,有的可能直接异常了。要避免出错,必须做特殊处理,自己手动去做解析。为了根除这种问题,最好的解决方案是用毫秒数表示日期。
\n另外,以前的项目中还出现过字符串的”true”和”false”,或者字符串的数字,甚至还出现过字符串的”null”,导致解析错误,尤其是”null”,导致App奔溃,后来查了好久才查出来是该问题导致的。这都是因为服务端对数据没处理好,导致有些数据转为了字符串。所以,在客户端,也不能完全信任服务端传回的数据都是对的,需要对所有异常情况都做相应处理。
\n服务器返回的数据结构,一般为:
\n{ |
不同错误需要定义不同的状态码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误。这里举几个例子:
\n错误信息一般有两种用途:一是客户端开发人员调试时看具体是什么错误;二是作为App错误提示直接展示给用户看。主要还是作为App错误提示,直接展示给用户看的。所以,大部分都是简短的提示信息。
\ndata字段只在请求成功时才会有数据返回的。数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。这里需要注意的就是,不要将data传入字符串或数字,即使请求需要的数据只有一个,比如token,那返回的data应该为:
\n// 正确 |
接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:
\n为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:
\n大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。
\n如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。
\n有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。
\n","categories":["转载"],"tags":["接口"]},{"title":"后端开发学习路径","url":"/2020/Backend-Developer-RoadMap/","content":"\n互联网 |
由于要内部使用,所以不需要配置 https 链接。
\n本文是基于 CAS 5.0.X 下进行的修改,修改方式如下:
\n我使用 overlay 的方式进行的部署,只需在 etc/cas/config/cas.properties
中配置如下三项即可
server.ssl.enabled=false |
但是现在还有一个问题时,几遍将 server.port
改为其他端口,http 的端口号也还是 8080,也就是说这里修改的 server.port
是修改的 https 的方式。
CPU 使用率 |
Counter
的 elements()
方法返回一个迭代器。元素被重复了多少次,在该迭代器中就包含多少个该元素。所有元素按照字母序排序,个数小于1的元素不被包含。
举例:
\n>>> c = Counter('ABCABC') |
源码如下:
\ndef elements(self): |
好!!!精!!!简!!!
\n从里往外看这行代码吧:
\n_starmap(_repeat, self.iteritems()) |
_starmap
是 itertools
模块中的一个实现了 __iter__
方法的类,构造器接收两个参数:一个函数(function)和一个序列(sequence),作用是创建一个迭代器,生成值function(*item),其中item来自sequence,只有当sequence生成的项适用于这种调用函数的方式时,此函数才有效。
itertools.starmap(function, iterable)
等价于:
def starmap(function, iterable): |
举例:
\nfrom itertools import starmap |
所以 _starmap(_repeat, self.iteritems())
等价于下边的代码:
for item in self.iteritems(): |
也就是返回一个迭代器,迭代器的每一项是使用item
解包作为参数来调用 _repeat
的结果。
下边再来看_repeat
:itertools.repeat(object[, times])
,同样也是实现了__iter__
方法的类,作用是创建一个迭代器,重复生成object,times(如果已提供)指定重复计数,如果未提供times,将无止尽返回该对象。
等价于:
\ndef repeat(object, times=None): |
举例:
\nfrom itertools import * |
repeat
很容易理解就不用解释了。
下边我们返回去看_starmap(_repeat, self.iteritems())
, 这些完这些,得到的结果是一个迭代器里边每一项依然是个迭代器,每个内层迭代器迭代出的结果是重复生成的项。
可以想象成这样:
\n>>> _starmap(_repeat, [{'A': 2, 'B': 3, 'C': 4}]) |
再来看一下chain
:itertools.chain(*iterables)
, 将多个迭代器作为参数, 但只返回单个迭代器, 它产生所有参数迭代器的内容, 就好像他们是来自于一个单一的序列。
等价于:
\ndef chain(*iterables): |
举例:
\nfor i in chain([1, 2, 3], ['a', 'b', 'c']): |
chain
的 类函数 from_iterable
可以理解成接收一个参数,然后将这个参数解包后调用构造器。
以上例子也可以写成:
\nfor i in chain.from_iterable([[1, 2, 3], ['a', 'b', 'c']]): |
所以,用 chain
来合并 _starmap(_repeat, self.iteritems())
得到的嵌套迭代器后得到的就是我们需要的结果了!
最后再次感叹下Python代码的精简!
\n更正前几篇中的出现过的一个错误:
\n字典调用 iteritems
方法得到的并不是一个列表,而是一个迭代器。
之前把 iteritems
一直当成 items
了。
>>> x = {'title':'python web site','url':'www.iplaypython.com'} |
这个方法可以传一个可选参数 n
, 代表获取数量最多的前 n
个元素。如果不传参数,则返回所有结果。
反回的结果是一个列表,里边的元素是一个元组,元组第0位是被计数的具体元素,元组第1位是出现的次数。如:[('a', 5), ('b', 4), ('c', 3)]
,当多个元素计数值相同时,按照字母序排列。
下边是 most_common
的源码:
def most_common(self, n=None): |
先来看n是None的情况,因为Counter类继承自dict,所以 self.iteritems
得到的是键值对元组的列表,用 sorted
对这个列表进行排序,因为是要按照元组的第1位的数字从大到小的顺序来排序,所以key应该是元组的第1位。代码中用 _itemgetter(1)
来取出元组的第1位,_itemgetter
是 operator
模块里的 itemgetter
类,这个类重写了 __call__
方法,所以这个类的实例可以当做函数来调用。 具体用法如下:
After f = itemgetter(2), the call f(r) returns r[2]. |
现在 key=itemgetter(1)
, 即 key(r)
就是 r[1]
这样就可以取到我们想要的那个值了,如果换作之前,我可能会重新定义一个函数,然后赋值给key,最多写一个lambda表达式: lambda x:x[1]
赋值给key,这些都是重造轮子的例子。。。不好不好。。。
此时我们实际要进行的是整数之间的比较,就不用再给 sorted
的 cmp
参数赋值了,因为我们要得到一个从大到小排列的结果,所以最后 reverse=True
如果 n
不为 None
,调用了 heapq
(最上边导入时将heapq
as 重命名成了 _heapq
) 模块中的 nlargest
函数,这个函数的实现有些略微复杂,等以后有时间再去看,直接看下函数的介绍:
Find the n largest elements in a dataset. |
这个函数的调用结果和用 sorted
排序后再取出前n个结果等价。
也就是 sorted(self.iteritems(), key=_itemgetter(1), reverse=True)[:n]
下一篇写Counter的elements()方法
\n","categories":["源码"],"tags":["源码","Python"]},{"title":"《关键对话》脑图","url":"/2020/Critical-Conversation-mind-map/","content":"\n为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。
\n从「心」开始 |
一直以为Python里两个datetime类型相减然后获取seconds拿到的是两个时间相差的总秒数,其实并不是。。。
\n正确的获取方法应该是用total_seconds()
\n示例:first_time = datetime.datetime(2013,11,10,11,11,11)
last_time = datetime.datetime(2014,11,10,11,11,11)
delta = last_time - first_time
print delta.total_seconds()
timedelta
相关的文档中这样写到:Only days, seconds and microseconds are stored internally. Arguments are converted to those units:
A millisecond is converted to 1000 microseconds.
A minute is converted to 60 seconds.
An hour is converted to 3600 seconds.
A week is converted to 7 days.
and days, seconds and microseconds are then normalized so that the representation is unique, with
0 <= microseconds < 1000000
0 <= seconds < 3600*24 (the number of seconds in one day)
-999999999 <= days <= 999999999
在Python2.7中加入了timedelta.total_seconds()
方法:
timedelta.total_seconds()
\n\n\nReturn the total number of seconds contained in the duration. Equivalent to (td.microseconds + (td.seconds + td.days 24 3600) * 106) / 106 computed with true division enabled.
\n
\n\nNote that for very large time intervals (greater than 270 years on most platforms) this method will lose microsecond accuracy.
\n
timedelta
官方示例:>>> from datetime import timedelta
>>> year = timedelta(days=365)
>>> another_year = timedelta(weeks=40, days=84, hours=23,
... minutes=50, seconds=600) # adds up to 365 days
>>> year.total_seconds()
31536000.0
>>> year == another_year
True
>>> ten_years = 10 * year
>>> ten_years, ten_years.days // 365
(datetime.timedelta(3650), 10)
>>> nine_years = ten_years - year
>>> nine_years, nine_years.days // 365
(datetime.timedelta(3285), 9)
>>> three_years = nine_years // 3;
>>> three_years, three_years.days // 365
(datetime.timedelta(1095), 3)
>>> abs(three_years - ten_years) == 2 * three_years + year
True
今天推荐一本我近期读到的质量很高的技术书(也可以说是我今年读到的最好的一本技术类书籍):《数据密集型应用系统设计》,属于「动物书」系列,封面是一只野猪。这本书我从上个月 18 号开始读,每天拿出一个半小时左右阅读,于昨天(12月28号)读完,刚好用了 40 天,全书 500 多页,也算是一本大部头了。
\n\n本书作者 Martin Kleppmann 是英国剑桥大学分布式系统方向的研究员。之前在 LinkedIn 和 Rapportive 等互联网公司做过软件工程师,负责大规模数据基础设施建设。在阅读过程中,我多次惊叹作者的知识面简直广得惊人,也善于举一反三,知识之间互相关联。
\n\n全书脉络清晰,分为三个部分:
\n第一部分介绍数据相关的基本思想,包括如何评价一个数据库(第一章),数据在逻辑上如何组织(第二章),在磁盘中如何分布(第三章),在表现上如何编码(第四章)。这些思想是一个数据系统的基本,无论它是单机的,还是分布式的。
\n第二部分介绍分布式环境下的技术,包括复制(第五章)、分区(第六章)、分布式事务与共识(第七、八、九章)。这些技术大多是基于同构系统的,分布式事务虽然也能在异构系统中应用,但是复杂度要高很多。
\n第三部分介绍异构系统中数据的处理技术,包括批处理(第十章)和流处理(第十一章),最后提出一种以流处理为主的异步数据处理方案,有可能在日后成为构建应用的主流方案(第十二章)。
\n作者在最后一小节还讨论了大数据的伦理问题,尽管在现实世界中、在金钱利益面前,可能无人理会这些事情,但是这些夫子自道,还是很体现作者情怀,可以说这也是全书升华的地方,同时让我对作者肃然起敬再次 +1。
\n书中把软件开发中(以后端为主)常用的技术本质、来龙去脉、使用场景、优点劣势都讲得非常清楚,并且讲解得深入浅出,把复杂的东西简单化,可见作者文笔之深厚。这一本书中囊括了几乎所有数据处理相关工作中可能遇到的内容,而且还提供了非常好的实操性。书中很多问题我在实际场景中也都遇到过,读起来使我醍醐灌顶、击节扼腕,每每读到我之前踩坑的地方都会想:如果我能早点读到这本书能少走很多弯路。
\n书中的配图也很到位,大部分是流程图,有时候文字读不懂的地方,看到配图就会明白,我贴几张图感受一下:
\nETL 介绍:
\n\n出现脏读的场景:
\n\n跨多个数据中心的多主复制:
\n\n最后再来说一下本书中的一些瑕疵,中文版中有不少错别字,而且有些词汇前后翻译不一致,可能会给读者的阅读带来困扰,尤其是第三部分,明显感觉到译者不太用心了。
\n本书英文版名为:《Designing Data-Intensive Application》,出版于 17 年 3 月份。这本书在网上有个开源的翻译版本,是因为那个开源作者在 17 年读完英文版后,觉得写得很好,而此时国内又没有出版计划,所以在 Github 开始了翻译的漫漫长路。中国的官方版本直到 18 年 9 月才发布,所以阅读过程中实际上可以对照两个版本一起来学习。开源版在线阅读地址:https://vonng.gitbooks.io/ddia-cn/content/
\n最后再立个 Flag,这本书我会在 2020 年进行 2 刷。
\n"},{"title":"从《我们的一天》看东西方文化差异中的情感表达","url":"/2023/Emotional-expression-in-cultural-differences-between-East-and-West/","content":"几周前的一个周会上,我提出让大家每人分享一个自己推荐的书影剧,当时一起旁听会议的HR小姐姐推荐了一个美剧,叫「我们的一天(This Is Us)」,她说非常感人,另外一个男同事也随声附和,并说非常适合我这种有两个小孩的人去看,他自己看的时候哭的不要不要的。
\n周末的时候我看了4集这个剧,情节围绕着同一个家庭中同一天出生的兄妹三人来展开,其中一个黑人不是他们的亲兄弟。剧中将他们小时候和他们成人后的场景相结合,故事情节很好,有多条平行的故事线,结尾经常有悬疑可以解开,比如第一集中三个主角是分别拍摄的,但在最后一刻才揭晓他们三个原来是一家人。
\n但实话实说,目前来说这个剧还没有让我掉过泪,相比较而言「请回答1988」是能让我在地铁上哭出来的一部剧。我想这和我所在的文化环境与这两部剧所使用的感人手段不同有关。
\n亚洲人,尤其是中国人都比较内敛、隐忍,更喜欢默默的付出,有话不说出来,不太在公众场合宣泄自己的情绪,在「1988」这部剧中让我奔泪的也是这样的场景。
\n举两个例子,德善生日那一集爸爸最后说的几句话:
\n\n\n\n爸爸我也不是一生下来就是爸爸,
\n爸爸也是头一次当爸爸,
\n所以我的女儿稍微体谅一下。
\n
另一个是德善奶奶去世,爸爸一天都嘻嘻哈哈招待来悼念的朋友,直到晚上远在海外的大哥回来,兄弟姐妹到齐了,屋里也只剩下了一家人,爸爸再也抑制不住自己的情绪兄弟几人抱头痛哭。
\n\n但「我们的一天」使用的手法就很直给,在公共场合表达爱、有了问题及时沟通,父母与孩子之间的关系平等交流。不过这也许并不是这部剧没有让我掉泪的原因,这部剧我也只看了4集,还不能这么早下定论,只是基于这几集记录下自己的想法。也许看过后边的部分后会有不同的结论。所谓生长不就是在不断推翻自己曾深信不疑的想法的过程吗?
\n写到最后,我想我没有被「我们的一天」所触动,更大的可能是「1988」更贴近于我的生活,里边的一些场景都是我有体会或者曾经经历过的。
\n"},{"title":"被 AdBlock 坑了","url":"/2018/FUCK-AdBlock/","content":"今天写了一个接口,用 postman 测试没有问题,但是用 swagger 测试一直无法访问。
\n刚开始怀疑是浏览器缓存,换火狐后发现也访问不了。然后怀疑是开了代理的原因,关掉 Surge 后发现还是不行。接着怀疑是 swagger 的 bug,Google 搜索 swagger admin banners
的关键字并没有发现什么有用的信息。
这个接口的 URL 是:/api/admin/banners
,我修改了接口对应的 URL 后再次尝试,发现又正常了,然后经过各种尝试,发现只要路径中带有 admin/banners
就无法访问,我把浏览器发送的请求转为 cURL
命令,使用终端发送也是正常的,所以确定问题应该出在浏览器身上。
这时候想起来我在上家公司工作时,写的一个广告接口,URL 中带有 advertisement,刚开始也是无法请求,最后发现是装了屏蔽广告的插件导致的,这次一定也是因为这个问题,所以我尝试把 AdBlock 关掉,再次测试,一切 ok。
\n这中间差不多花费了20多分钟来排查问题,没有很快找到问题的一个重要原因是,我的火狐浏览器恰好也装了 AdBlock,所以早早的就把浏览器的问题排除了。
\n最后兜了个圈又回到原点。
\n"},{"title":"关注当下与松驰感","url":"/2023/Focus-on-the-present-and-relaxation/","content":"这篇博客是由我、飞书妙记、ChatGPT和NotionAI共同完成。
\n在工作中,我们总会遇到各种各样的人。有些人能够做出非常出色的工作,让人钦佩不已;而有些人则在同样的工作中表现平平,甚至不及预期。这让人不得不思考:到底是什么让一些人在工作中表现突出呢?
\n我发现,那些比较厉害的人常常具备两种品质,它们分别是松弛感和关注当下。
\n所谓关注当下,就是在该做什么事情的时候,就去做这件事情,而不是被其他琐碎的事情所干扰。
\n我有两个下属,其中一个能力较差,参加会议时总是显得很慌张,总是在赶项目进度,从不听会,实际工作效率很低。相比之下,另一位比较优秀的下属经常表现出专注的态度,即使手头上还有其他紧急事情要处理,也会认真听会积极参与讨论。
\n人们认为一心多用能够提高效率,但是实际上反而会让自己的工作做得更加粗糙,并且效率不高。
\n这给我带来了启示:成功的人必须懂得将注意力集中在当前自己正在做的事情上,这样才能创造最大的价值。尝试同时处理多个任务只会分散自己的注意力,降低自己的效率。
\n其次,我发现厉害的人尝尝具备“松弛感”,他们可以在掌握自己情况的基础上,放松一些,面对压力从容应对。
\n看过美食节目的人应该都知道“盗月社”,其中的一名成员叫朱狒狒,她外表看起来很普通,但是通过观察,我们会发现她总是面对任何挑战都能够从容应对。对于那些对打压自己感到无能为力的人来说,这种气质是非常重要的。她总能以一种松弛的方式去处理一些复杂的问题,并找到一个高效的解决方案。后来我还了解到,朱狒狒是北大毕业的。
\n尽管一个人成功的原因有很多,但是松弛感和关注当下这两个品质确实是让一些人在工作中表现突出的关键。作为普通人,我们也可以通过不断地学习和实践,从中汲取营养,让自己成为一个更加优秀的人。
\n"},{"title":"逛菜市场","url":"/2023/Food-Market/","content":"昨天周五,因为家人都没在北京,所以晚上在公司吃了份轻食才走的。
\n众所周知,轻食吃完后没有任何满足感,而且为了控制体重我已经连续吃了一周轻食,加上白天因为家里的一些糟心事心情很差,晚上到家的时候又在楼下的街边烤串吃了个宵夜。
\n\n串儿有点咸,想到家里冰箱有可乐,撸串儿时没有买喝的,到家后喝了一罐可乐,又看了会美剧。因为是周五总觉得生活意犹未尽,躺下后又开始刷小红书,被算法支配到了11点半。
\n放下手机读了会《回忆爱玛侬》,读完了其中写的很压抑的《彷徨的命运》那一章后已经过0点了。
\n以上这些作死操作,再加上今天白天的午后,感觉很冷想喝点热乎的,于是喝了一杯瑞幸的热拿铁(早上已经喝过了一杯美式),导致久久无法入睡,翻来覆去、来回上了几次厕所之后,放弃了自然入睡的打算,吃了一片艾司唑仑睡去了。
\n第二天早上醒来已经将近九点,本来按照前两天的计划是周六一早开车回老家,但因为一些(长辈上的)家庭矛盾取消了这个计划,窝在被窝里刷手机。
\n拉开窗帘看了一眼,差点晃瞎眼镜。
\n\n这么好的天气不能虚度,刚好前几天听了一期《圆桌派》,聊的是关于「菜市场」的主题,而且中间提到了北京的新源里菜市场,我本人对吃非常感兴趣,也喜欢做菜和逛菜市场,所以打算今天也去菜市场逛逛。
\n查了下去新源里菜市场的路况很堵,又在小红书搜了下北京其他的有名菜市场,发现排名靠前的有个叫百姓菜篮子的菜市场,在百子湾,离我不远,开车只要20多分钟。
\n\n为了今天早饭也为了下周有的吃,起床后煮了锅鸡蛋,放上调料腌制好后,又配着吃了个面包,看了一集《我们的生活》后准备出门,在我起床后收拾的过程中就开始一阵一阵的下雨,所以出门时就考虑是作为休闲骑车过去还是开车过去。
\n\n考虑到我的车已经一周多没动过了,所以最后还是决定开车过去,就当溜溜车了。最终看来这个决定非常明智。
\n出门时天阴阴的,很凉爽,伴随着播放我最喜欢的音乐心情也很好。
\n\n\n开到一半的时候,天气突然大变,下起了瓢泼大雨,很庆幸自己选择了开车。
\n\n开到地点后雨势也减小很多,百姓菜篮子门脸不大,但进去后别有乾坤,是个极长极长的通道,中间、两边挤满了商家,与其说是个菜市场,不如说是个百货市场,里边卖什么的都有。
\n\n\n\n这个菜市场太棒了,但因为我的主要目的是逛,并不是买买买,新家这边装备也不齐全,即便买也只能买些好处理的,最后我买了一斤多蛏子、一斤多白蛤准备到家后白灼,又花1块钱买了根葱和一小块姜。
\n\n还买了几个巨大的桃子,夏天我最喜欢吃的两样水果是西瓜和桃子,路过一个零食小摊,混着买了些小零食,其中的香葱鸡片小饼干是我的童年记忆,虽然小时候吃顶过,现在还是喜欢吃。
\n\n最后出门前又买了个肉蛋堡,香迷糊了。
\n\n\n虽然买的不多,但还是庆幸自己开车来了,如果没有车我肯定拿不了这些东西,感谢我的小车车。
\n到家后把海鲜用白灼的手法处理了一下,真肥啊,在下吧台上吃着海鲜喝着小酒看着小雨很惬意。
\n大家周末愉快呀。
\n"},{"title":"GitHub使用SSH模式不能用sudo","url":"/2016/GitHub%E4%BD%BF%E7%94%A8SSH%E6%A8%A1%E5%BC%8F%E4%B8%8D%E8%83%BD%E7%94%A8sudo/","content":"今天在AWS上用git时一直报错:
\nPermission denied (publickey). |
因为我在/home/www下进行的操作,这个目录当前用户是没有写权限的,所以需要在操作git时前边加sudo,所以才会导致这个问题。
\n官方说明:
\nYou should not be using the sudo
command with Git. If you have a very good reason you must use sudo
, then ensure you are using it with every command (it’s probably just better to use su
to get a shell as root at that point). If you generate SSH keys without sudo
and then try to use a command like sudo
git push, you won’t be using the same keys that you generated.
解决方法,将www目录权限改为777。
\n","categories":["GitHub"],"tags":["Linux","GitHub"]},{"title":"Gradle 笔记","url":"/2018/Gradle-%E7%AC%94%E8%AE%B0/","content":"Maven Plugin
将构建上传到 Maven 仓库中:apply plugin: 'maven' |
build.dependsOn 'uploadArchives' |
gradle -q tasks
显示所有的任务Gradle提高效率的一个办法就是能够在命令行输入任务名的驼峰简写,当你的任务名称非常长的时候这很有用
\njar { |
所以,gradle目录
、gradlew
和 gradlew.bat
都应该放在版本控制内。
task wrapper(type: Wrapper) { |
更多包装器的特性查看:http://gradle.org/docs/current/dsl/org.gradle.api.tasks.wrapper.Wrapper.html
\n//Only initial declaration of extra property requires you to use ext namespace |
/.gradle路径
或者项目根目录下的 gradle.properties
文件来定义属性,可以直接注入到项目中:假设在 gradle.properties
文件中定义了下面的属性:
exampleProp = myValue |
可以在项目中访问这两个变量:
\nassert project.exampleProp == 'myValue' |
-P
命令行选项来定义项目属性-D
命令行选项来定义系统属性ORG_GRADLE_PROJECT_propertyName=someValue
task printVersion { |
输出
\n> Task :printVersion |
\n\n当你想添加动作的那个任务不是你自己写的时候这会非常有用,你可以添加一些自定义的逻辑,比如你可以添加
\ndoFirst
动作到compile-Java
任务来检查项目是否包含至少一个source
文件。
task first << { println "first" } |
输出
\nfirst |
\n\nGradle 并不保证依赖的任务能够按顺序执行,
\ndependsOn
方法只是定义这些任务应该在这个任务之前执行,但是这些依赖的任务具体怎么执行它并不关心(也就是和[second, first]
顺序 无关),因为任务不是顺序执行的,就可以并发的执行来提高性能。
task first << { println "first" } |
first
结束后自动触发任务 second
:
|
你可以用 allprojects
方法给所有的项目添加 group
和 version
属性,由于根项目不需要 Java 插件,你可以使用 subprojects
给所有子项目添加Java插件:
allprojects { |
自由软件基金会创始人理查德·斯托尔曼说:“出于兴趣而解决某个难题,不管它有没有用,这就是黑客。”
\n黑客的六条价值观:
\n人们区分程序员甚至不是看他们写了什么程序,而是看他们使用什么语言。
\n受父母的影响,书呆子被教导追求正确答案,而受欢迎的小孩被教导讨人喜欢。
\n青少年在心理上还没有摆脱儿童状态,许多人都会残忍地对待他人。他们折磨书呆子的原因就像拔掉一条蜘蛛腿一样,觉得很好玩。在一个人产生良知之前,折磨就是一种娱乐。
\n在任何社会等级制度中,那些对自己没自信的人就会通过虐待他们眼中的下等人来突显自己的身份。我已经意识到,正是因为这个原因,在美国社会中底层白人是对待黑人最残酷的群体。
\n不受欢迎是一种传染病,虽然善良的孩子不会去欺负书呆子,但是为了保护自己,也依然会与书呆子保持距离。难怪聪明的小孩读中学时往往是不快乐的。
\n公立学校的老师很像监狱的狱卒。看管监狱的人主要关心的是犯人都待在自己应该待的位置。然后,让犯人有东西吃,尽可能不要发生斗殴和伤害事件,这就可以了。除此以外,他们一件事也不愿多管,没必要自找麻烦。所以,他们就听任犯人内部形成各种各样的小集团。
\n真实世界的关键并非在于它是由成年人组成的,而在于它的庞大规模使得你做的每件事都能产生真正意义上的效果。
\n表面上,学校的使命是教育儿童。事实上,学校的真正目的是把儿童都关在同一个地方,以便大人们白天可以腾出手来把事情做完。
\n人类喜欢工作,在世界上大多数地方,你的工作就是你的身份证明。
\n以前的青少年似乎也更尊敬成年人,因为成年人都是看得见的专家,会传授他们所要学习的技能。如今的大多数青少年,对他们的家长在遥远的办公室所从事的工作几乎一无所知。他们看不到学校作业与未来走上社会后从事的工作有何联系。
\n校园生活的两大恐怖之处——残忍和无聊
\n校园生活的真正问题是空虚。
\n黑客与画家的共同之处,在于他们都是创作者。与作曲家、建筑师、作家一样,黑客和画家都是试图创作出优秀的作品。
\n黑客的最髙境界是创造规格。
\n创造优美事物的方式往往不是从头做起,而是在现有成果的基础上做一些小小的调整,或者将已有的观点用比较新的方式组合起来。
\n黑客搞懂“计算理论”(theory of computation)的必要性,与画家搞懂颜料化学成分的必要性差不多大。
\n大学里教给我的编程方法都是错的。你把整个程序想清楚的时间点,应该是在编写代码的同时,而不是在编写代码之前,这与作家、画家和建筑师的做法完全一样。
\n编程语言首要的特性应该是允许动态扩展(malleable)。编程语言是用来帮助思考程序的,而不是用来表达你已经想好的程序。它应该是一支铅笔,而不是一支钢笔。
\n大学和实验室强迫黑客成为科学家,企业强迫黑客成为工程师。
\n程序员被当作技工,职责就是将产品经理的“构想”(如果这个词是这么用的话)翻译成代码……。大公司这样安排的原因是为了减少结果的标准差……。但是当你排斥差异的时候,你不仅将失败的可能性排除在外,也将获得高利润的可能性排除在外。
\n开发优秀软件的方法之一就是自己创业。
\n自己创业的两个问题:
\n如果你想赚钱,你可能不得不去干那些很麻烦很讨厌的事情,因为这些事情没人愿意义务来干。
\n我们面试程序员的时候,主要关注的事情就是业余时间他们写了什么软件。因为如果你不爱一件事,你不可能把它做得真正优秀,要是你很热爱编程,你就不可避免地会开发你自己的项目。
\n黑客的出发点是原创,最终得到一个优美的结果;而科学家的出发点是别人优美的结果,最终得到原创性。
\n你不能盼望先有一个完美的规格设计,然后再动手编程,这样想是不现实的。如果你预先承认规格设计是不完美的,在编程的时候,就可以根据需要当场修改规格,最终会有一个更好的结果。
\n最容易修改的语言就是简短的语言。
\n坚持一丝不苟,就能取得优秀的成果。因为那些看不见的细节累加起来,就变得可见了。
\n需要合作,但是不要“合”得过头……。正确的合作方法是将项目分割成严格定义的模块,每一个模块由一个人明确负责。模块与模块之间的接口经过精心设计,如果可能的话,最好把文档说明写得像编程语言规范那样清晰。
\n通黑客与优秀黑客的所有区别之中,会不会“换位思考”可能是最重要的单个因素。
\n判断一个人是否具备“换位思考”的能力有一个好方法,那就是看他怎样向没有技术背景的人解释技术问题。
\n如果我只能让别人记住一句关于编程的名言,那么这句名言就是《计算机程序的结构与解释》一书的卷首语:程序写出来是给人看的,附带能在机器上运行。
\n所谓“流行”(传统观念也是一种流行),本质上就是自己看不见自己的样子。
\n历史的常态似乎就是,任何一个年代的人们,都会对一些荒谬的东西深信不疑。
\n最令人暴跳如雷的言论,就是被认为说出了真相的言论。
\n最先从你头脑中跳出来的想法,往往就是最困扰你、很可能为真的想法。你已经注意到它们,但还没有认真思考过。
\n如果某个观点在大部分时空都是不受禁止的,只有我们这个社会才把它当作禁忌,那么很可能是我们出错了。
\n孩子眼里的世界是不真实的,是一个被灌输进他们头脑的假想世界。将来当孩子长大以后接触社会,就会发现小时候以为真实的事情,在现实世界中是荒唐可笑的。
\n找出不能说的话的四种方式:
\n流行的时尚产生于某个有影响力的人物,他突发奇想,接着其他人纷纷模仿。
\n但是,流行的道德观念不是这样,它们往往不是偶然产生的,而是被刻意创造出来的。如果有些观点我们不能说出口,原因很可能是某些团体不允许我们说。
\n如果一个团体强大到无比自信,它根本不会在乎别人的抨击。美国人或者英国人对外国媒体的诋毁就毫不在意。
\n大多数的斗争,不管它们实际上争的是什么,都会以思想斗争的形式表现出来。
\n优秀作品往往来自于其他人忽视的想法,而最被忽视的想法就是那些被禁止的思想观点。
\n智力越高的人,越愿意去思考那些惊世骇俗的思想观点。这不仅仅因为聪明人本身很积极地寻找传统观念的漏洞,还因为传统观念对他们的束缚力很小,很容易摆脱。从他们的衣着上你就可以看出这一点:不受传统观念束缚的人,往往也不会穿流行的衣服。
\n做一个异端是有回报的,不仅是在科学领域,在任何有竞争的地方,只要你能看到别人看不到或不敢看的东西,你就有很大的优势。
\n训练自己去想那些不能想的事情,你获得的好处会超过所得到的想法本身。
\n与笨蛋辩论,你也会变成笨蛋。
\n自由思考比畅所欲言更重要……。在思想和言论之间划一条明确的界线。在心里无所不想,但是不一定要说出来……。你的思想是一个地下组织,绝不要把那里发生的事情一股脑说给外人听。
\n讨论一个观点会产生更多的观点,不讨论就什么观点也没有。
\n如果可能的话,你最好找一些信得过的知己,只与他们畅所欲言、无所不谈。这样不仅可以获得新观点,还可以用来选择朋友。能够一起谈论“异端邪说”并且不会因此气急败坏的人,就是你最应该认识的朋友。
\n人们喜欢讨论的许多问题实际上都是很复杂的,马上说出你的想法对你并没有什么好处。
\n如果你的思想很保守,你自己不会知道,而且你很可能还会持有相反的看法。
\n如果一个命题不是错的,却被加上各种标签,进行压制和批判,那就有问题。
\n在程序员眼里,“黑客”指的是优秀程序员……。对于程序员来说,“黑客”这个词的字面意思主要就是“精通”,也就是他可以随心所欲地支配计算机。
\n警方总是从犯罪动机开始调查。常见的犯罪动机不外乎毒品、金钱、性、仇恨等。满足智力上的好奇心并不在FBI的犯罪动机清单之上。
\n在计算机工业的历史上,新技术往往是由外部人员开发的,而且所占的比例可能要高于内部人员。
\n一个人们拥有言论自由和行动自由的社会,往往最有可能采纳最优方案,而不是采纳最有权势的人提出的方案。专制国家会变成腐败国家,腐败国家会变成贫穷国家,贫穷国家会变成弱小国家。
\n“你的电脑”这个概念正慢慢成为过去时,取而代之的是“你的数据”。
\n如果用户自己的硬盘坏了,他们不会发狂,因为不能去责怪别人;如果一家公司丢失了他们的数据,他们会怀着超乎寻常的怒火,冲着这家公司发飙。
\n对于开发者来说,互联网软件与桌面软件最显著的区别就是,前者不是一个单独的代码块。它是许多不同种类程序的集合,而不是一个单独的巨大的二进制文件。设计桌面软件就像设计一幢大楼,而设计互联网软件就像设计一座城市:你不仅需要设计建筑物,还要设计道路、路标、公用设施、警察局、消防队,并且制定城市发展规划和紧急事件的应对方案。
\n不同的语言适合不同的任务,你应该根据不同场合,挑选最合适的工具。
\n这只是公关伎俩啦,我们知道媒体喜欢听到版本号。如果你发布一个大的版本更新(版本号的第一位数发生变动),它们就会以大篇幅报道。
\n互联网软件的另一个技术优势在于,你能再现大部分的bug。
\n人数越来越多,开会讨论各个部分如何协同工作所需的时间越来越长,无法预见的互相影响越多越大,产生的bug也越多越多。幸运的是,这个过程的逆向也成立:人数越来越少,软件开发的效率将指数式增长。
\n\n\n软件项目是交互关系复杂的工作,需要大量的沟通成本,人力的增加会使沟通成本急剧上升,反而无法达到缩短工期的目的。
\n
互联网软件不仅把开发者与他的代码更紧密地联系在了一起,而且把开发者与他的用户也更紧密联系在了一起。
\n一定数量的盗版对软件公司是有好处的。不管你的软件定价多少,有些用户永远都不会购买。如果这样的用户使用盗版,你并没有任何损失。事实上,你反而赚到了,因为你的软件现在多了一个用户,市场影响力就更大了一些,而这个用户可能毕业以后就会出钱购买你的软件。
\n只要有可能,商业性公司就会采用一种叫做“价格歧视”(price discrimination)的定价方法,也就是针对不同的客户给出不同的报价,使得利润最大化。软件的定价特别适合采用价格歧视,因为软件的边际成本接近于零。
\n\n\n“边际成本”是一个经济学概念,指下一个单位产品的生产成本。软件的边际成本就是复制代码的成本,所以接近零。这意味着,对软件公司来说,增加一个用户几乎没有增加生产成本。它与价格歧视的关系在于,边际成本越低,厂商的定价空间就越大,它可以针对特定消费者定出很低的价格,从而达到扩大销售、利润最大化的目的。——译者注
\n
如果某样商品购买起来很困难,人们就会改变主意,放弃购买。反过来也成立,如果某样东西易于购买,你就会多买一点。
\n大公司付出的高价之中,很大一部分是商家为了让大公司买下这个商品而付出的费用。
\n我预计微软会推出某种服务器和桌面电脑的混合产品,让它的桌面操作系统专门与由它控制的服务器协同工作。
\n桌面软件迫使用户变成系统管理员,互联网软件则是迫使程序员变成系统管理员:用户的压力变小了,程序员的压力变大了。
\n管理企业其实很简单,只要记住两点就可以了:做出用户喜欢的产品,保证开支小于收入。
\n如何做出用户喜欢的产品,下面是一些通用规则:
\n如果竞争对手的产品很糟糕,你也不要自鸣得意。比较软件的标准应该是看对手的软件将来会有什么功能,而不是现在有什么功能。
\n如果你想致富,应该怎么做?我认为最好的办法就是自己创业,或者加入创业公司。
\n创业公司其实就是解决了某个技术难题的小公司。
\n创业公司不是变魔术。它们无法改变创造财富的法则,它们只是代表了财富创造曲线远端上的一点。这里有一个守恒定律:如果你想赚100万美元,就不得不忍受相当于100万美元的痛苦。
\n创造有价值的东西就是创造财富。我们需要的东西就是财富。
\n财富才是你的目标,金钱不是。……金钱是财富的一种简便的表达方式:金钱有点像流动的财富,两者往往可以互相转化。
\n金钱是专业化的副产品。
\n金钱就是交换中介,它必须数量稀少,并且便于携带。
\n大多数生意的目的是为了创造财富,做出人们真正需要的东西。
\n目前还存在的最大的手工艺人群体就是程序员。
\n公司不过是一群人在一起工作,共同做出某种人们需要的东西。真正重要的是做出人们需要的东西,而不是加入某个公司。
\n大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。
\n你在工作上投入的精力越多,就越能产生规模效应。
\n要致富,你需要两样东西:可测量性和可放大性。
\n任何一个通过自身努力而致富的个人,在他们身上应该都能同时发现可测量性和可放大性。
\n如果你有一个令你感到安全的工作,你是不会致富的,因为没有危险,就几乎等于没有可放大性。
\n乔布斯曾经说过,创业的成败取决于最早加入公司的那十个人。
\n在不考虑其他因素的情况下,一个非常能干的人待在大公司里可能对他本人是一件很糟的事情,因为他的表现被其他不能干的人拖累了。
\n创业公司为每个人提供了一条途径,同时获得可测量性和可放大性。
\n如果你有一个新点子去找VC,问他是否投资,他首先就会问你几个问题,其中之一就是其他人复制你的模式是否很困难。也就是说,你为竞争对手设置的壁垒有多高。
\n大公司不害怕打官司,这对它们是家常便饭。它们很清楚,打官司的成本高昂又很费时。
\n如果你有两个选择,就选较难的那个。
\n真正创业以后,你的竞争对手决定了你到底要有多辛苦,而他们做出的决定都是一样的:你能吃多少苦,我们就能吃多少苦。
\n创业公司如同蚊子,往往只有两种结局,要么赢得一切,要么彻底消失。
\n一家大到有能力收购其他公司的公司必然也是一家大到变得很保守的公司,而这些公司内部负责收购的人又比其他人更保守,因为他们多半是从商学院毕业的,没有经历过公司的创业期。他们宁愿花大钱做更安全的选择,所以向他们出售一家已经成功的创业公司要比出售还处在早期阶段的创业公司更容易,即使会让他们付出多得多的价码。
\n大多数时候,促成买方掏钱的最好办法不是让买家看到有获利的可能,而是让他们感到失去机会的恐惧。
\n你以为买家在收购前会做很多研究,搞清楚你的公司到底值多少钱,其实根本不是这么回事。他们真正在意的只是你拥有的用户数量。
\n你开办创业公司不是单纯地为了解决问题,而是为了解决那些用户关心的问题。
\n创造人们需要的东西,也就是创造财富。
\n为什么欧洲在历史上变得如此强大?……答案(或者至少是近因)可能就是欧洲人接受了一个威力巨大的新观点:允许赚到大钱的人保住自己的财富。
\n只要懂得藏富于民,国家就会变得强大。让书呆子保住他们的血汗钱,你就会无敌于天下。
\n有三个原因使得我们对赚钱另眼相看。
\n财富与金钱是两个概念。金钱只是用来交易财富的一种手段,财富才是有价值的东西,我们购买的商品和服务都属于财富。
\n由于每个人创造财富的能力和欲望强烈程度都不一样,所以每个人创造财富的数量很不平等。
\n每个人的技能不同,导致收入不同,这才是贫富分化的主要原因,正如逻辑学的“奥卡姆剃刀”原则所说,简单的解释就是最好的解释。
\n如果说某种工作的报酬过低,那就相当于说人们的需求不正确。
\n封建社会只有两个阶级:贵族与农奴(为贵族服务的人)。中产阶级是一个新的第三类团体,他们出现在城镇中,以制造业和贸易为生。
\n创造财富真正取代掠夺和贪污成为致富的最佳方式,并不是发生在中世纪,而是发生在工业革命时代。
\n双重误解:对一个已经过时的情况持有错误的看法。
\n技术应该会引起收入差距的扩大,但是似乎能缩小其他差距。一百年前,富人过着与普通人截然不同的生活。……由于技术的发展,富人的生活与普通人的差距缩小了。
\n技术无法使其变得更便宜的唯一东西,就是品牌。
\n只要存在对某种商品的需求,技术就会发挥作用,将这种商品的价格变得很低,从而可以大量销售。
\n无论在物质上,还是在社会地位上,技术好像都缩小了富人与穷人之间的差距,而不是让这种差距扩大了。
\n一个社会需要有富人,这主要不是因为你需要富人的支出创造就业机会,而是因为他们在致富过程做出的事情。……如果你让他致富,他就会造出一台拖拉机,使你不再需要使用马匹耕田了。
\n每个用户应该都分别有自己的概率分布表,这是根据他收到的邮件对每一个词进行统计后得出的。这样做可以:
\n人类的思想就是没有经过整理的无数杂念的混合。
\n优秀设计的原则:
\n编程语言的一个重要特点:一个操作所需的代码越多,就越难避免bug,也越难发现它们。
\n编译器处理的高级语言代码又叫做源码。它经过翻译以后产生的机器码就叫做目标码。
\n开源软件就像一篇经受同行评议的论文。
\n程序员的时间要比计算机的时间昂贵得多,后者已经变得很便宜了,所以几乎不值得非常麻烦地用汇编语言开发软件。
\n如果你长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考。所以,后来当你遇到其他任何一种有重大差异的语言,即使那种语言本身并没有任何不对的地方,你也会觉得它极其难用。缺乏经验的程序员对于各种语言优缺点的判断经常被这种心态误导。
\n语言设计者之间的最大分歧也许就在于,有些人认为编程语言应该防止程序员干蠢事,另一些人则认为程序员应该可以用编程语言干一切他们想干的事。
\n事实上有两种程度的面向对象编程:某些语言允许你以这种风格编程,另一些语言则强迫你一定要这样编程。
\n允许你做某事的语言肯定不差于强迫你做某事的语言。所以,至少在这方面我们可以得到明确的结论:你应该使用允许你面向对象编程的语言。至于你最后到底用不用则是另外一个问题了。
\n无论何时,选择进化的主干可能都是最佳方案。
\n那些内核最小、最干净的编程语言才会存在于进化的主干上。
\n编程语言进化缓慢的原因在于它们并不是真正的技术。
\n随着技术的发展,每一代人都在做上一代人觉得很浪费的事情。
\n我觉得一些最好的软件就像论文一样,也就是说,当作者真正开始动手写这些软件的时候,他们其实不知道最后会写出什么结果。
\n浪费程序员的时间而不是浪费机器的时间才是真正的无效率。
\n另一种消耗硬件性能的方法就是,在应用软件与硬件之间设置很多的软件层。
\n除了某些特定的应用软件,一百年后,并行计算不会很流行。如果应用软件真的大量使用并行计算,这就属于过早优化了。
\n应用软件运行速度提升的关键在于有一个好的性能分析器帮助指导程序开发。
\n新语言更多地以开源项目的形式出现,而不是以研究性项目的形式出现。
\n新语言的设计者更多的是本身就需要使用它们的应用软件作者,而不是编译器作者。
\n设计新语言的方法之一就是直接写下你想写的程序,不管编译器是否存在,也不管有没有支持它的硬件。……随便什么,只要能让你最省力地写出来就行。
\n真正非常严肃地把黑客作为人生目标的人,应该考虑学习Lisp。
\nLisp没有得到广泛使用的原因就是因为编程语言不仅仅是技术,也是一种习惯性思维,非常难于改变。
\n程序员关心的那种强大也许很难正式定义,但是有一个办法可以解释,那就是有一些功能在一种语言中是内置的,但是在另一种语言中需要修改解释器才能做到,那么前者就比后者更强大。
\n编程语言的特点之一就是它会使得大多数使用它的人满足于现状,不想改用其他语言。……人类天性变化的速度大大慢于计算机硬件变化的速度,所以编程语言的发展通常比CPU的发展落后一二十年。
\n编程语言不一样,与其说它是技术,还不如说是程序员的思考模式。
\n各种编程语言的编程能力是不相同的。
\n编程语言现在的发展不过刚刚赶上1958年Lisp语言的水平。
\n使用一种不常见的语言会出现的问题我想到了三个:
\n把软件运行在服务器端就可以没有顾忌地使用最先进的技术。
\n到目前为止,大家公认少于10个人的团队最适合开发软件。
\n选择更强大的编程语言会减少所需要的开发人员数量。因为:
\n如果你创业的话,千万不要为了取悦风险投资商或潜在并购方而设计你的产品。
\n衡量语言的编程能力的最简单方法可能就是看代码数量。……语言的编程能力越强大,写出来的程序就越短。
\n代码的数量很重要,因为开发一个程序所耗费的时间主要取决于程序的长度。
\n当团队规模超过某个门槛时,再增加人手只会带来净损失。
\n一种出色的工具到了真正优秀的黑客手里,可以发挥出更大的威力。
\n程序员使用某种语言能做到的事情是有极限的。
\n你的经理其实不关心公司是否真的能获得成功,他真正关心的是不承担决策失败的责任。所以对他个人来说,最安全的做法就是跟随大多数人的选择。……既然我选择的是“业界最佳实践”,如果不成功,项目失败了,那么你也无法指责我,因为做出选择的人不是我,而是整个“业界”。
\n编程语言的所谓“业界最佳实践”,实际上不会让你变成最佳,只会让你变得很平常。
\n如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。
\n编程语言本来就是为了满足黑客的需要而产生的,当且仅当黑客喜欢一种语言时,这种语言才能成为合格的编程语言。
\n虽然语言的核心功能就像大海的深处,很少有变化,但是函数库和开发环境之类的东西就像大海的表面,一直在汹涌澎湃。
\n发展最早的20个用户的最好方法可能就是使用特洛伊木马:你让人们使用一种他们需要的应用程序,这个程序偏巧就是用某种新语言开发的。
\n一种语言必须是某一个流行的计算机系统的脚本语言(scripting language),才会变得流行。
\n黑客欣赏的一个特点就是简洁。
\n简洁性是静态类型语言的力所不及之处。……只要计算机可以自己推断出来的事情,都应该让计算机自己去推断。
\n语言设计者应该假定他们的目标用户是一个天才,会做出各种他们无法预知的举动,而不是假定目标用户是一个笨手笨脚的傻瓜,需要别人的保护才不会伤到自己。
\n对于制造工具的人来说,总是会有用户以违背你本意的方式使用你的工具。
\n所谓一次性程序,就是指为了完成某些很简单的临时性任务而在很短时间内写出来的程序。……开发大型程序的另一个方法就是从一次性程序开始,然后不断地改进。
\n未来50年中,编程语言的进步很大一部分与函数库有关。
\n函数库的设计基础与语言内核一样,都是一个小规模的正交运算符集合。函数库的使用应该符合程序员的直觉,让他可以猜得出哪个函数能满足自己的需要。
\n编程时提高代码运行速度的关键是使用好的性能分析器(profiler),而不是使用其他方法,比如精心选择一种静态类型的编程语言。
\n一种编程语言要想变得流行,最后一关就是要经受住时间的考验。……让别人相信一种新事物是需要时间的。
\n大多数人接触新事物时都学会了使用类似的过滤机制。甚至有时要听到别人提起十遍以上他们才会留意。这样做完全是合理的,因为大多数的热门新商品事后被证明都是浪费时间的噱头,没多久就消失得无影无踪。
\n人们真正注意到你的时候,不是第一眼看到你站在那里,而是发现过了这么久你居然还在那里。
\n新技术被市场接纳的方式有两种,一种是自然成长式,另一种是大爆炸式。
\n最终来看,自然成长式会比大爆炸式产生更好的技术,能为创始人带来更多的财富。
\n著名散文家E.B.怀特说过,“最好的文字来自不停的修改”。
\n设计一样东西,最重要的一点就是要经常“再设计”,编程尤其如此,再多的修改都不过分。
\n为了写出优秀软件,你必须同时具备两种互相冲突的信念。一方面,你要像初生牛犊一样,对自己的能力信心万丈;另一方面,你又要像历经沧桑的老人一样,对自己的能力抱着怀疑态度。在你的大脑中,有一个声音说“千难万险只等闲”,还有一个声音却说“早岁哪知世事艰”。
\n你必须对解决难题的可能性保持乐观,同时对当前解法的合理性保持怀疑。
\n黑客心目中梦寐以求的语言:这种语言干净简练,具有最高层次的抽象和互动性,而且很容易装备,可以只用很少的代码就解决常见的问题。
\n设计与研究的区别看来就在于,前者追求“好”(good),后者追求“新”(new)。
\n艺术的各个领域有着巨大的差别,但是我觉得任何一个领域的最佳作品都不可能由对用户言听计从的人做出来。
\n让用户满意并不等于迎合用户的一切要求。用户不了解所有可能的选择,也经常弄错自己真正想要的东西。做一个好的设计师就像做一个好医生一样。你不能头痛医头,脚痛医脚。病人告诉你症状,你必须找出他生病的真正原因,然后针对病因进行治疗。
\n除非设定目标用户,否则一种设计的好坏根本无从谈起。
\n如果你正在设计某种新东西,就应该尽快拿出原型,听取用户的意见。
\n软件功能的增加并不必然带来质量的提高。
\n"},{"title":"使用 Hoverfly 虚拟化服务","url":"/2020/Hoverfly-API-simulation/","content":"\n\n在说明什么是服务虚拟化前,先介绍另外两个常见的概念:
\nMock 代指那些仅记录它们的调用信息的对象,在测试断言中我们需要验证 Mock 被进行了符合期望的调用。当我们并不希望真的调用生产环境下的代码或者在测试中难于验证真实代码执行效果的时候,我们会用 Mock 来替代那些真实的对象。
\nMock 典型的例子即是对邮件发送服务的测试,我们并不希望每次进行测试的时候都发送一封邮件,毕竟我们很难去验证邮件是否真的被发出了或者被接收了,我们更多地关注于邮件服务是否按照我们的预期在合适的业务流中被调用。
\nStub 代指那些包含了预定义好的数据并且在测试时返回给调用者的对象。Stub 常被用于我们不希望返回真实数据或者会造成其他副作用的场景。
\nStub 的典型应用场景即是当某个对象需要从数据库获取数据时,我们并不需要真正地与数据库进行交互,而是直接返回预定义好的数据。
\n服务虚拟化技术可以让你创建一个模拟外部服务行为的应用程序,但是无需实际运行或者连接到外部服务。与 mock 或者 stub 相比,服务虚拟化技术可以管理更复杂的服务行为。
\n服务虚拟化更像一种智能的 mock 或者 stub,非常适合内部逻辑复杂但接口定义良好,数据响应简单的服务。
\nHoverfly 是一款比较新的服务虚拟化工具,可以模拟遗留系统复杂的响应,以及支持许多服务相互依赖的微服务架构。Hoverfly 使用 Go 语言编写,轻量、高性能。
\nHoverfly 有非常丰富的功能和使用场景,今天我们仅介绍 Hoverfly 作为代理服务器时的两种常用模式:Capture 模式 和 Simulate 模式。
\n➜ brew install SpectoLabs/tap/hoverfly |
Hoverfly 带有一个称为 hoverctl 的命令行工具。
\n➜ hoverctl start |
可以看到默认情况下, Hoverfly 的代理服务运行在 8500
端口,后台页面运行在 8888
断口。
➜ hoverctl status |
如图所示,Hoverfly 最常见的使用场景是作为一个代理服务器在客户端和服务器之间传递请求。默认情况下,Hoverfly 作为代理服务器启动。
\nHoverfly 还可以作为 Web服务器启动,这个不作为本文的重点,在这里简单对代理和Web服务器做个区分:
\n代理服务器是一种特殊的Web服务器,二者主要区别在于:当Web服务器接收到来自客户端的请求时,它以预期的响应内容(例如HTML页面)进行响应。 通常,响应的数据存放在该服务器上或同一网络中。
\n代理服务器应将传入的请求转发到另一台服务器(目标),同时,它还需要设置一些适当的头信息,如 X-Forwarded-For
、X-Real-IP
、X-Forwarded-Proto
等。一旦代理服务器从目标接收到响应,再由它回传给客户端。
捕获模式用于创建 API 模拟数据。
\n\n在捕获模式下,Hoverfly 拦截客户端和外部服务之间的通信,并透明地记录来自客户端的传出请求和来自服务 API 的传入响应(类似于一个中间人)。
\n通常,捕获模式被用作创建 API 模拟过程的起点, 然后将捕获的数据导出并修改,再重新导入到 Hoverfly 中以用作后续的模拟。
\n默认情况下,如果请求未更改,Hoverfly 将忽略重复的请求。 所以尝试捕获有状态的端点可能会出现问题,因为每次发出请求时,该端点可能会返回不同的响应。当然,也可以通过配置禁用重复请求检查,所捕获的重复请求在仿真模式时会被循序使用。
\n在仿真模式下,Hoverfly 使用其模拟数据来仿真外部API。 每次 Hoverfly 收到请求时,它都会使用之前捕获到的数据进行响应(而不是将其转发到真实的服务)。 没有网络流量会到达真正的外部服务。
\n\n仿真数据可以通过在捕获模式下运行 Hoverfly 自动生成,也可以手动创建。
\n我们通过 curl
命令来做一个完整的演示。
http://time.jsontest.com/ 是一个可以提供当前时间的 API 端点:
\n➜ curl -s http://time.jsontest.com/ | jq |
➜ hoverctl mode capture |
进入捕获模式后,我们再次想上边的 API 发出一个请求,不过这次指定 Hoverfly 作为代理:
\n➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq |
通过指定 proxy
参数,请求会首先转到代理,也就是 Hoverfly,然后再被转发到真正的 API 服务。响应的接收过程也是类似,这是 Hoverfly 拦截网络流量的方式。可以通过 Hoverfly 的日志了解具体发生了什么。
➜ hoverctl logs |
➜ hoverctl mode simulate |
在仿真模式下,Hoverfly 会使用我们之前记录下来的请求来对客户端进行响应,而不是将流量转发到真正的 API。
\n➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq |
返回的数据与我们在捕获模式下请求的结果相同。
\n接下来,如果我们想在客户端中模拟一个 100 年之后的响应,可以之前将捕获到的数据进行导出,重新编辑后导入 Hoverfly:
\n➜ hoverctl export simulation.json |
查看 simulation.json` 的内容:
{ |
"body": "{\\n \\"date\\": \\"06-27-2120\\",\\n \\"milliseconds_since_epoch\\": 1593247241163,\\n \\"time\\": \\"08:40:41 AM\\"\\n}\\n", |
➜ hoverctl import simulation.json |
➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq |
时间快进到了 100 年后。
\n到这里,我们已经成功模拟了一个API端点,虽然我们这次演示了 curl 命令,但是在时间的测试中,应该由正在测试的应用程序向 Hoverfly 发起请求。一旦 Hoverfly 存储了请求和响应的数据,我们就不再需要访问真正的服务了,可以控制 Hoverfly 返回准确的响应。
\nHoverfly 还有一个可视化的管理页面,可以访问:http://localhost:8888/ 来进行查看,也可以通过这个界面来进行模式间的切换。
\n\n\n因为 hoverctl 提供了比较友好的交互命令,所以这个页面的用途不是太大。
\n使用 Hoverfly 这样的虚拟服务化工具,主要的好处是资源占用少、初始化速度快,因此我们可以在开发电脑上虚拟化出比实际更多的服务,也可以快速集成测试中使用的 Hoverfly。
\n"},{"title":"IM 系统如何保证消息时序的一致性","url":"/2023/IM-consistency/","content":"TT 作为国内 TOP3 的社交应用,IM 是非常核心的功能,下边我来介绍一下 TT 的 IM 是如何保证消息时序的。
\n消息的时序代表的是发送方的意见表述和接收方的语义逻辑理解。如果时序一致性不能保证,可能会导致聊天语义不连贯,容易出现曲解和误会。
\n比如,你给一个小姐姐发送了1、2、3、4、5几句话,小姐姐收到的却是4、5、2、3、1。这个小姐姐一定觉得你是个脑残,直接拉黑了。
\n对于单聊的场景,时序一致性需要保证接收方的接收顺序和发送方的发出顺序一致;
\n对于群聊的场景,时序一致性保证的是群里所有接收人看到的消息展现顺序都一样。
\n在讨论这个问题之前,我们先要知道为什么消息时序一致性不容易保证:因为对于后端服务来说,是不同机器、并发处理用户每个发消息请求的。也就是说用户发送过来的消息,被成功处理的先后顺序是不确定的,处理一条消息的内部逻辑非常复杂,举几个最常见的:消息过模型判断是否违规、判断双方用户状态、更新各种未读数等等。如果我们按照服务器处理一条消息成功后的时间将消息推送给对方,那么很有可能对方的接收顺序并不是之前的发送顺序。
\n这种情况下就需要给每一条消息提前分配好一个确定的发送时间点,也可以不是时间,只要是一个可比较大小的值就行,要满足后发送的消息一定比先发送的消息值要大。
\n我们称这个值为「时序基准」,多条消息之间可以根据一个共同的「时序基准」可以来进行比较。
\n接下来的问题就转变为了如何找到一个合适的「时序基准」。
\n客户端在发送消息时连同消息再携带一个本地的时间戳或者本地维护的一个序号给到 IM 服务端,IM 服务端再把这个时间戳或者序号和消息一起发送给消息接收方,消息接收方根据这个时间戳或者序号来进行消息的排序。
\n使用客户端时间或序号可能会有以下几个问题:
\n第3点不太容易理解,用一个例子解释一下:比如同一个群里,多个用户同时发言,多客户端间由于存在时钟不同步的问题,并不能保证客户端带上来的时间是准确的,可能存在群里的用户 A 先发言,B 后发言,但由于用户 A 的手机时钟比用户 B 的慢了半分钟,如果以这个时间作为「时序基准」来进行排序,可能反而导致用户 A 的发言被认为是晚于用户 B 的。
\n客户端把消息提交给 IM 服务器后,IM 服务器依据自身服务器的时钟生成一个时间戳,再把消息推送给接收方时携带这个时间戳,接收方依据这个时间戳来进行消息的排序。
\n在实际环境中,IM 服务都是集群化部署,集群化部署也就是许多服务器同时提供服务。
\n虽然多台服务器通过 NTP 时间同步服务,能降低服务集群机器间的时钟差异到毫秒级别,但仍然还是存在一定的时钟误差,而且 IM 服务器规模相对比较大,时钟的统一性维护上也比较有挑战,整体时钟很难保持极低误差,因此一般也不能用 IM 服务器的本地时钟来作为消息的「时序基准」。
\n如果有一个全局递增的序号生成器,就能避免多服务器时钟不同步的问题了。IM 服务端就能通过这个序号生成器发出的序号,来作为消息排序的「时序基准」。
\n这种「全局序号生成器」可以通过多种方式来实现,常见的比如 Redis 的原子自增命令 incr,DB 自带的自增 id,或者类似 Twitter 的 snowflake 算法、「时间相关」的分布式序号生成服务等。
\nTT 没有搭建独立的全局序号生成服务,而是利用 PostgreSQL 强大的 function 能力来实现的。TT 本身也在结构化存储上大规模使用了PostgreSQL,基建相对来说是比较完善。
\n我们自己在 PostgreSQL 内实现了发号器函数,可以根据自己的ID、对方 ID、当前时间、shard 等条件生成集群间毫秒级唯一、保证递增但不保证连续的ID。
\n我们 IM 使用的PostgreSQL集群分了8192个逻辑shard,每个shard每毫秒可生成1024个序号,理论上整个集群每秒最多了生成 (1024 * 1000 * 8192
)=83亿个序号,性能上完完全全是够用的。
PostgreSQL 有自身的高可用架构,另外我们还用了 PostgreSQL 强大的逻辑 shard 能力,两个用户间的消息ID通过哈希规则,固定选择其中一个的shard 来生成,即使某个shard真的出了故障也只会影响8192 / 2 =
4096分之1的用户。
考虑一个问题,如果不同的数据库实例的时间不一致,两个用户间的聊天顺序是否会有影响?答案是没有影响。
\n两个用户之间的消息ID始终通过一个固定的实例生成的。具体shard选取规则为:
\n(uid + other_uid) % shard_num |
通过以上规则,可以保证无论是用户A发给用户B的,还是用户B发给用户A的消息,都可以路由到同一个shard上。
\n这相当于两个用户间的消息ID是基于同一个单机的发号器来生成的,不会由于不同机器时间不一致而造成消息顺序错乱的问题。
\n群聊消息的序号是以群的唯一 ID 计算哈希后,找到对应数据库 shard 来生成的。也就是说,多个用户在同一个群内的发言也是通过同一个发号器来生成序号,同一个群内的消息时序可以得到保证。
\n我们的精度也相对更高。据我所知,微博和微信的消息只能做到秒间有序,而我们可以做到毫秒间有序(然并卵)。
\n"},{"title":"IM 系统存储设计","url":"/2023/IM-storage-design/","content":"为了查看历史消息或者暂存离线消息,大部分 IM 系统都需要对消息进行服务端存储。下面以一对一的单聊为例介绍一下业界一般是如何设计 IM 消息存储方案的,然后再介绍下 TT 具体是如何做的,有什么区别。
\n单聊消息的参与方有两个:
\n收发双方的历史消息是相互独立的。我用个例子解释一下:
\n女神给你发消息说:「今天七夕,给我发个7777红包,我要截图发朋友圈,一会还你」。你毫不犹豫给女神发了过去。女神在截图前将「今天七夕,给我发个7777红包……」这句话进行了删除。
\n相互独立的意思是说,一方删除消息不影响另一方的展示,女神那一侧删了,你这一侧是不受影响的,如果女神赖账不还,你可以把你这边的完整对话记录拿出来和她对峙。
\n基于以上逻辑,在设计数据库表结构时我们需要为收发双方维护各自的索引记录。
\n由于收发双方看到的消息内容实际是一致的,我们没有必要将内容存储两次,所以可以有一个表来独立存储消息内容。
\n「消息内容表」存储消息纬度的基本信息,如:
\n收发双方的「消息索引表」通过唯一的消息 ID 来和消息内容进行关联,同时还要有一个枚举字段来记录这是条发送消息还是接收消息。
\n假设用户123给用户456发送一条消息,消息存储在关系型数据库中,上边涉及的两张表大致如下:
\n\n\n123给456发了一条「你好」的消息,这个动作会在消息内容表中存储一条消息,这条消息的 ID 为1024。
\n同时往索引表里存储两条数据:
\n业界也常将消息的发出和接收这两个纬度抽象为发件箱和收件箱。
\n一般 IM 系统还需要一个最近联系人列表,来让互动双方快速查找需要聊天的对象,联系人列表一般还会携带两人最近一条聊天消息用于展示。
\n\n继续以 123 给 456 发消息为例,除了在内容表和索引表插入记录,还会更新各自的最近联系人表。上图中我们将 用户 ID=123 && 另一方用户 ID=456
和 用户 ID=456 && 另一方用户 ID=123
的两行数据中的最后一条消息 ID
字段更新为 1024。为了便于客户端排序和展示,很多时候我们还会在最近聊系人表中冗余其他信息,如最后一条消息时间。
在大部分业务场景中,如果 123 是第一次给 456 发消息,会在发送消息的时候通过其他数据(如互关、好友等)校验双方好友状态,校验通过后给双方创建出联系人记录,这一点 TT 和常规做法略有区别,后文会做介绍。
\nTT 中将索引表和内容表进行了合并成了一张消息表,同时通讯录表承担了更多的工作。
\n\n要查询 123 和 321 之间的聊天记录时,使用 WHERE ((user_id=123 AND other_user_id=456) OR (user_id=456 AND other_user_id=123))
条件来进行查询。
这样的好处是要维护的表和冗余的数据更少一些,之前一个 索引表+一个内容表
的形式,每发送一条消息,不算联系人表更新的话,要有3次插入操作:1次内容表插入,2次索引表插入。
为什么索引表是2次插入,而不是用1次插入来同时写两条数据?考虑到数据量,大部分情况下,索引表会根据主态的用户 ID 进行分片存储,收发双方的数据大概率不在同一分片上,进而导致无法通过一条语句写入两条数据。将索引和内容表合并后,只需要插入1条数据,对于单方可见的消息,我们在消息的一个额外字段中进行描述即可。
\n架构本质上是一个需要权衡的过程,这种模式有优点的同时也有缺点。最大的缺点就是使用不够灵活,索引效率不够高效。另一个缺点是发送系统提示类的消息时,只能通过关联双方 ID 来展示在双方的聊天列表中,且同一条系统消息无法复用。
\n使用合并后这种方式,在底层消息分片存储上也相对更复杂一些。有索引表的情况下,我们拉取一个用户和另一个用户的记录,不管是收发消息,只需要根据 userID + otherUserID
进行查询就够了。刚刚也提到,这种情况下我们可以将索引数据按照 userID 进行分片,一个用户所有索引数据落在同一个分片上,消息内容表根据消息ID 进行分片。获取到消息索引数据后,根据数据中的消息 ID 进行点查询来获取消息内容,效率很高。
将索引表和内容表合并后,考虑到收发双法都会使用同一份数据,所以不建议使用用户 ID 进行分片,而是依然用消息 ID 分片,然后将发送方 ID 和接收方 ID 做一个联合索引。在查询双方聊天记录时,需要业务并行查询所有分片节点,然后在内存中进行排序。在我们的场景中,公司将 RocketDB 进行了二次封装,实现了一个自研的高性能关系数据库:TTDB,此类查询在 DB 中完成,不需要在业务代码内处理这个情况,查询效率也很高。TTDB 涉及很多 DB 方面的底层架构,超出了我的知识边界,就不过多进行介绍了。
\n上文中介绍的联系人表在 TT 中叫 Conversation:会话,两个用户能否发消息就判断两个用户之间有没有会话记录。
\n会话是在用户形成互关、配对等可以聊天的关系时,由内部服务提前创建好的。与常规方式不同,常规方式是在用户建第一次发消息时创建联系人。
\n当一个用户给另一个用户发消息时,我们只需要校验有没有会话,没有会话就认为这是一个无效请求,直接拒绝掉。
\n最后思考一个问题,在有消息索引表或者 TT 中的消息表的情况下,为什么还需要联系人表(或会话表)?
\n联系人表或者会话表的必要性主要考虑以下几点:
\n回想一下微信中的聊天列表页,列表中的顺序按照两个用户间最后一条消息时间进行倒排,同时展示最后一条消息内容。如果没有联系人表,我们需要通过消息索引表按照 userID 和 otherUserID 进行分组(group by),然后按时间倒排取(order by)第一条,性能会非常差。
\n正常来说每条消息是有已读、未读字段的,如果要统计未读消息的数量,确实可以通过 SQL 进行未读消息的 count 来得到,但这样也是效率很差,通常的做法是在联系人表上冗余一个未读数字段。
\n举个例子,在微信中我可以将对方置为隐藏或删除,在没有联系人表的情况下很难实现。
\n"},{"title":"JavaScript 查漏补缺","url":"/2016/JavaScript-%E6%9F%A5%E6%BC%8F%E8%A1%A5%E7%BC%BA/","content":"实际上,JavaScript允许对任意数据类型做比较:
\nfalse == 0; // true |
要特别注意相等运算符==
。JavaScript在设计时,有两种比较运算符:
第一种是==
比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;
第二种是===
比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。
由于JavaScript这个设计缺陷,不要使用==
比较,始终坚持使用===
比较。
另一个例外是NaN
这个特殊的Number与所有其他值都不相等,包括它自己:
NaN === NaN; // false |
唯一能判断NaN的方法是通过isNaN()函数:
\nisNaN(NaN); // true |
在其他语言中,也有类似JavaScript的null
的表示,例如Java也用null
,Swift用nil
,Python用None
表示。但是,在JavaScript中,还有一个和null
类似的undefined
,它表示“未定义”。
JavaScript的设计者希望用null
表示一个空的值,而undefined
表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用null
。undefined
仅仅在判断函数参数是否传递的情况下有用。
strict模式
\nJavaScript在设计之初,为了方便初学者学习,并不强制要求用var申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量:
\ni = 10; // i现在是全局变量 |
在同一个页面的不同的JavaScript文件中,如果都不用var
申明,恰好都使用了变量i
,将造成变量i互相影响,产生难以调试的错误结果。
使用var
申明的变量则不是全局变量,它的范围被限制在该变量被申明的函数体内(函数的概念将稍后讲解),同名变量在不同的函数体内互不冲突。
为了修补JavaScript这一严重设计缺陷,ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制通过var
申明变量,未使用var
申明变量就使用的,将导致运行错误。
启用strict模式的方法是在JavaScript代码的第一行写上:
\n'use strict'; |
这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。
\n\nunshift
和shift
如果要往Array
的头部添加若干元素,使用unshift()
方法,shift()
方法则把Array
的第一个元素删掉:
var arr = [1, 2]; |
splice()
方法是修改Array
的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素:
var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle']; |
由于多行字符串用\\n写起来比较费事,所以最新的ES6标准新增了一种多行字符串的表示方法,用...
表示:
`这是一个 |
for … in
\nfor
循环的一个变体是for ... in
循环,它可以把一个对象的所有属性依次循环出来:
var o = { |
要过滤掉对象继承的属性,用hasOwnProperty()
来实现:
var o = { |
由于Array
也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in
循环可以直接循环出Array
的索引:
var a = ['A', 'B', 'C']; |
请注意,for ... in
对Array
的循环得到的是String
而不是Number
。
iterable
内置的forEach
方法,它接收一个函数,每次迭代就自动回调该函数。以Array
为例:
var a = ['A', 'B', 'C']; |
Set
与Array
类似,但Set
没有索引,因此回调函数的前两个参数都是元素本身:
var s = new Set(['A', 'B', 'C']); |
Map
的回调函数参数依次为value
、key
和map
本身:
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]); |
如果对某些参数不感兴趣,由于JavaScript的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Array
的element
:
var a = ['A', 'B', 'C']; |
在 7.x
中,elastic 公司开放了 x-pack
的认证功能,所以我们可以对 Kibana 的使用也进行登陆认证,保障了系统的安全性。
这样导致的问题是,我们将 Kibana 中创建好的报表通过 iFrame 的方式嵌入到其他系统中后,运营人员在查看报表时也需要进行登陆,有没有什么办法可以不登陆就查看报表呢?
\n可以通过 Nginx,将拼好的 Authorization 请求头传递给 Kibana 服务。
\n请求头的生成策略是:
\nbase64(用户名:密码) |
Nginx 示例配置:
\nserver { |
建议新建一个只有 read-only 权限的角色和用户,用他的 token 来进行免登陆查看报表。
\n通过 kibana 生成 iFrame 的嵌入代码后,只需将将里边的正常 kibana url 前缀部分替换为这个 Nginx 的地址和对应的端口号就可以了。
\n可以使用如下命令在 Linux 中生成 base64 编码:
\necho -n username:password | base64 |
localStorage
和 sessionStorage
都继承自 Storage
。除了 sessionStorage
是为了做非持久化的目的,两者没有区别。
意思是,数据会一直存储在 localStorage
中直到被明确删除。所做的修改将被保存并且对于当前和未来所有的访问都是有效的。
对于 sessionStorage
, 修改在每个窗口(或者 Chrome 和 Firefox 的 tab 中)下有效。所做的修改将被保存并且在当前页下有效,以及未来可以在当前窗口下访问。当窗口被关闭,存储就会被删除。
为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。
\n沟通和自我 |
为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。
\n定义 |
写这篇博客的原因是,我刚用 Mac 的时候 MySQL
是在官网下载的安装包进行的安装,那时候不知道有 Homebrew
这个神器。用官方的包安装好后又做何很多的配置最终才能正常使用。但还是有不少问题,比如无法在终端启动MySQL,只能在系统偏好里,通过官方的启动器启动。
后来有很长一段时间没有用过MySQL,今天再用的时候发现无法启动了,于是就把之前的进行了卸载,重新用Homebrew安装了一遍。
\nsudo rm /usr/local/mysql |
brew install mysql |
export PATH=$PATH:/usr/local/mysql/bin |
mysql -uroot |
今天在 Mac 上安装 MySqldb 和 pylibmc 的时候遇到两个问题,在这里记录一下。
\n安装 MySqldb 时,报:
\nFailed building wheel for MySQL-python |
产生原因,升级了Xcode,但是Xcode还没有启动过,里边的一些插件还没有升级。
\n解决方法,启动Xcode,让Xcode完成升级。
\n然后执行 xcode-select --install
等待完成后即可。
UPDATE AT: 2018-05-02
\n今天解决时问题时,上边的方法也不起作用了,stackoverflow 找到了一个方法:
\nsudo env LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" pip install MySQL-python
第二个问题是再安装 pylibmc 的时候报
\n./_pylibmcmodule.h:42:10: fatal error: 'libmemcached/memcached.h' file not found |
产生原因,Mac 使用 brew 安装的 memcached 的路径和 pylibmc 认为的默认路径不一致,所以需要指定一下路径。
\n$ which memcached |
在遇到困难或问题时,员工总会寻找各种理由来证明不是自己的问题,然后将责任推到其他人或事上。道理也并不复杂,那就是人的本性中始终都在重复的一个永恒的主题:规避风险。
\n在我们的传统教育中,缺乏一种培养孩子独立承担和解决问题的意识。
\n追求成功的领导要视管理者能否有效掌控源源不绝的“背上猴子”(monkey-on-the-back)。
\n管理者的贡献来自于他们的判断力与影响力,而非他们所投入的个人时间与埋头苦干。
\n管理者的绩效表现则是许多人群策群力的结果,这些人包括组织内部与外部的人,管理者惟有通过判断与影响才能加以控制。
\n管理者必须借着巧妙运用时间管理的内容与时机,尽可能增加自己的可支配时间(这些时间必须用于完成必要的判断)。
\n管理者的时间管理包括4大要素
\n老板与组织所指派的任务有惩罚执法在背后撑腰,如何恰当地运用自己的时间便成了最主要的考虑因素。
\n“猴子”就是双方谈话结束后的下一个步骤。猴子不是问题、项目、计划或机会;猴子只是解决问题、进行项目计划或是投入机会的下一个步骤、下一个措施、下一个行动步骤。
\n项目是包含一个以上的阶段流程。
\n每一只猴子都会有两边的人马介入——一方负责解决,另一方则是监督。
\n展开下一个步骤的人就有猴子。
\n老板花钱聘请管理者,便是要他们负责确定正确的人在正确的时间完成正确的事情。
\n下属占用的时间从猴子成功地从下属的背上跳到主管背上那一刻展开,除非猴子能回到照顾喂养它的正确饲养人身上,否则下属将永远占用你的时间。
\n任何能控制下属占用时间的人,就能够增加他们的可支配时间,让他们能够处理优先的工作或私人事务。
\n接受这只猴子的同时,你也自甘成为下属的下属。
\n管理者会累积无数的猴子等待照顾——在原本就忙碌异常的上班时间,其原因出在他们一开始就不明了猴子是如何往上爬到他们的背上。
\n“不服从”是管理与被管理者(上司与下属)关系里面一个相当重要的因素:没有这套规则,你就失去了一个重要的依据。
\n用“我们”来开头,轻而易举地让你进入这种思考模式,认为这是大家共同的问题。换言之,他安排猴子一开始就踩着你们两个人的背往上爬——一只脚踩在你的背上,另一只脚则在他的背上。
\n有两种方法可让你避免去背负别人的猴子。一种方式是训练猴子不要抬错脚,但更好的方法是,一开始便不要让它们把脚放在你的背上。
\n当团队成员对你说:“领导,我们有问题。”此人犯了越俎代疱的错误。你的下属没有立场替所有团队成员发言而说出:“我们有问题’这样的话。
\n下属向管理者报告时,惟一的正确发言方式就是:“我有问题。”如果他说的是:“我们有问题。”那么,他就是越俎代庖。
\n无论问题是什么,下属永远是承接下一个步骤的那一方。
\n如果你的管理工作进度落后,你愈赶得上进度,反而会更加遥遥落后。
\n在决定猴子属于谁之前,根本就不要让猴子跳到你的背上。如果这是下属的猴子,那么,猴子就是他们的下一个步骤。
\n不要弄丢东西的不二法则便是不要归档——把猴子交给下属去归档。
\n任何时候,我帮你解决你的问题时,你的问题绝不能变成是我的问题。
\n我必须重申一遍,要求下属出现,要求他在会议上有工作成果报告,这是相当合理的要求。
\n先安排讨论时间是为了减少延误的可能性。下属会重视你要检查的东西,而不是你期待的每件事。
\n挫折才是真正要人命。工作过量从来不会要你的命。你绝不可能用工作过量来害死一个人。
\n在组织中,我们需要的是独当一面,而非事事依赖上司的员工,能够自动自发者——采取必要的行动完成任务。
\n如果你期待员工在一个相辅相成的团队中,能够独当一面,千万不要帮他们做他们分内的事。当下属前来寻求你的协助时,通常他们要的不是协助,他们找的是杀人武器上印有你的指纹。
\n你应该帮帮自己和下属的忙。下一次你给他们其他猴子时,先约定好两人开会讨论“事情进行得如何”的时间。这个日期不需要是任务或项目完成的日期。
\n这意味着下属应该到主管的办公室去喂食猴子;主管不应该自己去寻找濒临饿死边缘的猴子。这样会让下属紧张兮兮,在此情况下,下属通常无法全力以赴。
\n组织实务上的基本原则便是,资深管理者不该在未知会直属部下之前,便绕过下属直接对后者的下属宣布指示(明显的例外是,攸关生死的情况)。
\n你不可能以扛下下属责任的方式来教导员工尽责。不过,我还是要鼓励他尽责,同时采取他迫使我去做的下一个步骤。
\n一个基本的领导原则,即职责总是以时间为优先,而非准备就绪。
\n「我们没有出任何问题,如果我必须提出看法的话,我们从来没有出过问题;问题不是出在你身上,便是出在我身上,但绝对不是我们的问题。所以第一件事,我们应该先弄清楚代名词,看看这是谁的问题。如果是你的问题,我很乐意协助你处理。但如果问题出在我身上,我希望你也会协助我。但它不是我们的问题。现在问题是什么?」
\n不要用纸、电子邮件,它们无法传递或创造彼此之间的了解。对话是惟一能够增进双方了解的工具。
\n带着你的下属一起进行,通常这是一种很好的管理做法,这么做,猴子将不会乱窜。
\n猴子一旦迷途,它会冲动地往上爬。它只想跑到上面,而不是回家。
\n光对结果有所承诺,其成就不会比新年愿望高出多少,而且还会留下恶劣的记录。
\n强调目标而忽略行动的人,根本就是无视于因果关系的科学原则。他们认为目标等于原因,而结局就是结果。
\n对目标有所承诺,不见得可以有效产生圆满的结果。只有针对完成目标采取的行动,才会有效达成结果。
\n管理者要正确针对自己的目标提出承诺,包含未来预定完成的时间表。
\n陈述下一个步骤时,应该以可量化的语句来表达行动,这样执行必要措施时的模棱两可才会降低,而且表现才能改进。
\n建立对员工的信赖时,最大的障碍是来自于你恐惧员工可以独当一面。
\n尽可能在下属处理的范围内,给与对等的责任与行动自由。让他们独立工作,但在他们需要你协助喂食时,务必要在他们身边。
\n只要你将猴子交付给某人,务必要排好追踪的会议时间表。这可让你将不预期的干预降至最低,掌控每天的行程。这也是管理猴子和确定它们跟好主人的惟一方法。
\n喂养猴子的六大规则
\n我上班有个习惯是比其他人至少早45分钟到公司,可以利用这段时间安静的写会代码或者学习一会。
\n不知道大家上班的动力都是什么,说起来你可能不信,促使我早早来公司的一个比较大的动力是「吃」。
\n就拿最近3年来说,之前在光华路办公的时候,那时候公司提供自助早餐,每天到公司后我都会为自己精心制作一份早餐,我会在上班路上想好今天的早餐如何搭配:吃几个煎蛋、面包片要烤到什么程度、今天的沙拉要用哪种酱等等。
\n\n\n\n后来公司搬到了望京,早上不再有自助早餐,又开始寻摸新的早餐吃食,于是发现了公司对面的汉堡王,办月卡后可以用非常低的价格买到早餐汉堡和一杯咖啡,而且汉堡种类很多,所以那段时间的动力变成了今天吃哪款汉堡。再后来大概是疫情期间服务很差的原因没有再续汉堡月卡了,又开始琢磨其他的东西吃,中间尝试过便利蜂的早餐、速食鸡胸肉等等。
\n关于汉堡王可以看这个:https://jiapan.me/2022/recent-breakfast-burger-king/
\n今年年初的时候开始换着花样在网上买各种早餐面包,不过较吸引我的并不单纯是面包,更多是来自到公司后用公司咖啡机的浓缩咖啡兑上冰农夫山泉后得到的冰美式的魅力,冰美式跟各类面包很搭。最近的搭配是山姆的杂粮奶酪包,吃到奶酪夹心的时候非常幸福,因为我到公司后整个楼层几乎没人,在大部分没有紧急的事情要处理的情况下,我会也不开电脑,也不看手机,在工位上细细品尝我的面包和咖啡,安静地吃顿早餐。
\n人,总该有点盼头吧。
\n最后再说说为什么我非要到公司后才吃早饭,而不能在家吃了再来公司?
\n这是个悲伤的故事,因为我有慢性甲状腺炎,每天早上要吃优甲乐。这个药的限制是吃药后半小时时间内不能吃东西,所以迫不得已只能到了公司才能吃早饭。
\n"},{"title":"MySQL创建utf-8格式数据库","url":"/2016/MySQL%E5%88%9B%E5%BB%BAutf-8%E6%A0%BC%E5%BC%8F%E6%95%B0%E6%8D%AE%E5%BA%93/","content":"CREATE DATABASE `test2` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; |
create database test2 DEFAULT CHARACTER SET gbk COLLATE gbk_chinese_ci; |
图数据库中使用以下概念来存储数据:
\nMATCH (ee:Person) |
Person
标签的单节点模式,并将匹配项分配给变量 ee
name
属性与 Emil
进行比较MATCH (ee:Person)-[:KNOWS]-(friends) |
Person
的节点开始模式KNOWS
关系(方向不限)MATCH (n:Person) |
或者
\nMATCH (n) |
匹配 :Car
或 :Person
标签
MATCH (n) |
匹配 :Car
与 :Person
标签
MATCH (n) |
MATCH (a:Person) |
返回属性 from
的值为 Sweden
的每个节点(和他们的关系)
Johan 正在学习冲浪,他想认识他的朋友中爱好冲浪的朋友
\nMATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer) |
每个节点都有一个内部的自增 ID,可以通过 <
, <=
, =
, >=
, <>
和 IN
操作进行查询。
通过 ID 查询
\nMATCH (n) |
查询多个 ID
\nMATCH (n) |
根据 ID 查询关系
\nMATCH ()-[n]-() |
CREATE (ee:Person { name: "Emil", from: "Sweden", klout: 99 }) |
Person
的新节点赋值给 ee
MATCH (ee:Person) WHERE ee.name = "Emil" |
Emil
赋给 ee
(a)-[:Label {key: value}]->(b)
MATCH (n), (m) |
或者使用 MERGE
,这样可以确保关系只创建一次
MATCH (n:User {name: "Allison"}), (m:User {name: "Emil"}) |
CREATE (n:Actor:Director) |
添加新的 owns
属性(如果已存在则执行修改)
MATCH (n) |
警告:如下操作会删除之前的属性并添加 plays
和 age
属性
MATCH (n) |
警告:如果 plays
或者 age
属性已经存在的情况下会被覆盖。
MATCH (n) |
MATCH (n) |
MATCH (n) |
或者
\nMATCH (n) |
给 id 为 7 和 8 的节点添加 :Food
标签
MATCH (n) |
MERGE (n:Person {name: "Rik"}) |
为了删除一个节点(比如,id=5),我们需要先删除他们的关系,然后才可以删除节点。
\nMATCH (n)-[r]-() |
2.3+
之后的简便写法:
MATCH (n) |
MATCH (n) |
或者
\nMATCH (n) |
MATCH (n) |
从全部节点上删除 :Person
标签
MATCH (n) |
从带有 :Food
和 :Person
标签的节点中删除 :Person
标签
MATCH (n) |
从带有 :Food
和 :Person
标签的节点中删除这两标签
MATCH (n) |
MATCH (n) |
2.3+
之后的简便写法:
MATCH (n) DETACH DELETE n |
在查询语句前使用 PROFILE
或者 EXPLAIN
PROFILE
:显示执行计划,查询信息和数据库命中。如:Cypher version: CYPHER 3.0, planner: COST, runtime: INTERPRETED. 84 total db hits in 32 ms.
EXPLAIN
:显示执行计划和查询信息。如:Cypher version: CYPHER 3.0, planner: COST, runtime: INTERPRETED.
全部节点数量
\nMATCH (n) |
全部关系数量
\nMATCH ()-->() |
最多返回 2 个 from
属性值为 Sweden
的节点(及其关系)
MATCH (a:Person) |
使带有 Person
标签节点的 name
属性值唯一
CREATE CONSTRAINT ON (n:Person) |
DROP CONSTRAINT ON (n:Person) |
投资组合理论和风险平价模型是两种与投资组合管理相关的重要概念,是金融领域中用于优化投资组合的方法。
\n投资组合理论是由美国经济学家哈里·马科维茨(Harry Markowitz)于20世纪50年代提出的理论框架,也被称为现代投资组合理论(Modern Portfolio Theory,MPT)。该理论旨在帮助投资者在风险和收益之间取得最佳平衡。投资组合理论的核心思想是通过将多种资产组合在一起,以最小化给定预期收益水平下的投资组合风险,或在给定风险水平下最大化预期收益。
\n想象一下,你有一个盒子,里面装着各种不同的玩具,比如娃娃、小车和积木。每个玩具就像是不同的投资。现在,假设你想保护你的玩具并确保它们的价值随着时间增长。
\n投资组合理论就像是一种决定你的盒子里应该有多少个不同玩具的方法。你要选择适当的玩具组合,这样你才能获得最好的结果。
\n但是,这里有个诀窍:不同的玩具有不同的风险和回报。有些玩具可能更有价值,但风险也更大,而其他的可能更安全,但增长速度较慢。所以你需要决定你愿意承担多少风险。
\n投资组合理论帮助你找到合适的平衡点。它建议你选择一种玩具组合,这样你就可以把风险分散开来。这意味着如果一个玩具表现不好,其他的玩具仍然可以让你获得回报。
\n简而言之,投资组合理论就是帮助你选择合适的玩具组合,以平衡风险,并让你的盒子里的玩具保持增值。
\n假设你有1000美元,你有三种不同的投资选项:股票、债券和黄金。
\n现代投资组合理论认为,投资者可以通过合理地分配资金来平衡风险和回报。
\n首先,你需要了解每种投资的预期回报和风险。假设股票的预期回报是10%,债券是5%,黄金是3%。同时,股票的风险最高,债券次之,黄金的风险最低。
\n现代投资组合理论建议你根据你的风险承受能力和目标来分配资金。假设你对风险比较保守,你可以将60%的资金分配给债券,30%分配给股票,剩下的10%分配给黄金。
\n通过这样的分配,你在投资组合中平衡了风险和回报。债券的较高配比可以提供稳定的回报,股票的适度配置可以获得更高的回报,黄金的配置可以提供一定的保值功能。
\n现代投资组合理论的关键思想是通过将资金分配到不同的资产上,以实现风险的分散化。这样,即使某个资产表现不佳,其他资产仍可以为你的投资组合提供回报。
\n风险平价模型(Risk Parity Model)是一种投资组合管理方法,旨在通过平等分配投资组合中不同资产的风险,实现更平衡的风险暴露。与传统的投资组合管理方法相比,风险平价模型更加关注风险分散和资产间的相关性。
\n风险平价起源自一个目标收益率为10%、波动率为10%~12%的投资组合,是美国桥水创始人瑞·达利欧在1996年创立的一个投资原则,既全天候资产配置原则。
\n现在,想象一下你有一张画纸,上面有很多不同的颜色。每种颜色就像是投资中的不同资产,比如红色代表股票,蓝色代表债券,黄色代表房地产等等。
\n风险平价模型就是一种方法,让你在画纸上均匀涂上不同的颜色。这样,每种颜色(也就是每种资产)都有相同的风险,就像画纸上每个区域的颜色一样多。
\n为什么要这样做呢?因为不同的颜色(或资产)有不同的风险和回报。有些颜色可能非常亮,表示它们的风险更高,但潜在回报也更大。而有些颜色可能相对较暗,表示它们的风险较低,但潜在回报也较小。
\n风险平价模型帮助你确保你的画纸上每个颜色(或资产)的风险都是一样的。这样,如果一个颜色表现不好,其他颜色仍然可以给你带来回报。
\n简而言之,风险平价模型就是让你在画纸上均匀地涂上不同的颜色,以确保不同资产的风险是平衡的,并让你的投资更加稳定。
\n假设你有1000美元,你想将其投资于两种不同的资产:股票和债券。
\n股票通常风险较高,但潜在回报也更高,而债券被认为更安全,但回报较低。
\n在风险平价模型中,你不仅仅是平均分配你的资金到股票和债券上(各500美元),而是根据每种资产的风险来分配你的投资。
\n假设股票的风险更高,你决定将70%的投资分配给债券,30%分配给股票。这种分配是根据每种资产的风险贡献应该相等的理念来确定的。
\n通过这样做,你在投资组合中平衡了风险。如果股票表现不佳,对债券的较高配置可以帮助抵消损失,并为你的整体投资提供更稳定性。另一方面,如果股票表现出色,较小的配置也不会对整体投资组合的表现产生太大影响。
\n风险平价模型旨在通过考虑不同资产的风险来实现资产间的平衡。它帮助你进行投资多样化,并更有效地管理风险。
\n\n\n之前在遇到字符串编码问题的时候都是跑到网上现去查资料,而且一直分不清
\ndecode
和encode
到底那个是解码,那个是编码,每次用的时候还要查一下文档,本次就做一个了断吧!
Python 内部字符串编码为 unicode
,因此在编码转换时,通常使用 unicode
作为中间编码, 先将其他编码的字符串解码(decode)成 unicode
,再从 unicode
编码(encode)成另一种编码。
世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。
\n可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是unicode
,就像它的名字都表示的,这是一种所有符号的编码。
decode
的作用是将其他编码的字符串解码成 unicode
编码。
如 str.decode('gbk')
,表示将 gbk
编码的字符串解码成 unicode
编码。
encode
的作用是将 unicode
字符串编码成其他编码的字符串。
如 str.encode('gbk')
, 表示将 unicode
编码的字符串编码成 gbk
编码。
\n\n因此转码的时候一定先搞明白,字符串是什么编码,然后
\ndecode
成unicode
,再然后encode
成其他编码。
如果这样写 s = u'中国'
,该字符串的编码就被指定为了 unicode
了,即 Python
的内部编码,而与代码文件本身编码无关,因此对于这种情况做编码转换,只需直接使用 encode
方法将其转换成指定编码即刻。
如果对一个 unicode
字符串进行解码将会报错,所以可以使用 isinstance(s, unicode)
来判断是否为 unicode
。
非 unicode
编码的字符串使用 encode
会报错。
unicode(str, 'gbk')
和 str.decode('gbk')
是一样的,都是将 gbk
编码的字符串转为 unicode
编码。
唉,想起来之前有个非常耻辱的事,就是有次面试的时候对方问我 Python 中默认字符编码是什么,我回答的是: ascii
\n还问我Python中如何进行编码和解码,其实我知道是用 decode
和 encode
但当时真的分不清楚哪个是做什么用的,所以忘了当时回答的对不对了。
为了让自己把 decode
和 encode
分清楚,我想了个办法, decode
开头发音是 弟(di)
,所以弟就应该有个姐姐,所以 decode
就是解码,另外一个 encode
就自然是编码了。哈哈哈。
对方还问了我,utf8
和 unicode
有什么区别,后来查了下资料,结论是: UTF-8是Unicode的实现方式之一。 详情见阮一峰老师的博客
在Python中当函数被定义时,默认参数只会运算一次,而不是每次被调用时都会重新运算。
\ndef add_to(num, target=[]): |
你应该永远不要定义可变类型的默认参数,除非你知道你正在做什么。你应该像这样做:
\ndef add_to(element, target=None): |
另一种三元运算符:
\n#(返回假,返回真)[真或假] |
例子:
\nfat = True |
这之所以能正常工作,是因为在Python中,True等于1,而False等于0,这就相当于在元组中使用0和1来选取数据。
\n上面的例子没有被广泛使用,而且Python玩家一般不喜欢那样,因为没有Python味儿(Pythonic)。这样的用法很容易把真正的数据与true/false弄混。
\n另外一个不使用元组条件表达式的缘故是因为在元组中会把两个条件都执行,而 if-else 的条件表达式不会这样。
\n例如:
\ncondition = True |
这是因为在元组中是先建数据,然后用True(1)/False(0)来索引到数据。 而if-else条件表达式遵循普通的if-else逻辑树, 因此,如果逻辑中的条件异常,或者是重计算型(计算较久)的情况下,最好尽量避免使用元组条件表达式。
\n当你在一个字典中对一个键进行嵌套赋值时,如果这个键不存在,会触发keyError
异常。 defaultdict
允许我们用一个聪明的方式绕过这个问题。 首先我分享一个使用dict
触发KeyError
的例子,然后提供一个使用defaultdict
的解决方案。
问题:
\nsome_dict = {} |
解决方案:
\nimport collections |
你可以用json.dumps打印出some_dict,例如:
\nimport json |
列表辗平
\n您可以通过使用itertools包中的itertools.chain.from_iterable轻松快速的辗平一个列表。下面是一个简单的例子:
\na_list = [[1, 2], [3, 4], [5, 6]] |
有两个字典:
\nuser = {'name': "Trey", 'website': "http://treyhunner.com"} |
现在想合并两个字典,得到一个新的字典,要求:
\n以上两个字典合并结果为:
\n{'website': 'http://treyhunner.com', 'name': 'Trey', 'page_name': 'Profile Page'} |
Python 3 中最优雅的实现方法:
\ncontext = {**defaults, **user} |
Python 2 中:
\ncontext = {} |
这里我们创建了一个新的空字典,并使用其update方法从其他字典中添加元素。请注意,我们首先添加的是defaults字典中的元素,以保证user字典中的重复键会覆盖掉defaults中的键。
\ncontext = defaults.copy() |
context = dict(ChainMap(user, defaults)) |
def safe_division(number, divisor, *, ignore_overflow=False, ingore_zero_division=False): |
参数列表里的 *
号,标志着位置参数结束,之后的参数都只能以关键字形式来指定。
save_division(10, 0, False, True) # error
save_division(10, 0, ignore_zero_division=True) # ok
Python 2 并没有明确的语法来定义这种只能以关键字形式指定的参数。不过我们可以在参数列表中使用 **
操作符,并且领函数遇到无效的调用时抛出TypeErrors,这样就可以实现与Python 3 相同的功能了。
为了使Python 2 版本的safe_division函数具备只能以关键字形式来指定的参数,我们可以先令该函数接受 **kwargs
参数,然后用 pop
方法把期望的关键字从 kwargs
字典里取走,如果字典的键里面没有那个关键字,那么 pop
方法的第二个参数就会成为默认值。最后为了防止调用者提供无效参数值,我们需要确认 kwargs
字典里面已经没有关键字参数了。
# Python 2 |
safe_division(1, 0, False, False) # error
safe_division(0, 0, unexpected=True) # error
save_division(10, 0, ignore_zero_division=True) # ok
nums = [] |
foo = 'foo' |
foo = 'foo' |
items = 'a b c d' # 首先指向字符串... |
\n\n重复使用命名对效率并没有提升:赋值时无论如何都要创建新的对象。然而随着复杂度的 提升,赋值语句被其他代码包括 ‘if’ 分支和循环分开,使得更难查明指定变量的类型。 在某些代码的做法中,例如函数编程,推荐的是从不重复对同一个变量命名赋值。Java 内的实现方式是使用 ‘final’ 关键字。Python并没有 ‘final’ 关键字而且这与它的哲学 相悖。尽管如此,避免给同一个变量命名重复赋值仍是是个好的做法,并且有助于掌握 可变与不可变类型的概念。
\n
\n\n如果一个函数接受的参数列表具有 相同的性质,通常把它定义成一个参数,这个参数是一个列表或者其他任何序列会更清晰。
\n
\n\n当一个函数在其正常过程中有多个主要出口点时,它会变得难以调试和返回其 结果,所以保持单个出口点可能会更好。这也将有助于提取某些代码路径,而且多个出口点 很有可能意味着这里需要重构。
\n
def complex_function(a, b, c): |
for index, item in enumerate(some_list): |
filename = 'foobar.txt' |
four_nones = [None] * 4 |
four_lists = [[] for __ in xrange(4)] |
letters = ['s', 'p', 'a', 'm'] |
s = set(['s', 'p', 'a', 'm']) |
在下列场合在使用集合或者字典而不是列表,通常会是个好主意:
\n集合体中包含大量的项
你将在集合体中重复地查找项
你没有重复的项
if attr == True: |
# 检查值 |
d = {'hello': 'world'} |
d = {'hello': 'world'} |
def append_to(element, to=[]): |
你可能认为
\nmy_list = append_to(12) |
实际结果为
\n# [12] |
当函数被定义时,一个新的列表就被创建一次 ,而且同一个列表在每次成功的调用中都被使用。
\n当函数被定义时,Python的默认参数就被创建 一次,而不是每次调用函数的时候创建。 这意味着,如果你使用一个可变默认参数并改变了它,你 将会 在未来所有对此函数的 调用中改变这个对象。
\ndef create_multipliers(): |
你期望的结果
\n0 |
实际结果
\n8 |
五个函数被创建了,它们全都用4乘以 x 。
\nPython的闭包是 迟绑定 。 这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。
\n这里,不论 任何 返回的函数是如何被调用的, i 的值是调用时在周围作用域中查询到的。 接着,循环完成, i 的值最终变成了4。
\ndef create_multipliers(): |
最一般的解决方案可以说是有点取巧(hack)。由于 Python 拥有为函数默认参数 赋值的行为,你可以创建一个立即绑定参数的闭包,像下面这样:
\ndef create_multipliers(): |
或者,可以使用 function.partial
函数
from functools import partial |
在机器学习中,回归模型和分类模型是两种常见的预测模型,它们的主要区别在于其预测目标和输出类型。
\n回归模型就像是一个预测机器,可以帮助我们猜测事物的未来。假设你喜欢吃冰淇淋,而冰淇淋的价格通常会随着天气变化而变化。现在,我们可以观察天气情况和冰淇淋的价格,然后用这些信息来猜测未来的价格。
\n比如,如果明天是个炎热的夏天,天气很热,那么冰淇淋的价格可能会比较高,因为很多人想要买冰淇淋来解暑。相反,如果明天是个寒冷的冬天,天气很冷,那么冰淇淋的价格可能会比较低,因为很少人会想要吃冰淇淋。
\n回归模型就是通过观察过去的天气和冰淇淋价格的关系,来预测将来的价格。它会考虑到很多因素,例如天气、季节和需求,然后给我们一个猜测的价格。虽然它不能百分之百准确地猜测价格,但它可以给我们一个大概的预测,帮助我们做决策。
\n分类模型就像是一个分类小助手,可以帮助我们将东西归类。想象一下,你有很多玩具,例如球、娃娃和积木。现在,你想要把它们分类整理,把球放在一起、把娃娃放在一起,积木也放在一起。
\n分类模型就是帮助我们做这个分类工作的机器。它会观察玩具的特点,比如形状、颜色和材质,然后根据这些特点把它们分成不同的类别。就像是在玩玩具时,你可以根据它们的外观和特点来决定它们应该放在哪个盒子里。
\n分类模型可以帮助我们在很多不同的情况下进行分类,比如识别动物、区分水果、辨别颜色等。它可以根据事物的特征将它们分成不同的组别,让我们更好地理解和组织世界。
\n今天在启动一个 Centos 上的 Nginx 时,死活启不起来,配置文件中 listen 9090 端口,看 log 启动时会报:2018/03/06 16:54:31 [emerg] 7984#0: bind() to 0.0.0.0:9090 failed (13: Permission denied)
。
经过一番查询,发现是因为 SELinux 导致的,平时用到的不多,直接将其关闭即可。
\n查看状态
\n/usr/sbin/sestatus -v ##如果SELinux status参数为enabled即为开启状态 |
or
\ngetenforce ##也可以用这个命令检查 |
关闭SELinux:
\n临时关闭(不用重启机器):
\nsetenforce 0 ##设置SELinux 成为permissive模式 |
修改配置文件需要重启机器:
\n修改/etc/selinux/config 文件 |
重启机器即可
\n看了下系统中并没有进程在占用 9090 端口。最后通过 http://www.err123.com/2017/08/29/nginx-emerg-bind-to-0-0-0-0-8081-failed-13-permission-denied/?lang=en 这篇文章的最后一个方案解决了这个问题。
已上外链已失效。
\n大致看了下 SELinxu 的作用一个 Linux 的强化方案,大部分情况下是需要启动的 https://www.zhihu.com/question/20559538。
\n"},{"title":"SOLID:面向对象设计的五个基本原则","url":"/2020/SOLID-Design-Principles/","content":"\n先来看下维基百科对 SOLID 的介绍:
\n\n\n在程序设计领域,SOLID 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。
\n
SOLID
是以下五个单词的缩写:
单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。
\n可以从两个角度来理解单一职责原则:
\n对于这两种理解方式,我分别举例来说明。
\n这里的模块可以看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。
\n来看下边的代码:
\npublic class UserInfo { |
站在不同的应用场景、不同阶段的需求背景下,对 UserInfo
类的职责是否单一的判定,可能都是不一样的:
UserInfo
现在的设计就是合理的。UserInfo
中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。UserInfo
进行拆分,将跟身份认证相关的信息(比如,email、telephone 等)抽取成独立的类。\n\n在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。
\n
我们这里以一个矩形类 Rectangle
为例,如图所示:
Rectangle
有两个方法:
draw()
area()
现在有两个应用程序要依赖这个 Rectangle
类:
在计算机屏幕上绘图是一件非常麻烦的事情,所以对于绘图这个需求来说,需要依赖专门的 GUI 库。一个 GUI 库可能有几十 M 甚至数百 M。
\n本来几何计算程序作为一个纯科学计算程序,主要是一些数学计算代码,现在程序打包完,却不得不把一个不相关的 GUI 库也打包进来。本来程序包可能只有几百 K,现在变成了几百 M。
\n当图形界面应用程序不得不修改 Rectangle
类的时候,还得重新编译几何计算应用程序,反之亦然。这个情况下,我们就可以说 Rectangle
类有两个引起它变化的原因。
当然,这里用前一种理解也是可以的(一个类或者模块只负责完成一个职责):Rectangle
承担了两个职责,一个是几何形状的计算,一个是在屏幕上绘制图形。
我们可以将 Rectangle
拆分成两个类:
GeometricRectangle
: 这个类负责实现图形面积计算方法 area()
Rectangle
:只保留单一绘图方法 draw()
现在绘制长方形的时候可以使用计算面积的方法,而几何计算应用程序则不需要依赖一个不相关的绘图方法以及一大堆的 GUI 组件。
\n拆分后的类图如下所示:
\n\n从事过 Java Web 开发的老码农都经历过下边这 3 个开发阶段。
\n阶段 1:请求处理以及响应的全部操作都在 Servlet
里,Servlet
获取请求数据,进行逻辑处理,访问数据库,得到处理结果,根据处理结果构造返回的 HTML
。
阶段 2:于是后来就有了 JSP
,如果说 Servlet
是在程序中输出 HTML
,那么 JSP
就是在 HTML
中调用程序。
这个阶段,基于 JSP
开发的 Web 程序在职责上进行了一些最基本的分离:构造页面的 JSP
和处理逻辑的业务模型分离。
阶段 3:各种 MVC 框架的出现,MVC 框架通过控制器将视图与模型彻底分离。
\n\n有了 MVC,就可以顺理成章地将复杂的业务模型进行分层了。通过分层方式,将业务模型分为业务层、服务层、数据持久层,使各层职责进一步分离,更符合单一职责原则。
\n\n\n\n也是因为 MVC 框架的出现,才使得前后端开发成为两个不同的工种,前端工程师只做视图模板开发,后端工程师只做业务开发,彼此之间没有直接的依赖和耦合,各自独立开发、维护自己的代码。
\n
前边提到过,不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。我们可以通过一些侧面指标来指导我们的判断。
\n出现下面这些情况就有可能说明类的设计不满足单一职责原则:
\n\n\n开闭原则是所有设计原则中最有用的,因为扩展性是代码质量最重要的衡量标准之一。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。
\n
开闭原则的英文是 Open Closed Principle,缩写为 OCP。
\n开闭原则说的是:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的。
\n两者结合起来表述为:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
\npublic class Alert { |
以上代码中的 AlertRule
存储告警规则,可以自由设置。
Notification
是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。
NotificationEmergencyLevel
表示通知的紧急程度,不同的紧急程度对应不同的发送渠道。
业务逻辑主要集中在 check()
函数中:当接口的 TPS 超过某个预先设置的最大值时,以及当接口请求出错数大于某个最大允许值时,就会触发告警。
现在,如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。
\n主要的改动有两处:
\ncheck()
函数的入参,添加一个新的统计数据 timeoutCount
,表示超时接口请求数check()
函数中添加新的告警逻辑
|
如此进行的代码修改导致将导致以下问题:
\ncheck()
函数,相应的单元测试都需要修改粗暴一点说,当我们在代码中看到 if/else 或者 switch/case 关键字的时候,基本可以判断违反开闭原则了。
\n重构一下之前的 Alert
代码,让它的扩展性更好一些:
check()
函数的多个入参封装成 ApiStatInfo
类handler
的概念,将 if
判断逻辑分散在各个 handler
中public class Alert { |
现在我们基于重构之后的代码来实现每秒钟接口超时请求个数超过某个最大阈值就告警的新功能就方便多了。
\n不考虑调用方修改的情况下,实现方只需进行两处的改动:
\nApiStatInfo
类中添加新的属性 timeoutCount
TimeoutAlertHander
类调用方的改动也很简单:
\nTimeoutAlertHander
类的实例注册到 alert
对象中apiStatInfo
对象 设置 timeoutCOunt
的值。修改后代码如下:
\npublic class Alert { // 代码未改动... } |
重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler
类即可,不需要改动原来的 check()
函数的逻辑。而且,我们只需要为新的 handler
类添加单元测试,老的单元测试都不会失败,也不用修改。
开闭原则的设计初衷是:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。
\n通过上边举过的例子可以看出:添加一个新功能,不可能任何模块、类、方法的代码都不「修改」,这个是做不到的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。
\n同样一个代码改动,在粗代码粒度下,被认定为「修改」,在细代码粒度下,又可以被认定为「扩展」。
\n为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些「潜意识」可能比任何开发技巧都重要。
\n最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
\n结合了多态、依赖注入、基于接口而非实现通过 Kafka 来发送异步消息。
\n
|
实现开闭原则的关键是抽象。当一个模块依赖的是一个抽象接口的时候,就可以随意对这个抽象接口进行扩展,这个时候,不需要对现有代码进行任何修改,利用接口的多态性,通过增加一个新实现该接口的实现类,就能完成需求变更。
\n开闭原则可以说是软件设计原则的原则,是软件设计的核心原则,其他的设计原则更偏向技术性,具有技术性的指导意义,而开闭原则是方向性的,在软件设计的过程中,应该时刻以开闭原则指导、审视自己的设计:当需求变更的时候,现在的设计能否不修改代码就可以实现功能的扩展?如果不是,那么就应该进一步使用其他的设计原则和设计模式去重新设计。
\n写出支持「对扩展开放、对修改关闭」的代码的关键是预留扩展点:
\n\n\n话外音:这种技术视野的前提是需要在某个领域进行深耕。
\n
最后提醒一下,天下没有免费的午餐,有些情况下,代码的扩展性会跟可读性相冲突。很多时候,我们都需要在扩展性和可读性之间做权衡。
\n单一职责原则的英文是 Liskov Substitution Principle,缩写为 LSP。
\n官方一些的介绍:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
\n通俗地说就是:子类型必须能够替换掉它们的基类型。
\n通俗地详细点说:程序中,所有使用基类的地方,都应该可以用子类代替。
\n如下代码中,父类 Transporter
使用 org.apache.http
库中的 HttpClient
类来传输网络数据。子类 SecurityTransporter
继承父类 Transporter
,增加了额外的功能,支持传输 appId
和 appToken
安全认证信息。
public class Transporter { |
子类 SecurityTransporter
的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
这样看来里氏替换原则不就是简单利用了多态的特性吗?我们通过一个反例来看下这两者的区别:
\n\n\n通俗地说,接口(抽象类)的多个实现就是多态。多态可以让程序在编程时面向接口进行编程,在运行期绑定具体类,从而使得类之间不需要直接耦合,就可以关联组合,构成一个更强大的整体对外服务。
\n
我们对刚刚那个例子中 SecurityTransporter
类的 sendRequest()
方法稍加改造一下。
appId
或者 appToken
没有设置,我们就不做校验;appId
或者 appToken
没有设置,则直接抛出 NoAuthorizationRuntimeException
未授权异常。public class SecurityTransporter extends Transporter { |
改造之后的代码仍然可以通过 Java 的多态语法,动态地用子类 SecurityTransporter
来替换父类 Transporter
,也并不会导致程序编译或者运行报错。但是,从设计思路上来讲,SecurityTransporter
的设计是不符合里式替换原则的。
虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。
\n所以,判断子类的设计实现是否违背里式替换原则,还有一个小窍门:那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。
\n我们来看个违反历史替换原则的例子:
\nCircle
和 Square
继承了基类 Shape
,然后在应用的方法中,根据输入 Shape
对象类型进行判断,根据对象类型选择不同的绘图函数将图形画出来。
void drawShape(Shape shape) { |
这种写法的代码既常见又糟糕,它同时违反了开闭原则和里氏替换原则。
\nif/else
代码,就可以判断违反了(我们刚刚在上个部分讲过的)开闭原则:当增加新的 Shape
类型的时候,必须修改这个方法,增加 else if
代码。Shape
类型的时候,如果没有修改这个方法,没有增加 else if
代码,那么这个新类型就无法替换基类 Shape
。 要解决这个问题其实也很简单,只需要在基类 Shape
中定义 draw
方法,所有 Shape
的子类,Circle
、Square
都实现这个方法就可以了:
public abstract Shape{ |
上面那段 drawShape()
代码也就可以变得更简单:
void drawShape(Shape shape) { |
这段代码既满足开闭原则:增加新的类型不需要修改任何代码。也满足里氏替换原则:在使用基类的这个方法中,可以用子类替换,程序正常运行。
\n子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定,这也是我们常说的「Design By Contract」,中文翻译就是「按照协议(契约、约定)来设计」。
\n以下是三种常见的违背约定的情况:
\nsortOrdersByAmount()
订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount()
订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。null
;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null
。ArgumentNullException
异常,那子类的设计实现中只允许抛出 ArgumentNullException
异常,任何其他异常的抛出,都会导致子类违背里式替换原则。withdraw()
提现函数的注释是这么写的:「用户的提现金额不得超过账户余额……」,而子类重写 withdraw()
函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额子类的协议不能比父类更严格,否则使用者在用子类替换父类的时候,就会因为更严格的协议而失败。
\n在类的继承中,如果父类方法的访问控制是 protected
,那么子类 override
这个方法的时候,可以改成是 public
,但是不能改成 private
。因为 private
的访问控制比 protected
更严格,能使用父类 protected
方法的地方,不能用子类的 private
方法替换,否则就是违反里氏替换原则的。相反,如果子类方法的访问控制改成 public
就没问题,即子类可以有比父类更宽松的协议。同样,子类 override
父类方法的时候,不能将父类的 public
方法改成 protected
,否则会出现编译错误。
实践中,当你继承一个父类仅仅是为了复用父类中的方法的时候,那么很有可能你离错误的继承已经不远了。一个类如果不是为了被继承而设计,那么最好就不要继承它。
\n粗暴一点地说,如果不是抽象类或者接口,最好不要继承它。
\n如果你确实需要使用一个类的方法,最好的办法是组合这个类而不是继承这个类,这就是人们通常说的组合优于继承。
\nClass A{ |
接口隔离原则的英文是 SInterface Segregation Principle,缩写为 ISP。
\n这个原则是说:客户端不应该强迫依赖它不需要的接口。
\n我们可以从三个角度理解「接口」:
\n下面我们逐个进行说明。
\n在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。
\n举例说明:
\npublic interface UserService { |
删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,所以这个接口只限于给后台管理系统使用。如果我们把它放到 UserService
中,那所有使用到 UserService
的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。
参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外一个接口 RestrictedUserService
中,然后将 RestrictedUserService
只打包提供给后台管理系统来使用。
隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。
\n接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。
\n接口隔离原则提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
\n举例说明:
\npublic class Statistics { |
在上面的代码中,count()
函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等等。如果某个统计需求只涉及 Statistics
罗列的统计信息中一部分,而 count()
函数每次都会把所有的统计信息计算一遍,就会做很多无用功,势必影响代码的性能
按照接口隔离原则,我们应该把 count()
函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能:
public Long max(Collection<Long> dataSet) { //... } |
接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数或方法。
\n使用接口隔离原则,就是定义多个接口,不同调用者依赖不同的接口,只看到自己需要的方法。而实现类则实现这些接口,通过多个接口将类内部不同的方法隔离开来。
\n那么如果强迫用户依赖他们不需要的方法,会导致什么后果呢?
\n举例说明:
\n假如我们需要开发一个支持根据远程配置中心配置来动态更改缓存配置的缓存服务。
\n\n这个缓存服务 Client
类的方法主要包含两个部分:
get()
、put()
、delete()
这些,这些方法是面向调用者的reBuild()
,这个方法主要是给远程配置中心调用的但是问题是,Cache
类的调用者如果看到 reBuild()
方法,并错误地调用了该方法,就可能导致 Cache
连接被错误重置,导致无法正常使用 Cache
服务。所以必须要将 reBuild()
方法向缓存服务的调用者隐藏,而只对远程配置中心的本地代理开放这个方法。
我们可以进行如下调整:
\n实现类同时实现 Cache
接口和 CacheManageable
接口,其中 Cache
接口提供标准的 Cache
服务方法,应用程序只需要依赖该接口。而 CacheManageable
接口则对外暴露 reBuild()
方法。
使用接口隔离原则,就是定义多个接口,不同调用者依赖不同的接口,只看到自己需要的方法。而实现类则实现这些接口,通过多个接口将类内部不同的方法隔离开来。
\n\n\n单一职责原则和开闭原则的原理比较简单,但是,想要在实践中用好却比较难。而依赖倒置原则正好相反。依赖倒置原则用起来比较简单,但概念理解起来比较难。
\n
依赖倒置原则的英文是 Dependency Inversion Principle,缩写为 DIP。
\n依赖倒置原则说的是:高层模块不依赖低层模块,它们共同依赖同一个抽象,这个抽象接口通常是由高层模块定义,低层模块实现。同时抽象不要依赖具体实现细节,具体实现细节依赖抽象。
\n所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。
\n在具体讲解依赖倒置原则前,我们先来看几个与之有关的常见概念:控制反转、依赖注入、依赖注入框架。
\n控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。
\n框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。
\n这里的「控制」指的是对程序执行流程的控制,而「反转」指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员「反转」到了框架。
\n我们举个例子来看一下:
\npublic class UserServiceTest { |
在上面的代码中,所有的流程都由程序员来控制。如果我们抽象出一个下面这样一个框架,我们再来看,如何利用框架来实现同样的功能。
\npublic abstract class TestCase { |
把这个简化版本的测试框架引入到工程中之后,我们只需要在框架预留的扩展点,也就是 TestCase
类中的 doTest()
抽象函数中,填充具体的测试代码就可以实现之前的功能了,完全不需要写负责执行流程的 main()
函数了。
public class UserServiceTest extends TestCase { |
控制反转的方式有很多,除了依赖注入,还有模板模式等,我们常用的 Spring 框架主要是通过依赖注入来实现的控制反转。
\n下面我们来看看依赖注入。
\n依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧。
\n依赖注入用一句话来概括就是:不通过 new()
的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
这里给出一个例子,分别用非依赖注入和依赖注入来实现同一个需求:Notification
类负责消息推送,依赖 MessageSender
类实现推送商品促销、验证码等消息给用户。
代码如下:
\n// 非依赖注入实现方式 |
通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类(将 MessageSender
定义成接口)。
在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。
\n这个框架就是「依赖注入框架」。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
\n常见的依赖注入框架有:Google Guice、Java Spring、Pico Container、Butterfly Container 等。
\n\n\n框架的一个特点是,当开发者使用框架开发一个应用程序时,无需在程序中调用框架的代码,就可以使用框架的功能特性。比如:
\n
HTTP
协议端口,处理 HTTP
请求最后回到我们这部分的主角。
\n这条原则主要也是用来指导框架层面的设计,跟前面讲到的控制反转类似。
\n我们先用 Tomcat 来说明一下这个原则:Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个「抽象」,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
\n再用 JDBC 为例子说明一下依赖倒置原则:我们在 Java 开发中访问数据库,代码并不直接依赖数据库的驱动,而是依赖 JDBC。各种数据库的驱动都实现了 JDBC,当应用程序需要更换数据库的时候,不需要修改任何代码。这正是因为应用代码,也就是高层模块,不依赖数据库驱动,而是依赖抽象 JDBC,而数据库驱动,作为低层模块,也依赖 JDBC。
\n这里可能会存在一个误区:我们在日常的 Web 开发中, Service
层会依赖 DAO
层提供的接口,但这种依赖并不是依赖倒置原则!在依赖倒置原则中,除了具体实现要依赖抽象,最重要的是,抽象是属于谁的抽象。
最后再举一个依赖倒置原则的例子:
\nButton
按钮控制 Lamp
灯泡,按钮按下的时候,灯泡点亮或者关闭。按照常规的设计思路,我们可能会设计出如下的类图关系,Button
类直接依赖 Lamp
类。
这样设计的问题在于,Button
依赖 Lamp
,那么对 Lamp
的任何改动,都可能会使 Button
受到牵连,做出联动的改变。同时,我们也无法重用 Button
类。
解决之道就是将这个设计中的依赖于实现,重构为依赖于抽象。这里的抽象就是:打开关闭目标对象。
\nButton
定义一个抽象接口 ButtonServer
,在 ButtonServer
中描述抽象:打开、关闭目标对象Lamp
实现这个接口,从而完成 Button
控制 Lamp
这一功能需求通过这样一种依赖倒置,Button
不再依赖 Lamp
,而是依赖抽象 ButtonServer
,而 Lamp
也依赖 ButtonServer
,高层模块和低层模块都依赖抽象。Lamp
的改动不会再影响 Button
,而 Button
可以复用控制其他目标对象,比如电机,或者任何由按钮控制的设备,只要这些设备实现 ButtonServer
接口就可以了。
依赖倒置原则也被称为好莱坞原则:Don’t call me,I will call you.
\n遵循依赖倒置原则有这样几个编码守则:
\nOverride
)包含具体实现的函数。\n\n软件开发有时候像变魔术一样,常常表现出违反常识的特性,让人目眩神晕,而这正是软件编程这门艺术的魅力所在,感受到这种魅力,在自己的软件设计开发中体现出这种魅力,你就迈进了软件高手的大门。
\n
去年年底利用工作之余开发了一个进销存相关的 SAAS 项目,ORM 用的 SQLAlchemy
,并且进行了一些分表操作,这里来做个简单的记录(也只能是简单记录了,我是小半年前进行的分表调研)。
我没有直接使用 继承自 db.Model
的 ORM 类来操作数据库,而是在其之上又封装了一层,将更具体的一些数据库操作进行了封装。
举个例子,我有一个继承自 db.Model
的 Item
类,同时还有一个自己的 Item
类,然后在自己的 Item
类中引用继承自 db.Model
的类,为了防止名称冲突,在引用时我会将继承自 db.Model
的类叫做 SqlaItem
。
分表是如何实现的呢,我通过 SQLAlchemy
的 automap_base()
将数据库中所有表进行了映射,然后自己实现了分表函数,通过分表函数得到分表的名称,然后动态的拿到那个表所对应的 ORM。
直接看代码:
\n# -*- coding: utf-8 -*- |
所有分了表的类,都要通过 ab_cls 来获取表映射出来的对象。
\n还是以 Item
为例,看一下我的相关代码
@classmethod |
我通过这几个方法实现了获取分表映射的功能,在具体使用时,可以直接用 get_dao(user_id)
获取表映射(我是通过用户ID的规则进行的分表)。
随便看一个操作:
\n@classmethod |
我先通过 get_dao(user_id)
获取到这个用户数据所在的表的映射,然后就可进行各种 CURD
操作了。
也就是说,在我的项目中其实有两种获取 SqlaXxxx
的方法,如果没有分表,那么直接用继承自 db.Model
的类即可,如果是分了表的,就用动态映射出来的,所以后者实际上是不需要写继承自 db.Model
的类的,但是为了在初始化时生成所有表结构,我还是写了这些类,只不过这些类所对应的表都是分表中的第一张表,以 Item
为例
class Item(db.Model): |
这样的话,我在执行 create_db
时所有需要分表的第一个表都会被建好,这个时候,我只需要再写个简单的脚本,就可以帮我把剩余的表建出来了,因为我所有分表结尾都是以下划线1
或者01
组成的,意思是,如果表需要分成 10 个,那么对应的第一个表的名称就是 xxx_1
,如果需要分成 100 个,那么对应的第一个表的名称就是 xxx_01
,所以我根据这个规则写了生成剩余表的脚本,如下:
# -*- coding: utf-8 -*- |
这种方式有个弊端,在项目启动时就需要将所有表结构读入到内存中,直接的表现是启动比较慢,占用内存比较多。
\n我不觉得这是个最佳方案,所以如果有更好的方案或者有任何疑问请通过邮件(jiapan.china#gmail.com)的方式告诉我,谢谢。
\n"},{"title":"《软技能:代码之外的生存指南》 读书笔记","url":"/2017/Soft-Skills-reading-note/","content":"之前读书大多都囫囵吞枣,雁过无痕,所以这次打算留下点什么,其实我准备写的内容也谈不上什么读书笔记,就是把原文感觉不错的句子记下来,有时再加上一些自己的想法。
\n对于大多数软件开发人员来说,生产力都是一场巨大的斗争,也是阻碍你成为成功人事的最大障碍(没有之一)。
\n\n\n我自己就是一个有着严重拖延症的人,对时间的管理能力很差,作者提到会在后文给出一个解决方法,我们拭目吧,希望可以对我有所帮助。这句话又可以联想到其他一些事情,比如可以提高生产力的生产工具,有了趁手的生产工具(包括软件在内)绝对是可以事半功倍的,还有就是既然我们是程序员,能自动化的地方就不要手动去搞,重复性工作交给机器,我们来做创作性的工作就好。
\n
只有你开始把自己当作一个企业去思考时,你才能开始做出良好的商业决策。如果你已经习惯领取一份固定的薪酬,这会很容易导致你产生另一个心态 – 你只是在为某家公司打工。
\n\n\n这里的重点是「把自己当做企业去思考」,把公司作为自己的客户,将自己的地位转为主动,既然你把公司作为了客户,那么你就一定会有其他的潜在客户,所以你就需要学会营销自己。把自己当做一个企业去思考,就需要为自己做一些规划。
\n
你需要做到:
\n\n\n我也有必要专注特定类型的「客户」。
\n
要实现任何目标,都必须先知道目标是什么。
\n大目标并不需要这么具体,但是必须足够清晰,能够让你知道自己是在向它前进还是离它越来越远。
\n较小的目标可以让你航行在自己的轨道上,激励你保持航向朝着更大的目标前进。
\n\n\n这几句话摘抄自 3 个段落。在接触编程 5 年后的我看来,不同领域的编程思想千差万别,可能站在初级程序员的角度就是写一些增删改查操作,但是深入到一定层次后都是针对某个领域来进行编程,这时需要的不光是你的代码能力,还有你对这个领域的理解程度。比如你在金融行业,你就需要知道很多的金融行业的业务和处理流程,大数据行业,也要了解大数据的业务和解决方案。我说这两个行业的原因是因为我上家公司算是一家金融公司,为什么说算是一家金融公司,因为我觉得它的金融属性并不完全,对外提供的产品都是对其他公司的产品进行包装,这样对于程序员来说,很多底层的业务实际上是接触不到的(我单从程序员的角度说一说就够了,毕竟我觉得那是一家不错的公司)。现在我所在的这家公司以大数据业务为主,所以我给自己的目标是成为大数据领域的专家。再写点题外话,很早之前我是有另一个小目标的,就是成为 Python Web 的专家,但是后来越写越发现 Web 这东西就所能接触到的技术层面不会太深,后来也是比较幸运在几乎没有大数据知识的背景下来到现在这家公司,我觉得这算是一种缘分,给了我更高的追求空间。BigData 专家,我来了!
\n
在软件开发领域,我们大多时候都是与人而非计算机打交道。甚至我们所写的代码首先是供人使用,其次才是让计算机可以理解的。
\n\n\n这句话里边包含两个涵义,作者写到要与人打交道而非计算机,是要我们提升自己人际交际的能力,毕竟想要成为一个 Leader 这种能力是必不可少的。我最早选择程序员这个职业,天真的认为我只需要和计算机对话就够了,几乎不需要做我不擅长的与人交流这件事,但是后来发现我错了,拿最简单的例子来说,你和产品对需求时,如果你连自己的想法也说不出来,我觉得你在实现功能的时候,也很难按照产品的原意来进行。原文的另一个涵义是,你写的代码是给人读的,所以写代码的时候请遵守一些编码规范、命名规范,让别人读你的代码时谈不上赏心悦目,但不至于心里骂娘。这也是我更喜欢写 Python 而不是 Java 的原因。并且我所认识的大多数 Java 程序员是不在乎代码风格这件事的(我说的是事实,真心不是黑)。
\n
一但你贬低他人,削弱他们的成就感,在某种程度上就如同切断了他们的氧气补给,获得的回馈将完全是抓狂和绝望的。
\n\n\n哎呀,鼓励别人这件事我一直学不来怎么办~
\n
我们常常容易犯的一项错误就是,轻率的否决同事的想法,以便于可以提出自己的想法。然而随着你做出这样的错误判断,你往往会发现他们对你的想法充耳不闻,仅仅因为你让他们感觉自己是无足轻重的。
\n\n\n我非常厌恶在我还没说完话就否决我的想法然后自己开始高谈阔论的人,那些人不值得去尊敬。大多数时候他们只是想炫耀一下自己的见闻。当然我自己偶尔也会犯这种错误,今后我会努力改正这个缺点,即便对方的想法有再大的不足,我也尽量等对方说完后再来纠正。
\n
一项又一项的研究表名,奖励积极行为要比惩罚消极行为有效得多。如果你身处管理岗位,这是一条值得遵守的重要原则。如果你想激励他人做出最好的表现,或者希望达到改变的目的,你必须学会管住自己的舌头,只说些鼓励的画。
\n\n\n我经常在别人犯了错误之后抱怨,完成很漂亮的时候会发出赞赏,但是很少鼓励。以后要管住嘴,减少抱怨。有一句话我特别喜欢,原话我忘记了,大致意思是:不要抱怨别人笨,毕竟他们之前没有你这么优越的条件。
\n
你可能会害怕专攻软件开发的某个区域,担心自己陷入很窄的专业领域,从而与其他的工作机会绝缘。虽然专业化确实会把你关在一些工作机会的大门之外,但与此同时它将打开的机会大门要比你用其他方式打开的多得多。
\n\n\n术业有专攻,虽然编程是一大家,但是每个专业领域编写代码时的思考方式是有很大区别的。下文中作者用律师来举例,当我们聘用律师时,如果不傻的话,都会根据我们遇到的官司找这个方向的律师,很少有人聘用通才律师。
\n
待续。。。
\n","tags":["reading"]},{"title":"SpringBoot 中统一包装响应","url":"/2018/SpringBoot-%E4%B8%AD%E7%BB%9F%E4%B8%80%E5%8C%85%E8%A3%85%E5%93%8D%E5%BA%94/","content":"SpringBoot 中可以基于 ControllerAdvice
和 HttpMessageConverter
实现对数据返回的包装。
实现如下,先来写一个 POJO
来定义一下返回格式:
import com.example.demo.common.exception.base.ErrorCode; |
\n\n这里用到了
\nlombok
,lombok
的使用介绍不在本文范围内。
用一个 ResponseBodyAdvice
类的实现包装 Controller
的返回值:
以下是我以前的实现方式:
\nimport com.example.demo.common.RequestContextHolder; |
为什么要对返回类型是 String
时进行特殊处理呢?因为如果直接返回 new Response<>(body)
的话,在使用时返回 String
类型的话,会报类型转换异常,当时也没有理解什么原因导致的,所以最后使用了 jackson
对 Response
又做了一次序列化。
今天找到了导致这个异常的原因:
\n\n\n因为在所有的
\nHttpMessageConverter
实例集合中,StringHttpMessageConverter
要比其它的Converter
排得靠前一些。我们需要将处理Object
类型的HttpMessageConverter
放得靠前一些,这可以在Configuration
类中完成:
import org.springframework.context.annotation.Configuration; |
然后 FormatResponseBodyAdvice
就可以修改为如下实现:
import org.springframework.core.MethodParameter; |
比之前的实现方式优雅了很多而且不用再处理 jackson
的异常了。
写一个 Controller
来尝试一下:
@RestController |
请求这个端点得到结果:
\n{ |
说明我们的配置是成功的,同时可以在相应头中看到:
\ncontent-type: application/json;charset=UTF-8 |
如果是之前的实现方式,这里的值就是:
\ncontent-type: html/text |
也不太符合 restful
规范。
众所周知编程语言有强弱类型之分,进一步还有动态和静态之分。比如 Java、C# 是强类型的(strongly typed)静态语言,Javascript、PHP 是弱类型的(weakly typed)动态语言。
\n强类型静态语言常常被称为类型安全(type safe)语言,一般在会编译期间进行强制类型检查,提前避免一些类型错误。弱类型动态语言虽然也有类型的概念,但是比较松散灵活,而且大多是解释型语言,一般没有强制类型检查,类型问题一般要在运行期才能暴露出来。
\n强弱类型的语言各有优劣、相互补充,各有适用的场景。比如服务端开发经常用强类型的,前端 Web 界面经常会用 Javascript 这种弱类型语言。
\n\n对于服务 API 也有强弱类型之分,传统的 RPC 服务一般是强类型的,RPC 通常采用订制的二进制协议对消息进行编码和解码,采用 TCP 传输消息。RPC 服务通常有严格的契约(contract),开发服务器前先要定义 IDL(Interface Definition Language),用 IDL 来定义契约,再通过契约自动生成强类型的服务端和客户端的接口。服务调用的时候直接使用强类型客户端,不需要手动进行消息的编码和解码,gRPC 和 Apache Thrift 是目前两款主流的 RPC 框架。
\n而现在的大部分 Restful 服务通常是弱类型的,Rest 通常采用 Json 作为传输消息,使用 HTTP 作为传输协议,Restful 服务通常没有严格的契约的概念,使用普通的 HTTP Client 就可以调用,但是调用方通常需要对 Json 消息进行手动编码和解码的工作。在现实世界当中,大部分服务框架都是弱类型 Restful 的服务框架,比方说 Java 生态当中的 SpringBoot 可以认为是目前主流的弱类型 Restful 框架之一。
\n\n当然以上区分并不是业界标准,只是个人基于经验总结出来的一种区分的方法。
\n强类型服务接口的好处是:接口规范、自动代码生成、自动编码解码、编译期自动类型检查。强类型接口的好处也带来不利的一面:首先是客户端和服务端强耦合,任何一方升级改动可能会造成另一方 break,另外自动代码生成需要工具支持,而开发这些工具的成本也比较高。其次强类型接口开发测试不太友好,一般的浏览器、Postman 这样的工具无法直接访问强类型接口。
\n弱类型服务接口的好处是客户端和服务器端不强耦合,不需要开发特别的代码生成工具,一般的 HTTP Client就可以调用,开发测试友好,不同的浏览器、Postman 可以轻松访问。弱类型服务接口的不足是需要调用方手动编码解码消息、没有自动代码的生成、没有编译器接口类型检查、代码不容易规范、开发效率相对低,而且容易出现运行期的错误。
\n有没有办法结合强弱类型服务接口各自的好处同时又规避他们的不足呢?
\n我们的做法是在 Spring Rest 弱类型接口的基础上借助 Spring Feign 支持的强类型接口的特性实现强类型 Rest 接口的调用机制,同时兼备强弱类型接口的好处。
\n首先我们来介绍下 Spring Feign
,Spring Feign
本质上是一种动态代理机制(Dynamic Proxy),只需要我们给出 Restful API
对应的 Java 接口,它就可以在运行期动态的拼装出对应接口的强类型客户端。拼装出的客户端的结构和请求响应流程如下图所示:
Request Bean
,这个请求通过 Java 接口首先被动态代理截获Encoder
)进行编码,成为 Request Json
Request Json
根据需要可以经过一些拦截器(Interceptor
)做进一步处理Response Json
会被 HTTP Client 接收到Decoder
)解码为 Response Bean
Response Bean
通过 Java 接口返回给调用方整个请求响应流程的关键步骤是编码和解码,也就是 Java 对象和 Json 消息的互转,这个过程也被称为序列化和反序列化,另外一种叫法为「Json 对象绑定」。对于一个服务框架而言,序列化、反序列化器的性能对于服务框架性能影响是最大的,也就是说可以认为 Decoder
和 Encoder
决定了服务框架总体的性能。
虽然我们开发出来的服务是弱类型的 Restful 服务,但是因为有 Spring Feign
的支持,我们只要简单的给出一个强类型的 Java API 接口就自动获得了一个强类型客户端,也就是说利用 Spring Feign
我们可以同时获得强弱类型的好处(编译器自动类型检查、不需要手动编码解码、不需要开发代码生成工具、客户端和服务器端不强耦合),这样可以同时规范代码风格,提升开发测试效率。
我们可以在项目内为每个微服务提供两个模块,一个是 API 接口模块(如 mail-api
),另一个是服务实现模块(如 mail-svc
)。API接口模块内就是强类型的 Java API 接口(包括请求响应的 DTO),可以直接被 Spring Feign 引用并动态拼装出强类型客户端。
. |
\n\n注:我这里没有采用
\nxxx-api
和xx-svc
的命名方式,直接是xxx-api
表示 API Client 模块,xxx
为服务实现模块。
// mail-api/src/main/java/com/demo/mail/client/MailClient.java |
@Data |
// account/src/main/java/com/demo/account/service/UserService.java |
为了体现异常处理流程,上边代码仅用于演示,在生产环境下发邮件应该为异步处理,无需检查发送结果。我们在服务内加了全局异常处理,所以直接向上抛出即可。
\n最后再补充一点,业界 Restful API 的设计通常采用 HTTP 协议状态码来传递和表达错误语义,但是我们的设计是将错误码打包在一个正常的 Json 消息当中,也就是 Response 当中,这一种称为封装消息 + 捎带的设计模式,这样设计的目标是为了支持强类型的客户端同时简化和规范错误处理,如果借用 HTTP 协议状态码来传递和表达错误语义,虽然也可以开发对应的强类型客户端,但是内部的调用处理逻辑就会比较复杂,需要处理各种 HTTP 的错误码,开发成本会比较高。
\n"},{"title":"闷嘴葫芦","url":"/2023/Stuffy-gourd/","content":"我们公司早些时候就调整了年终奖的发放制度,拆成4个季度随工资发放,好处显而易见,有离职想法的同学不需要再漫长的等待年终奖了,而且本季度内离职只要表现不太差,离开后在该季度奖金发放时也基本能拿到自己应得的部分。
\n因为奖金和绩效挂钩,在每次发奖金前都需要和组员完成结果同步,我们是每个月的第5个工作日发工资,所以下周一是我们8月的发薪日也是Q2季度的奖金发放日,这就意味着需要在本周内完成绩效结果的同步和沟通。
\n每次沟通由实线和虚线两个Lead一起沟通,之前我是带虚线团队,虽然也需要参与沟通,但只需要辅助输出就行,不需要讲太多,每次听实线逻辑清晰的表达,都庆幸多亏自己不用长篇阔论去说。
\n这次沟通我需要作为实线TL和组内另一个虚线TL跟下边同学沟通,但我发现我还是没有太多可说的,我缺乏绩效沟通时的基本技巧,加上平时的思考比较少,(而且不擅长画饼),很容易冷场,在对方问一些刁钻或者我没有考虑过的问题的时候脑子里很容易就出现一片空白或者像一团浆糊,这让我想到读过的「一句顶一万句」这部小说中经常用的闷嘴葫芦这个词。
还好和我配合的这位虚线TL足够有经验,基本都是他来对线,我继续做辅助,很幸运每次我遇到困难时都会有贵人相助。
\n另外因为上个季度离职的同学比较多,只需要和4位同学沟通就行,我这个资深 I 人在每沟通完一个人后都很疲惫,需要自己待一会来充电,想到下个季度要沟通8个同学就有些头大🥲
\n"},{"title":"4个最重要的企业财务指标","url":"/2023/The-4-most-important-business-financial-indicators/","content":"当谈到财务指标时,净资产收益率、毛利率、净利率和市盈率是经常被提及的几个指标,这些也是巴菲特最看重的4个指标。
\n巴菲特是历史上最伟大的价值投资者,他的交易逻辑的核心是:寻找优质企业并长期持有这些企业的股票。如何判断一个企业是否优质?这时就可以依据上边提到的4个指标来进行判断了。
\n这个指标可以帮助投资者评估企业的盈利能力和管理效率。
\n净资产收益率是用来衡量企业利润与其净资产之间关系的指标。
\n它反映了企业利用所有者权益实现的盈利能力。
\n净资产收益率的计算公式为:净资产收益率 = 净利润 / 平均净资产。
\n净利润
\n指的是企业在一定时期内扣除所有成本和费用后所剩下的利润。
\n平均净资产
\n是指企业在一定期间内的
\n资产净值的平均值
\n。
\n假设有一家公司,在某一年度的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。
\n首先,计算平均净资产: 平均净资产 = (期初净资产 + 期末净资产)/ 2 = (2000万元 + 2500万元)/ 2 = 2250万元
\n接下来,计算净资产收益率: 净资产收益率 = 净利润 / 平均净资产 = 500万元 / 2250万元 ≈ 0.2222 或 22.22%
\n在这个例子中,公司的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。
\n通过计算,得到平均净资产为2250万元。
\n最后,通过将净利润除以平均净资产,得到净资产收益率为22.22%。
\n这个例子中的数据说明了净资产收益率的计算方法。净资产收益率衡量了企业在一定期间内每单位净资产所创造的净利润水平。在这种情况下,净资产收益率为22.22%,表示该公司在该年度内每单位净资产创造了22.22%的净利润。
\n这个指标可以帮助投资者了解企业的盈利能力和生产成本控制情况。
\n毛利率是用来衡量企业销售产品或提供服务后的毛利润与销售收入之间的关系的指标。
\n它反映了企业在销售过程中所保留的利润比例。
\n毛利率的计算公式为:毛利率 = 毛利润 / 销售收入。
\n毛利润
\n是指企业在销售产品或提供服务后剩余的销售收入减去与销售直接相关的成本。
\n销售收入
\n是指企业在一定时期内通过销售产品或提供服务所获得的总收入。
\n如果一个行业的毛利率低于20%,那么几乎可以断定这个行业存在着过度竞争。
\n这个指标可以帮助投资者评估企业的盈利能力和经营效率。
\n净利率是用来衡量企业净利润与销售收入之间关系的指标。
\n它反映了企业在销售过程中实现的净利润比例。
\n净利率的计算公式为:净利率 = 净利润 / 销售收入。
\n假设有一家制造公司,它生产并销售手机。在某一年度,公司的销售收入为1000万元。与销售直接相关的成本包括原材料、直接劳动和制造费用,总计为600万元。此外,公司还有其他间接费用和费用,如销售费用、管理费用和利息费用等,总计为200万元。
\n根据上述数据,我们可以计算毛利润和净利润:
\n毛利润 = 销售收入 - 与销售直接相关的成本 = 1000万元 - 600万元 = 400万元
\n净利润 = 毛利润 - 其他间接费用和费用 = 400万元 - 200万元 = 200万元
\n在这个例子中,公司的销售收入为1000万元,与销售直接相关的成本为600万元,其他间接费用和费用为200万元。
\n毛利润表示在销售过程中,公司通过销售所保留的利润。在这种情况下,公司的毛利润为400万元,即销售收入减去与销售直接相关的成本。
\n净利润则是在扣除所有成本和费用后所得到的最终利润。在这个例子中,公司的净利润为200万元,即毛利润减去其他间接费用和费用。
\n毛利润关注销售过程中所保留的利润,而净利润则考虑了所有与经营活动相关的费用和收入。
\n毛利率和净利率是两个常用的财务指标,用于衡量企业的盈利能力。它们之间的区别在于考虑的成本因素不同。
\n总结起来,毛利率关注的是销售收入和与销售直接相关的成本之间的关系,它衡量了企业从核心业务中获得的利润比例。而净利率则考虑了所有成本和费用,包括销售成本以外的费用,衡量了企业在所有经营活动中实现的净利润比例。净利率相对于毛利率更全面地反映了企业的盈利能力和经营效率。
\n市盈率是衡量股票相对于每股盈利的价格的指标。
\n它是投资者评估一家公司的股票是否被低估或高估的重要指标。
\n市盈率的计算公式为:市盈率 = 股票市场价格 / 每股税后收益。
\n股票市场价格指的是股票在市场上的交易价格,也就是投资者购买或出售股票所需支付或获得的价格。
\n每股税后收益
\n是指企业每股普通股的税后净利润,也可以理解为每一股票所对应的盈利。
\n较高的市盈率可能意味着市场对该股票有较高的期望和溢价,而较低的市盈率可能意味着市场对该股票的期望较低。
\n假设有一家公司,它在某一年度的每股收益为10元,而股票的市场价格为100元。
\n首先,计算市盈率: 市盈率 = 市场价格 / 每股收益 = 100元 / 10元 = 10倍
\n在这个例子中,每股收益为10元,市场价格为100元。
\n通过计算,得到市盈率为10倍。
\nROE高和净利率高通常被视为公司财务状况较好的指标,但并不意味着这些公司一定是好的投资对象。以下是一些需要考虑的因素:
\n因此,高ROE和净利率只是投资决策的起点,而不是唯一的决策依据。投资者应该进行全面的研究和分析,以综合考虑多种因素,并结合自己的投资目标和风险承受能力做出决策。
\n虽然ROE和ROI都涉及利润和投资,但ROE主要关注企业的盈利能力和资产利用效率,而ROI主要关注特定投资项目或资产的回报率,它们评估的对象和应用范围不同。
\n在评估企业绩效时,ROE和ROI通常会结合使用,以提供更全面的分析。ROE可以衡量企业整体的盈利能力和管理效率,而ROI可以帮助评估具体投资项目的回报情况。
\n"},{"title":"国外大学教育中的开放思想","url":"/2023/The-Open-Mind-in-Foreign-University-Education/","content":"今天在观看哈佛幸福课时,老师在过程中讲述了适当休息、放下当前的工作对于创造力的重要性。
\n之后还用性爱做了个比喻,非常喜欢国外这种开放的教学方式,在进行这个比喻时非常自然。
\n反观国内,学生们只能私下里偷摸讨论这种事情,不管在任何场合都无法拿到台面上来说。
\n\n\n\n\n\n\n以上观点也应了蒋勋老师在讲解红楼梦时提到的一句话:「人生的领悟不是在知识里,人生的领悟其实是在生命的经验当中。」
\n"},{"title":"《金钱心理学》摘抄","url":"/2024/The-Psychology-of-Money/","content":"我所读过的理财类书籍并不多,在国庆后由于人性的贪婪,在股市中损失了(对我来说)一大笔钱,机缘巧合下读了这本名叫《金钱心理学》的理财类书籍。这是我读的为数不多的觉得写的非常好的理财书之一,哪怕不限于理财类,它也是一本用来了解人性和世界观的好书,由于得到了非常好的阅读体验,从另一方面来说这次的投资失利也许对我来说属于因祸得福了。
\n在读《金钱心理学》时,我脑海中经常飘出一句励志的话:「种一棵树最好的时间是十年前,其次是现在」,刚刚查了一下这句话的来源,出自非洲经济学家丹比萨·莫约的《援助的死亡》一书,巧合的是也是一位经济学家说的。我现在已经开始了超长线的定投计划,用十年时间来定投黄金和标普500,自从开始定投后就出现了两种有冲突的念头:既想让时间过快一点,好让我完成我的定投目标,见证时间和复利带来的强大效果,又想让时间过慢一些,自己还不想那么快的老去,想再多一些时间陪伴孩子们,更不想眼睁睁看着父母一天天的老去。这本书还纠正了我一个错误观念,我之前认为财富跟赚钱多少成非常强的正相关性,这本书告诉我并不是这样,收入当然是一部分,但对大部分人来说更重要的是节俭和储蓄。
\n这本书中没有教我们认识各种指标,都是一些软技能,下边是我从这本书中摘录下的句子,通过这些句子也能感受到这本书再讲的是什么样的理财观念。最后我会在写一写我准备开启的一段超长线投资计划。
\n我为自己设计了一个为期至少10年的理财规划,规则很简单:通过蚂蚁财富,每天分别定投100元到「黄金ETF」 和「标普500ETF」,如果行情出现1%的回撤时则当日加仓50元,当盈利超过30%时则赎回30%的仓位,后边再慢慢加仓定投回去。
\n我们来粗略估算了一下10年下来的投资金额:
\n# 每年大约有250个交易日,每年的投资额为: |
按照10年的投资回报率80%(虽然市场不可预测,但人总要有些盼头,况且我定投的两项在过去十年中回报都超过了200%),这样10年下来大概会有44万的投资回报,加上本金刚好100万。虽然这么长的时间只回报44万看起来不多,但这对我来说这是一种无痛的投资方式,用作者的话是:「用能让你睡踏实的方式来理财」。当然在这个过程中有可能还会根据我的生活水平来调整定投金额,比如5年后我提前把房贷还完了,也许就能出多个2、3倍的闲钱用于定投。另外,本段开头也说了,我这个投资计划短则持续10年,长则持续20年、30年,也许在复利的作用下收益远远不止于我上边计算出来的那么多。
\n"},{"title":"少有人走的路 阅读笔记","url":"/2019/The-Road-Less-Traveled-ReadNote/","content":"自律是解决人生问题最主要的工具,也是消除人生痛苦最重要的方法。
\n解决人生问题的关键在于自律。人若缺少自律,就不可能解决任何麻烦和问题。在某些方面自律,只能解决某些问题,全面的自律才能解决人生所有的问题。
\n人生是一个不断面对问题并解决问题的过程。问题可以开启我们的智慧,激发我们的勇气。为解决问题而努力,我们的思想和心灵就会不断成长,心智就会不断成熟。
\n所谓自律,就是主动要求自己以积极的态度去承受痛苦,解决问题。
\n自律的四个原则:
\n推迟满足感,就是不贪图暂时的安逸,先苦后甜,重新设置人生快乐与痛苦的次序:
\n在孩子稚嫩的心中,父母就是他们的上帝,神圣而威严。孩子缺乏其他的模仿对象,自然会把父母处理问题的方式全盘接受下来。如果父母懂得自律、自制和自尊,生活井然有序,孩子就会把这样的生活视为理所当然。而如果父母的生活混乱不堪,一塌糊涂,孩子也会照单全收。
\n对自我价值的认可是自律的基础,因为当一个人觉得自己很有价值时,就会采取一切必要的措施来照顾自己。自律是自我照顾,自我珍惜,而不是自暴自弃。
\n除非存在智力障碍,不然只要花时间学习,就没什么问题解决不了。
\n尽可能早地面对问题,意味着把满足感向后推迟,放弃暂时的安逸或是程度较轻的痛苦,去体验程度较大的痛苦,这才是对待问题和痛苦最明智的办法。现在承受痛苦,将来就可能获得更大的满足感;而现在不谋求解决问题,将来的痛苦会更大,延续的时间也更长。
\n只有通过大量的生活体验,让心灵充分成长,心智足够成熟,我们才能够正确认识自己,客观评定自己和他人应该承担的责任。
\n力图把责任推给别人或是组织,就意味着我们甘愿处于附属地位,把自由和权力拱手交给命运、社会、政府、独裁者和上司。
\n幼小的孩子依赖父母,当然情有可原,如果父母独断专行,孩子也没有选择的余地。头脑清醒的成年人则可不受限制,做出适合自己的选择。
\n我们越是了解事实,处理问题就越是得心应手;对事实了解得越少,思维就越是混乱。
\n有的人一过完青春期,就放弃了绘制地图。他们的地图狭小、模糊、粗略而又肤浅,从而导致对现实的认知过于狭隘和偏激。
大多数人过了中年,就自认为地图完美无缺,世界观没有任何瑕疵,甚至自以为神圣不可侵犯,而对新的信息和资讯缺乏兴趣。
只有极少数幸运者能继续努力,他们不停地探索、扩大和更新自己对于世界的认识,直到生命终结。
逃避现实的痛苦是人类的天性,只有通过自律,我们才能逐渐克服现实的痛苦,及时修改自己的地图,逐步成长。我们必须忠于事实,尽管这会带来暂时的痛苦,但远比沉湎于虚假的舒适中要好。
\n自我反省对于我们的生存至关重要。反省内心世界带来的痛苦,往往大于观察外在世界带来的痛苦,所以很多人逃避前者而选择后者。实际上,认识和忠于事实带给我们的非凡价值,将使痛苦显得微不足道。自我反省带来的快乐,甚至远远大于痛苦。
\n对于想进入政治和企业高层领域的人而言,有选择地保留个人意见极为重要。凡事直言不讳的人,极易被上司认为是桀骜不驯,甚至被视为“捣乱分子”,是对组织和集体的威胁。要想在组织或集体中发挥更大的作用,就要注重表达意见的时间、场合和方式。换句话说,一个人应该有选择地表达意见和想法。
\n一个人越是诚实,保持诚实就越是容易,而谎言说得越多,则越要编造更多的谎言自圆其说。敢于面对事实的人,能够心胸坦荡地生活,不必面临良心的折磨和恐惧的威胁。
\n我们在各阶段需要放弃的东西:
\n总体说来,这些就是我们在人生过程中必须放弃的生活环境、个人 欲望和处世态度。放弃这些的过程就是心智完美成长的过程。
\n爱,是为了促进自己和他人心智成熟,而不断拓展自我界限,实现自我完善的一种意愿。
\n坠入情网唯一的作用是消除寂寞,而不是有目的地促进心灵的成长。即使经过婚姻,使这一功用延长,也无助于心智的成熟。一旦坠入情网,我们便会以为自己生活在了幸福的巅峰,以为人生无与伦比,达到了登峰造极的境界。在我们眼中,对方近乎十全十美,虽然有缺点和毛病,那也算不上什么,甚至只会提升其价值,增加对方在我们眼中的魅力。在这种时候,我们会觉得心智成熟与否并不重要,重要的是当前的满足感。我们忘记了一个事实:我们和爱人的心智其实都还不完善,需要更多的滋养。
\n要学会自尊自爱,就需要自我滋养。我们需要为自己提供许多与心智有关的养分。
\n勇气,并不意味着永不恐惧,而是面对恐惧时能够坦然行动,克服畏缩心理,大步走向未知的未来。
\n我们应该坦然接受死亡,不妨把它当成“永远的伴侣”,想象它始终与我们并肩而行。
在死亡的指引下,我们会清醒地意识到,人生苦短,爱的时间有限,我们应该好好珍惜和把握。不敢正视死亡,就无法获得人生的真谛,无法理解什么是爱,什么是生活。万物永远处在变化中,死亡是一种正常现象,不肯接受这一事实,我们就永远无法体味生命的宏大意义。
真正有爱的人,绝不会随意指责爱的对象,或与对方发生冲突。他们竭力避免给对方造成傲慢的印象。动辄与所爱的人发生冲突,多半是以为自己在见识或道德上高人一等。真心爱一个人,就会承认对方是与自己不同的、完全独立的个体。
\n人们的感受和观点起源于过去的经验,却很少意识到经验并不是放之四海而皆准的法则,他们对自己的世界观并没有完整而深入的认识。
\n对于别人教给我们的一切,包括通常的文化观念以及一切陈规旧习,采取冷静和怀疑的态度,才是心智成熟不可或缺的元素。科学本身很容易成为一种文化偶像,我们亦应保持怀疑的态度。
\n你会意识到,你具备特有的生存能力,对意外事件有着某种特殊的抵抗力,而这并不是你自主选择的结果。
\n人类有潜在的欲望和愤怒,是自然而然的事,本身并不构成问题。只有当意识不愿面对这种情形,不愿承受处理消极情感造成的痛苦,宁可对其视而不见,甚至加以摒弃和排斥时,才导致了心理疾病的产生。
\n要让心智成熟,我们需要聆听潜意识的声音,让意识中对自己的认识更接近真实的自己。
\n我们的肉体可能随着生命周期而改变,不过它早已停止了进化的历程,不会产生新的生理模式。随着年龄的增长,肉体的衰老是不可避免的结果,但在人的一生中,心灵却可以不断进化,乃至发生根本性的改变。换句话说,心灵可以始终生长发育下去,其能力可以与日俱增,直到死亡为止。
\n我们之所以能够成长,在于持续的努力;我们之所以能够付出努力,是因为懂得自尊自爱。对自己的爱使我们愿意接受自律,对别人的爱让我们帮助他们去自我完善。自我完善的爱,是一种典型的进化行为,具有生生不息的特征。在生物世界中,存在着永久而普遍的进化力量,体现在人类身上,就是具有人性的爱。它违反熵增的自然规律,是一种永远走向进步的神奇的力量。
\n阻碍心智成熟最大的障碍就是懒惰,只要克服懒惰,其他阻力都能迎刃而解;如果无法克服懒惰,不论其他条件如何完善,我们都无法取得成功。
\n不管我们精力多么旺盛,野心多么炽烈,智慧多么过人,只要深入反省,就会发现自身懒惰的一面,它是我们内心中熵的力量。在心灵进化的过程中,它始终与我们对抗,阻止我们的心智走向成熟。
\n人们总是觉得新的信息是有威胁的,因为如果新信息属实,他们就需要做大量的辛苦工作,修改关于现实的地图。他们会本能地避免这种情形的发生,宁可同新的信息较量,也不想吸收它们。他们抗拒现实的动机,固然源于恐惧,但恐惧的基础却是懒惰。他们懒得去做大量的辛苦工作。
\n在每一个人的身体中,都拥有向往神性的本能,都有达到完美境界的欲望,同时也都有懒惰的原罪。无所不在的熵的力量,试图把我们推回到人类进化的初期——那里有我们的幼年,有母亲的子宫,还有荒凉的原始沼泽。
\n邪恶是真实存在的。
所谓邪恶,就是为所欲为、横行霸道式的懒惰。
至少到目前人类进化的这一阶段,邪恶是不可避免的。
熵是一种强大的力量,是人性极恶的体现。
任何训诫都不能免除心灵之路上的行者必经的痛苦。你只能自行选择人生道路,忍受生活的艰辛与磨难,最终才能达到上帝的境界。
\n"},{"title":"Tmux 使用笔记","url":"/2019/Tmux-Note/","content":"Tmux 是一个用于在终端窗口中运行多个终端会话的工具,即终端复用软件(terminal multiplexer)。
\n在 Tmux 中可以根据不同的工作任务创建不同的会话,每个会话又可以创建多个窗口来完成不同的工作,每个窗口又可以分割成很多小窗口。这些功能都是非常实用的。
\n$ brew install tmux
plugins=( |
开启后提供以下快捷命令:
\nta <session-name>
:接入某个已存在的会话ts <session-name>
:新建一个指定名称的会话tl
:查看当前所有的 Tmux 会话tksv
:用于杀死全部会话tkss <session-name>
:用于杀死某个会话tmux new -s my_sessio
n。ts my_session
Ctrl+b d
将会话分离。tmux attach-session -t my_session
。ta my_session
# session |
新建 ~/.tmux.conf
配置文件后写入如下内容:
set-option -g mouse on |
需杀掉全部会话(tksv),重新启动新的会话后以上配置才能生效。
\n历史记录默认上限为 2000 行,可通过在配置文件中加入如下配置来进行修改:
\nset-option -g history-limit 10000
http://www.ruanyifeng.com/blog/2019/10/tmux.html
\n"},{"title":"五分钟彻底解决 OutOfMemoryError: Unable to create native threads","url":"/2019/Unable-to-create-native-threads/","content":"\n\n\n作为 Java 程序员,我们几乎都会碰到 java.lang.OutOfMemoryError 异常。
\n
JVM 在抛出 java.lang.OutOfMemoryError
时,除了会打印出一行描述信息,还会打印堆栈跟踪,因此我们可以通过这些信息来找到导致异常的原因。
其中一种异常是 java.lang.OutOfMemoryError: Unable to create native threads
,我们通过这篇文章来彻底搞懂它。
抛出这个异常的过程大概是这样的:
\njava.lang.OutOfMemoryError: Unable to create new native thread
错误。因此关键在于第四步线程创建失败,JVM 就会抛出 OutOfMemoryError
,那具体有哪些因素会导致线程创建失败呢?
ulimit -u
限制sys.kernel.threads-max
限制sys.kernel.pid_max
限制第一种失败原因简单说一下:Java 创建一个线程需要消耗一定的栈空间,并通过-Xss参数指定。需要注意的是栈空间如果过小,可能会导致 StackOverflowError
,尤其是在递归调用的情况下,但是栈空间过大会占用过多内存。
同时还要注意,对于一个 32 位 Java 应用来说,用户进程空间是 4GB,内核占用 1GB,那么用户空间就剩下 3GB,因此它能创建的线程数大致可以通过这个公式算出来:
\nMax memory(3GB) = [-Xmx] + [-XX:MaxMetaSpaceSize] + number_of_threads * [-Xss] |
不过对于 64 位的应用,由于虚拟进程空间近乎无限大,因此不会因为线程栈过大而耗尽虚拟地址空间。但是请你注意,64 位的 Java 进程能分配的最大内存数仍然受物理内存大小的限制。
\n下边重点来介绍后边三种失败因素。
\n在搞明白 pid_max
、ulimit -u
和 thread_max
的区别前,需要先明白进程和线程之间的区别。
\n\n一个最典型的区别是,(同一个进程内的)线程运行时共享内存空间,而进程在独立的内存空间中运行。
\n
pid_max
参数表示系统全局的 PID
号数值的限制,每一个进程都有 ID,ID 的值超过这个数,进程就会创建失败,pid_max
参数可以通过以下命令查看:
cat /proc/sys/kernel/pid_max |
默认情况下,执行以上命令返回 32768,这意味着我们可以在系统中同时运行 32768 个进程,这些进程可以在独立的内存空间中运行。
\necho 65535 > /proc/sys/kernel/pid_max
上面只是临时生效,重启机器后会失效
\n永久生效方法:
\n在 /etc/sysctl.conf
中添加 kernel.pid_max = 65535
vi /etc/sysctl.conf |
或者:
\necho "kernel.pid_max = 65535" >> /etc/sysctl.conf |
treamd-max
用来限制操作系统全局的线程数,通过以下命令查看 treamd-max
参数:
cat /proc/sys/kernel/threads-max |
上边的命令返回 126406,这意味着我可以在共享内存空间中拥有 126406 个线程。
\nlimit -u
表示当前用户可以运行的最大进程数。
root
账号下 ulimit -u
得到的值默认是 cat /proc/sys/kernel/threads-max
的值 / 2,即系统线程数的一半。
普通账号下 ulimit -u
得到的值默认是 /etc/security/limits.d/20-nproc.conf
文件中指定的:
ulimit -u 65535 |
ulimit -u
的值受全局的 kernel.pid_max
的值限制。也就是说如果 kernel.pid_max=1024
,那么即使你的 ulimit -u
的值是 63203,用户能打开的最大进程数还是 1024。
陈奕迅的《红玫瑰》中有一句:得不到的永远在骚动。
\n红楼梦中的薛蟠就是一个很好的例子:
\n最早因为得不到香菱而打死冯渊,得到后几天就腻了;
\n第二次因为得不到柳湘莲而在酒席上大闹,被湘莲暴揍一顿;
\n第三次在娶了夏金桂不久又骚动着想要得到宝蟾。
\n\n\n说句题外话,在写上边的时候发现香菱和湘莲的读音有几分像,有没有可能是作者有意为之,湘莲就是来为香菱报仇的?
\n
在这几次骚动中,唯一一次求而不得就是调戏柳湘莲那一次,而那一次也是薛蟠得到最多成长的一次。薛蟠因为怕丢人,就跟随老管家出远门学做生意去了,按作者的意思是过程还不错,只是回来的路上遇到了土匪,不过最后又被柳湘莲久了,二人还拜了把子。
\n再说一个因得不到而骚动的例子,贾赦想要取老太太身边的丫鬟鸳鸯,但用了各种招数都没到手,最后被老太太臭骂一顿才罢休,于是他从外边买了个小媳妇,也是就稀罕了两天后边就让她独守空房了。
\n这么来看得到了又怎样呢?还不如在努力后发现得不到时把心态放平和,未来还能作为一个美好的回忆。
\nUpdatedAt2023年08月02日:再补充一个关于宝玉的故事。宝玉曾一度以为大观园里的女孩都会围着他转,听他的话讨好他。有一次,他看到龄官用金簪在地上写着”蔷”字,他被这个女孩迷住了,非常欣赏她。后来,在梨香院再次遇到龄官时,宝玉让龄官给他唱一出戏,龄官不肯唱,过了一会儿,贾蔷来了,跟龄官交流了一会儿,宝玉知道了龄官喜欢的是贾蔷。此时,宝玉才明白并不是所有的女孩都喜欢他,他也不能得到所有女孩,这次经历给宝玉上了一堂非常生动的爱情课。
\n我博客的样式改了很多版,唯一没改过的是首页的那三句话:
\n\n\n人会长大三次。
\n-> 第一次是在发现自己不是世界中心的时候。
\n-> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
\n-> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
\n
原话我已经忘记是从哪里看到的了,但也可以视为一种得不到手的成长。
\n我觉得大多数时候求而不得的人要比想要什么就能得到什么的人更幸福,这样的人更懂得珍惜当下,心智也更成熟,更能接受自己的失败,从失败中成长、从失败中学习。
\n求而不得可以让我们更加珍惜已经得到的部分,不再认为得到是理所当然的。我以前在求而不得时会有个很悲观很恶毒的想法:看到自己无法得到的东西另一个人却垂手可得就会觉得很不公平。现在会经常用黛玉的那句「事若求全何所乐」来让自己释怀,况且自己得到的已经够多了。
\n"},{"title":"使用 Cloudflare Zero Trust 保护你的 Web 应用","url":"/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/","content":"我经常在 VPS 上搭建一些小应用,很多应用是为了方便自己,并没有打算公开使用。
\n比如最近我打了一个 ChatGPT 的服务,自己用起来非常方便。但是有个问题是这个服务默认不支持用户登录认证,在启动时配置了 openai 的 key 后,就可以直接使用了。
\n\n我之前的做法是使用 Nginx 的 Auth 功能来实现,配置起来比较麻烦。它使用静态的用户名和密码,使用起来也不够优雅。
\n\n我这里的需求是,不需要获取具体的用户信息,只要确认这个人是经过我的同意的,就可以访问我的Web 页面。
\n我一直认为 Cloudflare 会提供这样的通用功能,但之前没有找到。今天我看了一篇文章:https://dmesg.app/zero-trust-access-web.html,突然意识到原来 Cloudflare Zero Trust 就是我一直在找的功能。
\n\n经过几步,我已经成功地为我的站点添加了邮箱验证码授权功能。
\n因为我要保护的服务是已经在自己服务器上部署好的,所以这里选择 Self-hosted。
\n\n想要在站点上使用Cloudflare Zero Trust,前提是域名已经接入 Cloudflare DNS。
\n如下图,我配置了一个 chatgpt 服务,要保护的域名是 chatgpt.jiapan.me,认证后过期时间为1个月:
\n\n如下图所示,我配置了一个允许策略,过期时间与上一步配置的应用 session 保持一致。
\n认证规则使用邮箱,要求邮箱后缀为 @jiapan.me
\n\n剩下的就保持默认,一直下一步就行了。
\n现在当我再打开 https://chatgpt.jiapan.me 时,会被重定向到 Cloudflare 的认证页面。需要输入一个邮箱地址:
\n\n如果输入的不是以 @jiapan.me 结尾的邮箱,也不会报错,会正常进入到输入验证码页面,但实际上收不到验证码邮件。这一步 Cloudflare 做得很好,不会让不法分子破解出具体能用什么样的邮箱可以收到验证码。
\n\n输入以 @jiapan.me 结尾的邮箱后,就可以正常收到邮件了。
\n\n当我们将验证码输入到 Code 框中后,就可以正常访问我们的服务了。
\n当然,也不是必须有自己独立的邮箱,Cloudflare Zero Trust也支持完整的邮箱地址匹配。比如,通过下面的方式,我补充了一个可以通过 jiapan@163.com 接收Code的规则:
\n\n现在,我不仅可以保证自己的服务不被未经授权的人访问,而且不需要自己去维护和管理用户认证信息。Cloudflare Zero Trust 还支持多种认证方式,比如 OAuth2,LDAP,JWT 等等,可以根据自己的需求选择合适的认证方式。(这一段是 ChatGPT 写的)
\n"},{"title":"WakaTime","url":"/2016/WakaTime/","content":"今天没啥好写的,记一个我刚刚发现的新东西吧。
\n网站宣传语是:Quantify your coding - Metrics, insights, and time tracking automatically generated from your programming activity
\n用来量化工作量,我用的PyCharm,只要安装官方提供的插件后,就能统计我当天为每个项目敲代码的时间,而且能统计我都谢了那些代码,比如JS占10%,Python占90%
\n看一眼我的Dashboard
\n\n具体有什么高端功能还没研究。
\n","categories":["有趣"],"tags":["有趣"]},{"title":"《伯恩斯焦虑自助疗法》摘抄(未整理)","url":"/2021/When-Panic-Attacks/","content":"焦虑有许多不同的形式,
\n焦虑的成因,
\n每当我们感到焦虑或害怕的时候,其实都是我们自己在杞人忧天。
\n当你改变自己的思考方式的时候,你的感受也会随之改变。
\n简单来说,焦虑是由我们的想法,或者说是认知导致的。
\n该理论认为,每一种想法或认知都可以创造出一种特定的感受。
\n每当我们感到焦虑或害怕的时候,都是我们自己在杞人忧天。
\n当你开始感到焦虑的时候,你的消极想法和情绪开始相互作用,形成了一个不断加深的恶性循环。这些消极想法会导致焦虑和恐惧,而焦虑和恐惧又会让你的想法更加消极。
\n当你感到焦虑时,心中的很多想法都并非现实。
\n健康的焦虑情绪源自对实际存在的危险的感知,
\n神经性焦虑并不是由真实的威胁导致的。导致神经性焦虑的想法往往都是扭曲的、不合逻辑的。
\n如果法”(What-If Technique)治疗。
\n他也发现自己其实无限放大了自己在别人眼中的重要性,却忽略了身边的很多律师其实都是以自我为中心的“自恋狂”。
\n接受悖论(acceptance paradox)。
\n“软弱”实际上是他最强的力量,而他引以为傲的“力量”恰恰一直是他最大的软肋。
\n试图隐藏的软弱、焦虑和对自己的怀疑恰恰是他和其他人连接的一根红线。
\n佛教教导我们痛苦不是来自现实,而是来自我们对现实的判断。
\n当你改变思维方式时,你就可以改变你的感受。
\n暴露模型
\n这种模型认为,当你焦虑时,你其实是在极力逃避你害怕的事情。
\n名为“洪水法”(Flooding)的治疗方法。在恐慌之时,我们不要刻意逃避,而是故意让自己暴露在害怕的事物面前,让自己充满焦虑。
\n向焦虑妥协,
\n情感隐藏模型认为“善良”(niceness)是焦虑的主要原因。
\n当你焦虑的时候,你几乎总是在刻意逃避困扰你的问题,但是你并没有意识到这一点。你之所以会把困扰自己的问题从意识中推出来,是因为你想要变得善良,不想让任何人因此感到沮丧或不安。
\n在这种模型之下,只要你将自己的情绪原原本本地展现出来,你的焦虑感自然会一扫而空。
\n了三种对抗焦虑的有效方式。认知法帮助我们识破那些让我们焦虑抑郁的消极想法。暴露法帮我们直面过去一直逃避的恐惧。情感隐藏法则帮助我们找到我们精心隐藏在内心深处的冲突或情感。
\n人,生而不同。
\n焦虑情绪源自对危险的感知。如果你一直告诉自己马上就会有不好的事情发生,你就会感到焦虑。
\n与杞人忧天的焦虑情绪不同,如果你感到抑郁,你会觉得悲剧已经发生了。
\n如果你感到抑郁,就一定会感到焦虑。如果你感到焦虑,你也许也会感到一丝抑郁。
\n抑郁会带来很大的痛苦,因为抑郁的情绪会剥夺你的自尊。而且,在抑郁的状态下,你也会更加容易觉得没有希望,会很容易认为自己的痛苦是永不休止的。
\n抑郁是这个世界上最古老、也是最残酷的骗子,因为你会骗自己去相信很多根本不真实的东西。抑郁情绪会让你认为自己很糟糕,认为自己应该做得更好,还会感觉自己从来不曾开心、满足,也不会拥有创造力,无法和他人建立亲密的关系。
\n很多神经病学家都认为,抑郁症和焦虑症的成因是大脑分泌的血清素不足,而躁狂症(极其欣快兴奋的状态)的成因则是大脑分泌的血清素过多
\n人脑和电脑的区别在于,人脑每天都会产生新的脑细胞和新的电信号回路。所以,每天早上我们醒来的时候,从字面意义上说,我们都是一个“全新的人”,因为我们的脑细胞在过去的24小时里已经全部更新一遍了。
\n医生们自己每天也不停地听到这种化学物质失衡理论。而这种理论的传播动力更多地是来自其背后的制药公司,
\n实际上,我们到现在为止,甚至都不知道大脑是怎么创造出意识的,更别提
\n我们对自己的期待有的时候会给我们的思维方式、感受以及行为方式带来意料之外的影响。
\n希望”是最有效的抗抑郁药。
\n你可以为抑郁
\n最近的研究证明,所有的处方类抗抑郁药其实除了安慰剂效应之外真的都没有其他的治疗效果。
\n抗抑郁药所谓的功效之中,大概有75%到80%都归功于安慰剂效应。
\n他们营销药物的需求和科学研究之间出现了矛盾。
\n认知行为疗法也是目前在美国实验最广的心理治疗方法,同时也是在临床治疗中使用最多的疗法。
\n于抑郁症和焦虑症的治疗来说,无论是从长期来看还是短期来看,认知行为疗法都比药物治疗更加有效。
\n都应该至少一周做一次“简明情绪量表”的测试(见本书第29页),以随时监测自己是否有康复的迹象。
\n从全球范围来看,焦虑和抑郁是两种最普通、最常见的心理健康问题,这两种情绪会让人觉得非常痛苦。
\n主动在生活中运用它们才行。 为此,你必须要做到以下三件事: 1. 你必须放弃焦虑和抑郁的某些隐藏的好处,这可能会给你造成损失。 2. 你必须敢于直面心中最大的恐惧,这需要你拥有极大的勇气和决心。 3. 你必须做一些笔头上的练习,这要求你必须脚踏实地地做出积极的努力。
\n列出所有让你抓狂的感受、想法或者习惯的优缺点。然后就这些列出来的优点和缺点做一个权衡,再来综合考虑到底要不要改变。
\n他认为焦虑感可以让他始终保持警惕,从而免受未来的其他潜在的伤害。这其实是所有焦虑患者都共有的一种想法。我把这称为“魔法思维”(magical thinking)。
\n真正能让你提高效率的焦虑情绪很少很少,更多情况下,焦虑会让你的效率越来越低。
\n如果你想要战胜你的焦虑,你就必须直面你心中的怪兽,战胜你心中最深处的恐惧。
\n并不是暴露本身让你不再恐惧,而是在暴露治疗的过程中出现了某一刻,这一刻你突然意识到,让你恐惧的想法其实并不是事实。
\n“每日情绪日志”的基本理念就是:只要你改变了自己的想法,你就可以改变自己的感受。
\n填写“每日情绪日志”可以分为五个步骤: 第一步 写下令你难受或不安的事件。
\n第二步 圈出你的“情绪”。
\n第三步 记录消极想法。
\n表6-1 每日情绪日志
\n表6-2 认知扭曲对照表
\n在你感到焦虑和沮丧的瞬间就可能发现你的所有问题。当你开始改变自己的思考和感受的方式的时候,你就可以找到解决你所有问题的方法了。
\n正确识别出消极想法中的扭曲认知与其说是科学,不如说是一门艺术,所以即使没有全部勾对,也无须担心。
\n当你在你的想法中发现这些扭曲认知的时候,你得先想一想首先需要用到哪个方法。
\n双重标准法本身利用的就是人类在处理事情时的天性。当我们沮丧的时候,我们就会觉得心烦意乱,抓狂不已。但是当我们和有同样情绪问题的朋友聊天的时候,又会变得格外客观冷静,并且有同情心。
\n6-5 玛莎的每日情绪日志(二)
\n如果你希望这个积极想法完完全全地改变你发自内心的感受,它必须满足两个条件。
\n这个积极想法一定得百分百真实,
\n积极想法需要让消极想法不攻自破。
\n“每日情绪日志”最大的好处就是它能够反映出你独特的想法和感受。
\n在本书的最后我还为你准备了另一份“每日情绪日志”的空白模板。你还可以复印更多来
\n第一步 写下令你难受的事件 在每日情感日志的顶部,简短地描述一件让你感到不安或难受的事。
\n你感到焦虑和沮丧的瞬间就可能会暴露你所有的问题。
\n你可以一次只解决一个问题。
\n人们宁愿只谈论自己生活中的问题,而不愿意写下来。
\n如果你真的想改变自己的生活,你迟早得关注你自己感到不安或难受的这个特殊时刻。
\n如果只是谈话而没有任何逻辑章法,反而会有可能无休止地拖延病情,也不会给病人带来任何真正的改变。
\n第二步 圈出消极情绪 在你描述完令你难受不安的事件后,在表内的情绪词汇中圈出能够准确描述你此刻情绪的词语,并且给情绪的强烈程度打分,
\n识别和评估你的消极情绪很重要,因为特定种类的感受是由某些特定的消极想法引起的。
\n第三步 识别消极想法 当你感到不安时,记录你脑中闪现出的所有消极想法。
\n在“每日情绪日志”的“消极想法”一栏中列出你的消极想法,并评估你对每个想法的相信程度,
\n记录消极想法的过程做一些小提示。
\n第四步 识别出想法中的认知扭曲 在“认知扭曲”一列中记录每个消极想法中的扭曲认知。
\n第五步 想出积极想法 思考出一些更积极更现实的想法,让你的消极想法不攻自破。
\n认知行为疗法认为焦虑、抑郁和愤怒都是由当下的消极想法导致的。
\n自我攻击信念(
\n你的态度和价值观可以解释你的心理脆弱性。
\n自我攻击信念有两种基本类型:个人自我挫败和人际自我挫败。
\n个人自我挫败常常与自尊相关,
\n人际自我攻击信念可能更容易导致与其他人之间的冲突。
\n自我攻击信念其实始终存在,但消极想法只有在你感到不安时才会浮现在你的脑海中。
\n从“每日情绪日志”中选择一个消极想法,并在其下方画一个向下的箭头“↓”。
\n7-2 拉希德的向下箭头法
\n常见的自我攻击信念
\n在使用向下箭头法时,我们可以从“每日情绪日志”的消极想法开始。你选择哪一个消极想法都行,选择一个你感兴趣的就好。在它下面画一个向下的箭头“↓”并问自己:“如果这是真的,那对我来说意味着什么?为什么这会让我难过?”这时,一个新的消极想法将浮现在你的脑海中,你可以在箭头下把这个想法写下来。
\n最终找到自己最深层的担忧。
\n我们应如何改变自己的自我攻击信念呢?我认为这个过程可以分为三步。
\n进行成本效益分析。
\n修正自己的想法。
\n测试新的想法。
\n表8-1 行为完美主义:成本效益分析
\n另一个人的爱永远不会让我有价值,他们的拒绝也永远不会让我变得毫无价值。
\n如果法就可以帮助你发现引发焦虑的可怕幻想。
\n你在“每日情绪日志”的消极想法下画一个向下的箭头“↓”,并问自己这样的问题:“如果这是真的怎么办?会发生什么呢?最坏的情况会是怎样的呢?我心里最害怕的到底是什么?”
\n表9-1 克里斯汀:如果法
\n自虐式解决方案就是指你认为只要你惩罚自己,你就可以惩罚别人。
\n当我们感到沮丧时,我们会毫不留情地批判自己,仿佛想要将自己撕成碎片。但是当我们和有同样情绪问题的朋友聊天的时候,又会变得格外客观冷静。
\n可以试着问问自己:如果我的亲人或者朋友和我有着同样的问题,我会对他们说什么?我会对他或她说这么严厉的话吗?
\n大多数时候,被别人拒绝的痛苦都是来自我们自己的想法,而不是拒绝本身。有时,这些想法是极度扭曲的,会给我们带来很大的伤害。
\n但根据我的经验,自责、内疚和缺陷感通常都不能激励人,也不能帮助我们从错误中吸取教训。
\n只有当我们感到快乐、放松和自我接纳时,我们才无所不能。
\n当你使用基于真相的治疗法时,就可以像科学家一样,通过实验来测试自己的消极想法,看看这些想法是不是真的有现实依据、这些依据是否真实有效。
\n检查证据法、实验法、调查法和重新归因法。
\n核心思想就是:真相使你自由。
\n当你的消极想法中包含“妄下结论”这种扭曲认知时,检查证据法就会特别有用。
\n妄下结论”的两种常见形式——臆测未来和读心。“臆测未来”是指你自己对未来进行了一些可怕的预测,而这些预测没有任何事实依据。
\n“情绪化推论”是很容易产生误导的,因为你的感受来自你的想法,而不是现实。
\n当你使用实验法时,你需要做一个实际的实验来测试消极想法或自我攻击信念是否真实,就像科学家测试他们提出的假设理论一样。
\n实验法是有史以来为治疗焦虑而开发的最强有力的方法。
\n实验法可以帮助我们治疗抑郁和焦虑,但它最大的效用是用来治疗惊恐发作。
\n惊恐发作是我们对无害的身体反应的过度解读引起的。
\n过度呼吸会导致血液中的氧气增加,并产生轻微的头晕,手指也会觉得刺痛。
\n人之所以会晕倒,是因为心跳减慢并且血压下降。这时心脏不能将足够的血液和氧气输送到大脑。而晕倒恰恰是身体自身的一种“关机”防御机制。
\n认知疗法背后的理念:当你改变思考方式的时候,你就可以改变你的感受。
\n真正发疯的人会认为全世界都是疯子,而自己却不是疯子。
\n命运的安排和你没有任何关系,你并没有错。你的问题不在于你到底是不是一个负担,而是你一直在责怪自己,并且不断地告诫自己不可以成为大家的负担。
\n每一个人活在世上都有成为负担的时候,这也是我们生而为人的一个特征呀。”
\n问一问身边的人,并找出答案,而不是对其他人的想法和感受自顾自地进行假设。
\n“重新归因”的目标不是使失败合理化,而是用一种更现实的角度来了解发生的每一件事。
\n“非黑即白”的思维模式会引发表现焦虑,你会认为自己的表现必须非常完美,否则自己就是一无是处的。
\n情绪变化的必要和充分条件吗?必要条件是,这个积极想法必须是百分百真实的。充分条件是,你必须要能够让自己相信消极想法是个彻头彻尾的谎言。
\n准备和过程中的努力都在你的掌控之中,但结果往往并非如此。
\n即使整件事没有按照我预想的方向发展,但我的处理方式仍然是正确的,在这样复杂的情况下,我已经做得很好了。
\n当你使用语义法时,你只需用更友善和更温和的语言来代替你在感到不安时使用的那些带有极强感情色彩和伤害性的语言。
\n当你焦虑或沮丧的时候,你很可能对自己使用“你应该”“你必须”“你不得不”一类的句式。
\n指向自己的“应该”句式会引起抑郁、焦虑、自卑、内疚和羞耻的感觉。
\n而指向他人的“应该”句式则会导致怨恨与愤怒的情绪。
\n乱贴标签和“应该”句式往往相伴而来。而语义法就可以帮助我们扭转这两种想法。
\n当你将“应该”句式指向整个世界时,你会感到沮丧。
\n“应该”句式是非常难以摆脱的,因为这种句式会让人上瘾,并让人感到一种道德优越感。
\n大多数情绪化的痛苦都源于我们对自己和他人的“应该”和绝对主义要求。
\n当你使用语义法时,你就需要在考虑自己的问题时,用较少感情色彩和情感负荷的语言来代替原先使用的那些伤害性语言。
\n在日常会话中,“应该”一词也是有实际用途的,比如说:道德意义上的“应该”,法律意义上的“应该”以及自然界中普遍法则意义上的“应该”。
\n导致情绪困扰的“应该”句式通常不属于这三类中的任何一类。
\n表现焦虑源于对失败的恐惧。
\n以偏概全”可能会让你产生焦虑和抑郁的情绪,因为你会觉得你的自尊和骄傲岌岌可危。
\n当你使用“具体法”时,你会坚持现实并避免对自己做出过于概括的判断。
\n可以将自己的目光聚焦于某种特定的优势或劣势上。
\n请始终记住,我们的痛苦不是来自现实,而是来自我们对现实的判断。而且,这些判断很多时候都是错觉。世间万事万物本无成败或强弱之分,有区别的只是我们脑中的想法罢了
\n当你为自己辩驳时,你会创造一种战争一触即发的紧张状态,这会让批评者更有冲动想要再次攻击你。
\n我们要把以偏概全的非针对性偏见具体化。
\n自我监控法真的非常简单。你所要做的就是数一数你全天所有的负面想法。
\n每次只要你有一个消极的想法,你就主动按下计数器边缘的按钮,表盘上的数字就会加1。
\n当你放弃强迫性的习惯时,你的焦虑几乎总会持续数天,这就有点像是戒毒时的戒断反应。但如果你坚持下去,你的强迫性冲动通常会消失。
\n如果你想尝试使用“自我监控法”,请记住,在沮丧的想法减少之前通常需要一段时间,因此你应该计划坚持至少三周。
\n“放任担忧法”是矛盾治疗法当中的一种。使用这种方法的时候,我们不去攻击自己的消极想法,而是顺其自然,并屈服于它们。
\n你每天可以自己安排一个或多个时段来放任自己感到忧虑、沮丧或内疚。在这些时间段,你可以尽可能地用消极的想法折磨自己,让自己最大程度地感到沮丧。剩下的时间,你就可以专注于积极并且富有成效的方式过你的生活了。你可以使用这种方法来克服引发焦虑或抑郁的想法。
\n笑声可以表达出很多言语无法直接表达的东西。当你在笑的时候,其实意味着你不再那么把自己当回事,你发现了一直以来困扰着你的恐惧、担忧和自我怀疑竟然如此荒谬。实际上,笑声传达了自我接纳和接受他人的信息。
\n其实我们也有三种方法可以有意地利用幽默来建立与病人之间的纽带,这三种方法分别是:害羞暴露练习法、悖论放大法和幽默想象法。
\n在做完蠢事之后,你会发现大多数人都不会看不起你,世界也并没有因为你做了一次蠢事就走到了尽头。
\n我们并不总是如此刻板,也不需要总是把自己太当回事。很多人其实都不排斥善意的小幽默,有的时候甚至奇怪一些也无所谓。因为大部分人的生活都是很无趣的,所以人们总是想要寻找笑料,点亮生活。
\n你可以相信并夸大自己的消极想法,而不是一味地反驳它们。使用这种方法就要求我们放弃和消极想法一辩高下,而是尽可能地将消极想法夸大,越夸张越好。
\n将你的焦虑放在一个更幽默的环境中,这样你在对待自己的不足和犯下的过错的时候,就不会过于内疚或自我怀疑,这无疑是焦虑的解毒剂。
\n幽默法的目标是帮助你看到你的恐惧中的荒谬。
\n“声音外化法”通常需要两个人在场。另一个人可以是朋友,也可以是家庭成员或是治疗师。
\n扮演消极想法角色的人听起来像是攻击你的另一个人,但我们必须学会抛开现象看本质。其实这“另一个人”正是你自己内心的消极想法。你其实是在和自己进行战斗。
\n扮演消极想法角色的人记得使用第二人称“你”,相反,扮演积极想法角色的人要以第一人称“我”来说话。
\n你也可以自己使用这种方法,而不需要其他人。你只需在纸上写下两个声音的对话,就像你在本章中读到的那些对话一样。
\n接受悖论法是一种反向运作的精神治疗方法。在使用接受悖论法的时候,你不是在一味攻击自己的消极思想,而是在这些消极想法中找到一些正确的地方。你同意这些消极的想法,但是要以一种幽默、平和和学习的方式。
\n如果你突然发现其实这些消极想法都是不实的,它们立刻就会失去力量。
\n接受悖论的目的不是隐瞒或否认你的缺点或瑕疵,也不是让你甘于平庸的生活,而是要把你的缺点暴露在光天化日之中,这样你才能不带一丝羞耻感地接受它们。如果你发现自己确实有问题,你就可以努力改善它。如果这些问题恰好是你无法改变的,你就可以简单地接受它并继续你的生活。
\n而当你使用激励治疗法时,则会问:“这种消极的想法或感觉对我有利吗?这种心态有什么好处?这样做对我来说有怎样的影响?”
\n虽然焦虑、抑郁和愤怒可能会给我们带来剧烈的痛苦,但它们往往会为我们提供可以让人上瘾的隐藏奖励。
\n成本效益分析有五种不同的形式: 1. 认知成本效益分析:评估一个消极思想的优点和缺点,
\n直接成本效益分析,
\n首先,我们将想要改变的想法、信念、感觉或习惯写在空白成本效益分析表的顶部(
\n矛盾成本效益分析法利用了这样一个事实:消极的思维模式、情绪和习惯可能会让你非常痛苦,也可能会给你带来好处。
\n恶魔建议法是为克服不良习惯和成瘾而开发的最强大的方法之一。这种方法基于一个简单而有力的想法——具有诱惑力的积极想法使我们屈服于习惯和成瘾。
\n大多数有坏习惯的人都不想改变。习惯和成瘾是会带来好处的,达到情绪的高潮状态也是一件有趣的事。
\n在上一章中,我给大家介绍了两种可以帮助我们克服拖延的方法:矛盾成本效益分析法和恶魔建议法。在本章中,你还会学习到另外四种技巧,它们可以帮助我们打破拖延的循环,提高工作效率和创造力,这四种方法分别是: 1. 快乐预测法 2. 任务拆解法 3. 反拖延法 4. 问题解决法
\n下文有一张“快乐程度预测表”。在“活动”一栏中,你可以记录下各种可能带来愉悦、促进学习或个人成长的活动。
\n在“预测满意度”这一栏中,你要预测每个活动的满意度并打分,打分范围从0分(完全不满意)到100分(完全令人满意)。
\n很多人都会发现,你最快乐的时候可能恰恰就是你和自己独处的时候。这就可以说明,真正的幸福只来自与其他人相处的经历的想法其实并不准确。
\n你可以将复杂的任务分解为一系列可以在几分钟内完成的小步骤。然后你可以一次只进行一个步骤,而不是试图一次完成所有事情。
\n当你只是一步一步地完成任务时,你会经常感到更有动力,而不会去关心自己为什么拖延这件事。
\n大多数拖延症患者都认为动机是第一位的,而行动则次之。但那些成功人士都知道,真实情况其实恰恰相反,行动才是最重要的,动机次之。
\n如果你每次都得等到“觉得想要”做的时候才去处理那些不愉快的任务,你就会永远等待。
\n真正的问题不在于“我能完成这项任务吗?”而是“我愿意完成这项任务吗?”以及“完成这项任务会对我有什么价值?”。
\n拖延者常常认为他们有权拒绝所有困难或不愉快的任务。他们觉得生活应该总是轻松愉快、没有挫败感的。
\n从来没有一条规则规定我们的生活永远都是轻松且有收获的。某些任务可能永远都不会令人愉快。
\n因为拖延的实质其实就是“明日复明日”。
\n没有任何事能阻碍你,你不需要很多花哨的步骤来解决问题。真正的问题一直都只是:你到底愿不愿意做这件事。
\n行为疗法认为,人们可以学会快速、直接地修正那些会造成精神问题的感受和行为,而不仅仅是靠在分析师的沙发上进行自由联想或探索过去。
\n焦虑的病人可以通过直接接触他们担心的事情来战胜自己内心的恐惧。
\n让患者暴露在担心的事物之前通常是一种有效的治疗手段。
\n暴露疗法其实源于《西藏渡亡经》中的一个传说。
\n如果你想要彻底战胜你的焦虑,你就必须要直面你心中的怪兽,战胜你心中最深的恐惧。这一概念也是暴露疗法的基石。恐惧让焦虑持续存在,而暴露在恐惧面前就是治疗焦虑的不二法门。
\n逃避会助长你的恐惧,增加你的焦虑。
\n暴露治疗法有三种基本类型:经典暴露法、认知暴露法和人际暴露法。
\n典暴露法需要我们在现实中面对恐惧。这
\n当你征服恐惧时,会有一种兴奋的感觉,那会使你曾经害怕的东西成为你快乐的
\n屈服和接受自己的恐惧通常是成功的关键。
\n我的恐惧层次结构图
\n强迫指的是人们为防范危险而采取的任何重复的、迷信的行为。
\n反应预防是所有强迫性仪式的首选治疗方法。使用这种方法,你只需要拒绝屈服于强迫性的冲动。停止这类仪式后,你会暂时变得更焦虑,就像戒断反应一样。但在你坚持一段时间后,冲动最终会消失。
\n当你屈服于你最害怕的事情时,康复可能只需要几分钟的时间。
\n当令你恐惧的东西仅仅只作为一个生动的记忆或可怕的幻想存在于你的大脑中的
\n知暴露法包括认知洪水法、图像替换法、记忆重写法和恐惧幻想法。这
\n如果你想要彻底战胜你的焦虑,你就必须要直面你心中的怪兽,战胜你心中最深的恐惧。
\n如果你想使用图像替换法,那么当你感到焦虑的时候,你就可以试着调整脑内消极的图像和幻想,让你的思绪充满想象力。
\n当你使用恐惧幻想法时,你会进入一个噩梦般的世界,在这个世界中你最害怕的事情会成真。
\n现实中不存在敌对的批评者,这一切只是你自己内心最深处的恐惧的投射。你真的是在和自己做斗争。
\n认知疗法认为,是我们的想法创造出了所有的积极情绪和消极情绪。
\n感到害羞的人并不愚蠢。为什么他们会相信这些扭曲的信息?这是因为,消极想法在此时此刻变成了自我实现的预言,所以它们看起来才如此真实。
\n害羞之中的认知扭曲
\n你觉得自己是一个受害者,你永远不会想到整个场景都是你自己的扭曲思维的直接结果。是你在强迫对方以你害怕的方式对待你。
\n五种人际暴露法分别是:微笑打招呼练习、搭讪练习、拒绝练习、自我揭露法和大卫·莱特曼法。
\n如果你很容易害羞,你可以做同样的事情。你可以强迫自己微笑,每天向十个陌生人问好。通常你会发现人们比你预期的要友善得多。
\n如果某次搭讪有效的话,应该会有如下的效果:
\n·别人会感觉到你很特别,并且敬佩你。
\n搭讪的第一个秘诀是要记住这只是一场游戏。搭讪本身就是为了追求乐趣。但如果你认真对待它,你可能就会失败,因为这个世界上不存在魔法。很多人对自己的生活感到厌倦,希望能够偶尔分分心。
\n如果他们感觉到你是以一种非常轻松的方式在搭讪,而不是严肃、认真地进行对话,他们会更喜欢你。但如果他们觉得你很饥渴或是想追他们,他们就会拒绝你。
\n人们总是喜欢那些他们求而不得的东西,而从不想要唾手可得的东西。
\n成年人基本上都是小孩,他们只是长大了而已,并看起来有些严肃,但究其根本,我们这些成年人仍然想玩,而且想玩得开心。
\n大部分人的生活都是很无趣的,所以人们总是想要寻找笑料,
\n如果你害怕被拒绝,你就可以试着尽可能多地积累被拒绝的经验,这样你就会知道,即使被拒绝,这个世界也会照常运转。
\n习惯被拒绝是开展更激动人心的社交生活的第一步。
\n自我暴露法要求我们不再在社交场合隐藏自己的害羞或紧张感,而是公开披露它们。
\n你完全可以向外界展示你的羞怯,而不是试图隐藏它,然后让自己看起来很“正常”。
\n自我暴露法认为你因为害羞而产生的羞耻感才是你真正的敌人。如果没有这种羞耻感,害羞实际上可以成为一种资产,因为它可以让你看起来更加脆弱和有吸引力。
\n其实,大多数人都对谈论自己更感兴趣,给别人留下深刻印象的最好方法就是把“他人”放在聚光灯下。你可以让其他人谈他们自己,然后你带着敬意去听。这可以让你成为观众,而不是表演者,这就可以大大减轻你的压力。
\n使用有效沟通的五个秘诀,
\n解除武装法:即使对方的言论听起来非常荒谬,也要努力找到对方言论中可圈可点的部分,每个人都喜欢被肯定。
\n思想同理和感受同理:试着通过对方的眼睛看世界。
\n你可以换一种方式总结对方说过的话,然后再加以反馈,这样一来,对方就知道你在听,并且也了解到了你的想法。
\n质询法:提出简单的问题来吸引对方。
\nEAR。在下面的图上,我们可以看到,EAR是三个词的首字母缩写,分别代表Empathy(同理心)、Assertiveness(肯定)和Respect(尊重)。
\n有效沟通的五个秘诀(EAR)
\n有效沟通的五个秘诀可以通过两种不同的方式帮助你在做公共演讲的时候摆脱焦虑。首先,因为你会发现你有一种神奇的方式处理人们在演讲期间对你说的任何话,所以焦虑自然而然就消失了。其次,如果某人确实提出了一个令人讨厌或困难的问题,并且你巧妙地使用了解除武装法和夸赞法,那么他们就会积极回应,因为他们会发现问问题是很安全的。这将让所有的观众情绪高涨。
\n大约75%的焦虑症患者都在隐藏自己的情绪和感觉。
\n只要我们把这些情绪问题拿到台面上来,焦虑很快就消失了,
\n大多数患有焦虑症的人都过于善良。我觉得,善良几乎是所有焦虑的原因。
\n你总是过于善良,而且你并不总是敢于展示你的真实感受。
\n这些感到焦虑的人甚至不知道自己的感受。
\n如果你感到焦虑,情感隐藏法绝对值得一试。这个方法有两个步骤: 1. 发现问题。
\n找到解决方案。
\n虑其实是你的身体在告诉你:“嘿,你对这件事感到不安,去查看一下吧。”
\n情感隐藏法涉及两个步骤: 1. 找出困扰你的问题或感觉。 2. 表达你的感受,并采取措施解决问题。
\n如果你很容易产生焦虑的情绪,你常常会无意识地忽略自己的感受,而被你忽视掉的感受会间接地出现,伪装成焦虑的样子。
\n当人们感到沮丧的时候,有些人会开始担心,有些人会产生恐惧症,有些人,比如特丽,会发生惊恐发作,还有一些人则可能会出现强迫症状。
\n·焦虑通常是对你的冲突或问题的象征性表现。这是你的大脑间接传达你的压抑感受的方式。
\n焦虑就像一个清醒的梦。焦虑的人就像艺术家和诗人一样,间接地用图像和隐喻来表达感情。
\n大多数人认为焦虑是一件坏事,不是好事。但我持相反的观点。没有人能一直感到幸福。我们都会不时地感到心碎和失望。
\n焦虑一定是人为的某些因素引起的,而焦虑背后真正的恐惧则是对真实情感和感受的恐惧。
\n战胜恐惧的40种方法
\n每日情绪日志”的五个步骤: 第一步:描述一件令人难受沮丧的事件,记录下任何一个让你觉得焦虑或沮丧的瞬间。 第二步:在表格中圈出符合你消极感受的词语,并按照从0(完全没有这样想)到100(完完全全是我的想法)的等级进行评分。 第三步:记录下你的消极想法,根据你对每个想法的相信程度在0~100之间进行打分。 第四步:找出每个消极想法中的扭曲认知。 第五步:用更积极、更现实的想法替换原有的消极想法。根据你对这些积极想法的相信程度对它们在0~100之间进行打分。现在,再次评估你对每个消极想法的相信程度。
\n如果康复圈中的消极想法让你感到焦虑,请确保你选择的方法中包含了三个种类的方法:认知治疗法、暴露治疗法和情感隐藏治疗法。这其实是一个很好的方法组合,你选择的方法中可以包括十二到十五种认知疗法、两种或三种暴露治疗法和情感隐藏治疗法。
\n平均而言,你必须尝试至少十到十五种方法,才能找到一个行之有效的识破消极想法的方法。
\n康复圈是为“每日情绪日志”提供动力的引擎。
\n在你想出一个能够满足情绪变化的必要和充分条件的积极想法之前,你的情况是不会有所改善的: ·必要条件:积极想法必须是百分百真实的。 ·充分条件:积极想法需要让消极想法不攻自破。
\n表22-15 基于消极想法中的认知扭曲选择方法
\n22-16 基于你正在克服的问题选择方法
\n战胜恐惧的40种方法
\n当你使用检查证据法时,你会问自己这样的问题:“有没有什么可靠的证据可以支持我的消极想法?我是怎么在第一时间得出这个结论的?”
\n没有公式或噱头可以盲目地应用于不同的问题或焦虑类型,相反,我给了你一些灵活、强大、个性化的方法,你可以用它们来克服困扰你的情绪问题。
\n如果你曾经因焦虑或抑郁而挣扎,你迟早会再次感到焦虑或沮丧。事实上,所有人的焦虑都会复发!
\n佛陀说,痛苦是人类的固有特征,这是不可避免的。没有人能够一直感到幸福,如果可能的话,一直开心其实也不会是件好事。如果我们一直很开心,我们的情绪就没有任何变化,也不存在任何挑战,生活很快就会变得无聊,因为我们总是感觉完全一样。古拉丁谚语说得好:饥饿是才最甜的酱汁。
\n复发时的认知扭曲
\n复发每日情绪日志
\n复发每日心情日志(续表)
\n要经常让自己面对心中的恐惧,这样你的信心就会增长。
\n焦虑或恐慌的感觉并不是一件坏事,而是一个重要的信号,表明有你需要注意的事情发生了。
\n可以看到我在去年8、9月份频繁更新了一批文章,然后在11月就戛然而止了。
\n昨天早上坐在旁边的同事告诉我,他的女朋友周末把我的博客通过文字转语音的方式边听边做家务,并且想要人肉催更。每次听到有人说读了我的博客,而且希望催更,我都既兴奋又诚惶诚恐。兴奋是因为有人能喜欢读我喜欢写的东西,惶恐是因为居然有人喜欢我写的东西。
\n实际在看似停更的这小半年来我并没有停,并且再坚持每日一更,只不过内容放在了另一个站点上,域名是 https://diary.jiapan.me/ 。从域名可以看出,这是我写日记的地方,站点标题叫「小小的避难所」,灵感来自毛姆写的《阅读是一座随身携带的避难所》这本书的书名,正如名字写的这样,我把那里作为我的避难所来记录、倾诉我的所感所想。当然那个站点上的内容也不是每天都会更新发布,而是根据我的心情,想起来了就整理一批我在Notion中写的内容发布出去。
\n进入避难所有一点小小的门槛,需要留下你的邮箱和阅读原因,邮箱只要是常见域名就可以,会收到一个入场验证码,输入验证码后再说明原因就可以进入了,原因我并不会审核,只要大于5个字符就可以了。
\n设置门槛的原因有两个,首先是我希望让这些内容可控,我需要知道都被谁访问过,其次是我不希望这些内容会被爬虫抓到,或者说可以通过搜索引擎搜到。
\n为什么我把那些内容单独隔离到了另一个站点内,而没有放在这里,因为那些都是我的日常碎碎念、流水账,每一篇内容都写的很零散,每天晚上我会回顾一下今天值得纪念的事情。拿出几样来记录一下,没有任何主题。
\n这个博客内大部分内容都是围绕着一个主题来写的,但这种写法很费精力,而且实话实说我并没有那么多干货。当然我也知道写这种结构化的文章相比写流水账,对个人来说会有更好的提升,我想先通过记录流水账的方式把写作这个习惯培养起来,然后再慢慢进阶。
\n所以,如果想继续读我流水账的朋友可以左转进去我的小小避难所,但我也先在这里做个免责声明(狗头保命),那些内容确实不体系化,没有营养,没有干货,读后可能会让你大失所望。引用曹公的一句话:满纸荒唐言。
\n顺便说一句,昨天和老板提了离职,准备开启一段新的征程大海,去向暂时保密,等未来有了水花再回来聊一聊这段经历叭。
\n"},{"title":"Actors 和 CSP 并发模型介绍","url":"/2020/actors-and-csp-introduce/","content":"\n介绍并发模型前,我们先来理解一下并发和并行的区别,下边这张图说明了两者之间的区别:
\n\n\n\n并行具有并发的含义,而并发则不一定并行。
\n
并发编程模型按照实现方式可以分为两类:
\n共享状态并发涉及到可变状态(Mutable state,即内存可修改)的概念。大多数语言,如 C、Java、C++ 等等,都有这个概念,即有一种叫内存的东西,我们可以修改它。
\n在只有一个进程(或线程)工作的情况下,这个模式可以很好地运行。但如果有多个进程共享和修改相同的内存,就会产生问题和危险。
\n为了防止同时修改共享内存,我们需要一个锁机制。你可以称它为互斥量或同步方法,但它本质上仍然是锁。
\n如果程序在关键区域发生崩溃(例如,当它在持有锁的时候)就会有灾难的发生:其他所有的进程都不知道该做什么。
\n多线程模型就是通过共享状态实现的并发,代表语言有:Java, C#, C++。
\n同时,根据上边的内容可以推导出:
\n不可变数据结构(Immutable) = 没有锁
\n不可变数据结构(Immutable)= 易于并发
\n在消息传递并发中,不存在共享状态。所有计算都是在进程中完成的,交换数据的唯一方法是通过异步消息传递。
\n如何理解这句话?
\n想象一群人,他们没有共享的状态。
\n我有自己的记忆,同时你也有你的记忆。它们是不共享的。我们通过传递信息(如,说话)进行交流,我们根据接收到的这些消息更新私有状态(也就是自己的记忆)。
\nActors 模型 和 CSP 模型 是通过消息传递实现的并发。
\n对于多线程模型大部分开发人员都是比较熟悉的,也知道它存在很多缺点,如:死锁、不易伸缩。
\n接下来的内容我们重点对两个基于消息传递并发的模型来进行介绍和对比,Actors 和 CSP 是实现程序并行工作的两种最有效的模型。
\nActors 模型,顾名思义,侧重的是 Actor。每个 Actor 与其他 Actor 进行直接通信,不经过中介,且消息是异步发送和处理的。
\n\nCSP 是 Communicating Sequential Processes(通信顺序进程)的简称。在 CSP 中,多了一个角色 Channel,Worker 之间不直接通信,而是通过 Channle 进行通信。
\nChannel 是过程的中间媒介,Worker1 想要跟 Worker2 发信息时,直接把信息放到 Channel 里(在程序中其实就是一块内存),然后 Worker2 在方便的时候到 Channel 里获取。
\nChannel 类似 Unix 中的 Pipe,后文将 Channel 称为通道。
\n\nCSP 是完全同步的。通道的写入方在接受方读取前会被一直阻塞。这种基于阻塞机制的优点是一个通道只需要保存一条消息,在很多方面也更容易推理。
\nActors 的发送方是异步的。无论消息接收方是否将消息读取出来,发送方都不会阻塞,而是将消息放入通常称为邮箱(mailbox)的队列中。这提供了很多的便利,但困难之处在于邮箱可能需要容纳大量的信息。
\nCSP 进程使用通道(channel)进行通信。程序可以将通道作为第一类对象(first class objects)创建并传递。Actors 有地址系统和收件箱,每个进程只有一个地址。在耦合度上两者是有区别的,CSP 更加松耦合。
\n在 CSP 中,发送和接收操作可能会阻塞。在 Actors 模型中,只有接收操作可能被阻塞。
\n在 CSP 中,消息是按发送顺序传递的,而在 Actors 模型中不是这样。事实上,系统可能根本无法传递某些消息(意味着消息可能会丢失)。
\n到目前为止,CSP 模型在一台机器上工作得最好,而 Actors 模型很容易实现跨多台机器的扩展。
\nActors 更适合于分布式系统。
\n由于 CSP 具有阻塞性,因此很难在多台计算机中使用它们。
\n"},{"title":"你方唱罢我登场","url":"/2023/after-you-sing/","content":"\n\n「乱哄哄,你方唱罢我登场,反认他乡是故乡。甚荒唐,到头来都是为他人作嫁衣裳。」
\n
这是《红楼梦》中的甄士隐听到跛足道人的「好了歌」后提的注解中的最后一句。表达的是:朝代兴亡就像演戏一样,你唱完了下台,轮到别人来唱。
\n大到国家,小到公司都是如此。下面说说最近一年多里我在公司中所经历的「你方唱罢我登场」的三件事。
\n与业务线并行,公司成立了一条职能线,由一位公司元老级别的 DBA 主管作为这条线的负责人,这位负责人找了另一位技术上比较有威望的同事,也就是我前领导辅助他一起推进职能线的建设。
\n前期风风火火,对未来规划的风生水起,各种畅想,计划了很多听起来非常牛逼的大工程和人员培养计划,推行了半年没有什么起色,实际上这半年来大部分工作也是他的副手也就是我的前领导来做的规划和进行的具体推进。
\n后来这位负责人因为休陪产假,一段时间内没有推进工作,公司高管对他不满,所以就把他赶下了台,让他的副手上任了。这位负责人在担任这个职位前是 DBA 团队的主管,担任这个工作后公司为 DBA 组补充了新的主管。他从这个位置下来后,只能在 DBA 组内做一名普通的员工了,不到一个月时间他就提了离职。
\n他的戏演到头了,为他之前的副手,也就是我的前领导做了「嫁衣裳」,接下来该副手登场了。
\n我的前领导上任后,也是新官上任三把火,通过安排大量会议而让这条线有存在感,还要考核大家的代码量,安排每人每周进行分享之类的工作。当然,这些也并不是他的本意,具体情况我就不讲了。
\n我能看出这也只是强弩之末,光是通过这些手段是做不起来的,但因为交情上的缘故,我还是会配合他做好他安排给我的工作。
\n随着下边抱怨的声音越来越大,逐渐降低了会议和分享的频率,再往后就慢慢取消了。这样苟延残喘了一年,也没有任何起色,公司之前给他的饼也没有兑现,刚好外部有不错的机会就提了离职。
\n他本来是一位技术方面非常让人信服的技术管理者,但因为想一直往上爬,为了证明自己,最后只能悻悻离场,之前那么高调的人最终却非常低调的离开了公司,很惋惜。但他明知不可为而为之的勇气非常领我佩服,他身上的那种人格魅力是值得我学习的地方。
\n第三个故事就和前两个没有关系了。我当前所在公司的创始人将公司卖给集团几年后就离开公司二次创业去了,我们公司作为集团的一个事业部独立运营。现在事业部负责人是去年十月底任命的,前两天也听说了他要离开的消息。具体是因为产出不及预期还是他的思路和集团高层有分歧就不得而知了。
\n下一个继任者是集团的创始团队之一,不知能把这场戏唱多久。
\n三个故事讲完了,最后再读一遍完整的《好了歌注》吧。
\n甄士隐
\n陋室空堂,当年笏满床;
\n衰草枯杨,曾为歌舞场;
\n蛛丝儿结满雕梁,绿纱今又在蓬窗上。
\n说甚么脂正浓、粉正香,如何两鬓又成霜?
\n昨日黄土陇头埋白骨,今宵红绡帐底卧鸳鸯。
\n金满箱,银满箱,转眼乞丐人皆谤。
\n正叹他人命不长,那知自己归来丧?
\n训有方,保不定日后作强梁。
\n择膏梁,谁承望流落在烟花巷!
\n因嫌纱帽小,致使锁枷扛;
\n昨怜破袄寒,今嫌紫蟒长。
\n乱烘烘你方唱罢我登场,反认他乡是故乡。
\n甚荒唐,到头来都是为他人作嫁衣裳。
\n人到底要走向哪里去,什么是生命的本体。我们追逐的东西是不是生命里面真正想要的、觉得最重要的?我们误认了世俗里面虚拟出来的假象,把它们当成了故乡,努力地飞奔而去。其实那只是「他乡」而已,并不是生命本质的东西。
\n"},{"title":"《格局》摘抄","url":"/2021/altitude-extract/","content":"人有多大的气度,就做多大的生意
\n格局大的人追求的是重复的成功和可叠加式的进步,格局小的人满足于自己某件事做得快、做得漂亮。
\n要做到高速率、可叠加式的进步,关键是做减法,懂得放弃。
\n管理上级不是给上级分配任务,也不是不服从上级的安排,而是让上级了解我们的工作,并且在必要时及时寻求上级的帮助。对于这样具有高度主动性的员工,上级都喜欢。
\n凡事总有“两面”——好的一面和坏的一面,当大家一致觉得一件事只有好的一面时,并不代表它不存在坏的一面,很可能是大家认识不够深刻,没有看到一些盲点。而那些没有被发现的问题,一旦发生,后果可能极为严重,甚至是灾难性的。
\n对于那些人们都觉得好的事情,我会格外小心,因为我们可能忽视了它们的问题。
\n众利勿为,众争勿往
\n很多投资人以为抢一条所谓的“赛道”就能分一杯羹,岂不知众人相争,最后只有一个结果——相互碾轧致死。
\n当一种特长被很多人掌握之后,就不叫特长了。
\n为什么中国人在硅谷**晋升得没有印度人快,原因有很多,其中一个小原因是,部分中国人在**分享利益**这件事上做得不好,不注重相互提携**。
\n我们的祖先在《礼记•大学》中这样告诫大家:“好而知其恶,恶而知其美者,天下鲜矣。”
\n对比较理性的人来讲,他们通常不问做错事是否有理由,而是确定当前是否做错了事。
\n我们要做的是超过他人的长处,而不是满足于超越别人的短处。
\n所谓不认命,就是以为世界上所有事情自己都能控制,这是一种妄念,是对自己的迷信。
\n尽人事,听天命。
\n散户在股市上亏损的根本原因在于,把偶然的成功归结为自己努力的必然结果,把失败归咎于别人,对市场完全没有敬畏之心。
\n为什么要听天命呢?因为世界上稍微难点儿的事情都非常复杂,超出我们的有限认知,更超出我们的控制能力。
\n承认天命的作用,我们在做人时就不会恃才傲物。但凡觉得自己了不起的人,通常都没有见过真正聪明能干的人。人只有到了人才荟萃的地方,才能体会到自己水平上的不足。
\n比才能更重要的是见识,而在见识之上还有运气。
\n人的命运是由大环境和自身做事情的方法决定的。
\n业余的水平再高也是业余的。
\n对绝大多数人来讲,一次好运气并不足以改变命运。
\n遇到任何倒霉的事情,一定要认命,不要总想着挽回损失,这样损失就会被限制在局部。
\n如果认识到自己只是一个普通人,自己的那点儿所得不过是上天的恩赐,得到了固然可喜,得不到也在情理之中,就愿意割舍,也就不会造成更大的损失。
\n人不会总有好运气,也不会永远走背运,但是不好的心态会让厄运不断被放大。
\n人在一个环境中待久了,难免产生思维定式。
\n跳出思维定式的最好办法就是放下手中的工作,休息休息。
\n从忙乱中退一步,思考一下目的,能省掉多余的需求和行动,减少不必要的麻烦,让我们更快地接近目标。在诸多目标中,终极目标当属生活本身。
\n每一次重大科技进步的结果总是财富进一步向少数人集中,大部分人的生活压力更大了。
\n很多事情,我们连做它们的目的都没有想清楚,就在世俗力量的驱赶下随着奔涌不停的人潮匆匆去做了。
\n人不在于开始了多少件事,而在于完美地结束了多少件事。
\n对于人来讲,说得通俗点儿,多任务并行就是一心多用。
\n如果一心多用,不仅不能多做事情,反而会因为来回切换任务而降低工作效率,还容易导致错误不断。
\n辛苦且回报低的专业能找到,但是轻松而回报高的专业几乎不存在。
\n对速成的崇拜也是“瞎忙族”的一大特点。他们相信自己能找到别人找不到的捷径,而不是沉住气慢慢提升自己。
\n只要把做事的节奏慢下来,先动脑,再动手,把可做可不做的事情从任务清单上删除;在做事的过程中按部就班地把事情做好,不要开了很多头却不结尾;做完事情,审视一下自己的得失,评估一下效果,以备将来参考。
\n战术上的勤奋掩盖战略上的懒惰。
\n当遇到困境时,我们首先应该慢下来,斩断厄运链。
\n世界上没有任何一个人重要到什么事情缺了他就不能运转了。
\n休息的本质是从外界获得信息和能量。
\n真正的成功者,真正有幸福生活的人,应该在现实生活中获得成功,获得最真实和最丰富的生活。
\n每一个人的具体生活是独一无二的,既不能由别人代替,也不可能等以后有时间再补上。
\n我们做的那些引以为豪的事情,其实远没有我们以为的那么重要。
\n幸福生活才是目的,个人的成功不过是实现这个目的的途径和手段而已。
\n新加坡最大的好处是“省心”,一个人只要从小当好学生,然后上好学校,将来努力工作,就能挣到钱,并且赢得他人的尊重。相比之下,我们的努力往往未必能得到回报。这种不确定性会让人觉得看不到希望,幸福感自然不会高。
\n人这一辈子,大部分时候需要的不是去战斗、去征服、去比别人考得好,而是要对别人有用。
\n《红楼梦》还有一个特点:它是一本关于女孩子的书。在《红楼梦》中,贾宝玉在某种程度上都被女性化了,这在中国的经典著作中很少见。男生若要读懂女生的心思,不妨读读它。
\n一个人一辈子的幸福在很大程度上取决于他(她)的婚姻。
\n很多在美国上市的中国公司,上市后业务增长得不错,但是由于根本不关心投资人的利益,股价几乎不上涨,甚至低于刚上市时的水平。这些公司就是对投资人不好的公司。
\n巴菲特所谓的好公司有这样几个共同的特点:
\n一个帮助过你的人,比一个你帮助过的人,更愿意帮助你。
\n一个人在选择工作单位时,应该把对自己好、能帮助自己成长的公司放在首位,而不是觉得某家公司很酷、很热门或者多给了一点儿薪水就选择它。
\n其实在所谓“命”的背后,起主导作用的是我们判断价值的方法。
\n至于生活的伴侣,对自己好是比金钱、门第和外貌更持久的依靠。
\n素质教育是以掌握一项技能为前提的。
\n我追求的是一种最好只有我能做,别人难以胜任的工作,也就是要体现出我的不可替代性。
\n这是真正自由的人的想法,只有在金钱和地位面前丢弃掉奴性,保持自由人的心态,才能赢得对方的尊重。
\n一些朋友问我如何判断一件事情是否有必要做,我的标准是,那些花了精力做的事情要尽可能对自己将来的进步有益。
\n永远待在舒适区,只会让人无法成长。每个人的成长,最终是在边界内最大程度上把事情做好。
\n一个人成长的过程,其实就是逐渐“杀死”心中那些超级英雄的过程。
\n孩子最终能走多远,不取决于父母给他们描绘的承诺,而更多地取决于他们自己在不停往前走方面有多大的意愿。
\n对那些仅仅满足不失败的人来讲,失败的教训可以让他们避免犯同样的错误;但是对于想成功的人而言,失败的教训远没有成功的经验重要。一个经常失败的人会习惯性失败,相反,成功才是成功之母。
\n从失败中固然可以学到经验教训,但是**效率实在太低了**。
\n虽然人很难做一件事情就成功一件,但总该尽量避免失败,这样才能少受挫折。
\n成就的多少至少取决于三个因素:做事情的速度或做事情的数量,每一件事的影响力,以及做事的成功率。
\n对一个人来讲,如果一辈子非常努力地做了很多没有影响力的事情,还不如认认真真做好一件有一定影响力的事情。
\n一个优秀的专业人士在做事之前,会梳理出一个做事清单,按照重要性和影响力的量级排序,然后集中资源把最重要、影响力最大的事情先做完。
\n做事的多少最多不过是几倍的差异,但做事的质量以及随后带来的影响力可以达到量级之差。
\n成功不在于是否努力多做两件事,而在于能否跃迁到更高的量级。
\n提升量级不仅需要时间,还常常需要在关键时间点实现跳跃。
\n不要醉心于重复做很多影响力微乎其微的事情,否则即使再努力,也难以有大成就。
\n所谓最具普遍意义的通向成功的方法论,从根本上说,就是搞清楚做事的边界或者极限,搞清楚做事的起点以及从起点通向边界的道路。
\n做事情最有效、最容易成功的办法,就是先将自己的基线提高,而不是从地下三层做起。
\n专业人士和业余爱好者的一个差别在于,是否了解极限的存在。
\n所谓工程化,就是依靠一套可循的,甚至相对固定的方法解决未知的问题。
\n失去的朋友大致有三类。
\n朋友关系有很多类型,常见的可以归为三类:合作型、依靠型和暧昧型。
\n我们的世界并非那么灰暗,即便有挫折,也是暂时性的。
\n不论形势是好是坏,总有人对我们的生活进行悲观的解读。对未来可能发生的灾难有防范意识当然好,但是用悲观主义(包括怀疑主义)的心态做事,弊要远远大于利。因为这种心态让人惶惶不可终日,难以专注做自己该做的事情,最后变得一事无成。
\n人的过分自信以及由此造成的与现实之间的反差,是导致悲观主义的第一个原因,也是根本原因。
\n人过高估计自己的能力,在现实生活中却得不到想要的东西,才会产生悲观情绪。
\n一个人能否做成一件事,和是否有信心无关。
\n一个人不断往上走,眼界越来越开阔后,就越知道自己能力的局限,会越谦逊,越有敬畏之心,就不会再有不切实际的奢望了。
\n在中华文化圈内的国家和地区,经济腾飞阶段的第一代人,主要的财富来自在房地产上一次性的增值获利,而非工资收人。
\n焦虑,反映出人们对未来的怀疑;如果没有对不确定性的担心,就不会焦虑。
\n我们不仅无法回到过去,也不会习惯过去的生活,除了往前走,没有第二个选择。
\n乐观主义者往往不会杞人忧天,安下心来把事情做好,自然就能得到想要的结果。
\n为人处世,成功的第一要素就是走正道,不要总想着出奇制胜,特别是在未来非常光明的时候。
\n很多人一件事没有做好,就想着改变,好像一变就有机会了。且不说变化是否能给有这样想法的人带来机会,就算有,没有积累的人也把握不住机会。
\n虽然盖茨和扎克伯格退学后创业成功了,那是因为他们已经知道怎么挣钱,而不是退了学才去想挣钱的方法。
\n临渊羡鱼,不如退而结网。
\n未来的三个特点,即不对称性、复杂性和不确定性。
\n是否利用了新技术不是核心,利用新技术实现提高效率、降低成本的目的才是关键,因为降低成本、提高利润才是核心,才是不变的道理。
\n技术从来都是手段而不是目的,搞不清楚这一点,就会为了技术而研发技术。
\n洞察本质才能立于不败之地
\n事实上对大多数人来讲,更好的改变方式是学会计算机思维,将它用于自己熟悉的行业,扩大自己原有的优势。
\n在当今的商业世界里什么比较重要呢?对于商家来讲,最直接、最重要的标准是ARPU。
\n一个公司在规模不大时,在关注度上和大公司进行全方位竞争是没有意义的,它更应该关心自己的核心用户,关心自己能给他们带来什么价值。
\n在当下这样一个风险投资资金过剩的年代,通过融资买关注是一件很容易的事情。花钱买用户的事情谁都会做,但是能提高ARPU值才是真本事。
\n互联网时代从来不缺乏免费的内容,最珍贵的资源是我们的时间。不要花太多工夫读那些免费、廉价,但是质量低的内容,读它们不仅浪费时间,甚至会误导我们。
\n无论是想得到关注,还是关注别人的,都需要记住一个关键词——优质。
\n在信息可以随意复制的年代,创造信息不是什么难事,提供自己特有的、人们原先不知道的信息才有价值,重复别人的内容完全没有意义。
\n免费能够成功,是因为过去的一些东西有稀缺性,消费者不得不购买,这时免费就变得特别吸引人。当那些东西不再有稀缺性时,免费就没有意义了。
\n超越免费的第一条是制造一种稀缺性,而这需要产品、服务本身具有一种难以复制的特性。
\n时效性、个性化、可用性(易理解性)、可靠性和黏性
\n终身学习的目的就是让自己领先同辈人一步,以便成为具有时效性的人才,避免在低水平上竞争。
\n要求所有人都有一样的表现是工业时代的特征,因为只有那样才能保证行动一致,做出来的东西品质才能一致。
\n在任何时代,把事情解释清楚这个本领都可以变成一个很赚钱的生意
\n数据的积累可以让企业的护城河越来越深。
\n在信息时代,信息越透明、越对称,流动性越好,李嘉图定律导致的势差就会越大。
\n在信息时代,李嘉图定律带来的势差放大效应,会导致一个地区人员结构、产业结构的巨变。
\n随着信息流动性增强以及智能技术的提高,个别能力超强的人可以在技术的帮助下发挥巨大作用,行业里不再需要四流、五流的从业者了。
\n聘用人员时,不要贪便宜雇一大堆三流人士来充数,因为一堆三流的人聚在一起,有时带来的麻烦比他们能解决的问题还多。
\n在市场上,第二名永远无法拿到第一名的估值,第三名之后的价值几乎等于零。
\n"},{"title":"又一晚没睡","url":"/2023/another-night-without-sleep/","content":"现在是早上5:11,昨晚11点半躺下后没有任何睡意,眼睁睁一直躺到现在
\n中间尝试读书、冥想、听相声都没有缓解
\n刚刚把小红书、Twitter、脉脉这些会给我制造焦虑或者杀时间的 APP 卸载了
\n我第一次失眠是在高中时,在这之前我是每天都要午睡的体质
\n高中时非常喜欢班里一个女生,她也喜欢我
\n第一次失眠的原因是我们考试考砸了,我向她保证我们一起好好学习
\n然后那个晚上整晚都在迫切的希望自己早点睡着,早上早点起来开始学习
\n结果第一个不眠之夜就这么诞生了
\n到现在十五年了,不要说午睡,晚上很容易整晚无法入睡
\n运气好的话有时可以靠一片处方安眠药胡乱睡几个小时
\n高中时就开始了为了治疗失眠的求医之路
\n我也忘了那时候都吃些什么药了,反正是一把一把吃,也不见效
\n从失眠第一天开始,就像突然失去了睡眠的这项基本技能
\n躺在床上很虚无,忘记了该如何入睡
\n现在我会定期去医院的神经内科,开精神类处方安眠药
\n为了方便我都是挂周末取药的临时号,好几次医生都劝我挂个普通号或者专家号好好看看
\n但当我说我这个症状已经十几年了之后,医生也就不再说什么
\n据说失眠的人会出现在别人的梦里
\n既然我失眠了,希望梦到我的那个人可以一夜好眠
\n"},{"title":"外貌","url":"/2023/appearance/","content":"人的外貌是个无形的加分项,不管是在校园中、职场中还是还是日常社交中,外貌都占了很重要的位置,颜值即正义,这也是为什么现在医美越来越火的原因。
\n爱美之心人皆有之,人是视觉型动物,看到的其他人后都会先根据外貌给对方做个评判。
\n在学校里老师更喜欢辅导长得好看的学生,这一点我可以通过自己见过的两个例子来佐证。第一个是我上初二时,班里换了一个英语老师,她之前是教高中的,看了我们班的男生后说你们都没有长开,我一点给你们上课的欲望都没有。另一个是前段时间听谐星聊天会,有一期一个上麦的女老师也提到类似观点,她更喜欢叫长得好看的男学生。
\n写到这里发现一个问题,如果同样的观点是出自男老师对女学生的,那么一定会在社会上被指责,可女老师偏爱好看的男学生却不会。
\n在职场上,领导也更喜欢给长得好看的人倾斜资源。我不是圣贤,我也承认自己在这方面有“偏心”。同样一件事,给长得好看的同事就愿意多讲几句,光怕对方没听明白。对于长得一般的就不会这么上心。在工作跟进和员工关怀上,对长得好看的同事我也有意无意地偏向很多一些。
\n人们还会根据对方的外貌来给同一个行为打上不同的标签。比如一个女生在公共场合大声喧哗、和男生勾肩搭背,对于长得好看的就是活泼开朗、可爱大方、不拘小节。对于长得丑的就是没教养、不讲究、太随便、没有分寸感。
\n有时候在地铁上闻到一股屁味,我也会环顾一下四周的人,猜测是哪个人放的,被我猜测的人大概率长得也不怎么好看。🤦🏻♂️真的是很不应该的偏见。
\n宝玉的爸爸贾政,本来对宝玉很厌恶,恨铁不成钢。在《红楼梦》第23回,大观园完成省亲的任务后,贾政遵嘱元春娘娘的旨意,让宝玉同姐姐妹妹们一起住进大观园。他把宝玉、贾环叫进房来训话,看到宝玉长的这么好看心情一下子也好了,和贾环形成了很大的对比,原文是:「贾政一举目,见宝玉站在跟前,神彩飘逸,秀色夺人;看看贾环,人物委琐,举止荒疏」。
\n此时的贾政不由得又想起了已经去世的大儿子贾珠,想到自己和王夫人年事已高,很欣慰自己有宝玉这么个好儿子,心一下子软了很多:「把素日嫌恶处分宝玉之心不觉减了八九」。
\n有个好看的外表固然值得庆幸,没有也不需要自暴自弃。贾环的「人物委琐,举止荒疏」多半来自他觉得自己的是庶出导致的不自信,这一点上探春就比他自信的多。
\n\n\n"},{"title":"自动备份数据库并上传到 S3","url":"/2020/auto-backup-database/","content":"\n\n我皮囊不够好看,灵魂也不算有趣,我生于尘埃,溺于人海,关于我的一切都平淡的不像话。即便是这样,我也是宇宙的孩子,和植物、星辰没什么两样。
\n
我开发的老板管库虽然没太多收入,但是还是有不少的用户量,为了节约成本,我并没有使用厂商提供的云数据库,而是在服务器本地搭了一个 MariaDB 实例。考虑到用户数据安全还是第一位的,所以我每天会通过定时任务的方式进行全量备份,并上传到我的七牛云,脚本如下:
\n!/bin/bash |
上边的命令会生成一个以执行时间为后缀的 .sql
文件并上传到我的七牛云中名为 bosskudb 的bucket中,同时我还会配置这个 bucket 的生命周期,只保留近7天的数据。这实际上是套比较通用的流程,昨天恰好看到一个 repo:https://github.com/appleboy/docker-backup-database 就是用来提供这套流程的封装的,看到作者又是个自己比较崇拜的开发者,于是准备上手用一用。
(P.S. appleboy 大神是个非常活跃的 golang开发者,在去年学习 go 的时候就 fo 了他)
\n这个工具目前支持备份 PG 和 MySQL 数据库,并上传到 S3(包括支持S3协议的 minio) 或者本地路径下,启用方式也非常方便,写个 docker-compose 文件就可以了。
\n以下是我的操作记录:
\n首先在我的 AWS 中新建了一个 bucket,我所选择的区域为亚太地区(香港) ap-east-1,bucket 名为 bossku-db-backup。
\n\n\nversion: '3' |
ACCESS_KEY_ID
和 SECRET_ACCESS_KEY
获取方式可以查看:Where’s My Secret Access Key?。
因为我所启动的数据库实例为 MariaDB:10.2,根据官方介绍,其所对应的 MySQL 版本为 5.7,所以上边命令中的 image 我指定的是 appleboy/docker-backup-database:mysql-5.7
。
测试的时候,为了方便查看效果,可以将 TIME_SCHEDULE
删掉,这样会立即执行,且执行一次后退出。
docker-compose up -d |
可以看到成功了,再到 S3 中验证一下文件有没有上传上来:
\n\n\n文件也传成功了!
\n如果在最后的上传步骤遇到无权限的错误,可以通过尝试调整 bucket 权限来解决。
\n通过日志和上传上来的文件名可以看出,其实他也是通过 mysqldump
先生成备份文件,然后通过 S3 的 SDK 进行上传,同时也是使用了日期最为文件名的命名方式。我也大致看了下代码,所使用的 SDK 为 minio 提供的,这样又可以同时支持上传到 minio 了。
创新就是将一些已有的东西进行重新组合,比如这里只是将 docker
、mysqldump
和S3
进行了组合,就创造出了这么一个好用且通用的工具,非常值得学习。
作为程序员,我们最擅长的事情就是用程序解决问题,恨不得天天拿着锤子找钉子。
\n我们公司的服务上线流程是先由服务负责人审批,然后再由团队的 Leader 审批。如果自己就是服务负责人,则只需经过团队的 Leader 审批即可。因此,我们大部分服务负责人设置的是最经常改动上线的那个人。
\n所以无论如何设置,最终都需要我来审批。每天平均要审批30多个上线单,不论是在医院看病、开车、吃饭、开会,随时都可能有审批。而且基本上都会伴随着一个「钉」,在公司里还好一些,一请假可就要了命了。上一次请假,下午四点之前我没有什么特别重要的事情,四点后准开车出去办点事,但是好巧不巧,四点之后开始不断有上线审批,换着人轮番上线。我一手握着方向盘,另一只手拿着手机审批,幸好我的车是自动挡,否则手动挡的话我真就忙不过来了。那个时候我真的有点火大,决定一定要写一个工具来自动帮我审批。
\n第二天上班后,我就让旁边的同事提了个上线单。在审批这个上线单的整个流程中,我进行了抓包,以查看每个步骤的请求内容。最后,我梳理了整个流程发现:将5个接口请求配合起来,就可以实现自动化审批,具体细节这里不展开。
\n\n\n这里不得不再吹一次 Python,从写第一行代码到完成,不到4个小时就实现了完整的功能。
\n
要实现这个自动审批功能,还需要解决两个问题:
\n我们公司的开发平台支持两种认证方式。在内网环境下,可以通过域账户登录;在非内网环境下,可以使用钉钉登录。无论使用哪种方式,最终都是设置后端所需的 Cookies。两种方式本质上并没有太大区别。
\n我需要一个稳定的机器来循环执行审批脚本,所以我将这个脚本放在了我的服务器上。为了快速验证第一版程序中的 Cookies 是否有效,我将 Cookies 写死在了代码内,并测试了它们的过期时间。经过验证,Cookies 的过期时间为12小时。因此,每天早上更新一次 Cookies 即可。
\n更新 Cookies 需要手动操作。因为登录界面和登录接口有许多校验和加密逻辑,无法通过简单的模拟来完成。每天早上手动登录一次,然后提取 Cookies 即可。这比以前已经方便了许多。
\n接下来需要解决每天如何方便更新Cookies的问题。一开始想到的解决方案是自己编写一个API,每天调用该API来更新Cookies。评估后觉得该方案有些太重,而且没有界面的话就不能随时随地更新,只能通过Postman或者Curl命令,不太友好。最后,我采用了一个非常方便的方式。这个方法有界面,足够安全,可用性有保障。
\n这个方法会在之后单独用一篇文章来介绍,写完后在这里补充链接(我以后在程序内读取需要更新数据类的需求,大概率都会使用这个方法,敬请期待)。
\n既然已经实现了自动审批逻辑,就一定要做好监控和通知,不能盲目审批,否则后果不堪设想。
\n最初我使用的是 Bark,每次审批后会向我的手机发送一条 Push,但这样不容易查看历史消息,也不方便聚合消息。另外,我认为这些通知并不一定只有我自己可以看到,可以让更多的人看到,比如全组的同学,这样好处是大家的信息更加同步。例如,之前有一个人上线服务时,除了他自己和 Leader,其他人是不知道的。因此,第二版的通知实现是与钉钉机器人对接。为了不干扰正常的组内聊天,我专门建了一个机器人通知群,把涉及到的组内同事拉进来。
\n\n我还在通知信息中加上了上线人在上线单中填写的描述,能非常方便的看出哪个服务、上了什么功能。
\n既然有了这个通知群,那可以再利用它做些其他通知。比如
\n订餐通知:
\n\n每天随机出一道算法题:
\n\n上下班打卡+毒鸡汤:
\n这里说明一下,我们打卡基本不要求时间,只是为了方便统计考勤
\n\n\n每周五实验延期提醒和周报提醒:
\n\n\n为什么我这么看重审批,进而想要将其自动化。
\n首先,我觉得它是一项重复性的工作,确实没有必要每一次都进行人工操作。每次操作对我来说都是一次打扰。
\n其次,更重要的是,我不想因为上线审批不及时而成为团队效率的瓶颈。我一直提倡高效、不加班的工作方式,但是如果一个审批需要十多分钟,就有些大家的浪费时间了。
\n"},{"title":"避免在 Go 中使用 append","url":"/2021/avoid-using-append-in-go/","content":"append
是我们向切片添加元素时的首选函数,但这可能不是最好用法。原因如下:
首先,我们创建两个函数,功能是将字符 “x” 填充进一个字符串切片。
\nWithAppend 调用 append 将 “x” 添加到一个字符串切片中
\nfunc WithAppend() []string { |
WithAssignAlloc 通过用 make 来创建一个指定大小的字符串切片,之后赋值 “x” 给指定索引位而不是使用 append
\nfunc WithAssignAlloc() []string { |
这两个函数返回相同的结果,但其实现方式完全不同。
\n现在,让我们对这些函数进行基准测试。
\nfunc BenchmarkWithAppend(b *testing.B) { |
结果如下:
\nBenchmarkWithAppend |
WithAppend
的性能最差,而 WithAssignAlloc
的性能最好,这个结论应该可以说服你应该避免 append 了吧?
但先别急着走。
\n我们再写一个使用 append
的函数,并通过指定大小和容量来创建一个字符串切片。
func WithAppendAlloc() []string { |
再次运行基准测试。
\nBenchmarkWithAppend |
现在我们在 WithAppendAlloc
和 WithAssignAlloc
上得到了同样好的性能。
为什么 WithAppend
性能很差?在使用 WithAppend
往切片中添加元素时,当切片的容量不足时,需要创建一个新的更大的切片来对切片进行扩容,这导致多次分配。
在你优化代码之前,应该通过基准测试来找到代码中的瓶颈。上面的例子过于简化,你可能并不总是知道应该提前分配切片的大小。
\n另外,过早地进行性能调整可能会矫枉过正。
\n"},{"title":"使用 awk 统计 p99","url":"/2022/awk-statistics-p99/","content":"最近在重构公司内的一个重要服务,目前已经把主要流程写完了,由于新写的服务对底层的存储组件进行了变更,所以要对性能进行一个对比。
\n老服务的监控不是太完善,接口平均时延、p99 之类的都没有上报到 Prometheus 里,只在日志文件中进行了每次请求响应时间的统计,所以我写了一个 Python 脚本遍历所有日志,从中抽取出我需要的数值,然后将这些时间进行加和再除以数量就可以得到平均时间了。但是 p90、p95、p99 这些还需要我再去编写额外的代码逻辑进行统计。
\n因为太懒了,不想去写那些统计逻辑,于是从网上搜了下有没有现成的脚本,找到了一个使用 awk 统计时延的脚本,如下:
\n
|
用法也很简单,准备好我们每次请求响应时间的数据,一行一条,如:
\n1.803322 |
假如我们的数据文件叫做 time.log
,将上边的脚本保存为 cal.awk
,用一下命令就可以得出时延的统计信息了:
cat time.log | sort -n | uniq -c | awk -f cal.awk |
这条命令组合了 sort
对数据进行排序、uniq
对数据进行去重+次数统计,最后调用我们的 awk
脚本实现统计。
可以看到统计的种类很全,p50、p90、p99 都有,还有平均值和方差,非常方便。
\n"},{"title":"Big Sur 开启HiDPI","url":"/2020/big-sur-turn-on-HiDPI/","content":"\n\n上周我的电脑升级了 Big Sur,果不其然之前配置的 HiDPI 失效了。而且这次苹果做了更严格的限制,即便禁用 SIP 也无法对系统目录进行修改了。
\n网上找了很多 Big Sur 开启 HiDPI 的方法,最后找到一种有效的方式,记录在这里,为了方便自己查阅,也希望能帮助到其他人。
\nbash -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)" |
最后手动输入我需要的分辨率:1920x1080 2560x1440
,重启后就可以通过 RDM(https://github.com/avibrazil/RDM) 来开启 HiDPI 了。
尝试删除 /Library/Displays/Contents/Resources/Overrides/DisplayVendorID-xxx
目录后再试一次(xxx 为你的 VendorID)。
\n\n我之前一直失败就是用这个方式才成功的,估计是用其他方法写入了脏数据,
\nhidpi.sh
脚本的数据一直写入失败,需要手动删除一下脏数据。
https://github.com/xzhih/one-key-hidpi/issues/136
https://blog.chajian110.com/macOS/32.html
https://blog.csdn.net/ymyz1229/article/details/109676446
前段时间将博客在七牛上部署了一份,并且为新的域名 jpanj.com 申请了 SSL 证书,但是发现一个问题,使用 http 请求还是可以访问的,想通过 https 的方式访问,需要手动将地址修改为 https,我想有没有什么办法能在用 http 访问时重定向到 https。
\n所以我开了个工单请教七牛的工作人员,得到的结果是他们也无法做强制跳转。
\n\n之前让 http 请求重定向到 https 的方法是通过 Nginx 的 rewrite 完成的,但是我现在的博客是一个纯静态站点,而且并没有托管在自己的服务器上,所以无法这样操作。
\n今天得到了一个解决方法,是通过修改主题源码来实现的,就我现在用的这个主题来说,layout
目录下所有模板都会继承 _layout.swig
,所以我只要在 <head>
标签中加入以下代码即可:
<script type="text/javascript"> |
我只需要判断 jpanj.com 就可以了,之前的 panmax.love 不做修改。
\n"},{"title":"如何在博客上安全发布私密日记","url":"/2022/blog-security-diary/","content":"我平时除了写一些可以公开的博客外,还会写点自己私密的想法,那些内容七零八落的散布在各个工具里,比如 Notion、Obsidian、Drafts、备忘录。我想把这些内容都发布到博客中,统一管理,但是无奈有些无法公之于众的内容,而且我的博客是纯静态的,无法进行访问控制。其实也可以用 Nginx 来实现一个简单的密码校验,但这么做不是很优雅,更何况我的博客目前完全托管在了 Cloudflare,控制权已经不在我手里了。
\n《黑客与画家》那本书里有这么一句话:
\n\n\n创造优美事物的方式往往不是从头做起,而是在现有成果的基础上做一些小小的调整,或者将已有的观点用比较新的方式组合起来。
\n
前段时间看到了这个网站,https://txtmoji.com/,它可以把我们的内容加密成 emoji,只有知道密码的人才可以解密。这给了我一个可以组合的灵感:我将私密日记先转成 emoji 后再然后发布就好了。
\n比如这段内容:
\n😹🙊😹👔👯🙊😰😵😰👐😵😱🙍👰😱👱😯👳😵👚👓🙄😲👦👑🙃👰👢👏👓😵👐😸👑👳👶😯🙃👰👚😹👦😫👖👏👤👯🙄👺👺👨👬👘👺👵👬👡🙇🙆👳😸😹😶👓👗👨👩👤🙅👧🙁👴👶👮👖🙎👐😸👔😲🙊👵👵👑😹🙅👫👓👡🙆👏👗😲👵😰🙆👴👗😸👑👵👏🙁👩👤👩😲👡😰👮👩😴👶👧😳👌👡👤👒🙃👬👏👷😴👹🙄👐👡😲👏👬👔👫👣😷👸👺🙊👲👷😽😽
\n密码是:1234
,看看我留下了什么悄悄话。
我看了下这个网站完全是通过前端加密,没有将我的内容上传到服务器。有点遗憾的是这个网站目前还没有开源,等以后开源了自己再私有化部署一个。
\n最近准备用这个方式发布几篇文章试试看,文章分两种,一种是标题可以外露的,这种只加密正文部分,另一种是标题和正文都不方便外露的,这样两个部分都会进行加密,加密的密码当然只有我自己知道。
\n后边想想有没有什么方式可以让那些加密的文章在未来指定的某一天改为明文显示,或者将密码展示出来。
\n"},{"title":"从零开始搭建一个全文检索引擎","url":"/2021/build-a-full-text-search-engine/","content":"全文检索是我们每天都使用的工具之一,在谷歌上搜索「golang 入门」或在淘宝上搜「智能音箱」,就会用到全文检索技术。
\n全文检索(FTS full text search)是一种在文档集合中搜索文本的技术。文档可以指网页、报纸上的文章、电子邮件或任何结构化文本。
\n今天我们将建立我们自己的 FTS 引擎。在这篇文章结束时,我们将实现一个能够在一毫秒内搜索数以百万计的文档的程序。我们从简单的搜索查询开始,比如:找出所有包含「cat」这个词的文档,然后我们将扩展这个引擎以支持更复杂的布尔查询。
\n\n\n注:目前最著名的 FTS 引擎是 Lucene(以及建立在它之上的 Elasticsearch 和 Solr)。
\n
在我们开始写代码之前,你可能会问:「我们就不能用 grep 或者用一个循环来检查每个文档是否包含我所要找的词吗?」。
\n是的,可以这样做。但这并不是最好的解决方案。
\n我们将搜索英文维基百科的一部分摘要。最新的离线数据可以在 dumps.wikimedia.org 上找到。压缩包解压后的 XML 文件为 956MB(截止2021年05月17日),包含60多万个文档。
\n文档的例子:
\n<title>Wikipedia: Kit-Cat Klock</title> |
首先,我们需要加载所有文件,这一步使用内置的 encoding/xml
包就就够了。
import ( |
每个被加载的文档都被分配了一个唯一 ID。简单起见,第一个文档的 ID=0,第二个 ID=1,以此类推。
\n现在我们已经把所有的文档都加载到了内存中了,我们可以试着找到所有关于 cat 的文档。
\n首先,让我们循环遍历所有的文档,检查它们是否包含 cat 这个子串。
\nfunc search(docs []document, term string) []document { |
在我的 Macbook Pro 上,搜索阶段耗时 68ms,还不错。
\n我们抽查结果中的几个文件,会发现这个函数匹配了 caterpillar(毛毛虫)和 category,但没有匹配大写字母 C 开头的 Cat,这不太符合我们的预期。
\n在继续前进之前,我们需要解决两件事:
\n使搜索不区分大小写(Cat 要匹配)。
\n在单词边界而不是在子字符串上匹配(caterpillar 和 category 不匹配)。
\n一种可以快速想到并实现这两个要求的方案是使用正则表达式。
\n在这里为 (?i)\\bcat\\b
。
(?i)
使正则表达式不区分大小写
\\b
匹配一个词的边界。
func search(docs []document, term string) []document { |
这次搜索花了近 2 秒。正如我们所看到的,即使只有 60 万个文档,搜索也开始变得缓慢。虽然这种方法很容易实现,但它不能很好地扩展。随着数据集的增大,我们需要扫描的文档越来越多。这种算法的时间复杂度是线性的——需要扫描的文档数量等于文档总数。如果我们有 600 多万个文档,搜索将需要 20 秒。
\n为了使搜索查询更快,我们要对文本进行预处理,并提前建立一个索引。
\nFTS 的核心是一个叫做倒排索引的数据结构,倒排索引将文档中的每个单词与包含该单词的文档关联起来。
\n例子:
\ndocuments = { |
下面是倒排索引的现实世界的例子:一本书中的索引,其中每个术语都引用了一个页码。
\n\n在我们开始建立索引之前,我们需要将原始文本分解成适合索引和搜索的单词(tokens)列表。
\n文本解析器由一个分词器和多个过滤器组成。
\n\n分词是文本解析的第一步,它的工作是将文本转换成一个单词列表。我们本次的实现是在一个词的边界上分割文本,并删除标点符号。
\nfunc tokenize(text string) []string { |
> tokenize("A donut on a glass plate. Only the donuts.") |
大部数情况下,仅仅将文本转换为一个单词列表是不够的。为了使文本更容易被索引和搜索,我们还需要做额外的规范化处理。
\n为了使搜索不区分大小写,小写过滤器将单词转换为小写。cAt、Cat 和 caT 被归一化为 cat。之后在我们查询索引时,也会将搜索词进行小写处理。这样就可以让搜索词 cAt 与文本 Cat 相匹配了。
\nfunc lowercaseFilter(tokens []string) []string { |
> lowercaseFilter([]string{"A", "donut", "on", "a", "glass", "plate", "Only", "the", "donuts"}) |
几乎所有英语文本都包含常用的单词,如 a、I、the 或 be。这样的词被称为停词,我们要将它们删掉,因为几乎任何文档都会与这些停顿词相匹配。
\n没有「官方」的停词表,这里我们把 OEC 排名的前10的词进行排除。
\nvar stopwords = map[string]struct{}{ |
> stopwordFilter([]string{"a", "donut", "on", "a", "glass", "plate", "only", "the", "donuts"}) |
由于语法规则的原因,文档中可能包括同一个词的不同形式。词干化将单词还原为其基本形式。例如,fishing、fished 和 fishe r可以被还原为基本形式(词干)fish。
\n实现词干化是一项很大的任务,在本文中不进行涉及。我们将采用现有的一个模块。
\nimport snowballeng "github.com/kljensen/snowball/english" |
> stemmerFilter([]string{"donut", "on", "glass", "plate", "only", "donuts"}) |
\n\n注:词干并不总是一个有效的词。例如,有些词干器可能会将 airline 简化为 airlin。
\n
func analyze(text string) []string { |
分词器和过滤器将句子转换为一个单词列表:
\n> analyze("A donut on a glass plate. Only the donuts.") |
这个列表已经做好了索引的主板内。
\n回到倒排索引,它把文档中的每个词都映射到文档 ID 上。内置的 map
是存储该映射很好的选择。map
中的键为单词(字符串),值为文档 ID 的列表。
type index map[string][]int |
建立索引的过程包括解析文档(调用前边的 analyze 函数)并将其 ID 添加到 map 中。
\nfunc (idx index) add(docs []document) { |
map
中的每个单词都指向包含该单词的文档 ID。
map[donut:[1 2] glass:[1] is:[2] on:[1] only:[1] plate:[1]] |
为了对索引进行查询,我们对查询词使用与索引的相同分词器和过滤器:
\nfunc (idx index) search(text string) [][]int { |
> idx.search("Small wild cat") |
最后,我们可以找到所有提到 cat 的文件。搜索 60 多万个文档只花了不到一毫秒的时间。
\n有了倒排索引,搜索查询的时间复杂度与要搜索单词的数量成线性关系。在上面的例子中,解析完输入文本后,搜索只需要进行三次 map
查询。
上边的查询为每一个单词都返回了一个文档 ID 列表。当我们在搜索框中输入 small wild cat 时,我们通常期望找到的是一个同时包含 small、wild 和 cat 的结果列表。下一步是计算这些列表之间的集合交集,这样我们就可以得到一个与所有单词相匹配的文件列表。
\n\n我们的倒排索引中的 ID 是以升序插入的。由于 ID 是有序的,所以可以在线性时间内计算两个列表之间的交集。intersection
函数同时遍历两个列表,并收集同时存在于两个列表中的 ID。
func intersection(a []int, b []int) []int { |
使用更新后的 search
方法解析给定的查询文本,查找单词并计算ID列表之间的集合交集:
func (idx index) search(text string) []int { |
维基百科的离线数据中只有两个文档同时与 small、wild 和 cat 相匹配。
\n> idx.search("Small wild cat") |
搜索到了我们所预期的结果!
\n我们刚刚建立了一个全文检索引擎,尽管它很简单,但它可以成为更高级项目的基础。
\n我没有触及到太多可以显著提高性能和使引擎更友好的内容,下面是几个进一步改进的想法:
\n扩展布尔查询,支持 OR 和NOT
\n在磁盘上存储索引
\n尝试用内存和 CPU 高效的数据格式来存储文档 ID 集合
\n支持对多个文档字段进行索引
\n按相关性对结果进行排序
\n缓存不一致 |
是 CDN 的 IP
\n所有请求都会发到 CDN,然后 CDN 在需要的情况下向后端服务器发出请求。
\n不是
\nCDN 会将内容缓存在不同地区的多个服务器上,这样你的用户无论在哪里都能迅速得到响应。
\n不是
\n它可以缓存整个 HTTP 响应,包括状态码和响应头。
\n所以,举例来说,如果你的服务器不小心返回了 404,CDN 将这个响应进行了缓存,那么即使服务器已经恢复,你的网站仍然可能是 404。
\n是的
\nCDN 提供方通常有清除缓存的方法。不过有时需要几分钟才能完成(CDN 可能要去告诉世界各地的数百台服务器来清理它们的缓存)。
\n你可以选择从缓存中只删除特定文件或者删除所有缓存的文件。
\n依赖于客户端的请求
\n当 CDN 收到一个资源请求时,它会从你的服务器上请求资源,然后会把资源放在它的缓存中,这样下次就不用再到你的服务器上请求了。
\n是的
\n如果你要求 CDN 进行缓存,它通常可以缓存任何你想要的 HTTP 响应,比如可以将响应头设置为: Cache-Control: public; max-age=3600
。
不过大多数 CDN 会对缓存内容的大小进行限制,所以你可能无法缓存一个电影。
\n也许
\n即使你的服务器没有运行,CDN 也可以继续提供缓存页面。
\n但是如果你告诉它只缓存一定时间(比如 1 小时),内容可能会在一段时间后过期,无法访问。而如果内容根本没有被缓存,CDN 也帮不了你。
\n是的
\n如果你想让CDN缓存内容,它需要能够解密和读取。
\n通常人们处理这个问题的方法是,只把静态内容(如 CSS/JS/图片)放在 CDN 后面的域名上,而使用一个单独的域名来处理带有用户数据的请求。例如,https://github.githubassets.com/ 在 CDN 后面,但https://github.com 不是。
\n不是
\n如果你想,可以配置你的 CDN 不进行缓存,只是代理每个请求到你的后端服务器。
\n是的
\n你通常可以从 header
中找出答案:运行curl -I https://jiapan.me,看看我使用的是什么 CDN。
是的
\nCDN 通常会设置一个响应头,比如 x-cache: HIT
,你可以用它来判断是缓存命中还是缓存失效。
是的
\n大部分情况下可以
\n许多CDN可以透明地将 HTTP/2
请求翻译成 HTTP/1
请求到你的后端服务器,所以你可以在不做任何工作的情况下获得 HTTP/2
的很多性能优势。
是的
\n您可以通过设置 Cache-Control
响应头来实现,比如 Cache-Control: max-age=600
。
是的
\n你可以通过设置 Cache-Control: private, max-age=3600
来实现。private
意味着内容只能存储在浏览器的缓存中,而不是 CDN 的缓存中。
视情况而定
\n默认情况下,会得到相同的响应。但如果服务器设置了 Vary:
头,那么 CDN 将为该头的每个值存储不同的缓存值。
例如,Vary: Accept-Encoding
将使 CDN 同时存储压缩和非压缩版本。
今天在服务器上用 pip 安装 MySQL-python 时报错,虽然之前处理过很多次,但都没有记录,这次记录一下。
\n报错如下:
\nDownloading/unpacking MySQL-python |
解决方法:
\nsudo yum install python-devel mysql-devel
Centos 默认提供的软件源资源很少,很多常用软件都没有:如 nginx
,htop
等。
EPEL(Extra Packages for Enterprise Linux) 是由 Fedora Special Interest Group 维护的 Enterprise Linux(RHEL、CentOS)中经常用到的包。
\n通过 EPEL 可以很容易地通过 yum 命令从 EPEL 源上获取在 CentOS 自带源上没有的软件。
\n首先安装 epel-release:
\nyum install epel-release |
大多数网站到了这一步就告诉你安装好了,但是在我尝试的时候,发现这种方式 EPEL 源默认并不会生效,可以通过下边的命令进行验证:
\nyum repolist | grep epel |
如果发现有类似下边的结果,说明 EPEL 源已生效:
\nepel/x86_64 Extra Packages for Enterprise Linux 7 - x86_64 12,716 |
如果没有输出这条命令,说明 EPEL 源默认没有开启,在安装软件时还需要手动指定源:
\nyum --enablerepo=epel install nginx |
这种方式使用时比较麻烦,我们可以通过修改 EPEL 的配置文件来启用它。
\nvi /etc/yum.repos.d/epel.repo |
可以看到里边有多个组,将 [epel]
组内的 enabled=0
改成 enabled=1
。
这样就可以开启 EPEL 源了。
\n\n\n\n"},{"title":"ChatGPT 参数介绍","url":"/2023/chatgpt-parameters/","content":"
好久好久没有写博客了,说因为忙肯定是借口,主要还是没什么动力。
\n最近我也追了追 AI 的风潮。不过相对来说,我开始使用 AI 的时间还算比较早,从前年开始就用 Github Coplit 辅助写代码,去年 12 月就用上了 ChatGPT 的网页版。ChatGPT 真正出圈的时间是在今年 1 月底。我还用 ChatGPT 的 API 写了几个小工具,甚至在公司的项目中也有使用。
\n在使用 GPT 的 API 期间,遇到了几个参数读不懂官方文档在说什么的情况,网上能查到的内容也不多。因此,我结合几个查到的资料和自己的使用体验,对这三个参数做下说明。
\n温度参数控制着生成文本的随机性。
\n频率惩罚参数控制模型重复预测的趋势,减少已生成单词的概率。惩罚取决于一个词在预测中已出现的次数,降低了一个词被多次选择的概率。该惩罚不考虑词频,只考虑词是否出现在文本中。
\n存在惩罚参数鼓励模型做出新颖的预测。如果某个词已经出现在预测文本中,则存在惩罚会降低该词的概率。与频率惩罚不同,存在惩罚不依赖于单词在过去预测中出现的频率。
\n本文总结了 ChatGPT 的三个主要参数:Temperature(温度)、Frequency penalty(频率惩罚)和Presence penalty(存在惩罚)。
\n\n\n\n\n代码中存在的循环依赖问题跟代码的维护工作有很大关系,也是日常开发中经常会碰到的一个问题。
\n
任何系统在开发了一段时间之后随着业务功能和代码数量的不断增加,代码之间的调用与被调用关系也会变得越来越复杂,各个类和组件之间就会存在出乎开发人员想象的复杂关系。
\n一种常见的复杂关系为类与类之间的循环依赖关系。
\n\n\n所谓循环依赖,简单来说就是一个类A会引用类B中的方法,而反过来类B也会引用类A中的方法,这就导致两者之间有了一种相互引用的关系,从而形成循环依赖。
\n合理的系统架构以及持续的重构优化工作能够减轻这种复杂关系,但是如果有效识别系统中存在的循环依赖,仍然是开发人员面临的一个很大的挑战。主要原因在于类之间的循环依赖存在传递性。
\n举个例子:如果系统中只存在类A和类B,那么他们之间的依赖关系就非常容易识别。
\n\n\n如果再来一个类C,那么这三个类之间的组合就有很多种情况了。
\n\n\n如果一个系统中存在几十个类,那么他们之间的依赖关系就很难通过简单的关系图进行逐一列举。一般的系统中类的数量显然不止几十个。更宽泛地讲,类之间的这种循环依赖关系也可以扩展到组件级别。产生组件之间的循环依赖的原因在于:组件1中的类A与组件2中的类B之间存在循环依赖,从而导致组件与组件之间产生了循环依赖关系。
\n\n\n在软件设计领域有一条公认的设计原则:无环依赖原则。
\n无环依赖原则:在组件之间不应该存在循环依赖关系。通过将系统划分为不同的可发布组件,对某一个组件的修改所产生的影响,可以不扩展到其他组件。
\n所谓的无环依赖指的是在包的依赖关系中不允许存在环,也就是说包之间的依赖关系必须是一个直接的无环图。
\n下面我们通过一个具体的代码示例,介绍一下组件之间循环依赖的产生过程。也是在为本文要介绍的如何消除循环依赖做好准备工作。
\n现在我们正在开发一款健康管理类APP,每个用户都有一份自己的健康档案,档案中记录着用户当前的健康等级,以及一系列可以让用户更加健康的任务列表(如:忌烟酒、慢跑)。用户当前的等级是和用户所需要完成的任务列表挂钩的,任务列表越多,说明越不健康,对应的健康等级也就越低(最低为1、最高为3)。
\n用户可以通过完成APP所指定的任务来获取一定的积分,这个积分的计算过程取决于这个用户当前的健康等级。也就是说不同的等级之下同一个任务所产生的积分也是不一样的。而每个任务也有自己的初始积分,每个任务最终所能得到的积分算法为 12 / <当前的等级> + <任务初始积分>
,健康等级越低,做任务所能得到的积分也就越高,这样可以鼓励用户多做任务。
背景就介绍到这里,对于这个常见我们可以抽象出两个类:一个是代表档案的 HealthRecord
类、另一个是代表健康任务的 HealthTask
类。
其中 HealthRecord
类中提供了一个获取健康等级的方法 getHealthLevel()
来计算健康等级,同时也提供了添加任务的方法 addTask()
。
public class HealthRecord { |
对应的 HealthTask
中,显然应该包含对 HealthRecord
的引用,同时也实现了计算任务积分的方法 calculateHealthPointForTask()
,calculateHealthPointForTask()
方法中用到了 HealthRecord
中的健康等级信息 getHealthLevel()
。
public class HealthTask { |
不难看出,HealthRecord
和 HealthTask
之间存在明显的相互依赖关系。
我们可以使用 IDEA 自带的 Analyze Dependency Matrix 对包含 HealthRecord
和 HealthTask
类的包进行分析,得出系统中存在循环依赖代码的提示。
Analyze Dependency Matrix 的使用细节可以参考官方文档:https://www.jetbrains.com/help/idea/dsm-analysis.html,这里我们只关心是否存在循环依赖,也就是那个红色的框框。
\n通过上边的例子,我们了解了如何有效识别代码中存在循环依赖的问题,下边再来看看如何消除代码中的循环依赖。
\n软件行业有一句非常经典的话:「当我们在碰到一个问题无从下手时,不妨考虑一下是否可以通过加一层的方法来解决」。消除循环依赖的基本思路也是一样的,有三种常见的方法:提取中介者、转移业务逻辑、采用回调接口。
\n提取中介者方法也被称为关系上移,其核心思想就是把两个相互依赖的组件中的交互部分抽象出来形成一个新的组件,而这个新的组件包含着原有两个组件的引用,这样就把循环依赖关系剥离出来,并提取到一个专门的中介者的组件中。
\n\n\n这个中介者组件的实现也不难,可以通过提供一个计算积分的方法对循环依赖进行剥离,这个方法同时依赖 HealthRecord
和 HealthTask
对象,并实现了原有 HealthTask
中根据 HealthRecord
的健康等级信息计算积分的业务逻辑。
public class HealthPointMediator { |
可以看到上边的 calculateHealthPointForTask()
方法中,我们从 HealthRecord
中获取了等级,然后再从传入的 HealthTask
中获取初始积分,从而完成了对整个积分的计算过程,这个时候的HealthTask
就变得非常简单了,因为已经不包含任何有关 HealthRecord
的依赖。
public class HealthTask { |
下边针对「提取中介者」这种消除循环依赖的实现方法来编写一个测试用例:
\npublic class HealthPointTest { |
在 HealthRecord
中我们创建了 5 个 HealthTask
,并赋予了不同的初始积分。然后通过 HealthPointMediator
这个中间者分别对每个 Task
进行积分计算。最后我们再次使用 Analyze Dependency Matrix 分析下当前的代码是否有循环依赖。
可以发现这次代码中已经不存在任何的环了。
\n转移业务逻辑也被称为关系下移,其实现思路在于提取一个专门的业务组件 HealthLevelHandler
来完成对健康等级的计算过程,HealthTask
原有的对 HealthRecord
的依赖,就转移到了对 HealthLevelHandler
的依赖,而 HealthLevelHandler
本身是不需要依赖任何业务对象的。
public class HealthLevelHandler { |
HealthLevelHandler
的实现也不难,包含了对等级的计算过程,具体到这里就是实现 getHealthLevel()
方法,随着业务组件的提取,HealthRecord
需要做相应的改造,getHealthPointHandler
就封装了对 HealthLevelHandler
的创建过程:
public class HealthRecord { |
对应的 HealthTask
也需要进行改造:
public class HealthTask { |
在 calculateHealthPointForTask()
方法中,传入一个 HealthLevelHandler
来获取等级,然后根据获取的等级计算最终的积分。
最后我们对测试类进行改造:
\npublic class HealthPointTest { |
现在 HealthTask
和 HealthRecord
都已经只剩下对 HealthLevelHandler
的依赖了。
所谓的回调本质上就是一种双向的调用关系,也就是说被调用方在调用别人的同时也会被别人所调用。
\n我们可以提取一个用于计算健康等级的业务接口(HealthLevelHandler
),然后让 HealthRecord
去实现这个接口,HealthTask
在计算积分的时候只需要依赖这个业务接口而不需要关心这个接口的具体实现类。
我们同样将这个接口命名为 HealthLevelHandler
,包含一个计算健康等级的方法定义。
public interface HealthLevelHandler { |
有了这个接口,HealthTask
就再不存在对 HealthRecord
的依赖,而是在构造函数中注入 Handler
接口:
public class HealthTask { |
在这里的 calculateHealthPointForTask()
方法中,我们也只会使用 Handler
接口所提供的方法来获取所需的健康等级,并计算积分。
现在的 HealthRecord
需要实现 HealLevelHandler
接口,并提供计算健康等级的具体业务逻辑:
public class HealthRecord implements HealthLevelHandler { |
在 addTask()
方法中,当创建 HealthTask
时,HealthRecord
需要把自己作为一个参数传入到 HealthTask
的构造函数中,这样我们就通过回调方法完成了对系统的改造。
采用回调方法,测试用例的代码业务变得更加简洁:
\npublic class HealthPointTest { |
我们没有发现除了 HealthRecord
和 HealthTask
之外的任何第三方对象,同样也可以使用 Analyze Dependency Matrix 来验证当前系统中是否存在循环依赖关系。
最后我放一张整体分析结果,从上到下依次为:回调接口、不采用任何措施、提取中介者、转移业务逻辑。
\n\n\n对于处理循环依赖问题而言,难点在于当识别了系统中存在循环依赖场景时如何采用一种合适的方法对代码进行重构。在日常开发过程中,有三种常见的消除循环依赖的方法,可以根据场景进行灵活的应用。
\n一般而言回调方法是优先推荐的,因为它将依赖关系抽象成了接口:一来方便后续的扩展,二来从测试用例中也可以看出这种方式不需要改变系统的使用过程。
\n在无法改变现有类的内存结构时,也就是说无法为现有类添加新的接口实现关系时,可采取提取中介者和转移业务逻辑这两种实现方式。其中提取中介者的方法相对比较固定,结构上与设计模式的中介者模式也比较类似。而转移业务逻辑需要根据具体的场景进行分析,具有最大的灵活性。
\n本文中的示例代码见:https://github.com/Panmax/CircularDependenceExample
\n"},{"title":"我关掉了Apple Watch的通知功能","url":"/2023/close-apple-watch-notify/","content":"长期以来,我使用Apple Watch的主要用途有两个。
\n首先是在收到消息时,及时利用振动反馈通知我。其次是用它来监测身体指标,例如心率、血氧和其他运动类指标。
\n我希望能够及时收到消息,主要是担心漏掉重要的信息,回复不及时会给对方和自己带来损失。但是,这也给我带来了极大的困扰。
\n不管是在钉钉还是微信中,每收到一条消息都会震动一次。这已经让我形成了条件反射,收到消息就想赶紧看,如果不看就会感到着急。再遇上个夺命连环call的主就更要命了,本以为有很多人找我,结果打开一看,发现是同一个人连续发了一堆短句。
\n这样导致的严重问题是在我和他人面对面沟通或正在一个会上发言时,如果这时候来了消息,会瞬间打乱我的思路。我能明显感觉到自己紧张了起来,就连面部表情也发生了变化。
\n因为前段时间把上线单审批做了自动化,这种相对来说更重要的事情不需要我再处理消息了。再加上一直以来被通知的打扰,我决定关闭手表上的通知功能。
\n到今天已经尝试了两周,明显感觉自己的慌张感少了很多。
\n以前,我总是通过手表振动来触发查看消息。在没有了这个触发事件后,我在很长一段时间内甚至会忘记查看消息,这使我更加专注于工作。而且,我发现即使没有及时阅读和回复消息,我也没有错失任何重要事项。
\n在此之前,我已经尽可能地减少了手机上的推送通知。只有几个重要的应用可以向我发送推送通知,我甚至关闭了不必要的红点提醒。
\n此次关闭Apple Watch的通知功能后,我松弛了很多。
\n"},{"title":"利用 Cloudflare Workers 托管静态站点","url":"/2021/cloudflare-workers-static-site/","content":"我们在选择静态站点(如博客、技术文档等)部署方案时会考虑以下几种情况:
\n我目前使用的就是静态博客,托管在了 3 个地方,而且各有一些弊端:
\n今天我们就利用 Cloudfalre Works 来部署一个满足上边所有条件的博客。
\n在使用 VPS + Cloudflare CDN 方案时,我们将博客的静态文件放在 VPS 上,并通过 Nginx 搭起一个静态站点,然后前置一个 Cloudflare CDN 来做静态资源加速和 https 的处理,即我们的 VPS 来作为静态文件的源站。
\n\n使用 Cloudflare Workers 方案可以无需准备 VPS。
\n\nCloudflare Workers 本质上是一个边缘计算服务,举几个例子:
\n了解更多可以参考:https://blog.cloudflare.com/zh-cn/cloudflare-workers-unleashed-zh-cn/
\n目前,生成静态站点的方案有很多,比如 Hugo、Hexo和 Jekyll 等,下边我以 Hugo 为例来生成一个站点,其他方案可以参考对应的官方文档。
\nbrew install hugo |
hugo new site quickstart |
执行这个命令后,会在执行的目录下创建出一个名为 quickstart
的目录,里边就是我们新站点的内容。
下载主题:
\ncd quickstart |
配置主题:
\necho theme = \\\"ananke\\\" >> config.toml |
hugo new posts/my-first-post.md |
修改 content/posts 下的文章源文件,将 draft 改为 false,就可以正常发布了,正文随便点什么。
\n--- |
hugo server -D |
此时我们可以访问:http://localhost:1313/ 看下效果。
\n使用 hugo -D
命令生成静态文件用来发布到 Cloudflare Workers 上,hugo 生成的静态文件在项目目录的 public
下。
发布前需要先注册自己的 Cloudflare 账号,开通 Workers 服务,在 Workers 页面右侧可以修改自己的子域名,比如我的子域名为 panmax.workers.dev
,即我发布的服务都是以 panmax.workers.dev
结尾,比如:https://hugo.panmax.workers.dev/
wrangler 是 Cloudflare workers 为开发人员提供的 CLI 工具。使用 npm 进行安装:
\nnpm i @cloudflare/wrangler -g |
如果提示 node 版本太低,可以通过 nvm 来切换版本:
\nbrew install nvmnvm install 12 |
在刚才生成的 quickstart 目录下执行以下命令来初始化 cloudflare workers 项目,这个命令会在当前目录下生成 wrangler.toml
文件和 workers-site
目录。
wrangler init --site hugo |
将 wrangler.toml 中的 bucket 改为我们静态目录的路径:
\n根据你的项目,将 bucket 改成生成静态文件的目录 |
其他参数暂时无需修改。
\n使用 wrangler login
来完成登录,在弹出的页面中点击 Allow 即可。当命令行打印出 「✨ Successfully configured. 」就说明我们登录成功了,会在 home
下生成 .wrangler
目录,里边记录了我们的用户信息。
\n\n这个操作只需进行一次,后续发布时就不用再执行了。
\n
最后,使用 wrangler publish
即可将我们的静态站点发布到 cloudflare workers 上了。同时还会将我们站点的地址打印出来:
用浏览器访问这个地址就能看到效果了:
\n\n如果你在 cloudflare 上托管了自己的域名,还可以将自己的域名映射到 workers 上。
\n在你的 DNS 配置中新增一条 CNAME 规则,名称是你想关联的子域名,目标为 workers 为你提供给的域名。
\n比如,我要将 hugo.jiapan.me
关联到刚才发布的站点上,此时我的名称填写 hugo,目标填写 hugo.panmax.workers.dev
。
在域名管理页面上边的菜单中点击 workers,点击「添加路由」,还是以我刚才配置的域名为例,路由填写 hugo.jiapan.me/*
,Workers 选择 hugo,点击保存。
之后我们就可以使用自定义域名来访问我们的站点了:
\n\n"},{"title":"非常好用的代码速查工具 cheat.sh","url":"/2020/code-serch-cheat-sh/","content":"\n\ncheat.sh 号称自己提供了世界优质技术社区中代码速查表的统一访问。
\ncurl https://cht.sh/:cht.sh | sudo tee /usr/local/bin/cht.sh |
基本的查询命令为:cht.sh 语言 问题关键词
比如,你不知道上边安装命令中 tee
是什么意思,可以尝试用下边的命令查看提示:
cht.sh linux tee |
由此可以看出 cht.sh
不限于查代码,还可以查命令的用法。
再举个例子,假如我不知道 go
中有没有能够判断字符串中是否包含某个字符的函数,可以使用:
cht.sh go string contain |
或者我想知道 go
中如何反转一个 list
:
cht.sh go reverse list |
想进入 cht.sh
的交互模式,需要先安装 rlwrap
这个工具。
Mac 安装方式:brew install rlwrap
进入交互模式的命令为:cht.sh --shell
进入交互模式后,就不用再输入 cht.sh
的命令了,直接问问题就可以,比如我想知道 go 中如何将 int 转为 string:
更进一步,如果想在后续的查询者固定查询某个语言,可以通过 cd
命令,这样在后续的查询中连语言都可以省掉:
也可以在进入交互界面时指定语言 cht.sh --shell go
:
在指定了语言的交互界面中,如果想在不 cd
到其他语言的情况下临时查询其他语言的用法,可以通过以 / 开头 临时指定语言:
https://github.com/chubin/cheat.sh
\n"},{"title":"collections.Counter源码阅读笔记","url":"/2016/collections-Counter%E6%BA%90%E7%A0%81/","content":"这几天用到 collections.Counter
的次数挺多的(比如热门标签、热门愿望、热门城市),看文档的时候就很好奇,这个类初始化的时候是如何实现既可以不传参数,可以传一个可迭代的值,可以传字典,而且还可以传关键字。
文档范例:
\n>>> c = Counter() # a new, empty counter |
先来看他的 __init__方法
:
def __init__(*args, **kwds): |
刚开始不明白,明明判断了 args
不存在的话就抛出异常,但是却可以是使用无参来初始化这个类,后来想明白了,类初始化的时候会默认给一个参数,我们通常把这个参数命名为 self
,所以才有了下边的语句,将 self
从 args[0]
中取出。所以 args[1:]
就是剩下的参数,注意这时候 args
还是一个元组,现在来判断 args
的长度,因为除了关键字参数外,只能使用一个可迭代的值或者一个字典来初始化,所以如果args长度大于1,说明除了关键字参数外,给了超过一个以上的参数,这时候程序抛出异常。
这时候我就想既然是一个参数,为什么不直接给个固定值,比如像这样:
\n __init(self, iterable_or_mapping, **kwds)__
而是非要作为一个可变参数传入,然后再判断长度,后来想到因为还需要接受无参调用,所以不能用这样写。
\n这时候又想到能不能这样写呢:
\n__init(self, iterable_or_mapping=None, **kwds)__
答案是不能,因为你不知道调用者在调用时关键字参数要传什么,万一调用者要传 iterable_or_mapping
作为关键字参数怎么办。
最后想到能不能这样:
\n__init(self, *args, **kwds)__
觉得好像没什么问题。。。不知道阅读者有没有看出来问题,如果看出这样写有什么问题麻烦告诉我,我把评论关闭了,可以通过邮箱: `jiapan.china@gmail.com`
\n接下来然后调用他的 update
方法:
def update(*args, **kwds): |
刚开始和 __init__
一样,就不说了,从 iterable = args[0] if args else None
开始说。
因为此时 args
是一个长度小于1的元组,所以 args[0]
可能是一个值也可能是 None
,如果不是 None
的话,就进入 if
, 首先判断这个值类型是不是 Mapping
,我猜测这里的 Mapping
就是 dict
的原始实现类,只是在注册为了dict
(如有误请更正),如果是字典类型的,就把键值对迭代出来。因为这个类继承自 dict
,所以继承了 get
方法, Python可以将方法作为参数传递,所以就有了这样的写法 self_get = self.get
,这时候 self_get
实际上还带有这个类的实例(这么说有点别扭。。。),然后 self[elem] = self_get(elem, 0) + count
从 self
中尝试获取 elem
的数量,如果没有就初始化为0然后加上字典的值,这样写而不是直接 self[elem] = count
是有原因的,因为我们还可能给关键字参数,这样写的好处是,一会用关键字参数再递归调用一下这个方法,就能把关键字参数的值也更新上去啦。
当 iterable
不是字典的时候,就会把这个值进行迭代取出每个元素计算出现的次数,具体代码就不解释了。
\n\n最后的疑惑:我不明白的是为什么要判断
\nself
存不存在,在什么情况下初始化一个类会没有self
?
下一篇准备写写 Counter
中两个常用方法 most_common
和 elements
。因为现在我还没看。。。
有一个需求是获取所有存在愿望的城市,并且找出热门城市。
\n刚好前几天看到了Python的计数器:collections.Counter
, 于是就拿来用了用。
Counter
:from collections import Counter |
wishes
数组中,然后遍历所有数据,把所有城市的adcode存在一个数组中:cities = [wish.adcode for wish in wishes] |
\n\n(我发现我最近越来越用推导式了。。。
\n
c = Counter(cities).most_common() |
这样得到的结果是一个数组,每个元素是一个长度为2的元组,元组第0位保存的是数据,第1位保存的是这个数据出现的次数,整个数组是按照元素出现次数排序的,most_common()
还可以带int型的参数,表示获取排名前n个的结果。不带参数表示获取所有结果。
以上得到的结果为:
\n[(110000, 4), (510800, 3)],... |
获取热门城市只需拿前4个数据就行(需求是4个热门城市),其他城市拿剩下的数据即可(注意,c
里每个元素是一个元组,元组的一个0位才是我们要的数据):
top4 = [_[0] for _ in c[:4]] |
现在 top4
里存放的就是排名前四的城市adcode,others
里存放的是其他有愿望地区的城市adcode按照愿望数量排序的数组。
docker run --env TZ='Asia/Shanghai' \\ |
# 查看所有支持下周的格式 |
# 下载指定格式 |
docker run -e PASSWORD=xxxxxx -e METHOD=aes-256-cfb \\ |
qshell account ak sk panmax |
查看物理CPU个数
\ncat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l |
查看每个物理CPU中core的个数(即核数)
\ncat /proc/cpuinfo| grep "cpu cores"| uniq |
查看逻辑CPU的个数
\ncat /proc/cpuinfo| grep "processor"| wc -l |
docker images|grep none|awk '{print $3 }'|xargs docker rmi |
docker stop $(docker ps -aq) |
docker rm $(docker ps -aq) |
sudo docker exec -it {{containerName or containerID}} bash |
昨天晚上我们的推荐服务出现故障,排查到很晚。影响了50多个主播和用户的曝光卡的使用效果,虽然没有产生特别大的事故,但我觉得自己还是有必要做个复盘,毕竟有自己做的不好的地方。
\n大概21:20分,服务开始有大量错误报警,推荐帧 v2接口和附近动态的 rpc 请求全部进入降级状态。
\n22:00 报错突然降成 0
\n回顾一下我的排查过程,在报警前几分钟,我更新了本周要扶持的荣耀主播名单,这个名单是一周一换,每周二更新,正常情况下运营会在白天把名单给我,但今天运营晚上19点才给我,当时我在吃饭,吃完饭后因为处理另一个问题就把改配置的事给忘了,晚上到家后才配置上。
\n报错时没有想到会是配置的问题,因为这个配置我已经配置好多周了,都没有出过问题,而且是配置完后过了几分钟才开始报错的,看日志报的都是空指针异常,但是没有具体定位是那一行,起初以为是 live 对象缺少字段或者本身为空,加日志看了下并没有问题。
\n大概21:53,我想到有没有可能是配置的问题,所以把新增的配置删掉,发现问题并没有解决,到了22:00 的时候突然不报错了,这个时候因为是个整点时间,我怀疑是不是某个活动或者某个有脏数据的主播下播了?心想明天到了公司查下这个时间点下播的主播找找原因。
\n因为我前几分钟把荣耀主播的名单下掉了,这个名单需要在凌晨4点生效,所以我看既然没问题了就把配置恢复吧,恢复完配置文件几分钟后,刚要去洗漱就又开始报警。我和另一个同事决定继续加日志排查,一直搞到23点也没发现代码有问题,这时候我决定再下线刚才的配置,下完后没有恢复,不过到了23:10突然降成了0。又等到23:30发现没有报错我才去睡的,因为经历了这么长时间的惊心动魄,凌晨3点才睡着。
\n为了验证是不是配置文件导致,第二天早上7点我重新把这份配置上去,7:10又开始报错。删除配置后,7:20恢复。
\n为什么会在整10分报错?
\nai 所使用的配置文件在 hbase 中,为了提升效率会定期同步到 redis 一份,resource 类的配置文件我设置的是10分钟同步一次,所以会出现当有配置变更时,整10分钟才会生效。
\n为什么要10分钟才加载一次呢,因为我并不知道业务实际会用到哪些,索性把库中所有的配置都 load 了一遍,这会导致 redis 的抖动,不易太频繁。如下图所示
\n\n后边我改成了,当下游调用配置时,我会记录下来调用配置的 key,刷新配置时只刷新在用的配置,这样可以做到秒级或者分钟级刷新。
\n优化后效果如下:
\n\n配置错在了哪里?
\n配置中存在重复项,代码中解析这个配置后会转成一个 map,用到了 lambda 表达式
\n.collect(Collectors.toMap(Pair::getKey, Pair::getValue); |
可以理解为,key 是主播的 ID,value 是要扶持的量,这段代码当有重复 key 时会报错,解决方法是传入第3个参数,告诉程序当 key 冲突时的 merge 逻辑,因为我这里不关心太具体保留哪个 value,可以简单实现:
\n.collect(Collectors.toMap(Pair::getKey, Pair::getValue,(value1, value2) -> value2)); |
有时候我们会放一些敏感信息到配置中心里,比如线上数据库密码等,我们直接将敏感信息以明文的方式存储于微服务应用的配置文件中是非常危险的,Spring Cloud Config 提供了对属性进行加密解密的功能,以保护配置文件中的信息安全。
\n在 Spring Cloud Config 中通过在属性值前使用 {cipher}
前缀来标注该内容是一个加密值,当微服务客户端加载配置时,配置中心就会自动为带有 {cipher}
前缀的值进行解密。这里有个需要注意的地方,如果配置文件使用的是 yml
格式的话,一定要用引号将内容包裹起来,properites
的配置文件不需要。如:
spring: |
Spring Cloud Config 同时支持对称加密和非对称加密,下边我们只介绍对称加密的使用方式,一般来说只要密钥不被泄露,对称加密的方式就足够了。
\n首先我们访问配置中心的 /encrypt/status
路径,可以看到返回结果提示我们还没有设置密钥,需要我们在配置文件中进行设置。
在配置中心项目的配置文件 application.yml
中加入以下配置即可(密钥根据根据需要自行修改):
encrypt: |
然后重新编译后运行,再次访问 /encrypt/status
看到如下错误:
这是因为在 JRE 中,自带的 JCE 默认是有长度限制的版本,我们需要从 Oracle 官网下载不限长度的版本:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html ,下载解压后可以看到下边三个文件:
\n\n我们需要将 local_policy.jar
和 US_export_policy.jar
两个文件复制到$JAVA_HOME/jre/lib/security
目录下,在复制前,最好将之前的两个文件进行备份,我将这两个文件放到了 lc0 机器的相应目录中,现在目录中的文件如下:
重新运行配置中心,访问 /encrypt/status
可以看到密钥已经生效了,并且配置中心已经支持对配置进行加密了。
此时,我们配置中心的加密解密功能就已经可以使用了,可以访问 /encrypt
和 /decrypt
来使用加密和解密功能。这两个端点都是 POST 请求,我们来用 curl
测试下:
我们用 my-password
为明文生成了49db5b628a4b722ef776262d67c0fa9676de7767e54ecfb2be70df5157677d20
这个密文,同时又测试了解密功能。
在 GitLab 中修改 app-a 的 dev 配置文件,加上如下配置:
\npassword: '{cipher}49db5b628a4b722ef776262d67c0fa9676de7767e54ecfb2be70df5157677d20' |
然后在 app-a 中新加一个 Controller:
\n@Value("${password}") |
重新编译后运行,访问它的 /password
路径,可以看到结果:
我们通过配置 encrypt.key
参数来指定密钥的实现方式采用了对称加密。这种方式实现起来比较简单,只需要配置一个参数即可。另外,我们也可以使用环境变量 ENCRYPT_KEY
来进行配置,让密钥信息外置。
8月份一眨眼就过去了,来到了Q3的最后一个月,打工人没有9月。
\n回头一看连自己都不敢相信:流水账在不知不觉中已经连续更新了一个月。我不敢称自己写的东西叫文章,它们缺乏逻辑、没有华丽的辞藻、没有育人的道理,都是些自己东拼西凑的碎碎念。
\n我在8月初的时候冒出一个想法,要不要尝试每天更新一篇博客,那个时候觉得这是不可能完成的任务,没有立flag,纯粹是内心的驱使想要挑战一下。每天想一个主题去写写,可能记录日常想法,可能是技术问题,也可能是所见所闻。没有给自己的内容设限,也许突然被什么事情触发了就会记下来写一写。
\n我在这期间写了篇叫「闷嘴葫芦」的流水账,主要是讲我在绩效沟通时无话可讲的尴尬场面。我这一次连续写30天也是想通过写作提升自己思维表达方面的能力,目前从自己的感受来看貌似还没有什么效果。
\n另一个触发我开始写的原因是,在5到7月期间,我因为生活和工作的两面夹击,博客停更了很长时间,七月底突然在钉钉上收到一位不认识同事的问候,询问我怎么好久没有更新了,是不是这段时间很忙,还说了一些鼓励我的话。看了一下她的信息,是一位远在成都的同事。那一瞬间我大受感动,没想到我这犄角旮旯的地方还会被发现,而且是被同一个公司的同事发现。被关注可以大大提升一个人的成就感,虽然这有点不成熟,但至少对那个低谷阶段的我来说确实像冥冥之中的安排,一只无形的手把我从谷底拉出。
\n开始连续写流水账后,我从之前的只摄取知识向输出内容转变,开始留意生活,想到了什么好的主题就赶紧记下来,因为有了主题,就会有意无意的收集可以作为内容的素材。在交流中、听播客节目时、阅读时、跳绳时甚至在写东西的过程中都会有灵感蹦出来。
\n我现在有1个固定+2个零碎的写作时间,固定时间是工作日的中午,我会在每周的一、三、五中午跳绳,跳完绳差不多12点50左右,然后拿出25分钟左右时间写一点东西,然后做5-10分钟冥想,一点半下楼吃个饭就开始下午的工作。周二和周四中午会有一个多小时的大块时间来写。零碎时间是每天晚上到家后,和地铁上通勤时。地铁上我会随心情或读书或写作,如果是写作我就用手机上的 Notion 来写。
\n作为连续写水文30天的奖励,今天就喝一杯瑞幸刚出的酱香型拿铁奖励一下自己吧。
\n希望自己能坚持下去这个习惯,100天见。
\n"},{"title":"Http Cookies 中 Max-age 和 Expires 有什么区别?","url":"/2017/cookies-max-age-vs-expires/","content":"快速回答
\nExpires
为 Cookie 的删除设置一个过期的日期Max-age
设置一个 Cookie 将要过期的秒数max-age
,所有的浏览器都支持 expires
深入一些来说明
\nexpires
参数是当年网景公司推出 Cookies 原有的一部分。在 HTTP1.1 中,expires
被弃用并且被更加易用的 max-age
所替代。你只需说明这个 Cookie 能够存活多久就可以了,而不用像之前那样指定一个日期。设置二者中的一个,Cookie 会在它过期前一直保存,如果你一个都没有设置,这个 Cookie 将会一直存在直到你关闭浏览器,这种称之为 Session Cookie
。
举个栗子
\n用 expires
的方式设置 foo=bar
在5分钟后过期
var d = new Date(); |
用 max-age
来做同样的事情
document.cookie = 'foo=bar;path=/;max-age='+5*60+';'; |
不幸的是,IE 浏览器 不支持 max-age
,如果你想跨浏览器存放 Cookie,应该坚持用 expires
。
下边我们来进行几个假设的问答
\n问:如果我在 Cookie 中同时设置了 expires
和 max-age
会发生什么?
答:所有支持 max-age
的浏览器会忽略 expires
的值,只有 IE 另外,IE 会忽略 max-age
只支持 expires
。
问:如果我只设了 max-age
会怎样?
答:除了 IE 之外的所有浏览器会正确的使用它。在 IE 浏览器中,这个 Cookie 将会作为一个 Session Cookie(当你关闭浏览器时它会被删除)。
\n问:如果我只设了 expires
?
答:所有浏览器会正确使用它来保存 Cookie,只需要记得像上边示例那样设置它的 GMT 时间就行了。
\n问:这篇文章的寓意是什么?
\n答:如果你关心你的 Cookies 功能在大多数 Web 用户下正常工作,不要用正确的方式(max-age
)存储你的 Cookies,应该用 expires
的方式让他们工作。
最近进入了雨季,穿普通的鞋子上下班在路上不小心灌上水会很不舒服,一家之主打算给我从网上买一双叫 Crocs 牌子的洞洞鞋,但是上周六,突然预告北京会有大暴雨,现在从网上买已经不赶趟了,正好家附近挨着燕莎奥特莱斯,一家之主告诉我那里,那里有 Crocs 的门店。
\n到了之后我看到价格后直接劝退,一双洞洞鞋竟然要399,这不是坑人吗。但是一家之主执意要让我买,告诉我这鞋特别舒适,而且能穿好多年。我对于穿衣装打扮没有什么追求,但是禁不住劝最后还是买了,买了一双白色的。
\n\n这双鞋穿起来确实非常舒适,但是最吸引我的并不是它的舒适性和外观,而是这鞋居然可以搞 DIY,鞋子上的洞洞是一个个插槽,可以自由组装自己喜欢的饰品,这真的是又让我眼前一亮。
\n洞洞鞋的样子千篇一律,但鞋上的每个洞洞都是标准尺寸,饰品厂只需要按照洞洞尺寸生产标准的饰品,就可以打造出一个生态。每个人都可以像玩乐高一样拼装出自己得意的作品,这卖点一下子就出来了。
\n每个人都有追求个性的愿望,买再好的鞋子也可能会撞鞋,而且也无法突出我的个性。而买洞洞鞋,我可以根据自己的心情来做搭配,比如今天我往上边搭配一个爱心,明天装饰一个咖啡杯,后天装饰一个皮卡丘,这都可以代表我这一天的心情。
\n看看我的搭配,猜猜我今天心情如何?
\n\n有没有可能,只要某种东西做到既方便携带,又可以通过组合的方式来折腾,佩戴上后还能展现一个人的性格和个性,人们就愿意来购买,进而会形成一个成规模的市场?我能想到的另一个例子是手串儿。
\n\n"},{"title":"大观园记","url":"/2022/da-guan-yuan/","content":"\n\n\n这篇小记写于早上上班的地铁上,我之前都是正襟危坐在电脑旁用 Obsidian 写,你这次尝试在手机上用 Drafts 写,然后到公司后再用电脑做些调整、配上图片后发表。
\n
上上个周末去了一趟大观园,也算圆了我这几年的一个愿望,其实大观园离我住的地方并不远,开车也就 15 分钟,只是由于疫情再加上自己的行动力不足一直拖到现在。
\n行动力不足的一个原因是大观园 40 每人的票价比其他公园高出很多。可是虽然贵,但它又包含在了公园通票中,所以我去年的时候就想今年办个通票,到时候去个痛快,然后一晃半年多就过去了。这次让我行动的一个触发点是前几天高考的作文题目中出现了大观园,又唤起了我去大观园的念头,刚好疫情也没那么紧张,索性就来了。
\n\n下图是我拍摄的作文题目中出现的「沁芳」:
\n\n实话实说,大观园没有我想想的那么大、那么宏伟奢华,可能也是因为当初的成本和地基大小所限,毕竟当时的建园最主要的目的是拍摄红楼梦电视剧,那些宏伟的场景可以通过镜头的运用来突显。气势上虽然没达到我的预期,但里边的景色还是极好的,待到冬天下雪后我会再来二刷。因为姓氏的缘故,我在逛大观园时总会幻想在逛自己家的园子。
\n\n\n当时大观园建在北京郊区,谁成想当年的郊区现在已经成了北京的核心地段。
\n
因为先前读过几遍红楼梦,所以看到每一处景观都能回想起书中在这里发生过的故事,比如看到花冢,就会想到黛玉的葬花词:「侬今葬花人笑痴,他年葬侬知是谁?」
\n\n看到省亲别墅,想到元妃说的那句「当日既送我到那不得见人的去处,好容易今日回家娘儿们一会,不说说笑笑,反倒哭起来。」
\n\n看到写着顾恩思义的祠堂,想到中秋节时祠堂内传出的几声叹息,暗示着贾家的败落。
\n\n看到潇湘馆和怡红院想到宝黛两个小冤家在这里或喜或悲或叹或惊的那些场景。
\n我小时候并没有看过红楼梦,甚至没看过他的相关影视作品,著名桥段也只听说过刘姥姥进大观园。在我的思维定势中红楼梦是一本讲儿女情长的小说,前几年读它的原因是随着阅读量越来越大,读到的对这本书引用的内容也越来越多,而且红楼梦在豆瓣上稳坐头把交椅,我就越来越对这本书产生好奇。
\n\n书一开始讲大荒山一块石头的故事,我差点弃读,但是往后读了读发现又讲到女娲补天,石头是最后没有使用的那一块,石头有思想后想去人间走一遭,跛足道人和癞头和尚答应带它去看一看这个繁杂的人世间,顺便让它看着了却几段姻缘,于是我遍产生了兴趣。
\n红楼梦中作者要表达的并不是那几对小情侣或者三角恋之间的恩怨情仇,而且讲了一个美好的青春王国的故事,这个王国的结束于在抄检大观园。每每读到大观园中宝玉与姐妹们嬉笑玩乐的情结,我也会回忆我自己的童年时光。作者在书中表达了对所有人和事的怜悯,作者从来不觉得一个人恶,没有批评书中的任何角色,而且书中很多为人处世之道挪到现在的职场和官场也非常适用。
\n《红楼梦》还有一个特点:它是一本关于女孩子的书。在《红楼梦》中,贾宝玉在某种程度上都被女性化了,这在中国的经典著作中很少见。男生若要读懂女生的心思,不妨读读它。
\n"},{"title":"简述数据库隔离级别","url":"/2022/database-isolation-level/","content":"常见的数据库有四种隔离级别,从强到弱分别为:可串行化(Serializable)、可重复读(Repeatable Read)、读已提交(Read Committed)、读未提交(Read Uncommitted)。
\n不同隔离级别在实现上的本质是各种锁的使用有所不同,包括锁的多样性和锁的粒度。
\n现有的文章大多是直接深入到数据库的细节中讨论这几种隔离级别,而且介绍的也很全面了,这里我尝试站在锁的角度来对这几种隔离级别做个讨论。
\n可串行化使用了最全的锁:写锁、读锁、范围锁。
\n读写锁平时比较常见,这里简单介绍下范围锁。
\n范围锁的定义为:对于某个范围直接加排他锁,在这个范围内的数据不能被写入。
\n要注意的时这里的访问内的数据不止包括已有的数据,即使不存在的数据也会被加锁,可以理解为不允许在这个范围内新增数据。
\n我举个例子,比如我现在有这样一些数据:
\nid\tprice |
当我们在一个事务中使用范围查询 price<100
时,在这个事务还未结束的情况下,其他事务无法在新增一个 price 为 20 的数据。
可串行化保障了最好的隔离级别,但也是这几种隔离级别中性能最差的。
\n可重复读只使用了读锁和写锁,未使用范围锁。
\n还用上边的数据举例,这种情况下会产生的一个问题是:当一个事务在第一次查询 price<100
时返回了 4 条数据,这时候另一个事务新增了一条 price 为 20 的数据,当第一个事务再次查询 price<100
的数据时发现变成了 5 条,也就是说出现了幻读。
幻读:在事务执行过程中,两个完全相同的范围查询得到了不同的结果集。
\n读已提交表面上看和可重复度使用的锁相同,都使用了读锁和写锁,但在读锁的加锁粒度上和之前有所区别。
\n在上边的可重复读中,读锁是一直锁到事务结束,但在读已提交中,读锁在查询完成后会立即释放,下边我写两个 Go 程序来演示下这两种情况的区别。
\npackage main |
输出:
\n1 |
package main |
输出:
\n1 |
这个程序和上边的程序区别在于读锁是锁了整个事务还是只锁了查询的瞬间,在读已提交的情况下,第一个事务读取数据并打印出 1 后就释放了读锁,这时候另一个事务可以拿到写锁并将数据修改为 2,之后第一个事务再次读取时就读到了另一个事务修改后的数据。
\n这种情况我们称之为不可重复读问题。
\n不可重复读问题:在事务执行过程中,对同一行数据的两次查询得到了不同的结果。
\n读未提交只使用了写锁,同样我们也通过一个 Go 程序观察下这个情况。
\npackage main |
输出:
\n2 |
这里演示的是,一个事务对数据进行修改,另一个事务只是读取数据,由于在读未提交下不存在读锁,可以直接读数据。
\n可以看到,我们的只读事务读到了写事务还没有提交的数据,我们称之为脏读。
\n脏读:在事务执行过程中,一个事务读取到了另一个事务未提交的数据。
\n锁 | \n脏读 | \n不可重复读 | \n幻读 | \n隔离级别 | \n
---|---|---|---|---|
写锁、读锁、范围锁 | \n否 | \n否 | \n否 | \n可串行化 | \n
读锁、写锁 | \n否 | \n否 | \n是 | \n可重复读 | \n
读锁(读完释放)、写锁 | \n否 | \n是 | \n是 | \n读已提交 | \n
写锁 | \n是 | \n是 | \n是 | \n读未提交 | \n
数据库行业有四种常见的隔离级别,分别是 RU、RC、RR、SERIALIZABLE,其中用到最多的是 RR 和 RR。下边分别看一下这四种隔离级别的异同。
\nRU 级别,实际上就是完全不隔离。每个进行中事务的中间状态,对其他事务都是可见的,所以有可能会出现「脏读」。
\n用户1设置 x=3
,在用户1的事务未提交之前,用户2 执行 get x
时却看到了 x=3
。
用户1设置 x=3
,在用户1的事务未提交之前,用户2 执行 get x
操作依旧返回的时旧值 2
。
RC 和 RR 唯一的区别在于“是否可重复读”:在一个事务执行过程中,它能不能读到其他已提交事务对数据的更新,如果能读到数据变化,就是“不可重复读”,否则就是“可重复读”。
\n继续上边的例子,如果用户2 读取 x 是在同一个事务内,那么永远读到的都是事务开始前x的值。也就是说每个事务都从数据库的一致性快照中读取数据。
\n\n\n在 RR 隔离级别下,在一个事务进行过程中,对于同一条数据,每次读到的结果总是相同的,无论其他会话是否已经更新了这条数据,这就是「可重复读」。
\n假设用户在银行有 1000 块钱,分别存放在两个账户上,每个账户 500。现在有这样一笔转账交易从账户1转 100 到账户2。如果用户在他提交转账请求之后而银行系统执行转账的过程中间,来查看两个账户的余额,他有可能看到帐号1收到转账前的余额(500元),和帐号2完成钱款转出后的余额(400元)。对于用户来说,貌似他的账户总共只有 900 元,有 100 元消失了。
\n这种异常现象称为不可重复读(nonrepeatable read)或读倾斜(read skew)。
\n串行化隔离通常被认为是最强的隔离级别。它保证即使事物可能会并行执行,但最终的结果与每次一个即串行执行结果相同。不过由于这种隔离级别性能较差,所以在实际开发中很少被用到,以下是三种实现串行化的技术方案:
\n客户端读到了其他客户端未提交的写入。
\n客户端覆盖了另一个客户端尚未提交的写入。
\n客户端在不同时间点看到了不同值。
\n两个客户端同时执行读-修改-写入操作序列,出现了其中一个覆盖了另一个的写入,但又没有包含对方最新值的情况,最终导致了部分修改发生了丢失。
\n事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。
\n事务读取了某些符合查询条件的对象,同时另一个客户端执行写入,改变了先前的查询结果。
\n幻读这个概念有些抽象,举例说明一下:
\n\n\n在实际业务中,很少能遇到幻读,即使遇到,也基本不会影响到数据准确性。
\nRU
级别隔即没有任何隔离,存在脏读、不可重复读、幻读的风险RC
可以避免脏读,还是会存在不可重复读和幻读NR
可以避免脏读和不可重复读(通常通过一致性快照),但无法避免幻读SERIALIZABLE
才可以避免幻读开始前声明,我不相信技术上存在任何银弹,包括微服务、DDD,不要指望用一套方法论或者架构能解决所有问题,能够根据当时的情况(资源、人才、业务)权衡出最符合当时场景的架构,才是一个合格的架构师的价值所在。
\n在讨论 DDD 时经常会一起讨论的是两种模型,传统贫血模型和 DDD 所推崇的充血模型。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。比如数据统计和分析,DDD 很多方法可能都用不上,或用得并不顺手,而传统的方法很容易就解决了。
\n我在大概 19 年左右看过一些 DDD 相关的内容,也在上家公司团队内推行过 DDD 的调研,但限于上家公司对技术升级、培养没有那么看重,并且推行 DDD 对人员的技术要求也比较高,只做了一次内部分享就没再有下文了。
\n最近来到新的部门后,这边在推行使用 DDD 来梳理我们的业务架构,从更高的视角审视我们目前的技术架构,用于评估架构是否合理、服务是否应当做一些拆分或者将应该归在一起的业务进行合并。我觉得这个做法是合理并且正确的,DDD 更推崇的是设计思想,可以用这个思想来指导我们做业务建模和服务设计。我们没必要为了 DDD 而 DDD,更不能脱离领域模型来空谈微服务设计。
\n由于时间久远,当时看过的内容已经忘的七七八八了,翻开之前看文章时做的一些记录,将 DDD 中常用的 “黑话” 回顾一下,记录在下文,尽量和其他人的认知对齐,在交流时能更通畅一些。
\nDDD 的领域就是这个边界内要解决的业务问题域。
\n\n我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
\n子域可以根据自身重要性和功能属性划分为三类子域:
\n用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。
\n可以将限界上下文拆解为两个词:限界和上下文
\n理论上限界上下文就是微服务的边界。
\n聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
\n高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位。
\n一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。
\n聚合的一个设计原则:在边界之外使用最终一致性。一次事务最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的最终一致性。
\n聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。
\n如果一个业务动作或行为跨多个实体,我们就需要设计领域服务。
\n在微服务内部,实体的方法被领域服务组合和封装,领域服务又被应用服务组合和封装。
\n每一个聚合都有一个仓储,仓储主要用来完成数据查询和持久化操作。
\n领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。
\n如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。
\n领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。
\n在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。
\n通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同微服务之间的流转,实现微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。
\n在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。
\n在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。
\n聚合根是一种特殊的实体,它有自己的属性和方法。聚合根可以实现聚合之间的对象引用,还可以引用聚合内的所有实体。
\n通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。
\n值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。
\n实体和值对象是组成领域模型的基础单元。
\n实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。
\n领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
\n聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
\n聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
\n聚合内数据强一致性,而聚合之间数据最终一致性。
\n在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。
\n产品愿景是对产品顶层价值设计,对产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。
\n场景分析是从用户视角出发,探索业务领域中的典型场景,产出领域中需要支撑的场景分类、用例操作以及不同子域之间的依赖关系,用以支撑领域建模。
\n领域建模是通过对业务和问题域进行分析,建立领域模型。
\n战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
\n战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
\n应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。
\n领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。
\n实体方法是最底层的原子业务逻辑。
\n\n用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化测试和批处理脚本等等。
\n应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。
\n领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。
\n领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
\n我在今年六月份转岗后被邀请加入了公司的架构组,这个组织的意图是提升公司后端整体技术能力。我们规划了每周一次公司内技术分享,一般在周五进行。
\n本周二下午架构组例会上讨论本周分享内容时,突然被老板点名希望我做一次分享,内容就是上一周他随机挑选 10 个人写了三道算法题,让我围绕这些代码做个代码质量相关的分享。
\n周二晚上告诉我要周五分享,留给我整理素材、做 PPT 的时间非常紧,所以晚上回家路上我就开始收集之前看过的资料,回家后又把《代码整洁之道》这本书翻出来,快速把里边之前标记了重点的地方进行了阅读,大致在脑子里形成了一个提纲。
\n第二天一早也就是周三,在 PPT 还没有开始做之前,老板的助理找我要分享的内容介绍,包括主题、听众收益,我基于昨晚的提纲写了一份介绍交给了她,之后我也就按照这些组织我的 PPT。
\n\n之后助理又找我要个人照片,我把去年公司给我拍的一张照片给了它,没想到过了没多久,公司所有投影仪、电视开屏背景就成了我的宣传页,有点受宠若惊。
\n\n也被公司其他同事看到纷纷发来问候:
\n\n找照片的过程也比较坎坷,因为我也没有艺术照啥的,唯一一张正式点的照片就是公司去年给我拍的形象照,当时因为运气好被评为了公司年度优秀员工。公司当时把这张照片修好后的原图发给了我,我只是打开看了下,并没有额外去保存,所以这次再找的时候就找不到了。我平时有清理 Download 目录的习惯,当时那张照片就是放到了 Download 里了。不过我还有另一个习惯,就是每次在清理 Download 前先把这个目录做个备份,将里边所有内容上传到 OneDrive 中,我会在 OneDrive 中建一个今天日期的目录,然后把此时 Download 中所有文件上传进去,再清理掉本地的文件。当然我还会定期把 OneDrive 日期太久远的目录删除,比如超过 1 年的备份,否则容量不太够用。我抱着试一试的态度在 OneDrive 的备份中找这张照片,竟然被我找到了,果然凡事需要留个后路。
\n\n在 Deadline、公司大力宣传的驱动下,我的效率倍增,在今天也就是周四下午完成了第一版的 PPT,自己都感慨效率如此之高,也多亏了之前阅读过的一些资料和书籍,将那些资料结合实际情况做下整理就成了我的 PPT。计划在发布完这篇 blog 后重头过一遍 PPT 做些微调,然后再找个会议室做做练习。
\nKeynote 可以将文稿转成 PDF、HTML 等格式,我尝试转成 HTML 后发现就是一个标准的前端项目,有一个 index.html
作为入口,这不就可以放到 Cloudflare 上做一个静态站了吗,于是我把这些文件上传到了 Cloudflare,并关联了我的域名:https://codestyle.jiapan.me/,这样就得到了一个可以在线浏览的版本,看了下效果分辨率比原始文档偏低,但又不是不能用🤷🏻♂️。
最后再感叹一声,Deadline 是第一生产力!
\n"},{"title":"常见部署策略介绍","url":"/2020/deployment-strategy-introduce/","content":"\n\n在持续交付和大规模互联网应用流行前,企业一般采用手动的方式进行部署,通常选择活动用户最少的时间(如周末或者凌晨),并且告诉大家需要一段时间来维护系统。在这段时间内,运维团队会停止旧的版本,部署新的版本并检查一切是否恢复正常。
\n但是现在,由于微服务的出现,我们会不断地将各服务的新版本部署到生产环境中,不能简单的认为部署就意味着停机,因为这样系统会一直有不同部分处于停机状态,我们需要考虑新的部署策略。
\n本文将介绍 6 种常见的部署策略:单目标部署、一次性全部部署、最小服务部署、滚动部署、蓝/绿部署、金丝雀部署,每种策略背后的理念都如其名。
\n为了更好地介绍和对比这些策略,我们先来说明一组术语:
\n这是当服务功能完全正常时,预计将运行的服务副本数量。
\n期望的实例数量简写为:desired
如:desired=3
,表示在任何一次部署中,需要将 3 个旧版本的服务实例更新为 3 个新版本的服务实例。
当删除旧实例、启动新实例的过程中,我们希望至少有一些实例是处于健康状态(无论是旧实例还是新实例),这样可以保证系统能够最低限度地提供服务。
\n最小的健康实例数量简写为:minimum
有时我们希望在删除旧实例前,先启动一些新版本的服务实例,以便减少服务部署过程中的停机时间,这意味着我们需要更多的资源。通过限制最大实例数量,我们同时也限制了部署过程中最大资源使用量。
\n最大的实例数量简写为:maximum
图表元素说明
\n对于每个策略,我们通过一张图来表示部署过程中的事件连续性。
\n这是最简单的策略,也是需要资源最少的策略。在这种策略下,我们可以假设服务只有一个正在运行的实例,无论何时都必须先停止它,然后再部署新的实例。这意味着服务会存在中断,但是不需要额外的资源。
\n单目标部署策略配置参数为:
\n下图展示了单目标部署策略的实施步骤:
\n\n\n改策略类似于单目标部署策略,唯一的区别是我们可以拥有任意固定数量的实例,而不是只有一个实例。和单目标部署的情况一样,一次性全部部署策略升级期间不需要额外的资源,但是也存在服务中断。
\n一次性全部部署策略配置参数为:
\n前边两个策略的问题在于,它们都会中断服务。我们可以调整策略来改善这一点。
\n最小服务部署表示确保始终存在着一部分健康的服务实例,我们可以先将一部分实例进行更新,等它们完成启动后再去更新另一部分旧实例。可以不断重复这个过程,直到所有的旧实例都被新的实例所替换。
\n这种方式可以在不需要额外资源的情况下避免服务中断,但风险是这些存活的实例需要能够承受住额外的流量。
\n最小服务部署策略配置参数为:
\n我们可以将滚动部署看作最小服务部署的另一种形式,不过它的重点不在于健康实例的最小数量,而在于停止实例的最大数量。
\n滚动部署最典型的情况是将停止实例的最大数量设置为 1,也就是任意时刻只有 1 个实例处于更新过程中。
\n与最小服务部署相比,滚动部署的最主要有点在于,通过限制同时停止实例的数量,我们可以控制需要保留多少实例来承接额外的负载,它的缺点是部署需要更长的时间。
\n最小服务部署策略配置参数为:
\n注:因为滚动部署相当于最小服务部署的另一种形式,所以 minimum = desired - 1
\n蓝/绿部署是微服务领域种最受欢迎的一种部署策略,之前介绍过的最小服务和滚动部署存在两个缺点:1)升级期间承担总负载的健康实例数量会减少。2)部署期间,生产环境种会混合新旧两个版本的应用程序。
\n蓝/绿部署无法简单地通过组合 desired、minimum、maximum 这几个参数来完成,它要求只有当所有的新实例都准备好的时候,用户才能访问新版本的服务,同时所有的旧实例立即变为不可用。为了实现这一目标,我们需要控制请求路由和服务编排。
\n\n\n蓝/绿部署提供了最佳的用户体验,但是代价是增加了复杂性,以及占用了更多资源。
\n金丝雀部署是另一种无法通过组合 desired、minimum、maximum 参数实现的策略。这种策略允许我们尝试新版本的服务,但不完全承诺切换到新版本。这样我们只需在原来的旧版本的实例中添加一个新版本的实例,而不必停止其中的旧版本。负载均衡器会将一部分请求转发到金丝雀实例上,我们可以通过检查日志、指标来了解新实例的运行情况。
\n金丝雀部署可以分为两个步骤执行:
\n\n\n金丝雀实例有时需要较长的时间,才能充分观察到它在新环境下的运行状况,在这个过程中,我们可能会部署其他的新版本,这时需要确保只重新部署金丝雀意外的实例。
\n真正用到金丝雀的实例情况非常少,如果我们只想向一部分用户开放新功能,可以使用功能开关来实现。金丝雀部署的另一个好处是可以测试一些底层的配置变化,例如日志、指标框架、垃圾回收或者新版 jvm 等。
\n事件溯源模式用于设计一个具有确定性的系统,这改变了普通系统设计的理念。我们通过一个电商系统来演示一下普通的 CRUD 和事件溯源模式的区别。
\n事件溯源模式不是在数据库中记录订单状态,而是将导致状态变化的事件保存在事件存储中,事件存储是一个仅附加的日志,类似于数据库中的 undo log。
\n事件必须有递增的主键 ID,以保证其顺序。订单状态通过回放事件来构建,并在订单视图(OrderView)中维护。如果订单视图发生故障,我们总是可以依赖事件存储进行修正,它是恢复订单状态的真实来源。
\n让我们来看看详细的步骤。
\n很多人并不了解软件架构和软件设计之间的区别。即使对于开发人员来说,对两者的界限也很模糊,他们可能还会把架构模式和设计模式中的内容搞混。作为一名开发人员,我想简述一下这些概念并解释软件设计和软件架构之间的区别。另外,我还会证明为什么软件架构和软件设计对我们来说很重要。
\n简单来说,软件架构是将软件特性(如灵活性、可伸缩性、可行性和安全性)转换为满足技术和业务期望的结构化解决方案的过程。这个定义使我们开始去思考可能会影响软件架构设计的各种特性。除了技术需求外,还有其他方面会影响软件架构(如商业需求或运营需求)。
\n如上所述,软件特性描述了软件在操作和技术层面上的需求和期望。所以,当老板说我们正在一个快速变化的市场当中竞争,企业需要迅速调整现有的商业模式,这时如果企业的业务需求很紧急,要求在短时间内完成的话,这个软件就应该有「可扩展、模块化和可维护」的特性。作为软件架构师,我们应该将性能(performance)、低容错性(low fault tolerance)、扩展性(scalability)和可用性(reliability)作为我们软件的关键特性。在定义了上边的几个特性后,老板告诉你我们当前预算有限,此时又要将另一个特性考虑进来,也就是「可行性」。
\n这个维基百科中列出了全部的软件特性:https://en.wikipedia.org/wiki/List_of_system_quality_attributes
\n很多人之前可能听说过「微服务」。微服务是众多软件架构模式之一,其他的架构模式还有分层模式(Layered)、事件驱动模式(Event-Driven)、无服务模式(Serverless)等。我会在后后面介绍几个常见的架构模式。微服务模式在被亚马逊和 Netflix 采纳后收获了很大的影响力。现在,让我们更深入地研究架构模式。
\n这里提醒一句,不要把设计模式(如工厂模式或适配器模式)与架构模式搞混,我们在稍后讨论设计模式。
\n无服务架构指的是依赖第三方服务来管理服务器和后端复杂基础设施的应用解决方案。无服务架构可以分为两类:第一类是「后端即服务(BaaS)」,另一类是「函数即服务(FaaS)」。
\n无服务架构帮助我们节省了大量服务器部署和例行维护任务所花费的时间。最著名的无服务 API 提供商是亚马逊的 AWS Lambda。
\n事件驱动架构依赖事件生产者( Event Producers)和事件消费者(Event Consumers)。它的主要思想是将系统各个模块进行解耦,当有某个模块中的一个事件发生后,对这个事件感兴趣的其他模块会被触发。听起来很复杂?我们来举个简单的例子:假如你设计了一个在线购物系统,它包含两个模块:订单模块和供应商模块。如果客户产生了购买行为,订单模块会生成一个 ORDER_PENDING
事件。由于供应商模块对 ORDER_PENDING
事件感兴趣,所以它会监听这个事件,用以触发后续的行为。一旦供应商模块收到这个事件,它会执行一些任务或者触发其他后续事件(比如从某个供货商处订购更多的商品、通知仓库发货等)。
需要记住的是,事件生产者并不知道有哪些事件消费者在监听哪些事件。
\n微服务架构已成为近几年最受欢迎的架构。它依赖于开发小而独立的模块化服务,其中每个服务都可以解决特定的问题或执行独特的任务,这些模块通过定义明确的 API 互相通信来实现业务目标。对于微服务架构无需介绍太多,来看看下边这张图:
\n\n软件架构负责软件框架和基础设施的选型,软件设计负责代码级别的设计,例如每个模块的作用、类的范围和函数用途等。
\n作为一名开发人员,了解 SOLID
原则和如何通过设计模式来解决常规问题是非常重要的。
SOLID
指的是单一职责原则(Single Responsibility)、开闭原则(Open Closed)、里式替换原则(Liskov substitution)、接口隔离原则(Interface Segregation)和依赖反转原则(Dependency Inversion)。
XyClass
继承自 AbClass
,那么 XyClass
不可以改变父类已实现功能的行为。因此你可以放心地使用 XyClass
对象而不是 AbClass
对象,而不用担心破坏应用的逻辑。Order
类依赖于 User
类,那么 User
对象应该在 Order
类之外进行实例化。工厂模式是 OOP 世界中最常用的设计模式,因为通过这个模式,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。来看个例子:
\n我们现在有一个接口和三个实现了这个接口的类:
\npublic class Rectangle implements Shape { |
假如你现在要实例化一个方形 Shape
,有两种方式可以实现:
第一种:
\nShape shape = new Square(); |
第二种:
\nShape shape = ShapeFactory.getShape("SQUARE"); |
public class ShapeFactory { |
我更喜欢第二种方式,有三点原因。首先一个调用者想创建一个对象,只要知道其名称就可以了。其次扩展性高,如果想增加一个新的形状,只要扩展 ShapeFactory
工厂类就可以。最后屏蔽了具体的实现,调用者只用关心 Shape
接口,即使需要传额外参数来进行实例化,调用者也无需去关心。
适配器模式是结构设计模式之一。根据这个名字可以判断出,我们可以期望它把类的意外用法转为我们所预期的用法。
\n假如我们的应用要调用了百度的 API,需要在发起请求前需要调用 getBaiduToken()
获取 token。我们在 20 多个不同的地方调用了这个函数。之后百度在发布的新版本中把这个函数改名为了 getAccessToken()
。
现在我们必须在应用代码的所有位置找到并替换这个函数名,或者可以创建一个适配器类:
\npublic class BaiduAdapter { |
应用中的调用改为:
\ntoken = BaiduAdapter.getToken(); |
这种情况下,即使百度修改了函数名,我们也只需修改一行代码,应用程序的其余部分将保持正常工作。
\npublic class BaiduAdapter { |
本文没有详细讨论各种设计模式,如果你想了解更多,我推荐 2 本关于设计模式的书:
\n最后再来说说软件架构师和软件开发人员之间的区别。
\n架构师通常是具有丰富经验的 team leader,他们对现有解决方案有很好的了解,这些方案可以帮助他们在计划阶段做出正确的决策。软件开发人员应该去了解更多的软件设计,并对软件架构有足够的了解,以使团队内部的沟通更佳高效。
\n"},{"title":"NotNull、NotEmpty、NotBlank 的区别","url":"/2017/different-notnull-notempty-notblank/","content":"The CharSequence, Collection, Map or Array object is not null, but can be empty.
\nThe CharSequence, Collection, Map or Array object is not null and size > 0.
\nThe string is not null and the trimmed length is greater than zero.
\nString name = null; |
\n\n\n本文是对《原则》一书的第二部分,第 4 章节的概括。
\n
作者认为,由于不同人的大脑构造不同,所以每个人的行为方式(原文中使用的是体验现实的方式)也千差万别。
\n开头处作者讲了一个真实案例,他交给同事鲍勃一项宏大的任务,鲍勃可以任意挑选自己的团队成员,但在事情进行一段时间后他们发现在具体落实方面毫无进展,经过长时间的讨论和研究,发现问题在于鲍勃挑选的每个角色跟他自己的长处、短板相似。
\n作者就此对人们不同的思维方式产生了浓厚的兴趣,开始探寻不同思维方式所带来的不同的力量。
\n我们很多的心理差异实际上是生理差异,大脑就像高矮胖瘦一样存在着的差异,从而影响到我们的心理能力。
\n我们都有过这样的情况:对其他人做出的决策感到愤怒或者沮丧,但在了解每个人的大脑在生理上就存在不同后,就会逐渐明白他们并不是有意识地采取在我们看来低效的做法,他们只不过是依据自己认为正确的做法来做事,而这种做事方式又是由他们大脑的运行方式决定的,人与人之间出现的分歧不是因为沟通不良导致的,而是因为我们不同的思维方式导致了沟通的不良。
\n每个人的天性各不相同,这些天性即可能帮助我们又可能伤害我们,取决于我们如何运用我们的天性。这也是我们为什么经常说,具有创造力的天才和疯子往往只有一步之遥的原因。很多杰出的有创造力的人都曾患有双相障碍,如贝多芬、海明威、柴可夫斯基、丘吉尔。
\n科学研究发现,人脑的构造先天地使人需要并享受社会合作。所以做有意义的工作和进行有意义的社交活动,不仅会让我们的生活更美好,更是我们天生就需要的生理需求。从社会合作中获得有意义的人际关系使我们更快乐、更健康、更有创造力,同时也会让我们的大脑发育得更好。我们的祖先进化出了支持合作功能的大脑,并以此支持狩猎等需要合作的活动,随着群体变得比个体更强大,大脑不断进化出管理更大群体的能力,这一进化使得利他意识、伦理观、良知和尊严意识发展起来。
\n我们的头脑中永远会存在两股势力间,分别是情绪和理性思考。
\n情绪主要是由潜意识性的杏仁核控制的,而理性思考主要是由意识性的前额皮层控制的。
\n杏仁核是一个小小的杏仁状构造,深深地隐藏在大脑底部,是大脑最强有力的区域之一。尽管你感觉不到它,但它控制着你的行为。作者把人们被情绪控制时的状态称为「杏仁核绑架」。如果你放任自己做出本能反应的话,你就很可能会反应过度,你也可以安慰自己,因为你已经知道,你经历的任何精神痛苦不久后都会自动消失。
\n大部分情况下,「杏仁核绑架」来得快去得也快,杏仁核产生的反应是一阵爆发然后平息,而前额皮层产生的反应更为稳定和持久。
\n我们所面临最大的挑战是让深思熟虑的较高层次的自我管理情绪性的较低层次的自我,做到这一点的最佳途径是有意识地养成习惯。习惯是大脑中最强有力的工具。习惯本质上是一种惯性,一种继续把你一直做的事情做下去(或者继续不做你一直不做的事情)的强烈倾向。研究显示,如果你能坚持某种行为约 18 个月,你就会形成一种几乎要永远做下去的强烈倾向。
\n在我们的大脑中有两种潜意识,一种就是我们上边提到的情绪性的潜意识,它们具有危险的动物性,但我们还有一部分潜意识比意识更聪明、反应更快。
\n人们所说的灵感就是来自这部分潜意识,你会发现大部分情况下我们是在放松、不试图刻意去思考的时候会产生创造性突破。这也解释了为什么我们经常在淋浴的时候产生创意。
\n很多人认为只要往我们大脑中(也就是意识里)不断地塞入东西才能让我们进步,这样做可能会适得其反,有时候清理我们的头脑可能是取得进展的最佳途径。
\n我们听说过一种说法,有的人是左脑思维者,有的人是右脑思维者。
\n简单来说左右脑的分工如下:
\n通常左脑思维者人们称为「明智」的人,右脑思维者被称为「机灵」的人。
\n\n如果我们了解到自己和其他人的思维倾向,认识到这两种思维方式都各有所长,并按照思维方式的不同来对每个人分配他更加擅长的工作,可以产生很好的结果。
\n大自然塑造万物皆是有目的的,每个人都有自己的长处和短处,每个人都在他们的生活中扮演着重要的角色。我们所需要的并不是战胜其他人的勇气,而是坚持做最真实自我的勇气,不必太过在意其他人对你的冀望。
\n在我们生活和工作中会遇到各式各样性格的人,有的人内向,有的人外向;有的人喜欢井然有序的生活方式,另一些喜欢灵活随性的方式;一些人理性分析客观事实,考虑所有与具体情况相关的已知、可证明因素,富有逻辑性地决定如何行动,而另一些偏好感觉者关注人与人之间的和谐;一些人可以看到全局,另一些人看到的是细节;一些人关注日常任务,另一些人关注目标及其实现途径。
\n可以把团队中的成员识别为5种类型,创造者、推进者、改进者、贯彻者和变通者。
\n作者认为在我们的生命历程中,了解人的特性是必要的一步。我们做什么并不重要,只要做的事符合自己的个性和人生理想就够了。经济水平在基本生活线之上人,幸福水平和大众所认为的成功标准之间是没有任何联系的。
\n回到刚开始作者遇到的问题,无论在生活还是工作中,我们和其他人合作的最好方式都是把具有互补性特征的人搭配在一起,这样才能创建最适于完成任务的团队组合。把不同的人组织起来,更好地发挥其长处,弥补其短板,就像指挥交响乐团一样,做得好就很漂亮,做不好就很糟糕。
\n最后作者举了一个团队管理的例子,我觉得很恰当,摘抄下来:
\n在管理其他人方面,我能想到的比方是一个好乐队。乐队指挥是塑造者、引导者,他主要不是“做”(例如他不演奏乐器,尽管他了解很多关于乐器的知识),而是勾勒结果,并确保乐队所有成员一起发力实现目标。指挥要确保每个乐队成员知道自己的长处和短处,以及各自的职责。不是每个人都自己演奏得最好,而是通过合作实现“1+1 > 2”的效果。指挥最吃力不讨好的工作之一是开除总是不能好好演奏或合作的人。最重要的是,指挥要确保演奏效果和他想的一样。他说:“音乐得是这样。”然后加以落实:“贝斯手,撑起整个格局。这里要连接得妙,这里要奏出神韵。”乐队的每个部分也有各自的领导者,如首席小提琴手等,他们也帮助把作曲者和指挥的设想表达出来。
\n"},{"title":"不要在工作中生气","url":"/2023/do-not-angry-at-work/","content":"我很容易在工作中生气,大部分情况下是因为对方打扰了我计划的节奏。
\n比如,当我还有很多工作没有完成的时候,被产品拉着开方案讨论会,或者被其他部门拉着讨论需求。这种感觉就像:我是砍柴的,他是放羊的,我和他聊了一天,他的羊吃饱了,我的柴还没着落;与此类似的是中午12点和下午7点后打扰我个人生活的工贼。
\n另一个容易生气的点是一堆群找我看问题,很多时候就是:一杯茶、一包烟、一个BUG看一天(尽管我不抽烟)。手头的需求做不完,还要去处理各种线上问题,甚至还要配合公司的安全部门一起打击黑灰产,把时间都浪费在了偶然复杂度的事情上了。
\n还有一种情况是被迫做一些跟业务成果、个人成长无关的事情,举个例子,为了降低运维成本,我们公司今年要做机房搬迁。我需要花大量时间和SRE讨论细节,他们会给我们提很多需求,列很多TODO,这些事情只有苦劳,没有功劳,都是些杂活。
\n\n\n不知道公司兴师动众要用一年时间完成的机房迁移,是真的能省很多成本,还是为了某个高层的个人绩效才要搞的。
\n
我生气、愤怒的本质是感受到了失控感,自己无法控制自己的时间,觉得自己宝贵的时间被别人浪费了。
\n不管是当面抢白还是打字怼对方,只要我给对方表现出过不耐烦、发脾气、发泄情绪,事后一定会后悔,会有歉意。以至于会做出一些补偿性回馈,在其他事情上补偿,但补偿的人可能并不是当事者。比如在跟下一个人沟通时就会非常有耐心,甚至百依百顺,破格答应他提的一些条件。或者在下班路上对路人友好了很多。
\n工作中完全犯不上生气,大家都是来这里给资本家打工赚钱的,都在争取自己的利益,没有谁要故意为难谁。产品经理临时找我插入需求或者调整方案,也一定是她的领导这么要求他的,她也有自己的苦衷,不会刻意来找我的茬,我完全没有必要把气撒在他的身上。
\n东东枪曾分享过一个观点:
\n\n\n我们何德何能?凭什么要求自己的工作环境、共事的伙伴都是完美的呢?
\n
谁也不是我肚子里的蛔虫,不知道我的所思所想很正常,难免在我不想被打扰的时候打扰我。
\n在工作上生气还会给同事留下非常糟糕的印象,我自己也不喜欢跟脾气不好的同事配合
\n管理好压力,管理好情绪,管理好预期。
\n气大伤身是有科学依据的:
\n\n时间总是不够用的,事情总是做不完的。今天做不完就明天做,明天做不完就分给别人做。
\n"},{"title":"为什么不应该使用 Docker 部署数据库","url":"/2020/do-not-run-database-in-docker/","content":"\n\n服务容器化变得越来越流行,如今大部分的 Web 服务会首选部署在容器中。容器的优点是否也适用于部署数据库?
\n很多文章在分析这个问题时会站在:性能、网络、资源隔离等方面来考虑。比如提到多加一层(Union FS)会导致性能下降甚至数据不可靠、Docker 在网络方面的诟病、Docker 的资源隔离不适合用于数据库(同时在一台机器上启动多个数据库实例,共享同一份数据,但两个实例由于隔离互相不可见,就会导致数据混乱的问题)。
\n我并没有找到 Union FS 会让性能下降多少的性能测试,网络方面虽然遇到过坑但也都能解决,资源隔离通常使用端口号进行互斥即可,保证只有一个实例运行。
\n上边那些并不是最核心的问题,我认为最核心的是 Docker 的使用场景并不适用于数据库组件。
\n我们来看一下使用 Docker 带来优势:
\n以上提到的大多数优势并不适用于数据库的运行环境:数据库通常是长期运行的,数据完整性是重中之重。我们不需要数据库自动扩容(在 Docker 中水平伸缩只能用于无状态计算服务,而不是数据库)、也不需要持续更新数据库的代码来进行持续集成。
\n除了场景不适合之外,另一个问题是数据库软件版本升级。对于无状态应用或者数据库的小版本更新来说,直接修改 Dockerfile 中的版本号并重新构建、重启即可完成升级,但数据库的大版本升级就没有这么简单了,大版本升级数据库版本会伴随数据存储结构的更新,仅仅升级版本是不行的。通常数据库提供商会提供相应的命令来让我们对数据库进行升级,但这样做的前提是数据库不能运行在容器中(需要进入容器才能执行命令、软件版本无法持久化)。
\n凡事也不是绝对的,在开发环境中通过 Docker 来运行数据库就是个不错的选择。或者将它用于数据量不大、对可靠性要求不是那么高并且所有东西都放在单机中运行的项目中也是可以的,不过前提是要做好数据的日常备份工作。
\n"},{"title":"不想上班","url":"/2022/do-not-want-to-work/","content":"\n今天是国庆节最后一天,可是我极度不想上班,所以又继续请了 2 天假。
\n最近这大半年来我特别不愿意面对工作,对工作持续性没有热情,偶尔有热情的时候是在纯粹写代码的那几个小时。那几个小时里不用考虑和人打交道,不用考虑怎么在晨会、周会上汇报工作,不用去迎合他人做让自己违心的事。
\n还有个原因是我不喜欢被当成未成年人那样去管理,我更喜欢靠完全的自驱去工作,和 LD 沟通好「双赢协议」后他的工作就完成了、就可以退场了,他所要做的就是做好后勤工作,而不是每天来问一问进度或者开会让每个人秀一下自己的工作量。对我的管理越紧我会越认为是对我的不信任,我也越会以敷衍作为回报。
\n另一个对工作不再有热情的原因是认清了一些现实,之前会幻想自己可以靠技术改变世界,靠技术发大财,现在不再有这样的想法了,对技术的热情也没有那么高了,反而会考虑如果可以的话应该在业务方面更深入一些,技术不是核心,至少对于大部分互联网公司是这样的。
\n我在刚工作的时候特别喜欢上班,虽然那个时候公司周末不加班,平时 6 点就下班,但我还是会在下班的时间在公司以外的地方写公司的代码。
\n我记得很清楚的是自己刚来北京的时候,那时候连房都没有租到,和一个大学认识的朋友一起住在他老家一个哥哥的工作室里,那里白天需要办公,我俩早上起床后把铺盖收到一个橱柜里,晚上再拿出来铺在地上睡觉。我有过几次整晚不睡觉去写代码,而且是非常心甘情愿非常开心地写代码。
\n找不到当时打地铺的照片了,只找到一张在那个工作室住了半年后租到房子时要搬家前的一张照片。
\n\n现在绝对不会再这样做了,现在我晚上到家后连打开电脑的欲望都没有,甚至周末都不想动一下电脑,也不会再去看技术书籍,周末的时候也不会看书了,就纯粹歇着虚度时光,我躺平了,这种躺平给我带来的坏处是技术方面不带成长,好处是我不用再那么频繁的复用抗焦虑药物了,从之前的一周有 4 天要吃药,降低到了现在的一周只需要吃 1-2 次。
\n虽然不认可现在公司的所作所为,但我也不想去找工作,我不是面试选手,而且现在整体经济也在下行,我在教育背景、工作履历上都没有优势。
\n我发现我现在越来越喜欢读鸡汤书了,因为工作中遇到的都是糟心事,读一读鸡汤多少能给我一些慰藉。
\n我甚至已经把工作当成了对自己的一种折磨,比如我不会在工作日吃美食,因为一点吃饭的心情都没有,而且那也是对美食的不尊重,工作日凑合吃一口让自己不至于饿死就行,工作日的时候朋友找我约饭我也都会推掉(不管是中午还是晚上)。这样导致的另一个问题是:到了周末我会暴饮暴食,每周工作日 5 天掉的称周末两天我可以翻倍补回来。国庆节休息这几天我已经涨了 6 斤了🙁。
\n去他妈的工作、去他们的 OKR、去他妈的 KPI。
\n"},{"title":"通过 Docker 部署常用组件","url":"/2020/docker-component/","content":"日常开发中,会用到一些数据库之类的基础组件,通过源码或者安装包的方式进行部署有时会比较麻烦,这种情况下可以用 Docker 来部署,以下是我积累的一些常用组件的启动命令。
\ndocker run -d --restart=always --env TZ='Asia/Shanghai' \\ |
docker run -d --restart=always \\ |
docker run -d --restart=always \\ |
docker run -d --restart=always \\ |
docker-compose.yml |
docker-compose.yml |
docker run --restart=always --name zipkin -d -p 9411:9411 openzipkin/zipkin |
docker-compose.yml |
tables_xxl_job.sql |
docker run -d --restart=always -p 8081:8081 \\ |
version: "3" |
前两天使用docker 通过一个一个启动的方式,将 ELK 部署了起来,但是逐个启动的方式有些麻烦,所以写了个 docker-compose.yml
来一键启动:
version: '2' |
logstash.conf
\ninput { |
最近在重构公司的直播推荐服务,特征数据的存储使用的是 Hbase,但有个问题是我们的开发环境并没有搭建 HBase 集群,开发环境和生产环境网络又不通,这样本地调试就很不方便,所以我需要在本地搭一个 HBase 集群。
\n第一时间想到的就是使用 docker-compose
牛顿老师说过,轮子还是别人的圆,于是我在 gayhub 上找到了这个轮子:https://github.com/big-data-europe/docker-hbase
\n但是在搭建过程中发现了它的问题,有两个重要的端口没有在 docker-compose.yml
文件中开放,并且说明中没有提示要修改 hosts,再看了下这个仓库的更新时间,已经有 3 年没有更新,所以我也就不提 pr 了,而是将仓库进行了 fork 并修改了源码,可以直接 clone 我的仓库来搭建,流程如下:
git clone git@github.com:Panmax/docker-hbase.git |
0.0.0.0 hbase-master |
docker-compose -f docker-compose-distributed-local.yml up |
等待所有服务完全启动后,就可以让我们的程序通过监听在本地 2181 端口的 zookeeper 去发现并访问 hbase 了。
\nHbase 数据录入
\n为了验证代码逻辑,我还需要写一些数据到 hbase 中,操作如下:
\ndocker exec -it hbase-master bash |
cd /opt/hbase-1.2.6/ |
bin/hbase shell |
然后就可以使用SQL语句进行操作了,例如:
\n> create 'mods_model_storage', 'f' |
为了更加熟悉我们现在所使用的微服务架构,了解每一个组件的特性,我将部署在 lc0 上的一系列微服务组件(网关、注册中心、配置中心、熔断监控等)尝试重新在 lc7 机器上再部署一遍。
\n在部署配置中心时,需要依赖一个 MQ 组件,项目中用的是 RabbitMQ,所以我需要在 lc7 上安装它。
\nRabbitMQ 是用 Erlang 编写的,直接部署的话需要先部署 Erlang 环境,比较麻烦。在 docker 环境下部署就比较简单了,直接使用 RabbitMQ 官方提供的镜像即可。
\n直接安装的方式可以参考 http://blog.didispace.com/spring-boot-rabbitmq/
这篇文章,下边我主要来说下如何使用 Docker 部署 RabbitMQ。
运行 docker pull rabbitmq:management
从官方下载镜像到本地,这里使用的是带 Web 管理插件的镜像。
启动容器:
\ndocker run -d --name rabbitmq --publish 5671:5671 \\ |
容器启动之后就可以访问 Web 管理界面了 http://IP:15672
默认创建了一个 guest 用户,密码也是 guest。
\n\n通过这种方式来部署 RabbitMQ 非常方便,今后可以在部署 测试环境 时用起来,因为我们还没有大规模使用 Docker,所以暂时不建议在 生产环境 来使用。
\n\n\nutf8 是 MySQL 早期版本中支持的一种字符集,只支持最长三个字节的 UTF-8 字符,也就是 Unicode 中的基本多文本平面。这可能是因为在 MySQL 发布初期,基本多文种平面之外的字符确实很少用到。而在 MySQL5.5.3 版本后,要在 MySQL 中保存 4 字节长度的 UTF-8 字符,就可以使用 utf8mb4 字符集了。例如可以用 utf8mb4 字符编码直接存储 emoj 表情,而不是存表情的替换字符。
\n
相信好多人都被 emoji 表情在 MySQL 中存储的问题坑过,被坑过的人都记住了在 MySQL 中创建库表时要使用 utf8mb4,而不是 utf8。
\n但如果 MySQL 的服务端没有设置为 utf8mb4
的话,使用 jdbc
往里写 emoji 同样会报错,即便是指定了 characterEncoding=utf8
也不会起作用,并且 characterEncoding
不支持设置为 utf8mb4
。
报错通常为:
\nIncorrect string value: '\\xF0\\x9F...' for column 'xxx' at row 1 |
解决办法是将服务端的默认字符集改为 utf8mb4
。我们的 MySQL 是使用 docker 启动的,需要把配置文件映射进去。在这里我们踩过一次坑,之前我们是将 my.conf
文件映射到容器的 /etc/my.cnf
,实际使用时发现配置文件并没有生效,需要映射到 /etc/mysql/my.cnf
才能生效。
先查看一下当前数据库的字符集:
\n> show variables like '%char%'; |
发现默认都是 latin1
,现在我们在 my.conf
中的相应模块中加入如下配置,然后重启 MySQL:
[client] |
再次查看字符集:
\n> show variables like '%char%'; |
发现已经更改为了 utf8mb4
,再次尝试插入 emoji 成功,问题解决。
jdbc
在连接 MySQL 时,如果不指定 characterEncoding
会默认使用 MySQL 服务端 的字符集,因为之前我们的 MySQL 服务端字符集为 latin1,所以手动指定了一下 characterEncoding=utf8
,但这样使用的是 utf8
编码建立连接,所以依旧不能插入 emoji。
在修改为 utf8mb4
之后也就不用设置 characterEncoding=utf8
和 useUnicode=true
参数了(我尝试了下,不去掉也没有什么问题)。
docker run --name mysql --net host -v /data04/docker/mysql:/var/lib/mysql -v /opt/tianhe/mysql/my.cnf:/etc/mysql/my.cnf -v /opt/data:/opt/data -e "MYSQL_ROOT_PASSWORD=xxxxxx" -d mariadb:10.2 |
我的 Docker 使用经验都是通过在项目中的运用学到的,实际上已经可以满足日常所需了,但是自认为缺乏一些细节方面的知识,所以这几天通过阅读一本掘金小册《开发者必备的 Docker 实践指南》,进行了一次系统性学习,以下是我记录的一些我认为的重点和我之前不太了解或不熟悉的内容。
\n本文不适合作为 Docker 初学者学习的指南,适合于查漏补缺时的参考。
\n所谓容器技术,指的是操作系统自身支持一些接口,能够让应用程序间可以互不干扰的独立运行,并且能够对其在运行中所使用的资源进行干预。
\n由于没有指令转换,运行在容器中的应用程序自身必须支持在真实操作系统上运行,也就是必须遵循硬件平台的指令规则。
\n属性 | \n虚拟机 | \nDocker | \n
---|---|---|
启动速度 | \n分钟级 | \n秒级 | \n
硬盘使用 | \nGB 级 | \nMB 级 | \n
性能 | \n较低 | \n接近原生 | \n
普通机器支撑量 | \n几个 | \n数百个 | \n
Docker 的实现,主要归结于三大技术:
\n与其他虚拟化实现甚至其他容器引擎不同的是,Docker 推崇一种轻量级容器的结构
\n四大组成对象
\n在 Docker Engine 中,实现了 Docker 技术中最核心的部分,也就是容器引擎这一部分。
\nDocker Engine 是由多个独立软件所组成的软件包。最核心的是 docker daemon 和 docker CLI。
\n\n在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API
\n\ndocker daemon 和 docker CLI 所组成的,正是一个标准 C/S ( Client-Server ) 结构的应用程序。衔接这两者的,正是 docker daemon 所提供的这套 RESTful API。
\nDocker Engine 的稳定版固定为每三个月更新一次,而预览版则每月都会更新。
\n\n不论是稳定版还是预览版,它们都会以发布时的年月来命名版本号,例如如 17 年 3 月的版本,版本号就是 17.03。
\n以目前 Docker 官方主要维护的版本为例,我们需要使用基于 Linux kernel 3.10 以上版本的 Linux 系统来安装 Docker。
\nDocker 官方为 Windows 和 macOS 系统单独开辟了一条产品线,名为 Docker Desktop,其定位是快速为开发者提供在 Windows 和 macOS 中运行 Docker 环境的工具。
\n既然 Windows 和 macOS 中没有 Docker 能够利用的 Linux 环境,那么我们需要提供一个 Linux 环境
\nDocker 容器的生命周期里分为五种状态:
\n在 Docker 的设计中,容器的生命周期其实与容器中 PID 为 1 这个进程有着密切的关系。
\nDocker 的写时复制与编程中的相类似,也就是在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。
\n通过 docker ps
命令,可以罗列出 Docker 中的容器。
docker ps
列出的容器是处于运行中的容器,如果要列出所有状态的容器,需要增加 -a
或 --all
选项。在开发过程中,我们更常使用它来作为我们进入容器的桥梁。
\ndocker exec
命令来启动 sh 或 bash,并通过它们实现对容器内的虚拟环境的控制。docker exec -it nginx bash
-i
( –interactive ) 表示保持我们的输入流-t
( –tty ) 表示启用一个伪终端,形成我们与 bash 的交互Docker 为我们提供了一个 docker attach
命令,用于将当前的输入输出流连接到指定的容器上。
docker attach nginx
docker run
中的 -d
选项有相反的意思 )docker attach
限制较多,功能也不够强大,所以并没有太多用武之地。在 Docker 网络中,有三个比较核心的概念:沙盒 ( Sandbox )、网络 ( Network )、端点 ( Endpoint )。
\n这三者形成了 Docker 网络的核心模型,也就是容器网络模型 ( Container Network Model )。
\n目前 Docker 官方为我们提供了五种 Docker 网络驱动,分别是:Bridge Driver、Host Driver、Overlay Driver、MacLan Driver、None Driver。
\nDocker 为容器网络增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问。
\n端口的暴露可以通过 Docker 镜像进行定义,也可以在容器创建时进行定义。
\ndocker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
这里我们为 MySQL 暴露了 13306 和 23306 这两个端口,暴露后我们可以在 docker ps
中看到这两个端口已经成功的打开。
… PORTS NAMES |
在 Docker 里,我们也能够创建网络,形成自己定义虚拟子网的目的。
\ndocker network create -d bridge individual
通过 docker network ls
或是 docker network list
可以查看 Docker 中已经存在的网络。
我们创建容器时,可以通过 --network
来指定容器所加入的网络
--network bridge
让其加入 )。docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network individual mysql:5.7
docker run -d --name webapp --link mysql --network bridge webapp:latest
在实际使用中,还有一个非常常见的需求,就是我们需要在容器外通过网络访问容器中的应用。
\n\n通过 Docker 端口映射功能,我们可以把容器的端口映射到宿主操作系统的端口上,当我们从外部访问宿主操作系统的端口时,数据请求就会自动发送给与之关联的容器端口。
\ndocker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
使用端口映射选项的格式是 -p <ip>:<host-port>:<container-port>
,其中 ip 是宿主操作系统的监听 ip,可以用来控制监听的网卡,默认为 0.0.0.0
,也就是监听所有网卡。
基于底层存储实现,Docker 提供了三种适用于不同场景的文件系统挂载方式:Bind Mount、Volume 和 Tmpfs Mount。
\n使用 -v
或 --volume
来挂载宿主操作系统目录的形式是 -v <host-path>:<container-path>
或 --volume <host-path>:<container-path>
,其中 host-path
和 container-path
分别代表宿主操作系统中的目录和容器中的目录。
Docker 还支持以只读的方式挂载,通过只读方式挂载的目录和文件,只能被容器中的程序读取,但不接受容器中程序修改它们的请求。在挂载选项 -v
后再接上 :ro
就可以只读挂载了。
docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
Bind Mount 常见场景:
\n/etc/timezone
这个文件挂载并覆盖容器中的时区配置。Tmpfs Mount 是一种特殊的挂载方式,它主要利用内存来存储数据。由于内存不是持久性存储设备,所以其带给 Tmpfs Mount 的特征就是临时性挂载。
\n挂载临时文件目录要通过 --tmpfs
这个选项来完成。
docker run -d --name webapp --tmpfs /webapp/cache webapp:latest
Tmpfs Mount 常见场景:
\n数据卷的本质其实依然是宿主操作系统上的一个目录,只不过这个目录存放在 Docker 内部,接受 Docker 的管理。
\ndocker run -d --name webapp -v /webapp/storage webapp:latest
为了方便识别数据卷,我们可以像命名容器一样为数据卷命名。在我们未给出数据卷命名的时候,Docker 会采用数据卷的 ID 命名数据卷。我们也可以通过 -v <name>:<container-path>
这种形式来命名数据卷。
$ docker run -d --name webapp -v appdata:/webapp/storage webapp:latest
数据卷常见场景:
\n数据卷的另一大作用是实现容器间的目录共享,也就是通过挂载相同的数据卷,让容器之间能够同时看到并操作数据卷中的内容。
\ndocker run -d --name webapp -v html:/webapp/html webapp:latest
docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12
-v
选项挂载数据卷时,如果数据卷不存在,Docker 会为我们自动创建和分配宿主操作系统的目录,而如果同名数据卷已经存在,则会直接引用。通过 docker volume rm
来删除指定的数据卷
docker volume rm appdata
在 docker rm
删除容器的命令中,我们可以通过增加 -v
选项来删除容器关联的数据卷。
docker rm -v webapp
Docker 向我们提供了 docker volume prune
命令,可以删除那些没有被容器引用的数据卷。
数据卷容器,就是一个没有具体指定的应用,甚至不需要运行的容器,我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用。
\n由于不需要容器本身运行,因而找个简单的系统镜像都可以完成创建。
\ndocker create --name appdata -v /webapp/storage ubuntu
Docker 的 Network 是容器间的网络桥梁,如果做类比,数据卷容器就可以算是容器间的文件系统桥梁。
\n--volumes-from
选项即可。docker run -d --name webapp --volumes-from appdata webapp:latest
利用数据卷容器,我们能够更方便的对数据卷中的数据进行迁移。
\n数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程。
\ndocker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage
--rm
选项,我们可以让容器在停止后自动删除,而不需要我们再使用容器删除命令来删除它,这对于我们使用一些临时容器很有帮助。/backup
下找到数据卷的备份文件,也就是 backup.tar
了。docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
Docker 里为我们提供了一个相对支持丰富的挂载方式,也就是通过 --mount
这个选项配置挂载。
sudo docker run -d --name webapp webapp:latest --mount 'type=volume,src=appdata,dst=/webapp/storage,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>' webapp:latest
--mount
中,我们可以通过逗号分隔这种 CSV 格式来定义多个参数。type
我们可以定义挂载类型,其值可以是:bind,volume 或 tmpfs。--mount
选项能够帮助我们实现集群挂载的定义,例如在这个例子中,我们挂载的来源是一个 NFS 目录。Docker 镜像的本质是多个基于 UnionFS 的镜像层依次挂载的结果,而容器的文件系统则是在以只读方式挂载镜像后增加的一个可读可写的沙盒环境。Docker 中为我们提供了将容器中的这个可读可写的沙盒环境持久化为一个镜像层的方法。
\n将容器修改的内容保存为镜像的命令是 docker commit
docker commit webapp
docker commit -m "Configured" webapp
docker tag 0bc42f7ff218 webapp:1.0
使用 docker tag
能够为未命名的镜像指定镜像名,也能够对已有的镜像创建一个新的命名。
docker tag
,旧的镜像依然会存在于镜像列表中。docker tag webapp:1.0 webapp:latest
还可以直接在 docker commit
命令里指定新的镜像名,这种方式在使用容器提交时会更加方便。
docker commit -m "Upgrade" webapp webapp:2.0
docker save
命令可以将镜像输出,提供了一种让我们保存镜像到 Docker 外部的方式。
docker save
命令会将镜像内容放入输出流中,这就需要我们使用管道进行接收docker save webapp:1.0 > webapp-1.0.tar
-o
选项,用来指定输出文件,使用这个选项可以让命令更具有统一性。docker save -o ./webapp-1.0.tar webapp:1.0
导入镜像的方式也很简单,使用与 docker save
相对的 docker load
命令即可。
docker load < webapp-1.0.tar
-i
选项指定输入文件。docker load -i webapp-1.0.tar
docker images
看到它了,导入的镜像会延用原有的镜像名称通过 docker save
和 docker load
命令我们还能够批量迁移镜像,只要我们在 docker save
中传入多个镜像名作为参数,它就能够将这些镜像都打成一个包,便于我们一次性迁移多个镜像。
docker save -o ./images.tar webapp:1.0 nginx:1.12 mysql:5.7
使用 docker export
命令我们可以直接导出容器
docker commit
与 docker save
的结合体。docker export -o ./webapp.tar webapp
使用 docker export
导出的容器包,使用 docker import
导入。
docker import
并非直接将容器导入,而是将容器运行时的内容以镜像的形式导入。docker import
的参数里,我们可以给这个镜像命名。docker import ./webapp.tar webapp:1.0
docker export
的应用场景主要用来制作基础镜像,比如你从一个ubuntu镜像启动一个容器,然后安装一些软件和进行一些设置后,使用docker export保存为一个基础镜像。然后,把这个镜像分发给其他人使用,比如作为基础的开发环境。
docker save
和 docker export
的区别:
通常来说,我们不会从零开始搭建一个镜像,而是会选择一个已经存在的镜像作为我们新镜像的基础,这种方式能够大幅减少我们的时间。
\n通过 FROM
指令指定一个基础镜像,接下来所有的指令都是基于这个镜像所展开的。
FROM 指令支持三种形式:
\nFROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]
Dockerfile 中的第一条指令必须是 FROM 指令,因为没有了基础镜像,一切构建过程都无法开展。
\n在 RUN 指令之后,我们直接拼接上需要执行的命令,在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化。
\nRUN <command>
RUN ["executable", "param1", "param2"]
基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为 1 的进程。而这个命令的定义,就是通过 Dockerfile 中的 ENTRYPOINT 和 CMD 实现的。
\nENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
当 ENTRYPOINT 与 CMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。
\n通过 EXPOSE 指令可以为镜像指定要暴露的端口。
\nEXPOSE <port> [<port>/<protocol>...]
当我们通过 EXPOSE 指令配置了镜像的端口暴露定义,那么基于这个镜像所创建的容器,在被其他容器通过 --link
选项连接时,就能够直接允许来自其他容器对这些端口的访问了。
在 Dockerfile 里,提供了 VOLUME 指令来定义基于此镜像的容器所自动建立的数据卷。
\nVOLUME ["/data"]
COPY [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
COPY 与 ADD 指令的定义方式完全一样,需要注意的仅是当我们的目录中存在空格时,可以使用后两种格式避免空格产生歧义
\nADD 能够支持使用网络端的 URL 地址作为 src 源,并且在源文件被识别为压缩包时,自动进行解压。
\n构建镜像的命令为 docker build。
\ndocker build ./webapp
在默认情况下,docker build 也会从这个目录下寻找名为 Dockerfile 的文件,将它作为 Dockerfile 内容的来源。如果我们的 Dockerfile 文件路径不在这个目录下,或者有另外的文件名,我们可以通过 -f 选项单独给出 Dockerfile 文件的路径。
\ndocker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp
构建中使用变量
在 Dockerfile 里,我们可以用 ARG 指令来建立一个参数变量,我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它。
FROM debian:stretch-slim |
我们可以在构建时通过 docker build 的 –build-arg 选项来设置参数变量
\ndocker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat
环境变量也是用来定义参数的东西,与 ARG 指令相类似,环境变量的定义是通过 ENV 这个指令来完成的。
\nFROM debian:stretch-slim |
环境变量与参数变量的区别:
\n-e
或是 --env
选项,可以对环境变量的值进行修改或定义新的环境变量。docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
看似连续的镜像构建过程,其实是由多个小段组成。
\nDocker 判断镜像层与之前的镜像间不存在变化的两个维度:
\n我们在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。
\n另外一些时候,我们可能不希望 Docker 在构建镜像时使用构建缓存,这时我们可以通过 –no-cache 选项来禁用它。
\ndocker build --no-cache ./webapp
两个指令的区别在于,ENTRYPOINT 指令的优先级高于 CMD 指令。
\nENTRYPOINT 和 CMD 设计的目的不同:
\n容器启动时覆盖启动命令也只是覆盖 CMD 中定义的内容,不会影响 ENTRYPOINT 中的内容。
\n使用脚本文件来作为 ENTRYPOINT 的内容是常见的做法,因为对容器运行初始化的命令相对较多,全部直接放置在 ENTRYPOINT 后会特别复杂:
\n## ...... |
Redis 中的 ENTRYPOINT 脚本:
\n#!/bin/sh |
在很多镜像的 ENTRYPOINT 脚本里,我们都会看到 exec "$@"
命令,其作用其实很简单,就是运行一个程序,而运行命令就是 ENTRYPOINT 脚本的参数。
另外一篇 Dockerfile 最佳实践的文章:https://www.practicemp.com/2018/10/docker-best-practices-for-writing-dockerfiles.html
\n对于一些复杂的应用,除了版本外,还存在很多的变量,镜像的维护者们也喜欢将这些变量一同组合到镜像的 Tag 里,所以我们在使用镜像前,一定要先了解不同 Tag 对应的不同内容。
\n\n镜像标签中的 Alpine 指的是这个镜像内的文件系统内容,是基于 Alpine Linux 这个操作系统的。
\nAlpine 镜像的缺点就在于它实在过于精简
\n如果说 Dockerfile 是将容器内运行环境的搭建固化下来,那么 Docker Compose 我们就可以理解为将多个容器运行的方式和配置固化下来。
\n最常使用的 Docker Compose 命令就是 docker-compose up
和 docker-compose down
了。
docker-compose up
命令类似于 Docker Engine 中的 docker run,它会根据 docker-compose.yml
中配置的内容,创建所有的容器、网络、数据卷等等内容,并将它们启动。
docker-compose up -d
docker run
一样,默认情况下 docker-compose up
会在“前台”运行,我们可以用 -d
选项使其“后台”运行。docker-compose
命令默认会识别当前控制台所在目录内的 docker-compose.yml
文件,而会以这个目录的名字作为组装的应用项目的名称。-p
选项来定义项目名。docker-compose -f ./compose/docker-compose.yml -p myapp up -d
与 docker-compose up
相反,docker-compose down
命令用于停止所有的容器,并将它们删除,同时消除网络等配置内容
docker-compose down
在 Docker Compose 里,可以通过两种方式为服务指定所采用的镜像。
\n在配置文件里,我们还能用 Map 的形式来定义 build,在这种格式下,我们能够指定更多的镜像构建参数,例如 Dockerfile 的文件名,构建参数等等:
\n## ...... |
如果我们的服务间有非常强的依赖关系,就必须告知 Docker Compose 容器的先后启动顺序。
\n使用 volumes 配置可以像 docker CLI
里的 -v 选项一样来指定外部挂载和数据卷挂载。
在使用外部文件挂载的时候,我们可以直接指定相对目录进行挂载,这里的相对目录是指相对于 docker-compose.yml
文件的目录。
docker-compose.yml
和所有相关的挂载文件放置到同一个文件夹下,形成一个完整的项目文件夹。如果我们要在项目中使用数据卷来存放特殊的数据,我们也可以让 Docker Compose 自动完成对数据卷的创建,而不需要我们单独进行操作。
\n在上面的例子里,独立于 services 的 volumes
配置就是用来声明数据卷的。定义数据卷最简单的方式仅需要提供数据卷的名称。
如果我们想把属于 Docker Compose 项目以外的数据卷引入进来直接使用,我们可以将数据卷定义为外部引入,通过 external 这个配置就能完成这个定义。
\n## ...... |
在加入 external 定义后,Docker Compose 在创建项目时不会直接创建数据卷,而是优先从 Docker Engine 中已有的数据卷里寻找并直接采用。
\n在 Docker Compose 里,我们可以为整个应用系统设置一个或多个网络。
\n声明网络的配置同样独立于 services 存在,是位于根配置下的 networks 配置。
\n除了简单的声明网络名称,让 Docker Compose 自动按默认形式完成网络配置外,我们还可以显式的指定网络的参数。
\nnetworks: |
在这里,我们为网络定义了网络驱动的类型,并指定了子网的网段。
\n网络别名的定义方式很简单,这里需要将之前简单的网络 List 定义结构修改成 Map 结构,以便在网络中加入更多的定义。
\n## ...... |
在进行这样的配置后,便可以使用这里所设置的网络别名对其他容器进行访问了。
\nports 配置项,是用来定义端口映射的。可以利用它进行宿主机与容器端口的映射,这个配置与 docker CLI 中 -p 选项的使用方法是近似的。
\n需要注意的是,由于 YAML 格式对 xx:yy
这种格式的解析有特殊性,在设置小于 60 的值时,会被当成时间而不是字符串来处理,所以我们最好使用引号将端口映射的定义包裹起来,避免歧义。
"8080:8080"
restart
配置主要是用来控制容器的重启策略的。
restart 选项:
\n配置值 | \n说明 | \n
---|---|
no | \n不设重启机制 | \n
always | \n总是重启 | \n
on-failure | \n在异常退出时重启 | \n
unless-stopped | \n除非由停止命令结束,其他情况都重启 | \n
Overlay Network 能够跨越物理主机的限制,让多个处于不同 Docker daemon 实例中的容器连接到同一个网络,并且让这些容器感觉这个网络与其他类型的网络没有区别。
\nDocker Swarm 是 Docker 内置的集群工具,它能够帮助我们更轻松地将服务部署到 Docker daemon 的集群之中。
\n\n在真实的服务部署里,我们通常是使用 Docker Compose 来定义集群,而通过 Docker Swarm 来部署集群。
\n我们在任意一个 Docker 实例上都可以通过 docker swarm init
来初始化集群。
$ docker swarm init |
在集群初始化后,这个 Docker 实例就自动成为了集群的管理节点,而其他 Docker 实例可以通过运行这里所打印的 docker swarm join
命令来加入集群。
加入到集群的节点默认为普通节点,如果要以管理节点的身份加入到集群中
\ndocker swarm join-token
命令来获得管理节点的加入命令。$ docker swarm join-token manager |
通过 docker network create
命令来建立 Overlay 网络。
docker network create --driver overlay --attachable mesh
--attachable
选项以便不同机器上的 Docker 容器能够正常使用到它。docker network ls
查看一下其下的网络列表。将网络的 external 属性设置为 true
,就可以让 Docker Compose 将其建立的容器都连接到这个不属于 Docker Compose 的项目上了。
networks: |
我们常用下列几种方式来获得程序的配置文件:
\nMySQL 文档中关于配置文件的参考:
https://dev.mysql.com/doc/refman/5.7/en/server-options.html
大部分软件,特别是开源软件都会直接给出一份示例配置文件作为参考。 我们可以直接拿到这份配置,达到我们的目的。
\n大多数 Docker 镜像为了实现自身能够直接启动为容器并马上提供服务,会把默认配置直接打包到镜像中,以便让程序能够直接读取。
\n以 Tomcat 为例,说说如何从 Tomcat 镜像里拿到配置文件:
\ndocker run --rm -d --name temp-tomcat tomcat:8.5
server.xml
和 web.xml
这两个文件,所以接下来我们就把这两个文件从容器中复制到宿主机里。docker cp temp-tomcat:/usr/local/tomcat/conf/server.xml ./server.xml
docker cp temp-tomcat:/usr/local/tomcat/conf/web.xml ./web.xml
docker stop temp-tomcat
--rm
选项,所以我们在这里只需要使用 docker stop
停止容器,就可以在停止容器的同时直接删除容器,实现直接清理的目的。docker run -d -p 5001:5000 --rm --name xxx jiapan/some-image:label |
docker build -t jiapan/some-image:label . |
docker push jiapan/some-image:label |
docker exec -it <container_name> /bin/bash |
在 Dockerfile
内添加:
RUN echo "Asia/Shanghai" > /etc/timezone |
COPY
和 ADD
都是 Dockerfile
中的指令,有着类似的作用。它们允许我们将文件从特定位置复制到 Docker 镜像中。
COPY
指令从 <src>
复制新的文件或目录,并将它们添加到 Docker 容器文件系统的 <dest>
的路径下。
COPY
有两种格式:
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
(包含空格的路径使用这种格式)ADD
有两种格式:
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
(包含空格的路径使用这种格式)通过 URL 进行复制的效率通常很低,最佳实践是使用其他策略来包含所需的远程文件。
\nCOPY
只支持基础的复制:将本地文件复制到容器中。
而 ADD 有一些额外的功能 :
\n<src>
参数。当遇到 URL 时候,可以通过 URL 下载文件并且复制到 <dest>
。<src>
参数是一个可识别压缩格式(tar, gzip, bzip2, …)的本地文件(注:无法实现同时下载并解压),就会被解压到指定容器文件系统的路径 <dest>
下。因此,ADD
的最佳用途是将本地压缩包文件自动提取到镜像中:
ADD code.tar.gz /app/ |
由于镜像的体积很重要,所以强烈建议不要使用 ADD
从远程 URL 获取文件,我们应该使用 curl
或 wget
来代替。这样我们可以在解压后删除这些不再需要的文件,同时还也可以避免在镜像中生成额外的层。
我们应该避免以下操作:
\nADD http://example.com/big.tar.xz /usr/src/things/ |
这个压缩包解压后,rm
命令处于独立的镜像层。
我们可以这样做:
\nRUN mkdir -p /usr/src/things \\ |
curl
会下载这个压缩包并通过管道传给 tar
命令进行解压,这样也就不会在文件系统中留下这个压缩文件了。
对于不需要自动解压的文件或目录,应该始终使用 COPY
。
最后,认准一个原则:总是使用 COPY
(除非我们明确需要 ADD
)。
今年年初我确诊了桥本甲状腺炎,其实好几年前在体检时就发现了甲状腺有问题,但一直都没有去医院复查过,也错过了最佳干预时间。
\n我是去的家附近的一家三甲医院,前几次都是找的同一个医生,刚开始她一直给我开的甲功七项检查单,大概检查几次抽了几次血后,她确诊我是甲状腺功能减退症,开始让我服用优甲乐,从半片开始吃。一周后复查,还是查的甲功七项,优甲乐计量改为一片,之后又找这个医生查了一次,依旧开甲功七项,药的计量加到了一片半,并且我基本每次找这个医生的时候都会问,有没有什么注意事项,这个医生说没有,每次开的单子上,描述处也都是写的甲状腺功能减退,从来没用过桥本甲状腺炎这个词。中间我有一次好奇,就问医生桥本甲状腺炎和我的病是什么关系,医生说是一个意思。
\n后来有一次我去医院,刚好之前一直看的医生不在,我找了另一个医生,她给我开的化验单是甲功三项,而且跟我说了很多注意事项,比如不能吃海带、饮食清淡。临走时我问她坐诊时间是哪几天,她说不用必须找她,其他医生也可以,但之前那个医生跟我说了她哪天当班,让我固定找她治疗。
\n再往后我就什么时候有时间什么时候挂号复查,每次遇到的医生也不一样,我发现医生的口径各有区别,比如:
\n我也不知道为什么会有这么大差别,也许在医生看来这些小小的不同无关紧要,但是对病人来说会让他们不知所措。我不知道该听谁的,如果选择的话,我当然想听最权威的那个,但是这几个医生的经验和资历对我来说都是黑盒。
\n也许这些医生所接受的教育、培训不同,才给出了不同的回答,让我想到我之前看的读库上边介绍过的循证医学。当前医生对患者的判断大多是基于主观的,中间掺杂了自己的个人信仰。希望在未来能通过一些技术手段使循证医学得到推广。
\n从确诊桥本到现在,快半年时间了,我的优甲乐计量还在不停的调整,中间 TSH 降低到过 0.4 以下,还涨到过 20 多,药量不变的情况下,TSH 在两个极端徘徊,每过几周抽管血,优甲乐要每日、终身服用,这是我在未来要习以为常的事项。想找一些桥本的患者交流交流经验,但目前在认识的人里还没有找到同样得这个病的,只有一个同事的母亲有这个病,交流起来也不大方便,我查了一下患病率大约是 5%,也就是说 20 个人里会有一个。
\n有一个症状我能明显感觉得到了大大的改善,就是喉咙的压迫感,上周五感受尤其明显,当天下午在新的组内做了一次接近两小时的业务串讲,晚上还去参与了一个饭局,也说了很多话,没有任何不适感。治疗前我只要说话时间久一点,比如主持一场会议、做一次分享,讲一会话后就会有被锁喉的感觉,喉咙中卡住了东西,咽也咽不下去,说话声音明显变得沙哑,必须很用力才能发出声音。
\n"},{"title":"正确地做事与做正确的事","url":"/2023/doing-things-correctly-and-doing-the-right-thing/","content":"如何理解做事和做正确的事情?
\n举个例子,前两周我们做了一个业务需求,为了促进两个平台用户之间的交流,用户可以借助AI为对方生成卡通头像。
\n在开发过程中,我们考虑到可能存在隐私风险,因此产品经理向公司法务部门咨询。果不其然,法务部门告知存在风险,暂时无法上线。如果仅止于此,我们只能说法务部门在「正确地做事」。
\n接下来,法务部门和产品经理一起商讨方案,增加用户确认提醒,让用户明确授权对方可以使用自己的照片来制作卡通头像。这样一来,可以避免法律风险,这就是「做正确的事」。
\n作为业务方,我们承担着业务压力和责任。在与其他团队协作时,应该站在专业角度,告诉我如何能够做好,而不是直接告诉我不能做。例如,如果存在法律风险,你应该提供避免此类风险的方法或者如何变得合规。如果你怕这样说了有风险,这也是「正确地做事」,但一定不是「做正确的事」,因为这对公司发展没有任何好处。
\n很多大公司的员工都很痛苦,因为大部分人都在正确地做事。每个团队站在自己的角度考虑问题,而且他们的理由你还无法反驳,总要面对一堆不背业务责任的横向部门给提出的建议。
\n按照做事方式,公司里的员工可以分成两组,一组是只关心正确地完成自己任务的员工。他们的想法是,我只要在公司生存下来就好了,其他的我不关心。从人性上来讲,我可以理解他们这样的想法:只要我不犯错就行了。
\n另一组员工则注重做正确的事,只要这个事对公司有帮助就去做,不在乎自己能获得什么利益或者面临什么风险,因为大家的目标是一致的。
\n对管理者与领导者的理解,常常有不少人将其混为一谈,觉得管理者就是领导者, 领导者也就是管理者。
\n事实上,这是一种误解。管理学大师彼得·德鲁克对领导和管理做过经典区分:
\n\n\n「管理」是正确地做事,「领导」则是做正确的事。
\n
管理一个团队只需要让团队不犯错就可以了(正确地做事),如果要领导一个团队就得有目标,遇山开路,遇水搭桥(做正确的事)。
\n管理者「正确地做事」强调的是效率,领导者「做正确的事」强调的是效能。
\n效率注重做一件工作的最佳方法。
\n而效能则重视时间的最优利用,包括是否应该做某项工作。
\n「做正确的事」是更高层次的「正确地做事」。
\n"},{"title":"国内服务器访问 github 加速","url":"/2021/domestic-server-speed-github/","content":"众所周知的原因,国内访问 github 的速度非常受限,在个人电脑上还可以挂个代理之类的来提速,但是如果是在服务器上操作的话,配代理就没那么方便了。
\n前几天帮朋友在他的服务器上部署一个 github 上的开源项目,clone 的速度真的感人。
\n\n\n4k 左右的下载速度。
\n可以通过阿里提供的代理来解决,只需把 rep 地址中的 github.com
替换为 github.com.cnpmjs.org/
就可以了。
比如,之前的地址是:https://github.com/gin-gonic/gin
,替换后为:https://github.com.cnpmjs.org/gin-gonic/gin
。
效果如下:
\n\n\n虽然没有快的飞起,但是已经相当不错了。
\nP.S. 域名后边的 cnpmjs.org 这个地址是提供 npm 加速的,前端童鞋可以通过 cnpm 来实现前端构建的加速。
\n"},{"title":"编辑 docker 容器中的文件","url":"/2020/edit-docker-container-file/","content":"\n\n实际上我们并不需要也不建议直接编辑容器中的文件。Docker 容器是不可变的工作单元,用于运行单个、特定的进程。镜像应该在没有任何干预的情况下够建和运行。
\n只有在开发期间,对 Docker 容器中的文件进行编辑可能才有些用处,这让我们在无需重新够建镜像的状态下验证我们的修改是否达到了预期的效果,可以达到节省时间、提高开发效率的目的,但是在完成验证后,应该删除添加到镜像中的多于软件包,并将验证后的结果持久化到镜像中。
\n另外需要提醒的一点是,当我们在一个运行着的容器中编辑一个文件后需要确保所依赖这个文件的进程收到了文件编辑的通知并进行了配置更新,如果没有类似的通知机制,需要手动重启这些进程使修改生效。
\n本文假设你所使用的容器中没有 vi 等文本编辑工具,我们以 openjdk:11
作为演示镜像:
➜ docker run -it openjdk:11 bash |
准备 Dockerfile:
\nFROM openjdk:11 |
编译镜像:
\ndocker build -t lol . |
最后,运行带有挂载的容器:
\ndocker run --rm -it --name=lol -v $PWD/app-vol:/app lol bash |
如果本地 $PWD/app-vol
目录不存在,会被自动创建。此后在 $PWD/app-vol
下的文件操作会映射在容器的 /app
目录下。
docker run --rm -it --name=lol lol bash |
如果需要重复使用,更好的做法是写在 Dockerfile 中:
\nFROM openjdk:11 |
docker cp Lol.java lol:/app |
另一个与之类似的方法是将 docker exec 和 cat 结合使用,下边的命令同样把 Lol.java 文件复制到了正在运行的容器中:
\ndocker exec -i lol sh -c 'cat > /app/Lol.java' < Lol.java |
虽然容器中通常没有安装编辑工具,但是其他 Linux 工具,如:sed, awk, echo, cat, cut 等是具备的,可以派上用场。比如 sed 和 awk 可以编辑文件的适当位置,还可以将 echo, cat, cut 联合起来并借助强大的重定向流创建和编辑文件。正如前文所示,这些工具可以与 docker exec 命令结合使用,从而发挥更强大的威力。
\n这种方法只是为了开拓思路,并不会在实际中使用。
\n修改 Dockerfile:
\nFROM openjdk:11 |
因为我们要借助 scp
来远程进行文件编辑,所以需要安装 openssh-server
并开放其端口。
编译并运行:
\ndocker build -t lol . |
现在我们可以使用以下命令来编辑 Lol.java 文件了:
\nvim scp://root@localhost:2222//app/Lol.java |
注:在 vi 中需要先执行 :set bt=acwrite
命令再去编辑文件,相关讨论见:https://github.com/vim/vim/issues/2329
编辑完成保存并退出后,可以使用下边的命令来验证文件确实被创建和保存了:
\ndocker exec -it lol cat /app/Lol.java |
\n\n前几天把 Effective Go 这本小书读了一下,里边有些比较生疏或者实用的知识点,在此记录。
\n
包应当以小写的单个单词来命名,且不应使用下划线或驼峰记法。
\n另一个约定就是包名应为其源码目录的基本名称。在 src/pkg/encoding/base64
中的包应作为 “encoding/base64” 导入,其包名应为 base64
, 而非 encoding_base64
或 encodingBase64
。
\n\n我们的代码中给包起别名时也应该遵循这个规则,即:livedomain “gitlab.xxx.com/backend/xxx-live/proto”,不应该是 live_domain
\n
长命名并不会使其更具可读性。一份有用的说明文档通常比额外的长名更有价值。
\n\n\n避免 Java 那样的长命名
\n
若你有个名为 owner
(小写,未导出)的字段,其获取器应当名为 Owner
(大写,可导出)而非 GetOwner
。大写字母即为可导出的这种规定为区分方法和字段提供了便利。 若要提供设置器方法,SetOwner
是个不错的选择。两个命名看起来都很合理:
owner := obj.Owner() |
\n\nGo 中的 Set 方法无需以 Set 开头,只需实现一个大写开头的方法就可以了。(不过大部分常见下,变量可以直接用导出的)
\n
按照约定,只包含一个方法的接口应当以该方法的名称加上 er 后缀来命名,如 Reader、Writer、 Formatter、CloseNotifier 等。
\n\n\n自己实现接口时也尽量遵循这个规范。
\n
Go 中约定使用驼峰记法 MixedCaps 或 mixedCaps。
\n\n\n即便是常量也不例外,即:不应该写为 LIVE_USER_TABLE 而应该是 LiveUserTable。
\n
若在新行前的最后一个标记为标识符(包括 int 和 float64 这类的单词)、数值或字符串常量之类的基本字面或以下标记之一,词法分析器会使用一条简单的规则来自动插入分号,因此因此源码中基本就不用分号了。
\nbreak continue fallthrough return ++ -- ) } |
\n\n所以
\n
if a == 1 && |
\n\n可以编译通过
\n
if a == 1 |
\n\n不能编译通过,因为词法分析器会自动在
\nif a == 1
后边插入分号。
通常Go程序只在诸如 for 循环子句这样的地方使用分号,以此来将初始化器、条件及增量元素分开。如果你在一行中写多个语句,也需要用分号隔开。
\n\n\n如
\nfor i := 0; i <= 10; i++
无论如何,你都不应将一个控制结构(if、for、switch 或 select)的左大括号放在下一行。如果这样做,就会在大括号前面插入一个分号,这可能引起不需要的效果。 你应该这样写
\nif i < f() { |
Go 不再使用 do 或 while 循环,只有一个更通用的 for;switch 要更灵活一点;if 和 switch 像 for 一样可接受可选的初始化语句; 此外,还有一个包含类型选择和多路通信复用器的新控制结构:select。
\nGo 的 for 循环类似于 C,但却不尽相同。它统一了 for 和 while,不再有 do-while 了。它有三种形式,但只有一种需要分号。
\n// Like a C for |
\n\n体现出 go 的简洁,不用费心的去考虑应该用 for 还是while 或者 do while。
\n
由于 if 和 switch 可接受初始化语句, 因此用它们来设置局部变量十分常见。
\nif err := file.Chmod(0664); err != nil { |
switch 并不会自动下溯,但 case 可通过逗号分隔来列举相同的处理条件。
\nfunc shouldEscape(c byte) bool { |
\n\n不用担心因为漏写 break 而导致的bug,case 中支持多个判断条件也很实用。
\n
尽管它们在 Go 中的用法和其它类 C 语言差不多,但 break 语句可以使 switch 提前终止。不仅是 switch, 有时候也必须打破层层的循环。在 Go 中,我们只需将标签放置到循环外,然后 “蹦” 到那里即可
\nfunc main() { |
\n\n这种用法很少使用,我之前甚至不知道有这种
\nlabel break
的用法,类似于其他语言中的 goto。
switch 也可用于判断接口变量的动态类型。
\nvar t interface{} |
\n\n我们的工具库中也有这样的用法,比如将一个 interface{} 类型转为 int64类型,代码如下:
\n
func Int64(num interface{}, defaultValue ...int64) int64 { |
Go 与众不同的特性之一就是函数和方法可返回多个值。这种形式可以改善 C 中一些笨拙的习惯: 将错误值返回(例如用 -1 表示 EOF)和修改通过地址传入的实参。
\n\n\nJava 中由于也不支持多返回值,也经常将引用传入一个方法,方法执行完后根据传入引用中的数据进行后续处理,这种方法通常被称为有副作用的方法。
\n
Go 函数的返回值或结果 “形参” 可被命名,并作为常规变量使用,就像传入的形参一样。 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值; 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。
\n此名称不是强制性的,但它们能使代码更加简短清晰:它们就是文档。若我们命名了 nextInt 的结果,那么它返回的 int 就值如其意了。
\n\n\n避免在函数签名上命名返回值变量,除非无法从上下中判断返回值的含义用作文档用途,或者希望在 defer 中改变变量值
\n
Go 的 defer 语句用于预设一个函数调用(即推迟执行函数),该函数会在执行 defer 的函数返回之前立即执行。它显得非比寻常, 但却是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁互斥和关闭文件。
\n// Contents returns the file's contents as a string. |
\n\n类似于 Java 中的 finally。
\n
推迟诸如 Close 之类的函数调用有两点好处:
\n被推迟的函数按照后进先出(LIFO)的顺序执行,我们可以充分利用这个特点,即被推迟函数的实参在 defer 执行时才会被求值。 跟踪例程可针对反跟踪例程设置实参。以下例子:
\nfunc trace(s string) string { |
输出:
\nentering: b |
new
是个用来分配内存的内建函数, 但与其它语言中的同名函数不同,它不会初始化内存,只会将内存置零。 也就是说,new(T)
会为类型为 T
的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T
的值。用 Go 的术语来说,它返回一个指针, 该指针指向新分配的,类型为 T
的零值。
表达式 new(File)
和 &File{}
是等价的。
\n\n开发时更常用到的是
\n&File{}
这种形式,因为可以同时对成员进行初始化。
复合字面的字段必须按顺序全部列出。但如果以 字段: 值
对的形式明确地标出元素,初始化字段时就可以按任何顺序出现,未给出的字段值将赋予零值。
内建函数 make(T, args)
的目的不同于 new(T)
。它只用于创建切片、映射和信道,并返回类型为 T
(而非 *T
)的一个已初始化 (而非置零)的值。 出现这种用差异的原因在于,这三种类型本质上为引用数据类型,它们在使用前必须初始化。
make
只适用于映射、切片和信道且不返回指针。若要获得明确的指针, 请使用 new
分配内存。
\n\n这就是 slice, map, channel 需要使用 make 进行初始化的原因。
\n
映射可使用一般的复合字面语法进行构建,其键-值
对使用冒号分隔,因此可在初始化时很容易地构建它们。
var timeZone = map[string]int{ |
\n\nmap 可以在初始化时同时赋值,很方便。
\n
集合可实现成一个值类型为 bool 的映射。将该映射中的项置为 true 可将该值放入集合中,此后通过简单的索引操作即可判断是否存在。
\nattended := map[string]bool{ |
\n\nGo 中没有 Set,可以用这种方法代替,有些人习惯将 map 的 value 值声明为 interface 类型,我个人不是很喜欢,bool 更方便使用一些。
\n
在使用 map 时,有时你需要区分某项是不存在还是其值为零值。如对于一个值本应为零的 “UTC” 条目,也可能是由于不存在该项而得到零值。你可以使用多重赋值的形式来分辨这种情况。
\nvar seconds int |
在下面的例子中,若 tz 存在, seconds 就会被赋予适当的值,且 ok 会被置为 true; 若不存在,seconds 则会被置为零,而 ok 会被置为 false。
\nfunc offset(tz string) int { |
若仅需判断映射中是否存在某项而不关心实际的值,可使用 空白标识符 (_
)来代替该值的一般变量。
_, present := timeZone[tz] |
要删除映射中的某项,可使用内建函数 delete,它以映射及要被删除的键为实参。 即便对应的键不在该映射中,此操作也是安全的。
\ndelete(timeZone, \"PDT\") // Now on Standard Time |
当打印结构体时,改进的格式 %+v 会为结构体的每个字段添上字段名,而另一种格式 %#v 将完全按照 Go 的语法打印值。
\n常量只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制, 定义它们的表达式必须也是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 math.Sin 的函数调用在运行时才会发生。
\n在 Go 中,枚举常量使用枚举器 iota 创建。由于 iota 可为表达式的一部分,而表达式可以被隐式地重复,这样也就更容易构建复杂的值的集合了。
\ntype ByteSize float64 |
以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。
\n之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。在我们的例子中,变量 b 是可寻址的,因此我们只需通过 b.Write 来调用它的 Write 方法,编译器会将它重写为 (&b).Write。
\n\n\n通常我们会将方法写为指针接收者,这种情况下,即便是用值调用这个方法,编辑器会自动帮我们改为指针调用。
\n
并发是用可独立执行的组件构造程序的方法,而并行则是为了效率在多 CPU 上平行地进行计算。
\n\n\n并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机
\n
若调用者关心错误的完整细节,可使用类型选择或者类型断言来查看特定错误,并抽取其细节。
\nfor try := 0; try < 2; try++ { |
当 panic
被调用后(包括不明确的运行时错误,例如切片检索越界或类型断言失败),程序将立刻终止当前函数的执行,并开始回溯 Go 程的栈,运行任何被推迟的函数。 若回溯到达 Go 程栈的顶端,程序就会终止。不过我们可以用内建的 recover
函数来重新或来取回 Go 程的控制权限并使其恢复正常执行。
调用 recover
将停止回溯过程,并返回传入 panic
的实参。 由于在回溯时只有被推迟函数中的代码在运行,因此 recover
只能在被推迟的函数中才有效。
recover
的一个应用就是在服务器中终止失败的 Go 程而无需杀死其它正在执行的 Go 程。
func server(workChan <-chan *Work) { |
上个季度我在业务中做了个看似很简单的功能,却得到了非常好的收益。这个功能简单来说就是帮用户给对方打个招呼,提升两个人聊天的概率,进而提升日活等指标。
\n我们是个社交平台,但是发现很多用户在形成匹配后并没有说过话,浪费了很多的机会。之前已经在产品形态上给了用户非常便捷的方式去选择一个文案发送,但渗透率还是很低,这次我们尝试直接帮用户去发开场打招呼这条消息。
\n我们把一种特殊类型的系统消息实现成用户自己发送消息的样式,只在男性侧展示,男性会认为是女性主动给她了条消息。文案上我们只使用常见且无意义的打招呼文案,不容易被对方察觉,比如:hello、你好、hi、嗨,等等。
\n我们通过一些特征匹配到受众的男性(并不是所有用户都适用),这些特征也是我们不断摸索出来的。再为这个男性找一个最适合他的女性,通过上边说的那条消息引导男性活跃、主动起来,我们帮用户迈出第一步,当第一层窗户纸捅破后用户大概率可以继续聊起来。
\n当然这中间我们还打磨了大量细节,比如负反馈、冷却期等,这里不再详述。上线经过几轮实验迭代后得到了非常好的收益,日活、次留、七留等指标都有超大幅度提升。
\n这个功能并不复杂,实现起来也比较简单,却取得了巨大的成功,主要还是巧妙利用了人性,找到了非常好的触发点。
\n这个功能最终被下线了,原因是其他业务线产品发现这个套路后,过度使用这种方法来拉动短期增长,以达成 Q2指标,求快过程中没有经过多次实验迭代直接全量,产生了大量客诉,给生态造成了影响,最后使用相关特性的功能全部停止。虽然最终下线了,但整个过程让我产生很多思考,而且我也很荣幸引领了一股产品决策的潮流。
\n最有效的方法往往没那么复杂,生活也一样,可以很简单,你只需要不假思索地做正确的事:
\n现在是2023年08月12日早上4点43分,我的眼睛瞪得像铜铃。周六计划了好几件事情看起来都要泡汤了,因为涉及做较大额度钱方面的决策,在前一晚严重缺乏睡眠的状态下无法进行合理决策。
\n前几天听了一档播客节目事,播主安利了一个酒吧,被种了草,查了一下刚好在家和公司中间。昨天周五,下班后微信里摇了个同事就来了。
\n\n价格不算便宜,但想着来都来了,况且酒的味道还不错,伴随着小吃就又多喝了一杯。
\n小吃1,拼盘:
\n\n小吃2,taco:
\n\n第一杯的名字叫「朕的糖葫芦」,是一款山楂口味的啤酒:
\n\n第二杯叫「双城记」,苦度很高,对于喜欢喝美式的我来说很对口:
\n\n点第二杯的时候为了下酒又加了份毛豆,辣辣的很下酒。结果天空不作美下起了大雨,等了好久都没有转小雨的迹象,这种天气也打不上车,就冒雨走到地铁站乘地铁回家,到家已经11点半。
\n洗漱完过了0点,舍不得一天就这么过去、加上明天又到了周末,导致刷手机刷到了1点,结果躺下后就没有了困意,翻来覆去到三点多,又起来看了半小时书,之后吃了片安眠药躺到4点半还是无法入眠,就又起来开始写这篇文章。
\n实际上我自己是有酒精过敏的,每次喝完酒都会全身发红。而且我也明知酒精是一级致癌物,但每过一段时间还会想喝点,抱着小喝怡情的侥幸心理。昨晚喝的酒后劲还非常大,我在乘地铁回来的路上,如果再多坐一站可能就吐了。
\n每次喝多了都会难受,每次自己都会告诫自己以后不要再喝酒。同样晚睡也是,每次超过晚上11点半后就很难入睡,每次失眠都会告诫晚上早些上床做睡前准备,睡前远离手机。虽然一段时间内会有效,但自己是好了伤疤忘了疼。
\n每个人都有适合自己的作息方式,按照睡眠类型来分有两种:
\n我无疑是云雀星,白天也没有午睡习惯。但我很羡慕周五晚上去嗨,然后利用周末补觉的那些人,我自己尝试了多次后发现真的不适合自己,毕竟这些东西都是基因里已经决定了的。
\n我在昨天属实算「纵欲」了,因为最近一段时间睡眠质量不错,就放松了警惕,白天喝了两杯咖啡,上午一杯、下午一杯,晚上还喝了度数较高的啤酒,为了配酒还吃了高热量实物,深夜又刷了很久手机。
\n有必要给自己约法三章了,虽然之前也已经约法过,但希望这次是最后一次…
\n再补充一个让我有点难过的事情,凌晨4点多我决定不再尝试入睡,起来写这篇文章前,看了眼手机才注意到微信里有一条昨天晚上9点40多的语音消息,念念和我说她要一个人睡了,还给我发了照片。我当时在喝酒,没有看到也没有回复她,她当时一定很期待我的回复吧。
\n"},{"title":"阅读 Eureka 的 StringCache 源码 get 到的知识","url":"/2018/eureka-StringCache-get-new-knowledge/","content":"今天在读 Eureka 源码时,看到了它里边实现了一个工具类 StringCache
阅读后我产生了几个疑问,查阅资料后一一进行了解决,受益良多并以此文进行记录。
StringCache
实现了一个字符串缓存,代码如下
public class StringCache { |
String s = "a" + "bc"; |
上边这段程序会打印 true
(尽管我们没有使用正确比较字符串的 equals
方法)
当编译器优化字符串的字面值时,它看到 s
和 t
有相同的值,因为字符串在 Java 中是不可变的,所以提供同一个字符串对象也是安全的,因此 s
和 t
指向了同一个对象并且节省了一丢丢的内存。
「字符串常量池」的灵感来源于这样的想法:所有已定义的字符串都存储在一个「池子」中,在创建新的 String
对象前,编译器需要检查这个字符串是否已经被定义,若已经在「池子」中存在就直接拿出来用。
也就是说 Java 编译器已经用字符串常量池实现了字符串缓存的特性,在我们直接使用双引号来声明 String
对象时会自动利用以上特性,如果不是用双引号声明的,可以用 String
提供的 intern()
方法。intern()
方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
示例程序:
\nString a1 = "aaa"; |
既然 Java 编译器已经对相同的字符串进行了优化,为什么 Eureka 还要再造一个轮子呢,因为字符串常量池在存储大量的字符串后,会出现严重的性能问题。
\n以下解释来自美团点评技术团队编写的 深入解析String#intern 一文:
\n\n\nJava 使用 JNI 调用 C++ 实现的 StringTable 的 intern 方法,StringTable 的 intern 方法跟 Java 中的 HashMap 的实现是差不多的,只是不能自动扩容。默认大小是1009。
\n
\n\n要注意的是,String 的 String Pool 是一个固定大小的 Hashtable,默认值大小长度是1009,如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降(因为要一个一个找)。
\n
好了原因解释清楚了,我们来看一下具体实现中有那哪些问题。
\nStringCache
的代码不难理解,大致就是声明一个锁和一个 Map
,取值时先获取锁,如果存在就直接从 Map
中 get
出来然后返回,不存在就 put
进去作为缓存以便下次使用。
这里声明的 Map
类型是 WeakHashMap
,这种 Map
的特点是,当除了自身有对 key
的引用外,此 key
没有其他引用那么这个 map
会自动丢弃此值。
示例程序:
\nString a = new String("a"); |
我们声明了两个 Map
对象,一个是 HashMap
,一个是 WeakHashMap
,同时向两个 map
中放入 a
、b
两个对象,从 HashMap
中 remove
掉 a
并且将 a
、b
都指向 null
时,WeakHashMap
中的 a
将自动被回收掉。出现这个状况的原因是,对于 a
对象而言,当从 HashMap
中 remove
掉 a
并且将 a
指向 null
后,除了 WeakHashMap
中还保存 a
外已经没有指向 a
的指针了,所以 WeakHashMap
会自动舍弃掉 a
,而对于 b
对象虽然指向了null
,但 HashMap
中还有指向 b
的指针,所以 WeakHashMap
将会保留 b
。
以上程序得到的结果是:
\nmap: b:bbb |
可以看到我们的 StringCache
中的 Map
值类型用的是 WeakReference<String>
,如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference。
如果不这样用,会导致我们 Map
的值也会引用我们想缓存的字符串,这就导致即使 key
已经没有任何地方引用了,这个 WeakHashMap
也不会丢弃此值。
ReentrantReadWriteLock
是一个读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。
在我们现在的架构中,一切都是由 Eureka 开始的,因为业务中的配置中心地址是使用 service-id
的形式,从注册中心来自动发现配置中心地址。
这就出现了一个矛盾,如果我们想将 Eureka Server 的配置外置的话就不太可行了,因为 Eureka 启动前,配置中心还没有注册进来,所以它也无法发现配置中心。
\n现在我们项目中的做法是:Eureka Server 的所有配置文件都是写在自己的 application.yml
中。
今天我想到一个思路,并验证了其可行性:
\n可以先将 Config Server 启动起来,因为 Config Server 需要注册到 Eureka Server 上,但是注册失败并不会导致服务的终止,只是在发心跳包时会有一些错误信息。也就是说,即便不注册上去,配置中心也是可以通过 IP+端口号
的形式来访问的。
Config Server 的 application.yml
如下:
management: |
然后我们在 Git 中新建 eureka 仓库,并创建 eureka-dev.yml
文件,其内容如下:
server: |
然后在 Eureka Server 项目中新建 bootstrap.yml
文件,内容如下:
spring: |
这里我们使用 IP+端口号
的形式来访问配置中心,然后将之前的 application.yml
配置文件删除。
现在我们来分别启动这两个项目,注意启动顺序:先启动 Config Server,再启动 Eureka Server,启动完 Config Server 后会看到一些错误,暂时不用理会,启动 Eureka Server 时我们可以在控制台中看到可以正常拉取配置,如图:
\n\n待 Eureka Server 启动完后,再回来看 Config Server 的控制台,已经不报错了,到 http://localhost:7011/
看到,配置中心也成功注册上来,这也说明我们的 Eureka Server 已经成功读到了 配置中心 提供的配置文件。
以上验证了这种思路是可行的,回头可以将线上环境也修改为这种方式。
\n"},{"title":"关于事件循环的 15 个问题","url":"/2021/event-loop-questions/","content":"不可能。
\n一个事件循环中的所有代码都在一个操作系统线程中运行,所以任何时刻只能有一段代码在运行。
\n是的。
\n例如,在 node.js
中,所有的 Javascript 代码都在一个线程中运行,但还有其他工作线程来处理网络请求和其他 I/O。
不是。
\n例如,用Python的 asyncio
,做调度的代码是一个 Python 程序。
for
循环或 while
循环?)是的。
\n通常事件循环都是以while
循环的形式实现的,它看起来像这样。
while True: |
(以上是 Python 的 asyncio
事件循环的实际代码)
通过队列。
\n当函数准备好运行时,它们会被推到队列中,然后事件循环按顺序执行队列中的函数。
\n是的。
\n当网络请求或其他 I/O 完成后,或者用户点击了某些东西,或者因为该函数计划在那时运行等,函数可能会被推入事件循环的队列中。
\n不一样。
\n异步函数特殊之处在于,它们可以被“暂停”并在稍后的事件循环中重新启动。
\n例如,在下边这段 Javascript 代码中
\nasync function panda() { |
事件循环调度 elephant()
,暂停 panda
,并在 elephant()
运行完毕后调度 panda()
重新启动。普通的非 async
函数不能像这样暂停和重启。这些可以暂停和重启的异步函数的另一个名字是协程。
setTimeout
),它能保证在那个时间运行吗?不能。
\n事件循环会尽力而为,但有时会延迟。
\n是的。
\n虽然它们的语法不同,但它们是安排代码稍后运行的不同方式。
\nx = 3; |
不能。
\n你需要显式让步给事件循环,让它运行一个不同的函数,例如使用 await
\nwhile(true) { |
不会。
\n通常,你可以通过运行一些 CPU 密集型操作来长时间阻塞事件循环。
\n不能。
\n如果你的事件循环的 CPU 总是很忙,那么新进来的事件就不会得到及时处理。
\n是的。
\n至少在 node.js
和浏览器中是这样的,Javascript 代码总是有一个事件循环运行。
没有。
\n在不同的编程语言中,有很多不同的事件循环实现。
\n可以。
\n大多数编程语言并没有像 Javascript 那样的“一切都在事件循环中运行”的模式,但许多语言都有事件循环库。而且从理论上讲,即使还没有自己语言的事件循环库,你也可以编写一个。
\n"},{"title":"事件溯源与事件驱动架构的区别","url":"/2022/event-sourcing-vs-event-driven-architectures/","content":"事件驱动架构:Event Driven Architecture,简称 EDA
事件溯源:Event Sourcing,简称 ES
我之前对「事件驱动架构」和「事件溯源」这两个概念的理解是比较模糊的,所以查了下资料,结论是「事件驱动架构」和「事件溯源」没有太多的可对比性。为了说明这两个概念的区别,后文我会从目的、范围、数据存储、可测试性这几方面分别对「事件驱动架构」和「事件溯源」做下介绍。
\n事件溯源指的是将应用状态的所有变化存储为一连串事件的系统。一个常见的例子是支持事务的数据库系统,它将所有状态变化存储在事务日志中。
\n在事件溯源中,术语「事件」指的不仅仅是「通知」,更多指的是「状态变化」。事件溯源使用只追加存储来记录对数据采取的完整系列操作,而不是仅存储域中数据的当前状态。因此,所有的历史操作都会被保留。
\n事件驱动架构这一术语可用于任何类型的软件系统,它基于仅通过事件进行通信的组件。事件驱动架构是一种松耦合、分布式的驱动架构,收集到某应用产生的事件后实时对事件采取必要的处理后路由至下游系统,无需等待系统响应。
\n在事件驱动架构中,一个事件可以被定义为「状态的重大变化」。在事件驱动架构的背景下,术语「事件」通常意味着「通知」。
\n\n | 事件溯源模式 | \n事件驱动架构模式 | \n
---|---|---|
目的 | \n保留历史 | \n提高适应性和扩展性 | \n
范围 | \n单一系统或应用 | \n多个系统或应用 | \n
存储 | \n中央事件仓库 | \n分布式存储 | \n
测试 | \n更简单 | \n更困难 | \n
两者都以事件为基础,但其目的、范围和属性却截然不同。
\n"},{"title":"每个男生心中都有自己的女神","url":"/2023/every-boy-has-his-own-goddess/","content":"我在想一个问题,是不是每个男生心中都有自己的女神?比如刘亦菲、林志玲、新结恒衣,我的女神有点特殊。
\n初中时和我一个班的有个L姓女生,因为长得好看性格又好非常受欢迎,那个时候她有非常多的追求者。她和班里一个当时身高已经超过一米九的韩国籍男生交往过(我当时上的是一个国际学校,有很多韩国交换生),和体育委员交往过,还和其他班的男生交往过,这几个仅是我知道的,不知道的可能更多。
\n我和她是同一个县的,每周五会一起坐大巴车回老家,我也鼓起过勇气约她来我家一起写暑假作业,那时候真的只是写作业而已。我知道自己几斤几两,也听到过她在私下里对我的评价,知道自己不可能,而且看到她身边整天有那么多人围绕,很是羡慕,甚至有些自卑,所以不敢有任何逾矩的想法。
\n好巧不巧,我们两个高中又去了一个学校,但这次没有在一个班里。她凭借着自己的优势又成了学校里的小红人,我们班也有好几个仰慕者。其中有一个W姓的男生看她戴了红框眼镜,在还不知道她名字的情况下就用了小红这个昵称来称呼她,当这个W姓的男生知道我和她是老乡后羡慕不已,整天问我很多问题,比如:你说小红有男朋友了吗?小红喜欢什么样的男生?我作为他的可靠线人乐此不疲的和他一起探讨。
\n高中时她偶尔遇到糟心事的时候会和我这个不可能的备胎倾诉,可能因为我那时候没什么经历,也不能给她出什么好建议,给她出主意的人很多,能静静听她讲的没几个,她就把我当成了一个特别好的倾诉对象。
\n高考时她通过艺考去了湖南的一所大学,我留在了河北,她的大学生活非常丰富,我就通过她的朋友圈又看了她四年,我也会有一搭没一搭的在微信问候一下她。那时候流行微博,我还在微博上偷偷关注了她。她和我说她想用Instagram,我就指导她一步步进行科学上网的配置,后来也顺理成章关注了她的Instagram账号,她Ins上的很多照片是没有发在朋友圈和微博的,我就觉得自己发现了她的秘密基地,有些窃喜。
\n一转眼又4年过去了,大学毕业前我在石家庄实习,本来是打算留在石家庄了,可看同学们都来了北京有些心痒痒。毕业第二天给公司领导提了离职,同一天收到了北京的一个面试通知,我在公司楼道里和对方聊了几句,对方问了我一些问题就给我发了offer,如果是现在这么卷的环境我肯定连一个offer也拿不到。
\n等我到北京开始上班后,看到L回石家庄了,准备在石家庄创业之类的,而且看起来是单身状态。我有些后悔来北京,幻想如果没有来我是不是也许会有什么机会?但既然已经来了就好好在北京发展吧,我们继续有一搭没一搭偶尔互相发个消息。
\n又过了半年她可能在石家庄不太顺利,也来了北京,在北京找了份工作,没多久在北京认识了新的男朋友,又没多久和男朋友吵架对方把他赶出去,她当时不敢和家里说,也没钱在外边住,就找我借了几千块钱,我毫不犹豫借给了她,这笔钱过了好久才还回来。
\n我刚来北京不久有段创业经历,是做一个类似探探的产品,我邀请她来我们APP注册发照片。每天通过后台数据看到她被很多人点赞我内心里替她高兴。
\n再后来我结婚了,作为同学、老乡的身份会继续每隔几个月问问她怎么样,不知道是不是巧合,有好几次我问她的时候都碰巧她遇上困难,和我聊聊她的遭遇。
\n实际上我们从高中毕业后就再也没见过面,之后的所有交流都是在微信上,她有时也会突然来找,甚至还和我说她梦到了我。
\n\n现在她的生活依然丰富多彩,全国各处旅游打卡吃美食,而且是个滑雪手、摩托车手。工作也是换了一份又一份,很早之前我问时是在做婚礼策划,过一段时间再问时准备开个精酿小酒馆,她就像一个神,让人捉摸不定,我是泯然众生中的一个守望者。
\n2021年她在朋友圈晒了结婚证,巧的是那个男生也姓贾。去年他们举办了婚礼,她穿婚纱真好看。
\n"},{"title":"exa 和 zsh-syntax-highlighting","url":"/2017/exa-and-zsh-syntax-highlighting/","content":"今天再来记录两个命令行神器。
\n第一个是 exa: https://the.exa.website/
\n官方的介绍为
\n\n\nexa is a modern replacement for ls.
\n
顾名思义 exa
是一个用来替代 ls
的工具,官方介绍了很多关于 exa
的特性,对于我来说,使用它的原因是可以支持不同文件类型可以用不同颜色来展示这个特性。至于官方还提到,exa
比 ls
要更快一些,这我倒是没有什么感觉。
在 Mac 下直接用 Homebrew
安装就行了: brew install exa
,为了方便使用,我直接修改 alias 为 ls
,这样之后再使用 ls
命令时,系统就自动用 exa
来代替了,毫无学习成本。
来看下效果:
\n\n我这个目录下不同类型的文件不多,没有展示出特别好的效果
\n第二个神器是 zsh-syntax-highlighting,看名字就知道,它是一个在 zsh 下使用的工具,官方介绍为:
\n\n\nFish shell-like like syntax highlighting for Zsh.
\n
zsh-syntax-highlighting
是用来在命令行中提供语法高亮的工具 (很抱歉我没有用过 Fish)。
效果图:
\n\n安装方法:
\nbrew install zsh-syntax-highlighting
然后将
\nsource /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
加入到 .zshrc
文件中即可。
最后再来说下我现在用的 iTerm2 配色,之前一直用的都是自己配的,会遇到文字和背景色不太搭的情况,比如 Date 和 Modified 那两列:
\n\n所以最近换用了: https://github.com/dracula/iterm 这个配色方案。看起来挺舒服的,所以就不再自己折腾了。
\n\n"},{"title":"Idea 启动 SpringBoot 加速","url":"/2019/fast-idea-in-mac/","content":"# 查看自己的 hostname |
\n\n我之前在一年多的时间里,在很少运动的情况下,减掉了 50 多斤的体重(从 90 多公斤到目前稳定在 65 公斤内),同时内脏脂肪也降了下来(我的体脂称告诉我的),今年体检后各项指标也都正常,所以对本文谈到的东西也算有话语权。
\n
\n\n本文不会给出任何具体的减肥方法,只是表达我的一些观点,不带有任何异端邪说请放心服用。
\n
肥胖是一种状态,是身体发炎的表现。肥胖是慢性病的主角,就像一股闷火在体内 24 小时不停的烧,烧着烧着就出了大问题。
\n以前人们提到心血管疾病都只是简单的归结为心血管阻塞,为什么阻塞,以前把罪嫁祸于胆固醇。
\n但是如果你的血管是健康的,它是没有理由被任何东西阻塞的,血管内壁是很光滑的组织,只有发炎的时候才会变得粗糙,才会粘附东西,不过粘附的也不一定是胆固醇。
\n胆固醇并没有罪,所以还是请大家多吃鸡蛋,不要害怕吃鸡蛋。
\n香烟在 2016 年被世界卫生组织认为是健康最大的杀手,但是 2016 年的下半年,世卫组织改口称肥胖才是人类最大的杀手,香烟已经退居到了第二位,所以最害怕的人其实是既肥胖又抽烟的人,这种绝对就是在自杀。
\n世界卫生组织关于肥胖的说明:
\n\n发炎时我们第一个想到的就是去吃消炎药,这是不对的。最好的方法是减肥,体重降了炎症也就降了,慢性病就会渐渐的消失。
\n结合上边的香烟有害健康,可以得出减肥、戒烟可以治百病。
\n减肥吃减肥药是件最愚蠢的事,如果尝试过吃药减肥的人,都会感到很痛苦,但凡减肥药,都会有一种叫做安非他命的成分,安非他命是一种中枢神经兴奋剂,吃完会心跳加快、没有食欲。也早就已经被证明是一种无效的手段。
\n你认为你吃了一顿很丰盛的饭,你觉得很营养吗?其实并不是,大部分都是吃了很多的热量,热量不等于营养。
\n每克蛋白质、脂肪、淀粉中的热量分别为4卡、9卡、4卡,这个叫做热量。维他命 C 是营养,可是它并没有热量。
\n拿我本人来说,自从瘦下来后对冷的敏感度就提高了,这也只是说明我的热量小了,不代表我营养不良,没什么大不了的,多穿几件衣服,睡觉的时候盖厚点就可以了。
\n肥胖的状态就是热量过剩,还可能伴随营养不良。脂肪是没有营养的,但是我们都知道它的热量也是最多的。
\n但是脂肪是个很好的东西,不要害怕它,减肥就是要燃烧脂肪。我们要做的是热量尽量少,营养尽量多。
\n现代人都知道病是吃出来的,吃出病来后又在想我要吃些什么才能把病治好?
\n答案并不是要吃什么,而是不吃什么。
\n人类吃三餐的历史并不长,其实现在还有很多地方保持一日两餐的习惯。
\n三餐是工业化后的产物,餐饮业越来越发达、便利店越来越多,让我们吃东西越来越方便,方便的后果就是吃东西吃过剩、吃太多。
\n来看一个英文单词:breakfast。
\n我们都知道它是早餐的意思,但是它是两个单词的组合,break 和 fast。
\nbreak 的意思是 打破、阻断、破坏。
\nfast 大多数人只知道它有快的意思,但实际上它是一个医学专有名词,叫做「禁食」。
\nbreakfast 这个单词已经告诉我们了,早餐是破坏禁食状态,不吃才是对的,常态应该是不吃。但是很多人就会跳起来反驳我,不吃我会饿啊。别急,听我继续说。
\n现在的上班族,大多是 8 点多吃早餐,不到 12 点又要去吃午餐,甚至有的是 10 点吃早餐,12 点又要去吃,为什么?
\n这是因为我们认为吃饭的时间到了。大家都知道时间到了,就要去吃啊。
\n这种想法是不对的,正确的应该是饿了才去吃。
\n什么叫饿?饿是可以训练的,具体如何做不在本文的讨论范围内,不然又会到具体的方法上,又会被认为是异端邪说。
\n好了,今天就聊这些,本文的意图只是给大家提供一些新的视角来思考关于健康的问题。
\n"},{"title":"乔迁第一顿饭","url":"/2023/first-meal-after-moving-into-a-new-house/","content":"今天一家人在新家吃了一顿团员饭,作为我们拥有新家后的第一次正式庆祝。不过还并没有完全搬过来,小登还太小、小念还需要在现在的幼儿园上完大班,所以在之后的很长一段时间内还是只有我一个人在这边住😂
\n\n上午和路秘书一起送小念去上陶艺课,她和我一起来的原因是想给那个安排我们进来的老师送两盒月饼,她知道这个活我这种笨嘴笨舌的人肯定完不成,而且我一点都不擅长这些。一开始我也觉得她完不成,认为老师不会轻易收家长东西的。没想到在路秘书的再四推让下,那个老师最后还是接了我们的东西,还主动和我们说下学期可以再给我们推荐一些其他课程。我非常非常佩服路秘书这种有社交牛逼症的人。
\n距下课还有一个半小时,我和路秘书压了40分钟马路,走到了一个距离上课地点最近的一个瑞幸,中间经过铁路高架桥看到一列高铁经过,路秘书跟我讲了一个当年追她的男生后来进了铁路局工作的一段故事。我们到瑞幸后我点了一杯之前没喝过的咖啡,在那里歇了20分钟,之后一人骑了一个共享单车回到了上课地点。因为平时上下班路程上的需要,我开了哈罗和滴滴两个共享单车平台的月卡,所以今天我用每个平台扫了一个,骑车就没有花钱。
\n中午回家后路秘书亲自操刀给我剪了个头发,以后又可以在剪头发的开销上省下一笔钱了。过程中我爸作为有8年理发经验的人进行了友情指导。之后去稻香村买了些熟食,我还给自己买了三块在疫情居家办公期间发现的一个好吃的糕点——山楂锅盔,强烈爱吃山楂口味的小伙伴尝一尝。买完熟食回家收拾了一些东西就来新家了,吃饭过程中还喝了两盅酒,现在还晕乎乎的。
\n小念今天带回了她的第一件陶艺作品,一只啄木马笔筒,里边插了扭扭棒做的花:
\n\n"},{"title":"修复 Docker 安装 MySQL-python 失败的问题","url":"/2019/fix-docker-install-MySQL-python/","content":"之前开发的一个 Python 项目今天在编译 Docker 镜像时无法通过了,使用的基础镜像是 python:2.7
,报错原因是在执行 pip install MySQL-python==1.2.5
时出了问题,详细报错如下:
ERROR: Command errored out with exit status 1: |
错误原因是 MariaDB 10.2、10.3 的 MySQL 版本没有定义 reconnect
,需要自己来声明,只需在 pip install
前插入如下命令即可:
RUN sed '/st_mysql_options options;/a unsigned int reconnect;' /usr/include/mysql/mysql.h -i.bkp |
非 Docker 环境执行一下 RUN 之后的命令就可以了。
\n参考:https://github.com/DefectDojo/django-DefectDojo/issues/407
\n"},{"title":"2019 Flags","url":"/2019/flag-2019/","content":"flask-bootstrap
默认走的是国外CDN,所以在天朝使用起来速度奇慢。
解决办法:
\nflask-bootstrap
的__init__.py
文件,将里边控制CDN部分的代码修改为:bootstrap = lwrap( |
只修改这两段即可。
\nflask-bootstrap
,源码我已修改为使用国内CDN:pip install git+https://github.com/Panmax/flask-bootstrap.git |
或
\ngit+https://github.com/Panmax/flask-bootstrap.git |
将此写在requirements.txt
文件中
如果你是一位程序员,一定在编程教材或网上文档的示例代码中见到过使用 foo、bar 这两个词为变量命名。如:
\nString foo = \"Hello, \"; |
或者:
\n#include <stdio.h> |
foo、bar 的来源究竟是什么呢?我尝试查了一些资料来解答这个问题。
\n对于 foobar 的来源,主要有两种解释:
\n这一派认为,foo和bar源自美国陆军二战缩写 FUBAR,“Fouled Up Beyond All Recognition”(操蛋到无法修复)。
\n\nfoo 表示电子学中反转的信号,bar 表示一个低电平有效的数字信号。
\n老一辈的程序员们很喜欢在示例代码中使用这两个词作为变量名,发展到后来甚至已经成为 C 和 UNIX 文化的一部分。
\n在 linux/lib/test_debug_virtual.c 中,使用 foo 作为结构名称,使用 bar 作为内部字段名称。:
\nstruct foo { |
在 linux/tools/testing/selftests/bpf/test_cgroup_attach.c 中将临时文件夹命名为 foo 和 bar:
\n#define FOO\t\t\"/foo\" |
这个解释虽然有些牵强,但也说的通。
\nfoo 和 bar 很容易在代码块中发现,这使得在用眼睛浏览和扫描代码时可以轻松找到和替换。
\nfoo 和bar 在代码中无任何实际含义,在教学或写文档过程中为了快速说明一个特性、操作符的使用方法,同时作者又不想大费周章的想一个恰当的变量名,就统统使用 foo、bar 来表示一些无意义的变量,久而久之这个习惯就流传了下来。
\n这两个词在这种用法中没有任何意义,仅仅表示一个变量占位符,就像代数中使用的字母 x 和 y 一样。
\n如果你在示例代码中看到 foo、bar,需要明白这个变量的名称是不重要且随意的,将重点放在后边的代码或者整体逻辑上即可。foo 和 bar 作为两个最常用的临时变量,它们实际上并没有任何词语含义,通常为了方便起见,用来代替更准确的名称。
\nfoo 和 bar 比其他临时变量更受欢迎,因为它们的受欢迎,而且它们不可理解的性质使它们很容易被精确定位。
\n也因为 foobar 这个术语非常流行,后来有一个 Windows 上的音频播放器将自己命为 foobar2000。
\n"},{"title":"为了碎银几两","url":"/2023/for-money/","content":"最近几天北京下暴雨,公司启动了远程办公模式,我之前好像在一篇文章中写到,相对来说我更喜欢到公司上班,因为去公司工作更有条理和规划,(基本上)能事先规划好每个时间段要做的事情,也能保持生物钟的稳定状态。
\n周末的时候,我整理了新家的吧台,然后在吧台旁看了一会书。
\n\n周一早上把电脑搬了过来,把吧台作为了一个可以观景的办公环境。
\n\n在家办公效率总觉得很低,主要有以下几个原因:
\n但在家办公又觉得事件过得很快,一天还没做什么事时间就过去了。因为登登的出生,加上念念幼儿园也放了暑假,家里人这段时间回老家了。家人也不在北京,工作上有没有非常紧急且具体的事情要做,晚上就很空虚。
\n躺在床上脉脉、小红书、Twitter 这三个轮番刷,刷到将近零点,放下手机后一阵巨大的失落感袭来。
\n我突然意识到,我的女儿现在已经五岁半了,明年就要上小学。想起了在我刚工作的时候有个前辈,那时候他的孩子大概也是这么大,他和我说这个时候的小孩是最好玩的,过了这个阶段再大一些就没这么好玩了。
\n我错过了念念最好玩的一段时间,前段时间她掉了第一颗乳牙,她把这颗乳牙放进一个小瓶子里,然后跟我发微信视频炫耀,我看到她的喜悦,可心里却酸酸的。
\n\n在生登登出生之前的很长一段时间,每个周末我都会抽出时间来陪念念玩一会,周日下午四点半还会带着她去上美术课,我们两个的小秘密是每次带她出门我都会买一瓶可乐和她分享,或者去 DQ 吃个冰淇淋。那段时间应该是我陪她最多的时候了,念念这么大了,我还没有带她去过远方旅行,所以打算今年国庆前后带她去一趟上海迪士尼,圆一次她的公主梦。
\n\n\n我们每个人都是小丑,一生当中就在玩这五个球:家庭、工作、健康、朋友和灵魂。五个球当中只有工作这个球是橡胶做的,砸下去还会弹起来,其他四个球是玻璃做的,砸碎了再也不会复原。
\n
背负着北京的房贷,和家人暂时分居,我经常思考是否应该继续这样的生活,不知道这种生活还要坚持多久。我总是用「熬过这段时间,孩子们大一些就好了」这样的想法来宽慰自己。
\n我也有过回老家的想法,但面临着工资和生活习惯上的落差。我还琢磨过各种副业,想通过副业增加些额外收入,也避免自己被裁员后没有经济来源,但都由于各种原因(大部分是看不到赚钱的希望或者时间投入太多)最终都放弃了。
\n为了碎银几两,为了三餐有汤。希望现在的困难只是暂时的,就像北京当前的暴雨,终会拨云见日见彩虹。
\n"},{"title":"Panmax 的香煎鸡胸肉教程","url":"/2018/fried-chicken-breast-tutorial/","content":"可能很多人不知道,我的第二职业是厨师,之后会考虑写一些简单易做的美食文章。
\n今天尝试做了一次煎鸡胸肉,味道非常棒!有健身需求的朋友们可以了解一下。
\n做法非常简单:
\n是不是非常简单?
\n最后,上几张图:
\n\n\n\n"},{"title":"functools.partial","url":"/2016/functools-partial/","content":"今天在看flask源码时看到了这样的写法:
\nrequest = LocalProxy(partial(_lookup_req_object, 'request'))
第一次见partial的使用,所以查了查资料学习了下。
\n我们可以简单的理解为 partial(func, ‘request’) 就是使用 ‘request’ 作为func的第一个默认参数来产生另外一个function。
\n所以, partial(_lookup_req_object, ‘request’) 我们可以理解为:
\n生成一个callable的function,这个function主要是从 _request_ctx_stack 这个LocalStack对象获取堆栈顶部的第一个RequestContext对象,然后返回这个对象的request属性。
\nfunctools.partial 通过包装手法,允许我们 “重新定义” 函数签名
\n用一些默认参数包装一个可调用对象,返回结果是可调用对象,并且可以像原始对象一样对待
\n冻结部分函数位置函数或关键字参数,简化函数,更少更灵活的函数参数调用
\n#args/keywords 调用partial时参数 |
urlunquote = functools.partial(urlunquote, encoding='latin1')
当调用 urlunquote(args, *kargs)
相当于 urlunquote(args, *kargs, encoding='latin1')
典型的,函数在执行时,要带上所有必要的参数进行调用。
\n然后,有时参数可以在函数被调用之前提前获知。
\n这种情况下,一个函数有一个或多个参数预先就能用上,以便函数能用更少的参数进行调用。
\n","categories":["Code"],"tags":["Python","flask"]},{"title":"functools.wraps 的作用","url":"/2016/functools-wraps-%E7%9A%84%E4%BD%9C%E7%94%A8/","content":"本文翻译自Stackoverflow:What does functools.wraps do?
\n当你使用一个装饰器,就是用另一个函数替换当前函数。换一种说法,如果你有一个装饰器:
\ndef logged(func): |
你这样使用它:
\n@logged |
实际上和这种用法相同:
\ndef f(x): |
而且你的函数 f
被 with_loging
替换。不幸的是,这意味着当你使用:
print f.__name___ |
它会打印出 with_logging
因为这是你新函数的名字。事实上,如果你查看 f
的 文档字符串,它将是空的,因为 with_logging
没有文档字符串所以你写的文档字符串不会在这里出现。并且,如果你查看这个函数使用 pydoc
生成结果,他的参数列表不是一个参数 x
,取而代之的是 *args
和 **kwargs
因为这是 with_logging
所持有的。
如果使用装饰器总是意味着丢失这个函数的信息,这将是个严重的问题。这就是为什么我们有 functools.wraps
的原因。给函数使用一个装饰器并且给函数增加复制名字、文档字符串、参数列表等功能性(This takes a function used in a decorator and adds the functionality of copying over the function name, docstring, arguments list, etc)。当 wraps
是它自己的装饰器,下边的代码将会做正确的事情。
from functools import wraps |
python 在访问属性的方法上定义了__getattr__()
和 __getattribute__()
2种方法,其区别非常细微,但非常重要。
__getattribute__()
方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。__getattr__()
方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x
定义了属性 color
, x.color
将 不会 调用x.__getattr__('color')
;而只会返回 x.color
已定义好的值。下边举几个栗子:
\n__getattr__
和__getattribute__
时,在访问类的实例一个不存在的属性时会报错class GetAttrClass(object): |
上边程序运行后得到一下错误:
\nTraceback (most recent call last): |
__getattr__
或__getattribute__
时,在访问类的实例一个不存在的属性时会返回Noneclass GetAttrClass(object): |
程序运行结果为:None
__getattr__
而是直接返回定义好的值class GetAttrClass(object): |
程序运行结果为:
\nred |
class GetAttrClass(object): |
程序运行结果为:
\nblack |
__getattribute__
后,每次引用属性和方法都会调用它class GetAttrClass(object): |
程序运行结果为:
\nred |
即便已经显式地设置 gac.color,在获取 gac.color 的值时, 仍将调用 __getattribute__()
方法。如果存在 __getattribute__()
方法,将在每次查找属性和方法时 无条件地调用 它,哪怕在创建实例之后已经显式地设置了属性。
\n\n如果定义了类的
\n__getattribute__()
方法,你可能还想定义一个__setattr__()
方法,并在两者之间进行协同,以跟踪属性的值。否则,在创建实例之后所设置的值将会消失在黑洞中。
__getattribute__()
方法,因为 Python 在查找类的方法名称时也将对其进行调用。class GetAttrClass(object): |
以上程序报错:
\nTraceback (most recent call last): |
__getattribute__()
方法。没有属性或方法的查询会成功。gac.hello()
时,Python 将在 GetAttrClass 类中查找 hello()
方法。该查找将执行整个 __getattribute__()
方法,因为所有的属性和方法查找都通过__getattribute__()
方法。在此例中, __getattribute__()
方法引发 AttributeError 异常,因此该方法查找过程将会失败,而方法调用也将失败。\n\n由于项目代码中存放了一些大文件在 git 仓库中(比如训练后的模型数据),所以最近收到公司的通知,需要给 git 进行瘦身。
\n本文内容是摘自公司的通知。
\n
瘦身将从此库中永久删除此文件,且无法恢复。包括所有“分支”中的引用,所有“Tag”中的引用,连同提交此文件的log记录也一并清除。
\n请在操作之前将要永久删除的文件备份,并记录目录位置。待瘦身结束后,将此大文件以「LFS」的形式 commit 到此库中。 详见《GitLab lfs 使用》。
\n先在本地对 git 库瘦身,再镜像推送到 GitLab 新创建的库。
\n待新库测试稳定后,通知管理员将旧的 git 库归档,组内使用新库,新/旧库 rename 互换。
\ngit clone --bare https://git.server.com/group/name.git |
du -sh ./name.git |
cd name.git |
查询结果对应关系:<SHA-1
> <类型> <size
> <size-in-packfile
> <offset-in-packfile
>
如:
\n950dae43f100f6586884893eab3b258a09da1076 blob 173244608 172458659 28056 |
git rev-list --objects --all | grep "$(git verify-pack -v ./objects/pack/*.idx | sort -k 3 -n | tail -10 | awk '{print$1}')" |
git filter-branch --force --index-filter 'git rm -rf --cached --ignore-unmatch folder/file1 folder/file2 folder/file3' --prune-empty --tag-name-filter cat -- --all |
filter-branch
是让 git 重写每一个分支
--force
假如遇到冲突也让 git 强制执行。--index-filter
重写索引的过滤器。--prune-empty
如果修改后的提交为空则扔掉不要。--tag-name-filter
表示对每一个 tag 如何重命名,重命名的命令紧跟在后面,当前的 tag--
表示分割符,最后的 --all
表示对所有的分支和 tag 都考虑在内。rm -rf ./refs/original/ |
git reflog expire --expire=now --all |
git repack -A -d |
git gc --aggressive --prune=now |
--aggressive
最大限度的压缩,会比较缓慢
git fsck --full --unreachable |
du -sh ../name.git |
git push --mirror https://git.server.com/group/name_new.git
commit
到新库中git lfs 要求 git >= 1.8.2
yum install git-lfs -y |
运行 brew install git-lfs
即可
git 版本大于 2.12
\n关闭 Windows 的 ssl 校验
\ngit config --global http.sslVerify false |
走流程申请一个 aritfactory –git lfs 仓库
\n比如 3.pdf
,运行命令 git lfs track 3.pdf
,会产生 git lfs
管理文件 .gitattributes
支持通配符比如 git lfs track *.exe
.lfsconfig
文件,指定 git lfs
文件存放位置我申请的 git lfs 仓库叫做 git-lfs
登陆 aritfactory 后,如下操作:
\n\n.gitattributes
、.lfsconfig
、3.pdf
,然后在 gitlab 中查看前几天调研了一下 hugo,准备后边把自己的博客迁移过去。hugo 教程中新增主题推荐通过 git 子模块的方式(前提是原始文件就已经在一个 git 项目下),比如我要新增一个名字叫 zen 的主题,可以通过以下命令进行安装:
\ngit submodule add https://github.com/frjo/hugo-theme-zen.git themes/zen |
这个命令会将主题仓库中的文件 clone 到 themes/zen 路径下,同时会在我的的仓库根路径下新建一个 .gitmodules
文件,之后 add 其他子模块时,会往这个文件中追加数据,格式如下:
[submodule "themes/zen"] |
我们需要把 .gitmodules
文件加入到 git 的版本控制中。
git submodule update --recursive --remote |
首次在一个新的环境中 clone 我们的仓库后是不带子模块代码的,可以通过下边这个命令来把所有子模块代码拉下来:
\ngit submodule update --init --recursive |
使用循环时下边几个容易尝试混乱的编码方式我们要尽量避免。
\n考虑到效率,在进行循环遍历过程中,迭代出的变量会赋值到同一个地址。这可能会导致无意识的错误。
\nin := []int{1, 2, 3} |
以上代码得到的结果是:
\nValues: 3 3 3 |
原因很容易解释:每次迭代时我们将 v
的地址追加到 out
切片中,前边提到,v
在每次遍历时为同一个变量,在输出的第二行可以看到打印出了相同的地址。
简单的修复方法是,将每一次的迭代出的变量复制给一个新的变量:
\nin := []int{1, 2, 3} |
输出:
\nValues: 1 2 3 |
同样的问题会出现在将迭代出的变量用在 Goroutine 中:
\nlist := []int{1, 2, 3} |
输出:
\n3 3 3 |
这个 bug 也可以使用上边提到的方法解决。(注:如果不在 Goroutine 中执行,上边的代码是没有问题的)
\n下边代码循环中的 group.Wait()
会被阻塞,导致无法执行后边的循环。
var wg sync.WaitGroup |
正确的写法是把 Wait()
放在循环外:
var wg sync.WaitGroup |
只有当函数返回时,defer
才会被执行。除非你知道你在做什么,否则不应该将 defer
用在循环中。
var mutex sync.Mutex |
在上边的例子中,在完成第一次循环后,之后的循环无法获得互斥锁从而被阻塞。应该改成下边的显性释放锁的方式:
\nvar mutex sync.Mutex |
如果你确实需要在循环中使用 defer
,可以考虑将工作委托给另一个函数:
var mutex sync.Mutex |
func doReq(timeout time.Duration) obj { |
上边的代码模拟这样一个行为:超时前获得到结果将结果返回,若超时则返回 nil。
\n我们通过一个 Goroutine 异步获取结果,并通过一个 channel
配合 select
来阻塞代码往后执行。
上边代码使用了 unbuffered channel
,这会导致的问题是,如果代码因超时提前返回了,Goroutine 在获取到结果后,会阻塞在 ch <- obj
这一行(因为没有其他的 Goroutine 来读取这个 channle
),从而这个 Goroutine 无法退出,进而会发生 Goroutine 泄露。
解决方法是使用一个长度为 1 的 buffered channel
:
func doReq(timeout time.Duration) obj { |
还有一种修复方式是在 Goroutine 中使用一个 select
配合一个空的 default
:
... |
当没有其他 Goroutine 来读取这个 channel
时,会走到 default
行为,这个 Goroutine 也就可以正常退出了。
接口可以使代码更具灵活性,是在代码中引入多态的一种方法。接口允许我们关注一组行为而非特定类型。不使用接口不会有错误产生,但会让我们的代码看起来不那么优雅、不具有可扩展性。
\n在众多接口中,io.Reader
和 io.Writer
可能是最受欢迎的一对。
type Reader interface { |
这些接口非常强大, 假设我们需要将一个对象写入一个文件,可以这样定义一个 Save
方法:
func (o *obj) Save(file os.File) error |
如果明天我们有需要将这个文件写入 http.ResponseWriter
呢?我们可不想重新定义一个新的方法,这时 io.Writer
就派上用场了:
func (o *obj) Save(w io.Writer) error |
还需明白的一点是:我们应该只关心我们要使用的行为。在上边的例子中,使用 io.ReadWriteCloser
虽然也行得通,但如果我们只用到了 Write
方法,就不是特别好的实践了。接口面积越大,抽象能力越弱。
因此,在大部分情况下,我们应关注行为而不是具体类型。
\n下边的代码不会出现错误,但会有使用更多的内存:
\ntype BadOrderedPerson struct { |
上边的 struct
看起来会分配 21 bytes 的内存,但实际上分配的是 32 bytes。出现这个情况原因是数据结构对齐。在 64 位架构中,内存以 8 bytes 为一个连续单元,改成下边的声明顺序可以优化到分配 24 bytes:
type OrderedPerson struct { |
在频繁使用不合理字段顺序的类型时,会导致额外的内存开销。
\n不过,我们也不必手动计算和优化结构体内存,可以使用 go tool
提供的 fieldalignment 工具来检测并修复不合理的声明顺序。
cd $GOPATH |
➜ fieldalignment . |
数据竞争会导致一些很迷的问题,而且通常是在部署一段时间后才会发生。所以此类问题在并发系统中是最常见而且最难排查的 bug。为了更方便找出此类 bug,Go 1.1 中引入了一个内置的数据竞争检测器,只需加上 -race
标识就可以了。
$ go test -race pkg // to test the package |
当开启竞争检测器时,编译器会记录代码对内存进行了何时、何种方式的访问,同时 runtime
监控共享变量的非同步访问。
发现数据竞争时,竞争检测器会打印包含访问冲突的调用栈记录,如下所示:
\nWARNING: DATA RACE |
\n\n"},{"title":"Docker 部署 Go 服务并实现热加载","url":"/2019/go-docker-reload/","content":"写在最后:人类从历史中学到的唯一教训,就是人类无法从历史中学到任何教训。
\n
Docker 足够轻量、也非常易用,并且可以确保我们所有的运行环境保持一致。
\n在这篇文章中,我将通过创建 Docker 容器来部署一个 Go API 服务。当我对源码进行修改时,这个 Go 服务也会立即重新加载。
\n通过这个方式我们就不需要再在开发过程中多次重新编译 Docker 镜像了。
\n\n官方在 Go 的 1.13 版本中介绍了模块的概念。这意味着我们不再需要把整个工程放在 Go 的工作空间下了。
\n开始前,我创建一个新的目录 go-docker
来放置所有文件。
然后初始化一个 Git 仓库并创建 Go 模块。
\ngit init |
你会看到在项目目录下出现了一个 go.mod
文件。这个文件将存有这个模块下所有的依赖,类似于 Node 开发中用到的 package.json
或 Python 中的 requirements.txt
。
模块设置好了,现在来构建一个简单的 API 服务。
\n我准备在构建这个 API 服务时使用 gorilla/mux
路由包。我也可以只用 Go 中提供的标准模块来实现路由,但我想确保模块依赖可以按照预期工作,并且利用 mux
可以支持我们构建更加复杂的应用。
go get -u github.com/gorilla/mux |
执行这个命令后,你会看到它被作为依赖写入了 go.mod
文件。
### module github.com/Panmax/go-docker |
接下来,创建这个 Go 项目的主文件 commands/runserver.go
。
package main |
这个 API 只是简单返回一条消息:「Hello World!」
\n在把这个程序放进 Docker 容器前我们最好先来测试一下。通过 go run
命令来运行这个服务。
go run commands/runserver.go |
API 服务可以正常工作。
\n我们开始为这个项目构建 Docker 镜像。Docker 镜像包含一组用来告诉 Docker 需要提供什么环境的指令。
\nFROM golang:latest |
使用 golang:latest
镜像作为这个自定义镜像的基础镜像。这样就可以免去 Go 开发环境的配置。
将整个项目拷贝到了镜像的 /app
目录下,然后通过 go mod download
下载依赖。
最后,我们告诉 Docker 执行 go run commands/runserver.go
命令来启动服务。
执行以下命令来构建这个镜像:
\ndocker build -t go-docker-image . |
现在我已经构建好了 Docker 镜像,接下来我们实际启动一下这个 Docker。
\ndocker run go-docker-image |
服务已经监听在了 Docker 容器中,但是当我通过浏览器中打开 localhost
时却发现无法访问。
出现这个情况的原因是,虽然程序在 Docker 容器内监听了 80 端口的传入请求,但是它并没有在宿主机的 80 端口上进行监听。因此我们给 localhsot
发送一个 GET 请求,它是找不到正在运行的服务的。
我用一张逻辑图来表述一下这个问题:
\n\n为了解决这个问题,我们需要把容器内的 80 端口映射到主机的 80 端口。
\ndocker run -p 80:80 go-docker-image |
端口映射后的逻辑图如下:
\n\n现在再来访问 localhost
,就可以看到「Hello World!」显示在了页面上。
我们来对这个 API 做一点调整:
\npackage main |
我在这个 API 的返回结果中新加了一行消息,我们再来启动一个新的 Docker 容器。
\ndocker run -p 80:80 go-docker-image |
但是如果我现在访问 localhost
,看到的仍然是旧消息。
这是因为 Docker 镜像没有变化。为了使变更生效,我们必须重新构建这个镜像。
\ndocker build -t go-docker-image . |
现在就可以看到更新后的消息了。
\n\n每次对代码修改后,重新构建 Docker 镜像会花费很长时间,我们来让这个系统更好用一点。
\n我要使用的是 Compile Daemon 包。如果有任何 Go 源码发生了变更,这个包会重新编译并重启我们的 Go 程序。
\nFROM golang:latest |
我修改了 Dockerfile
来下载 CompileDaemon
包。
之后修改了 ENTRYPOINT
后面的命令来运行 CompileDaemon
程序,同时为它指定了项目编译和服务启动命令。每次有文件变化后,以上命令就会被执行。
重新编译这个镜像:
\ndocker build -t go-docker-image . |
启动 Docker 时,我添加了 -v ~/Projects/go-docker:/app
参数。这样就可以把我本机的 go-docker
目录挂载到 Docker 容器内的 /app
目录下了。
当我修改了本机 go-docker
目录内的文件时,容器 /app
目录下的文件也会变化。
最终的启动命令如下。
\ndocker run -v ~/Projects/go-docker:/app -p 80:80 go-docker-image |
容器运行过程中,尝试修改源码,你会看到更改自动生效了。
\n2019/11/20 11:59:53 Running build command! |
每次运行容器时,我都要输入很长的启动命令:docker run -v ~/Projects/go-docker:/app -p 80:80 go-docker-image
。在这个项目中倒没有太大问题,毕竟我才只有一个容器要启动。
但假设我有一个需要启动很多容器的项目,执行多个 docker run
命令会非常麻烦。
解决方案是使用 Docker Compose。利用这个工具,我们可以指定运行 docker-compose up
命令时要启动哪些容器。
为了配置它,我们需要创建一个 docker-compose.yml
文件:
version: "3" |
这里,我声明了要创建一个名为 go-docker-image
的镜像。这个镜像使用当前目录下的 Dockerfile
来构建。同时我配置了端口映射和目录挂载。
执行 docker-compose up
来启动 docker-compose.yml
中指定的容器。
现在,我有了一个运行在 Docker 内的 API 服务,与此同时,当代码变化时这个服务也会自动重新加载。
\n可以在这里查看项目源码:https://github.com/Panmax/go-docker
\n"},{"title":"由于粗心,Go 变量作用域导致的问题","url":"/2020/go-field-scope-question/","content":"\n\n昨天写代码的时候因为变量作用域的问题被坑了好久,在这里记录一下,避免今后再犯。
\n先看下面这段代码,大致功能是为传进来的引用填充一个带有自增的 ID
的对象,同时这个 ID
中不能包含 4,自增 ID
是使用 Redis 的 incr
来维护的。
func fillNewUserLiveRightAttribute(ctx context.Context, userLiveRight *po.UserLiveRight, right *po.LiveRight) error { |
但是之后发现填充进去的 ID
永远是 0,检查了一下 Redis 中 那个自增 ID 也确实存在。
这里当时还饶了一下远路,因为那个 Attribute
字段在数据库中使用 jsonb 存储的,所以我前期先检查了插入时执行的 SQL
语句,发现每次 Attribute
都是打印的 {}
,就以为是我自己没有赋上值。真实的原因是我指定了 Attribute
中的 ID
字段在转 Json 时启用 omitempty
,即:
type UserLiveRightAttribute struct { |
使用 omitempty
可以告诉 Marshal
函数如果 field
的值是对应类型的 zero-value
,那么序列化之后的 JSON object 中不包含此 字段,所以 ID=0
转 Json 后自然就没有这个字段了。
回到为啥上边的代码拿到的 ID
总是 0 的问题:因为 for
中赋值的 incrId
是在一个新的作用域内,只在 for 的花括号内有效,退出 for 后拿到的是最开始初始化 incrId
的 0 值,这里使用的 := 进行的赋值,因为 err 是个新字段,所以并没有提示错误。
修复方法很简单,err 也在作用域外声明,里边使用 =
来赋值。
func fillNewUserLiveRightAttribute(ctx context.Context, userLiveRight *po.UserLiveRight, right *po.LiveRight) error { |
今天又踩到了一个 go 语言的坑,其实也不算坑,本质上还是自己对这门语言的不熟悉。
\n来看一下我犯的错误,直接上代码:
\nfunc Int64ToStrings(ids []int64) []string { |
我的需求很简单,将一个 int64
的切片转为 string
类型的切片,写这段代码的时候想到可以预先分配 slice
的大小,所以写了开头的 make
,后边就直接逐个将转为 string
的元素 append
进去了。
但是,make
时实际已经帮我完成了里边指定数量元素的初始化,即:
s = make([]string, 5) // s == []string{"", "", "", "", ""} |
所以我之后再往里边 append
时是往最后一个空字符串后边追加元素。
修复后的代码如下:
\nfunc Int64ToStrings(ids []int64) []string { |
或者在初始化时指定 slice
长度为0,容量为我们需要的长度:
func Int64ToStrings(ids []int64) []string { |
与 C/C++ 不同,Go 有 GC,所以我们不需要手动处理内存的分配和释放。不过,我们仍然应该谨慎对待内存泄漏问题。
\n来看一个由 slice
引起的内存泄漏案例。
package main |
我们可以看到,即使有一个对象被删除,a 的容量仍然是8,这意味着remove
函数可能导致潜在的内存泄漏。
来看一个例子
\npackage main |
在这种情况下,有两种内存泄漏。
\n底层数组的容量只会增加,但不会减少,第一个例子已经证明了这一点。
\n如果我们认为容量太大,我们可以创建一个新的 slice,并将原 slice 中的所有元素复制到新 slice 中。这是一个复制操作(时间)和内存使用(空间)之间的权衡。
\nfunc remove(s []*Object, i int) []*Object { |
解决方法:将未使用的元素设置为nil,它将会被 GC 释放。
\nfunc remove(s []*Object, i int) []*Object { |
Golang 在序列化和反序列化一个 Struct 时,如果指定了 JSON tag 会严格按照指定的 tag 内容来执行,在没有指定 tag 或 tag 大小写不精准时,会有一些默认规则。
\n序列化的情况比较简单:
\ntype A struct { |
上边这段代码输出:
\n{\"Case\":1,\"Cas_E\":3,\"ok\":4} |
casE
这个字段没有输出,原因是因为他是个不可导出的私有字段,即使设置了 tag 也不可序列化。CaSE
序列话后的 key 为 ok
是因为我们给它指定了 tag序列化的情况稍微有点复杂,其整体的优先级为:
\n我们看几个例子:
\n情况1,带 tag 的两个字段都无法匹配上(精准匹配+模糊匹配),不带 tag 的两个字段都可以模糊匹配上,优先赋值给前边声明的字段:
\ntype B struct { |
情况2,带 tag 的其中一个字段可以模糊匹配上:
\ntype B struct { |
情况3,带 tag 的两个字段都可以匹配上,第一个模糊匹配,第二个精准匹配:
\n"},{"title":"使用 Go 语言时没有关注值传递和误用 for 循环导致的 bug","url":"/2020/go-value-for-mistake/","content":"\n\n我们的业务代码中习惯使用 Map
维护一些 LocalCache
,前两天发现自己维护的一个 LocalCache
数据有些不对:Cache 的 Key
为某个对象的ID,值为这个ID对应的 PO
(即数据库中的对象),调试时发现所有的 Key
对应的值都是一样的,这是因为自己对一些细节没有关注到,还把 Java 那套东西搬来用导致的问题。
为了简化,我就不把业务代码搬上来了,写个简单的示例:
\npackage main |
上边代码输出如下:
\n{"1":{"ID":3,"Name":"王五","Age":20},"2":{"ID":3,"Name":"王五","Age":20},"3":{"ID":3,"Name":"王五","Age":20}} |
可以看到所有的 value
是同一个 Student
,为什么会出现这样的问题呢?因为 students
存储的是 Student
的值,在给 for
循环中的 student
赋值时,是复制了一个新的值给它,而 for
循环中的 student
变量所指向的地址是不变的。
可以打印 student 的地址看一下:
\nfor _, student := range students { |
输出为:
\n0xc0000a6040 |
这种情况下我们应该用 students
中索引对应数据的指针,上边 for 循环修改如下:
for i, student := range students { |
输出为:
\n0xc0000b8000 |
上边的情况给 student
赋值也是有问题的:
for _, student := range students { |
输出:
\n[{"ID":1,"Name":"张三","Age":18},{"ID":2,"Name":"李四","Age":19},{"ID":3,"Name":"王五","Age":20}] |
Java 写习惯了就以为迭代时的 student
指向的是 students
中的地址。
公司的服务间调用使用的 gRPC,所以开发过程中需要写一些 .proto
文件来生成 pb
,写的时候借助语法检查可以更高效一些。
GoLand 中提供了一个叫 Protoco Buffer Editor
的插件,但是这个插件在我的环境中是有 bug 的,无法处理 import 进来的包。
询问同事他们的都没有问题,所以我从网上查了一下这个问题,网上推荐的插件名叫 Protobuf Support
,显然我的 IDE 的插件市场中没有这个插件,又借助搜索引擎找到了这个插件的描述页面,看到已经被打上 deprecated 的标签了。
我猜测这个插件之前可以用,后来被官方下掉了,我的同事们是在下掉之前安装的,为了印证我的想法,让其中一个同事看了一下他的插件名,果然是 Protobuf Support
。
好歹顺着官方页面找到了GitHub 的地址 https://github.com/ksprojects/protobuf-jetbrains-plugin ,又在 Releases 页面中找到了插件的压缩包,通过压缩包在本地进行了安装,并 disable 了 Protoco Buffer Editor
插件,重启 GoLand 后问题解决。
今天一下午光处理坑了,上线了一个限流功能,一直没有拿到打点数据,和治理组同事检查了所有地方都没有问题,最后治理组同事想起来,需要触发限流后才会打点,在监控中看到数据。
\n"},{"title":"《论语》公冶长篇中孔子点评过的弟子","url":"/2024/gong-ye-chang/","content":"公冶长一生治学,德才兼备,虽然做过牢,可孔子不觉得他有罪,就把女儿嫁给了他;
\n\n\n子谓公冶长:“可妻也,虽在缧绁之中,非其罪也!”以其子妻之。
\n
南宫适知晓分寸,进退有度,好社会能干、坏社会能自保,孔子把侄女嫁给了他;
\n\n\n子谓南容:“邦有道不废;邦无道免于刑戮。”以其兄之子妻之。
\n
宓子贱仁德好学,刚正不阿;
\n\n\n子谓子贱:“君子哉若人!鲁无君子者,斯焉取斯?”
\n
子贡心高气傲,有点喜欢自我标榜,孔子总是苦口婆心地对他旁敲侧击;
\n\n\n子贡问曰:“赐也何如?”子曰:“女,器也。”曰:“何器也?”曰:“瑚琏也。”
\n
子贡曰:“我不欲人之加诸我也,吾亦欲无加诸人。”子曰:“赐也,非尔所及也。”
冉雍朴素踏实、勇于实干;
\n\n\n或曰:“雍也仁而不佞。”子曰:“焉用佞?御人以口给,屡憎于人。不知其仁,焉用佞?”
\n
漆雕开稳重谦虚,志向高远但也沉得住气;
\n\n\n子使漆雕开仕,对曰:“吾斯之未能信。”子说。
\n
子路是个急性子,自视甚高,直来直去,有什么说什么;
\n\n\n子曰:“道不行,乘桴浮于海,从我者其由与?”子路闻之喜,子曰:“由也好勇过我,无所取材。”
\n
子路有闻,未之能行,唯恐有闻。
公西赤善于外交,口才一流;
\n\n\n“赤也何如?”子曰:“赤也,束带立于朝,可使与宾客言也。不知其仁也。”
\n
冉求多才多艺,特别会管钱,但是因为帮季氏敛财,受到孔子的严厉批评,后来跟孔子学习之后逐渐成为仁德之人;
\n\n\n“求也何如?”子曰:“求也,千室之邑,百乘之家,可使为之宰也,不知其仁也。”、
\n
宰我调皮捣蛋,能言善辩,他最出名的事就是白天睡觉被老师骂;
\n\n\n宰予昼寝,子曰:“朽木不可雕也,粪土之墙不可杇也,于予与何诛?”子曰:“始吾于人也,听其言而信其行;今吾于人也,听其言而观其行。于予与改是。”
\n
申枨精通六艺,但欲望比较强,孔子觉得他还没培养出刚健的气质。
\n\n\n子曰:“吾未见刚者。”或对曰:“申枨。”子曰:“枨也欲,焉得刚。”
\n
参考:
\n\n"},{"title":"好物推荐","url":"/2023/good-things-2023/","content":"以下是我最近几年用过的觉得还不错、值得推荐的好物,有些之前也推荐过,好东西值得多次推荐。
\n这些商品网上的介绍非常多,我在这里不做详细介绍。结合自己的使用感受,尝试用自己的一句话描述。
\n注:排名分先后。
\n戴上它,播放你喜欢的音乐,不管周围多么嘈杂,仿佛整个世界都是你的。
\n在地铁上,戴着它不播放音乐或者播放轻音乐,捧起一本书,享受一段安静的阅读时光。
\n我最贴身的助理。
\n我的是S6,已经用了3年了,感觉还能再战3年。如果觉得旧了,30元左右在淘宝买个新表带,换上后跟新的一样。
\n最适合程序员使用的键盘,没有之一。
\n如果不知道给你的程序员朋友送什么礼物,送这个键盘准没错。
\n用之前很抗拒,用习惯后爽死了。现在如果不用它就觉得粑粑没擦干净。
\n动力十足,气流吹到头上后又很轻柔。向我这种头发不太长的,1分钟以内结束战斗。
\n用它吸一吸你的床,就知道你每天睡的床有多脏了。用它打扫卫生,看着集尘桶内的灰尘越来越多,很有成就感也很解压。
\n贴合手型,就像握住了D罩杯。滚轮像指尖陀螺,没有思路时可以用它来解压。侧边按键写代码,浏览网页时前后推很实用。
\n这个鼠标是跟一个朋友交换的,她买这个鼠标后觉得太大,刚好我买了个小的,就和她换了。刚用的时候没觉得太好用,等习惯后就发现离不开了。
\n我用的去屑清爽那一款,我之前一直尝试各重洗发水,自从用了这款后就再没有换过。
\n各位男程序员同胞,给自己换个好洗发水把。
\n这个味道我太爱了,穿着用它洗过的衣服去上班,无意间闻到它的芬香后,整个人都快乐了许多。
\n被烘过的衣服穿起来太舒服了,松松软软,和在夏日的暖阳下晾干的一样,一股清新阳光的味道。
\n之前我一直用的是飞利浦电动牙刷,用坏两个后,在被安利下购买了 Usmile 的牙刷,功能性和质量超出我的预期,一点不比飞利浦差,价格也十分平易近人。
\n跑步鞋界的国货之光。
\n太适合我这种爱做饭不爱刷碗的人了。
\n"},{"title":"gradle 编译时强制刷新依赖","url":"/2017/gradle-%E7%BC%96%E8%AF%91%E6%97%B6%E5%BC%BA%E5%88%B6%E5%88%B7%E6%96%B0%E4%BE%9D%E8%B5%96/","content":"最近团队封装了个 springboot
的 starter
,用起来很爽,后来优化代码的时候,看到下边的代码中已经指定了 profile
cloud: |
所以理所当然的认为不需要指定 spring
的 active 了,就把 active
给删掉了(如下):
spring: |
发布到 maven
仓库后,重新测试没啥问题。结果过了个周末来了再编译,发现程序无法启动了,找了很多原因才发现是上边的操作导致的。
后来将配置改了回来,发现还是不行,又鼓捣了好久发现这次的问题是 gradle
编译缓存的问题,通过这个网站: https://pkaq.gitbooks.io/gradletraining/content/book/ch5/4.%E4%BE%9D%E8%B5%96%E7%9A%84%E6%9B%B4%E6%96%B0%E4%B8%8E%E7%BC%93%E5%AD%98.html 找到了解决办法,编译的时候在后边加上 --refresh-dependencies
可以强制刷新缓存。
虽然问题解决了,但是我还有个疑问,我们的 starter
明明已经指定版本号为 0.0.1-SNAPSHOT
了,按理说应该在 build
的时候无条件的重新拉取最新的依赖,但是这个时候为什么没有生效?
Apache TinkerPop 是一个开源的图计算框架。在这其中,TinkerPop 代表了很多的功能和技术,并且在它广阔的生态系统下还另外扩展了第三方贡献图库和系统的世界。TinkerPop 的生态系统对于新手来说可能是复杂的,尤其是第一次浏览参考文档的时候。
\n所以,你要从哪里开始使用 TinkerPop 呢?你如何快速入门并且获得成果?
\nGremlin,TinkerPop 世界里最知名的公民,让它来帮助你完成入门,之后,你也可以使用 TinkerPop 构建图应用程序了。
\n\nGremlin 可以帮助你浏览一个图中的点和边。他本质上是你用来查询图数据库的语言,就和 SQL
是用来查询关系型数据库的语言一样。为了告诉 Gremlin 他应该如何「遍历」图(也就是你想做的查询)你需要一种方法来用他能明白的语言下达命令,这个语言当然被叫做「Gremlin」。对于这个任务,你需要一个 TinkerPop 的最重要的工具:Gremlin 控制台。
\n\n你现在可能还不知道点和边是什么,这会在后文中进行介绍,不过请允许我带你先认识一下 Gremlin 控制台,让你能够了解这个可以帮助你学习体验的工具。
\n
我们来下载控制台然后解压并启动它:
\n$ unzip apache-tinkerpop-gremlin-console-3.3.0-bin.zip |
Gremlin 控制台是个 REPL 环境,它提供了很 nice 的方式来学习 Gremlin,因为你可以在输入代码后立刻得到反馈。这消除了需要「创建项目」才能尝试的复杂方式。控制台不仅仅是用来「入门」的,你将发现你会使用它来进行和 TinkerPop 相关的各种活动,比如加载数据、管理图、编写复杂的遍历等等。
\n为了让 Gremlin 遍历一个图,你需要一个 Graph
实例,它保存着图的结构和数据。TinkerPop 是不同图数据库和图处理器之上的图抽象层,所以控制台中有很多可以实例化的实例供你选择。开始时最好的 Grahp
实例当然是 TinkerGraph。TinkerGraph 是一个快速、运行于内存的图数据库,有少量配置项,使其成为初学者不错的选择。
\n\nTinkerGraph 不仅仅是提供给初学者的玩具。它在以下几个场景也是非常有用的:分析从大图中取出的子图时,使用不会有太大变化的静态图时,编写单元测试和其他能适应内存的图用例时。
\n
待续
\n","tags":["BigData Graph"]},{"title":"面向程序员的数据挖掘指南-知识点","url":"/2021/guidetodatamining-points/","content":"前段时间读了《面向程序员的数据挖掘指南》,原文链接:https://dataminingguide.books.yourtion.com/,把里边的知识点做下整理。
\nx之差的绝对值加上y之差的绝对值
勾股定理
r值越大,单个维度的差值大小会对整体距离有更大的影响。
\n利用他人的喜好来进行推荐,也就是说,是大家一起产生的推荐。
\n用于衡量两个变量之间的相关性,它的值在-1至1之间,1表示完全吻合,-1表示完全相悖。
\n皮尔逊相关系数的计算公式是:
皮尔逊相关系数的近似值:
“·”号表示数量积。
\n“||x||”表示向量x的模,计算公式是:
如:
它们的模是:
数量积的计算:
因此余弦相似度是:
余弦相似度的范围从1到-1,1表示完全匹配,-1表示完全相悖。
\n而基于物品的协同过滤则是找出最相似的物品,再结合用户的评价来给出推荐结果。
\n基于用户的协同过滤又称为内存型协同过滤,因为我们需要将所有的评价数据都保存在内存中来进行推荐。
\n修正的余弦相似度是一种基于模型的协同过滤算法。这种算法的优势之一是扩展性好,对于大数据量而言,运算速度快、占用内存少。
\n用户的评价标准是不同的,比如喜欢一个歌手时有些人会打4分,有些打5分;不喜欢时有人会打3分,有些则会只给1分。修正的余弦相似度计算时会将用户对物品的评分减去用户所有评分的均值,从而解决这个问题。
\n\nU表示同时评价过物品i和j的用户集合
\n\n表示将用户u对物品i的评价值减去用户u对所有物品的评价均值,从而得到修正后的评分。
\ns(i,j)表示物品i和j的相似度,分子表示将同时评价过物品i和j的用户的修正评分相乘并求和,分母则是对所有的物品的修正评分做一些汇总处理。
\n计算Kacey Musgraves和Imagine Dragons的相似度
我已经标出了同时评价过这两个歌手的用户,代入到公式中:
所以这两个歌手之间的修正余弦相似度为0.5260
\n比如我想知道David有多喜欢Kacey Musgraves?
p(u,i)表示我们会来预测用户u对物品i的评分,所以p(David, Kacey Musgraves)就表示我们将预测David会给Kacey打多少分。
N是一个物品的集合,有如下特性:
Si,N表示物品i和N的相似度,Ru,N表示用户u对物品N的评分。
\n为了让公式的计算效果更佳,对物品的评价分值最好介于-1和1之间。
MaxR表示评分系统中的最高分(这里是5),MinR为最低分(这里是1),Ru,N是用户u对物品N的评分,NRu,N则表示修正后的评分(即范围在-1和1之间)。
\n若已知NRu,N,求解Ru,N的公式为:
比如一位用户打了2分,那修正后的评分为:
反过来则是:
修正David对各个物品的评分:
结合物品相似度矩阵,代入公式:
将其转换到5星评价体系中:
一种比较流行的基于物品的协同过滤算法
\n分为两个步骤:
\n计算物品之间差异的公式是:
card(S)表示S中有多少个元素;X表示所有评分值的集合;card(Sj,i(X))则表示同时评价过物品j和i的用户数。
\n计算Taylor Swift 和 PSY之间的差值
card(Sj,i(X))的值是2——因为有两个用户(Amy和Ben)同时对PSY和Taylor Swift打过分。
\n分子uj-ui表示用户对j的评分减去对i的评分,代入公式得:
即用户们给Taylor Swift的评分比PSY要平均高出两分。
\n比如说Taylor Swift和PSY的差值是2,是根据9位用户的评价计算的。当有一个新用户对Taylor Swift打了5分,PSY打了1分时,更新后的差值为:
使用加权的Slope One算法进行预测
公式为:
PWS1(u)j表示我们将预测用户u对物品i的评分。
表示遍历Ben评价过的所有歌手,除了Whitney Houston以外(也就是-{j}的意思)。
\n整个分子的意思是:对于Ben评价过的所有歌手(Whitney Houston除外),找出Whitney Houston和这些歌手之间的差值,并将差值加上Ben对这个歌手的评分。同时,我们要将这个结果乘以同时评价过两位歌手的用户数。
\nBen的评分情况和两两歌手之间的差异值展示如下:
在线性代数中,向量(vector)指的是具有大小和方向的几何对象。向量支持多重运算,包括相加、相减及数乘等。
\n在数据挖掘中,向量则可简单认为是物品的一组特征,比如音乐乐曲的特征。做文本挖掘时,会将一篇文章也用向量来表示——每个元素的位置表示一个特定的单词,这个位置上的值表示单词出现的次数。
\n分类器是指通过物品特征来判断它应该属于哪个组或类别的程序。
\n分类器程序会基于一组已经做过分类的物品进行学习,从而判断新物品的所属类别。
\n要让数据变得可用我们可以对其进行标准化,最常用的方法是将所有数据都转化为0到1之间的值。
\n标准分计算公式:
mean:平均值
standard deviation:标准差
标准差的计算公式是:
card(x)表示集合x中的元素个数。
\n计算方法:将标准分公式中的均值改为中位数,将标准差改为绝对偏差。
中位数指的是将所有数据进行排序,取中间的那个值。如果数据量是偶数,则取中间两个数值的均值。
\n计算工资的对偏差:
首先将所有人按薪水排序,找到中位数,然后计算绝对偏差:
可以计算得出Yun的修正标准分:
当物品的特征数值尺度不一时,就有必要进行标准化。
\n需要进行标准化的情形:
\n将数据集随机分割成十个等份,每次用9份数据做训练集,1份数据做测试集,如此迭代10次。
\n在数据挖掘领域,N折交叉验证又称为留一法。
\n上面已经提到了留一法的优点之一:我们用几乎所有的数据进行训练,然后用一个数据进行测试。
\n十折交叉验证是一种不确定的验证。相反,留一法得到的结果总是相同的,这是它的一个优点。
\n最大的缺点是计算时间很长。
\n留一法的另一个缺点是分层问题。
\n在留一法中,所有的测试集都只包含一个数据。所以说,留一法对小数据集是合适的,但大多数情况下我们会选择十折交叉验证。
\n表格的行表示测试用例实际所属的类别,列则表示分类器的判断结果。
\n混淆矩阵可以帮助我们快速识别出分类器到底在哪些类别上发生了混淆,因此得名。
\n这个数据集中有300人,使用十折交叉验证,其混淆矩阵如下:
可以看到,100个体操运动员中有83人分类正确,17人被错误地分到了马拉松一列;92个篮球运动员分类正确,8人被分到了马拉松;85个马拉松运动员分类正确,9人被分到了体操,16人被分到了篮球。
\n混淆矩阵的对角线(绿色字体)表示分类正确的人数,因此求得的准确率是:
从混淆矩阵中可以看出分类器的主要问题。
\n在这个示例中,我们的分类器可以很好地区分体操运动员和篮球运动员,而马拉松运动员则比较容易和其他两个类别发生混淆。
\nKappa指标可以用来评价分类器的效果比随机分类要好多少。
\nKappa指标可以用来衡量我们之前构造的分类器和随机分类器的差异,公式为:
P(c)表示分类器的准确率,P(r)表示随机分类器的准确率。
以下是该分类器的混淆矩阵,尝试计算出它的Kappa指标并予以解释。
准确率 = (50+75+123+170)/600= 0.697
\n计算列合计和百分比:
然后根据百分比来填充随机分类器的混淆矩阵:
随机分类器准确率 = (8 + 24 + 51 + 92) / 600 = (175 / 600) = 0.292
\n最后,计算Kappa指标:
这说明分类器的效果还是要好过预期的。
\n考察这条新记录周围距离最近的k条记录,而不是只看一条,因此这种方法称为k近邻算法(kNN)。
\n每个近邻都有投票权,程序会将新记录判定为得票数最多的分类。比如说,我们使用三个近邻(k = 3),其中两条记录属于体操,一条记录属于马拉松,那我们会判定x为体操。
\n我们需要预测Ben对Funky Meters的喜好程度,他的三个近邻分别是Sally、Tara、和Jade。
\n下表是这三个人离Ben的距离,以及他们对Funky Meters的评分:
在计算平均值的时候,我希望距离越近的用户影响越大,因此可以对距离取倒数,从而得到下表:
下面,我们把所有的距离倒数除以距离倒数的和(0.2 + 0.1 + 0.067 = 0.367),从而得到评分的权重:
我们可以注意到两件事情:权重之和是1;原始数据中,Sally的距离是Tara的二分之一,这点在权重中体现出来了。
\n最后,我们求得平均值,也即预测Ben对Funky Meters的评分:
近邻算法又称为被动学习算法。这种算法只是将训练集的数据保存起来,在收到测试数据时才会进行计算。
\n贝叶斯算法则是一种主动学习算法。它会根据训练集构建起一个模型,并用这个模型来对新的记录进行分类,因此速度会快很多。
\n能够给出分类结果的置信度
\n它是一种主动学习算法
\n我们用符号P(h)来表示,即事件h发生的概率:
\nP(h|D)来表示D条件下事件h发生的概率。比如:P(女生|弗兰克学院的学生) = 0.86
\n计算的公式是:
下表是一些人使用笔记本电脑和手机的品牌:
使用iPhone的概率是多少?
如果已知这个人使用的是Mac笔记本,那他使用iPhone的概率是?
首先计算出同时使用Mac和iPhone的概率:
使用Mac的概率则是:
从而计算得到Mac用户中使用iPhone的概率:
为了简单起见,我们可以直接通过计数得到:
贝叶斯法则描述了P(h)、P(h|D)、P(D)、以及P(D|h)这四个概率之间的关系:
现实问题中要计算P(h|D)往往是很困难的
\n朴素贝叶斯计算得到的概率其实是真实概率的一种估计,而真实概率是对全量数据做统计得到的。
\n在朴素贝叶斯中,概率为0的影响是很大的,甚至会不顾其他概率的大小。此外,抽样统计的另一个问题是会低估真实概率。
\n解决方法是将公式变为以下形式:
n表示训练集中y类别的记录数;nc表示y类别中值为x的记录数。
\nm是一个常数,表示等效样本大小。
\n决定常数m的方法有很多,我们这里使用值的类别来作为m,比如投票有赞成和否决两种类别,所以m就为2。
\np则是相应的先验概率,比如说赞成和否决的概率分别是0.5,那p就是0.5。
\n标准差是用来衡量数据的离散程度的,如果所有数据都接近于平均值,那标准差也会比较小。
\n样本标准差的公式是:
我们把有限集合A的元素个数记为card(A)。例如A={a,b,c},则card(A)=3
\n正态分布、钟型曲线、高斯分布等术语,他们指的是同一件事:68%的数据会落在标准差为1的范围内,95%的数据会落在标准差为2的范围内:
概率计算公式:
假设我们要计算P(100k|i500)的概率,即购买i500的用户中收入是100,000美元的概率。之前我们计算过购买i500的用户平均收入(106.111)以及样本标准差(21.327),我们用希腊字母μ(读“谬”)来表示平均值,σ(读“西格玛”)来表示标准差。
xi = 100 指的是收入100k
e是自然常数,约等于2.718。
当我们使用已经标记好分类的数据集进行训练时,这种类型的机器学习称为“监督式学习”。文本分类就是监督式学习的一种。
\n如果训练集没有标好分类,那就称为“非监督式学习”,聚类就是一种非监督式学习
\n通过物品特征来计算距离,并自动分类到不同的群集或组中。
\n我们可以使用误差平方和(或称离散程度)来评判聚类结果的好坏,它的计算方法是:计算每个点到中心点的距离平方和。
上面的公式中,第一个求和符号是遍历所有的分类,比如i=1时计算第一个分类,i=2时计算第二个分类,直到计算第k个分类;第二个求和符号是遍历分类中所有的点;Dist指代距离计算公式(如曼哈顿距离、欧几里得距离);计算数据点x和中心点ci之间的距离,平方后相加。
\n前面我们提到k-means是50年代发明的算法,它的实现并不复杂,但仍是现今最流行的聚类算法。不过它也有一个明显的缺点。在算法一开始需要随机选取k个起始点,正是这个随机会有问题。
有时选取的点能产生最佳结果,而有时会让结果变得很差。k-means++则改进了起始点的选取过程,其余的和k-means一致。
以下是k-means++选取起始点的过程:
\nk-means++选取起始点的方法总结下来就是:第一个点还是随机的,但后续的点就会尽量选择离现有中心点更远的点。
\n"},{"title":"hadoop 上传文件时报 Checksum error","url":"/2018/hadoop-%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E6%97%B6%E6%8A%A5-Checksum-error/","content":"今天在用 hadoop 上传文件到 HDFS 时,报错:put: Checksum error: file:/home/magneto/fb_friend.csv at 0 exp: 1005486446 got: 441437096
。
经过 Google 发现是因为当前目录下存在一个名为:.fb_friend.csv.crc
的文件,将此文件删除后即可成功上传。
究其原因是因为 Hadoop 的 CRC 数据校验机制,Hadoop 系统为了保证数据的一致性,会对文件生成相应的校验文件,并在读写的时候进行校验,确保数据的准确性。
\n在上传的过程中,Hadoop 将通过 FSInputChecker 判断需要上传的文件是否存在进行校验的 crc 文件,即 .fb_friend.csv.crc
,如果存在 crc 文件,将会对其内容一致性进行校验,如果校验失败,则停止上传该文件。
在使用 hadoop fs -getmerge srcDir destFile
命令时,本地磁盘一定会生成相应的 .crc
文件。
所以如果需要修改 getmerge
获取的文件的内容,再次上传到 DFS 时,可以采取以下 2 种策略进行规避:
删除 .crc
文件
将 getmerge
获取的文件修改后重新命名,如使用 mv
操作,再次上传到 DFS 中。
我是个喜聚不喜散的人,而且我觉得这和喜不喜欢social没有关系。聚了、认识了、熟悉了就不想再散。
\n黛玉也说过:“人有聚就有散,聚时欢喜,到散时岂不清冷?既清冷,则生伤感,所以不如倒是不聚的好。比如那花开时令人爱慕,谢时则增惆怅,所以倒是不开的好。”
\n在经过5、6两个月努力招聘后,我的小组加上我在内有9个人,本来应该是10个但被砍了一个HC。看起来一切都在步入正轨,但上周老板通知考虑到我们大部门的成本问题和另一个创新部门的业务扩张,我们需要为那边提供十几个后端人力,我们团队也要背一个名额,让我从团队中挑选一个合适的人。
\n当时也没有定好具体过去的时间,我们只是大致讨论了人选,因为还没实际执行,那时候我对这件事还没太大的想法。今天下班前突然通知本周内就要调过去,需要尽快沟通。我本来打算后天再和那个同学沟通,今天先问了问他手中的项目进展,提醒了一下他尽量不要延期。最后他问我是不是给他排了其他工作,此刻我当然可以说没有,然后等后天再和他说这件事,但我觉得我不应该为了让他把需求做完或者只图自己一时心里安稳就骗他,应该实事求是坦诚相告,于是把他带到会议室沟通了前边的内容,唯一没有提到的是为了缩减这边的成本,只说了各种好处、那边的成长空间之类的。
\n沟通完后我能看出他失落的表情,说实话我自己也非常失落,尽管这不是在聊裁员,但我还是很失落,感觉是自己没有保护好队友。我们一起磨合了一个多月,帮他度过过了onboarding期,他马上就要大展宏图做一些重要工作的时候被调走了。
\n和他沟通过程中也能感觉到自己没有那么坚定,如果真的让我和一个同学聊优化的事情也许会更难过吧。
\n回到开头黛玉那句话,如果我知道有这次的散,我还会招进那个人吗?我可能还是会的,虽然相处时间不是很长,但对他来说还是有成长,对我们团队来说他也确实有相应的贡献,而且整个过程中也是比较愉悦的。
\n"},{"title":"提供健康的工作环境","url":"/2023/health-work-environment/","content":"近三个月我的团队除了入职一位去年招到的校招生外,还通过社招渠道加入了4位新同学,他们中有两位上家公司就职于字节跳动、一位上家就职于快手、一位上家就职于小米。
\n从大厂跳到中小厂一般几个原因:
\n关于第1点,跳槽的前提条件一般都是薪资待遇不能比之前差,既然他们选择了跳槽,说明第1点现阶段是基本满足了,再往后还要看自己的能力和公司的发展。
\n关于第2点,我目前的团队业务包含了公司的核心场景和推荐工程领域,有非常多的事情可以做,有足够的挑战和 scope 来提供给大家。
\n关于第3点,之前我做不了主,现在有了一定可以做主的空间,我会给团队提供一个我认为是比较的健康工作环境,具体有以下几点:
\n尽管我会在可行范围内提供比较好的工作环境,且在绩效考核时我更看重的是结果,不会用类似不够「不卷」的原因来打差绩效,但有一点要说明的事实就是,通常在工作上投入更多时间确实更有可能取得好的结果。我提供了健康工作的可选性,至于更看重什么、最终如何选择还要看个人,比如现阶段你更追求金钱个个人成功,那把大部分精力放在工作上没有问题,好的绩效确实可以带来客观的现金回报。我也希望每个人可以现在跟长远的角度来思考这个问题。
\n我的某些行为有点像宝玉在守护着大观园中的姐姐妹妹,不知道这种环境是会豢养出偷虾须镯的坠儿,还是会培育出香菱那样的诗仙。虽然不知道最终会结出什么果,但我会坚持自己认为正确的事。
\n"},{"title":"Homebrew 修改国内源","url":"/2020/homebrew-china-mirror/","content":"\nHomebrew 是 Mac 上最常用的软件包管理工具,可以简化 macOS 系统是的软件安装过程。但由于国内网络环境,每次更新时速度都不忍直视。为了提升速度和体验,建议修改为国内源。
\n目前有两个常用 Homebrew 源,分别是阿里镜像和清华大学镜像。其中清华镜像在阿里镜像已有的 brew
和 homebrew-core
之外,还额外提供了 homebrew-cask
源。所以我采用的策略是:brew
和 homebrew-core
使用阿里的,homebrew-cask
使用清华的,原因是阿里在程序员心目中的地位是要高于清华的。
直接复制以下命令在终端运行即可:
\ngit -C "$(brew --repo)" remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git |
没有安装使用 homebrew-cask
的情况下,最后一条命令会报错,可以忽略。
之后执行 brew update
使配置生效并测试工作是否正常。
Homebrew 还提供了一个核心组件 Homebrew-bottles,可以提供一些包的二进制预编译版本,省去本地下载源码、编译源码的时间,提升安装效率,所以可以把 Homebrew-bottles
的源地址也进行替换,Homebrew-bottles
的地址是通过环境变量加载的,所以有两种修改方式:
临时生效:
\nexport HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles |
永久生效(以 zsh
为例):
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc |
报错内容:
\nERROR:root:code for hash md5 was not found. |
修复方法:
\n执行:ls /usr/local/Cellar/openssl
,可以看到当前可用的 openssl
版本:
➜ ls /usr/local/Cellar/openssl |
根据列出的版本,执行 brew switch openssl <版本号>
来指定版本(有可能在你本地只存在一个版本或和我这里有其他区别):
➜ brew switch openssl 1.0.2q |
问题解决~
\nPython 程序在连接 MySQL 时,报错:
\nTraceback (most recent call last): |
你的报错可能和我这里稍有区别,主要是倒数第三行,我这里是 libmysqlclient.20.dylib
,你那里可能是 libmysqlclient.18.dylib
或其他的,不过理论上都可以通过这个方法解决。
修复方法:
\n执行 ls /usr/local/lib | grep libmysqlclient
,我这里可以看到如下内容:
libmysqlclient.21.dylib -> ../Cellar/mysql/8.0.19/lib/libmysqlclient.21.dylib |
查看列表中有没有和报错中完全相同的文件,如果存在完全匹配的就直接建立对应软链到 /usr/local/opt/mysql/lib/
,没有的话就用 libmysqlclient.dylib
代替。
我这里没有 libmysqlclient.20.dylib
,所以我使用的命令如下:
ln -s /usr/local/lib/libmysqlclient.dylib /usr/local/opt/mysql/lib/libmysqlclient.20.dylib |
问题解决~
\n参考:
\n\n\n以下内容以时间轴顺序进行罗列,不作人名、地名的分类。
\n
虽我未学,下笔无文,又何妨用假语村言,敷演出一段故事来,亦可使闺阁昭传,复可悦世之目,破人愁闷,不亦宜乎?”故曰“贾雨村云云。
\n原来女娲氏炼石补天之时,于大荒山**无稽崖炼成高经十二丈,方经二十四丈顽石三万六千五百零一块。娲皇氏只用了三万六千五百块,只单单剩了一块未用,便弃在此山青埂峰**下。谁知此石自经煅炼之后,灵性已通,因见众石俱得补天,独自己无材不堪入选,遂自怨自叹,日夜悲号惭愧。
\n这阊门外有个十里街,街内有个仁清巷,巷内有个古庙,因地方窄狭,人皆呼作葫芦庙。庙旁住着一家乡宦,姓甄,名费,字士隐。
\n因这甄士隐禀性恬淡,不以功名为念,每日只以观花修竹、酌酒吟诗为乐,倒是神仙一流人品。只是一件不足:如今年已半百,膝下无儿,只有一女,乳名唤作英莲,年方三岁。
\n后来既受天地精华,复得雨露滋养,遂得脱却草胎木质,得换人形,仅修成个女体,终日游于离恨天外,饥则食蜜青果为膳,渴则饮灌愁海水为汤。
\n这士隐正痴想,忽见隔壁葫芦庙内寄居的一个穷儒──姓贾名化、表字时飞、别号雨村者走了出来。这贾雨村原系胡州人氏,也是诗书仕宦之族,因他生于末世,父母祖宗根基已尽,人口衰丧,只剩得他一身一口,在家乡无益,因进京求取功名,再整基业。自前岁来此,又淹蹇住了,暂寄庙中安身,每日卖字作文为生,故士隐常与他交接。
\n真是闲处光阴易过,倏忽又是元宵佳节矣。士隐命家人霍启抱了英莲去看社火花灯,半夜中,霍启因要小解,便将英莲放在一家门槛上坐着。待他小解完了来抱时,那有英莲的踪影?急得霍启直寻了半夜,至天明不见,那霍启也就不敢回来见主人,便逃往他乡去了。
\n方才在咱门前过去,因见娇杏那丫头买线,所以他只当女婿移住于此。
\n子兴道:“便是贾府中,现有的三个也不错。政老爹的长女,名元春,现因贤孝才德,选入宫作女史去了。二小姐乃赦老爹之妾所出,名迎春;三小姐乃政老爹之庶出,名探春;四小姐乃宁府珍爷之胞妹,名唤惜春。
\n只眼前现有二子一孙,却不知将来如何。若问那赦公,也有二子,长名贾琏,今已二十来往了,亲上作亲,娶的就是政老爹夫人王氏之内侄女,今已娶了二年。
\n贾不假,白玉为堂金作马。
阿房宫,三百里,住不下金陵一个史。
东海缺少白玉床,龙王来请金陵王。
丰年好大雪,珍珠如土金如铁。
门子笑道:“不瞒老爷说,不但这凶犯的方向我知道,一并这拐卖之人我也知道,死鬼买主也深知道。待我细说与老爷听:这个被打之死鬼,乃是本地一个小乡绅之子,名唤冯渊,自幼父母早亡,又无兄弟,只他一个人守着些薄产过日子。
\n警幻冷笑道:“此香尘世中既无,尔何能知!此香乃系诸名山胜境内初生异卉之精,合各种宝林珠树之油所制,名‘群芳髓’。”宝玉听了,自是羡慕而已。
\n警幻道:“此茶出在放春山遣香洞,又以仙花灵叶上所带之宿露而烹,此茶名曰‘千红一窟’。”宝玉听了,点头称赏。因看房内,瑶琴、宝鼎、古画、新诗,无所不有,更喜窗下亦有唾绒,奁间时渍粉污。
\n更不用再说那肴馔之盛。宝玉因闻得此酒清香甘冽,异乎寻常,又不禁相问。警幻道:“此酒乃以百花之蕊,万木之汁,加以麟髓之醅,凤乳之曲酿成,因名为‘万艳同杯’。”宝玉称赏不迭。
\n如尔则天分中生成一段痴情,吾辈推之为‘意淫’。‘意淫’二字,惟心会而不可口传,可神通而不可语达。汝今独得此二字,在闺阁中,固可为良友;然于世道中未免迂阔怪诡,百口嘲谤,万目睚眦。今既遇令祖宁荣二公剖腹深嘱,吾不忍君独为我闺阁增光,见弃于世道,是以特引前来,醉以灵酒,沁以仙茗,警以妙曲,再将吾妹一人,乳名兼美字可卿者,许配于汝。
\n\n\n意淫二字最早也是处于此处
\n
说着,果然出去带进一个小后生来,较宝玉略瘦些,眉清目秀,粉面朱唇,身材俊俏,举止风流,似在宝玉之上,只是怯怯羞羞,有女儿之态,腼腆含糊,慢向凤姐作揖问好。凤姐喜的先推宝玉,笑道:“比下去了!”便探身一把携了这孩子的手,就命他身傍坐了,慢慢的问他:几岁了,读什么书,弟兄几个,学名唤什么。秦钟一一答应了。
\n谁知到穿堂,便向东向北绕厅后而去。偏顶头遇见了门下清客相公詹光**单聘仁**二人走来,一见了宝玉,便都笑着赶上来,一个抱住腰,一个携着手,都道:“我的菩萨哥儿,我说作了好梦呢,好容易得遇见了你。”说着,请了安,又问好,劳叨半日,方才走开。
\n可巧银库房的总领名唤吴新登与仓上的头目名戴良,还有几个管事的头目,共有七个人,从帐房里出来,一见了宝玉,赶来都一齐垂手站住。独有一个买办名唤钱华,因他多日未见宝玉,忙上来打千儿请安,宝玉忙含笑携他起来。
\n如今何不用计制伏,又止息口声,又伤不了脸面。”想毕,也装作出小恭,走至外面,悄悄的把跟宝玉的书童名唤茗烟者唤到身边,如此这般,调拨他几句。
\n衣裳任凭是什么好的,可又值什么,孩子的身子要紧,就是一天穿一套新的,也不值什么。我正进来要告诉你:方才冯紫英来看我,他见我有些抑郁之色,问我是怎么了。我才告诉他说,媳妇忽然身子有好大的不爽快,因为不得个好太医,断不透是喜是病,又不知有妨碍无妨碍,所以我这两日心里着实着急。
\n里面凤姐见日期有限,也预先逐细分派料理,一面又派荣府中车轿人从跟王夫人送殡,又顾自己送殡去占下处。目今正值缮国公诰命亡故,王邢二夫人又去打祭送殡,西安郡王妃华诞,送寿礼,镇国公诰命生了长男,预备贺礼,又有胞兄王仁连家眷回南,一面写家信禀叩父母并带往之物,又有迎春染病,每日请医服药,看医生启帖,症源,药案等事,亦难尽述。
\n贾蔷又近前回说:“下姑苏聘请教习,采买女孩子,置办乐器行头等事,大爷派了侄儿,带领着来管家两个儿子,还有单聘仁,卜固修两个清客相公,一同前往,所以命我来见叔叔。”贾琏听了,将贾蔷打谅了打谅,笑道:“你能在这一行么?这个事虽不算甚大,里头大有藏掖的。”贾蔷笑道:“只好学习着办罢了。”
\n贾珍因想着贾蓉不过是个黉门监,灵幡经榜上写时不好看,便是执事也不多,因此心下甚不自在。可巧这日正是首七第四日,早有大明宫掌宫内相戴权,先备了祭礼遣人来,次后坐了大轿,打伞鸣锣,亲来上祭。贾珍忙接着,让至逗蜂轩献茶。贾珍心中打算定了主意,因而趁便就说要与贾蓉捐个前程的话。
\n此一匾一联书于正殿“大观园”园之名。“有凤来仪”赐名曰“潇湘馆”。“红香绿玉”改作“怡红快绿”即名曰“怡红院”。“蘅芷清芬”赐名曰“蘅芜苑”。
\n这里林黛玉见宝玉去了,又听见众姊妹也不在房,自己闷闷的。正欲回房,刚走到梨香院墙角上,只听墙内笛韵悠扬,歌声婉转。
\n贾芸出了荣国府回家,一路思量,想出一个主意来,便一径往他母舅卜世仁家来。原来卜世仁现开香料铺,方才从铺子里来,忽见贾芸进来,彼此见过了,因问他这早晚什么事跑了来。
\n一时,只见一个小丫头子跑来,见红玉站在那里,便问道:“林姐姐,你在这里作什么呢?”红玉抬头见是小丫头子坠儿。红玉道:“那去?”坠儿道:“叫我带进芸二爷来。”说着一径跑了。这里红玉刚走至蜂腰桥门前,只见那边坠儿引着贾芸来了。
\n丫头方进来时,忽有人来回话:“傅二爷家的两个嬷嬷来请安,来见二爷。”宝玉听说,便知是通判傅试家的嬷嬷来了。那傅试原是贾政的门生,历年来都赖贾家的名势得意,贾政也着实看待,故与别个门生不同,他那里常遣人来走动。
\n谁知就有一个不知死的冤家,混号儿世人叫他作石呆子,穷的连饭也没的吃,偏他家就有二十把旧扇子,死也不肯拿出大门来。
\n门下庄头乌进孝叩请爷、奶奶万福金安,并公子小姐金安。新春大喜大福,荣贵平安,加官进禄,万事如意。
\n原来贾赦已将迎春许与孙家了。这孙家乃是大同府人氏,祖上系军官出身,乃当日宁荣府中之门生,算来亦系世交。如今孙家只有一人在京,现袭指挥之职,此人名唤孙绍祖,生得相貌魁梧,体格健壮,弓马娴熟,应酬权变,年纪未满三十,且又家资饶富,现在兵部候缺题升。
\n因他家多桂花,他小名就唤做金桂。他在家时不许人口中带出金桂二字来,凡有不留心误道一字者,他便定要苦打重罚才罢。
\n两人正说着,门上的进来回道:“江南甄老爷到来了。”贾政便问道:“甄老爷进京为什么?”那人道:“奴才也打听了,说是蒙圣恩起复了。”贾政道:“不用说了,快请罢。”那人出去请了进来。那甄老爷即是甄宝玉之父,名叫甄应嘉,表字友忠,也是金陵人氏,功勋之后。原与贾府有亲,素来走动的。
\n这个启发来自「谐星聊天会」的第三季第 6 期,大概 46 分钟左右家宇提出的。
\n在红楼梦中有这么一回,贾瑞勾引王熙凤后被王熙凤下了「相思局」,致使贾瑞卧床不起。破足道人为了救他给了他一把风月宝鉴,也就是一面镜子,并告诉贾瑞只能看镜子背面,不要看正面,但是贾瑞不听劝告,非要看正面,每一次看正面就看到凤姐在里边搔首弄姿勾引他,笑盈盈的招手让他进去和她交欢,这么几十次后,贾瑞就因下溺连精,精尽人亡而死。
\n贾瑞明知道把镜子翻过来能治自己的病,可是翻过来看到的是个骷髅,而另一面有个王熙凤在里边扭动,他在欲望面前无法控制自己,最后使自己命丧黄泉。
\n这个就很像我们现在刷短视频,比如快手、抖音这些。我们一刷就几个小时过去了,刷完之后很有负罪感。短视频是这个时代的精神鸦片,过一段时间不刷心里就会发痒,刷之前认为自己有足够的控制力,只刷几分钟就停下来。刷的过程中也明知道自己只要把手机扣过来就可以解决问题,可自己就是控制不住不停的往下刷,像贾瑞一样不停的看镜子中虚幻的王熙凤。贾瑞丢掉的是性命,我们何尝不是在消耗自己的生命呢。
\n跛足道人作为红楼梦中菩萨的象征,是无法直接救贾瑞和我们的,菩萨只能点化我们,能救赎我们的只有自己。
\n"},{"title":"CDN 是如何工作的?","url":"/2022/how-cdn-work/","content":"维基百科给 CDN 的定义如下:
\n\n\n内容分发网络(Content Delivery Network 或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。
\n
我们用更精简的语句概括一下:CDN 是一种利用分布在各个地理位置的服务器来提供快速内容交付的技术。
\n假如住在北京的小贾想要访问一个部署在杭州的电商网站,如果这个请求历经大半个中国进入位于杭州的服务器再返回,响应会非常慢。因此,那个电商网站可以在小贾居住地附近部署 CDN 服务器,网站的内容将从附近的 CDN 服务器加载。
\n下图说明了这个过程:
\n\n\n\n\n在判断列表是否为空时,你更喜欢哪种方式?决定因素是什么?
\n
在 Python 中有很多检查列表是否是空的方式,在讨论解决方案前,先说一下不同方法涉及到的不同因素。
\n我们可以把判断表达式分为两个阵营:
\n这是什么意思?
\n我们从显式比较开始说起,无论我们使用列表符号 []
还是声明空列表的函数 list()
,遵循的策略是查看待检查列表是否与空列表完全相等。
# 都是用来创建空列表 |
另外,我们可以使用 len() 函数返回列表中的元素个数。
\na = [] |
和显式比较相反,隐式求值遵循的策略是:将空列表求值为布尔值的 False
,将有元素填充的列表求值为布尔值的 True
。
a = [] |
那么,显式比较和隐式求值有什么区别呢?
\n很多人习惯于使用显式比较的方式。但是如果你遵循鸭子类型的设计风格,那么会更加偏向于使用的是隐式方法。
\n「鸭子类型」这个词来自以下短语:
\n\n\n当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
\n
从功能上讲,这是对对象实际数据类型压力较小的一种确认。在鸭子类型中,关注点在于对象的行为,能做什么(比如,可迭代 iterable);而不是关注对象所属的类型。鸭子类型在动态语言中经常使用,非常灵活。
\n鸭子类型优先考虑便利性而非安全性,从而可以使用更灵活的代码来适应更广泛的用途,它不会像传统方式那么严格。
\n当我们越了解隐式求值,就越倾向于使用这种方式,因为我们知道空列表将被求值为 False
。
a = [] |
这使得我们可以合并那些很长的检查表达式,如:
\n# 之前 |
当然,最终的选择还取决于本次判断的意图:
\n\n\n\n编程是一种技能,可以让我们不断提升和学习新知识。
\n
编程是一门永远学不完的手艺。我们无法掌握所有与编程相关的主题,因为这会涉及太多的内容。如果想要自己不断进步,需要保持开放的思维,不断获取新的知识,并接受无法掌握全部知识的事实,让自己每天都有进步就够了。
\n可以通过以下三种方式实现这一目标。
\n编码是一项与其他技术一样的技能。想要把它做好,需要大量的练习和努力。没有人会在一觉醒来后就突然变得擅长编码。所有优秀的工程师都夜以继日地工作,以完善他们的编码技能。无论你在做什么项目,用的什么编程语言,都要养成每天编写代码的习惯 —— 重要的是每天都要写代码。
\n\n不要只是写代码,尝试阅读其他程序员的代码,与其他程序员讨论代码,并尝试寻找高手来 review 你的代码。编程是一门技术精湛的手艺,不能仅仅通过学习语法规则就能精通这门手艺,只有不断的练习和反思,才能取得好成绩。
\n大学课程中引入多种编程语言是有原因的,编码知识通过语言进行传播。例如,熟悉 Java 语言的面向对象编程使你更容易理解 Go 语言中的概念,因为一些相同的编码概念适用于这两种语言。
\n\n当我们从多种语言中学习到不同的概念时,编程才开始真正地有趣起来。我从 Go 中学到结构体,从 Python 中学到了函数式编程,从 Java 中学到了面向对象编程。将多种语言的特性结合起来无疑有助于我巩固整体思维格局,并使我在编程方面做得更好。不要局限在一个小角落,经常尝试和探索未知的事物,哪怕觉得自己什么都不知道也没关系,毕竟吸收新的信息是我们学习的唯一方式。
\n\n\n人最害怕的不是自己什么都不会,而是自己不知道自己不会。
\n
听说过门徒效应吗?这是一种通过教别人来学习的有趣方式。门徒效应是一种现象,在这种现象中,教授或准备将知识传授给他人可以帮助一个人学习这些知识。
\n教授一门课程意味着你必须从不同的角度来掌握它,因为你不知道学生已经掌握了多少。因此,你需要假设学生对该主题了解不多,同时意味着你必须从最基础的知识开始教学。而教授基础知识的唯一方法是你要彻底搞懂基础知识。
\n\n通过教学来学习可以借鉴小黄鸭调试法。有证据表明,教一个无生命的物体可以提高对所教知识的理解和掌握。
\n我们可以从小事开始,试着每天帮助一个人:在 GitHub 上挑选一个 issue 并解决它。为了尽可能多地学习和帮助他人,也可以在 SegmentFault 或 StackOverflow 上回答问题。
\n尽管编程很难掌握,但它非常有趣。问问自己:如果你真的想掌握编程,是否愿意付出额外的努力?我想你已经知道答案了。
\n"},{"title":"如何让生活更轻松","url":"/2023/how-to-make-life-easier/","content":"给自己设立一些值得期待的事情,比如:
\n有了期待,每一天就会有盼头。引用《基督山伯爵》的一句话:“人类的一切智慧是包含在这四个字里面的:’等待’和’希望’!”
\n自己完成一件工作后一定要奖励一下自己,可以是奖励自己一个包包、一块手表。或者简单一些的,一个项目上线后,奖励自己一杯咖啡、奶茶都可以。
\n在你完成自己觉得很有成就感的事情后,你的老板、同事不一定能给你即时的正向反馈,我们可以自己给自己即时正反馈。
\n建立及时正反馈很有必要的,只有这样才能积小胜成大胜。
\n奖励自己的时机并不限于完成了一个工作之后,当自己失落、状态不好时也可以奖励自己。比如今天早上下着雨,我来公司的路上淋湿了,加上昨天晚上有些没睡好,心情有些糟糕,所以到了公司楼下去瑞幸点了一杯自己爱喝的咖啡。
\n对自己好一点,先学会爱自己,才能好好爱别人。
\n尽管我们一天当中大部分时间都在公司度过,但这并不意味着在公司的一定要把全部精力投在公司的工作上,要有自己可以掌控的时间。
\n我这里并不是说要让大家在公司接私活,主要指的利用有限的时间高效工作,留出一些时间来提升自己。
\n拿我自己为例,我每天有效的工作时间只有4小时,超过这个时间工作效率会很低下,所以我会给自己安排一些工作工作以外属于自己的事情,比如读书、写 leetcode、学习一些专项课程,最近还加入的写流水账的时间。这些事情和工作都是相辅相成的。
\n这一点在职场中太重要了,同事、领导交给你的任务不要全盘接收。接的工作太多,哪一个也完成不好,最后还搞的自己压力巨大、身心俱疲。
\n现在我会参与每周的需求会,根据人力情况决定下周接几个需求,在已经得知人力不足的情况下,我会好不犹豫把需求拒掉,同时也不会给每个同学排的太紧,结合上一条,我会刻意给他们留出一些自己的时间。
\n要把自己遇到的困难告诉领导,俗话说会哭的孩子有奶吃。
\n关于说不这件事,我想再用个其他人没有用过的角度补充一点:不要觉得自己比别人聪明,屁股决定脑袋,别人不说是因为他的屁股没有在那个位置上。用宝钗的处事哲学就是「事不关己莫开口,一问摇头三不知」。尤其是在人多的场合开会的时候,不要做话唠,不要试图在他人面前证明自己。与其给一个模糊不清的信息,倒不如大方承认自己不知道。
\n中文翻译为:允许自己为人,听起来感觉有些别扭。
\n放下过度控制的欲望,接受不能改变的事。过去的事已成过往,许可、接受已经发生的事。
\n允许自己有焦虑、烦恼、悲伤或不快乐。失望、烦乱、悲伤是人性的一部分。接纳这些,并把它们当成自然之事,允许自己偶尔的失落和伤感。然后问问自己,能做些什么来让自己感觉好过一点。
\n想休息的时候就休息休息,想堕落的时候也可以偶尔堕落一下,不要把自己逼得太紧,承认自己某些方面就是不行,比如现在我去理发店,他们再给我推荐办卡时,我会很坦诚的承认自己没有钱,不再找各种借口。
\n"},{"title":"如何写作","url":"/2022/how-to-write/","content":"\n翻译自:https://www.swiss-miss.com/2019/09/how-to-write-by-elizabeth-gilbert.html
\n1) 向某人讲述你的故事。挑选一个你爱的或钦佩的或想与之联系的人,把整个故事直接写给他们——就像你在写一封信。这将带来你的自然声音。无论你做什么,都不要写信给人群。
\n(想象我们所写的内容是在给一个喜欢的姑娘或者我钦佩的人写信,而不是写给一群人的说教。)
\n2) 从故事的开头开始,写出发生的事情,一直写到最后。
\n(有头有尾,循序渐进)
\n3) 使用极其简单的句子。
\n(能说清楚事情就行,不追求华丽的辞藻)
\n4) 不要担心它是否好;只要完成它。无论你的项目是否好,结束后你会成为一个不同的人,这总是值得做的。
\n(完成比完美更重要,只要完成就会有成长。写作不是一蹴而就的,而来你有的是机会来修改、打磨它。)
\n5) 不要以改变任何人的生活为目的而写作。这将导致沉重的、令人恼火的散文。只需分享令你高兴、愤怒或着迷的东西。如果有人的生活因此而改变,那是一种奖励。
\n(写作的目的不是要改变其他人,而是记录自己的所思所想。曹雪芹的《红楼梦》中从来没有批判过一个人的好坏,只是在做客观的描写。)
\n6) 只要你可以,就讲故事而不是解释东西。人类喜欢故事,而我们讨厌别人向我们解释东西。以耶稣为例。他几乎只用比喻说话,并允许每个人从他伟大的讲故事中吸取自己的教训。而且他做得非常好。
\n(多讲故事,人们更容易记住有画面感的东西)
\n7) 你的作品不必是任何特定的长度,或为任何特定的市场而写。它甚至不一定要被另一个人看到。如何以及是否出版你的作品是另一个问题。今天,就写吧。
\n(想写什么写什么,不追求写多长。)
\n8) 记住,您一直在研究自己的一生,只是因为存在。你是你自己经验中的唯一专家。拥抱这一点是你的最高资格。
\n(这句话没明白什么意思,是只有自己才了解自己的意思吗?)
\n9) 每位作家在第一天都是从同一个地方开始的:超级兴奋,并准备好做大事。第二天,每个作家都看着她在第一天写的东西,恨死自己了。专业作家和非专业作家区别在于,专业的作家在第三天回到他们的任务中。让你达到目的的不是骄傲而是怜悯。向自己表示宽恕,因为你不够好。然后继续前进。
\n(成功最大的威胁不是失败,而是倦怠。当你感到心烦意乱,苦不堪言或筋疲力竭时,是鼓足干劲还是萌生退意,这是专业人士和业余人士的分水岭。)
\n10) 愿意让它变得简单。你可能会感到惊讶。
\n(这句话也没有太理解,是想说我们应该把写作当成一件简单的事情去做的意思吗?)
\n"},{"title":"HTTPie介绍——一个轻量级HTTP客户端","url":"/2019/httpie-introduce/","content":"\n\n\nHTTPie 是一个用于与 HTTP 服务器进行交互的命令行客户端。
\n
HTTPie(发音为 H-T-T-派)是一个基于命令行的 HTTP 客户端,可以提供更加人类友好的命令行交互,HTTPie 可用于测试、调试以及与 HTTP 服务器进行交互。
\nHTTPie 提供了一个 http
命令,这个命令可以使用简单自然的语法发送任意 HTTP 请求,并以精美的彩色输出作为响应结果。
在这篇文章中,我们将学习如何使用此工具访问 REST 服务。
\n作为一个现代化命令行工具,HTTPie 提供了如下功能:
\n在后边的文章中,你将看到这些功能的介绍。
\n可以通过多种方式来安装 HTTPie。
\nbrew install httpie |
apt-get install httpie |
pip install --upgrade pip setuptools |
或者
\neasy_install httpie |
现在 HTTPie 已经安装在了本地电脑上,可以来调用各种 HTTP 接口。
\n后边的文章中,我将会使用下边三个网站来演示相关功能:
\nhttp
HTTPie 提供 http
命令来访问 HTTP 服务器。以下是 http
命令最基本的用法,返回了 HTTP 响应头和其他服务器信息。
➜ http httpie.org |
最常见的 HTTP 操作是从服务器检索信息,通常通过 HTTP GET 方法来实现。HTTP GET 请求的查询参数是可选的。
\n下边是一个 HTTPie 的 GET 方法示例(无查询参数):
\nhttp GET http://httpbin.org/get |
但是,不带查询参数的 GET 请求很少见。可以通过在原始请求后边追加 param==value
的方式来添加参数。
下边的示例演示了如何在 GET 请求中携带参数。我们来获取 userId 为 1 的所有帖子。
\n➜ http https://jsonplaceholder.typicode.com/posts userId==1 |
下边是多个参数的例子:
\n➜ http https://jsonplaceholder.typicode.com/posts userId==1 id==9 |
在 HTTP 请求头中携带信息是很常见的做法,在 HTTPie 中我们可以使用 Header:Value
格式添加 HTTP 请求头,如下所示:
http example.org X-Foo:Bar Sample:Value |
HTTP 的 POST 方法通常用于在服务器上创建资源,下边的示例演示了如何使用内联方式提供 JSON 数据并发送 POST 请求,注意:非字符串类型参数的格式为 Param:=Value
。
➜ http POST https://jsonplaceholder.typicode.com/posts title=foo body=bar userId:=9 |
HTTPie 允许我们将 JSON 数据存入文件中,并在命令行中指定这个文件的路径。
\n➜ cat data.txt |
HTTP PUT 方法通常用于更新服务器中已存在的资源,用法和 POST 类似:
\n➜ http PUT https://jsonplaceholder.typicode.com/posts/10002 data=@data.txt |
HTTP DELETE 方法用于删除 HTTP 服务器中的资源,示例如下:
\n➜ http DELETE https://jsonplaceholder.typicode.com/posts/1 |
上边的示例中我们演示了 HTTPie 的核心用法,在这些示例中,我们假设资源都是可以在不需要任何身份认证的情况下就能够访问的。但在实际场景中很少有这种情况,大多数服务都有安全防护,并强制要求它的用户在访问资源前进行身份认证。
\n现代化 HTTP 客户端程序为多种认证模式提供了很好的支持,HTTPie 也不例外,支持主流如:Basic、摘要、密码等认证类型。
\nHTTP Basic 认证是 HTTP 协议中的身份验证方案。在 Basic 认证中,HTTP Authorization
请求头设置为 Basic
,用户名和密码以明文形式提供。Basic 认证总是需要配合其他安全机制,如:HTTPS。
以下示例演示如何访问一个要求用户通过 Basic 认证来进行身份验证的资源:
\n➜ http --default-scheme=https https://httpbin.org/basic-auth/username/password |
可以看到,请求在未提供用户名和密码的情况下,服务器的响应状态码为 401 UNAUTHORIZED
。HTTPie 通过以 -a username:password
的方式提供 Basic 认证所需要的用户名密码:
➜ http --default-scheme https https://httpbin.org/basic-auth/username/password -a username:password |
Basic 认证的主要问题是它将用户名和密码以明文的方式发送至服务器。摘要认证略有不同,在摘要认证而非明文模式中,它采用基于哈希的方法与服务器传递凭据。
\n以下是摘要认证的流程:
\n401 Unauthorized
响应代码,并提供认证域(realm),以及一个随机生成的、只使用一次的数值,称为密码随机数 nonce。HTTPie 使用 -A 摘要标志
并通过 -a
参数提供相应的用户名和密码即可进行摘要认证,如下所示:
➜ http --default-scheme https -A digest -a aa:bb https://httpbin.org/digest-auth/auth/aa/bb |
插件方面,HTTPie 还支持其他身份验证机制,如:jwt-auth、OAuth 等。
\nHTTPie 是一个轻量但强大的工具,可以轻松与 HTTP 服务器通信。通过 http
命令并配合合理参数调用各种 HTTP 方法的能力使其成为 RESTful 和微服务生态的理想选择。
结论先行,先说说我是吃了什么药康复的:
\n是的,就这么些东西。
\n下边记录一些中间的过程。
\n12月13日,星期二
\n中午的时候嗓子开始不舒服,有点沙沙的感觉,而且腰有些酸,以为是坐姿有问题,总想躺着,多亏是在家办公,工作一会躺一会。
\n5:30左右测了个体温,37.3℃,没太当回事,但是明显感觉体力开始急剧下降。开完公司晚会后,7:30再次测了个体温 38.5℃,这时候已经完全不想动弹了,浑身发冷一直打寒颤,冷到想盖上10层棉被。
\n晚上挣扎着洗了澡,睡前吃了粒对乙酰氨基酚,钻被窝盖了两层被子,半昏半睡、睡一会醒一会,中间还出现过幻觉,虽然一晚上无法动弹没有测过体温,但我自己估摸着肯定到了40℃,晚上摸身上火烧火燎的,浑身疼,甚至蛋蛋也疼。。。
\n2022年12月14日,星期三
\n艰难的爬起床,浑身疼,那种疼像是跑了10公里步或者被揍了一顿似的,于是和老板请假,之后将手机通知关闭、静音。这一天除了吃饭喝水上厕所其余时间就是躺着,中间还吃了个黄桃罐头,冰冰凉凉的吃下去的时候很舒服。
\n\n就这么难受我还加持把今天的多邻国学习了,为了不破坏200多天的连胜记录😂
\n我把两层窗帘拉紧,灯关掉,屋里完全黑的,就这样睡一会、醒一会、刷一会小红书,这天还有些拉肚子,但不是很稀。晚上睡觉前吃了一粒对乙酰氨基酚。
\n晚上八点多睡的,到第二天早上5点,加上白天的时间,这应该是我近些年卧床时间最长的一次。
\n2022年12月16日,星期四
\n一觉醒来感觉舒服多了,测了下体温也基本退烧了,身上也没那么疼了,就是嗓子巨疼无比,开始咳嗽,咳嗽时喉咙和肺疼,能咳出浓痰。
\n考虑的目前是居家办公,也不用通勤,实在累了也可以躺会,于是就没有再请假,强行开机开始上班搬砖了。
\n下午的时候测了个抗原,阳气十足。
\n\n2022年12月16日,星期五
\n今天算起来是得病的第4天,嗓子有些疼、咳嗽,说话非常非常吃力且沙哑,除此之外就没有其他症状了。
\n嗅觉味觉还在,但是貌似不那么灵敏了,预计还需要3、5天才能转阴。
\n昨晚睡的有些晚,而且睡前玩了会手机,之前从来不玩,但是根据前两天生病的经验发现玩手机也能睡着。将近12点放下手机的时候感觉还是没有困意就开始看书,看到胳膊举不动书了放下书开始尝试入睡,两点多还是没睡着我意识到失眠了,于是起来吃了安眠药。
\n只有生病最严重的那两天我真正让自己放轻松了,不管再晚再难受也不会感觉有什么焦虑,不再考虑工作或者其他烦心的事情,可能是身体的本能告诉我狗命要紧,别考虑乱七八糟的了。
\n我得新冠后只耽误了一天工作,真是个合格的打工人。🙂
\nP.S. 我发现这几天都没有晨勃过了,可能是不行了吧。
\n我前几天续订了独库的2023全年阅读计划,今天刚好收到了一份读库提前送来的小礼物,如果今年让我推荐一本书的话,毫无疑问我会推荐读库,他不是一本书,而是一个每两个月发行一期的综合性人文社科读物,以中篇非虚构文章为主,内容包括传记、书评、影评、历史事件等。
\n\n\n因为一篇文章而种草的一辆摩托车
\n
四月份的时候读《读库2205》那一期的时候,有一篇文章介绍了本田超级幼兽的发展史,结果我就被种草了,当时查了一下刚好有一款新的幼兽要在中国上市,而且是全球最低的定价13000元,样式很复古,一看就是小巧精悍类型的,我特别喜欢。
\n\n(图片来自小红书,目前这个车还需要订购,预计2个月才能到货)
\n有摩托车的前提是先有个摩托车驾照,今晚(2023年6月12日)我要去趟山东德州,以特种兵的方式训练,24小时内拿到驾照。
\n祝我好运~
\n小幼兽我来了,男人至死是少年。
\n"},{"title":"解决 IDEA 启动非常慢和生成 getter setter 不是 public 的问题","url":"/2017/idea-slow-getter-and-getter-setter-public/","content":"今天在用 IDEA 运行 Spring Boot 项目的时候,每次重启都会卡住,过好一会才能恢复,同时 IDEA 底部显示 Finished, saving caches,经过 Google 找到了解决办法,但是不明白为什么这样能解决。
\n方法很简单,修改 hosts
文件,在里边 127.0.0.1
和 ::1
后边加上 <hostname>.local
,比如我电脑的 hostname 是 panmax,所以我的 host
文件修改完后为127.0.0.1 localhost panmax.local
...
::1 localhost panmax.local
重启 IDEA,发现已经不会卡顿了。
\n再有一个是我使用 IDEA 生成的 getter
和 setter
是 protected
的,我用同事电脑测了一下,他的生成的确是 public
的,经过如下设置改回了正常:
File | Settings | Editor | Code Style | Java |
改为 Public
即可。
在公司的 CentOS 上通过 yum 安装了一个 Java,但是使用时发现没有 jps 命令,解决方法是安装 jdk-devel 这个包,它提供了 jps 工具。
\n先查看有哪些可用的安装包:
\nyum list | grep jdk-devel
然后找到对应自己 Java 版本和系统的那个包进行安装:
\nsudo yum install java-1.8.0-openjdk-devel.x86_64
搞定~
\n"},{"title":"Linux 安装 node 和 npm","url":"/2019/install-node-and-npm-on-linux/","content":"网上介绍 Node 如何安装的文章数不胜数,但我还是决定自己写一篇记录一下,最主要的原因是网上的文章比较混乱,有的建议通过包管理工具安装,还有的让一步步编译源码来安装。
\n通过包管理工具安装的通常版本不会太新,通过源码安装的方式非常麻烦,还需要提前安装 gcc
之类的,只有极少部分良心博主介绍了通过二进制文件直接安装的方式,但操作上都不是特别规范。
网上已有的文章还有一个很严重的问题,就是没有考虑国内的网络环境,不管从 Node 官方下载源码包还是二进制包,都巨慢无比,所以我把已经下载好的包放在 CDN 上供自己和大家之后使用。同时我还提供了其他常用软件的安装包,如 Nginx,Java,Neo4j 等等,后边有机会列个清单出来,并准备长期维护更新版本。
\n下边进入正题:
\n\n\n我推荐以下操作在
\n/opt
目录下进行
wget http://developer.jpanj.com/node-v10.15.3-linux-x64.tar.xz
xz -d node-v10.15.3-linux-x64.tar.xz
tar -xvf node-v10.15.3-linux-x64.tar
node
目录出来\n\n这样做的好处是,未来升级版本非常方便,只需要更新这个软链就行
\n
ln -s ./node-v10.15.3-linux-x64 ./node
# echo $PATH |
可以看到我的列表中有:
\n/usr/local/bin
/usr/bin
大家约定成俗逻辑是:
\n/usr/bin
下面的都是系统预装的可执行程序,会随着系统升级而改变。/usr/local/bin
目录是给用户放置自己的可执行程序的地方所以我推荐将软链放在 /usr/local/bin
目录下:
ln -s /opt/node/bin/node /usr/local/bin/node |
[root@dc8 ~]# node -v |
\n\n\n
requests
是一个简单优雅的 Python HTTP 库,相较于 Python 标准库中的urllib
和urllib2
,requests
更加的便于理解使用。
由于某地区热点事件持续升温,我们的客户想要通过我们系统的搜索功能导出一批数据,目前我们的搜索结果是不支持导出的,并且搜索功能也是通过调用几个子服务后对数据进行了合并,所以无法直接通过 ElasticSearch
来捞数据。
我们在评估需求后,预计编写这个统计程序大概需要 1 天的时间,但是客户认为事态紧急,当天就要结果,我们本着顾客就是上帝的原则,又进行了一番讨论,结论是可以写一个类似爬虫的工具,来「爬取」我们自己的搜索接口来拿到这些数据。
\n用 Python
来实现最合适不过,而且我对编写爬虫也比较熟悉,所以就采用了最简单粗暴的方法:用 requests
包作为一个 HTTP Client
来收发请求,但是客户现场是个离线环境,之前我们也没有安装过 requests
,所以才有了本文:在离线环境中安装 requests
。
为方便后期使用,我将所有用到的文件打包在了一起,可直接解压使用,无需从网上东奔西走寻找资源。
\n压缩包内涉及到的文件如下:
\nsetuptools-41.1.0.post1.tar |
打包资源下载链接:http://developer.jpanj.com/requests-offline.tar.gz
\n解压 requests-offline.tar.gz
后进入 requests-offline 目录开始安装。
\n\n\n
setuptools
能帮助我们更好的创建和分发 Python 的包,尤其是具有复杂依赖关系的包。
tar -zxvf setuptools-41.1.0.post1.tar.gz |
\n\n\n
pip
是 Python 官方推荐的包管理工具。
tar -zxvf pip-19.2.2.tar.gz |
# CA 认证模块 |
tar -zxvf requests-2.22.0.tar.gz |
➜ python |
客户是下午两点半提出的需求,内部讨论好实现方案后三点半,我在准备好安装 requests
所需资源后直接奔赴现场,而不是在公司编码完再过去,因为我相信只要有我们的人到达现场客户就可以踏下心来了,同时我也想挑战一下自己,所以本次点亮了一个技能点:现场使用 vi 进行 coding 和 debug。
作为一个北漂或在大城市打工的人,您是否有过融入这个城市的瞬间?如果有,是在什么样的时刻产生的?
\n我感到自己真正融入北京,成为这座城市的一份子,是在拥有了自己的车之后。就像小时候搬家,到了一个陌生的胡同,真正融入环境的最好证明就是:和已经生活在胡同里新认识的小伙伴们一起奔跑玩耍。成年后在路上开车也有类似小时候在胡同里玩耍的感觉,身旁经过的车辆都成了我的玩伴。
\n夜晚驾车穿过一条条马路,看着两边的路灯从身后闪过,车内播放着自己喜欢的歌曲,边开车边欣赏这个城市,有一种很兴奋且奇妙的感觉。最有感觉的一次是2020年中旬,,那时候疫情还很严重,公司搬到了新的办公楼,有自己的停车场。虽然公司在二环,但因为还存在封锁和居家办公,路上的车不是很多,我尝试了一段时间开车上下班。一天晚上下班回家路上下起了大雨,我打开了雨刷,把车内音响音量开到最大,在暴雨中前行,除了雨声和音乐,其他什么声音都听不到。看着窗外的大雨,听着动感十足的乐曲,那一瞬间仿佛与这个城市产生了共鸣,我被车被小心翼翼的保护着,仿佛这个城市也在对我说:“好的,我允许你加入我们了,现在开始我们是同志了。”
\n因为有这辆车,在疫情严重到我居家办公,一家之主还在上班的时候,我每天早晚接送她上下班,路上车很少,天气晴的很好,早晚出门转一圈心情也很好。
\n因为有这辆车,周末的时候我们一家可以说走就走,去爬山、去野生动物园、去吃好吃的,不用再风吹日晒骑车、挤地铁或者花时间打车。
\n车成为我连结北京的纽带,让我更好融入这个城市。无论是穿梭于城市的大街小巷,还是在周末的远足中,我都能够更好的体验到这座城市的魅力,享受到这里的美好时光。
\n\n我的车并不高端,是一辆很普通的新能源比亚迪,我为拥有它而骄傲。到现在3年多了,跑了两万多公里,没有出过任何大问题,非常感谢它带着我在这个城市实现梦想,在一次次出行时为我保驾护航。
\n\n"},{"title":"有趣的概率","url":"/2023/interesting-probability/","content":"今天发生了一件哭笑不得的事情,一个我不认识的人申请加我微信,一开始我没有通过,问她有什么事,她又留言让我通过一下。
\n我的微信号只能通过一个6位数字来添加,也就是我的 QQ 号,因此,能够添加我的方式一般只有两种:一种是我们在同一个群里,另一种是我主动把我的号码告诉对方。通过手机号是无法添加我的。
\n这个陌生人一上来他就问我是不是快递员。我看到她添加我的方式为:通过 QQ 号搜索,我第一反应是 QQ 里的某个长年不用的群有人乱发小广告什么的导致的,或者我的 QQ 号在什么地方泄露了。
\n\n我告诉她我不是,接下来她发给我一张淘宝收货页面的截图,这个取件码可不就是我的 QQ 号嘛。
\n\n我跟她解释了那个6位数字是她的取件码,不是联系快递员的方式。看她的收货地址和朋友圈猜测她应该是偏远地区的家庭妇女,平时淘宝用的也不多,第一次收货不知道取件码怎么回事,以为是添加这个微信来取件。
\n这件事让我想到之前看到过的一个定理叫做:「无限猴子定理」。
\n\n\n让一只猴子在打字机上随机地按键,当按键时间达到无穷时,几乎必然能够打出任何给定的文字,比如莎士比亚的全套著作。
\n
任何随机现象,事件只要概率为正,不论概率值多小,都有可能发生。(这是不是也有点像墨菲定律?)
\n一部小说都有概率通过随机的方式被组合出来,区区6位数字命中的概率就更高了。生活就是因为充满了这么多随机事件才变得更丰富多彩。
\n生活中最难的就是如何辨别什么是偶然,什么是必然。我们期待把生活全部变成必然,但其实你会发现人的一生很短暂,我们一生的经历很难都是必然的。
\n"},{"title":"关于 IP 的 11 个问题","url":"/2021/ip-questions/","content":"32 位
\n比如, 127.0.0.1:
\n127 0 |
192.168.1.0 到 192.168.1.255
\n192.168.1.0/24 表示所有与 192.168.1.0 前 24 位相同的地址
\n有 2^(32-24)=256 个这样的 IP 地址
\n这被称为「CIDR 表示法」
\n是的,有两个
\n下面是 TCP 包的结构:
\n+-+-+-+-+-+-+-+-+--+- |
IP 头包含一个源和目的 IP 地址,一个 TTL,一个长度字段,以及一些其他东西。
\n是的
\n它的结构与 TCP 数据包相同,只是有一个 UDP 头而非 TCP 头。
\n不是
\nIP 报头含一个 IP 地址,但没有端口
\n例如,ICMP 数据包(ping 使用的数据包)有一个IP地址,但没有端口
\nTCP 和 UDP 数据包有端口
\n不允许
\n以下 3 个 IP 范围是为私有网络保留的:
\n127.0.0.0/8 也被保留用于同一台计算机内部的连接。
\n不是
\n192.168.1.123是本地 IP 地址,所以谷歌将无法联系到你的电脑
\n路由器会改写数据包,将数据包的源 IP 改写为计算机的公网 IP
\n这被称为「网络地址转换」 或 NAT(network address translation)
\n不行
\nIPv4 数据包的长度字段为 16 位,所以最大长度是 65535。
\n在实践中,数据包往往必须比这个数字更小(常见的限制是1500字节)。
\n目的IP地址
\n它们一般不使用数据包中的任何其他信息。
\n根据你的源 IP 地址
\n很多服务使用你的源 IP 地址来确定你在哪个国家。这就是为什么很多人使用 VPN:他们通过 VPN 进行代理,从而使数据包的源 IP 地址在一个不同的国家。
\n它被允许进行 63 次跳跃
\n这意味着在它完成了大约 63 跳(到 63 个服务器或路由器)之后,就不会被进一步发送。
\nTTL(time to live)字段的存在是为了避免数据包在互联网上陷入循环。
\n"},{"title":"jQuery实现网页无限上拉","url":"/2016/jQuery%E5%AE%9E%E7%8E%B0%E7%BD%91%E9%A1%B5%E6%97%A0%E9%99%90%E4%B8%8A%E6%8B%89/","content":"无限上拉,说起来很高端,实际就是 APP 里边的上拉加载更多。
\n现在做的一个小 Web 项目里刚好有这个需求,之前我做的 Web 应用都是通过翻页来查看其他内容的,没有做过这种加载更多的功能,所以刚好借这个机会接触下。
\n在没做之前,想的是加载更多可能就跟手机 APP 那样,通过 js
异步加载 json
数据,然后更改 DOM 来完成这个操作。于是我就昂首阔步开始做了,刚开始想通过 vue.js
来完成,整个页面都是通过 json 数据来渲染,后来遇到各种问题,(比如,通过url来过滤数据,/tag/xxx
过滤出 tag 为 xxx 的数据,但是没有找到非常便捷的方法来传递这个值给接口)所以放弃,再然后想要不就第一页通过 jinja
来渲染出来,剩下的页通过 jQuery
来加载,然后还是感觉各种麻烦。
这时候想到了谷歌,查找资料后发现一个 jQuery
插件是专门来实现这个需求的,而且实现方法跟我设想的完全不一样,大致原理是:就像做普通翻页那样,告诉它下一页的地址,再告诉它需要加载更多部分的节点,这个插件会异步请求那个页面,然后把相应部分取出,加载到当前页面的底部。
我了个擦,我居然没有想到这种方法,我之前设想方法,还要单独去写个用来翻页的接口,增加了很多工作量,这种方法简直是棒呆了!
\n下边来介绍一下这个插件的使用:
\n插件名称:infinite-scroll
\n项目地址: Infinite Scroll jQuery Plugin
\n参考地址: http://ifxoxo.com/jquery-infinite-scroll.html
\n首先要在页面底部新增一个类似于下一页按钮的部分,这个部分用什么包裹都可以,但最里边需要有个a标签,href对应的是下一页的地址。例如: <a id="next" href="?page=2"></a>
我这里什么也没有包裹,而且 a
标签里也没加文字,这样翻到底部时看不到任何提示信息。这里可以自行写个 div
什么的,里边写着上拉加载更多这样的提示信息。当加载更多被触发时,这个部分会自动隐藏掉。
下边来看看 js
代码部分:
$(document).ready(function () { |
因为我这里需要提取出来加载更多的部分是这样的:
\n<div class="col-xs-12 section"> |
所以我的 itemSelector
的值为 div.section
。
还有一点,我这个用这个插件的时候,刚开始的时候一直有问题,是因为没有给 path
写值, path
的作用是每次加载下一页的时候所对应的地址。
还可以给加载更多时候的 loading 编写样式,例如:
\n#infscr-loading { |
至此,我又 get 到一个新技能。
\n","categories":["Code"],"tags":["JS","jQuery","Web"]},{"title":"Java 8 StringJoiner 示例","url":"/2019/java-8-stringjoiner/","content":"在 Java8 中,java.util
包中引入了一个新类 StringJoiner
。利用这个类,我们可以使用指定分隔符连接多个字符串,还可以为最终字符串添加前缀和后缀。
下边介绍一些 StringJoiner
类的示例。
在这个例子中,我们使用 StringJoiner
连接多个字符串,在创建 StringJoiner
实例时我们将分隔符指定为连字符(-
)。
import java.util.StringJoiner; |
输出:
\n张三-李四-王五-赵六 |
import java.util.StringJoiner; |
输出:
\n(张三,李四,王五,赵六) |
import java.util.StringJoiner; |
输出:
\nFirst String: (张三,李四,王五,赵六) |
在上边的例子中我们学习了 StringJoiner
类的 add()
和 merge()
方法,再来看看这个类的其他方法。
import java.util.StringJoiner; |
输出:
\nDefault String: 这是个默认字符串 |
\n\n工欲善其事,必先利其器
\n
算起来我用 Macbook 也有三年多的时间了,中间由于一次工作原因,开发环境一直有问题,就重装了一次系统,也仅仅这一次,而且这次其实也是冤枉了系统,当时是因为我的 host 文件没有配置正确导致的,可以说 macOS 是相当稳定的,并且也是开发利器,我写这篇文章的目的主要是为了之后自己在换新的 Macbook 时有记录可寻,同时帮助其他 Macbook 用户来发现一些好用的工具。
\niTerm2 是 Mac 下最好的终端工具,大部分功能都是开箱即用,简单介绍下 iTerm2 的特色功能:
\n在 iTerm2 中,双击选中单词,三击选中整行,四击智能选中(智能规则可配置),可以识别网址,引号引起的字符串,邮箱地址等。(很多时候双击的选中就已经很智能了)
\n在 iTerm2 中,选中即复制。即任何选中状态的字符串都被放到了系统剪切板中。
\n按住⌘键:
\n⌘+←
, ⌘+→
, ⌘+{
, ⌘+}
。⌘+数字
直接定位到该 tab⌘+t
⌘+[
, ⌘+]
⌘+Option+方向键
⌘+d
水平切分,⌘+Shift+d
垂直切分⌘+f
。用于搜索关键字,按 Tab 键可以自动补全单词,且补全的单词可以直接粘贴到其他地方
\n\n分屏功能很实用啊有木有
\niTerm2 可以自动补齐命令,输入若干字符,按 ⌘+;
弹出自动补齐窗口,列出当前可用的命令。
⌘+Option+e
全屏展示所有的 tab,可以搜索
一个标签页中开的窗口太多,有时候会找不到当前的鼠标,⌘+/
找到它。
你可以自由定制喜欢的配色,这里 收集了大量 iTerm2 的主题,你可以选择使用。我用的是Zenburn。在其 github repo 里下载对应的xxx.itermcolors文件,双击安装使用。
\n都用了这么好用的终端了,不考虑再换个 shell 吗?
\nzsh
的安装方法和介绍见:http://macshuo.com/?p=676
macOS 系统不能像 Windows 那样最大化是不是很不爽?一言不合就全屏!用 Moom 来解决这个问题吧!
\n安装后,将鼠标悬浮在你想调整窗口的全屏按钮上(就是那个绿色按钮),下方就会出现一些扩展选项:
\n\n从左到右依次为:最大化、将窗口平铺在屏幕左半边、将窗口平铺在屏幕右半边、将窗口平铺在屏幕上半边、将窗口平铺在屏幕下半边
\n这几个选项非常实用,比如你想打开一个网页同时打开一个笔记工具,这时你就可以直接让浏览器占用左半边,笔记工具占用右半边,不需要自己手动拖拽调整大小啦。
\n复制粘贴是我们日常工作和开发中常用的功能,Paste
为我们提供了剪切板历史记录的功能,我们可以通过 cmd+shift+v
来查看记录,通过方向键选择我们需要的内容后敲回车完成之前复制内容的粘贴:
我日常用 HHKB
来码字,这个键盘最大的优点就是小巧,最大的缺点也是小巧,很多键是没有的,比如方向键。
我通过 Keyboard Maestro
来设置一些组合键作为方向键,同时设置另外一些组合键作为 App 启动热键。
介绍看我另一篇博客:使用-KM-处理-HHKB-方向键/
\n这条是后来补充的,此时我已经将 HHKB
组合实现方向键的功能由楼上的 Keyboard Maestro 改为了 Karabiner-Elements,Keyboard Maestro 只留下了通过组合键启动应用的功能,Karabiner-Elements 可以更多的对键盘进行自定义,比如为了防止误触发,我开起了敲击 command+q
两次才退出应用的功能,同时还开启了当我接入外接键盘时,自动禁用自带键盘的功能。
\n\n已有楼下的 OpenInTerminal 代替
\n
当你在 finder 中进入一个目录后,这时你想用命令行在这个目录中做一些操作,你需要手动打开终端然后一层一层 cd
进去。
让 Go2shell 来解救你吧,安装完之后,会在你的 finder 上部出现它的 logo,不管你当前在哪个目录,如果你想让你的命令行也进到这个目录中时,只需点一下那个小 logo 就行了:
\n\n比 Go2shell 功能更丰富,OpenInTerminal 不仅可以直接打开终端 并 cd
到相应目录,同时还提供了复制路径、用编辑器打开的便捷功能。
不多介绍,官方定义为:「高级网络工具箱」。
\n\n我最喜欢的 GTD 应用,没有之一。
\n\n我们普遍都有很多不同的帐号,生活中还有各种重要信息需要记忆,单纯靠脑子真的很难记忆和管理。而所有账号使用同一密码绝对是巨大的安全隐患,因此我们还需要一款安全可靠,而且足够方便使用的密码管理器软件。
\nEnpass 是一款安全可靠的跨平台密码管理器软件,提供了包括 Windows、Mac、Linux 以及 iOS、Android 在内的几乎所有平台的客户端,并且提供主流浏览器的一键登录扩展,基本能覆盖你所有的密码应用场景。
\n\nBartender 3 是一款Mac菜单栏自定义工具,简单说就是可以将指定的程序图标隐藏起来,需要时呼出。
\n\nTODO…
\n"},{"title":"陆地冲浪板学习-week2","url":"/2022/land-surfboard-study-week2/","content":"本周学会了通过小幅度 pumping 加速,也学了大幅度的 pumping,但是姿势很不协调,尤其是上半身,不过我自己很满意,现在已经可以不下板的情况下一直滑了。
\n昨天,也就是周六,去公司做了一天的校招面试官,所以没有去上滑板课。周日上午学习了一小时,中午训练了 20 分钟,晚上训练了一个半小时,而且找到了一个非常棒的训练地点,全程无车无人,滑的非常进行。下周继续加油,把姿势做的优雅一些。
\n今天上课的时候右脚小拇指磨了一个泡,晚上训练的时候裹了个创可贴,不那么疼了。
\n\n找到了两个入门陆地冲浪板的比较好的教程:
\n\n因为学会了新的技巧,晚上有些过度兴奋,到了后半夜才睡着,最后放个视频留个纪念。
\n"},{"title":"陆地冲浪板学习-week3","url":"/2022/land-surfboard-study-week3/","content":"没想到自己在 30 岁的时候找到了一个爱好,陆冲滑起来很上头,可以靠肩、跨、腿、脚的配合让板子动起来,从而不用蹬地也可以前行。
\n我为什么喜欢陆冲呢?
\n我想是因为我喜欢快速滑动时的速度感,还喜欢突然找到某种感觉、某个发力点和学会某个技巧后的喜悦感。喜欢体验一个人独处时专注沉浸在滑板上的那种心流,还有运动时带来的多巴胺。
\n这周是学习陆地冲浪板第 3 周,继续练习 pumping,姿势还不能做得特别优雅,主要是往正手侧转动的幅度太小,另一个问题是视线没有打开、头没有跟着肩膀一起转动。这周还练习了小幅度荡板,但我的重心一直保持不好,做的不是很好,平时还要多练习。自己练习荡板的时候重重的摔了一跤,多亏当时戴着护具,只把手腕顶了一下,没有大碍。
\n我在家附近找到一个体育场,在一个大院里,里边很大,有足球场、篮球场羽毛球馆等等,不过人很少,很像一个部队大院。我绕着大院最里边的一个场馆外的路练习,没有来往的车辆和行人,只有风声、树声,因为路的两边都有高墙所以很凉爽。
\n最后再放个视频记录下这周的练习进程:
\n\n\n又找到了一个教陆冲很好的 Up 主,比之前找到的教学内容更全面、更详细:纷飞的大脚
\n"},{"title":"陆地冲浪板学习-week4","url":"/2022/land-surfboard-study-week4/","content":"这周陆冲学习了三个技能,折叠 pumping、单膝跪板转弯、slide。
\nslide⬇️:
\n\n\n给各位跪一个⬇️:
\n\n\n\n这几个都不是一时半会能学会的,需要熟能生巧,尤其是折叠要自己找感觉,我现在做得很扭捏、肩膀无法放松,做 slide 需要一定的胆量,我在做 slide 的时候摔了两次,从滑板上下来了几次。
\n我通常是开车去学滑板的地方,单程差不多 25 到 30 分钟,在来回的路上我一般听博客,优先听最新一期的「谐星聊天会」,上周尝试坐了次地铁去上课,路上没有听,所以就攒了两期,这两期一期是讨论短视频给生活带来的影响,另一期是讨论和朋友在一起的时候能玩点什么。
\n短视频那个有一段用红楼中的贾瑞之死来做比喻,简直太棒了,我准备之后单独水一篇短文来介绍下贾瑞之死和短视频之间的关系,而且计划写一系列红楼梦带给我的启发文章。
\n和朋友一起玩什么那一期,开头问到最近和朋友在什么时间玩了什么,我想了想,我想现在几乎没有任何社交活动了,顶多偶尔和两三个同事约顿饭,频率也不会超过两周一次,而且通常选择中午时间,1 小时纯吃,晚上会耽误下班。至于上一次玩是什么时候,我想了想大概是 7 月中旬参加的一次团建,吃完饭后一起打了打德扑,也是那次学会了德扑。
\n我本身也不太喜欢很多人一起的 Social 活动,所以滑板很适合我这样的人,能多人一起练活、也能自己一个人享受滑行时的沉浸感。
\n周六我在上完课回来的路上天气突然阴了下来,不知道为什么我特别喜欢这样阴阴的天气,于是拍了几张照片记录下:
\n\n\n\n这么快一个月就过去了,截止目前一共上了 8 次课,还剩最后 4 次,预计再有 2-3 周就上完了。按照平均每次上课和课后练习一共 3 小时,再加上平时的一些练习来算的话,我在陆冲上投入差不多有 30 个小时,已经可以使用陆冲来刷街了。
\n刷街⬇️:
\n\n\n今天周一,我开始尝试滑着滑板到地铁,然后下地铁后滑到公司,很顺利。为了减负,我把背包也换了个更轻便的,里边只有一个 iPad 和一把雨伞。
\n"},{"title":"陆地冲浪板学习-week5","url":"/2022/land-surfboard-study-week5/","content":"本周是学习陆地冲浪板的第五周,我对它的喜爱依然是热度不减,看来真的是找到自己的爱好了。
\n这周因为接到一个 P0 的项目,周六到公司加了天班,所以只上了一次课,这应该是我们搬到望京 SOHO 后第一次周末因为项目进度到公司加班,而且这几个月我印象中周末只加过 3 次班。第一次是 5 月份居家办公期间,也是接了个倒排期的需求,那时候是在家加班,正好小区也不能出去,加班还能换天调休。第二次是上个月到公司做校招的面试官,第三次就是昨天了。
\n这周学习了前两周学习过的荡板,课程最后十几分钟还学了 piovt 180。荡板是为 piovt 180 打基础,而 piovt 180 又是为尾刹 180 和 360 打基础,piovt 还可以跟 slide 180 连起实现 360 的旋转(如下边第一个视频),而且会了 piovt 180 就可以去刷碗池了。
\n别人的 slide180 接 piovt 180:
\n\n\n我的荡板练习:
\n\n\n我的 piovt 180 练习:
\n\n\n刷短视频的时候看到一个用陆冲刷街的小姐姐,太帅了!
\n\n\n今天北京一上午都在下雨,我上课的地方虽然是在一个地下二层的商业街,但是那个场地上边被建筑物覆盖了,练习的时候看着前后瀑布一样的雨水很惬意,而且因为下雨今天天气也格外凉爽。
\n\n\n开车来回的路上还是听了「谐聊」,这周讨论的是关于浪漫的话题。我也回想了一下,自己早在几年前也是个浪漫的人,尤其是高中和大学期间,现在越来越不浪漫了。当年我也写过藏头诗、拍过 MV、折过千纸鹤,用红楼梦里的一句话就是:「甚荒唐,到头来都是为他人作嫁衣裳」,后边有机会的话会聊聊这段历史,也算得上一段青葱岁月的浪漫往事。
\n这段时间由于转岗没多久的原因,有一段时间没请过假了,我目前有 12 天调休,11 天年假,几乎是用不完的状态。打算等再过一段时间手头工作捋顺了,准备假到「谐聊」现场收听几次。
\n上滑板课回来后吃了个超豪华的螺蛳粉,螺蛳粉里放了「炸响铃」兼职是太好吃了,还用空气炸锅炸了鸡块和鸡米花,看了一集极限挑战,饭后还吃了一根梦龙和榴莲千层切角,腐败了一个下午。
\n\n\n我工位后边的墙边已经被我的东西承包了。
\n\n"},{"title":"陆地冲浪板学习-week6(摔惨了)","url":"/2022/land-surfboard-study-week6/","content":"这周是学习陆地冲浪板的第六周,正常来说也应该是最后一周了(按照每周两次课来算),不过因为之前有一周只上了一次课,所以这周上完还剩最后一次课。
\n周六上课的前半段学习挥臂肩带转,我一直找不到感觉,做起来像是在自由泳。后半节课学习初级的 drop in,用了个大概 40 厘米的台子,摔了好几次也没学会,因为我这个教练的胳膊肘前段时间骑摩托车摔了,所以他不能拉着我从上往下冲,最后是等另一个教练下课后带我做了几次次找到了感觉。
\n来看下周六学习入门 drop in 的效果:
\n\n\n越恐惧越容易摔。
\n周六学习过程中受了点皮肉伤
\n\n\n下课后吃了个豪华冰淇淋聊以慰藉
\n\n然后还去 miniso 根据小红书上的推荐买了个薰衣草味的香水,打算以后没事也喷点香水让自己心情愉悦下
\n\n之后又去我常去的那个体育场练了 2 小时,这天的天气真好
\n\n然而噩梦发生在周日,本来计划周日休息一天,但是实在有些无聊,所以中午的时候和教练约了下午 2 点的课,到另一个有碗池的场地上课,这里的台子是标准 1 米 2 的,在这里练习 drop in 一次也没成功,而且一直摔,教练看我摔的是在有些惨,让我练会别的,尝试在斜面上做 pivot 180,成功了几次,也重重摔了几次,中间有两次摔的连话也说不出来(应该是震到心脏或者肺了),缓了好久。
\n\n现在心脏部位生疼,上半身不能大幅度活动,胳膊腿也又受了几处伤,胯骨的位置摔得一片紫
\n\n\n\n整个腿面上也是青一块紫一块
\n\n跟教练沟通了下,最后一节课还是用来改善体态吧,不学这高难度的了🥲
\n极限运动的归宿是骨科。🙂
\n"},{"title":"Last Day","url":"/2020/last-day-in-qianxin/","content":"\n\n是的,今天是我在这家公司的最后一天,2017年4月1日-2020年9月29日,3年零6个月,在我目前的职业生涯中是最长的一次经历。在这中间已经有过几次想要离开,但是都以各种各样的理由说服了自己。任何公司都有自己的问题,所以我在这里不会对这家公司进行过多的评判。
\n下图分别是我入职时发的动态和离职时的离职证明。
\n\n\n\n\n在这段经历中,也得到了老板的器重,去年年底的时候被任命了「研发总监」,需要管理部门内40+人的研发团队(之前成都的负责人协助我做副手),说实话当时的压力非常的大,尤其是对另一个团队的业务并不是那么了解。好在今年4月份,集团调整了政策,各职能线只能有一个 L3,经过慎重的考虑,老板认为成都负责人对整体业务更熟悉、管理经验更丰富,所以最终任命了他为留下来的研发总监。
\n老板当时还跟我做了一些解释,担心我有什么想法。说实话,我对这事还是有些窃喜的,因为压力不至于那么大了,而且我目前不太有意愿投非常大的精力在管理上。私下里也有同事私聊我说你这咋还降了,我云淡风轻的回一句「不重要」。后来我就管理大约一半的研发,也就是做平台开发的这些,加上成都那边有两个小组长的帮助,我的压力少了很多。
\n上个月底(8月)的一个周末,我坐下来疏理了一下自己这几年的工作,发觉到自己的成长空间受到了限制、发展方向偏离了主流,于是下定了决心换一份工作了,于是又抽出了 2 小时的时间整理了一份简历出来。周一的时候考虑从哪家开始面起,在看翻看微信的时候,偶然间在一个技术交流群中看到了一条内推「探探」的消息,于是和对方加了好友,让他帮我推探探的直播部门。
\n我选择探探的原因,是我在去年学习 Go 期间参与了探探举办的 Gopher China 大会,当时探探给我留下了不错的印象,感受到探探地技术气氛不错。
\n探探 HR 效率很高,当天下午就收到了HR的反馈,约了第二天的面试,当天和两个面试官进行了沟通,晚上的时候又约了第三轮面试,当时告诉我是终面。
\n第三天上午面完后,HR 反馈说因为职级可以到技术专家,所以需要加一轮交叉面试,约了当天的下午,和四面面试官聊的也有一个小时,当天晚上同时进行了 HR 面。
\n第四天我上报了自己的期望薪资,剩下的就是等待反馈了。大概是第二周的周二HR给了我反馈,顺利拿到了 offer。在此之前我已经和老板沟通了我要走的打算,也和一些朋友进行了沟通,很多人都劝我去大厂,让我面面大厂还有朋友主动帮我内推大厂,但我这个人不太爱做选择,而且加上本身没有特别想去大厂的欲望再加上对探探的印象也算不错,于是没有投入太多的精力去进行后边的事情了。其实这里还有一个原因,是我自己并没有为这一次跳槽做太多的事前准备,没有准备面经、八股、刷算法啥的,找到一个自己还算满意的也就定了,希望自己这次选择的是一家「小而美」。
\n\n\n以上大概就是这次的一个跳槽经历。
\n
自从入职这家公司后,从来还没有休过长假,每年的年假都被作废掉,于是在交接的这一个月中,请了一周的假带家人跑北疆玩了一圈,看了看祖国的大好河山,感受了一下什么叫地大物博,顶部图选自旅途中的一张照片。
\n说到探探还有一段渊源,我在5年前和一群有趣的人做过一个创业项目,和探探早期的模式非常像(如下图),但是后边因为各种原因失败了,但那次之后我内心深处还是想回到社交这个领域。
\n\n\n去年在参加 Gopher China 大会的时候脑海中也闪过一个想法,未来如果有机会去探探就好了。
\n于是5年后,我回来了。
\nLife is a circle.
\n"},{"title":"又又又要学着管理了","url":"/2023/learn-manage-again-again-again/","content":"我有一个其他人都不知道,但并没有什么卵用的技能:「克领导」。
\n基本上每次跳槽、换部门后直属领导都会在1-2年内离职,然后我就会被迫莫名往上走一个阶梯,我有时候真的怀疑是自己把领导气走的,凡事有再一再二,没有再三再四,这种事发生在我身上已经不下四次了。
\n\n\n如果你不喜欢自己现在的领导,请联系我。
\n
近几年我换工作或者部门的很大一部分原由是不想做管理,虽然我没有做过那个很火的人格测试,但我确信自己是个 I 人,从小参加家庭聚会什么的几乎都是一言不发那种,别人问一句我答一句,非常不擅长与陌生人沟通,遇到事不想麻烦别人。
\n但不知为什么,在工作中总是做着做着就到了管理岗,每次在做了一段时间管理后我总给自己找个理由:我还年轻,还有技术上的成长空间,不能把太多时间花费在管理上,还不到那个时机。
\n关于做着做着就到了管理岗这个事,我想和中国的「学而优则仕」这个传统有关,但我真的不是什么优等生,至于为什么经常是我被选中,再容我想想,后边有机会我会再做个复盘。
\n我在去年5月份转岗到了公司的推荐工程部门,但因为各种原因自己并没有在推荐这个领域上有特别大的精进,但今年6月初的时候,公司核心产品部门的后端TL和推荐工程后端TL(也就是我的直属TL)都提了离职,果不其然,我又被推了上来。涉及的这两个团队恰好都在我的+2层的TL下,他在一番考虑后决定把两个团队合并,都挂到我下边,这对我来说是个很大的挑战。
\n我在一个月前已经提了陪产假,准备在6月份休个安心的假期,这突如其来的变故让我措手不及,其一是我对核心产品的项目几乎没有了解,其二是我对推荐领域也不在行,这使得我非常焦虑。
\n焦虑也不只是因为我的新职位,而是不光是他们两个离职,随着人心惶惶那一批离职潮一下子走了 4 个人。
\n因为缺乏相关经验,加上突然多人离职的人手不足,我就拼命的把活往自己身上揽,让自己忙碌起来,因为担心一不忙了整个团队就会崩塌,另一个让自己忙的目的是故意把自己塞满,想表现出来的样子就是「老板你看,我都这么努力还是没扛下来,这就不怪我了」。以至于经过一多月的狂奔,现在养成了一个心态是一无事可做就会慌张,感觉自己是不是错失了什么。
\n实际的现状是经过HR和其他部门协助面试,我们迅速补够了人手,工作节奏可以正常流转开了,我也不应该再在具体的工作上投入太大的精力,而是站在相对的高度做一些顶层设计,但我还是转不过这种心态。
\n我这是典型的:「用战术上的勤奋掩盖战略上的懒惰」。因为不想思考,就自己在行动上瞎忙,明明可以交给其他人的不重要工作,非要花费自己大量时间去做。
\n前段时间学习了哈佛的积极心理学,其中有一点应用在自己身上就是我太渴望被多数人认同,而不是被理解,以至于自己用极其忙碌的方式来证明自己。
\n在后边的工作中我需要更多的松驰感,而且不能再用自己年龄还小还没到管理的年纪来逃避了,具体来说:
\n以下部分是在5月中旬,团队内有2个人先后提了离职(那时候我还没有意识到问题的严重性,最终一共有4人离职),在一次周会上我做的发言:
\n首先感谢zm和dw两位同学之前为团队作出的贡献。zm(18年12月5日入职)在探探工作了4年半,从一个互联网从业者的角度来看,算是很长了。在推荐工程团队做出了不可磨灭的贡献,回老家也算是荣归故里了。dw虽然刚刚工作了两年(21年5月21日入职),但在推荐和核心业务方向都做出了很大的贡献。特别是上个季度,在核心缺少人手的情况下,cover 了大部分的需求,快速掌控了核心的几个业务子域。现在刚好有个肉翻的机会,去国外看看也挺好。
\n不知道大家是否看过《权利的游戏》,里面小拇指说过一句话:“混乱不是深渊,混乱是阶梯”。短期动荡状态是让自己成长和脱颖而出的一个很好的机会。一个人的成功不在于是否努力多做两件事,而在于能否跃迁到更高的量级。前边说的成长不止技术上的成长,更是心智上的,因为你比别人见过更多的起起落落。
\n如果真的问我团队里突然有两个人离职,现在把指挥棒交到了我手里,我慌不慌,我会很诚实地回答我会有点慌。但我不会觉得这是个坏事,我会把它当成一个挑战。两位同学的离职确实给我们的团队带来了一定的影响,但我们要正视现实,勇敢接受挑战。有句老话叫:“铁打的营盘流水的兵”。我在去年转到推荐组的时候,给我的最大感受是推荐组是整个探探最优秀的团队,只要我们的营盘还在,流水的兵是个很正常的事情。
\n在推荐领域,大家都是我的老师,在核心业务方向,我们剩下来的这些同学都是新人。我并不认为只有能力最强的人才可以做 TL,有问题我可以和这么多优秀的同学一起商量着来,我的主要工作是为大家做好后勤工作,可以帮大家扫清前进的障碍。一个好的 TL 应该是一个没什么存在感的角色,我相信在这么多优秀同学的一起努力下我们可以顺利度过这段时间的小动荡。不管大家是否承认,这段时间我们都有一些懈怠,大家振作起来,踏下心来把手头的工作做好,是对公司的负责,也是对自己的负责。
\n我原本的计划是从上周开始休陪产假,但是知道泽明的事后就往后推了一个月。我是我们组里唯一有娃的,甚至是有两个娃的,大家可能不太能理解我现在的心情,谁不想多在家陪陪刚出生水嫩嫩的娃。但是我还是想把这段时间支持下来。
\n了解我的人都知道,我比较希望追求 work-life balance(迫于现在由于通勤太远无法实现),所以不会特别卷大家,这跟前几任领导人是一样的。在管理风格上,我也不是一个爱 push 大家去工作的风格,大家各自约定好自己的 promise 承诺,在约定时间交付成果,中间做好必要的反馈就可以。我可能和大部分程序员不太一样,我属于早起鸟类型的,可以早起但是不能熬夜。
\n关于我自己的稳定性问题,我在短期内是不会走的。其一是出于责任心,其二是出于房贷和两个娃的压力,当然公司给我大礼包让我走除外。
\n这段时间核心产品的需求我会分给每个同学去做,jz的业务方向会逐渐往核心业务转移,必要时也会给rp、kq分核心业务的需求。新同学ml尽快熟悉业务,估计三周后也可以逐渐接需求,缓解一些业务压力。再往后我们还有三个 HC,所以困难是延期的,未来是光明的。
\n最后,大家一定要多注意身体,规律作息,多运动,身体是革命的本钱。
\n"},{"title":"念念在少年宫学陶艺","url":"/2023/learn-pottery/","content":"少年宫这个词我印象中只在我的小学阶段出现过,应该还是在一些青少年报刊杂志上看到的。
\n百度对其定义是:
\n\n\n我国在学校以外对少年儿童进行政治教育和开展集体文化活动的机构。
\n
我的理解就是经国家认可的、公立性质的课外培训机构。
\n今年一月底,春节后从老家开车回北京,在服务区休息的时候遇到了另外一对也在休息的家长,稍微攀谈了一会发现是老乡,聊着聊着就聊到的对方爸爸的工作。对方爸爸在北京的少年宫里做老师,细问之下是在我们住的丰台区少年宫,再细问他教的是我娃一直想学的陶艺课程。
\n他说今年9月1日办理新学期入学,每周末上课,到时候如果我们想进的话可以联系他,他可以给我们塞个名额。正常来说少年宫是非常不好进的,几万人争几千个名额。8月份的时候我们联系他,他给我们加上了塞,让我们9月1日来上课就行了,价格也非常公道。
\n今天是念念第一天来少年宫上课,从家开车过来大概20分钟,这是我第一次带念念开车出门,之前也出过一次,不过距离很短就是从家门口到地铁站5分钟不到的路程,所以那次就不算了。
\n我让念念坐在后排,给她记上安全带,后排有一本装修公司之前给我们选装修风格和材料的书,她因为无聊就翻那本书看,时不时问我一些关于装修的问题,出奇的乖。
\n\n开到地方后发现少年宫不对外开放停车,而且因为是在一条繁华街道上,路上也停不了车。其他之前已经来过的家长会把车临时停一下,孩子下车后直接进去。我们是第一次来,人生地不熟,念念也不认识老师不知道教室在哪。于是我就跟她说我们需要找个地方停车,大概又开了5分钟,拐进一个胡同的小区里,找了地方停了车。
\n我看到导航上显示如果步行回去需要10分钟,再考虑到念念的步行速度可能就要15分钟了,上课就会迟到。我不想让念念第一次上课就迟到,于是跟她商量了一下,扫了一辆共享单车她坐在座位上我推着他走。时间还是有些紧张,中间有一段我就开始小跑,念念第一次坐在这么高的自行车座位上,脚够不到车蹬,既害怕又兴奋,刚开始跟我说她害怕,后边我跑起来后她说太刺激了😂。
\n跑到学校门口后我已经满身大汗,刚要进去保安拦住我说家长不能进,我和保安解释说我们第一次来,保安说孩子往里走有老师接她,后来来了个老师带着念念去了她们上课的教室。
\n看着念念进去后,我先步行回刚才停车的小区,把车开到离学校稍微近一些的另一个停车场停好了车,在附近买了瓶阿萨姆奶茶,坐在学校附近一条小路的石阶上用手机扣这篇流水账。
\n\n没多久老师把念念上课的照片发了过来,看到她满脸发自内心的喜悦,老父亲也就满足了。
\n\n在上课来的路上,念念说她以后要给我做酒杯、咖啡杯,哈哈,期待!
\n"},{"title":"少立 flag","url":"/2023/less-flag/","content":"\n\nflag 就像个咒语,立了基本都会反向达成。比如:今年我一定要减5斤,现在已经涨了5斤。
\n
6月12日的时候,我写了一篇流水账,当时立了个 flag 说要在一天之内考下摩托车驾照,但第二天在科目二考试中挂了,拖着狼狈的身体在回家的高铁上,又写了篇流水账记录当时丧气、失望的心情。
\n前一天还「男人至死是少年」的豪言壮志,第二天就成了「摩托车也不是我的必需品」的泄了气的气球。
\n经过那次考试失利后,我脑海中一直回荡着考试过程的景象,反复在脑子里对那次考试进行复盘,设想如果我当时怎么怎么做就不会挂了。越是想让自己不去回忆这件事,反而回忆的更多(白熊效应)。
\n经过了好几天都无法消化这次失败,机缘巧合在一篇知乎回答里看到有用户推荐「哈佛幸福课(积极心理学)」,我觉得应该会对我有帮助,所以开始学习这门课程。
\n学到一半多的时候,我挥之不去的挫败感基本被课程中的观点治愈了,尤其是那些关于失败的论点:
\n于是在给自己做好心理建设后,我准备二战。
\n上一次考试是在我休陪产假期间,我选择了周二这个工作日。考试地点一周有三天可以考试:周二、周四、周六。这一次为了不耽误工作,只能选择周六考试。我在周四报名并缴纳了补考费。周五下午5点我去吃了个驴肉火烧,打包了一个火烧准备路上吃。6点多我提前下班从公司出发,7点多到达大巴车集合地点,再次坐上去德州的大巴车。这一天是6月30日。
\n为了积攒一些好运,我花了99元在小宇宙购买了「谐星聊天会特别季」节目。第一次去的时候就看到这个节目,但没有购买。当时心中有个念头一闪而过:我不会因为没买而挂吧?最后果然挂了,我在复盘的时候也想过有没有可能是没买这个节目导致的😂。
\n7月1日凌晨1点多到达训练地点,开始为期7小时的「特种兵」训练,因为这次我只需要补考科目二,所以可以把所有精力都放在科目二上。由于是周末,考试的人很多,每练一轮要等20多分钟,这中间我没有休息,一直练到天亮。
\n8点左右,我们被拉到车管所办事大厅办理考试报名,由于我是补考,手续比较简单。
\n这一批有3个人要补考,8点半提前把我们拉到了考试地点,9点开始考试,我们几个是当天最先考试的三个人,我是第二个考试,三个人都过了。
\n然后就是等待科目四考试,十点半左右满分考完了科目四,至此我的摩托车考试流程结束了,所有科目都是满分。
\n这一天是7月1日,和党的生日同一天:
\n\n定了下午的高铁票回老家,看看我的大儿子,还有大女儿。
\n打车去高铁站的路上让出租车司机推荐了一家当地人经常买的扒鸡店,买了3只扒鸡,一只给岳父家,两只我们吃。
\n这一次买的是无座票,在高铁上找了个角落席地而坐,2个小时就到家了。
\n\n这一次来考试,我没有再立 flag,安静的来,安静的走。
\n因为没有立 flag,也没有人知道我又来考试了,所以这一次的心情也不一样,没有给自己太高的预期和压力,就当是来德州旅游,体验生活,如果再挂了就找机会再来。在练习场通宵练习,看着太阳落下,月亮升起;月亮落下,太阳升起,整个过程很奇妙。
\n其实我通常情况下都是先把事办成再对外宣布,因为担心对外宣布后就像泄露了天机,容易遭天谴而失败,这一次属实轻敌了,以后也不会轻易再立flag。
\n"},{"title":"Linux 常用网络工具清单","url":"/2022/linux-network-util-list/","content":"「这些计算机还在线吗?」
\n发送任何你需要的 HTTP 请求。
\n和 curl 一样,但操作更简单
\n下载文件
\n流量控制命令,可以降低其他人的网速
\n「这个域名的 IP 地址是多少?」(DNS 查询)
\n「这个域名注册了吗?」
\n安全的 shell
\n通过 SSH 协议拷贝文件
\n只拷贝有过改动的文件(通过 SSH 协议)
\n网络版的 grep 命令
\n「把 80 端口的所有网络包展示给我!」
\n通过 GUI 查看 tcpdump 抓的包
\n非常强大的网络报分析命令行工具
\n抓取与聚合 TCP 流
\n「我的 IP 地址是多少?」
\n查看和修改路由表
\n用于代替 ifconfig、route 等其他命令
\n查看你的 ARP 表
\n具有 SSL/TLS 功能的交互式拦截侦听代理
\n\n\nMITM 是 Man-in-the-middle 的缩写。
\n
网络连接端扫描软件
\nnmap 的 GUI 版本
\n被动网络指纹识别工具
\nVPN 软件
\n新的 VPN 软件
\nNetcat,手动建立 TCP 连接
\nNetcat 的加强版,主要特点是在两个数据流之间建立通道
\n类似于 ssh,但不安全
\n用于文件拷贝,sftp 是基于 ssh 的。
\n「服务器的哪些端口号被占用了?」
\n配置防火墙和 NAT
\n新版 iptables
\nTCP/IP 数据包组装/分析工具
\n「数据包到达服务器的路径是什么?」
\n使用 TCP 包代替 ICMP 包的 traceroute 命令
\n\n\n现代网络广泛使用防火墙,导致传统路由跟踪工具发出的(ICMP应答(ICMP echo)或UDP)数据包都被过滤掉了,所以无法进行完整的路由跟踪。尽管如此,许多情况下,防火墙会准许TCP数据包通过防火墙到达指定端口,这些端口是主机内防火墙背后的一些程序和外界连接用的。通过发送TCP SYN数据包来代替UDP或者ICMP应答数据包,tcptraceroute可以穿透大多数防火墙。
\n
管理物理以太网连接和网卡
\n管理无线网络设备的配置工具
\n配置 Linux 内核的网络栈
\n用 SSL 证书做任何事
\n为不安全的服务器做一个SSL代理
\n查看什么在占用带宽
\n基准测试工具
\n搭建当前目录下的文件服务器
\nIP 地址计算器,比如查看 13.21.2.3/15
是什么意思
~ ➜ ipcalc 13.21.2.3/15 |
进入一个容器进程的网络命名空间
\n"},{"title":"Linux 笔记","url":"/2017/linux-note/","content":"在添加用户时,最好用 adduser
,虽然 adduser
和 useradd
这两个命令在其他发行版的 Linux 系统下一样,但是在 Ubuntu 下是有区别的:adduser
会自动创建用户的 home 目录,并且创建用户同名的组,而 useradd
不会。
如果不小心将用户 home 目录删除了,可以使用下边的方法来重建:
\nsudo mkdir /home/user # 这里的 /home/user 里的 user 最好改成跟你原来用户名一样 |
755 是同组的还有别的组的用户可以查看并且可以执行的。如果不想同组的和别的组的用户查看,可以把权限设置为 700。
\n新建用户后可能还需要给用户添加 sudo 权限,有两种方法:
\nsudo usermod -aG sudo username
/etc/sudoers
将自己电脑上的公钥内容插入到主机用户 home 目录下的 .ssh/authorized_keys
中,通常新建的用户没有这个目录文件,需要手动创建一下。
如果本地没有生成过公钥和私钥,或者想生成新的,可使用 ssh-keygen
。
运行上面的命令后,系统会出现一系列提示,可以一路回车。特别说明,其中有一个问题是,要不要对私钥设置口令(passphrase),如果担心私钥的安全,可以设置一个。运行结束以后,会在 ~/.ssh/
目录下新生成两个文件:id_rsa.pub
和 id_rsa
,前者是公钥,后者是私钥。
shell
jiapan@ubuntu:~$ cat /etc/shells |
shell
echo $SHELL/bin/bash |
sudo apt-get install zsh git wget |
sudo add-apt-repository ppa:webupd8team/java # 添加仓库源 |
安装过程中需要接受协议,选择 Yes
。
查看 Java 版本: java -version
(我每次都输成 --version
)
date -R
tzselect
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
Control + ] |
修改 /etc/update-motd.d/
下的几个文件就行了。
scp -r ~/local_dir user@host.com:/var/www/html/target_dir |
# 总核数 = 物理CPU个数 X 每颗物理CPU的核数 |
今天在读《掌控习惯》和听得到拆解《清单革命》这本书的时候,发现这两本书使用了不同的视角来解释同一个现象,而且最终将达成效果的原因各自归功于自己要介绍的方案,有点公说公有理婆说婆有理的感觉,很有意思(也很巧合,同一天读到同一个case)。
\n这两本书都介绍了巴基斯坦卡拉奇这里的平民窟,由于卫生条件差导致死亡率高的问题,解决方法是培养那里的人使用香皂的习惯。
\n《掌控习惯》认为能培养起他们习惯的原因是给他们使用的香皂是「舒肤佳」这种高品质香皂,因为使用起来会产生大量泡沫、洗完手后有香味,给使用者带来极大的愉悦感,所以人们就逐渐养成了使用香皂的习惯。
\n而在《清单革命》这本书中,作者认为是专家给什么时候使用肥皂列了个包含6条内容的清单,人们只要照着做就可以了,由于清单体的有效性所以培养起了人们使用肥皂的习惯。
\n我觉得《清单革命》介绍的方法和《掌控习惯》的第三个原则:「让它简便易行」也是一个意思。
\n"},{"title":"负载均衡方案介绍","url":"/2022/load-balancing/","content":"本文只讨论请求进入数据中心后的负载均衡方案,DNS 负载均衡不在讨论范围内。
\n负载均衡(Load Balancing)定义:调度后方的多台机器,以统一的接口对外提供服务,承担此职责的技术组件。
\n总体来说负载均衡只有两种:
\n四层负载均衡的优势是性能高,七层负载均衡的优势是功能强。
\n“四层”的来历:“四层负载均衡”其实是多种均衡器工作模式的统称,“四层”的意思是说这些工作模式的共同特点是维持着同一个 TCP 连接,而不是说它只工作在第四层,如:
\n出于习惯和方便,现在几乎所有的资料都把它们统称为四层负载均衡。
\n如果在某些资料上看见“二层负载均衡”、“三层负载均衡”的表述,描述就是它们工作的层次。
\n对于一些大的网站,一般会采用 DNS+四层负载+七层负载的方式进行多层次负载均衡。
\n数据链路层负载均衡所做的工作,是修改请求的数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器的网卡上,这样真实服务器就获得了一个原本目标并不是发送给它的数据帧。
\n负载均衡服务器和集群内的真实服务器配置相同的虚拟 IP 地址(Virtual IP Address,VIP),也就是说,在网络通信的 IP 层面,负载均衡服务器变更 MAC 地址的操作是透明的,不影响 TCP/IP 的通信连接。所以真实的搜索服务器处理完搜索请求,发送应答响应的时候,就会直接发送回请求的客户端,不会再经过负载均衡服务器,避免负载均衡器网卡带宽成为瓶颈,因此数据链路层的负载均衡效率是相当高的。
\n\n只有请求经过负载均衡器,而服务的响应无须从负载均衡器原路返回的工作模式,整个请求、转发、响应的链路形成一个“三角关系”,所以这种负载均衡模式也常被很形象地称为 “三角传输模式”(Direct Server Return,DSR),也有叫“单臂模式”(Single Legged Mode)或者“直接路由”(Direct Routing)。
\n二层负载均衡器直接改写目标 MAC 地址的工作原理决定了它与真实的服务器的通信必须是二层可达的,通俗地说就是必须位于同一个子网当中,无法跨 VLAN。
\n数据链路层负载均衡最适合用来做数据中心的第一级均衡设备,用来连接其他的下级负载均衡器。
\n我们可以沿用与二层改写 MAC 地址相似的思路,通过改变数据包里面的 IP 地址来实现数据包的转发。
\n有两种常见的修改方式。
\n保持原来的数据包不变,新创建一个数据包,把原来数据包的 Headers 和 Payload 整体作为另一个新的数据包的 Payload,在这个新数据包的 Headers 中写入真实服务器的 IP 作为目标地址,然后把它发送出去。
\n设计者给这种“套娃式”的传输起名叫做“IP 隧道”(IP Tunnel)传输。
\nIP 隧道的转发模式仍然具备三角传输的特性,即负载均衡器转发来的请求,可以由真实服务器去直接应答,无须在经过均衡器原路返回。
\nIP 隧道工作在网络层,所以可以跨越 VLAN,因此摆脱了直接路由模式中网络侧的约束。
\n
IP 隧道的缺点:
NAT(Network Address Translation) 模式通过改变目标数据包:直接把数据包 Headers 中的目标地址改掉,修改后原本由用户发给均衡器的数据包,也会被三层交换机转发送到真实服务器的网卡上。
\nNAT 模式需要让应答流量先回到负载均衡,由负载均衡把应答包的源 IP 改回自己的 IP,再发给客户端,这样才能保证客户端与真实服务器之间的正常通信。在流量压力比较大的时候,NAT 模式的负载均衡会带来较大的性能损失,比起直接路由和 IP 隧道模式,甚至会出现数量级上的下降,此时整个系统的瓶颈很容易就出现在负载均衡器上。
\n\n工作在四层之后的负载均衡模式就无法再进行转发了,只能进行代理,此时真实服务器、负载均衡器、客户端三者之间由两条独立的 TCP 通道来维持通信。
\n
我们先对代理做个简单介绍,根据“哪一方能感知到”的原则,可以分为“正向代理”、“反向代理”和“透明代理”三类。
七层负载均衡器它就属于反向代理中的一种。
\n言归正传,七层均衡器工作在应用层,可以感知应用层通信的具体内容,往往能够做出更明智的决策,玩出更多的花样来。
\n列举了一些七层代理可以实现的功能:
\n参考:
\n五一期间读完了《一本小小的蓝色逻辑书》,这本书不厚,内容很干,总的来说还是比较推荐的。
\n里边的附录部分列出了一些逻辑上的漏洞,我将其汇总如下:
\n\n儿子:“老爸,你现在手里不就拿着酒杯吗?”
\n蒂凡尼:“应该不能。”
\n皮特:“那我们干吗浪费时间学它呢?”
\n学生乙:“是的,可我曾祖父每天一包烟,现在都90多岁了,还是活得好好的,这该怎么解释呢?”
\n反方:“你的意思是,不论砍掉多少棵树,都要去印更多课本吗?”
\n现在时间是2022年02月05日凌晨5点10分,春节假期的倒数第二天,我经历了新年的第一次彻夜失眠。2点半的时候吃了一粒安眠药但到现在还是没睡着,索性就不睡了。
\n我基本上每周会有一次小失眠(差不多睡3、4小时),每几个月有一次大失眠。
\n我的失眠好像有规律,又好像没有过滤,昨晚睡得确实比较晚,差不多0点才上床,躺下后感觉心口疼,一直辗转反侧到现在。
\n上床晚的一个原因是晚上的时候发现了一个有趣的功能,QSpace 可以连接各个网盘,我把每个网盘进行了授权,并尝试了一下文件上传和下载。
\n另一个原因是白天的时候打开了几个页面,给自己定下了学习目标准备今天学完,但是由于拖延只进行了一半,到了晚上有些焦虑。
\n说到失眠有规律,是因为有一部分时候的失眠是因为白天喝了咖啡,但我自己总觉得跟喝咖啡关系不大,因为我几乎不会在下午喝咖啡。
\n昨天上午我喝了一个大杯美食,下午又喝了两杯啤酒,我自己本身有酒精过敏,但是为了消遣没事又想和两口🤷🏻♂️
\n前段时间看到一个人提到的「妈妈法则」,我也准备给自己制定几项,虽然有些是我暗自里回尽量去做的,但终究没有落到纸面上,这次索性写下来,起到监督自己的目的:
\n\n\n妈妈法则:你长大成人,背井离乡,没人再盯着你刷牙,洗衣服,完成作业,所以你需要成为自己的妈妈,制定一些规则,和自己约法三章。
\n
它们是一些简单,有效的规则,让你处理好自己的生活,健康,甚至情绪。
今年也没做什么总结,生活上平淡无奇,平日里读了40多本书,公司里给了个优秀员工。
\n2022年,我想把 Rust 学一学,参与一些高质量、大型开源项目,多一些输出,做一项 Side Project,加入一个纯技术公司(也许是明年)。
\n"},{"title":"2018 减肥计划","url":"/2018/lose-weight/","content":"今年优先级最高的事情是 lose weight,给自己设置了两个时间节点:第一个节点是自己的生日(农历5月12),目标是从 90kg 减至 80kg,第二个节点是我拍脑袋想的日期:222天后(农历9月18),减到 70kg,立文为据。明天起每日博客记录体重变化。
\n本次算是人生中的第二次减肥,上一次从 98kg 减到了 69kg,但是没有注意保持,两年时间又回来了,这一次再用 200 多天减下去,再之后准备上一些器械,争取练出 6 块腹肌🌚。
\n\n\n本周计划:80.5
\n
\n\n本周计划:85.0
\n
\n\n本周计划:85.4(未完成。本周过了个清明,没有好好控制,体重也没称)
\n
\n\n本周计划:86.0(未达成)
\n
\n\n本周计划:87.2(达成)
\n
\n\n本周计划:87.8(达成)
\n
\n\n本周目标:88.5(达成)
\n
今天脑补一下,如果我能够娶红楼梦中一位女子为妻,我会选谁。用正排的方法有点困难,尝试用排除法逐个过滤掉我无法接受性格的人物。另外,红楼梦中所有女子范围太大,我从中选取一些有鲜明性格的人出来。
\n\n虽然本书中宝玉最钟爱的是黛玉,二人从小青梅竹马,互相将对方视为 soulmate,但我是无法接受黛玉这种性格的,她孤傲、矫情,总爱使小性子,喜欢说「暗语」不把话说明白。而且书中对黛玉外貌的描写给我的印象是娇瘦,该丰满的地方不够丰满。唔,你懂的。
\n\n\n态生两靥之愁,娇袭一身之病。泪光点点,娇喘微微。闲静时如娇花照水,行动处似弱柳扶风,心较比干多一窍,病如西子胜三分。
\n
黛玉饭量很小,对吃没太大兴趣,如果我和一个妹子约饭,还没吃几口对方就说吃饱了,或者对方一开始就说今天不饿,你点你的,我会非常恼火,不能成为饭搭子的另一半我是不能接受的。
\n当然,人家黛玉是绛珠仙草转世,我一凡人也配不上她。
\n王熙凤性格火辣,贾母给她起了个外号「泼皮破落户儿」, 但她性格上太要强了,心机很重,小厮兴儿对她的描述是:「嘴甜心苦,两面三刀」,「上头笑着,脚底下使绊子」,「明是一盆火,暗是一把刀」,可见王熙凤手段还很毒辣。
\n凤姐这样的女人需要一位性格也很强势的夫君来配才能驾驭得住,书中懦弱的贾琏在王熙凤面前时服服帖帖,王熙凤实时监视着他的行踪,不给他半点接触其他女人的机会,连自己的陪嫁丫鬟也不可以(在古代陪嫁丫鬟默认是对方的妾),但是只要王熙凤不在跟前贾琏就抓紧时间从外边拉个女人到自己房中,贾琏的主要目的就是为了发泄欲望,想在其他女人面前获得「一家之主」的体验,在尤二姐那几回更是如此。
\n尤二姐的下场可以看出,王熙凤还是个嫉妒心非常重的人。外表贤良、内心恶毒,这种女人太恐怖了。
\n书中对宝钗外貌描写和黛玉形成了鲜明对比,一个丰满、一个瘦弱。
\n\n\n可巧宝钗左腕上笼着一串,见宝玉问她,少不得褪了下来。宝钗生得肌肤丰泽,容易褪不下来。宝玉在旁看着雪白一段酥臂,不觉动了羡慕之心……再看看宝钗形容,只见脸若银盆,眼似水杏,唇不点而红,眉不画而翠,比黛玉另具一种妩媚风流,不觉就呆了。
\n
宝钗的心理年龄应该比园子里其他姐妹大不少,原因是她的父亲去世的早,而她们家又是皇商,弟弟薛蟠又不务正业,只能她和母亲担起家族的事业,很小的时候就接触到了成人「污浊」的世界。
\n宝钗太冷了,缺少小姑娘们那种灵巧活泛的劲儿,在男朋友面前不会发嗲,不会要求亲亲抱抱举高高,不爱开玩笑,和她一起生活会缺少一些生活上的乐趣。
\n太耿直,缺少风趣。处事不够圆融、爱挑刺,有些愤世嫉俗,最后她自己也是选择远离这个到处是窟窿的家族,远嫁到他乡。
\n兴儿在尤二姐面前这样描述探春:
\n\n\n三姑娘的浑名是‘玫瑰花’…玫瑰花又红又香,无人不爱的,只是刺戳手。……
\n
撇开可卿的真实死因不谈,在我看来她有些扶弟魔,为了促成秦钟和宝玉的见面,动用了心机。而且当她得知秦钟在学堂被人欺负后把自己气得不行。
\n\n\n今儿听见有人欺负了她兄弟,又是恼,又是气。恼的是那群混账狐朋狗友的扯是搬非、调三惑四的那些人;气的是她兄弟不学好,不上心念书,以致如此学里吵闹。她听了这事,今日索性连早饭也没吃。
\n
湘云性格豪爽,平时大大咧咧的,喜欢穿男装,跟她论兄弟一定是不错的,但是缺少一些女人味,有些过于粗犷,不精致。
\n假清高。
\n\n\n欲洁何曾洁,云空未必空!可怜金玉质,终陷淖泥中。
\n
袭人给人一种大姐姐的感觉,不需要被保护,但是男生恰恰容易喜欢上自己想保护的女生。但也不得不说袭人是个非常合适的结婚对象,勤家持家、为人和善,处事方面尽量大事化小、小事化了,作者在判词中也写到娶到她的人是有福的。
\n\n\n堪羡优伶有福,谁知公子无缘。
\n
作者还有意将袭人映衬为宝钗,将晴雯影射为黛玉,因为上边说过黛玉了,下边不再对晴雯进行赘述。
\n\n\n王夫人眼中的晴雯:水蛇腰,削肩膀,眉眼有点像林黛玉。
\n
实话实说,香菱是我喜欢的类型,虽遭遇了各种不幸,仍然那么天真无邪。她的脾气好,模样也好,作者从头到尾都在透露对香菱的怜悯。我喜欢香菱也可能有可能出于同情,被拐卖后到薛蟠这个不懂得怜香惜玉之人身边做妾,薛蟠娶夏金桂前,香菱还高兴地东跑西跑地帮忙,从这一点看出,香菱是那种真心想让别人好的人。夏金桂过门后香菱被百般欺凌,薛蟠也不分青红皂白地打她,香菱只能忍气吞声。香菱的结局有很多说法,我们这里不展开讨论。
\n香菱嫁给薛蟠后我内心也是愤愤不已,好歹贾琏替我骂了他:「方才我见姨妈去,不妨和一个年轻的小媳妇撞了个对面,生得好齐整模样……谁知就是上京来买的那小丫头,名叫香菱的,竟与薛大傻子做了房里人,开了脸,越发出挑的标致了,那薛大傻子真玷辱了她。」
\n从香菱学诗可以看出,香菱极其聪明好学,她很羡慕那些可以读书的女孩子,不管自己处境多么糟糕也要想办法去学习,吾辈之楷模。
\n脂砚斋对香菱的评语极高,集其他人的优点于一身。
\n\n\n细想香菱之为人也,根基不让迎探,容貌不让凤秦,端雅不让纨钗,风流不让湘黛,贤惠不让袭平…
\n
所以,如果让我选一个红楼梦中的女子作为妻子的话,我想我会选择香菱。
\n"},{"title":"凌晨四点想娃了","url":"/2022/miss-my-child/","content":"现在时间是北京时间 2022 年 07 月 23 日凌晨 4 点
\n我没有失眠,而是被一个噩梦惊醒了
\n梦的具体内容记不清了,最后给我的启发是珍惜眼前人
\n这让我突然特别想我的一念了
\n我们一家蜗居在一个不到 70 平的小房子里,平时我爸妈也都在
\n我爸喜欢出去玩,而且不喜欢在北京挤着,所以经常离京
\n他在今年年初的时候离京,后来疫情严重就一直没回来
\n直到两周前疫情好转,他回京把我妈和一念一起接回了老家
\n一念在北京的时候我也只能周末的时候陪陪她
\n因为我的睡眠不好,加上一些心理障碍晚上经常不和她们睡在一起
\n而是去次卧的上铺自己睡或者在客厅把沙发铺开了睡
\n但是我喜欢把一念搂在怀里或者她躺在我胳膊上的感觉
\n现在家里公司远了
\n如果公司到地铁的路上运气好能骑到车的话,大概要 1 小时 40 分才能到家
\n比之前在国贸附近足足多了 1 个小时
\n撇开路程,打车时间也从 9 点延后到了 9 点 30
\n而换来的只是公司给我们每个月多发的 600 元交通补助
\n还有之前的一次性 3000 元搬家补贴
\n按照当前的最佳情况 8 点下班,那也要 9 点 40 才能到家
\n之前的最佳情况是 7 点,现在的情况在以肉眼可见的速度恶化
\n每天的通勤时间 3 个多小时
\n就好像我已经成了一个差生,即便再怎么努力也改变不了现状
\n陈映真小说《上班族的一日》中有这么一句话:“上班,几乎没有人知道,上班,是一个大大的骗局。一点点可笑的生活的保障感,折杀多少才人志士啊。”
\n由于国内现阶段的种种问题,我们的努力往往未必能得到回报
\n这种不确定性会让人觉得看不到希望,幸福感自然不会高
\n「幸福生活才是目的,个人的成功不过是实现这个目的的途径和手段而已」
\n我一直在追求个人的成功,很少去想目的
\n可是我现在个人也不成功,相对幸福的生活也没有
\n懦弱而又无力的自己
\n照片拍摄于年 5 月,一念在认真的给我扎小辫。
\n一念和黛玉一样容易咳嗽,所以一般很少让她吃凉东西
\n作为我俩的小秘密,也是为了贿赂她
\n每次我带她出去玩都会和她一起吃冰淇淋
\n\n\n\n"},{"title":"将博客字体修改为「霞鹜文楷」","url":"/2022/modify-blog-font/","content":"\n一日不见,甚是想念
\n
之前访问过我博客的人可能会发现我博客的字体变了,这款字体是我前段时间在 Twitter 上看到一个很喜欢的博主推荐的,是一款开源字体,名字叫「霞鹜文楷」,非常适合作中文展示,阅读起来使人感受愉悦。
\n\n我也将这款字体改为了我 Drafts 上的默认字体,试用了几天感受确实不错,所以想作为博客字体来使用。因为官方仓库的下载链接只提供了 ttf 格式,我不知道如何应用在 Web 上,所以搁置了几天,今天想再次折腾下看,再次阅读官方文档看到注意事项中写着:
\n\n\n\n正应了我前两天说的:我想的事情其他人已经想到了。
\n
我打开那个 Issue 提供的另一个项目地址,看到里边提供了好几种安装、使用方式。因为我是在其他人模板的基础上进行修改,所以准备用引入现成 CDN 的方式来做字体修改。
\n我的博客当前使用的是 Next 主题,使用其他主题的也可参考这个方式。
\n编辑博客根目录下的 themes/next/layout/_layout.swig
,在 head
中插入如下代码:
<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/lxgw-wenkai-lite-webfont@1.0.0/style.css\" /> |
我对 CSS 不太精通,刚开始只在 style 中填入了 body
,发现有些正文部分没有生效,我觉得应该是优先级问题:博客的正文指定了其它字体所以将我这里的设置进行了覆盖。
我通过 Chrome 的元素查找定位到了 div 下 的 class="post-body"
为正文部分的筛选器,于是加上了 div.post-body
,接下来后发现 h1、h2
这些格式也没生效,于是有逐个进行了添加。这样基本上所有地方都能生效了,有两处我故意没做处理,一处是左上角博客标题另一处是文章标题下方的 meta 区域,这两块我觉得可以保留更正式一些的字体。
这个字体有些偏小,于是我还将字号做了稍许放大,也就是 style 中的 font-size: 108%
。
如果你也喜欢这款字体,不妨参考这篇文章也自己尝试修改一下。另外如果你知道如何只在 style 中指定 body
就可以让全局字体生效,请留言告诉我。
\n\n我在去年和前年主导了公司两个产品后端的技术选型和整体架构,并分别尝试了两种源码组织模式:多仓库和单体仓库。对两种仓库的利弊也有了很大程度上的感受,基于这个前提对这两种模式做个总结。
\n
阅读本文后你会明白:什么是单体仓库?为什么 Google 采用单体仓库?
\n在介绍单体仓库和多仓库前,先来说说什么叫单体应用和微服务应用。
\n微服务相比单体应用最大的好处是可以独立的开发测试部署和扩展。单体应用一般采用单体仓库,但是微服务的代码仓库该如何组织呢?一定是每个服务一个仓库吗?
\n其实也不一定,针对微服务的代码组织,业界有两种主要的实践,一种是多仓库(multi-repo
)也就是每个服务开一个源码仓库,另一种叫单体仓库(mono-repo
)所有源码都在同一个仓库中,尽管整个应用采用的微服务架构。
单体仓库和多仓库都是有利有弊的。
\n单体仓库可以解决部分上边提到的问题。
\n在工业界,世界上采用单体仓库管理源码的公司并不少,如 Google、Facebook、Twitter 这些互联网巨头,包括通过去年B站泄露的源码也可以看出,B站也是用的单体仓库进行的管理。虽然这些公司系统庞大、服务众多,内部研发团队人数众多,但是依然采用了单体仓库并且都很成功。
\n单体仓库也是有弊端的,随着公司业务团队规模的变大,单一的代码库会变得越来越庞大复杂性也呈极度的上升,所以这些互联网巨头之所以能够玩转单体仓库,一般都有独立的代码管理和集成团队进行支持,也有配套的自动化构建工具来支持,如 Google 自研的面向单体仓库的构建工具 Bazel:https://bazel.build/ 和 Facebook 的 Buck:https://buck.build/。
\n初创公司在早期服务不是特别多的情况下,采用单体仓库比较合适。
\n##总结:
\n微服务架构并不是主张所有的东西都要独立自治,至少代码仓库就可以集中管理,而且这也是业界的最佳实践之一。
\n"},{"title":"绝知此事要躬行","url":"/2022/must-know-this-thing-to-practice/","content":"最近一个月解锁了两个新技能,一个是在几周前团建的时候学会了德州扑克,另一个是今天利用两个小时入门了陆冲板。虽然这两个目前还都是新手级别,但是比起之前只是看一些介绍、教程,通过实践来学习进步的速度可快太多了。
\n上上周的周五下午小组团建,吃了个西餐之后去了一个轰趴馆,有打台球的,有打麻将的。一开始我无所事事,后来有同事叫我打德州,因为我只听说过但从来没玩过,所以一开始说自己不会,就不参与了。后来在几个同事的鼓励下坐在了牌桌前,把规则给我讲了下,并且发给我一张图片,告诉我按照图里的规则判断自己手牌的大小就可以了。
\n\n我把图片保存到手机上,时不时看一下自己的牌有没有和这些规则对应上。因为之前玩过炸金花,所以不一会就了解规则了,但是各种套路和黑话还是不太懂,包括什么时候可以看牌什么时候可以跳过也需要问下其他人,有时候还需要让其他人帮忙算一下钱之类的。
\n这次学习德州扑克是实打实的用钱学习的,大家初金都是 200 块钱,没想到我在新手光环的照耀下不仅没有输钱,最后还赚了 150 多。
\n因为从家到地铁站、下地铁后再到公司,这两段路程都比较远,从家到地铁可以骑自己的自行车,但是公司那边地铁站下车后通常骑不上车,所以就萌生了用滑板当代步工具的想法。
\n之前网上查了一些资料,一心想学习双翘,找了个家附近的滑板俱乐部,预约了今天的体验课,老师问我的学习目的,我说简单、能代步就可以。教练给我推荐陆地冲浪板(简称陆冲)。对于我不了解的领域,我是很相信专业和权威的,所以听了教练的话体验了一节陆冲课。
\n陆冲适合平时代步,它的轮子比较大,更适合在马路上使用,而且相对来说比较容易入门,学会后还能体验到冲浪的乐趣(这也是它名字的由来)。
\n一节课体验课结束后,感觉很有意思,而且这东西确实看起来容易,到自己滑的时候相当困难,加上陆冲板还可以来回扭动,刚开始上板平衡都很难掌握。为了更深入地学习,我报了 10 节一对一课程,一节 399,还买一块属于自己的板子。板子的话我也一步到位,买的应该是陆冲板中最好的牌子 Caver,虽然价格并不是最高的,但这块板子颜值深得我意,如下图:
\n这块板长度是 30.5 寸,大概是 6 斤多重,比起其他款式稍微宽一点,所以看起来很大气,售价 2200 软妹币。我还差一套护具没买,训练的时候用的店里的,准备自己从网上买一套,加上头盔大概又是 700 左右的花销。好不容易能有个爱好,该省省该花花吧。
\n肯定有朋友会说我这钱花的不值,陆冲板这么简单靠自学就够了,但我的想法是随着年龄越来越大,我们应该尽可能用更高的效率去学习,不能再花太多时间自己琢磨了,请个教练可以少走弯路,自己跟着视频练习的话根本不知道哪个姿势不对、哪里应该注意,也不知道应该加强练习哪些内容,而且容易受伤。这和软件开发时常用的空间换时间一个道理,我这算是拿钱换时间了,我几年前学习游泳也是报了 10 节私教课,学完后可以用基本标准的姿势蛙泳了,虽然很久没有游泳了,但那些注意事项我还是记得,再游的话很快就能找到感觉。
\n10 节课周六日各上一节,估计一个多月就能学完,再加上平时的练习,到时候我应该就能达到刷街的级别了。
\n作为报课的优惠,教练赠送了我一节课,所以买完板后我又上了正式的第一节课,今天一共学了两个小时,最后,再欣赏一下我的滑板初体验的视频吧。
\n\n\n\n这个视频是教练一手打着绷带,另一手拿着手机,脚下踩着滑板拍出来的,完整视频比较长我剪了其中十来秒出来,可以看出镜头运的很好。我准备最后一节课让教练帮我拍一组酷酷的视频作为结果汇报的材料。
\n这个泰山原浆啤酒很好喝,口感非常好,而且很鲜,强烈推荐。
\n"},{"title":"我在使用软件时的七宗罪","url":"/2022/my-app-seven-deadly-sins/","content":"我打开微信的「看一看」,是想了解最近疫情发展情况,盘算下是不是又可以居家办公了,总幻想着世界可以毁灭;打开「朋友圈」是想看看在我无聊时其他人都在做什么、那些土豪们又去哪里潇洒了;我发朋友圈是想炫耀些什么。(怠惰、愤怒、嫉妒、傲慢)
\n插个题外话,这几天被「天堂超市酒吧」事件搞的又来了一波疫情反扑,严重程度不亚于上一波,但是政府已经不再提居家办公的事情了,看来国家已经意识到了恢复经济成了现阶段的重中之重的任务,经济这个烂摊子已经到了不可不救的地步了,实际也早该这样了,别再打肿脸充胖子。
\n我打开 Twitter 是想看一看真实世界的样子、技术界有哪些新的轮子,有时候会有这些见闻作为自己的谈资,还想看看有没有什么赚钱的路子。(傲慢、贪婪)
\n我打开 Youtube 为了看看那几个常看的国内美食 up 主最近出了什么新作品(我不用 bilibili),想看看国外 up 主对国内的一些事件发表了哪些(我们敢怒而不敢言)看法,偶尔也会看看我在私人目录里上传的一些(小)电影。(暴食、愤怒、色欲)
\n我打开 Telegram 和短信是因为总幻想着有人会通过这种方式联系我,有时候回去 Telegram 一些 Porn 的群组逛逛。(傲慢、色欲)
\n我打开脉脉是想看看职场人们现在在吐槽什么事情,在批判哪个大场,还会从那些生活在水深火热公司的人身上得到一些安抚。(嫉妒:那些在好好公司的人、愤怒、傲慢)
\n我打开小红书是为了看一些短视频杀时间,大部分视频是关于吃的,我很早之前卸载了快手,因为不想这么沉迷,因为小红书的推荐算法没这么完善,所以作为完全戒除前的过度。(怠惰、暴食、傲慢:看不起快手)
\n我打开得到、知乎是因为此时此刻的焦虑,看不到方向,想在这上边找些捷径或者速成的魔法,不想慢慢提升自己。(怠惰、贪婪)
\n我打开淘宝和京东是因为一时心血来潮想买那个让我心动、但可能没什么用的东西。(贪婪)
\n我打开蚂蚁财富是想看看最近又亏了多少钱,想看看国家经济究竟烂成了什么样子。(贪婪、愤怒)
\n"},{"title":"我的 IDEA 配置指南","url":"/2017/my-idea-settings/","content":"\n\n本指南理论上适用于 IntelliJ 家的所有产品。
\n
Alt + w
写代码时,有时候需要同时打开多份文件,在 IDEA 中有两种分屏方式,一种是上下分,一种是左右分,我觉得上下的方式基本上看不了几行代码,所以我都是使用左右分。默认的快捷键需要用到方向键,但在 HHKB 中使用方向键还需要其他按键组合
\n\nControl + ,
默认的代码补全快捷键为 Control + Space
,但是这个组合被我的 iTerm2 的弹出栏占用了,所以我改为了:Control + ,
Alt + m
Alt + f
Shift + Conmmand + p
因为 push 默认快捷键为 Shift + Command + k
,但是 pull 没有默认快捷键,所以我用了这样的组合。
代码拖拽是我非常不喜欢的功能,经常不小心误操作,如下图,去掉勾即可。
\n\n默认的代码补全提示是会区分大小写的,比如我们在 Java 文件中输入 stringBuffer
IDEA 是不会帮我们提示的,我们需要输入 StringBuffer
才行。
如图所示,将选项改为 None 即可。
\n\n对于我这种 8G 内存的 Mac 用户来说,打开这个功能很有必要性,而且点击内存信息展示的那个条可以进行部分的内存回收
\n\n使用 Command + /
快捷键可以对代码进行注释,IDEA 对 Java 代码的单行注释是把斜杠放在本行最开头,这种注释方式非常丑,所以我修改为将斜杠放在代码之前,并且加一个空格。
调整后
\n\n\n\n写在前边:我写这篇博客的目的不是为了跳槽,而是为了边写边梳理一下我到底想要什么样的工作环境、我当前的工作状态、今后有哪些规划。
\n
从15年5月开始算的话,到现在我已经工作6年半了,但是从来没有在所谓的大厂工作过。我觉得原因出在我大学度过的一本书上,书名叫《黑客与画家》。
\n书中的其中一个观点是:「一个非常能干的人待在大公司里可能对他本人是一件很糟糕的事情,因为他的表现被其他不能干的人拖累了」,另一个观点是:「编程语言之间是有优劣之分的,黑客欣赏的语言才是好语言,使用更高级语言的黑客可能比别的程序员更聪明」。(这里的黑客不是大多数人理解的骇客,具体含义可以查阅资料或者阅读本书了解,这里不过多解释)。
\n因为我大学用 Java 和 Python 都开发过不算小的项目,那个时候的 Java 还没有现在的 Spring 全家桶光环,写起业务来相当的繁杂,所以我毕业找工作时,既没想着去大厂,又不想做 Java 开发。基于以上这些情况,我的前两份工作都是选择的规模不大的创业公司,使用的 Python 语言。之后又在第二家公司 Leader 的推荐下到一家 toB 的公司做了几年 Java 开发。再之后又来到探探做 Golang 开发。
\n在我看来,我应该还算比较聪明的那类人,有非常好的自驱能力,工程、架构、沟通能力也不错,所以任职过的这几家公司混的都还不错。上家公司巅峰时实线带40+人,后边做了些有业务调整,我离职时实线也有20来人;探探这边目前虚线10来人。
\n我喜欢追求高效、简洁、优雅的代码风格,这里说题外话:一个我观察到但不一定对的现象,那些把 Leetcode、八股文常挂在嘴边的人,实际开发时编码能力通常好不到哪去。
\n我喜欢读书,每天会读5本左右不同类型的书,而且阅读非常广泛,不限于技术书,以当前在读的举个例子:晚上睡觉前我会读《红楼梦》、《伯恩斯焦虑自助疗法》,早晚上下班通勤的地铁上读《人类简史》,早上到公司后读《Google SRE 工作手册》。我到公司比较早,9点到9.30之间就到公司了,因为其他同事大多10点半后陆续才来,所以到公司后我会有一个多小时的阅读时间。我这么早来公司也是处于想多读书的目的。中午午休时间还会再读一本技术相关的书,最近读的是《Go 专家编程》。
\n\n\n我会享受学习或完成有挑战事情时的成就感,随着年龄的增长,当我没有取得任何成就时,那种焦虑的感觉就演变成了一种厌恶感。
\n
另外我习惯于早睡早起,晚上睡得再晚早上也会在7点前起床。
\n(作为一个92年的程序员,是不是生活的有点像老年人?)
\n基于以上这些原因,我想我在选择工作时会比较看重下边几点:
\n以上只是我个人站在对生活的态度上列出来的几点,其他更通用一些的考量肯定还包括团队氛围、领导风格、业务方向、公司战略、薪资不能低于业界水平等。
\n目前看来我当前所在的公司以上几条都是满足条件的,据我所知目前市面上绝大多数大厂都无法满足这些点。
\n不知道没进过大厂算不算是一种遗憾,有时候我甚至会因为没有进过大厂而沮丧和焦虑,觉得自己是个 Loser,如果有符合这些条件的大厂能有幸进去体验一下也是可以的。——但是话又说回来,谁规定进过大厂的人生才是完整的?哪里不是围城呢?
\n我也想进一家小而美的公司,最好是 B、C 轮之后,和一群聪明的人在一个细分领域去做一些有挑战的事。
\n大部分人进大厂是因为大厂给的太多了,但我觉得薪资是一个不应该作为决定性决策,我们上班寻求的应该是整个职业生涯利益的最大化,而不仅仅是最近这一份工作利益的最大化,拿上3、5年的高出业界50%的收入也不能解决太多的问题,更何况还搭上了健康和生活乐趣。我说不能解决问题是出于以下考虑:
\n很多人想进大厂的另一个目的是「镀金」,但是所谓的镀金不也是为了未来能进另一个大厂么,但你现在已经到了大厂,为了镀金开始忍耐着在一群人中卷起来,之后跳到另一家继续卷,还是走不出那个圈子。想要走出这个圈子还是要改变自己的想法,用智慧而不是蛮力去解决问题。
\n\n\n如果你所在的大厂没有内卷,比如 Apple、AWS 这种外企大厂,就另当别论了。
\n
当然,我的以上观点可能纯属吃不着葡萄说葡萄酸,没有进过大厂也却是有我自己的原因,因为我从小就讨厌应试教育,在我看来现在的面试也接近应试教育:「面试官清楚自己在问八股文,你也清楚自己在背八股文,你们心照不宣的完成了面试。」我从最开始工作到现在,在面试前没有刷过题(我看面试题的唯一的目的就是用在面试别人上),没有做过刻意的准备,都是看缘分,人家看得上我我就去,看不上就是缘分没到,所以可能也是因为这个原因,通常我在入职后的表现都会超出一些预期,具体表现有这几个:
\n\n\n最后,理想和追求的多样化,才是避免内卷的终极方法。
\n
P.S.
\n《黑客与画家》这本的作者叫保罗·格雷厄姆(Paul Graham),是著名创业投资公司 Y Combinator 的创始人,之前开发的 Viaweb 卖给了雅虎,Lisp 传道士。最近我看到了他的博客:http://paulgraham.com/articles.html,里边文章内容质量很高、见解独特,所以我建了一个中文站:https://paulgraham-cn.com/ 来做搬运,作者也在 FAQ 中提到允许翻译成其他语言,只要带上他的原文链接就可以了(见下图)。我会不定期的翻译一篇作者的文章到这个中文站中。
\n\n"},{"title":"我的小摩托没有了","url":"/2023/my-motorcycle-is-gone/","content":"昨天刚信誓旦旦立了个flag准备1天内搞定摩托车驾照,然后买一辆本田幼兽。
\n山东德州这边允许一天考4项,昨晚8点从北京坐驾照大巴出发,凌晨1点到德州,一夜没睡,练习到早上8点,人比较多,教练也基本不看,练习效果就一般,然后开始考试。不出意外的出现了意外,我挂在了科目二上,科目一满分,科目三满分。我在训练时是表现比较好的,考试的时候和训练车带速不一样,第一次自己大意没有回头看导致撞杆了,第二次熄火两次,扣20分,坡道起步的时候离边线太远又扣10分,最后没有通过。
\n因为无法继续科目四考试,我买了最近的一班高铁回北京,不用晚上坐大巴回了。下次再考试需要等10天,我准备放弃了,就当2000块钱买了个教训吧∶捷径是世界上最远的距离。
\n而且摩托车也不是我的必需品,考完驾照后又要花更多的钱买车,倒不如及时止损,到此为止。
\n小幼兽,拜拜啦。
\n我心态超好!
\n\n打了败仗,溜了溜了。
\n\n前两天找人写了个扇面,内容是红楼梦里黛玉说的一句话「事若求全何所乐」,很应景。
\n"},{"title":"新房子初步竣工","url":"/2023/my-new-house/","content":"去年十一前在潘家园附近买了一套小三居,今年年后开始装修装到五一前,过程中陆续置办了一些家电。
\n今天去宜家给我的小书房兼卧室选了床和床垫,之前定的窗帘也约的今天上门安装,这样下来基本就可以入住了。
\n\n我选的床垫其实很厚,被压缩的状态下看起来有些单薄。
\n\n办公桌和衣柜比较实用,准备在办公桌上挂上一张洞洞板,挂一些小东西。
\n目前家具方面还差一张主卧的软床,一个电脑椅和一套餐桌椅。
\n电脑椅可选的太多了,一直没有选好,趁着这两天618活动,打算赶紧选一个,想选个白色的。
\n截止目前已经在装修上花费了30多万,真是太费钱了,下边是我在装修期间做的记录
\n\n房子有三个卧室,其中两个卧室住人,另外一个通阳台的卧室准备做一个游戏房,放一张大地毯,两个懒人沙发,装个投影仪,再搭配一套Xbox、PS5之类的。
\n下边是东边的主卧,需要再放置一张一米八的床,再单独配个床头柜。
\n\n下边是通阳台的卧室,准备做成游戏房
\n\n洗手间做了干湿分离,可以在客厅洗漱,洗手间洗澡上厕所
\n\n\n洗衣机和烘干机也放在了客厅,这次单独买了专业的烘干机,目的是为了不在阳台挂满衣服挡住阳光
\n\n洗衣机和烘干机是我很得意的两件家电,选的西门子,本来想选博世,后来得知西门子和博世祖上是一家,而且西门子的洗碗机和冰箱有比较喜欢的,所以洗衣机和洗碗机也就买了西门子的。
\n\n前段时间我二阳时来这小住了几天,用这俩设备洗烘了一次,出来的衣服非常舒适。
\n为了解决爱做饭不爱刷碗的问题,还在厨房配置了洗碗机,也是选的西门子的。
\n\n虽然冰箱不是双开门,但容量也非常大,有4个独立空间,燃气热水器也选的比较好的史密斯。
\n\n房子的阳台也非常给力,整体朝南,很长很大、采光非常好,后边没有其他楼层遮挡视线,楼下可以看到垂杨柳小学。
\n\n\n\n这套房子在交钱之前只知道是6层,交完钱后才知道,房间号是606,大吉大利。
\n"},{"title":"我的小屋配置了电脑椅","url":"/2023/my-room-equipped-computer-chair/","content":"在小红书上研究人体工学椅,琳琅满目看的我头晕眼花,有官方账号自己发的,有恰饭的,最后选的是网易严选探险家3D,趁着618有活动下单了。
\n\n选这个椅子没有什么特别的原因,就是选到最后实在不想选了,正好最后看到了这个,就定它了。
\n今天看物流信息显示已送达,但是并没有小哥和我联系,打电话过去问说放在了门口,我兴高采烈开车到新房,到了之后才发现没拿钥匙,这时候纠结是再开回去拿一趟钥匙还是找个跑腿给我送过来,虽然时间上差不多,但我选择了后者。
\n进屋后花了20分钟进行组装,之后坐下感受了一会,很舒适。
\n\n我买的是带脚踏的版本,累了还可以半躺着休息,官方还送了一个午休毯可以盖。
\n\n脚踏也可以收回当成正常的办公椅使用,有好几个地方可以调节的。
\n\n准备7月份搬进来,这样到公司的距离可以缩短一般,幸福指数可以有很大提升。
\n"},{"title":"我儿子名字的由来","url":"/2023/my-son-name/","content":"我儿子叫「贾登一」
\n登一连起来的寓意是:「登峰造极,一路顺风」(然而并不是这个理由)
\n真实的情况是:
\n从小我给别人介绍我名字的时候都是说:我叫贾攀,攀登的攀
\n实话实说,在我小的时候压根不知道攀登是什么意思。
\n不过那个时候就对「登」这个字很感兴趣,所以一开始打算给儿子取名贾登。
\n后来想还是用三个字吧,于是在最后补了个「一」字,跟姐姐名字也能呼应。
\n以后我儿子给别人介绍自己名字的时候可以说:我叫贾登一,攀登的登。
\n我是攀登的攀,
\n他是攀登的登,
\n一看就知道是父子俩。
\n\n"},{"title":"Panmax 的 Things 实践","url":"/2022/my-things-practice/","content":"\n\n今天是端午节,祝各位端午安康。
\n
作为一名员工,在职场中非常重要的一个品质就是要稳定输出,这很像我们监控指标中经见的 P99。
\n比如说有这样两个视频 App,一个 App 在打开视频的时候,有 99.99% 的概率会最多缓冲 3s,后面就会顺畅播放视频。另一个 App 有 80% 的概率视频一秒不卡,还有 20% 的概率每一帧都卡,卡到忧伤。如果只能选一个 App,你会选择哪个 App?我想大部分没有自虐倾向的人都会选择第一个。
\n只有长时间稳定输出,老板才放心让我们承担更多、更大的职责,同事们也更愿意和稳定靠谱的同事合作「不求有惊喜,但求无惊吓」。
\n说到这里,通过最近发生在自己身上的一件事,获得的感想是,任何寻常的跨部门合作都要认真对待,说不定日会有意想不到的结果。
\n我可以很自信的说,我绝对是一个靠谱、输出稳定的打工人,而我能做到这一点,除了一些习惯外,给我提供最大帮助的是一款叫 Things3 的任务管理工具。我现在差不多每天的工作事项都是靠 Things3 驱动,说的通俗点是靠 Todo 驱动,这有点像我们常说的 deadline 是第一生产力。
\n「君子生非异也,善假於物也。」
\n\n\n下文提到的 Things 皆为 Things3,为了简化称呼我将去掉 3 这个数字。
\n
我用过很多 Todo 类的工具,比如嘀嗒清单、微软的 Todo,Sorted,最终还是留在了 Things,它的界面、交互、易用性吸引了我。
\n使用 Things 的好处是我能在每天一早就知道今天有哪些重点工作,一天中想到任何要做的事情都可以记录下来,领导安排的临时工作或者向其他人承诺的事项也可以记录下来,好记性不如烂笔头,人脑不适合存储这种临时的、用完就可以扔的记忆。
\n我使用 Things 有 4、5 年了,下面就来介绍下我是如何使用这款工具的。这里我不介绍 GTD 的方法论,只说我的实践。也不会面面俱到介绍 Things 的各种细节,你下载个 Things 跟着首次使用的入门教程走一遍就明白了。
\n以下我通过 Things 的 Mac 版本来做演示,手机上的功能完全相同,不管是哪一端体验都非常棒,我自己双端都有在高频使用,路上使用手机端、工作时使用 Mac 端,同时还在手机桌面和 Apple Watch 表盘上放了 Things 的小组件,可以在不打开 App 的时候就看到待办事项。
\n\n另外补充下,Things 的三端(Mac、iOS、iPad)都是要独立收费,手机上的价格还好,但 Mac 上的价格有些感人。
\nThings 根据事项要完成的时间分了这些目录:
\n我自己使用的时候只会用到「今天」、「计划」、「日志簿」,其他的几乎不用。
\n我个人的习惯是,所有新建的任务不管分类、不管是不是一定要今天做,一律先放入「今天」,按照 GTD 的理论应该是先放收件箱。我前边提到,我是靠 Todo 驱动,也就是每天都要把今天的任务消灭掉。这里的消灭可以不必是完成,而是把任务安排到其他更合适的时间或者拆解成更小粒度的任务打散到多天完成。
\n举个例子,比如今天周一我接了一个需求,排期要周五上线,我可能因为其他事正在赶工,先在 Things 的「今天」里加上一条「周五上线 xxx 功能」。之后在我不忙的时候,再次打开 Things 就会看到我刚才记录的那件事情,这时候可以根据我的经验将这个任务拆成若干小任务,并安排到后边的时间里,比如:「完成 A 模块开发」并把它安排到周二(可以在「计划」中找到),「完成 B 模块开发」并把它安排到周三,「完成功能测试」并把它安排到周四,「xx 功能上线」并安排到周五。这样后边每一天我都有这个项目的合理进度安排,同时做每一项的时候你都可以给未来的自己留言,比如我周二周三开发完模块 AB,对应的 merge request 可以记录在周五的上线那个事项里,开发过程中修改的配置也可以记录下来,避免上线时忘记。
\n下边是一个我前段时间上线功能的截图,:
\n\n里边的 mr 地址和配置项都是在前期开发过程中记下来的,到了上线那天完全不用担心漏掉什么,也不用现去翻找我们的 mr 给其他同事 review。
\n我们工作和生活中一定有很多枯燥、例行的事项要去完成,如果每个我们都靠脑子记肯定是记不过来的,这种情况下我们可以使用循环事件。
\n如下图所示:在「计划」中,事件前边带有一个循环小圆圈标记的就是循环事件,最近我们在家办公,上午下午需要使用钉钉打卡,所以我建了两个循环打卡事件,同时给每个设置了提醒时间,设置提醒时间的事项后边会带有一个小铃铛。
\n\n上边图中国年还可以看到,我有一个叫「日课」的循环项,后边有三条圆点线,表示这个事项中包含有子事项,也就是我给自己约定的每天要做的事情:
\n\n这里边的子事项我会根据近期的工作学习的测重点来进行调整,比如最近我的工作方向要偏重于信息检索相关,所以我加了一项读信息检索导论这本书。
\n我之所以没有把这几项作为独立的事项列出来,是因为不想有「红点焦虑」,前边提到我每天是靠 Todo 驱动,当看到有这么多待办项没有做时,会很焦虑,会出现为了消事项而匆忙赶工的情况。把这几项收在一个里边,也表示这几项有一定的宽容度,如果今天时间太紧张,可以只选其中的 1、2 个子项完成就行。
\n我把「多邻国学习」放到了外边而没有收入「日课」中,是因为我把这一项作为了一个必选项而不是可选项,我希望学英语这件事情不要中断。
\n下边图中的「Apple Family 收费」和「提公积金」也是我的循环事件,循环时间不进支持每日,还支持每几日、每周几、每月几号等等。
\n比如「Apple Family 收费」我设置的是每 3 个月的 15 号提醒,我还记录了每人应收的钱,都需要找谁要钱,如果没有 Things 的帮助我肯定是记不住的。「提公积金」那项我设置了每月 20 号提醒。
\n在这张图里还能看到,我在 7 月 20 日安排了一个一次性事件,如果你在很久后有什么事情要做,一定记得要记录下来,并放入「计划」,我个人习惯是将这种很久后的事项放置在提前两三天,提前看到能给我一个预期,留个缓冲时间,毕竟很久后还需要做的事一般都不是什么小事。
\n\n因为每周三需要和组内的同事一起开周会,这之前需要写好周报,会后我需要合大家的周报,所以我给「提交周报、合周报」也建了一个每周三的循环事件,同时在里边把每个人周报的地址进行了记录:
\n\n职场中我们不免要写周报、月报、年度总结等等,如果自己平时有记录的习惯那还好,如果没有,到写总结的时候一定是大脑空白,仿佛自己失忆了,忘记了自己这段时间忙忙活活干了点啥。
\n如果我们用 Things 将工作事项管理起来,在需要的时候通过「日志簿」来回顾就会很方便。比如现在你问我在一月份做了些什么,我只需要翻到一月份部分,看下我当时的记录知道了。
\n这些内容有些描述过于简单,其他人也许看不懂,但因为每件事情都是我亲自做的,我看下提示就能明白这说的是什么事,由此可见我们并不需要为每个事项做特别详细的说明,能让自己能看明白就够了。人脑存储能力很强,检索能力很弱,需要借助些外力来补足检索能力。
\n\n不止待办事项,实际上我用 Things 管理了我的方方面面。
\nThings 支持将事项进行分类管理,上边那些事项我没有计划做的时间点,有些只是为了保存起来,这时候我们可以不设置时间,将它收在我们对应的分类中就可以了。
\n这里借助的是 Things 提供的创建区域和在区域中创建项目的能力。实际上 Things 的这个功能是用来做更大一些的目标管理的,不过我个人将它作为了一个分类功能来使用。
\n\n比如,我给自己分了个人和公司两大区域,每个区域中建了一些属于这个区域的分类(也就是项目):
\n\n把所有事情都记录下来还有个好处是,在我无所事事时可以看下这些事项中哪个是我现在有心情可以做的,或者读完一本书后下一本要读什么。
\n由于我会有「红点焦虑」的情况,所以想要一个在某个时间点后再出现事项的功能,有些事必须过了某个时间点才能去做。不过我觉得 Things 没有支持这个功能也有它的考虑,「今天」就应该把计划在今天做的事情都列出来,让我们可以提前规划。
\nThings 提供了一个退而求其次的方案,将晚上做的事情通过分割线放到下方,在事项上点击右键就可以操作或者使用快捷键 Command + E
。
我们再看一眼上边那张图,在「今天」下方显示今日天气状况和今天的节日,这是我在系统的日历中订阅了两个事件,Things 可以将系统日历中的事件列在我们自己事项的上边,就可以实现这样的效果。
\n中国节假日事件:
\n\n北京天气事件:
\n\n天气事件大家可以根据自己所在城市,在这个网站生成订阅链接。
\nThings 还有一些功能我自己也没有深入探索过,比如标签、截止日期,大家可以自行探索。
\n通过这篇文章我梳理了一下自己使用 Things 的习惯,如果大家觉得可行、适用,可以尝试将自己的事项管理起来,做一个「靠谱」的人。
\n如果你发现文章中有观点介绍有误或者不明确的地方欢迎留言讨论。
\n"},{"title":"mycli fzf thefuck","url":"/2017/mycli-fzf-thefuck/","content":"今天装了3个命令行下的神器,分别是 mycli
fzf
thefuck
,都是通过 Homebrew
装的。
thefuck
装完后在 .zshrc
的 plugin 中配上了插件,这样的话用起来就更方便了,当输错命令或者需要 root 权限却没加 sudo
时,只需要双击 esc 就可以了。
mycli
是一个支持语法高亮和命令补全 mysql 客户端,类似于 ipython。安装过程比较长,主要是中间安装 Pyhton 2.7.13
占用了很长时间。装完后直接 mycli -uroot
就进入数据库的交互状态了。有一个地方不太习惯,没执行一个命令出来结果后,需要再按一下 q
才能返回交互状态。
fzf
是命令行下模糊搜索工具。通过 brew install fzf
安装完后,还需要执行 /usr/local/opt/fzf/install
安装 shell 扩展,之后 Control+r
时出来的就不是之前那种很简单的历史命令搜索结果了,而是交互性很棒的结果。输入 kill -9 + <TAB>
能通过模糊搜索的方式搜到需要杀掉的进程,再也不用先 ps -ef | grep xxx
找到对应的进程然后在执行 kill
或者 通过管道 + xargs
的方式来杀进程了。
fzf
还有几种用法我感觉没多少用:
cd **<TAB> |
今天产品经理给了我一个需求,让我统计下我们数据中的某些类型关系的数量,因为涉及到保密,我就假设他让我统计的关系名称为 FRIEND
和 FATHER
,其中 FRIEND
在创建时没有指定方向,FATHER
创建时有方向。
当统计这两种关系的数量时,我的建议是 Cypher
语句中不要带有节点标签,或者只在一端带标签,因为在我的测试中,两端都带有节点标签时,查询会超时(因为我们的数据量确实比较大):
MATCH (:Person)-[r:FATHER]->(:Person) return COUNT(r); |
MATCH ()-[r:FATHER]->() return COUNT(r); |
在查询 FRIEND
这种在创建时没有指定方向的关系是,也需要用带方向的查询语句,因为 Neo4j 实际存储时是带有方向的,详情见:http://blog.csdn.net/hwz2311245/article/details/54602706),在不指定方向的情况下,我这里的查询也是超时:
MATCH ()-[r:FRIEND]-() return COUNT(r); |
MATCH ()-[r:FRIEND]->() return COUNT(r); |
今天在构建一批新的关系时,需要用 LOAD CSV 批量导入一些关系数据进去,但是这次的关系类型并不是固定的,而是在文件中指定的,CSV 文件格式如下:
\nAWI9o1sbC5n_pY7tOUdL|AWI96tqyetLN4cQh5GnC|EMERGENCY|紧急联络人 |
第一列和第二列分别两个 Person
节点的 UUID,第三列是关系类型(type),第四列是关系的名称(name)。
我刚开始这样写的导入语句:
\nUSING PERIODIC COMMIT 10000 |
但是发现无法执行,由错误信息可知(下图所示),Neo4j 原始的 LOAD CSV 语句是不支持动态创建关系类型的。
\n\n我之前在介绍 Neo4j 冷启动预热缓存 时介绍过一个插件:APOC
,这个插件功能非常强大,比如提供了很多好用的路径算法和强大的函数,之后有机会的话会慢慢介绍,今天介绍一下他的动态创建关系的函数 apoc.create.relationship
,函数说明如下:
apoc.create.relationship(person1,'KNOWS',{key:value,…}, person2)
create relationship with dynamic rel-type
所以我的导入语句只需要改成:
\nUSING PERIODIC COMMIT 10000 |
就完事大吉了。
\n但是考虑到这个 CSV 文件中的关系可能存在重复,所以我通过文档找到了另一个函数:
\napoc.merge.relationship(startNode, relType, {key:value, …}, {key:value, …}, endNode)
- merge relationship with dynamic type
这个函数中需要传两组 key-value
,第一组是用来判断关系是否重复的,第二组是一些其他属性。
最终的导入语句如下:
\nUSING PERIODIC COMMIT 10000 |
\n\n\nNeo4j 是世界上最流行的图数据库管理系统(DBMS),同样是最流行的 NoSQL 数据库之一。
\n
Neo4j 以图的方式存储和展示数据,数据由节点和节点间的关系来表示。
\nNeo4j 数据库(和任何图数据库一样)与关系型数据库(如:MS Access、SQL Server、MySQL)有很大不同。关系数据库使用表、行和列来存储数据,他们以表格的形式展示数据。
\nNeo4j 不使用表、行或者列存储或展示数据。
\nNeo4j 非常适合存储有很多关联关系的数据。这是图数据库可以发挥巨大作用的地方。实际上,像 Neo4j 这样的图数据库在处理关系数据方面要优于关系型数据库。
\n图模型通常不需要预定义结构,你不需要在加载数据前创建数据库结构(就像在关系型数据库中那样)。在 Neo4j 中,数据就是结构。Neo4j 是一个 「结构可选」的 DBMS。
\n然而 Neo4j 能更好地处理关系数据的主要原因在于它允许你创建关系。Neo4j 是围绕关系而建立的。它不需要设置主键/外键约束来预先确定哪些字段或者哪些数据间有关系。在 Neo4j 中,只要在你需要时添加任何点之间的关系就行了。
\n所以,这使得 Neo4j 非常合适社交网络应用,比如 Facebook、Twitter 等。同时,Neo4j 还可以应用于很多其他领域。
\n以下是一些 Neo4j 主要应用领域:
\n\n\nNeo4j 浏览器是一个可以通过 Web 浏览器运行的图形用户界面(GUI)
\n
Neo4j 浏览器可以用来添加数据、运行查询语句、创建关系等等。它还提供了一种简单的方式来可视化数据库中的数据。
\n下图是 Neo4j 浏览器概览
\n\n这里是你输入查询语句和命令的地方,比如创建或检索数据。你可以随时通过输入 :help
并按下回车键(或者点击编辑器右侧的「运行」箭头)来获取帮助。
这里是展示查询结果的地方,每个结果有自己的框架,新的结果框会出现在前一个结果框的上边。所以如果需要的话,你可以向下滚动并查看之前的查询结果。你可以随时使用 :clear
命令清空这个流。
这些代表了数据库中的数据。点击顶部的任意图标都会在框架的底部显示可选信息。
\n侧边栏有多个选线,例如查看数据库详情,查看或修改 Neo4j 浏览器设置,查看 Neo4j 文档等等。
\n点击一个选项会打开更宽一些的侧边栏并提供该选项的详情。
\n比如,点击「数据库」图标会打开有关数据库的详细信息。
\n\n你能够以不同的方式查看数据。例如,点击 「Table」将会以表格的方式显示节点和关系。
\n下图是一个以表格方式显示数据的例子:
\n\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 创建约束","url":"/2018/neo4j-tutorial-create-constraint/","content":"\n\n约束允许你对节点或关系的输入数据进行限制。
\n
约束有助于数据的完整性,因为它们阻止用户输入错误的数据类型。如果某个用户在应用了约束时输入了错误的类型会收到错误消息。
\n在 Neo4j 中你可以创建唯一约束和属性存在约束。
\n唯一约束
\n属性存在约束
\n在 Neo4j 中创建唯一约束需要使用 CREATE CONSTRAINT ON
语句,像下边这样:
CREATE CONSTRAINT ON (a:Artist) ASSERT a.name IS UNIQUE |
在上边的例子中,我们为 Artist
标签的所有节点的 name
属性创建了唯一约束。
我们的语句执行成功后,展示如下信息:
\n\n\n\n当你创建一个唯一约束时,Neo4j 将同时创建一个索引。Cypher 将使用该索引进行查询,就像使用其他索引一样。
\n
因此不需要单独创建索引了,如果你尝试在已经有索引的情况下创建约束,你将会收到一个错误。
约束(和索引)成为数据库模式的一部分。
\n我们可以通过使用 :schema
名来来查看我们刚刚创建的约束,就像下边这样:
:schems |
你将会看到新创建的约束以及使用它创建的索引,也可以看到我们之前创建的索引:
\n\n我们可以通过尝试创建两个相同的艺术家来测试这个约束是否起作用。
\n执行下边的语句两次:
\nCREATE (a:Artist {name: "周杰伦"}) |
第一次运行这条语句时,节点将会被创建。第二次运行时,你应该会收到以下错误信息:
\n\n属性存在约束能够确保具有特定标签的所有节点具有特定的属性。比如你可以指定 Artist
标签的所有节点都必须包含 name
属性。
使用 ASSERT exists(variable.propertyName)
语法来创建属性存在约束。像下边这样:
CREATE CONSTRAINT ON (a:Artist) ASSERT exists(a.name) |
\n\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 创建索引","url":"/2018/neo4j-tutorial-create-index/","content":"请注意,属性存在约束只能在 Neo4j 企业版中使用。
\n
\n\n索引是一种数据结构,可以提高数据库数据检索的速度。
\n
在 Neo4j 中,你可以给有标签的点的任何属性创建索引。一旦你创建了一个索引,Neo4j 将会管理它,在数据更新时保持最新的索引。
\n使用 CREATE INDEX ON
语句创建索引,像下边这样:
CREATE INDEX ON :Album(name) |
在上边的例子中,我们为所有标签为 Album
的点的 name
属性创建了一个索引。
语句执行成功后,将展示如下信息:
\n\n\n\n当你创建一个索引时,Neo4j 会在后台进行操作。如果你的数据库很大,可能需要一段时间。只有当 Neo4j 完成索引创建后,这个索引才会被上线并用于查询。
\n
索引(约束)成为了数据库模式的一部分。
\n在 Neo4j 浏览器中,你可以使用 :schema
命令查看所有索引和约束。
来试一试吧:
\n:schema |
你可以看到一个索引和约束的列表:
\n\n索引创建完成后,当你在执行查询时会自动使用。
\n然而 Neo4j 也允许你强制提示一个或多个索引,你可以在你的查询语句中使用 USING INDEX ...
创建一个索引提示。
所以上边的示例可以像下边这样强制索引:
\nMATCH (a:Album {name: "猛龙过江"}) |
我们也可以使用多个提示,为每个想强制的索引添加一个新的 USING INDEX
即可。
当 Neo4j 创建索引时,它会在数据库中创建冗余的副本,因此使用索引会占用更多的硬盘空间并减慢写入速度。
\n因此在决定索引哪些数据时你需要进行一些权衡。
\n一般来说当你知道某些节点数量很多时,创建索引是个不错的主意。或者你发现查询时间太长可以尝试通过添加索引来解决。
\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 创建节点","url":"/2018/neo4j-tutorial-create-node/","content":"\n\n要用 Cypher 创建节点和关系,请使用
\nCREATE
语句
这个语句由 CREATE
组成,后边跟上你要创建的点或关系。
我们来创建一个包含乐队名和他们专辑的音乐数据库。
\n第一个乐队被称为筷子兄弟,我们创建一个艺术家节点,并称之为筷子兄弟。
\n我们第一个点看起来像下边这样
\n\n下边是创建筷子兄弟节点的 Cypher CREATE
语句:
CREATE (a:Artist { name : "筷子兄弟" }) |
这个 Cypher 语句创建一个带有 Artist 标签的节点,节点有一个 name
属性,该属性的值是筷子兄弟。
a
前缀是我们提供的变量名,我们可以使用任意变量名。如果我们需要在后边的语句中用到这个点(上边的情况中我们没有用到),这个变量就会是很有用的。注意,变量仅限于在单条语句中使用。
到 Neo4j 浏览器中执行上边的语句,该语句将创建一个节点。
\n一旦 Neo4j 创建完节点,你将看到这样的消息:
\n\nCREATE
语句创建节点但是不展示节点。为了展示节点我们需要在它后边跟上 RETURN
语句。
我们来创建另一个节点,这次我们创建一个专辑,与之前不同的是这次我们在后边跟上 RETURN
语句
CREATE (b:Album { name : "猛龙过江", released : "2014" }) |
上边语句创建了一个带有 Album 标签的节点,它有两个属性:name
和 released
。
注意,我们通过使用它的变量名(本例中是 b
)返回了这个节点。
我们可以通过用逗号分隔来一次性创建多个节点:
\nCREATE (a:Album { name: "我们是太阳"}), (b:Album { name: "小水果"}) |
或者可以使用多个 CREATE
语句:
CREATE (a:Album { name: "我们是太阳"}) |
接下来,我们将在节点间建立关系。
\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 创建关系","url":"/2018/neo4j-tutorial-create-repationship/","content":"\n\n就像在 Neo4j 中创建节点一样,可以用
\nCREATE
来创建节点间的关系。
创建关系的语句由 CREATE
组成,后边跟着要创建的关系详情。
我们在先前创建的点之间创建一个关系,首先创建一个乐队和专辑之间的关系。
\n我们将创建如下关系
\n\n这是 Cypher 的 CREATE
创建上边关系的语句:
MATCH (a:Artist),(b:Album) |
首先我们使用 MATCH
语句查找我们要创建关系的两个点。
可能有很多节点带有 Artist
或 Album
标签,所以我们需要找到我们感兴趣的节点。在这个例子中,我们使用属性值来过滤它:使用之前赋值给每个节点的 name
属性。
接下来是用来创建关系的 CREATE
语句,在这个例子中,它通过我们在第一行中给出的变量名称(a
和 b
)来引用两个节点,关系是通过字符画模式,用箭头指示关系方向来建立的:(a)-[r:RELEASED]->(b)
。
我们给这个关系一个变量名 r
并且给了一个 RELEASE
类型(乐队发行专辑)。关系类型和节点的标签概念类似。
上边是一个非常简单的例子,Neo4j 擅长的事情是处理很多相互关联的关系。
\n为了看到继续创建更多节点和它们之间的关系是多么容易,让我们在刚刚的基础上继续构建。我们来创建一个点外加两个关系。
\n我们将要达到下边图效果
\n\n这张图展示了王太利在筷子兄弟乐队中参与演奏,在专辑中进行表演并且专辑是由他来创作的
\n我们为王太利创建一个节点:
\nCREATE (p:Person { name: "王太利" }) |
现在来创建关系并返回图:
\nMATCH (a:Artist),(b:Album),(p:Person) |
执行完成后你应该就可以看到前边截图中的图了。
\n\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 查询语言 Cypher","url":"/2018/neo4j-tutorial-cypher/","content":"\n\nNeo4j 有自己的查询语言称为 Cypher。Cypher 使用与 SQL(结构化查询语言)类似的语法。
\n
下边是一条 Cypher 语句:
\nMATCH (p:Person { name:"Homer Flinstone" }) |
这条 Cypher 语句返回属性 name
为 Homer Flinstone
的 Person
节点。
如果通过 SQL 来查询关系型数据库,看起来可能更像这样:
\nSELECT * FROM Person |
不过请记住,Neo4j 不像关系数据库模型那样将数据存储在表中,Neo4j 的所有数据都在节点和关系中存储。所以上边的 Cypher 语句查询的是节点、节点的标签和节点的属性,而 SQL 查询的是表、行和列。
\nSQL 被设计为适用于关系数据库管理系统(DBMS)。Neo4j 是一个 NoSQL DBMS,所以它不使用关系模型同样也不使用 SQL。
\nCypher 是专门为 Neo4j 的数据模型而设计,用来查询节点及其相互关系的。
\nCypher 使用字符画来表示模式,使得我们在第一次学习这门语言时很容易记住它。如果你忘记了如何编写,只需要想一想图的样子就会对你有所帮助。
\n(a)-[:KNOWS]->(b) |
主要记住如下几点:
\n(node)
->
[:KNOWS]
在使用 Cypher 时请记住以下几点:
\n让我们再来看一下上边的例子:
\nMATCH (p:Person { name:"Homer Flinstone" }) |
我们可以看到:
\n()
包围Person
是节点的标签name
是节点的属性\n\n要使用
\nCpyher
删除节点和关系,可使用DELETE
子句。
在 MATCH
语句中使用 DELETE
子句来删除任何匹配的数据。
因此 DELETE
子句用在之前例子中的 RETURN
子句的地方。
下边的语句删除标签为 Album,name
属性为 Panmax
的节点:
MATCH (a:Album {name: "Panmax"}) DELETE a; |
\n\n在实际删除前认真检查语句是否删除的是正确的数据是个不错的主意。
\n
为此可以先使用RETURN
子句构造语句,然后运行它。这样可以检查要删除的是不是正确的数据。一旦你对匹配的结果数据满意后,只需将RETURN
子句改为DELETE
子句即可。
你也可以一次性删除多个节点。只需要让你的 MATCH
语句包含所有你想要删除的节点就行了。
MATCH (a:Artist {name: "jiapan"}), (b:Album {name: "Panmax"}) |
你可以通过省略过滤条件来删除数据库中的所有节点,就像我们从数据库中选取所有节点一样,你也可以删除它们。
\nMATCH (n) DELETE n; |
在删除节点时有一个小细节需要注意,就是你只能删除没有连接任何关系的节点。换句话说,在删除节点本身前,必须先删除和它相关的关系。
\n如果你尝试在具有关系的节点上执行上述 DELETE
语句,你将看到如下所示的错误消息:
这个错误消息告诉我们,我们在删除节点前必须先删除它的关系。
\n幸运的是有一种便捷的方式可以做到这一点,我们会在下一课来介绍它。
\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 删除关系","url":"/2018/neo4j-tutorial-delete-relationship-using-cypher/","content":"\n\n你可以像删除节点一样删除关系 - 通过匹配你想要删除的关系。
\n
你可以一次性删除一个或多个关系,甚至可以删除数据库中的所有关系。
\n首先,作为复习,以下是我们之前创建的关系:
\n\n我们来删除类型为 RELEASED
的关系。
有几种方法可以解决这个问题,我们来看其中的三种。
\n下边的语句范围非常广 - 它将删除所有类型为 RELEASED
的关系。
MATCH ()-[r:RELEASED]-() |
你也可以写的更具体一些,就像这样:
\nMATCH (:Artist)-[r:RELEASED]-(:Album) |
上边的语句将匹配所有的 Artist
节点和 Album
节点间具有 RELEASED
的关系。
你甚至可以更具体一些,就像这样:
\nMATCH (:Artist {name: "筷子兄弟"})-[r:RELEASED]-(:Album {name: "猛龙过江"}) |
上边的任意一条语句都可以将 RELEASED
关系删掉,图将看起来是这样的:
节点存在关系将不能被删除,如果我们尝试执行下边的语句:
\nMATCH (a:Artist {name: "筷子兄弟"}) DELETE a |
会看到如下错误:
\n\n这是因为节点上有连接的关系。
\n一种选择是删除所有的关系,然后再删除节点。
\n另一种选择是使用 DETACH DELETE
子句。DETACH DELETE
子句允许你删除一个节点的同时删除与其相连的所有关系。
所以我们可以将上面的语句改为:
\nMATCH (a:Artist {name: "筷子兄弟"}) DETACH DELETE a |
执行这条语句将看到下边的成功消息:
\n\n你可以进一步使用 DETACH DELETE
并删除整个数据库。
只需将过滤条件去掉就可以删除所有的点和关系了。
\n继续来执行下边的语句:
\nMATCH (n) DETACH DELETE n |
至此,我们的数据库中不再有任何数据。所以这节课就作为我们 Neoj4 入门教程的最后一课吧🙂
\n如果你有兴趣了解更多关于 Neo4j 的知识,请查看 https://neo4j.com/docs/developer-manual/current/ 。
\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 删除索引和约束","url":"/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/","content":"\n\n你可以使用
\nDROP INDEX ON
语句删除索引,这将从数据库中删除索引。
因此要删除我们之前创建的索引,我们可以使用以下语句:
\nDROP INDEX ON :Album(name); |
语句执行成功后会展示以下消息:
\n\n你现在可以使用 :schema
命令来验证对应的索引是否已经从模式中删除。
只需输入:
\n:schema |
可以看到索引已经不在模式中了:
\n\n\n\n你可以使用
\nDROP CONSTRAINT
语句删除约束,这将从数据库中删除约束和相关索引。
那么让我们来删除之前创建的约束(和它关联的索引)吧,我们可以使用下边的语句:
\nDROP CONSTRAINT ON (a:Artist) ASSERT a.name IS UNIQUE |
语句执行成功后会展示下边的消息:
\n\n你现在可以使用 :schema
命令来验证对应的索引(和相关联的约束)是否已经从模式中删除。
只需输入:
\n:schema |
可以看到约束已经不在模式中了:
\n\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 导入来自 CSV 文件的数据","url":"/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/","content":"\n\n你可以将 CSV 文件中的数据导入到 Neo4j 数据库中,为此我们来学习下 Cypher 中的
\nLOAD CSV
语句。
将 CSV 文件导入到 Neo4j 的能力,可以实现从其他类型的数据库来导入数据(比如关系型数据库)。
\n在 Neo4j 中,你可以通过本地或远端 URL 来加载 CSV 文件。
\n要访问本地(在数据库服务器上)文件,使用 file:///
路径。除此之外,可以使用任何 HTTPS,HTTP 和 FTP 协议。
我们使用 HTTP 协议加载一个名为 genres.csv
的 CSV 文件。它不是一个大文件,这个列表里包含了 115 个音乐流派,所以它将创建 115 个节点(和 230 个属性)。
这个文件上传到了开放的网络中,所以你可以在你的 Neo4j 浏览器中运行下边的代码,它可以直接导入到你的数据库中。
\nLOAD CSV FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/genres.csv' AS line |
\n\n你也可以忽略 CSV 文件中的某些字段,比如,如果你不希望将第一个字段导入到数据库中,可以从上边的代码中省略
\ngenreId: line[0],
。
运行上边的 Cypher 语句会产生以下成功消息:
\n\n你可以通过以下查询来查看刚刚新创建的节点:
\nMATCH (n:Genre) RETURN n |
下边是通过数据可视化界面看到的节点结果:
\n\n之前的 CSV 文件不包含任何标题,如果 CSV 文件包含标题,可以使用 WITH HEADERS
。
使用这个方法还允许你通过它的列名(标题名)来引用每个字段。
\n我们有另一个带标题的 CSV 文件,该文件包含专辑曲目列表。
\n同样,这个文件不大,列表中包含了 32 个专辑,所以它将创建 32 个节点(和 96 个属性)。
\n这个文件也上传到了开放的网络中,所以你可以在你的 Neo4j 浏览器中运行下边的代码,它可以直接导入到你的数据库中。
\nLOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line |
这将产生下边的成功消息:
\n\n下边的查询语句可以查看新创建的节点:
\nMATCH (n:Track) RETURN n |
同样我们通过可视化框架看到的节点的结果。
\n点击 Table 图标可以看到每个点和它的三个属性值:
\n\n如果需要的话你可以指定自定义字段分隔符,假如 CSV 文件中的分隔符是分号的话,你可以指定使用分号作为分隔符而不是逗号。
\n只需将 FIELDTERMINATOR
子句添加到语句中就可以做到了,像下边这样:
LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line |
如果你需要导入包含大量数据的文件,可以使用 PERODIC COMMIT
来处理。
在 Neo4j 中使用定期提交功能可以在导入一定数量的行之后提交一次数据,这减少了事务状态的内存开销。
\n默认是 1000 行,所以数据会每 1000 行提交一次。
\n要使用定期提交,只需在语句开头插入 USING PERIODIC COMMIT
(在 LOAD CSV
之前)。
下边有个例子:
\nUSING PERIODIC COMMIT |
你还可以将 1000 行的默认值更改为另一个数字,只需将数字加在 USING PERIODIC COMMIT
后边就行了,就像这样:
USING PERIODIC COMMIT 800 |
以下是使用 LOAD CSV
时应该如何格式化 CSV 文件的一些要求:
\\n
,在 Windows 上是 \\r\\n
FIELDTERMINATOR
特殊指定\\
来转义本篇来简单介绍下如何下载并安装 Neo4j,篇目很短,因为真的很简单。
\n首先在 https://neo4j.com/download/ 下载 Neo4j。你可以选择企业体验版或者免费的社区版,这里我是用的社区版。点击 Download 按钮即可开始下载。
\n网站会自动下载适合您操作系统的文件,如果你不想要这个,可以选择通过 这个链接 选择另一个操作系统的版本。
\n当文件下载下来后,就可以安装 Neo4j 了。下载页面包含了将 Neo4j 安装到你的操作系统的一步步指导说明,我在这里介绍下 Mac、Windows 和 Linux 的安装。这里列出的说明,是为了让你快速了解安装 Neo4j 所涉及的步骤,实际步骤可能会随着未来的版本而变化,所以请务必按照下载时网站上的说明来进行安装。当你下载 Neo4j 时,Neo4j 会在感谢页面展示这些说明。
\n这个安装程序包含了运行 Neo4j 所需要的 Java 版本。
\nneo4j
账户修改密码tar -xvf <file>
来提取存档的内容。比如 tar -xvf neo4j-community-3.2.8-unix.tar
,如果你下载的是 tar.gz
的压缩包,那么使用 tar -zxvf
来进行解压$NEO4J_HOME/bin/neo4j console
来运行 Neo4j,或者用 $NEO4J_HOME/bin/neo4j start
让服务进程在后台运行neo4j
账户修改密码这个安装程序包含了运行 Neo4j 所需要的 Java 版本。
\nneo4j
账户修改密码NEO4J_HOME
,比如 D:\\neo4j\\
Windows PowerShell
来启动和管理 Neo4jneo4j
账户修改密码这里是一个已经启动起来的 Neo4j 服务,启动方法取决于你的操作系统,我这里用 Mac 来举例,在应用目录中点击 Neo4j Community Edition 3.2.6
,点击打开窗口中 Start 按钮即可启动 Neo4j 服务。
服务启动后,在浏览器中打开 http://localhost:7474 然后按照提示进行操作。
\n下图是我第一次进入的界面(未来版本可能会看到不同的界面)
\n\n使用界面上提供的用户名和密码来登录,默认的密码是 neo4j
第一次登录时,系统会提示你修改密码
\n密码修改完成后这个界面将会被展示
\n\n在这里,你可以使用当前界面提供的链接来学习更多关于 Neo4j 的知识以及如何创建数据库和运行查询语句
\n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 的 MATCH 语句选取数据","url":"/2018/neo4j-tutorial-select-data-with-match-using-cypher/","content":"\n\nCypher 的
\nMATCH
语句允许你查询符合条件的数据。你可以使用MATCH
来返回数据或对这些数据执行一些其他操作。
MATCH
语句用于匹配给定的条件,但实际上它并不返回数据。为了从 MATCH
语句返回数据,我们仍然需要使用 RETURN
子句。
这里有个使用 MATCH
语句检索一个节点的的例子。
MATCH (p:Person) |
WHERE
子句与 SQL 的 WHERE
子句工作方式相同,它允许你提供额外的条件来缩小查询范围。
同时你可以在不使用 WHERE
子句的情况下获得相同的结果。你可以通过使用像创建节点那样的符号来查询节点。
下边的代码提供和上边语句相同的结果:
\nMATCH (p:Person {name: "王太利"}) |
运行上边任意一条查询语句将会看到如下的节点被展示出来:
\n\n你可能已经注意到,点击一个节点会展开一个分成三部分的外部圆,每个部分有不同的选项:
\n\n点击底部选项将展开节点的关系:
\n\n你还可以通过 MATCH
语句遍历关系。事实上,这才是 Neoj4 真正擅长的事情之一。
举个栗子,如果我们想找出哪个乐队发布了名为「猛龙过江」的专辑,可以使用如下查询语句:
\nMATCH (a:Artist)-[:RELEASED]->(b:Album) |
这将返回以下节点:
\n\n可以看到我们在 MATCH
中使用的模式几乎是不言自明的,它匹配了所有发布过名为 猛龙过江 专辑的乐队。
我们使用了变量(a
,b
)以便在稍后的查询中引用他们。我们没有为关系提供任何变量,因为我们不需要在之后的查询中引用关系。
你可能还会注意到第一行使用的是我们在创建关系时相同的模式,这突出了 Cypher 语言的简单性,我们可以在不同的上下文中使用相同的模式(比如创建数据和遍历数据)。
\n你可以通过省略过滤条件来返回数据库中所有的节点。因此以下查询将返回数据库中的所有节点:
\nMATCH (n) return n; |
我们所有的节点将被返回:
\n\n你还可以点击侧面的 Table 图标用表格来展示数据:
\n\n\n\n返回所有节点时要小心,在大型数据库中执行这个操作可能会产生很大的性能影响。通常建议限制结果以避免意想不到的问题。
\n
使用 LIMIT
来限制输出记录的数量,当你不确定结果集有多大时,使用 LIMIT
是个好主意。
因此我们可以简单的将 LIMIT 5
追加到前边的语句上来将输出限制为5条记录:
MATCH (n) RETURN n |
你可能发现有些查询在第二次运行时非常的快,这是因为在冷启动时服务节点中没有任何缓存,需要到硬盘中查找所有的记录。每当部分或全部记录被缓存,你将发现有了很大的性能提升。
\n一种被广泛使用的技术是「缓存预热」,借助这个技术,我们运行一个查询语句来触发图中所有的点和关系。假设内存可以容纳这些数据,整个图会被缓存起来。否则将会缓存尽可能多的数据。尝试一下它是如何给你带来帮助的吧!
\nCypher(Server,Shell)
\nMATCH (n) |
上边的例子用到了 count(n.prop) + count(r.prop)
,来强制让优化器在点或关系中搜索名为 prop
的属性。用 count(*)
替代它将不够充分,因为这样不会加载所有的点和关系属性。
内嵌方式(Java):
\n@GET @Path("/warmup") |
在 3.0 之后的版本并且使用了 APOC 插件的话,可以运行如下存储过程来完成缓存预热
\nCALL apoc.warmup.run()
\n\nproperty record loading for warmup, apoc.warmup.run(true)
\n
CALL apoc.warmup.run()
默认不读取属性记录,更加建议使用 call apoc.warmup.run(true)
,这个是 3.2.0 以上版本插件的新功能。
这样做除了纯粹的提升性能外还可以提供更多方面的帮助,如果你使用的是 Neo4j集群的话,还可以帮助缓解由于查询滞后而导致的上游问题。例如,如果节点繁忙并且负载均衡超时时间很短,图中没有任何数据在内存中,很可能会显示该集群最初不可用。如果缓存处于预热状态,那么冷启动应该就不会有短暂超时的问题了。
\nAPOC
安装dbms.security.procedures.unrestricted=apoc.*
neo4j
Neo4j 中创建节点时,可以指定多个标签:
\nCREATE (n:Person:China) |
但是在创建关系时,只能指定一种类型,其实官方通过这两个不同词汇(标签 和 类型)也能体现出节点和关系关于分类方面的不同。
\n如下图所示,在尝试用多种类型创建关系时,会报错:
\n\n\n一个节点可以有多个标签,一个关系只能有一种类型。
\n在 GitHub issues 中也有一个简短的解释:
\n\n\nThis is unfortunately out of scope right now the property-graph model fared well with a single relationship-type so far.
\n
\n\nI’d suggest you create multiple relationships between your two nodes that is the way to go.
\n
属性图模型在单一关系类型时表现更好,如果需要在两个节点间表示多个关系,直接创建多条关系就可以了。
\n","tags":["neo4j"]},{"title":"又入手了一个 HHKB","url":"/2023/new-hhkb/","content":"15年我刚工作不久,当时有个 App 叫网易海淘,我通过这个平台在日本亚马逊买到了自己的第一个机械键盘 HHKB,记得当时的价格是1550左右。大学时候好几个舍友都因为玩游戏买了机械键盘,但我一直用的都是罗技很便宜的那款。
\n这个 HHKB 键盘到现在陪伴了我8年多年,电脑换了好几个、工作换了好几份,没换的一直是这个键盘。有点像换马不换鞍的感觉,HHKB 一直作为我最亲密的战友陪伴着我,我在公司写代码、调戏妹子、和其他人对喷都离不开它,我在公司拍工位照片时也都有它的身影。
\n17年:
\n\n18年:
\n\n22年:
\n\n这个键盘我是越用越顺手,越用越喜欢,配合上 Keyboard Maestro,大部分工作都可以通过键盘完成,我也会在 IDE 里配置很多自己顺手的快捷键。
\n入职现在这家公司的时候,公司给我配的是一个15年的15寸 MacbookPro,感觉性能差,所以我在公司一直用的自己的19年有 Touchbar 版本的 Pro,21年左右把自己的 Mac 出了爱回收,换了 M1 Pro。
\n公司配的电脑就长期在家里搁置,周末的时候偶尔用来处理一些临时的工作,公司配的电脑性能又差电池也不够用,不插电源的状态下半小时就没电了。
\n在公司用公司配的电脑的同事陆续都换了新电脑,好一些的换了 M1,差一些的也换了我之前用的 Mac 同等的配置,我也在上个月初休陪产假前找 IT 换了个新的电脑,虽然不是 M1,但性能也不错,i9的 Intel 处理器。
\n用过这款 Mac 的都知道,这个系列最大的槽点就是蝶式键盘,键程极短,毫无打字体验,这就促成了我想在配一个键盘的想法,最开始想着在闲鱼上淘个便宜的,但看来看去没有心怡的,毕竟自己用过的只有 HHKB。
\n今天我在逛咸鱼的时候看到北京有个人卖 HHKB,他说这个键盘是公司年会奖品,拆封后用了一下不适应就收起来了,标价900。看他的配图是 Professional Classic 的无刻(键帽上没有字)版本,网上说这个版本就是我在用的 Professional2的升级版。我在淘宝查了下价格,基本在1650左右,如果真的如他描述只是拆开试了一下,那么这个900的价格还是很有吸引力的。
\n我在闲鱼上跟他交流了一下,通过他的回答来看确实是个外行,也不像是骗子,他说键盘在公司,公司在华茂写字楼,今天下午要去公司开会,可以面交。华茂写字楼离我不远,我和他一番周旋后讲到了850的价格,在交易前他再三让我确认是否会用这个键盘,我就说我可以学习。
\n因为今天北京下暴雨,取键盘的过程还是很坎坷的。我把车听错了停车场,听到了 SKP 购物中心的地下,从停车场上去后冒着雨找对方的写字楼,对方因为要开会,没办法给我,只能把键盘放在了大厅的一个角落里让一名保洁阿姨帮忙看着,我找了好久才找到他的写字楼,当时全身已经湿透了,拿到键盘后往回走找自己的车又找了好久,而且回去的时候才知道,负一层是互通的,早知道我就不冒这么大雨狂奔了。
\n到家后迫不及待打开盒子开始欣赏这个键盘,真新啊,非常喜欢 HHKB 这种设计的简洁感,HHKB 全名 Happy Hacking Keyboard,果然是程序员的开心键盘。
\n\n\n这篇文章就是我用新的键盘完成的,新的键盘相较于 Professional2 来说更软、更轻、更柔一些,相对更静音,Professional2稍微清脆一些,两者不分伯仲,我都喜欢。虽然新键盘上没有刻字,但用起来毫无违和感,毕竟之前的键盘已经用了8年多。
\n\n最开始打算用不到100的价格随便买个普通机械键盘,最后缺花了850买了个自己心怡的HHKB,虽然花了多8倍的价格,但真的是买到心坎儿里了。我对自己不熟悉的领域很谨慎,哪怕不到100块钱也不愿轻易去花,对自己热爱的东西很果断,花多一些钱也愿意。
\n如果你的男朋友是程序员,相信我,送他这款键盘准没错👨🏻💻
\nBTW,开车回家的路上还在下雨,我在一个十字路口亲眼目睹了一场车祸,两个车都赶在变黄灯前加速,一个左转一个执行,我眼看着两个车就想游乐场的碰碰车一样撞在了一起,听到 duang 一声、地面颤动了一下,事情太突然,当时的感觉不太真实,车上的人应该都没有大碍,过十字路口还是要注意安全,黄灯能不抢还是不抢。
\n\n"},{"title":"nginx 配置反向代理 + ssl 模板","url":"/2019/nginx-reverse-proxy-and-ssl-template/","content":"server { |
前几天在对开发环境中的服务进行压测时 Nginx 出现 Too many open files 的错误,这里记录下解决方法。
\n\n先来通过两个命令检查下 master 进程 和 worker 进程的文件句柄限制。
\n在 Nginx 运行时,检查当前 master 进程的限制:
\ncat /proc/$(cat /var/run/nginx.pid)/limits|grep open.files |
检查 worker 进程:
\nps --ppid $(cat /var/run/nginx.pid) -o %p|sed '1d'|xargs -I{} cat /proc/{}/limits|grep open.files |
上边返回结果的第二列和第三列分别为软限制(soft limit
)和硬限制(hard limit
),下边我们来对其进行调整。
/etc/sysctl.conf
中加上 fs.file-max = 70000
/etc/security/limits.conf
中加上 nginx soft nofile 10000
和 nginx hard nofile 30000
sysctl -p
使配置生效/etc/nginx/nginx.conf
中加上 worker_rlimit_nofile 30000;
虽然 Nginx 可以通过 nginx -s reload
使配置生效,但这种方式并不会让全部进程都应用上新的配置,如果你在多核机器下,可以实验下:在执行这个操作后,通过检查 worker 进程句柄限制(方法见上文),还是有部分进程的句柄被限制为 S1024/H4096
,即使试用 nginx -s quit
也不管用。解决方法是用 kill 命令杀掉 Nginx 后重新启动,这样所有的 Nginx 进程就都有了 S10000/H30000
的文件句柄限制。
pkill -9 nginx |
再次验证 worker 进程
\nps --ppid $(cat /var/run/nginx.pid) -o %p|sed '1d'|xargs -I{} cat /proc/{}/limits|grep open.files |
可以看到配置已在全部 worker 进程上生效。
\n"},{"title":"念念的房间","url":"/2023/nian-nian-room/","content":"昨晚又是一整晚没睡,因为一家人来新家开荒,除了我爸,其他人都在这里过夜。由于新家有一个卧室还没有安床,所以我妈和念念就睡在我的床上了,我打的地铺。但因为不太适应,整晚都没睡着。
\n半夜睡不着时,我想起了白天一件有点内疚的事:
\n我们有个卧室是专门给念念准备的,墙壁刷成了淡粉色,还买了她喜欢的床。装好床的那天,她高兴极了,在自己的床上蹦了好久,一直想着如何装饰自己的房间。
\n这次回来,她看到自己的床上放了登登的衣服,地上也有一些其他的杂物。于是,她把那些不属于她的东西全都扔到了其他房间。我当时很严肃地批评了她,告诉她如果不让别人把东西放到她的房间,她以后也就别进其他房间了。她当时一脸惶恐,赶紧把她刚才扔出去的东西一件件搬回来,以讨好我。
\n深夜静悄悄的时候,我想到念念在这件事上并没有错。既然我已经告诉过她那是她的房间,那么她就有权利让自己的房间保持干净和整洁。再者,还有一个月念念就6岁了,我们之前蜗居在60多平的房子里,她一直没有属于自己的空间。第一次拥有自己的房间肯定是非常想占为己有的,我可以理解她,因为我小时候也有这样的想法。想想自己小时候,如果得到了自己非常喜爱的东西,肯定也不愿意让别人糟蹋。在拥有自己房间这一点上,我觉得非常亏欠她,在北京这个寸土寸金的地方只能委屈一下她了。
\n我们计划国庆节前带念念去趟上海迪士尼实现她的公主梦,我对自己的唯一要求是对她多一些耐心,不要因为她的一些小孩子的无理要求而对她发脾气。我就她这么一个女儿,不宠着她宠谁呢。去迪士尼的钱用的是我准备买摩托车的钱,之前因为考试失利,摩托车驾照考了两次,第二次考完后摩托车就对我没那么大吸引力了,所以也迟迟没有订车,这笔钱拿出来带念念去玩一趟把。
\n距上次去远的地方玩刚好过去3年,上一次是离职上家公司入职 TT 之前,到新疆玩了一个星期,一晃三年过去了,时间真快。说到这里,我奉劝各位还没结婚、没生娃的朋友及时行乐,趁着自由能出去玩就多出去玩。也奉劝那些不想结婚、不想生娃的朋友,如果一个人过得开心,请坚持你们的想法。
\n"},{"title":"注定进不去的大厂","url":"/2023/not-get-into-big-factory/","content":"前几天,从我当前所在公司离职不久去了程序员终点站字节跳动的领导联系我,问我考不考虑机会,我考虑几分钟后委婉的拒绝了,这不是我第一次拒绝大厂基本唾手可得的机会,之前也有其他前领导联系过我去小红书负责他下边新开的业务线,也有过百度、快手之类的机会。
\n这篇流水账我想聊聊我选择不去大厂的几个原因。
\n大学刚毕业时,因为年少轻狂,那时候互联网环境也比较好,两年内跳了3次。因为有过频繁换工作的经历,到后来我就对换工作这件事没那么强的意愿了,再换工作时会认真权衡利弊,而且给自己定下了之后每份工作要做3年以上的目标。
\n到今年我已经工作8年多了,已经换过不下4份工作,换工作都是一件成本极高的事,不管是对个人还是对前东家或者新东家。尤其是对个人,换工作后要重新熟悉环境,重新结交人脉、重新认识上下游、重新了解新公司的技术栈…
\n刚换工作后的半年内很多事情对我来说都会是全新的。因为成本极高,所以换工作一定一定要慎重,今年五月份我们组有过一轮人员地震,有三个同学因为出国或者回老家发展,在深思熟虑后选择了离职,还有两个看到突然走了好几个人心里痒,仓促的面了外边的机会,匆匆忙忙跳了槽,前段时间聊起来那些匆忙跳槽的都有些后悔。
\n我现在所在公司,平均工作时间是10:30-19:30,去掉中午2小时休息时间,工作时长为7小时。尽管我中午不午休,拿这个时间来运动、看书、刷题、写流水账,但这也是一大块属于我自己的时间,不管上午的事情有没有完成,午休这段时间都不会有人来找我。
\n去大厂后,晚上七点半下班基本属于奢望了,至少会再多出2个多小时的工作时长,相比现在的工作时长多出了30%,按照现在的市场行情,我不确定我通过跳槽可以再获得30%以上的涨幅,而且即便是获得了30%涨幅,按照工作时长来算,我也只是平薪跳槽,划不来。
\n我现在的团队也招了2个从字节跳槽进来的新同学,这边让他们很满意的一点是晚上9点后不可能有人突然拉他们进会。我告诉他们,不仅晚上9点后不会,晚上8点后就不会有人再找你了,除非线上炸了。
\n不知道是不是自己身体不行,我是真的卷不动,下午7点后没有任何想工作的动力,不知道大厂里每天干到晚上十点多的同行们是怎么坚持下来的。
\n这不是谦虚,我在很多方面都不具备大厂喜欢的能力,比如应试能力。我觉得大厂面试和中国的应试制度有些相似,通过背一些工作中实际用不到的八股问题进行面试,通过多伦面试后进入公司,而不是看一些更实际的能力,我也能理解这种做法,因为找工作的人太多了,这是最高效筛选人才的一种方法。
\n我在做面试官的时候不喜欢问八股文,我会主要关注对方在工作之余做了些什么、写过什么软件。如果一个人不爱一件事,他就不可能把它做得真正优秀,要是他很热爱编程,就不可避免地会开发自己的项目。
\n我那个去字节的领导跟我说他们在新员工入职第三个月的时候要做工作汇报,入职这三个月内并不是像我现在公司这样给新员工充分的时间安心学习新东西,而是上来就介入工作,在汇报时不仅要讲自己对这三个月工作的理解,还要讲工作的成果和输出。这种做汇报展示成果的能力也是我欠缺的,我也不擅长公众演讲。
\n大厂因为发展快,人员变动也相对较快,我遇到好几个朋友和我说他的 Leader 比他小,另一个说他的 Leader 是95后之类的。
\n一个好的直属 Leader 对工作体验太重要了,在工作中伴随我们最久对我们的影响最大的人就是直属 Leader。我不太相信一个工作两三年的人有特别好的管理能力。对管理的认识虽然可以靠书本学习一些驭人之术来提升,但更多的是靠人生阅历,前者是 PUA,后者是真正的管理。但要做到后者是需要时间的,就像我们不可能找10个孕妇来一个月内生出一个宝宝一样。
\n我那个去了字节的领导第三天就要求去参加季度规划会,之前他的话语权很重,大家都会听他的,但他在字节的第三天,就在会上比被自己小的产品经理diss,问他是不是不了解背景,质疑他的能力。这也是我前边说过的温情,一个稍微成熟点的,有点社会阅历的成年人不会对一个刚入职3天的人讲出那种话。我的自尊心很重、心眼很小,承受不了职场PUA…
\n有人生阅历的 Leader 更加善而坚定,更加有管理上的温情,这样的领导能站在员工的角度理解员工,照顾员工的感受,真正为员工着想。
\n另外大厂里还会有各种「嫡系」文化,在有裁员指标时,通常裁的不是能力不行的,而是非嫡系的。在有晋升指标时最先安排的也是嫡系里的“自己人”。
\n我深知人外有人天外有天,我可以在小公司里混的如鱼得水,但放到大厂的人才荟聚的地方也许就是一颗再普通不过的螺丝钉。
\n我不想在一个默默无闻的岗位工作,这种地方不会让我感受到成就感,很容易失去工作的动力。而大公司就是这么一个地方,大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。
\n我也许更擅长把一件事情从0做到80分,但从80做到100甚至120分不是我擅长的,而这是在大厂里需要具备的精益求精的能力。我更喜欢做宽而不是做专,喜欢做个八面手而不是一颗螺丝钉,由此也可以看出小公司更适合我一些。
\n我不想离开现在的公司的最主要原因还是工作时长方面,虽然现阶段的我需要钱,去大公司确实可以用时间换钱,但综合考虑各种因素,对于这个年龄和家庭情况的我已经不再合适。留给那些还年轻、还有梦想的年轻人们去闯一闯吧,未来属于他们。
\n"},{"title":"由日本排放核废水引发的思考","url":"/2023/nuclear-wastewater/","content":"首先声明,我不是精日,只是想就事论事反思一下最近看到的新闻。
\n日本放出要排放核废水的消息后,国内声讨的新闻铺天盖地,官方对这件事大肆渲染,带百姓们的节奏。官方宣传这件事有他自己的目的,其中之一是最近出现的很多人民内部矛盾已经不可协调,需要一些外部事件来转移和宣泄人民的情绪。
\n官方的做法无可厚非,毕竟是一种维稳的政治手段,但百姓们的各种行为就让人大跌眼镜了。超市的盐被大妈们抢购一空,女士和小学生边撕心裂肺的哭边骂街,年轻人上街打砸日本车。各个年龄段的人民都在用自己的方式来宣泄情绪。
\n对比较理性的人来讲,他们通常不问做错事是否有理由,而是先确定当前是否做错了事。
\n在日本排放核废水这件事上,人们不判断日本的做法到底符不符合规范,只要是日本做的事,我们的人民从来都不判断对错,直接默认就是对方的错,上来就是破口大骂。在其他很多事情上也是类似。
\n实际上日本排放核废水是经过国际原子能机构同意的,已经达到了安全标准。同时韩国和中国也都参与了监管。
\n美国之音的原文如下:
\n\n\n日本政府決定將核廢水排入海洋的作法已獲得國際機構的背書。自2021年開始評估約兩年後,聯合國核監督機構國際原子能機構(IAEA)上個月初批准了日本排放的計畫,得出的結論是將核廢水排入海洋,對人類和環境的輻射影響“可以忽略不計”。
\n
另外,日本排放核废水受影响最大的是哪个国家?肯定是日本自己本国,只能说我们的国民咸吃萝卜淡操心,自己过的不好还要去操心别人。
\n我们根本不需要成为所有领域的专家,只要有一点点批判性思维,先问有没有再问为什么,就能够避免大部分常识性错误。
\n由此可以看出,我们国家国民的思想进步还有很长的一段路要走。
\n"},{"title":"Obsidian换成Notion","url":"/2023/obsidian-to-notion/","content":"我之前经常给别人吹嘘Obsidian的强大,甚至在公司的内部分享中也给大家推荐过Obsidian。
\n我现在最常用的是Notion,很早前就放弃了Obsidian。
\n我放弃Obsidian的几个主要原因是有:
\n这些缺点在Notion上都得到了解决,Notion本身就是基于Web的,数据自始至终都在云端,不存在数据同步问题。我一开始吹嘘Obsidian时用到的一个理由是数据属于我自己,不信任任何第三方,第三方跑路后你的数据就再也找不回来了。现在想想真是既可笑又狂妄,况且Notion支持数据导出,导出后的数据就是纯文本Markdown格式,很容易迁移。
\nNotion在手机端做了大量优化,弱网环境下也可以使用离线数据,离线编辑,有网后自动进行同步和合并,界面也极其流畅。
\nNotion完全可以开箱即用,你可以使用它的Web端,也可以使用它的客户端,即使一个全新的用户也能很快上手用起来。
\nNotion的数据库系统和模板库也很强大,我用数据库系统记录Twitter上感兴趣的推文,还用它来维护我在Github上star的Repo。这两个用到数据库的功能都是使用别人开源的代码实现的。模板系统我用的不多,公司项目有专门的项目管理工具,我的待办事项使用Things。在数据库系统和模板系统方面,我在今后有精力了还需要深入学习一下。
\n有一说一,Obsidian由双向链接构成的知识图谱确实非常强大,猛一看也很唬人,但一般用户很难维护起自己的网络结构。Obsidian的插件生态也很好,有各种强大的插件可以使用,但还是有一定的上手难度。
\n我觉得Obsidian和Notion这两个阵营很像手机操作系统中的安卓和苹果。
\n我在大学使用安卓手机的时候,最喜欢折腾的事情就是刷机、装插件、改主题、改字体等等。后来改用iPhone后一开始也喜欢折腾越狱之类的,后来随着苹果的生态越来越好,加上自己精力有限也就不折腾了。在折腾iPhone越狱期间,我发现我每次改完一个地方,过段时间就会刷回原生操作系统,比如改了个字体、加了个图标,过段时间腻了就又会刷回去,到头来还是觉得自带的顺眼、舒服,自己整的花里胡哨的一点用都没有。
\n想想也是,苹果这么大的公司,由那么多专业的设计师设计出来的界面、选择出的字体,一定是符合绝大多数用户的最佳方案。我一个非专业人事居然会认为自己改的风格会超过专业的设计师。
\n\n\n专业人士和业余爱好者的一个差别在于,是否了解极限的存在。
\n
Notion也是一样,Notion内部一定有非常专业的产品经理和设计团队去考虑如何更好的为用户服务,让用户有更好的使用体验。我们作为普通用户,享受他们的服务就可以了,业余的水平再高也是业余的,专业的事就交给专业的人去做就好了。
\n既然专业的人做专业的事,同理专业的事应该交给专业的工具,所以我不会用 Notion 作为任务管理工具,因为他在这个领域并不专业。
\nNotion 的产品完整度很高,每个功能都进过了精细的打磨,整个产品用起来有很扎实稳重的感觉,而 Obsidian 给我一种轻飘飘的感觉。
\n苹果和Notion虽然一直在听取市场上用户们的需求,但他们的每次改动都是经过深思熟虑的。让用户满意并不等于迎合用户的一切要求。用户不了解所有可能的选择,也经常弄错自己真正想要的东西。
\n做一个好产品就像做一个好中医一样,不能头痛医头,脚痛医脚。病人告诉你症状,你必须找出他生病的真正原因,然后针对病因进行治疗。
\n"},{"title":"一行 Python 代码能做什么","url":"/2021/one-line-python/","content":"大学中虽然教授过 C、C++、Java,但我当时选择自学了 Python,并且在工作的前几年也是用的 Python,我非常喜欢这门语言。
\n虽然我在平时开发时,不喜欢为了让代码精简些、酷炫些而写出匪夷所思的代码,那些代码大部分杂乱无章、可读性差,第二次再读自己的代码就不一定能读懂。但 Python在这方面做得非常好,这也是为什么它经常成为编码挑战、面试手写代码的首选。
\n下面我对 Python 中用一行代码就能解决的问题做了下整理,通过这些代码片段和技巧也能看出这门语言设计的精妙和优雅:
\n这个单行代码对于计算一个数字的位数之和非常有用:
\nsum_of_digit = lambda x: sum(map(int, str(x))) |
在其他语言中,条件式有时看起来有点笨重,如:
\nx = 10 |
用Python简化它:
\nx = 10 |
读起来和正常的英语有些像。
\n你可以用以下结构在一行代码内形成一个if语句:
\n<条件-真> if 条件 else <条件-假>
我们有时会使用大量的 if-else
语句,我们使用 elif
关键字,它是其他语言 else if
关键字组合的缩写,这对于转换为单行的python代码来说比较困难,看一个在代码里面使用 elif
的例子:
x = 200 |
这段代码将打印第二条语句,即 x is equal to 200
。
现在我们把这段代码转换成一行代码:
\nx = 200 |
这里使用的依然是上一个技巧,只是将其扩展为了多个条件,不过我不建议这样写,它也许很快就会变得难以阅读和维护。你需要权衡好什么时候使用这个技巧。
\n使用字符串切片操作,在一行代码中反转字符串:
\ninput_string = "Namaste World!" |
在一行中为每个变量分配不同的值,甚至不同的数据类型:
\nname, age, single = ‘jc’, 35, False |
列表推导是一种简单而优雅的方法,它可以从现有的列表中定义和生成新的列表:
\n举个例子,生成填充了数字 0 到 4 的列表:
\nscores = [] |
同样的结果我们可以用列表推导法来实现:
\nscores = [x for x in range(5)] |
这是 Python 最伟大的功能之一。
\n继续扩展上一个技巧,如果我们想根据一个条件来跳过一些项呢?
\n例如,如果我们只想要奇数:
\nscores = [] |
在列表推导中使用条件语句同样可以实现:
\nscores = [x for x in range(20) if x % 2 == 1] |
优点:列表推理不仅更清晰,而且在大部分情况下其性能也比单次循环好得多。
\n斐波那契数列是一组数字的集合,其中每个数字都是它前面两个数字之和。
\n在一行代码中,我们使用列表推理和 for
循环生成一个斐波那契数列:
n=10 |
我们可以使用**
操作符在一行代码中合并多个字典。
我们只需要将字典和**
操作符一起传给{}
,它就会为我们合并字典:
dictionary1 = {"name": "Joy", "age": 25} |
dict = {'Name': 'Joy', 'Age': 25, 'Language':'Python'} |
这个交换键值对的代码非常实用。
\n在其他语言中,交换两个变量需要借助第三个变量(一个临时变量)来实现:
\ntmp = var1 |
在Python中,可以直接在一条语句中完成:
\nvar1, var2 = var2, var1 |
甚至更进一步,可以使用相同的技巧来交换数组中的元素
\ncolors = ['red', 'green', 'blue'] |
列表推理还可以用在矩阵(多维数组)上:
\nmy_list = [(x, y) for x in [3, 4, 6] for y in [3, 4, 7] if x != y] |
与列表推理的概念相同,举个例子,我们需要一个键/值对,其中值是键的平方:
\nsquare_dict = dict() |
下面使用字典推导:
\nsquare_dict = { num: num * num for num in range(1, 11) } |
数据工程师经常与列表和多维数据打交道,有时他们需要将多维列表转换成一维的。他们经常使用 numpy
之类的包来做这件事。
下面的例子展示了如何使用纯 Python 的单行代码来完成同样的工作:
\nmy_list = [[1,2], [4, 6], [8, 10]] |
是的,这依然时列表推导的一种应用。
\n假设你有一个列表,你想把它前边的几个值捕捉到变量中,其余值都放进另一个列表。这在处理参数的时候会很有用。
\n让我们看一个例子:
\nx, y, *z = [1, 2, 3, 4, 5] |
脚本最常用的一个场景是处理文本文件,特别是将文件的每一行读入到列表中,这样我们就可以对数据进行我们需要的操作了。
\n在 Python 中,我们可以用强大的列表推导法将文件所有行读入一个列表:
\nmy_list = [line.strip() for line in open('countries.txt', 'r')] |
Python 是一门神奇的语言。今天我展示了几个强大的 Python 技巧,它们将帮助我们开发更优雅、更简单、更高效的代码。
\n"},{"title":"对配置中心进行优化","url":"/2017/optimization-config-server/","content":"现在我们的配置中心使用 Spring Cloud Bus
与 Spring Cloud Config
的整合,并以 RabbitMQ
作为消息代理,实现了应用配置的动态更新。
架构如下图所示:
\n\n但是,现在的架构有个很大的缺陷,就是每次在修改配置文件后,需要手动地触发下应用的 /bus/refresh
接口,才能完成更新操作。假如我们后端有上百个不同的服务在运行的话,手动去更新简直就是灾难,更新某一个应用时,需要先查到他的 IP + 端口号
。而且如果同时修改了很多服务的配置的话,一个一个去发更新请求就有些太痛苦了。
解决这个问题的办法是借助 GitLab 的 Webhook
机制,让 GitLab 帮我们去发这个请求。Webhook
用于当 GitLab 上的项目有变化的时侯以 HTTP 接口的形式通知第三方。
进入我们 GitLab 的 config-repo/app-a
仓库,在 Settings - Integrations 中可以对 Webhook
进行设置:URL 填写我们刷新配置的地址,触发器选择 Push events
就够了,然后直接保存。
现在可以测试一下,修改配置后,不需要再手动访问 /bus/refresh
也能完成更新操作了。
初步优化到这里就结束了,已经可以省去很多人力成本,简单来说就是服务的配置更新需要 GitLab 的 Webhook
通过向具体服务中的某个实例发送请求,再触发对整个服务集群的配置更新,不过这样做还是有问题的:首先一个问题是,我们在配每个服务 Webhook
的时候,其实也需要根据自己在线上不同的 IP + 端口号
来配置,另一个更严重一些的问题是,虽然我们现在的方式可以依赖消息总线,通过更新一个实例达到更新所有实例的目的,但这样做有个前提是,接受 /bus/refresh
的那个实例要保证没有宕掉,如果它挂了,配置依然不会被修改。比如我们的 app-a
服务有很多实例,我们分别取名叫 app-a-1
,app-a-2
,app-a-3
…,现在我们在 Webhook
中设置的地址是 app-a-1
实例的 /bus/refresh
地址,假如在我们更新完 GitLab 上的配置文件后,app-a-1
那台机器刚好出了问题,这个时候其他的实例也就得不到更新了。
其实这个时候依赖哪个节点都不合适,谁也不知道哪个节点在什么时候会挂掉,可能有人会想到,我可以给所有节点都发一遍请求,我来分析一下这样做的缺点:
\nWebhook
RabbitMQ
通知其他所有实例,这样做非常浪费资源而且消息总线的意义就不存在了Webhook
中的配置所以我们需要做一些调整,让服务集群中的各个节点是对等的:我们在 Config Server
中也引入 Spring Cloud Bus
,将配置服务端也加入到消息总线中来。/bus/refresh
请求不再发送到具体实例上,而是发送给 Config Server
,并通过 destination
参数来指定需要更新配置的服务或实例。
在 Config Server
项目的 build.gradle
中加入消息总线的依赖:
compile('org.springframework.cloud:spring-cloud-starter-bus-amqp')
然后修改 application.yml
,加入
management: |
然后在 app-a
的 GitLab 仓库中修改我们刚才设置的 Webhook
,将地址改为:http://172.24.8.100:7020/bus/refresh?destination=app-a
注意端口号已经变了,对应的是配置中心的端口,destination
的值是要刷新的服务名称,这样的话配置其他服务 Webhook
的时候,只需要修改这个名称就可以了。
通过上面的改动,我们的服务实例就不需要再承担触发配置更新的职责。同时,对于Git的触发等配置都只需要针对 Config Server
即可,从而简化了集群上的一些维护工作。
保存 Webhook
后再次修改 GitLab 上 app-a
的配置文件,提交修改后刷新页面看到结果已经变为最新的配置了。
今天终于把哈佛幸福课的23集都看完了,每集一个半小时,一开始一天看一集,真的有点长,时间很紧张,而且每看完一集我都会通过 AI 提取出文章中的关键点,把这些内容读一遍改一改病句还要再花一些时间,每天花在学习幸福课上的时间超过2小时,导致我一整天的时间安排都很紧凑,学习这门课的初衷是让自己更幸福,现在反而更不幸福了,这段时间也通过这种方式水了好几篇文章。
\n\n一天我突然意识到,我为什么要这么匆匆忙忙的着急看完,课程中 Tal 说过一句话:「比物质充裕更能带来幸福的是时间充裕」,让自己慢下来,过程中更投入一些,所以剩下的课程是每天看25分钟,一周学一节课。同时我把之前水的那些又臭又长的文章删掉了,在这篇文章中用小量的篇幅记录几个对我影响最大的观点。
\n下边进入正题:
\n积极的问题会引导人正向地思考。
\n\n\n如果我们只问消极的问题,比如「为什么这么多人失败」,我们就没法看到潜藏在每个人心中的伟大,如果我们只问「我的人际关系该怎样改善」,我们就无法看见身边的人所拥有的宝贵财富和奇迹。
\n
我们要多问积极的问题:
\n问题会带来探索,探索的内容取决于我们所问的问题。
\n我们如何理解现实才是最后所得的结果。
\n信念常常会成为自我实现的预言,但它是如何作用的? 有两个机制:
\n将卓越和平庸划分开的有两样东西:
\n通过拉伸自我(走出舒适区),多去尝试,挑战自我,通过具象化使我们明白自己可以做到。
\n勇气并不是没有畏惧,而是有了畏惧还坚持向前
\n研究表明,失败真的是成功之母。最成功的人往往是失败得最多的。学会面对自己的失败,在失败中学习。这是学习的不二法门。
\n爱迪生比任何科学家获得专利都多的人,同样也是失败过最多次的人。真正来自于失败的痛苦远小于我们想象的。
\n不要因为害怕失败而放弃去尝试自己真正想做的事,
\n允许自己有缺点、犯错误,允许自己做人而不是神。在合理合法的范围内,对自己宽容一点。
\n当经历感情创伤时,你会看着它说“我只是普通人,我很难过,真希望事情不是这样,但我接受它,就像接受重力定律一样,因为重力定律是一种物理本质,就像感情创伤是一种人性本质,允许为人。”
\n过犹不及,多则劣,少则精。
\n两首好歌同时放,就是噪音。
\n留下自己真正想要的,扔掉并没有很想要的,就算它很珍贵。比物质充裕更能带来幸福的是时间充裕。
\n少做点事,可以完成得更多。时间充裕的人,往往更容易获得幸福感。
\n宁缺毋滥,简化与效率是以曲线形式存在。
\n果断坚决,在适当的时候学会说”不”,弄清楚你究竟真正想做的东西,然后去做。
\n甚至比问问题更重要,比考试更重要,比我们有多成功,多被人景仰更重要得多。
\n最能给人幸福感的东西,是良好的人际关系。亲密关系比很多事情都重要,它会给人带去有治愈能力的爱和温暖。
\n最成功恋情的四个特点:
\n性在长久美好恋情中很重要。爱情,准确地说,性的至高点使爱具体化,使爱具体化。
\n基因决定的基准幸福水平,当我们不锻炼时,就像打了镇静剂。
\n运动是一项对现在和未来的投资。
\n我们必须和本性抗争,和本性抗争是很难的,提升我们幸福的水平是很难的,而同时要和本性抗争则是难以想象的困难。
\n从希望被认可变成希望被了解。一个人很多时候不是因为完美而被喜欢,是因为真实而被喜欢。因为真实而被喜欢,才是持久、轻松、可持续发展的。
\n并不是说要完全去除我们依赖别人的自尊,而是明白更重要的是被了解;去表达自己,而非给他人留下印象。这样人生会变得更轻松,更简单。
\n那些成功人士,一是他们有习惯,二是他们有恢复,有休息。
\n我们要转变对生活的理解:
\n应心怀感激,不要等到不幸发生时才意识到。
\n有很多好事值得我们感激,但我们都把它们习以为常,认为理所当然。例如我们把父母、朋友对我们的好视为理所当然。
\n把感激培养成一种生活习惯,对身体的好处,包括心率变异性,它能预测我们是否能长寿,预测我们是否健康。当我们感激时,副交感神经系统功能增强,使我们变平静,从而加强免疫系统,当感激成为我们的性格。还有很多好处,所以感激不只上一种心情,也是一种性格。
\n表达感激时我们感觉很好,对方也会感觉很好,他们的获益良多,于是你创造了一个双赢的局面,一个上升的螺旋。
\n怎样培养感激?
\n每天两次花一分钟时间留意周遭的一切。
\n花一分钟的时间,在上班的路上看看美丽的草地,青翠的树,美丽的雪。
\n晚上用一分钟去回忆,回想你度过的一天,写下让你心怀感激的事物。
\n"},{"title":"借《饮食男女》聊偏见","url":"/2023/prejudice-Eat-Drink-Man-Woman/","content":"\n\n道德的偏见会让我们在面对事情的时候,根本没有办法启动理性思维,而一个不成熟的社会会有特别多的道德偏见。
\n
最近一两年我很少完整地看电影或电视剧,没有时间也没有机会去电影院,更多的是在短视频平台上看一些由小美和小帅主演的电影剪辑,不过有一部电影我在去年完整刷了两遍,叫《饮食男女》。这部电影是我很喜欢的一个叫《文化有限》做书影剧解读的播客节目推荐的。
\n《饮食男女》由李安在1994年出品,剧情讲述每周末等待三位女儿回家吃饭的退休厨师,面临的家庭问题与两代冲突。借由彼此的生活与冲突,建构出不同年龄层、不同职业的价值观,描述90年代台北都会的两代关系。
\n我为什么提到这部电影呢?因为看完这部电影后,我心中一直挥之不去的一个词关键词是「偏见」。不是说这部电影带有什么偏见,而是我们这些看这部电影的人可能会有的先入为主所带来的道德偏见。
\n男主朱爸爸有三个女儿,大女儿家珍、二女儿家倩,三女儿家宁,三个女儿都和朱爸爸一起住,因为朱爸爸是大厨,每周末一家人都会有个聚餐仪式。
\n我的偏见体现在这三个女儿身上。
\n大女儿家珍,是一名化学老师,母亲过世后因为她是最大的孩子,自然就担任起家中母亲的角色。被初恋男友抛弃后(后边是有反转的,为了不剧透就先这样简单说明),心情失落看不到希望,就信了耶稣,平时也不化妆,家里人给她介绍对象也很抗拒。给人的感觉是压抑、冷漠、古板,一位非常传统的大龄剩女。
\n每每看到家珍的形象,就会让我想起小学时候的语文老师。
\n\n二女儿家倩,事业有成,担任航空公司副处长,很会打扮,知性、大方也很开放,有体力非常好的男朋友(你们懂得),还做得一手好菜。因为职场优秀,赚了不少钱,自己独立买了房子,房子是期房,再过一段时间才能盖好。
\n该说不说,吴倩莲真漂亮。
\n\n三女儿家宁,还在读大学,典型的乖乖女,有些唯唯诺诺,空闲时会在一家汉堡店兼职打工。
\n插句题外话,我们组之前招了一个新人也叫家宁,一直觉得这个名字我在哪里见过,而且我每次叫他都感觉特别顺嘴,今天写这篇文章的时候才意识到家宁是《饮食男女》中三女儿的名字。
\n\n经过这样的背景介绍,如果是你,猜一下三个女儿中谁会第一个搬出去住?谁最不可能搬出去住?
\n按照正常推理,二女儿家倩一定是第一个搬出去的,因为他有自己的房子,也有男朋友,事实上家倩确实是第一个提出要搬出去住的,可结果是房地产公司跑路,男朋友劈腿。
\n最不可能搬出去的是谁?我在第一遍看时确信一定是大女儿家珍,其次是二女儿家宁。家珍被男朋友分手后就成了不婚主义,平时上下班坐公交时都是把耳机放最大声听教会的圣歌。三女儿乖乖女,而且还在读大学,短时间内也不会离开家。
\n可实际上,最早搬出去住的是三女儿家宁,非常出人意料。虽然家宁乖巧,但她的内心非常勇敢。她喜欢上了闺蜜的男朋友,并大胆在一起(这里我站家宁,剧情并不狗血,是闺蜜太傻逼)。两个小年轻一来二去,没多久家宁就怀孕了,对方是个富二代,家宁在一次晚宴上宣告这个消息后就搬去了男朋友家住。
\n第二个搬出去的是大女儿家珍,她在偶遇体育老师后周明道后,就被对方的真诚、热情吸引了,两个人也开始交往,家珍内心积攒的压抑也开始释放,不再自己承受孤单、不再沉默,也在一次家宴上提出要搬出去和男朋友住。
\n电影的最后,留在朱爸爸身边吃饭的人是二女儿家倩,二女儿家倩其实是最关心父亲的,但只是说不出口。
\n看似最不能守住传统和孤独的人,缺坚持到最后。
\n\n《饮食男女》关于偏见的这个角度我之前就想写一写,但最终触发我写出来的是我刚刚遇到的下边这件事。
\n周末我去星巴克点了一杯咖啡,读了一个多小时书。
\n坐在我隔壁桌的女人,看起来比我年长几岁,桌子上空空的,坐着看手机,我下意识认为她是来这里占便宜吹空调的。过了很久,我快要离开的时候,她点了一杯咖啡。原来人家只是暂时不想喝,而且不在意别人的看法,也不是为了吹会空调进来坐会,我当时这么揣测人家感到非常羞愧。
\n顺便给自己洗一下,我去星巴克读书不是为了装逼,而是确实需要一个高效阅读的环境,就像当年JK罗琳要去五星级酒店才有灵感写哈利波特,咱没有人家的经济实力,只能去个星巴克。我经常去的那家星巴克人很少,适合在里边看书。
\n在日常生活中我们着有各种各样的偏见,北京人对外地人的偏见,正式工对外包的偏见,其他省对河南、东北的偏见等等。荀子曰「凡人之患,蔽于一曲而暗于大理」。意思是:人的认识,由于受到视野范围的局限或由于个人认识上的偏见,大都易于被局部的小道理所蒙蔽,而看不到、认不清全局的大道理。
\n是人就会有偏见,我们习惯评判身边的人谁好谁不好,喜欢比较。红楼梦作者曹雪芹也在提醒我们,一个生命的存在自然有他存在的价值。小说的五十回之前,晴雯一直没有什么特殊表现,大家会觉得她大概也就是个配角,可是在第五十二回里晴雯因为病补雀金裘变成了主角。
\n我们都是凡夫俗子,无法避免偏见,只能通过尽可能扩展自己的认知来理解和尊重别人的不同。就连大思想家歌德也承认:「我能确保正直,却不能保证没有偏见。」
\n《饮食男女》中最最最大的偏见(用偏见可能不太合适,意外或者惊喜更恰当一些)来自朱爸爸和锦荣,我不想再剧透,大家自行观看。豆瓣的评分9.2,非常高的分数。
\n这部电影前几分钟做菜的镜头行云流水,分镜用的也很好,据说做菜的片断常常作为一些学校影视基础课的范例。这部电影的故事情节也不止我说的这么简单,是一部非常有深度的家庭亲情剧,推荐大家去看一看。一万个人心中有一万个哈姆雷特,不同的人有不同的解读,这部剧也有很多关于「性」的解读,毕竟电影名取自礼记中的:「饮食男女,人之大欲存焉」。
\n最后值得一提的是,这部电影在做菜时的用配乐特别好听,很欢快、有烟火味、听的时候心情也会好起来,大家也可以听一下:https://music.163.com/song?id=531878137
\n"},{"title":"量化喝水量","url":"/2022/quantify-drink-water/","content":"我习惯在思考的间隙或者做完一整块工作后喝水,但之前一直不知道自己一天会喝多少水,经常会因为喝水太多导致半夜被尿憋醒上厕所,从而影响睡眠。我曾经还给自己立了一个原则:下午 6 点后不要再喝水,发现效果不大,晚上该去厕所还是要去,后来改成 5 点后不要喝水效果也不太大,我觉得根源是其他时间喝水量太多了。
\n最近我自创了一个方法来量化我的喝水量,已经尝试了两周,效果很不错,而且比较简单、易操作,我定期用叮咚买菜购买 4-5 桶怡宝 1.555 升装的纯净水,每天就定量喝这一桶水。因为我平时都在有空调的环境中办公,而且也没有特别大的运动消耗,所以 1.555 升是个比较合理的量。而且这个水的价格也并不贵,每天不到 3 块钱花在喝水上很值,并且因为有了这个水,我就不会再去买其他饮料了,反而节省了一笔开支。
\n\n之前早上到公司后我会吃个凉的水煮蛋并喝一杯水,吃完之后八成会拉肚子,我曾怀疑是因为没喝热水所以才拉肚子,但换成热水还是如此。最近两周刚好没有再吃煮鸡蛋,水也换成了纯净水,反而不拉肚子了。但我这里没有控制好变量,两个变量(凉鸡蛋、水)都变了所以其实不太好说是哪里的问题,但我觉得不太可能是公司水质问题,应该就是凉鸡蛋导致的,如果是水的问题,其他人应该也会有拉肚子的情况。况且公司的水使用的商用的滤水装置,定期检查,不会有什么问题。
\n我在喝怡宝这个水时确实喝出了甘冽都口感,这种口感会让我心情愉悦起来。我现在每天早上到公司后,会先用公司的咖啡机接一份意式浓缩(之前是直接接美式),然后兑上怡宝的纯净水,口感会好很多,也会好喝很多。
\nP.S. 通过图片可以看出,我司的咖啡豆还是不错的,油脂很丰富。
\n经过两周的观察,我发现我一上午能喝掉将近三分之二的水,照这么来看之前下午喝的会比这个量还大,远远超过 1.555 升,估计接近 2.5 升了。
\n下边是我这几天的战果:
\n\n施行这种量化方式后,最近一段时间半夜没有再上过厕所了。
\n"},{"title":"快速搭建一个静态文件服务","url":"/2021/quick-start-static-file-service/","content":"\n\n前几天朋友问我如何在没有 root 权限且无法编辑 Nginx 配置的条件下,搭建一个静态文件服务。
\n我最快想到的是用 Go 写一个程序,直接在目标机器上执行 Go 编译好的二进制文件,应该不会超过 10 行代码。
\n本着不重复造轮子的想法(另一个主要原因是朋友当时非常着急),尝试找了找前人造好的轮子,于是找到了这个项目:https://github.com/philippgille/serve。
\n看了一下介绍,和我的想法相同,同时提供了便于扩展的参数,而且提供了各个平台编译好的可执行文件。
\nscoop install serve |
brew install philippgille/tap/serve |
wget https://download.jpanj.com/serve_v0.3.2_Linux_x64.zip |
$ serve -v |
$ serve -p 8100 |
$ serve -p 8100 -d "/opt" |
$ serve -p 8100 -d "/opt" -a "test:test" |
$ serve -p 8100 -d "/opt" -a "test:test" -s |
$ serve -p 8100 -d "/opt" -a "test:test" -b "0.0.0.0" |
$ serve -h |
开始前先来思考一个问题,如果一个文件的大小超过了一块磁盘的大小,该如何存储?
\n独立硬盘冗余阵列(RAID, Redundant Array of Independent Disks),简称磁盘阵列,利用虚拟化存储技术把多个磁盘组合起来,成为一个或多个磁盘阵列组,目的为提升性能或数据冗余,或是两者同时提升。
\n简单来说,RAID
把多个磁盘组合成为一个逻辑磁盘,因此,操作系统只会把它当作一个实体磁盘。
RAID 0
假设服务器有 N 块磁盘,RAID 0
是数据在从内存缓冲区写入磁盘时,根据磁盘数量将数据分成 N 份,这些数据同时并发写入 N 块磁盘,使得数据整体写入速度是一块磁盘的 N 倍;读取的时候也一样,所以在所有的级别中,RAID 0
的速度是最快的
但是 RAID 0
不做数据备份,N 块磁盘中只要有一块损坏,数据完整性就被破坏,其他磁盘的数据也都无法使用了。
RAID 1
RAID 1
是数据在写入磁盘时,将一份数据同时写入两块磁盘,这样任何一块磁盘损坏都不会导致数据丢失,插入一块新磁盘就可以通过复制数据的方式自动修复,具有极高的可靠性,RAID 1
的数据安全性在所有的 RAID
级别上来说是最好的。但无论用多少磁盘做 RAID 1
,仅算一个磁盘的容量,是所有 RAID
中磁盘利用率最低的一个级别。
RAID 1
在一些多线程操作系统中能有很好的读取速度,理论上读取速度等于磁盘数量的倍数,与 RAID 0
相同。写入速度有微小的降低。
RAID 10
结合 RAID 0
和 RAID 1
两种方案构成了 RAID 10
,它是将所有磁盘 N 平均分成两份,数据同时在两份磁盘写入,相当于 RAID 1
;但是平分成两份,在每一份磁盘(也就是 N/2 块磁盘)里面,利用 RAID 0
技术并发读写,这样既提高可靠性又改善性能。不过 RAID 10
的磁盘利用率较低,有一半的磁盘用来写备份数据。
RAID 3
RAID 3 可以在数据写入磁盘的时候,将数据分成 N-1 份,并发写入 N-1 块磁盘,并在第 N 块磁盘记录校验数据,这样任何一块磁盘损坏(包括校验数据磁盘),都可以利用其他 N-1 块磁盘的数据修复。
\n由于数据内的比特分散在不同的磁盘上,因此就算要读取一小段数据资料都可能需要所有的磁盘进行工作,所以这种规格比较适于读取大量数据时使用。
\n在数据修改较多的场景中,任何磁盘数据的修改,都会导致第 N 块磁盘重写校验数据。频繁写入的后果是第 N 块磁盘比其他磁盘更容易损坏,需要频繁更换,所以 RAID 3
很少在实践中使用。
RAID 5
相比 RAID 3
,RAID 5
是使用更多的方案。RAID 5
和 RAID 3
很相似,但是校验数据不是写入第 N 块磁盘,而是螺旋式地写入所有磁盘中。这样校验数据的修改也被平均到所有磁盘上,避免 RAID 3
频繁写坏一块磁盘的情况。
RAID 5
至少需要三块磁盘,RAID 5
不是对存储的数据进行备份,而是把数据和相对应的奇偶校验信息存储到组成 RAID 5
的各个磁盘上,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上。当 RAID 5
的一个磁盘数据发生损坏后,可以利用剩下的数据和相应的奇偶校验信息去恢复被损坏的数据。RAID 5
可以理解为是 RAID 0
和 RAID 1
的折衷方案。
RAID 6
如果数据需要很高的可靠性,在出现同时损坏两块磁盘的情况下(或者运维管理水平比较落后,坏了一块磁盘但是迟迟没有更换,导致又坏了一块磁盘),仍然需要修复数据,这时候可以使用 RAID 6
。
与 RAID 5
相比 RAID 6
增加第二个独立的奇偶校验信息块。两个独立的奇偶系统使用不同的算法,数据的可靠性非常高,任意两块磁盘同时失效时不会影响数据完整性。
RAID类型 | \n访问速度 | \n数据可靠性 | \n磁盘利用率 | \n目的 | \n
---|---|---|---|---|
RAID 0 | \n很快 | \n很低 | \n100% | \n追求最大容量、速度 | \n
RAID 1 | \n很慢 | \n很高 | \n50% | \n追求最大安全性 | \n
RAID 10 | \n中等 | \n很高 | \n50% | \n总和 RAID 0/1 优点,理论速度较快 | \n
RAID 5 | \n较快 | \n很高 | \n(N-1)/N | \n追求最大容量、最小预算 | \n
RAID 6 | \n较快 | \n较 RAID 5 高 | \n(N-2)/N | \n同 RAID 5,但更安全 | \n
RAID
可以看作是一种垂直伸缩,一台计算机集成更多的磁盘实现数据更大规模、更安全可靠的存储以及更快的访问速度。而 HDFS
则是水平伸缩,通过添加更多的服务器实现数据更大、更快、更安全存储与访问。
Hadoop
分布式文件系统 HDFS
的设计目标是管理数以千计的服务器、数以万计的磁盘,将这么大规模的服务器计算资源当作一个单一的存储系统进行管理,对应用程序提供数以 PB
计的存储容量,让应用程序像使用普通文件系统一样存储大规模的文件数据。
HDFS
已经为同一个文件保留了多个副本,如果磁盘发生故障 HDFS
可以将其恢复。HDFS
同样可以一次从多个节点(DataNode)读取数据,如果使用 RAID 1
,将浪费更多的存储空间,如果使用 RAID 0
,产生失败的可能性会提升 N 倍(N = 磁盘数量)。使用 RAID 5/6
的话,读写速度将受到影响,也更昂贵。
不过由于 NameNode
在 HDFS
中容易出现单点故障,因此需要更可靠的硬件配置,所以建议在 NameNode
上使用 RAID
。
在一些 Master 处理节点上,如:Hive 的 MetaStore
,也推荐使用 RAID
。同时建议为所有的系统盘配置 RAID
,你不会希望仅仅因为系统盘故障而导致节点故障。
对于 ElasticSearch,其本身也提供了很好的 HA
机制,同样无需使用 RAID
。
回到开头的那个问题,我的回答是:
\nRAID
从本质上来说,人就是为了生活而工作。
\n在我看来,为了钱而工作,并不是可耻的事情,这是理所当然的事,我认为是非常了不起的。
\n赚多少钱倒没那么重要,如果能够支撑自己和家人的日常生活,这就足够了。人生,就是这样活着而已。
\n至于“人生价值”“自我成长”之类,是要等自己立足安稳后,在闲暇之余慢慢思考的问题。人生很长,慢慢思考就好了。
\n有些人痛苦地向我倾诉,说自己“在现在的公司没有发展”,或者“失去了工作的目标”。我认为,这些可能都是想得太多的缘故。
\n如果被权力、地位、名誉之类的东西紧紧束缚住,在工作中一味地在意别人的眼光,很快就会疲于应对。要是这般勉强度过几十年,迟早会被工作击垮。
\n我不关心头衔和职位,这些都如过眼云烟。如果自己和家人健康、精力充沛,有几个知己可以交谈,还有什么其他奢望呢?
\n相反,如果你拼命工作,身体状况因此变得糟糕,自己和家人关系疏远,即使你挣了很多钱,那又有什么幸福可言?
\n不要把自我价值全部建立在工作上,带着“为身边的人略尽绵力”的想法去工作,或许会更好。
\n工作中的人际关系比工作内容重要得多。从我的经验来看,不喜欢工作的大多数是人际关系出了问题。对有些人而言,不管他们做什么工作,他们都讨厌工作,这也许是与人交往上出现了问题。
\n过多的“空闲”,有时会带来负面影响,“适当忙碌”的状态反而更好。
\n如果工作让你一直做出巨大的牺牲,那一定要果断离开,毫不犹豫。
\n我并不提倡过度工作,甚至过劳死。公司不过是“别人赚钱的工具”,如果这个工具紧紧地束缚住了自己的宝贵生命或家人的幸福,那么逃离也无妨。一旦决定“逃离”,你应该自信地离开。
\n习惯遇事不抱怨,依靠自己解决,无论发生什么事情,你都能想办法解决。
\n如果工作让你一直做出巨大的牺牲,那一定要果断离开,毫不犹豫。
\n过于强调“应该如此”而拼命努力,多半是因为欲望过高。此外,欲望过高的本质,或是“想被人称赞自己努力上进真厉害”
\n在人生中,很多事情不会按照你的想法发生,这会让我们感觉很痛苦。
\n人生不可思议之处在于,即使去了新环境,也会遇到讨厌的人、合不来的人,尽管程度不同,但或多或少都会出现。
\n不要试图通过改变他人来获得快乐,而是想“自己如何做才会快乐”或“怎么努力让自己在这里心情愉快地度过”,我觉得这才是应该考虑的关键。
\n人生的本质就是一个人活着。
\n人际关系是无法预测的。人与人之间可能因为一些小事而结缘,也会因一些小事分离。人会快速地向着有利于自己的方向前行,由于时间或距离的原因不能见面,缘分也会渐渐变浅。这就是人际关系。
\n“情”这个东西看起来是一件好事,但从另一方面来说,它会让你对别人产生期待、执着,让你在关系中变得“自私”。
\n不要有太多的期望。
\n只要是别人给予的东西,自己就应该感谢对方
\n在一生中,任何人都会遇到几次大的转折点,也就是人生的十字路口。
\n总想着得失,那么就会觉得勉强自己,甚至产生心结。与其如此,还不如率性而为,跟随心的决定。
\n即使你不能给出建议,没有提供令人豁然开朗的方法,就是简简单单设身处地地倾听,对方也会轻松许多。
\n如果被人说了不好的话,就不妨想想“那家伙在家里有什么惹他烦心的事吗?”,也许心情就会好转。
\n无论是费劲地想要主动交往,还是试图引起对方的关注,都显得不自然、不正常。
\n当你想到“自己这么努力,为什么没有得到回报”,也许对待别人就会变得苛刻。
\n人为什么会感到不安?大多数情况下,这种不安是因为对未来考虑太多。
\n我认为,只要想清楚今天一天的事情就可以了。
\n任何事物都有两面性,痛苦的经历可以扩展人的本性,就像肌肉可以锻炼、拉伸一样。
\n如果总也不顺利,那么你就要意识到,“人生本来就是这样”。
\n保持心平气和的另一个有效方法,就是“工作时间以外,不考虑工作上的事”
\n在非工作时间,尽量不要考虑工作上的事。
\n自信绝非一成不变的,我们只能在某一段时间或某一领域经历它。
\n总而言之,不被负面情绪影响的最大秘诀就是好好生活
\n痛苦与伤心,其实也是与生俱来的东西。人活着,肯定会经历苦难。
\n不要事事都想咬紧牙关挺过去,只要抱着“今天这样做基本就可以了”的态度,日复一日地坚持积累。
\n在我看来,与其追求完美而挫折不断,不如以笨拙的方式坚持下去。
\n如果母亲的情绪不稳定,孩子的精神状态就会受到影响。
\n父母的心情会扰乱孩子的内心,孩子的波动反过来又会反弹给父母。
\n有多种选择的时候我们往往左右瞻顾,当“只有一个选择”的时候,反而会意外地突破现状。
\n生活如果没有目标,就会变得懒散。一旦决定“今天这样做”,生活一下子就会张弛有度。
\n家庭问题能忍耐就忍耐,工作方面能放松就放松。
\n我非常推荐大家和同事一起出去玩,你可以发现同事在工作之外的真性情,也可以与趣味相投的人成为好朋友。
\n提醒别人的事情,自己如果做不到,更加不好。即使是孩子,也会看穿大人的一言一行。因此,要想改变孩子,首先得改变自己。这样,通过育儿,也会注意到自己一些为人处事的方式。
\n育儿基本的原则是,对待大人和孩子一视同仁。
\n孩子成长的每个过程,比任何一部动漫或电影都令人感动。
\n最关键的是,父母应该真心为孩子的幸福考虑,并付诸行动。这样做,才能将爱传递给孩子
\n父母和孩子的人生车轮虽然驶向不同的方向,由于桥梁的存在,你们可以随时往来。
\n担心死亡来临、提前做好计划终究无济于事。把最基本的要求告诉家人,其余的事情顺其自然就可以。
\n在追求的过程中,一定要分清自己是自己,他人在实践他人的人生,我们不需要追寻别人的脚步。
\n越是对别人讨厌、反感,这些情绪就越容易在自己的表情和态度上反映出来,进而传达给对方。
\n由于过于害怕孤独,就会迎合别人,或者对别人妥协,从而使自己痛苦不已。
\n若想人际关系变好,就更应该珍惜一个人的时光。也许,这才是最根本、最重要的事情。
\n“对患者来说,能接待他们的医生很多;但对孩子而言,母亲只有一个
\n和周围的人交往要保持适当的距离,这是维系和谐关系的关键。我们也是有感情的人,在不知不觉中平衡就会被打破。
或许是对别人期待太多,或许是对自己太过严苛,总之与他人交往总会有感觉不舒服的时候。
人生的满足感并非由别人决定,也绝不应该追求和别人同样的生活。
\n"},{"title":"【阅读笔记】微服务架构设计模式—第1章","url":"/2022/read-notes-microservices-patterns-1/","content":"软件架构对功能性需求影响并不大,架构的重要性在于它影响了应用的非功能性需求,
\n
之前只听过垂直扩容和水平扩容,本书中将微服务比喻成一个立方体,X、Y、Z 三个轴表示对应用扩展的 3 种方式:
把应用程序功能性分解为一组服务的架构风格。
\n「两个披萨」原则是指某个事情的参与人数不能多到两个披萨饼还不够他们吃饱的地步。亚马逊CEO贝索斯认为事实上并非参与人数越多越好,他认为人数多不利于决策的形成,并会提高沟通的成本,这被称为「两个披萨」原则。
\n采用微服务架构以后,如果仍旧沿用瀑布式开发流程,那就跟用一匹马来拉法拉利跑车没什么区别。如果你希望通过微服务架构来完成一个应用程序的开发,那么采用类似 Scrum 或 Kanban 这类敏捷开发和部署实践就是必不可少的。
\n抽象定义:计算机系统的软件架构是构建这个系统所需要的一组结构,包括软件元素、它们之间的关系以及两者的属性。
\n更容易理解的定义:应用程序的架构是将软件分解为元素(element)和这些元素之间的关系(relation)。
\n应用程序的架构可以从多个视角来看:
\n功能性需求:决定一个应用程序做什么。
非功能性需求:决定一个应用程序在运行时的质量。
六边形架构风格选择以业务逻辑为中心的方式组织逻辑视图。
\n\n六边形架构风格的一个重要好处是它将业务逻辑与适配器中包含的表示层和数据访问层的逻辑分离开来。业务逻辑不依赖于表示层逻辑或数据访问层逻辑。
\n松耦合服务是改善开发效率、提升可维护性和可测试性的关键。小的、松耦合的服务更容易被理解、修改和测试。
\n保证数据的私有属性是实现松耦合的前提之一。
\n服务是一个单一的、可独立部署的软件组件,它实现了一些有用的功能。
\n服务的 API 封装了其内部实现。
\n将应用程序构建为松耦合、可独立部署的一组服务。
\n定义应用程序架构的三步式流程:
\n领域驱动为每一个子域定义单独的领域模型。
\nDDD 把领域模型的边界称为限界上下文(bounded context)。
\n我们可以通过 DDD 的方式定义子域,并把子域对应为每一个服务,这样就完成了微服务架构的设计工作。
\n一个理想的微服务架构应该是在内部由松散耦合的若干服务组成,这些服务使用异步消息相互通信。REST 等同步协议主要用于服务与外部其他应用程序的通信。
\n考虑交互方式将有助于你专注于需求,并避免陷人特定进程间通信技术的细节。
\n交互方式的选择会影响应用程序的可用性,交互方式还可以帮助你选择更合适的集成测试策略。
\n它们可以分为两个维度。
\n一对一的交互方式有以下几种类型:
\n一对多的交互方式有以下几种类型:
\n一个设计良好的接口会在暴露有用功能同时隐藏实现的细节。
\n„„你应该努力只进行向后兼容的更改。向后兼容的更改是对 API 的附加更改或功能增强:
\n「严以律己,宽以待人」:服务应该为缺少的请求属性提供默认值;客户端应忽略任何额外的响应属性。
\n进程间通信的本质是交换消息。消息通常包括数据,因此一个重要的设计决策就是这些数据的格式。消息格式的选择会对进程间通信的效率、API 的可用性和可演化性产生影响。
\n消息的格式可以分为两大类:文本和二进制。
\nREST 是一种使用 HTTP 协议的进程间通信机制
\nREST 最初没有 IDL。幸运的是,开发者社区重新发现了 RESTful API 的 IDL 价值。最流行的 REST IDL 是 Open API 规范 。
\ngRPC API 由一个或多个服务和请求/响应消息定义组成。服务定义类似于 Java 接口,是强类型方法的集合。除了支持简单的请求 /响应 RPC 之外,gRPC 还支持流式 RPC。
\ngRPC 使用 Protocol Buffers 作为消息格式。
\n服务保护自己的方法包括以下机制的组合:
\n服务发现在概念上非常简单:其关键组件是服务注册表,它是包含服务实例网络位置信息的一个数据库。
\n服务实例启动和停止时,服务发现机制会更新服务注册表。当客户端调用服务时,服务发现机制会查询服务注册表以获取可用服务实例的列表,并将请求路由到其中一个服务实例。
\n实现服务发现有以下两种主要方式:
\n有以下几种不同类型的消息:
\n有以下两种类型的消息通道:点对点和发布-订阅:
\n好处:
\n弊端:
\n消息代理是所有消息的中介节点。
\n好处:
\n弊端:
\n选择消息代理时,你需要考虑以下各种因素:
\n使用多个线程和服务实例来并发处理消息可以提高应用程序的吞吐量。但同时处理消息的挑战是确保每个消息只被处理一次,并且是按照它们发送的顺序来处理的。
\n现代消息代理(如 Apache Kafka 和 AWS Kinesis) 使用的常见解决方案是使用分片(分区)通道。该解决方案分为三个部分。
\n处理重复消息有以下两种不同的方法:
\n程序的幂等性,是指即使这个应用被相同输入参数多次重复调用时,也不会产生额外的效果。
\n跟踪消息并丢弃重复消息方案:
\n服务通常需要在更新数据库的事务中发布消息。
\n确保消息的可靠发送的机制:
\n领域事件是聚合 (业务对象)在创建、更新或删除时触发的事件。服务使用 DomainEventPublisher 接口发布领域事件。
\n如果你想最大化一个系统的可用性,就应该设法最小化系统的同步操作量。
\n消除同步交互的方法:
\n在微服务架构中实现查询操作有两种不同的模式:
\nAPI 组合器应尽可能地并行调用提供方服务,最大限度地缩短查询操作的响应时间。
\nCQRS 是命令查询职责隔离 (Command Query Responsibility Segregation) 的简称,它涉及隔离或问题的分隔。它将持久化数据模型和使用数据的模块分为两部分:命令端和查询端。
\nCQRS 视图模块包括由一个或多个查询操作组成的 APl。它通过订阅由一个或多个服务发布的事件来更新它的数据库视图,从而实现这些查询操作。
\n\nNoSQL 数据库通常具有有限的事务模式和较少的查询功能。在一些情况下, NOSQL 数据库比 SQL 数据库更有优势,包括更灵活的数据模型以及更好的性能和可扩展性。
\nNoSQL 数据库通常是 CQRS 视图的一个很好的选择,CQRS 可以利用它们的优势并忽略其弱点。CQRS 视图受益于 NoSQL 数据库更丰富的数据模型和性能。它不受 NoSQL 数据库事务处理能力的限制,因为 CQRS 只需要使用简单的事务并执行一组固定的查询即可。
\n命令和查询模块 API 可以使客户端使用以下方法检测不一致性。
\nOrderHistoryDaoDynamoDb DAO
可以使用名为 «aggregateType>><<aggregateId>>
的属性跟踪从每个聚合实例接收的事件,其值是接收到的最高事件 ID。如果属性存在且其值小于或等于事件 ID,则事件是重复的。
假设我现在要用 Go 编写一个 Web 应用。在这个应用里,我要实现给用户发送消息的功能。我可以通过邮件或短信等方式来发送这条消息,这是一个完美的接口使用场景。
\n在这个虚构的 Web 应用中,先来创建如下 main.go
文件:
package main |
这里的 User
结构体代表一个用户。
可以看到我创建了只有一个函数 SendMessage()
的 UserNotifier
接口。
为了实现这个接口,我需要创建一个结构体来实现 SendMessage()
函数。
type EmailNotifier struct { |
正如你看到的,我创建了一个新的 EmailNotifier
结构体。然后我给这个结构体实现了 SendMessage()
方法。在这个例子中,EmailNotifier
只是简单打印了一条消息。在现实世界中你可能需要调用发送邮件的 API,比如 Mailgun。
到此,UserNotifier
接口已经实现了,就是这么简单。
下一步要做的是使用 UserNotifier
接口为用户发送一份邮件。
func main() { |
运行这个程序,EmailNotifier
的 SendMessage
方法被正确调用了。
go build -o main main.go |
下边我们来实现发送短信的接口。
\ntype SmsNotifier struct { |
我们可以把 Notifier
放进用户结构体中,这样每个用户都有一个属于自己的 Notifier
,是不是很酷。
type User struct { |
然后,我们向 User
结构体中添加一个便捷方法 notify()
,这个方法使用 UserNotifier
接口给用户发送消息。
func (user *User) notify(message string) error { |
最后,我在 main()
函数中创建两个用户,分别调用了它们的 notify()
方法。
func main() { |
最终结果正是我们所预期的:
\ngo build -o main main.go |
本文介绍了 Go 接口是如何工作的,同时用一个现实中简单的例子进行了演示。
\n希望对你有所帮助。
\n"},{"title":"最近一个月的「汉堡王」早餐心得分享","url":"/2022/recent-breakfast-burger-king/","content":"今天这篇博客和之前的有些不同,主要区别是这篇是我用语音写的。也就是把「飞书妙记」打开录音,之后转成文字整理出来。
\n为什么要拿语音写呢?我之前写博客都是通过电脑编写,写的过程中会浏览一些其他网页,找找资料之类的,而且打字的话很容易打断思绪。所以我这次尝试用说的形式来整理一篇博客出来。
\n用语音写的另一个原因是它会逼着我一直的去往下不停地去说,会尽量的不去中断,尽量的去保持思考状态,往外输出,也许还能提升我的口语表达能力。如果是打字的话,一会儿看看这个一会儿看看那个很容易被中断。
\n使用语音录入完转文字后,再通过电脑把段落顺序做些调整,把口语化表达改为书面表达就可以发布了。我发现整理所花费的时间比说的时间还要长,录了 20 分钟,整理了 1 小时。
\n明天就是国庆节了,周围有小一半的同事请了假,我下午也请了半天假,所以利用中午的时间把这篇博客发完也就要下班了,祝大家国庆节快乐。
\n这篇博客想聊一下我近一个月来如何解决上班时间早饭和午饭的问题。
\n我们公司(望京 Soho)附近有很多餐厅、便利店,我看到公司对面有一个店面很大的「汉堡王」,我之前对汉堡王的印象或者说对汉堡的印象,是一个不是特别健康的食品。不过我依然抱着试一试的态度搜了一下他家的菜单,顺便看一看有没有优惠之类的,毕竟打工人还是想找一些经济实惠的东西解决温饱问题。
\n经过我一番调查后发现,「汉堡王」有一个有 3 种会员:
\n看了一下他家的早餐,早餐里边有一个汉堡特特别吸引我,名字叫「双蛋双牛堡」。它的厉害之处就在于上下两层的面包皮换成了两个煎蛋,中间是两片牛肉。所以这是个对健身人士非常友好的汉堡,当然我也不是健身人士,我只是说它里面的蛋白质非常的丰富。
\n我办了 29.9 的那个会员,因为我还喜欢喝咖啡。平时的话我不会频繁去买外边的咖啡,而是喝公司免费的美式。外边的咖啡比如瑞幸 15 起,星巴克 30 起,我一周也就喝一次外边的咖啡,通常是周五。但是有了这个会员,我每天都可以喝一杯咖啡,8 块钱不心疼,虽然和之前相比花的更多了,但是心理更愉悦了。
\n我大概已经用使用这个会员一个月了,基本上每天早晨都是买「双蛋双牛堡」套餐,套餐包括一个汉堡再加一个小杯的美式,我一般换成豆浆,因为公司有免费的美式。偶尔想解解馋,或者是想吃一些别的汉堡的话也会换一换。今天主要就说「双蛋双牛堡」,我觉得我一个男生一次性把它吃完都会有些顶,女生应该是不太好一次性吃完的。我刚开始是每天早晨就把汉堡吃完,中午吃一块鸡胸肉或者就不吃了,这样我发现下午会有些饿。最近这两周我换了个办法:把汉堡分成两份,因为它是上下各一个煎蛋、中间两片牛肉,我就从中间均分,早晨吃一片牛肉加一个煎蛋,中午吃一片牛肉加一个煎蛋,而且还有一小杯口味不错的热豆浆,这样的话我的蛋白质也是够的,并且还能保证下午在吃晚饭前不那么饿。
\n\n这种方式,早饭加午饭也只花了 10 块钱,中午的时候我还会去买一杯咖啡,8 元咖啡可以在所有的「汉堡王」销售的咖啡里任选一种,包括杯型、口味都是任选,不管多少钱,最后都会减成 8 元。所以我一定是考虑着自身利益最大化的原则,我每次都是买最贵的澳白,而且是买大杯。
\n\n「汉堡王」的大杯就是「星巴克」的超大杯,它没有从中杯开始算,用的小杯、中杯、大杯三种规格,它的大杯跟星巴克的超大杯是一样的。「星巴克」超大杯的澳白我记着好像是 38,「汉堡王」 8 块钱,瞬间就省了 30。口味的话我觉得差的不是特别多。我现在就是边喝「汉堡王」的咖啡,边写这篇博客的。
\n这个汉堡叫「帕斯雀牛肉可颂堡」,也特别好吃的,偶尔想解一次馋了的话我也会去买这个。
\n\n还有一个汉堡叫「牛肉蛋可颂」,也是在早餐里我也是觉得比较好吃。
\n\n其实我也没吃太多种,目前吃的每一种都觉得很好吃。其他早餐还有咸蛋黄鸡肉粥、老北京烤鸡卷、咸蛋黄鸡肉卷等等。这些我还没有尝试过,后边有机会的话可以尝试一下。
\n我中午就不用再去出去排队去吃饭了,多出来的时间就可以看看书或者是出去玩一会陆地冲浪板,陆冲这个东西是特别的上头,建议大家有机会都试一试。
\n下边这张图是我今天早晨买的,因为下午要请假,所以中午就不再单独去买杯咖啡喝了,早晨就一起把咖啡买了,可以看到一个大杯的澳白,加一个小杯美式(今天没换豆浆),再加一个汉堡(今天汉堡卖相不太好),这一套下来才 18 块钱。在其他地方,这些钱也就只能买一杯咖啡,同样的价格能在「汉堡王」买下三件套,真的是能开心一整天。
\n\n可以算一下,每个月我们按照 22 天工作日算,每天花费 18 块钱,也就是早饭+午饭再加一杯咖啡(晚餐公司提供),一共是 396 的话再加上一个月的会员费 30 元。这样的话一个月只需花费 426 就能把早饭和午饭解决掉。我觉着是比较划算的,而且蛋白质摄入一定是没有太大问题的,而且还能让我解咖啡的馋,让我每天都可以通过一杯超大澳白续命,提升工作的幸福感。工作已经很苦了,不如再来一杯好喝的咖啡让自己幸福一下。
\n关于打工人的早午餐就分享到这里,如果你附近有汉堡王的话,也可以试一试。因为我是喜欢喝咖啡又想解决双餐的问题,所以选的是 29.9 的会员。如果你只想喝咖啡,可以办 9.9 的。如果你只需要早餐,也可以办 19.9 的。
\n我觉得 29.9 的这个会员是很值。如果「汉堡王」的「双蛋双牛堡」不下架,且我不换公司的话,我应该会去一直续费。
\n今天入职「探探」两周年,过的真快,下班了,拜了个拜。👋🏻
\n"},{"title":"最近养成的新习惯","url":"/2022/recent-new-habit/","content":"又到了每个打工人都喜闻乐见的周五,我今天想聊一个关于我最近一个多月养成的一个习惯。
\n先介绍下背景,在工作之前,也就是上学期间,我的贴身衣服,比如内裤、袜子并没有换的很勤,平均三四天换一次。在工作后不管是内裤还是袜子,不管冬天还是夏天都是一天一换。
\n虽然卫生问题解决了,但并没有一天一洗,这些衣物我不使用洗衣机而是手洗,这就导致我要把换下来的衣服堆起来,等到没得换了或者周末的时候再统一去洗,这样会经常性出现资源紧张的情况,或者突然发现没得换了迫不得已再穿一天前一天的。这样还会导致的另一个问题是每次洗的时候工作量都很大,比如我要洗 5 条内裤、5 双袜子,每个内裤 3 分钟,每个袜子 2 分钟,这就要花去 25 分钟的时间。
\n最近我看了一本书叫《福格行为模型》,这本书不是完全讲习惯养成的,但也有一些习惯养成的内容,我通过这本书受到一些启发,使我最近养成了可以每天洗掉当日穿过的内裤和袜子的习惯。
\n这本书里提到一个概念:「我们在多数情况下没有去做一件事,并不是因为缺乏动机,而是缺乏一个很好的提示」。提示很重要,作者在书内将提示称作「锚点」。再早之前我读的另一本书《掌控习惯》里也有类似的概念,作者将之成为「触发器」。我有洗袜子的动机,但是每次脱下来时顺手堆起来忘记去洗,我需要给自己一个洗袜子的触发器,而且最好在合适的时机触发我。
\n我给自己设置的触发器是每晚洗澡淋浴开启的时候。不管什么季节我都会每晚冲个澡,每次冲澡开启淋浴后都需要一些时间等热水,我发现可以利用这个空挡用淋浴出来的凉水去洗衣服,这个时候只有手接触凉水也不会特别冷。我会快速用凉水把衣服湿润,然后打肥皂搓一搓,这个时候差不多就要出热水了,我拿着衣服一起站在水里,把衣服上残留的泡沫冲洗掉。
\n书中另一个观点是「先从小习惯开始、先从简单的习惯开始、从最紧迫的开始」。所以我并没有再一开始就洗内裤和袜子,因为我担心双倍的工作了会让我知难而退,一开始我只洗内裤,然后培养了三周洗内裤的习惯后把袜子也叠加上了。
\n我最近很长一段时间都是两条内裤和两双袜子换着穿,因为每天都会把当日的洗了,晾一天一夜肯定可以干。而且我也不再需要周末腾一块时间来干这个活了,之前也是由于堆的衣物太多想想就头大所以总不想去做。
\n我想我成功养成这个习惯的很重要的原因是我选择对了一个很合适的触发器,这个触发器的时间合适、场合合适,甚至还提供了我要养成习惯所需的资源(水)。
\n其实触发器这个概念我之前就一直在用,只是通过这本书我才知道我用了一个培养习惯合适的方法。比如我每天上大号的时间固定是早上洗漱完后,我会将坐在马桶上的这个事件作为一个触发器来学习英语,正好 10 分钟左右可以把当天要学的内容学完,我最近半年使用的「多邻国」,从而也避免了我在马桶上刷短视频的情况;我还将上地铁作为一个阅读触发器,上了地铁我就会掏出 Pad 或者纸质书来阅读。
\n这本书里还有个比较有趣的观点,我也在这里讲一讲吧。之前我一直将自己标榜为工具党,并且了解我的人也知道我工具党的习惯,就是说不管做什么我先把配套工具准备好、搞一套好装备或者时不时的折腾些工具,工具不限于实体工具和电子工具。这本书里提到「如果一种行为会让你感到沮丧,那它就很难成为习惯。从买一套好用的厨房刀具到准备一双舒适的运动鞋,借助任何工具都有可能让行为变得更容易做到」。这么看来工具党并没有错,为了给自己执行一个行为增添点乐趣,准备一套好的工具无可厚非。
\n比如我之前特别不喜欢拖地,每次拖地都要用手清理拖把上的很多毛发而且涮拖把的时候也很费力。前段时间我买了一个非常好用的拖把,拖把自带一个刷洗的桶,有两个槽,一个用来清洗拖把,另一个用来刮掉拖把上的水,清洗和刮水的时候就可以把上边粘的毛发刮下来,而且很干净,不用再去上手处理一次了,这就让我觉得拖地这件事没有那么困难。为了再增加些愉悦感,我会在拖地的水里放些「滴露」,不知道为什么我很喜欢闻滴露的味道。
\n再比如我现在玩的陆地冲浪板,我不确定如果几个月前我在刚学陆冲时买一块四五百块钱的普通板子现在还是不是这么有热情,我在刚学的时候就买了一块上等的板子,体验极佳,我现在对陆冲热情不减这块好板子有很大的贡献。
\n以上,关于工具党,在我看来并不是一件糟糕的事情,如果成为工具党后可以驱动自己把事情做下去,那就是有益的。
\n《福格行为模型》这本书里还有一些其他有用的内容,后边我会做一些摘抄单独再发一篇 blog。
\n"},{"title":"推荐一本红楼梦 -《红楼梦脂评汇校本》","url":"/2022/recommend-hongloumeng-zhipinghuijiaoben/","content":"好久没有更新博客了,前段时间忙于开发一个新 App,等正式上架后而且用户量还不错的话我再透漏相关信息吧。
\n今天想推荐一本红楼梦,可能有人会奇怪为什么是推荐一本红楼梦呢,红楼梦不是本来就是一本书吗?红楼梦有很多脂评本,每版脂评本上都有不同年代的人对红楼梦的指点。我要推荐这一版本叫《红楼梦脂评汇校本》,它把之前所有版本红楼梦中的评语汇集到了一起,边读原文边看古人写的评语仿佛在和古人对话,他们的评语也很朴实有些还很俏皮,偶尔还有剧透的情况,读一句原文看一句评语,非常好玩,也平添了几分乐趣。
\n比如下边截图中有个很贫嘴的评语,在第一回里提到宝玉的那块玉石是女娲补天剩下没有使用的那一块,评语抱怨到就因为这多出来的一块石头,生出来这么多鬼话,还不如把这块石头拿去补地,让地面平坦一些。有的评语就像相声里的捧哏,比如原文写石头对僧道说:「大师,弟子蠢物」,评语写到:「岂敢岂敢」,原文继续写:「弟子质虽粗蠢」,评语有写:「岂敢岂敢」,用现代的话说就是碎嘴子。
\n\n评语还会把文中的谐音梗做个解释,比如下边这一页中解释贾化就是「假话」,贾雨村是「粗言粗语」,胡州对应「胡诌」。
\n\n上边截图中还对香菱身世的那首诗做了注解,比如第二句「菱花空对雪澌澌」,暗示香菱后边要嫁给薛蟠,评语写到「生不遇时,遇又不偶」。诗的最后一句「便是烟消火灭时」,评语也写出这是为后文埋下的伏笔。
\n"},{"title":"记录感激","url":"/2023/record-gratitude/","content":"\n\n“对生活的感激程度其实就是生活的充实程度。当我们对生活麻木,对一切习以为常的时候,其实我们的生活就已经死亡了”
\n
「哈佛幸福课」的第8节,讲得是感激的重要性。作者建议我们把感激培养成一种习惯,当我们感激时,副交感神经系统功能增强,使我们变平静,从而加强免疫系统。
\n在提到如何培养感激时,作者提了一个行动方式:每天睡前写下5件值得感激的事。
\n培养一个能力的最佳方式就是实践,通过一次又一次感激来培养感激,我从6月21日开始实施这个行动,不过我稍稍给自己降低了一点点要求,每天记录3条值得感激的事,我同时把这个行动项录入到 Things 中对我进行每日提醒。
\n\n我是用 Notion 来记录这些感激内容的,每个月新建一个新的页面,每天一个大标题。使用 Notion 我可以随时随地记录,比如在地铁上、公司里、家里,从第一天开始记录到今天已经将近4个月了。
\n\n每天的持续记录使我发现,原来我身边有那么多事值得感激,但我之前已经习以为常,认为这些都理所当然。在写感激过程中,感激最多的肯定是在背后支持我的家人,除此之外我还会感激之前没有意识到的事物,感激的对象也不止有实实在在的人,还有身边给我提供便利的物品。
\n比如下边这段:
\n\n最上边两条我感激了两位同事,一位帮我一起沟通绩效结果,另一位是我现在的 Leader,和我一起梳理一些重点项目;接下来我还感激了「哈罗单车」,那一天是个周五,天气很好,晚上下班早,我骑着单车从公司回家,一路上风景也很好;第二天8月5日是个周六,我早上开车回老家,路上狂风大作电闪雷鸣遇上了大暴雨和大雾,我和我的车经过4小时路程,它安全的把我带回了家;最下边那条,是我回家后带念出去玩,突然感觉她长大了,之前在游乐场玩的时候一定要我陪着,这一次她可以自己玩耍了。
\n再来随便看两天的:
\n\n这两天也是周末,我感激了华为安装师傅、感激了家具安装师傅、感激了木工师傅、感激了北京的交通、感激了念念、感激了游戏厅的抓娃娃机。
\n在我写这篇流水账翻看这些感激记录的过程中,又能回忆起当时的喜悦,一定程度上起到了日记的作用。每条记录用一句话描述,不会有很大的写作压力,刚开始确实不容易发现那么多值得感激的事,随着自己记录的越来越多,就会越擅长发现生活中值得自己感激的地方。
\n有时我还会感激自己,比如下边几条:
\n\n在记录感激的时候,我不会强迫自己,如果某一天心情实在糟糕,可以允许自己只写一两条,某一天过得充实的时候也写过六七条。
\n通过记录这几个月的感激,我能很明显感受到自己情绪好了很多,不再那么偏激,能够从积极的方面思考问题了,注意力会放在积极正面的事情上,和其他人打交道时会思考对方有什么优点值得我学习,有时我还会把之前遇到后会非常生气的事换个思路去看。
\n我们应该心怀感激,而不是等到不幸发生时才意识到之前的自己错过了多么美好的时光。
\n世界上有很多美好的事物,但我们很快就会适应且不再察觉它们。每天两次花一分钟时间留意周遭的一切,花一分钟的时间,在上班的路上看看美丽的草地、青翠的树、美丽的雪。晚上用一分钟去回忆,回想你度过的一天,写下让你心怀感激的事物。
\n"},{"title":"从 Go 语言的 panic 中恢复","url":"/2021/recovery-form-painc-in-golang/","content":"\n\nPanic 是 Go 语言中一个内置函数,它会中断正常的控制流并开始 panic 流程。当函数 F 调用 panic 时,F 的执行停止,F 中的任何延迟函数(deferred function)都被正常执行,然后 F 返回给它的调用者。对于调用者来说,F 的行为就像对 panic 的调用。这个过程继续在堆栈中进行,直到当前 goroutine 中的所有函数都返回,这时程序就会崩溃。painc 可以通过直接调用 panic 函数来启动,也可以由运行时错误引起,如数组越界。
\n
简单地说,painc 使一个函数不执行其预期的流程,并可能导致整个程序退出。
\nGo 原生提供了一些功能,可以帮助我们从这种情况下恢复。
\nGo 的 defer 语句安排了一个函数:这个函数在执行 defer
的函数返回之前立即运行。
我们称 defer 调用的函数为:延迟函数
\n// Contents 将文件内容以字符串形式返回。 |
当 panic
被调用时,它立即停止执行当前函数,并沿 goroutine 的堆栈运行所有延迟函数。
对 recover
的调用会终止 panic,并返回传递给 panic
的参数。recover
只在延迟函数中有效,因为 panic 后唯一能够运行的代码在延迟函数中。
func server(workChan <-chan *Work) { |
让我们来实现一个简单的数学函数,它可以将两个数字相除,如果分母是 0,就会 panic Divide by zero error!
。
下边的函数检查分母的值,如果它是 0,就会 panic。
\nfunc checkForError(y float64) { |
下边这个函数负责对提供的数字进行除法操作并返回,同时它使用上面定义的函数来检查分母是否为 0。
\n由于 checkForError
会破坏流程,因此这个函数实现了recover()
和defer
,以便在发生 panic 时返回 0。
func safeDivision(x, y float64) float64 { |
将上边的代码组合起来:
\npackage main |
输出为:
\nPre panic execution |
最近自己的 VPS 上部署了几个 Docker 服务,其中一个打算用 Reids 做个计数的功能,因为我的偏好是数据库类的程序不用 Docker 来部署,所以在本地安装了 Redis 服务,但是这样如果不做任何配置的话 Docker 容器中的服务是访问不到宿主机的 Redis 服务的。
\n从网上搜了下解决方法,都挺复杂的,需要配置网桥之类的,所以我就走了个捷径,直接将 Redis 的端口进行开放,然后设置一个密码:
\n修改 /etc/redis/redis.conf
:
将里边的 bind 127.0.0.1
改为 bind 0.0.0.0
,这样的话 Redis 就可以监听外部请求了。
接下来为 Redis 配置一个认证密码:
\n找到 #requirepass foobared
将注释去掉,同时将 foobared
改为自己想设置的密码。
修改完后,保存退出,然后重启 Redis 服务:sudo /etc/init.d/redis-server restart
这样就完成了,在我本地尝试登录服务器的 Redis:
\nredis-cli -h ipaddress
发现登录成功,发送个命令试试看: keys *
,这是会得到:
(error) NOAUTH Authentication required.
这样的结果,告诉我们没有权限,因为我们设置了访问密码。
\n正确的登录姿势是:redis-cli -h ipaddress -a password
同时,Python 程序中连接 Redis 的时候也要记得加上 password
参数。
备注:
\n内存大页机制:Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。
\nNUMA 架构:在主流的服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。
\n\n"},{"title":"解决低版本 GoLand 启动服务报 Version of Delve is too old for this version of Go","url":"/2020/resolve-old-goland-delve-bug/","content":"\n\n今天算是入职新公司的第一天,配置好开发环境后,尝试用 GoLand 来启动服务,结果报了:Version of Delve is too old for this version of Go (maximum supported version 1.13, suppress this error with --check-go-version=false)
这个错误。
查询后发现这个是 JetBrain 在将 delve 嵌入到 他们的 IDE 时导致的 bug,按照官方的说法是升级 IDE 就可以解决了。详细讨论见这个 issue:https://github.com/go-delve/delve/issues/1710
\n但是我的 ToolBox 在 Check for updates 时没有响应,所以需要通过其他方式进行了解决。
\n1) go get -u github.com/go-delve/delve/cmd/dlv
2) 执行以下命令并将打印的路径复制下来:
➜ echo `go env | grep GOPATH | cut -d "\\"" -f 2`/bin/dlv |
3) 在 GoLand 中 Help -> Edit Custom Properties(之前没编辑过会提示新建)
4) 新增一项 dlv.path={你复制的路径}
,比如我的:
dlv.path=/Users/jiapan/go/bin/dlv |
再次启动服务,问题解决。
\n\n\n"},{"title":"解决 ssh 登录慢的问题","url":"/2018/resolve-ssh-slow/","content":"delve 是 go 语言的 debug 工具,delve 的意思是:钻研、探索,用这个来命名一个 debug 工具还是非常形象的。
\n
OpenSSH 在用户登录的时候会验证 IP,它根据用户的 IP 使用反向 DNS 找到主机名,再使用 DNS 找到 IP 地址,最后匹配一下登录的 IP 是否合法。如果客户机的 IP 没有域名,或者 DNS 服务器很慢或不通,那么登录就会很花时间。
\n解决办法:
\n在目标服务器上修改 sshd 服务器端配置,并重启 sshd。
\nvi /etc/ssh/sshd_config
设置 UseDNS
为 no
即可。
最后
\nsystemctl restart sshd
\n\n\n"},{"title":"敬畏上天的启示","url":"/2022/revere-god/","content":"
我是个无神论者,但是有时候也会在意一些外界给我启示,比如最近发生在自己身上的事。
\n众所周知,我在一个多月前开始玩陆冲,这周开始用陆冲代步上下班地铁站之间的通勤,为了轻装上阵就没有戴护具。
\n\n周三下班后,公司楼下的地面刚刚被擦地机器人打扫过,还有些潮湿,我当时注意到这个情况了,就比较小心单腿滑着走,结果还是打滑了,直接摔了个四仰八叉,不过伤的不重,扯了一下大腿,右手直撑地面的时候手腕顶了一下。当时我就在想要不要下次滑的时候戴上护具。是的,也只是想了一想,不然就不会有后文了。
\n昨天晚上,也就是周五下班出地铁后往家滑,天已经很黑了,路上有一块小半个砖头那么大的石头没有看到,板子直接冲了上去,石头卡住轮子,我也顺势飞了出去。正常来说陆冲是不用担心小石子的,因为它的轮子相对来说比较大,而且有可以容错的桥,但是那块石头太大了。
\n后果是把胳膊肘、膝盖擦破了,前两天刚刚顶过的手腕再次受到冲击,大拇指下边有个小软骨突了出来,一碰还很疼。我是由于板子突然停止,身体因为惯性飞了出去爬到地上的,手腕、胳膊、膝盖着地,但装在我背包里的玻璃饭盒还是被震的稀碎,可想而知力度有多大。不过最后还好,没有什么大碍,而且幸好是直着摔出去,落地的地方还是非机动车道,如果是斜着出去摔倒机动车道后果不堪设想,当时刚好有车在我旁边经过。
\n上天通过这种越来越严重的方式启示我注意安全、佩戴护具,我要敬畏他,以后出行一定要佩戴护具,不再抱有侥幸心理,而且我的护具也很漂亮,是一套复古风的护具。
\n\n"},{"title":"富过三代才懂吃穿","url":"/2023/rich-three-generations/","content":"读过《红楼梦》的朋友一定对其中的一道菜印象深刻:「茄鲞」。王熙凤讲述这道菜的做法是:把才下来的茄子把皮削了,只要净肉,切成碎钉子,用鸡油炸了,再用鸡脯子肉并香菌、新笋、蘑菇,五香腐干、各色干果子,俱切成钉子,用鸡汤煨了,将香油一收,外加糟油一拌,盛在瓷罐子里封严,要吃时拿出来,用炒的鸡瓜一拌就是。
\n“鸡瓜子”是什么?就是用手撕出来的鸡小腿部分的腱子肉。因为常常活动,所以那块肉的弹性最好。富贵之家能把一个食之无味的茄子,经过这么复杂的环节来制作,做的这么精细。
\n还有一次宝玉被他的爸爸暴打后,王夫人问他想吃什么,宝玉回说:“也倒不想什么吃,倒是那一回做的那小荷叶儿莲蓬儿的汤还好”。这个莲蓬汤倒不是什么山珍海味,只是做起来很麻烦,当年元妃省亲时做过一次。因为是给皇帝准备吃的,非同小可,既不能过于奢华,又要十分讲究。莲蓬是用银模子刻出来的,库房的人把模子找出来后,薛姨妈看到后说:“你们府上都想绝了,吃碗汤还有这些样子。若不说出来,我见这个也不认得这是作什么用的”。薛姨妈也是大户人家,就连她都没见过这么精细的模子,可想而知贾家在饮食上有多么讲究了。
\n相较于富贵过好几代的家族,暴发户是不知道怎么吃的,以为大鱼大肉就叫吃了。富贵人家吃的其实并不是山珍海味,他们讲究的是做工的细腻,到最后就变成了文化。
\n除了在吃上,贾家在穿戴上也是非常讲究,举几个例子:贾母的软烟罗、平儿的虾须镯、宝玉的雀金裘、湘云的凫魇裘……。
\n通过上边这些内容,我想引出一个更普世的观点:没有钱的人永远无法想象有钱人过的是什么样的生活,平时会使用什么样的东西,就像段子中皇帝的金锄头一样。
\n下边用几个我使用过的稍微好一点的物品举例,这些物品价格确实会稍贵一点,但也不是什么奢侈品,限于我目前的人生阅历也只能用这些来说明了。
\n一个戴森吹风机3000多,普通家庭是绝对不会买的。我们家几年前一直在用其他品牌的吹风机,也没感觉有什么问题,后来我们帮一个保险销售介绍客户,完成了很多任务,作为奖励她送了我们一台戴森吹风机,自从用上以后就再也用不惯其他吹风机了。
\n前一阵子搬家,我把戴森拿到了新家使用,因为我爸妈还要在之前的房子住,那边需要一个新吹风机。我看到这两年一个国产的品牌「徕芬」吹风机很火,外形也和戴森很像,就买了一个给他们用。前两天我回家用了一次徕芬,实话实说,如果我之前没有用过戴森,我一定觉得这个吹风机非常好用,但用过了更好的对比之下才知道还有很大差距。
\n传统的门锁都会外露一个弹簧的探头,用来在关门时将门卡住。探头上下两个角很尖,不注意时会磕碰到人,家里有小孩的情况下,如果小孩正好跟门锁差不多高,在跑来跑去时会更危险。日常关门时,因为探头要和门框上的凹槽摩擦,还会有很大的噪音。因为探头存在阻力,在关这种门时,通常是用手把门把手转到下边,再去将门关严,或者需要很用力地去关。
\n装修新房时,才知道现在已经有了无形的锁具,门在开启状态时探头是不会外漏的,只有将门关闭后探头才会弹出,避免了磕碰还更静音了。想把门关严时也不用捉着把手去关了,直接推门就可以。我没有研究它的原理,猜测是用了磁铁之类的。
\n另一个和装修有关的是新家里的淋浴设备和零冷水燃气热水器。在我没有用新的花洒之前,没觉得之前用过的花洒有什么问题,用过之后觉得之前花洒水量太小了。前两天再去用之前的就觉得身上的沫子半天才能冲干净,新的淋浴一瞬间就冲完了。
\n还有支持恒温的零冷水燃气热水器,如果没有接触过,我真的不知道洗澡水居然可以不需要等待,每次打开直接出热水,温度也是之前设置好的恒定温度,完全不用担心忽冷忽热的问题。
\n上个周末和家人去吃了一次比格自助,79一位。如果家庭条件一般,自助吃的比较少的话,就会觉得比格很不错了,当然比格在这个价位里也确实不错。但如果吃过更好的,就会知道比格的食材还差很多。
\n其实我也没什么资格评判比格,因为我吃的比较多的也是比格或者比格这个价位的自助,只是在公司团建的时候有幸吃过其他稍微高档一些的,比如第六季、水木锦堂之类的。但次数有限,那些更高级的,上千块的自助还没有体验过。
\n我现在只开过 20w 以内的车,已经觉得很好了。50w 以上的车还没有开过,更别提百万级别的豪车了。我相信我现在一定无法想象出开豪车的体验和惊喜。
\n如果我以后有机会能开上,再来更新使用体验😂
\n"},{"title":"因为没控制好情绪毁了半天休息时间","url":"/2023/ruined-half-a-day/","content":"今天是8月30日,星期三,娃的幼儿园过两天开学,需要开个家长会,时间是下午两点半。考虑到上班来回的路程,加上最近也想在工作日放松一天了,索性就请了一整天的假。
\n昨天运维在切换一台生产环境网关机的时候,我们有一个调用第三方的业务产生了大量报错,出现了一段时间不可用。可能是我们的调用方式有问题,原因还没有查清楚。
\n那个业务重要程度不高,而且是最近刚上线,用来提升用户体验的一个功能。但因为影响的请求数超过了天级的千分之一,按照惯例需要进行复盘。
\n因为影响的业务不太重要,而且原因还没查清楚,需要查一下根因,我就没有准备复盘工作。今天早上 SRE 直接给我定了会议室和时间,要求我复盘,看到我请假了就问我能否让另一个同事参加。我的防御心理一下子就开启了,在我的潜意识中认为复盘是我做错了什么事情。另外一点是我不想让其他人的时间被这种偶然复杂事件耽误,况且那段代码的底层调用逻辑也不是他写的,写这段代码的人已经离职了。因为这也算做一个故障,让同事参与可能会让他认为需要他来背故障责任。
\n当时我就一下子就暴怒了,在群里用比较激动的言辞指责SRE。结果就是整个上午我都在和SRE那边掰扯这件事,心情非常糟糕,而且那个群里还有我组内的其他同事,他们也看到了我情绪激动的言辞。
\n等我情绪宣泄完,心情平复后我就又开始后悔了,事后还跟SRE那边委婉的道了歉。
\n我当时的处理方式也有问题,SRE本来的计划是我如果不方便参加,他就和我另一个同事排查一下问题,把复盘做了,我因为在气头上,不让我同事配合,跟SRE说后边我查清楚了再和他们复盘。我做错了两点,首先我不应该认为这个事情会耽误其他同事工作,这也许是一个锻炼他排查问题的好机会,他可能也很乐意排查。其次我不应该把这件事揽到自己头上,我今天请假,本该今天做的事情挪到了周四周五,排查这个问题可能就要占用我一天的时间,时间根本不够用。
\n我当时正确的做法应该跟SRE和我的同事说先尝试定位下问题,能定位到今天就进行复盘,定位不到就等我回去了再一起看下。这样既可以留下今天先不复盘的 buffer,也可以让同事没那么大压力。
\n以后千万不能再在情绪激动的情况下发消息回消息了😭
\n休假期间也尽量不回消息、不读消息。
\n时刻牢记宝钗的金玉良言「事不关己莫开口,一问摇头三不知」。
\n"},{"title":"让前台程序转为后台运行","url":"/2018/run-in-background/","content":"我们在登录服务器,执行一个很耗时的任务时,通常我们会使用 nohup
+ &
的方式执行,如果我们在启动时,忘记加上这 nohup
该如何补救呢?
control + z
让当前进程挂起(Suspend)。jobs
查看它的作业号。bg %jobspec
来将它放入后台并继续运行。disown -h %jobspec
来使这个作业忽略 HUP 信号。这个方法可以用在 scp
的命令中,在没有设置 ssh 无密码登录的情况下,我们不能使用 nohup 来执行 scp
命令,所以只能在开始大文件拷贝后,通过上述流程来让这个作业放置在后台执行。
当我们使用 git
命令将本地新开分支的代码推到远端仓库时,需要先使用 --set-upstream
命令声明要推到远端的哪个分支,比如:
git push --set-upstream origin test && git push |
当我们忘记使用 --set-upstream
时就会报如下错误:
git push |
大多数情况下我们都是将本地同名分支推到远端仓库,那么有没有办法可以让我们在 push 时自动使用本地分支名作为远端的分支呢?当然有!
\n我们可以如下配置 git,将 push 到远端的分支名自动使用当前本地分支名:
\ngit config --global --add push.autoSetupRemote true |
如此配置后,就可以跟 --set-upstream
说再见了。
补充:git 官方文档对 pushautoSetupRemote 的介绍:https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushautoSetupRemote
\n\n"},{"title":"Serverless 入门","url":"/2019/serverless-introduce/","content":"\n\n\n云技术已经彻底改变了我们管理应用程序的方式,尽管很多公司早已不再使用物理服务器,但他们仍然从服务器的角度来看待他们的系统。
\n
如果我们试图把服务器的概念忘掉,并开始把基于云的应用程序视为工作流、分布式逻辑和外部管理的数据存储,会是什么情况?
\n本文文我们一起探讨下 Serverless。
\n和其它软件开发趋势一样,Serverless 并没有一个清晰的概念,它可以用在两种不同但又有些相似的领域:
\n尽管有 Serverless 这个名字,但实际并不是在没有服务器的情况下运行代码。之所以使用「无服务器计算(serverless computing)」这个名称,是因为拥有系统的企业或个人不必为运行后端应用而采购、租用、配置服务器或虚拟机。
\nServerless 有以下优势:
\nServerless 模式鼓励将开发重点放在定义明确的业务逻辑单元上,而无需考虑如何部署、扩容或其它一些过早优化。因此开发的重点也应该是单个功能或模块,而不是一个具有大范围功能的服务。Serverless 将开发人员从部署的麻烦中解放出来,使得他们能够专注于按照逻辑封装应用。
\n一个典型的例子是将图片上传到文件存储,此事件调用一个 Serverless 函数,这个函数创建图片的缩略图然后把该缩略图存入文件存储中,并将缩略图位置记录在 NoSQL 数据库中。数据写入 NoSQL 数据库的事件可能还会触发其他函数。这个缩略图创建函数只需按需运行,唯一的成本是调用该函数的次数。
\n和其他技术一样,Serverless 并不完美。它的缺点是应用监控和调试将会变得困难,只能依靠于服务产生的日志记录。同时,在有服务间调用事件时,可能会出现供应商锁定。并且现有的 IDE 对 Serverless 函数支持也不够友好。
\n\n\nServerless 框架 —— 可以构建由微服务组成的应用,这些微服务在响应事件时运行,并且可以自动扩容、只在运行期间收费。
\n
下边的例子将演示如何实现一个简单的 HTTP GET
端点,调用它时会返回当前的时间。内部函数名为 currentTime
,HTTP 端点为 ping
。
npm install -g serverless |
我们需要新建 handler.js
和 serverless.yml
文件来描述和部署我们的 severless 函数。
// handler.js |
// serverless.yml |
在命令行中执行
\nserverless invoke local --function currentTime |
返回结果如下:
\n{ |
部署应用只需执行
\nserverless deploy |
在安全凭证配置正确的情况下会看到类似下边的结果:
\nServerless: Packaging service... |
现在,我们可以直接调用 AWS Lambda 服务,并且可以同时获取执行日志:
\nserverless invoke --function currentTime --log |
或者使用如 curl
等工具发送一个 HTTP
请求来查看结果:
curl https://qnye7m4dwf.execute-api.us-east-1.amazonaws.com/dev/ping |
甚至可以直接用浏览器访问:
\n\n"},{"title":"山丘 - 李宗盛","url":"/2018/shangqiu-video/","content":"\n\n\n\n"},{"title":"Java 单例模式完整指南","url":"/2019/singleton-design-pattern-in-java/","content":"想说却还没说的 还很多
\n
攒着是因为想写成歌
让人轻轻地唱着 淡淡地记着
就算终于忘了 也值了
说不定我一生涓滴意念
侥幸汇成河
然后我俩各自一端
望着大河弯弯 终于敢放胆
嘻皮笑脸 面对 人生的难
也许我们从未成熟
还没能晓得 就快要老了
尽管心里活着的还是那个
年轻人
因为不安而频频回首
无知地索求 羞耻于求救
不知疲倦地翻越 每一个山丘
越过山丘 虽然已白了头
喋喋不休 时不我予的哀愁
还未如愿见着不朽
就把自己先搞丢
越过山丘 才发现无人等候
喋喋不休 再也唤不回温柔
为何记不得上一次是谁给的拥抱
在什么时候
我没有刻意隐藏 也无意让你感伤
多少次我们无醉不欢
咒骂人生太短 唏嘘相见恨晚
让女人把妆哭花了 也不管
遗憾我们从未成熟
还没能晓得 就已经老了
尽力却仍不明白
身边的年轻人
给自己随便找个理由
向情爱的挑逗 命运的左右
不自量力地还手 直至死方休
越过山丘 虽然已白了头
喋喋不休 时不我予的哀愁
还未如愿见着不朽
就把自己先搞丢
越过山丘 才发现无人等候
喋喋不休 再也唤不回了温柔
为何记不得上一次是谁给的拥抱
在什么时候
越过山丘 虽然已白了头
喋喋不休 时不我予的哀愁
还未如愿见着不朽
就把自己先搞丢
越过山丘 才发现无人等候
喋喋不休 再也唤不回了温柔
为何记不得上一次是谁给的拥抱
在什么时候
喋喋不休 时不我予的哀愁
向情爱的挑逗 命运的左右
不自量力地还手 直至死方休
为何记不得上一次是谁给的拥抱
在什么时候
\n\n\n设计模式一直流行于程序员之间,本文讨论被许多人认为最简单但最有争议的设计模式 —— 单例模式
\n
在软件工程中,设计模式描述了如何解决重复出现的设计问题,以设计出灵活、可复用的面向对象的应用程序。设计模式一共有 23 种,可以将它们分为三个不同的类别 —— 创建型、结构型和行为型。
\n创建型设计模式是处理对象创建机制的模式,试图以适合的方式创建对象。
\n对象创建的基本形式可能会导致设计问题或增加设计的复杂性。创建型设计模式通过某种方式控制对象的创建来解决此问题。
\n结构型设计模式处理类和对象的组成。这类模式使我们将对象和类组装为更大的结构,同时保持结构的高效和灵活。
\n行为型设计模式讨论对象的通信以及它们之间如何交互。
\n我们对设计模式和其类型进行了概述,接下来我们重点介绍单例设计模式。
\n单例模式提供了控制程序中允许创建的实例数量的能力,同时确保程序中有一个单例的全局访问点。
\n单例设计模式可以通过多种方式实现。每一种都有其自身的优点和局限性,我们可以通过以下几种方式实现单例模式:
\n本节我们将讨论实现单例模式的各种方法。
\npublic class EagerInitialization { |
import java.util.Objects; |
import java.util.Objects; |
import java.util.Objects; |
说明一下为什么这种方法在多线程常见下可能存在问题:
\nINSTANCE = new DoubleCheckSingleton();
这句代码,实际上可以分解成以下三个步骤:
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
\n在多线程中就会出现第二个线程判断对象不为空,但此时对象还未初始化的情况。
\nimport java.util.Objects; |
为了解决上述问题,需要加入关键字 volatile
。使用了 volatile
关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
public enum Singleton { |
在所有的单例实现中(枚举方法除外),我们通过提供私有构造函数来确保单例。但是,可以通过反射来访问私有构造函数,反射是在运行时检查或修改类的运行时行为的过程。
\u0005
让我们演示如何通过反射访问单例:
import java.lang.reflect.Constructor; |
上边代码输出如下:
\nInstance 1 hashcode: 21049288 |
如果单例对象已经初始化,则可以通过禁止对构造函数的访问来防止通过反射访问单例类。如果在对象初始化之后调用构造函数,可以通过抛出异常的方式来实现。
\npublic final class EagerInitializedSingleton { |
在分布式应用程序中,有时我们会序列化一个对象,以将对象状态保存在持久化存储中,并用以之后的检索。保存对象状态的过程称为序列化,而检索操作称为反序列化。
\n如果单例没有被正确实现,那么可能出现一个单例对象有两个实例的情况。
\n让我们看看如何出现这种情况:
\nimport java.io.FileInputStream; |
以上代码输出如下:
\nFirstSingletonInstance hashcode: 23090923 |
这说明现在有两个单例实例。
\n\n\n注意,单例类必须实现
\nSerializable
接口才能序列化实例。
为了避免序列化产生多个实例,我们可以在单例类中实现 readResolve()
方法。这个方法将会替换从流中读取的对象。实现代码如下:
import java.io.Serializable; |
再次执行 SingletonWithSerialization
输出如下:
FirstSingletonInstance hashcode: 24336889 |
Java API 中有很多类是用单例设计模式设计的:
\njava.lang.Runtime#getRuntime() |
单例模式是最重要和最常用的设计模式之一。尽管很多人批评它是一种反模式,并且有很多实现时的注意事项,但在实际生活中有很多使用这种模式的示例。本文尝试介绍了常见的单例设计和与之相关的缺陷。
\n"},{"title":"分享一个我已经持续半年的运动","url":"/2023/skipping-rope/","content":"今年2月中旬,我开始尝试一个新运动:跳绳。
\n到今天已经持续半年多,最开始使用无绳跳绳跳2000个,逐渐到5000,一个多月后改成有绳跳2000,逐渐到5000。
\n虽然体重没有太大变化,但精神状态好多了,每次跳完后都是暴汗,多巴胺大量分泌,跳绳过程中也会冒出一些灵感,有工作上的也有生活上的。
\n最近换成了一个重量比较大的绳(半斤重),根据当天精神状态跳2500-3000个,分成250个一组,每组中间休息20秒。每次运动时间大概花25分钟,加上运动后换洗衣服总耗时约35分钟,每周平均运动3-4次。
\n用大重量绳的好处是可以顺便锻炼手臂,同时还能节约时间,追求质量不再追求数量,跳的太多对膝盖也有负担。因为绳子重量较大,即便数量少了一些出汗效果一点也不差。在换成用大重量跳绳的过程中我还发现对耐力上限的阈值可以不断调教,之前用大重量的绳最多跳500个胳膊就抡不动了,而且在中午吃饭时会手抖,现在可以持续3000个。
\n我在公司放了一件运动T恤、一条运动短裤,还有一双运动鞋。每天中午12点多换上运动鞋拿上运动衣和跳绳到公司办公楼28层——这一层是空的,在洗手间换好衣服,带上耳机打开YouTube随机播一集圆桌派,边听边跳。跳完后再去洗手间把汗擦干,换回便装,运动衣用清水rua一把晾回工位,第二天再用时也就干了。
\n回工位后休息一下就可以下楼吃饭了,这时候吃饭的人已经不多,可以找个地方悠闲的吃个饭。我通常去一个称重计价的自助餐厅,中午一点半后6折,不到20块钱就能吃的非常好,公司餐补30元,剩下10块还能用来喝杯咖啡😂。如果想在1点半后来这家吃,我回工位后会看一会书,或者写篇流水账,一点半前进行5到10分钟冥想,1点半准时下楼吃饭。
\n跳绳一共买了6、7条,最推荐的是超飞跃家的。我买了两条超飞跃,一条6mm 的,一条8mm,8mm 的那条有半斤重。
\n我将6mm 的打了个结,可以稍微提高一些摇绳时的惯性。
\n\n下边这条是8mm 的:
\n\n6mm 的价格是69.9,8mm 的价格是130,从京东购入。
\n强烈建议再从拼多多买羽毛球拍防汗带把手柄缠上,这样手感非常好。
\n我用 AppleWatch 采集心率,通过 YaoYao 这个跳绳专用 App 在运动期间查看心率并进行间歇训练计数。
\n我会将心率心率控制在135左右,中间会有几十秒努力将心率提升至极限150+,有效训练心肺功能。
\n\n\n因为是在室内,摇绳的声音就比较大,再加上升级成半斤重的跳绳后,甩绳子的声音整个楼层都能听到,带上 AirPods Pro2 开启降噪整个空间都是我的。
\n每次跳绳都会听一集圆桌派,既可以在运动过程中分散坚持不住时的注意力,又可以长见识,听听大咖们思考问题的方式。YouTube 已经完全学会了我的喜好,每次中午只要一打开它,列表中第一个一定是一集我没有听过的圆桌派,而且不是按顺序播放的甚至推荐的不是同一季。
\n我跳绳穿的是一双国产品牌的运动鞋,叫「必迈」,具体型号是远征者4.0Plus。因为非常舒服我买了两双,一双放公司专门用来在中午跳绳用,另一双放在家运动或者散步时穿。第一双是半年前买的价格是330,第二双是前两周买的,价格降到了295,都是在拼多多买的。
\n我没有穿过非常贵的运动鞋,但我可以说这双鞋在300这个价位内绝对是无敌的存在,鞋子很轻、鞋底较厚且非常有弹性。
\n\n跳绳是项对场地要求很低的运动,只需要2平米的空间就可以开始,枯燥的时候还可以加上各种花式动作,比如下边这个视频就非常酷炫,一看就会一学就废。
\n\n\n看了下今天的数据,我已经累计跳了48万次,继续加油。
\n\n"},{"title":"睡眠少究竟有没有危害?","url":"/2022/sleepless-is-not-harmful/","content":"\n我在今年 2 月份的时候读了一本书叫《我们为什么睡觉》,作者提出了很多睡眠相关的研究成果,比如充足睡眠的必要性、失眠对大脑的永久不可逆损伤和睡眠少会大大提高人们犯错的概率等。本身我的睡眠就不是特别好,看这本书是想从这本书中找寻能让我睡得更好的方法,虽然这本书中也有涉猎,但篇幅不多,总的来说读完这本书后因为书中罗列的那些睡眠不足带来的负面影响,反而使我的睡眠压力更大了,但我不否认这是一本很好的科普书和畅销书。
\n但是这几天读到的 这篇文章 让我对睡眠这件事有了新的思考,这是一篇刷新率很高的文章,颠覆了我们大多数人之前对睡眠的认知。
\n\n\n刷新率这个词是我今天早上在地铁上读「写作是门手艺」这本书中新学到的,指的是读者看完你的研究后,想法改变了多少。
\n
这篇文章认为睡眠少并没有我们常见科普文中描写的那么多危害,文中提到急性睡眠剥削,也就是突然减少睡眠,对健康有益,可以提升我们的睡眠效率。就我自己来说确实是这样,我在一个晚上失眠后,后边几天会睡得比较好,自我感觉睡眠效率也有挺高。
\n作者用断食来做对比,一些宗教和追求健康的人都会定期进行断食,睡眠少和断食一样,都会让我们有不适感,比如怕冷、注意力难以集中,但人们从来不认为断食是坏事,它能激发细胞的自噬,对我们的健康有利,同理「断」睡眠也不应该被认为是不好的。另一个对比是运动,我们运动后会出现肌肉疼痛和其他不适感,但这并不代表运动对我们有害。
\n作者还认为睡眠剥削并不会影响我们的认知能力,他还拿自己做过实验,作者尝试每天只睡 4 小时,然后正常上班,一周后询问他的同事有没有发现什么异常,他的同事们表示没有,作者本人也没找到任何变化。而且作者写这篇文章用了 38 个小时,期间只睡了 1.5 小时。
\n马斯克也曾经表达过自己曾每周工作 120 小时,剩余时间如果全拿来睡觉每天也只有 6.8 小时。
\n我自己感受到的情况也大致如此,在前一晚没睡或睡眠不足的情况下,对第二天的工作实际并没有什么影响,更多的是自己心理的不适感,多少次我在失眠的第二天上班,没有任何同事表达过我这天不对劲。
\n这篇文章作者认为睡眠少不仅不会减少寿命,反而会增加寿命,以每天睡 6 小时为例,每年可以增加 33 天生命、每 11 年可以增加 1 年生命、每 55 年增加 5 年生命。我也经常用这样的比喻开玩笑:我每天都能比别人多活几小时,看来是真的。应了中国那句老话:生前何必久睡死,后自会长眠。
\n我们祖先并没有我们这么好的睡眠环境,我们有事适宜的睡眠温度、柔软的床垫。一万年前我们的祖先睡在山洞里、小屋里或者天空下,周围有掠食者和敌对部落,所以他们不可能肆无忌惮的去睡觉,就像食物一样,虽然我们现在食物充足,但大家都知道应该避免暴食,但是对于睡眠却认为多多益善。
\n作者甚至认为睡得太多更容易患抑郁症,现在确实在医疗界会用限制睡眠来缓解抑郁症,《我们为什么睡觉》这本书里也有介绍。
\n还有一个与我们之前认知向背的观点:作者认为记忆力的巩固并不需要睡眠,这点我也有体会,比如我前一天背了英语、Anki,在失眠一晚后的第二天再次复习那些内容时依然可以背下来,没有任何影响。
\n很多人(包括我自己)认为睡眠不足会影响第二天的情绪,但我觉得心理因素的影响比身体因素影响要大的多。我低落的原因大多是因为前一晚翻来覆去睡不着而恼火影响了第二天的情绪,回想一下初高中时候逃课去网吧通宵,第二天也没觉得情绪有啥影响。
\n说到逃课去网吧这里跑个题,我的初恋是高中同学,我俩确定关系是有次我们学校搞活动,搞到晚上 10 点多,她是走读生,我是住宿生,我知道她家离学校很远,坐公交要 1 小时,想要做次护花使者送她回家。我记得当时我们打了个车把她送到她家附近,这时候回学校已经进不了宿舍了,学校大门也进不了,索性我就没回去(也多亏了那个活动结束后宿舍比较混乱,没有查寝),我在她家附近找了个网吧玩了一晚上,当然我的另一个目的也是第二天早上能和她一起乘公交去学校,第二天早上我们在公交车上汇合,她给我带了一包牛奶,那一天是我的高光时刻,觉得世界上其他事情都不重要了,虽然前一晚没睡觉,但心情反而乐到极点,课上也睡得死去活来。
\n我在睡眠少的时候更容易亢奋,更容易在这一天发朋友圈或者写博客,我觉得这也符合我们祖先的特征:缺乏睡眠大多是因为周围有危险情况导致的,他们为了活下来需要更警觉。作者在文章中也补充了几件发生在其他人身上的轶事,其中有一个叫 Brian Timar 的提到:自己在本科阶段每次重大考试前一晚都不睡觉,第二天会超级兴奋和敏锐。
\n\n\nsleep anecdote- In undergrad I had zero sleep before several major tests; also before quals in grad school. Basically wouldn’t sleep before things I really considered important (this included morning meetings I didn’t want to miss!). On such occasions I would feel:
\n\n
\n- miserable, then
\n- absurd and in a good humor, weirdly elated, then
\n- Super PumpedTM, and
\n
really sharp when the test (or whatever) actually started.
很多人表达过睡觉多的孩子学习更好,我们是不是可以从另一个角度去解释这个现象:学生们睡觉多了学习时间就少了,他们需要集中精力去学习,这样的话效率自然就高了;那些睡觉少的学生因为时间多,所以做事总是磨磨蹭蹭,效率不高,效率不高更不容易学会,就不愿意学,这样就形成了负向螺旋下降。如果那些睡眠少的学生把大量时间都用在学习上我相信不会比其他学生差吧。
\n我大部分失眠情况可能都是因为担心失眠而失眠了,担心自己睡不好影响第二天的表现,读完这篇文章我觉得自己以后可以不这么紧张了,我要告诉自己:睡眠少可以让我更亢奋,发挥的可以更好。
\n"},{"title":"Spark 操作 Elasticsearch 示例","url":"/2017/spark-operator-elasticsearch-demo/","content":"上周五调研了下如何用 Spark 读写 Elasticsearch(下文简称 es),中间被官方提供的 jar 包卡了很久,所以本来想周末记录一下,结果一发懒就没做,就蹭到周一晚上来写一下了,最近调研的东西很多,有很多要记得东西,一点一点来吧。
\n不废话,直接 Show you the code:
\nimport org.apache.spark.{SparkConf, SparkContext} |
其实上边这些代码从网上一搜一大堆,重点是下边 sbt
部分的配置:
name := "spark-es-demo" |
需要注意 scala 版本,spark 版本还有 es 版本一定要对应,否则无法运行
\n比如
\nscalaVersion
版本是 2.11.11
spark
版本是 1.6.2
es
版本是 5.5.2
依赖需要写成下边这样:
\nlibraryDependencies += "org.apache.spark" %% "spark-core" % "1.6.2" // 这里指定 spark-core 的版本 |
解释一下 "elasticsearch-spark-13_2.11" % "5.5.2"
这部分
-13
是给 Spark1.3-1.6
提供的-20
是给 Spark2.0
提供的
_2.11
是 scalaVersion
的前边两位
5.5.2
是 elasticsearch
的版本号
官方文档中提到
\n\n\nThe Spark connector framework is the most sensitive to version incompatibilities.
\n
\n\nSpark 连接器框架是对版本号非常敏感并且不兼容的。
\n
另外一个坑是,elasticsearch-spark-13_2.11
这个 jar 包所依赖的包无法在 maven
官方源中找到,需要添加另一个源:conjars: http://conjars.org/repo
在 ~/.sbt
下新建 repositories
文件,我的 repositories
内容如下:
[repositories] |
将阿里源放在上边,可以让官方依赖下载更快。
\n完整代码见:https://github.com/Panmax/spark-es-demo
\n","tags":["BigData"]},{"title":"Spring Boot 与 Docker 结合","url":"/2017/spring-boot-docker/","content":"\n\nDocker 是一个具有社交倾向的 Linux 容器管理工具包,允许用户发布容器镜像,其他用户可以使用这些镜像。Docker 镜像是运行容器化进程的基础,本文将介绍如何编译一个简单的
\nSpring Boot
应用的镜像。
Docker 的安装和基本使用不在本文中介绍,之后可以单独拿出来写一写。
\n本文将使用 Gradle 作为编译工具,基础项目工程直接使用 IDEA 的 Spring Initializr
生成,如下图:
直接下一步,注意这里的 Type 修改为 Gradle Project
,然再下一步
然后只需要勾选 Web 即可
\n\nbuild.gradle
,新增:jar { |
它的作用是让编译出来的 jar 包文件名为:my-spring-boot-docker-0.1.0.jar
build.gradle
如下:buildscript { |
然后我们写一个最简单的 Controller
,为了方便直接写在 main
方法的类中:
package com.jpanj; |
./gradlew build |
然后访问 localhost:8080 可以看到 Hello Docker World
的返回结果。
Docker 有一个简单的 Dockerfile 文件格式用来指定生成镜像的层次,所以我们在 Spring Boot
项目中创建一个 Dockerfile,将这个文件放在项目根目录下即可,现在这个项目结构是这个样的:
FROM frolvlad/alpine-oraclejdk8:slim |
这个 Dockerfile 非常简单,不过这就是你运行一个 Spring Boot
应用的全部了,只需要 Java
和 JAR
文件就够了。这个项目 JAR
文件被作为 app.jar
加入到容器中,然后通过 ENTRYPOINT
来执行它。
Docker容器 运行时应该尽量保持容器存储层不发生写操作,在这里我们添加一个 VOLUME
指向 /tmp
是因为 Spring Boot
应用在默认情况下会为 Tomcat
创建工作目录。这里的 /tmp
目录就会在运行时自动挂载为匿名卷,任何向 /tmp
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。
当然,也可以在运行时可以覆盖这个挂载设置
\ndocker run -d -v mytmp:/tmp xxxx
在这行命令中,就使用了 mytmp
这个命名卷挂载到了 /tmp
这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。
不过此步骤在这个简单的应用中是可选的,但是在其他会写入文件系统的 Spring Boot
应用中是必须的。
接下来就是把这个项目编译成一个可以到处运行的 Docker 镜像了,在 build.gradle
中我们添加一些新的插件:
buildscript { |
jar
文件从编译目录复制到 docker
编译目录的 target
目录下,这就是我们在 Dockerfile 中看到的 ADD target/my-spring-boot-docker-0.1.0.jar app.jar
为什么会生效的原因。build.gradle
如下:buildscript { |
现在你可以使用下边的命令编译这个 docker 镜像,然后将它推送到远端仓库中分享给其他用户使用(本教程不介绍推送远端仓库的方法)。
\n./gradlew build buildDocker
执行上边命令后,可以在本地的 docker 镜像仓库中看到,已经有了我们自己编译出来的 my-spring-boot-docker
镜像:
docker run -p 8080:8080 -t my-spring-boot-docker:0.0.1-SNAPSHOT
\n\n这里说明一下 -p 的用途, -p 8080:8080 的意思是将本地的 8080 端口映射到容器的 8080 端口,因为容器相对于主机来说是完全隔离的,所以必须要有此设置,不然外部是无法访问到 8080 端口的。
\n
现在再次访问 localhost:8080 就可以看到 Hello Docker World 啦。
\n当容器运行时你可以通过 docker ps
的命令看到正在运行的容器列表:
而且可以通过 docker stop
加上这个容器的 ID 来停掉它:
如果你想删掉这个容器,可以使用 docker rm + 容器 ID
。
到此你已经为 Spring Boot
应用创建了一个 docker 容器,默认运行在容器内的 8080 端口上,我们在命令行中使用 -p
参数将它映射到主机的相同端口上。
今天遇到一个问题,搞了将近小半天也没有解决,最后我给出来的结论是 Spring Boot Redis Starter 里用的 Jedis 的 Bug 导致。
\n我来描述一下问题过程,今天我为了测试 Spring Boot 和 Redis 相结合,写了一个 Demo 程序,先来连接我本地的 Redis 服务,测试了一下没有问题,然后我想试试连接远程的 Redis 服务,我之前在自己的一台服务器上搭了 Redis 服务,在我的一个正在运行的 Python 项目中用到了这个库,说明服务是没有问题的。并且我已经将这个服务端口监听到了 0.0.0.0
,所以外部是可以访问的,唯一的区别是我加了密码验证。我在 application.yml
中配置 host
和 password
后,进行测试,发现报错:
2017-08-16 17:47:21.908 ERROR 55442 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool] with root cause |
然后我根据网上的给出的方案,尝试修改 spring.redis.pool
相关的各种参数,都没有解决。网上有的说是 Redis 的连接数太高导致的,我看了等下 Redis 的 info 都正常。
我在本地用命令 redis-cli -h hostname -a password
也是可以连上的,先不输密码登录成功后用 AUTH + password
的方式也可以访问。
因为我自己服务器上的 Redis 在使用中,不方便修改密码,所以我在公司 lc7 的服务器上搭了个 Redis,修改配置监听 0.0.0.0
不过没有设置密码,修改待测试程序的配置文件,发现可以读到,然后又把 lc7 的 Redis 服务加上了密码,再次测试还是没有问题。
后来我猜是不是我自己服务器上的密码设置的太长了,我又把 lc7 的密码改为和我自己服务器相同的密码,测试后还是没问题。
\n然后我又对比了下两个机器上安装的 Redis 版本,发现我的服务器上的版本为 2.x,而 lc7 的为 3.x 版本,于是我又冒着风险将我自己服务器的 Redis 进行了升级,升级完先检查了下用到这个 Redis 的其他应用能不能正常工作,检查没有问题后,修改我要测试程序的配置文件来连接这个 Redis,结果还是报那个错误。
\n最后,我冒着自己服务器上所部署的应用暂时不可用的风险,去掉了 Redis 的密码,这时候测试发现没问题了。
\n综上,就是这个 Bug 非常迷的论述。暂时没有找到解决方法。
\n"},{"title":"SpringBoot(2.0) + JPA + MySQL 实现 Restful CURD API","url":"/2018/spring-boot-with-mysql-jpa/","content":"Spring Boot 将 Spring 框架提升了一个新的水平,极大地缩短了 Spring 项目的配置与设置的时间。你几乎可以零配置的开始一个项目并构建你真正关心的部分。
\n我将通过一个记事本应用来演示一下 JPA 的使用,一篇笔记有标题和内容。我们先来编写增、删、改、查接口,然后使用 postman 来进行测试。
\nSpring Boot 提供一个 web 工具叫做 Spring Initializer 来引导一个应用。访问 http://start.spring.io 然后按照下边的步骤来生成一个新的项目:
\n输入完所有详情后,将上边的 Generate a 改为 Gradle Project 点击 Generate Project 来生成并下载项目。Spring Initializer 将根据你输入的信息生成项目并提供一个 zip 包含所有项目目录。下一步解压下载下来的 zip 文件,并将导入到你喜欢的 IDE 中。
\n\n下边是我们记事本程序的目录结构
\n\n让我们来理解几个重要文件和目录的详情
\n这是我们 Spring Boot 应用的主要入口。
\npackage com.example.easynotes; |
它包含一个名为 @SpringBootApplication
的简单注解,这个注解是以下 Spring 注解的组合:
@Configuration
注解的类都由 Spring 引导,并且也被视为其他 bean 定义的来源。build.gradle
文件中添加的依赖来自动配置你的应用。spring-data-jpa
位于 classpath 中,它会通过从 application.properties
文件中读取数据库属性来自动尝试配置一个 DataSource。main()
方法调用 Spring Boot 的 SpringApplication.run()
方法启动这个应用。
顾名思义,这个目录用于存放所有静态资源、模板和属性文件。
\nEasyNotesApplicationTests
在这里定义单元测试和集成测试
\nbuild.gradle
包含所有项目依赖
\n如果 spring-data-jpa
位于 classpath 中,Spring Boot 会尝试从 application.properties
文件中读取数据库配置自动配置 DataSource
。所以我们只需要添加配置,Spring Boot 将负责其他部分。
我更习惯于使用 yml 的方式管理配置,所以我们将 application.properties
删掉,新建名为 application.yml
的文本文件。
# Spring 数据源 (DataSourceAutoConfiguration & DataSourceProperties) |
我们需要在 MySQL 中创建一个名为 notes_app 的数据库并将配置文件中的 username
和 password
属性改为你安装的 MySQL 对应的值。
spring.jpa.properties.hibernate.dialect
和 spring.jpa.hibernate.ddl-auto
这两个配置是提供给 hibernate 的,Spring Boot 使用 Hibernate 作为默认 JPA 实现。
spring.jpa.hibernate.ddl-auto
配置用于数据库初始化,我使用 update 值作为属性。
它做了两件事:
\n对于 spring.jpa.hibernate.ddl-auto
属性来说使用 update 值对于开发阶段来说非常好,但是对于生产阶段,应该保留这个属性值为 validate,并使用数据库迁移工具来管理数据库结构的修改,如 Flyway。
接下来创建 Note 模型,我们 Note 模型有如下字段:
\nid
:自增主键title
:笔记的标题(非空字段)content
:笔记的内容(非空字段)createAt
:笔记的创建时间updateAt
:笔记的更新时间现在来看一下如何在 Spring 中对它进行建模。在 com.example.easynotes
创建一个名为 model
的包,并添加一个名为 Note.java
的类,内容如下:
package com.example.easynotes.model; |
@Entity
进行注解,他用于将该类标记为持久 Java 类@Table
注解用于提供此实体将映射到表的详细信息@Id
注解用于定义主键@GeneratedValue
注解用于定义主键生成策略,上例中我们声明主键是一个自增字段@NotBlank
注解用于验证被注释的字段不为 null 或 空@Column
注解用于定义被映射到注解字段列的属性,可以定一个多个属性如名称、长度、可为空、可更新等
默认情况下,名为 createAt 的字段将映射到数据库表中名为 create_at
的列,即所有驼峰命名将使用下划线替代,如果你想映射这个字段到不同的列,可以使用以下命令指定它:
@Column(name = "created_on") |
@Temporal
注解与 java.util.Date
和 java.util.Calendar
类一起使用,它将 Java 对象中的时间和日期转换为兼容数据库的类型,反之亦然。
@JsonIgnoreProperties
注解是一个 Jackson 注解,Spring Boot 使用 Jackson 在 Java 对象和 JSON 直接进行序列化和反序列化使用这个注解是因为我们不希望客户端通过 rest api 提供 createdAt
和 updatedAt
的值,如果它们提供这些值,我们会简单忽略他们,但是我们将在 JSON 响应中包含这些值。
在 Note
模型中,我们分别用 @CreatedDate
和 @LastModifiedDate
注解标注了 createdAt
和 updatedAt
字段。现在我们想要的效果是只要我们创建或更新实体,这些字段会自动填充。
为了做到这一点,我们要做两件事:
\n添加 Spring Data JPA 的 AuditingEntityListener
到领域模型中
我们已经在 Note 模型中使用注解 @EntityListeners(AuditingEntityListener.class)
来完成了这个工作
在主应用程序中开启 JPA 审计
打开 EasyNotesApplication.java
并添加 @EnableJpaAuditing
注解。
|
接下来我们要做的是创建一个仓库来访问数据库中的 Note 数据。
\nSpring Data JPA 让我们覆盖这里,它带有一个 JpaRepository 接口,该接口定义了实体上所有 CURD 操作的方法,JpaRepository 的默认实现为 SimpleJpaRepository。
\n现在来创建仓库,首先在 com.example.easynotes
下创建一个名为 repository
的包,然后创建一个名为 NoteRepository
的接口并从 JpaRepository
扩展它:
package com.example.easynotes.repository; |
请注意我们使用 @Repository
注解标注了接口,这会告诉 Spring 在组件扫描期间引导这个仓库。
以上这些就是你在仓库层所要做的所有工作了,你现在可以使用像 save()
、findOne()
、findAll()
、count()
、delete()
等 JpaRepository 方法。
你不需要实现这些方法,他们已经由 Spring Data JPA 的 SimpleJpaRepository
实现,这个实现在运行时被 Spring 自动插入。
查看 SimpleJpaRepository 文档 中提供的所有方法。
\n我们将在后边定义 Rest API 用来创建、检索、更新和删除笔记。API 会在数据库找不到具有指定 ID 的笔记时抛出 ResourceNotFoundException
异常。
以下是 ResourceNotFoundException 的定义,我们在 com.example.easynotes
中创建一个名为 exception
的包来存放这个异常类。
package com.example.easynotes.exception; |
注意,上边的异常类使用了 @ResponseStatus
注解,在你的 Controller 中抛出此异常时,Spring boot 会相应指定的 HTTP 状态码。
最后一步,我们将编写 REST API 来创建、检索、更新和删除笔记。
\n首先在 com.example.easynotes
中创建一个新的包 controller
,然后创建一个新的类 NoteController.java
内容如下
package com.example.easynotes.controller; |
@RestController
注解是 Spring 中 @Controller
和 @ResponseBody
注解的组合。
@Controller
注解用于定义一个控制器,@ResponseBody
注解用来表示方法的返回值应该用作请求的响应体。
@RequestMapping("/api")
声明这个控制器中所有 api 的 URL 将以 /api
开头。
接下来我们来一个一个实现这些 api。
\n// Get All Notes |
上边的方法非常简单,它调用 JapRepository 的 findAll() 方法来检索数据库中所有的笔记并返回整个列表。
\n另外,@GetMapping("/notes")
注解是 @RequestMapping(value="/notes", method=RequestMethod.GET)
的简写形式。
// Create a new Note |
@RequestBody
注解用于将请求体与方法参数绑定。
@Valid
注解确保请求体是有效的,记不记得我们在 Note
模型中用 @NotBlank
注解标记了 Note 的 title 和 content。
如果请求体中没有 title 或 content,Srping 将向客户端返回 400 BadRequest
错误。
// Get a Single Note |
顾名思义,@PathVariable
注解用于将路径变量与方法参数绑定。
在上面的方法中,只要没找到指定 ID 的笔记,我们就抛出一个 ResourceNotFoundException
异常。
这将导致 Spring Boot 向客户端返回一个 404 Not Found 错误(我们已经为 ResourceNotFoundException
类添加了 @ResponseStatus(value=HttpStatus.NOT_FOUND)
注解)。
// Update a Note |
|
我们已经成功为我们的应用程序构建了所有的 api,现在运行该应用并测试 api。
\n在你的 IDE 中直接运行 EasyNotesApplication 类即可,应用将使用 Spring Boot 的默认 tomcat 端口启动。
\n接下来我们使用 postman 来测试我们的 api。
\nPOST /api/notes
创建一个新的笔记GET /api/notes
检索全部笔记GET /api/notes/{noteId}
检索单个笔记PUT /api/notes/{noteId}
更新一个笔记@Controller
用于标记在一个类上,使用它标记的类就是一个 SpringMVC Controller
对象,分发处理器将会扫描使用了该注解的类方法,并检测该方法是否使用了 @RequestMapping
注解Request
请求 header 部分的值绑定到方法参数上@Controller
和 @ResponseBody
的组合效果dao
层,在 daoImpl
类上面注解Controller
的方法返回的对象通过适当的 HttpMessageConverter
转换为指定格式后,写入到 Response
对象的 body
数据区@Autowired
的使用来消除 set
、get
方法Spring MVC
后台控制层获取参数,类似的一种做法是: request.getParamter("name")
今天在帮助 datamaster 接入配置中心时发现一个问题,就是如果在应用的 application.yml
像这样:
spring: |
指定环境的话,在启动时会自动寻找 datamaster-scheduler-lc10.yml
这个配置文件,不论使用 bootrun
启动或者打成 jar
包都没有问题。
但是如果不在 application.yml
中指定 spring.profiles.active
,而是打成 jar
包,使用 --spring.profiles.active=lc10
参数启动的话,就注册不上 Eureka,所以也就没办法正常拉取配置文件了。
出现这个问题可以理解,因为我们在封装微服务接入的 starter 时,只定义了两个 active:
\n--- |
所以使用这两种之外的环境是找不到配置中心的地址的,但是之前那种方式所得到的行为就不太理解了。
\n这个问题我得到的结论是这样的:在应用系统的 application.yml
中定义的 active
是不具有穿透性的,所以我们的 微服务 starter 是不会得到这里定义的 active
的,而且 微服务starter 中的 bootstrap.yml
中定义了:
spring: |
所以在没有指定时会使用 dev,没有任何问题。
\n使用 --spring.profiles.active=lc10
指定的 active 具有穿透性,会让这个应用系统依赖的其他组件也使用指定的 active
,所以这个时候 微服务 starter 也会切换到 lc10 的 active
,因为我们没有在 starter 中配 lc10 对应的 active
,也就就相当于没有指定 eureka 的注册地址,所以接下来的所有流程就都跑不通了。
为了解决这个问题,我在 微服务 starter 的 bootstrap.yml
中加上了默认的 eureka 注册地址和其他需要用到的配置,这样即便是指定的环境不存在的话也没有任何问题。
之后在为客户部署时,只需要在 微服务 starter 中增加一个客户所对应的环境,然后启动应用时指定客户的 active
就可以了。
新公司使用的Java技术栈,我们有部分新业务需要调用 OpenAI 的接口进行交互,之前我找了一个比较轻量的SDK来调用OpenAI的接口,地址是:https://github.com/Lambdua/openai4j ,这个库作为日常使用足够了,但是一些高阶能力无法满足,而这些也是我们未来会用到的,比如:
\n把第一版功能完成后,这几天工作不是那么多,于是我从Github上找到了这个库https://github.com/langchain4j/langchain4j ,从名字就能看出来,这个项目是参考的 Python 的LangChain,Java 库的命名很有意思,很喜欢叫 xxxx4j,4j 的意思是 for Java,比如 log4j。
\n我大致看了一下介绍,功能还算完备,给出的demo来看使用方式上可读性也很高,更重要的一点是支持古老的Java8。于是我在项目中进行了引入,将已有代码进行了改造,在跑直接调用 OpenAI 的例子时很顺利,当我切换为 Azure 后问题出现了,报错堆栈如下:
\nException in thread \"main\" java.lang.NoClassDefFoundError: reactor/core/Disposable |
我按照堆栈的引导,一步一步去看代码,发现是在创建 HttpClient 对象时挂了,我进到 ConnectionProvider
源码中查看,确实找不到上边说的 Disposable
类,这个类来自 reactor-core
包。通过IDE跳转进的路径看到,目前项目中所使用的 reactor-core
版本是 2.0.8.RELEASE,我找到最新 3.6.7 版本的 reactor-core
源码看了下是有Disposable
这个类的。
一开始我认为是 langchain4j 的这个项目有问题,去 Github 的 Issue 中搜了下并没有相关的提问,于是我自己开始尝试动手解决,尝试了以下几种方式都不行:
\nreactor-core
langchain4j-azure-open-ai
下的 reactor-core
依赖,保证我自己引入的最新版本生效reactor-netty-core
的最新版langchain4j
的依赖在做上边的第2步时,启动调试后可以看到,IDE在进入ConnectionProvider
后确实可以正常跳转进Disposable
了,但最终还是报错。通过依赖分析也没有发现和 reactor
的任何冲突,一直搞到晚上下班也没解决。
今天早上上班后我换了个思路来排查这个项目,创建了一个新项目,只引入 langchain4j
的依赖,可以正常执行,接下来我把我们项目中其他依赖项引进来,发现还是没问题,当我把 parent 引入后问题出现了。虽然 parent 的 pom 文件在远端,但IDEA提供了一个功能,可以修改本地的文件来进行调试,我用二分法删除 parent 中的依赖,最终将问题定位在了:
<dependency> |
parent 中 spring.boot.version
的值是 1.5.7.RELEASE
,我在上上家公司写Java时就有这个版本了,是个非常老的版本,但升级 SpringBoot 关联的问题会更多。我继续深入进去看,在 spring-boot-dependencies
的 pom 文件中 properties
指定了reactor.version
为 2.0.8.RELEASE,这下破案了。之前我无法通过依赖分析找到冲突,也是因为依赖是在 parent 指定的,且这个依赖版本无法在后续进行修改。
有种覆盖 parent 版本号的方式是在自己项目的父 pom 中的dependencyManagement
下进行声明,我尝试在 dependencyManagement
加上如下片段:
<dependency> |
此时报了另一个错误:
\njava.lang.VerifyError: class io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe overrides final method close.(Lio/netty/channel/ChannelPromise;)V |
回到最开始的问题,报错误的根本原因是,初始化 Azure模型时需要构造一个 HttpClient,默认情况下会使用ConnectionProvider
来构造。看了下 AzureOpenAiChatModel
的 builder 方法,支持自己传入 OpenAIClient
,而 OpenAIClient
可以自己构造 HttpClient
,通过这个文档看到 https://learn.microsoft.com/en-us/azure/developer/java/sdk/http-client-pipeline HttpClient 有多种实现,其中可以用 OkHttpClient 来实现,于是我进行了以下魔改:
private static OpenAIClient setupSyncClient(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) { |
从开源代码中拷贝出 setupSyncClient
和 setupOpenAIClientBuilder
方法,并对setupOpenAIClientBuilder
中的HttpClient httpClient 的创建逻辑进行了调整
// before |
初始化Azure模型时传入我自己的 client:
\n// 默认生成的client使用NettyAsyncHttpClientProvider和SpringBoot所依赖的版本不兼容,改用OkHttpAsyncClientProvider进行重写 |
并在工程中引入 azure-core-http-okhttp
的依赖
<dependency> |
再次执行还是报错了,不过这次的错误变为:
\njava.lang.NoClassDefFoundError: reactor/util/context/ContextView |
还是 reactor 的问题,但可以看到,现在已经不再使用 reactor.core.Disposable
了,也许升级一下 reactor-core
可以解决,我再次在项目的 parent 的dependencyManagement
下引入
<dependency> |
再次尝试,问题解决。
\n"},{"title":"通过蜜罐技术自制一个暴力破解字典库","url":"/2019/ssh-honeypot/","content":"\n\n\n蜜罐指具有缺陷的,用于吸引网络的计算机病毒侵占以便用于病毒的研究和破解的计算机。
\n
互联网就像一个黑暗丛林,当你拥有一个面向公网的服务器时,永远不知道会有多少双眼睛在盯着你。
\n基于这个信条,我相信我的几台 vps 经常受到各种 ssh 的暴力破解的骚扰,为了安全考虑我也早就把默认 ssh 端口号 22 改为了某个随机值(有些是运营商强行修改)。
\n前段时间突发奇想,是否可以监听下 22 端口,感受下在这个黑暗丛林中来自各方的打击?继而又想到,既然来感受打击,为何不把这些打击详细记录一下,假以时日,我是不是就可以得到一个「丛林常用爆破密码库」了?
\n说干就干,选择了我其中一个坐落于米国的服务器来部署蜜罐程序。修改 ssh 默认端口号的步骤就不再介绍了,直奔主题。
\n为了方便,我通过 Docker 来部署,只需 1 行命令:
\ndocker run -itd --name ssh-honeypot -p 22:22 txt3rob/docker-ssh-honey |
这里用 Docker 启动了一个 ssh 蜜罐镜像,然后把蜜罐的 22 端口映射到本地 22 端口,验证一下 22 端口的开放情况:
\n# lsof -i:22 |
后边就坐等蜜罐来收集登录信息吧。
\n程序默认把登录日志输出到 Docker 容器的控制台中,日志格式如下:
\n[Thu Nov 7 17:59:00 2019] 187.189.55.192 root 123456 |
我们可以通过管道(|
) + 重定向(>
)的方式把结果导出出来:
docker logs $(docker ps -f name=ssh-honeypot -q) | grep -v 'Error exchanging' | grep -v 'Session' | awk '{print $6, $7, $8}' > ./ssh_password.log |
上边命令只保留了 ip、username、password 三列,同时过滤掉了程序自己打印的日志(包含 Session
或者 Error exchanging
的行)。
去掉行首行尾的空格sed -i 's/^[ \\t]*//g' ssh_password.log
sed -i 's/[ \\t]*$//g' ssh_password.log
去除空行
\nsed -i '/^$/d' ssh_password.log |
去掉数据结尾的 ^M
dos2unix -f ssh_password.log |
运行三周后,我一共收到了 1340万+ 的用户名密码(看到结果后有些震惊),去重后(cat ssh_password.log | awk '{print $2$3}' | uniq -c | wc -l
)也有 1290万,之后我对这些数据进行了统计。
# awk '$2!="" {sum[$2]+=1} END {for(k in sum) print k ":" sum[k]}' ssh_password.log | sort -n -r -k 2 -t ':' | head -n 5 |
# awk '$3!="" {sum[$3]+=1} END {for(k in sum) print k ":" sum[k]}' ssh_password.log | sort -n -r -k 2 -t ':' | head -n 5 |
# awk '$2!="" {sum[$2"_"$3]+=1} END {for(k in sum) print k ":" sum[k]}' ssh_password.log | sort -n -r -k 2 -t ':' | head -n 5 |
数据很有意思,也确实是我们最常用的那些用户名密码。
\n简单通过修改 ssh 端口号的方式也不是最为稳妥的方法,真正安全的做法应该是:
\n我把我的蜜罐中采到的蜂蜜分享出来,下载链接:http://developer.jpanj.com/ssh_password.log
\n大家一起来享用这份喜悦吧。
\n"},{"title":"开始刷 leetcode 了","url":"/2022/start-leetcode/","content":"我从上个月 26 号开始每天做 1-2 道 leetcode 算法题,到今天刚好一个月时间。当时在 github 上建了个私有仓库,今天也公开了,其中一个目的也是为了每天督促自己。之前不想公开的原因是担心后边真的有面试的时候面试官认为我是个刷题选手,但是 whatever 无所谓了,反正短期内也没打算换工作。
https://github.com/Panmax/go-leetcode
\n我从毕业刚工作开始面试就没有在刷算法题上下过功夫,觉得意义不是很大,到现在也是这么认为。
\n这次开始做题的目的是考虑到人过了 30,记忆力和思维力都不如年轻时候了,所以需要一些刻意练习,在这里记录一下做算法题的过程。每天写写题倒不是为了去面试,而是为了保持思维的敏捷,语法的熟练,以及对算法的理解。
\n做题的另一个契机是公司每隔一段时间会抽一些人去写三道算法题,评估下公司研发人员的平均水平,这给了我一个开始的提示。最近刚好在读一本书:《福格行为模型》,里边提到开始一个行为需要是三个要素:动机+能力+触发器。我的触发器是公司的水平测验,动机是提升自己思维敏感度,能力方面自己还是不错的,所以最终促成了这个行为的实施。
\n我不是每天疯狂的做新题,毕竟不是为了突击面试,而是每天做 1-2 道新题(根据题的难易程度而定),回顾 3-4 道之前的题目。所以在每个目录下可以看到一个 main.go
文件是我第一次做这道题的代码,其他 review_<日期>.go
文件是我复习的代码。
比如 206-reverse-linked-list
目录,意思是 leetcode 里的第 206 道题,我在 8 月 2 号、8 月 5 号、8 月 15 号复习过。
. |
当天该复习哪道题是我通过 Anki 来记录的,Anki 根据我对每道题的掌握程度会在不同的时间点提醒我复习。
\n做题的顺序是找了一个 leetcode 组合好的题库,目前做的是这个库: https://leetcode.cn/problem-list/2cktkvj/
\n我每做一道题就在 Anki 里新增一道,同时给这个题标记不同的颜色,绿色为 easy、橙色为 middle、红色为 hard。
\n\n做完一道题后我会给这道题一个主观的难易评价,Anki 会决定我下一次的复习时机。
\n\n尝试了一个月,每天抽出来一小时写写题,目前已经转起来了,我相信自己可以一直做下去。
\n"},{"title":"静态网站也很好玩","url":"/2022/static-site-very-fun/","content":"最近几天用 Cloudflare 的 Pages 部署了几个纯静态的网站,也就是完全不需要后端,只需要 HTML+JS+CSS 驱动的网站,发现静态站也很强大,再配上十分方便的 Cloudflare 域名托管,秒级搭建起一个网站并关联上自己的域名,同时支持 HTTPS 访问。
\n匿名发布内容,不需要后端存储内容,而是把内容编码到 URL 上,比如打开这个链接 就可以看到我写的这段话了。
\n\n还可以给字体改颜色,加超链接、插入图片等富文本功能。
\n一个制作手绘图的网站,功能很强大,自己部署的纯静态版本除了不支持多人协作,其他功能都是完整的。
\n不支持登录,你绘制的内容保存在你的浏览器里,只要不清浏览器内容,下次访问内容不会丢失。
\n\n给一个你常用、好记的密码,并填写一个区分场景,这个工具会帮你转成一个强度很高的密码,每次你在输入密码时可以来这里转换出你的密码。
\n比如下图中,我生成了一个用于 QQ 登录的密码,下次登录 QQ 时来这里查我的密码是什么就可以了。密码是纯前端生成的,没有任何后端逻辑。
\n\nddia 那本书中文翻译的开源版本,没什么用,放在自己域名下就是好玩🤗。
\n\n最后我索性把自己当前这个博客也托管到了 Cloudflare 的 Pages,之前是把静态页放在一个海外的服务器上,前边用 Cloudflare 的 DNS 做一次代理,现在一身轻松了。
\n以上这些我都是让 Cloudflare Pages 关联了我 GitHub 上的 Repo,当那些 Repo 有更新这些网站也会跟着自动更新,对应的 Repo 分别是:
\n五月初的时候读了一本短篇小说集《暴雨下载病房里》,作者叫苏方,之前并没有读过她的其他书,这本书是在「小宇宙」中一档叫「文化有限」的节目听到的,感觉苏方的写作风格和王烁的有点像,都带着一股子痞劲。
\n这本书作者写于疫情期间,有些内容我们很有感触。不过我这里要节选的不是那些隔离的情结,而是一段情话,这段话我当时反复多了好几遍,作者一定是为心上人写过这样的话或者收到过这样的情书才能写出这样的句子吧。
\n下文摘自《暴雨下载病房里》的「十三封情书」一节:
\n我当然爱你,像白纸爱笔尖一样爱你,像空旷爱拥挤一样爱你,像海浪爱山崖一样爱你,像母亲爱她未来的孩子一样爱你。一只喜鹊哗啦啦飞来,站定在窗边,长尾巴一扫,又一扫,比往日多了神气,像质问:人呢?人到哪里去了?
\n我在家里,我一直在家里,可是我越来越不见了。我记得旧我,旧我自私多疑,虚伪虚荣,贪恋过去,贪图将来,独不把目前放在眼里。可原来只有目前,是我们不断在失去。
\n请你原谅,我爱你,这没什么了不起,你笑一笑吧。 只要是爱,就没什么不同,爱不争夺,爱也不等待,爱不抱希望,爱本来就是希望。
\n所以我不得不写,不得不记,我写下来只为告诉自己,却万不能告诉你。这爱是我的,这勇气和力量却来自你,我在你中看到我,已有无限的无限的感激。远远地有人拨琴,远远地有人哼唱,这就是伙伴,这就是人生的意义。
\n愿你们好。
\n愿这酒后呓语永不为人知晓。
\n"},{"title":"大p故事会002:关于 https 那些事","url":"/2019/story-https/","content":"大p在刚刚开始追求小h的时候,正值风华正茂,所以比较喜欢写写书信什么的来交流彼此。由于大p的脸皮比较薄,每次写完信后,并不是把信直接交给小h,而且大部分传信的时间是在上课的时候,所以大p会把信交个离她比较近的一个同学,再由这个同学交给离她更近的同学,最后一步步的传到小h手中,小h写完回信后再使用这个过程传过来。
\n\n使用这种方式虽然浪漫,但有几个问题让大p和小h很困扰:
\n\n\n以上三中情况对应的是 HTTP 协议传输时存在的风险:
\n
窃听风险:第三方节点可以获知通信内容
篡改风险:第三方节点可以修改通信内容
冒充风险:第三方节点可以冒充他人身份参与通信
大p想到了一个对策,把信放在一个带有密码锁的盒子里,这样就由之前的直接传递小纸条改为了传递有密码锁的盒子。示意图如下:
\n\n现在对策有了,带密码锁的盒子有了,信也写好了,但另一个问题来了,密码怎么告诉小h呢,最简单的办法肯定是大p直接把密码告诉小h,但他们两个平时单独约会的时间非常少,每次独处时大p都把心思用在其他地方了,把给密码这件事忘的一干二净。另一个方法是把写有密码的信还通过之前的方法传给小h,但是这样的话中间那个传信的同学就有可能打开信看到密码,把信放在密码盒中就没有任何意义了。
\n因为锁子的密码安全传递问题解决不了,大p暂时否定了这个方案。
\n\n\n以上介绍的是对称加密算法,带有密码锁的盒子和密码分别对应的算法和密钥,常见的对称加密算法有AES、DES、3DES、RC5、RC6
\n
因为担心信中的内容被其他人看到,两个人之间的信件交流就越来越少了,这让大p很苦恼。有一天大p在学校小卖部里看到了一种新的密码锁,这种密码锁神奇之处在于它需要配合一对密码来使用,由一个密码锁上的锁头必须由另一个密码才能解开,反之亦然。
\n大p立刻来了精神,买了把这样的锁回去,并且生成了两对密码。大p和小h协商好,给每对密码中的每个密码分别起个名字:公钥、私钥,公钥表示这个密码能够随意分发,让任何人得到:可以直接把写有公钥的纸条传给对方,甚至把公钥直接写在黑板上都没有问题,但是私钥只能自己知道,甚至连对方都不能告诉。
\n也就是说此时他们两个每人有一个属于自己的私钥。这样只需要每次写完信后,用对方的的公钥把盒子锁上,对方拿到后用自己的私钥解开盒子取出信件,写完回信后再用另一方的公钥锁上盒子即可。
\n假如
\n\n\n以上介绍的是非对称加密也叫公钥加密,这套密码算法包含配对的密钥对,分为加密密钥和解密密钥。发送者用加密密钥进行加密,接收者用解密密钥进行解密。加密密钥是公开的,任何人都可以获取,因此加密密钥又称为公钥(public key),解密密钥不能公开,只能自己使用,因此它又称为私钥(private key),常见的公钥加密算法有 RSA
\n
于是两个人又开始频繁的给对方写信了,但是在慢慢使用中,他们两个都人发现了一个问题,这个密码锁的加密和解密的效率很低,简直就是写信5分钟,加/解密2小时。
\n后来,大p想到一个方法,他们可以结合两种密码锁,先通过非对称密码锁把之前对称密码锁的密码传给对方,两个人后边直接用对称密码锁来加密解密就可以了。
\n为了后边不再出什么漏洞,大p决定对这种方案进行了严格的推敲,推敲过程中他突然意识到一种可怕的情况,虽然他们两个都持有对方的公钥,但他们自己并不知道自己拿到的是不是真的就是对方的公钥,假如中间传信的人里有一个既邪恶又聪明的同学小x,他可能就会想到一种破解方法:
\n当大p和小h想要获取对方的公钥时,小x拿到大p的公钥abc后记下来,但是小x却告诉小h:大p的公钥是 xyz(这是小x的公钥),反过来也是,小h的公钥也被小x拿到并且掉了包,打p拿到的也是小x生成的公钥456。
\n当大p写完信后用他认为是小h的公钥加密时,实际用的是小x的公钥,小x只需拿到加密的信后用自己的私钥解开看一看,可能再改改信的内容,然后再用小h的公钥把信加密后交给小h,反过来同理。
\n\n因为大p是个阴谋论者,所以他相信这样的事情一定是存在的,所以之前所有的加密方案瞬间都因为这种可能有中间人攻击的存在而崩塌了。
\n由于现在市面上的密码锁只有这两种,而且大p还在读高中,所以造一种可以防中间人攻击的新型加密锁对他来说难度太大了(真实情况是大p毕业参加工作后依然造不出来),大p决定找到一种方案可以让他和小h拿到的一定是对方的公钥,而不是中间人的。大p想到,既然我们可能会收到中间人的攻击,那么我们能不能也找个可靠的「中间人」来解决这个问题呢。
\n找班长来做这个「中间人」最合适不过了,为了防止再出其他幺蛾子,大p和班长进行了面基,班长也有一对自己的密钥,大p让班长当面把公钥给了大p,此时大p可以确定他拿到的班长的公钥一定就是班长的公钥(这个实际是根证书预装进操作系统或浏览器的过程)。
\n小h为了让大p(也包括其他追求者)拿到不被篡改的公钥,需要把自己的公钥交给班长,她虽然和班长没有进行面交,但班长经过一系列严格而且复杂的检查确认了这个公钥确确实实是小h的,然后班长会把小h的基本信息(比如姓名、学号)和小h的公钥放在一起,然后对以上内容做一次散列计算后得到一个信息摘要(也叫指纹),这个指纹可以保证只要班长拿到的小h的基本信息或小h的公钥有任何修改,再次散列计算后得到的指纹一定不同。
\n\n\n消息摘要(message digest)函数是一种用于判断数据完整性的算法,也称为散列函数或哈希函数,函数返回的值叫散列值,散列值又称为消息摘要或者指纹(fingerprint)。这种算法是一个不可逆的算法,因此你没法通过消息摘要反向推倒出消息是什么,所以它也称为单向散列函数。常用的散列算法有MD5、SHA。
\n
再之后班长使用自己的私钥把之前计算出来的信息摘要进行了签名(实际就是用私钥对这个值进行了加密),加密后的值我们叫它数字签名,最后把数字签名和原始信息一起打包,生成了最终的数字证书,也就是说数字证书中有两块内容,一块是小h的公钥+小h基本信息组成的明文,另外一块是把明文部分进行散列计算后的值再次通过私钥加密后得到的数字签名。
\n\n之后班长把带有自己签名的证书(数字证书)交给了小h。大p找小h索要公钥时,小h只需要把这个数字证书交给他就行了,大p需要用相同的散列算法将明文部分进行计算得到一个散列值a,并且因为大p确定自己手中拿的班长的公钥是可信的,于是大p用班长的公钥对证书中的数字签名进行解密得到得到班长计算出的散列值b,散列值a和散列值b进行比对,如果相同就可以确定明文部分是没有被篡改过的,也就是说此时大p可以相信自己拿到的小h的公钥一定是可靠的了。
\n这也印证了一句名言:“一切计算机问题都可以通过添加中间层解决”。
\n\n\n上边的部分班长就是认证机构(CA),CA 把用户的姓名、组织、邮箱地址等个人信息收集起来,加上公钥,由 CA 提供数字签名生成公钥证书(Public-Key Certificate)PKC
\n
至此,一套比较完善的数据传输方案就完成了。HTTPS(SSL/TLS)就是在这样一套流程基础之上建立起来的。
\nhttps 简化流程图如下:
\n\n","tags":["https"]},{"title":"大p故事会001:同步、异步、阻塞、非阻塞那些事","url":"/2019/story-sync-async-blocking/","content":"高中生大p就读于一所很一般的学校,大p所在的班里有一个来自s市的女孩小h。
\n小h娇小活泼可爱而且很个性,很受同学们的欢迎,小h不知道的是大p也已经关注她很久了,虽然两个人都是寄宿生,但他们两个都偷偷带了手机到学校来,大p从其他同学那里问来了小h的手机号码,大p鼓起勇气拨通了小h的电话,还没等小h接听,大p就退缩了,挂了电话。那时的手机对学生来说还很新鲜,小h 看到这个未接电话,不知道是谁打来的,处于好玩就给这个陌生号码回了过来,但大p 却没有勇气接听,任由手机振动着。当时很流行彩铃,大p 也不例外地设置了彩铃,用的是张震岳的《思念是一种病》,这首歌刚好是当时小h最喜欢的音乐,于是后来小h在想听这首歌的时候就会给这个号码打电话。
\n高二那年下了一场暴雪,导致全市所有设施瘫痪,于是学校放假了,刚好那天学校期中考试,也是在这一天大p告诉了小h那个手机号是他的。晚上回家后大p假装很不在乎地发短信问了小h这次考的怎么样,小h很客套的应付了几句,但大p这天异常兴奋,他隐约感受到如果再不做点什么可能就会永远错过了,于是他寻找各种话题和小h聊天,不知不觉从晚上10点聊到了早上5点…
\n这之后小h也明白了大p的用意,大p在1个月后向小h表白了,但遭到了小h的拒绝,小h告诉大p学业要紧,等高中毕业后再考虑。
\n转眼间高二下学期快要结束了,这一天小h把大p叫到一个没有人的地方,跟大p说:「我答应你之前那件事了,你也答应我,等我回来好吗?」,大p不知道她在说什么,他只听到小h说她同意了,现在的大p 不管什么事情都会答应的,后来大p才知道,小h 是要回s市读高三,然后要在s市参加高考。
\n高三,小h去了s市,他们两个每天通过手机短信的方式进行交流(当然是偷偷的),刚开始的时候,大p总是心心念地等着短信回复,每次给小h发过去短信后就茶不思饭不想题也看不进去,只能两眼直勾勾地盯着手机等着短信回来,因为手机是静音所以要想第一时间知道短信到了只能盯着看屏幕有没有亮,这样过了一段时间,大p的成绩一落千丈,因为他有太多的时间花在了等着短信回复上。
\n大p认为这样下去也不是办法于是调整了一下自己的心态,每次发完短信后不再一直盯着手机等回复了,而是用这个时间去看书、做题,每过一段时间就查看一下有没有短信过来,虽然大p的成绩慢慢爬了上来,但在这种状态下大p的心里还是需要一直惦记着手机,因为他不知道短信什么时候过来所以要时不时的去看一眼。
\n于是大p就想有没有什么办法可以让自己不用频繁去看手机,又能在第一时间知道有短信来了呢,大p想了三天三夜想到了一个主意:用手机的振动功能,打开短信的振动提醒,可以把手机放在裤兜里,每次短信来了可以立刻感受到而且不会被老师发现。大p 像平常一样给小h发了条短信,为了验证这个振动功能的有效性,大p这次还是盯着手机等回复,直到短信回来手机震了,大p确信了方案的可行性。
\n此后,大p的学习效率更高了,也可以安心的听课做题了,大p 只需要在手机振动的时候去看短信就行。大p很开心,就这样,高三的时光一闪而过,他们两个约定好要考同一所大学。
\n预知后事如何,请听下回分解。
\n\n\n阻塞非阻塞都是相对于大p来说的,取决于大p等待短信时的状态。全心投入等短信达到的大p是阻塞的,可以抽出时间来做其他事情的大p是非阻塞的。
\n
\n\n","tags":["同步","异步","阻塞","非阻塞"]},{"title":"Go 结构体方法值传递与引用传递区别","url":"/2021/struct-method-value-vs-pointer/","content":"而同步和异步是对手机来说的,同步需要让大p自己去检查有没有新短信达到,而异步(也就是手机开启振动模式)可以主动告诉大p有短信来了。
\n
Go 结构体方法中,有一个很重要的点就是值传递和引用传递,我们通过一个例子来看下什么是值传递、什么是引用传递,二者有什么区别。
\n我们声明 person
结构体,里边有一个 name
字段,用两种方式实现 SetName
方法,分别是值传递和引用传递。
type person struct { |
如上,SetName1
就是值传递,SetName2
为引用传递。
在 main
方法中,我们分别调用两个 SetName
方法对 name
进行赋值,并打印每次赋值后的 name
值。
func main() { |
执行这个程序得到如下结果:
\n
|
可以看到 张三
并没有打印出来,而 李四
打印了出来。
在调用 SetName1
时,实际上是复制了一个新的 person
,方法内操作的也是那个新 person
把复制 person
的 name
改为了张三,而我们在 main 方法中打印的确是原始 person
的 name
字段。因为我们在初始化 person
时并没有指定 name
的值,所以第一次打印出来的是个空串。
对程序稍作调整,先调用 SetName2
再调用 SetName1
:
func main() { |
这时输出的结果为:
\n李四 |
第一次调用后,p
结构体指针中 name
的值已经被改为了 李四
,接下来我们调用 SetName1
时,因为是复制了一个新的 person
,并没有影响之前的 person
,所以打印结果还是 李四
。
**我们日常开发中,编写结构体方法时大部分情况都是用引用传递。
\n值传递的问题是,如果我们结构体的成员数量非常多时,每次调用方法都会进行一次拷贝,会有额外的内存开销。
\n验证一下值传递有没有分配新的内存:
\ntype person struct{ |
运行结果:
\nOrigin: 0xc000010200 |
可以看到,原始的 person
地址为 0xc000010200,在通过值传递时位置发生了改变,变为了0xc000010210,这也就意味着系统为这个新的 person
分配了新的内存地址,而用引用传递的方式地址是不会变的。
我们假设要实现一个发送邮件的功能,定义一个 email
结构体,里边有两个成员 from
和 to
,实现两个方法用来更新这两个成员变量。
type email struct { |
再实现一个发送邮件的方法,这里简单将 from
和 to
打印出来即可:
func (e *email) Send() { |
在 main
方法中,我们写一个循环,实现发送 10 次邮件,0 发送给 1,1 发送个 2,一次以此类推:
func main() { |
输出如下:
\nfrom: 0, to: 1 |
这时候,如果我们改成并发发送这些邮件,同时发给10个人,很容易就会把上边的代码改写如下:
\nfunc main() { |
再次运行,结果如下:
\nfrom: 2, to: 3 |
没有按照递增的顺序发送是在我们意料之中的,但是我们可以看到其中有一行输出为:from: 9, to: 1
,这个并不是我们想要的结果。
出现这个问题的原因是在每个 go routine
中都是对原始 email
进行的修改,再并发操作的过程中,from
和 to
有可能被其他的 go routine
改掉,这是个非常严重的 bug。
func main() { |
package main |
读者可以自己想一想,为什么这种写法可以解决并发赋值出现的问题。
\n"},{"title":"sublime 实现多处文本替换","url":"/2022/sublime-replace-multi/","content":"最近接到一个任务,配合 DBA 进行数据库升级,其实也没有多大难度,就是将依赖这个库的服务配置文件中的 URI、端口号替换下就好了,但我们这个库由于历史原因依赖的服务非常多,有 30 多个,而且每个服务的配置文件可能都有些许差别,还有就是有可能采用了不同的连接池,每个配置文件中即便是同一个字段也可能会声明在多处,修改时要处理多处。
\n前期我使用人肉查找替换的方式来做这件事,但由于要做两轮上线,第一轮是先将配置文件中的从库修改后上线,第二轮再修改主库配置。
\n由于工作太机械化,所以我准备发挥程序员的最大美德:懒惰。
\n先来分析下任务,其实就是把文本中命中的多个字段进行替换,如:
\nmaster.db.com
替换为 new.master.db.com
username
替换为 new_username
password
替换为 new_password
普通的文本编辑器只支持单个字段的替换,上边这种替换多个的情况需要人工手动进行多次操作。
\n我在一开始准备写个 Python 脚本,把每个服务的配置文件复制下来保存成文件,然后用脚本遍历这些文件,将里边的内容替换掉。
\n分析后觉得有些用杀鸡用宰牛刀,不能拿着锤子找钉子,于是就想探索下 sublime 中有没有类似的插件可以实现这个需求。于是就找到了这个 RegReplace 插件。
\n下面我记录下我使用这个插件的过程:
\n\n\n这里避免数据敏感,我用个其他例子做为演示:将一个文档中的
\n<p>
和</p>
替换为<h1>
和</h1>
。
command+shift+p
,输入 install
从列表中搜索 RegReplace 回车安装就可以了。
编辑上边打开的配置文件,添加以下配置:
\n{ |
估计一眼就能看明白,replace_opening_ps
将 <p>
替换为 <h1>
,replace_closing_ps
将 </p>
替换为 </h1>
。
填入:
\n[ |
也是一看就懂,这里不多解释了。
\n<p>hello,world</p> |
command+shift+p
输入 replace ps
定位到我们配置的命令上,回车即可完成多处替换工作:替换后的效果如下:
\n<h1>hello,world</h1> |
现在时间凌晨 5.35 分,先写到这,准备去切换主库了。
\n"},{"title":"讨论成功与失败","url":"/2023/success-failure/","content":"关于成功和失败,有我见过两派说法,一派认为成功的经历很重要,另一派认为失败的经历很重要。
\n认为成功的经历很重要的人们这样说:对那些仅仅满足不失败的人来讲,失败的教训可以让他们避免犯同样的错误;但是对于想成功的人而言,失败的教训远没有成功的经验重要。一个经常失败的人会习惯性失败,相反,成功才是成功之母。 虽然人很难做一件事情就成功一件,但总该尽量避免失败,这样才能少受挫折。
\n认为失败的经历很重要的人们这样说:学会失败,从失败中学习,要想进步就必须学会失败。把失败看成成长的工具,历史上最成功的艺术家、科学家,也是失败最多的,经历过最多的失败。
\n我们不要有二极管思维,上述两种观点乍一看似乎相互矛盾,但实际上它们各有各的道理。它们从不同的角度阐述了成功和失败的经验。
\n认为成功更重要,是在强调成功的经验比失败的经验更重要,只有不断积小胜才能获大胜。成功会让一个人越来越自信,而失败会让一个人越来越对失败免疫(有点类似于习得性无助)。
\n认为失败更重要,强调的是不要把失败看作是过不去的坎,因为失败无法避免,真正来自失败的痛苦远小于我们的想象。面对失败是磨练心性和个人成长的绝佳机会。
\n一个强调成功的经验很宝贵,一个强调失败的痛苦并不可怕。
\n许多讲述失败重要性的故事都会以爱迪生为例,说他在找到最适合电灯泡的灯丝前失败了5000多次实验。但他们没有说的是,爱迪生在1879年发明出灯泡前已经有过多次成功发明经历,例如1868年的投票计数器、1870年的印刷机、1877年的留声机等等。正是因为有这么多次成功经验才给了爱迪生很大的信心。我相信,爱迪生在进行电灯泡实验时,更多的经验一定来自之前成功发明的经历。
\n成本和机会这两个因素也极其重要,穷人家的孩子失败一次就再没有重试的机会了,而富人家的孩子失败了还可以重头再来,他们有可以试错的资本。
\n在初始成功和失败率相同的情况下,富人家的孩子有更多的重试机会。随着重试次数增多,成功次数也随之增加,成功经验也会积累得更多,这样就越不容易失败。很多企业家的儿子就是很好的例子,比如王思聪。
\n作为普通人,我们既无法避免失败,也没有太多试错成本,我们能做些什么?
\n成功的经验稀缺不可多得,失败的经历也不可或缺。
\n"},{"title":"在 Mac 上 使用 Surge 做旁路由的周边搭配","url":"/2022/surge-soft-router-perimeter/","content":"这段时间在家办公,家里的网络无法进行科学上网,不过我的所有设备上都装有 Surge,所以科学上网这件事对我倒是没太大影响,但有一个不方便的点是在需要访问公司内网的一些资源时(比如 Gitlab、有敏感数据的后台),需要先连接上 EasyConnect(即 VPN) 才可以访问。我平时使用 Surge 习惯开启「增强模式」,这样 Surge 可以接管我的全部网络,就不用再在一些软件中单独配置代理了,比如 iTerm、GoLand、Telegram。不过 EasyConnect 和 Surge 的「增强模式」有冲突,在开启「增强模式」时是无法使用 EasyConnect 的,每次都要先停用「增强模式」才行。具体冲突的原因在原理上我不是很清楚,我猜测是因为它们两个都是要接管所有网卡流量导致的。
\n为了解决这个问题,也为了让家里所有设备都能实现无感知地科学上网,同时还可以做一些广告屏蔽和隐私保护,我准备使用我那台早已配淘汰了的 15 年 13 寸 Mac 做一个软路由。我所参考的教程是:https://qust.me/post/MacSurgeRouter/ ,博主还非常贴心的录制了视频:https://www.youtube.com/watch?v=68lcT7ItyP4 。不管是文章还是视频,都将如何配置软路由介绍的非常详细了,我在本文中补充几点配置好后我们还可以做的那些辅助工作。
\n我一直使用 Amphetamine 这个小工具来让我的电脑在我需要的时候保持不休眠状态。
\n如果我们需要合盖后继续保持让设备不休眠,需要取消掉「当显示器关闭时允许系统休眠」的选项。
\n\n在取消这个选项时,Amphetamine 会提醒我们安装一个增强工具(Amphetamine EnHancer),用来保护我们的电脑,这里我也建议安装,安装后需要将增强工具内置的两个组件也要安装上才算启用成功。
\n\n由于我的 13 寸 Mac 和公司配的 15 寸 Mac 电源适配器相同,所以我经常会让两个设备使用同一个电源,哪个没电了充哪个——我日常都是用自己的M1 Pro 所以不会太频繁给公司电脑充电,用一个电源足够。由于作为软路由的 13 寸 Mac 长时间处于合盖状态,我不知道它的剩余电量,有一次充 15 寸 Mac 后忘了充回去,导致晚上设备因没电关机了。我一开始只是发现手机无法连上 WIFI,以为是信号弱,但是到路由器旁边依然连不上,后来才想到是电脑关机了。因为电脑接管了 DHCP 服务,手机分配不到 IP 自然无法连上。
\n为了避免这种情况,同时也为了避免不小心报错网线,我简单写了一个监控脚本来监控设备的状态。
\n脚本实现功能如下:
\n可以看出这个监控需要两组脚本,一个是部署在 Lambda 上的,用来判断设备网络情况,另一个是客户端本地用来 ping 服务和监控电量。
\ndef mac_health(event, context): |
serverless.yml:
\nfunctions: |
这里我不再介绍 Lambda 如何使用,可以参考我之前的其他文章:
\n上边代码我做个补充说明:
\n/tmp
目录来临时存储这个数据,AWS 为每个 Lambda 提供 500MB /tmp
下的存储空间。?update=1
这个参数,这样就可以把最新时间记录下来,而系统通过 cronjob 调用自己的时候不记录时间,只进行时间差判断。import os |
逻辑也比较简单,这里是通过一个系统 shell
调用来获取的电量。
电脑像这样长期插着电源并保持开机状态对电池的损耗非常大,为了避免对电池损耗过快,建议将最大充电量设置为 60%-80% 之间,我们可以借助 bclm 这个小工具实现,工具名是 Battery Charge Level Max 的缩写。安装方式参考官方文档,我自己是将二进制包下下来放到 bin(/usr/local/bin) 目录来直接使用的。
\nbclm 提供命令非常简单:
\n获取当前电池的最大充电量 |
当前我的剩余电量是 84%,通过其他监控工具可以看到即使我插上充电器,也不会进入充电状态:
\n\n(电池已经要不行了 👋🏻)
\n软路由用了两周多了,给我最明显的体验是,电视上之前每次开机都要等待的 60 秒广告没有了;因为在我的工作 Mac 上已经不用再运行 Surge,在连公司网络 VPN 时也不用先去关闭 Surge 的 「增强模式」了。
\n也存在不方便的地方,我在外边使用手机,通过流量上网的时候要手动打开 Surge,而到家连上 WIFI 后又要手动关闭,如果忘记操作就会有打不开网站的情况。不知道能否通过快捷指令将个操作自动化,我目前还没有找到自动化解决方案。
\n"},{"title":"再论《阻塞、非阻塞 I/O 与同步、异步 I/O》","url":"/2020/sync-async-blocking/","content":"去年的时候以一篇比较尬的故事(同步、异步、阻塞、非阻塞那些事)的形式介绍了一下阻塞、非阻塞 I/O 与同步、异步 I/O的区别和联系,这次重新把知识点总结一下,这篇只留下干货,湿货继续看那篇故事。
\n根据应用程序是否阻塞自身运行,可以把 I/O 分为阻塞 I/O 和非阻塞 I/O。
\n根据 I/O 响应的通知方式的不同,可以把文件 I/O 分为同步 I/O 和异步 I/O。
\n阻塞 / 非阻塞和同步 / 异步,其实就是两个不同角度的 I/O 划分方式。它们描述的对象也不同:
\n比如在 Linux I/O 调用中:
\nread
是同步读,所以,在没有得到磁盘数据前,read
不会响应应用程序。aio_read
是异步读,系统收到 AIO
读请求后不等处理就返回了,而具体的 read
结果,再通过回调异步通知应用程序。再如,在网络套接字的接口中:
\nsend()
直接向套接字发送数据时,如果套接字没有设置 O_NONBLOCK
标识,那么 send()
操作就会一直阻塞,当前线程也没法去做其他事情。epoll
,系统会告诉你这个套接字的状态,那就可以用非阻塞的方式使用。当这个套接字不可写的时候,你可以去做其他事情,比如读写其他套接字。\n\n\n“计算机科学只存在两个难题:缓存失效和命名。” ——Phil KarIton
\n
缓存的主要目的是通过减少对底层慢速存储层的访问,提高数据的检索性能,以空间换取时间,缓存通常是临时存储一个数据的子集,而数据库中的数据通常是完整且持久的。
\n缓存利用了「最近被请求的数据很可能再次被请求」的原则。
\n与计算机的内存类似,缓存是一种紧凑的、高性能内存,它以层的方式存储数据,从第一层开始,依次递进,这些层被标记为 L1、L2、L3……,依此类推。在需要时,缓存可以写入数据,比如在更新场景下,新的内容需要写入到缓存中替换掉旧内容。
\n无论读缓存还是写缓存,都是一次执行一个块。每个块都有一个标签,这个标签表示数据在缓存中的存储位置。当从缓存中请求数据时,会通过标签进行搜索,首先在第一层(L1)内存中搜索,如果没有找到,就会在拥有更多数据的 L2 中进行搜索。如果在 L2 中也没有找到数据,就继续在 L3 搜索,然后是 L4,以此类推,直到找到数据为止,然后读取并加载数据。如果在缓存中没有找到数据,那么就把它写进缓存中,以便下次快速检索。
\n「缓存命中」描述的是内容成功从缓存中找到的情况。
\n标签在内存中快速查询,当数据被找到并成功读取时,我们称之为「缓存命中」。
\n缓存命中还可以区分为冷、温、热,不同情况表示不同的数据读取速度。
\n「热缓存」是指以最快的速度从内存中读取数据的情况,发生在数据从 L1 检索的时候。
\n「冷缓存」是以最慢的速度读取数据,尽管如此,它仍然是成功从缓存中读出的(数据只是在内存层次中的较低位置被发现,比如在 L3,或者更低的位置),所以仍然被认为是一次缓存命中。
\n「温缓存」是用来描述在 L2 或 L3 找到数据的情况。温缓存没有热缓存那么快,但要比冷缓存快。一般来说,称一个缓存为温缓存是用来表达它比热缓存慢,更接近于冷缓存。
\n「缓存缺失」指的是在搜索内存时没有找到数据的情况。当这种情况发生时,内容会被转移并写入缓存。
\n「缓存失效」是一个过程,计算机系统将缓存项声明为无效,并将其删除或替换。如果数据被修改了,就应该在缓存中失效,否则会造成应用行为的不一致。
\n有三种缓存系统:
\n
数据同时被写入缓存和相应的数据库中。
优点:快速检索,缓存和存储之间的数据完全一致。
\n缺点:写操作的延迟较高。
\n直接写到数据库或永久存储,绕过缓存。
\n优点:可以减少写操作延迟。
\n缺点:增加了缓存失效。在缓存失效的情况下,缓存系统必须从数据库中读取信息。因此,在应用程序快速写入和重新读取信息的情况下,这可能导致更高的读取延迟。读取发生在较慢的后端存储中并经历较高的延迟。
\n只对缓存层进行写入,一旦写入缓存完成,就会确认写入。之后,缓存异步地将这个写入同步到数据库中。
\n有点:降低写密集应用的延迟并提高吞吐量。
\n缺点:在缓存层崩溃的情况下,存在数据丢失的风险。我们可以通过让一个以上的副本确认缓存写入成功改善这个问题。
\n以下是一些最常见的缓存淘汰策略:
\n分布式缓存是一种系统,它将多台联网计算机的随机存取存储器(RAM)集中到一个单一内存数据存储中,用作数据缓存,以提供对数据的快速访问。
\n虽然传统中的大部分缓存都在一个物理服务器或硬件组件中,但通过将多台计算机连接在一起,分布式缓存可以超越单个计算机的内存限制。
\n顾名思义,我们将有一个单一的共享缓存,所有的应用节点都访问这个缓存。当请求的数据在全局缓存中找不到时,缓存负责从底层数据存储中查找缺失的数据。
\n在现实世界中缓存有许多使用场景,如:
\n我们也来看看在哪些情况下不应该使用缓存:
\n另外需要注意的是,缓存不应该被用作永久的数据存储。大部分缓存都是在易失性内存中实现的,因为它的速度更快,因此缓存应该被认为是临时性的。
\n以下是缓存的几个优点:
\n下面是一些常用的缓存技术:
\n\n"},{"title":"日拱一卒 - 内容分发网络(CDN)","url":"/2022/system-design-cdn/","content":"内容分发网络(CDN)是一组在地理上广泛分布的服务器,它们一起工作以提供互联网内容的快速交付。通常静态文件,如 HTML/CSS/JS、照片和视频,都是由 CDN 提供的。
\n\n内容分发网络(CDN)增加了内容的可用性和冗余度,同时降低了带宽成本并提高了安全性。通过 CDN 获取内容可以显著提高性能,因为用户从靠近他们的数据中心接收内容,我们的服务器也不必为 CDN 满足的请求提供服务。
\n在 CDN 中,源服务器包含内容的原始版本,边缘服务器分布在世界各地并且数量众多。
\n为了最大限度地减少访问者与服务器之间的距离,CDN 将其内容的缓存版本存储在多个地理位置,称为边缘位置。每个边缘位置包含一些缓存服务器,负责向其附近的访问者提供内容。
\n一旦静态资源被缓存在特定位置的所有 CDN 服务器上,之后所有网站访问者对静态资源的请求都将由这些边缘服务器提供(而不是源站),从而减少源站负载并提高可扩展性。
\n假如一名位于英国的用户请求本站,本站服务器当前托管在美国,他们将从最近的边缘位置(如伦敦)获得服务。这比让访问者向源站服务器发出完整的请求要快得多,后者会增加延迟。
\nCDN 通常分为两种类型。
\n当服务器上内容发生更改时,使用了推模式的 CDN 会收到新内容。我们完全负责提供内容,直接上传至 CDN,并重写 URL 以指向 CDN。我们可以配置内容何时过期、何时更新。内容只有在新的或改变的时候才会被上传,最大程度地减少流量,最大限度地提高存储。
\n流量小的网站或内容不经常更新的网站使用推模式效果很好。内容被放置在 CDN 上一次,而不是定期被重新拉取。
\n在拉模式的情况下,缓存是根据请求更新的。当客户端发送一个要求从 CDN 获取静态资源的请求时,如果 CDN 中没有,那么它将从源站服务器获取新的资源,并用这个新资源填充其缓存,然后将这个新的缓存资源发送给用户。
\n与推模式 CDN 相反,拉模式需要较少的维护,因为 CDN 节点上的缓存更新是基于客户端对源站服务器的请求进行的。流量大的网站使用拉模式效果很好,因为流量分散地更均匀,只有最近请求的内容留在 CDN 上。
\n以下是一些广泛使用的 CDN:
\n\n"},{"title":"关于 TCP 的 14 个问题","url":"/2021/tcp-questions/","content":"是的
\n下边是一个TCP数据包的结构
\n+-+-+-+-+-+-+-+-+-- |
这就是为什么我们称它为 「TCP/IP」– TCP数据包总是有一个 IP 头。
\n是的
\nTCP 头中的端口字段为16位。
\nSYN
\n每个TCP连接都是以三次向握手开始的:
\n不能
\n被防火墙拦截的常见症状是 SYN 数据包发出后无 ACK 返回。
\n是的
\n电子邮件是通过SMTP发送的,它使用了 TCP。
\nFTP、POP3、HTTP/1、HTTP/2、websockets 等很逗互联网协议也都使用了 TCP。
\n是的
\n一个HTTP 请求/响应 是个 TCP 连接 —— 客户端发送 HTTP 请求,然后服务器发送响应。
\n是的
\n例如,websockets 使用 TCP 使客户端和服务器根据需要来回发送数据。
\n是的
\n数据包有最大限制(通常是1500字节),所以需要将一个 TCP 消息分割成许多数据包。
\n不是
\n无法确保网络数据包会按照你发送的顺序到达。
\n通常是在操作系统中
\nLinux、Mac、Windows等都有 TCP 实现。如果你愿意,也可以编写自己的TCP实现。
\n序列号(sequence number)
\n数据包的序列号告诉你它在整个数据流中的位置。序列号计算的是字节数,而不是包数。
\n下面是一组数据包内容和序列号的例子(每个字符是1个字节):
\n+-+-+-+-+-+-+-+-+-+-+- |
如果按照正确的顺序排列,这些数据包将重新排列为“hello I’m panmax!”
\n不是
\n通常连接中的第一个序列号是一个很大的数字,比如:1737132723,其他序列号都是与这个数字相对的。所以在计算时需要减去第一个序列号。
\n如果你在 Wireshark/tcpdump 中查看TCP数据包,它们会进行减法处理,使其看起来像序列号从0开始,让人类更易阅读。
\n你会收到一个ACK
\n例如,服务器收到了一个序列号为1200的数据包,并且也已收到所有在它之前的数据包,服务器会发送一个序列号为1200的ACK数据包。
\n是的
\n如果客户端(或服务器)在一定时间内没有收到其发送数据包的ACK,它将会重试发送该数据包。
\n"},{"title":"不到 40 行代码实现 Telegram 自动发消息机器人","url":"/2019/telegram-bot-send-taily-message/","content":"\n创建一个 Telegram 机器人,定时发送消息,并部署到 AWS Lambda。
\n\n\nAWS Lambda 是一项计算服务,可使你无需预配置或管理服务器即可运行代码。
\n
AWS Lambda 可以在一定配额内免费使用,所以需要避免发送大量请求。
\nAWS Lambda 定价方案如下:
\n\n待办清单上的第一件事是创建一个机器人,遵循 Telegram 官方说明:
\n@BotFather
。/newbot
并为你的机器人指定 name
和 username
。现在机器人准备好了,开始编写代码。
\n有很多部署 Lambda 的方法,我准备使用 serverless 框架,所以我们先来安装它:
\n$ npm install serverless --global |
Serverless 的文档中提供了一些范例,还支持生成模板,像下边这样:
\n$ serverless create --template aws-python3 --path scheduled_telegram_bot |
执行这个命令后,会创建出一个 scheduled_telegram_bot
目录,并已经生成了 3 个文件:.gitignore
,serverless.yml
和 handler.py
。
serverless.yml
文件用来描述:部署什么、何时运行、如何运行。 handler.py
文件包含将要运行的代码,所以我们先来编写它。
我们将使用是一个封装好的包来调用 Telegram 的 API:python-teletram-bot,创建一个新的文件 requirements.txt
写入:
python-telegram-bot==12.2.0 |
我们需要在程序中导入这个库,不过后边我们会遇到一个问题:由于 python-telegram-bot
不是 AWS Lambda 所提供的标准库,因此我们在部署时需要同时包含这个包中的文件。
所以我们后边会在本地安装这个包的所有内容。
\npip install requirements.txt --target=. |
现在让我们来定义一个发送消息的函数,打开 handler.py
修改内容如下:
import telegram |
你需要把 CHAT_ID
修改为你想让机器人互动的群组、频道或者会话的 ID。获取 ID 的方法如下,我以频道为例:
首先创建自己的频道,将机器人拉入频道并设置为管理员,随意在频道内发送一个消息。
\n添加 GetIDsBot,并将上边发的那条消息转发给这个机器人,它会返回这个频道相关信息:
\n👤 You |
这样可以得到我所在这个频道的 ID 为 -1001156324531
。
现在我们来定义如何运行我们的代码。
\n编辑 serverless.yml
:
service: scheduled-telegarm-bot |
这里我们告诉了 AWS 我们所需要的运行环境,并且让它从我们的环境变量中获取 Telegram token,这样我们就不需要把 token 硬编码到代码中了。
\n最后我们还定义了一个定时器,声明每两分钟触发一次这个函数。当然,定时器有很多选项,通过这个文档可以了解更多配置方式,比如每小时或者每周一发送消息。
\n我们已经准备好了所有需要的东西。
\n好吧,准确来说是几乎所有的东西。我们还需要获取 AWS 的凭证,然后和 token 一样,在部署前设置为环境变量,获取凭证步骤如下:
\n通过 AWS 的控制台:
\n进入 我的安全凭证 - 用户 - 添加用户
\n\n设置用户名并选择编程访问
\n\n下一步:选择直接附加现有策略 - AdministratorAccess
\n\n下一步会来到添加标签页,直接点击下一步,确认信息无误后点击 创建用户,将 访问密钥 ID 和 私有访问密钥 拷贝并存放起来。
\n现在,让我们把 AWS 凭证和 Telegram token 导出到环境变量。打开终端,输入:
\n$ export AWS_ACCESS_KEY_ID=[your key goes here] |
在本地安装 Python 的依赖包(这也是 AWS Lambda 所需要的):
\n$ pip3 install -r requirements.txt -t . |
最后将所有的东西部署到 AWS:
\n$ serverless deploy |
如果前边配置没有问题,会看到如下输出:
\nServerless: Packaging service... |
完成!机器人会在每 2 分钟给我们发送一次消息。
\n\nAWS Python Scheduled Cron Example:https://github.com/serverless/examples/tree/master/aws-python-scheduled-cron
\n"},{"title":"诱惑本身是隐藏","url":"/2023/temptation-is-hidden/","content":"问各位男同胞一个问题,你们觉得裸女更有吸引力,还是穿着半遮半掩的衣服时更有吸引力?
\n我会选择后者,把一切看穿就没有那么大诱惑了。对方在半遮半掩的状态下会给你无尽的关于性想象,各位看过小电影的同学也一定会有同感吧。
\n红楼梦中,晴雯生病,一个新入行的太医来给她瞧病,因为古代讲究男女授受不亲,需要把幔帐放下来,只把手漏出去给大夫把脉。原文是这样写的:「有三四个老嬷嬷放下暖阁上的大红绣幔,晴雯从幔帐中单伸出手去。那太医见这只手上有两根指甲,足有二三寸长,尚有金凤花染的通红的痕迹」。年轻太医哪见过这场面,一个漂亮的手上留着长长的指甲,还用金凤花涂过颜色,最主要的是还看不到本人长什么样,这一下子把太医迷的五迷三道的,整个人脸红心跳呆在那里,后来另一个老嬷嬷拿手帕把手盖住太医才平缓下来继续治疗。
\n如果晴雯真的直接站在他的面前,大概也不会有这么大诱惑。
\n和这个场景类似的是胡君荣给尤二姐看病那一会,尤二姐本来是怀孕了,医生先通过把脉,看到这么美的女孩的手已经心神不定了,无法断定病因,后来要求再看一看脸。在古代男性根本没有机会直接看到贵族女性,都是隔着帘子,这会带来极大的诱惑。就是因为隔着一层帘子,性的幻想就会特别严重。医生在这种情况下开除的药,结果就可想而知了。
\n东方自古以来都喜欢半遮半掩、半掩半开、犹抱琵琶半遮面的朦胧美。后边尤三姐调戏贾珍和贾琏时这样写到:「尤三姐松松挽着头发,大红袄子半掩半开」。了不起就在「半掩半开」上,如果全开了就没什么意思了,那就不是红楼梦,而是金瓶梅的写法了。
\n在穿衣方面,东方也喜欢把鲜艳颜色的衣服穿在里边,比如红内裤。西方常常把艳的东西放在外面,东方常常把艳的东西放在里面。用很典雅的话来讲叫做含蓄,用比较不典雅的话叫做闷骚。
\n最后再说一点关于隐藏的阴暗面。我们这个国家就因为隐藏太多,才让人们有非常大的偷窥欲,社会应该多给大家提供面对事物真相的机会。
\n古代讲究儿媳妇要回避公公,也许就是这么严防死守、过度回避,才产生了秦可卿淫丧天香楼的悲剧。回避太多,隐藏太多,反而会产生更多诱惑。
\n"},{"title":"测试 https 页面中嵌入 http 元素","url":"/2020/test-http-in-https/","content":"\n"},{"title":"《感谢自己的不完美》摘抄","url":"/2021/thanks-myself-unperfect/","content":"一个人的心里健康程度与接纳痛苦的程度成正比。
\n改变恶习最关键的一点是:不和恶习较劲,接受恶习。因为,积习就是你的本性,恶习代表着你内心的需要,你只有理解它并接受它,它才能得到最有效的改造。
\n每一种习惯的形成都必然会经历以下这个循环:行为发生—得到奖励—强化
\n成人需要同时进行几件事情,而且必须为自己的行为负责。这个时候,成人就必须有“延迟满足”这个意识。
\n真正能自控的人是内心和谐的人,他们将自己内心的每一部分需求都当作朋友来看待,这样每一部分都不会捣乱。这样的人不是试图控制或压制一些缺点,而总能从它们当中找到正面的信息。
\n当你真正想做一件事情时,动力会从内心自动产生,你自然会自律。不要从外界去寻找迫使你改变习惯的东西,因为它们很容易被你放弃。
\n增强自控力的唯一根本在于要找到你真正爱做的事情是什么,真正想成为怎样的人,也就是要找到你的人生使命。
\n改变恶习仍需要一点:立即去做。因为,每一个旧习惯对应着的神经回路是无法消失的,只能靠新习惯打造更强大的新神经回路,用新的神经回路去战胜旧的神经回路。
\n养成新习惯的策略:
\n人生最大的痛苦莫过于知道该怎么做却没有去做,你会自责,你会对自己不满意,你会觉得自己是渺小的、不讲信誉不可信的。总而言之,就是你开始不信任自己,自信心降低了。
\n痛苦时,不要只沉浸在痛苦中,或者以寻找刺激的方式来降低或麻木自己的痛苦,而要思考一下“我为什么这么痛苦,我重复了童年的什么体验?”
\n自己的惧怕与愤怒是建立在有限的人生体验上的,是不合理的。
\n痛苦背后的问题恰恰是我们的一部分,须臾不可分离,根本逃避不了。所谓的逃避,只不过是运用种种自欺的方式扭曲了我们对问题的认识,从而减少我们的痛苦。我们以为看不到它们了,但其实它们还是我们甩不掉的尾巴。
\n潜意识的特点是,我们越想控制它,就越控制不了,它的活动会越来越频繁。
\n不要总是和潜意识过不去,不必和走神、坏念头等偶尔出现的问题较真。否则,它们就会成为真正的问题。
\n按照存在主义哲学,只要你渴望触及人类、社会乃至世界的真相,那么你会一直焦虑下去。因为,不管成长到哪一层次,你一定会发现新的局限性,这时焦虑就势必会发生。所以,许多哲人越深入这个世界,就越明白自己无知。从这一点而言,焦虑是推动我们认识世界的动力。
\n一个人在原生家庭中的关系决定了这个人的心理健康程度。
\n人生的悲剧本身并不一定会导致心理问题,它之所以最后令我们陷入困境,是因为我们想否认自己人生的悲剧性。
\n我们的力量不在于我们看上去有多快乐,而在于我们的心离我们的人生真相有多近。
\n作为一个人,我们必须深入地探讨自己经历过的所有事件以及教训,只有在这个深度上我们才能发现我们自己的真实,找到自己的决策能力。
\n一个总是不断诞生强人的社会,必然是一个失序与窒息不断轮回的社会。
\n对世界而言,控制欲望是万恶之源。对个人而言,控制欲望是万病之源。
\n强人们其实首先想控制自己内心的失序,但他们做不到,于是他们去追求控制别人。他们内心越失序,就越渴望控制更多的人。最终,不管他们意识上的目的是什么,制造的或留下的多是苦难。
\n\n\n想到了希特勒
\n
不管一份体验带给我多大的痛苦,只要不作任何抵抗地沉到这份痛苦中,体会它、看着它,那么它最多半个小时后就会融解并转化。
\n看心理医生,随着安全感和信任感的增加,患者一些更深层的痛苦反而会映现出来,于是会体会到平时生活中都体会不到的痛苦。
\n任何一次袭来的痛苦,不管多么难过,只要你沉入其中体会它觉察它,那么最多半个小时就会融解并转化,有时会以喜悦结束,有时会以平静结束。
\n当你非要压制自己的悲伤,并相反表现出极大的快乐时,你最终收获的,会是更大的悲伤。
\n我们都在寻求价值感,如果童年时,某一种方式令我们找到了价值感,此后我们便会执着在这个方式上。并且,这世界上的大多数人一般只找到了一套寻求价值感的方式,越困难的时候,我们会越执着于这一套方式,认为这是唯一的,但其实在最困难的时候,改变或调整这一方式会更好。
\n你想让一个人对你好,就请他帮你一个忙。这个办法之所以更好,是因为我们都很自恋。多数时候,我们看似爱的是别人,其实爱的是自己在这个人身上的付出。
\n每一次挫折事件都是一次机遇,因为它暴露了自己的缺点和弱点。进行自我归因的人会借此完善自己。这样一来,挫折就成了人生的一种财富。
\n好的愤怒,针对的必须是导致你愤怒的那个人。你对这个人愤怒,你才能捍卫自己的空间,并且愤怒的表达才会有效果。如果这个人惹了你,你不敢对他愤怒,你跑去把愤怒发泄到其他人身上。那么,你发泄得再厉害都没用,因为对象选错了,那样愤怒就没有任何意义。
\n治疗痛苦的唯一办法就是直面并接受人生悲剧。
\n抑郁症常源自两个原因:一是重大的丧失;二是压抑的愤怒。
\n失去发生时的第一时间所产生的悲伤与泪水,是有治疗效果的,只要悲伤能在我们身体上自然流动,这份疗愈就会自然产生。
\n爱的关系中,付出和接受的循环被破坏,很多时候不是因为不愿意给予,而是因为不愿意接受。
\n假如你没有一点儿负罪感,而只有清白感,那其实就是你把负罪感强加给其他人了,而那个被强加者一般都是你最亲密的人。
\n从情感上看,单纯的“付出者”其实并不伟大,他们不计得失的付出,从根本上是一种自恋。
\n“付出者”其实在享受这种逻辑:既然我是付出的一方,那么我们的关系无论出现什么问题都是你的错了。
\n最好的关系是彼此慷慨地付出和坦然地接受,通过这种交换,双方的接受和付出达成了一种平衡,且彼此都感到自己在这个关系中富有价值。
\n好人,势必有一个特点——牺牲自己的需要。坏人,势必有一个特点——纵容自己的需要。
\n关系有两种,一种是我与你,一种是我与它。
\n我们在与别人交往时,多数时候不过是在重复小时候我们与父母等亲人打交道的方式而已。
\n童年得到的爱越多,一个人就越是难追。这样的人会相信自己的感觉,凭感觉去找到适合自己的人。如果他觉得你是他想要的,那他可能很快接纳你;如果不是,那么可能无论你怎么努力,都是没有用的。
相对而言,童年得到的爱越少,一个人就越容易追。只要你对他很好,他就很容易感动,而暂时接纳你。但是,他是一开始容易追到,而以后会很难相处,因为他会过于敏感。
作为人类一种最基本的情绪,恐惧和其他情绪一样,也有着它的独特价值,而一味地追求战胜恐惧,就忽略了恐惧所传递的重要信息。
\n我们越恐惧一件事情,那件事情背后隐藏着的信息可能就越重要。
\n恐慌的背后,常藏着我们生命中重要的答案;恐慌程度越高,答案就越重要。
\n关系匮乏所带来的恐惧,在相当的程度上可以说是源自对死亡的恐惧。
\n我们的人格也源自我们与父母的关系,父母和我们的原生关系,最终被我们内化为“内在的父母”和“内在的小孩”。
\n当我们想与死去的亲人同甘共苦的时候,我们忽视了很重要的一点:死去的亲人不希望我们这样做。
\n我们很容易只沉浸在自己的痛苦自己的幻想中,自以为死去的亲人希望我们怎么样,却忘记了他们对我们真切的叮嘱。如果真是这样,那才是对爱的误解。
\n追求优秀不是克服自卑的良药,特别自控也不是情绪化的答案。
\n替别人承担问题,这会令自己获得一种价值感。但若心理医生在咨询室中追求这种价值感,他便在一定程度上阻碍了病人的自我发展。
\n只要你在乎一个关系,那么你一定会把你的内在的关系投射到这个外部关系上。
\n任何一个你在乎的关系,其实都是一面心灵的镜子,可以照出你内心的秘密来。
\n假若我们渴望变成一个健康、和谐的人,那么,我们就要好好地观察自己在重要关系上的表现。
\n重要的亲密关系是我们生命中的拯救者,遇到一个真心爱自己的人,那是生命中最有价值的事情。
\n你越在乎一个关系,你的那个内在的关系模式就越会淋漓尽致地展现在这个关系上。
\n什么是内心的声音?就是你的感觉,你那些说不出来但却又模模糊糊捕捉到的信息。这种声音,要学会聆听它,并尊重它。
\n追求人格的自由,结束已经发生的事实对我们心灵的羁绊只有一条途径:接受已经发生的事实,承认它已不可改变。
\n一个人的人格就是这个人过去所有人生体验的总和。
\n一个人假若常常失去控制,那么一个重要的原因是他把自己太多的事情压抑进了潜意识。
\n所谓接受,即直面我们人生中的所有真相,深深地懂得,任何事实一旦发生就无可更改,而且不管多么亲密的人,我们都不能指望他们为自己而改变。
\n多数心理问题,就是因为我们小时候拒绝接受自己的父母,拒绝接受这个生命中最大的命运。相反,我们渴望改变父母。这种渴望注定会失败,于是我们将这个渴望深埋在心底,长大了,再按照这个渴望去选择配偶,并像童年渴望改变父母一样来改造配偶。
\n童年时所受过的苦,长大后我们会再受一次,不过,这次的受苦,目的是纠正童年的错误。
\n愤怒其实是在提醒我们,别人对你侵犯得太厉害了,你要告诉对方:停!你不能再侵入我的空间。
\n内疚,本来是一个信号,告诉你,某个关系的付出与接受已经失去了平衡,需要调整了。
\n一个关系,就是在相互的付出和接受的循环中不断发展的。假若一个人只付出不接受,那么他就不可能与人建立很深的亲密关系。
\n最不讨人喜欢的恐惧,其实具备着最重要的价值。只有恐惧,才能强有力地打破我们的自恋状态,告诉我们:你,真的很渺小;你,必须放弃一些虚假的自大,而去寻找真正重要的东西。
\n从心理学角度而言,人生宛如一个轮回,我们有一个相对固定的人格结构,也即我常写的“内在关系模式”,这导致我们会不断地在同一个地方摔倒。
\n一个对自己太苛刻的人,很难做到宽以待人。相反,对自己苛刻的人,更可能的选择,是挑剔别人。
\n宽容胜于挑剔。所以,一个宽容而温和的朋友,要胜于一个优秀而挑剔的朋友。后者或许会把“严于律己,宽以待人”当作座右铭,但因为不符合最基本的心理学原理,他在过于挑剔自己的同时,也势必会苛责别人。
\n在生活中,我们的人生不断发生变化,每一次转变,我们都需要一些仪式来提醒自己。
\n仪式并不一定是一个刻意的程序,其实,入学、毕业、工作、恋爱、结婚乃至为人父母,都是一个仪式。
\n仪式只是为了告别,而不是为了忘却,因为事实一旦发生,就注定是我们命运中的一部分,我们必须接受这一部分,忘却既不能真正做到,也不利于心灵的康复。
\n仪式只是一道门,这道门,把我们的人生路划成两段,前一段属于过去,后一段属于未来,但门仍是通的,属于门那边的过去并未消失。也就是说,它只是一个象征,在提示我们,转变已发生。
\n","tags":["摘抄"]},{"title":"《高效能人士的七个习惯》 脑图 && 好句","url":"/2020/the-7-habits-of-highly-effective-people/","content":"\n对自己要有耐心,因为自我成长是神圣的,同时也是脆弱的,是人生中最大规模的投资。
\n人的一生包含了许多成长和进步阶段,必须循序渐进,每一步都十分重要,并且需要时间,不能挑过。
\n承认自己的无知往往是求知的第一步。
\n教育孩子应该有充分的耐心让他们体会拥有的感觉,同时用足够的智慧告诉他们付出的价值,另外还要以身作则。
\n人们越是依赖立竿见影的解决办法,越是加剧了问题潜在的隐患。
\n如果不能持续投资以增进自己的产能,眼光就会受到局限,只能在现有的职位上踏步。p45
\n所有积极主动的人都深谙其道,不会把自己的行为归咎于环境、外界条件或他人的影响。p57
\n对力不能及之事处之泰然,对能够改变的则全力以赴。p65
\n对于已经无法返回的错误,积极主动的人不是懊恨不已,而是承认往日错误已属关注圈的事实,那是人力无法企及的范畴,既不能从头来过,也不能改变结果。p65
\n学会做照亮他人的蜡烛,而不是评判对错的法官;以身作则,而不是一心挑错;解决问题,而不是制造事端。p67
\n如果你一直认为问题「存在于外部」,那么请马上打住,因为这种想法本身就是问题。p67
\n太多人成功之后,反而感到空虚;得到名利之后,却发现牺牲了更可贵的东西。因此我们务必紧盯真正重要的愿景,然后勇往直前坚持到底,使生活充满意义。p69
\n管理是正确地做事,领导则是做正确的事。p74
\n一个人的应变能力取决于他对自己的本性、人生目标以及价值观不变信念。p78
\n有效管理是掌握重点式的管理,它把最重要的事放在第一位。由领导决定什么是重点后,再靠自制力来掌握重点,时刻把他们放在第一位,以免被感觉、情绪或冲动所左右。p96
\n对人不讲效率,对事才可如此。对人应讲效用,即某一行为是否有效。p109
\n管理者注重建立制度,然后汇集群力共同完成工作。p111
\n信任是促使人们进步的最大动力,因为信任能够让人们表现出自己最好的一面。p113
\n责任型授权是关于授权的全新思维方式,它改变了人际关系的性质:因为分得工作的人成为自己的老板,受自己内心良知的指引,努力兑现自己的承诺,达到既定目标。p114
\n当孩子感觉受重视的时候,亲子之间就建起了一座爱与信任的坚实桥梁。p126
\n经验表明,在家族式或者建立在友谊基础上的生意启动之前,最好先就「不能双赢就好聚好散」这一点达成协议,这样的繁荣才不会导致关系的破裂。p133
\n成熟就是在表达自己的情感和信念的同时又能体谅他人的想法和感受的能力。p135
\n领导所要做的就是放手,让有责任心、积极处事以及具有自我领导能力的人独立完成任务。p140
\n双赢协议是管理的核心内容。有了这样的一个协议,员工就可以在协议规定的范围内进行有效的自我管理,而经理就像是赛跑中的开路车一样,待一切顺利开展后悄悄退出,做好后勤工作。p141
\n你应该时刻想着先理解别人,这是你力所能及的。如果你把精力放在影响圈内,就能真正且深入地了解对方。你会获得准确的信息,能迅速抓住事件的核心,建立自己的情感账户,还能给对方提供给你有效合作所必须的「心里空气」。p157
\n与所见略同嗯人沟通,益处不大,要有分歧才有收获。p159
\n不要在意别人的无理行径,避开那些消极力量,发现并利用别人的优势,提高自己的认识,扩展自己的视野。p172
\n工作本身并不能带来经济上的安全感,具备良好的思考、学习、创造与适应能力,才能立于不败之地。p175
\n我们越擅长发觉别人的潜力,越能在配偶、子女、同事或雇员身上发挥自己的想象力,而不是记忆力。p183
\n良知是一种天赋,帮助我们判断自己是否背离了正确的原则,然后引导我们向这些原则靠拢。p186
\n"},{"title":"部署 jar 包到生产环境的科学方法","url":"/2017/the-best-way-to-deploy-jar/","content":"我们现在的线上环境都是简单的使用 nohup java -jar xxx.jar &
的命令来将项目启起来的(感谢 Spring Boot),不过这种方式有诸多不便,比如我们想停掉或者重启某个项目,都需要通过 ps
命令先找到程序对应的 pid
,然后再执行 kill
命令,然后再手动启动一遍这个程序。
下边我介绍一种(自我认为)比较科学的方式:
\n以我们已有的 demo
项目 app-c
为例,在 build.gradle
中加入:
springBoot { |
这样可以编译出来可执行的 jar
包,看下前后对比,下边两张图分别是加之前和加之后 app-c
,可以看到多了 x
权限。
不带 x
权限的 app-c
带 x
权限的 app-c
我们来直接用 output/app-c-0.0.1-SNAPSHOT.jar
运行一下试试:
没有任何问问题。
sudo ln -s /opt/demo-projects/output/app-c-0.0.1-SNAPSHOT.jar /etc/init.d/app-c
这样就可以使用 start
、stop
、restart
来管理我们的应用了
|
并且 status
可以查看运行状态:
用这样的方式来管理线上应用,比之前的方式方便了很多:不需要再进到存放 jar
包的目录来用冗长的代码启动,只需知道应用名称,就可以直接启动、重启、停止我们想管理的应用。
看到第三者这个词是不是想歪了?我这里指的是一个事件中的第三方参与者。
\n我举个例子,你媳妇和你妈吵架,你在他们中间就属于第三者,你起到的作用举足轻重,处理好能家和万事兴,处理不好能鸡飞狗跳。我不知道其他人,我是非常不擅长处理这种事的,我经历的鸡飞狗跳太多了🥲。
\n我不是一个合格的第三者,但我非常敬佩能把事处理的非常妥当的那些第三者。在我看来合格的第三者应该像袭人那样,大事化小、小事化无。
\n有一回宝玉去薛姨妈家,伺候宝玉的李奶妈拦着不让他多吃酒,回去后李奶妈还把宝玉给晴雯留的豆腐皮包子吃了,宝玉要喝茶时小丫头们说李奶妈把他泡好的枫露茶喝了,宝玉气的摔了茶碗,嚷嚷着要把李奶妈赶出去。不一会贾母房里的小丫头就来问发生了什么事,袭人站出来说是她不小心喝水时打碎了杯子,她不想大晚上的让贾母担心,没有提任何李奶妈的事。
\n后边还有一回李奶妈把宝玉留给袭人的酥酪吃了,袭人外出回来后,宝玉让人去把酥酪取来,丫鬟们回李奶妈吃了,宝玉正要发火,袭人说“原来是留的这个,多谢费心。前儿我吃的时候好吃,吃过了好肚子疼,足的吐了才好。他吃了倒好,搁在这里白糟蹋了。”就这样又化解了这一次危机。如果换成其他爱作妖的丫鬟,比如晴雯这种爆炭脾气的,非得把事闹大了不可(一会我说个关于晴雯的事)。
\n宝玉也有很多大事化小、小事化无的高光时刻,说一个例子,一次藕官在大观园里烧纸钱祭奠已经死去的、她之前的戏搭子菂官,被一个老婆子撞见,老婆子抓着藕官要去找太太。宝玉经过遇到此事,按常理,宝玉也可能会责备烧纸钱的人,但他看到藕官满面泪痕,他心想这个小戏子一定有她的心事,背后有无法言说的委屈,宝玉先把事情的真实原因放在后边,先自己站出来说是他让藕官烧的,就这样救下了藕官。
\n\n\n只要将心比心,你就会对一个人的伤心有所关怀,它既不是法律,也不是道德,而是在法律跟道德之外人内心最柔软的那个部分。
\n
公司里,小领导在不同场合对自己的下属进行评价也能看出是否是一个合格的第三者。大老板们不了解一线员工的状态,需要小领导来反馈一下,如果小领导总抓着其他人的缺点去评判,不能避重就轻、善于发现别人的优点是万万不可的。你随随便便几句话,可能带给对方的就是天差地别的结果。
\n我们不要做老好人,也不要做煽风点火、唯恐天下不乱的人。如果能预测到一件小事在往恶性的方面发展,而你又是参与其中的一个人,不妨尝试化解一下。
\n不光宝玉身边的袭人,凤姐的特别助理平儿在这方面做的也非常出色,最著名的一回莫过于「俏平儿情掩虾须镯」,这一回中平儿和晴雯的处理方式形成了极大的反差。平儿在大雪天跟宝玉、湘云一起在野外烧烤,吃鹿肉时把镯子摘下放在了一旁,吃完后发现不见了,经过排查发现是宝玉屋里小丫鬟坠儿偷拿了。
\n平儿考虑到宝玉对丫鬟们很好,原文是这样写的:“我赶忙接了镯子,想了一想:宝玉是偏在你们身上留心用意、争胜要强的。”,「留心用意」,是说宝玉没有用管理丫头的方法管理她们,他相信人性有一种更高的自觉;「争强要胜」是说他希望自己房里的丫头,没有严格的法的约束也能有人性的自觉。如果平儿把这件事爆出来宝玉肯定会被人议论过于放纵自己的丫鬟,而那个丫鬟也会被赶出去,在那个社会如果一个丫鬟被一个大户人家赶出去基本就等于判了死刑(后边晴雯就是这么死的),所以平儿想把这件事掩盖下来,以后让大家提防着点坠儿就好了。她和麝月商议后打算不把这件事告诉正在生病的爆炭脾气的晴雯,谁成想他们的对话被宝玉听到了,宝玉还是转述给了晴雯,晴雯气的对那个小丫鬟又打又骂,假借宝玉之名把坠儿赶了出去。
\n\n\n思考:坠儿出了这种事,等于是宝玉对人性实验的一次失败,可是最大的为难在于,十次有九次失败,我们还要不要为那一次留下余地。
\n
同样的事件,用不同的方式表达,起到的效果也大不一样,比如一个总打败仗的将军,我们可以说他屡战屡败,也可以说他屡败屡战,两个读起来相近的句子,含义却差了十万八千里,这又涉及到了语言的艺术。
\n最后讲个有趣的典故吧,大家都听过一个顺口溜「二十三,糖瓜粘」。”糖瓜”是一种用黄米和麦芽熬制成的粘性很大的糖,为什么腊月二十三要做糖瓜呢,因为传说这一天灶王爷要去天上,像玉帝报告每户人家这一年做了好事还是坏事,所以百姓们就把糖黏在炉口来贿赂灶王爷,意思是让灶王爷嘴巴甜一点,上天以后讲这一家人的好话。
\n民间的有趣就在于,他们会觉得没有什么东西是躲不过去的,就看你用什么方法。这个跟人的生命力有关。所谓生命力,就是灾难不再是灾难,危机不再是危机。我们在生活中,有时候遇到一点小事就觉得过不去了,其实就是生命力弱了。
\n"},{"title":"是不是该考虑换个环境了?","url":"/2022/think-change-a-environment/","content":"\n\n本文使用了 emoji 对部分内容进行了加密,可以看我这篇文章了解详情。
\n
马老师说过,离职无非两个原因:1、钱,没给到位;2、心,委屈了。
\n最近读到 MacTalk 的一篇文章,这篇文章将离职原因分成了三点,前两点是把马老师的第二点做了个个拆分:
\n最近遇到一些烦心事,所以考虑是不是该换个有利于身心健康的环境了,我有些厌恶现在的团队文化,不喜欢每天被推着做事情,先列举下这些让我不舒服的事吧:
\n😸😸🙂🙃🙆😷👵👴👕👺👤😵🙅😴😲👯👚👧👒👡👚👲😫👵👵🙇👏😲👵👕🙃👰👓👤👯👴👫👫🙉👸😷👥🙆👣👷👒👶👙😶😶👐👕😶👪😸👶👥🙎🙇😯😯👦👣👌👗👱👶👒👰👪😷👙👭👕👭👬👫😲👨😴🙄👮👨😸🙂👦👐👪👩🙅🙂👘🙈👨👐🙁👘👴👬👦👤👵👸😱👮👘👶🙁🙉👡👔😳👬👚🙋👧👗😰👴👹👡😫😷👪👲😰😳😲👙🙋🙆👯👴👥🙊👗👙🙂👶👭👏😫👦👢😱😳😵👥👵😷🙎👣👖😳🙎🙅👳👹👧👖👏😷👏👱👡😱👙👶👌😯😱😱😹🙇👏🙄👵👺🙊🙍🙆👨😳👌👬👓😸👥👰🙅👳😴😳😫👷👩👗😳😶👱👫👮🙃👓👨🙇👓👮👘😯👗👐👺🙁👚👓👷👬🙁🙉😲👩👮🙇👭🙄👰👒🙃🙄😲😵😫👔👵👧🙃👧👮👑👑👺😹👳😲👤🙆🙂🙋👷👓👦🙂🙃😳👮😳👩😷😲👙👧👓🙋👬👙👯👮👢👔🙋👮👹👳👩😰👔👵👒🙍👤😳👌👯👸👷👫👒😯👗😸👧😯😶👯👌👲👶😲🙃🙃👘👚😷👦👕😸👬👣👙🙆👧😷🙍👚🙁👫👚👕👏👰👯👵😶🙅😫👙😷😰👶🙇👴🙍👓👥😳😰👺👨😱🙋👧👺😵🙉👌🙈😯🙄😸👖👤👪👭👚👡👲🙉😹👚👒👐👏😲👵👴👬😵👵👨👮👬👤😵👘🙈🙁😫🙅😯👢😹🙂👕🙃😴👦👺👨😯👣👤👯👧👐👡👡👫🙈🙁👹👫🙄👏😯👣😰👯👓👔😹👵😰😸👨🙄🙂😳👘🙂👌🙃😴👹🙃👑🙅👬👨🙉🙊👢😴😹👨👘👖👧😷👴😴😯🙁👡👹👏🙁👰🙈👴😳👫🙈😵👥🙆😳👱👙😸👲🙈👪👘👩👷😴👧👷👒👲👲😸👷👥👒👖👮🙋👤😰😸👒👵👫😲🙂👵👑👣👥👭😳👺🙁👱👦👸🙍👷🙇👡🙆🙂👚👘😱😴😫👌👚👳🙉😶👒😲👥😵👌👶😵👸👬👗😹👪😫🙍👑👓👤👩👙👬👨👚🙎😱🙇🙉😵🙉😲🙂🙍👑👬👖😷👲🙅👚👬😫👬👰👫👡👹😶🙂👵👡😵🙄👱🙂👌👌👰🙃👶👲🙁👕😲🙍👯😶😷🙎👑🙈👏👖👕😯🙁😳👑🙍😳😷😹👲👵👧🙈🙋👶👱👏😫😵😯🙆👑👐🙋🙉👬👗🙃👱👦👺🙊👩👐👲👏👔👌🙈👙🙋👚👺👒👭👳👥😸👯🙆👡👶👺🙁🙃👚😶👓👩👡👘👏👏👰😶👏😳🙆👘🙂👶😯👳👳👥👥👡👦👳👓😸👑👯👲👬👌👤👔🙃👓🙆👕👳👖👡👱😳👴🙁👶👔👌👖😱😲🙅👏👌😵👌😲😫🙋👯👨🙁👗😵😶👫👚🙂👏👢🙅👺🙈👮😷🙃👚👸👘😳🙇🙄👒👺👨👭👱🙇😵👖🙍👢👗👏👚👭🙍👕😫👣😵🙍👦👱👓👷👓😸👶👧👳😯👐😷👓🙉😹👸👐👕😸👣🙉🙊😸👚👨🙁👣🙋👫👸👬👣👒👵😶😸😹😲👗👑👲👚👶👏👲😹👱👡👑🙊👲👬😴👡👓👰👵👮👖😶👸👐👹😷👭😷👵👳🙄🙇👢👚👰🙈🙉👯👥👡👭👗😸👙👩😶👭👸👗😶👸👹👑🙈👥🙉🙂😳🙊👸🙈🙄👔👚🙅👹😳👴👒👏👏😹👗👲👩👤👮👨👮👯🙂👵👥🙇😳👴😵👺👚😱👖👦👷😹👤🙋👗👱👪👑🙆👯👵😫👤😰🙂😵👯👕👮👸👹👹👕😷👡👧😴👷😹👷🙄👺👺👏👏👘👷👤👧👓😶👶👳🙍🙋👥👥👕😴😲👵🙆👥👳🙅👡🙋😷🙇👪🙄👚👥👦🙉🙎🙂🙎👑👺👖😷👤👙👘👪👥😰👢👚👹🙇👤🙄👗🙉😱🙇😫👨🙅😴🙊👮👨👳👣😰👔😱👹😫👮😷👖👡👐🙋🙁👳👴👚👪👏👓🙃😵👣😲👌👰🙅👖😴👥👳😴👘👔😴👙👫👺👐👙👦👑🙆🙊👗🙆👘🙃👺👒👓👥👧👐👺🙂😱👢🙉🙇👡😰😱👳👳👒🙆👭🙃👐👳👨😷👗👥😫👖👸🙎🙃👘👖🙆😴😰👨👦👨😷😹😷👕👩🙎👐😯😰👖👤👱👹👸👮😴👧👤😲👸🙎🙍👔👶👐👳👌👖👺👺👷👘👒👡👥👕😶😱🙋👏🙎👺😯🙁🙁👣👢😰👡👰😱😯😸👶🙂😳😴👏👙👮👐🙇👴👨👶😷👴😵👷😲👪🙆👥😰🙍👌👨😶👏👕🙋👤👡👷👌👬👹👡👯🙅😸🙊😶🙂🙂👒👺😲👴👷😵👨👓👒👦👹👶😱😹😷😯😹😯👬👌😫👬👘👪👭👸🙄😱🙎👔👣👔🙋😷👑👫😯👫😹🙊👐😫😰😴👯🙊👙👣👹👏🙆👱👗😰👢😱👔😯😳👸👙🙁🙄👖👘👯👫🙁👓😰👔😯👬😲👣👙🙎👙👌👒🙄👧👚😳😰👏🙂👒🙈👌👧👴👱👙🙈👯🙈👗👖😹👕👩👚😶👰😷🙂👹👹😴👖👭👱🙃🙎👦👚👫👕👷👫👡👏😫👣👺👤👪👸👴😷😫😰👕👒👶👦🙆👔🙁🙃👖👔👘😳👏👓🙋🙈👶👭👷👚🙂👖👯👢🙍👭👰🙂🙋🙉👷👣👶👙🙉👹🙍👯👺👲👓👩😹👒👲😹👹😴👷🙇👣😳👶👴👤🙆👏👯👌😲👷👌👔👌👑👶👘👰👭👚🙃😶🙍🙈👢🙂👱🙅👴👥👧👱👑👫👺👰👗👺🙁🙍😳👑👱👩🙃😯😰👕🙉😯👧🙃👬👨👚👗🙇😹👘🙅😷👓😫🙎👲👸🙁🙂👢😫👏😰👙🙄👷😯🙍👤😲👯👪😷👹👌👥👌👵👚👗👓😸😰👱👨👨😲👲👣😵😶👴🙂👌👗😷🙅😯😷😰👬🙊👳🙂👬👙🙊👓🙍😴🙃👤👹👷👨👘😲👸👷🙅😵🙊👴😳😵🙊👗😷👤👡👗👙👰👪👗👩🙇👦🙊👶👥😰😱😳🙍😲👲🙈👺👩👖👰👗😹👷👺👖😫🙈😷👚👫🙍😫🙋👰👗👴👚😸🙉😯👚👚👐🙄😫👷👷😫👔😹🙎👬👚👗👧🙆👬🙇👔😵👑👪😸👗👌😱👣🙇👶🙉👶👌😴🙍👤🙋😱😰😶👯👰👐😫👣🙂👭👚😱👳👷🙋👧🙎👑🙎👸👮👣🙉👘😰👙👸👖🙊🙉👹👹👺👗👧👗😱👴👶🙆👓👲😵👖👚👙👖😵👺👱😳🙊👭😴👵👢🙅👥👘👴👨👲👦👘👸👫👒😶👨👮😴👲🙋👕👕🙁👪👲👚👡👕👐😱👭👸🙊😶👺🙊😱👩🙇👑😷👤👬👨👸👴😸👕👬👦🙉👕👕👒🙁👥👯👘👐👘👌👱👩👰👏👤🙃👮🙃👱🙂👹🙆🙍🙇😹👗🙍👏🙊😷👩🙉👌🙋🙁😫👢😴👚🙎🙇😶👒👓👩👰👱👳👥😱👗😲👙👕👚👤👢😸😫👚😱👚🙆😰🙁😯😲👱🙁🙆👏🙆👕👵🙁👑👷👙😰👙👦🙊😶👣👺👘👸😱🙋👰👵👴😵🙍👨😵😵🙄👢😲👩👱😱👴🙄👓👹👙👘👮🙃😯👐👷👑👰🙈🙍👑😳👩👕👱👢🙉👑👦👪👦😰👵🙈🙊🙊👱🙍👲👮😷👖😰😵😷🙇😫👏😵👨😯👭👖😳👦👧👒😲👥😯🙆👴🙎👶👫👺👒😳😳👷👭👸👗👵😹👲😫😹😹👣👩👣👗👳🙋👮🙄👪👌👴👔👰👪😷👫👶👔👒👢👚👶👰👪👤🙄😫👺👖👥👔👖👥👪👏👨👐👥😲😱👪👕👥👙👬🙉🙋👵😱🙊👕👓👬👨👑😳😳👌👺👌👹🙇👪👙🙊🙊🙂👱🙆🙊👗👩👴👦👵🙎👒👕👥😲🙂👪👡👹👪🙉👌👵👫👨🙅👹🙇👌👐👚👣🙋😹👓👪👔😲👹👷👨🙇🙈👡👖😶👲🙇👶👌👭😹👧👗👲👘👣🙅😰😫🙄😳👣🙍🙈👙🙈😷🙂😷👡👔👳🙁🙃👣👡👭👵👕👲🙆👚👲😯👵👯😫👪👪👴👢👙👴😲🙄🙁👑👹😳😫😱👤🙆😯👪👔👱👨🙉😫👚👬👲👡👹👓👕👪👱👌👢👰👸🙁😳😳😲🙁👳👏👲👺👬😵👫👗👡👷😫👨😳😱👧👫👥🙋😷👧👌👒🙍😲👤😷👷🙄😴👲👢🙇👸👗🙂🙅👳👮👓👙👺👵👖👥👱🙇👳👦👭🙄🙋👴👤👐🙃👔👑👙👣🙍🙃😯👕👙🙆👶👬👶👷👴😳🙊😶👺🙇👘👬🙎👦👳👧👪👦👷👷👺👭👐👱😵👷😳👶🙍👬😶🙋👺👰👵👌😵🙃🙈👢👱👺🙂🙈👐😷🙉👔😴🙉😯👐👳👱😯🙅👓👒😳👷👵😶👢😷😷🙂👮👏👗👑🙎🙅👺😸😷👒👢😱👰🙆🙅😶👨👴👨👐😴👩👦😳👤👪😸👭👪😸🙉😹👸👫👲😳👥👰🙃🙄😯👧👷😸👒👩👳👴👭👣👚👭😫👫👕👶👑👓👔😯👓🙁👺👡😯👌👶👡👺👌👷👦😹👩🙊😶😵🙇👶👔👖🙁👫🙁👔🙊👖👕👐👤👏👏👺👴👶😸🙉👘👌🙎👗👪👥🙇👶👷👑👗😳😵👚👷👗😲😶👷🙄😳👯😱👹👨👳🙈👔😯👔😶🙊👸👡😸👨👵👪👫👓👏👸👣👢🙈👔👘👘🙉👢👪🙃🙂👺🙄👤👭🙇👵👑👮😷😯👣👡👯🙍👗👭🙆👑👓🙊😱🙊🙈👱👪👦😹👡👬👐👡👥👯👥👶👢😰👰😹👩👹😷👏👴👢🙈👑👬🙂🙈😵👙🙆👶👑👸🙅👤👲😯👒👏👴🙄🙊👯👕👵😳🙃😶👘🙎😵👷🙅👣👥🙁👗😵👏👶👧👏👔🙇👌😷👙👮👷👹😷👘👗👑😶👶👐😵👹👕👌🙄👨👑😳👸👮😹👐😴😸🙍😴🙊👚🙈👫🙇🙆👩👔👐👣🙂😵👕😷👑😳😹👮👰👸👕😶👧😹😰👦👶👱👰👢🙉👺👶👭👬👙👱👳🙆🙃🙇😲👲👳🙍👶👶👬👤😳🙍👘🙆👧😴👲👶👨🙇🙈😰👢😸😯👑👨👩🙆😱👢🙆👔👕👑👢😫👏👡👙👰👤😹👒👨🙆🙄👱😲👩👙👮👔😶👯🙎🙁👦😳👡🙋👫👔👖👳😴😲👷👪👌👨😶😶👡😳👨👏🙋😰😳🙃😹👥👱👮👨👶👤👸👴👷👗😹😴🙇👴👔😵👱👫😫👣🙂👯👰👷👚🙈👢👕😷👔👨🙋🙆🙎👪👱👢🙄🙅👖👡🙍🙉👯🙁👓👹🙍🙇😯👴👐👌🙎😹👨👶👨🙂👚👙👑😯👒👺😱👚👗😰👳😷🙃😲👌🙋🙍👲👗🙍😰🙂👩🙄👢👸👵😳👱👮👪👖😲👩👪😷👓🙆😱🙍🙆👶👬🙁😶🙅😱👹👯😹👗👬😯😷👘👕👭😲🙃😸👮🙂🙁👒👨🙃🙅🙎👺🙉👏🙈😴👳😴👺👬👺👲😰👲🙁👌👫🙃🙃👘😯🙄😵👔😴👷🙋😲👹🙅😱👲🙍👭👧👗👫👢😫👫👏😲👓🙉👲👗👘👴👣👣😫👵🙃👖👵👴👢👳👔👳🙊👢👌🙃👭👙😫🙉👫👹👚🙇🙈😯👱😱👴👔👓👓😲👘👩🙄🙊🙃🙆😵🙋👷😰👧😱😹👏👱😫😴🙆👺👑👢👐🙃👣👬😵👘👩👔🙂👢😯👌🙍👘👑🙁😳👗🙈👴🙋👐👔👯👢👰😫👘👌👤👓👬👏👵😶😹👖👙👪😱👡😷👚🙋🙋👣👩👷👪👦👐🙁👣👚😫👫👕👳🙆😯👔😳👪👒👗🙍👯👣👑👒👘👖👘😵😶👭😰🙂👬👐🙆👣😲🙉👭👯👫👣👷🙋🙎👏👲👔😲👳👧👑😹🙎👭🙍😴🙉👘👧😹👗🙆😴👒👲🙂👭👵👦👯🙂👌😵👌👦👸👒😵👺👶👣🙉🙂😷👕👷👣😲👫👖🙇👹👸🙄😱🙂👐😸👨👮👕👓👷👳👶👸👨🙇👪👒👏😫👓👭😫👕😱👡😯👱👤👧👏👧👹👧👒👘👲👌🙎👏👢👖👹👹🙎👏🙇👤🙆👺👣🙃🙃👨👏👢🙉👔👌😵👭😸🙊👧👭👪👑👗👺🙍👓👷👬👷👶😱👯👡👘👧👒🙉👐👑👥👯👷👒👶😲👩👥👫👩👘👚🙉😹👢😫👶👔👩👶👥😷👶😸👙👥👩👷👗🙇👏👒👭👣👲👌👩👡👢👹👨👵👌👲👢😲👤👡👣🙈👑👰😹👰👨😵😸👥👑🙄👴🙉👰👷🙁🙁👣👕👒👬👏😯😵👹👭👱👮👮🙉🙈👑👫😱👒👑👑👦👲👦🙁👹😱👷👖👲😫🙈👣🙇👺👖👑👫👔👦👲😶👚👌🙊👘👭👓🙊🙍😴👵🙁👧😫🙈🙈👸👸👢👘👸👳👫👺🙅👐👣👴😶🙄👓😰🙈👩👵😵👯🙅👬😲👸👦🙇👸👨😷👦👶😫👣👡😹😯👶👢👶🙍👧🙂🙇👮😳👖😶👌👨🙅👏👬👕🙍👏👨👩😵😳😸🙈👡👭👱👱👩👙😳🙅🙂😱👭😲🙇👲🙊👭👙👢👚🙆👘😶👏👑🙉👥👢👕😳🙇😶😱🙍🙍😷👳👨👸😯🙋😰👙😵🙉🙈👢👱👲🙎👳🙆👳😹🙈🙄🙇👏🙇👖😹🙅👘😯👒👲👒👸👬👪👗🙋👶👮👮😸👨👳👵👤👧👶👫😫👱😷🙉👓🙋🙆🙍👑😶👬🙁👹👙👐👚👕👲😯👥🙃👭👭👸🙄🙈👯👺🙇👓🙊🙉🙁😵🙂😲😰😰👶👏👗🙋👱😫👭👣👙👶😲👖👣👡😰👯👰🙎👶🙉👐👬😴👐👷👲👐😯👗👨👓🙆👺👏👙👩👨👵👩👪😷👴👸🙆👪👯👹👑🙍👧😸😰🙋👺🙁👹😹👤👖👣👷😲👒👣😴👘😰🙆👥😸🙄😷😷😷👰🙋👐😫😷👑👐👷😲😯👖👴🙂👮👶🙇👺👴👧👓👐😳👳👸😫👫👮👬👦😰😫😱🙄👖👏👵😳😶🙁😹🙁👶👭👗👔🙊👢👮👷😲😹👔🙊👐👐👮👤👶🙃😯😱👩👢👸😳👶👒👒👸👗🙎👶👣😱🙅🙎👏👰👪🙋👚👔🙅👰👏👸👐🙆👲😴🙂🙇👓😯🙅👷👹😵👐👯😶😰👺🙄👚😷🙉👕👌👳🙂👙👳👩👪🙅🙊👕👭🙃👒👶👷👪👹👚👤👲👢👙👭😴👌👨🙍😹👓👚👥👙😴🙆👙👡😲😱👥😯👶😰🙋👡👡😳🙂🙅👧🙍👮👰😱👥👰👬👴👌😷👴🙈👸🙍👐🙉👺👘🙋👒🙈👷😷👙👓👚😶🙎🙆🙄👴👑👦😴😶👖😰👒🙈🙊🙆🙆👮👺👭🙄🙅👢👖😹👴👢👩🙁😴👓🙉👓😵👕👌👙😶👵👒🙁😲👓👚👹👸😲👮👤👡🙂👷🙂👌👗👙👥👔🙊👷👸🙈😫🙁😴👮👴👒👲😰😴👴🙆😫🙋🙇👳😹👗👘👓😶👱👙👬👵👷😵👴🙇😰👥👤👡👚🙇😫👢👣🙃👚👥👺👩😳👩👴👣👫👑👔👚👔👚👏👚👩🙎👑👯😴👭😲👵😳🙎🙎👩👳👩😲👡👐😶🙈😫👖😫👙😯👬👢👤🙄🙇🙉🙆🙍😴🙁😲🙆👵🙄😫👮🙎👙🙃😶🙂👡👨👐🙋😴👒👚🙄😳👌😳👒👬👥😴🙅👴👷👔🙃👩👦👺👘😯🙎👘👮🙄👕👢😱👰👮😳👡🙍😳👌😶👹🙊😹😰👳👣👯👶🙎👲👚😳👓👮😯🙊👧👧🙂😶🙊👧🙈😰👡🙎🙂😴👧🙄🙈👺👒👱👫👢👗🙂👬👤👫👨👗👗👗👹👹👴👖👏👖😳👖👰👤👲😫👶👴👢👩🙋😳👰👴👣👐🙍😴👧👢👭👌👬👌🙃👶🙎👵👫🙅🙇👕👭👚👓👣😯😷😲👑😶👲👩👌👬😳👐😴🙊👘🙉👹👱😯🙍👣👖👨👕👳👹👐👧👲👢👗👺👕🙎😰😶👹👮👱😵😫👯😷👘🙅😰👚🙊🙆😹👗👌👯👲👖👴🙊🙅👖👧👺👮👮👘🙊👷👗👳👒👘🙃🙇🙍👫👦👩👷👬😷👲👹😳🙂👘👬👹😹👢🙎😹😲👘👺👗👌🙇😫🙍👯🙄😰👏👩👳👵👙👒👕👤😹😶👫👰👌👸👑🙈🙋👓👓👰👔👦🙊🙇🙄👹👺👘👑🙉👣👥🙅👤😵😯🙅😴😵👓👯😱🙇👺👴😴🙁😲🙂👡👤🙃👑👨👤🙄👖🙉🙊🙆🙍👱👤👷😰👦👮😰👰👰👭🙁👡👵👴👴😱👌👩👧🙁👏👡🙁👐👧👗👙👩👱🙁😱🙆👢🙍👓🙇👗👢👔👏👚👑👒👔👓👵👌👷👧👓👰🙊😳😱😴😴🙃👫👬👚🙅🙅👱😶👯🙆👚😶👴😴🙍👤🙎👘👘👔😯😳👷🙈😶😷👴👤👙🙋👕😹👸👮👚👴👣👵👔👱👩👢👤😴🙄👱👮😴🙁🙅👧👐😶😫👓👰🙃😯👨🙎👷🙂👧👩😱👵😷👚👯👘👦👧👴👗😯👙👹👒👸👗👙👭😯👕🙅😫👸👌👖👔😷😵👩👒👔👰🙎👧😱👸😱😴👖👭🙄🙋😱👰😲👪👐👮🙈🙈👐👩😳👥👙👚😷😫😸👳😯👴😶👮👖👨👢👫👡😰👺👴🙂👭👚🙂👨😶👮😴👢👺👕😴👬😫👧👵👖👰😯🙄👲🙉😳👲👱🙁👣👐🙁👱👪👷👒😳👳👢👑😫🙋👗👱👮🙊🙋🙂🙆👗🙎😰👓👥👕😲🙋😹👗👐👡👫🙇😹😳😴👰🙄🙊👢👣👸👱🙎👤👯😹👢👧👏👲👙😷👸👶👱👺👳👕👕😫👺👣👱👲👕😰🙁😫🙄👦🙄👳🙉👵😴👪👸🙍👓👐😹😶👨😰👤👲😰🙁👏👚👑👬🙎😸👗👳🙋👪👶😶🙅👧👏👲👵👭🙅👖😶👖👴👹👐👡👑😵👓👩👙🙂👓👤😶😷👵😸😲😱👦👌👥👗🙋😵👖👱😱🙋👦👚👙👑😽😽
\n综上,现在的环境让我待的有些挺难受,于是有了换个环境的打算。
\n有没有预期的公司?倒是有两个,一个是 Tubi,一家外企,做海外免费影视剧的;另一个是 MegaEase,做基础设施研发的。之前读过一些介绍他们公司的价值观的文章,很合我的口味。MegaEase 的创始人是陈皓,也是引领我入开发这个门的一位大牛。
\n这里有一个 Tubi 的介绍:https://mp.weixin.qq.com/s/ZCQerV2HKPq9k9EhDocOhA
\n"},{"title":"关于早会的思考","url":"/2023/thoughts-about-morning-meetings/","content":"今天周一,照例我们下午开了全组的周会,我思考了很久决定取消每日晨会,下边是我准备的发言稿。
\n本月最后一天是我入职 TT 的三周年,我依然向往我刚入职 TT 后近一年左右的时光,那个时候 TT 还有一点点外企文化,不具体展开讲了,用几个词形容就是:包容、信任、自驱、敢于试错。我那时也非常庆幸自己入职一家好公司,当时的 TT 被称为互联网最后一片净土,也确实对得起小而美的称号。
\n我一年半前主动要求过一次转岗,从直播转到推荐,刚来推荐组的时候,每次晨会听到大家工作那么饱和我都很焦虑,所以我也能体会大家现在的感受。
\n上周有一天kq因为白天开了一整天的会,但他手里的一个技术驱动项目进度还差一些,晚上下班后我问他走不走,他说得加班把技术驱动搞完,不然第二天早会没得说。我知道他是在开玩笑,不过那句「不然第二天早会没得说」这句话我确实也在心中说过好多次。
\n我不希望大家每天为了考虑早会上要说什么而有压力,甚至出现为了说点什么而被迫找点琐碎而无意义的事情做,也不希望大家靠堆砌很多工作量来证明自己的能力和重要性。我希望大家的工作可以更专注、聚焦、深入、认真、细致一些,不要东一榔头西一棒槌。我特别喜欢一句话:不要用战术上的勤奋,来掩盖战略的懒惰。
\n所以我打算尝试取消早会,取消也许是长期的,也许是暂时的,还要看取消后的效果和公司的要求。对于我来说开晨会是正确地做事,现在取消周会是做正确的事(大家可以想想这两句话的区别),结果是否正确现在不得而知。
\n不开晨会建立在大家自驱的基础上,也建立在我对大家充分了解和信任的基础上,我一直相信信任是促使人们进步的最大动力,因为信任能够让人们表现出自己最好的一面。
\n我们组内的方向比较多,每个人的工作内容不尽相同,每日同步给所有人的意义不是很大,靠每周周会来做一次相互了解和同步就够了。
\n我们现在早会最大的益处其实是收集大家日常工作中遇到的问题,我们取消了早会,大家的问题就不要再等到第二天早会上再提了,有了问题随时提,不要因为没了早会的要求就掩盖问题,如果后边发现出现了问题被掩盖的现象,我们还会恢复早会。
\n在团队划分上,为了便于管理和领域打通,jw 没有再把工程和核心拆成两条线,但大家也能看到kq在推荐工程上的经验比我多的多,而且在核心需求比较多的时候我也确实无法两头都顾及到。再加上由于取消早会后反馈周期的加长,项目的跟进上不可避免会相较之前难度更大,所以我在这里也给kq提个要求,后边我们两个做下分工,所有核心项目我这边都会去了解背景、方案、进度和风险,所有推荐项目kq也要做到这几点,包括内部、产品和对外支持的项目。
\n再回到大家的工作上,大家在有项目、有工作任务的时候就聚焦于手头的工作,力求完美。如果有几天真的没有那么忙时就适当放松,学习一些感兴趣的东西,工作应该有张有弛,一直紧绷和一直放松都不是正常的状态。大家学习的时候尽量学习和我们业务相关的东西,我们组包含了公司内两大块最重要的业务:推荐和 IM,所以要想学肯定是有的学的。我也非常鼓励大家去发现、解决、优化工作中遇到的业务和技术痛点,这会让大家获取更大收益,包括能力上的和绩效结果上的。如果公司内的业务无法满足自己,也可以学习其他自己感兴趣的东西,比如 Web3或者学一门新的编程语言等等。我推荐作为程序员的大家,有精力的话每年学一门新的语言。编程语言会限制我们的思维模式,如果你长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考。
\n除了工作还有大家的工作状态,每个月总有那么几天不想工作,实在不想工作的那一天就让自己松弛一些。我自己很容易焦虑,所以我很羡慕能拥有松驰感的人。根据我的经验,一个正常排期3-5天的项目如果在状态佳而且无打扰的情况下,大概率一天就能把代码写完,这种状态也叫心流,有本叫《心流》的书大家感兴趣也可以看看。
\n最后,希望大家未来有一天回忆起在 TT 的工作(或实习)经历觉得是有意义的,而不是给大家留下痛苦、无效忙碌的一段经历。
\n"},{"title":"停止在你的循环中使用 i++","url":"/2019/stop-using-post-increment-in-your-loops/","content":"\n\n\n为什么 ++i 通常比 i++ 更好?
\n
如果你之前写过 for 循环,那么你一定使用过 i++
来增加你的循环变量。
然而,你是否考虑过为什么要选择这种做法呢?
\n我们在执行完 i++
后,i
的值会比它先前大 1,这是我们想要的结果。与此同时还有很多方法可以做到,比如:++i
甚至 i = i + 1
。
接下来,我会对比介绍两种实现变量加 1 的方法:++i
和 i++
,并解释为什么大多数情况下 ++i
可能好于 i++
。
i++
方法(或者叫后递增)是最常见的使用方式。
在伪代码中,后递增操作符对变量 i
的操作大致如下:
int j = i; |
由于后递增需要返回 i
的原值而不是返回 i + 1
后的增量值,所以需要将 i
的旧值进行存储。
这意味着 i++
需要额外的内存来存储这个值,但这是不必要的。因为在大多数情况下,我们并不会使用 i
的旧值,而是直接将其丢弃。
++i
方法(或者叫前递增)比较少见,通常是使用 C
和 C++
的老程序员在用。
在伪代码中,前递增操作符对变量 i
的操作大致如下:
i = i + 1; |
需要注意的是,在前递增中,我们不必保存 i
的旧值,我们只需简单的对它加 1 并返回。这与 for
循环中的经典用例更加匹配:正如上文所说,我们很少需要 i
的旧值。
看过后递增和前递增之间的区别后,你可能会想到:由于 i
的旧值在后递增中未被使用,因此在编译阶段,编译器将会优化掉这一行,使两个操作符等价。
对于基本类型来说(如整形)确实如此。
\n但是对于复杂类型,例如(在 C++
中)用户自定义类型或带有 +
操作重载的迭代器,编译器就无法对此进行优化了。
所以如果说在你用不到所要递增变量旧值的情况下,使用前递增运算符要好过(或等价于)后递增。
\n"},{"title":"Titan 边标签 SIMPLE 和 ONE2ONE 的区别","url":"/2017/titan-edge-label-simple-and-one2one/","content":"昨天在读 Titan 文档关于边的多样性时看到两个设置,分别是 SIMPLE
和 ONE2ONE
,这两个设置的介绍有点绕,我琢磨了很久,最终通过程序弄明白了这两种模式的区别。
SIMPLE: Allows at most one edge of such label between any pair of vertices. In other words, the graph is a simple graph with respect to the label. Ensures that edges are unique for a given label and pairs of vertices.
ONE2ONE: Allows at most one incoming and one outgoing edge of such label on any vertex in the graph. The edge label marriedTo is an example with ONE2ONE multiplicity since a person is married to exactly one other person.
先给结论,一张图来解释:
\n\ngremlin> graph = TitanFactory.open("conf/titan.properties") |
先分别创建 multiplicity
为 SIMPLE
和 ONE2ONE
的 Edge Label
,然后创建 a b c d 四个点。
gremlin> a.addEdge("simple", b) |
得到的结论是,只要两点之间不存在相同方向的 SIMPLE
边就可以。
gremlin> a.addEdge("one2one", b) |
结论是,一个点上的 ONE2ONE
边只能有一次 in
和一次 out
。
\n\n\nJava 中注解的引入改变了 Java 开发人员配置应用的方式。注解在 Java 的 1.5 版本中引入进来,它使开发人员能够在代码中维护配置而不必依赖于外部配置文件。
\n
注解是可以添加到 Java 类、方法、变量、参数或者包中的一种语法元数据。
\nSpring 框架推荐开发人员通过使用它提供的大量内置注解来配置应用。在这篇文章中,我们重点介绍 Spring Core
框架中最常用的几个注解。
这个注解用于声明类中的依赖项。基于这个注解,Spring DI 框架可以注入对应的依赖。@Autowired
可以用在构造函数、属性和 setter
方法上。它是 JSR-330(Java 依赖注入)@Inject
注解的替代方法。
下面的代码演示了如何将属性作为依赖项注入:
\nimport org.springframework.beans.factory.annotation.Autowired; |
也可以通过 setter
方法完成依赖注入,如下所示:
import org.springframework.beans.factory.annotation.Autowired; |
还可以在构造函数上使用:
\nimport org.springframework.beans.factory.annotation.Autowired; |
这个注解应用在方法上,并生成由 Spring 管理的 bean。Spring 配置类通常包含 bean 声明。通常,应用的 POJO
部分被声明为 Spring 组件,并且 Spring 提供的组件扫描机制会在 Spring IoC 容器中自动创建 bean
。但是,当 POJO
的代码不可用并且我们需要创建 Sprint 管理的 bean
时,@bean
注解就会非常有用。
以下代码演示了 @Bean
注解的用法:
import org.springframework.context.annotation.Bean; |
此外,这个注解还提供给了一些属性用以管理 bean
配置,例如:名称、初始化方法、销毁方法等。
Spring 的 @Value
注解非常有用,可以方便地使用 Spring 表达式语言(Spring Expression Language,SpEL)提供默认值或控制变量的值。
在下边的示例中,name
变量配置了 @Value
注解。如果实例化 User
类时没有为 name
提供值,就会使用 @Value
配置的默认值 default-user
。
import org.springframework.beans.factory.annotation.Value; |
下面的示例演示如何从环境中读取一个值并赋给变量:
\npublic class User { |
下边的示例演示如何使用 Spring 表达式语言来获取值并赋值给变量。注意这里使用了 #
来替代之前使用的 $
。
public class User { |
在开发的生命周期中,应用会经历多个环境和阶段。比如,dev
、test
、uat
(预发布)、industry
(生产)等。根据不同的环境和阶段,需要有不同的配置。在这种情况下,@Profile
注解非常方便,它使开发人员可以灵活地控制应该激活的组件。
@Profile("dev") |
在上边的示例中,我们提供了两个配置,一个用于 dev
环境,另一个用于 test
环境。根据环境类型,我们可以提供不同的配置,Spring 将会确保加载与之对应的配置。
@Import
注解使我们可以将一个或多个组件的配置导入到另一个配置类中。
@Configuration |
在上边的配置类中,我们导入了另一个配置类中定义的配置。
\n本文我们演示了在 Spring 应用开发中使用最频繁的一些注解。尽管有大量的 Spring 注解,但那些注解在大多数 Spring 应用中用到的不多。
\n"},{"title":"信托暴雷","url":"/2023/trust-crash/","content":"这两天有不少信托暴雷相关的新闻,之前听说「信托」过这个词,但不知道具体是什么意思,于是想探索了一下,在这里做个记录。
\n信托(Trust)是指委托人基于对受托人的信任,将其财产权委托给受托人,由受托人按委托人的意愿以自己的名义,为受益人的利益或特定目的,进行管理和处分的行为。受托人有责任确保信托资产的安全和管理。信托可以投资于各种资产,包括股票、债券、不动产等。
\n\n\n信托就像一个保险箱。当一个人有很多钱或贵重物品时,他们可能不想自己管理它们,所以他们把钱或物品放进了保险箱。信托就是这样一个保险箱,它帮助人们管理和保护他们的财产。
\n信托由一个叫做信托公司的专业机构管理。他们会根据人们的要求,把钱或贵重物品放进信托中,并负责管理它们。信托公司的工作就像一个看守人,他们会确保财产安全,并按照人们的指示处理这些财产。比如,当一个人长大后,他们可以告诉信托公司把钱用来支付学费。信托公司也可以帮助人们管理财产直到他们长大。
\n信托还有一个好处是可以帮助人们避免纳税问题。当一个人把钱放进信托时,他们可以减少需要支付的税款。这就像一个特殊的规则,可以帮助人们保留更多的钱。
\n
在国外信托的主要用途是保障资金安全,比如家族继承、公司避税、企业破产、富豪离婚这类才用得上信托,但是国内信托成了中高产家庭获取高收益的理财产品。
\n国内的销售人员在销售信托产品时夸大其词,盲目追求业绩,承诺回报率在年化8%-12%。这个操作是不是很眼熟?前几年暴雷的 P2P 也是这个套路,我一直以为 P2P 后再没有这么高的收益,没想到是因为我自己没接触到更高端的圈子才不知道这些信息。
\n如果整个池子一直有源源不断的新钱进来,或者市场行情确实不错,用这些钱投资其他产品的回报能获取更高收益,cover 住成本,整个游戏还是可以玩的,但近两年流进来的资金越来越少,其中一个原因是近几年理财产品合规性要求,这些信托产品无法再通过银行渠道销售,损失了很大的销售渠道。
\n再加上市场行情低迷,因为信托公司拿到的这些钱后,大多还是投在上市公司中,近一两年的大 A 股行情惨目忍睹,稳定在3000点左右国家都拉不动。
\n更糟糕的是还有不少储户要提现退出,出现了挤兑最后形成崩盘暴雷,这么来看本质上还是回归到了庞氏骗局。当然国内信托的初衷一定不是想做成庞氏骗局,只是在过程中产生了变形,由于体量太大最后无法挽回。
\n通过这次探索,我也刷新了对中国中高产家庭存款的认知,300万是基操,几千万很常见,最高的能到50亿。
\n理财是指通过投资来增加财富。理财产品通常包括存款、基金、债券等。理财的风险和收益因产品而异。
\n股票是公司发行的证券之一,代表着公司的一部分所有权。股票的价格在证券市场上波动,投资者可以通过买入和卖出股票来获得利润。
\n\n\n股票就像是你买了一小部分一家公司的东西,比如买了一小块蛋糕。如果这家公司做得好,蛋糕会变大,你会得到更多的蛋糕。
\n
基金是由一群投资者的资金组成的投资组合,由专业的基金经理进行管理。基金通常投资于股票、债券、商品等各种资产,以实现投资组合的分散化和风险控制。
\n\n\n基金就像是一个大大的钱袋子,里面有很多人的钱。这些钱会被专业的人士拿去买很多的蛋糕,也就是投资不同的东西。赚到的蛋糕会分给里面的每个人。
\n
私募基金是只向特定投资者销售的基金,通常要求投资者有一定的财务资格和投资经验。私募基金通常能够提供更高的收益和更高的风险,因为它们不受公开市场的监管。
\n\n\n私募基金是一种特别的钱袋子,只有一些特别有钱的人才能买。这些人把自己的钱放进去,让专业的人帮他们买更好的蛋糕,帮他们赚更多的钱。
\n
公募基金是向公众开放的基金,任何人都可以购买。公募基金通常受到监管,在投资组合和风险方面有一定的限制。
\n\n\n公募基金是大家都能买的钱袋子。任何人都可以把自己的钱放进去,由专业的人来帮助大家买蛋糕,一起分享赚到的钱。
\n
债券是企业或政府发行的借款证券,代表着借款人向债权人的债务。债券的价格通常与市场利率相关,投资者可以通过购买债券来获得固定收益。
\n\n\n债权就像是你借给别人的钱,别人会约定在一定的时间还给你。就像你借给小朋友一块糖,他会答应过一会还给你。
\n
保险是一种金融产品,向投保人提供赔偿保障。保险公司通过收取保费来为投保人提供保障。各种类型的保险产品包括寿险、医疗保险、汽车保险等。
\n\n\n保险就像是一把伞,可以帮助你在出现问题的时候得到帮助。就像下雨时,伞可以遮挡雨水,保护你不被淋湿。
\n
市场是残酷且真实的,不论你的研究多么到位、预测多么合理,面对整个市场你都是汪洋大海上的一叶扁舟,一个浪头打过来,一切可能瞬间就不复存在。哪怕你真得赢了几次,都可能是在为后面更大的失败埋下伏笔。
\n理解市场、尊重市场、敬畏市场,长久地活下去,才是获得成功的正道。
\n作为个人应该多学习理财知识,适当投资一些美股、港股,分散投资做好资产配置。还是那句话:「不要把鸡蛋放在一个篮子里」。
\n但话说回来,整个市场是个整体,没有一个人是无辜的,表面上是那些富人损失惨重,但所有人都要承担后果,有没有可能这是多米诺骨牌开始倒塌的开始?网上更惊悚的描述是「中国版雷曼兄弟」。
\n"},{"title":"关于 UDP 的 10 个问题","url":"/2021/udp-questions/","content":"不可以
\nTCP 或 UDP 数据包中的端口字段为 16 位,2^16 是 65536,所以最大的端口号是 65535。
\n是的
\nUDP 报头为 8 个字节。
\n根据 RFC,源端口是可选的,但目的端口是必须的。以下是 UDP 的报头结构:
\n<- 16 bits -> |
不可以
\nUDP 数据包的长度字段同样为 16 位,所以单个包的最大长度是 65535。
\n可以
\nUDP 数据包中可以放入任何字节,甚至可以放一个很短的 MP3 文件。
\n不能
\n没有
\n协议没有提供。
\n不能
\n即使在同一台计算机内发送,数据包仍然可能被丢弃(例如:缓冲区满了)。
\n那就真的就丢了
\n如果想在 UDP 之上实现重试,只能自己去实现。
\n操作系统的 TCP 协议实现了 TCP 包的重试。
\n不一样
\nUDP 和 TCP 都支持相同的端口号(1-65535),但它们是不同的协议。
\n你可以同时在 UDP 的 80 端口和 TPC 的 80 端口运行 2 个不同的服务。
\nDNS、DHCP, QUIC, NTP, statsd 和各种视频会议协议。
\n","tags":["udp"]},{"title":"关于 Unix 权限的 13 个问题","url":"/2021/unix-permission-questions/","content":"12位
\n分为 4 个组,每组 3 位。
\n例如,4755
对应的是 100 111 101 101
。
下面是各部分对应的内容:
\n100: setuid, setgid, sticky bits |
读、写、执行
\n每个文件有 3 套 读/写/执行
权限:
不能
\n0644
在二进制中是 000 110 100 100
。
说明如下:
\n000 |
所以任何人都可以读取该文件,但只有拥有该文件的用户才可以写文件。
\n不关心
\n内核基于 用户ID/组ID
进行所有的权限检查 —— 用户名和组名的存在只是为了让人类更容易识别和使用。
意味着你可以列出该目录中的文件
\n对于目录来说,下面是读/写/执行
的含义:
0666
,这是否意味着任何人都可以阅读它?不一定
\n如果该文件的父目录的执行位
被置为 0,这将使你无法读取该目录下的任何文件。
不是
\nroot 可以读写权限为 0000 的文件。
\n是的
\n当你以用户身份登录时,几乎你启动的所有进程都会把它们的 UID 设置为你的 UID。
\n是的
\n进程有一个主 GID,也有一个补充(supplementary)组ID列表。文件权限检查将检查进程的任何一个组ID是否与文件的所有者匹配。
\n不会
\n退出并重新登陆后才会生效。
\nsetuid
位的作用是什么?在一个可执行文件上,它意味着该进程将以文件所有者的 UID 运行
\n例如,passwd(用来修改密码)通常设置了setuid位,因为它需要以 root 身份运行,以便能够写入修改密码的文件。
\n不可能
\n你必须有超级用户的权限来改变你的 UID。
\n它设置了 setuid 位
\nsudo 总是以 root 身份运行。
\n所以如果 /etc/sudoers
允许你以 root 身份启动程序,则它将以 root 身份为你启动程序。
因为我的 bossku ,需要定期将全量数据库数据进行备份,我之前写过一篇文章分享我是如何将数据库备份到 S3 的:https://jiapan.me/2020/auto-backup-database/
\n由于不想为这个存储付费,所以我在 Things 中创建了一个周期性的任务,每周六提醒我来清理前一段时间的过期数据,通常我只保留最近两天的,将其余的删除。
\n最开始我是登录到 S3 的网站上进行操作,后来嫌麻烦,就将 S3 挂载到了本地(使用的是 QSpace 这个软件),每周六定期在本地进行删除操作。
\n本着 DRY(Don’t repeat yourself)原则,能自动化的事就不要自己重复去做,所以我准备写个脚本定期处理。
\nS3 提供了很完善的 API 可以让程序方便的进行操作,各个语言也都提供了 S3 API 的 SDK 封装,我要做的就是周期性的调取文件列表,判断如果文件超过 2 天则进行删除。这样的动作使用 Serverless 最合适不过了,这一次我还是选择使用我最熟悉的 AWS Lambda ,使用的语言也是万能、灵活的 Python。
\n首先我们初始化一个 Serverless 项目:
\nSLS_GEO_LOCATION=en serverless create --template aws-python --path s3-clean |
没有 serverless
的可以先参考官方手册 进行安装,这不是本文的重点。
注意下上边命令最前边的 SLS_GEO_LOCATION=en
,这个一定要加,因为 serverless 做了一件有些流氓的事:判断你的所在地是中国的话,会走腾讯的服务,他们是没有 aws 模板的,报错如下:
加上 SLS_GEO_LOCATION=en
可以将我们的地区强制指定到国外,这样就会走官方的逻辑(腾讯这个行为太 low 了)。
进入上边 serverless
为我们创建出来的项目,可以看到生成好了两个文件,我们来编辑 handler.py
文件:
import os |
流程比较简单:
\n上边代码中一些参数通过环境变量进行获取,稍后我们会在配置文件中介绍这几个参数。
\n然后我们编辑 serverless.yml
文件,这个是我们服务的配置文件:
service: bossku-s3-clean |
provider.region
用来指定我们的服务启动在哪个地区,我这里配置了一个 S3_REGION
的占位,用来从我本地环境变量获取,目的是和我们的 S3 在同一个地区,这样理论上连通性会跟好一些。
provider.environment
就是给程序提供运行时环境变量的地方,也就对应我们程序中 os.environ['xxx']
,每一个我都和本地一个同名的环境变量相关联。
S3_REGION
表示存储文件时使用的 S3 区域,比如:ap-east-1S3_BUCKET
用来指定程序要读写的 bucketS3_ACCESS_KEY_ID
S3 API 的 ACCESS KEYS3_SECRET_ACCESS_KEY
S3 API 的 SECRET KEY再往下的 functions
是用来声明函数的区域,我将我们的 delete_backup
关联了两个事件:
做完这些我们还有一个工作,将程序所依赖的 boto3
安装在项目目录下,这样就会在发布时会一起上传到 Lambda 中,Lambda 本身是不带这个包的,而且不支持 pip 安装。
pip install boto3==1.23.8 --target=. |
接下来就可以部署到 Lambda 进行验证了,我们可以先将程序中的 item.delete()
进行注释,观察下日志看看流程是否正常。
部署脚本如下:
\nexport AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID |
如图可以看到发布成功了,我们访问 Lambda 提供给我们的 endpoint 来手动触发下这个函数。
\n\n成功了,我们再到 Lambda 的操作界面看下日志,我通常是在函数的【监控】-【查看 CloudWatch 中的警报】-【日志组】中看日志,应该有我不知道的更方便的方式,后边学会了再做补充吧。
\n\n可以看到整个流程是 ok 的。
\n我们将 item.delete()
的注释取消掉再发布一次就可以了。
为了在每次删除后都能及时的收到通知,我通过 Bark 给我的手机发个通知。我们只需将 delete_backup
函数改成这样就可以了:
def delete_backup(event, context): |
别忘了在本地目录安装 requests
包:
pip install requests==2.27.1 --target=. |
再次发布,然后我手动触发两次,第一次文件超过 2 个所以可以执行成功,第二次文件不足两个执行失败,符合我们的预期。
\n\n这样我就不用再在每周六手动清理这些文件了。
\n"},{"title":"利用 AWS Labmda 推送博客评论","url":"/2022/use-aws-lambda-push-blog-comment/","content":"昨天写了篇博客 《 如何写作》 ,在这篇文章中我翻译了别人的一篇短文,同时加了点自己的叙述。
\n晚上用手机浏览这篇博客的时候发现收到一个新留言,赶紧打开电脑进行了回复。
\n\n让我兴奋的是,没想到有人会看我的博客,而且还能指正我理解不到位的地方。在被人关注并且能收获积极反馈的情况下会给我们正向激励,让我们更愿意做一些输出。
\n我博客的评论系统后端是用 Leancloud 做的存储,默认不支持通知,所以之前我并没有太关注过评论,甚至不知道哪些文章有评论,如果需要的话就到 Leancloud 后台去看看。
\n\n为了以后能更及时的接收与回复评论,我准备给博客评论加个监控,当有新评论时通过 Bark 提醒我,有朋友之前实现过这个功能,但我忘记怎么做的了,索性这次重新再造一个。
\n这次继续使用 AWS 的 Lambda 运行我们的服务,关于 Lambda 的使用姿势可以看下我前几天的一篇文章:《利用 AWS Lambda 定期清理 S3 文件》 ),这里直接介绍实现细节。
\n流程图如下:
\n\n说明下如何判断有没有新评论:这里我们继续借助 Leancloud 的存储,为了不影响 Comment
表,我们新建一个 BarkComment
表来存储已经发过通知的评论,只需在 Class 名称处填入 BarkComment
即可,其他保持默认:
我们在通过 SDK 写入数据时会自动帮我们将需要的列创建出来,所以也不用做列的新增。
\n再来看下核心代码:
\nimport os |
看过之前文章的已经知道,我们需要将 leancloud
的 SDK
在项目目录下也下载一份:
pip install leancloud==2.9.10 --target=.
provider.environment
新增本次需要的环境变量:
provider: |
functions
空间内添加:
functions: |
表示每 5 分钟执行一次,同时提供给我们一个 HTTP 终端来进行调试。
\n我们将这个服务进行发布,手动访问下分配给我们的 endpoint,通过下图可以看到我已经收到了通知:
\n\n因为之前 BarkComment
表中没有数据,所以系统认为这 10 条都是新评论。
然后我自己在博客里留了 3 条评论,等待每个 5 分自动执行的时候再看下效果:
\n\n在 14 点 25 分时,我们收到了「博客收到3条新评论」的通知,说明我们的程序成功判断出了新的增量数据,同时可以检查下 BarkComment
表也确实有了新数据。
其实我们可以做的更完善一些,比如通知我们具体是哪篇文章有新评论,评论的内容是什么等等。大家需要的话可以自己实现,我只在这里进行抛砖。
\n"},{"title":"使用 gist 管理动态配置","url":"/2023/use-gist-manage-config/","content":"在上一篇文章中提到,我找到了一个非常方便的方法来管理token,那就是使用Github提供的 Gist 功能。
\nhttps://gist.github.com/ 是 Github 的一个子服务,通常用于托管或分享一些代码片段。与 git 不同的是,无需创建仓库,一个文件就是一个 gist。在打开 gist 首页后,可以直接填写文件描述和文件内容。
\n\n点击右下角会默认创建一个私密的 Gist,但它并不是真正的私密,Github 只是保证其他人在不知道这个 Gist 链接的情况下看不到其中的内容,且里面的内容不会被搜索引擎索引。当你分享这个 Gist 链接后,任何拿到链接的人都可以访问它。
\n例如,我刚刚创建的 gist 链接是:https://gist.github.com/Panmax/5e3444141772e987719147a316782f54
\n通过浏览器的无痕模式打开这个链接:
\n\n如果我是这个文件的所有者,还可以对文件内容进行更新,在浏览界面点 edit 按钮,或者直接在 url 最后拼上 /edit
访问,如: https://gist.github.com/Panmax/5e3444141772e987719147a316782f54/edit 就可以进入编辑页面。
链接尾部加上 /raw
可以取得原始内容。
如:https://gist.githubusercontent.com/Panmax/5e3444141772e987719147a316782f54/raw/
\n\n通过上述特性,我们可以将Gist用作动态配置的管理工具。
\n程序可以通过使用 /raw
获取动态内容,我们可以使用 /edit
页面更新内容。理论上,只要不泄露 Gist 链接,您的内容就不会泄露。当然,还要确保源代码不会泄露。
有些人可能认为这样不安全,因为数据存放在互联网上,获取链接的人就可以访问数据。
\n但我不这样认为。我们可以将 Gist 链接后面的路径看作数字钱包的私钥。如果你泄露了私钥,谁也帮不了你。只要你妥善保管私钥,通常情况下就没有问题。通过碰撞来暴力破解路径的成本极高,而且里面的内容大部分情况下只是一个简单的代码片段,比数字钱包私钥的价值要小得多。所以黑客们不会费力不讨好地去破解这个东西。
\n"},{"title":"用现金","url":"/2023/using-cash/","content":"前两天萌发了一个想法,随着电子支付越来给方便,现在的孩子会不会对金钱越来越没有概念?
\n他们只看到大人在买东西时用手机扫个二维码就可以把东西拿走,好像我们没有减少任何东西、没有任何损失,这种情况恶化后,孩子可能就会出现看到什么就想买什么的情况。
\n《黑客与画家》这本书的作者有个观点:
\n\n\n以前的青少年似乎也更尊敬成年人,因为成年人都是看得见的专家,会传授他们所要学习的技能。如今的大多数青少年,对他们的家长在遥远的办公室所从事的工作几乎一无所知。他们看不到学校作业与未来走上社会后从事的工作有何联系。
\n
我们家之前是个体户,自己开门市的,所以我每天都能看到父母在做什么,怎么赚钱。那时候即便父母是在上班或者在事业单位工作,孩子们也有机会到父母工作地点去参观,了解父母的工作内容。现在这种机会非常少,尤其在一二线城市。
\n对孩子来说,如今坐办公室工作的父母就是个黑盒,除了看到父母早出晚归,其他就一无所知了,也无法通过观察父母来学习。
\n以前家里的桌椅板凳小电器坏了、衣服破了,大多是由父母自己来修补,还有句老话:「新三年,旧三年,缝缝补补又三年」。现在大部分父母的做法都是找人上门维修,或者干脆不要了、直接换新的,孩子们不再对大人产生崇拜感,自然也不会有之前的那种尊敬。
\n基于上边的原因,我准备做一些尝试:在孩子面前使用现金。让他们对钱的概念更具象。要让他们看到在买东西时是使用了物理上的钱来交换的,看到父母从钱包或者口袋里掏钱的动作,买了东西之后,钱包或者口袋里的钱会减少。
\n同时也要让他们知道钱有不同的面值,拿大面值买东西,可能会找回小面值,小面值再花出去就没了,大面值的钱尺寸也更大,小面值的钱相对较小。
\n这样做目的也不是为了让孩子们节俭、少花钱,而是让他们对钱有概念,从小产生理财意识,孩子对于买东西的渴望是无尽的,要让他们去思考什么该买什么不该买,当然这也是作为大人要思考的。
\n"},{"title":"virtualenvwrapper 安装 与 iPython for Python2","url":"/2017/virtualenvwrapper-%E5%AE%89%E8%A3%85-%E4%B8%8E-iPython/","content":"这篇文章完全是要写两件事:
\nvirtualenvwrapper
后如何配置Python 2
上安装 iPython
如果分成两篇文章来写的话,每篇文章就会非常短,不值当的,所以直接合成一篇来写。
\n安装 virtualenvwrapper
的过程就不再讲解了,直接 pip install
就可以完成,主要是安装完成后的配置,因为每次我装完都需要问一下谷歌然后才能继续,所以不如记到自己的 Blog 下,即便下次再忘了也能快速找到解决方法。
安装完 virtualenvwrapper
后,要根据自己使用的 shell
来配置不同的文件,比如 bash
需要配置 .bashrc
、zsh
配置 .zshrc
。
配置如下:
\nexport WORKON_HOME=$HOME/.virtualenvs |
第一行是给定一个虚拟环境保存的目录,第二行是执行 virtualenvwrapper
的脚本使 workon
, mkvirtualenv
等命令生效。
大多数时候都卡在第二行那个命令上,因为不同发行版的机器 virtualenvwrapper.sh
所在位置不同,所以需要通过:
find / -name virtualenvwrapper.sh
找到 virtualenvwrapper.sh
所在的位置后,根据自己机器上的实际位置来写那一行脚本。
修改完成后保存退出,重新启动一个命令窗口,检查有没有配置成功。
\n最新版的 iPython
已经不支持 Py2 了,所以直接用 pip
安装 iPython
时,会提示安装失败,所以要手动指定安装版本。
最后一个支持 Py2 的版本是 5.4.0
,所以用 pip install ipython==5.4.0
就行了。
\n\nUPDATE AT 2017-07-19
\n
今天在一台服务器上将 pip 改为了阿里源后发现安装 ipython==5.4.0
时会报错:
Running setup.py egg_info for package ipython |
这个问题使用 pip install pip --upgrade
将 pip 更新为最新版本就可以解决了。
顺便再记一下 pip 源的地址,虽然知道修改方法,但每次还要去网上搜一下源地址
\nvi ~/.pip/pip.conf
[global] |
最近一段时间学习了一下 Go 这门语言,其中提到最多的就是 GoLang 的高性能 & 高并发,所以本着没有对比就没有伤害的原则,我准备将其与另外两个我所掌握的语言(Python、Java)进行一个简单的性能对比。
\n我的 MacBook Pro,12个逻辑CPU + 16G内存
\nwrk -t8 -c100 -d30s --latency http://www.baidu.com
模拟8线程、100个并发,持续30秒的性能测试
\n\n\n以下程序完整源码已放在 GitHub:https://github.com/Panmax/web-benchmark
\n
框架:Flask
容器:Gunicorn
运行环境:Docker
# -*- coding: utf-8 -*- |
FROM ubuntu:14.04 |
这里设置 24 个 worker,因为我的机器有 12 个逻辑CPU
\ndocker build -t panmax/docker-flask-benchmark . |
框架:SpringBoot
\n容器采用 SpringBoot
的默认 tomcat
容器,不进行其他修改。
package com.jpanj.benchmark; |
server: |
./gradlew build -xtest |
框架:Gin
\n不需要配置任何容器
\npackage main |
go build . |
go build 可以直接编译出一个可以执行文件,这个二进制文件可以直接放在其他机器上无需安装任何环境就可以运行起来,甚至可以在 Mac 上编译 Linux / Windows 的可执行文件,在 Linux 上编译 Mac / Windows 的可执行文件,这个特性非常爽。
\n通过浏览器可以验证以上使用 3 种语言开发的简单 Web 程序已经启起来了:
\n\n➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8081/ |
➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8082/ |
➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8083/ |
可以看到,在每秒请求数量(Requests/sec),也就是并发能力方面,测试结果为:
\n线程平均延迟(Thread Stats - Avg - Latency)的测试结果为:
\n可以看出,Go 在性能方面甩出 Python 几十条街是没有问题的,比 Java 的性能确实也好很多。
\n\n\n"},{"title":"微信登录流程梳理","url":"/2018/wechat-login/","content":"最后说明一下,这个测试可能存在不严谨性,但是我所采用的部署方案是大部分公司或者程序员最常使用的方式,也能在一定程度上说明问题。
\n
如果没什么意外,接下来要实现一个对接微信登录的需求,今天浏览了下相关文档,简单在这里用自己的文字把整个流程描述一下。因为还没有实际操作,所以可能会有理解上的偏差,真正实现后会再来修改和补充。
\n文档链接:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
\n首先在微信公众平台中的「开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息」设置一个回调 URL,这里配置一个二级域名就行了,例如 wechat.xxx.com
。
用户在登录时,先通过 snsapi_base
接口获取用户 open_id
,然后在我们的系统中通过这个 open_id
来获取用户信息,如果存在则直接完成登录,因为这种方式是用户无感的,使用体验比较友好。但这种方式无法获取用户最新的微信信息,不过通常来说不重要,因为用户会在应用内重新维护自己的资料,所以首次拿到用户头像昵称等信息后就可以了,之后不需要再通过重新获取用户信息来更新了。
如果查询 open_id
不存在的话,再通过 snsapi_userinfo
方式来获取用户信息,获取用户信息的流程如下:
snsapi_userinfo
授权页面,用户点击登录按钮,然后系统会访问我们的回调地址并带上一个 code
。code
换取 access_token
和 refresh_token
,可以把 refresh_token
保存起来,这样的话近 30 天内都可以通过这个直接换取 access_token
来拉取用户资料,但通常没必要。access_toekn
+ open_id
拉取用户资料。拉取完资料后将用户 open_id
和资料保存在我们自己的数据库中,这个步骤可以看具体业务逻辑,大部分业务会在首次登录时将用户资料拉取下来展示在一个编辑页面,用户可以编辑后再提交保存,不管那种方式我们都已经为这个 open_id
创建了用户,这样之后用户再访问我们的应用时就无需再点授权按钮了。
人们常说减肥是一辈子的事情,我以前也是这么认为的。我也一直在为减肥而努力,最早从大学就开始努力了(for a girl)。直到今年放下了,不再那么挣扎了。
\n因为减肥,我养成了一个习惯:记录体重。
\n尽管现在的体重还在超重线以上,但也不再追求降低体重了,减几斤肉实在太难了。虽然不以减肥为目的,现在仍然每天称一次体重,已经养成了一个固定的习惯。
\n\n我使用「瘦身旅程」APP 来记录体重,最早一次的记录产生于2015年8月1日,记录半年后断了2年。之后又持续记录,从最早的记录点算起到现在已经8年了。
\n我曾经最重达到90kg,最轻的时候是65kg。最高和最低相差25kg,也就是50斤。这么大的跨度我说自己减肥成功过不过分吧?
\n把时间拉到一个月为纬度时,可以发现一个规律:每周一早上的体重为波谷,周六早上的体重为波峰。这是因为周一至周五为工作日,作息相对规律,中午还会有运动,饮食也比较注意,所以周六早上的体重是一段时间内的最低点。
\n周六日的放纵会导致体重反弹至高点。周六、周日两天我会出去找好吃的,炫冰淇淋、可乐、炸鸡。我知道这是个非常不好的习惯,但很难改掉。五天的工作让我的欲望被压抑,只能在这两天得到释放。
\n\n持续记录体重有好处,可以观察身体变化。如果今天体重低了,可以回想一下前一天做了什么;如果今天体重高了,也可以回忆一下昨天的饮食。虽然不再追求减肥,但仍在努力阻止熵增。我知道在没有外力的作用下,我的体重只会无限制上涨。
\n此外,持之以恒地记录使我每次看到历史跨度很长的趋势图时都很有成就感。
\n我养成的另一个长期习惯是学习英语。以前我用了「不背单词」这个APP,在连续365天后解锁了其中所有的权益,后来突然有一天感觉没有意思,就卸载了。
\n现在我在使用「多邻国」学习英语,它不需要背单词,而是通过语境学习每个单元,包括完整的句子、对话和语法。到今天,我已经持续学习了将近500天。
\n\n我每天学习英语的时间点是早晨上厕所(shit)的时候,利用10分钟学习,时间刚刚好可以学到获得当天宝石的进度。选择这个时间点的最主要原因是,这段时间是当天最早一次长时间使用手机的时间段,我不想把每天最早的宝贵时间用在刷无用的内容上,而是用学习英语当做一天好的开始。
\n上完厕所、学完英语后就是就是称体重环节,两个习惯就这么串起来了。这种叠加方式也是「掌控习惯」这本书中介绍过的培养习惯的一个非常好的方式:继【当前习惯】之后,我将会养成【新习惯】。
\n将学习英语和称体重这两个习惯作为每天早上的固定仪式,就像是生活中的支点一样,因为生活和工作的节奏太快,需要给自己找到一些确定性,以便能够掌控自己的生活。虽然这两件事很小,但它们帮我获得了很大的掌控力。
\n这种超长期周期的持续也让我明白了一个道理:
\n\n\n"},{"title":"我获取信息的 6 个渠道","url":"/2022/what-i-access-to-information/","content":"坚持一件小事,是靠意志力;长期坚持一件小事,是靠习惯。
\n坚持一件大事,是靠价值观;长期坚持一件大事,是靠信仰。
\n
我日常获取信息主要有如下 6 个渠道:
\n我在上大学期间做过的最有价值、对我人生影响最大的一件事,可能就是掌握了科学上网的手段,并且从大二开始没有再用过百度搜索,把搜索这个任务全部交给了Google ,毕业工作后在 Google 的加持下,我的工作产出和效率应该能比初入职场使用百度的同辈高出 30%,我认为做技术人员的第一步就是弃用百度、使用 Google。至今我仍然会为了提升自己的访问外网的体验,每年投入上千元在购买线路、软件这件事上。
\n但 Google 终究是个搜索引擎,是我们在遇到问题时使用的工具,接下来我要说的是另一个可以让我们被动获取信息的,等同于国内微博的 Twitter。在 Twitter 上言论相对自由,因为 Twitter 是个面向全世界的产品,所以在这里可以关注很多国内外的技术牛人(没错,也包括国内,由于国内的言论控制很多国内牛人也不在国内平台发言了),看他们的分享,而且信息的时效性很高。同时在上边活跃的国内圈子里的人,大部分都很 geek,愿意分享自己的发现的新玩意或者自己造的新轮子,我通过他们可以获取一手信息,玩到最新的玩具。
\n有人会说在 Twitter 上看不到自己需要的内容,那是因为你关注的人还不够多,或者还没关注到你想关注那个圈子里的 KOL,稍微耐心一些,再投喂给 Twitter 算法一些你的偏好,早晚能进入你的圈子,看到一个新世界。
\n今年是我使用 Twitter 的第十个年头,我经常在推上看到让我眼前一亮的内容,给我提供新点子、新工具、新观念。之前会偶尔随手点个 like,但这样在回顾时不便于索引和分享,最近尝试将内容收集到 Notion,方便自己也方便分享给更多的人看∶ https://panmax.notion.site/286a2dbe19ca4a8badcf2e06470964a6 ,这个列表我会随时、持续更新。
\n今年 NewsLetter 在国内有流行起来的趋势,国内也出现了做 NewsLetter 业务的平台,如竹白,我也订阅了一些 Letters,有免费的也有付费的,下边分享几个我觉得质量不错的 Letter(排名不分先后),这些 Letter 我基本都是通过 Twitter 发现的:
\n我会在上班想摸🐟时翻看一下近期的 NewsLetter,我统一用 Google 邮箱接收这些信件,同时设置好了规则,让这些信件汇总在一个目录下,并且不会实时给我发推送(因为我并不需要立即阅读它们)。
\n\n我用掘金提供的插件作为浏览器新标签页的默认首页,这个页面中间一栏有 GitHub 上的项目列表,我会不定期看一些我关注语言(如 Go、Rust、Python)又出了哪些新玩具,按热门排序,如果想看新鲜有趣的就看今日,如果想看长盛不衰的就看本周或者本月。
\n\n上边提到的掘金的 GitHub 项目列表,是按照 Star 增量和项目创建时间计算得出的,所有人看到的都是相同的静态数据,GitHub 官方也有自己的两个 Feed 流,分别叫 Following 和 For you。
\nFollowing 是看你关注的人的动态(如他关注了什么项目、他贡献了什么项目)
\n\nFor you 是 GitHub 今年新推出的根据我们的喜好,使用算法推荐给我们的与我们相关、我们可能感兴趣的项目。
\n\nTelegram channel 是个小宝藏,类似于一个除了群主其他人禁止发言的群,群主产生优质内容后随时发到群里,列几个我自己常看的 channel:
\n上边介绍的那些方式获取的信息大多具有时效性,我们不仅要掌握时效信息,还要掌握能经历岁月洗礼的信息,这就要靠读书了,我读书不挑种类,只有一个前提,这本书在豆瓣上的评分要在 9 分以上,我不想把时间浪费在低质量的书上。额外情况是,如果我关注或者敬佩的人推荐了一本书,并且他对书的介绍吸引了我,我也会去读一下。
\n我还会去挑一些经典书目来读,因为这些书经过时间的淘洗,回应了人类社会最根本的问题,具有跨时代的意义。
\n我认为读书这件事没有太多捷径和技巧,拿到一本书后按部就班一页一页读就好,我从来没用过网上介绍的那些快速阅读方法,当然在读的过程中手里拿支笔写写画画是有必要的。对了,一定要读纸质书,原因见:纸质书赢了
\n我的书单:https://jiapan.me/book-list/
\n互联网时代从来不缺乏免费的内容,最珍贵的资源是我们的时间。不要花太多工夫读那些免费、廉价,但是质量低的内容,读它们不仅浪费时间,甚至会误导我们。
\n"},{"title":"我想的事情其他人已经想到了","url":"/2022/what-i-think-other-already-think/","content":"因为6月份工作有些变动,本来计划的7月份要做的甲状腺复查打算趁着这段时间的空窗期趁早先做一次,避免7月份太忙不方便请假,所以挂了今天早上的号去医院。
\n医生给我开了一系列检查项目和药品后,我就去缴了费并打印了发票。其中有一项彩超,约的是下周一上午10点,今天可以先做抽血,抽血后今天的事情基本就结束了,然后我想到如果彩超约上午10点,可能会和早会冲突,当时医生给了我几个选项,上午10点、下午1、2、3点,但我习惯性地选择了第一个。所以我打算找医生去帮我换成下午1点,这样可以不请假就能把检查做了。
\n我在回去找医生的路上看了下检查单,上边已经标记了预约好的时间,当时就在想医生该怎么帮我改时间呢,单子会重新打吗?还是在系统中改了时间就行,单子上可以不改?反正替医生想了好几种方案,最后找到医生后,医生用了一个最简单的方案:直接开张新的,重新缴费,旧的那张去人工口办理退费。
\n好吧,这个问题看来医院已经考虑到了,那么下一个问题又开始在我脑子里旋转了,我已经把之前的费缴了,退费和缴费的内容是同一个项目、同样的价格,这样的话是否还需要我再缴一次费?而且我已经打印了发票,这个已经打印的发票如何处理。进一步我又想到,如果我把之前的项目退了,但是并不给新的缴费,那么后边在走商保报销的时候是不是就会有漏洞:可以把退了的钱也给报了。脑海中又替工作人员想了几种方案。
\n到了人工口,办理方式也是很简单粗暴,医生查询到我之前的缴费单已经打印过发票,要求我先把发票交回去,但是我的发票上是把今天所有项目打在了一起的,如果收回去后只给我把这次新缴费的打印发票,那我剩下那些就没办法报销了。带着这个问题我把发票递给了工作人员,工作人员看后说这个发票上的所有项目需要统一退费,再重新缴一次费,又是一个既简单有清晰的处理方式,这让我想到了软件设计模式中的 KISS(Keep It Simple, Stupid)原则。
\n我想的事情其他人已经想到了,而且有了最优解。
\n"},{"title":"何时使用事件溯源","url":"/2019/when-to-use-event-sourcing/","content":"\n\n\n事件溯源也许很不错,但这确实增加了系统的复杂性。
\n
大多数 Web 应用将系统状态存放在数据库中。
\n假设让你来做一个在线购物网站的数据库设计,按照传统的数据库设计方案将会有 users
、products
和 orders
表 —— 用来代表系统的状态。
再假设你完成了前期的编码工作并发布上线了这个在线购物网站,几周后你的老板想知道用户平均更新电子邮箱的次数。
\n在这种传统的数据库设计中,当用户更新电子邮箱时,执行的查询语句大致如下:
\nUPDATE users SET email='newemail@mail.com' WHERE id=1; |
问题在于,我们并没有在数据库中存储修改电子邮件的事件日志。
\n你可以创建一个额外的列 event_log
并在每次用户更新电子邮箱后记录一次 user changed email
。但这样仍存在一些问题:
这是让事件溯源派上用场的绝佳场景。
\n根据事件溯源设计,你不需要存储系统状态。取而代之的是存储事件。
\n比如:当用户注册时,一个 UserCreated
事件被存储。之后当用户更新电子邮箱时,一个 UserChangedEmail
事件被存储。
在现实世界里,人们思考的是事件:当其他人询问你今天过得怎么样时,你会告诉他们一些发生过的有趣事件,不大可能描述你在某一时刻的确切状态。
\n同样,一个领域专家在描述业务流程时谈论的也是一系列事件。通过事件溯源让我们在系统中对其建模变得更加容易。
\n想知道用户修改了多少次电子邮箱?通过事件溯源,你已经将这些数据详尽记录在案了。
\n想知道一个商品在购物车中被用户移除了多少次?只需简单的对 EventRemovedFromCart
事件进行计算就可以了。
通过事件溯源,你可以对你的数据进行全方位的洞察,同时使生成的报告可以追溯。
\n你可以生成审计日志用来准确记录系统如何进入了某个状态。
\n比如,考虑一下你的银行账户,事件溯源生成了交易事件的日志,这样就可以清楚的说明为什么你每个月的工资都不够花了。
\n听起来不错,但事件溯源有什么要注意的呢?
\n事件溯源增加了系统额外的复杂性,更多的复杂性意味着难以让新进入的开发人员上手,花更多的时间添加新功能并且也让系统更难以维护。
\n如果你要构建的是一个规模较小的系统,不需要安全审计日志,此时使用事件溯源方法带来的麻烦可能比它的价值更大。
\n"},{"title":"为什么我说宝玉是双子座","url":"/2023/why-baoyu-is-gemini/","content":"我在上一篇流水账中提到贾宝玉是双子座,虽然宝玉生日在《红楼梦》原文中一直没有明确写明,但通过一些线索可以推断出宝玉生日是在夏天,且是在农历四、五月左右。证据如下:
\n不管哪个证据,宝玉肯定是农历四月底五月初的生日,既阳历(公历)五月底六月初。我按照农历四月二十六这个日期,随机查了几个农历对应的公历(能力有限,只能查到1900年以后的),都是落在双子座的时间范围内。双子座的时间范围是公历5月21日~6月21日。
\n\n\n\n\n"},{"title":"为什么 DDD 是设计微服务的最佳实践","url":"/2019/why-ddd-is-best-practice-desgin-for-micro-service/","content":"在本人的前一篇文章《不要把微服务做成小单体》中,现在很多的微服务开发团队在设计和实现微服务的时候觉得只要把原来的单体拆小,就是微服务了。但是这不一定是正确的微服务,可能只是一个拆小的小单体。这篇文章让我们从这个话题继续,先看看为什么拆出来的是小单体。
\n在微服务架构诞生之前,几乎所有的软件系统都是采用单体架构来构建的,因此大部分软件开发者喜欢的开发路径就是单体架构模式。在这样的背景下,根据经济学和心理学的路径依赖法则,当这些开发者基于新的技术想要把原来的大单体拆分成多个部分时,就必然会习惯性地采用自己最擅长的单体架构来设计每个部分。
\n\n\n\n路径依赖法则:是指人类社会中的技术演进或制度变迁均有类似于物理学中的惯性,即一旦进入某一路径(无论是「好」还是「好」)就可能对这种路径产生依赖。一旦人们做了某种选择,就好比走上了一条不归之路,惯性的力量会使这一选择不断自我强化,并让你轻易走不出去。第一个使「路径依赖」理论声名远播的是道格拉斯・诺斯,由于用「路径依赖」理论成功地阐释了经济制度的演进,道格拉斯・诺斯于 1993 年获得诺贝尔经济学奖。「路径依赖」理论被总结出来之后,人们把它广泛应用在选择和习惯的各个方面。在一定程度上,人们的一切选择都会受到路径依赖的可怕影响,人们过去做出的选择决定了他们现在可能的选择,人们关于习惯的一切理论都可以用「路径依赖」来解释。
\n
在现实中我们经常看到这个法则随处都会发生,微信刚出来的时候很多人说这不就是手机上的 QQ 吗,朋友圈刚出来的时候他们又会说这不就是抄袭微博吗。很多时候当你兴致冲冲给朋友介绍一个新的东西时,朋友一句话就能让你万念俱灰:这不就是 XXX 吗?之所以这样,是因为人类在接触到新知识新概念的时候,都会下意识的使用以前知道的概念进行套用,这样的思维方式是人类从小到大学习新事物的时候使用的模式,它已经固化成我们大脑操作系统的一部分了。
\n理解了这个法则,我们就可以很容易的明白,已经在单体架构下开发了多年的软件工程师,当被要求要使用微服务架构来进行设计和开发的时候,本能的反应方式肯定是:这不就是把原来的单体做小了吗?但是这样做出来的「微服务」真的能够给我们带来微服务架构的那些好处吗?真的能提高一个企业的数字化响应力吗?
\n不断变化的软件需求和经常被视为效率低下的软件开发一直都是这个行业里最难解决的顽疾,从瀑布到敏捷,都是在尝试找到一个解决这个顽疾的方法,领域驱动设计(Domain Driven Design)也是其中一个药方,而且随着十多年的不断实践,我们发现这个药方有它自己的独特之处,下面我们先来介绍一下这个药方。
\n领域驱动设计这个概念出现在 2003 年,那个时候的软件还处在从 CS 到 BS 转换的时期,敏捷宣言也才发表 2 年。但是 Eric Evans 做为在企业级应用工作多年的技术顾问,敏锐的发现了在软件开发业界内(尤其是企业级应用)开始涌现的一股思潮,他把这股思潮称为领域驱动设计,同时还出版了一本书,在书中分享了自己在设计软件项目时采用的建模方法,并为设计决策者提供了一个框架。
\n但是从那以后 DDD 并没有和敏捷一样变得更加流行,如果要问原因,我觉得一方面是这套方法里面有很多的新名词新概念,比如说聚合,限界上下文,值对象等等,要理解这些抽象概念本身就比较困难,所以学习和应用 DDD 的曲线是非常陡峭的。另一方面,做为当时唯一的「官方教材」《领域驱动设计》,阅读这本书是一个非常痛苦的过程,在内容组织上经常会出现跳跃,所以很多人都是刚读了几页就放下了。
\n虽然入门门槛有些高,但是对于喜欢智力挑战的软件工程师们来说,这就是一个难度稍为有一点高的玩具,所以在小范围群体内,逐渐有一批人开始能够掌控这个玩具,并且可以用它来指导设计能够控制业务复杂性的软件应用出来了。虽然那时候大部分的软件应用都是单体的,但是使用 DDD 依然可以设计出来容易维护而且快速响应需求变化的单体应用出来。
\n\n到了 2013 年,随着各种分布式的基础设施逐渐成熟,而 SOA 架构应用在实践中又不是那么顺利,Martin Fowler 和 James Lewis 把当时出现的一种新型分布式架构风潮总结成微服务架构。然后微服务这股风就呼呼的吹了起来,这时候软件工程师们发现一个问题,就是虽然知道微服务架构的应用具有什么特征,但是如何把原来的大单体拆分成微服务是完全不知道怎么做了。然后熟悉 DDD 方法的工程师发现,由于 DDD 可以有效的从业务视角对软件系统进行拆解,并且 DDD 特别契合微服务的一个特征:围绕业务能力构建。所以用 DDD 拆分出来的微服务是比较合理的而且能够实现高内聚低耦合,这样接着微服务 DDD 迎来了它的第二春。
\n下面让我们站在软件工程这个大视角看看 DDD 究竟是在做什么。
\n从计算机发明以来,人类用过表达世界变化的词有:电子化,信息化,数字化。这些词里面都有一个「化」字,代表着转变,而这些转变就是人类在逐渐的把原来在物理世界中的一个个概念一个个工作,迁移到虚拟的计算机世界。但是在转变的过程中,由于两个世界的底层逻辑以及底层语言不一致,就必须要有一个翻译和设计的过程。这个翻译过程从软件诞生的第一天起就天然存在,而由于有了这个翻译过程,业务和开发之间才总是想两个对立的阶级一样,觉得对方是难以沟通的。
\n\n于是乎有些软件工程界的大牛就开始思考,能不能有一种方式来减轻这个翻译过程呢。然后就发明了面向对象语言,开始尝试让计算机世界有物理世界的对象概念。面向对象还不够,这就有了 DDD,DDD 定义了一些基本概念,然后尝试让业务和开发都能够理解这些概念名词,然后让领域专家使用这些概念名词来描述业务,而由于使用了规定的概念名词,开发就可以很好的理解领域业务,并能够按照领域业务设计的方式进行软件实现。这就是 DDD 的初衷:让业务架构绑定系统架构。
\n\n上面介绍了使用 DDD 可以做到绑定业务架构和系统架构,这种绑定对于微服务来说有什么关系呢。所谓的微服务拆分困难,其实根本原因是不知道边界在什么地方。而使用 DDD 对业务分析的时候,首先会使用聚合这个概念把关联性强的业务概念划分在一个边界下,并限定聚合和聚合之间只能通过聚合根来访问,这是第一层边界。然后在聚合基础之上根据业务相关性,业务变化频率,组织结构等等约束条件来定义限界上下文,这是第二层边界。有了这两层边界作为约束和限制,微服务的边界也就清晰了,拆分微服务也就不再困难了。
\n\n而且基于 DDD 设计的模型中具有边界的最小原子是聚合,聚合和聚合之间由于只通过聚合根进行关联,所以当需要把一个聚合根从一个限界上下文移动到另外一个限界上下文的时候,非常低的移动成本可以很容易地对微服务进行重构,这样我们就不需要再纠结应不应该这样拆分微服务?拆出的微服务太少了以后要再拆分这样的问题了。
\n所以,经过理论的严密推理和大量实践项目的验证,ThoughtWorks 认为 DDD 是当前软件工程业界设计微服务的最佳实践。虽然学习和使用 DDD 的成本有点高,但是如果中国的企业想在软件开发这个能力上从冷兵器时代进入热兵器时代,就应该尝试一下 DDD 了解一下先进的软件工程方法。
\n\n\n"},{"title":"为什么用个人博客","url":"/2023/why-use-personal-blog/","content":"本文转自:https://www.jianshu.com/p/e1b32a5ee91c 并修改了几处错别字
\n
为什么我要用一个关联了个人域名,毫不起眼的个人博客写流水账,而不是在时下更流行的公众号、简书、知乎之类这些平台发布?有以下几个原因:
\n我有时会写一些不想在日常中表达的内容,这些内容不太想被熟悉的人看到。
\n但互联网是公开的,既然我把内容发在了网上就应该有被看到的准备,即使未来某一天看到了其实也不要紧。
\n使用自己的博客想写点什么就写点什么。
\n我的这个博客域名 jiapan.me 在国外注册、没有实名、没有备案,所以基本上没有被审查的风险,但我通常也会遵纪守法,所幸我的域名还没有被墙,站点也托管在 Cloudflare,国内大部分地区也是可以正常访问的。
\n使用自己的博客想怎么魔改就怎么魔改。
\n博客实际上是个网站,只要你懂点前端就可以对自己的站点就行修改,如果用的是一个开源的博客生成工具,那么可以直接使用其他人提前写好的主题,那么多主题总有一款符合你的审美。
\n再搭配上独一无二的域名,更能体现出个性了。
\n公众号后台可以看到粉丝数、浏览量这些数据,这无形中给了写作者很大的压力,每次写文章都会考虑这篇文章可以涨几个粉、能带来多少阅读量之类的数据。
\n我这个站点干脆不统计这些数据,我不在意有多少人读、多少人访问。不用每天绞尽脑汁去想如何打造10万+,如何打造爆款。
\n使用自己的博客想写什么主题就写什么主题。
\n在平台上写作还要为垂直领域而困扰,不同领域要迎合不同的读者,在个人网站上就没这个困扰了,我的地盘听我的。
\n在这里,我心情不好时可以吐槽,有了兴趣可以聊聊技术,郁闷时可以抒发感情,激情时可以干一碗鸡汤。
\n在商业平台内发布,如果这个平台需要变现的话,会在你的文章中间或者四周插入一些广告,有些广告会很 low,很影响阅读体验。
\n除了平台会插入广告,在一些平台上写作时写作者也可以允许平台插入广告,和平台进行广告收益分成,我目前没有将写流水账当成个营收手段的计划。
\n有一个词叫「私域流量」或者「私域变现」,我总觉得这种词带一些诈骗性质,我天然反感这种硬生生造出来的词。好多人说做公众号就是做私域,这也致使我反感去运营一个公众号。
\n使用自己的博客想什么时候写就什么时候写。
\n在平台上写,发布是一个问题,登录后台困难重重,要经过好几道验证,发布的时候也很麻烦,需要确认一堆内容,而且大部分平台不支持Markdown,但程序员最喜欢的编辑格式就是Markdown。
\n在公众号发文章,还有发布频率限制,修改起来也很麻烦,何必受这个窝囊气。
\n公众号这类的平台采用的是 push 模式,写完一篇文章后会 push 给你的订阅者,订阅者们会陷入在一个围墙内,只能看到他们订阅的内容,逐渐生成知识壁垒,每次收到 push 来的新内容还会产生焦虑感。
\n博客类的站点才用的是pull 模式,内容写完后就放在这里,各位看官想什么时候看就什么时候看,在你需要的时候来它就静静的在这里。
\n这也跟我的性格相符,我喜欢自己做决定,不喜欢被 push 的感觉。
\n微信只是国内的一个聊天工具,虽然国外用的也比较多,但绝大部分还是中国用户。包括知乎、简书这些平台,我不保证它们在50年后还能活着,如果它真的不在了,用户在上边发布的内容是不是也就不在了。
\n自己搭建一个博客,给域名续上几十年费,页面托管在一个国际主流服务商上,比如 Cloudflare或者 Github,基本就可以永存了。
\n在平台上写作需要遵守平台规范、更加谨言慎行,一个不注意触犯了平台上的限制那篇文章可能就没了,更严重一些的整个账号就没了,意味着之前发布的文章跟着受到了牵连,之前经营的成果付之一炬。
\n大部分平台,尤其是公众号,是很封闭的,这也意味着你写的内容在搜索引擎上检索不到,不仅搜索引擎搜不到,出了微信就很少看到了。
\n我发现谷歌对独立的博客还是很友好的,我有好几篇流水账通过某个关键字可以排在首页,而且有几篇我随手记问题解决方案帮助过很多人。
\nWeb 和域名都是伟大的发明,就像我前边说的,微信未来有一天会死去,但 Web 和域名服务一定会长期留在这个世界上。
\n虽然我是个悲观主义者,但我还是非常向往活着,我希望我的这些碎碎念能一直留在这个世界上,证明我存在过。
\n就像《寻梦环游记》里所说的:死亡不是生命的终点,遗忘才是。
\n"},{"title":"为什么健康宝不支持微信扫码?","url":"/2022/why-wechat-not-support-scan-health-code/","content":"前天我发了一条朋友圈,内容是:『我有一个问题想问下圈里做客户端开发的朋友:健康宝扫码是个特别频繁的使用场景,为什么微信不支持用自带的「扫一扫」自动跳转到健康宝,而是必须先打开健康宝,使用健康宝扫码?』这条朋友圈收到了不少评论,大家集思广益站在不同角度给了很多回答,为了不辜负大家的劳动成功,我在下文对这些回复做个分类总结,并结合自己的想法来说说我对各个原因的判断。
\n很多评论都认为不能微信扫码的原因是技术上不支持,比如有说 URL 限制的、有说健康码和微信二维码不兼容的还有说数据不互通的等等。
\n首先我站在技术角度发表下我的观点:技术上没有任何障碍,健康码本质上也是二维码,二维码的原理都是相同的,就是将一段字符串生成一个设备容易识别的图片。应用扫描图片时先解析成文本,根据文本内容执行不同的动作就可以了,并且二维码和文本互转的编解码标准是统一的。
\n另外我朋友圈中居住在北京以外的其他城市的小伙伴,如深圳、新疆、广西都表示他们城市的健康宝是支持直接用微信「扫一扫」的,这直接打破了技术不支持的“谣言”。我的问题也应该改为「为什么北京健康宝不支持微信扫码?」才更精确。
\n有评论说因为北京健康宝是北京的,不是国家的,所以微信不愿意花精力来做这个适配,但你看人家深圳不就支持吗,当然可以说因为腾讯总部就在深圳,近水楼台,但广西、新疆也支持了又该怎么说。
\n有朋友提到,不让用微信扫一扫是为了提升小程序列表的渗透率,这个脑洞开的相当大。
\n我们每次打开健康宝都要先下拉展开小程序列表,其他的小程序也会不可避免的被我们看到,同时可以培养我们下拉打开小程序列表的使用习惯,小程序的使用率会大大增加。
\n我认为这个原因的可能性也不大,首先在这种关头微信是不会冒着个险来发这么蝇头小利的“国难财”的,还有上边也提到了其他城市是可以用微信二维码扫描的,微信并没有对他们做限制。
\n这个是有可能的,评论区有几个人也提到:「又不是不能用」、「反正体验不好你也要用」。
\n这条就不展开聊了,不用恶意来揣摩别人,我只是说有可能但不一定。
\n当然还有可能是开发效率低下,当前小程序等待迭代的需求排的太多了,这个低优的功能还没有排上开发日程。
\n打个岔:不知道北京健康宝的开发人员是不是在事业单位做了个有铁饭碗的程序员?
\n最后再来说两个我觉得比较靠谱的原因,第一个原因是为用户安全着想。
\n大家都知道微信「扫一扫」支持的场景很多,比如扫商品条码、花草、动物等等,我们最常用的场景是使用扫一扫付款,这几年电子支付几乎渗透到了每一个人。产品经理担心出现恶意换码,比如把健康码换成支付码或者其他 URL,这样有可能给人们的财产带来损失,尤其是对智能手机使用不太熟悉的老年人。即便是跳转到其他地方,也会给当时扫码的人们和企业主带来不便。为了避免这种情况的出现,健康宝干脆就只在自己的应用内识别自己的专码,来提升安全性,自然也不会推荐让用户通过微信「扫一扫」进入。
\n我认为比较靠谱的第二个原因就是信息安全问题,如果要让微信「扫一扫」支持跳转,肯定要对外暴露接口(或者 schema),这都或多或少地增加了风险。如果接口鉴权没有做好再加上被图谋不轨的人发现了这个漏洞,那么人们的信息就会有暴露的风险(责任全在美方)。
\n综上,我认为北京健康宝没支持微信扫码的原因有以下三个(排名分先后):
\n昨天是我的生日,最近也是我很长时间以来最灰暗的一段时期。
\n再前一天是我来这家公司的第1000天。
\n这段时间一个人在北京,昨天凌晨一点吃了片安眠药胡乱睡了几个小时。
\n最近组内突然离职很多人,有出国的、有回老家躺平的。
\n我去年因为不想做太多管理工作,并且希望在技术上更精进一些,所以在去年这个月份申请转岗到了现在的新组。
\n但转过来后还是有一部分精力花在虚线带人上,技术上也没什么长进。
\n因为上级离职,最近又转成了实现带人,但是之前的虚线组也还挂在我身上,实线人员招齐后会带一个10人团队,加上虚线组一共能有25人,是在有点应付不过来,打算跟老板聊聊把虚线分出去。
\n最近业务需求压力也非常大,接需求的带宽又很小,我每天忙的脚不沾地,恨不得把自己分成几个人用,跟同事开玩笑说我快学会飞了。
\n\n这段时间的作息是:
\n希望这段灰暗的时间赶快过去,一切都会好起来的。
\n此刻心情非常down,花几分钟时间记录一下,也算有个释放的口子,自己诉一诉苦。
\n想到基督山伯爵里的一句话(虽然我还没看过全书):
\n\n\n"},{"title":"werkzeug.middleware.proxy_fix.ProxyFix 问题复盘","url":"/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/","content":"人类的一切智慧是包含在这四个字里面的:「等待」和「希望」
\n
很多人知道我在运营着一个 SaaS 站点:https://bossku.cn/,用来给中小商家提供进销存服务,对于没有特殊需求的商户来说基本是免费使用的,目前注册商家 5000+,平稳运行 5 年多了。为了降低成本,中间做了好几次迁移,最早是在阿里云,后来迁移到了新浪的SAE,后边因为腾讯云有活动,又迁移到了腾讯云。
\n这段时间由于疫情的原因在家办公,这周末时间比较充裕所以就看了看 Sentry 日志准备改几个 bug,改 bug 花了 15 分钟,踩坑踩了大半天,接下来我把这次遇到的坑进行一下复盘。
\n这个项目是刚毕业没多久开始写的,那个时候也不懂什么自动化运维的东西,每次改完代码都是手动把新代码更新到服务器上(项目用 Python 写的,所以线上服务器 git pull
一下再重启一下 gunicorn 就可以了)。
迁移到 SAE 后就完全不用考虑运维的事情了,项目目录下写好描述文件,把代码推到指定地址上就可以完成一次发布了(甚至连负载均衡、HA 这些都不用考虑,很省心)。用了两年 SAE 感觉成本还是有些高,毕竟这个项目并没有太多收入,正好又看到腾讯云的活动,算了一下如果迁移到腾讯云可以实现收支平衡甚至能有些小盈,于是大概两年前把项目迁移到了现在在用的腾讯云上,那个时候已经对自动化和容器化有意识了,在调研一些方案后,最后选择了把项目进行容器化,借助 DaoCloud 实现持续发布。
\n这个方案的实现流程很简单:
\n因为那时候 Github 的私有仓库还不是免费的,所以我用了国内的 coding 作为项目代码的托管仓库,完成迁移后,每过一段时间就改几个小 bug,小日子一直安逸地前进着。由于去年下半年以来工作比较紧张,就没有再去管过这个项目了,在这期间 coding 貌似被腾讯收购了,中间的很多流程发生了变动,每次登录后都会跳转到腾讯开发者的页面,我也并没有太关心。
\n直到昨天我再去维护的时候,发现之前的配置的 git 地址失效了,DaoCloud 上的 coding 授权也失效了,再去关联也关联不上(吐槽一下,这么严重的问题这么长时间了都没发现吗),DaoCloud hook 不到我的代码更新,自然也不能完成后续的流程,所以我在第一时间把源码迁移到了 Github 的 private repo(话外语:如果之前 Github 的私有仓库是免费的我肯定也不会用国内的)。
\n由于 DaoCloud 无法修改项目所关联的 Git 地址,只能删掉重新创建新的项目来关联新的地址,都配置好后又出了幺蛾子, DaoCloud 的镜像仓库挂了,镜像打包后无法 push 到仓库,自然也就无法发布应用了,当时找了客服,客服没有回复(直到我写这篇 blog 的时候,一天过去了客服依然没有任何响应)。
\n\n\n下午的时候再去看,仓库恢复了,项目启动后却报错了:
\n[2020-02-08 20:50:15 +0000] [10] [ERROR] Exception in worker process |
其实这个原因写的很清楚了,/code/wsgi.py
文件的第 5 行包导入失败。因为在处理前期问题时花费了很长时间,已经没有太多耐心了,而且在我本地直接启动是没有问题的,所以没有太关心项目代码的报错,只注意到了最后的:
ImportError: No module named contrib.fixers |
Google 后发现遇到这个问题的人很少,而且根据几个有限的回答进行修改尝试后也都无济于事,比如更新 pip 和 setuptools 等。
\n于是我就开始自己找问题。最开始怀疑的是 Python 源有问题,我一直使用的是阿里的源:
\nRUN pip install -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com |
我尝试换成豆瓣源和清华源都不行,再后来我怀疑是 DaoCloud 打的包有问题,可是自己在本机打包还是有这个问题,再往后我又尝试修改镜像的 Python 的版本,因为这个项目用的 Python2,最新的 Python 版本是 2.7.17,我尝试降到 2.6、2.7.16、2.7.15等都不起作用,我甚至把上午写的代码进行了回滚,这个问题依然存在,这个时候心态有些爆炸了,于是准备探究下 werkzeug.contrib.fixers
这个包究竟是做什么的,于是在 Google 上搜索了这个包名,就在这时我看到一句话使我眼前一亮:
点进去后:
\n\n这个 ProxyFix 类已经在 werkzeug 1.0
版本中移除了,通过查看我的 requirement.txt
的文件:
celery==3.1.24 |
可以看到我并没有指定要安装 Werkzeug
的版本,Werkzeug
是一个 WSGI 工具包。熟悉 Flask
的同学都知道,Flask
依赖了 Werkzeug
,大部分情况下都只需要安装 Flask
就可以直接使用 Werkzeug
这个工具包了。
查看 Flask
的源码,setupy.py 部分内容如下:
setup( |
可以看到 Flask 声明了自己需要依赖 0.7 以上的 Werkzeug,这个时候答案已经浮出水面了。
\n因为我本地的依赖包是很久之前安装的,所以我本地的 Werkzeug 版本是 0.14.1 的:
\n➜ pip freeze | grep Werk |
因为 DaoCloud 打包时有缓存层,从第一次构建完后,pip install 那一层就被缓存了下来,所以后边的都是用的缓存,安装的包也都是老版本,但是我昨天把 DaoCloud 上原有项目进行了删除、创建了一个新项目,之前的那些缓存层也就失效了,重新 pip install
时自然会去安装最新版本的 Werkzeug
,这时候就安装了 1.0 以上版本,所以我在代码里引用的 ProxyFix
就找不到了。
我在部署时采用了 Nginx 作为反向代理,这时候需要重写一些 HTTP 头来让应用正常工作,如:
\nserver { |
Nginx 添加了一些请求头来辅助应用获取真实的请求来源,这个时候需要用到 ProxyFix
处理一下这些请求头来让 WSGi 去正确处理这个请求,否则 WSGI 就可能认为这个请求来自服务器而不是真实的客户端。
所以我在程序入口前加入了:
\nfrom werkzeug.contrib.fixers import ProxyFix |
修复这个问题有两种做法:
\nwerkzeug.middleware.proxy_fix
导入 ProxyFix
类。Werkzeug
的版本号。我选择了第二种方式,原因是我不清楚新版本的 Werkzeug
还会有哪些不兼容问题,于是我在 requirements.txt
中安装 Flask
前加入了一行 werkzeug==0.14.1
,重新在本地 Docker build
并启动,一切正常,问题解决了。
回顾整个问题的解决过程,还是因为自己太浮躁导致的,只想着快速把问题解决,而没有踏下心来看看每一行报错提示,更没有想着去官方文档中看看这个类库有没有变动,其实一开始也压根没想到是因为依赖的类库做了不兼容更新导致的。
\n通过解决这个问题,还让我知道了为什么很多地方推荐 requirements.txt
这个文件要通过在本地执行 pip freeze > requirements.txt
来生成(我通常也都是这样做的),这样生成的描述文件中依赖所依赖的那些包的版本号也会固定下来。但是在这个项目中我为了追求精简和美观,采取了手动维护这个文件:每新增一个依赖,就手动在这个文件内新增一行,这就导致了依赖所依赖的那些包的版本可能在一台新机器上重新安装时发生变动。
原文地址:http://www.ruanyifeng.com/blog/2006/12/why_i_keep_blogging.html
\n到今年12月为止,我写Blog已经满3年了,一共写了接近600篇,平均每2天写一篇。今后应该还会继续写下去。
\n3年前,我开始写的时候,并没有想过自己会坚持这么久。3年中,也遇见过几次有人问我”为什么要写Blog?”
\n是啊,为什么要写Blog?毕竟这里没有人支付稿酬,也看不出有任何明显的物质性收益。
\n\nDarren Rowse在他的Blog上,讲到了7个理由,我觉得说得很好。
\n1. 学会写作Blog的技巧(teach you the skills of blogging)
\n没有人天生会写Blog,我刚开始的时候也不知道该怎么写。但是,经过不断的尝试,现在我知道怎么可以写出受欢迎的文章。
\n2. 熟悉Blog工具(familiarize you with the tools of blogging)
\n写作Blog,可以选择自己搭建平台,也可以选择网上的免费Blog提供商。我曾经试用过不少Blog软件,最后才选择了现在的Moveable Type,这本身也是一个学习过程。
\n3. 便于更好地安排时间(help you work out how much time you have)
\n写作Blog花费的时间,要比大家想象的多,甚至也比我自己想象的多。但是,另一方面,每天我们又有很多时间被无谓地浪费了。坚持写作Blog的过程,也是进行更好的时间安排的过程。
\n4. 便于你了解自己是否可以长期做一件喜欢的事情(help you work out if you can sustain blogging for the long term)
\n很多人都有自己的爱好,但是只有当你享受到这种爱好时,你才会长期坚持下去。写作Blog可以帮助你体验到这种感觉。
\n5. 便于体验Blog文化(give you a taste of blogging ‘culture’)
\nBlog的世界有一种无形的礼仪、风格和用语。熟悉它们,会使你更好地表达自己和理解他人。
\n6. 便于你形成和了解自我(help you define a niche)
\n长期写作Blog最大的好处之一就是,写着写着,你的自我会变得越来越清晰。你最终会明白自己是一个什么样的人,以及自己热爱的又是什么东西。
\n7. 帮助你找到读者(help you find a readership)
\n与他人交流是生命最大的乐趣之一。写作Blog可以帮助我们更好地做到这一点。
\n如果你觉得你想说的东西不适宜让他人知道,你可以在自己的电脑里写,不用放到网上。这样除了上面第7点以外,其他6点的好处也还是适用的。
总之,正是因为以上7个理由,所以我强烈建议,每一个朋友都应该有一个自己的Blog,尝试将自己的生活和想法记录下来,留下一些印记。
之前已经做过对 Blog 静态资源的优化,但是都没有进行记录,今天又做了一个比较实用的优化,赶紧记一下。
\n我打开 Chrome 的调试器进入站点时,看到 Network 里有一条访问 fonts.gstatic.com
,这个请求应该是访问的 谷歌 CDN,但是国内的访问速度是非常不友好的,由于我平时都是开着 Surge 所以没什么感觉。
这个资源是在由主题渲染成静态站点时插入进来的,因为我用的是自己修改的 Next
模板,所以在 themes/next
目录下查找带 fonts.gstatic.com
关键字的文件竟然没有找到,很是费解。后来我又看那条请求,发现它的 referer
是 fonts.googleapis.com
,所以我又尝试用这个关键字进行查找,最后在 themes/next/layout/_partials/head/external-fonts.swig
找到了它。
{% if font_families !== '' %} |
然后我在网上找了找 fonts.googleapis.com
国内镜像,有两个用的比较多的:360网站卫士 和 中科大。但是一些地方写到 360网站卫士 提供的源不支持 HTTPS,虽然我的博客现在并不是 HTTPS 的,但保不齐以后我要改呢。所以我选择使用中科大镜像。只需用 fonts.lug.ustc.edu.cn
替代之即可。然后再 hexo g
重新生成下站点就可以了。
下边是几个常用的替代镜像:
\n然鹅,在我用了一段时间后,发现中科大的源速度我也不满意,最终将那个 css
文件包括 css
文件里边用到的 ttf
文件都下载到了本地,使用本地路径来路由,这样的话我部署到七牛后的速度比之前快了很多。
在 themes/next/source/css
下新建 fontes
目录,把 https://fonts.proxy.ustclug.org/css?family=Lato:300,300italic,400,400italic,700,700italic&subset=latin,latin-ext
下载到 fonts
目录并重命名为 fonts.css
,然后把里边的 url 对应的 ttf 文件也都下载到此目录,并修改 fonts.css
内的 url 地址为本地地址:
最后将之前修改的 themes/next/layout/_partials/head/external-fonts.swig
文件中的地址改为:/css/fonts.css
对于上了 HHKB 这条贼船的人来说,刚开始使用起来最大的别扭可能就是没有方向键的问题了。
\n最早的我使用 Karabiner 来解决,里边有一些内置的组合可以替代方向键,我用 control + hjkl
(同vi) 替代四个方向键,因为 HHKB 的 control 在 caps lock 的位置,所以使用起来还是很舒服的,But 当系统升级到 macOS Sierra 后,Karabiner 就不能工作了,作者也在官网中写了:
\n\nKarabiner does not work on macOS Sierra at the moment.
\n
同时也给出了替代方案,使用 Karabiner-Elements,但是新版的 Karabiner 并不支持这样的组合,所以我就又走上了寻找解决方向键之路。
\n后来找到了 Keyboard Maestro(简称 KM) 这个神器,这个软功能非常多,不过我只用了里边的设置组合键的功能,我自定义了 5 个组合,用来解决 HHKB 中的不方便的方向键问题。
\n分别是 control + hjkl
来操作方向和 control + delete
来反向删除(也就是删除光标后边的内容),但是用起来有些问题:不能连击(比如按住 control + h
光标不可以一直前移,需要手动敲击多次),然鹅就在我将就用了小一年后,今天尝试将触发方式 is pressed
改成了 is down
,成功解决了不能连击的问题,所以 HHKB 方向键 的问题现在可以说是完美解决了。
不过我现在并没有将 Karabiner-Elements 删掉,因为里边有一个比较实用的功能:可以在插入外置键盘时禁用内置键盘的功能,防止意外点击。因为我之前是把 HHKB 垫到 Mac 上用的,经常不小心按住了某个键,现在我把 Mac 放在了支架上,已经不会再出现这个问题了,不过我还是留下了它。
\n最后上一张设置截图(我将 KM 中自带的其他组合全都关闭了,只留下 5 个我自己写的组合):
\n\n"},{"title":"使用 Supervisord 实现进程监控","url":"/2018/%E4%BD%BF%E7%94%A8-Supervisord-%E5%AE%9E%E7%8E%B0%E8%BF%9B%E7%A8%8B%E7%9B%91%E6%8E%A7/","content":"Supervisord 是用 Python 实现的一款非常实用的进程管理工具,supervisord 还要求管理的程序是非 daemon 程序,supervisord 会帮你把它转成 daemon 程序,可以管理和监控类 UNIX 操作系统上面的进程。它可以同时启动,关闭多个进程,使用起来特别的方便。
\nsupervisor 主要由两部分组成:
\n在有 Python 环境的 Linux 机器上(基本上所有 Linux 发行版都有),直接通过 sudo easy_install supervisor 即可完成安装。
\n然后初始化配置文件:
\nmkdir /etc/supervisor |
所有需要管理的进程需要在上边的配置文件中进行管理,但是都放在一起并不是一个好主意,一旦管理的进程过多,就很麻烦。
\n所以一般都会新建一个目录来专门放置进程的配置文件,然后通过 include 的方式来获取这些配置信息
\n修改配置文件,在最下边加上:
\n[include] |
并且新建相应目录:mkdir /etc/supervisor/conf.d
然后可以通过 supervisord 命令启动 supervisord
\n$ ps -ef | grep super |
可以看到 supervisord 已经被启动了, 然后进入 supervisorctl 的 shell 界面。
\n$ supervisorctl |
由于目前没有添加任何需要管理的进程,所以 status 没有输出任何结果,接下来我们添加一个需要管理的进程:
\ncd /etc/supervisor/conf.d |
然后运行以下命令更新配置并启动进程:
\n$ supervisorctl reread (只更新配置文件) |
来检查下 cat 进程有没有真的启动了:
\n$ ps -ef | grep cat |
然后杀掉这个进程号,再次检查有没有重启:
\n$ kill -9 6537 |
vi eyes-tw |
然后再次执行:
\n$ supervisorctl reread |
可以看到我们需要监控的项目进程已经启动成功,其他项目按照上边的 conf 模板添加就行了。
\n我把其他几个项目的配置文件写好后,执行 reread、update,完成了所有项目的启动。
\n现在 supervisorctl status 如下:
\neyes-hk RUNNING pid 32180, uptime 1:09:14 |
命令详解
\n这段时间在调研 MySQL HA 方面的东西,看到大多数实现方法都是通过虚IP + IP 漂移实现,所以打算先将此过程实现一下。
\n虚IP,就是一个未分配给真实主机的IP,也就是说对外提供数据库服务器的主机除了有一个真实IP外还有一个虚IP,使用这两个 IP 中的任意一个都可以连接到这台主机,所有项目中数据库链接一项配置的都是这个虚IP,当服务器发生故障无法对外提供服务时,动态将这个虚IP切换到备用主机。这个切换的过程我们称之为IP漂移
\n其实现原理主要是靠 TCP/IP 的 ARP 协议。因为 IP 地址只是一个逻辑 地址,在以太网中 MAC 地址才是真正用来进行数据传输的物理地址,每台主机中都有一个 ARP缓存,存储同一个网络内的IP地址与 MAC 地址的对应关系,以太网中的主机发送数据时会先从这个缓存中查询目标 IP 对应的MAC地址,会向这个 MAC 地址发送数据。操作系统会自动维护这个缓存。这就是整个实现的关键。
\n我们可以通过 Keepalived 来实现这个过程。 Keepalived 是一个基于 VRRP 协议(Virtual Router Redundancy Protocol,即虚拟路由冗余协议)来实现的LVS(负载均衡器)服务高可用方案,可以利用其来避免单点故障。
\n一个 LVS 服务会有2台服务器运行 Keepalived,一台为主服务器(MASTER),另一台为备份服务器(BACKUP),但是对外表现为一个虚拟IP,主服务器会发送特定的消息给备份服务器,当备份服务器收不到这个消息的时候,即主服务器宕机的时候,备份服务器就会接管虚拟IP,这时就需要根据 VRRP 的优先级来选举一个 backup 当 master,保证路由器的高可用,继续提供服务,从而保证了高可用性。
\nlc1: 172.24.8.101 |
我们现在要实现添加一个虚IP:172.24.8.150
,当 lc1
机器正常时,172.24.8.150
指向 lc1
,当 lc1
出现故障时指向 lc7
。
此时通过 ping
可以看到 172.24.8.150
是无法 ping
通的。
$ sudo yum install -y keepalived |
lc1
的配置
$ cat keepalived.conf |
lc7
的配置
vrrp_instance VI_1 { |
sudo systemctl restart keepalived.service |
sudo systemctl enable keepalived.service |
通过 ping 172.24.8.150
发现已经可以通了。
$ ip addr show enp7s0f0 |
其中可以看到 inet 172.24.8.150/32 scope global enp7s0f0,说明现在 lc1 是作为虚拟IP的 master 来运行的。
\n$ ip addr show enp7s0f0 |
此时 lc7 中没有虚拟IP 的信息。
\nsudo systemctl stop keepalived.service |
$ ip addr show enp7s0f0 |
可以看到 lc1 已经不在有 虚拟IP 的信息了。
\nip addr show enp7s0f0 |
可以看到 lc7
的 IP信息中 已经有虚拟IP 172.24.8.150
的信息了。
此时如果再把 lc1
上的 Keepalived 启动,可以看到 虚拟IP 又重新绑定到了 lc1
上。
根据这篇教程:http://blog.csdn.net/cl_andywin/article/details/53998986 使用 spring-boot-starter-security
集成了 CAS 单点登录的功能,但是发现一个问题是,单点退出一直不成功。
然后我就拿出各种 debug 手段,最后发现问题是服务器在收到 POST
请求时需要 CSRF
验证(CAS 在单点退出时是发送的 POST
请求),原因是使用了 spring-security
导致的,它默认是开启 CSRF
的,所以解决办法就是关掉这个特性。
在重写的 configure()
方法最后加上 http.csrf().disable();
就行了。
__slots__
的用法,想起项目中的更新用户资料相关代码可以用上边的知识(适配器模式, __slots__
, __setattr__
)优化一下:class UserModel(object): |
class Account(object): |
这段时间的工作内容,让我更加体会到「情景」的重要性。
\n想把事情做好,就要有一个已经存在的情景设定,空穴来潮地去做一定做不完美,或者做着做着会失去动力。
这段时间学习了很多图数据库知识,接触了一下Titan,深入学习了 Neo4j 。用 Hadoop/Spark 写了一些大数据处理工具。
因为有上亿级的数据需要处理,所以不得不使用分布式计算引擎;为了将上亿级的数据导入到图数据库,不得不使用 Neo4j 的初始化导入工具;为了将一些增量关系通过计算后追加到图中,不得不学习通过编码的导入方式;为了满足产品需求,不得不学习各种复杂的查询语句,不断 debug 语句的正确性;因为有上亿数据的存在,不得不学习如果优化语句性能,在合适的地方添加索引。以上这些都是在工作内容这个场景下进行的,因为有了这个场景,推动着我不断的探索和进步,如果没有这个场景,我就算会去自己学习,也不会学的这么深入。
\n还可以从另一个方面说一下场景的重要性:如果你想开发一个软件/工具来让大家用,也需要一个场景,要么当下你用得到,要么你的家人或者朋友用得到,否则我觉得空想是做不来的。
\n"},{"title":"单点登录流程梳理","url":"/2017/%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95%E6%B5%81%E7%A8%8B%E6%A2%B3%E7%90%86/","content":"之前研究了一段时间的单点登录系统,在这里做一下流程上的总结吧。
\n先说下我对几个词的认识:我觉得 统一认证、单点登录、集中认证、统一登录 这几个词的想表达的目的都是一样的,都是提供一个登录中心或者叫认证中心的地方,当某个系统需要用户进行登录时,统一跳转到这里来进行处理。
\n进入正文:
\n假定一个场景,现在有系统A(a.com)、系统B(b.com)、和认证中心(sso.com)。我们想实现的效果是,其中一个系统登录一次后,访问其他系统的需要登录页面时无需再次手动提交帐号密码。
\n注意:我这里说的是无需再次输入帐号密码,内部的登录流程还是要执行的,只是不需要用户的参与。
\n下边是基于 CAS 的 SSO 的流程介绍:
\n用户通过浏览器访问系统A www.a.com/pageA
,这个 pageA
是个需要登录才能访问的页面,系统A发现用户没有登录,这时候系统A需要做一个额外的操作,就是重定向到认证中心: www.sso.com/login?service=www.a.com/pageA
。
这个 service
参数的作用其实可以认为是一个回跳的 url
,将来通过认证后,还要重定向到系统A,所以其实用 redirect
可能更合适一些,但是在这里还有一个作用就是注册服务,简单来说注册服务为的是让我们的认证中心能够知道有哪些系统在我们这里完成过登录,其中一个重要目的是为了完成单点退出的功能,单点退出的一会我再来介绍。
接下来浏览器会用 www.sso.com/login?service=www.a.com/pageA
访问认证中心,认证中心一看没登录过,就会展示一个登录框让用户去登录,登录成功以后,认证中心要做几件重要的事情:
session
ticket
(可以认为是个随机字符串)www.a.com/pageA?ticket=T123
与此同时之前建立 session 对应的 cookie 也会发送给浏览器,比如: Set cookie : ssoid=1234, sso.com
到这里会产生一个疑惑,为什么认证中心要写一个 cookie
,其他系统由于跨域的限制根本读不到它啊。
对于这个问题的回答是, sso.com
产生的 cookie
不是给其他系统用的(至于是给谁用的一会会说明),注意那个 ticket
,这个东西是个重要标识,系统拿到以后需要再次向认证中心验证。所以 ticket
才是系统们要用到的东西。
系统A拿着这个 ticket
,去问下认证中心:这是您签发的 ticket
吗,认证中心确认无误后,系统A就可以认为用户在认证中心登录过了。这时候系统A应该为这个用户建立 session
然后返回 pageA
的资源。也就是说,系统A也需要给浏览器发一个属于自己的 cookie:Set cookie : sessionid=xxxx, a.com
。这时候浏览器实际上有两个 cookie,一个是系统A发的,一个是认证中心发的。
当用户再次访问系统A的另一个需要登录的页面时,因为系统A已经在浏览器中放入了自己的cookie,就知道它登录过了,不需要再次到认证中心去了。
\n\n接下来看看,当用户访问系统A时已经通过认证中心登录了,再访问系统B www.b.com/pageB
时是什么样的情况。
其实和首次访问 www.a.com/pageA
非常类似,唯一不同就是不需要用户输入用户名密码来登录了,因为浏览器已经有了认证中心的 cookie,直接发送给 www.sso.com
就可以了。这里解释了我上边提到的认证中心写入浏览器 cookie 的用途。
同样,认证中心会返回 ticket
,系统B需要做验证。
整个流程的本质是一个认证中心的 cookie
,加上多个子系统的 cookie
而已。
下边来说说单点退出的原理。
\n单点退出的作用是用户在一个地方退出,所有系统都要进行退出。这怎么来实现呢。还记得我前边提到的注册服务吗?没错,就是使用之前登录时给认证中心传的 service
参数,认证中心记录下来都有哪些系统进行过登录,当用户访问认证中心的 /logout
需要退出的时候,认证中心需要把自己的会话和 cookie 干掉,然后给之前注册过那些服务的地址发送退出登录的请求,默认是对根路径发一个 POST
请求,Body
中携带一些字段,比如比如之前登录时用到的ticket
,这时候各个子系统根据传过来的这个 ticket
来将对应的用户 session
干掉即可。
所以用户在系统A点击退出登录后,系统A取消本地会话然后重定向到认证中心的退出登录地址,剩下的交给认证中心来处理就好了。这里也可以传一个回跳地址参数,当认证中心完成退出后,可以再跳会到设置的地址。
\n\n\n我觉得用户在一个系统中退出登录时,系统此时结不结束会话其实都可以,因为最终还是要被认证中心调用一次退出。
\n
说一个我做单点退出时遇到的坑,由于我把 CAS Server 部署在了机房中的一台设备上,然后在我本地启了一个 WEB 服务,这个时候登录填的 service
是 127.0.0.1:8081/xxx
登录完之后的重定向是没有问题的,但是当我访问 CAS Logout 页面后,再访问我的系统,发现并没有退出登录,也没有访问退出登录的记录。原因是我注册服务时的 127.0.0.1
这个url认证中心根本访问不到。
后来我在本地起了一个 CAS Server 再次验证后没有问题。
\n还有人问道那个 ticket
如果被其他人截获了,岂不是就可以冒充我来登录了?并不会。
首先来说,默认情况下,CAS 要求子系统和它之间的通讯为 https
,再有就是这个 ticket
只有一次有效性,验证一次后即失效,而且有效期还很短,默认我记得只有5秒。最重要的是即便这个 ticket
被其他人获取了也没啥用,验证这个 ticket
时,还需要带上申请这个 ticket
时的 url
信息,而且认证中心鉴定 ticket
为真后也只是返回用户的用户名、认证时间等最基本的信息,由于子系统没有拿到这些信息,所以对于子系统来说,你还是没有登录的。
最后敬上官方的 CAS 协议流程图
\n\n"},{"title":"基于 Eureka 的服务注册与发现调研","url":"/2017/%E5%9F%BA%E4%BA%8E-Eureka-%E7%9A%84%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C%E4%B8%8E%E5%8F%91%E7%8E%B0%E8%B0%83%E7%A0%94/","content":"Eureka 由两个组件组成: Eureka 服务器 和 Eureka 客户端。Eureka 服务器 用作服务注册服务器。Eureka 客户端 是一个 Java 客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。
\n\n\nEureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers.
\n
服务注册与发现就像是在一个聊天室,每个用户来的时候去服务器上注册,这样你的好友们就能看到你,你同时也将获取好友的上线列表。在微服务中,服务就相当于聊天室的用户,而服务注册中心就像聊天室服务器一样。
\nService Registry
和 Service Discovery
实现首先是提供了完整的实现,并且也经受住了 Netflix 自己的生产环境考验,相对使用起来会比较省心。
\nSpring Cloud
无缝集成服务端和客户端都是 Java 编写的,针对微服务场景,并且和 Netflix 的其他开源项目以及 Spring Cloud 都有着非常好的整合,具备良好的生态。
\n最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。
\nspring-cloud-starter-eureka-server
使用 Spring Cloud 需要在 pom.xml 中加入 Spring Cloud 的父级引用,让 Spring 帮我们管理依赖版本。
\n<dependencies> |
Eureka 是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳(因此可以在内存中完成),在默认情况下 Erureka Server 也是一个 Eureka Client,必须要指定一个 Server。
\nserver: |
@EnableEurekaServer
注解@SpringBootApplication |
<dependency> |
server: |
需要指明 spring.application.name
,这个很重要,这在以后的服务与服务之间相互调用一般都是根据这个 name。
启动上边两个程序后,访问 http://localhost:8761/
可以看到下边的页面,同时看到我们的 service-hi
也注册上来了。
在本地调试时出现了这样的问题,如上图所示,中间部分有一行红色大字 EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.,原因是 Eureka 的自我保护机制:
\n\n\nEureka Server 在运行期间,会统计心跳成功率在 15分钟 之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server 会将当前的实例注册信息保护起来,同时提示这个警告。至于为什么需要有这个自我保护机制,官方的解释是:Service 不是强一致的,所以会有部分情况下没发现新服务导致请求出错,对于Service 发现服务而言,宁可返回某服务 5分钟 之前在哪几个服务器上可用的信息,也不能因为暂时的网络故障而找不到可用的服务器,而不返回任何结果。
\n
DNS 可以算是最为原始的服务发现系统,但是在服务变更较为频繁,即服务的动态性很强的时候,DNS 记录的传播速度可能会跟不上服务的变更速度,这将导致在一定的时间窗口内无法提供正确的服务位置信息,所以这种方案只适合在比较静态的环境中使用,不适用于微服务。
\n基于 ZooKeeper、Etcd 等分布式键值对存储服务来建立服务发现系统在现在看起来 也不是一种很好的方案,一方面是因为它们只能提供基本的数据存储功能,还需要在外围做大量的开发才能形成完整的服务发现方案。另一方面是因为它们都是强一致性系统,在集群发生分区时会优先保证一致性、放弃可用性,而服务发现方案更注重可用性,为了保证可用性可以选择最终一致性,这两方面原因共同导致了 ZooKeeper、Etcd 这类系统越来越远离服务发现方案的备选清单,像 SmartStack 这种依赖 ZooKeeper 的服务发现方案也逐渐发觉 ZooKeeper 成了它的薄弱环节。与 ZooKeeper、Etcd 或者依赖它们的方案不同,Eureka 是个专门为服务发现从零开始开发的项目,Eureka 以可用性为先,可以在多种故障期间保持服务发现和服务注册功能可用,虽然此时会存在一些数据错误,但是 Eureka 的设计原则是“存在少量的错误数据,总比完全不可用要好”,并且可以在故障恢复之后按最终一致性进行状态合并,清理掉错误数据。
\nEureka 有个强大的对手 Consul。Consul 是 HashiCorp 公司的商业产品,它有一个开源的基础版本,这个版本在基本的服务发现功能之外,还提供了多数据中心部署能力,包括内存、存储使用情况在内的细粒度服务状态检测能力,和用于服务配置的键值对存储能力(这是一把双刃剑,使用它可以带来便捷,但是也意味着和 Consul 的较强耦合性),这几个能力 Eureka 目前都没有。但是 Consul 对业务的侵入性较大,在与 SpringBoot 项目对接时没有那么方便,而且 Consul 由一家商业软件公司提供,那么必然或多或少的存在商业软件的某些弊端。
\n"},{"title":"基于 Hystrix 的熔断器调研","url":"/2017/%E5%9F%BA%E4%BA%8E-Hystrix-%E7%9A%84%E7%86%94%E6%96%AD%E5%99%A8%E8%B0%83%E7%A0%94/","content":"在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因 服务提供者 的不可用导致 服务消费者 的不可用,并将不可用逐渐放大的过程。
\n如果下图所示:A 作为服务提供者,B 为 A 的服务消费者,C 和 D 是 B 的服务消费者。A 不可用引起了 B 的不可用,并将不可用像滚雪球一样放大到 C 和 D 时,雪崩效应就形成了。
\n\n熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费 CPU 时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。
\n熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定是否允许操作继续,或者立即返回错误。熔断器开关相互转换的逻辑如下图:
\n\n在分布式系统,我们一定会依赖各种服务,那么这些个服务一定会出现失败的情况,Hystrix 就是这样的一个工具,它通过提供了逻辑上延时和错误容忍的解决力来协助我们完成分布式系统的交互。Hystrix 通过分离服务的调用点,阻止错误在各个系统的传播,并且提供了错误回调机制,这一系列的措施提高了系统的整体服务弹性。
\n1.熔断器机制
\n熔断器很好理解,当 Hystrix Command 请求后端服务失败数量超过一定比例(默认50%),熔断器会切换到开路状态(Open)。这时所有请求会直接失败而不会发送到后端服务。熔断器保持在开路状态一段时间后(默认5秒),自动切换到半开路状态(HALF-OPEN),这时会判断下一次请求的返回情况,如果请求成功,熔断器切回闭路状态(CLOSED),否则重新切换到开路状态(OPEN)。Hystrix 的熔断器就像我们家庭电路中的保险丝,一旦后端服务不可用,熔断器会直接切断请求链,避免发送大量无效请求影响系统吞吐量,并且熔断器有自我检测并恢复的能力。
\n2.Fallback
\nFallback 相当于是降级操作。对于查询操作, 我们可以实现一个 fallback 方法,当请求后端服务出现异常的时候,可以使用 fallback 方法返回的值。fallback 方法的返回值一般是设置的默认值或者来自缓存。
\n3.资源隔离
\n在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池. 这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。但是带来的代价就是维护多个线程池会对系统带来额外的性能开销。如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话,可以使用 Hystrix 的信号模式(Semaphores)来隔离资源。
\n因为 Hystrix 是一个开源的 Java 库,所以可以进行像官方示例那样直接用起来,下面我们介绍如何将 Hystrix 集成到 SpringBoot 项目中。
\n<dependency> |
@EnableHystrix
@HystrixCommand(fallbackMethod = "yyy") |
yyy
就是在 熔断器开启时 是我们要调用的函数。
最近开发的项目,需要对接公司已有的 CAS 服务,之前的项目都是用的传统模式(后端渲染或前后端不分离)来开发的,所以后端可以很容易的实现控制跳转,完成校验,写入 Cookies 等逻辑。在传统的开发模式下,使用 session-cookie 可以保证接口安全,在没有登录的情况下访问关键数据会跳转到登录界面或者请求失败。而使用 REStful API 之后,session-cookie 存在以下 3 个问题:
\n解决这个问题的方案是让前端传数据时,在 URL 参数中或者 header 中携带一个 参数,我们成这个参数为 token,后端通过这个 token 来判断用户身份,这样可以免去对 Cookies 的管理。
\n顺着这个思路,我们很容易想到一种方案,就是后端维护一个 Map
,key
值为 token
,value
为用户信息,这样只要用户登录时生成这个 key
后,放到 Map
中,然后将 key
返回给前端就行了,但是这样做有个很严重的问题,Map
是在内存中的,如果后端服务为集群时,还需要做 key
同步,非常麻烦,当然也有人会提出可以将后端生成的 token
和对应的用户信息放在 键-值 数据库中,这样就不用考虑同步问题了,当然这样做没有什么问题,但是会额外引入基础组件(我们现在做第一版,为了快速开发,不打算引入太多的组件),而且还要保证键值数据库的高可用性。
这里我使用了一个更加优雅的方案:JSON Web Token
\nJSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。
\n一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
\nPayload 是 JWT 存储信息的部分。Payload 也是一个 json 数据,每一个 json 的 key-value 称为一个声明。
\n我们将用户信息描述成一个 json 对象。其中添加了一些其他的信息,帮助今后收到这个 JWT 的服务器理解这个 JWT 。
\n{ |
这里面的前五个字段都是由JWT的标准所定义的
\n上面这个 payload 中,user_id
和 username
为自定义声明。
将上面的 json 对象进行base64编码可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
\newogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==
\n\n注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
\n
WT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
\n{ |
在这里,我们说明了这是一个 JWT,并且我们所用的签名算法(后面会提到)是 HS256 算法。
\n对它也要进行Base64编码,之后的字符串就成了 JWT 的 Header(头部)。
\newogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了
\newogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ewogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==
最后,我们将上面拼接完的字符串用 HS256 算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密之后,得到一串加密字符串,最后把这串加密字符串也是用 . 拼接在 header.payload 后面,形成完整的 JWT。
\n最终生成的JWT格式如下:
\nxxx.yyy.zzz
这里签名的目的是为了保证 payload 数据的完整性。如果 JWT 在传输过程中被第三方劫持,中间人对 header.payload 进行修改,并且使用自己的密钥重新签名。服务端收到中间人修改过的 JWT,使用自己的密钥对 header.payload 进行再次加密,由于中间人和服务端使用的是不同的密钥签名,所以服务端再次加密的结果肯定和中间人加密的结果不一致,由此可以断定该 JWT 被恶意篡改。
\n经过上边的介绍,我们可以看出 JWT 中 Payload 的信息是可以解码会明文的,也就是说信息会泄露,所以 JWT 中不应该存放任何敏感信息,用于登录时我们只需放入用户ID或者用户名就可以了,不要把身份号或者密码等信息放入JWT中,应该让后端拿到用户ID或者用户名后进行查询得到身份证号等隐私信息。
\n好的,以上就是JWT的科普部分,下边介绍下我这边CAS对接实现方案。
\n我单独写了一个服务,命名为 hodor(hold the door),作为一个中间层来验证 CAS ticket 并且根据用户信息生成 JWT Token。
\n我们用 zuul 作为网关服务,当请求过来后,网关判断有没有携带 token,并且判断 token 的有效性,我们需要把网关里验证 jwt 的密钥和 hodor 中生成 jwt 的密钥设置为相同的字符串就可以完成验证工作。
\n当网关验证 token 没有被篡改并且还在有效期内后,从 Payload 中取出我们需要的信息,将这些信息明文放在 header 中继续往后请求各个应用,对于应用来说从网关过来的请求是可信的,直接从头中取出相应的用户名或者ID就行了。这里有一个坑,就是网关将信息放入 header 的时候,只能传 ASCII 编码字符串,我们的 CAS 返回用户信息时会同时返回中文姓名,所以中文在传入 header 时,需要做 urlencode 处理,同时应用内接受时也需要做 urldecode。
\n如果没有携带 token 或 token 无效,网关会返回 HTTP 401 错误,前端收到这个返回码后,会跳转到 CAS 认证地址,让用户来登录。同时 service 是前端配置好的一个地址,当用户登录成功后,会回到前端配置好那个地址,前端拿到 CAS 给的 ticket 后,用这个 ticket 和申请 ticket 时的 service 来请求我写的 hodor 服务,hodor根据 ticket 和service 来完成ticket验证工作,获取用户信息,生成jwt,返回给前端,前端保存这个 token,再之后的请求中携带这个 token 来访问就行了。
\n"},{"title":"如何将一个已存在的项目push到GitHub","url":"/2016/%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%80%E4%B8%AA%E5%B7%B2%E5%AD%98%E5%9C%A8%E7%9A%84%E9%A1%B9%E7%9B%AEpush%E5%88%B0GitHub/","content":"这需要登录到GitHub并且穿件一个仓库。你可以选择是否初始化一个README。这不重要,因为你将会覆盖在这个远程仓库里的所有东西。
\n通过你的终端并且确保Git已经安装在你的电脑上,导航到你想要添加的目录后运行下边的命令。
\ngit init |
git add -A |
git commit -m "Added my project" |
git remote add git@github.com:Panmax/playground.git |
or
\ngit remote add https://github.com/Panmax/playground.git |
git push -u -f origin master |
到这里有些事情需要注意,-f
标记代表force
,这将会自动的覆盖远程目录中所有的东西。我们只是用来覆盖GitHub自动初始化README。如果你跳过了,那么-f
标记就不是必须的了。
-u
标记将远程源设置为默认,这让你以后容易的使用git push
和git pull
而不用指定一个源。在这种情况下,我们总是想要的GitHub。
git init |
写这篇文章的原因是前几天做了一道面试题,问题大致是这样的:
\nl = [1, 2, 3, 4, 5] |
我理所应当的认为,Python 列表的内部实现应该是一个链表,而链表的插入和追加操作应该都是 O(1)
,但今天看到一篇文章原文地址,发现并不是我认为的那样。
Python 在 C 实现中,存储数据的部分是一块连续的内存数组,不过这个数组里存放的也是指针,指向具体的元素,并且会在 结构体 中记录元素的实际个数,结构体如下。
\ntypedef struct { |
当追加新元素的时候,可以直接通过 ob_item[ob_size]=n
来完成,所以时间复杂度为 O(1)
。
在插入元素时,操作如下,将要插入位置下方的所有元素向下移动一个位置,然后将要插入位置指向我们插入的元素即可。所以时间复杂度其实是 O(n)
。
(以上都没有考虑 allocated
分配的情况)
再来看下 pop
这个操作,pop
时只需将 ob_size
减一即可,所以时间复杂的也是 O(1)
。
remove
就没这么简单了,需要先通过遍历的方式找到要移除的元素,然后将找到的位置到最后一个有效位置这之间的指针都指向其 next
指向的元素(也就是 ob_item[i]=ob_item[i+1]
),然后 ob_size-1
,所以时间复杂度也为 O(n)
这就是我对 list
几个常见操作的理解,如有错误之处请通过邮件方式指出: jiapan.china#gmail.com
我觉得我是不折腾就会死的人,现在的博客部署在 Github Pages 上,访问速度一直令我非常不爽,所以准备迁移到七牛。
\n七牛新建 bucket 后,在空间设置,把默认首页设置改成启动就完成了。
\n还有一个就是一定要绑定自己的独立域名,否则站内数据改变后,用七牛提供的临时域名来访问的话,缓存一时半会是不会刷新的,独立域名可以设置缓存刷新时间,前提是那个域名需要在国内进行备案。
\n缓存可以根据不同类型的数据有不同的策略,但我为了省事,直接将所有配置改为了 0,也就是不进行缓存,因为不是什么大流量站点,再加上七牛的 CDN 优化,所以即便不缓存速度也非常快,这样可以保证我每次在发布或者修改内容后能够及时更新。其实完全可以把非 HTML 类型的数据设置一些缓存时间。
\n\n可以点这里试一试部署在七牛上的速度,只是我之前的一个备案过的域名,但我没打算用这个域名来做我的国内博客地址,新地址正在备案中: jpanj.com
贾攀家。
接下来就是把博客生成出来的静态站按照目录结构完整上传到七牛就行了,但是这个工作非常麻烦,每次上传时如果有二级目录的话,需要自己填写前缀,而且每次生成后都需要自己登录到七牛上传一下。身为程序员的我,这不是在侮辱我的智商吗?所以我写了一个 Python 脚本,可以帮我自动完成这个工作。
\n我之前是使用的 hexo 的 github 插件来进行发布的,其实也非常简单,只需要执行: hexo d -g
即可完成生成静态站和部署的工作。(g=Generate static files. d=Deploy your website.),现在只需要在执行玩这个操作后,再执行下发布到七牛的脚本,就可以完成双发布了。
我是配合 Alfred 来用的,如果没装 Alfred Workflow 的话,直接执行脚本也是可以的,把脚本简单修改下就行了。
\n上效果图:
\n\n\n代码已上传到 Github:https://github.com/Panmax/qiniu-blog-deploy
\n我把缓存策略改为了如下所示
\n\n图片和样式文件进行缓存,html
等文本文件不缓存。因为七牛会自动寻找 index.html
,所以在真正访问时,不带 /index.html
后缀的页面也可以打开,所以我把全局配置设为了不缓存,也就是说不在这个配置中的文件也不进行缓存。
原文地址:http://zhuanlan.zhihu.com/zhengyun/20270317
\n\n本文档适用人员:技术人员
\n面试的时候,我会问面试者,你日常如何构建自己的知识体系,如何让自己更高更快更强?多数工程师并没有深入地思考过这个问题,基本上是零敲碎打,随机性大。本着不能让你白来一趟的精神,好为人师的我会娓娓道来:
\n\n十几年前我投身软件行业的时候,光是讲解数据库原理、操作系统、TCP/IP、组网、算法等等基础知识的英文原版书摞起来就等身,认认真真看完,各种上手实践,入行后,读遍 C++ 各种经典著作,读遍各种协议原文,认认真真打基础。
\n很多工程师都说自己平常就是在某些 IT 门户上看看推荐的博文或新闻,我说这属于典型的零敲碎打,不够刺激。
\n聊到这时,我会举一个例子,为什么要阅读长篇小说,因为中短篇小说就像用针扎你,而长篇小说就像把你装进一个沙袋里吊起来,从四面八方用狼牙棒打你,酣畅淋漓。构建可用的知识体系,就得读书,书是有体系结构的,你关心不关心,现阶段你用到用不到,它都讲到了,从头到尾看几遍,针扎得透透的。
\n何谓知识体系?
\n几年前,前支付宝架构师姚建东曾经在我们公司做过技术人员如何规划自己的分享讲座,他是这么论述的:
\n技术与技巧包括:
\n语言与工具
\n调试与测试
\n网络与系统
\n需求挖掘与分析
\n以上其实就是一份从业基础知识清单,你可以按图索骥,阅读相关书籍。
\n无论公司业务还是自己喜欢做的事,都可以抽象出通用性课题,然后以做论文的方式杀进去。这个事情得反复操练,有意识操练。
\n做事方式为:
\n其中一个重点是汇总和分享。05年时,应电信级统一消息业务需要,我去研究了 SIP 协议,做了各种试验,分析报文,写了一系列的幻灯片,做了公开分享,一时间还颇受欢迎:
\n为什么要写出来、讲出来呢?
因为有一个学习金字塔理论,如下图所示:
我们读过的事情能够记住学习内容的10%,
\n我们听过的事情能够记住20%,
\n我们看过的事情能够记住30%,
\n我们听过和看过的事情能够记住50%——如看影像/看展览/看演示/现场观摩,
\n我们说过的事情能够记住70%——如参与讨论/发言,
\n我们说过和做过的事情能够记住90%——如做报告,给别人讲,亲身体验,动手做。
\n这也就是我在《窝窝研发过去几年做对了哪些事》中阐述的管理方法:我们从入职之后就有意识地训练大家,让大家能够公开陈述、清晰表达。所以,试用期内,新人必须做一次技术分享和一次技术评审,面对各方的 challenge;预研的中间和结尾都要有分享会;平时也要定期组织技术讲座。
\n知识体系慢慢构建,与业务相关的抽象 Topic 也在探索中。
但这还不够。
因为你亲身接触到的世界太小,可能不足以构成挑战,你可能意识不到自己缺多少知识和技能,不利于你分析问题、提出问题和解决问题的能力培养。
\n所以,要主动出击:
\n疯狂回答问题。
\n我曾经在入行的头几年里几乎把我关注的垂直领域(包括语言领域和业务领域)里的所有问题都回答了一遍。我对外宣扬知无不言言无不尽,放出邮件地址和 MSN(那时候 MSN 很高大上),很多网友都会发邮件或者加我好友,问各种开发疑难问题,平均每天都有几个,然后我把解决问题的过程写成微软 KB(KnowledgeBase) 文体发表在我的博客上。
\n你想想看,工作中的问题你平均每隔几天才能遇到一个,而这么做,每天你都会遇到几个乃至于十几个,第一让你脑力激荡,第二接触到更多新知。
\n05年到06年期间,我因工作需要学习了 JavaME(或古老的称呼 J2ME),早年间 Symbian 手机上的客户端开发。那段时间我天天扫中文论坛的帖子,力求回答所有问题,尤其是那些 BUG 或故障。对于那些暂时没有人解决的,如流媒体实时播放,如仿 OperaMini 二级菜单界面,都上下求索,最后放出思路以及源码。
\n同时,我经常整理常见问题,梳理成册并发布。譬如我整理过的 J2ME 疑难问题:
\n为什么?
\n因为你要信奉:
\n\n\n你学过的每一样东西,你遭受的每一次苦难,都会在你一生中的某个时候派上用场。
\n
——佩内洛普·菲兹杰拉德 《离岸》
\n\nEverything that you’ve learnt and all the hardships you’ve suffered will all come in handy at some point in your life.
\n
现在是你把经验教训变为财富的时刻了。
\n什么是好的技术 Leader?
\n随便一个业务需求或业务场景讲出来,你立刻把它抽象为几个模块/系统/Topic,然后侃侃而谈,业界都是怎么解决的,我们以前又是怎么分析怎么解决的,现在咱们这种情况下应该如何设计,可能会遇到什么问题,我们应该做哪些预防设计,blabla。
\n怎么做到这一点?
\n第一,写 RCA 报告。
\n我以前说过,『窝窝从 2011 年开始,一直坚持每错必查、错了又错就整改、每错必写,用身体力行告诉每一个新员工直面错误、公开技术细节、分享给所有人,长此以往,每一次事故和线上漏测都会变为我们的财富。这就是我们的 RCA(Root Cause Analysis)制度,截止到目前已经收集整理了近两百个详尽的 RCA 报告。』
\nRCA 报告格式为:
\n这样,作为一名合格的老兵,你见过了足够多的血,并且把它们变成了你的人生财富。
\n第二,写总结。
\n话说,要经常拉清单。
\n侃侃而谈得有资料,这些都得是你自己写才能印象深刻,关键时刻想得起来。
\n好了,这就是我告诉面试者的高手炼成四个阶段。
\n","categories":["转载"],"tags":["知乎","成长","知识体系"]},{"title":"接入 sentry 时遇到的坑","url":"/2017/%E6%8E%A5%E5%85%A5sentry%E6%97%B6%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91/","content":"将 进销存SaaS 接入 Sentry,但是接入后发现无法通过 ajax 来 POST
或者 PUT
数据, 会报:
<title>400 Bad Request</title> |
解决办法是在 js 的 ajax 方法中加上:contentType:"application/json; charset=utf-8",
。
注:只需要在提交的数据类型为 Json
的 POST
或 PUT
的方法上添加就行了,不用在提交 Form 表单
的地方添加,否则加上后 Form 表单
类型的 ajax 就无法提交了。
datetime = datetime.datetime.now() |
以前知道python中有个彩蛋,在Python shell下,输入
\nimport this |
会输出:
\nThe Zen of Python, by Tim Peters |
当时认为this
模块就是直接把上边字符串print出来而已。
今天心血来潮,看了下this.py
的源码
s = """Gur Mra bs Clguba, ol Gvz Crgref |
当时我就震惊了。。。
\n乍一看,s
保存的是个什么鬼😂,还以为是什么小语种,然后进行国际化后再输出呢,再往下看,才明白了原理。
先把所有大小写字母经过一定算法转换后,将对照表保存在一个字典中,逐个遍历”加密”后的字符串,从字典中取出对应结果然后进行拼接后再输出。。。
\n顺便也知道了chr(c)
的用处。
选择一个版本控制系统并且使用它。我推荐Git。在我看来,Git是最近新项目最流行的选择。能够删除代码而不用担心造成不可逆的错误是很宝贵的。你可以让你的项目中大量被注释的代码快自由了,因为你现在可以删除它们并且在随后需要的时候恢复这些改变。另外,你将拥有完整的项目备份在GitHub,Bitbucket或者你自己的Gitolite服务上。
\n\n我通常把一个文件放在版本控制之外因为两个原因之一。其中一个是杂乱的东西另一个是秘密的东西。例如编译后的.pyc
文件和虚拟环境(如果你因为某些原因没有使用virtualenvwrapper)是杂乱的东西。它们不需要在版本控制中,因为它们可以各自通过.py
文件和你的requirements.txt
文件被重新创建。
例如API密钥,应用密钥和数据库证书是秘密的东西。它们不应该在版本控制中因为它们的暴露将会造成很大的安全问题。
\nFlask自带一个很好用的功能叫调试模式。你只需要在你的开发配置中设置debug = True
就可以打开它。当调试模式打开,服务器将会在代码发生变化时重新加载并且带有堆栈跟踪和交互控制台。
Flask-DebugToolbar是另外一个很棒的工具用来调试你应用中的问题。在调试模式中,它在你的应用中的每一个页面上覆盖一个边栏。这个边栏给你关于SQL查询,日志,版本,模板,配置的信息和其他有趣的东西用来更容易的跟踪问题。
\n原文地址:http://blog.csdn.net/gzlaiyonghao/article/details/6601123
\n# -*- coding:utf-8 -*- |
\n\n(此处修改为我自己的规范,原文是使用2个空格)
\n
a = b + c |
d = {'key' : 'value'} |
do_something(arg1, arg2) |
而不是
\ndo_something( arg1, arg2 ) |
尽管现在的宽屏显示器已经可以单屏显示超过 256 列字符,但本规范仍然坚持行的最大长度不得超过 78 个字符的标准。折叠长行的方法有以下几种方法:
\n为长变量名换一个短名,如:
\nthis._is.a.very.long.variable_name = this._is.another.long.variable_name |
- 应改为:\n\nvariable_name1 = this._is.a.very.long.variable_name
variable_name2 = this._is.another.variable_name
variable_name1 = variable_name2s
\n\n\n- 在括号(包括圆括号、方括号和花括号)内换行,如:\n\nclass Edit(Widget):
def __init__(self, parent, width,
font = FONT, color = BLACK, pos = POS, style = 0): # 注意:多一层缩进
pass
\n\n\n或\n\nvery_very_very_long_variable_name = Edit(parent,
width,
font,
color,
pos) # 注意:多一层缩进
do_sth_with(very_very_very_long_variable_name)
\n\n\n- 如果行长到连第一个括号内的参数都放不下,则每个元素都单独占一行:\n\nvery_very_very_long_variable_name = ui.widgets.Edit(
panrent,
width,
font,
color,
pos) # 注意:多一层缩进
do_sth_with(very_very_very_long_variable_name)
\n\n\n- 在长行加入续行符强行断行,断行的位置应在操作符前,且换行后多一个缩进,以使维护人员看代码的时候看到代码行首即可判定这里存在换行,如:\n\nif color == WHITE or color == BLACK \\
or color == BLUE: # 注意 or 操作符在新行的行首而不是旧行的行尾,上一行的续行符不可省略
do_something(color);
else:
do_something(DEFAULT_COLOR);
\n
WHITE = 0xffffffff |
color = WHITE |
class ThisIsAClass(object): |
|
命名应当尽量使用全拼写的单词,缩写的情况有如下两种:
\n常用的缩写,如 XML、ID等,在命名时也应只大写首字母,如:
\nclass XmlParser(object):pass |
- 命名中含有长单词,对某个单词进行缩写。这时应使用约定成俗的缩写方式,如去除元音、包含辅音的首字符等方式,例如:\n - function 缩写为 fn\n - text 缩写为 txt\n - object 缩写为 obj\n - count 缩写为 cnt\n - number 缩写为 num,等。\n
__xxx__
形式的系统保留字命名法。项目中也可以使用这种命名,它的意义在于这种形式的变量是只读的,这种形式的类成员函数尽量不要重载。如:class Base(object): |
其中 __id__
、__parent__
和 __message__
都采用了系统保留字命名法。
import 语句有以下几个原则需要遵守:
\n当从模块中 import 多个对象且超过一行时,使用如下断行法(此语法 py2.5 以上版本才支持):
\nfrom module import (obj1, obj2, obj3, obj4, |
- 不要使用 from module import *,除非是 import 常量定义模块或其它你确保不会出现命名空间冲突的模块。\n
a = 1 # 这是一个行注释 |
没有必要做这种对齐,原因有两点:一是这种对齐会打乱编程时的注意力,大脑要同时处理两件事(编程和对齐);二是以后阅读和维护都很困难,因为人眼的横向视野很窄,把三个字段看成一行很困难,而且维护时要增加一个更长的变量名也会破坏对齐。直接这样写为佳:
\na = 1 # 这是一个行注释 |
对于分枝和循环,有如下几点需要注意的:
\n不要写成一行,如:
\nif not flg: pass |
和\n\nfor i in xrange(10): print i
\n\n\n都不是好代码,应写成\n\nif not flg:
pass
for i in xrange(10):
print i
\n\n\n注:本文档中出现写成一行的例子是因为排版的原因,不得作为编码中不断行的依据。\n
if len(alist) != 0: do_something() |
上面的语句应该写成:
\n
|
昨晚写代码时遇到一个坑,导致12点半左右才合上电脑。这个坑是自己挖出来的,大致原因是在使用 sqlalchemy 读取一个数据前,给这个数据进行了操作,导致每次读出来的值都不准。之前没想到是因为前边的代码操作了数据,恰好我在这之前为了验证一些逻辑,手动改了下表数据,所以我一度怀疑是 sqlalchemy 或者 mysql 的缓存导致,或者有事务没有提交导致,然后各种查资料,尝试关闭 mysql 缓存啥的,都没有解决问题,但是后来发现用不同参数调用时,有时又能得到正确的数据,使我不禁开始怀疑人生。
\n眼看到了0点,我静下心来,一行一行检查代码运行路径,最终捉住了这只虫子。。。在我印象中,自从去年7、8月份后,就没有写代码到这么晚了,因为之前每次写代码都会兴奋,导致休息不好,所以就改掉了深夜写代码的习惯。
\n今天白天在打开终端时,我的 oh-my-zsh 例行提示我是否要检查更新,我进行了更新工作后,饶有兴趣的查了查 oh-my-zsh 的常用插件,自己也收入囊肿几个。在此做下记录:
\n先说下如何配置插件,打开 ~/.zshrc 里边有个
\nplugins=(...)
编辑括号中的内容就可以了
这个插件可以记录我本次窗口进入过的目录历史记录,当在几个目录之间来回穿梭时,可以输入 d 回车,按照提示的数字直接进入之前进入过的目录。
\n之前在命令行下,为了快速编辑一个文件,我通常使用 vi, 或者做复杂编辑的时候使用 atom,其实我更喜欢用 sublime 一些,但是一直找不到如何让 终端 调起它的方法,今天终于知道到了。常用命令如下
\nst # 直接打开sublime |
我觉得这个插件真的解决了我的痛点,之前每次解压文件,都需要先去网上查下命令,比如解 gz.tar 需要用什么命令 解压 tar 需要什么命令,解压 zip 需要什么命令,现在好了,需要解压文件时直接 x file_name
就完成了。
作用和 autojump 相同,autojump 是使用 j
作为启动键,z 是用 z
作为启动键,但是查阅资料后解释 z 的速度更快一些,z 是使用 shell 直接编写的,而 autojump 则是用 Python 编写(又黑 Python )。。。
这个是用来在终端中启用搜索的命令,比如 输入 google Python
会自动用默认浏览器打开 google 并用 Python 作为关键字进行查询。同时也支持 baidu、bing。
我现在的插件列表如下:
\nplugins=(git d sublime extract z web-search)
git 的 Aliases 见: https://github.com/robbyrussell/oh-my-zsh/wiki/Plugin:git
Flask将你的应用的组织工作有你来决定。这也是我像初学者一样喜欢Flask的原因之一,但是这确实意味着你必须对如何构建你的代码做一番思考。你可以把你整个应用放在一个文件中,或者把它分散在多个包中。这里有一些你可以遵循的组织模式,可以更轻松的开发和部署。
\n\n让我们来定义一些在本章中会遇到的术语
\n版本库 - 这是放置你的项目的基础文件夹。这个术语传统上指的是版本控制系统,你应该使用版本控制系统。当我在本章中提到你的存放库,我将说的是你的项目的跟目录。当在你的应用中工作时,你可能将不会离开这个目录。
\n包 - 这个指的是一个包含你的应用代码的Python包。我将会在本章谈论更多的关于设置你的作为一个包,但是现在只需要知道包是版本库的一个子目录。
\n模块 - 一个模块是一个单独的Python文件可以被其他Python文件所导入。一个包本质上是多个模块被包裹在一起。
\n你会遇到很多Flask示例将所有的代码放在一个文件中,常常是app.py
。这对于快速项目是很好的(比如一个用来教学的项目),你只需要在这个文件里提供一些路由并且你已经获得少于几百行的应用代码了。
app.py |
应用逻辑将放在清单的app.py
中。
当你工作的项目稍微有些复杂时,一个单独的模块会导致混乱。你将需要为模型和表单定义类,并且他们将与你的路由和配置代码混合起来。所有的这些会阻碍开发。为了解决这个问题,我们能够把我们应用的不同的组件分解出来进行分组形成相互连接的模块 – 一个包。
\nconfig.py |
在这个列表中展示的这种结构允许你将应用中不同的组件按照符合逻辑的方式进行分组。定义模型的类一起放在models.py
中,路由定义放在views.py
中,表单定义放在forms.py
中(我们稍后有整章来介绍表单)。
下边的表提供一个基本的组件概述,你会在大多数的Flask应用中见到。你可能最终在你的版本库中有很多其它文件,但是这些是大多数Flask应用中最普遍的。
\n文件 | \n描述 | \n
---|---|
run.py | \n这个文件被调用启动开发服务器。它从你的包中获得应用的副本并且运行它。这个不能用在生产中,但是它将在开发中有很多的用途。 | \n
requirements.txt | \n这个文件列出所有你的应用所依赖的Python包。你可能为生产和开发依赖准备了各自的文件。 | \n
config.py | \n这个文件包含了大多数你的应用所需要的配置变量。 | \n
/instance/config.py | \n这个文件包含不应该在版本控制中的配置变量。这里包括的东西比如API密钥和数据库RUIs包含的密码。这个也包含你的应用特定情况下的特殊的变量。比如你可能在config.py中有DEBUG = False ,但是设置DEBUG = True 在 instance/config.py在你作为开发的本地设备中。因为这个文件会在config.py后读取,这将会覆盖它并且设置DEBUG = True 。 | \n
/yourapp/ | \n这是一个包含你应用程序的包。 | \n
/yourapp/__init__.py | \n这个文件初始化你的应用并且汇集把所有不同的组件汇集在一起。 | \n
/yourapp/views.py | \n这是路由被定义的地方。它可能切分为它自己的包(yourapp/views)与有联系的视图组合在一起成为一个模块。 | \n
/yourapp/models.py | \n这是定义你的应用模型的地方。这个可能以views.py相同的方式分成多个模块。 | \n
/yourapp/static/ | \n这个文件包含公开的CSS,JavaScript,图片和其他你想通过应用程序公开的文件。这些默认是可以通过 yourapp.com/static/ 来访问的。 | \n
/yourapp/templates/ | \n这是将要放置你的应用Jinja2模板的地方。 | \n
有时你可能会发现你有很多相关的路由。如果你像我一样,你的第一个想法是将views.py
切分成一个包并且将这些视图分组到一个模块中。这种情况下,可能是时候考虑将你的应用放在蓝图中了。
蓝图本质上是有点自包含方式定义你的应用的组件。他们作为你应用程序中的应用。你可能为管理员控制台、前后端和用户仪表盘有不同的蓝图。这让你通过组件来分组视图、静态文件和模板,同时来人让你分享模型、表单和其他你的应用程序在这些组件之间的部分。我们马上将会谈论使用蓝图组织你的应用。
\n今天有同事说自己的CSDN博客被盗了,我才隐约想起我以前好像也在CSDN写过东西。找了好久才找到当年的博客,都是那时候学C++时候的笔记,如果我到现在还在坚持用C++的话,也许也已经能和「轮子哥」谈笑风生了吧。
\n\nC++让我懂了很多直接学习动态语言和其他高级语言(比如Java,我并没有黑Java)所接触不到的和更底层的东西,比如指针、内存动态分配、构造函数和析构函数的作用等等。
那时候还亲自动手写过各种数据结构和算法的C++实现,也走过很多很多的弯路。
翻到这个东西还能证明一点,我已经符合至少三年开发经验的要求了😂
\n还有,不要报有在达X培训3个月就能成大牛的想法,我都鼓捣快四年了也才刚刚入门。。。
\n","categories":["小攀说"],"tags":["CSDN"]},{"title":"自己总结feed流的产生","url":"/2015/%E8%87%AA%E5%B7%B1%E6%80%BB%E7%BB%93feed%E6%B5%81%E7%9A%84%E4%BA%A7%E7%94%9F/","content":"推模式:被关注者产生一条news,会给所有的关注者每人生成一条feed数据。
\n缺点:产生数据条目多,写入量大
\n最严重缺点:这种模式类似朋友圈,只有关注时开始,才给关注者产生feed数据,之前的被关注者发布的news是收不到feed的(其实也可以收到,不过相当麻烦,假如之前被关注者已经发布了很多news了,需要逐个为之前的news生成feed)
\n拉模式:被关注者产生一条news,只产生一条和被关注者相关的feed数据,其他用户在看自己关注的人feed流时,逐个查询他所关注人的feed数据,然后展示结果。
\n以上都没有考虑使用缓存进行优化
\n如有错误请指正~
\n","categories":["小攀说"],"tags":["feed流"]},{"title":"被 Chrome 坑了一次","url":"/2017/%E8%A2%AB-Chrome-%E5%9D%91%E4%BA%86%E4%B8%80%E6%AC%A1/","content":"今天继续研究单点登录,正常来说完成登录回跳到 client 端后,业务系统本身应该写一个自己的session,为了测试,我搭了个很简单的 client 端,但是发现 session 一直写不进去,用 Chrome 的调试工具看到 response 确实有写 Cookies 的操作,但是浏览器中却没有保存这个Cookies,折腾了小一天的时候,后来我抱着没啥希望的态度,用safari浏览器试了下,居然没有任何问题,然后用 Firefox 试了下,也没有问题。。。接着我尝试清除 Chrome 中的Cookies,发现问题解决了。
\n恩,写流水账好开心。
\n"},{"title":"explore flask 视图和路由的高级模式","url":"/2015/%E8%A7%86%E5%9B%BE%E5%92%8C%E8%B7%AF%E7%94%B1%E7%9A%84%E9%AB%98%E7%BA%A7%E6%A8%A1%E5%BC%8F/","content":"Python装饰器是用来改变其他函数的函数。当装饰函数被调用,这个装饰器被调用替代。然后装饰器能够才去行动,修改参数,停止执行或者调用原函数。我们能使用装饰器来包装视图来运行他们执行前的代码。
\n@decorator_function |
如果你浏览过Flask的教程,你可能很熟悉这个代码块中的语法。`@app.route`Flask应用中是用来匹配URL到视图函数的装饰器。
\n来看一些其他你能够在你的Flask应用中使用的装饰器。
\n\nFlask-Login扩展使可以很容易的实现一个登录系统。出了处理用户认证的细节,Flask-Login给了我们一个装饰器用来限制某些视图给已经认证的用户:@login_required
。
# app.py |
@app.route应该永远是最远的视图装饰器。
\n只有被认证的用户将能够访问/dashboard路由。我们能配置Flask-Login让没有认证的用户跳转到登录页面,返回一个HTTP 401状态或者任何其他我们希望他们做的。
\n在官方文档阅读更多关于Flask-Login的使用。
\n想象一篇提及到我们应用的文章刚刚发表在CNN和其他新闻站点。我们每秒钟获得成千上万的请求。我们的主页为每个请求前往数据库多次,所以这一切注意力都放慢下来到爬行。我们如何让速度快速加快,因此所有这些访问者就不会错过我们的站点。
\n这里有很多好的回答,但是这个部分是关于缓存的,所以我们将要谈谈关于缓存的东西。明确来说,我们将要使用Flask-Cache扩展。这个扩展提供给我们一个装饰器,我们可以用在我们的主页视图上用来在一段时间内缓存响应。
\nFlask-Cache 能够被配置和很多不同的缓存后端一起工作。一个流行的选择是Redis,这个我们可以简答设置和使用。假定Flask-Cache已经被配置完成,这段代码块展示我们的装饰器视图是什么样子的。
\n# app.py |
现在这个函数将会每60秒只运行一次,这时候缓存过期。这个响应将会被保存在我们的缓存中并且为任何有障碍的请求从这里获取响应。
\nFlask-Cache 也让我们memoize函数或者缓存用确定参数调用的函数的结果。我们甚至能够缓存计算昂贵的Jinja2模板片段。
\n在这部分,让我们想象我们有一个应用来让我们的用户每个月付费,如果一个用户的账户到期了,我们将会跳转他们到结账页面并且告诉他们去升级。
\n# myapp/util.py |
行数 | \n注释 | \n
---|---|
10 | \n当一个函数被@check_expried 装饰,check_expried() 被调用并且被装饰的函数被作为参数传递。 | \n
11 | \n@warps 是一个装饰器用来做一些簿记,使被装饰的函数() 显示为func() 的文档和调试的目的。这使这个函数的行为更正常一点。 | \n
12 | \n。。。 | \n
16 | \n。。。 | \n
当我们把装饰器叠在一起时,最上边的装饰器将会第一个运行,然后调用下一行的函数:视图函数或者下一个装饰器之一。装饰器语法只是一点点语法糖。
\n# This code: |
这个代码块使用我们自定义装饰器和来自Flask-Login扩展的@login_required
装饰器展示一个例子。我们能使用多个装饰器通过把他们叠在一起。
# myapp/views.py |
现在当一个用户尝试访问 /user_app,check_expired()
将会在运行这个视图函数前确定他们的账户没有过期。
在Python docs了解更多关于warps()
函数的作用。
当你在Flask里定义一个路由,你能够指定它的一部分转换成Python变量并且传递给视图函数。
\n@app.route('/user/<username>') |
无论。。URL标签<username>
将会被传递到视图作为username参数。你也能够指定一个转换器在变量被传到视图前过滤它。
@app.route('/user/id/<int:user_id>') |
在这个代码块,这个URL http://myapp.com/user/id/Q29kZUxlc3NvbiEh 将会返回404代码-未找到。这是因为这个URL的部分被支持变成整型实际上是一个字符串。
\n我们还可以有第二个视图来查找字符串。这会被/user/id/Q29kZUxlc3NvbiEh/调用,与此同时第一个会被/user/id/124调用。
\n这个表展示Flask的内置URL转换器。
\n名称 | \n描述 | \n
---|---|
string | \n。。。 | \n
int | \n。。。 | \n
float | \n。。。 | \n
path | \n。。。 | \n
我们同样可以自定义转换器来满足我们度需要。在Reddit上(一个流行的连接分享站点),用户创建和主持主题讨论型社区和链接分享。一些例子是/r/python和/r/flask,被表示为URL的路径:分别是reddit.com/r/python和reddit.com/r/flask。一个Reddit有意思的功能是你能够观看来自多个子reddits的文章通过加号作为连接多个名字,例如reddit.com/r/python+flask。
\n我们可以在我们自己的Flask应用中使用自定义转换器实线这个功能。
\n","categories":["exploreflask"],"tags":["flask"]},{"title":"解决Linux下VIM中文乱码","url":"/2016/%E8%A7%A3%E5%86%B3Linux%E4%B8%8BVIM%E4%B8%AD%E6%96%87%E4%B9%B1%E7%A0%81/","content":"编辑~/.vimrc文件,加上如下几行:
\nset fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936
set termencoding=utf-8
set encoding=utf-8
我一直想转 VIM 党,但转了好多次了都没转成功,后来就放弃了,给了一个安慰自己的说法:「某些事情,键盘就是没有鼠标快。」所以依旧在使用 Pycharm 作为我的主要开发工具。
\n但是。。
\n因为我是鼠标党或者触摸板党,所以经常不小心在用鼠标的时候拖拽代码,尤其是升级完 Sierra 后(其实我也不太清楚是因为换了Magic Mouse2 以后还是升级导致的),就经常不小心拖拽代码,将代码弄乱,每次都要按 command + z 撤销,然后还要反复检查是不是搞乱了,大大影响我的工作效率,今天突然想到,设置中是不是能关闭代码拖拽的功能,于是通过关键词 drag 找了找,果然,没有找到。
\n然而我并没有放弃,在 Editor –> General 中看到一项叫 Enable Drag’n’Drop functionality in Editor ,把这个项前边的勾勾去掉后,发现奇迹般的貌似成功了,好吧,运气不错。。
\n开门红~
\n2017-04-08 UPDATE:
\n由于最近的工作需要,要读一些 Java 代码,默认 IDEA 配置中会自动将 import 和单行函数折叠,为了取消这个限制,需要在 Preferences > Editor > General > Code Folding在右侧窗口选择哪些要折叠,哪些不需要折叠。
\n","categories":["Code"],"tags":["IDE"]},{"title":"解决Jinja2与Vue.js的模板冲突","url":"/2016/%E8%A7%A3%E5%86%B3jinja2%E4%B8%8EVue-js%E7%9A%84%E6%A8%A1%E6%9D%BF%E5%86%B2%E7%AA%81/","content":"主要思路是通过修改Jinja2的配置,让他只渲染之间的数据,注意空格,而Vue.js处理不加空格的模板。
操作:
\napp.jinja_env.variable_start_string = '{{ ' |
就酱~
\n我这个项目中还使用了flask-bootstrap
作为模板,不幸的是,flask-bootstrap
使用的大括号都没加空格,导致页面渲染时出现问题。所以我将flask-bootstrap
源码进行了修改,安装时,只要用我的数据源安装即可git+https://github.com/Panmax/flask-bootstrap.git
看到阿里云的美国硅谷区的ECS正在高活动,而且最近正好想学习学习Linux服务器相关的只是,所以就购入了一部。
\n我选择的是1G内存,1G CPU,按流量计费。
\n支付宝付完款后,居然没有自动进入完成付款页面。。。
\n首先进入系统后,我先更改了root密码,因为初始化设置root密码时候要求有大小写。命令:sudo passwd root
,然后按照要求输入两遍密码就好了。
为了不直接使用root用户进行操作,所以又创建了自己的用户,刚开始实用的是sudo useradd jiapan
结果发现创建出来的用户没有主目录,后来有删除了重新创建的,删除用户命令sudo userdel jiapan
,第二次创建实用的是adduser
命令:sudo adduser jiapan
。输入两遍密码后,还让输入一些用户信息,我直接一路回车回去了。为了让jiapan
用户有root权限,执行sudo vim /etc/sudoers
进行编辑,在# User privilege specification
的root下边新增jiapan ALL=(ALL)ALL
然后保存退出,就可以了。保存的时候需要用w!
来进行保存。
执行< /etc/shells grep zsh
后发现ubuntu没有自带zsh,所以又进行了zsh的安装:sudo apt-get install zsh
,之前要需要先安装git:sudo apt-get install git
设置登录时就使用zshchsh -s /bin/zsh jiapan
然后为了不折腾zsh,直接安装了oh-my-zsh:sh -c "$(wget https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)"
弱弱的说一句,硅谷区下载国外的资源真心快。。。
\n按照池建强的教程,进行了一些zsh简单的配置:http://macshuo.com/?p=676
本来想直接安装virtualenvwrapper结果发现,python原生不带pip,所以进行pip的安装:
\ncurl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py
发现curl也没装。。。所以先安装curl。
\nsudo apt-get update
sudo apt-get install curl
终于可以安装virtualenvwrapper了:sudo pip install virtualenvwrapper
安装完成后,将下边内容放在~/.bashrc
# where to store our virtual envs |
然后执行source ~/.zshrc
安装完成!然后就可以创建虚拟环境搞Python开发了~
\n今天就到这里。。。
\n","categories":["Linux"],"tags":["Linux"]},{"title":"explore flask 配置","url":"/2015/%E9%85%8D%E7%BD%AE/","content":"原文地址:https://exploreflask.com/configuration.html
当你正在学习Flask时,配置看起来很简单。你只需要在config.py
中定义一些变量所以工作就完成了。当你不得不为了生产环境的应用管理配置时,这简单就开始消失了。你可能需要保护你的密钥或者在不同的环境下使用不同的配置(例如开发和生产环境)。在这一章我们将介绍一些Flask先进的功能来更简单的管理配置。
一个简单的应用可能不需要任何复杂的功能。你可能只需要放置config.py
在你代码库的根目录下并且在app.py
或者yourapp/__init__.py
中加载它。
这个config.py
问卷需要每行包含一个变量赋值。当你的app初始化后,这些在config.py
中的变量用于配置Flask并且它们增加通过app.config
字典的访问方式。例如app.config["DEBUG"]
。
# app.py or app/__init__.py |
变量 | \n描述 | \n
---|---|
DEBUG | \ntodo… | \n
SECRET_KEY | \ntodo… | \n
BCRYPT_LEVEL | \ntodo… | \n
!警告\n请确定在生产环境下`DEBUG`设置为`False`。不然将会允许用户在你的服务器上运行任何Python代码。\n
有时你需要定义包含敏感信息的配置变量。我们想将这些变量从config.py
分离出来并且将他们保存在代码库之外。你可能要隐藏保密的东西比如数据库密码和API密钥或者给机器定义详细的变量。为了让这容易,Flask给我们提供了一个叫实例文件夹的功能。实例文件夹是代码库根目录的子目录并且为这个应用实例包含了一个特殊的配置文件。我们不想在版本控制中提交它。
config.py |
为了加载来自实例文件夹的配置变量,我们使用app.config.from_pyfile()
。当我们创建我们的应用并用Flask()
调用时,如果我们设置instance_relative_config=True
,app.config.from_pyfile()
将会通过instance/directory加载指定的文件。
# app.py or app/__init__.py |
实例文件夹的私有本质为定义密钥而不想暴露在版本库中提供了很好的候选。这些可能包含你应用的密钥或者第三方API的密钥。如果你的应用是开源的这尤其重要或者也许会在未来某一时刻开源。我们通常想让其他用户或者贡献者使用他们自己的密钥。
\n# instance/config.py |
如果你的生产环境和开发环境之间的不同很小,你可能想使用你的实例文件夹处理配置的改变。定义在instance/config.py
文件中的变量能够覆盖config.py
中的变量。你只需要在app.config.from_object()
之后调用app.config.from_pyfile()
。利用这个方法是改变你的应用在不同机器配置的一种方式。
# config.py |
在生产中,我们将离开列表中的值,在instance/config.py
之外,并且它将会回落到config.py
定义的值。
实例文件夹不应该在版本控制中。这意味着你将无法跟踪你实例配置的变化。这可能对于一两个值来说不是问题,但是如果你在不同环境中(生产、升级、开发等)有微调的配置,你不想冒险失去这些。
\nFlask给我们基于环境变量的值去选择一个配置文件来加载的能力。这意味着我们能有多个配置文件在我们的代码库中并且总是加载正确的那个。每次我们有几个不同的配置文件,我们能够移动他们到他们自己的config
目录中。
requirements.txt |
在这个列表中我们有少量不同的配置文件。
\n文件 | \n介绍 | \n
---|---|
config/default.py | \n… | \n
config/development.py | \n… | \n
config/production.py | \n… | \n
config/staging.py | \n… | \n
为了决定加载哪个配置文件,我们将调用app.config.from_envvar()
。
# yourapp/__init__.py |
环境变量的值应该是配置文件的绝对路径。
\n我们如何设置这个环境变量,取决于我们的应用正在运行的平台。如果我们运行在一个普通的Linux服务器上,我们能够设置一个shell脚本来设置我们的环境变量并且运行run.py
。
# start.sh |
start.sh
在每个环境中是唯一的,所以它应该离开版本控制。在Herok中,我们想要使用Heroku工具设置环境变量。相同的思路应用于其他PaaS平台上。
config.py
。app,config.from_envvar()
。今天要求实现一个功能是从被推荐的用户中,随机取出n个用户,并打乱顺序返回。
\n看了看random模块刚好有这种功能的实现,所以就直接拿来用了。
\n所有被推荐用户的列表为:recommend_users
,已经确定的是,这个列表的长度一定大于n。
我们需要将结果保存在recommend_user_list
中。
首先,从这个列表中随机取出n个元素:
\nimport random |
这个方法是从recommend_users
列表中按顺序随机取出n个元素,因为我们需要打乱顺序,所以还需要调用另一个方法。
# 将这个数组打乱顺序 |
搞定!~
\n","categories":["Code"],"tags":["Python","魔镜"]},{"title":"《大话数据结构》阅读笔记","url":"/2017/%E3%80%8A%E5%A4%A7%E8%AF%9D%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E3%80%8B%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0/","content":"\n\nupdate at 2017-04-12
\n
逻辑结构:指数据对象中数据元素之间的相互关系
\n1. 集合结构\n2. 线性结构\n3. 树形结构\n4. 图形结构\n
物理结构:指数据的逻辑结构在计算机中的存储形式
\n1. 顺序存储结构\n2. 链式存储结构\n
逻辑结构是面相问题的,物理结构是面相计算机的,基本目标就是将数据及其逻辑关系存储到计算机的内存中。
\n\n\nupdate at 2017-04-13
\n
算法具有五个基本特性:输入、输出、有穷性、确定性和可行性
\n好的算法应该具有正确性、可读性、健壮性、高效率和低存储量的特征
\n判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数
\n\n\nupdate at 2017-04-17
\n
推到大 O 阶:
\n得到的结果就是大 O 阶。
\n\n\nupdate at 2017-04-18
\n
线性表顺序存储结构需要三个属性:
\n线性表顺序存储结构的优缺点:
\n优点:
\n缺点:
\n\n\nupdate at 2017-04-19
\n
链式结构
\n为了表示每个数据元素 ai
与其直接后继数据元素 ai+1
之间的逻辑关系,对数据元素 ai
来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)
把存储数据元素信息的域称为数据域,把存储直接后继位置的域成为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素 ai
的存储映像,称为结点(Node)
链表中第一个节点的存储位置叫做头指针
\n头指针
\n头结点
\n\n\nupdate at 2017-04-20
\n
存储分配方式
\n时间性能
\n查找
\n插入和删除
\n空间性能
\n结论
\n线性表的双向链表存储结构
\ntypedef struct DulNode |
双向链表插入元素
\n\ns -> prior = p |
双向链表删除元素
\n\np -> prior -> next = p -> next; |
线性表的两种结构
\n\n\n\n栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
\n
把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)
\n不含任何数据元素的栈称为空栈,栈又称为后进先出(Last In First Out)的线性表,简称 FIFO 结构。
\n栈的插入操作叫作进栈,也称压栈、入栈。
\n栈的删除操作叫做出栈。
\n中缀表达式转后缀表达式:
\n如:9+(3-1)3+10/2 -> 9 3 1 - 3 + 10 2 / +
\n规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是有括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
\n\n\n队列(queue)是只允许在一段进行插入操作,而在另一端进行删除操作的线性表。
\n
队列是一种先进先出(First In First Out)的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
\n循环队列满时没我们有两种办法来判断:
\n第二种方法,队列满的条件是 (rear+1) % QueueSize == front
\n计算队列长度公式:(rear-front+Queue) % QueueSize
\n对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。
\n对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除时间是 O(n) 的时间复杂度变成了 O(1)。
\n他们也都可以通过链式存数结构来实现。
\n串(string)是由零个或多个字符组成的有限序列,又名叫字符串。
\n","tags":["读书"]},{"title":"记一次业务逻辑优化","url":"/2015/%E8%AE%B0%E4%B8%80%E6%AC%A1%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91%E4%BC%98%E5%8C%96/","content":"我们之前新鲜的逻辑是这样的:每个用户在redis中有一个自己的队列,队列中记录的照片的ID,当有新照片产生的时候,不区分性别,往每个人队列最前边插入(lpush)
这条数据,每个人的队列最大值为1000,超出部分被截断,当用户看过队列中某张照片后,这张照片会从队列中移除(lrem)
。
这种模式刚开始没有问题,后来新鲜增加了可以筛选性别的需求。因为队列中所有性别都是混在一起的,所以每次从队列中取出数据后,需要把Photo实体取出来,然后进行性别过滤,把过滤出来的结果再返回给客户端。如果筛选结果后发现数量不足(通常是20),就重新从库中重新查询,拿出前2000张,过滤掉我要的性别(因为我们用的leancloud平台,他们不支持关联查询,我们的photo不记录性别,需要先取出后再通过user才能知道性别),再过滤掉我看过的,能找到就返回,找不到就算了。假如我把筛选改为女,然后我看啊看,看啊看,早晚我会把队列中的女性照片看完。因为我们只查询前2000张,所以后边的照片我根本看不到,除非等着有女性用户新发照片。而且看的照片越多,查询速度越慢。
\n下边讲一下优化的方法
\n\n这个优化是基于假设用户很少切换性别的基础上进行的:
\ncache:feed:last:sex:u_id
,并且记录上次查询到最后那张照片的ID cache:feed:last:photo:p_id
这样做之后性能提升了很多很多~
\n","categories":["小攀说"],"tags":["魔镜","redis"]},{"title":"解决在 SAE 中使用 Flask-SQLAlchemy 出现 MySQL server has gone away 的问题","url":"/2017/%E8%A7%A3%E5%86%B3%E5%9C%A8-SAE-%E4%B8%AD%E4%BD%BF%E7%94%A8-Flask-SQLAlchemy-%E5%87%BA%E7%8E%B0-MySQL-server-has-gone-away-%E7%9A%84%E9%97%AE%E9%A2%98/","content":"这段时间尝试把 进销存SAAS 迁移到 新浪云(SAE),这样的话减少了运维的麻烦而且降低了成本,我现在暂时只用到了一个最基础的容器+一个最低配置的 MySQL
服务,每天的成本只有 2 块多。打算迁移完之后改成两个容器实例。
但是在迁移中发现一个问题,当过一会不访问服务后,再次访问时会出现 MySQL server has gone away
的错误,在 SAE 提供的数据库中用 SHOW VARIABLES;
检查了下,SAE 给数据库配置的 wait_timeout
是 300 秒,我之前阿里云上的数据库没有改配置,所以默认为 8 小时,而且 SAE 数据库 的这个值是不允许修改的,所以既然无法改变环境,就来适应环境吧。
尝试了很多解决办法,比如 配置SQLALCHEMY_COMMIT_ON_TEARDOWN=True
和 在每次请求完成后关闭 db 的 session
都没有解决问题,再次阅读文档时看到了这个配置:SQLALCHEMY_POOL_RECYCLE
,作用是设置多少秒后回收连接,在使用 MySQL
时是必须设置的。如果不提供值,默认为 2 小时,而我之前的数据库默认 wait_timeout
为 8 小时,所以一直没有出过问题。我在我的配置文件中,将这个值设置为 280 秒(小于 300 秒),最终解决了问题。
最后帖一下SQLALCHEMY_POOL_RECYCLE
参数的原文解释:
\n\n"},{"title":"程序员需要知道的缩写和专业名词","url":"/2018/Awsome-Abbreviation/","content":"Number of seconds after which a connection is automatically recycled. This is required for MySQL, which removes connections after 8 hours idle by default. Note that Flask-SQLAlchemy automatically sets this to 2 hours if MySQL is used.
\n
\n\n程序员的世界里充斥着很多的专业名词和英文缩写,我打算对一些常见的词汇进行一个汇总,同时会在 GitHub 上进行同步:https://github.com/Panmax/Awsome-Programmer-Abbreviation,欢迎 PR。
\n
应用程序接口(英语:Application Programming Interface,简称:API),又称为应用编程接口,就是软件系统不同组成部分衔接的约定。由于近年来软件的规模日益庞大,常常需要把复杂的系统划分成小的组成部分,编程接口的设计十分重要。程序设计的实践中,编程接口的设计首先要使软件系统的职责得到合理划分。良好的接口设计可以降低系统各部分的相互依赖,提高组成单元的内聚性,降低组成单元间的耦合程度,从而提高系统的维护性和扩展性。
\nACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。
\nAJAX即“Asynchronous JavaScript and XML”(异步的 JavaScript 与 XML 技术),指的是一套综合了多项技术的浏览器端网页开发技术。
\nJPA 是 Java Persistence API 的简称,中文名 Java 持久层 API,是 JDK 5.0 注解或 XML 描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
\nJSON(JavaScript Object Notation)是一种轻量级的数据交换语言,以文字为基础,且易于让人阅读。尽管 JSON 是 Javascript 的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于 C语言 家族的一些习惯。
\nPOJO(Plain Ordinary Java Object)简单的 Java 对象,实际就是普通 Java Beans。使用 POJO 名称是为了避免和 EJB 混淆起来,而且简称比较直接。其中有一些属性及其 getter setter 方法的类,没有业务逻辑,有时可以作为VO(Value Object) 或 DTO(Data Transform Object) 来使用。当然,如果你有一个简单的运算属性也是可以的,但不允许有业务方法,也不能携带有 connection 之类的方法。
\n领域专用语言(Domain Specific Language/DSL),其基本思想是「求专不求全」,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。
\n在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理,称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会。垃圾回收最早起源于LISP语言。目前许多语言如 Smalltalk、Java、C# 和 D 语言都支持垃圾回收器。
\n数据操纵语言(Data Manipulation Language, DML)是 SQL 语言中,负责对数据库对象运行数据访问工作的指令集,以 INSERT、UPDATE、DELETE 三种指令为核心,分别代表插入、更新与删除,是开发以数据为中心的应用程序必定会使用到的指令,因此有很多开发人员都把加上SQL的SELECT语句的四大指令以“CRUD”来称呼。
\n数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言,由 CREATE、ALTER 与 DROP 三个语法所组成,最早是由Codasyl(Conference on Data Systems Languages)数据模型开始,现在被纳入 SQL 指令中作为其中一个子集。
\nDependency Injection,依赖注入。在软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端) 。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务是将会变成客户端的状态的一部分。 传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。
\n域名系统(英文:Domain Name System)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。
\n图形用户界面(Graphical User Interface)是指采用图形方式显示的计算机操作用户界面。与早期计算机使用的命令行界面相比,图形界面对于用户来说在视觉上更易于接受。
\n超文本传输协议(英文:HyperText Transfer ProtocolP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。
\n控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
\nJSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,特别适用于分布式站点的单点登录(SSO)场景。
\n轻型目录存取协定(英文:Lightweight Directory Access Protocol)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。
\nMVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。MVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。
\nModel-view-presenter,简称MVP,是电脑软件设计工程中一种对针对MVC模式,再审议后所延伸提出的一种软件设计模式。被广范用于便捷自动化单元测试和在呈现逻辑中改良分离关注点(separation of concerns)。
\nMVVM(Model–view–viewmodel)是一种软件架构模式,有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来,这是通过置标语言或 GUI 代码实现的。
\n联机分析处理(英语:On-Line Analytical Processing),是一套以多维度方式分析数据,而能弹性地提供积存(英语:Roll-up)、下钻(英语:Drill-down)、和透视分析(英语:pivot)等操作,呈现集成性决策信息的方法,多用于决策支持系统、商务智能或数据仓库。其主要的功能,在于方便大规模数据分析及统计计算,对决策提供参考和支持。与之相区别的是联机交易处理(OLTP)。
\nSQL(结构化查询语言)是一种特定目的程序语言,用于管理关系数据库管理系统(RDBMS),或在关系流数据管理系统(RDSMS)中进行流处理。
\n单页 Web 应用(single page web application),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。
\n面向服务的体系结构(英语:service-oriented architecture)并不特指一种技术,而是一种分散式运算的软件设计方法。软件的部分组件(呼叫者),可以透过网络上的通用协定呼叫另一个应用软件元件执行、运作,让呼叫者获得服务。SOA原则上采用开放标准、与软件资源进行交互并采用表示的标准方式。因此应能跨越厂商、产品与技术。一项服务应视为一个独立的功能单元,可以远端存取并独立执行与更新,例如在线上线查询信用卡账单。
\nSOAP(原为Simple Object Access Protocol的首字母缩写,即简单对象访问协议)是交换数据的一种协议规范,使用在计算机网络Web服务(web service)中,交换带结构信息。SOAP为了简化网页服务器(Web Server)从XML数据库中提取数据时,节省去格式化页面时间,以及不同应用程序之间按照HTTP通信协议,遵从XML格式执行资料互换,使其抽象于语言实现、平台和硬件。
\nNoSQL 是对不同于传统的关系数据库的数据库管理系统的统称。
\n可扩展标记语言(英语:eXtensible Markup Language,简称:XML),是一种标记语言。标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种信息的文章等。如何定义这些标记,既可以选择国际通用的标记语言,比如HTML,也可以使用像XML这样由相关人士自由决定的标记语言,这就是语言的可扩展性。XML是从标准通用标记语言(SGML)中简化修改出来的。它主要用到的有可扩展标记语言、可扩展样式语言(XSL)、XBRL和XPath等。
\n前端(英语:front-end)和后端(英语:back-end)是描述进程开始和结束的通用词汇。前端作用于采集输入信息,后端进行处理。计算机程序的界面样式,视觉呈现属于前端。
\n在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。
\n在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
\n自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
\n递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。递归一词还较常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。
\n主键,又称主码(英语:primary key或unique key)。数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
\n外键(英语:foreign key,台湾译外来键,又称外部键)。其实在关系数据库中,每个数据表都是由关系来连系彼此的关系,父数据表(Parent Entity)的主键(primary key)会放在另一个数据表,当做属性以创建彼此的关系,而这个属性就是外键。
\n浏览器-服务器(Browser/Server)结构,与C/S结构不同,其客户端不需要安装专门的软件,只需要浏览器即可,浏览器通过Web服务器与数据库进行交互,可以方便的在不同平台下工作;服务器端可采用高性能计算机,并安装Oracle、Sybase、Informix等大型数据库。B/S结构简化了客户端的工作,它是随着Internet技术兴起而产生的,对C/S技术的改进,但该结构下服务器端的工作较重,对服务器的性能要求更高。
\n主从式架构 (英语:Client–server model) 也称客户端-服务器(Client/Server)架构、C/S架构,是一种网络架构,它把客户端 (Client) (通常是一个采用图形用户界面的程序)与服务器 (Server) 区分开来。每一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。有很多不同类型的服务器,例如文件服务器、游戏服务器等。
\n根据W3C的定义,Web服务(Web service)应当是一个软件系统,用以支持网络间不同机器的互动操作。网络服务通常是许多应用程序接口(API)所组成的,它们透过网络,例如国际互联网(Internet)的远程服务器端,执行客户所提交服务的请求。
\n"},{"title":"《掌控习惯》——读后行动","url":"/2022/Atomic-Habits-action/","content":"今天是六一儿童节,祝各位大宝宝小宝宝儿童节快乐🌸
\n上周读完了 《掌控习惯》 这本书,里边总结了四个定律来培养好习惯或者戒除不良习惯,我列出这四个定律和每个定律给出的几个主要方案,在每个方案下写上自己可以将此应用在哪些地方,同时会写一些自己的感想。
\n+早上不赖床
=洗脸刷牙上厕所
+洗漱期间听红楼梦、播客
-上厕所时候玩游戏
+上厕所时候学英语
+手冲一杯很淡的美式,可以提神、让自己多喝水
+使用 Things 管理自己的待办事项
-工作或者看书的空档没有思路时会刷会手机
-外边走路时会不自主的拿出手机,虽然也不知道要做什么
=边走路变听播客
+读书
=带娃
+背 Anki
=看邮件
-看 Telegram、Twitter
+每周一次慢跑
+晚上在 10:30 前做好睡眠准备
这个我没有想出与我自己相匹配的场景,先从书中抄几条吧(同时会应用在自己身上),等自己有了灵感再补充。
\n在地铁上读书时我会戴上 AirPods Pro,播放我长期在听的那些钢琴曲,可能是已经听的太长时间吧(2年?),每当听到这些音乐后我很容易就能进入阅读状态,同时 AirPods Pro 的降噪效果也能给我提供一个不那么嘈杂的环境,更容易让我沉浸在阅读中。
\n这里附上我的歌单
\n工作时将手机屏幕扣在桌面上,晚上睡觉前将手机放在卧室充电,十点后就不再玩手机和其他点子设备(Kindle除外),这样可以避免睡前刷手机影响睡眠。
\n这也是为什么家长喜欢让自己的孩子和那些更优秀的孩子在一块玩的原因,人会相互影响,尤其是那些和我们亲近的人。
\n这也是为什么有时候我们学习一样东西,自学的效率没有报一个班和大家一起学高的原因之一吧(另一个原因是能得到专业的指导),好多人一起学可以创造一种氛围,让你觉得这个事情也并没有那么难。
\n同样和自己志趣相投的一群人一起工作更能保持充足的热情,书中提到「没有什么比群体归属感更能维持一个人做事的动力了」。
\n通过这一节我也知道了我们为什么会在焦虑、无所事事时喜欢刷朋友圈、抖音、淘宝的原因:「当我们不确定改如何做时,我们都会期待得到团体的指导」。我们想看看其他人在做什么,想看看其他人在玩什么,想看看其他人在买什么。
\n我们会从众,希望能被这个社会所接纳,哪怕整个社会都在做的可能是一件不正确的事,我们为了得到认可同样也会选择做这件事。比如最近一段时间的每天一次核酸监测,再过几年再回头来看,我不认为这是正确的。「我们宁愿跟众人一起犯错,也不愿特立独行坚持真理。」
\n我现在只在电脑上安装了 Anki,手机上的 Anki 是付费的,价格还不低,就一直没装,我准备今天就购买并安装上手机版。我现在每天做知识回顾的时候都需要使用电脑,无法随时随地的回顾知识,多少有些阻力。
\n我的书包里时刻装着一本书和 Kindle,出门乘坐交通工具或者等人的时候,可以随时拿出来进行阅读,同时书包里还有铅笔、荧光笔,可以随时让我做标记使用。
\n再有,比如我要求自己在周六下午慢跑一次,我在白天或者提前一天就把跑步需要的衣服鞋子准备好,去跑步的概率会更大一些,因为到了那个时间我只需把衣服换好就可以出门跑步,不需要再去想着需要先找衣服、换衣服才能出门。
\n我现在刚刚开始尝试练习写作,写作一定是一个对未来非常好的投资,每当我需要写点什么的时候,即使自己不想写我也会要求自己坐在电脑前,打开写作工具只写一段就好。
\n想到另一个用途:当我非常想做点杀时间的事情,比如打局游戏、刷会短视频,我会跟自己说先看会书吧,就看两分钟就行。如果两分钟过去心里没那么浮躁可以读下去了就会继续读,如果还是读不下去就按照之前的计划想做什么做点什么。
\n这个方法可以利用在避免浪费太多时间在刷手机上,我将自己容易沉迷的软件收在一个目录中,而不是直接平铺在桌面上,这样在每次打开手机时就不会直接看到它们,同时将能关闭的推送关闭,很多不必要的软件如果没有推送我们是不会主动想到去打开它的,更进一步,我们可以尝试卸载 APP,比如我手机中现在就没有抖音、快手这钟既浪费我时间又会降低我心智的软件。
\n如果我们把手机的面部识别功能关闭,是不是能较少我们划手机的次数?有时候我们打开手机是个无意识行为,看一眼手机随手往上一滑就解锁了,然后人们会顺着这个动作不自主的启动后边一系列的动作。前段时间人们在公共场合都需要戴口罩,在使用面部识别时会比较麻烦,这应该多少也会减少玩手机的次数吧,然而后来 Apple 支持了带口罩识别,「破坏」了这个隐性的好处。我因为有 Apple Watch,所以自始至终都可以在佩戴口罩的情况下解锁 😂
\n我在两周前发了个朋友圈,表示自己又又又要开始减肥了,算是一种对公共的承诺,虽然没有奖惩措施,但也这会给我提供动力,好让我在约定时间内再次在公共面前交上答卷。这样能带来的好处是我能让自己恢复到正常体重,而且能让其他人看到我是一个言必行、行必果,做事靠谱的人。
\n\nApple Watch 会在我每次完成计划的运动量后亮出三环的烟花,每月完成运动天数后会奖励一个奖牌。
\n这个方法我在保持体重和培养阅读习惯上有所使用。为了不让自己体重超出某个范围,我会每天早上称一次体重,如果体重和前一天有较大的 diff,我就会回顾昨天做了什么吃了什么。在阅读方面,我会将自己计划读、正在读、读完的书单记录下来,对自己能起到一定的激励作用。
\n我阅读时更喜欢读纸质书是因为便于追踪,人是视觉动物,读纸质书可以看到自己已经读过了多少,还剩多少没有读,肉眼可见读过的页数越来越多、剩余的页数越来越少,也能继续激励自己往下读。相比来说,电子书在这上面就没有这个优势了,只有右下角冷冰冰的数字告诉我们当前进度是多少。
\n减肥是长期的事,如果前一天因为聚餐或者嘴馋吃了过量的食物,也不要过于自责,第二天要快速调整状态,可以选择跳过一顿早餐,午餐也减些量作为弥补。
\n学习也是一样,我给自己设置了一些固定每天要学的东西作为日课,如果有一天因为某种原因(如身体不适、公司事项积压)而错过了,我要在第二天或者周末的时候做些追赶,最差的情况哪怕不做追赶,只要第二天不要再错过就好。「成功最大的威胁不是失败,而是倦怠。」
\n现在没有这样的伙伴了,自己监督好自己吧。
\n还是上边减肥的那个朋友圈,无论有没有减到自己承诺的体重,我都会在十月一日当天把结果公布出来。
\n"}] \ No newline at end of file diff --git a/sub-list/index.html b/sub-list/index.html new file mode 100644 index 0000000000..481eb079ba --- /dev/null +++ b/sub-list/index.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +